Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,14 @@ Compresses response with gzip.

### RealIP middleware

RealIP is a middleware that sets a http.Request's RemoteAddr to the results of parsing either the X-Forwarded-For or X-Real-IP headers.
RealIP is a middleware that sets a http.Request's RemoteAddr to the results of parsing various headers that contain the client's real IP address. It checks headers in the following priority order:

1. `X-Real-IP` - trusted proxy (nginx/reproxy) sets this to actual client
2. `CF-Connecting-IP` - Cloudflare's header for original client
3. `X-Forwarded-For` - leftmost public IP (original client in CDN/proxy chain)
4. `RemoteAddr` - fallback for direct connections

Only public IPs are accepted from headers; private/loopback/link-local IPs are skipped. This makes the middleware compatible with CDN setups like Cloudflare where the leftmost IP in `X-Forwarded-For` is the actual client.

### Maybe middleware

Expand Down
2 changes: 1 addition & 1 deletion benchmarks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ func TestBenchmark_CustomTimeRange(t *testing.T) {
bench := NewBenchmarks().WithTimeRange(tt.timeRange)
now := time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC)

// Add data points
// add data points
for i := 0; i < tt.dataPoints; i++ {
bench.nowFn = func() time.Time {
return now.Add(time.Duration(i) * time.Second)
Expand Down
4 changes: 2 additions & 2 deletions logger/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -220,8 +220,8 @@ func (l *Middleware) getBody(r *http.Request) string {

// "The Server will close the request body. The ServeHTTP Handler does not need to."
// https://golang.org/pkg/net/http/#Request
// So we can use ioutil.NopCloser() to make io.ReadCloser.
// Note that below assignment is not approved by the docs:
// so we can use ioutil.NopCloser() to make io.ReadCloser.
// note that below assignment is not approved by the docs:
// "Except for reading the body, handlers should not modify the provided Request."
// https://golang.org/pkg/net/http/#Handler
r.Body = io.NopCloser(reader)
Expand Down
13 changes: 9 additions & 4 deletions middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ func Wrap(handler http.Handler, mws ...func(http.Handler) http.Handler) http.Han
return handler
}


// AppInfo adds custom app-info to the response header
func AppInfo(app, author, version string) func(http.Handler) http.Handler {
f := func(h http.Handler) http.Handler {
Expand Down Expand Up @@ -146,13 +145,19 @@ func Maybe(mw func(http.Handler) http.Handler, maybeFn func(r *http.Request) boo
}
}

// RealIP is a middleware that sets a http.Request's RemoteAddr to the results
// of parsing either the X-Forwarded-For or X-Real-IP headers.
// RealIP is a middleware that sets a http.Request's RemoteAddr to the client's real IP.
// It checks headers in the following priority order:
// 1. X-Real-IP - trusted proxy (nginx/reproxy) sets this to actual client
// 2. CF-Connecting-IP - Cloudflare's header for original client
// 3. X-Forwarded-For - leftmost public IP (original client in CDN/proxy chain)
// 4. RemoteAddr - fallback for direct connections
//
// Only public IPs are accepted from headers; private/loopback/link-local IPs are skipped.
//
// This middleware should only be used if user can trust the headers sent with request.
// If reverse proxies are configured to pass along arbitrary header values from the client,
// or if this middleware used without a reverse proxy, malicious clients could set anything
// as X-Forwarded-For header and attack the server in various ways.
// as these headers and spoof their IP address.
func RealIP(h http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
if rip, err := realip.Get(r); err == nil {
Expand Down
13 changes: 7 additions & 6 deletions nocache.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,21 +29,22 @@ var etagHeaders = []string{
// a router (or subrouter) from being cached by an upstream proxy and/or client.
//
// As per http://wiki.nginx.org/HttpProxyModule - NoCache sets:
// Expires: Thu, 01 Jan 1970 00:00:00 UTC
// Cache-Control: no-cache, private, max-age=0
// X-Accel-Expires: 0
// Pragma: no-cache (for HTTP/1.0 proxies/clients)
//
// Expires: Thu, 01 Jan 1970 00:00:00 UTC
// Cache-Control: no-cache, private, max-age=0
// X-Accel-Expires: 0
// Pragma: no-cache (for HTTP/1.0 proxies/clients)
func NoCache(h http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {

// Delete any ETag headers that may have been set
// delete any ETag headers that may have been set
for _, v := range etagHeaders {
if r.Header.Get(v) != "" {
r.Header.Del(v)
}
}

// Set our NoCache headers
// set our NoCache headers
for k, v := range noCacheHeaders {
w.Header().Set(k, v)
}
Expand Down
82 changes: 53 additions & 29 deletions realip/real.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,49 +33,73 @@ var privateRanges = []ipRange{
{start: net.ParseIP("fe80::"), end: net.ParseIP("febf:ffff:ffff:ffff:ffff:ffff:ffff:ffff")},
}

// Get returns real ip from the given request
// Prioritize public IPs over private IPs
// Get returns real IP from the given request.
// It checks headers in the following priority order:
// 1. X-Real-IP - trusted proxy (nginx/reproxy) sets this to actual client
// 2. CF-Connecting-IP - Cloudflare's header for original client
// 3. X-Forwarded-For - leftmost public IP (original client in CDN chain)
// 4. RemoteAddr - fallback for direct connections
//
// Only public IPs are accepted from headers; private/loopback/link-local IPs are skipped.
func Get(r *http.Request) (string, error) {
var firstIP string
for _, h := range []string{"X-Forwarded-For", "X-Real-Ip"} {
addresses := strings.Split(r.Header.Get(h), ",")
for i := len(addresses) - 1; i >= 0; i-- {
ip := strings.TrimSpace(addresses[i])
realIP := net.ParseIP(ip)
if firstIP == "" && realIP != nil {
firstIP = ip
}
// Guard against nil realIP
if realIP == nil || !realIP.IsGlobalUnicast() || isPrivateSubnet(realIP) {
continue
// check X-Real-IP first (single value, set by trusted proxy)
if xRealIP := strings.TrimSpace(r.Header.Get("X-Real-IP")); xRealIP != "" {
if ip := net.ParseIP(xRealIP); isPublicIP(ip) {
return xRealIP, nil
}
}

// check CF-Connecting-IP (Cloudflare's header)
if cfIP := strings.TrimSpace(r.Header.Get("CF-Connecting-IP")); cfIP != "" {
if ip := net.ParseIP(cfIP); isPublicIP(ip) {
return cfIP, nil
}
}

// check X-Forwarded-For, find leftmost public IP
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
addresses := strings.Split(xff, ",")
for _, addr := range addresses {
ip := strings.TrimSpace(addr)
if parsedIP := net.ParseIP(ip); isPublicIP(parsedIP) {
return ip, nil
}
return ip, nil
}
}

if firstIP != "" {
return firstIP, nil
// fall back to RemoteAddr
return parseRemoteAddr(r.RemoteAddr)
}

// isPublicIP checks if the IP is a valid public (globally routable) IP address.
func isPublicIP(ip net.IP) bool {
if ip == nil {
return false
}
if !ip.IsGlobalUnicast() {
return false
}
return !isPrivateSubnet(ip)
}

// handle RemoteAddr which may be just an IP or IP:port
remoteIP := r.RemoteAddr
// parseRemoteAddr extracts and validates IP from RemoteAddr (handles both "ip" and "ip:port" formats).
func parseRemoteAddr(remoteAddr string) (string, error) {
if remoteAddr == "" {
return "", fmt.Errorf("empty remote address")
}

// try to extract host from host:port format
host, _, err := net.SplitHostPort(remoteIP)
host, _, err := net.SplitHostPort(remoteAddr)
if err == nil {
remoteIP = host
remoteAddr = host
}

// at this point remoteIP could be either:
// 1. the host part extracted from host:port
// 2. yhe original RemoteAddr if it doesn't contain a port

// try to parse it as a valid IP address
if netIP := net.ParseIP(remoteIP); netIP == nil {
return "", fmt.Errorf("no valid ip found in %q", r.RemoteAddr)
// validate it's a proper IP address
if netIP := net.ParseIP(remoteAddr); netIP == nil {
return "", fmt.Errorf("no valid ip found in %q", remoteAddr)
}

return remoteIP, nil
return remoteAddr, nil
}

// isPrivateSubnet - check to see if this ip is in a private subnet
Expand Down
Loading
Loading