241 lines
5.4 KiB
Go
241 lines
5.4 KiB
Go
package internal
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"time"
|
|
|
|
"github.com/go-acme/lego/v4/providers/dns/internal/errutils"
|
|
)
|
|
|
|
const apiBaseURL = "https://admin.vshosting.cloud/clouddns"
|
|
|
|
const authorizationHeader = "Authorization"
|
|
|
|
// Client handles all communication with CloudDNS API.
|
|
type Client struct {
|
|
clientID string
|
|
email string
|
|
password string
|
|
ttl int
|
|
|
|
apiBaseURL *url.URL
|
|
|
|
loginURL *url.URL
|
|
|
|
HTTPClient *http.Client
|
|
}
|
|
|
|
// NewClient returns a Client instance configured to handle CloudDNS API communication.
|
|
func NewClient(clientID, email, password string, ttl int) *Client {
|
|
baseURL, _ := url.Parse(apiBaseURL)
|
|
loginBaseURL, _ := url.Parse(loginURL)
|
|
|
|
return &Client{
|
|
clientID: clientID,
|
|
email: email,
|
|
password: password,
|
|
ttl: ttl,
|
|
apiBaseURL: baseURL,
|
|
loginURL: loginBaseURL,
|
|
HTTPClient: &http.Client{Timeout: 5 * time.Second},
|
|
}
|
|
}
|
|
|
|
// AddRecord is a high level method to add a new record into CloudDNS zone.
|
|
func (c *Client) AddRecord(ctx context.Context, zone, recordName, recordValue string) error {
|
|
domain, err := c.getDomain(ctx, zone)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
record := Record{DomainID: domain.ID, Name: recordName, Value: recordValue, Type: "TXT"}
|
|
|
|
err = c.addTxtRecord(ctx, record)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return c.publishRecords(ctx, domain.ID)
|
|
}
|
|
|
|
// DeleteRecord is a high level method to remove a record from zone.
|
|
func (c *Client) DeleteRecord(ctx context.Context, zone, recordName string) error {
|
|
domain, err := c.getDomain(ctx, zone)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
record, err := c.getRecord(ctx, domain.ID, recordName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = c.deleteRecord(ctx, record)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return c.publishRecords(ctx, domain.ID)
|
|
}
|
|
|
|
func (c *Client) addTxtRecord(ctx context.Context, record Record) error {
|
|
endpoint := c.apiBaseURL.JoinPath("record-txt")
|
|
|
|
req, err := newJSONRequest(ctx, http.MethodPost, endpoint, record)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return c.do(req, nil)
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
func (c *Client) getDomain(ctx context.Context, zone string) (Domain, error) {
|
|
searchQuery := SearchQuery{
|
|
Search: []Search{
|
|
{Name: "clientId", Operator: "eq", Value: c.clientID},
|
|
{Name: "domainName", Operator: "eq", Value: zone},
|
|
},
|
|
}
|
|
|
|
endpoint := c.apiBaseURL.JoinPath("domain", "search")
|
|
|
|
req, err := newJSONRequest(ctx, http.MethodPost, endpoint, searchQuery)
|
|
if err != nil {
|
|
return Domain{}, err
|
|
}
|
|
|
|
var result SearchResponse
|
|
err = c.do(req, &result)
|
|
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
|
|
}
|
|
|
|
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)
|
|
if err != nil {
|
|
return Record{}, err
|
|
}
|
|
|
|
var result DomainInfo
|
|
err = c.do(req, &result)
|
|
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)
|
|
}
|
|
|
|
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)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return c.do(req, nil)
|
|
}
|
|
|
|
func (c *Client) do(req *http.Request, result any) error {
|
|
at := getAccessToken(req.Context())
|
|
if at != "" {
|
|
req.Header.Set(authorizationHeader, "Bearer "+at)
|
|
}
|
|
|
|
resp, err := c.HTTPClient.Do(req)
|
|
if err != nil {
|
|
return errutils.NewHTTPDoError(req, err)
|
|
}
|
|
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
if resp.StatusCode/100 != 2 {
|
|
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 = json.Unmarshal(raw, result)
|
|
if err != nil {
|
|
return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
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)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create request JSON 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)
|
|
}
|
|
|
|
req.Header.Set("Accept", "application/json")
|
|
|
|
if payload != nil {
|
|
req.Header.Set("Content-Type", "application/json")
|
|
}
|
|
|
|
return req, nil
|
|
}
|
|
|
|
func parseError(req *http.Request, resp *http.Response) error {
|
|
raw, _ := io.ReadAll(resp.Body)
|
|
|
|
var response APIError
|
|
err := json.Unmarshal(raw, &response)
|
|
if err != nil {
|
|
return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)
|
|
}
|
|
|
|
return fmt.Errorf("[status code %d] %w", resp.StatusCode, response.Error)
|
|
}
|