Resolve real client IP behind trusted proxies (closes #94)
check / check (push) Successful in 2m31s
check / check (push) Successful in 2m31s
RFC1918 ranges are the default trusted proxy set on an omitted key; an explicit list replaces the default; an explicit empty list trusts no one; unparseable values abort startup; forwarded headers honored only from trusted peers. Independent review passed: #127 (comment) model: claude-opus-4-8 (implementation and review); merged by claude-fable-5
This commit was merged in pull request #127.
This commit is contained in:
@@ -3,7 +3,6 @@ package middleware
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
@@ -14,6 +13,7 @@ import (
|
||||
ghmm "github.com/slok/go-http-metrics/middleware"
|
||||
"github.com/slok/go-http-metrics/middleware/std"
|
||||
"go.uber.org/fx"
|
||||
"sneak.berlin/go/pixa/internal/clientip"
|
||||
"sneak.berlin/go/pixa/internal/config"
|
||||
"sneak.berlin/go/pixa/internal/logger"
|
||||
)
|
||||
@@ -58,31 +58,34 @@ type Params struct {
|
||||
|
||||
// Middleware provides HTTP middleware functions.
|
||||
type Middleware struct {
|
||||
log *slog.Logger
|
||||
config *config.Config
|
||||
log *slog.Logger
|
||||
config *config.Config
|
||||
clientIP *clientip.Resolver
|
||||
}
|
||||
|
||||
// New creates a new Middleware instance.
|
||||
func New(_ fx.Lifecycle, params Params) (*Middleware, error) {
|
||||
s := &Middleware{
|
||||
log: params.Logger.Get(),
|
||||
config: params.Config,
|
||||
log: params.Logger.Get(),
|
||||
config: params.Config,
|
||||
clientIP: clientip.NewResolver(params.Config.TrustedProxies),
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func ipFromHostPort(hp string) string {
|
||||
h, _, err := net.SplitHostPort(hp)
|
||||
if err != nil {
|
||||
return ""
|
||||
// ClientIP returns a middleware that resolves the real client IP,
|
||||
// honoring X-Forwarded-For only from trusted proxies, and stores it in
|
||||
// the request context for the logging middleware and handlers to read.
|
||||
func (s *Middleware) ClientIP() func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ip := s.clientIP.Resolve(
|
||||
r.RemoteAddr, r.Header.Values(clientip.ForwardedForHeader))
|
||||
ctx := clientip.WithClientIP(r.Context(), ip)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
|
||||
if len(h) > 0 && h[0] == '[' {
|
||||
return h[1 : len(h)-1]
|
||||
}
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
type loggingResponseWriter struct {
|
||||
@@ -127,7 +130,7 @@ func (s *Middleware) Logging() func(http.Handler) http.Handler {
|
||||
"request_id", reqID,
|
||||
"referer", r.Referer(),
|
||||
"proto", r.Proto,
|
||||
"remoteIP", ipFromHostPort(r.RemoteAddr),
|
||||
"remoteIP", clientip.FromContext(ctx),
|
||||
"status", lrw.statusCode,
|
||||
"response_bytes", lrw.bytesWritten,
|
||||
"latency_ms", latency.Milliseconds(),
|
||||
|
||||
Reference in New Issue
Block a user