282 lines
		
	
	
		
			8.1 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
		
		
			
		
	
	
			282 lines
		
	
	
		
			8.1 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
|  | // 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) | ||
|  | } |