lightmux-contrib/realip/realip.go
juancwu b26ef7439e add realip, requestlog, recoverer middlewares
Initial implementation of lightmux-contrib, a sibling module to
lightmux that hosts opinionated middlewares with one sub-package per
middleware:

- realip: resolves the originating client IP from CF-Connecting-IP,
  True-Client-IP, X-Real-IP, or X-Forwarded-For. Optional peer-CIDR
  allowlist via netip.Prefix.
- requestlog: emits a structured http.request record (method, path,
  status, duration, client) per request via splinter.
- recoverer: catches panics, wraps with errx under op "recoverer",
  logs with stack, and writes a 500 response.

Each package exposes a single New(...) constructor returning
func(http.Handler) http.Handler. The contrib module intentionally
does not import lightmux — middlewares interoperate via the standard
stdlib middleware shape.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 14:03:04 +00:00

78 lines
1.8 KiB
Go

// Package realip resolves the originating client IP from common reverse-proxy
// and CDN headers (Cloudflare, nginx) and replaces r.RemoteAddr so downstream
// handlers and middlewares see the real client.
package realip
import (
"net"
"net/http"
"net/netip"
"strings"
)
var headers = []string{
"CF-Connecting-IP",
"True-Client-IP",
"X-Real-IP",
"X-Forwarded-For",
}
// New returns a real-IP middleware.
//
// With no trusted prefixes, it always honors the proxy headers — only register
// it when the service sits behind a trusted proxy.
//
// With one or more prefixes, the headers are honored only when the immediate
// peer (parsed from r.RemoteAddr) falls within one of them; requests from
// outside the allowlist pass through untouched.
func New(trusted ...netip.Prefix) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if len(trusted) > 0 && !peerTrusted(r.RemoteAddr, trusted) {
next.ServeHTTP(w, r)
return
}
if ip := extract(r); ip != "" {
r2 := *r
r2.RemoteAddr = ip
next.ServeHTTP(w, &r2)
return
}
next.ServeHTTP(w, r)
})
}
}
func extract(r *http.Request) string {
for _, h := range headers {
v := r.Header.Get(h)
if v == "" {
continue
}
if i := strings.IndexByte(v, ','); i >= 0 {
v = v[:i]
}
v = strings.TrimSpace(v)
if net.ParseIP(v) != nil {
return v
}
}
return ""
}
func peerTrusted(remoteAddr string, trusted []netip.Prefix) bool {
var peer netip.Addr
if ap, err := netip.ParseAddrPort(remoteAddr); err == nil {
peer = ap.Addr()
} else if a, err2 := netip.ParseAddr(remoteAddr); err2 == nil {
peer = a
} else {
return false
}
for _, p := range trusted {
if p.Contains(peer) {
return true
}
}
return false
}