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 baseAPIURL = "https://api.servercow.de/dns/v1/domains"
18
19 // Client the Servercow client.
20 type Client struct {
21 username string
22 password string
23
24 baseURL *url.URL
25 HTTPClient *http.Client
26 }
27
28 // NewClient Creates a Servercow client.
29 func NewClient(username, password string) *Client {
30 baseURL, _ := url.Parse(baseAPIURL)
31
32 return &Client{
33 username: username,
34 password: password,
35 baseURL: baseURL,
36 HTTPClient: &http.Client{Timeout: 5 * time.Second},
37 }
38 }
39
40 // GetRecords from API.
41 func (c *Client) GetRecords(ctx context.Context, domain string) ([]Record, error) {
42 endpoint := c.baseURL.JoinPath(domain)
43
44 req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)
45 if err != nil {
46 return nil, err
47 }
48
49 var records []Record
50
51 err = c.do(req, &records)
52 if err != nil {
53 return nil, err
54 }
55
56 return records, nil
57 }
58
59 // CreateUpdateRecord creates or updates a record.
60 func (c *Client) CreateUpdateRecord(ctx context.Context, domain string, data Record) (*Message, error) {
61 endpoint := c.baseURL.JoinPath(domain)
62
63 req, err := newJSONRequest(ctx, http.MethodPost, endpoint, data)
64 if err != nil {
65 return nil, err
66 }
67
68 var msg Message
69
70 err = c.do(req, &msg)
71 if err != nil {
72 return nil, err
73 }
74
75 if msg.ErrorMsg != "" {
76 return nil, msg
77 }
78
79 return &msg, nil
80 }
81
82 // DeleteRecord deletes a record.
83 func (c *Client) DeleteRecord(ctx context.Context, domain string, data Record) (*Message, error) {
84 endpoint := c.baseURL.JoinPath(domain)
85
86 req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, data)
87 if err != nil {
88 return nil, err
89 }
90
91 var msg Message
92
93 err = c.do(req, &msg)
94 if err != nil {
95 return nil, err
96 }
97
98 if msg.ErrorMsg != "" {
99 return nil, msg
100 }
101
102 return &msg, nil
103 }
104
105 func (c *Client) do(req *http.Request, result any) error {
106 req.Header.Set("X-Auth-Username", c.username)
107 req.Header.Set("X-Auth-Password", c.password)
108
109 resp, err := c.HTTPClient.Do(req)
110 if err != nil {
111 return errutils.NewHTTPDoError(req, err)
112 }
113
114 defer func() { _ = resp.Body.Close() }()
115
116 // Note the API always return 200 even if the authentication failed.
117 if resp.StatusCode/100 != 2 {
118 return errutils.NewUnexpectedResponseStatusCodeError(req, resp)
119 }
120
121 if result == nil {
122 return nil
123 }
124
125 raw, err := io.ReadAll(resp.Body)
126 if err != nil {
127 return errutils.NewReadResponseError(req, resp.StatusCode, err)
128 }
129
130 err = unmarshal(raw, result)
131 if err != nil {
132 return err
133 }
134
135 return nil
136 }
137
138 func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {
139 buf := new(bytes.Buffer)
140
141 if payload != nil {
142 err := json.NewEncoder(buf).Encode(payload)
143 if err != nil {
144 return nil, fmt.Errorf("failed to create request JSON body: %w", err)
145 }
146 }
147
148 req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)
149 if err != nil {
150 return nil, fmt.Errorf("unable to create request: %w", err)
151 }
152
153 req.Header.Set("Accept", "application/json")
154
155 // Content-Type should be added even if there is no request body.
156 req.Header.Set("Content-Type", "application/json")
157
158 return req, nil
159 }
160
161 func unmarshal(raw []byte, v any) error {
162 err := json.Unmarshal(raw, v)
163 if err == nil {
164 return nil
165 }
166
167 var utErr *json.UnmarshalTypeError
168
169 if !errors.As(err, &utErr) {
170 return fmt.Errorf("unmarshaling %T error: %w: %s", v, err, string(raw))
171 }
172
173 var apiErr Message
174
175 errU := json.Unmarshal(raw, &apiErr)
176 if errU != nil {
177 return fmt.Errorf("unmarshaling %T error: %w: %s", v, err, string(raw))
178 }
179
180 return apiErr
181 }
182