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