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 const defaultBaseURL = "https://api.mittwald.de/v2/"
17
18 const authorizationHeader = "Authorization"
19
20 // Client the Mittwald client.
21 type Client struct {
22 token string
23
24 baseURL *url.URL
25 HTTPClient *http.Client
26 }
27
28 // NewClient Creates a new Client.
29 func NewClient(token string) *Client {
30 baseURL, _ := url.Parse(defaultBaseURL)
31
32 return &Client{
33 token: token,
34 baseURL: baseURL,
35 HTTPClient: &http.Client{Timeout: 5 * time.Second},
36 }
37 }
38
39 // ListDomains List Domains.
40 // https://api.mittwald.de/v2/docs/#/Domain/domain-list-domains
41 func (c *Client) ListDomains(ctx context.Context) ([]Domain, error) {
42 endpoint := c.baseURL.JoinPath("domains")
43
44 req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)
45 if err != nil {
46 return nil, err
47 }
48
49 var result []Domain
50
51 err = c.do(req, &result)
52 if err != nil {
53 return nil, err
54 }
55
56 return result, nil
57 }
58
59 // GetDNSZone Get a DNSZone.
60 // https://api.mittwald.de/v2/docs/#/Domain/dns-get-dns-zone
61 func (c *Client) GetDNSZone(ctx context.Context, zoneID string) (*DNSZone, error) {
62 endpoint := c.baseURL.JoinPath("dns-zones", zoneID)
63
64 req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)
65 if err != nil {
66 return nil, err
67 }
68
69 result := &DNSZone{}
70
71 err = c.do(req, result)
72 if err != nil {
73 return nil, err
74 }
75
76 return result, nil
77 }
78
79 // ListDNSZones List DNSZones belonging to a Project.
80 // https://api.mittwald.de/v2/docs/#/Domain/dns-list-dns-zones
81 func (c *Client) ListDNSZones(ctx context.Context, projectID string) ([]DNSZone, error) {
82 endpoint := c.baseURL.JoinPath("projects", projectID, "dns-zones")
83
84 req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)
85 if err != nil {
86 return nil, err
87 }
88
89 var result []DNSZone
90
91 err = c.do(req, &result)
92 if err != nil {
93 return nil, err
94 }
95
96 return result, nil
97 }
98
99 // CreateDNSZone Create a DNSZone.
100 // https://api.mittwald.de/v2/docs/#/Domain/dns-create-dns-zone
101 func (c *Client) CreateDNSZone(ctx context.Context, zone CreateDNSZoneRequest) (*DNSZone, error) {
102 endpoint := c.baseURL.JoinPath("dns-zones")
103
104 req, err := newJSONRequest(ctx, http.MethodPost, endpoint, zone)
105 if err != nil {
106 return nil, err
107 }
108
109 result := &DNSZone{}
110
111 err = c.do(req, result)
112 if err != nil {
113 return nil, err
114 }
115
116 return result, nil
117 }
118
119 // UpdateTXTRecord Update a record set on a DNSZone.
120 // https://api.mittwald.de/v2/docs/#/Domain/dns-update-record-set
121 func (c *Client) UpdateTXTRecord(ctx context.Context, zoneID string, record TXTRecord) error {
122 endpoint := c.baseURL.JoinPath("dns-zones", zoneID, "record-sets", "txt")
123
124 req, err := newJSONRequest(ctx, http.MethodPut, endpoint, record)
125 if err != nil {
126 return err
127 }
128
129 return c.do(req, nil)
130 }
131
132 // DeleteDNSZone Delete a DNSZone.
133 // https://api.mittwald.de/v2/docs/#/Domain/dns-delete-dns-zone
134 func (c *Client) DeleteDNSZone(ctx context.Context, zoneID string) error {
135 endpoint := c.baseURL.JoinPath("dns-zones", zoneID)
136
137 req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)
138 if err != nil {
139 return err
140 }
141
142 return c.do(req, nil)
143 }
144
145 func (c *Client) do(req *http.Request, result any) error {
146 req.Header.Set(authorizationHeader, "Bearer "+c.token)
147
148 resp, err := c.HTTPClient.Do(req)
149 if err != nil {
150 return errutils.NewHTTPDoError(req, err)
151 }
152
153 defer func() { _ = resp.Body.Close() }()
154
155 if resp.StatusCode/100 != 2 {
156 return parseError(req, resp)
157 }
158
159 if result == nil {
160 return nil
161 }
162
163 raw, err := io.ReadAll(resp.Body)
164 if err != nil {
165 return errutils.NewReadResponseError(req, resp.StatusCode, err)
166 }
167
168 err = json.Unmarshal(raw, result)
169 if err != nil {
170 return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
171 }
172
173 return nil
174 }
175
176 func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {
177 buf := new(bytes.Buffer)
178
179 if payload != nil {
180 err := json.NewEncoder(buf).Encode(payload)
181 if err != nil {
182 return nil, fmt.Errorf("failed to create request JSON body: %w", err)
183 }
184 }
185
186 req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)
187 if err != nil {
188 return nil, fmt.Errorf("unable to create request: %w", err)
189 }
190
191 req.Header.Set("Accept", "application/json")
192
193 if payload != nil {
194 req.Header.Set("Content-Type", "application/json")
195 }
196
197 return req, nil
198 }
199
200 func parseError(req *http.Request, resp *http.Response) error {
201 raw, _ := io.ReadAll(resp.Body)
202
203 var response APIError
204
205 err := json.Unmarshal(raw, &response)
206 if err != nil {
207 return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)
208 }
209
210 return fmt.Errorf("[status code %d] %w", resp.StatusCode, response)
211 }
212