-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathclient.go
More file actions
166 lines (145 loc) · 4.44 KB
/
client.go
File metadata and controls
166 lines (145 loc) · 4.44 KB
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
package servercow
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"time"
)
const baseAPIURL = "https://api.servercow.de/dns/v1/domains"
// client is an HTTP client for the Servercow DNS API.
type client struct {
username string
password string
baseURL *url.URL
httpClient *http.Client
}
// newClient creates a new Servercow API client.
func newClient(username, password string) *client {
base, _ := url.Parse(baseAPIURL)
return &client{
username: username,
password: password,
baseURL: base,
httpClient: &http.Client{Timeout: 30 * time.Second},
}
}
// getRecords fetches all DNS records for the given zone (bare domain, no trailing dot).
func (c *client) getRecords(ctx context.Context, zone string) ([]record, error) {
endpoint := c.baseURL.JoinPath(zone)
req, err := newJSONRequest(ctx, http.MethodGet, endpoint.String(), nil)
if err != nil {
return nil, fmt.Errorf("servercow: build request: %w", err)
}
c.setAuth(req)
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("servercow: GET %s: %w", endpoint, err)
}
defer resp.Body.Close()
if resp.StatusCode/100 != 2 {
return nil, fmt.Errorf("servercow: unexpected status %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("servercow: read response: %w", err)
}
var records []record
if err := json.Unmarshal(body, &records); err != nil {
return nil, fmt.Errorf("servercow: decode records: %w (body: %s)", err, body)
}
return records, nil
}
// createUpdateRecord creates or fully replaces the DNS record for a given
// (name, type) pair. If a record with the same name and type already exists,
// the Servercow API replaces it entirely.
func (c *client) createUpdateRecord(ctx context.Context, zone string, r record) error {
endpoint := c.baseURL.JoinPath(zone)
req, err := newJSONRequest(ctx, http.MethodPost, endpoint.String(), r)
if err != nil {
return fmt.Errorf("servercow: build request: %w", err)
}
c.setAuth(req)
resp, err := c.httpClient.Do(req)
if err != nil {
return fmt.Errorf("servercow: POST %s: %w", endpoint, err)
}
defer resp.Body.Close()
if resp.StatusCode/100 != 2 {
return fmt.Errorf("servercow: unexpected status %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("servercow: read response: %w", err)
}
var msg message
if err := json.Unmarshal(body, &msg); err != nil {
// non-JSON response on success is fine
return nil
}
if msg.ErrorMsg != "" {
return fmt.Errorf("servercow: API error: %s", msg.ErrorMsg)
}
return nil
}
// deleteRecord removes the DNS record for a given (name, type) pair.
func (c *client) deleteRecord(ctx context.Context, zone string, r record) error {
endpoint := c.baseURL.JoinPath(zone)
// Only name and type are required for DELETE
payload := struct {
Name string `json:"name"`
Type string `json:"type"`
}{Name: r.Name, Type: r.Type}
req, err := newJSONRequest(ctx, http.MethodDelete, endpoint.String(), payload)
if err != nil {
return fmt.Errorf("servercow: build request: %w", err)
}
c.setAuth(req)
resp, err := c.httpClient.Do(req)
if err != nil {
return fmt.Errorf("servercow: DELETE %s: %w", endpoint, err)
}
defer resp.Body.Close()
if resp.StatusCode/100 != 2 {
return fmt.Errorf("servercow: unexpected status %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("servercow: read response: %w", err)
}
var msg message
if err := json.Unmarshal(body, &msg); err != nil {
return nil
}
if msg.ErrorMsg != "" {
return fmt.Errorf("servercow: API error: %s", msg.ErrorMsg)
}
return nil
}
// setAuth adds the Servercow authentication headers to the request.
func (c *client) setAuth(req *http.Request) {
req.Header.Set("X-Auth-Username", c.username)
req.Header.Set("X-Auth-Password", c.password)
}
// newJSONRequest creates an HTTP request with JSON content type.
// payload may be nil for GET requests.
func newJSONRequest(ctx context.Context, method, endpoint string, payload any) (*http.Request, error) {
var body io.Reader
if payload != nil {
b, err := json.Marshal(payload)
if err != nil {
return nil, fmt.Errorf("marshal payload: %w", err)
}
body = bytes.NewReader(b)
}
req, err := http.NewRequestWithContext(ctx, method, endpoint, body)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
return req, nil
}