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