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 "sync"
13 "time"
14
15 "github.com/go-acme/lego/v4/providers/dns/internal/errutils"
16 )
17
18 const defaultBaseURL = "https://myaddr.tools"
19
20 // Client the myaddr.{tools,dev,io} API client.
21 type Client struct {
22 baseURL *url.URL
23 HTTPClient *http.Client
24
25 credentials map[string]string
26 credMu sync.Mutex
27 }
28
29 // NewClient creates a new Client.
30 func NewClient(credentials map[string]string) (*Client, error) {
31 if len(credentials) == 0 {
32 return nil, errors.New("credentials missing")
33 }
34
35 baseURL, _ := url.Parse(defaultBaseURL)
36
37 return &Client{
38 baseURL: baseURL,
39 HTTPClient: &http.Client{Timeout: 10 * time.Second},
40 credentials: credentials,
41 }, nil
42 }
43
44 func (c *Client) AddTXTRecord(ctx context.Context, subdomain, value string) error {
45 c.credMu.Lock()
46 privateKey, ok := c.credentials[subdomain]
47 c.credMu.Unlock()
48
49 if !ok {
50 return fmt.Errorf("subdomain %s not found in credentials, check your credentials map", subdomain)
51 }
52
53 payload := ACMEChallenge{Key: privateKey, Data: value}
54
55 req, err := newJSONRequest(ctx, http.MethodPost, c.baseURL.JoinPath("update"), payload)
56 if err != nil {
57 return err
58 }
59
60 return c.do(req, nil)
61 }
62
63 func (c *Client) do(req *http.Request, result any) error {
64 resp, err := c.HTTPClient.Do(req)
65 if err != nil {
66 return errutils.NewHTTPDoError(req, err)
67 }
68
69 defer func() { _ = resp.Body.Close() }()
70
71 if resp.StatusCode/100 != 2 {
72 raw, _ := io.ReadAll(resp.Body)
73 return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)
74 }
75
76 if result == nil {
77 return nil
78 }
79
80 raw, err := io.ReadAll(resp.Body)
81 if err != nil {
82 return errutils.NewReadResponseError(req, resp.StatusCode, err)
83 }
84
85 err = json.Unmarshal(raw, result)
86 if err != nil {
87 return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
88 }
89
90 return nil
91 }
92
93 func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {
94 buf := new(bytes.Buffer)
95
96 if payload != nil {
97 err := json.NewEncoder(buf).Encode(payload)
98 if err != nil {
99 return nil, fmt.Errorf("failed to create request JSON body: %w", err)
100 }
101 }
102
103 req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)
104 if err != nil {
105 return nil, fmt.Errorf("unable to create request: %w", err)
106 }
107
108 req.Header.Set("Accept", "application/json")
109
110 if payload != nil {
111 req.Header.Set("Content-Type", "application/json")
112 }
113
114 return req, nil
115 }
116