...

Source file src/golang.org/x/crypto/acme/http.go

Documentation: golang.org/x/crypto/acme

     1  // Copyright 2018 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package acme
     6  
     7  import (
     8  	"bytes"
     9  	"context"
    10  	"crypto"
    11  	"crypto/rand"
    12  	"encoding/json"
    13  	"errors"
    14  	"fmt"
    15  	"io"
    16  	"math/big"
    17  	"net/http"
    18  	"runtime/debug"
    19  	"strconv"
    20  	"strings"
    21  	"time"
    22  )
    23  
    24  // retryTimer encapsulates common logic for retrying unsuccessful requests.
    25  // It is not safe for concurrent use.
    26  type retryTimer struct {
    27  	// backoffFn provides backoff delay sequence for retries.
    28  	// See Client.RetryBackoff doc comment.
    29  	backoffFn func(n int, r *http.Request, res *http.Response) time.Duration
    30  	// n is the current retry attempt.
    31  	n int
    32  }
    33  
    34  func (t *retryTimer) inc() {
    35  	t.n++
    36  }
    37  
    38  // backoff pauses the current goroutine as described in Client.RetryBackoff.
    39  func (t *retryTimer) backoff(ctx context.Context, r *http.Request, res *http.Response) error {
    40  	d := t.backoffFn(t.n, r, res)
    41  	if d <= 0 {
    42  		return fmt.Errorf("acme: no more retries for %s; tried %d time(s)", r.URL, t.n)
    43  	}
    44  	wakeup := time.NewTimer(d)
    45  	defer wakeup.Stop()
    46  	select {
    47  	case <-ctx.Done():
    48  		return ctx.Err()
    49  	case <-wakeup.C:
    50  		return nil
    51  	}
    52  }
    53  
    54  func (c *Client) retryTimer() *retryTimer {
    55  	f := c.RetryBackoff
    56  	if f == nil {
    57  		f = defaultBackoff
    58  	}
    59  	return &retryTimer{backoffFn: f}
    60  }
    61  
    62  // defaultBackoff provides default Client.RetryBackoff implementation
    63  // using a truncated exponential backoff algorithm,
    64  // as described in Client.RetryBackoff.
    65  //
    66  // The n argument is always bounded between 1 and 30.
    67  // The returned value is always greater than 0.
    68  func defaultBackoff(n int, r *http.Request, res *http.Response) time.Duration {
    69  	const max = 10 * time.Second
    70  	var jitter time.Duration
    71  	if x, err := rand.Int(rand.Reader, big.NewInt(1000)); err == nil {
    72  		// Set the minimum to 1ms to avoid a case where
    73  		// an invalid Retry-After value is parsed into 0 below,
    74  		// resulting in the 0 returned value which would unintentionally
    75  		// stop the retries.
    76  		jitter = (1 + time.Duration(x.Int64())) * time.Millisecond
    77  	}
    78  	if v, ok := res.Header["Retry-After"]; ok {
    79  		return retryAfter(v[0]) + jitter
    80  	}
    81  
    82  	if n < 1 {
    83  		n = 1
    84  	}
    85  	if n > 30 {
    86  		n = 30
    87  	}
    88  	d := time.Duration(1<<uint(n-1))*time.Second + jitter
    89  	if d > max {
    90  		return max
    91  	}
    92  	return d
    93  }
    94  
    95  // retryAfter parses a Retry-After HTTP header value,
    96  // trying to convert v into an int (seconds) or use http.ParseTime otherwise.
    97  // It returns zero value if v cannot be parsed.
    98  func retryAfter(v string) time.Duration {
    99  	if i, err := strconv.Atoi(v); err == nil {
   100  		return time.Duration(i) * time.Second
   101  	}
   102  	t, err := http.ParseTime(v)
   103  	if err != nil {
   104  		return 0
   105  	}
   106  	return t.Sub(timeNow())
   107  }
   108  
   109  // resOkay is a function that reports whether the provided response is okay.
   110  // It is expected to keep the response body unread.
   111  type resOkay func(*http.Response) bool
   112  
   113  // wantStatus returns a function which reports whether the code
   114  // matches the status code of a response.
   115  func wantStatus(codes ...int) resOkay {
   116  	return func(res *http.Response) bool {
   117  		for _, code := range codes {
   118  			if code == res.StatusCode {
   119  				return true
   120  			}
   121  		}
   122  		return false
   123  	}
   124  }
   125  
   126  // get issues an unsigned GET request to the specified URL.
   127  // It returns a non-error value only when ok reports true.
   128  //
   129  // get retries unsuccessful attempts according to c.RetryBackoff
   130  // until the context is done or a non-retriable error is received.
   131  func (c *Client) get(ctx context.Context, url string, ok resOkay) (*http.Response, error) {
   132  	retry := c.retryTimer()
   133  	for {
   134  		req, err := http.NewRequest("GET", url, nil)
   135  		if err != nil {
   136  			return nil, err
   137  		}
   138  		res, err := c.doNoRetry(ctx, req)
   139  		switch {
   140  		case err != nil:
   141  			return nil, err
   142  		case ok(res):
   143  			return res, nil
   144  		case isRetriable(res.StatusCode):
   145  			retry.inc()
   146  			resErr := responseError(res)
   147  			res.Body.Close()
   148  			// Ignore the error value from retry.backoff
   149  			// and return the one from last retry, as received from the CA.
   150  			if retry.backoff(ctx, req, res) != nil {
   151  				return nil, resErr
   152  			}
   153  		default:
   154  			defer res.Body.Close()
   155  			return nil, responseError(res)
   156  		}
   157  	}
   158  }
   159  
   160  // postAsGet is POST-as-GET, a replacement for GET in RFC 8555
   161  // as described in https://tools.ietf.org/html/rfc8555#section-6.3.
   162  // It makes a POST request in KID form with zero JWS payload.
   163  // See nopayload doc comments in jws.go.
   164  func (c *Client) postAsGet(ctx context.Context, url string, ok resOkay) (*http.Response, error) {
   165  	return c.post(ctx, nil, url, noPayload, ok)
   166  }
   167  
   168  // post issues a signed POST request in JWS format using the provided key
   169  // to the specified URL. If key is nil, c.Key is used instead.
   170  // It returns a non-error value only when ok reports true.
   171  //
   172  // post retries unsuccessful attempts according to c.RetryBackoff
   173  // until the context is done or a non-retriable error is received.
   174  // It uses postNoRetry to make individual requests.
   175  func (c *Client) post(ctx context.Context, key crypto.Signer, url string, body interface{}, ok resOkay) (*http.Response, error) {
   176  	retry := c.retryTimer()
   177  	for {
   178  		res, req, err := c.postNoRetry(ctx, key, url, body)
   179  		if err != nil {
   180  			return nil, err
   181  		}
   182  		if ok(res) {
   183  			return res, nil
   184  		}
   185  		resErr := responseError(res)
   186  		res.Body.Close()
   187  		switch {
   188  		// Check for bad nonce before isRetriable because it may have been returned
   189  		// with an unretriable response code such as 400 Bad Request.
   190  		case isBadNonce(resErr):
   191  			// Consider any previously stored nonce values to be invalid.
   192  			c.clearNonces()
   193  		case !isRetriable(res.StatusCode):
   194  			return nil, resErr
   195  		}
   196  		retry.inc()
   197  		// Ignore the error value from retry.backoff
   198  		// and return the one from last retry, as received from the CA.
   199  		if err := retry.backoff(ctx, req, res); err != nil {
   200  			return nil, resErr
   201  		}
   202  	}
   203  }
   204  
   205  // postNoRetry signs the body with the given key and POSTs it to the provided url.
   206  // It is used by c.post to retry unsuccessful attempts.
   207  // The body argument must be JSON-serializable.
   208  //
   209  // If key argument is nil, c.Key is used to sign the request.
   210  // If key argument is nil and c.accountKID returns a non-zero keyID,
   211  // the request is sent in KID form. Otherwise, JWK form is used.
   212  //
   213  // In practice, when interfacing with RFC-compliant CAs most requests are sent in KID form
   214  // and JWK is used only when KID is unavailable: new account endpoint and certificate
   215  // revocation requests authenticated by a cert key.
   216  // See jwsEncodeJSON for other details.
   217  func (c *Client) postNoRetry(ctx context.Context, key crypto.Signer, url string, body interface{}) (*http.Response, *http.Request, error) {
   218  	kid := noKeyID
   219  	if key == nil {
   220  		if c.Key == nil {
   221  			return nil, nil, errors.New("acme: Client.Key must be populated to make POST requests")
   222  		}
   223  		key = c.Key
   224  		kid = c.accountKID(ctx)
   225  	}
   226  	nonce, err := c.popNonce(ctx, url)
   227  	if err != nil {
   228  		return nil, nil, err
   229  	}
   230  	b, err := jwsEncodeJSON(body, key, kid, nonce, url)
   231  	if err != nil {
   232  		return nil, nil, err
   233  	}
   234  	req, err := http.NewRequest("POST", url, bytes.NewReader(b))
   235  	if err != nil {
   236  		return nil, nil, err
   237  	}
   238  	req.Header.Set("Content-Type", "application/jose+json")
   239  	res, err := c.doNoRetry(ctx, req)
   240  	if err != nil {
   241  		return nil, nil, err
   242  	}
   243  	c.addNonce(res.Header)
   244  	return res, req, nil
   245  }
   246  
   247  // doNoRetry issues a request req, replacing its context (if any) with ctx.
   248  func (c *Client) doNoRetry(ctx context.Context, req *http.Request) (*http.Response, error) {
   249  	req.Header.Set("User-Agent", c.userAgent())
   250  	res, err := c.httpClient().Do(req.WithContext(ctx))
   251  	if err != nil {
   252  		select {
   253  		case <-ctx.Done():
   254  			// Prefer the unadorned context error.
   255  			// (The acme package had tests assuming this, previously from ctxhttp's
   256  			// behavior, predating net/http supporting contexts natively)
   257  			// TODO(bradfitz): reconsider this in the future. But for now this
   258  			// requires no test updates.
   259  			return nil, ctx.Err()
   260  		default:
   261  			return nil, err
   262  		}
   263  	}
   264  	return res, nil
   265  }
   266  
   267  func (c *Client) httpClient() *http.Client {
   268  	if c.HTTPClient != nil {
   269  		return c.HTTPClient
   270  	}
   271  	return http.DefaultClient
   272  }
   273  
   274  // packageVersion is the version of the module that contains this package, for
   275  // sending as part of the User-Agent header.
   276  var packageVersion string
   277  
   278  func init() {
   279  	// Set packageVersion if the binary was built in modules mode and x/crypto
   280  	// was not replaced with a different module.
   281  	info, ok := debug.ReadBuildInfo()
   282  	if !ok {
   283  		return
   284  	}
   285  	for _, m := range info.Deps {
   286  		if m.Path != "golang.org/x/crypto" {
   287  			continue
   288  		}
   289  		if m.Replace == nil {
   290  			packageVersion = m.Version
   291  		}
   292  		break
   293  	}
   294  }
   295  
   296  // userAgent returns the User-Agent header value. It includes the package name,
   297  // the module version (if available), and the c.UserAgent value (if set).
   298  func (c *Client) userAgent() string {
   299  	ua := "golang.org/x/crypto/acme"
   300  	if packageVersion != "" {
   301  		ua += "@" + packageVersion
   302  	}
   303  	if c.UserAgent != "" {
   304  		ua = c.UserAgent + " " + ua
   305  	}
   306  	return ua
   307  }
   308  
   309  // isBadNonce reports whether err is an ACME "badnonce" error.
   310  func isBadNonce(err error) bool {
   311  	// According to the spec badNonce is urn:ietf:params:acme:error:badNonce.
   312  	// However, ACME servers in the wild return their versions of the error.
   313  	// See https://tools.ietf.org/html/draft-ietf-acme-acme-02#section-5.4
   314  	// and https://github.com/letsencrypt/boulder/blob/0e07eacb/docs/acme-divergences.md#section-66.
   315  	ae, ok := err.(*Error)
   316  	return ok && strings.HasSuffix(strings.ToLower(ae.ProblemType), ":badnonce")
   317  }
   318  
   319  // isRetriable reports whether a request can be retried
   320  // based on the response status code.
   321  //
   322  // Note that a "bad nonce" error is returned with a non-retriable 400 Bad Request code.
   323  // Callers should parse the response and check with isBadNonce.
   324  func isRetriable(code int) bool {
   325  	return code <= 399 || code >= 500 || code == http.StatusTooManyRequests
   326  }
   327  
   328  // responseError creates an error of Error type from resp.
   329  func responseError(resp *http.Response) error {
   330  	// don't care if ReadAll returns an error:
   331  	// json.Unmarshal will fail in that case anyway
   332  	b, _ := io.ReadAll(resp.Body)
   333  	e := &wireError{Status: resp.StatusCode}
   334  	if err := json.Unmarshal(b, e); err != nil {
   335  		// this is not a regular error response:
   336  		// populate detail with anything we received,
   337  		// e.Status will already contain HTTP response code value
   338  		e.Detail = string(b)
   339  		if e.Detail == "" {
   340  			e.Detail = resp.Status
   341  		}
   342  	}
   343  	return e.error(resp.Header)
   344  }
   345  

View as plain text