client.go raw
1 package internal
2
3 import (
4 "bytes"
5 "context"
6 "encoding/json"
7 "errors"
8 "fmt"
9 "io"
10 "net/http"
11 "net/url"
12 "strconv"
13 "time"
14
15 "github.com/go-acme/lego/v4/providers/dns/internal/errutils"
16 )
17
18 const defaultBaseURL = "https://api.binarylane.com.au/v2/"
19
20 const authorizationHeader = "Authorization"
21
22 // Client the Binary Lane API client.
23 type Client struct {
24 apiToken string
25
26 baseURL *url.URL
27 HTTPClient *http.Client
28 }
29
30 // NewClient creates a new Client.
31 func NewClient(apiToken string) (*Client, error) {
32 if apiToken == "" {
33 return nil, errors.New("credentials missing")
34 }
35
36 baseURL, _ := url.Parse(defaultBaseURL)
37
38 return &Client{
39 apiToken: apiToken,
40 baseURL: baseURL,
41 HTTPClient: &http.Client{Timeout: 10 * time.Second},
42 }, nil
43 }
44
45 // CreateRecord Creates a new domain record.
46 // https://api.binarylane.com.au/reference/#tag/Domains/paths/~1v2~1domains~1%7Bdomain_name%7D~1records/post
47 func (c *Client) CreateRecord(ctx context.Context, domain string, record Record) (*Record, error) {
48 endpoint := c.baseURL.JoinPath("domains", domain, "records")
49
50 if record.Name == "" {
51 record.Name = "@"
52 }
53
54 req, err := newJSONRequest(ctx, http.MethodPost, endpoint, record)
55 if err != nil {
56 return nil, err
57 }
58
59 var result APIResponse
60
61 err = c.do(req, &result)
62 if err != nil {
63 return nil, err
64 }
65
66 return result.DomainRecord, nil
67 }
68
69 // DeleteRecord Deletes an existing domain record.
70 // https://api.binarylane.com.au/reference/#tag/Domains/paths/~1v2~1domains~1%7Bdomain_name%7D~1records~1%7Brecord_id%7D/delete
71 func (c *Client) DeleteRecord(ctx context.Context, domainName string, recordID int64) error {
72 endpoint := c.baseURL.JoinPath("domains", domainName, "records", strconv.FormatInt(recordID, 10))
73
74 req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)
75 if err != nil {
76 return err
77 }
78
79 return c.do(req, nil)
80 }
81
82 func (c *Client) do(req *http.Request, result any) error {
83 req.Header.Set(authorizationHeader, "Bearer "+c.apiToken)
84
85 resp, err := c.HTTPClient.Do(req)
86 if err != nil {
87 return errutils.NewHTTPDoError(req, err)
88 }
89
90 defer func() { _ = resp.Body.Close() }()
91
92 if resp.StatusCode/100 != 2 {
93 return parseError(req, resp)
94 }
95
96 if result == nil {
97 return nil
98 }
99
100 raw, err := io.ReadAll(resp.Body)
101 if err != nil {
102 return errutils.NewReadResponseError(req, resp.StatusCode, err)
103 }
104
105 err = json.Unmarshal(raw, result)
106 if err != nil {
107 return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
108 }
109
110 return nil
111 }
112
113 func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {
114 buf := new(bytes.Buffer)
115
116 if payload != nil {
117 err := json.NewEncoder(buf).Encode(payload)
118 if err != nil {
119 return nil, fmt.Errorf("failed to create request JSON body: %w", err)
120 }
121 }
122
123 req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)
124 if err != nil {
125 return nil, fmt.Errorf("unable to create request: %w", err)
126 }
127
128 req.Header.Set("Accept", "application/json")
129
130 if payload != nil {
131 req.Header.Set("Content-Type", "application/json")
132 }
133
134 return req, nil
135 }
136
137 func parseError(req *http.Request, resp *http.Response) error {
138 raw, _ := io.ReadAll(resp.Body)
139
140 var errAPI APIError
141
142 err := json.Unmarshal(raw, &errAPI)
143 if err != nil {
144 return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)
145 }
146
147 return &errAPI
148 }
149