client.go raw

   1  package internal
   2  
   3  import (
   4  	"context"
   5  	"fmt"
   6  	"io"
   7  	"net/http"
   8  	"net/url"
   9  	"time"
  10  
  11  	"github.com/go-acme/lego/v4/providers/dns/internal/errutils"
  12  	"github.com/go-acme/lego/v4/providers/dns/internal/useragent"
  13  	querystring "github.com/google/go-querystring/query"
  14  )
  15  
  16  const (
  17  	addAction    = "add"
  18  	deleteAction = "delete"
  19  )
  20  
  21  type Client struct {
  22  	token     string
  23  	serverURL string
  24  
  25  	HTTPClient *http.Client
  26  }
  27  
  28  func NewClient(serverURL, token string) (*Client, error) {
  29  	_, err := url.Parse(serverURL)
  30  	if err != nil {
  31  		return nil, fmt.Errorf("server URL: %w", err)
  32  	}
  33  
  34  	return &Client{
  35  		serverURL:  serverURL,
  36  		token:      token,
  37  		HTTPClient: &http.Client{Timeout: 10 * time.Second},
  38  	}, nil
  39  }
  40  
  41  func (c *Client) AddTXTRecord(ctx context.Context, zone, fqdn, content string) error {
  42  	return c.updateRecord(ctx, UpdateRecord{Action: addAction, Zone: zone, Type: "TXT", Record: fqdn, Data: content})
  43  }
  44  
  45  func (c *Client) DeleteTXTRecord(ctx context.Context, zone, fqdn, recordContent string) error {
  46  	return c.updateRecord(ctx, UpdateRecord{Action: deleteAction, Zone: zone, Type: "TXT", Record: fqdn, Data: recordContent})
  47  }
  48  
  49  func (c *Client) updateRecord(ctx context.Context, action UpdateRecord) error {
  50  	req, err := c.newRequest(ctx, action)
  51  	if err != nil {
  52  		return err
  53  	}
  54  
  55  	return c.do(req)
  56  }
  57  
  58  func (c *Client) do(req *http.Request) error {
  59  	useragent.SetHeader(req.Header)
  60  
  61  	req.SetBasicAuth("anonymous", c.token)
  62  
  63  	resp, err := c.HTTPClient.Do(req)
  64  	if err != nil {
  65  		return errutils.NewHTTPDoError(req, err)
  66  	}
  67  
  68  	defer func() { _ = resp.Body.Close() }()
  69  
  70  	// The endpoint uses the `DefaultDdnsResponseWriter`,
  71  	// and this writer uses HTTP status code to determine if the request was successful or not.
  72  	// - https://github.com/mhofer117/ispconfig-ddns-module/blob/8b011a5bb138881d9f13360a5c4fec10c0084613/lib/updater/DdnsUpdater.php#L53-L57
  73  	// - https://github.com/mhofer117/ispconfig-ddns-module/blob/master/lib/updater/response/DefaultDdnsResponseWriter.php
  74  	if resp.StatusCode/100 != 2 {
  75  		raw, _ := io.ReadAll(resp.Body)
  76  
  77  		return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)
  78  	}
  79  
  80  	return nil
  81  }
  82  
  83  func (c *Client) newRequest(ctx context.Context, action UpdateRecord) (*http.Request, error) {
  84  	endpoint, err := url.Parse(c.serverURL)
  85  	if err != nil {
  86  		return nil, err
  87  	}
  88  
  89  	endpoint = endpoint.JoinPath("ddns", "update.php")
  90  
  91  	values, err := querystring.Values(action)
  92  	if err != nil {
  93  		return nil, err
  94  	}
  95  
  96  	endpoint.RawQuery = values.Encode()
  97  
  98  	method := http.MethodPost
  99  	if action.Action == deleteAction {
 100  		method = http.MethodDelete
 101  	}
 102  
 103  	req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), nil)
 104  	if err != nil {
 105  		return nil, err
 106  	}
 107  
 108  	req.Header.Set("Accept", "application/json")
 109  
 110  	return req, nil
 111  }
 112