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 "strconv"
12 "time"
13
14 "github.com/go-acme/lego/v4/providers/dns/internal/errutils"
15 )
16
17 // DefaultEndpoint default API endpoint.
18 const DefaultEndpoint = "https://api.autodns.com/v1/"
19
20 // DefaultEndpointContext default API endpoint context.
21 const DefaultEndpointContext int = 4
22
23 // Client the Autodns API client.
24 type Client struct {
25 username string
26 password string
27 context int
28
29 BaseURL *url.URL
30 HTTPClient *http.Client
31 }
32
33 // NewClient creates a new Client.
34 func NewClient(username, password string, clientContext int) *Client {
35 baseURL, _ := url.Parse(DefaultEndpoint)
36
37 return &Client{
38 username: username,
39 password: password,
40 context: clientContext,
41 BaseURL: baseURL,
42 HTTPClient: &http.Client{Timeout: 5 * time.Second},
43 }
44 }
45
46 // AddRecords adds records.
47 func (c *Client) AddRecords(ctx context.Context, domain string, records []*ResourceRecord) (*DataZoneResponse, error) {
48 zoneStream := &ZoneStream{Adds: records}
49
50 return c.updateZone(ctx, domain, zoneStream)
51 }
52
53 // RemoveRecords removes records.
54 func (c *Client) RemoveRecords(ctx context.Context, domain string, records []*ResourceRecord) (*DataZoneResponse, error) {
55 zoneStream := &ZoneStream{Removes: records}
56
57 return c.updateZone(ctx, domain, zoneStream)
58 }
59
60 // https://github.com/InterNetX/domainrobot-api/blob/bdc8fe92a2f32fcbdb29e30bf6006ab446f81223/src/domainrobot.json#L21090
61 func (c *Client) updateZone(ctx context.Context, domain string, zoneStream *ZoneStream) (*DataZoneResponse, error) {
62 endpoint := c.BaseURL.JoinPath("zone", domain, "_stream")
63
64 req, err := newJSONRequest(ctx, http.MethodPost, endpoint, zoneStream)
65 if err != nil {
66 return nil, err
67 }
68
69 var resp *DataZoneResponse
70 if err := c.do(req, &resp); err != nil {
71 return nil, err
72 }
73
74 return resp, nil
75 }
76
77 func (c *Client) do(req *http.Request, result any) error {
78 req.Header.Set("X-Domainrobot-Context", strconv.Itoa(c.context))
79 req.SetBasicAuth(c.username, c.password)
80
81 resp, err := c.HTTPClient.Do(req)
82 if err != nil {
83 return errutils.NewHTTPDoError(req, err)
84 }
85
86 defer func() { _ = resp.Body.Close() }()
87
88 if resp.StatusCode/100 != 2 {
89 return parseError(req, resp)
90 }
91
92 if result == nil {
93 return nil
94 }
95
96 raw, err := io.ReadAll(resp.Body)
97 if err != nil {
98 return errutils.NewReadResponseError(req, resp.StatusCode, err)
99 }
100
101 err = json.Unmarshal(raw, result)
102 if err != nil {
103 return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
104 }
105
106 return nil
107 }
108
109 func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {
110 buf := new(bytes.Buffer)
111
112 if payload != nil {
113 err := json.NewEncoder(buf).Encode(payload)
114 if err != nil {
115 return nil, fmt.Errorf("failed to create request JSON body: %w", err)
116 }
117 }
118
119 req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)
120 if err != nil {
121 return nil, fmt.Errorf("unable to create request: %w", err)
122 }
123
124 req.Header.Set("Accept", "application/json")
125
126 if payload != nil {
127 req.Header.Set("Content-Type", "application/json")
128 }
129
130 return req, nil
131 }
132
133 func parseError(req *http.Request, resp *http.Response) error {
134 raw, _ := io.ReadAll(resp.Body)
135
136 var errAPI APIError
137
138 err := json.Unmarshal(raw, &errAPI)
139 if err != nil {
140 return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)
141 }
142
143 return &errAPI
144 }
145