hurricane: add API rate limiter. (#1417)
This commit is contained in:
parent
e8750f50ae
commit
ed5c0a3869
5 changed files with 39 additions and 7 deletions
1
go.mod
1
go.mod
|
@ -52,6 +52,7 @@ require (
|
|||
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad
|
||||
golang.org/x/net v0.0.0-20210220033124-5f55cee0dc0d
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
|
||||
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba
|
||||
google.golang.org/api v0.20.0
|
||||
gopkg.in/ns1/ns1-go.v2 v2.4.4
|
||||
gopkg.in/square/go-jose.v2 v2.5.1
|
||||
|
|
3
go.sum
3
go.sum
|
@ -631,8 +631,9 @@ golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxb
|
|||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 h1:Hir2P/De0WpUhtrKGGjvSb2YxUgyZ7EFOSLIcSSpiwE=
|
||||
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba h1:O8mE0/t419eoIwhTFpKVkHiTs/Igowgfkj25AcZrtiE=
|
||||
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package hurricane
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
@ -87,7 +88,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
|
|||
func (d *DNSProvider) Present(domain, _, keyAuth string) error {
|
||||
_, txtRecord := dns01.GetRecord(domain, keyAuth)
|
||||
|
||||
err := d.client.UpdateTxtRecord(domain, txtRecord)
|
||||
err := d.client.UpdateTxtRecord(context.Background(), domain, txtRecord)
|
||||
if err != nil {
|
||||
return fmt.Errorf("hurricane: %w", err)
|
||||
}
|
||||
|
@ -97,7 +98,7 @@ func (d *DNSProvider) Present(domain, _, keyAuth string) error {
|
|||
|
||||
// CleanUp updates the TXT record matching the specified parameters.
|
||||
func (d *DNSProvider) CleanUp(domain, _, _ string) error {
|
||||
err := d.client.UpdateTxtRecord(domain, ".")
|
||||
err := d.client.UpdateTxtRecord(context.Background(), domain, ".")
|
||||
if err != nil {
|
||||
return fmt.Errorf("hurricane: %w", err)
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package internal
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
|
@ -10,6 +11,8 @@ import (
|
|||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
const defaultBaseURL = "https://dyn.dns.he.net/nic/update"
|
||||
|
@ -20,14 +23,19 @@ const (
|
|||
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
|
||||
baseURL string
|
||||
HTTPClient *http.Client
|
||||
rateLimiters sync.Map
|
||||
|
||||
baseURL string
|
||||
|
||||
credentials map[string]string
|
||||
credMu sync.Mutex
|
||||
|
@ -43,7 +51,7 @@ func NewClient(credentials map[string]string) *Client {
|
|||
}
|
||||
|
||||
// UpdateTxtRecord updates a TXT record.
|
||||
func (c *Client) UpdateTxtRecord(domain string, txt string) error {
|
||||
func (c *Client) UpdateTxtRecord(ctx context.Context, domain string, txt string) error {
|
||||
hostname := fmt.Sprintf("_acme-challenge.%s", domain)
|
||||
|
||||
c.credMu.Lock()
|
||||
|
@ -59,6 +67,13 @@ func (c *Client) UpdateTxtRecord(domain string, txt string) error {
|
|||
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
|
||||
|
@ -95,6 +110,8 @@ func evaluateBody(body string, hostname string) error {
|
|||
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:
|
||||
|
@ -104,3 +121,14 @@ func evaluateBody(body string, hostname string) error {
|
|||
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/go-acme/lego/issues/1415
|
||||
func limit(burst int) rate.Limit {
|
||||
return 1 / rate.Limit(120/(10-burst+1))
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package internal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
|
@ -74,7 +75,7 @@ func TestClient_UpdateTxtRecord(t *testing.T) {
|
|||
client := NewClient(map[string]string{"example.com": "secret"})
|
||||
client.baseURL = server.URL
|
||||
|
||||
err := client.UpdateTxtRecord("example.com", "foo")
|
||||
err := client.UpdateTxtRecord(context.Background(), "example.com", "foo")
|
||||
test.expected(t, err)
|
||||
})
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue