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://console.cloud.ru/api/clouddns/v1"
21 AuthBaseURL = "https://auth.iam.cloud.ru/auth/system/openid/token"
22 )
23
24 // Client the Cloud.ru API client.
25 type Client struct {
26 keyID string
27 secret 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(login, secret string) *Client {
39 apiEndpoint, _ := url.Parse(APIBaseURL)
40 authEndpoint, _ := url.Parse(AuthBaseURL)
41
42 return &Client{
43 keyID: login,
44 secret: secret,
45 APIEndpoint: apiEndpoint,
46 AuthEndpoint: authEndpoint,
47 HTTPClient: &http.Client{Timeout: 5 * time.Second},
48 }
49 }
50
51 func (c *Client) GetZones(ctx context.Context, parentID string) ([]Zone, error) {
52 endpoint := c.APIEndpoint.JoinPath("zones")
53
54 query := endpoint.Query()
55 query.Set("parentId", parentID)
56 endpoint.RawQuery = query.Encode()
57
58 req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)
59 if err != nil {
60 return nil, err
61 }
62
63 var zones APIResponse[Zone]
64
65 err = c.do(req, &zones)
66 if err != nil {
67 return nil, err
68 }
69
70 return zones.Items, nil
71 }
72
73 func (c *Client) GetRecords(ctx context.Context, zoneID string) ([]Record, error) {
74 endpoint := c.APIEndpoint.JoinPath("zones", zoneID, "records")
75
76 req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)
77 if err != nil {
78 return nil, err
79 }
80
81 var records APIResponse[Record]
82
83 err = c.do(req, &records)
84 if err != nil {
85 return nil, err
86 }
87
88 return records.Items, nil
89 }
90
91 func (c *Client) CreateRecord(ctx context.Context, zoneID string, record Record) (*Record, error) {
92 endpoint := c.APIEndpoint.JoinPath("zones", zoneID, "records")
93
94 req, err := newJSONRequest(ctx, http.MethodPost, endpoint, record)
95 if err != nil {
96 return nil, err
97 }
98
99 var result Record
100
101 err = c.do(req, &result)
102 if err != nil {
103 return nil, err
104 }
105
106 return &result, nil
107 }
108
109 func (c *Client) DeleteRecord(ctx context.Context, zoneID, name, recordType string) error {
110 endpoint := c.APIEndpoint.JoinPath("zones", zoneID, "records", name, recordType)
111
112 req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)
113 if err != nil {
114 return err
115 }
116
117 return c.do(req, nil)
118 }
119
120 func (c *Client) do(req *http.Request, result any) error {
121 tok := getToken(req.Context())
122 if tok != nil {
123 req.Header.Set("Authorization", "Bearer "+tok.AccessToken)
124 } else {
125 return errors.New("not logged in")
126 }
127
128 resp, err := c.HTTPClient.Do(req)
129 if err != nil {
130 return errutils.NewHTTPDoError(req, err)
131 }
132
133 defer func() { _ = resp.Body.Close() }()
134
135 if resp.StatusCode != http.StatusOK {
136 return errutils.NewUnexpectedResponseStatusCodeError(req, resp)
137 }
138
139 raw, err := io.ReadAll(resp.Body)
140 if err != nil {
141 return errutils.NewReadResponseError(req, resp.StatusCode, err)
142 }
143
144 if result == nil {
145 return nil
146 }
147
148 err = json.Unmarshal(raw, result)
149 if err != nil {
150 return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
151 }
152
153 return nil
154 }
155
156 func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {
157 buf := new(bytes.Buffer)
158
159 if payload != nil {
160 err := json.NewEncoder(buf).Encode(payload)
161 if err != nil {
162 return nil, fmt.Errorf("failed to create request JSON body: %w", err)
163 }
164 }
165
166 req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)
167 if err != nil {
168 return nil, fmt.Errorf("unable to create request: %w", err)
169 }
170
171 req.Header.Set("Accept", "application/json")
172
173 if payload != nil {
174 req.Header.Set("Content-Type", "application/json")
175 }
176
177 return req, nil
178 }
179