-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathprovider.go
169 lines (145 loc) · 5.57 KB
/
provider.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
// Package libdnstemplate implements a DNS record management client compatible
// with the libdns interfaces for ACMEProxy.
package acmeproxy
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"github.com/libdns/libdns"
)
type HTTPClient interface {
Do(req *http.Request) (*http.Response, error)
}
type acmeProxy struct {
FQDN string `json:"fqdn"`
Value string `json:"value"`
}
// Credentials represents the username and password required for authentication.
// The fields are optional and can be omitted.
type Credentials struct {
Username string `json:"username,omitempty"` // Username represents the user's name for authentication.
Password string `json:"password,omitempty"` // Password represents the user's password for authentication.
}
// Provider facilitates DNS record manipulation with ACMEProxy.
type Provider struct {
// Credentials are the username and password required for authentication.
// The fields are optional and can be omitted.
Credentials
// Endpoint is the URL of the ACMEProxy server.
Endpoint string `json:"endpoint"`
// HTTPClient is the client used to communicate with the ACMEProxy server.
// If nil, a default client will be used.
HTTPClient HTTPClient
}
func (p *Provider) getClient() HTTPClient {
if p.HTTPClient == nil {
return http.DefaultClient
}
return p.HTTPClient
}
func (p *Provider) doAction(ctx context.Context, endpoint *url.URL, _ string, zone string, record libdns.Record) (libdns.Record, error) {
// We only support TXT records
rawRec, err := record.RR().Parse()
if err != nil {
return nil, fmt.Errorf("ACMEProxy provider could not parse record: %w", err)
}
txtRecord, ok := rawRec.(libdns.TXT)
if !ok {
return nil, fmt.Errorf("ACMEProxy provider only supports TXT records")
}
// Create Request Body
reqBody := new(bytes.Buffer)
{
msg := acmeProxy{
FQDN: libdns.AbsoluteName(txtRecord.Name, zone),
Value: txtRecord.Text,
}
if err := json.NewEncoder(reqBody).Encode(msg); err != nil {
return nil, fmt.Errorf("ACMEProxy provider could not marshal JSON: %w", err)
}
}
// Create Request
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint.String(), reqBody)
if err != nil {
return nil, fmt.Errorf("ACMEProxy provider could not create request: %w", err)
}
req.Header.Set("Accept", "application/json")
req.Header.Set("Content-Type", "application/json")
// Add Basic Auth
if p.Username != "" || p.Password != "" {
req.SetBasicAuth(p.Username, p.Password)
}
// Send Request
resp, err := p.getClient().Do(req)
if err != nil {
return nil, fmt.Errorf("ACMEProxy provider could not send request: %w", err)
}
defer resp.Body.Close()
// Validate Response
{
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("ACMEProxy provider received status code %d", resp.StatusCode)
}
var respMsg acmeProxy
if err := json.NewDecoder(resp.Body).Decode(&respMsg); err != nil {
return nil, fmt.Errorf("ACMEProxy provider could not unmarshal JSON: %w", err)
}
if respMsg.FQDN != libdns.AbsoluteName(txtRecord.Name, zone) {
return nil, fmt.Errorf("ACMEProxy provider received unexpected FQDN %q expected %q", respMsg.FQDN, libdns.AbsoluteName(txtRecord.Name, zone))
}
if respMsg.Value != txtRecord.Text {
return nil, fmt.Errorf("ACMEProxy provider received unexpected Value %q", respMsg.Value)
}
}
return txtRecord, nil
}
func (p *Provider) doActions(ctx context.Context, action string, zone string, records []libdns.Record) ([]libdns.Record, error) {
// Validate Endpoint
uri, err := url.ParseRequestURI(p.Endpoint)
if err != nil {
return nil, fmt.Errorf("ACMEProxy provider invalid endpoint [%s]: %w", p.Endpoint, err)
}
endpoint := uri.JoinPath(action)
// Loop through records
// This is not atomic, but we want to try to complete as many records as possible
// So when an error occurs, we return the completed records along with the error
completedRecords := []libdns.Record{}
for _, record := range records {
completedRecord, err := p.doAction(ctx, endpoint, action, zone, record)
if err != nil {
return completedRecords, fmt.Errorf("ACMEProxy provider could not %s record %q: %w", action, record.RR().Name, err)
}
// Add Record to completed set
completedRecords = append(completedRecords, completedRecord)
}
return completedRecords, nil
}
// GetRecords lists all the records in the zone.
// This is not supported by the ACMEProxy provider.
func (p *Provider) GetRecords(ctx context.Context, zone string) ([]libdns.Record, error) {
return nil, fmt.Errorf("ACMEProxy provider does not support listing records")
}
// AppendRecords adds records to the zone. It returns the records that were added.
// It does the same as SetRecords.
func (p *Provider) AppendRecords(ctx context.Context, zone string, records []libdns.Record) ([]libdns.Record, error) {
return p.SetRecords(ctx, zone, records)
}
// SetRecords sets the records in the zone, either by updating existing records or creating new ones.
// It returns the updated records.
func (p *Provider) SetRecords(ctx context.Context, zone string, records []libdns.Record) ([]libdns.Record, error) {
return p.doActions(ctx, "present", zone, records)
}
// DeleteRecords deletes the records from the zone. It returns the records that were deleted.
func (p *Provider) DeleteRecords(ctx context.Context, zone string, records []libdns.Record) ([]libdns.Record, error) {
return p.doActions(ctx, "cleanup", zone, records)
}
// Interface guards
var (
_ libdns.RecordGetter = (*Provider)(nil)
_ libdns.RecordAppender = (*Provider)(nil)
_ libdns.RecordSetter = (*Provider)(nil)
_ libdns.RecordDeleter = (*Provider)(nil)
)