| // 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 maxVal = 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 | |
| return min(d, maxVal) | |
| } | |
| // 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.NewRequestWithContext(ctx, "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.NewRequestWithContext(ctx, "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) | |
| } |