summaryrefslogtreecommitdiff
path: root/internal/api/middleware.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/api/middleware.go')
-rw-r--r--internal/api/middleware.go70
1 files changed, 70 insertions, 0 deletions
diff --git a/internal/api/middleware.go b/internal/api/middleware.go
new file mode 100644
index 0000000..76885ac
--- /dev/null
+++ b/internal/api/middleware.go
@@ -0,0 +1,70 @@
+package api
+
+import (
+ "context"
+ "net"
+ "net/http"
+ "strings"
+)
+
+// contextKey for real IP.
+type contextKey string
+
+const ctxRealIP contextKey = "real_ip"
+
+// RealIP middleware extracts the client's real public IP.
+// Priority: X-Forwarded-For (from Traefik) > X-Real-IP > RemoteAddr.
+func RealIP(next http.HandlerFunc) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ ip := extractRealIP(r)
+ if ip != "" {
+ r = r.WithContext(context.WithValue(r.Context(), ctxRealIP, ip))
+ }
+ next(w, r)
+ }
+}
+
+// GetRealIP returns the client IP from context.
+func GetRealIP(r *http.Request) string {
+ if ip, ok := r.Context().Value(ctxRealIP).(string); ok {
+ return ip
+ }
+ return ""
+}
+
+func extractRealIP(r *http.Request) string {
+ // 1. X-Forwarded-For (Traefik, nginx, etc.)
+ if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
+ // Can contain multiple IPs: client, proxy1, proxy2
+ // First one is the original client
+ parts := strings.Split(xff, ",")
+ if len(parts) > 0 {
+ ip := strings.TrimSpace(parts[0])
+ if isValidIP(ip) {
+ return ip
+ }
+ }
+ }
+
+ // 2. X-Real-IP (some proxies use this)
+ if xri := r.Header.Get("X-Real-IP"); xri != "" {
+ ip := strings.TrimSpace(xri)
+ if isValidIP(ip) {
+ return ip
+ }
+ }
+
+ // 3. RemoteAddr fallback (direct connection)
+ host, _, err := net.SplitHostPort(r.RemoteAddr)
+ if err == nil && isValidIP(host) {
+ return host
+ }
+
+ return ""
+}
+
+func isValidIP(ip string) bool {
+ // Accept both IPv4 and IPv6
+ parsed := net.ParseIP(ip)
+ return parsed != nil
+}