File: desec.go

package info (click to toggle)
golang-github-nrdcg-desec 0.6.0-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, forky, sid, trixie
  • size: 228 kB
  • sloc: makefile: 13
file content (152 lines) | stat: -rw-r--r-- 3,450 bytes parent folder | download
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)
	}
}