Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lx9FNLG2CqeBbVGGd9bFsP
134 lines
3.4 KiB
Go
134 lines
3.4 KiB
Go
package auth
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"net/netip"
|
|
"strings"
|
|
)
|
|
|
|
// Proxies is the set of reverse proxies whose X-Forwarded-* headers nmon believes. A request from any
|
|
// other peer has those headers ignored (and removed), because anyone can send them.
|
|
type Proxies struct{ nets []netip.Prefix }
|
|
|
|
// ParseProxies parses CIDRs or single addresses ("10.0.0.0/8", "192.0.2.7", "::1").
|
|
func ParseProxies(cidrs []string) (*Proxies, error) {
|
|
p := &Proxies{}
|
|
for _, c := range cidrs {
|
|
c = strings.TrimSpace(c)
|
|
if pf, err := netip.ParsePrefix(c); err == nil {
|
|
p.nets = append(p.nets, pf.Masked())
|
|
continue
|
|
}
|
|
a, err := netip.ParseAddr(c)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("trusted_proxy %q: not a CIDR or address", c)
|
|
}
|
|
p.nets = append(p.nets, netip.PrefixFrom(a.Unmap(), a.Unmap().BitLen()))
|
|
}
|
|
return p, nil
|
|
}
|
|
|
|
func (p *Proxies) trusts(a netip.Addr) bool {
|
|
if p == nil {
|
|
return false
|
|
}
|
|
a = a.Unmap()
|
|
for _, n := range p.nets {
|
|
if n.Contains(a) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Client is what a request says about its origin after proxy headers have been judged.
|
|
type Client struct {
|
|
// IP is the original client: the direct peer, or the nearest untrusted hop in X-Forwarded-For when
|
|
// the peer is a trusted proxy.
|
|
IP netip.Addr
|
|
Scheme string
|
|
Host string
|
|
// ViaProxy is true when the headers were honoured.
|
|
ViaProxy bool
|
|
}
|
|
|
|
// Resolve returns the Client for r.
|
|
func (p *Proxies) Resolve(r *http.Request) Client {
|
|
c := Client{Scheme: "http", Host: r.Host}
|
|
if r.TLS != nil {
|
|
c.Scheme = "https"
|
|
}
|
|
host, _, err := net.SplitHostPort(r.RemoteAddr)
|
|
if err != nil {
|
|
host = r.RemoteAddr
|
|
}
|
|
peer, err := netip.ParseAddr(host)
|
|
if err != nil {
|
|
return c
|
|
}
|
|
c.IP = peer.Unmap()
|
|
if !p.trusts(peer) {
|
|
return c
|
|
}
|
|
c.ViaProxy = true
|
|
c.IP = p.originalClient(peer, r.Header.Values("X-Forwarded-For"))
|
|
if v := firstToken(r.Header.Get("X-Forwarded-Proto")); v == "https" || v == "http" {
|
|
c.Scheme = v
|
|
}
|
|
if v := firstToken(r.Header.Get("X-Forwarded-Host")); v != "" {
|
|
c.Host = v
|
|
}
|
|
return c
|
|
}
|
|
|
|
// originalClient walks X-Forwarded-For from the right, skipping hops that are themselves trusted
|
|
// proxies; the first other address is the client. Entries to its left were supplied by it and are not believed.
|
|
func (p *Proxies) originalClient(peer netip.Addr, xff []string) netip.Addr {
|
|
var hops []string
|
|
for _, v := range xff {
|
|
for _, h := range strings.Split(v, ",") {
|
|
hops = append(hops, strings.TrimSpace(h))
|
|
}
|
|
}
|
|
client := peer.Unmap()
|
|
for i := len(hops) - 1; i >= 0; i-- {
|
|
a, err := netip.ParseAddr(hops[i])
|
|
if err != nil {
|
|
return client
|
|
}
|
|
client = a.Unmap()
|
|
if !p.trusts(a) {
|
|
return client
|
|
}
|
|
}
|
|
return client
|
|
}
|
|
|
|
func firstToken(v string) string {
|
|
first, _, _ := strings.Cut(v, ",")
|
|
return strings.ToLower(strings.TrimSpace(first))
|
|
}
|
|
|
|
// scrub removes the forwarding headers from a request whose peer is not a trusted proxy, so no later
|
|
// handler can be fooled by them.
|
|
func scrub(r *http.Request) {
|
|
for _, h := range []string{"X-Forwarded-For", "X-Forwarded-Proto", "X-Forwarded-Host", "X-Forwarded-Port", "Forwarded"} {
|
|
r.Header.Del(h)
|
|
}
|
|
}
|
|
|
|
type clientKey struct{}
|
|
|
|
// ClientFromContext returns the Client Middleware resolved for the request.
|
|
func ClientFromContext(ctx context.Context) (Client, bool) {
|
|
c, ok := ctx.Value(clientKey{}).(Client)
|
|
return c, ok
|
|
}
|
|
|
|
func withClient(ctx context.Context, c Client) context.Context {
|
|
return context.WithValue(ctx, clientKey{}, c)
|
|
}
|