-
Notifications
You must be signed in to change notification settings - Fork 6
/
client.go
210 lines (175 loc) · 5.56 KB
/
client.go
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
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
package goacmedns
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net"
"net/http"
"runtime"
"time"
)
const (
// ua is a custom user-agent identifier.
ua = "goacmedns"
)
// userAgent returns a string that can be used as a HTTP request `User-Agent`
// header. It includes the `ua` string alongside the OS and architecture of the
// system.
func userAgent() string {
return fmt.Sprintf("%s (%s; %s)", ua, runtime.GOOS, runtime.GOARCH)
}
var (
// defaultTimeout is used for the httpClient Timeout settings.
defaultTimeout = 30 * time.Second
// httpClient is a `http.Client` that is customized with the `defaultTimeout`.
httpClient = http.Client{
CheckRedirect: nil,
Jar: nil,
Timeout: defaultTimeout,
Transport: &http.Transport{ //nolint:exhaustivestruct
Proxy: http.ProxyFromEnvironment,
Dial: (&net.Dialer{ //nolint:exhaustivestruct
Timeout: defaultTimeout,
KeepAlive: defaultTimeout,
}).Dial,
TLSHandshakeTimeout: defaultTimeout,
ResponseHeaderTimeout: defaultTimeout,
ExpectContinueTimeout: 1 * time.Second,
},
}
)
// postAPI makes an HTTP POST request to the given URL, sending the given body
// and attaching the requested custom headers to the request. If there is no
// error the HTTP response body and HTTP response object are returned, otherwise
// an error is returned.. All POST requests include a `User-Agent` header
// populated with the `userAgent` function and a `Content-Type` header of
// `application/json`.
func postAPI(url string, body []byte, headers map[string]string) ([]byte, *http.Response, error) {
ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(body))
if err != nil {
return nil, nil, fmt.Errorf("Failed to make req: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", userAgent())
for h, v := range headers {
req.Header.Set(h, v)
}
resp, err := httpClient.Do(req)
if err != nil {
return nil, resp, fmt.Errorf("Failed to do req: %w", err)
}
defer func() { _ = resp.Body.Close() }()
respBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, resp, fmt.Errorf("Failed to read body: %w", err)
}
return respBody, resp, nil
}
// ClientError represents an error from the ACME-DNS server. It holds
// a `Message` describing the operation the client was doing, a `HTTPStatus`
// code returned by the server, and the `Body` of the HTTP Response from the
// server.
type ClientError struct {
// Message is a string describing the client operation that failed
Message string
// HTTPStatus is the HTTP status code the ACME DNS server returned
HTTPStatus int
// Body is the response body the ACME DNS server returned
Body []byte
}
// Error collects all of the ClientError fields into a single string.
func (e ClientError) Error() string {
return fmt.Sprintf("%s : status code %d response: %s",
e.Message, e.HTTPStatus, string(e.Body))
}
// newClientError creates a ClientError instance populated with the given
// arguments.
func newClientError(msg string, respCode int, respBody []byte) ClientError {
return ClientError{
Message: msg,
HTTPStatus: respCode,
Body: respBody,
}
}
// Client is a struct that can be used to interact with an ACME DNS server to
// register accounts and update TXT records.
type Client struct {
// baseURL is the address of the ACME DNS server
baseURL string
}
// NewClient returns a Client configured to interact with the ACME DNS server at
// the given URL.
func NewClient(url string) Client {
return Client{
baseURL: url,
}
}
// RegisterAccount creates an Account with the ACME DNS server. The optional
// `allowFrom` argument is used to constrain which CIDR ranges can use the
// created Account.
func (c Client) RegisterAccount(allowFrom []string) (Account, error) {
var body []byte
if len(allowFrom) > 0 {
req := struct {
AllowFrom []string
}{
AllowFrom: allowFrom,
}
reqBody, err := json.Marshal(req)
if err != nil {
return Account{}, fmt.Errorf("Failed to marshal account: %w", err)
}
body = reqBody
}
url := fmt.Sprintf("%s/register", c.baseURL)
// golangci-lint doesn't know it but postAPI() defers a body close.
respBody, resp, err := postAPI(url, body, nil) //nolint:bodyclose
if err != nil {
return Account{}, err
}
if resp.StatusCode != http.StatusCreated {
return Account{}, newClientError(
"failed to register account", resp.StatusCode, respBody)
}
var acct Account
err = json.Unmarshal(respBody, &acct)
if err != nil {
return Account{}, fmt.Errorf("Failed to unmarshal account: %w", err)
}
acct.ServerURL = c.baseURL
return acct, nil
}
// UpdateTXTRecord updates a TXT record with the ACME DNS server to the `value`
// provided using the `account` specified.
func (c Client) UpdateTXTRecord(account Account, value string) error {
update := struct {
SubDomain string
Txt string
}{
SubDomain: account.SubDomain,
Txt: value,
}
updateBody, err := json.Marshal(update)
if err != nil {
return fmt.Errorf("Failed to marshal update: %w", err)
}
headers := map[string]string{
"X-Api-User": account.Username,
"X-Api-Key": account.Password,
}
url := fmt.Sprintf("%s/update", c.baseURL)
// golangci-lint doesn't know it but postAPI() defers a body close.
respBody, resp, err := postAPI(url, updateBody, headers) //nolint:bodyclose
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK {
return newClientError(
"failed to update txt record", resp.StatusCode, respBody)
}
return nil
}