client.go raw
1 package internal
2
3 import (
4 "context"
5 "fmt"
6 "io"
7 "net/http"
8 "net/url"
9 "time"
10
11 "github.com/go-acme/lego/v4/providers/dns/internal/errutils"
12 "github.com/go-acme/lego/v4/providers/dns/internal/useragent"
13 querystring "github.com/google/go-querystring/query"
14 )
15
16 const (
17 addAction = "add"
18 deleteAction = "delete"
19 )
20
21 type Client struct {
22 token string
23 serverURL string
24
25 HTTPClient *http.Client
26 }
27
28 func NewClient(serverURL, token string) (*Client, error) {
29 _, err := url.Parse(serverURL)
30 if err != nil {
31 return nil, fmt.Errorf("server URL: %w", err)
32 }
33
34 return &Client{
35 serverURL: serverURL,
36 token: token,
37 HTTPClient: &http.Client{Timeout: 10 * time.Second},
38 }, nil
39 }
40
41 func (c *Client) AddTXTRecord(ctx context.Context, zone, fqdn, content string) error {
42 return c.updateRecord(ctx, UpdateRecord{Action: addAction, Zone: zone, Type: "TXT", Record: fqdn, Data: content})
43 }
44
45 func (c *Client) DeleteTXTRecord(ctx context.Context, zone, fqdn, recordContent string) error {
46 return c.updateRecord(ctx, UpdateRecord{Action: deleteAction, Zone: zone, Type: "TXT", Record: fqdn, Data: recordContent})
47 }
48
49 func (c *Client) updateRecord(ctx context.Context, action UpdateRecord) error {
50 req, err := c.newRequest(ctx, action)
51 if err != nil {
52 return err
53 }
54
55 return c.do(req)
56 }
57
58 func (c *Client) do(req *http.Request) error {
59 useragent.SetHeader(req.Header)
60
61 req.SetBasicAuth("anonymous", c.token)
62
63 resp, err := c.HTTPClient.Do(req)
64 if err != nil {
65 return errutils.NewHTTPDoError(req, err)
66 }
67
68 defer func() { _ = resp.Body.Close() }()
69
70 // The endpoint uses the `DefaultDdnsResponseWriter`,
71 // and this writer uses HTTP status code to determine if the request was successful or not.
72 // - https://github.com/mhofer117/ispconfig-ddns-module/blob/8b011a5bb138881d9f13360a5c4fec10c0084613/lib/updater/DdnsUpdater.php#L53-L57
73 // - https://github.com/mhofer117/ispconfig-ddns-module/blob/master/lib/updater/response/DefaultDdnsResponseWriter.php
74 if resp.StatusCode/100 != 2 {
75 raw, _ := io.ReadAll(resp.Body)
76
77 return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)
78 }
79
80 return nil
81 }
82
83 func (c *Client) newRequest(ctx context.Context, action UpdateRecord) (*http.Request, error) {
84 endpoint, err := url.Parse(c.serverURL)
85 if err != nil {
86 return nil, err
87 }
88
89 endpoint = endpoint.JoinPath("ddns", "update.php")
90
91 values, err := querystring.Values(action)
92 if err != nil {
93 return nil, err
94 }
95
96 endpoint.RawQuery = values.Encode()
97
98 method := http.MethodPost
99 if action.Action == deleteAction {
100 method = http.MethodDelete
101 }
102
103 req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), nil)
104 if err != nil {
105 return nil, err
106 }
107
108 req.Header.Set("Accept", "application/json")
109
110 return req, nil
111 }
112