1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152
|
package desec
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"path"
"github.com/hashicorp/go-retryablehttp"
)
const defaultBaseURL = "https://desec.io/api/v1/"
type httpDoer interface {
Do(req *http.Request) (*http.Response, error)
}
type service struct {
client *Client
}
// ClientOptions the options of the Client.
type ClientOptions struct {
// HTTPClient HTTP client used to communicate with the API.
HTTPClient *http.Client
// Maximum number of retries
RetryMax int
// Customer logger instance. Can be either Logger or LeveledLogger
Logger interface{}
}
// NewDefaultClientOptions creates a new ClientOptions with default values.
func NewDefaultClientOptions() ClientOptions {
return ClientOptions{
HTTPClient: http.DefaultClient,
RetryMax: 5,
Logger: nil,
}
}
// Client deSEC API client.
type Client struct {
// Base URL for API requests.
BaseURL string
httpClient httpDoer
token string
common service // Reuse a single struct instead of allocating one for each service on the heap.
// Services used for talking to different parts of the deSEC API.
Account *AccountService
Tokens *TokensService
Records *RecordsService
Domains *DomainsService
}
// New creates a new Client.
func New(token string, opts ClientOptions) *Client {
// https://github.com/desec-io/desec-stack/blob/main/docs/rate-limits.rst
retryClient := retryablehttp.NewClient()
retryClient.RetryMax = opts.RetryMax
retryClient.HTTPClient = opts.HTTPClient
retryClient.Logger = opts.Logger
client := &Client{
httpClient: retryClient.StandardClient(),
BaseURL: defaultBaseURL,
token: token,
}
client.common.client = client
client.Account = (*AccountService)(&client.common)
client.Tokens = (*TokensService)(&client.common)
client.Records = (*RecordsService)(&client.common)
client.Domains = (*DomainsService)(&client.common)
return client
}
func (c *Client) newRequest(ctx context.Context, method string, endpoint fmt.Stringer, reqBody interface{}) (*http.Request, error) {
buf := new(bytes.Buffer)
if reqBody != nil {
err := json.NewEncoder(buf).Encode(reqBody)
if err != nil {
return nil, fmt.Errorf("failed to marshal request body: %w", err)
}
}
req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
if c.token != "" {
req.Header.Set("Authorization", fmt.Sprintf("Token %s", c.token))
}
return req, nil
}
func (c *Client) createEndpoint(parts ...string) (*url.URL, error) {
base, err := url.Parse(c.BaseURL)
if err != nil {
return nil, err
}
endpoint, err := base.Parse(path.Join(base.Path, path.Join(parts...)))
if err != nil {
return nil, err
}
endpoint.Path += "/"
return endpoint, nil
}
func handleResponse(resp *http.Response, respData interface{}) error {
body, err := io.ReadAll(resp.Body)
if err != nil {
return &APIError{
StatusCode: resp.StatusCode,
err: fmt.Errorf("failed to read response body: %w", err),
}
}
err = json.Unmarshal(body, respData)
if err != nil {
return fmt.Errorf("failed to umarshal response body: %w", err)
}
return nil
}
func handleError(resp *http.Response) error {
switch resp.StatusCode {
case http.StatusNotFound:
return readError(resp, &NotFoundError{})
default:
return readRawError(resp)
}
}
|