lego/providers/dns/vercel/internal/client.go
2022-09-02 09:05:52 +02:00

143 lines
3.4 KiB
Go

package internal
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"path"
"time"
"github.com/go-acme/lego/v4/challenge/dns01"
)
const defaultBaseURL = "https://api.vercel.com"
// Client Vercel client.
type Client struct {
authToken string
teamID string
baseURL *url.URL
HTTPClient *http.Client
}
// NewClient creates a Client.
func NewClient(authToken string, teamID string) *Client {
baseURL, _ := url.Parse(defaultBaseURL)
return &Client{
authToken: authToken,
teamID: teamID,
baseURL: baseURL,
HTTPClient: &http.Client{Timeout: 10 * time.Second},
}
}
// CreateRecord creates a DNS record.
// https://vercel.com/docs/rest-api#endpoints/dns/create-a-dns-record
func (c *Client) CreateRecord(zone string, record Record) (*CreateRecordResponse, error) {
endpoint, err := c.baseURL.Parse(path.Join(c.baseURL.Path, "v2", "domains", dns01.UnFqdn(zone), "records"))
if err != nil {
return nil, err
}
body, err := json.Marshal(record)
if err != nil {
return nil, err
}
req, err := c.newRequest(http.MethodPost, endpoint.String(), bytes.NewReader(body))
if err != nil {
return nil, err
}
resp, err := c.HTTPClient.Do(req)
if err != nil {
return nil, err
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode >= http.StatusBadRequest {
return nil, readError(req, resp)
}
content, err := io.ReadAll(resp.Body)
if err != nil {
return nil, errors.New(toUnreadableBodyMessage(req, content))
}
// Everything looks good; but we'll need the ID later to delete the record
respData := &CreateRecordResponse{}
err = json.Unmarshal(content, respData)
if err != nil {
return nil, fmt.Errorf("%w: %s", err, toUnreadableBodyMessage(req, content))
}
return respData, nil
}
// DeleteRecord deletes a DNS record.
// https://vercel.com/docs/rest-api#endpoints/dns/delete-a-dns-record
func (c *Client) DeleteRecord(zone string, recordID string) error {
endpoint, err := c.baseURL.Parse(path.Join(c.baseURL.Path, "v2", "domains", dns01.UnFqdn(zone), "records", recordID))
if err != nil {
return err
}
req, err := c.newRequest(http.MethodDelete, endpoint.String(), nil)
if err != nil {
return err
}
resp, err := c.HTTPClient.Do(req)
if err != nil {
return err
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode >= http.StatusBadRequest {
return readError(req, resp)
}
return nil
}
func (c *Client) newRequest(method, reqURL string, body io.Reader) (*http.Request, error) {
req, err := http.NewRequest(method, reqURL, body)
if err != nil {
return nil, err
}
if c.teamID != "" {
query := req.URL.Query()
query.Add("teamId", c.teamID)
req.URL.RawQuery = query.Encode()
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.authToken))
return req, nil
}
func readError(req *http.Request, resp *http.Response) error {
content, err := io.ReadAll(resp.Body)
if err != nil {
return errors.New(toUnreadableBodyMessage(req, content))
}
var errInfo APIErrorResponse
err = json.Unmarshal(content, &errInfo)
if err != nil {
return fmt.Errorf("API Error unmarshaling error: %w: %s", err, toUnreadableBodyMessage(req, content))
}
return fmt.Errorf("HTTP %d: %w", resp.StatusCode, errInfo.Error)
}
func toUnreadableBodyMessage(req *http.Request, rawBody []byte) string {
return fmt.Sprintf("the request %s sent a response with a body which is an invalid format: %q", req.URL, string(rawBody))
}