feat: add trusted proxy identity and overload stats
This commit is contained in:
105
httpx/clientIP.go
Normal file
105
httpx/clientIP.go
Normal file
@@ -0,0 +1,105 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user