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  	"time"
  12  
  13  	"github.com/go-acme/lego/v4/providers/dns/internal/errutils"
  14  )
  15  
  16  // defaultBaseURL for reaching the jSON-based API-Endpoint of netcup.
  17  const defaultBaseURL = "https://ccp.netcup.net/run/webservice/servers/endpoint.php?JSON"
  18  
  19  // Client netcup DNS client.
  20  type Client struct {
  21  	customerNumber string
  22  	apiKey         string
  23  	apiPassword    string
  24  
  25  	baseURL    string
  26  	HTTPClient *http.Client
  27  }
  28  
  29  // NewClient creates a netcup DNS client.
  30  func NewClient(customerNumber, apiKey, apiPassword string) (*Client, error) {
  31  	if customerNumber == "" || apiKey == "" || apiPassword == "" {
  32  		return nil, errors.New("credentials missing")
  33  	}
  34  
  35  	return &Client{
  36  		customerNumber: customerNumber,
  37  		apiKey:         apiKey,
  38  		apiPassword:    apiPassword,
  39  		baseURL:        defaultBaseURL,
  40  		HTTPClient:     &http.Client{Timeout: 10 * time.Second},
  41  	}, nil
  42  }
  43  
  44  // UpdateDNSRecord performs an update of the DNSRecords as specified by the netcup WSDL.
  45  // https://ccp.netcup.net/run/webservice/servers/endpoint.php
  46  func (c *Client) UpdateDNSRecord(ctx context.Context, domainName string, records []DNSRecord) error {
  47  	payload := &Request{
  48  		Action: "updateDnsRecords",
  49  		Param: UpdateDNSRecordsRequest{
  50  			DomainName:      domainName,
  51  			CustomerNumber:  c.customerNumber,
  52  			APIKey:          c.apiKey,
  53  			APISessionID:    getSessionID(ctx),
  54  			ClientRequestID: "",
  55  			DNSRecordSet:    DNSRecordSet{DNSRecords: records},
  56  		},
  57  	}
  58  
  59  	err := c.doRequest(ctx, payload, nil)
  60  	if err != nil {
  61  		return fmt.Errorf("error when sending the request: %w", err)
  62  	}
  63  
  64  	return nil
  65  }
  66  
  67  // GetDNSRecords retrieves all dns records of an DNS-Zone as specified by the netcup WSDL
  68  // returns an array of DNSRecords.
  69  // https://ccp.netcup.net/run/webservice/servers/endpoint.php
  70  func (c *Client) GetDNSRecords(ctx context.Context, hostname string) ([]DNSRecord, error) {
  71  	payload := &Request{
  72  		Action: "infoDnsRecords",
  73  		Param: InfoDNSRecordsRequest{
  74  			DomainName:      hostname,
  75  			CustomerNumber:  c.customerNumber,
  76  			APIKey:          c.apiKey,
  77  			APISessionID:    getSessionID(ctx),
  78  			ClientRequestID: "",
  79  		},
  80  	}
  81  
  82  	var responseData InfoDNSRecordsResponse
  83  
  84  	err := c.doRequest(ctx, payload, &responseData)
  85  	if err != nil {
  86  		return nil, fmt.Errorf("error when sending the request: %w", err)
  87  	}
  88  
  89  	return responseData.DNSRecords, nil
  90  }
  91  
  92  // doRequest marshals given body to JSON, send the request to netcup API
  93  // and returns body of response.
  94  func (c *Client) doRequest(ctx context.Context, payload, result any) error {
  95  	req, err := newJSONRequest(ctx, http.MethodPost, c.baseURL, payload)
  96  	if err != nil {
  97  		return err
  98  	}
  99  
 100  	req.Close = true
 101  
 102  	resp, err := c.HTTPClient.Do(req)
 103  	if err != nil {
 104  		return errutils.NewHTTPDoError(req, err)
 105  	}
 106  
 107  	defer func() { _ = resp.Body.Close() }()
 108  
 109  	if resp.StatusCode >= http.StatusMultipleChoices {
 110  		return errutils.NewUnexpectedResponseStatusCodeError(req, resp)
 111  	}
 112  
 113  	respMsg, err := unmarshalResponseMsg(req, resp)
 114  	if err != nil {
 115  		return err
 116  	}
 117  
 118  	if respMsg.Status != success {
 119  		return respMsg
 120  	}
 121  
 122  	if result == nil {
 123  		return nil
 124  	}
 125  
 126  	err = json.Unmarshal(respMsg.ResponseData, result)
 127  	if err != nil {
 128  		return errutils.NewUnmarshalError(req, resp.StatusCode, respMsg.ResponseData, err)
 129  	}
 130  
 131  	return nil
 132  }
 133  
 134  // GetDNSRecordIdx searches a given array of DNSRecords for a given DNSRecord
 135  // equivalence is determined by Destination and RecortType attributes
 136  // returns index of given DNSRecord in given array of DNSRecords.
 137  func GetDNSRecordIdx(records []DNSRecord, record DNSRecord) (int, error) {
 138  	for index, element := range records {
 139  		if record.Destination == element.Destination && record.RecordType == element.RecordType {
 140  			return index, nil
 141  		}
 142  	}
 143  
 144  	return -1, errors.New("no DNS Record found")
 145  }
 146  
 147  func newJSONRequest(ctx context.Context, method, endpoint string, payload any) (*http.Request, error) {
 148  	buf := new(bytes.Buffer)
 149  
 150  	if payload != nil {
 151  		err := json.NewEncoder(buf).Encode(payload)
 152  		if err != nil {
 153  			return nil, fmt.Errorf("failed to create request JSON body: %w", err)
 154  		}
 155  	}
 156  
 157  	req, err := http.NewRequestWithContext(ctx, method, endpoint, buf)
 158  	if err != nil {
 159  		return nil, fmt.Errorf("unable to create request: %w", err)
 160  	}
 161  
 162  	req.Header.Set("Accept", "application/json")
 163  
 164  	if payload != nil {
 165  		req.Header.Set("Content-Type", "application/json")
 166  	}
 167  
 168  	return req, nil
 169  }
 170  
 171  func unmarshalResponseMsg(req *http.Request, resp *http.Response) (*ResponseMsg, error) {
 172  	raw, err := io.ReadAll(resp.Body)
 173  	if err != nil {
 174  		return nil, errutils.NewReadResponseError(req, resp.StatusCode, err)
 175  	}
 176  
 177  	var respMsg ResponseMsg
 178  
 179  	err = json.Unmarshal(raw, &respMsg)
 180  	if err != nil {
 181  		return nil, errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
 182  	}
 183  
 184  	return &respMsg, nil
 185  }
 186