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