2019-03-11 16:56:48 +00:00
|
|
|
package internal
|
2018-07-01 00:13:22 +00:00
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
2023-05-05 07:49:38 +00:00
|
|
|
"context"
|
2018-07-01 00:13:22 +00:00
|
|
|
"crypto/hmac"
|
|
|
|
"crypto/sha1"
|
|
|
|
"encoding/base64"
|
|
|
|
"encoding/xml"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
2023-05-05 07:49:38 +00:00
|
|
|
"io"
|
2018-07-01 00:13:22 +00:00
|
|
|
"net/http"
|
2023-05-05 07:49:38 +00:00
|
|
|
"net/url"
|
2018-07-01 00:13:22 +00:00
|
|
|
"time"
|
2023-05-05 07:49:38 +00:00
|
|
|
|
|
|
|
"github.com/go-acme/lego/v4/providers/dns/internal/errutils"
|
2018-07-01 00:13:22 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
2019-01-22 07:34:45 +00:00
|
|
|
defaultBaseURL = "https://dns.api.nifcloud.com"
|
2018-09-15 17:07:24 +00:00
|
|
|
apiVersion = "2012-12-12N2013-12-16"
|
2020-08-09 14:39:44 +00:00
|
|
|
// XMLNs XML NS of Route53.
|
2018-12-06 21:50:17 +00:00
|
|
|
XMLNs = "https://route53.amazonaws.com/doc/2012-12-12/"
|
2018-07-01 00:13:22 +00:00
|
|
|
)
|
|
|
|
|
2023-05-05 07:49:38 +00:00
|
|
|
// Client the API client for NIFCLOUD DNS.
|
|
|
|
type Client struct {
|
|
|
|
accessKey string
|
|
|
|
secretKey string
|
2018-07-01 00:13:22 +00:00
|
|
|
|
2023-05-05 07:49:38 +00:00
|
|
|
BaseURL *url.URL
|
|
|
|
HTTPClient *http.Client
|
2018-07-01 00:13:22 +00:00
|
|
|
}
|
|
|
|
|
2020-05-08 17:35:25 +00:00
|
|
|
// NewClient Creates a new client of NIFCLOUD DNS.
|
2020-07-09 23:48:18 +00:00
|
|
|
func NewClient(accessKey, secretKey string) (*Client, error) {
|
2021-03-04 19:16:59 +00:00
|
|
|
if accessKey == "" || secretKey == "" {
|
2018-10-12 17:29:18 +00:00
|
|
|
return nil, errors.New("credentials missing")
|
|
|
|
}
|
|
|
|
|
2023-05-05 07:49:38 +00:00
|
|
|
baseURL, _ := url.Parse(defaultBaseURL)
|
|
|
|
|
2018-07-01 00:13:22 +00:00
|
|
|
return &Client{
|
2018-09-15 17:07:24 +00:00
|
|
|
accessKey: accessKey,
|
|
|
|
secretKey: secretKey,
|
2023-05-05 07:49:38 +00:00
|
|
|
BaseURL: baseURL,
|
|
|
|
HTTPClient: &http.Client{Timeout: 10 * time.Second},
|
2018-10-12 17:29:18 +00:00
|
|
|
}, nil
|
2018-07-01 00:13:22 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// ChangeResourceRecordSets Call ChangeResourceRecordSets API and return response.
|
2023-05-05 07:49:38 +00:00
|
|
|
func (c *Client) ChangeResourceRecordSets(ctx context.Context, hostedZoneID string, input ChangeResourceRecordSetsRequest) (*ChangeResourceRecordSetsResponse, error) {
|
|
|
|
endpoint := c.BaseURL.JoinPath(apiVersion, "hostedzone", hostedZoneID, "rrset")
|
2018-07-01 00:13:22 +00:00
|
|
|
|
2023-05-05 07:49:38 +00:00
|
|
|
req, err := newXMLRequest(ctx, http.MethodPost, endpoint, input)
|
2018-07-01 00:13:22 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2023-05-05 07:49:38 +00:00
|
|
|
output := &ChangeResourceRecordSetsResponse{}
|
|
|
|
err = c.do(req, output)
|
2018-07-01 00:13:22 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2023-05-05 07:49:38 +00:00
|
|
|
return output, nil
|
|
|
|
}
|
2018-07-01 00:13:22 +00:00
|
|
|
|
2023-05-05 07:49:38 +00:00
|
|
|
// GetChange Call GetChange API and return response.
|
|
|
|
func (c *Client) GetChange(ctx context.Context, statusID string) (*GetChangeResponse, error) {
|
|
|
|
endpoint := c.BaseURL.JoinPath(apiVersion, "change", statusID)
|
2018-07-01 00:13:22 +00:00
|
|
|
|
2023-05-05 07:49:38 +00:00
|
|
|
req, err := newXMLRequest(ctx, http.MethodGet, endpoint, nil)
|
2018-07-01 00:13:22 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2023-05-05 07:49:38 +00:00
|
|
|
output := &GetChangeResponse{}
|
|
|
|
err = c.do(req, output)
|
2018-07-01 00:13:22 +00:00
|
|
|
if err != nil {
|
2023-05-05 07:49:38 +00:00
|
|
|
return nil, err
|
2018-07-01 00:13:22 +00:00
|
|
|
}
|
|
|
|
|
2023-05-05 07:49:38 +00:00
|
|
|
return output, nil
|
2018-07-01 00:13:22 +00:00
|
|
|
}
|
|
|
|
|
2023-05-05 07:49:38 +00:00
|
|
|
func (c *Client) do(req *http.Request, result any) error {
|
|
|
|
err := c.sign(req)
|
2018-07-01 00:13:22 +00:00
|
|
|
if err != nil {
|
2023-05-05 07:49:38 +00:00
|
|
|
return fmt.Errorf("an error occurred during the creation of the signature: %w", err)
|
2018-07-01 00:13:22 +00:00
|
|
|
}
|
|
|
|
|
2023-05-05 07:49:38 +00:00
|
|
|
resp, err := c.HTTPClient.Do(req)
|
2018-07-01 00:13:22 +00:00
|
|
|
if err != nil {
|
2023-05-05 07:49:38 +00:00
|
|
|
return errutils.NewHTTPDoError(req, err)
|
2018-07-01 00:13:22 +00:00
|
|
|
}
|
|
|
|
|
2023-05-05 07:49:38 +00:00
|
|
|
defer func() { _ = resp.Body.Close() }()
|
2018-07-01 00:13:22 +00:00
|
|
|
|
2023-05-05 07:49:38 +00:00
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
|
|
return parseError(req, resp)
|
|
|
|
}
|
2018-07-01 00:13:22 +00:00
|
|
|
|
2023-05-05 07:49:38 +00:00
|
|
|
if result == nil {
|
|
|
|
return nil
|
|
|
|
}
|
2018-07-01 00:13:22 +00:00
|
|
|
|
2023-05-05 07:49:38 +00:00
|
|
|
raw, err := io.ReadAll(resp.Body)
|
|
|
|
if err != nil {
|
|
|
|
return errutils.NewReadResponseError(req, resp.StatusCode, err)
|
2018-07-01 00:13:22 +00:00
|
|
|
}
|
|
|
|
|
2023-05-05 07:49:38 +00:00
|
|
|
err = xml.Unmarshal(raw, result)
|
2018-07-01 00:13:22 +00:00
|
|
|
if err != nil {
|
2023-05-05 07:49:38 +00:00
|
|
|
return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
|
2018-07-01 00:13:22 +00:00
|
|
|
}
|
|
|
|
|
2023-05-05 07:49:38 +00:00
|
|
|
return nil
|
2018-07-01 00:13:22 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Client) sign(req *http.Request) error {
|
|
|
|
if req.Header.Get("Date") == "" {
|
|
|
|
location, err := time.LoadLocation("GMT")
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
req.Header.Set("Date", time.Now().In(location).Format(time.RFC1123))
|
|
|
|
}
|
|
|
|
|
|
|
|
if req.URL.Path == "" {
|
|
|
|
req.URL.Path += "/"
|
|
|
|
}
|
|
|
|
|
|
|
|
mac := hmac.New(sha1.New, []byte(c.secretKey))
|
|
|
|
_, err := mac.Write([]byte(req.Header.Get("Date")))
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
hashed := mac.Sum(nil)
|
|
|
|
signature := base64.StdEncoding.EncodeToString(hashed)
|
|
|
|
|
|
|
|
auth := fmt.Sprintf("NIFTY3-HTTPS NiftyAccessKeyId=%s,Algorithm=HmacSHA1,Signature=%s", c.accessKey, signature)
|
|
|
|
req.Header.Set("X-Nifty-Authorization", auth)
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
2023-05-05 07:49:38 +00:00
|
|
|
|
|
|
|
func newXMLRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {
|
|
|
|
buf := new(bytes.Buffer)
|
|
|
|
|
|
|
|
if payload != nil {
|
|
|
|
body := new(bytes.Buffer)
|
|
|
|
body.WriteString(xml.Header)
|
|
|
|
err := xml.NewEncoder(body).Encode(payload)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("failed to create request XML body: %w", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("unable to create request: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if payload != nil {
|
|
|
|
req.Header.Set("Content-Type", "text/xml; charset=utf-8")
|
|
|
|
}
|
|
|
|
|
|
|
|
return req, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func parseError(req *http.Request, resp *http.Response) error {
|
|
|
|
raw, _ := io.ReadAll(resp.Body)
|
|
|
|
|
|
|
|
errResp := &ErrorResponse{}
|
|
|
|
err := xml.Unmarshal(raw, errResp)
|
|
|
|
if err != nil {
|
|
|
|
return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)
|
|
|
|
}
|
|
|
|
|
|
|
|
return errResp.Error
|
|
|
|
}
|