A new internal/clientip says who sent a request and how: the connection's address, scheme and Host, or through a configured proxy what it forwarded. The base URL in pages and setup scripts, isHTTPS for cookies, and the per-address limits of auth and the sign-in forms use it. /setup scripts are no-store. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
123 lines
2.9 KiB
Go
123 lines
2.9 KiB
Go
// Package clientip says who sent a request and how, believing a proxy's
|
|
// X-Forwarded-* headers only when the request came from a proxy the
|
|
// configuration names.
|
|
//
|
|
// Believed from anyone, X-Forwarded-Host decides the address written into the
|
|
// setup scripts clients pipe to a shell, and X-Forwarded-For lets one client
|
|
// claim to be any number of others to whatever counts attempts per address.
|
|
// Believed from nobody, every client behind a proxy is one address, and a
|
|
// limit per address is a limit on all of them together.
|
|
package clientip
|
|
|
|
import (
|
|
"net"
|
|
"net/http"
|
|
"net/netip"
|
|
"strings"
|
|
"sync/atomic"
|
|
)
|
|
|
|
var trusted atomic.Pointer[[]netip.Prefix]
|
|
|
|
// SetTrusted names the proxies whose forwarding headers are believed:
|
|
// addresses or CIDR prefixes. Anything unparsable is skipped and returned.
|
|
func SetTrusted(list []string) (skipped []string) {
|
|
var out []netip.Prefix
|
|
for _, s := range list {
|
|
s = strings.TrimSpace(s)
|
|
if p, err := netip.ParsePrefix(s); err == nil {
|
|
out = append(out, p.Masked())
|
|
continue
|
|
}
|
|
if a, err := netip.ParseAddr(s); err == nil {
|
|
out = append(out, netip.PrefixFrom(a, a.BitLen()))
|
|
continue
|
|
}
|
|
skipped = append(skipped, s)
|
|
}
|
|
trusted.Store(&out)
|
|
return skipped
|
|
}
|
|
|
|
func isTrusted(addr string) bool {
|
|
a, err := netip.ParseAddr(addr)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
a = a.Unmap()
|
|
list := trusted.Load()
|
|
if list == nil {
|
|
return false
|
|
}
|
|
for _, p := range *list {
|
|
if p.Contains(a) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// peer is the address the connection came from.
|
|
func peer(r *http.Request) string {
|
|
host, _, err := net.SplitHostPort(r.RemoteAddr)
|
|
if err != nil {
|
|
return r.RemoteAddr
|
|
}
|
|
return host
|
|
}
|
|
|
|
// fromProxy reports whether the request came through a trusted proxy.
|
|
func fromProxy(r *http.Request) bool { return isTrusted(peer(r)) }
|
|
|
|
// Addr is the client's address: the connection's, or through trusted
|
|
// proxies the nearest address in X-Forwarded-For that is not one of them.
|
|
func Addr(r *http.Request) string {
|
|
p := peer(r)
|
|
if !isTrusted(p) {
|
|
return p
|
|
}
|
|
hops := strings.Split(r.Header.Get("X-Forwarded-For"), ",")
|
|
for i := len(hops) - 1; i >= 0; i-- {
|
|
h := strings.TrimSpace(hops[i])
|
|
if h == "" {
|
|
continue
|
|
}
|
|
if !isTrusted(h) {
|
|
return h
|
|
}
|
|
}
|
|
return p
|
|
}
|
|
|
|
// Proto is "https" when the client spoke TLS: to this server, or to a
|
|
// trusted proxy that says so.
|
|
func Proto(r *http.Request) string {
|
|
if r.TLS != nil {
|
|
return "https"
|
|
}
|
|
if fromProxy(r) {
|
|
if v := first(r.Header.Get("X-Forwarded-Proto")); v != "" {
|
|
return strings.ToLower(v)
|
|
}
|
|
}
|
|
return "http"
|
|
}
|
|
|
|
// Host is the host the client asked for: Host, or what a trusted proxy says
|
|
// it was asked for.
|
|
func Host(r *http.Request) string {
|
|
if fromProxy(r) {
|
|
if v := first(r.Header.Get("X-Forwarded-Host")); v != "" {
|
|
return v
|
|
}
|
|
}
|
|
return r.Host
|
|
}
|
|
|
|
func first(v string) string {
|
|
if i := strings.IndexByte(v, ','); i >= 0 {
|
|
v = v[:i]
|
|
}
|
|
return strings.TrimSpace(v)
|
|
}
|