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