mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 19:45:25 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			395 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Go
		
	
	
	
		
			Vendored
		
	
	
	
			
		
		
	
	
			395 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Go
		
	
	
	
		
			Vendored
		
	
	
	
| // Copyright 2020 Matthew Holt
 | |
| //
 | |
| // Licensed under the Apache License, Version 2.0 (the "License");
 | |
| // you may not use this file except in compliance with the License.
 | |
| // You may obtain a copy of the License at
 | |
| //
 | |
| //     http://www.apache.org/licenses/LICENSE-2.0
 | |
| //
 | |
| // Unless required by applicable law or agreed to in writing, software
 | |
| // distributed under the License is distributed on an "AS IS" BASIS,
 | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | |
| // See the License for the specific language governing permissions and
 | |
| // limitations under the License.
 | |
| 
 | |
| package acme
 | |
| 
 | |
| import (
 | |
| 	"bytes"
 | |
| 	"context"
 | |
| 	"crypto"
 | |
| 	"encoding/json"
 | |
| 	"errors"
 | |
| 	"fmt"
 | |
| 	"io"
 | |
| 	"net/http"
 | |
| 	"regexp"
 | |
| 	"runtime"
 | |
| 	"strconv"
 | |
| 	"strings"
 | |
| 	"sync"
 | |
| 	"time"
 | |
| 
 | |
| 	"go.uber.org/zap"
 | |
| )
 | |
| 
 | |
| // httpPostJWS performs robust HTTP requests by JWS-encoding the JSON of input.
 | |
| // If output is specified, the response body is written into it: if the response
 | |
| // Content-Type is JSON, it will be JSON-decoded into output (which must be a
 | |
| // pointer); otherwise, if output is an io.Writer, the response body will be
 | |
| // written to it uninterpreted. In all cases, the returned response value's
 | |
| // body will have been drained and closed, so there is no need to close it again.
 | |
| // It automatically retries in the case of network, I/O, or badNonce errors.
 | |
