Ctrl K
ComunidadGuías…

coderd/httpmw/realip.go

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

Archivo original ↗Preguntar a KiBot
package httpmw

import (
	"context"
	"net"
	"net/http"
	"strings"

	"golang.org/x/xerrors"

	"github.com/coder/coder/v2/coderd/httpapi"
)

const (
	headerXForwardedFor   string = "X-Forwarded-For"
	headerXForwardedProto string = "X-Forwarded-Proto"
)

// RealIPConfig configures the search order for the function, which controls
// which headers to consider trusted.
type RealIPConfig struct {
	// TrustedOrigins is a list of networks that will be trusted. If
	// any non-trusted address supplies these headers, they will be
	// ignored.
	TrustedOrigins []*net.IPNet

	// TrustedHeaders lists headers that are trusted for forwarding
	// IP addresses. e.g. "CF-Connecting-IP", "True-Client-IP", etc.
	TrustedHeaders []string
}

// ExtractRealIP is a middleware that uses headers from reverse proxies to
// propagate origin IP address information, when configured to do so.
func ExtractRealIP(config *RealIPConfig) func(next http.Handler) http.Handler {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
			// Preserve the original TLS connection state and RemoteAddr
			req = req.WithContext(context.WithValue(req.Context(), ctxKey{}, &RealIPState{
				Config:             config,
				OriginalRemoteAddr: req.RemoteAddr,
			}))

			info, err := ExtractRealIPAddress(config, req)
			if err != nil {
				httpapi.InternalServerError(w, err)
				return
			}
			req.RemoteAddr = info.String()

			next.ServeHTTP(w, req)
		})
	}
}

// ExtractRealIPAddress returns the original client address according to the
// configuration and headers. It does not mutate the original request.
func ExtractRealIPAddress(config *RealIPConfig, req *http.Request) (net.IP, error) {
	if config == nil {
		config = &RealIPConfig{
			TrustedOrigins: nil,
			TrustedHeaders: nil,
		}
	}

	cf := isContainedIn(config.TrustedOrigins, getRemoteAddress(req.RemoteAddr))
	if !cf {
		// Address is not valid or the origin is not trusted; use the
		// original address
		return getRemoteAddress(req.RemoteAddr), nil
	}

	for _, trustedHeader := range config.TrustedHeaders {
		// X-Forwarded-For is a list-valued header. Per RFC 7230, multiple
		// field lines with the same name are equivalent to a single
		// comma-separated value. Join them so a client cannot hide a spoofed
		// address in the first field line, which Header.Get would return on
		// its own. Other forwarding headers carry a single edge-proxy value,
		// so use Header.Get to preserve their first-value semantics.
		value := req.Header.Get(trustedHeader)
		if http.CanonicalHeaderKey(trustedHeader) == headerXForwardedFor {
			value = strings.Join(req.Header.Values(trustedHeader), ",")
		}
		addr := extractForwardedAddress(config, value)
		if addr != nil {
			return addr, nil
		}
	}

	return getRemoteAddress(req.RemoteAddr), nil
}

// FilterUntrustedOriginHeaders removes all known proxy headers from the
// request for untrusted origins, and ensures that only one copy
// of each proxy header is set.
func FilterUntrustedOriginHeaders(config *RealIPConfig, req *http.Request) {
	if config == nil {
		config = &RealIPConfig{
			TrustedOrigins: nil,
			TrustedHeaders: nil,
		}
	}

	cf := isContainedIn(config.TrustedOrigins, getRemoteAddress(req.RemoteAddr))
	if !cf {
		// Address is not valid or the origin is not trusted; clear
		// all known proxy headers and return
		for _, header := range config.TrustedHeaders {
			req.Header.Del(header)
		}
		return
	}

	for _, header := range config.TrustedHeaders {
		// X-Forwarded-For is a list-valued header whose field lines are
		// equivalent to a single comma-separated value (RFC 7230 section
		// 3.2.2). Join them so later hops are not dropped when collapsing to a
		// single line. Other forwarding headers carry a single value.
		if http.CanonicalHeaderKey(header) == headerXForwardedFor {
			req.Header.Set(header, strings.Join(req.Header.Values(header), ","))
			continue
		}
		req.Header.Set(header, req.Header.Get(header))
	}
}

// EffectiveHost returns the host Coder should trust for request handling.
// It uses X-Forwarded-Host only when the immediate peer is a configured
// trusted proxy. Otherwise it uses the received Host header.
func EffectiveHost(config *RealIPConfig, r *http.Request) string {
	if config == nil {
		config = &RealIPConfig{
			TrustedOrigins: nil,
			TrustedHeaders: nil,
		}
	}

	// When ExtractRealIP has run, r.RemoteAddr may hold the forwarded
	// client IP, and we should use the original socket peer for proxy
	// trust decisions.
	remoteAddr := r.RemoteAddr
	state := RealIP(r.Context())
	if state != nil && state.OriginalRemoteAddr != "" {
		remoteAddr = state.OriginalRemoteAddr
	}

	if isContainedIn(config.TrustedOrigins, getRemoteAddress(remoteAddr)) {
		if host := r.Header.Get(httpapi.XForwardedHostHeader); host != "" {
			return host
		}
	}

	return r.Host
}

