client.go raw

   1  package internal
   2  
   3  import (
   4  	"context"
   5  	"encoding/json"
   6  	"fmt"
   7  	"io"
   8  	"net/http"
   9  	"net/url"
  10  	"time"
  11  
  12  	"github.com/go-acme/lego/v4/providers/dns/internal/errutils"
  13  )
  14  
  15  // DefaultBaseURL the default API endpoint.
  16  const DefaultBaseURL = "https://api.dreamhost.com"
  17  
  18  const (
  19  	cmdAddRecord    = "dns-add_record"
  20  	cmdRemoveRecord = "dns-remove_record"
  21  )
  22  
  23  // Client the Dreamhost API client.
  24  type Client struct {
  25  	apiKey string
  26  
  27  	BaseURL    string
  28  	HTTPClient *http.Client
  29  }
  30  
  31  // NewClient Creates a new Client.
  32  func NewClient(apiKey string) *Client {
  33  	return &Client{
  34  		apiKey:     apiKey,
  35  		BaseURL:    DefaultBaseURL,
  36  		HTTPClient: &http.Client{Timeout: 5 * time.Second},
  37  	}
  38  }
  39  
  40  // AddRecord adds a TXT record.
  41  func (c *Client) AddRecord(ctx context.Context, domain, value string) error {
  42  	query, err := c.buildEndpoint(cmdAddRecord, domain, value)
  43  	if err != nil {
  44  		return err
  45  	}
  46  
  47  	return c.updateTxtRecord(ctx, query)
  48  }
  49  
  50  // RemoveRecord removes a TXT record.
  51  func (c *Client) RemoveRecord(ctx context.Context, domain, value string) error {
  52  	query, err := c.buildEndpoint(cmdRemoveRecord, domain, value)
  53  	if err != nil {
  54  		return err
  55  	}
  56  
  57  	return c.updateTxtRecord(ctx, query)
  58  }
  59  
  60  // action is either cmdAddRecord or cmdRemoveRecord.
  61  func (c *Client) buildEndpoint(action, domain, txt string) (*url.URL, error) {
  62  	endpoint, err := url.Parse(c.BaseURL)
  63  	if err != nil {
  64  		return nil, err
  65  	}
  66  
  67  	query := endpoint.Query()
  68  	query.Set("key", c.apiKey)
  69  	query.Set("cmd", action)
  70  	query.Set("format", "json")
  71  	query.Set("record", domain)
  72  	query.Set("type", "TXT")
  73  	query.Set("value", txt)
  74  	query.Set("comment", url.QueryEscape("Managed By lego"))
  75  	endpoint.RawQuery = query.Encode()
  76  
  77  	return endpoint, nil
  78  }
  79  
  80  // updateTxtRecord will either add or remove a TXT record.
  81  func (c *Client) updateTxtRecord(ctx context.Context, endpoint *url.URL) error {
  82  	req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), http.NoBody)
  83  	if err != nil {
  84  		return fmt.Errorf("unable to create request: %w", err)
  85  	}
  86  
  87  	resp, err := c.HTTPClient.Do(req)
  88  	if err != nil {
  89  		return errutils.NewHTTPDoError(req, err)
  90  	}
  91  
  92  	defer func() { _ = resp.Body.Close() }()
  93  
  94  	if resp.StatusCode != http.StatusOK {
  95  		return errutils.NewUnexpectedResponseStatusCodeError(req, resp)
  96  	}
  97  
  98  	raw, err := io.ReadAll(resp.Body)
  99  	if err != nil {
 100  		return errutils.NewReadResponseError(req, resp.StatusCode, err)
 101  	}
 102  
 103  	var response apiResponse
 104  
 105  	err = json.Unmarshal(raw, &response)
 106  	if err != nil {
 107  		return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
 108  	}
 109  
 110  	if response.Result == "error" {
 111  		return fmt.Errorf("add TXT record failed: %s", response.Data)
 112  	}
 113  
 114  	return nil
 115  }
 116