README update and missing dependencies.

This commit is contained in:
2019-02-25 17:24:19 +05:00
parent afb076d116
commit a0f3ddfa16
166 changed files with 48876 additions and 2 deletions

View File

@@ -0,0 +1,416 @@
// Copyright 2018 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 acmetest provides types for testing acme and autocert packages.
//
// TODO: Consider moving this to x/crypto/acme/internal/acmetest for acme tests as well.
package acmetest
import (
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"math/big"
"net/http"
"net/http/httptest"
"sort"
"strings"
"sync"
"time"
)
// CAServer is a simple test server which implements ACME spec bits needed for testing.
type CAServer struct {
URL string // server URL after it has been started
Roots *x509.CertPool // CA root certificates; initialized in NewCAServer
rootKey crypto.Signer
rootCert []byte // DER encoding
rootTemplate *x509.Certificate
server *httptest.Server
challengeTypes []string // supported challenge types
domainsWhitelist []string // only these domains are valid for issuing, unless empty
mu sync.Mutex
certCount int // number of issued certs
domainAddr map[string]string // domain name to addr:port resolution
authorizations map[string]*authorization // keyed by domain name
errors []error // encountered client errors
}
// NewCAServer creates a new ACME test server and starts serving requests.
// The returned CAServer issues certs signed with the CA roots
// available in the Roots field.
//
// The challengeTypes argument defines the supported ACME challenge types
// sent to a client in a response for a domain authorization.
// If domainsWhitelist is non-empty, the certs will be issued only for the specified
// list of domains. Otherwise, any domain name is allowed.
func NewCAServer(challengeTypes []string, domainsWhitelist []string) *CAServer {
var whitelist []string
for _, name := range domainsWhitelist {
whitelist = append(whitelist, name)
}
sort.Strings(whitelist)
ca := &CAServer{
challengeTypes: challengeTypes,
domainsWhitelist: whitelist,
domainAddr: make(map[string]string),
authorizations: make(map[string]*authorization),
}
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
panic(fmt.Sprintf("ecdsa.GenerateKey: %v", err))
}
tmpl := &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{
Organization: []string{"Test Acme Co"},
CommonName: "Root CA",
},
NotBefore: time.Now(),
NotAfter: time.Now().Add(365 * 24 * time.Hour),
KeyUsage: x509.KeyUsageCertSign,
BasicConstraintsValid: true,
IsCA: true,
}
der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key)
if err != nil {
panic(fmt.Sprintf("x509.CreateCertificate: %v", err))
}
cert, err := x509.ParseCertificate(der)
if err != nil {
panic(fmt.Sprintf("x509.ParseCertificate: %v", err))
}
ca.Roots = x509.NewCertPool()
ca.Roots.AddCert(cert)
ca.rootKey = key
ca.rootCert = der
ca.rootTemplate = tmpl
ca.server = httptest.NewServer(http.HandlerFunc(ca.handle))
ca.URL = ca.server.URL
return ca
}
// Close shuts down the server and blocks until all outstanding
// requests on this server have completed.
func (ca *CAServer) Close() {
ca.server.Close()
}
// Errors returns all client errors.
func (ca *CAServer) Errors() []error {
ca.mu.Lock()
defer ca.mu.Unlock()
return ca.errors
}
// Resolve adds a domain to address resolution for the ca to dial to
// when validating challenges for the domain authorization.
func (ca *CAServer) Resolve(domain, addr string) {
ca.mu.Lock()
defer ca.mu.Unlock()
ca.domainAddr[domain] = addr
}
type discovery struct {
NewReg string `json:"new-reg"`
NewAuthz string `json:"new-authz"`
NewCert string `json:"new-cert"`
}
type challenge struct {
URI string `json:"uri"`
Type string `json:"type"`
Token string `json:"token"`
}
type authorization struct {
Status string `json:"status"`
Challenges []challenge `json:"challenges"`
id int
domain string
}
func (ca *CAServer) handle(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Replay-Nonce", "nonce")
if r.Method == "HEAD" {
// a nonce request
return
}
// TODO: Verify nonce header for all POST requests.
switch {
default:
err := fmt.Errorf("unrecognized r.URL.Path: %s", r.URL.Path)
ca.addError(err)
http.Error(w, err.Error(), http.StatusBadRequest)
// Discovery request.
case r.URL.Path == "/":
resp := &discovery{
NewReg: ca.serverURL("/new-reg"),
NewAuthz: ca.serverURL("/new-authz"),
NewCert: ca.serverURL("/new-cert"),
}
if err := json.NewEncoder(w).Encode(resp); err != nil {
panic(fmt.Sprintf("discovery response: %v", err))
}
// Client key registration request.
case r.URL.Path == "/new-reg":
// TODO: Check the user account key against a ca.accountKeys?
w.Write([]byte("{}"))
// Domain authorization request.
case r.URL.Path == "/new-authz":
var req struct {
Identifier struct{ Value string }
}
if err := decodePayload(&req, r.Body); err != nil {
ca.addError(err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
ca.mu.Lock()
defer ca.mu.Unlock()
authz, ok := ca.authorizations[req.Identifier.Value]
if !ok {
authz = &authorization{
domain: req.Identifier.Value,
Status: "pending",
}
for _, typ := range ca.challengeTypes {
authz.Challenges = append(authz.Challenges, challenge{
Type: typ,
URI: ca.serverURL("/challenge/%s/%s", typ, authz.domain),
Token: challengeToken(authz.domain, typ),
})
}
ca.authorizations[authz.domain] = authz
}
w.Header().Set("Location", ca.serverURL("/authz/%s", authz.domain))
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(authz); err != nil {
panic(fmt.Sprintf("new authz response: %v", err))
}
// Accept tls-alpn-01 challenge type requests.
// TODO: Add http-01 and dns-01 handlers.
case strings.HasPrefix(r.URL.Path, "/challenge/tls-alpn-01/"):
domain := strings.TrimPrefix(r.URL.Path, "/challenge/tls-alpn-01/")
ca.mu.Lock()
defer ca.mu.Unlock()
if _, ok := ca.authorizations[domain]; !ok {
err := fmt.Errorf("challenge accept: no authz for %q", domain)
ca.addError(err)
http.Error(w, err.Error(), http.StatusNotFound)
return
}
go func(domain string) {
err := ca.verifyALPNChallenge(domain)
ca.mu.Lock()
defer ca.mu.Unlock()
authz := ca.authorizations[domain]
if err != nil {
authz.Status = "invalid"
return
}
authz.Status = "valid"
}(domain)
w.Write([]byte("{}"))
// Get authorization status requests.
case strings.HasPrefix(r.URL.Path, "/authz/"):
domain := strings.TrimPrefix(r.URL.Path, "/authz/")
ca.mu.Lock()
defer ca.mu.Unlock()
authz, ok := ca.authorizations[domain]
if !ok {
http.Error(w, fmt.Sprintf("no authz for %q", domain), http.StatusNotFound)
return
}
if err := json.NewEncoder(w).Encode(authz); err != nil {
panic(fmt.Sprintf("get authz for %q response: %v", domain, err))
}
// Cert issuance request.
case r.URL.Path == "/new-cert":
var req struct {
CSR string `json:"csr"`
}
decodePayload(&req, r.Body)
b, _ := base64.RawURLEncoding.DecodeString(req.CSR)
csr, err := x509.ParseCertificateRequest(b)
if err != nil {
ca.addError(err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
names := unique(append(csr.DNSNames, csr.Subject.CommonName))
if err := ca.matchWhitelist(names); err != nil {
ca.addError(err)
http.Error(w, err.Error(), http.StatusUnauthorized)
return
}
if err := ca.authorized(names); err != nil {
ca.addError(err)
http.Error(w, err.Error(), http.StatusUnauthorized)
return
}
der, err := ca.leafCert(csr)
if err != nil {
err = fmt.Errorf("new-cert response: ca.leafCert: %v", err)
ca.addError(err)
http.Error(w, err.Error(), http.StatusBadRequest)
}
w.Header().Set("Link", fmt.Sprintf("<%s>; rel=up", ca.serverURL("/ca-cert")))
w.WriteHeader(http.StatusCreated)
w.Write(der)
// CA chain cert request.
case r.URL.Path == "/ca-cert":
w.Write(ca.rootCert)
}
}
func (ca *CAServer) addError(err error) {
ca.mu.Lock()
defer ca.mu.Unlock()
ca.errors = append(ca.errors, err)
}
func (ca *CAServer) serverURL(format string, arg ...interface{}) string {
return ca.server.URL + fmt.Sprintf(format, arg...)
}
func (ca *CAServer) matchWhitelist(dnsNames []string) error {
if len(ca.domainsWhitelist) == 0 {
return nil
}
var nomatch []string
for _, name := range dnsNames {
i := sort.SearchStrings(ca.domainsWhitelist, name)
if i == len(ca.domainsWhitelist) || ca.domainsWhitelist[i] != name {
nomatch = append(nomatch, name)
}
}
if len(nomatch) > 0 {
return fmt.Errorf("matchWhitelist: some domains don't match: %q", nomatch)
}
return nil
}
func (ca *CAServer) authorized(dnsNames []string) error {
ca.mu.Lock()
defer ca.mu.Unlock()
var noauthz []string
for _, name := range dnsNames {
authz, ok := ca.authorizations[name]
if !ok || authz.Status != "valid" {
noauthz = append(noauthz, name)
}
}
if len(noauthz) > 0 {
return fmt.Errorf("CAServer: no authz for %q", noauthz)
}
return nil
}
func (ca *CAServer) leafCert(csr *x509.CertificateRequest) (der []byte, err error) {
ca.mu.Lock()
defer ca.mu.Unlock()
ca.certCount++ // next leaf cert serial number
leaf := &x509.Certificate{
SerialNumber: big.NewInt(int64(ca.certCount)),
Subject: pkix.Name{Organization: []string{"Test Acme Co"}},
NotBefore: time.Now(),
NotAfter: time.Now().Add(90 * 24 * time.Hour),
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
DNSNames: csr.DNSNames,
BasicConstraintsValid: true,
}
if len(csr.DNSNames) == 0 {
leaf.DNSNames = []string{csr.Subject.CommonName}
}
return x509.CreateCertificate(rand.Reader, leaf, ca.rootTemplate, csr.PublicKey, ca.rootKey)
}
func (ca *CAServer) addr(domain string) (string, error) {
ca.mu.Lock()
defer ca.mu.Unlock()
addr, ok := ca.domainAddr[domain]
if !ok {
return "", fmt.Errorf("CAServer: no addr resolution for %q", domain)
}
return addr, nil
}
func (ca *CAServer) verifyALPNChallenge(domain string) error {
const acmeALPNProto = "acme-tls/1"
addr, err := ca.addr(domain)
if err != nil {
return err
}
conn, err := tls.Dial("tcp", addr, &tls.Config{
ServerName: domain,
InsecureSkipVerify: true,
NextProtos: []string{acmeALPNProto},
})
if err != nil {
return err
}
if v := conn.ConnectionState().NegotiatedProtocol; v != acmeALPNProto {
return fmt.Errorf("CAServer: verifyALPNChallenge: negotiated proto is %q; want %q", v, acmeALPNProto)
}
if n := len(conn.ConnectionState().PeerCertificates); n != 1 {
return fmt.Errorf("len(PeerCertificates) = %d; want 1", n)
}
// TODO: verify conn.ConnectionState().PeerCertificates[0]
return nil
}
func decodePayload(v interface{}, r io.Reader) error {
var req struct{ Payload string }
if err := json.NewDecoder(r).Decode(&req); err != nil {
return err
}
payload, err := base64.RawURLEncoding.DecodeString(req.Payload)
if err != nil {
return err
}
return json.Unmarshal(payload, v)
}
func challengeToken(domain, challType string) string {
return fmt.Sprintf("token-%s-%s", domain, challType)
}
func unique(a []string) []string {
seen := make(map[string]bool)
var res []string
for _, s := range a {
if s != "" && !seen[s] {
seen[s] = true
res = append(res, s)
}
}
return res
}

281
vendor/golang.org/x/crypto/acme/http.go generated vendored Normal file
View File

@@ -0,0 +1,281 @@
// Copyright 2018 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 (
"bytes"
"context"
"crypto"
"crypto/rand"
"encoding/json"
"fmt"
"io/ioutil"
"math/big"
"net/http"
"strconv"
"strings"
"time"
)
// retryTimer encapsulates common logic for retrying unsuccessful requests.
// It is not safe for concurrent use.
type retryTimer struct {
// backoffFn provides backoff delay sequence for retries.
// See Client.RetryBackoff doc comment.
backoffFn func(n int, r *http.Request, res *http.Response) time.Duration
// n is the current retry attempt.
n int
}
func (t *retryTimer) inc() {
t.n++
}
// backoff pauses the current goroutine as described in Client.RetryBackoff.
func (t *retryTimer) backoff(ctx context.Context, r *http.Request, res *http.Response) error {
d := t.backoffFn(t.n, r, res)
if d <= 0 {
return fmt.Errorf("acme: no more retries for %s; tried %d time(s)", r.URL, t.n)
}
wakeup := time.NewTimer(d)
defer wakeup.Stop()
select {
case <-ctx.Done():
return ctx.Err()
case <-wakeup.C:
return nil
}
}
func (c *Client) retryTimer() *retryTimer {
f := c.RetryBackoff
if f == nil {
f = defaultBackoff
}
return &retryTimer{backoffFn: f}
}
// defaultBackoff provides default Client.RetryBackoff implementation
// using a truncated exponential backoff algorithm,
// as described in Client.RetryBackoff.
//
// The n argument is always bounded between 1 and 30.
// The returned value is always greater than 0.
func defaultBackoff(n int, r *http.Request, res *http.Response) time.Duration {
const max = 10 * time.Second
var jitter time.Duration
if x, err := rand.Int(rand.Reader, big.NewInt(1000)); err == nil {
// Set the minimum to 1ms to avoid a case where
// an invalid Retry-After value is parsed into 0 below,
// resulting in the 0 returned value which would unintentionally
// stop the retries.
jitter = (1 + time.Duration(x.Int64())) * time.Millisecond
}
if v, ok := res.Header["Retry-After"]; ok {
return retryAfter(v[0]) + jitter
}
if n < 1 {
n = 1
}
if n > 30 {
n = 30
}
d := time.Duration(1<<uint(n-1))*time.Second + jitter
if d > max {
return max
}
return d
}
// retryAfter parses a Retry-After HTTP header value,
// trying to convert v into an int (seconds) or use http.ParseTime otherwise.
// It returns zero value if v cannot be parsed.
func retryAfter(v string) time.Duration {
if i, err := strconv.Atoi(v); err == nil {
return time.Duration(i) * time.Second
}
t, err := http.ParseTime(v)
if err != nil {
return 0
}
return t.Sub(timeNow())
}
// resOkay is a function that reports whether the provided response is okay.
// It is expected to keep the response body unread.
type resOkay func(*http.Response) bool
// wantStatus returns a function which reports whether the code
// matches the status code of a response.
func wantStatus(codes ...int) resOkay {
return func(res *http.Response) bool {
for _, code := range codes {
if code == res.StatusCode {
return true
}
}
return false
}
}
// get issues an unsigned GET request to the specified URL.
// It returns a non-error value only when ok reports true.
//
// get retries unsuccessful attempts according to c.RetryBackoff
// until the context is done or a non-retriable error is received.
func (c *Client) get(ctx context.Context, url string, ok resOkay) (*http.Response, error) {
retry := c.retryTimer()
for {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
res, err := c.doNoRetry(ctx, req)
switch {
case err != nil:
return nil, err
case ok(res):
return res, nil
case isRetriable(res.StatusCode):
retry.inc()
resErr := responseError(res)
res.Body.Close()
// Ignore the error value from retry.backoff
// and return the one from last retry, as received from the CA.
if retry.backoff(ctx, req, res) != nil {
return nil, resErr
}
default:
defer res.Body.Close()
return nil, responseError(res)
}
}
}
// post issues a signed POST request in JWS format using the provided key
// to the specified URL.
// It returns a non-error value only when ok reports true.
//
// post retries unsuccessful attempts according to c.RetryBackoff
// until the context is done or a non-retriable error is received.
// It uses postNoRetry to make individual requests.
func (c *Client) post(ctx context.Context, key crypto.Signer, url string, body interface{}, ok resOkay) (*http.Response, error) {
retry := c.retryTimer()
for {
res, req, err := c.postNoRetry(ctx, key, url, body)
if err != nil {
return nil, err
}
if ok(res) {
return res, nil
}
resErr := responseError(res)
res.Body.Close()
switch {
// Check for bad nonce before isRetriable because it may have been returned
// with an unretriable response code such as 400 Bad Request.
case isBadNonce(resErr):
// Consider any previously stored nonce values to be invalid.
c.clearNonces()
case !isRetriable(res.StatusCode):
return nil, resErr
}
retry.inc()
// Ignore the error value from retry.backoff
// and return the one from last retry, as received from the CA.
if err := retry.backoff(ctx, req, res); err != nil {
return nil, resErr
}
}
}
// postNoRetry signs the body with the given key and POSTs it to the provided url.
// The body argument must be JSON-serializable.
// It is used by c.post to retry unsuccessful attempts.
func (c *Client) postNoRetry(ctx context.Context, key crypto.Signer, url string, body interface{}) (*http.Response, *http.Request, error) {
nonce, err := c.popNonce(ctx, url)
if err != nil {
return nil, nil, err
}
b, err := jwsEncodeJSON(body, key, nonce)
if err != nil {
return nil, nil, err
}
req, err := http.NewRequest("POST", url, bytes.NewReader(b))
if err != nil {
return nil, nil, err
}
req.Header.Set("Content-Type", "application/jose+json")
res, err := c.doNoRetry(ctx, req)
if err != nil {
return nil, nil, err
}
c.addNonce(res.Header)
return res, req, nil
}
// doNoRetry issues a request req, replacing its context (if any) with ctx.
func (c *Client) doNoRetry(ctx context.Context, req *http.Request) (*http.Response, error) {
res, err := c.httpClient().Do(req.WithContext(ctx))
if err != nil {
select {
case <-ctx.Done():
// Prefer the unadorned context error.
// (The acme package had tests assuming this, previously from ctxhttp's
// behavior, predating net/http supporting contexts natively)
// TODO(bradfitz): reconsider this in the future. But for now this
// requires no test updates.
return nil, ctx.Err()
default:
return nil, err
}
}
return res, nil
}
func (c *Client) httpClient() *http.Client {
if c.HTTPClient != nil {
return c.HTTPClient
}
return http.DefaultClient
}
// isBadNonce reports whether err is an ACME "badnonce" error.
func isBadNonce(err error) bool {
// According to the spec badNonce is urn:ietf:params:acme:error:badNonce.
// However, ACME servers in the wild return their versions of the error.
// See https://tools.ietf.org/html/draft-ietf-acme-acme-02#section-5.4
// and https://github.com/letsencrypt/boulder/blob/0e07eacb/docs/acme-divergences.md#section-66.
ae, ok := err.(*Error)
return ok && strings.HasSuffix(strings.ToLower(ae.ProblemType), ":badnonce")
}
// isRetriable reports whether a request can be retried
// based on the response status code.
//
// Note that a "bad nonce" error is returned with a non-retriable 400 Bad Request code.
// Callers should parse the response and check with isBadNonce.
func isRetriable(code int) bool {
return code <= 399 || code >= 500 || code == http.StatusTooManyRequests
}
// responseError creates an error of Error type from resp.
func responseError(resp *http.Response) error {
// don't care if ReadAll returns an error:
// json.Unmarshal will fail in that case anyway
b, _ := ioutil.ReadAll(resp.Body)
e := &wireError{Status: resp.StatusCode}
if err := json.Unmarshal(b, e); err != nil {
// this is not a regular error response:
// populate detail with anything we received,
// e.Status will already contain HTTP response code value
e.Detail = string(b)
if e.Detail == "" {
e.Detail = resp.Status
}
}
return e.error(resp.Header)
}

213
vendor/golang.org/x/crypto/acme/http_test.go generated vendored Normal file
View File

@@ -0,0 +1,213 @@
// Copyright 2018 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 (
"context"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"reflect"
"strings"
"testing"
"time"
)
func TestDefaultBackoff(t *testing.T) {
tt := []struct {
nretry int
retryAfter string // Retry-After header
out time.Duration // expected min; max = min + jitter
}{
{-1, "", time.Second}, // verify the lower bound is 1
{0, "", time.Second}, // verify the lower bound is 1
{100, "", 10 * time.Second}, // verify the ceiling
{1, "3600", time.Hour}, // verify the header value is used
{1, "", 1 * time.Second},
{2, "", 2 * time.Second},
{3, "", 4 * time.Second},
{4, "", 8 * time.Second},
}
for i, test := range tt {
r := httptest.NewRequest("GET", "/", nil)
resp := &http.Response{Header: http.Header{}}
if test.retryAfter != "" {
resp.Header.Set("Retry-After", test.retryAfter)
}
d := defaultBackoff(test.nretry, r, resp)
max := test.out + time.Second // + max jitter
if d < test.out || max < d {
t.Errorf("%d: defaultBackoff(%v) = %v; want between %v and %v", i, test.nretry, d, test.out, max)
}
}
}
func TestErrorResponse(t *testing.T) {
s := `{
"status": 400,
"type": "urn:acme:error:xxx",
"detail": "text"
}`
res := &http.Response{
StatusCode: 400,
Status: "400 Bad Request",
Body: ioutil.NopCloser(strings.NewReader(s)),
Header: http.Header{"X-Foo": {"bar"}},
}
err := responseError(res)
v, ok := err.(*Error)
if !ok {
t.Fatalf("err = %+v (%T); want *Error type", err, err)
}
if v.StatusCode != 400 {
t.Errorf("v.StatusCode = %v; want 400", v.StatusCode)
}
if v.ProblemType != "urn:acme:error:xxx" {
t.Errorf("v.ProblemType = %q; want urn:acme:error:xxx", v.ProblemType)
}
if v.Detail != "text" {
t.Errorf("v.Detail = %q; want text", v.Detail)
}
if !reflect.DeepEqual(v.Header, res.Header) {
t.Errorf("v.Header = %+v; want %+v", v.Header, res.Header)
}
}
func TestPostWithRetries(t *testing.T) {
var count int
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
count++
w.Header().Set("Replay-Nonce", fmt.Sprintf("nonce%d", count))
if r.Method == "HEAD" {
// We expect the client to do 2 head requests to fetch
// nonces, one to start and another after getting badNonce
return
}
head, err := decodeJWSHead(r)
switch {
case err != nil:
t.Errorf("decodeJWSHead: %v", err)
case head.Nonce == "":
t.Error("head.Nonce is empty")
case head.Nonce == "nonce1":
// Return a badNonce error to force the call to retry.
w.Header().Set("Retry-After", "0")
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(`{"type":"urn:ietf:params:acme:error:badNonce"}`))
return
}
// Make client.Authorize happy; we're not testing its result.
w.WriteHeader(http.StatusCreated)
w.Write([]byte(`{"status":"valid"}`))
}))
defer ts.Close()
client := &Client{
Key: testKey,
DirectoryURL: ts.URL,
dir: &Directory{AuthzURL: ts.URL},
}
// This call will fail with badNonce, causing a retry
if _, err := client.Authorize(context.Background(), "example.com"); err != nil {
t.Errorf("client.Authorize 1: %v", err)
}
if count != 4 {
t.Errorf("total requests count: %d; want 4", count)
}
}
func TestRetryErrorType(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Replay-Nonce", "nonce")
w.WriteHeader(http.StatusTooManyRequests)
w.Write([]byte(`{"type":"rateLimited"}`))
}))
defer ts.Close()
client := &Client{
Key: testKey,
RetryBackoff: func(n int, r *http.Request, res *http.Response) time.Duration {
// Do no retries.
return 0
},
dir: &Directory{AuthzURL: ts.URL},
}
t.Run("post", func(t *testing.T) {
testRetryErrorType(t, func() error {
_, err := client.Authorize(context.Background(), "example.com")
return err
})
})
t.Run("get", func(t *testing.T) {
testRetryErrorType(t, func() error {
_, err := client.GetAuthorization(context.Background(), ts.URL)
return err
})
})
}
func testRetryErrorType(t *testing.T, callClient func() error) {
t.Helper()
err := callClient()
if err == nil {
t.Fatal("client.Authorize returned nil error")
}
acmeErr, ok := err.(*Error)
if !ok {
t.Fatalf("err is %v (%T); want *Error", err, err)
}
if acmeErr.StatusCode != http.StatusTooManyRequests {
t.Errorf("acmeErr.StatusCode = %d; want %d", acmeErr.StatusCode, http.StatusTooManyRequests)
}
if acmeErr.ProblemType != "rateLimited" {
t.Errorf("acmeErr.ProblemType = %q; want 'rateLimited'", acmeErr.ProblemType)
}
}
func TestRetryBackoffArgs(t *testing.T) {
const resCode = http.StatusInternalServerError
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Replay-Nonce", "test-nonce")
w.WriteHeader(resCode)
}))
defer ts.Close()
// Canceled in backoff.
ctx, cancel := context.WithCancel(context.Background())
var nretry int
backoff := func(n int, r *http.Request, res *http.Response) time.Duration {
nretry++
if n != nretry {
t.Errorf("n = %d; want %d", n, nretry)
}
if nretry == 3 {
cancel()
}
if r == nil {
t.Error("r is nil")
}
if res.StatusCode != resCode {
t.Errorf("res.StatusCode = %d; want %d", res.StatusCode, resCode)
}
return time.Millisecond
}
client := &Client{
Key: testKey,
RetryBackoff: backoff,
dir: &Directory{AuthzURL: ts.URL},
}
if _, err := client.Authorize(ctx, "example.com"); err == nil {
t.Error("err is nil")
}
if nretry != 3 {
t.Errorf("nretry = %d; want 3", nretry)
}
}