// EnsureXForwardedForHeader ensures that the request has an X-Forwarded-For
// header. It uses the following logic:
//
//  1. If we have a direct connection (remoteAddr == proxyAddr), then
//     set it to remoteAddr
//  2. If we have a proxied connection (remoteAddr != proxyAddr) and
//     X-Forwarded-For doesn't begin with remoteAddr, then overwrite
//     it with remoteAddr,proxyAddr
//  3. If we have a proxied connection (remoteAddr != proxyAddr) and
//     X-Forwarded-For begins with remoteAddr, then append proxyAddr
//     to the original X-Forwarded-For header
//  4. If X-Forwarded-Proto is not set, then it will be set to "https"
//     if req.TLS != nil, otherwise it will be set to "http"
func EnsureXForwardedForHeader(req *http.Request) error {
	state := RealIP(req.Context())
	if state == nil {
		return xerrors.New("request does not contain realip.State; was it processed by httpmw.ExtractRealIP?")
	}

	remoteAddr := getRemoteAddress(req.RemoteAddr)
	if remoteAddr == nil {
		return xerrors.Errorf("failed to parse remote address: %s", remoteAddr)
	}

	proxyAddr := getRemoteAddress(state.OriginalRemoteAddr)
	if proxyAddr == nil {
		return xerrors.Errorf("failed to parse original address: %s", proxyAddr)
	}

	if remoteAddr.Equal(proxyAddr) {
		req.Header.Set(headerXForwardedFor, remoteAddr.String())
	} else {
		forwarded := req.Header.Get(headerXForwardedFor)
		if forwarded == "" || !remoteAddr.Equal(getRemoteAddress(forwarded)) {
			req.Header.Set(headerXForwardedFor, remoteAddr.String()+","+proxyAddr.String())
		} else {
			req.Header.Set(headerXForwardedFor, forwarded+","+proxyAddr.String())
		}
	}

	if req.Header.Get(headerXForwardedProto) == "" {
		if req.TLS != nil {
			req.Header.Set(headerXForwardedProto, "https")
		} else {
			req.Header.Set(headerXForwardedProto, "http")
		}
	}

	return nil
}

// getRemoteAddress extracts a single IP address from the given string,
// stripping a port if present. If the string contains commas, only the
// portion before the first comma is parsed. This helper does not select the
// real client from a multi-hop X-Forwarded-For chain; use
// extractForwardedAddress for that, which accounts for client-supplied values.
func getRemoteAddress(address string) net.IP {
	// A value may contain a port and, for a raw X-Forwarded-For value, more
	// than one comma-separated address. Parse only the part before the first
	// comma.
	i := strings.IndexByte(address, ',')
	if i == -1 {
		i = len(address)
	}

	// If the address contains a port, remove it
	firstAddress := address[:i]
	host, _, err := net.SplitHostPort(firstAddress)
	if err != nil {
		// This will error if there is no port, so try to parse the address
		return net.ParseIP(firstAddress)
	}
	return net.ParseIP(host)
}

// extractForwardedAddress parses a comma-separated forwarding header value and
// returns the rightmost address that is not a trusted origin. Reverse proxies
// append the peer that connected to them, so when every trusted proxy hop is
// listed in TrustedOrigins, the rightmost untrusted address is the real client;
// any values a client prepends to spoof its address sit to the left of the
// addresses inserted by trusted proxies and are ignored. If every parsed address
// is a trusted origin, the leftmost address is returned. It returns nil when no
// address can be parsed.
func extractForwardedAddress(config *RealIPConfig, value string) net.IP {
	parts := strings.Split(value, ",")
	var leftmost net.IP
	for i := len(parts) - 1; i >= 0; i-- {
		ip := getRemoteAddress(strings.TrimSpace(parts[i]))
		if ip == nil {
			continue
		}
		// Iterating right-to-left, so the last assignment is the leftmost
		// valid address, used as the fallback when all hops are trusted.
		leftmost = ip
		if !isContainedIn(config.TrustedOrigins, ip) {
			return ip
		}
	}
	return leftmost
}

// isContainedIn checks that the given address is contained in the given
// network.
func isContainedIn(networks []*net.IPNet, address net.IP) bool {
	for _, network := range networks {
		if network.Contains(address) {
			return true
		}
	}

	return false
}

// RealIPState is the original state prior to modification by this middleware,
// useful for getting information about the connecting client if needed.
type RealIPState struct {
	// Config is the configuration applied in the middleware. Consider
	// this read-only and do not modify.
	Config *RealIPConfig

	// OriginalRemoteAddr is the original RemoteAddr for the request.
	OriginalRemoteAddr string
}

type ctxKey struct{}

// FromContext retrieves the state from the given context.Context.
func RealIP(ctx context.Context) *RealIPState {
	state, ok := ctx.Value(ctxKey{}).(*RealIPState)
	if !ok {
		return nil
	}
	return state
}

// ParseRealIPConfig takes a raw string array of headers and origins
// to produce a config.
func ParseRealIPConfig(headers, origins []string) (*RealIPConfig, error) {
	config := &RealIPConfig{
		TrustedOrigins: []*net.IPNet{},
		TrustedHeaders: []string{},
	}
	for _, origin := range origins {
		_, network, err := net.ParseCIDR(origin)
		if err != nil {
			return nil, xerrors.Errorf("parse proxy origin %q: %w", origin, err)
		}
		config.TrustedOrigins = append(config.TrustedOrigins, network)
	}
	for index, header := range headers {
		headers[index] = http.CanonicalHeaderKey(header)
	}
	config.TrustedHeaders = headers

	return config, nil
}