mirror of
				https://github.com/traefik/traefik.git
				synced 2025-11-04 10:21:15 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			266 lines
		
	
	
		
			7.2 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			266 lines
		
	
	
		
			7.2 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
package forwardedheaders
 | 
						|
 | 
						|
import (
 | 
						|
	"net"
 | 
						|
	"net/http"
 | 
						|
	"net/textproto"
 | 
						|
	"os"
 | 
						|
	"slices"
 | 
						|
	"strings"
 | 
						|
 | 
						|
	"github.com/traefik/traefik/v3/pkg/ip"
 | 
						|
	"golang.org/x/net/http/httpguts"
 | 
						|
)
 | 
						|
 | 
						|
const (
 | 
						|
	xForwardedProto             = "X-Forwarded-Proto"
 | 
						|
	xForwardedFor               = "X-Forwarded-For"
 | 
						|
	xForwardedHost              = "X-Forwarded-Host"
 | 
						|
	xForwardedPort              = "X-Forwarded-Port"
 | 
						|
	xForwardedServer            = "X-Forwarded-Server"
 | 
						|
	xForwardedURI               = "X-Forwarded-Uri"
 | 
						|
	xForwardedMethod            = "X-Forwarded-Method"
 | 
						|
	xForwardedTLSClientCert     = "X-Forwarded-Tls-Client-Cert"
 | 
						|
	xForwardedTLSClientCertInfo = "X-Forwarded-Tls-Client-Cert-Info"
 | 
						|
	xRealIP                     = "X-Real-Ip"
 | 
						|
	connection                  = "Connection"
 | 
						|
	upgrade                     = "Upgrade"
 | 
						|
)
 | 
						|
 | 
						|
var xHeaders = []string{
 | 
						|
	xForwardedProto,
 | 
						|
	xForwardedFor,
 | 
						|
	xForwardedHost,
 | 
						|
	xForwardedPort,
 | 
						|
	xForwardedServer,
 | 
						|
	xForwardedURI,
 | 
						|
	xForwardedMethod,
 | 
						|
	xForwardedTLSClientCert,
 | 
						|
	xForwardedTLSClientCertInfo,
 | 
						|
	xRealIP,
 | 
						|
}
 | 
						|
 | 
						|
// XForwarded is an HTTP handler wrapper that sets the X-Forwarded headers,
 | 
						|
// and other relevant headers for a reverse-proxy.
 | 
						|
// Unless insecure is set,
 | 
						|
// it first removes all the existing values for those headers if the remote address is not one of the trusted ones.
 | 
						|
type XForwarded struct {
 | 
						|
	insecure          bool
 | 
						|
	trustedIPs        []string
 | 
						|
	connectionHeaders []string
 | 
						|
	ipChecker         *ip.Checker
 | 
						|
	next              http.Handler
 | 
						|
	hostname          string
 | 
						|
}
 | 
						|
 | 
						|
// NewXForwarded creates a new XForwarded.
 | 
						|
