client.go raw

   1  package internal
   2  
   3  import (
   4  	"context"
   5  	"errors"
   6  	"fmt"
   7  	"io"
   8  	"net/http"
   9  	"net/url"
  10  	"strings"
  11  	"sync"
  12  	"time"
  13  
  14  	"github.com/go-acme/lego/v4/providers/dns/internal/errutils"
  15  )
  16  
  17  const (
  18  	removeAction = "rm"
  19  	addAction    = "add"
  20  )
  21  
  22  const successCode = "successfully"
  23  
  24  const defaultBaseURL = "https://www.dnshome.de/dyndns.php"
  25  
  26  // Client the dnsHome.de client.
  27  type Client struct {
  28  	baseURL    string
  29  	HTTPClient *http.Client
  30  
  31  	credentials map[string]string
  32  	credMu      sync.Mutex
  33  }
  34  
  35  // NewClient Creates a new Client.
  36  func NewClient(credentials map[string]string) *Client {
  37  	return &Client{
  38  		HTTPClient:  &http.Client{Timeout: 10 * time.Second},
  39  		baseURL:     defaultBaseURL,
  40  		credentials: credentials,
  41  	}
  42  }
  43  
  44  // Add adds a TXT record.
  45  // only one TXT record for ACME is allowed, so it will update the "current" TXT record.
  46  func (c *Client) Add(ctx context.Context, hostname, value string) error {
  47  	domain := strings.TrimPrefix(hostname, "_acme-challenge.")
  48  
  49  	return c.doAction(ctx, domain, addAction, value)
  50  }
  51  
  52  // Remove removes a TXT record.
  53  // only one TXT record for ACME is allowed, so it will remove "all" the TXT records.
  54  func (c *Client) Remove(ctx context.Context, hostname, value string) error {
  55  	domain := strings.TrimPrefix(hostname, "_acme-challenge.")
  56  
  57  	return c.doAction(ctx, domain, removeAction, value)
  58  }
  59  
  60  func (c *Client) doAction(ctx context.Context, domain, action, value string) error {
  61  	endpoint, err := c.createEndpoint(domain, action, value)
  62  	if err != nil {
  63  		return err
  64  	}
  65  
  66  	req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint.String(), http.NoBody)
  67  	if err != nil {
  68  		return fmt.Errorf("unable to create request: %w", err)
  69  	}
  70  
  71  	resp, err := c.HTTPClient.Do(req)
  72  	if err != nil {
  73  		return errutils.NewHTTPDoError(req, err)
  74  	}
  75  
  76  	defer func() { _ = resp.Body.Close() }()
  77  
  78  	if resp.StatusCode != http.StatusOK {
  79  		return errutils.NewUnexpectedResponseStatusCodeError(req, resp)
  80  	}
  81  
  82  	raw, err := io.ReadAll(resp.Body)
  83  	if err != nil {
  84  		return errutils.NewReadResponseError(req, resp.StatusCode, err)
  85  	}
  86  
  87  	output := string(raw)
  88  
  89  	if !strings.HasPrefix(output, successCode) {
  90  		return errors.New(output)
  91  	}
  92  
  93  	return nil
  94  }
  95  
  96  func (c *Client) createEndpoint(domain, action, value string) (*url.URL, error) {
  97  	if len(value) < 12 {
  98  		return nil, fmt.Errorf("the TXT value must have more than 12 characters: %s", value)
  99  	}
 100  
 101  	endpoint, err := url.Parse(c.baseURL)
 102  	if err != nil {
 103  		return nil, err
 104  	}
 105  
 106  	c.credMu.Lock()
 107  	password, ok := c.credentials[domain]
 108  	c.credMu.Unlock()
 109  
 110  	if !ok {
 111  		return nil, fmt.Errorf("domain %s not found in credentials, check your credentials map", domain)
 112  	}
 113  
 114  	endpoint.User = url.UserPassword(domain, password)
 115  
 116  	query := endpoint.Query()
 117  	query.Set("acme", action)
 118  	query.Set("txt", value)
 119  	endpoint.RawQuery = query.Encode()
 120  
 121  	return endpoint, nil
 122  }
 123