client.go raw

   1  package internal
   2  
   3  import (
   4  	"bytes"
   5  	"context"
   6  	"encoding/json"
   7  	"errors"
   8  	"fmt"
   9  	"io"
  10  	"net/http"
  11  	"net/url"
  12  	"sync"
  13  	"time"
  14  
  15  	"github.com/go-acme/lego/v4/providers/dns/internal/errutils"
  16  )
  17  
  18  type Client struct {
  19  	username    string
  20  	password    string
  21  	domainName  string
  22  	projectName string
  23  
  24  	IdentityEndpoint string
  25  	token            string
  26  	muToken          sync.Mutex
  27  
  28  	baseURL   *url.URL
  29  	muBaseURL sync.Mutex
  30  
  31  	HTTPClient *http.Client
  32  }
  33  
  34  func NewClient(username, password, domainName, projectName string) *Client {
  35  	return &Client{
  36  		username:         username,
  37  		password:         password,
  38  		domainName:       domainName,
  39  		projectName:      projectName,
  40  		IdentityEndpoint: DefaultIdentityEndpoint,
  41  		HTTPClient:       &http.Client{Timeout: 5 * time.Second},
  42  	}
  43  }
  44  
  45  func (c *Client) GetZoneID(ctx context.Context, zone string, privateZone bool) (string, error) {
  46  	zonesResp, err := c.getZones(ctx, zone, privateZone)
  47  	if err != nil {
  48  		return "", err
  49  	}
  50  
  51  	if len(zonesResp.Zones) < 1 {
  52  		return "", fmt.Errorf("zone %s not found", zone)
  53  	}
  54  
  55  	for _, z := range zonesResp.Zones {
  56  		if z.Name == zone {
  57  			return z.ID, nil
  58  		}
  59  	}
  60  
  61  	return "", fmt.Errorf("zone %s not found", zone)
  62  }
  63  
  64  // https://docs.otc.t-systems.com/domain-name-service/api-ref/apis/public_zone_management/querying_public_zones.html
  65  func (c *Client) getZones(ctx context.Context, zone string, privateZone bool) (*ZonesResponse, error) {
  66  	c.muBaseURL.Lock()
  67  	endpoint := c.baseURL.JoinPath("zones")
  68  	c.muBaseURL.Unlock()
  69  
  70  	query := endpoint.Query()
  71  	query.Set("name", zone)
  72  
  73  	if privateZone {
  74  		query.Set("type", "private")
  75  	}
  76  
  77  	endpoint.RawQuery = query.Encode()
  78  
  79  	req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)
  80  	if err != nil {
  81  		return nil, err
  82  	}
  83  
  84  	var zones ZonesResponse
  85  
  86  	err = c.do(req, &zones)
  87  	if err != nil {
  88  		return nil, err
  89  	}
  90  
  91  	return &zones, nil
  92  }
  93  
  94  func (c *Client) GetRecordSetID(ctx context.Context, zoneID, fqdn string) (string, error) {
  95  	recordSetsRes, err := c.getRecordSet(ctx, zoneID, fqdn)
  96  	if err != nil {
  97  		return "", err
  98  	}
  99  
 100  	if len(recordSetsRes.RecordSets) < 1 {
 101  		return "", errors.New("record not found")
 102  	}
 103  
 104  	if len(recordSetsRes.RecordSets) > 1 {
 105  		return "", errors.New("to many records found")
 106  	}
 107  
 108  	if recordSetsRes.RecordSets[0].ID == "" {
 109  		return "", errors.New("id not found")
 110  	}
 111  
 112  	return recordSetsRes.RecordSets[0].ID, nil
 113  }
 114  
 115  // https://docs.otc.t-systems.com/domain-name-service/api-ref/apis/record_set_management/querying_all_record_sets.html
 116  func (c *Client) getRecordSet(ctx context.Context, zoneID, fqdn string) (*RecordSetsResponse, error) {
 117  	c.muBaseURL.Lock()
 118  	endpoint := c.baseURL.JoinPath("zones", zoneID, "recordsets")
 119  	c.muBaseURL.Unlock()
 120  
 121  	query := endpoint.Query()
 122  	query.Set("type", "TXT")
 123  	query.Set("name", fqdn)
 124  	endpoint.RawQuery = query.Encode()
 125  
 126  	req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)
 127  	if err != nil {
 128  		return nil, err
 129  	}
 130  
 131  	var recordSetsRes RecordSetsResponse
 132  
 133  	err = c.do(req, &recordSetsRes)
 134  	if err != nil {
 135  		return nil, err
 136  	}
 137  
 138  	return &recordSetsRes, nil
 139  }
 140  
 141  // CreateRecordSet creates a record.
 142  // https://docs.otc.t-systems.com/domain-name-service/api-ref/apis/record_set_management/creating_a_record_set.html
 143  func (c *Client) CreateRecordSet(ctx context.Context, zoneID string, record RecordSets) error {
 144  	c.muBaseURL.Lock()
 145  	endpoint := c.baseURL.JoinPath("zones", zoneID, "recordsets")
 146  	c.muBaseURL.Unlock()
 147  
 148  	req, err := newJSONRequest(ctx, http.MethodPost, endpoint, record)
 149  	if err != nil {
 150  		return err
 151  	}
 152  
 153  	return c.do(req, nil)
 154  }
 155  
 156  // DeleteRecordSet delete a record set.
 157  // https://docs.otc.t-systems.com/domain-name-service/api-ref/apis/record_set_management/deleting_a_record_set.html
 158  func (c *Client) DeleteRecordSet(ctx context.Context, zoneID, recordID string) error {
 159  	c.muBaseURL.Lock()
 160  	endpoint := c.baseURL.JoinPath("zones", zoneID, "recordsets", recordID)
 161  	c.muBaseURL.Unlock()
 162  
 163  	req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)
 164  	if err != nil {
 165  		return err
 166  	}
 167  
 168  	return c.do(req, nil)
 169  }
 170  
 171  func (c *Client) do(req *http.Request, result any) error {
 172  	c.muToken.Lock()
 173  
 174  	if c.token != "" {
 175  		req.Header.Set("X-Auth-Token", c.token)
 176  	}
 177  
 178  	c.muToken.Unlock()
 179  
 180  	resp, err := c.HTTPClient.Do(req)
 181  	if err != nil {
 182  		return errutils.NewHTTPDoError(req, err)
 183  	}
 184  
 185  	defer func() { _ = resp.Body.Close() }()
 186  
 187  	if resp.StatusCode >= http.StatusBadRequest {
 188  		return errutils.NewUnexpectedResponseStatusCodeError(req, resp)
 189  	}
 190  
 191  	if result == nil {
 192  		return nil
 193  	}
 194  
 195  	raw, err := io.ReadAll(resp.Body)
 196  	if err != nil {
 197  		return errutils.NewReadResponseError(req, resp.StatusCode, err)
 198  	}
 199  
 200  	err = json.Unmarshal(raw, result)
 201  	if err != nil {
 202  		return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
 203  	}
 204  
 205  	return nil
 206  }
 207  
 208  func newJSONRequest[T string | *url.URL](ctx context.Context, method string, endpoint T, payload any) (*http.Request, error) {
 209  	buf := new(bytes.Buffer)
 210  
 211  	if payload != nil {
 212  		err := json.NewEncoder(buf).Encode(payload)
 213  		if err != nil {
 214  			return nil, fmt.Errorf("failed to create request JSON body: %w", err)
 215  		}
 216  	}
 217  
 218  	req, err := http.NewRequestWithContext(ctx, method, fmt.Sprintf("%s", endpoint), buf)
 219  	if err != nil {
 220  		return nil, fmt.Errorf("unable to create request: %w", err)
 221  	}
 222  
 223  	req.Header.Set("Accept", "application/json")
 224  
 225  	if payload != nil {
 226  		req.Header.Set("Content-Type", "application/json")
 227  	}
 228  
 229  	return req, nil
 230  }
 231