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