client.go raw

   1  package internal
   2  
   3  import (
   4  	"context"
   5  	"encoding/json"
   6  	"errors"
   7  	"fmt"
   8  	"io"
   9  	"net/http"
  10  	"net/url"
  11  	"strings"
  12  	"time"
  13  
  14  	"github.com/go-acme/lego/v4/providers/dns/internal/errutils"
  15  	querystring "github.com/google/go-querystring/query"
  16  )
  17  
  18  const statusSuccess = "ok"
  19  
  20  // Client the Technitium API client.
  21  type Client struct {
  22  	apiToken string
  23  
  24  	baseURL    *url.URL
  25  	HTTPClient *http.Client
  26  }
  27  
  28  // NewClient creates a new Client.
  29  func NewClient(baseURL, apiToken string) (*Client, error) {
  30  	if apiToken == "" {
  31  		return nil, errors.New("missing credentials")
  32  	}
  33  
  34  	if baseURL == "" {
  35  		return nil, errors.New("missing server URL")
  36  	}
  37  
  38  	apiEndpoint, err := url.Parse(baseURL)
  39  	if err != nil {
  40  		return nil, err
  41  	}
  42  
  43  	return &Client{
  44  		apiToken:   apiToken,
  45  		baseURL:    apiEndpoint,
  46  		HTTPClient: &http.Client{Timeout: 10 * time.Second},
  47  	}, nil
  48  }
  49  
  50  // AddRecord adds a resource record for an authoritative zone.
  51  // https://github.com/TechnitiumSoftware/DnsServer/blob/master/APIDOCS.md#add-record
  52  func (c *Client) AddRecord(ctx context.Context, record Record) (*Record, error) {
  53  	endpoint := c.baseURL.JoinPath("api", "zones", "records", "add")
  54  
  55  	req, err := c.newFormRequest(ctx, endpoint, record)
  56  	if err != nil {
  57  		return nil, fmt.Errorf("create request: %w", err)
  58  	}
  59  
  60  	result := &APIResponse[AddRecordResponse]{}
  61  
  62  	err = c.do(req, result)
  63  	if err != nil {
  64  		return nil, err
  65  	}
  66  
  67  	if result.Status != statusSuccess {
  68  		return nil, result
  69  	}
  70  
  71  	return result.Response.AddedRecord, nil
  72  }
  73  
  74  // DeleteRecord deletes a record from an authoritative zone.
  75  // https://github.com/TechnitiumSoftware/DnsServer/blob/master/APIDOCS.md#delete-record
  76  func (c *Client) DeleteRecord(ctx context.Context, record Record) error {
  77  	endpoint := c.baseURL.JoinPath("api", "zones", "records", "delete")
  78  
  79  	req, err := c.newFormRequest(ctx, endpoint, record)
  80  	if err != nil {
  81  		return fmt.Errorf("create request: %w", err)
  82  	}
  83  
  84  	result := &APIResponse[any]{}
  85  
  86  	err = c.do(req, result)
  87  	if err != nil {
  88  		return err
  89  	}
  90  
  91  	if result.Status != statusSuccess {
  92  		return result
  93  	}
  94  
  95  	return nil
  96  }
  97  
  98  func (c *Client) do(req *http.Request, result any) error {
  99  	resp, err := c.HTTPClient.Do(req)
 100  	if err != nil {
 101  		return errutils.NewHTTPDoError(req, err)
 102  	}
 103  
 104  	defer func() { _ = resp.Body.Close() }()
 105  
 106  	if resp.StatusCode >= http.StatusBadRequest {
 107  		return parseError(req, resp)
 108  	}
 109  
 110  	raw, err := io.ReadAll(resp.Body)
 111  	if err != nil {
 112  		return errutils.NewReadResponseError(req, resp.StatusCode, err)
 113  	}
 114  
 115  	err = json.Unmarshal(raw, result)
 116  	if err != nil {
 117  		return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
 118  	}
 119  
 120  	return nil
 121  }
 122  
 123  func (c *Client) newFormRequest(ctx context.Context, endpoint *url.URL, payload any) (*http.Request, error) {
 124  	values := url.Values{}
 125  
 126  	if payload != nil {
 127  		var err error
 128  
 129  		values, err = querystring.Values(payload)
 130  		if err != nil {
 131  			return nil, fmt.Errorf("failed to create request body: %w", err)
 132  		}
 133  	}
 134  
 135  	values.Set("token", c.apiToken)
 136  
 137  	req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint.String(), strings.NewReader(values.Encode()))
 138  	if err != nil {
 139  		return nil, fmt.Errorf("unable to create request: %w", err)
 140  	}
 141  
 142  	if payload != nil {
 143  		req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
 144  	}
 145  
 146  	return req, nil
 147  }
 148  
 149  func parseError(req *http.Request, resp *http.Response) error {
 150  	raw, _ := io.ReadAll(resp.Body)
 151  
 152  	var errAPI APIResponse[any]
 153  
 154  	err := json.Unmarshal(raw, &errAPI)
 155  	if err != nil {
 156  		return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)
 157  	}
 158  
 159  	return &errAPI
 160  }
 161