Ctrl K
ComunidadGuías…

engine/revproxy.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package engine

import (
	"bufio"
	"fmt"
	"io"
	"net"
	"net/http"
	"net/http/httputil"
	"net/url"
	"strconv"
	"strings"
	"time"

	"github.com/sirupsen/logrus"
	"github.com/xyproto/algernon/utils"
)

// proxyTransport is shared by all reverse proxies, so that connections to the
// backends are pooled and kept alive across requests.
var proxyTransport http.RoundTripper = &http.Transport{
	Proxy: http.ProxyFromEnvironment,
	DialContext: (&net.Dialer{
		Timeout:   10 * time.Second,
		KeepAlive: 30 * time.Second,
	}).DialContext,
	ForceAttemptHTTP2: true,
	MaxIdleConns:      256,
	// The default is 2, which re-dials the backend for almost every request
	MaxIdleConnsPerHost:   128,
	IdleConnTimeout:       90 * time.Second,
	TLSHandshakeTimeout:   10 * time.Second,
	ExpectContinueTimeout: time.Second,
	ResponseHeaderTimeout: 60 * time.Second,
}

// peekedBody is a response body that is read through a bufio.Reader,
// while still closing the original body
type peekedBody struct {
	io.Reader
	io.Closer
}

// proxyRecorder records the status code and size of a proxied response.
// Unwrap lets http.ResponseController reach the underlying Flusher and Hijacker.
type proxyRecorder struct {
	http.ResponseWriter
	written int64
	status  int
}

func (pr *proxyRecorder) WriteHeader(status int) {
	if pr.status == 0 {
		pr.status = status
	}
	pr.ResponseWriter.WriteHeader(status)
}

func (pr *proxyRecorder) Write(p []byte) (int, error) {
	if pr.status == 0 {
		pr.status = http.StatusOK
	}
	n, err := pr.ResponseWriter.Write(p)
	pr.written += int64(n)
	return n, err
}

func (pr *proxyRecorder) Unwrap() http.ResponseWriter { return pr.ResponseWriter }

// Hijack passes WebSocket upgrades through, while recording that the
// connection was upgraded, so that the access log gets a status code
func (pr *proxyRecorder) Hijack() (net.Conn, *bufio.ReadWriter, error) {
	conn, brw, err := http.NewResponseController(pr.ResponseWriter).Hijack()
	if err == nil && pr.status == 0 {
		pr.status = http.StatusSwitchingProtocols
	}
	return conn, brw, err
}

// ReverseProxy holds which path prefix (like "/api") should be sent where (like "http://localhost:8080")
type ReverseProxy struct {
	proxy      *httputil.ReverseProxy
	PathPrefix string
	Endpoint   url.URL
}

// ReverseProxyConfig holds several "path prefix --> URL" ReverseProxy structs,
// together with structures that speeds up the prefix matching.
type ReverseProxyConfig struct {
	proxyMatcher   utils.PrefixMatch
	prefix2rproxy  map[string]int
	ReverseProxies []ReverseProxy
}

// NewReverseProxyConfig creates a new and empty ReverseProxyConfig struct
func NewReverseProxyConfig() *ReverseProxyConfig {
	return &ReverseProxyConfig{}
}

// Add can add a ReverseProxy and will also (re-)initialize the internal proxy matcher
func (rc *ReverseProxyConfig) Add(rp *ReverseProxy) {
	rc.ReverseProxies = append(rc.ReverseProxies, *rp)
	rc.Init()
}

// newProxyHandler builds the httputil.ReverseProxy that does the actual proxying.
// The prefix is stripped from the request path and the endpoint path is prepended.
func newProxyHandler(pathPrefix string, endpoint url.URL) *httputil.ReverseProxy {
	basePath := strings.TrimSuffix(endpoint.Path, "/")
	endpointString := endpoint.String()
	return &httputil.ReverseProxy{
		Transport: proxyTransport,
		Rewrite: func(r *httputil.ProxyRequest) {
			r.SetXForwarded()
			// Forward the Host the client asked for, so that the backend
			// generates absolute URLs that point back at Algernon
			r.Out.Host = r.In.Host
			r.Out.URL.Scheme = endpoint.Scheme
			r.Out.URL.Host = endpoint.Host
			p := strings.TrimPrefix(r.In.URL.Path, pathPrefix)
			if !strings.HasPrefix(p, "/") {
				p = "/" + p
			}
			r.Out.URL.Path = basePath + p
			// Let EscapedPath re-derive the escaped form from Path
			r.Out.URL.RawPath = ""
		},
		ModifyResponse: func(res *http.Response) error {
			// Peek one byte before sending the status: if upstream fails
			// before any body arrives, send 502 instead of a partial 200.
			// io.EOF is the legitimate empty-body case.
			//
			// Only responses that promised a body of a given length are
			// peeked at. A response of unknown length may be a stream, an
			// upgrade or a long poll, where the next bytes may only arrive
			// once the client has spoken, and waiting for them here would
			// keep the headers from ever reaching the client.
			if res.ContentLength <= 0 {
				return nil
			}
			br := bufio.NewReader(res.Body)
			if _, err := br.Peek(1); err != nil && err != io.EOF {
				return err
			}
			res.Body = &peekedBody{Reader: br, Closer: res.Body}
			return nil
		},
		ErrorHandler: func(w http.ResponseWriter, _ *http.Request, err error) {
			logrus.Errorf("reverse proxy %s -> %s: %v", pathPrefix, endpointString, err)
			w.WriteHeader(http.StatusBadGateway)
			w.Write([]byte("reverse proxy error\n"))
		},
	}
}

