Skip to content

Commit

Permalink
acme: add function to check rate limits
Browse files Browse the repository at this point in the history
This change exposes a function to extract rate limit duration
from a client error using Retry-After response header.
Author: David Calavera <[email protected]>.

Fixes golang/go#19304.
Change-Id: Iec9cfab398b84c6f216b95d3265ffad1ce2f29a7
Reviewed-on: https://go-review.googlesource.com/37463
Run-TryBot: Brad Fitzpatrick <[email protected]>
TryBot-Result: Gobot Gobot <[email protected]>
Reviewed-by: Brad Fitzpatrick <[email protected]>
  • Loading branch information
desdeel2d0m authored and Alex Vaghin committed Apr 25, 2017
1 parent 0c803cf commit 54fe9c8
Show file tree
Hide file tree
Showing 2 changed files with 89 additions and 0 deletions.
26 changes: 26 additions & 0 deletions acme/types.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
// Copyright 2016 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package acme

import (
"errors"
"fmt"
"net/http"
"strings"
"time"
)

// ACME server response statuses used to describe Authorization and Challenge states.
Expand Down Expand Up @@ -79,6 +84,27 @@ func (a *AuthorizationError) Error() string {
return fmt.Sprintf("acme: authorization error for %s: %s", a.Identifier, strings.Join(e, "; "))
}

// RateLimit reports whether err represents a rate limit error and
// any Retry-After duration returned by the server.
//
// See the following for more details on rate limiting:
// https://tools.ietf.org/html/draft-ietf-acme-acme-05#section-5.6
func RateLimit(err error) (time.Duration, bool) {
e, ok := err.(*Error)
if !ok {
return 0, false
}
// Some CA implementations may return incorrect values.
// Use case-insensitive comparison.
if !strings.HasSuffix(strings.ToLower(e.ProblemType), ":ratelimited") {
return 0, false
}
if e.Header == nil {
return 0, true
}
return retryAfter(e.Header.Get("Retry-After"), 0), true
}

// Account is a user account. It is associated with a private key.
type Account struct {
// URI is the account unique ID, which is also a URL used to retrieve
Expand Down
63 changes: 63 additions & 0 deletions acme/types_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Copyright 2017 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package acme

import (
"errors"
"net/http"
"testing"
"time"
)

func TestRateLimit(t *testing.T) {
now := time.Date(2017, 04, 27, 10, 0, 0, 0, time.UTC)
f := timeNow
defer func() { timeNow = f }()
timeNow = func() time.Time { return now }

h120, hTime := http.Header{}, http.Header{}
h120.Set("Retry-After", "120")
hTime.Set("Retry-After", "Tue Apr 27 11:00:00 2017")

err1 := &Error{
ProblemType: "urn:ietf:params:acme:error:nolimit",
Header: h120,
}
err2 := &Error{
ProblemType: "urn:ietf:params:acme:error:rateLimited",
Header: h120,
}
err3 := &Error{
ProblemType: "urn:ietf:params:acme:error:rateLimited",
Header: nil,
}
err4 := &Error{
ProblemType: "urn:ietf:params:acme:error:rateLimited",
Header: hTime,
}

tt := []struct {
err error
res time.Duration
ok bool
}{
{nil, 0, false},
{errors.New("dummy"), 0, false},
{err1, 0, false},
{err2, 2 * time.Minute, true},
{err3, 0, true},
{err4, time.Hour, true},
}
for i, test := range tt {
res, ok := RateLimit(test.err)
if ok != test.ok {
t.Errorf("%d: RateLimit(%+v): ok = %v; want %v", i, test.err, ok, test.ok)
continue
}
if res != test.res {
t.Errorf("%d: RateLimit(%+v) = %v; want %v", i, test.err, res, test.res)
}
}
}

0 comments on commit 54fe9c8

Please sign in to comment.