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