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 "strconv"
12 "time"
13
14 "github.com/go-acme/lego/v4/challenge/dns01"
15 "github.com/go-acme/lego/v4/providers/dns/internal/errutils"
16 "golang.org/x/oauth2"
17 )
18
19 const defaultBaseURL = "https://api.timeweb.cloud/api"
20
21 // Client Timeweb Cloud client.
22 type Client struct {
23 baseURL *url.URL
24 httpClient *http.Client
25 }
26
27 // NewClient creates a Client.
28 func NewClient(hc *http.Client) *Client {
29 baseURL, _ := url.Parse(defaultBaseURL)
30
31 if hc == nil {
32 hc = &http.Client{Timeout: 10 * time.Second}
33 }
34
35 return &Client{
36 baseURL: baseURL,
37 httpClient: hc,
38 }
39 }
40
41 // CreateRecord creates a DNS record.
42 // https://timeweb.cloud/api-docs#tag/Domeny/operation/createDomainDNSRecord
43 func (c *Client) CreateRecord(ctx context.Context, zone string, record DNSRecord) (*DNSRecord, error) {
44 endpoint := c.baseURL.JoinPath("v1", "domains", dns01.UnFqdn(zone), "dns-records")
45
46 req, err := newJSONRequest(ctx, http.MethodPost, endpoint, record)
47 if err != nil {
48 return nil, err
49 }
50
51 respData := &CreateRecordResponse{}
52
53 err = c.do(req, respData)
54 if err != nil {
55 return nil, err
56 }
57
58 return respData.DNSRecord, nil
59 }
60
61 // DeleteRecord deletes a DNS record.
62 // https://timeweb.cloud/api-docs#tag/Domeny/operation/deleteDomainDNSRecord
63 func (c *Client) DeleteRecord(ctx context.Context, zone string, recordID int) error {
64 endpoint := c.baseURL.JoinPath("v1", "domains", dns01.UnFqdn(zone), "dns-records", strconv.Itoa(recordID))
65
66 req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)
67 if err != nil {
68 return err
69 }
70
71 return c.do(req, nil)
72 }
73
74 func (c *Client) do(req *http.Request, result any) error {
75 resp, err := c.httpClient.Do(req)
76 if err != nil {
77 return errutils.NewHTTPDoError(req, err)
78 }
79
80 defer func() { _ = resp.Body.Close() }()
81
82 if resp.StatusCode/100 != 2 {
83 return parseError(req, resp)
84 }
85
86 if result == nil {
87 return nil
88 }
89
90 raw, err := io.ReadAll(resp.Body)
91 if err != nil {
92 return errutils.NewReadResponseError(req, resp.StatusCode, err)
93 }
94
95 err = json.Unmarshal(raw, result)
96 if err != nil {
97 return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
98 }
99
100 return nil
101 }
102
103 func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {
104 buf := new(bytes.Buffer)
105
106 if payload != nil {
107 err := json.NewEncoder(buf).Encode(payload)
108 if err != nil {
109 return nil, fmt.Errorf("failed to create request JSON body: %w", err)
110 }
111 }
112
113 req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)
114 if err != nil {
115 return nil, fmt.Errorf("unable to create request: %w", err)
116 }
117
118 req.Header.Set("Accept", "application/json")
119
120 if payload != nil {
121 req.Header.Set("Content-Type", "application/json")
122 }
123
124 return req, nil
125 }
126
127 func parseError(req *http.Request, resp *http.Response) error {
128 raw, _ := io.ReadAll(resp.Body)
129
130 var response ErrorResponse
131
132 err := json.Unmarshal(raw, &response)
133 if err != nil {
134 return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)
135 }
136
137 return response
138 }
139
140 func OAuthStaticAccessToken(client *http.Client, accessToken string) *http.Client {
141 if client == nil {
142 client = &http.Client{Timeout: 10 * time.Second}
143 }
144
145 client.Transport = &oauth2.Transport{
146 Source: oauth2.StaticTokenSource(&oauth2.Token{AccessToken: accessToken}),
147 Base: client.Transport,
148 }
149
150 return client
151 }
152