client.go raw
1 package internal
2
3 import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "io"
8 "net/http"
9 "net/url"
10 "strings"
11 "time"
12
13 "github.com/go-acme/lego/v4/challenge/dns01"
14 "github.com/go-acme/lego/v4/providers/dns/internal/errutils"
15 )
16
17 const baseURL = "https://api.wedos.com/wapi/json"
18
19 // Client the API client for Webos.
20 type Client struct {
21 username string
22 password string
23
24 baseURL string
25 HTTPClient *http.Client
26 }
27
28 // NewClient creates a new Client.
29 func NewClient(username, password string) *Client {
30 return &Client{
31 username: username,
32 password: password,
33 baseURL: baseURL,
34 HTTPClient: &http.Client{Timeout: 10 * time.Second},
35 }
36 }
37
38 // GetRecords lists all the records in the zone.
39 // https://kb.wedos.com/en/wapi-api-interface/wapi-command-dns-rows-list/
40 func (c *Client) GetRecords(ctx context.Context, zone string) ([]DNSRow, error) {
41 payload := map[string]any{
42 "domain": dns01.UnFqdn(zone),
43 }
44
45 req, err := c.newRequest(ctx, commandDNSRowsList, payload)
46 if err != nil {
47 return nil, err
48 }
49
50 result := APIResponse[Rows]{}
51
52 err = c.do(req, &result)
53 if err != nil {
54 return nil, err
55 }
56
57 return result.Response.Data.Rows, err
58 }
59
60 // AddRecord adds a record in the zone, either by updating existing records or creating new ones.
61 // https://kb.wedos.com/en/wapi-api-interface/wapi-command-dns-add-row/
62 // https://kb.wedos.com/en/wapi-api-interface/wapi-command-dns-row-update/
63 func (c *Client) AddRecord(ctx context.Context, zone string, record DNSRow) error {
64 payload := DNSRowRequest{
65 Domain: dns01.UnFqdn(zone),
66 TTL: record.TTL,
67 Type: record.Type,
68 Data: record.Data,
69 }
70
71 cmd := commandDNSRowAdd
72
73 if record.ID == "" {
74 payload.Name = record.Name
75 } else {
76 cmd = commandDNSRowUpdate
77 payload.ID = record.ID
78 }
79
80 req, err := c.newRequest(ctx, cmd, payload)
81 if err != nil {
82 return err
83 }
84
85 return c.do(req, &APIResponse[json.RawMessage]{})
86 }
87
88 // DeleteRecord deletes a record from the zone.
89 // If a record does not have an ID, it will be looked up.
90 // https://kb.wedos.com/en/wapi-api-interface/wapi-command-dns-row-delete/
91 func (c *Client) DeleteRecord(ctx context.Context, zone, recordID string) error {
92 payload := DNSRowRequest{
93 Domain: dns01.UnFqdn(zone),
94 ID: recordID,
95 }
96
97 req, err := c.newRequest(ctx, commandDNSRowDelete, payload)
98 if err != nil {
99 return err
100 }
101
102 return c.do(req, &APIResponse[json.RawMessage]{})
103 }
104
105 // Commit not really required, all changes will be auto-committed after 5 minutes.
106 // https://kb.wedos.com/en/wapi-api-interface/wapi-command-dns-domain-commit/
107 func (c *Client) Commit(ctx context.Context, zone string) error {
108 payload := map[string]any{
109 "name": dns01.UnFqdn(zone),
110 }
111
112 req, err := c.newRequest(ctx, commandDNSDomainCommit, payload)
113 if err != nil {
114 return err
115 }
116
117 return c.do(req, &APIResponse[json.RawMessage]{})
118 }
119
120 func (c *Client) Ping(ctx context.Context) error {
121 req, err := c.newRequest(ctx, commandPing, nil)
122 if err != nil {
123 return err
124 }
125
126 return c.do(req, &APIResponse[json.RawMessage]{})
127 }
128
129 func (c *Client) do(req *http.Request, result Response) error {
130 resp, err := c.HTTPClient.Do(req)
131 if err != nil {
132 return errutils.NewHTTPDoError(req, err)
133 }
134
135 raw, err := io.ReadAll(resp.Body)
136 if err != nil {
137 return errutils.NewReadResponseError(req, resp.StatusCode, err)
138 }
139
140 if resp.StatusCode/100 != 2 {
141 return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)
142 }
143
144 err = json.Unmarshal(raw, result)
145 if err != nil {
146 return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
147 }
148
149 if result.GetCode() != codeOk {
150 return fmt.Errorf("error %d: %s", result.GetCode(), result.GetResult())
151 }
152
153 return err
154 }
155
156 func (c *Client) newRequest(ctx context.Context, command string, payload any) (*http.Request, error) {
157 requestObject := map[string]any{
158 "request": APIRequest{
159 User: c.username,
160 Auth: authToken(c.username, c.password),
161 Command: command,
162 Data: payload,
163 },
164 }
165
166 object, err := json.Marshal(requestObject)
167 if err != nil {
168 return nil, fmt.Errorf("failed to create request JSON body: %w", err)
169 }
170
171 form := url.Values{}
172 form.Add("request", string(object))
173
174 req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL, strings.NewReader(form.Encode()))
175 if err != nil {
176 return nil, fmt.Errorf("unable to create request: %w", err)
177 }
178
179 req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
180
181 return req, nil
182 }
183