lego/providers/dns/internal/hostingde/client.go
2023-09-20 01:40:26 +00:00

150 lines
3.7 KiB
Go

package hostingde
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"time"
"github.com/cenkalti/backoff/v4"
"github.com/go-acme/lego/v4/providers/dns/internal/errutils"
)
const (
DefaultHostingdeBaseURL = "https://secure.hosting.de/api/dns/v1/json"
DefaultHTTPNetBaseURL = "https://partner.http.net/api/dns/v1/json"
)
// Client the API client for Hosting.de.
type Client struct {
apiKey string
BaseURL *url.URL
HTTPClient *http.Client
}
// NewClient creates new Client.
func NewClient(apiKey string) *Client {
baseURL, _ := url.Parse(DefaultHostingdeBaseURL)
return &Client{
apiKey: apiKey,
BaseURL: baseURL,
HTTPClient: &http.Client{Timeout: 5 * time.Second},
}
}
// GetZone gets a zone.
func (c Client) GetZone(ctx context.Context, req ZoneConfigsFindRequest) (*ZoneConfig, error) {
var zoneConfig *ZoneConfig
operation := func() error {
response, err := c.ListZoneConfigs(ctx, req)
if err != nil {
return backoff.Permanent(err)
}
if response.Data[0].Status != "active" {
return fmt.Errorf("unexpected status: %q", response.Data[0].Status)
}
zoneConfig = &response.Data[0]
return nil
}
bo := backoff.NewExponentialBackOff()
bo.InitialInterval = 3 * time.Second
bo.MaxInterval = 10 * bo.InitialInterval
bo.MaxElapsedTime = 100 * bo.InitialInterval
// retry in case the zone was edited recently and is not yet active
err := backoff.Retry(operation, bo)
if err != nil {
return nil, err
}
return zoneConfig, nil
}
// ListZoneConfigs lists zone configuration.
// https://www.hosting.de/api/?json#list-zoneconfigs
func (c Client) ListZoneConfigs(ctx context.Context, req ZoneConfigsFindRequest) (*ZoneResponse, error) {
endpoint := c.BaseURL.JoinPath("zoneConfigsFind")
req.AuthToken = c.apiKey
response := &BaseResponse[*ZoneResponse]{}
rawResp, err := c.post(ctx, endpoint, req, response)
if err != nil {
return nil, err
}
if response.Status != "success" && response.Status != "pending" {
return nil, fmt.Errorf("unexpected status: %q, %s", response.Status, string(rawResp))
}
if response.Response == nil || len(response.Response.Data) == 0 {
return nil, fmt.Errorf("no data, status: %q, %s", response.Status, string(rawResp))
}
return response.Response, nil
}
// UpdateZone updates a zone.
// https://www.hosting.de/api/?json#updating-zones
func (c Client) UpdateZone(ctx context.Context, req ZoneUpdateRequest) (*Zone, error) {
endpoint := c.BaseURL.JoinPath("zoneUpdate")
req.AuthToken = c.apiKey
// but we'll need the ID later to delete the record
response := &BaseResponse[*Zone]{}
rawResp, err := c.post(ctx, endpoint, req, response)
if err != nil {
return nil, err
}
if response.Status != "success" && response.Status != "pending" {
return nil, fmt.Errorf("unexpected status: %q, %s", response.Status, string(rawResp))
}
return response.Response, nil
}
func (c Client) post(ctx context.Context, endpoint *url.URL, request, result any) ([]byte, error) {
body, err := json.Marshal(request)
if err != nil {
return nil, fmt.Errorf("failed to create request JSON body: %w", err)
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint.String(), bytes.NewReader(body))
if err != nil {
return nil, fmt.Errorf("unable to create request: %w", err)
}
resp, err := c.HTTPClient.Do(req)
if err != nil {
return nil, errutils.NewHTTPDoError(req, err)
}
defer func() { _ = resp.Body.Close() }()
raw, err := io.ReadAll(resp.Body)
if err != nil {
return nil, errutils.NewReadResponseError(req, resp.StatusCode, err)
}
err = json.Unmarshal(raw, result)
if err != nil {
return nil, errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
}
return raw, nil
}