hurricane: add API rate limiter. (#1417)

This commit is contained in:
Ludovic Fernandez 2021-05-31 23:42:24 +02:00 committed by GitHub
parent e8750f50ae
commit ed5c0a3869
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 39 additions and 7 deletions

1
go.mod
View file

@ -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
View file

@ -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=

View file

@ -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)
}

View file

@ -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))
}

View file

@ -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)
})
}