lego/providers/dns/clouddns/internal/client.go

242 lines
5.4 KiB
Go
Raw Normal View History

2020-04-24 01:13:25 +00:00
package internal
import (
"bytes"
2023-05-05 07:49:38 +00:00
"context"
2020-04-24 01:13:25 +00:00
"encoding/json"
"fmt"
"io"
"net/http"
2023-05-05 07:49:38 +00:00
"net/url"
"time"
2020-04-24 01:13:25 +00:00
2023-05-05 07:49:38 +00:00
"github.com/go-acme/lego/v4/providers/dns/internal/errutils"
2020-04-24 01:13:25 +00:00
)
2023-05-05 07:49:38 +00:00
const apiBaseURL = "https://admin.vshosting.cloud/clouddns"
const authorizationHeader = "Authorization"
2020-04-24 01:13:25 +00:00
// Client handles all communication with CloudDNS API.
type Client struct {
2023-05-05 07:49:38 +00:00
clientID string
email string
password string
ttl int
apiBaseURL *url.URL
loginURL *url.URL
HTTPClient *http.Client
2020-04-24 01:13:25 +00:00
}
// NewClient returns a Client instance configured to handle CloudDNS API communication.
2020-07-09 23:48:18 +00:00
func NewClient(clientID, email, password string, ttl int) *Client {
2023-05-05 07:49:38 +00:00
baseURL, _ := url.Parse(apiBaseURL)
loginBaseURL, _ := url.Parse(loginURL)
2020-04-24 01:13:25 +00:00
return &Client{
2023-05-05 07:49:38 +00:00
clientID: clientID,
email: email,
password: password,
ttl: ttl,
apiBaseURL: baseURL,
loginURL: loginBaseURL,
HTTPClient: &http.Client{Timeout: 5 * time.Second},
2020-04-24 01:13:25 +00:00
}
}
// AddRecord is a high level method to add a new record into CloudDNS zone.
2023-05-05 07:49:38 +00:00
func (c *Client) AddRecord(ctx context.Context, zone, recordName, recordValue string) error {
domain, err := c.getDomain(ctx, zone)
2020-04-24 01:13:25 +00:00
if err != nil {
return err
}
record := Record{DomainID: domain.ID, Name: recordName, Value: recordValue, Type: "TXT"}
2023-05-05 07:49:38 +00:00
err = c.addTxtRecord(ctx, record)
2020-04-24 01:13:25 +00:00
if err != nil {
return err
}
2023-05-05 07:49:38 +00:00
return c.publishRecords(ctx, domain.ID)
2020-04-24 01:13:25 +00:00
}
// DeleteRecord is a high level method to remove a record from zone.
2023-05-05 07:49:38 +00:00
func (c *Client) DeleteRecord(ctx context.Context, zone, recordName string) error {
domain, err := c.getDomain(ctx, zone)
2020-04-24 01:13:25 +00:00
if err != nil {
return err
}
2023-05-05 07:49:38 +00:00
record, err := c.getRecord(ctx, domain.ID, recordName)
2020-04-24 01:13:25 +00:00
if err != nil {
return err
}
2023-05-05 07:49:38 +00:00
err = c.deleteRecord(ctx, record)
2020-04-24 01:13:25 +00:00
if err != nil {
return err
}
2023-05-05 07:49:38 +00:00
return c.publishRecords(ctx, domain.ID)
2020-04-24 01:13:25 +00:00
}
2023-05-05 07:49:38 +00:00
func (c *Client) addTxtRecord(ctx context.Context, record Record) error {
endpoint := c.apiBaseURL.JoinPath("record-txt")
req, err := newJSONRequest(ctx, http.MethodPost, endpoint, record)
2020-04-24 01:13:25 +00:00
if err != nil {
return err
}
2023-05-05 07:49:38 +00:00
return c.do(req, nil)
2020-04-24 01:13:25 +00:00
}
2023-05-05 07:49:38 +00:00
func (c *Client) deleteRecord(ctx context.Context, record Record) error {
endpoint := c.apiBaseURL.JoinPath("record", record.ID)
req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)
if err != nil {
return err
}
return c.do(req, nil)
2020-04-24 01:13:25 +00:00
}
2023-05-05 07:49:38 +00:00
func (c *Client) getDomain(ctx context.Context, zone string) (Domain, error) {
2020-04-24 01:13:25 +00:00
searchQuery := SearchQuery{
Search: []Search{
2023-05-05 07:49:38 +00:00
{Name: "clientId", Operator: "eq", Value: c.clientID},
2020-04-24 01:13:25 +00:00
{Name: "domainName", Operator: "eq", Value: zone},
},
}
2023-05-05 07:49:38 +00:00
endpoint := c.apiBaseURL.JoinPath("domain", "search")
2020-04-24 01:13:25 +00:00
2023-05-05 07:49:38 +00:00
req, err := newJSONRequest(ctx, http.MethodPost, endpoint, searchQuery)
2020-04-24 01:13:25 +00:00
if err != nil {
return Domain{}, err
}
var result SearchResponse
2023-05-05 07:49:38 +00:00
err = c.do(req, &result)
2020-04-24 01:13:25 +00:00
if err != nil {
return Domain{}, err
}
if len(result.Items) == 0 {
return Domain{}, fmt.Errorf("domain not found: %s", zone)
}
return result.Items[0], nil
}
2023-05-05 07:49:38 +00:00
func (c *Client) getRecord(ctx context.Context, domainID, recordName string) (Record, error) {
endpoint := c.apiBaseURL.JoinPath("domain", domainID)
req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)
2020-04-24 01:13:25 +00:00
if err != nil {
return Record{}, err
}
var result DomainInfo
2023-05-05 07:49:38 +00:00
err = c.do(req, &result)
2020-04-24 01:13:25 +00:00
if err != nil {
return Record{}, err
}
for _, record := range result.LastDomainRecordList {
if record.Name == recordName && record.Type == "TXT" {
return record, nil
}
}
return Record{}, fmt.Errorf("record not found: domainID %s, name %s", domainID, recordName)
}
2023-05-05 07:49:38 +00:00
func (c *Client) publishRecords(ctx context.Context, domainID string) error {
endpoint := c.apiBaseURL.JoinPath("domain", domainID, "publish")
payload := DomainInfo{SoaTTL: c.ttl}
req, err := newJSONRequest(ctx, http.MethodPut, endpoint, payload)
2020-04-24 01:13:25 +00:00
if err != nil {
return err
}
2023-05-05 07:49:38 +00:00
return c.do(req, nil)
2020-04-24 01:13:25 +00:00
}
2023-05-05 07:49:38 +00:00
func (c *Client) do(req *http.Request, result any) error {
at := getAccessToken(req.Context())
if at != "" {
req.Header.Set(authorizationHeader, "Bearer "+at)
}
2020-04-24 01:13:25 +00:00
2023-05-05 07:49:38 +00:00
resp, err := c.HTTPClient.Do(req)
2020-04-24 01:13:25 +00:00
if err != nil {
2023-05-05 07:49:38 +00:00
return errutils.NewHTTPDoError(req, err)
2020-04-24 01:13:25 +00:00
}
2023-05-05 07:49:38 +00:00
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode/100 != 2 {
return parseError(req, resp)
2020-04-24 01:13:25 +00:00
}
2023-05-05 07:49:38 +00:00
if result == nil {
return nil
}
2020-04-24 01:13:25 +00:00
2023-05-05 07:49:38 +00:00
raw, err := io.ReadAll(resp.Body)
2020-04-24 01:13:25 +00:00
if err != nil {
2023-05-05 07:49:38 +00:00
return errutils.NewReadResponseError(req, resp.StatusCode, err)
2020-04-24 01:13:25 +00:00
}
2023-05-05 07:49:38 +00:00
err = json.Unmarshal(raw, result)
2020-04-24 01:13:25 +00:00
if err != nil {
2023-05-05 07:49:38 +00:00
return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
2020-04-24 01:13:25 +00:00
}
return nil
}
2023-05-05 07:49:38 +00:00
func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {
buf := new(bytes.Buffer)
if payload != nil {
err := json.NewEncoder(buf).Encode(payload)
2020-04-24 01:13:25 +00:00
if err != nil {
2023-05-05 07:49:38 +00:00
return nil, fmt.Errorf("failed to create request JSON body: %w", err)
2020-04-24 01:13:25 +00:00
}
}
2023-05-05 07:49:38 +00:00
req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)
2020-04-24 01:13:25 +00:00
if err != nil {
2023-05-05 07:49:38 +00:00
return nil, fmt.Errorf("unable to create request: %w", err)
2020-04-24 01:13:25 +00:00
}
2023-05-05 07:49:38 +00:00
req.Header.Set("Accept", "application/json")
2020-04-24 01:13:25 +00:00
2023-05-05 07:49:38 +00:00
if payload != nil {
req.Header.Set("Content-Type", "application/json")
2020-04-24 01:13:25 +00:00
}
return req, nil
}
2023-05-05 07:49:38 +00:00
func parseError(req *http.Request, resp *http.Response) error {
raw, _ := io.ReadAll(resp.Body)
2020-04-24 01:13:25 +00:00
2023-05-05 07:49:38 +00:00
var response APIError
err := json.Unmarshal(raw, &response)
2020-04-24 01:13:25 +00:00
if err != nil {
2023-05-05 07:49:38 +00:00
return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)
2020-04-24 01:13:25 +00:00
}
2023-05-05 07:49:38 +00:00
return fmt.Errorf("[status code %d] %w", resp.StatusCode, response.Error)
2020-04-24 01:13:25 +00:00
}