client.go raw
1 package internal
2
3 import (
4 "context"
5 "encoding/json"
6 "errors"
7 "fmt"
8 "io"
9 "net/http"
10 "net/url"
11 "strings"
12 "time"
13
14 "github.com/go-acme/lego/v4/providers/dns/internal/errutils"
15 querystring "github.com/google/go-querystring/query"
16 )
17
18 const statusSuccess = "ok"
19
20 // Client the Technitium API client.
21 type Client struct {
22 apiToken string
23
24 baseURL *url.URL
25 HTTPClient *http.Client
26 }
27
28 // NewClient creates a new Client.
29 func NewClient(baseURL, apiToken string) (*Client, error) {
30 if apiToken == "" {
31 return nil, errors.New("missing credentials")
32 }
33
34 if baseURL == "" {
35 return nil, errors.New("missing server URL")
36 }
37
38 apiEndpoint, err := url.Parse(baseURL)
39 if err != nil {
40 return nil, err
41 }
42
43 return &Client{
44 apiToken: apiToken,
45 baseURL: apiEndpoint,
46 HTTPClient: &http.Client{Timeout: 10 * time.Second},
47 }, nil
48 }
49
50 // AddRecord adds a resource record for an authoritative zone.
51 // https://github.com/TechnitiumSoftware/DnsServer/blob/master/APIDOCS.md#add-record
52 func (c *Client) AddRecord(ctx context.Context, record Record) (*Record, error) {
53 endpoint := c.baseURL.JoinPath("api", "zones", "records", "add")
54
55 req, err := c.newFormRequest(ctx, endpoint, record)
56 if err != nil {
57 return nil, fmt.Errorf("create request: %w", err)
58 }
59
60 result := &APIResponse[AddRecordResponse]{}
61
62 err = c.do(req, result)
63 if err != nil {
64 return nil, err
65 }
66
67 if result.Status != statusSuccess {
68 return nil, result
69 }
70
71 return result.Response.AddedRecord, nil
72 }
73
74 // DeleteRecord deletes a record from an authoritative zone.
75 // https://github.com/TechnitiumSoftware/DnsServer/blob/master/APIDOCS.md#delete-record
76 func (c *Client) DeleteRecord(ctx context.Context, record Record) error {
77 endpoint := c.baseURL.JoinPath("api", "zones", "records", "delete")
78
79 req, err := c.newFormRequest(ctx, endpoint, record)
80 if err != nil {
81 return fmt.Errorf("create request: %w", err)
82 }
83
84 result := &APIResponse[any]{}
85
86 err = c.do(req, result)
87 if err != nil {
88 return err
89 }
90
91 if result.Status != statusSuccess {
92 return result
93 }
94
95 return nil
96 }
97
98 func (c *Client) do(req *http.Request, result any) error {
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 >= http.StatusBadRequest {
107 return parseError(req, resp)
108 }
109
110 raw, err := io.ReadAll(resp.Body)
111 if err != nil {
112 return errutils.NewReadResponseError(req, resp.StatusCode, err)
113 }
114
115 err = json.Unmarshal(raw, result)
116 if err != nil {
117 return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
118 }
119
120 return nil
121 }
122
123 func (c *Client) newFormRequest(ctx context.Context, endpoint *url.URL, payload any) (*http.Request, error) {
124 values := url.Values{}
125
126 if payload != nil {
127 var err error
128
129 values, err = querystring.Values(payload)
130 if err != nil {
131 return nil, fmt.Errorf("failed to create request body: %w", err)
132 }
133 }
134
135 values.Set("token", c.apiToken)
136
137 req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint.String(), strings.NewReader(values.Encode()))
138 if err != nil {
139 return nil, fmt.Errorf("unable to create request: %w", err)
140 }
141
142 if payload != nil {
143 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
144 }
145
146 return req, nil
147 }
148
149 func parseError(req *http.Request, resp *http.Response) error {
150 raw, _ := io.ReadAll(resp.Body)
151
152 var errAPI APIResponse[any]
153
154 err := json.Unmarshal(raw, &errAPI)
155 if err != nil {
156 return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)
157 }
158
159 return &errAPI
160 }
161