client.go raw

   1  package vegadns
   2  
   3  import (
   4  	"context"
   5  	"encoding/json"
   6  	"fmt"
   7  	"io"
   8  	"net/http"
   9  	"net/url"
  10  	"strings"
  11  	"time"
  12  )
  13  
  14  const contentType = "application/x-www-form-urlencoded"
  15  
  16  type Option func(*Client) error
  17  
  18  func WithBasicAuth(user, pass string) Option {
  19  	return func(c *Client) error {
  20  		c.user = user
  21  		c.pass = pass
  22  
  23  		return nil
  24  	}
  25  }
  26  
  27  func WithOAuth(key, secret string) Option {
  28  	return func(c *Client) error {
  29  		c.apiKey = key
  30  		c.apiSecret = secret
  31  
  32  		return nil
  33  	}
  34  }
  35  
  36  func WithHTTPClient(client *http.Client) Option {
  37  	return func(c *Client) error {
  38  		if client == nil {
  39  			c.httpClient = client
  40  		}
  41  
  42  		return nil
  43  	}
  44  }
  45  
  46  type Client struct {
  47  	// Basic Auth
  48  	user string
  49  	pass string
  50  
  51  	// OAuth
  52  	apiKey    string
  53  	apiSecret string
  54  
  55  	httpClient *http.Client
  56  	baseURL    *url.URL
  57  	token      Token
  58  }
  59  
  60  // NewClient create a new [Client].
  61  func NewClient(baseURL string, opts ...Option) (*Client, error) {
  62  	bu, err := url.Parse(baseURL)
  63  	if err != nil {
  64  		return nil, fmt.Errorf("parsing base URL: %w", err)
  65  	}
  66  
  67  	c := &Client{
  68  		httpClient: &http.Client{Timeout: 15 * time.Second},
  69  		baseURL:    bu.JoinPath("1.0"),
  70  	}
  71  
  72  	for _, opt := range opts {
  73  		err := opt(c)
  74  		if err != nil {
  75  			return nil, err
  76  		}
  77  	}
  78  
  79  	return c, nil
  80  }
  81  
  82  func (c *Client) do(req *http.Request, expectedStatusCode int, result any) error {
  83  	resp, err := c.httpClient.Do(req)
  84  	if err != nil {
  85  		return err
  86  	}
  87  
  88  	defer func() { _ = resp.Body.Close() }()
  89  
  90  	if resp.StatusCode != expectedStatusCode {
  91  		body, _ := io.ReadAll(resp.Body)
  92  
  93  		return fmt.Errorf("bad answer from VegaDNS (code: %d, message: %s)", resp.StatusCode, string(body))
  94  	}
  95  
  96  	if result == nil {
  97  		return nil
  98  	}
  99  
 100  	raw, err := io.ReadAll(resp.Body)
 101  	if err != nil {
 102  		return err
 103  	}
 104  
 105  	err = json.Unmarshal(raw, result)
 106  	if err != nil {
 107  		return fmt.Errorf("unmarshalling: %w", err)
 108  	}
 109  
 110  	return nil
 111  }
 112  
 113  func (c *Client) newRequest(ctx context.Context, method string, endpoint *url.URL, params url.Values) (*http.Request, error) {
 114  	var (
 115  		err error
 116  		req *http.Request
 117  	)
 118  
 119  	if method == http.MethodGet || method == http.MethodDelete {
 120  		endpoint.RawQuery = params.Encode()
 121  
 122  		req, err = http.NewRequestWithContext(ctx, method, endpoint.String(), nil)
 123  	} else {
 124  		req, err = http.NewRequestWithContext(ctx, method, endpoint.String(), strings.NewReader(params.Encode()))
 125  	}
 126  
 127  	if err != nil {
 128  		return nil, fmt.Errorf("preparing request: %w", err)
 129  	}
 130  
 131  	err = c.setAuth(ctx, req)
 132  	if err != nil {
 133  		return nil, err
 134  	}
 135  
 136  	req.Header.Set("Content-Type", contentType)
 137  
 138  	return req, nil
 139  }
 140  
 141  func (c *Client) setAuth(ctx context.Context, req *http.Request) error {
 142  	switch {
 143  	// Basic Auth
 144  	case c.user != "" && c.pass != "":
 145  		req.SetBasicAuth(c.user, c.pass)
 146  
 147  	// OAuth
 148  	case c.apiKey != "" && c.apiSecret != "":
 149  		if c.token.valid() != nil {
 150  			token, err := c.getAuthToken(ctx)
 151  			if err != nil {
 152  				return err
 153  			}
 154  
 155  			c.token = token
 156  		}
 157  
 158  		req.Header.Set("Authorization", c.token.formatBearer())
 159  	}
 160  
 161  	return nil
 162  }
 163