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 "github.com/go-acme/lego/v4/providers/dns/internal/useragent"
15 )
16
17 const defaultBaseURL = "https://api.hosting.nl"
18
19 type Client struct {
20 apiKey string
21
22 BaseURL *url.URL
23 HTTPClient *http.Client
24 }
25
26 func NewClient(apiKey string) *Client {
27 baseURL, _ := url.Parse(defaultBaseURL)
28
29 return &Client{
30 apiKey: apiKey,
31 BaseURL: baseURL,
32 HTTPClient: &http.Client{Timeout: 5 * time.Second},
33 }
34 }
35
36 func (c Client) AddRecord(ctx context.Context, domain string, record Record) (*Record, error) {
37 endpoint := c.BaseURL.JoinPath("domains", domain, "dns")
38
39 req, err := newJSONRequest(ctx, http.MethodPost, endpoint, []Record{record})
40 if err != nil {
41 return nil, err
42 }
43
44 var result APIResponse[Record]
45
46 err = c.do(req, &result)
47 if err != nil {
48 return nil, err
49 }
50
51 if len(result.Data) != 1 {
52 return nil, fmt.Errorf("unexpected response data: %v", result.Data)
53 }
54
55 return &result.Data[0], nil
56 }
57
58 func (c Client) DeleteRecord(ctx context.Context, domain, recordID string) error {
59 endpoint := c.BaseURL.JoinPath("domains", domain, "dns")
60
61 req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, []Record{{ID: recordID}})
62 if err != nil {
63 return err
64 }
65
66 var result APIResponse[Record]
67
68 err = c.do(req, &result)
69 if err != nil {
70 return err
71 }
72
73 return nil
74 }
75
76 func (c Client) do(req *http.Request, result any) error {
77 useragent.SetHeader(req.Header)
78
79 req.Header.Set("API-TOKEN", c.apiKey)
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 != http.StatusOK {
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 parseError(req *http.Request, resp *http.Response) error {
110 raw, _ := io.ReadAll(resp.Body)
111
112 var apiErr APIError
113
114 err := json.Unmarshal(raw, &apiErr)
115 if err != nil {
116 return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)
117 }
118
119 return fmt.Errorf("[status code: %d] %w", resp.StatusCode, apiErr)
120 }
121
122 func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {
123 buf := new(bytes.Buffer)
124
125 if payload != nil {
126 err := json.NewEncoder(buf).Encode(payload)
127 if err != nil {
128 return nil, fmt.Errorf("failed to create request JSON body: %w", err)
129 }
130 }
131
132 req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)
133 if err != nil {
134 return nil, fmt.Errorf("unable to create request: %w", err)
135 }
136
137 req.Header.Set("Accept", "application/json")
138
139 if payload != nil {
140 req.Header.Set("Content-Type", "application/json")
141 }
142
143 return req, nil
144 }
145