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 defaultBaseURL = "https://api.metaregistrar.com"
  18  
  19  const tokenHeader = "token"
  20  
  21  // Client is a client to interact with the Metaregistrar API.
  22  type Client struct {
  23  	token string
  24  
  25  	baseURL    *url.URL
  26  	HTTPClient *http.Client
  27  }
  28  
  29  // NewClient creates a new Client.
  30  func NewClient(token string) (*Client, error) {
  31  	if token == "" {
  32  		return nil, errors.New("token missing")
  33  	}
  34  
  35  	baseURL, _ := url.Parse(defaultBaseURL)
  36  
  37  	return &Client{
  38  		token:      token,
  39  		baseURL:    baseURL,
  40  		HTTPClient: &http.Client{Timeout: 10 * time.Second},
  41  	}, nil
  42  }
  43  
  44  // UpdateDNSZone updates the DNS zone for a domain.
  45  // To add or remove a TXT record we make a PATCH request.
  46  // https://metaregistrar.dev/docu/metaapi/requests/patch_Update_dns_zone.html
  47  func (c *Client) UpdateDNSZone(ctx context.Context, domain string, updateRequest DNSZoneUpdateRequest) (*DNSZoneUpdateResponse, error) {
  48  	endpoint := c.baseURL.JoinPath("dnszone", domain)
  49  
  50  	req, err := newJSONRequest(ctx, http.MethodPatch, endpoint, updateRequest)
  51  	if err != nil {
  52  		return nil, err
  53  	}
  54  
  55  	result := &DNSZoneUpdateResponse{}
  56  
  57  	err = c.do(req, result)
  58  	if err != nil {
  59  		return nil, err
  60  	}
  61  
  62  	return result, nil
  63  }
  64  
  65  func (c *Client) do(req *http.Request, result any) error {
  66  	req.Header.Add(tokenHeader, c.token)
  67  
  68  	resp, err := c.HTTPClient.Do(req)
  69  	if err != nil {
  70  		return errutils.NewHTTPDoError(req, err)
  71  	}
  72  
  73  	defer func() { _ = resp.Body.Close() }()
  74  
  75  	if resp.StatusCode != http.StatusOK {
  76  		return parseError(req, resp)
  77  	}
  78  
  79  	if result == nil {
  80  		return nil
  81  	}
  82  
  83  	raw, err := io.ReadAll(resp.Body)
  84  	if err != nil {
  85  		return errutils.NewReadResponseError(req, resp.StatusCode, err)
  86  	}
  87  
  88  	err = json.Unmarshal(raw, result)
  89  	if err != nil {
  90  		return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
  91  	}
  92  
  93  	return nil
  94  }
  95  
  96  func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {
  97  	buf := new(bytes.Buffer)
  98  
  99  	if payload != nil {
 100  		err := json.NewEncoder(buf).Encode(payload)
 101  		if err != nil {
 102  			return nil, fmt.Errorf("failed to create request JSON body: %w", err)
 103  		}
 104  	}
 105  
 106  	req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)
 107  	if err != nil {
 108  		return nil, fmt.Errorf("unable to create request: %w", err)
 109  	}
 110  
 111  	req.Header.Set("Accept", "application/json")
 112  
 113  	if payload != nil {
 114  		req.Header.Set("Content-Type", "application/json")
 115  	}
 116  
 117  	return req, nil
 118  }
 119  
 120  func parseError(req *http.Request, resp *http.Response) error {
 121  	raw, _ := io.ReadAll(resp.Body)
 122  
 123  	var errAPI APIError
 124  
 125  	err := json.Unmarshal(raw, &errAPI)
 126  	if err != nil {
 127  		return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)
 128  	}
 129  
 130  	return &errAPI
 131  }
 132