// 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" "errors" "fmt" "io" "math/big" "net/http" "runtime/debug" "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) } } } // postAsGet is POST-as-GET, a replacement for GET in RFC 8555 // as described in https://tools.ietf.org/html/rfc8555#section-6.3. // It makes a POST request in KID form with zero JWS payload. // See nopayload doc comments in jws.go. func (c *Client) postAsGet(ctx context.Context, url string, ok resOkay) (*http.Response, error) { return c.post(ctx, nil, url, noPayload, ok) } // post issues a signed POST request in JWS format using the provided key // to the specified URL. If key is nil, c.Key is used instead. // 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. // It is used by c.post to retry unsuccessful attempts. // The body argument must be JSON-serializable. // // If key argument is nil, c.Key is used to sign the request. // If key argument is nil and c.accountKID returns a non-zero keyID, // the request is sent in KID form. Otherwise, JWK form is used. // // In practice, when interfacing with RFC-compliant CAs most requests are sent in KID form // and JWK is used only when KID is unavailable: new account endpoint and certificate // revocation requests authenticated by a cert key. // See jwsEncodeJSON for other details. func (c *Client) postNoRetry(ctx context.Context, key crypto.Signer, url string, body interface{}) (*http.Response, *http.Request, error) { kid := noKeyID if key == nil { if c.Key == nil { return nil, nil, errors.New("acme: Client.Key must be populated to make POST requests") } key = c.Key kid = c.accountKID(ctx) } nonce, err := c.popNonce(ctx, url) if err != nil { return nil, nil, err } b, err := jwsEncodeJSON(body, key, kid, nonce, url) 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) { req.Header.Set("User-Agent", c.userAgent()) 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 } // packageVersion is the version of the module that contains this package, for // sending as part of the User-Agent header. var packageVersion string func init() { // Set packageVersion if the binary was built in modules mode and x/crypto // was not replaced with a different module. info, ok := debug.ReadBuildInfo() if !ok { return } for _, m := range info.Deps { if m.Path != "golang.org/x/crypto" { continue } if m.Replace == nil { packageVersion = m.Version } break } } // userAgent returns the User-Agent header value. It includes the package name, // the module version (if available), and the c.UserAgent value (if set). func (c *Client) userAgent() string { ua := "golang.org/x/crypto/acme" if packageVersion != "" { ua += "@" + packageVersion } if c.UserAgent != "" { ua = c.UserAgent + " " + ua } return ua } // 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, _ := io.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) }