2023-04-14 07:44:20 +00:00
|
|
|
package internal
|
|
|
|
|
|
|
|
import (
|
2023-05-05 07:49:38 +00:00
|
|
|
"context"
|
2023-04-14 07:44:20 +00:00
|
|
|
"crypto/hmac"
|
|
|
|
"crypto/sha256"
|
|
|
|
"encoding/hex"
|
|
|
|
"encoding/json"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"net/http"
|
|
|
|
"net/url"
|
|
|
|
"strings"
|
|
|
|
"time"
|
2023-05-05 07:49:38 +00:00
|
|
|
|
|
|
|
"github.com/go-acme/lego/v4/providers/dns/internal/errutils"
|
2023-04-14 07:44:20 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
const defaultBaseURL = "https://portal.brandit.com/api/v3/"
|
|
|
|
|
|
|
|
// Client a BrandIT DNS API client.
|
|
|
|
type Client struct {
|
|
|
|
apiUsername string
|
|
|
|
apiKey string
|
2023-05-05 07:49:38 +00:00
|
|
|
|
|
|
|
baseURL string
|
|
|
|
HTTPClient *http.Client
|
2023-04-14 07:44:20 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// NewClient creates a new Client.
|
|
|
|
func NewClient(apiUsername, apiKey string) (*Client, error) {
|
|
|
|
if apiKey == "" || apiUsername == "" {
|
|
|
|
return nil, errors.New("credentials missing")
|
|
|
|
}
|
|
|
|
|
|
|
|
return &Client{
|
|
|
|
apiUsername: apiUsername,
|
|
|
|
apiKey: apiKey,
|
2023-05-05 07:49:38 +00:00
|
|
|
baseURL: defaultBaseURL,
|
2023-04-14 07:44:20 +00:00
|
|
|
HTTPClient: &http.Client{Timeout: 10 * time.Second},
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// ListRecords lists all records.
|
|
|
|
// https://portal.brandit.com/apidocv3#listDNSRR
|
2023-05-05 07:49:38 +00:00
|
|
|
func (c *Client) ListRecords(ctx context.Context, account, dnsZone string) (*ListRecordsResponse, error) {
|
2023-04-14 07:44:20 +00:00
|
|
|
query := url.Values{}
|
|
|
|
query.Add("command", "listDNSRR")
|
|
|
|
query.Add("account", account)
|
|
|
|
query.Add("dnszone", dnsZone)
|
|
|
|
|
2023-05-05 07:49:38 +00:00
|
|
|
result := &Response[*ListRecordsResponse]{}
|
2023-04-14 07:44:20 +00:00
|
|
|
|
2023-05-05 07:49:38 +00:00
|
|
|
err := c.do(ctx, query, result)
|
2023-04-14 07:44:20 +00:00
|
|
|
if err != nil {
|
2023-05-05 07:49:38 +00:00
|
|
|
return nil, err
|
2023-04-14 07:44:20 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
for len(result.Response.RR) < result.Response.Total[0] {
|
|
|
|
query.Add("first", fmt.Sprint(result.Response.Last[0]+1))
|
|
|
|
|
2023-05-05 07:49:38 +00:00
|
|
|
tmp := &Response[*ListRecordsResponse]{}
|
|
|
|
err := c.do(ctx, query, tmp)
|
2023-04-14 07:44:20 +00:00
|
|
|
if err != nil {
|
2023-05-05 07:49:38 +00:00
|
|
|
return nil, err
|
2023-04-14 07:44:20 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
result.Response.RR = append(result.Response.RR, tmp.Response.RR...)
|
|
|
|
result.Response.Last = tmp.Response.Last
|
|
|
|
}
|
|
|
|
|
2023-05-05 07:49:38 +00:00
|
|
|
return result.Response, nil
|
2023-04-14 07:44:20 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// AddRecord adds a DNS record.
|
|
|
|
// https://portal.brandit.com/apidocv3#addDNSRR
|
2023-05-05 07:49:38 +00:00
|
|
|
func (c *Client) AddRecord(ctx context.Context, domainName, account, newRecordID string, record Record) (*AddRecord, error) {
|
|
|
|
value := strings.Join([]string{record.Name, fmt.Sprint(record.TTL), "IN", record.Type, record.Content}, " ")
|
2023-04-14 07:44:20 +00:00
|
|
|
|
|
|
|
query := url.Values{}
|
|
|
|
query.Add("command", "addDNSRR")
|
|
|
|
query.Add("account", account)
|
|
|
|
query.Add("dnszone", domainName)
|
2023-05-05 07:49:38 +00:00
|
|
|
query.Add("rrdata", value)
|
2023-04-14 07:44:20 +00:00
|
|
|
query.Add("key", newRecordID)
|
|
|
|
|
|
|
|
result := &AddRecord{}
|
|
|
|
|
2023-05-05 07:49:38 +00:00
|
|
|
err := c.do(ctx, query, result)
|
2023-04-14 07:44:20 +00:00
|
|
|
if err != nil {
|
2023-05-05 07:49:38 +00:00
|
|
|
return nil, err
|
2023-04-14 07:44:20 +00:00
|
|
|
}
|
2023-05-05 07:49:38 +00:00
|
|
|
|
|
|
|
result.Record = value
|
2023-04-14 07:44:20 +00:00
|
|
|
|
|
|
|
return result, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// DeleteRecord deletes a DNS record.
|
|
|
|
// https://portal.brandit.com/apidocv3#deleteDNSRR
|
2023-05-05 07:49:38 +00:00
|
|
|
func (c *Client) DeleteRecord(ctx context.Context, domainName, account, dnsRecord, recordID string) error {
|
2023-04-14 07:44:20 +00:00
|
|
|
query := url.Values{}
|
|
|
|
query.Add("command", "deleteDNSRR")
|
|
|
|
query.Add("account", account)
|
|
|
|
query.Add("dnszone", domainName)
|
|
|
|
query.Add("rrdata", dnsRecord)
|
|
|
|
query.Add("key", recordID)
|
|
|
|
|
2023-05-05 07:49:38 +00:00
|
|
|
return c.do(ctx, query, nil)
|
2023-04-14 07:44:20 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// StatusDomain returns the status of a domain and account associated with it.
|
|
|
|
// https://portal.brandit.com/apidocv3#statusDomain
|
2023-05-05 07:49:38 +00:00
|
|
|
func (c *Client) StatusDomain(ctx context.Context, domain string) (*StatusResponse, error) {
|
2023-04-14 07:44:20 +00:00
|
|
|
query := url.Values{}
|
|
|
|
|
|
|
|
query.Add("command", "statusDomain")
|
|
|
|
query.Add("domain", domain)
|
|
|
|
|
2023-05-05 07:49:38 +00:00
|
|
|
result := &Response[*StatusResponse]{}
|
2023-04-14 07:44:20 +00:00
|
|
|
|
2023-05-05 07:49:38 +00:00
|
|
|
err := c.do(ctx, query, result)
|
2023-04-14 07:44:20 +00:00
|
|
|
if err != nil {
|
2023-05-05 07:49:38 +00:00
|
|
|
return nil, err
|
2023-04-14 07:44:20 +00:00
|
|
|
}
|
|
|
|
|
2023-05-05 07:49:38 +00:00
|
|
|
return result.Response, nil
|
2023-04-14 07:44:20 +00:00
|
|
|
}
|
|
|
|
|
2023-05-05 07:49:38 +00:00
|
|
|
func (c *Client) do(ctx context.Context, query url.Values, result any) error {
|
|
|
|
values, err := sign(c.apiUsername, c.apiKey, query)
|
2023-04-14 07:44:20 +00:00
|
|
|
if err != nil {
|
2023-05-05 07:49:38 +00:00
|
|
|
return err
|
2023-04-14 07:44:20 +00:00
|
|
|
}
|
|
|
|
|
2023-05-05 07:49:38 +00:00
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL, strings.NewReader(values.Encode()))
|
2023-04-14 07:44:20 +00:00
|
|
|
if err != nil {
|
2023-05-05 07:49:38 +00:00
|
|
|
return fmt.Errorf("unable to create request: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
|
|
|
|
|
|
resp, err := c.HTTPClient.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
return errutils.NewHTTPDoError(req, err)
|
2023-04-14 07:44:20 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
|
|
|
|
raw, err := io.ReadAll(resp.Body)
|
|
|
|
if err != nil {
|
2023-05-05 07:49:38 +00:00
|
|
|
return errutils.NewReadResponseError(req, resp.StatusCode, err)
|
2023-04-14 07:44:20 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Unmarshal the error response, because the API returns a 200 OK even if there is an error.
|
|
|
|
var apiError APIError
|
|
|
|
err = json.Unmarshal(raw, &apiError)
|
|
|
|
if err != nil {
|
2023-05-05 07:49:38 +00:00
|
|
|
return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
|
2023-04-14 07:44:20 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if apiError.Code > 299 || apiError.Status != "success" {
|
|
|
|
return apiError
|
|
|
|
}
|
|
|
|
|
2023-05-05 07:49:38 +00:00
|
|
|
if result == nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-04-14 07:44:20 +00:00
|
|
|
err = json.Unmarshal(raw, result)
|
|
|
|
if err != nil {
|
2023-05-05 07:49:38 +00:00
|
|
|
return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
|
2023-04-14 07:44:20 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func sign(apiUsername, apiKey string, query url.Values) (url.Values, error) {
|
|
|
|
location, err := time.LoadLocation("GMT")
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("time location: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
timestamp := time.Now().In(location).Format("2006-01-02T15:04:05Z")
|
|
|
|
|
|
|
|
canonicalRequest := fmt.Sprintf("%s%s%s", apiUsername, timestamp, defaultBaseURL)
|
|
|
|
|
|
|
|
mac := hmac.New(sha256.New, []byte(apiKey))
|
|
|
|
_, err = mac.Write([]byte(canonicalRequest))
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
hashed := mac.Sum(nil)
|
|
|
|
signature := hex.EncodeToString(hashed)
|
|
|
|
|
|
|
|
query.Add("user", apiUsername)
|
|
|
|
query.Add("timestamp", timestamp)
|
|
|
|
query.Add("signature", signature)
|
|
|
|
|
|
|
|
return query, nil
|
|
|
|
}
|