diff --git a/internal/handlers/auth.go b/internal/handlers/auth.go index 5e74e1f..5ea2a32 100644 --- a/internal/handlers/auth.go +++ b/internal/handlers/auth.go @@ -8,6 +8,7 @@ import ( "strconv" "time" + "sneak.berlin/go/pixa/internal/clientip" "sneak.berlin/go/pixa/internal/encurl" "sneak.berlin/go/pixa/internal/imgcache" "sneak.berlin/go/pixa/internal/templates" @@ -47,7 +48,8 @@ func (s *Handlers) handleLoginPost(w http.ResponseWriter, r *http.Request) { // Constant-time comparison to prevent timing attacks if subtle.ConstantTimeCompare([]byte(submittedKey), []byte(s.config.SigningKey)) != 1 { - s.log.Warn("failed login attempt", "remote_addr", r.RemoteAddr) + s.log.Warn("failed login attempt", + "remote_addr", clientip.FromContext(r.Context())) s.renderLogin(w, r, "Invalid signing key") return @@ -62,7 +64,8 @@ func (s *Handlers) handleLoginPost(w http.ResponseWriter, r *http.Request) { return } - s.log.Info("successful login", "remote_addr", r.RemoteAddr) + s.log.Info("successful login", + "remote_addr", clientip.FromContext(r.Context())) // Redirect to generator page http.Redirect(w, r, "/", http.StatusSeeOther) diff --git a/internal/middleware/middleware.go b/internal/middleware/middleware.go index 00d59f7..916e3c8 100644 --- a/internal/middleware/middleware.go +++ b/internal/middleware/middleware.go @@ -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(), diff --git a/internal/server/routes.go b/internal/server/routes.go index 3b7dfc5..8e41fd7 100644 --- a/internal/server/routes.go +++ b/internal/server/routes.go @@ -18,6 +18,7 @@ func (s *Server) SetupRoutes() { s.router.Use(middleware.Recoverer) s.router.Use(middleware.RequestID) + s.router.Use(s.mw.ClientIP()) s.router.Use(s.mw.SecurityHeaders()) s.router.Use(s.mw.Logging())