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 "github.com/go-acme/lego/v4/providers/dns/internal/useragent"
16 )
17
18 const defaultBaseURL = "https://www.webnames.ca/_/APICore"
19
20 // Client the webnames.ca API client.
21 type Client struct {
22 user string
23 key string
24
25 BaseURL *url.URL
26 HTTPClient *http.Client
27 }
28
29 // NewClient creates a new Client.
30 func NewClient(user, key string) (*Client, error) {
31 if user == "" || key == "" {
32 return nil, errors.New("credentials missing")
33 }
34
35 baseURL, _ := url.Parse(defaultBaseURL)
36
37 return &Client{
38 user: user,
39 key: key,
40 BaseURL: baseURL,
41 HTTPClient: &http.Client{Timeout: 10 * time.Second},
42 }, nil
43 }
44
45 func (c *Client) AddTXTRecord(ctx context.Context, domainName, hostName, value string) ([]DNSRecordSet, error) {
46 endpoint := c.BaseURL.JoinPath("domains", domainName, "add-txt-record")
47
48 query := endpoint.Query()
49 query.Set("hostName", hostName)
50 query.Set("txt", value)
51
52 endpoint.RawQuery = query.Encode()
53
54 req, err := newJSONRequest(ctx, http.MethodPost, endpoint, nil)
55 if err != nil {
56 return nil, err
57 }
58
59 var result APIResponse[*DNSInfo]
60
61 err = c.do(req, &result)
62 if err != nil {
63 return nil, err
64 }
65
66 return result.Result.DNSRecordSets, nil
67 }
68
69 func (c *Client) DeleteTXTRecord(ctx context.Context, domainName, hostName, value string) ([]DNSRecordSet, error) {
70 endpoint := c.BaseURL.JoinPath("domains", domainName, "delete-txt-record")
71
72 query := endpoint.Query()
73 query.Set("hostName", hostName)
74 query.Set("txt", value)
75
76 endpoint.RawQuery = query.Encode()
77
78 req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)
79 if err != nil {
80 return nil, err
81 }
82
83 var result APIResponse[*DNSInfo]
84
85 err = c.do(req, &result)
86 if err != nil {
87 return nil, err
88 }
89
90 return result.Result.DNSRecordSets, nil
91 }
92
93 func (c *Client) do(req *http.Request, result any) error {
94 useragent.SetHeader(req.Header)
95
96 req.Header.Set("API-User", c.user)
97 req.Header.Set("API-Key", c.key)
98
99 resp, err := c.HTTPClient.Do(req)
100 if err != nil {
101 return errutils.NewHTTPDoError(req, err)
102 }
103
104 defer func() { _ = resp.Body.Close() }()
105
106 if resp.StatusCode/100 != 2 {
107 return parseError(req, resp)
108 }
109
110 if result == nil {
111 return nil
112 }
113
114 raw, err := io.ReadAll(resp.Body)
115 if err != nil {
116 return errutils.NewReadResponseError(req, resp.StatusCode, err)
117 }
118
119 err = json.Unmarshal(raw, result)
120 if err != nil {
121 return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
122 }
123
124 return nil
125 }
126
127 func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {
128 buf := new(bytes.Buffer)
129
130 if payload != nil {
131 err := json.NewEncoder(buf).Encode(payload)
132 if err != nil {
133 return nil, fmt.Errorf("failed to create request JSON body: %w", err)
134 }
135 }
136
137 req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)
138 if err != nil {
139 return nil, fmt.Errorf("unable to create request: %w", err)
140 }
141
142 req.Header.Set("Accept", "application/json")
143
144 if payload != nil {
145 req.Header.Set("Content-Type", "application/json")
146 }
147
148 return req, nil
149 }
150
151 func parseError(req *http.Request, resp *http.Response) error {
152 raw, _ := io.ReadAll(resp.Body)
153
154 var errAPI APIError
155
156 err := json.Unmarshal(raw, &errAPI)
157 if err != nil {
158 return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)
159 }
160
161 return &errAPI
162 }
163