func NewXForwarded(insecure bool, trustedIPs []string, connectionHeaders []string, next http.Handler) (*XForwarded, error) {
 | 
						|
	var ipChecker *ip.Checker
 | 
						|
	if len(trustedIPs) > 0 {
 | 
						|
		var err error
 | 
						|
		ipChecker, err = ip.NewChecker(trustedIPs)
 | 
						|
		if err != nil {
 | 
						|
			return nil, err
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	hostname, err := os.Hostname()
 | 
						|
	if err != nil {
 | 
						|
		hostname = "localhost"
 | 
						|
	}
 | 
						|
 | 
						|
	return &XForwarded{
 | 
						|
		insecure:          insecure,
 | 
						|
		trustedIPs:        trustedIPs,
 | 
						|
		connectionHeaders: connectionHeaders,
 | 
						|
		ipChecker:         ipChecker,
 | 
						|
		next:              next,
 | 
						|
		hostname:          hostname,
 | 
						|
	}, nil
 | 
						|
}
 | 
						|
 | 
						|
func (x *XForwarded) isTrustedIP(ip string) bool {
 | 
						|
	if x.ipChecker == nil {
 | 
						|
		return false
 | 
						|
	}
 | 
						|
	return x.ipChecker.IsAuthorized(ip) == nil
 | 
						|
}
 | 
						|
 | 
						|
// removeIPv6Zone removes the zone if the given IP is an ipv6 address and it has {zone} information in it,
 | 
						|
// like "[fe80::d806:a55d:eb1b:49cc%vEthernet (vmxnet3 Ethernet Adapter - Virtual Switch)]:64692".
 | 
						|
func removeIPv6Zone(clientIP string) string {
 | 
						|
	if idx := strings.Index(clientIP, "%"); idx != -1 {
 | 
						|
		return clientIP[:idx]
 | 
						|
	}
 | 
						|
	return clientIP
 | 
						|
}
 | 
						|
 | 
						|
// isWebsocketRequest returns whether the specified HTTP request is a websocket handshake request.
 | 
						|
func isWebsocketRequest(req *http.Request) bool {
 | 
						|
	containsHeader := func(name, value string) bool {
 | 
						|
		h := unsafeHeader(req.Header).Get(name)
 | 
						|
		for {
 | 
						|
			pos := strings.Index(h, ",")
 | 
						|
			if pos == -1 {
 | 
						|
				return strings.EqualFold(value, strings.TrimSpace(h))
 | 
						|
			}
 | 
						|
 | 
						|
			if strings.EqualFold(value, strings.TrimSpace(h[:pos])) {
 | 
						|
				return true
 | 
						|
			}
 | 
						|
 | 
						|
			h = h[pos+1:]
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return containsHeader(connection, "upgrade") && containsHeader(upgrade, "websocket")
 | 
						|
}
 | 
						|
 | 
						|
func forwardedPort(req *http.Request) string {
 | 
						|
	if req == nil {
 | 
						|
		return ""
 | 
						|
	}
 | 
						|
 | 
						|
	if _, port, err := net.SplitHostPort(req.Host); err == nil && port != "" {
 | 
						|
		return port
 | 
						|
	}
 | 
						|
 | 
						|
	if unsafeHeader(req.Header).Get(xForwardedProto) == "https" || unsafeHeader(req.Header).Get(xForwardedProto) == "wss" {
 | 
						|
		return "443"
 | 
						|
	}
 | 
						|
 | 
						|
	if req.TLS != nil {
 | 
						|
		return "443"
 | 
						|
	}
 | 
						|
 | 
						|
	return "80"
 | 
						|
}
 | 
						|
 | 
						|
func (x *XForwarded) rewrite(outreq *http.Request) {
 | 
						|
	if clientIP, _, err := net.SplitHostPort(outreq.RemoteAddr); err == nil {
 | 
						|
		clientIP = removeIPv6Zone(clientIP)
 | 
						|
 | 
						|
		if unsafeHeader(outreq.Header).Get(xRealIP) == "" {
 | 
						|
			unsafeHeader(outreq.Header).Set(xRealIP, clientIP)
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	xfProto := unsafeHeader(outreq.Header).Get(xForwardedProto)
 | 
						|
	if xfProto == "" {
 | 
						|
		// TODO: is this expected to set the X-Forwarded-Proto header value to
 | 
						|
		// ws(s) as the underlying request used to upgrade the connection is
 | 
						|
		// made over HTTP(S)?
 | 
						|
		if isWebsocketRequest(outreq) {
 | 
						|
			if outreq.TLS != nil {
 | 
						|
				unsafeHeader(outreq.Header).Set(xForwardedProto, "wss")
 | 
						|
			} else {
 | 
						|
				unsafeHeader(outreq.Header).Set(xForwardedProto, "ws")
 | 
						|
			}
 | 
						|
		} else {
 | 
						|
			if outreq.TLS != nil {
 | 
						|
				unsafeHeader(outreq.Header).Set(xForwardedProto, "https")
 | 
						|
			} else {
 | 
						|
				unsafeHeader(outreq.Header).Set(xForwardedProto, "http")
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	if xfPort := unsafeHeader(outreq.Header).Get(xForwardedPort); xfPort == "" {
 | 
						|
		unsafeHeader(outreq.Header).Set(xForwardedPort, forwardedPort(outreq))
 | 
						|
	}
 | 
						|
 | 
						|
	if xfHost := unsafeHeader(outreq.Header).Get(xForwardedHost); xfHost == "" && outreq.Host != "" {
 | 
						|
		unsafeHeader(outreq.Header).Set(xForwardedHost, outreq.Host)
 | 
						|
	}
 | 
						|
 | 
						|
	// Per https://www.rfc-editor.org/rfc/rfc2616#section-4.2, the Forwarded IPs list is in
 | 
						|
	// the same order as the values in the X-Forwarded-For header(s).
 | 
						|
	if xffs := unsafeHeader(outreq.Header).Values(xForwardedFor); len(xffs) > 0 {
 | 
						|
		unsafeHeader(outreq.Header).Set(xForwardedFor, strings.Join(xffs, ", "))
 | 
						|
	}
 | 
						|
 | 
						|
	if x.hostname != "" {
 | 
						|
		unsafeHeader(outreq.Header).Set(xForwardedServer, x.hostname)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// ServeHTTP implements http.Handler.
 | 
						|
func (x *XForwarded) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 | 
						|
	if !x.insecure && !x.isTrustedIP(r.RemoteAddr) {
 | 
						|
		for _, h := range xHeaders {
 | 
						|
			unsafeHeader(r.Header).Del(h)
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	x.rewrite(r)
 | 
						|
 | 
						|
	x.removeConnectionHeaders(r)
 | 
						|
 | 
						|
	x.next.ServeHTTP(w, r)
 | 
						|
}
 | 
						|
 | 
						|
func (x *XForwarded) removeConnectionHeaders(req *http.Request) {
 | 
						|
	var reqUpType string
 | 
						|
	if httpguts.HeaderValuesContainsToken(req.Header[connection], upgrade) {
 | 
						|
		reqUpType = unsafeHeader(req.Header).Get(upgrade)
 | 
						|
	}
 | 
						|
 | 
						|
	var connectionHopByHopHeaders []string
 | 
						|
	for _, f := range req.Header[connection] {
 | 
						|
		for _, sf := range strings.Split(f, ",") {
 | 
						|
			if sf = textproto.TrimString(sf); sf != "" {
 | 
						|
				// Connection header cannot dictate to remove X- headers managed by Traefik,
 | 
						|
				// as per rfc7230 https://datatracker.ietf.org/doc/html/rfc7230#section-6.1,
 | 
						|
				// A proxy or gateway MUST ... and then remove the Connection header field itself
 | 
						|
				// (or replace it with the intermediary's own connection options for the forwarded message).
 | 
						|
				if slices.Contains(xHeaders, sf) {
 | 
						|
					continue
 | 
						|
				}
 | 
						|
 | 
						|
				// Keep headers allowed through the middleware chain.
 | 
						|
				if slices.Contains(x.connectionHeaders, sf) {
 | 
						|
					connectionHopByHopHeaders = append(connectionHopByHopHeaders, sf)
 | 
						|
					continue
 | 
						|
				}
 | 
						|
 | 
						|
				// Apply Connection header option.
 | 
						|
				req.Header.Del(sf)
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	if reqUpType != "" {
 | 
						|
		connectionHopByHopHeaders = append(connectionHopByHopHeaders, upgrade)
 | 
						|
		unsafeHeader(req.Header).Set(upgrade, reqUpType)
 | 
						|
	}
 | 
						|
	if len(connectionHopByHopHeaders) > 0 {
 | 
						|
		unsafeHeader(req.Header).Set(connection, strings.Join(connectionHopByHopHeaders, ","))
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	unsafeHeader(req.Header).Del(connection)
 | 
						|
}
 | 
						|
 | 
						|
// unsafeHeader allows to manage Header values.
 | 
						|
// Must be used only when the header name is already a canonical key.
 | 
						|
type unsafeHeader map[string][]string
 | 
						|
 | 
						|
func (h unsafeHeader) Set(key, value string) {
 | 
						|
	h[key] = []string{value}
 | 
						|
}
 | 
						|
 | 
						|
func (h unsafeHeader) Get(key string) string {
 | 
						|
	if len(h[key]) == 0 {
 | 
						|
		return ""
 | 
						|
	}
 | 
						|
	return h[key][0]
 | 
						|
}
 | 
						|
 | 
						|
func (h unsafeHeader) Values(key string) []string {
 | 
						|
	return h[key]
 | 
						|
}
 | 
						|
 | 
						|
func (h unsafeHeader) Del(key string) {
 | 
						|
	delete(h, key)
 | 
						|
}
 |