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/providers/dns/internal/errutils"
  14  )
  15  
  16  // DefaultBaseURL default API endpoint.
  17  const DefaultBaseURL = "https://www.versio.nl/api/v1/"
  18  
  19  // Client the API client for Versio DNS.
  20  type Client struct {
  21  	username string
  22  	password string
  23  
  24  	BaseURL    *url.URL
  25  	HTTPClient *http.Client
  26  }
  27  
  28  // NewClient creates a new Client.
  29  func NewClient(username, password string) *Client {
  30  	baseURL, _ := url.Parse(DefaultBaseURL)
  31  
  32  	return &Client{
  33  		username:   username,
  34  		password:   password,
  35  		BaseURL:    baseURL,
  36  		HTTPClient: &http.Client{Timeout: 5 * time.Second},
  37  	}
  38  }
  39  
  40  // UpdateDomain updates domain information.
  41  // https://www.versio.nl/RESTapidoc/#api-Domains-Update
  42  func (c *Client) UpdateDomain(ctx context.Context, domain string, msg *DomainInfo) (*DomainInfoResponse, error) {
  43  	endpoint := c.BaseURL.JoinPath("domains", domain, "update")
  44  
  45  	req, err := newJSONRequest(ctx, http.MethodPost, endpoint, msg)
  46  	if err != nil {
  47  		return nil, err
  48  	}
  49  
  50  	respData := &DomainInfoResponse{}
  51  
  52  	err = c.do(req, respData)
  53  	if err != nil {
  54  		return nil, err
  55  	}
  56  
  57  	return respData, nil
  58  }
  59  
  60  // GetDomain gets domain information.
  61  // https://www.versio.nl/RESTapidoc/#api-Domains-Domain
  62  func (c *Client) GetDomain(ctx context.Context, domain string) (*DomainInfoResponse, error) {
  63  	endpoint := c.BaseURL.JoinPath("domains", domain)
  64  
  65  	query := endpoint.Query()
  66  	query.Set("show_dns_records", "true")
  67  	endpoint.RawQuery = query.Encode()
  68  
  69  	req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)
  70  	if err != nil {
  71  		return nil, err
  72  	}
  73  
  74  	respData := &DomainInfoResponse{}
  75  
  76  	err = c.do(req, respData)
  77  	if err != nil {
  78  		return nil, err
  79  	}
  80  
  81  	return respData, nil
  82  }
  83  
  84  func (c *Client) do(req *http.Request, result any) error {
  85  	if c.username != "" && c.password != "" {
  86  		req.SetBasicAuth(c.username, c.password)
  87  	}
  88  
  89  	resp, err := c.HTTPClient.Do(req)
  90  	if resp != nil {
  91  		defer func() { _ = resp.Body.Close() }()
  92  	}
  93  
  94  	if err != nil {
  95  		return errutils.NewHTTPDoError(req, err)
  96  	}
  97  
  98  	if resp.StatusCode/100 != 2 {
  99  		return parseError(req, resp)
 100  	}
 101  
 102  	if result == nil {
 103  		return nil
 104  	}
 105  
 106  	raw, err := io.ReadAll(resp.Body)
 107  	if err != nil {
 108  		return errutils.NewReadResponseError(req, resp.StatusCode, err)
 109  	}
 110  
 111  	if err = json.Unmarshal(raw, result); err != nil {
 112  		return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
 113  	}
 114  
 115  	return nil
 116  }
 117  
 118  func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {
 119  	buf := new(bytes.Buffer)
 120  
 121  	if payload != nil {
 122  		err := json.NewEncoder(buf).Encode(payload)
 123  		if err != nil {
 124  			return nil, fmt.Errorf("failed to create request JSON body: %w", err)
 125  		}
 126  	}
 127  
 128  	req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)
 129  	if err != nil {
 130  		return nil, fmt.Errorf("unable to create request: %w", err)
 131  	}
 132  
 133  	req.Header.Set("Accept", "application/json")
 134  
 135  	if payload != nil {
 136  		req.Header.Set("Content-Type", "application/json")
 137  	}
 138  
 139  	return req, nil
 140  }
 141  
 142  func parseError(req *http.Request, resp *http.Response) error {
 143  	raw, _ := io.ReadAll(resp.Body)
 144  
 145  	response := &ErrorResponse{}
 146  
 147  	err := json.Unmarshal(raw, response)
 148  	if err != nil {
 149  		return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)
 150  	}
 151  
 152  	return fmt.Errorf("[status code: %d] %w", resp.StatusCode, response.Message)
 153  }
 154