1package middleware
2
3import (
4 "net/http"
5 "time"
6
7 "gordon/internal/logging"
8 "github.com/rs/zerolog/log"
9)
10
11// ResponseWriter wraps http.ResponseWriter to capture status code and bytes written
12type ResponseWriter struct {
13 http.ResponseWriter
14 statusCode int
15 bytes int
16}
17
18func NewResponseWriter(w http.ResponseWriter) *ResponseWriter {
19 return &ResponseWriter{
20 ResponseWriter: w,
21 statusCode: http.StatusOK, // Default status code
22 }
23}
24
25func (rw *ResponseWriter) WriteHeader(code int) {
26 rw.statusCode = code
27 rw.ResponseWriter.WriteHeader(code)
28}
29
30func (rw *ResponseWriter) Write(b []byte) (int, error) {
31 n, err := rw.ResponseWriter.Write(b)
32 rw.bytes += n
33 return n, err
34}
35
36func (rw *ResponseWriter) StatusCode() int {
37 return rw.statusCode
38}
39
40func (rw *ResponseWriter) BytesWritten() int {
41 return rw.bytes
42}
43
44// RequestLogger is a middleware that logs HTTP requests
45func RequestLogger(next http.Handler) http.Handler {
46 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
47 start := time.Now()
48
49 // Wrap the response writer to capture status and bytes
50 rw := NewResponseWriter(w)
51
52 // Get client IP (handle X-Forwarded-For and X-Real-IP headers)
53 clientIP := getClientIP(r)
54
55 // Call the next handler
56 next.ServeHTTP(rw, r)
57
58 // Calculate duration
59 duration := time.Since(start)
60
61 // Log the request using proxy logger
62 logging.ProxyLogger.Info().
63 Str("method", r.Method).
64 Str("path", r.URL.Path).
65 Str("query", r.URL.RawQuery).
66 Str("host", r.Host).
67 Str("user_agent", r.UserAgent()).
68 Str("referer", r.Referer()).
69 Str("client_ip", clientIP).
70 Int("status", rw.StatusCode()).
71 Int("bytes", rw.BytesWritten()).
72 Dur("duration", duration).
73 Str("proto", r.Proto).
74 Msg("HTTP Request")
75 })
76}
77
78// getClientIP extracts the client IP from various headers
79func getClientIP(r *http.Request) string {
80 // Check X-Forwarded-For header (comma-separated list, first is original client)
81 if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
82 // Take the first IP (original client)
83 for idx := 0; idx < len(xff); idx++ {
84 if xff[idx] == ',' {
85 return xff[:idx]
86 }
87 }
88 return xff
89 }
90
91 // Check X-Real-IP header
92 if xri := r.Header.Get("X-Real-IP"); xri != "" {
93 return xri
94 }
95
96 // Check X-Forwarded header (less common)
97 if xf := r.Header.Get("X-Forwarded"); xf != "" {
98 return xf
99 }
100
101 // Fallback to RemoteAddr
102 return r.RemoteAddr
103}
104
105// ContainerRequestLogger logs requests with container-specific information
106func ContainerRequestLogger(containerID string, domain string) func(http.Handler) http.Handler {
107 return func(next http.Handler) http.Handler {
108 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
109 start := time.Now()
110
111 // Wrap the response writer to capture status and bytes
112 rw := NewResponseWriter(w)
113
114 // Get client IP
115 clientIP := getClientIP(r)
116
117 // Call the next handler
118 next.ServeHTTP(rw, r)
119
120 // Calculate duration
121 duration := time.Since(start)
122
123 // Log the request with container information using proxy logger
124 logging.ProxyLogger.Info().
125 Str("method", r.Method).
126 Str("path", r.URL.Path).
127 Str("query", r.URL.RawQuery).
128 Str("host", r.Host).
129 Str("domain", domain).
130 Str("container_id", containerID).
131 Str("user_agent", r.UserAgent()).
132 Str("referer", r.Referer()).
133 Str("client_ip", clientIP).
134 Int("status", rw.StatusCode()).
135 Int("bytes", rw.BytesWritten()).
136 Dur("duration", duration).
137 Str("proto", r.Proto).
138 Msg("Container Request")
139 })
140 }
141}
142
143// PanicRecovery middleware recovers from panics and logs them
144func PanicRecovery(next http.Handler) http.Handler {
145 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
146 defer func() {
147 if err := recover(); err != nil {
148 log.Error().
149 Interface("panic", err).
150 Str("method", r.Method).
151 Str("path", r.URL.Path).
152 Str("host", r.Host).
153 Msg("Panic recovered")
154
155 http.Error(w, "Internal Server Error", http.StatusInternalServerError)
156 }
157 }()
158
159 next.ServeHTTP(w, r)
160 })
161}
162
163// CORS middleware adds CORS headers
164func CORS(next http.Handler) http.Handler {
165 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
166 w.Header().Set("Access-Control-Allow-Origin", "*")
167 w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
168 w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
169
170 // Handle preflight OPTIONS request
171 if r.Method == "OPTIONS" {
172 w.WriteHeader(http.StatusOK)
173 return
174 }
175
176 next.ServeHTTP(w, r)
177 })
178}
179
180// Chain combines multiple middleware functions
181func Chain(middlewares ...func(http.Handler) http.Handler) func(http.Handler) http.Handler {
182 return func(final http.Handler) http.Handler {
183 for i := len(middlewares) - 1; i >= 0; i-- {
184 final = middlewares[i](final)
185 }
186 return final
187 }
188}