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  	"time"
  13  
  14  	"github.com/go-acme/lego/v4/providers/dns/internal/errutils"
  15  )
  16  
  17  const baseAPIURL = "https://api.servercow.de/dns/v1/domains"
  18  
  19  // Client the Servercow client.
  20  type Client struct {
  21  	username string
  22  	password string
  23  
  24  	baseURL    *url.URL
  25  	HTTPClient *http.Client
  26  }
  27  
  28  // NewClient Creates a Servercow client.
  29  func NewClient(username, password string) *Client {
  30  	baseURL, _ := url.Parse(baseAPIURL)
  31  
  32  	return &Client{
  33  		username:   username,
  34  		password:   password,
  35  		baseURL:    baseURL,
  36  		HTTPClient: &http.Client{Timeout: 5 * time.Second},
  37  	}
  38  }
  39  
  40  // GetRecords from API.
  41  func (c *Client) GetRecords(ctx context.Context, domain string) ([]Record, error) {
  42  	endpoint := c.baseURL.JoinPath(domain)
  43  
  44  	req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)
  45  	if err != nil {
  46  		return nil, err
  47  	}
  48  
  49  	var records []Record
  50  
  51  	err = c.do(req, &records)
  52  	if err != nil {
  53  		return nil, err
  54  	}
  55  
  56  	return records, nil
  57  }
  58  
  59  // CreateUpdateRecord creates or updates a record.
  60  func (c *Client) CreateUpdateRecord(ctx context.Context, domain string, data Record) (*Message, error) {
  61  	endpoint := c.baseURL.JoinPath(domain)
  62  
  63  	req, err := newJSONRequest(ctx, http.MethodPost, endpoint, data)
  64  	if err != nil {
  65  		return nil, err
  66  	}
  67  
  68  	var msg Message
  69  
  70  	err = c.do(req, &msg)
  71  	if err != nil {
  72  		return nil, err
  73  	}
  74  
  75  	if msg.ErrorMsg != "" {
  76  		return nil, msg
  77  	}
  78  
  79  	return &msg, nil
  80  }
  81  
  82  // DeleteRecord deletes a record.
  83  func (c *Client) DeleteRecord(ctx context.Context, domain string, data Record) (*Message, error) {
  84  	endpoint := c.baseURL.JoinPath(domain)
  85  
  86  	req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, data)
  87  	if err != nil {
  88  		return nil, err
  89  	}
  90  
  91  	var msg Message
  92  
  93  	err = c.do(req, &msg)
  94  	if err != nil {
  95  		return nil, err
  96  	}
  97  
  98  	if msg.ErrorMsg != "" {
  99  		return nil, msg
 100  	}
 101  
 102  	return &msg, nil
 103  }
 104  
 105  func (c *Client) do(req *http.Request, result any) error {
 106  	req.Header.Set("X-Auth-Username", c.username)
 107  	req.Header.Set("X-Auth-Password", c.password)
 108  
 109  	resp, err := c.HTTPClient.Do(req)
 110  	if err != nil {
 111  		return errutils.NewHTTPDoError(req, err)
 112  	}
 113  
 114  	defer func() { _ = resp.Body.Close() }()
 115  
 116  	// Note the API always return 200 even if the authentication failed.
 117  	if resp.StatusCode/100 != 2 {
 118  		return errutils.NewUnexpectedResponseStatusCodeError(req, resp)
 119  	}
 120  
 121  	if result == nil {
 122  		return nil
 123  	}
 124  
 125  	raw, err := io.ReadAll(resp.Body)
 126  	if err != nil {
 127  		return errutils.NewReadResponseError(req, resp.StatusCode, err)
 128  	}
 129  
 130  	err = unmarshal(raw, result)
 131  	if err != nil {
 132  		return err
 133  	}
 134  
 135  	return nil
 136  }
 137  
 138  func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {
 139  	buf := new(bytes.Buffer)
 140  
 141  	if payload != nil {
 142  		err := json.NewEncoder(buf).Encode(payload)
 143  		if err != nil {
 144  			return nil, fmt.Errorf("failed to create request JSON body: %w", err)
 145  		}
 146  	}
 147  
 148  	req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)
 149  	if err != nil {
 150  		return nil, fmt.Errorf("unable to create request: %w", err)
 151  	}
 152  
 153  	req.Header.Set("Accept", "application/json")
 154  
 155  	// Content-Type should be added even if there is no request body.
 156  	req.Header.Set("Content-Type", "application/json")
 157  
 158  	return req, nil
 159  }
 160  
 161  func unmarshal(raw []byte, v any) error {
 162  	err := json.Unmarshal(raw, v)
 163  	if err == nil {
 164  		return nil
 165  	}
 166  
 167  	var utErr *json.UnmarshalTypeError
 168  
 169  	if !errors.As(err, &utErr) {
 170  		return fmt.Errorf("unmarshaling %T error: %w: %s", v, err, string(raw))
 171  	}
 172  
 173  	var apiErr Message
 174  
 175  	errU := json.Unmarshal(raw, &apiErr)
 176  	if errU != nil {
 177  		return fmt.Errorf("unmarshaling %T error: %w: %s", v, err, string(raw))
 178  	}
 179  
 180  	return apiErr
 181  }
 182