106 lines
3.1 KiB
Go
106 lines
3.1 KiB
Go
package httpx
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"net/netip"
|
|
"strings"
|
|
)
|
|
|
|
// ClientIPResolver trusts X-Forwarded-For only from explicitly configured
|
|
// proxy addresses. It is immutable and safe to share between requests.
|
|
// The zero value (and a nil resolver) trusts no proxy.
|
|
type ClientIPResolver struct {
|
|
trusted []netip.Prefix
|
|
}
|
|
|
|
// NewClientIPResolver accepts IP addresses or CIDR networks, never hostnames.
|
|
func NewClientIPResolver(proxies ...string) (*ClientIPResolver, error) {
|
|
r := &ClientIPResolver{}
|
|
for _, value := range proxies {
|
|
value = strings.TrimSpace(value)
|
|
if address, err := netip.ParseAddr(value); err == nil && address.Zone() == "" {
|
|
address = address.Unmap()
|
|
r.trusted = append(r.trusted, netip.PrefixFrom(address, address.BitLen()))
|
|
continue
|
|
}
|
|
prefix, err := netip.ParsePrefix(value)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid trusted proxy %q: expected an IP address or CIDR", value)
|
|
}
|
|
if prefix.Addr().Is4In6() {
|
|
if prefix.Bits() < 96 {
|
|
return nil, fmt.Errorf("invalid trusted proxy %q: mapped IPv4 prefix must be at least /96", value)
|
|
}
|
|
prefix = netip.PrefixFrom(prefix.Addr().Unmap(), prefix.Bits()-96)
|
|
}
|
|
r.trusted = append(r.trusted, prefix.Masked())
|
|
}
|
|
return r, nil
|
|
}
|
|
|
|
func (r *ClientIPResolver) trusts(address netip.Addr) bool {
|
|
if r != nil {
|
|
for _, prefix := range r.trusted {
|
|
if prefix.Contains(address) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// ClientIP walks X-Forwarded-For from right to left, stopping at the first
|
|
// untrusted address. Missing or malformed forwarding data falls back to the
|
|
// socket peer. X-Real-IP, Forwarded and provider-specific headers are ignored.
|
|
func (r *ClientIPResolver) ClientIP(request *http.Request) string {
|
|
if request == nil {
|
|
return ""
|
|
}
|
|
host := request.RemoteAddr
|
|
if value, _, err := net.SplitHostPort(host); err == nil {
|
|
host = value
|
|
}
|
|
peer, err := netip.ParseAddr(host)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
peer = peer.WithZone("").Unmap()
|
|
if !r.trusts(peer) {
|
|
return peer.String()
|
|
}
|
|
values := request.Header.Values("X-Forwarded-For")
|
|
var chain []netip.Addr
|
|
for _, value := range values {
|
|
for _, part := range strings.Split(value, ",") {
|
|
address, err := netip.ParseAddr(strings.TrimSpace(part))
|
|
if err != nil || address.Zone() != "" {
|
|
return peer.String()
|
|
}
|
|
chain = append(chain, address.Unmap())
|
|
}
|
|
}
|
|
current := peer
|
|
for i := len(chain) - 1; i >= 0 && r.trusts(current); i-- {
|
|
current = chain[i]
|
|
}
|
|
return current.String()
|
|
}
|
|
|
|
type clientIPResolverKey struct{}
|
|
|
|
// WithClientIPResolver configures ClientIP for Context values derived from ctx.
|
|
// Adapters should set this before running application middleware.
|
|
func WithClientIPResolver(ctx context.Context, resolver *ClientIPResolver) context.Context {
|
|
return context.WithValue(ctx, clientIPResolverKey{}, resolver)
|
|
}
|
|
|
|
// ClientIP returns the verified client address without a port. By default it
|
|
// returns the socket peer; App's trusted proxy configuration enables XFF.
|
|
func (c *Context) ClientIP() string {
|
|
resolver, _ := c.Context().Value(clientIPResolverKey{}).(*ClientIPResolver)
|
|
return resolver.ClientIP(c.request)
|
|
}
|