cert-manager/third_party/crypto/acme/acme.go

930 lines
26 KiB
Go

// Copyright 2015 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 provides an implementation of the
// Automatic Certificate Management Environment (ACME) spec.
// See https://tools.ietf.org/html/draft-ietf-acme-acme-09 for details.
//
// Most common scenarios will want to use autocert subdirectory instead,
// which provides automatic access to certificates from Let's Encrypt
// and any other ACME-based CA.
//
// This package is a work in progress and makes no API stability promises.
package acme
import (
"bytes"
"context"
"crypto"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"io"
"io/ioutil"
"math/big"
"net/http"
"net/url"
"strconv"
"sync"
"time"
)
// LetsEncryptURL is the Directory endpoint of Let's Encrypt CA.
const LetsEncryptURL = "https://acme-v02.api.letsencrypt.org/directory"
const (
// max length of a certificate chain
maxChainLen = 5
// max size of a certificate chain response, in bytes
maxChainSize = (1 << 20) * maxChainLen
// Max number of collected nonces kept in memory.
// Expect usual peak of 1 or 2.
maxNonces = 100
)
// Client is an ACME client.
// The only required field is Key. An example of creating a client with a new key
// is as follows:
//
// key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
// if err != nil {
// log.Fatal(err)
// }
// client := &Client{Key: key}
//
type Client struct {
// Key is the account key used to register with a CA and sign requests.
// Key.Public() must return a *rsa.PublicKey or *ecdsa.PublicKey.
Key crypto.Signer
// HTTPClient optionally specifies an HTTP client to use
// instead of http.DefaultClient.
HTTPClient *http.Client
// DirectoryURL points to the CA directory endpoint.
// If empty, LetsEncryptURL is used.
// Mutating this value after a successful call of Client's Discover method
// will have no effect.
DirectoryURL string
noncesMu sync.Mutex
nonces map[string]struct{} // nonces collected from previous responses
urlMu sync.Mutex // urlMu guards writes to dir and accountURL
dir *Directory // cached result of Client's Discover method
accountURL string
}
// Discover performs ACME server discovery using c.DirectoryURL.
//
// It caches successful result. So, subsequent calls will not result in
// a network round-trip. This also means mutating c.DirectoryURL after successful call
// of this method will have no effect.
func (c *Client) Discover(ctx context.Context) (Directory, error) {
c.urlMu.Lock()
defer c.urlMu.Unlock()
if c.dir != nil {
return *c.dir, nil
}
dirURL := c.DirectoryURL
if dirURL == "" {
dirURL = LetsEncryptURL
}
res, err := c.get(ctx, dirURL)
if err != nil {
return Directory{}, err
}
defer res.Body.Close()
c.addNonce(res.Header)
if res.StatusCode != http.StatusOK {
return Directory{}, responseError(res)
}
var v struct {
NewNonce string
NewAccount string
NewOrder string
NewAuthz string
RevokeCert string
KeyChange string
Meta struct {
TermsOfService string
Website string
CAAIdentities []string
ExternalAccountRequired bool
}
}
if err := json.NewDecoder(res.Body).Decode(&v); err != nil {
return Directory{}, err
}
c.dir = &Directory{
NewNonceURL: v.NewNonce,
NewAccountURL: v.NewAccount,
NewOrderURL: v.NewOrder,
NewAuthzURL: v.NewAuthz,
RevokeCertURL: v.RevokeCert,
KeyChangeURL: v.KeyChange,
Terms: v.Meta.TermsOfService,
Website: v.Meta.Website,
CAA: v.Meta.CAAIdentities,
ExternalAccountRequired: v.Meta.ExternalAccountRequired,
}
return *c.dir, nil
}
// CreateOrder creates a new certificate order. The input order argument is not
// modified and can be built using NewOrder.
func (c *Client) CreateOrder(ctx context.Context, order *Order) (*Order, error) {
if _, err := c.Discover(ctx); err != nil {
return nil, err
}
req := struct {
Identifiers []wireAuthzID `json:"identifiers"`
NotBefore string `json:"notBefore,omitempty"`
NotAfter string `json:"notAfter,omitempty"`
}{
Identifiers: make([]wireAuthzID, len(order.Identifiers)),
}
for i, id := range order.Identifiers {
req.Identifiers[i] = wireAuthzID{
Type: id.Type,
Value: id.Value,
}
}
if !order.NotBefore.IsZero() {
req.NotBefore = order.NotBefore.Format(time.RFC3339)
}
if !order.NotAfter.IsZero() {
req.NotAfter = order.NotAfter.Format(time.RFC3339)
}
res, err := c.postWithJWSAccount(ctx, c.dir.NewOrderURL, req)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusCreated {
return nil, responseError(res)
}
var v wireOrder
if err := json.NewDecoder(res.Body).Decode(&v); err != nil {
return nil, err
}
l, err := resolveLocation(c.dir.NewOrderURL, res.Header)
if err != nil {
return nil, err
}
o := v.order(l, "")
if o.Status == StatusInvalid {
return nil, OrderInvalidError{o}
}
return o, nil
}
// FinalizeOrder finalizes an order using the Certificate Signing Request csr
// encoded in DER format. If the order has not been fully authorized,
// an OrderPendingError will be returned.
//
// After requesting finalization, FinalizOrder polls the order using WaitOrder
// until it is finalized and then fetches the associated certificate and returns
// it.
//
// Callers are encouraged to parse the returned certificate chain to ensure it
// is valid and has the expected attributes.
func (c *Client) FinalizeOrder(ctx context.Context, finalizeURL string, csr []byte) (der [][]byte, err error) {
if _, err := c.Discover(ctx); err != nil {
return nil, err
}
req := struct {
CSR string `json:"csr"`
}{
CSR: base64.RawURLEncoding.EncodeToString(csr),
}
res, err := c.postWithJWSAccount(ctx, finalizeURL, req)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, responseError(res)
}
var v wireOrder
if err := json.NewDecoder(res.Body).Decode(&v); err != nil {
return nil, err
}
l, err := resolveLocation(finalizeURL, res.Header)
if err != nil {
return nil, err
}
o := v.order(l, res.Header.Get("Retry-After"))
if o.Status == StatusProcessing || o.Status == StatusPending {
o, err = c.WaitOrder(ctx, o.URL)
if err != nil {
return nil, err
}
}
if o.Status != StatusValid {
return nil, fmt.Errorf("acme: unexpected order status %q", o.Status)
}
return c.getCert(ctx, o.CertificateURL)
}
// GetOrder retrieves an order identified by url.
//
// If a caller needs to poll an order until its status is final,
// see the WaitOrder method.
func (c *Client) GetOrder(ctx context.Context, url string) (*Order, error) {
res, err := c.get(ctx, url)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
err = responseError(res)
return nil, err
}
var v wireOrder
if err := json.NewDecoder(res.Body).Decode(&v); err != nil {
return nil, err
}
return v.order(url, res.Header.Get("Retry-After")), nil
}
// WaitOrder waits for an order to transition from StatusProcessing to a final
// state (StatusValid/StatusInvalid), it retries the request until the order is
// final, ctx is cancelled by the caller, or an error response is received.
//
// It returns a non-nil Order only if its Status is StatusValid. In all other
// cases WaitOrder returns an error. If the Status is StatusInvalid, the
// returned error will be of type OrderInvalidError. If the status is
// StatusPending, the returned error will be of type OrderPendingError.
func (c *Client) WaitOrder(ctx context.Context, url string) (*Order, error) {
sleep := timeSleeper(ctx)
for {
o, err := c.GetOrder(ctx, url)
if err != nil {
return nil, err
}
switch o.Status {
case StatusValid:
return o, nil
case StatusInvalid:
return nil, OrderInvalidError{o}
case StatusPending:
return nil, OrderPendingError{o}
case StatusProcessing: // continue retry loop
default:
return nil, fmt.Errorf("acme: unexpected order status %q", o.Status)
}
if err := sleep(o.RetryAfter); err != nil {
return nil, err
}
}
}
// RevokeCert revokes a previously issued certificate cert, provided in DER
// format.
//
// If key is nil, the account must have been used to issue the certificate or
// have valid authorizations for all of the identifiers in the certificate. If
// key is provided, it must be the certificate's private key.
func (c *Client) RevokeCert(ctx context.Context, key crypto.Signer, cert []byte, reason CRLReasonCode) error {
if _, err := c.Discover(ctx); err != nil {
return err
}
body := &struct {
Cert string `json:"certificate"`
Reason int `json:"reason"`
}{
Cert: base64.RawURLEncoding.EncodeToString(cert),
Reason: int(reason),
}
var res *http.Response
var err error
if key == nil {
res, err = c.postWithJWSAccount(ctx, c.dir.RevokeCertURL, body)
} else {
res, err = c.postWithJWSKey(ctx, key, c.dir.RevokeCertURL, body)
}
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return responseError(res)
}
return nil
}
// CreateAccount creates a new account. It returns the account details from the
// server and does not modify the account argument that it is called with.
func (c *Client) CreateAccount(ctx context.Context, a *Account) (*Account, error) {
if _, err := c.Discover(ctx); err != nil {
return nil, err
}
return c.doAccount(ctx, c.dir.NewAccountURL, false, a)
}
// GetAccount retrieves the account that the client is configured with.
func (c *Client) GetAccount(ctx context.Context) (*Account, error) {
if _, err := c.Discover(ctx); err != nil {
return nil, err
}
return c.doAccount(ctx, c.dir.NewAccountURL, true, nil)
}
// UpdateAccount updates an existing account. It returns an updated account
// copy. The provided account is not modified.
func (c *Client) UpdateAccount(ctx context.Context, a *Account) (*Account, error) {
return c.doAccount(ctx, a.URL, false, a)
}
// GetAuthorization retrieves an authorization identified by the given URL.
//
// If a caller needs to poll an authorization until its status is final,
// see the WaitAuthorization method.
func (c *Client) GetAuthorization(ctx context.Context, url string) (*Authorization, error) {
res, err := c.get(ctx, url)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, responseError(res)
}
var v wireAuthz
if err := json.NewDecoder(res.Body).Decode(&v); err != nil {
return nil, fmt.Errorf("acme: invalid response: %v", err)
}
return v.authorization(url), nil
}
// DeactivateAuthorization relinquishes an existing authorization identified by
// the given URL.
//
// If successful, the caller will be required to obtain a new authorization
// before a new certificate for the domain associated with the authorization is
// issued.
//
// It does not revoke existing certificates.
func (c *Client) DeactivateAuthorization(ctx context.Context, url string) error {
res, err := c.postWithJWSAccount(ctx, url, json.RawMessage(`{"status":"deactivated"}`))
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return responseError(res)
}
return nil
}
// WaitAuthorization retrieves authorization details. If the authorization is not in
// a final state (StatusValid/StatusInvalid), it retries the request until the authorization
// is final, ctx is cancelled by the caller, an error response is received, or the ACME CA
// responded with a 4xx error.
//
// It returns a non-nil Authorization only if its Status is StatusValid.
// In all other cases WaitAuthorization returns an error.
// If the Status is StatusInvalid or StatusDeactivated, the returned error will be of type AuthorizationError.
func (c *Client) WaitAuthorization(ctx context.Context, url string) (*Authorization, error) {
sleep := sleeper(ctx)
for {
res, err := c.get(ctx, url)
if err != nil {
return nil, err
}
if res.StatusCode != http.StatusOK {
err = responseError(res)
res.Body.Close()
return nil, err
}
if res.StatusCode >= 400 && res.StatusCode <= 499 {
// Non-retriable error. For instance, Let's Encrypt may return 404 Not Found
// when requesting an expired authorization.
defer res.Body.Close()
return nil, responseError(res)
}
var raw wireAuthz
err = json.NewDecoder(res.Body).Decode(&raw)
res.Body.Close()
if err != nil {
return nil, err
}
switch raw.Status {
case StatusValid:
return raw.authorization(url), nil
case StatusInvalid, StatusDeactivated:
return nil, AuthorizationError{raw.authorization(url)}
case StatusPending, StatusProcessing: // fall through to sleep
default:
return nil, fmt.Errorf("acme: unknown authorization status %q", raw.Status)
}
if err := sleep(res.Header.Get("Retry-After")); err != nil {
return nil, err
}
}
}
// GetChallenge retrieves the current status of a challenge.
//
// A client typically polls a challenge status using this method.
func (c *Client) GetChallenge(ctx context.Context, url string) (*Challenge, error) {
res, err := c.get(ctx, url)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, responseError(res)
}
v := wireChallenge{URL: url}
if err := json.NewDecoder(res.Body).Decode(&v); err != nil {
return nil, fmt.Errorf("acme: invalid response: %v", err)
}
return v.challenge(), nil
}
// AcceptChallenge informs the server that the client accepts one of its
// authorization challenges previously obtained with
// CreateOrder/GetAuthorization.
//
// The server will then perform the validation asynchronously.
func (c *Client) AcceptChallenge(ctx context.Context, chal *Challenge) (*Challenge, error) {
if _, err := c.Discover(ctx); err != nil {
return nil, err
}
auth, err := keyAuth(c.Key.Public(), chal.Token)
if err != nil {
return nil, err
}
req := struct {
Auth string `json:"keyAuthorization"`
}{auth}
res, err := c.postWithJWSAccount(ctx, chal.URL, req)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, responseError(res)
}
var v wireChallenge
if err := json.NewDecoder(res.Body).Decode(&v); err != nil {
return nil, fmt.Errorf("acme: invalid response: %v", err)
}
return v.challenge(), nil
}
// DNS01ChallengeRecord returns a DNS record value for a dns-01 challenge response.
// A TXT record containing the returned value must be provisioned under
// "_acme-challenge" name of the domain being validated.
//
// The token argument is a Challenge.Token value.
func (c *Client) DNS01ChallengeRecord(token string) (string, error) {
ka, err := keyAuth(c.Key.Public(), token)
if err != nil {
return "", err
}
b := sha256.Sum256([]byte(ka))
return base64.RawURLEncoding.EncodeToString(b[:]), nil
}
// HTTP01ChallengeResponse returns the response for an http-01 challenge.
// Servers should respond with the value to HTTP requests at the URL path
// provided by HTTP01ChallengePath to validate the challenge and prove control
// over a domain name.
//
// The token argument is a Challenge.Token value.
func (c *Client) HTTP01ChallengeResponse(token string) (string, error) {
return keyAuth(c.Key.Public(), token)
}
// HTTP01ChallengePath returns the URL path at which the response for an http-01 challenge
// should be provided by the servers.
// The response value can be obtained with HTTP01ChallengeResponse.
//
// The token argument is a Challenge.Token value.
func (c *Client) HTTP01ChallengePath(token string) string {
return "/.well-known/acme-challenge/" + token
}
// doAccount creates, updates, and reads accounts.
//
// A non-nil acct argument indicates whether the intention is to mutate data of
// the Account. Only the Contact field can be updated.
func (c *Client) doAccount(ctx context.Context, url string, getExistingWithKey bool, acct *Account) (*Account, error) {
req := struct {
Contact []string `json:"contact,omitempty"`
TermsAgreed bool `json:"termsOfServiceAgreed,omitempty"`
GetExisting bool `json:"onlyReturnExisting,omitempty"`
}{
GetExisting: getExistingWithKey,
}
var accountURL string
if url != c.dir.NewAccountURL {
accountURL = url
}
if acct != nil {
req.Contact = acct.Contact
req.TermsAgreed = acct.TermsAgreed
}
res, err := c.retryPostJWS(ctx, c.Key, accountURL, url, req)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode < 200 || res.StatusCode > 299 {
return nil, responseError(res)
}
if getExistingWithKey {
l, err := resolveLocation(url, res.Header)
if err != nil {
return nil, err
}
return c.doAccount(ctx, l, false, nil)
}
var v struct {
Status string
Contact []string
Orders string
}
if err := json.NewDecoder(res.Body).Decode(&v); err != nil {
return nil, fmt.Errorf("acme: invalid response: %v", err)
}
l, err := resolveLocation(url, res.Header)
if err != nil {
return nil, err
}
a := &Account{
URL: l,
Status: v.Status,
Contact: v.Contact,
OrdersURL: v.Orders,
}
if a.URL == "" {
a.URL = url
}
c.urlMu.Lock()
defer c.urlMu.Unlock()
c.accountURL = a.URL
return a, nil
}
// cacheAccount ensures that the account URL is cached and returns it.
func (c *Client) cacheAccountURL(ctx context.Context) (string, error) {
c.urlMu.Lock()
defer c.urlMu.Unlock()
if c.accountURL != "" {
return c.accountURL, nil
}
res, err := c.postWithJWSKey(ctx, c.Key, c.dir.NewAccountURL, json.RawMessage(`{"onlyReturnExisting":true}`))
if err != nil {
return "", err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return "", responseError(res)
}
l, err := resolveLocation(c.dir.NewAccountURL, res.Header)
if err != nil {
return "", err
}
c.accountURL = l
return c.accountURL, nil
}
func (c *Client) postWithJWSKey(ctx context.Context, key crypto.Signer, url string, body interface{}) (*http.Response, error) {
return c.retryPostJWS(ctx, key, "", url, body)
}
func (c *Client) postWithJWSAccount(ctx context.Context, url string, body interface{}) (*http.Response, error) {
accountURL, err := c.cacheAccountURL(ctx)
if err != nil {
return nil, err
}
return c.retryPostJWS(ctx, c.Key, accountURL, url, body)
}
// retryPostJWS will retry calls to postJWS if there is a badNonce error,
// clearing the stored nonces after each error.
// If the response was 4XX-5XX, then responseError is called on the body,
// the body is closed, and the error returned.
func (c *Client) retryPostJWS(ctx context.Context, key crypto.Signer, accountURL, url string, body interface{}) (*http.Response, error) {
sleep := sleeper(ctx)
for {
res, err := c.postJWS(ctx, key, accountURL, url, body)
if err != nil {
return nil, err
}
// handle errors 4XX-5XX with responseError
if res.StatusCode >= 400 && res.StatusCode <= 599 {
err := responseError(res)
res.Body.Close()
if ae, ok := err.(*Error); ok && ae.Type == "urn:ietf:params:acme:error:badNonce" {
// clear any nonces that we might've stored that might now be
// considered bad
c.clearNonces()
retry := res.Header.Get("Retry-After")
if err := sleep(retry); err != nil {
return nil, err
}
continue
}
return nil, err
}
return res, nil
}
}
// postJWS signs the body with the given key and POSTs it to the provided url.
// The body argument must be JSON-serializable.
// The accountURL should be empty for account creation and certificate revocation.
func (c *Client) postJWS(ctx context.Context, key crypto.Signer, accountURL, url string, body interface{}) (*http.Response, error) {
nonce, err := c.popNonce(ctx)
if err != nil {
return nil, err
}
b, err := jwsEncodeJSON(body, key, accountURL, url, nonce)
if err != nil {
return nil, err
}
res, err := c.post(ctx, url, "application/jose+json", bytes.NewReader(b))
if err != nil {
return nil, err
}
c.addNonce(res.Header)
return res, nil
}
// popNonce returns a nonce value previously stored with c.addNonce
// or fetches a fresh one.
func (c *Client) popNonce(ctx context.Context) (string, error) {
c.noncesMu.Lock()
defer c.noncesMu.Unlock()
if len(c.nonces) == 0 {
return c.fetchNonce(ctx)
}
var nonce string
for nonce = range c.nonces {
delete(c.nonces, nonce)
break
}
return nonce, nil
}
// clearNonces clears any stored nonces
func (c *Client) clearNonces() {
c.noncesMu.Lock()
defer c.noncesMu.Unlock()
c.nonces = make(map[string]struct{})
}
// addNonce stores a nonce value found in h (if any) for future use.
func (c *Client) addNonce(h http.Header) {
v := nonceFromHeader(h)
if v == "" {
return
}
c.noncesMu.Lock()
defer c.noncesMu.Unlock()
if len(c.nonces) >= maxNonces {
return
}
if c.nonces == nil {
c.nonces = make(map[string]struct{})
}
c.nonces[v] = struct{}{}
}
func (c *Client) httpClient() *http.Client {
if c.HTTPClient != nil {
return c.HTTPClient
}
return http.DefaultClient
}
func (c *Client) get(ctx context.Context, urlStr string) (*http.Response, error) {
req, err := http.NewRequest("GET", urlStr, nil)
if err != nil {
return nil, err
}
return c.do(ctx, req)
}
func (c *Client) head(ctx context.Context, urlStr string) (*http.Response, error) {
req, err := http.NewRequest("HEAD", urlStr, nil)
if err != nil {
return nil, err
}
return c.do(ctx, req)
}
func (c *Client) post(ctx context.Context, urlStr, contentType string, body io.Reader) (*http.Response, error) {
req, err := http.NewRequest("POST", urlStr, body)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", contentType)
return c.do(ctx, req)
}
func (c *Client) do(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) fetchNonce(ctx context.Context) (string, error) {
resp, err := c.head(ctx, c.dir.NewNonceURL)
if err != nil {
return "", err
}
defer resp.Body.Close()
nonce := nonceFromHeader(resp.Header)
if nonce == "" {
if resp.StatusCode > 299 {
return "", responseError(resp)
}
return "", errors.New("acme: nonce not found")
}
return nonce, nil
}
func nonceFromHeader(h http.Header) string {
return h.Get("Replay-Nonce")
}
func (c *Client) getCert(ctx context.Context, url string) ([][]byte, error) {
res, err := c.get(ctx, url)
if err != nil {
return nil, err
}
defer res.Body.Close()
data, err := ioutil.ReadAll(io.LimitReader(res.Body, maxChainSize+1))
if err != nil {
return nil, fmt.Errorf("acme: error getting certificate: %v", err)
}
if len(data) > maxChainSize {
return nil, errors.New("acme: certificate chain is too big")
}
var chain [][]byte
for {
var p *pem.Block
p, data = pem.Decode(data)
if p == nil {
if len(chain) == 0 {
return nil, errors.New("acme: invalid PEM certificate chain")
}
break
}
if len(chain) == maxChainLen {
return nil, errors.New("acme: certificate chain is too long")
}
if p.Type != "CERTIFICATE" {
return nil, fmt.Errorf("acme: invalid PEM block type %q", p.Type)
}
chain = append(chain, p.Bytes)
}
return chain, nil
}
// 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)
}
// sleeper returns a function that accepts the Retry-After HTTP header value
// and an increment that's used with backoff to increasingly sleep on
// consecutive calls until the context is done. If the Retry-After header
// cannot be parsed, then backoff is used with a maximum sleep time of 10
// seconds.
func sleeper(ctx context.Context) func(ra string) error {
sleep := timeSleeper(ctx)
return func(ra string) error {
return sleep(retryAfter(ra))
}
}
func timeSleeper(ctx context.Context) func(time.Time) error {
var count int
return func(t time.Time) error {
d := backoff(count, 10*time.Second)
count++
if !t.IsZero() {
d = t.Sub(timeNow())
}
wakeup := time.NewTimer(d)
defer wakeup.Stop()
select {
case <-ctx.Done():
return ctx.Err()
case <-wakeup.C:
return nil
}
}
}
// retryAfter parses a Retry-After HTTP header value,
// trying to convert v into an int (seconds) or use http.ParseTime otherwise.
func retryAfter(v string) time.Time {
if i, err := strconv.Atoi(v); err == nil {
return timeNow().Add(time.Duration(i) * time.Second)
}
t, err := http.ParseTime(v)
if err != nil {
return time.Time{}
}
return t
}
// backoff computes a duration after which an n+1 retry iteration should occur
// using truncated exponential backoff algorithm.
//
// The n argument is always bounded between 0 and 30.
// The max argument defines upper bound for the returned value.
func backoff(n int, max time.Duration) time.Duration {
if n < 0 {
n = 0
}
if n > 30 {
n = 30
}
var d time.Duration
if x, err := rand.Int(rand.Reader, big.NewInt(1000)); err == nil {
d = time.Duration(x.Int64()) * time.Millisecond
}
d += time.Duration(1<<uint(n)) * time.Second
if d > max {
return max
}
return d
}
// keyAuth generates a key authorization string for a given token.
func keyAuth(pub crypto.PublicKey, token string) (string, error) {
th, err := JWKThumbprint(pub)
if err != nil {
return "", err
}
return fmt.Sprintf("%s.%s", token, th), nil
}
func resolveLocation(base string, h http.Header) (string, error) {
u, err := url.Parse(base)
if err != nil {
return "", err
}
u, err = u.Parse(h.Get("Location"))
if err != nil {
return "", fmt.Errorf("acme: error parsing Location: %s", err)
}
return u.String(), nil
}
// timeNow is useful for testing for fixed current time.
var timeNow = time.Now