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