-
Notifications
You must be signed in to change notification settings - Fork 0
/
client.go
296 lines (252 loc) · 8.53 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
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
// Package mydnshost_go_api provides an API client for MyDNSHost.
package mydnshost_go_api
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
)
const apiHost = "api.mydnshost.co.uk"
const apiVersion = "1.0"
type apiResponse struct {
ResponseId string `json:"respid"`
Method string `json:"method"`
Error *string `json:"error"`
ErrorData map[string]string `json:"errorData"`
Response *json.RawMessage `json:"response"`
}
type apiRequest struct {
Data interface{} `json:"data"`
}
// ClientAuthenticator adds authentication headers to outgoing requests to the API.
type ClientAuthenticator interface {
AddHeaders(r *http.Request)
}
// Client is the client API for communicating with MyDNSHost. For most requests it will require a ClientAuthenticator
// to be provided that can supply credentials to the API.
type Client struct {
Authenticator ClientAuthenticator
}
// PingResponse is the API response to a ping request, containing the time the request was sent.
type PingResponse struct {
Time string `json:"time"`
}
// Ping sends a ping request to the API. It does not require authentication.
func (c *Client) Ping(ctx context.Context) (*PingResponse, error) {
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("ping/%d", time.Now().Unix()), nil)
if err != nil {
return nil, err
}
response := &PingResponse{}
return response, json.Unmarshal(*res.Response, response)
}
// UserDataResponse is the API response to a user data request, describing the current user and access levels.
type UserDataResponse struct {
User struct {
Id string `json:"id"`
Email string `json:"email"`
RealName string `json:"realname"`
} `json:"user"`
Access struct {
DomainsRead bool `json:"domains_read"`
DomainsWrite bool `json:"domains_write"`
UserRead bool `json:"user_read"`
UserWrite bool `json:"user_write"`
} `json:"access"`
}
// UserData sends a request for details on the current user.
func (c *Client) UserData(ctx context.Context) (*UserDataResponse, error) {
res, err := c.request(ctx, http.MethodGet, "userdata", nil)
if err != nil {
return nil, err
}
response := &UserDataResponse{}
return response, json.Unmarshal(*res.Response, response)
}
// AccessLevel describes a level of access to a domain.
type AccessLevel string
const (
LevelOwner AccessLevel = "owner"
LevelAdmin AccessLevel = "admin"
LevelWrite AccessLevel = "write"
LevelRead AccessLevel = "read"
LevelNone AccessLevel = "none"
)
// Domains lists all domains accessible by the current user, and gives the access level to each.
func (c *Client) Domains(ctx context.Context) (map[string]AccessLevel, error) {
res, err := c.request(ctx, http.MethodGet, "domains", nil)
if err != nil {
return nil, err
}
response := make(map[string]AccessLevel)
return response, json.Unmarshal(*res.Response, &response)
}
// Record contains the basic details of a DNS record.
type Record struct {
Name string `json:"name,omitempty"`
Type string `json:"type,omitempty"`
Content string `json:"content,omitempty"`
TTL int `json:"ttl,omitempty"`
Priority *int `json:"priority,omitempty"`
Disabled *bool `json:"disabled,omitempty"`
}
// ExistingRecord is a Record that has been stored by MyDNSHost and thus has an ID and change history.
type ExistingRecord struct {
Record
Id int `json:"id"`
ChangedAt int `json:"changed_at,omitempty"`
ChangedBy *int `json:"changed_by,omitempty"`
}
// ChangedRecord is a record that has been modified by a call to ModifyRecords.
type ChangedRecord struct {
ExistingRecord
Updated bool `json:"updated,omitempty"`
Deleted bool `json:"deleted,omitempty"`
}
// RecordsResponse lists all records for a domain, as well as the NS and SOA data for it.
type RecordsResponse struct {
Records []ExistingRecord `json:"records"`
HasNS bool `json:"hasNS"`
Soa struct {
PrimaryNS string `json:"primaryNS"`
AdminAddress string `json:"adminAddress"`
Serial uint64 `json:"serial"`
Refresh uint64 `json:"refresh"`
Retry uint64 `json:"retry"`
Expire uint64 `json:"expire"`
MinTTL uint64 `json:"min_ttl"`
} `json:"soa"`
}
// Records retrieves all records associated with the specified domain.
func (c *Client) Records(ctx context.Context, domain string) (*RecordsResponse, error) {
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("domains/%s/records", domain), nil)
if err != nil {
return nil, err
}
response := &RecordsResponse{}
return response, json.Unmarshal(*res.Response, response)
}
// RecordOperation is an operation performed on a record when calling ModifyRecords.
type RecordOperation json.RawMessage
// ModifyRecord changes an existing record with the given ID. Any field populated in the record will be updated.
func ModifyRecord(id int, record Record) RecordOperation {
res, _ := json.Marshal(ExistingRecord{
Record: record,
Id: id,
})
return res
}
// DeleteRecord deletes an existing record with the given ID.
func DeleteRecord(id int) RecordOperation {
res, _ := json.Marshal(struct {
Id int `json:"id"`
Delete bool `json:"delete"`
}{
Id: id,
Delete: true,
})
return res
}
// CreateRecord creates a new record. All non-pointer fields of the given Record must be supplied.
func CreateRecord(record Record) RecordOperation {
res, _ := json.Marshal(record)
return res
}
// ModifyRecordsResponse lists all changed records as a result of a ModifyRecords request.
type ModifyRecordsResponse struct {
Serial uint64 `json:"serial"`
Changed []ChangedRecord `json:"changed"`
}
// ModifyRecords performs one or more operations on the records of a domain, including adding, modifying and deleting.
func (c *Client) ModifyRecords(ctx context.Context, domain string, operations ...RecordOperation) (*ModifyRecordsResponse, error) {
records := make([]json.RawMessage, len(operations))
for i := range operations {
records[i] = json.RawMessage(operations[i])
}
r := apiRequest{
Data: struct {
Records []json.RawMessage `json:"records"`
}{
Records: records,
},
}
res, err := c.request(ctx, http.MethodPost, fmt.Sprintf("domains/%s/records", domain), r)
if err != nil {
return nil, err
}
response := &ModifyRecordsResponse{}
return response, json.Unmarshal(*res.Response, response)
}
// FindRecordsResponse lists all records for a domain that match provided search terms.
type FindRecordsResponse struct {
Records []ExistingRecord `json:"records"`
}
// NamedRecords finds records with the given name under the specified domain.
// recordType may be left blank to match all record types.
func (c *Client) NamedRecords(ctx context.Context, domain, recordName, recordType string) (*FindRecordsResponse, error) {
res, err := c.request(
ctx,
http.MethodGet,
strings.TrimSuffix(fmt.Sprintf("domains/%s/record/%s/%s", domain, recordName, recordType), "/"),
nil,
)
if err != nil {
return nil, err
}
response := &FindRecordsResponse{}
return response, json.Unmarshal(*res.Response, response)
}
// DeletedNamedRecordsResponse describes the result of deleting named records
type DeletedNamedRecordsResponse struct {
Deleted int `json:"deleted"`
Serial uint64 `json:"serial"`
}
// DeleteNamedRecords deletes records with the given name under the specified domain.
// recordType may be left blank to match all record types.
func (c *Client) DeleteNamedRecords(ctx context.Context, domain, recordName, recordType string) (*DeletedNamedRecordsResponse, error) {
res, err := c.request(
ctx,
http.MethodDelete,
strings.TrimSuffix(fmt.Sprintf("domains/%s/record/%s/%s", domain, recordName, recordType), "/"),
nil,
)
if err != nil {
return nil, err
}
response := &DeletedNamedRecordsResponse{}
return response, json.Unmarshal(*res.Response, response)
}
func (c *Client) request(ctx context.Context, method string, route string, body interface{}) (*apiResponse, error) {
var reader io.Reader = nil
if body != nil {
b, err := json.Marshal(body)
if err != nil {
return nil, err
}
reader = bytes.NewReader(b)
}
req, err := http.NewRequestWithContext(ctx, method, fmt.Sprintf("https://%s/%s/%s", apiHost, apiVersion, route), reader)
if err != nil {
return nil, err
}
if c.Authenticator != nil {
c.Authenticator.AddHeaders(req)
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
response := &apiResponse{}
if err := json.NewDecoder(res.Body).Decode(response); err != nil {
return nil, err
}
if response.Error != nil {
return nil, fmt.Errorf("API error: %s", *response.Error)
}
return response, nil
}