mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-10-31 17:22:26 -05:00 
			
		
		
		
	
		
			
				
	
	
		
			325 lines
		
	
	
	
		
			9.8 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			325 lines
		
	
	
	
		
			9.8 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"
 | |
| 	"errors"
 | |
| 	"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)
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // postAsGet is POST-as-GET, a replacement for GET in RFC8555
 | |
| // 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. It's set in version_go112.go.
 | |
| var packageVersion string
 | |
| 
 | |
| // 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, _ := 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)
 | |
| }
 |