client.go raw

   1  package internal
   2  
   3  import (
   4  	"bytes"
   5  	"encoding/xml"
   6  	"errors"
   7  	"fmt"
   8  	"io"
   9  	"net/http"
  10  	"net/url"
  11  	"slices"
  12  	"time"
  13  
  14  	"github.com/go-acme/lego/v4/providers/dns/internal/errutils"
  15  )
  16  
  17  const defaultBaseURL = "https://dynamic.zoneedit.com"
  18  
  19  // Client the ZoneEdit API client.
  20  type Client struct {
  21  	user      string
  22  	authToken string
  23  
  24  	baseURL    *url.URL
  25  	HTTPClient *http.Client
  26  }
  27  
  28  // NewClient creates a new Client.
  29  func NewClient(user, authToken string) (*Client, error) {
  30  	if user == "" || authToken == "" {
  31  		return nil, errors.New("credentials missing")
  32  	}
  33  
  34  	baseURL, _ := url.Parse(defaultBaseURL)
  35  
  36  	return &Client{
  37  		user:       user,
  38  		authToken:  authToken,
  39  		baseURL:    baseURL,
  40  		HTTPClient: &http.Client{Timeout: 10 * time.Second},
  41  	}, nil
  42  }
  43  
  44  func (c *Client) CreateTXTRecord(domain, rdata string) error {
  45  	return c.perform("txt-create.php", domain, rdata)
  46  }
  47  
  48  func (c *Client) DeleteTXTRecord(domain, rdata string) error {
  49  	return c.perform("txt-delete.php", domain, rdata)
  50  }
  51  
  52  func (c *Client) perform(actionPath, domain, rdata string) error {
  53  	endpoint := c.baseURL.JoinPath(actionPath)
  54  
  55  	query := endpoint.Query()
  56  	query.Set("host", domain)
  57  	query.Set("rdata", rdata)
  58  	endpoint.RawQuery = query.Encode()
  59  
  60  	req, err := http.NewRequest(http.MethodGet, endpoint.String(), http.NoBody)
  61  	if err != nil {
  62  		return err
  63  	}
  64  
  65  	return c.do(req)
  66  }
  67  
  68  func (c *Client) do(req *http.Request) error {
  69  	req.SetBasicAuth(c.user, c.authToken)
  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/100 != 2 {
  79  		raw, _ := io.ReadAll(resp.Body)
  80  
  81  		return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)
  82  	}
  83  
  84  	raw, err := io.ReadAll(resp.Body)
  85  	if err != nil {
  86  		return errutils.NewReadResponseError(req, resp.StatusCode, err)
  87  	}
  88  
  89  	if bytes.Contains(raw, []byte("SUCCESS CODE")) {
  90  		return nil
  91  	}
  92  
  93  	raw = bytes.TrimSpace(raw)
  94  
  95  	// The answer is not an XML valid (missing closing), so I fix it to parse it.
  96  	if bytes.HasSuffix(raw, []byte(">")) {
  97  		raw = slices.Concat(raw[:len(raw)-1], []byte("/>"))
  98  	}
  99  
 100  	var apiErr APIError
 101  
 102  	err = xml.Unmarshal(raw, &apiErr)
 103  	if err != nil {
 104  		return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)
 105  	}
 106  
 107  	return fmt.Errorf("[status code: %d] %w", resp.StatusCode, apiErr)
 108  }
 109