| func (c *Client) httpPostJWS(ctx context.Context, privateKey crypto.Signer,
 | |
| 	kid, endpoint string, input, output interface{}) (*http.Response, error) {
 | |
| 
 | |
| 	if err := c.provision(ctx); err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	var resp *http.Response
 | |
| 	var err error
 | |
| 
 | |
| 	// we can retry on internal server errors just in case it was a hiccup,
 | |
| 	// but we probably don't need to retry so many times in that case
 | |
| 	internalServerErrors, maxInternalServerErrors := 0, 3
 | |
| 
 | |
| 	// set a hard cap on the number of retries for any other reason
 | |
| 	const maxAttempts = 10
 | |
| 	var attempts int
 | |
| 	for attempts = 1; attempts <= maxAttempts; attempts++ {
 | |
| 		if attempts > 1 {
 | |
| 			select {
 | |
| 			case <-time.After(250 * time.Millisecond):
 | |
| 			case <-ctx.Done():
 | |
| 				return nil, ctx.Err()
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		var nonce string // avoid shadowing err
 | |
| 		nonce, err = c.nonce(ctx)
 | |
| 		if err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 
 | |
| 		var encodedPayload []byte // avoid shadowing err
 | |
| 		encodedPayload, err = jwsEncodeJSON(input, privateKey, keyID(kid), nonce, endpoint)
 | |
| 		if err != nil {
 | |
| 			return nil, fmt.Errorf("encoding payload: %v", err)
 | |
| 		}
 | |
| 
 | |
| 		resp, err = c.httpReq(ctx, http.MethodPost, endpoint, encodedPayload, output)
 | |
| 		if err == nil {
 | |
| 			return resp, nil
 | |
| 		}
 | |
| 
 | |
| 		// "When a server rejects a request because its nonce value was
 | |
| 		// unacceptable (or not present), it MUST provide HTTP status code 400
 | |
| 		// (Bad Request), and indicate the ACME error type
 | |
| 		// 'urn:ietf:params:acme:error:badNonce'.  An error response with the
 | |
| 		// 'badNonce' error type MUST include a Replay-Nonce header field with a
 | |
| 		// fresh nonce that the server will accept in a retry of the original
 | |
| 		// query (and possibly in other requests, according to the server's
 | |
| 		// nonce scoping policy).  On receiving such a response, a client SHOULD
 | |
| 		// retry the request using the new nonce." §6.5
 | |
| 		var problem Problem
 | |
| 		if errors.As(err, &problem) {
 | |
| 			if problem.Type == ProblemTypeBadNonce {
 | |
| 				if c.Logger != nil {
 | |
| 					c.Logger.Debug("server rejected our nonce; retrying",
 | |
| 						zap.String("detail", problem.Detail),
 | |
| 						zap.Error(err))
 | |
| 				}
 | |
| 				continue
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		// internal server errors *could* just be a hiccup and it may be worth
 | |
| 		// trying again, but not nearly so many times as for other reasons
 | |
| 		if resp != nil && resp.StatusCode >= 500 {
 | |
| 			internalServerErrors++
 | |
| 			if internalServerErrors < maxInternalServerErrors {
 | |
| 				continue
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		// for any other error, there's not much we can do automatically
 | |
| 		break
 | |
| 	}
 | |
| 
 | |
| 	return resp, fmt.Errorf("request to %s failed after %d attempts: %w",
 | |
| 		endpoint, attempts, err)
 | |
| }
 | |
| 
 | |
| // httpReq robustly performs an HTTP request using the given method to the given endpoint, honoring
 | |
| // the given context's cancellation. The joseJSONPayload is optional; if not nil, it is expected to
 | |
| // be a JOSE+JSON encoding. The output is also optional; if not nil, the response body will be read
 | |
| // into output. If the response Content-Type is JSON, it will be JSON-decoded into output, which
 | |
| // must be a pointer type. If the response is any other Content-Type and if output is a io.Writer,
 | |
| // it will be written (without interpretation or decoding) to output. In all cases, the returned
 | |
| // response value will have the body drained and closed, so there is no need to close it again.
 | |
| //
 | |
| // If there are any network or I/O errors, the request will be retried as safely and resiliently as
 | |
| // possible.
 | |
| func (c *Client) httpReq(ctx context.Context, method, endpoint string, joseJSONPayload []byte, output interface{}) (*http.Response, error) {
 | |
| 	// even if the caller doesn't specify an output, we still use a
 | |
| 	// buffer to store possible error response (we reset it later)
 | |
| 	buf := bufPool.Get().(*bytes.Buffer)
 | |
| 	defer bufPool.Put(buf)
 | |
| 
 | |
| 	var resp *http.Response
 | |
| 	var err error
 | |
| 
 | |
| 	// potentially retry the request if there's network, I/O, or server internal errors
 | |
| 	const maxAttempts = 3
 | |
| 	for attempt := 0; attempt < maxAttempts; attempt++ {
 | |
| 		if attempt > 0 {
 | |
| 			// traffic calming ahead
 | |
| 			select {
 | |
| 			case <-time.After(250 * time.Millisecond):
 | |
| 			case <-ctx.Done():
 | |
| 				return nil, ctx.Err()
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		var body io.Reader
 | |
| 		if joseJSONPayload != nil {
 | |
| 			body = bytes.NewReader(joseJSONPayload)
 | |
| 		}
 | |
| 
 | |
| 		var req *http.Request
 | |
| 		req, err = http.NewRequestWithContext(ctx, method, endpoint, body)
 | |
| 		if err != nil {
 | |
| 			return nil, fmt.Errorf("creating request: %w", err)
 | |
| 		}
 | |
| 		if len(joseJSONPayload) > 0 {
 | |
| 			req.Header.Set("Content-Type", "application/jose+json")
 | |
| 		}
 | |
| 
 | |
| 		// on first attempt, we need to reset buf since it
 | |
| 		// came from the pool; after first attempt, we should
 | |
| 		// still reset it because we might be retrying after
 | |
| 		// a partial download
 | |
| 		buf.Reset()
 | |
| 
 | |
| 		var retry bool
 | |
| 		resp, retry, err = c.doHTTPRequest(req, buf)
 | |
| 		if err != nil {
 | |
| 			if retry {
 | |
| 				if c.Logger != nil {
 | |
| 					c.Logger.Warn("HTTP request failed; retrying",
 | |
| 						zap.String("url", req.URL.String()),
 | |
| 						zap.Error(err))
 | |
| 				}
 | |
| 				continue
 | |
| 			}
 | |
| 			break
 | |
| 		}
 | |
| 
 | |
| 		// check for HTTP errors
 | |
| 		switch {
 | |
| 		case resp.StatusCode >= 200 && resp.StatusCode < 300: // OK
 | |
| 		case resp.StatusCode >= 400 && resp.StatusCode < 600: // error
 | |
| 			if parseMediaType(resp) == "application/problem+json" {
 | |
| 				// "When the server responds with an error status, it SHOULD provide
 | |
| 				// additional information using a problem document [RFC7807]." (§6.7)
 | |
| 				var problem Problem
 | |
| 				err = json.Unmarshal(buf.Bytes(), &problem)
 | |
| 				if err != nil {
 | |
| 					return resp, fmt.Errorf("HTTP %d: JSON-decoding problem details: %w (raw='%s')",
 | |
| 						resp.StatusCode, err, buf.String())
 | |
| 				}
 | |
| 				if resp.StatusCode >= 500 && joseJSONPayload == nil {
 | |
| 					// a 5xx status is probably safe to retry on even after a
 | |
| 					// request that had no I/O errors; it could be that the
 | |
| 					// server just had a hiccup... so try again, but only if
 | |
| 					// there is no request body, because we can't replay a
 | |
| 					// request that has an anti-replay nonce, obviously
 | |
| 					err = problem
 | |
| 					continue
 | |
| 				}
 | |
| 				return resp, problem
 | |
| 			}
 | |
| 			return resp, fmt.Errorf("HTTP %d: %s", resp.StatusCode, buf.String())
 | |
| 		default: // what even is this
 | |
| 			return resp, fmt.Errorf("unexpected status code: HTTP %d", resp.StatusCode)
 | |
| 		}
 | |
| 
 | |
| 		// do not retry if we got this far (success)
 | |
| 		break
 | |
| 	}
 | |
| 	if err != nil {
 | |
| 		return resp, err
 | |
| 	}
 | |
| 
 | |
| 	// if expecting a body, finally decode it
 | |
| 	if output != nil {
 | |
| 		contentType := parseMediaType(resp)
 | |
| 		switch contentType {
 | |
| 		case "application/json":
 | |
| 			// unmarshal JSON
 | |
| 			err = json.Unmarshal(buf.Bytes(), output)
 | |
| 			if err != nil {
 | |
| 				return resp, fmt.Errorf("JSON-decoding response body: %w", err)
 | |
| 			}
 | |
| 
 | |
| 		default:
 | |
| 			// don't interpret anything else here; just hope
 | |
| 			// it's a Writer and copy the bytes
 | |
| 			w, ok := output.(io.Writer)
 | |
| 			if !ok {
 | |
| 				return resp, fmt.Errorf("response Content-Type is %s but target container is not io.Writer: %T", contentType, output)
 | |
| 			}
 | |
| 			_, err = io.Copy(w, buf)
 | |
| 			if err != nil {
 | |
| 				return resp, err
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return resp, nil
 | |
| }
 | |
| 
 | |
| // doHTTPRequest performs an HTTP request at most one time. It returns the response
 | |
| // (with drained and closed body), having drained any request body into buf. If
 | |
| // retry == true is returned, then the request should be safe to retry in the case
 | |
| // of an error. However, in some cases a retry may be recommended even if part of
 | |
| // the response body has been read and written into buf. Thus, the buffer may have
 | |
| // been partially written to and should be reset before being reused.
 | |
| //
 | |
| // This method remembers any nonce returned by the server.
 | |
| func (c *Client) doHTTPRequest(req *http.Request, buf *bytes.Buffer) (resp *http.Response, retry bool, err error) {
 | |
| 	req.Header.Set("User-Agent", c.userAgent())
 | |
| 
 | |
| 	resp, err = c.httpClient().Do(req)
 | |
| 	if err != nil {
 | |
| 		return resp, true, fmt.Errorf("performing request: %w", err)
 | |
| 	}
 | |
| 	defer resp.Body.Close()
 | |
| 
 | |
| 	if c.Logger != nil {
 | |
| 		c.Logger.Debug("http request",
 | |
| 			zap.String("method", req.Method),
 | |
| 			zap.String("url", req.URL.String()),
 | |
| 			zap.Reflect("headers", req.Header),
 | |
| 			zap.Int("status_code", resp.StatusCode),
 | |
| 			zap.Reflect("response_headers", resp.Header))
 | |
| 	}
 | |
| 
 | |
| 	// "The server MUST include a Replay-Nonce header field
 | |
| 	// in every successful response to a POST request and
 | |
| 	// SHOULD provide it in error responses as well." §6.5
 | |
| 	//
 | |
| 	// "Before sending a POST request to the server, an ACME
 | |
| 	// client needs to have a fresh anti-replay nonce to put
 | |
| 	// in the 'nonce' header of the JWS.  In most cases, the
 | |
| 	// client will have gotten a nonce from a previous
 | |
| 	// request." §7.2
 | |
| 	//
 | |
| 	// So basically, we need to remember the nonces we get
 | |
| 	// and use them at the next opportunity.
 | |
| 	c.nonces.push(resp.Header.Get(replayNonce))
 | |
| 
 | |
| 	// drain the response body, even if we aren't keeping it
 | |
| 	// (this allows us to reuse the connection and also read
 | |
| 	// any error information)
 | |
| 	_, err = io.Copy(buf, resp.Body)
 | |
| 	if err != nil {
 | |
| 		// this is likely a network or I/O error, but is it worth retrying?
 | |
| 		// technically the request has already completed, it was just our
 | |
| 		// download of the response that failed; so we probably should not
 | |
| 		// retry if the request succeeded... however, if there was an HTTP
 | |
| 		// error, it likely didn't count against any server-enforced rate
 | |
| 		// limits, and we DO want to know the error information, so it should
 | |
| 		// be safe to retry the request in those cases AS LONG AS there is
 | |
| 		// no request body, which in the context of ACME likely includes an
 | |
| 		// anti-replay nonce, which obviously we can't reuse
 | |
| 		retry = resp.StatusCode >= 400 && req.Body == nil
 | |
| 		return resp, retry, fmt.Errorf("reading response body: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	return resp, false, nil
 | |
| }
 | |
| 
 | |
| func (c *Client) httpClient() *http.Client {
 | |
| 	if c.HTTPClient == nil {
 | |
| 		return http.DefaultClient
 | |
| 	}
 | |
| 	return c.HTTPClient
 | |
| }
 | |
| 
 | |
| func (c *Client) userAgent() string {
 | |
| 	ua := fmt.Sprintf("acmez (%s; %s)", runtime.GOOS, runtime.GOARCH)
 | |
| 	if c.UserAgent != "" {
 | |
| 		ua = c.UserAgent + " " + ua
 | |
| 	}
 | |
| 	return ua
 | |
| }
 | |
| 
 | |
| // extractLinks extracts the URL from the Link header with the
 | |
| // designated relation rel. It may return more than value
 | |
| // if there are multiple matching Link values.
 | |
| //
 | |
| // Originally by Isaac: https://github.com/eggsampler/acme
 | |
| // and has been modified to support multiple matching Links.
 | |
| func extractLinks(resp *http.Response, rel string) []string {
 | |
| 	if resp == nil {
 | |
| 		return nil
 | |
| 	}
 | |
| 	var links []string
 | |
| 	for _, l := range resp.Header["Link"] {
 | |
| 		matches := linkRegex.FindAllStringSubmatch(l, -1)
 | |
| 		for _, m := range matches {
 | |
| 			if len(m) != 3 {
 | |
| 				continue
 | |
| 			}
 | |
| 			if m[2] == rel {
 | |
| 				links = append(links, m[1])
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	return links
 | |
| }
 | |
| 
 | |
| // parseMediaType returns only the media type from the
 | |
| // Content-Type header of resp.
 | |
| func parseMediaType(resp *http.Response) string {
 | |
| 	if resp == nil {
 | |
| 		return ""
 | |
| 	}
 | |
| 	ct := resp.Header.Get("Content-Type")
 | |
| 	sep := strings.Index(ct, ";")
 | |
| 	if sep < 0 {
 | |
| 		return ct
 | |
| 	}
 | |
| 	return strings.TrimSpace(ct[:sep])
 | |
| }
 | |
| 
 | |
| // retryAfter returns a duration from the response's Retry-After
 | |
| // header field, if it exists. It can return an error if the
 | |
| // header contains an invalid value. If there is no error but
 | |
| // there is no Retry-After header provided, then the fallback
 | |
| // duration is returned instead.
 | |
| func retryAfter(resp *http.Response, fallback time.Duration) (time.Duration, error) {
 | |
| 	if resp == nil {
 | |
| 		return fallback, nil
 | |
| 	}
 | |
| 	raSeconds := resp.Header.Get("Retry-After")
 | |
| 	if raSeconds == "" {
 | |
| 		return fallback, nil
 | |
| 	}
 | |
| 	ra, err := strconv.Atoi(raSeconds)
 | |
| 	if err != nil || ra < 0 {
 | |
| 		return 0, fmt.Errorf("response had invalid Retry-After header: %s", raSeconds)
 | |
| 	}
 | |
| 	return time.Duration(ra) * time.Second, nil
 | |
| }
 | |
| 
 | |
| var bufPool = sync.Pool{
 | |
| 	New: func() interface{} {
 | |
| 		return new(bytes.Buffer)
 | |
| 	},
 | |
| }
 | |
| 
 | |
| var linkRegex = regexp.MustCompile(`<(.+?)>;\s*rel="(.+?)"`)
 |