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 // Default API endpoints.
19 const (
20 APIBaseURL = "https://api.mythic-beasts.com/dns/v2"
21 AuthBaseURL = "https://auth.mythic-beasts.com/login"
22 )
23
24 // Client the Mythic Beasts API client.
25 type Client struct {
26 username string
27 password string
28
29 APIEndpoint *url.URL
30 AuthEndpoint *url.URL
31 HTTPClient *http.Client
32
33 token *Token
34 muToken sync.Mutex
35 }
36
37 // NewClient Creates a new Client.
38 func NewClient(username, password string) *Client {
39 apiEndpoint, _ := url.Parse(APIBaseURL)
40 authEndpoint, _ := url.Parse(AuthBaseURL)
41
42 return &Client{
43 username: username,
44 password: password,
45 APIEndpoint: apiEndpoint,
46 AuthEndpoint: authEndpoint,
47 HTTPClient: &http.Client{Timeout: 5 * time.Second},
48 }
49 }
50
51 // CreateTXTRecord creates a TXT record.
52 // https://www.mythic-beasts.com/support/api/dnsv2#ep-get-zoneszonerecords
53 func (c *Client) CreateTXTRecord(ctx context.Context, zone, leaf, value string, ttl int) error {
54 resp, err := c.createTXTRecord(ctx, zone, leaf, "TXT", value, ttl)
55 if err != nil {
56 return err
57 }
58
59 if resp.Added != 1 {
60 return fmt.Errorf("did not add TXT record for some reason: %s", resp.Message)
61 }
62
63 // Success
64 return nil
65 }
66
67 // RemoveTXTRecord removes a TXT records.
68 // https://www.mythic-beasts.com/support/api/dnsv2#ep-delete-zoneszonerecords
69 func (c *Client) RemoveTXTRecord(ctx context.Context, zone, leaf, value string) error {
70 resp, err := c.removeTXTRecord(ctx, zone, leaf, "TXT", value)
71 if err != nil {
72 return err
73 }
74
75 if resp.Removed != 1 {
76 return fmt.Errorf("did not remove TXT record for some reason: %s", resp.Message)
77 }
78
79 // Success
80 return nil
81 }
82
83 // https://www.mythic-beasts.com/support/api/dnsv2#ep-post-zoneszonerecords
84 func (c *Client) createTXTRecord(ctx context.Context, zone, leaf, recordType, value string, ttl int) (*createTXTResponse, error) {
85 endpoint := c.APIEndpoint.JoinPath("zones", zone, "records", leaf, recordType)
86
87 createReq := createTXTRequest{
88 Records: []createTXTRecord{{
89 Host: leaf,
90 TTL: ttl,
91 Type: "TXT",
92 Data: value,
93 }},
94 }
95
96 req, err := newJSONRequest(ctx, http.MethodPost, endpoint, createReq)
97 if err != nil {
98 return nil, err
99 }
100
101 resp := &createTXTResponse{}
102
103 err = c.do(req, resp)
104 if err != nil {
105 return nil, err
106 }
107
108 return resp, nil
109 }
110
111 // https://www.mythic-beasts.com/support/api/dnsv2#ep-delete-zoneszonerecords
112 func (c *Client) removeTXTRecord(ctx context.Context, zone, leaf, recordType, value string) (*deleteTXTResponse, error) {
113 endpoint := c.APIEndpoint.JoinPath("zones", zone, "records", leaf, recordType)
114
115 query := endpoint.Query()
116 query.Add("data", value)
117 endpoint.RawQuery = query.Encode()
118
119 req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)
120 if err != nil {
121 return nil, err
122 }
123
124 resp := &deleteTXTResponse{}
125
126 err = c.do(req, resp)
127 if err != nil {
128 return nil, err
129 }
130
131 return resp, nil
132 }
133
134 func (c *Client) do(req *http.Request, result any) error {
135 tok := getToken(req.Context())
136 if tok != nil {
137 req.Header.Set("Authorization", "Bearer "+tok.Token)
138 } else {
139 return errors.New("not logged in")
140 }
141
142 resp, err := c.HTTPClient.Do(req)
143 if err != nil {
144 return errutils.NewHTTPDoError(req, err)
145 }
146
147 defer func() { _ = resp.Body.Close() }()
148
149 if resp.StatusCode != http.StatusOK {
150 return errutils.NewUnexpectedResponseStatusCodeError(req, resp)
151 }
152
153 raw, err := io.ReadAll(resp.Body)
154 if err != nil {
155 return errutils.NewReadResponseError(req, resp.StatusCode, err)
156 }
157
158 err = json.Unmarshal(raw, result)
159 if err != nil {
160 return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
161 }
162
163 return nil
164 }
165
166 func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {
167 buf := new(bytes.Buffer)
168
169 if payload != nil {
170 err := json.NewEncoder(buf).Encode(payload)
171 if err != nil {
172 return nil, fmt.Errorf("failed to create request JSON body: %w", err)
173 }
174 }
175
176 req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)
177 if err != nil {
178 return nil, fmt.Errorf("unable to create request: %w", err)
179 }
180
181 req.Header.Set("Accept", "application/json")
182
183 if payload != nil {
184 req.Header.Set("Content-Type", "application/json")
185 }
186
187 return req, nil
188 }
189