client.go raw
1 package internal
2
3 import (
4 "bytes"
5 "context"
6 "crypto/hmac"
7 "crypto/sha1"
8 "encoding/base64"
9 "encoding/xml"
10 "errors"
11 "fmt"
12 "io"
13 "net/http"
14 "net/url"
15 "time"
16
17 "github.com/go-acme/lego/v4/providers/dns/internal/errutils"
18 )
19
20 const (
21 defaultBaseURL = "https://dns.api.nifcloud.com"
22 apiVersion = "2012-12-12N2013-12-16"
23 // XMLNs XML NS of Route53.
24 XMLNs = "https://route53.amazonaws.com/doc/2012-12-12/"
25 )
26
27 // Client the API client for NIFCLOUD DNS.
28 type Client struct {
29 accessKey string
30 secretKey string
31
32 BaseURL *url.URL
33 HTTPClient *http.Client
34 }
35
36 // NewClient Creates a new client of NIFCLOUD DNS.
37 func NewClient(accessKey, secretKey string) (*Client, error) {
38 if accessKey == "" || secretKey == "" {
39 return nil, errors.New("credentials missing")
40 }
41
42 baseURL, _ := url.Parse(defaultBaseURL)
43
44 return &Client{
45 accessKey: accessKey,
46 secretKey: secretKey,
47 BaseURL: baseURL,
48 HTTPClient: &http.Client{Timeout: 10 * time.Second},
49 }, nil
50 }
51
52 // ChangeResourceRecordSets Call ChangeResourceRecordSets API and return response.
53 func (c *Client) ChangeResourceRecordSets(ctx context.Context, hostedZoneID string, input ChangeResourceRecordSetsRequest) (*ChangeResourceRecordSetsResponse, error) {
54 endpoint := c.BaseURL.JoinPath(apiVersion, "hostedzone", hostedZoneID, "rrset")
55
56 req, err := newXMLRequest(ctx, http.MethodPost, endpoint, input)
57 if err != nil {
58 return nil, err
59 }
60
61 output := &ChangeResourceRecordSetsResponse{}
62
63 err = c.do(req, output)
64 if err != nil {
65 return nil, err
66 }
67
68 return output, nil
69 }
70
71 // GetChange Call GetChange API and return response.
72 func (c *Client) GetChange(ctx context.Context, statusID string) (*GetChangeResponse, error) {
73 endpoint := c.BaseURL.JoinPath(apiVersion, "change", statusID)
74
75 req, err := newXMLRequest(ctx, http.MethodGet, endpoint, nil)
76 if err != nil {
77 return nil, err
78 }
79
80 output := &GetChangeResponse{}
81
82 err = c.do(req, output)
83 if err != nil {
84 return nil, err
85 }
86
87 return output, nil
88 }
89
90 func (c *Client) do(req *http.Request, result any) error {
91 err := c.sign(req)
92 if err != nil {
93 return fmt.Errorf("an error occurred during the creation of the signature: %w", err)
94 }
95
96 resp, err := c.HTTPClient.Do(req)
97 if err != nil {
98 return errutils.NewHTTPDoError(req, err)
99 }
100
101 defer func() { _ = resp.Body.Close() }()
102
103 if resp.StatusCode != http.StatusOK {
104 return parseError(req, resp)
105 }
106
107 if result == nil {
108 return nil
109 }
110
111 raw, err := io.ReadAll(resp.Body)
112 if err != nil {
113 return errutils.NewReadResponseError(req, resp.StatusCode, err)
114 }
115
116 err = xml.Unmarshal(raw, result)
117 if err != nil {
118 return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
119 }
120
121 return nil
122 }
123
124 func (c *Client) sign(req *http.Request) error {
125 if req.Header.Get("Date") == "" {
126 req.Header.Set("Date", time.Now().UTC().Format(http.TimeFormat))
127 }
128
129 if req.URL.Path == "" {
130 req.URL.Path += "/"
131 }
132
133 mac := hmac.New(sha1.New, []byte(c.secretKey))
134
135 _, err := mac.Write([]byte(req.Header.Get("Date")))
136 if err != nil {
137 return err
138 }
139
140 hashed := mac.Sum(nil)
141 signature := base64.StdEncoding.EncodeToString(hashed)
142
143 auth := fmt.Sprintf("NIFTY3-HTTPS NiftyAccessKeyId=%s,Algorithm=HmacSHA1,Signature=%s", c.accessKey, signature)
144 req.Header.Set("X-Nifty-Authorization", auth)
145
146 return nil
147 }
148
149 func newXMLRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {
150 body := new(bytes.Buffer)
151
152 if payload != nil {
153 body.WriteString(xml.Header)
154
155 err := xml.NewEncoder(body).Encode(payload)
156 if err != nil {
157 return nil, fmt.Errorf("failed to create request XML body: %w", err)
158 }
159 }
160
161 req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), body)
162 if err != nil {
163 return nil, fmt.Errorf("unable to create request: %w", err)
164 }
165
166 if payload != nil {
167 req.Header.Set("Content-Type", "text/xml; charset=utf-8")
168 }
169
170 return req, nil
171 }
172
173 func parseError(req *http.Request, resp *http.Response) error {
174 raw, _ := io.ReadAll(resp.Body)
175
176 errResp := &ErrorResponse{}
177
178 err := xml.Unmarshal(raw, errResp)
179 if err != nil {
180 return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)
181 }
182
183 return errResp.Error
184 }
185