client.go raw
1 package internal
2
3 import (
4 "context"
5 "fmt"
6 "io"
7 "net/http"
8 "net/url"
9 "strconv"
10 "strings"
11 "time"
12
13 "github.com/go-acme/lego/v4/challenge/dns01"
14 "github.com/go-acme/lego/v4/providers/dns/internal/errutils"
15 "github.com/miekg/dns"
16 )
17
18 const defaultBaseURL = "https://www.duckdns.org/update"
19
20 // Client the DuckDNS API client.
21 type Client struct {
22 token string
23
24 baseURL string
25 HTTPClient *http.Client
26 }
27
28 // NewClient Creates a new Client.
29 func NewClient(token string) *Client {
30 return &Client{
31 token: token,
32 baseURL: defaultBaseURL,
33 HTTPClient: &http.Client{Timeout: 5 * time.Second},
34 }
35 }
36
37 func (c *Client) AddTXTRecord(ctx context.Context, domain, value string) error {
38 return c.UpdateTxtRecord(ctx, domain, value, false)
39 }
40
41 func (c *Client) RemoveTXTRecord(ctx context.Context, domain string) error {
42 return c.UpdateTxtRecord(ctx, domain, "", true)
43 }
44
45 // UpdateTxtRecord Update the domains TXT record
46 // To update the TXT record we just need to make one simple get request.
47 // In DuckDNS you only have one TXT record shared with the domain and all subdomains.
48 func (c *Client) UpdateTxtRecord(ctx context.Context, domain, txt string, clearRecord bool) error {
49 endpoint, _ := url.Parse(c.baseURL)
50
51 mainDomain := getMainDomain(domain)
52 if mainDomain == "" {
53 return fmt.Errorf("unable to find the main domain for: %s", domain)
54 }
55
56 query := endpoint.Query()
57 query.Set("domains", mainDomain)
58 query.Set("token", c.token)
59 query.Set("clear", strconv.FormatBool(clearRecord))
60 query.Set("txt", txt)
61 endpoint.RawQuery = query.Encode()
62
63 req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), http.NoBody)
64 if err != nil {
65 return fmt.Errorf("unable to create request: %w", err)
66 }
67
68 resp, err := c.HTTPClient.Do(req)
69 if err != nil {
70 return errutils.NewHTTPDoError(req, err)
71 }
72
73 defer func() { _ = resp.Body.Close() }()
74
75 raw, err := io.ReadAll(resp.Body)
76 if err != nil {
77 return errutils.NewReadResponseError(req, resp.StatusCode, err)
78 }
79
80 body := string(raw)
81 if body != "OK" {
82 return fmt.Errorf("request to change TXT record for DuckDNS returned the following result (%s) this does not match expectation (OK) used url [%s]", body, endpoint)
83 }
84
85 return nil
86 }
87
88 // DuckDNS only lets you write to your subdomain.
89 // It must be in format subdomain.duckdns.org,
90 // not in format subsubdomain.subdomain.duckdns.org.
91 // So strip off everything that is not top 3 levels.
92 func getMainDomain(domain string) string {
93 domain = dns01.UnFqdn(domain)
94
95 split := dns.Split(domain)
96 if strings.HasSuffix(strings.ToLower(domain), "duckdns.org") {
97 if len(split) < 3 {
98 return ""
99 }
100
101 firstSubDomainIndex := split[len(split)-3]
102
103 return domain[firstSubDomainIndex:]
104 }
105
106 return domain[split[len(split)-1]:]
107 }
108