client.go raw

   1  package internal
   2  
   3  import (
   4  	"bytes"
   5  	"context"
   6  	"encoding/json"
   7  	"errors"
   8  	"fmt"
   9  	"io"
  10  	"net/http"
  11  	"net/url"
  12  	"strconv"
  13  	"time"
  14  
  15  	"github.com/go-acme/lego/v4/providers/dns/internal/errutils"
  16  )
  17  
  18  const defaultBaseURL = "https://api.dynect.net/REST"
  19  
  20  // Client the Dyn API client.
  21  type Client struct {
  22  	customerName string
  23  	username     string
  24  	password     string
  25  
  26  	baseURL    *url.URL
  27  	HTTPClient *http.Client
  28  }
  29  
  30  // NewClient Creates a new Client.
  31  func NewClient(customerName, username, password string) *Client {
  32  	baseURL, _ := url.Parse(defaultBaseURL)
  33  
  34  	return &Client{
  35  		customerName: customerName,
  36  		username:     username,
  37  		password:     password,
  38  		baseURL:      baseURL,
  39  		HTTPClient:   &http.Client{Timeout: 5 * time.Second},
  40  	}
  41  }
  42  
  43  // Publish updating Zone settings.
  44  // https://help.dyn.com/update-zone-api/
  45  func (c *Client) Publish(ctx context.Context, zone, notes string) error {
  46  	endpoint := c.baseURL.JoinPath("Zone", zone)
  47  
  48  	payload := &publish{Publish: true, Notes: notes}
  49  
  50  	req, err := newJSONRequest(ctx, http.MethodPut, endpoint, payload)
  51  	if err != nil {
  52  		return err
  53  	}
  54  
  55  	_, err = c.do(req)
  56  	if err != nil {
  57  		return err
  58  	}
  59  
  60  	return nil
  61  }
  62  
  63  // AddTXTRecord creating TXT Records.
  64  // https://help.dyn.com/create-txt-record-api/
  65  func (c *Client) AddTXTRecord(ctx context.Context, authZone, fqdn, value string, ttl int) error {
  66  	endpoint := c.baseURL.JoinPath("TXTRecord", authZone, fqdn)
  67  
  68  	payload := map[string]any{
  69  		"rdata": map[string]string{
  70  			"txtdata": value,
  71  		},
  72  		"ttl": strconv.Itoa(ttl),
  73  	}
  74  
  75  	req, err := newJSONRequest(ctx, http.MethodPost, endpoint, payload)
  76  	if err != nil {
  77  		return err
  78  	}
  79  
  80  	_, err = c.do(req)
  81  	if err != nil {
  82  		return err
  83  	}
  84  
  85  	return nil
  86  }
  87  
  88  // RemoveTXTRecord deleting one or all existing TXT Records.
  89  // https://help.dyn.com/delete-txt-records-api/
  90  func (c *Client) RemoveTXTRecord(ctx context.Context, authZone, fqdn string) error {
  91  	endpoint := c.baseURL.JoinPath("TXTRecord", authZone, fqdn)
  92  
  93  	req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)
  94  	if err != nil {
  95  		return err
  96  	}
  97  
  98  	resp, err := c.HTTPClient.Do(req)
  99  	if err != nil {
 100  		return errutils.NewHTTPDoError(req, err)
 101  	}
 102  
 103  	defer func() { _ = resp.Body.Close() }()
 104  
 105  	if resp.StatusCode != http.StatusOK {
 106  		return errutils.NewUnexpectedResponseStatusCodeError(req, resp)
 107  	}
 108  
 109  	return nil
 110  }
 111  
 112  func (c *Client) do(req *http.Request) (*APIResponse, error) {
 113  	resp, err := c.HTTPClient.Do(req)
 114  	if err != nil {
 115  		return nil, errutils.NewHTTPDoError(req, err)
 116  	}
 117  
 118  	defer func() { _ = resp.Body.Close() }()
 119  
 120  	if resp.StatusCode >= http.StatusInternalServerError {
 121  		return nil, errutils.NewUnexpectedResponseStatusCodeError(req, resp)
 122  	}
 123  
 124  	raw, err := io.ReadAll(resp.Body)
 125  	if err != nil {
 126  		return nil, errutils.NewReadResponseError(req, resp.StatusCode, err)
 127  	}
 128  
 129  	var response APIResponse
 130  
 131  	err = json.Unmarshal(raw, &response)
 132  	if err != nil {
 133  		return nil, errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
 134  	}
 135  
 136  	if resp.StatusCode >= http.StatusBadRequest {
 137  		return nil, fmt.Errorf("%s: %w", response.Messages, errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw))
 138  	}
 139  
 140  	if resp.StatusCode == http.StatusTemporaryRedirect {
 141  		// TODO add support for HTTP 307 response and long running jobs
 142  		return nil, errors.New("API request returned HTTP 307. This is currently unsupported")
 143  	}
 144  
 145  	if response.Status == "failure" {
 146  		return nil, fmt.Errorf("%s: %w", response.Messages, errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw))
 147  	}
 148  
 149  	return &response, nil
 150  }
 151  
 152  func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {
 153  	buf := new(bytes.Buffer)
 154  
 155  	if payload != nil {
 156  		err := json.NewEncoder(buf).Encode(payload)
 157  		if err != nil {
 158  			return nil, fmt.Errorf("failed to create request JSON body: %w", err)
 159  		}
 160  	}
 161  
 162  	req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)
 163  	if err != nil {
 164  		return nil, fmt.Errorf("unable to create request: %w", err)
 165  	}
 166  
 167  	req.Header.Set("Accept", "application/json")
 168  
 169  	if payload != nil {
 170  		req.Header.Set("Content-Type", "application/json")
 171  	}
 172  
 173  	tok := getToken(req.Context())
 174  	if tok != "" {
 175  		req.Header.Set(authTokenHeader, tok)
 176  	}
 177  
 178  	return req, nil
 179  }
 180