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 "strconv"
13 "strings"
14 "time"
15
16 "github.com/go-acme/lego/v4/providers/dns/internal/errutils"
17 )
18
19 const defaultBaseURL = "https://api.simply.com/2/"
20
21 // Client is a Simply.com API client.
22 type Client struct {
23 accountName string
24 apiKey string
25
26 baseURL *url.URL
27 HTTPClient *http.Client
28 }
29
30 // NewClient creates a new Client.
31 func NewClient(accountName, apiKey string) (*Client, error) {
32 if accountName == "" {
33 return nil, errors.New("credentials missing: accountName")
34 }
35
36 if apiKey == "" {
37 return nil, errors.New("credentials missing: apiKey")
38 }
39
40 baseURL, err := url.Parse(defaultBaseURL)
41 if err != nil {
42 return nil, err
43 }
44
45 return &Client{
46 accountName: accountName,
47 apiKey: apiKey,
48 baseURL: baseURL,
49 HTTPClient: &http.Client{Timeout: 5 * time.Second},
50 }, nil
51 }
52
53 // GetRecords lists all the records in the zone.
54 func (c *Client) GetRecords(ctx context.Context, zoneName string) ([]Record, error) {
55 endpoint := c.createEndpoint(zoneName, "/")
56
57 req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)
58 if err != nil {
59 return nil, fmt.Errorf("failed to create request: %w", err)
60 }
61
62 result := &apiResponse[[]Record, json.RawMessage]{}
63
64 err = c.do(req, result)
65 if err != nil {
66 return nil, err
67 }
68
69 return result.Records, nil
70 }
71
72 // AddRecord adds a record.
73 func (c *Client) AddRecord(ctx context.Context, zoneName string, record Record) (int64, error) {
74 endpoint := c.createEndpoint(zoneName, "/")
75
76 req, err := newJSONRequest(ctx, http.MethodPost, endpoint, record)
77 if err != nil {
78 return 0, fmt.Errorf("failed to create request: %w", err)
79 }
80
81 result := &apiResponse[json.RawMessage, recordHeader]{}
82
83 err = c.do(req, result)
84 if err != nil {
85 return 0, err
86 }
87
88 return result.Record.ID, nil
89 }
90
91 // EditRecord updates a record.
92 func (c *Client) EditRecord(ctx context.Context, zoneName string, id int64, record Record) error {
93 endpoint := c.createEndpoint(zoneName, strconv.FormatInt(id, 10))
94
95 req, err := newJSONRequest(ctx, http.MethodPut, endpoint, record)
96 if err != nil {
97 return fmt.Errorf("failed to create request: %w", err)
98 }
99
100 return c.do(req, &apiResponse[json.RawMessage, json.RawMessage]{})
101 }
102
103 // DeleteRecord deletes a record.
104 func (c *Client) DeleteRecord(ctx context.Context, zoneName string, id int64) error {
105 endpoint := c.createEndpoint(zoneName, strconv.FormatInt(id, 10))
106
107 req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)
108 if err != nil {
109 return fmt.Errorf("failed to create request: %w", err)
110 }
111
112 return c.do(req, &apiResponse[json.RawMessage, json.RawMessage]{})
113 }
114
115 func (c *Client) createEndpoint(zoneName, uri string) *url.URL {
116 return c.baseURL.JoinPath("my", "products", zoneName, "dns", "records", strings.TrimSuffix(uri, "/"))
117 }
118
119 func (c *Client) do(req *http.Request, result Response) error {
120 req.SetBasicAuth(c.accountName, c.apiKey)
121
122 resp, err := c.HTTPClient.Do(req)
123 if err != nil {
124 return errutils.NewHTTPDoError(req, err)
125 }
126
127 defer func() { _ = resp.Body.Close() }()
128
129 if resp.StatusCode >= http.StatusInternalServerError {
130 return errutils.NewUnexpectedResponseStatusCodeError(req, resp)
131 }
132
133 raw, err := io.ReadAll(resp.Body)
134 if err != nil {
135 return errutils.NewReadResponseError(req, resp.StatusCode, err)
136 }
137
138 err = json.Unmarshal(raw, result)
139 if err != nil {
140 return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
141 }
142
143 if result.GetStatus() != http.StatusOK {
144 return fmt.Errorf("unexpected error: %s", result.GetMessage())
145 }
146
147 return nil
148 }
149
150 func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {
151 buf := new(bytes.Buffer)
152
153 if payload != nil {
154 err := json.NewEncoder(buf).Encode(payload)
155 if err != nil {
156 return nil, fmt.Errorf("failed to create request JSON body: %w", err)
157 }
158 }
159
160 req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)
161 if err != nil {
162 return nil, fmt.Errorf("unable to create request: %w", err)
163 }
164
165 req.Header.Set("Accept", "application/json")
166
167 if payload != nil {
168 req.Header.Set("Content-Type", "application/json")
169 }
170
171 return req, nil
172 }
173