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/providers/dns/internal/errutils"
15 "github.com/pquerna/otp/totp"
16 )
17
18 const (
19 defaultBaseURL = "https://api.nicmanager.com/v1"
20 headerTOTPToken = "X-Auth-Token"
21 )
22
23 // Modes.
24 const (
25 ModeAnycast = "anycast"
26 ModeZone = "zones"
27 )
28
29 // Options the Client options.
30 type Options struct {
31 Login string
32 Username string
33
34 Email string
35
36 Password string
37 OTP string
38
39 Mode string
40 }
41
42 // Client a nicmanager DNS client.
43 type Client struct {
44 username string
45 password string
46 otp string
47
48 mode string
49
50 baseURL *url.URL
51 HTTPClient *http.Client
52 }
53
54 // NewClient create a new Client.
55 func NewClient(opts Options) *Client {
56 c := &Client{
57 mode: ModeAnycast,
58 username: opts.Email,
59 password: opts.Password,
60 otp: opts.OTP,
61 HTTPClient: &http.Client{Timeout: 10 * time.Second},
62 }
63
64 c.baseURL, _ = url.Parse(defaultBaseURL)
65
66 if opts.Mode != "" {
67 c.mode = opts.Mode
68 }
69
70 if opts.Login != "" && opts.Username != "" {
71 c.username = fmt.Sprintf("%s.%s", opts.Login, opts.Username)
72 }
73
74 return c
75 }
76
77 func (c *Client) GetZone(ctx context.Context, name string) (*Zone, error) {
78 endpoint := c.baseURL.JoinPath(c.mode, name)
79
80 req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)
81 if err != nil {
82 return nil, err
83 }
84
85 var zone Zone
86
87 err = c.do(req, http.StatusOK, &zone)
88 if err != nil {
89 return nil, err
90 }
91
92 return &zone, nil
93 }
94
95 func (c *Client) AddRecord(ctx context.Context, zone string, payload RecordCreateUpdate) error {
96 endpoint := c.baseURL.JoinPath(c.mode, zone, "records")
97
98 req, err := newJSONRequest(ctx, http.MethodPost, endpoint, payload)
99 if err != nil {
100 return err
101 }
102
103 err = c.do(req, http.StatusAccepted, nil)
104 if err != nil {
105 return err
106 }
107
108 return nil
109 }
110
111 func (c *Client) DeleteRecord(ctx context.Context, zone string, record int) error {
112 endpoint := c.baseURL.JoinPath(c.mode, zone, "records", strconv.Itoa(record))
113
114 req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)
115 if err != nil {
116 return err
117 }
118
119 err = c.do(req, http.StatusAccepted, nil)
120 if err != nil {
121 return err
122 }
123
124 return nil
125 }
126
127 func (c *Client) do(req *http.Request, expectedStatusCode int, result any) error {
128 req.SetBasicAuth(c.username, c.password)
129
130 if c.otp != "" {
131 tan, err := totp.GenerateCode(c.otp, time.Now())
132 if err != nil {
133 return err
134 }
135
136 req.Header.Set(headerTOTPToken, tan)
137 }
138
139 resp, err := c.HTTPClient.Do(req)
140 if err != nil {
141 return errutils.NewHTTPDoError(req, err)
142 }
143
144 defer func() { _ = resp.Body.Close() }()
145
146 if resp.StatusCode != expectedStatusCode {
147 return parseError(req, resp)
148 }
149
150 if result == nil {
151 return nil
152 }
153
154 raw, err := io.ReadAll(resp.Body)
155 if err != nil {
156 return errutils.NewReadResponseError(req, resp.StatusCode, err)
157 }
158
159 err = json.Unmarshal(raw, result)
160 if err != nil {
161 return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
162 }
163
164 return err
165 }
166
167 func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {
168 buf := new(bytes.Buffer)
169
170 if payload != nil {
171 err := json.NewEncoder(buf).Encode(payload)
172 if err != nil {
173 return nil, fmt.Errorf("failed to create request JSON body: %w", err)
174 }
175 }
176
177 req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)
178 if err != nil {
179 return nil, fmt.Errorf("unable to create request: %w", err)
180 }
181
182 req.Header.Set("Accept", "application/json")
183
184 if payload != nil {
185 req.Header.Set("Content-Type", "application/json")
186 }
187
188 return req, nil
189 }
190
191 func parseError(req *http.Request, resp *http.Response) error {
192 raw, _ := io.ReadAll(resp.Body)
193
194 errAPI := APIError{StatusCode: resp.StatusCode}
195 if err := json.Unmarshal(raw, &errAPI); err != nil {
196 return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)
197 }
198
199 return errAPI
200 }
201