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