client.go raw
1 package internal
2
3 import (
4 "bytes"
5 "context"
6 "encoding/json"
7 "fmt"
8 "io"
9 "net/http"
10 "net/url"
11 "time"
12
13 "github.com/go-acme/lego/v4/providers/dns/internal/errutils"
14 )
15
16 // DefaultBaseURL represents the API endpoint to call.
17 const DefaultBaseURL = "https://api.godaddy.com"
18
19 const authorizationHeader = "Authorization"
20
21 type Client struct {
22 apiKey string
23 apiSecret string
24
25 baseURL *url.URL
26 HTTPClient *http.Client
27 }
28
29 func NewClient(apiKey, apiSecret string) *Client {
30 baseURL, _ := url.Parse(DefaultBaseURL)
31
32 return &Client{
33 apiKey: apiKey,
34 apiSecret: apiSecret,
35 baseURL: baseURL,
36 HTTPClient: &http.Client{Timeout: 5 * time.Second},
37 }
38 }
39
40 // GetRecords retrieves DNS Records for the specified Domain.
41 // https://developer.godaddy.com/doc/endpoint/domains#/v1/recordGet
42 func (c *Client) GetRecords(ctx context.Context, domainZone, rType, recordName string) ([]DNSRecord, error) {
43 endpoint := c.baseURL.JoinPath("v1", "domains", domainZone, "records", rType, recordName)
44
45 req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)
46 if err != nil {
47 return nil, err
48 }
49
50 var records []DNSRecord
51
52 err = c.do(req, &records)
53 if err != nil {
54 return nil, err
55 }
56
57 return records, nil
58 }
59
60 // UpdateTxtRecords replaces all DNS Records for the specified Domain with the specified Type.
61 // https://developer.godaddy.com/doc/endpoint/domains#/v1/recordReplaceType
62 func (c *Client) UpdateTxtRecords(ctx context.Context, records []DNSRecord, domainZone, recordName string) error {
63 endpoint := c.baseURL.JoinPath("v1", "domains", domainZone, "records", "TXT", recordName)
64
65 req, err := newJSONRequest(ctx, http.MethodPut, endpoint, records)
66 if err != nil {
67 return err
68 }
69
70 return c.do(req, nil)
71 }
72
73 // DeleteTxtRecords deletes all DNS Records for the specified Domain with the specified Type and Name.
74 // https://developer.godaddy.com/doc/endpoint/domains#/v1/recordDeleteTypeName
75 func (c *Client) DeleteTxtRecords(ctx context.Context, domainZone, recordName string) error {
76 endpoint := c.baseURL.JoinPath("v1", "domains", domainZone, "records", "TXT", recordName)
77
78 req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)
79 if err != nil {
80 return err
81 }
82
83 return c.do(req, nil)
84 }
85
86 func (c *Client) do(req *http.Request, result any) error {
87 req.Header.Set(authorizationHeader, fmt.Sprintf("sso-key %s:%s", c.apiKey, c.apiSecret))
88
89 resp, err := c.HTTPClient.Do(req)
90 if err != nil {
91 return errutils.NewHTTPDoError(req, err)
92 }
93
94 defer func() { _ = resp.Body.Close() }()
95
96 if resp.StatusCode/100 != 2 {
97 return parseError(req, resp)
98 }
99
100 if result == nil {
101 return nil
102 }
103
104 raw, err := io.ReadAll(resp.Body)
105 if err != nil {
106 return errutils.NewReadResponseError(req, resp.StatusCode, err)
107 }
108
109 err = json.Unmarshal(raw, result)
110 if err != nil {
111 return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
112 }
113
114 return nil
115 }
116
117 func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {
118 buf := new(bytes.Buffer)
119
120 if payload != nil {
121 err := json.NewEncoder(buf).Encode(payload)
122 if err != nil {
123 return nil, fmt.Errorf("failed to create request JSON body: %w", err)
124 }
125 }
126
127 req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)
128 if err != nil {
129 return nil, fmt.Errorf("unable to create request: %w", err)
130 }
131
132 req.Header.Set("Accept", "application/json")
133
134 if payload != nil {
135 req.Header.Set("Content-Type", "application/json")
136 }
137
138 return req, nil
139 }
140
141 func parseError(req *http.Request, resp *http.Response) error {
142 raw, _ := io.ReadAll(resp.Body)
143
144 var errAPI APIError
145
146 err := json.Unmarshal(raw, &errAPI)
147 if err != nil {
148 return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)
149 }
150
151 return fmt.Errorf("[status code: %d] %w", resp.StatusCode, &errAPI)
152 }
153