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