forked from go-acme/lego
-
Notifications
You must be signed in to change notification settings - Fork 0
/
client.go
134 lines (110 loc) · 3.28 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
package internal
import (
"bytes"
"context"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/url"
"strings"
"sync"
"time"
"golang.org/x/time/rate"
)
const defaultBaseURL = "https://dyn.dns.he.net/nic/update"
const (
codeGood = "good"
codeNoChg = "nochg"
codeAbuse = "abuse"
codeBadAgent = "badagent"
codeBadAuth = "badauth"
codeInterval = "interval"
codeNoHost = "nohost"
codeNotFqdn = "notfqdn"
)
const defaultBurst = 5
// Client the Hurricane Electric client.
type Client struct {
HTTPClient *http.Client
rateLimiters sync.Map
baseURL string
credentials map[string]string
credMu sync.Mutex
}
// NewClient Creates a new Client.
func NewClient(credentials map[string]string) *Client {
return &Client{
HTTPClient: &http.Client{Timeout: 5 * time.Second},
baseURL: defaultBaseURL,
credentials: credentials,
}
}
// UpdateTxtRecord updates a TXT record.
func (c *Client) UpdateTxtRecord(ctx context.Context, domain string, txt string) error {
hostname := fmt.Sprintf("_acme-challenge.%s", domain)
c.credMu.Lock()
token, ok := c.credentials[domain]
c.credMu.Unlock()
if !ok {
return fmt.Errorf("hurricane: Domain %s not found in credentials, check your credentials map", domain)
}
data := url.Values{}
data.Set("password", token)
data.Set("hostname", hostname)
data.Set("txt", txt)
rl, _ := c.rateLimiters.LoadOrStore(hostname, rate.NewLimiter(limit(defaultBurst), defaultBurst))
err := rl.(*rate.Limiter).Wait(ctx)
if err != nil {
return err
}
resp, err := c.HTTPClient.PostForm(c.baseURL, data)
if err != nil {
return err
}
defer func() { _ = resp.Body.Close() }()
bodyBytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
body := string(bytes.TrimSpace(bodyBytes))
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("%d: attempt to change TXT record %s returned %s", resp.StatusCode, hostname, body)
}
return evaluateBody(body, hostname)
}
func evaluateBody(body string, hostname string) error {
words := strings.SplitN(body, " ", 2)
switch words[0] {
case codeGood:
return nil
case codeNoChg:
log.Printf("%s: unchanged content written to TXT record %s", body, hostname)
return nil
case codeAbuse:
return fmt.Errorf("%s: blocked hostname for abuse: %s", body, hostname)
case codeBadAgent:
return fmt.Errorf("%s: user agent not sent or HTTP method not recognized; open an issue on go-acme/lego on Github", body)
case codeBadAuth:
return fmt.Errorf("%s: wrong authentication token provided for TXT record %s", body, hostname)
case codeInterval:
return fmt.Errorf("%s: TXT records update exceeded API rate limit", body)
case codeNoHost:
return fmt.Errorf("%s: the record provided does not exist in this account: %s", body, hostname)
case codeNotFqdn:
return fmt.Errorf("%s: the record provided isn't an FQDN: %s", body, hostname)
default:
// This is basically only server errors.
return fmt.Errorf("attempt to change TXT record %s returned %s", hostname, body)
}
}
// limit computes the rate based on burst.
// The API rate limit per-record is 10 reqs / 2 minutes.
//
// 10 reqs / 2 minutes = freq 1/12 (burst = 1)
// 6 reqs / 2 minutes = freq 1/20 (burst = 5)
//
// https://github.com/simonmittag/lego/issues/1415
func limit(burst int) rate.Limit {
return 1 / rate.Limit(120/(10-burst+1))
}