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 "strconv"
13 "time"
14
15 "github.com/go-acme/lego/v4/providers/dns/internal/errutils"
16 )
17
18 const defaultBaseURL = "https://api360.yandex.net/"
19
20 type Client struct {
21 oauthToken string
22 orgID int64
23
24 baseURL *url.URL
25 HTTPClient *http.Client
26 }
27
28 func NewClient(oauthToken string, orgID int64) (*Client, error) {
29 if oauthToken == "" {
30 return nil, errors.New("OAuth token is required")
31 }
32
33 if orgID == 0 {
34 return nil, errors.New("orgID is required")
35 }
36
37 baseURL, _ := url.Parse(defaultBaseURL)
38
39 return &Client{
40 oauthToken: oauthToken,
41 orgID: orgID,
42 baseURL: baseURL,
43 HTTPClient: &http.Client{Timeout: 10 * time.Second},
44 }, nil
45 }
46
47 // AddRecord Adds a DNS record.
48 // POST https://api30.yandex.net/directory/v1/org/{orgId}/domains/{domain}/dns
49 // https://yandex.ru/dev/api360/doc/ref/DomainDNSService/DomainDNSService_Create.html
50 func (c *Client) AddRecord(ctx context.Context, domain string, record Record) (*Record, error) {
51 endpoint := c.baseURL.JoinPath("directory", "v1", "org", strconv.FormatInt(c.orgID, 10), "domains", domain, "dns")
52
53 req, err := newJSONRequest(ctx, http.MethodPost, endpoint, record)
54 if err != nil {
55 return nil, err
56 }
57
58 var newRecord Record
59
60 err = c.do(req, &newRecord)
61 if err != nil {
62 return nil, err
63 }
64
65 return &newRecord, nil
66 }
67
68 // DeleteRecord Deletes a DNS record.
69 // DELETE https://api360.yandex.net/directory/v1/org/{orgId}/domains/{domain}/dns/{recordId}
70 // https://yandex.ru/dev/api360/doc/ref/DomainDNSService/DomainDNSService_Delete.html
71 func (c *Client) DeleteRecord(ctx context.Context, domain string, recordID int64) error {
72 endpoint := c.baseURL.JoinPath("directory", "v1", "org", strconv.FormatInt(c.orgID, 10), "domains", domain, "dns", strconv.FormatInt(recordID, 10))
73
74 req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)
75 if err != nil {
76 return err
77 }
78
79 return c.do(req, nil)
80 }
81
82 func (c *Client) do(req *http.Request, result any) error {
83 req.Header.Set("Authorization", "OAuth "+c.oauthToken)
84
85 resp, err := c.HTTPClient.Do(req)
86 if err != nil {
87 return errutils.NewHTTPDoError(req, err)
88 }
89
90 defer func() { _ = resp.Body.Close() }()
91
92 if resp.StatusCode/100 != 2 {
93 return parseError(req, resp)
94 }
95
96 if result == nil {
97 return nil
98 }
99
100 raw, err := io.ReadAll(resp.Body)
101 if err != nil {
102 return errutils.NewReadResponseError(req, resp.StatusCode, err)
103 }
104
105 err = json.Unmarshal(raw, result)
106 if err != nil {
107 return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
108 }
109
110 return nil
111 }
112
113 func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {
114 buf := new(bytes.Buffer)
115
116 if payload != nil {
117 err := json.NewEncoder(buf).Encode(payload)
118 if err != nil {
119 return nil, fmt.Errorf("failed to create request JSON body: %w", err)
120 }
121 }
122
123 req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)
124 if err != nil {
125 return nil, fmt.Errorf("unable to create request: %w", err)
126 }
127
128 req.Header.Set("Accept", "application/json")
129
130 if payload != nil {
131 req.Header.Set("Content-Type", "application/json")
132 }
133
134 return req, nil
135 }
136
137 func parseError(req *http.Request, resp *http.Response) error {
138 raw, _ := io.ReadAll(resp.Body)
139
140 var apiErr APIError
141
142 err := json.Unmarshal(raw, &apiErr)
143 if err != nil {
144 return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)
145 }
146
147 return fmt.Errorf("[status code: %d] %w", resp.StatusCode, apiErr)
148 }
149