// ServeHTTP proxies the given request to where the ReverseProxy points.
// Redirects, streaming responses and WebSocket upgrades are passed through.
func (rp *ReverseProxy) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	proxy := rp.proxy
	if proxy == nil {
		// Not added with Add, which prepares the proxy handler
		proxy = newProxyHandler(rp.PathPrefix, rp.Endpoint)
	}
	proxy.ServeHTTP(w, req)
}

// DoProxyPass tries to proxy the given http.Request to where the ReverseProxy points
//
// Deprecated: use ServeHTTP instead. This follows redirects instead of passing them
// on, sets no X-Forwarded-* headers and can not handle WebSocket upgrades.
func (rp *ReverseProxy) DoProxyPass(req http.Request) (*http.Response, error) {
	client := &http.Client{Transport: proxyTransport}
	endpoint := rp.Endpoint
	req.RequestURI = ""
	req.URL.Path = strings.TrimPrefix(req.URL.Path, rp.PathPrefix)
	req.URL.Scheme = endpoint.Scheme
	req.URL.Host = endpoint.Host
	res, err := client.Do(&req)
	if err != nil {
		logrus.Errorf("reverse proxy error: %v\nPlease check your server config for AddReverseProxy calls.\n", err)
		return nil, err
	}
	return res, nil
}

// Init prepares the proxyMatcher and prefix2rproxy fields according to the ReverseProxy structs
func (rc *ReverseProxyConfig) Init() {
	keys := make([]string, 0, len(rc.ReverseProxies))
	rc.prefix2rproxy = make(map[string]int)
	for i := range rc.ReverseProxies {
		rp := &rc.ReverseProxies[i]
		if rp.proxy == nil {
			rp.proxy = newProxyHandler(rp.PathPrefix, rp.Endpoint)
		}
		keys = append(keys, rp.PathPrefix)
		rc.prefix2rproxy[rp.PathPrefix] = i
	}
	rc.proxyMatcher.Build(keys)
}

// FindMatchingReverseProxy checks if the given URL path should be proxied.
// A prefix like "/api" matches "/api" and "/api/x", but not "/apiary".
// If several prefixes match, the longest one wins.
func (rc *ReverseProxyConfig) FindMatchingReverseProxy(path string) *ReverseProxy {
	var match *ReverseProxy
	maxlen := -1
	for _, prefix := range rc.proxyMatcher.Match(path) {
		if len(path) > len(prefix) && !strings.HasSuffix(prefix, "/") && path[len(prefix)] != '/' {
			continue
		}
		if len(prefix) > maxlen {
			maxlen = len(prefix)
			match = &rc.ReverseProxies[rc.prefix2rproxy[prefix]]
		}
	}
	return match
}

// NewReverseProxy creates a ReverseProxy that sends requests for the given
// path prefix to the given endpoint. The endpoint can be a full URL, or just
// "host:port", ":port" or "port", in which case http and localhost are assumed.
func NewReverseProxy(pathPrefix, endpoint string) (*ReverseProxy, error) {
	if !strings.HasPrefix(pathPrefix, "/") {
		pathPrefix = "/" + pathPrefix
	}
	if !strings.Contains(endpoint, "://") {
		if _, err := strconv.Atoi(endpoint); err == nil {
			endpoint = ":" + endpoint
		}
		if strings.HasPrefix(endpoint, ":") {
			endpoint = "localhost" + endpoint
		}
		endpoint = "http://" + endpoint
	}
	u, err := url.Parse(endpoint)
	if err != nil {
		return nil, fmt.Errorf("could not parse endpoint URL %s: %w", endpoint, err)
	}
	if u.Scheme == "" || u.Host == "" {
		return nil, fmt.Errorf("endpoint URL needs a scheme and a host: %s", endpoint)
	}
	return &ReverseProxy{PathPrefix: pathPrefix, Endpoint: *u}, nil
}

// ParseReverseProxyFlag parses a --proxy value, which is either "ENDPOINT"
// (proxy everything) or "/PREFIX=ENDPOINT".
func ParseReverseProxyFlag(value string) (*ReverseProxy, error) {
	if strings.HasPrefix(value, "/") {
		prefix, endpoint, found := strings.Cut(value, "=")
		if !found || endpoint == "" {
			return nil, fmt.Errorf("expected /PREFIX=ENDPOINT, got %q", value)
		}
		return NewReverseProxy(prefix, endpoint)
	}
	return NewReverseProxy("/", value)
}