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  	"strings"
  14  	"time"
  15  
  16  	"github.com/go-acme/lego/v4/providers/dns/internal/errutils"
  17  )
  18  
  19  const defaultBaseURL = "https://api.simply.com/2/"
  20  
  21  // Client is a Simply.com API client.
  22  type Client struct {
  23  	accountName string
  24  	apiKey      string
  25  
  26  	baseURL    *url.URL
  27  	HTTPClient *http.Client
  28  }
  29  
  30  // NewClient creates a new Client.
  31  func NewClient(accountName, apiKey string) (*Client, error) {
  32  	if accountName == "" {
  33  		return nil, errors.New("credentials missing: accountName")
  34  	}
  35  
  36  	if apiKey == "" {
  37  		return nil, errors.New("credentials missing: apiKey")
  38  	}
  39  
  40  	baseURL, err := url.Parse(defaultBaseURL)
  41  	if err != nil {
  42  		return nil, err
  43  	}
  44  
  45  	return &Client{
  46  		accountName: accountName,
  47  		apiKey:      apiKey,
  48  		baseURL:     baseURL,
  49  		HTTPClient:  &http.Client{Timeout: 5 * time.Second},
  50  	}, nil
  51  }
  52  
  53  // GetRecords lists all the records in the zone.
  54  func (c *Client) GetRecords(ctx context.Context, zoneName string) ([]Record, error) {
  55  	endpoint := c.createEndpoint(zoneName, "/")
  56  
  57  	req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)
  58  	if err != nil {
  59  		return nil, fmt.Errorf("failed to create request: %w", err)
  60  	}
  61  
  62  	result := &apiResponse[[]Record, json.RawMessage]{}
  63  
  64  	err = c.do(req, result)
  65  	if err != nil {
  66  		return nil, err
  67  	}
  68  
  69  	return result.Records, nil
  70  }
  71  
  72  // AddRecord adds a record.
  73  func (c *Client) AddRecord(ctx context.Context, zoneName string, record Record) (int64, error) {
  74  	endpoint := c.createEndpoint(zoneName, "/")
  75  
  76  	req, err := newJSONRequest(ctx, http.MethodPost, endpoint, record)
  77  	if err != nil {
  78  		return 0, fmt.Errorf("failed to create request: %w", err)
  79  	}
  80  
  81  	result := &apiResponse[json.RawMessage, recordHeader]{}
  82  
  83  	err = c.do(req, result)
  84  	if err != nil {
  85  		return 0, err
  86  	}
  87  
  88  	return result.Record.ID, nil
  89  }
  90  
  91  // EditRecord updates a record.
  92  func (c *Client) EditRecord(ctx context.Context, zoneName string, id int64, record Record) error {
  93  	endpoint := c.createEndpoint(zoneName, strconv.FormatInt(id, 10))
  94  
  95  	req, err := newJSONRequest(ctx, http.MethodPut, endpoint, record)
  96  	if err != nil {
  97  		return fmt.Errorf("failed to create request: %w", err)
  98  	}
  99  
 100  	return c.do(req, &apiResponse[json.RawMessage, json.RawMessage]{})
 101  }
 102  
 103  // DeleteRecord deletes a record.
 104  func (c *Client) DeleteRecord(ctx context.Context, zoneName string, id int64) error {
 105  	endpoint := c.createEndpoint(zoneName, strconv.FormatInt(id, 10))
 106  
 107  	req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)
 108  	if err != nil {
 109  		return fmt.Errorf("failed to create request: %w", err)
 110  	}
 111  
 112  	return c.do(req, &apiResponse[json.RawMessage, json.RawMessage]{})
 113  }
 114  
 115  func (c *Client) createEndpoint(zoneName, uri string) *url.URL {
 116  	return c.baseURL.JoinPath("my", "products", zoneName, "dns", "records", strings.TrimSuffix(uri, "/"))
 117  }
 118  
 119  func (c *Client) do(req *http.Request, result Response) error {
 120  	req.SetBasicAuth(c.accountName, c.apiKey)
 121  
 122  	resp, err := c.HTTPClient.Do(req)
 123  	if err != nil {
 124  		return errutils.NewHTTPDoError(req, err)
 125  	}
 126  
 127  	defer func() { _ = resp.Body.Close() }()
 128  
 129  	if resp.StatusCode >= http.StatusInternalServerError {
 130  		return errutils.NewUnexpectedResponseStatusCodeError(req, resp)
 131  	}
 132  
 133  	raw, err := io.ReadAll(resp.Body)
 134  	if err != nil {
 135  		return errutils.NewReadResponseError(req, resp.StatusCode, err)
 136  	}
 137  
 138  	err = json.Unmarshal(raw, result)
 139  	if err != nil {
 140  		return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
 141  	}
 142  
 143  	if result.GetStatus() != http.StatusOK {
 144  		return fmt.Errorf("unexpected error: %s", result.GetMessage())
 145  	}
 146  
 147  	return nil
 148  }
 149  
 150  func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {
 151  	buf := new(bytes.Buffer)
 152  
 153  	if payload != nil {
 154  		err := json.NewEncoder(buf).Encode(payload)
 155  		if err != nil {
 156  			return nil, fmt.Errorf("failed to create request JSON body: %w", err)
 157  		}
 158  	}
 159  
 160  	req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)
 161  	if err != nil {
 162  		return nil, fmt.Errorf("unable to create request: %w", err)
 163  	}
 164  
 165  	req.Header.Set("Accept", "application/json")
 166  
 167  	if payload != nil {
 168  		req.Header.Set("Content-Type", "application/json")
 169  	}
 170  
 171  	return req, nil
 172  }
 173