Skip to content
quarry Artifact [46aa8d3379]
Login

Artifact [46aa8d3379]

Artifact 46aa8d3379bedc288e6b9cfc7166f3cd8f3467124ef0389fcbc38b0e3600264b:


package web

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

// TrustedProxy names the one request header that the proxy in front of
// the forge sets and a client cannot, such as Cloudflare's
// CF-Connecting-IP. Empty trusts no proxy, which is the default.
//
// It is the forge's single answer to "may a forwarded header be
// believed for this request": the address the login and intake limiters
// key on, and the scheme that decides Secure cookies and the https
// redirect, are both read through it. Two separate rules would let a
// request be trusted for one and not the other.
//
// A request is vouched for when it carries the configured header. That
// is sound only because the forge listens on loopback behind the proxy,
// which overwrites the header on every request it relays; a client
// reaching the listener directly is already on the forge's own box.
type TrustedProxy string

type ctxProxy struct{}

// vouches says whether the configured proxy relayed this request.
func (p TrustedProxy) vouches(req *http.Request) bool {
	return p != "" && req.Header.Get(string(p)) != ""
}

// ClientAddr is the address of the person at the other end: the
// proxy's header when it vouches, else the socket. When the header is a
// chain, the last hop is the one the proxy itself appended; everything
// before it came from the client. It is exported so the API's stream
// cap keys on this same rule rather than on one of its own.
func (p TrustedProxy) ClientAddr(req *http.Request) string {
	if p.vouches(req) {
		v := req.Header.Get(string(p))
		if i := strings.LastIndexByte(v, ','); i >= 0 {
			v = v[i+1:]
		}
		return strings.TrimSpace(v)
	}
	if h, _, err := net.SplitHostPort(req.RemoteAddr); err == nil {
		return h
	}
	return req.RemoteAddr
}

// forwardedProto is the scheme the reader used as the proxy reports it,
// lower-cased, or "" when no trusted proxy vouches for the request.
func (p TrustedProxy) forwardedProto(req *http.Request) string {
	if !p.vouches(req) {
		return ""
	}
	return strings.ToLower(req.Header.Get("X-Forwarded-Proto"))
}

// within makes p the rule for every helper below next that has only
// the request to go on.
func (p TrustedProxy) within(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
		next.ServeHTTP(w, req.WithContext(context.WithValue(req.Context(), ctxProxy{}, p)))
	})
}

// proxyOf is the rule the handler was built with; none outside it.
func proxyOf(req *http.Request) TrustedProxy {
	p, _ := req.Context().Value(ctxProxy{}).(TrustedProxy)
	return p
}

// WithTrustedProxy tells the web which header its proxy sets. Without
// it no forwarded header is believed.
func WithTrustedProxy(p TrustedProxy) Option {
	return func(o *handlerOpts) { o.proxy = p }
}