Skip to content
quarry Artifact [eac6c5102a]
Login

Artifact [eac6c5102a]

Artifact eac6c5102a3ade6aa2dc41a7546937ecf16982c68e78c19788976996df7b95f9:


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 AND
// arrived over loopback. quarry sits behind cloudflared on 127.0.0.1,
// and the proxy overwrites the header on every request it relays. A
// listener reachable from elsewhere must not let a client vouch for
// itself by writing the header, so off loopback it is ignored and the
// socket address decides.
type TrustedProxy string

type ctxProxy struct{}

// vouches says whether the configured proxy relayed this request.
func (p TrustedProxy) vouches(req *http.Request) bool {
	if p == "" || req.Header.Get(string(p)) == "" {
		return false
	}
	host, _, err := net.SplitHostPort(req.RemoteAddr)
	if err != nil {
		host = req.RemoteAddr
	}
	ip := net.ParseIP(host)
	return ip != nil && ip.IsLoopback()
}

// 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.
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 }
}