engine/revproxy.go
Rama main · réplica de lectura del repositorio original.
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)
}