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