lego/providers/dns/nifcloud/internal/client.go
2023-05-05 09:49:38 +02:00

185 lines
4.2 KiB
Go

package internal
import (
"bytes"
"context"
"crypto/hmac"
"crypto/sha1"
"encoding/base64"
"encoding/xml"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"time"
"github.com/go-acme/lego/v4/providers/dns/internal/errutils"
)
const (
defaultBaseURL = "https://dns.api.nifcloud.com"
apiVersion = "2012-12-12N2013-12-16"
// XMLNs XML NS of Route53.
XMLNs = "https://route53.amazonaws.com/doc/2012-12-12/"
)
// Client the API client for NIFCLOUD DNS.
type Client struct {
accessKey string
secretKey string
BaseURL *url.URL
HTTPClient *http.Client
}
// NewClient Creates a new client of NIFCLOUD DNS.
func NewClient(accessKey, secretKey string) (*Client, error) {
if accessKey == "" || secretKey == "" {
return nil, errors.New("credentials missing")
}
baseURL, _ := url.Parse(defaultBaseURL)
return &Client{
accessKey: accessKey,
secretKey: secretKey,
BaseURL: baseURL,
HTTPClient: &http.Client{Timeout: 10 * time.Second},
}, nil
}
// ChangeResourceRecordSets Call ChangeResourceRecordSets API and return response.
func (c *Client) ChangeResourceRecordSets(ctx context.Context, hostedZoneID string, input ChangeResourceRecordSetsRequest) (*ChangeResourceRecordSetsResponse, error) {
endpoint := c.BaseURL.JoinPath(apiVersion, "hostedzone", hostedZoneID, "rrset")
req, err := newXMLRequest(ctx, http.MethodPost, endpoint, input)
if err != nil {
return nil, err
}
output := &ChangeResourceRecordSetsResponse{}
err = c.do(req, output)
if err != nil {
return nil, err
}
return output, nil
}
// 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)
req, err := newXMLRequest(ctx, http.MethodGet, endpoint, nil)
if err != nil {
return nil, err
}
output := &GetChangeResponse{}
err = c.do(req, output)
if err != nil {
return nil, err
}
return output, nil
}
func (c *Client) do(req *http.Request, result any) error {
err := c.sign(req)
if err != nil {
return fmt.Errorf("an error occurred during the creation of the signature: %w", err)
}
resp, err := c.HTTPClient.Do(req)
if err != nil {
return errutils.NewHTTPDoError(req, err)
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusOK {
return parseError(req, resp)
}
if result == nil {
return nil
}
raw, err := io.ReadAll(resp.Body)
if err != nil {
return errutils.NewReadResponseError(req, resp.StatusCode, err)
}
err = xml.Unmarshal(raw, result)
if err != nil {
return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
}
return nil
}
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
}
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
}