client.go raw
1 package internal
2
3 import (
4 "bytes"
5 "context"
6 "encoding/json"
7 "fmt"
8 "io"
9 "net/http"
10 "net/url"
11 "time"
12
13 "github.com/go-acme/lego/v4/providers/dns/internal/errutils"
14 )
15
16 // DefaultBaseURL default API endpoint.
17 const DefaultBaseURL = "https://www.versio.nl/api/v1/"
18
19 // Client the API client for Versio DNS.
20 type Client struct {
21 username string
22 password string
23
24 BaseURL *url.URL
25 HTTPClient *http.Client
26 }
27
28 // NewClient creates a new Client.
29 func NewClient(username, password string) *Client {
30 baseURL, _ := url.Parse(DefaultBaseURL)
31
32 return &Client{
33 username: username,
34 password: password,
35 BaseURL: baseURL,
36 HTTPClient: &http.Client{Timeout: 5 * time.Second},
37 }
38 }
39
40 // UpdateDomain updates domain information.
41 // https://www.versio.nl/RESTapidoc/#api-Domains-Update
42 func (c *Client) UpdateDomain(ctx context.Context, domain string, msg *DomainInfo) (*DomainInfoResponse, error) {
43 endpoint := c.BaseURL.JoinPath("domains", domain, "update")
44
45 req, err := newJSONRequest(ctx, http.MethodPost, endpoint, msg)
46 if err != nil {
47 return nil, err
48 }
49
50 respData := &DomainInfoResponse{}
51
52 err = c.do(req, respData)
53 if err != nil {
54 return nil, err
55 }
56
57 return respData, nil
58 }
59
60 // GetDomain gets domain information.
61 // https://www.versio.nl/RESTapidoc/#api-Domains-Domain
62 func (c *Client) GetDomain(ctx context.Context, domain string) (*DomainInfoResponse, error) {
63 endpoint := c.BaseURL.JoinPath("domains", domain)
64
65 query := endpoint.Query()
66 query.Set("show_dns_records", "true")
67 endpoint.RawQuery = query.Encode()
68
69 req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)
70 if err != nil {
71 return nil, err
72 }
73
74 respData := &DomainInfoResponse{}
75
76 err = c.do(req, respData)
77 if err != nil {
78 return nil, err
79 }
80
81 return respData, nil
82 }
83
84 func (c *Client) do(req *http.Request, result any) error {
85 if c.username != "" && c.password != "" {
86 req.SetBasicAuth(c.username, c.password)
87 }
88
89 resp, err := c.HTTPClient.Do(req)
90 if resp != nil {
91 defer func() { _ = resp.Body.Close() }()
92 }
93
94 if err != nil {
95 return errutils.NewHTTPDoError(req, err)
96 }
97
98 if resp.StatusCode/100 != 2 {
99 return parseError(req, resp)
100 }
101
102 if result == nil {
103 return nil
104 }
105
106 raw, err := io.ReadAll(resp.Body)
107 if err != nil {
108 return errutils.NewReadResponseError(req, resp.StatusCode, err)
109 }
110
111 if err = json.Unmarshal(raw, result); err != nil {
112 return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
113 }
114
115 return nil
116 }
117
118 func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {
119 buf := new(bytes.Buffer)
120
121 if payload != nil {
122 err := json.NewEncoder(buf).Encode(payload)
123 if err != nil {
124 return nil, fmt.Errorf("failed to create request JSON body: %w", err)
125 }
126 }
127
128 req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)
129 if err != nil {
130 return nil, fmt.Errorf("unable to create request: %w", err)
131 }
132
133 req.Header.Set("Accept", "application/json")
134
135 if payload != nil {
136 req.Header.Set("Content-Type", "application/json")
137 }
138
139 return req, nil
140 }
141
142 func parseError(req *http.Request, resp *http.Response) error {
143 raw, _ := io.ReadAll(resp.Body)
144
145 response := &ErrorResponse{}
146
147 err := json.Unmarshal(raw, response)
148 if err != nil {
149 return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)
150 }
151
152 return fmt.Errorf("[status code: %d] %w", resp.StatusCode, response.Message)
153 }
154