client.go raw

   1  package internal
   2  
   3  import (
   4  	"context"
   5  	"encoding/xml"
   6  	"errors"
   7  	"fmt"
   8  	"io"
   9  	"net/http"
  10  	"net/url"
  11  	"strconv"
  12  	"strings"
  13  	"time"
  14  
  15  	"github.com/go-acme/lego/v4/providers/dns/internal/errutils"
  16  )
  17  
  18  // Default API endpoints.
  19  const (
  20  	DefaultBaseURL = "https://api.namecheap.com/xml.response"
  21  	SandboxBaseURL = "https://api.sandbox.namecheap.com/xml.response"
  22  )
  23  
  24  // Client the API client for Namecheap.
  25  type Client struct {
  26  	apiUser  string
  27  	apiKey   string
  28  	clientIP string
  29  
  30  	BaseURL    string
  31  	HTTPClient *http.Client
  32  }
  33  
  34  // NewClient creates a new Client.
  35  func NewClient(apiUser, apiKey, clientIP string) *Client {
  36  	return &Client{
  37  		apiUser:    apiUser,
  38  		apiKey:     apiKey,
  39  		clientIP:   clientIP,
  40  		BaseURL:    DefaultBaseURL,
  41  		HTTPClient: &http.Client{Timeout: 5 * time.Second},
  42  	}
  43  }
  44  
  45  // GetHosts reads the full list of DNS host records.
  46  // https://www.namecheap.com/support/api/methods/domains-dns/get-hosts.aspx
  47  func (c *Client) GetHosts(ctx context.Context, sld, tld string) ([]Record, error) {
  48  	request, err := c.newRequestGet(ctx, "namecheap.domains.dns.getHosts",
  49  		addParam("SLD", sld),
  50  		addParam("TLD", tld),
  51  	)
  52  	if err != nil {
  53  		return nil, err
  54  	}
  55  
  56  	var ghr getHostsResponse
  57  
  58  	err = c.do(request, &ghr)
  59  	if err != nil {
  60  		return nil, err
  61  	}
  62  
  63  	if len(ghr.Errors) > 0 {
  64  		return nil, ghr.Errors[0]
  65  	}
  66  
  67  	return ghr.Hosts, nil
  68  }
  69  
  70  // SetHosts writes the full list of DNS host records .
  71  // https://www.namecheap.com/support/api/methods/domains-dns/set-hosts.aspx
  72  func (c *Client) SetHosts(ctx context.Context, sld, tld string, hosts []Record) error {
  73  	req, err := c.newRequestPost(ctx, "namecheap.domains.dns.setHosts",
  74  		addParam("SLD", sld),
  75  		addParam("TLD", tld),
  76  		func(values url.Values) {
  77  			for i, h := range hosts {
  78  				ind := strconv.Itoa(i + 1)
  79  				values.Add("HostName"+ind, h.Name)
  80  				values.Add("RecordType"+ind, h.Type)
  81  				values.Add("Address"+ind, h.Address)
  82  				values.Add("MXPref"+ind, h.MXPref)
  83  				values.Add("TTL"+ind, h.TTL)
  84  			}
  85  		},
  86  	)
  87  	if err != nil {
  88  		return err
  89  	}
  90  
  91  	var shr setHostsResponse
  92  
  93  	err = c.do(req, &shr)
  94  	if err != nil {
  95  		return err
  96  	}
  97  
  98  	if len(shr.Errors) > 0 {
  99  		return shr.Errors[0]
 100  	}
 101  
 102  	if shr.Result.IsSuccess != "true" {
 103  		return errors.New("setHosts failed")
 104  	}
 105  
 106  	return nil
 107  }
 108  
 109  func (c *Client) do(req *http.Request, result any) error {
 110  	resp, err := c.HTTPClient.Do(req)
 111  	if err != nil {
 112  		return errutils.NewHTTPDoError(req, err)
 113  	}
 114  
 115  	defer func() { _ = resp.Body.Close() }()
 116  
 117  	if resp.StatusCode >= http.StatusBadRequest {
 118  		return errutils.NewUnexpectedResponseStatusCodeError(req, resp)
 119  	}
 120  
 121  	raw, err := io.ReadAll(resp.Body)
 122  	if err != nil {
 123  		return errutils.NewReadResponseError(req, resp.StatusCode, err)
 124  	}
 125  
 126  	return xml.Unmarshal(raw, result)
 127  }
 128  
 129  func (c *Client) newRequestGet(ctx context.Context, cmd string, params ...func(url.Values)) (*http.Request, error) {
 130  	query := c.makeQuery(cmd, params...)
 131  
 132  	endpoint, err := url.Parse(c.BaseURL)
 133  	if err != nil {
 134  		return nil, err
 135  	}
 136  
 137  	endpoint.RawQuery = query.Encode()
 138  
 139  	req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil)
 140  	if err != nil {
 141  		return nil, fmt.Errorf("unable to create request: %w", err)
 142  	}
 143  
 144  	return req, nil
 145  }
 146  
 147  func (c *Client) newRequestPost(ctx context.Context, cmd string, params ...func(url.Values)) (*http.Request, error) {
 148  	query := c.makeQuery(cmd, params...)
 149  
 150  	req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.BaseURL, strings.NewReader(query.Encode()))
 151  	if err != nil {
 152  		return nil, fmt.Errorf("unable to create request: %w", err)
 153  	}
 154  
 155  	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
 156  
 157  	return req, nil
 158  }
 159  
 160  func (c *Client) makeQuery(cmd string, params ...func(url.Values)) url.Values {
 161  	queryParams := make(url.Values)
 162  	queryParams.Set("ApiUser", c.apiUser)
 163  	queryParams.Set("ApiKey", c.apiKey)
 164  	queryParams.Set("UserName", c.apiUser)
 165  	queryParams.Set("Command", cmd)
 166  	queryParams.Set("ClientIp", c.clientIP)
 167  
 168  	for _, param := range params {
 169  		param(queryParams)
 170  	}
 171  
 172  	return queryParams
 173  }
 174  
 175  func addParam(key, value string) func(url.Values) {
 176  	return func(values url.Values) {
 177  		values.Set(key, value)
 178  	}
 179  }
 180