client.go raw
1 package internal
2
3 import (
4 "bytes"
5 "context"
6 "encoding/json"
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 )
16
17 // defaultBaseURL represents the API endpoint to call.
18 const defaultBaseURL = "https://napi.arvancloud.ir"
19
20 const authorizationHeader = "Authorization"
21
22 // Client the ArvanCloud client.
23 type Client struct {
24 apiKey string
25
26 baseURL *url.URL
27 HTTPClient *http.Client
28 }
29
30 // NewClient Creates a new Client.
31 func NewClient(apiKey string) *Client {
32 baseURL, _ := url.Parse(defaultBaseURL)
33
34 return &Client{
35 apiKey: apiKey,
36 baseURL: baseURL,
37 HTTPClient: &http.Client{Timeout: 5 * time.Second},
38 }
39 }
40
41 // GetTxtRecord gets a TXT record.
42 func (c *Client) GetTxtRecord(ctx context.Context, domain, name, value string) (*DNSRecord, error) {
43 records, err := c.getRecords(ctx, domain, name)
44 if err != nil {
45 return nil, err
46 }
47
48 for _, record := range records {
49 if equalsTXTRecord(record, name, value) {
50 return &record, nil
51 }
52 }
53
54 return nil, fmt.Errorf("could not find record: Domain: %s; Record: %s", domain, name)
55 }
56
57 // https://www.arvancloud.ir/docs/api/cdn/4.0#operation/dns_records.list
58 func (c *Client) getRecords(ctx context.Context, domain, search string) ([]DNSRecord, error) {
59 endpoint := c.baseURL.JoinPath("cdn", "4.0", "domains", domain, "dns-records")
60
61 if search != "" {
62 query := endpoint.Query()
63 query.Set("search", strings.ReplaceAll(search, "_", ""))
64 endpoint.RawQuery = query.Encode()
65 }
66
67 req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)
68 if err != nil {
69 return nil, err
70 }
71
72 response := &apiResponse[[]DNSRecord]{}
73
74 err = c.do(req, http.StatusOK, response)
75 if err != nil {
76 return nil, fmt.Errorf("could not get records %s: Domain: %s: %w", search, domain, err)
77 }
78
79 return response.Data, nil
80 }
81
82 // CreateRecord creates a DNS record.
83 // https://www.arvancloud.ir/docs/api/cdn/4.0#operation/dns_records.create
84 func (c *Client) CreateRecord(ctx context.Context, domain string, record DNSRecord) (*DNSRecord, error) {
85 endpoint := c.baseURL.JoinPath("cdn", "4.0", "domains", domain, "dns-records")
86
87 req, err := newJSONRequest(ctx, http.MethodPost, endpoint, record)
88 if err != nil {
89 return nil, err
90 }
91
92 response := &apiResponse[*DNSRecord]{}
93
94 err = c.do(req, http.StatusCreated, response)
95 if err != nil {
96 return nil, fmt.Errorf("could not create record; Domain: %s: %w", domain, err)
97 }
98
99 return response.Data, nil
100 }
101
102 // DeleteRecord deletes a DNS record.
103 // https://www.arvancloud.ir/docs/api/cdn/4.0#operation/dns_records.remove
104 func (c *Client) DeleteRecord(ctx context.Context, domain, id string) error {
105 endpoint := c.baseURL.JoinPath("cdn", "4.0", "domains", domain, "dns-records", id)
106
107 req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)
108 if err != nil {
109 return err
110 }
111
112 err = c.do(req, http.StatusOK, nil)
113 if err != nil {
114 return fmt.Errorf("could not delete record %s; Domain: %s: %w", id, domain, err)
115 }
116
117 return nil
118 }
119
120 func (c *Client) do(req *http.Request, expectedStatus int, result any) error {
121 req.Header.Set(authorizationHeader, c.apiKey)
122
123 resp, err := c.HTTPClient.Do(req)
124 if err != nil {
125 return errutils.NewHTTPDoError(req, err)
126 }
127
128 defer func() { _ = resp.Body.Close() }()
129
130 if resp.StatusCode != expectedStatus {
131 return errutils.NewUnexpectedResponseStatusCodeError(req, resp)
132 }
133
134 if result == nil {
135 return nil
136 }
137
138 raw, err := io.ReadAll(resp.Body)
139 if err != nil {
140 return errutils.NewReadResponseError(req, resp.StatusCode, err)
141 }
142
143 err = json.Unmarshal(raw, result)
144 if err != nil {
145 return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
146 }
147
148 return nil
149 }
150
151 func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {
152 buf := new(bytes.Buffer)
153
154 if payload != nil {
155 err := json.NewEncoder(buf).Encode(payload)
156 if err != nil {
157 return nil, fmt.Errorf("failed to create request JSON body: %w", err)
158 }
159 }
160
161 req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)
162 if err != nil {
163 return nil, fmt.Errorf("unable to create request: %w", err)
164 }
165
166 req.Header.Set("Accept", "application/json")
167
168 if payload != nil {
169 req.Header.Set("Content-Type", "application/json")
170 }
171
172 return req, nil
173 }
174
175 func equalsTXTRecord(record DNSRecord, name, value string) bool {
176 if record.Type != "txt" {
177 return false
178 }
179
180 if record.Name != name {
181 return false
182 }
183
184 data, ok := record.Value.(map[string]any)
185 if !ok {
186 return false
187 }
188
189 return data["text"] == value
190 }
191