Spaces:
Paused
Paused
| // Package middleware provides HTTP middleware components for the CLI Proxy API server. | |
| // This file contains the request logging middleware that captures comprehensive | |
| // request and response data when enabled through configuration. | |
| package middleware | |
| import ( | |
| "bytes" | |
| "io" | |
| "net/http" | |
| "strings" | |
| "time" | |
| "github.com/gin-gonic/gin" | |
| "github.com/router-for-me/CLIProxyAPI/v6/internal/logging" | |
| "github.com/router-for-me/CLIProxyAPI/v6/internal/util" | |
| ) | |
| const maxErrorOnlyCapturedRequestBodyBytes int64 = 1 << 20 // 1 MiB | |
| // RequestLoggingMiddleware creates a Gin middleware that logs HTTP requests and responses. | |
| // It captures detailed information about the request and response, including headers and body, | |
| // and uses the provided RequestLogger to record this data. When full request logging is disabled, | |
| // body capture is limited to small known-size payloads to avoid large per-request memory spikes. | |
| func RequestLoggingMiddleware(logger logging.RequestLogger) gin.HandlerFunc { | |
| return func(c *gin.Context) { | |
| if logger == nil { | |
| c.Next() | |
| return | |
| } | |
| if shouldSkipMethodForRequestLogging(c.Request) { | |
| c.Next() | |
| return | |
| } | |
| path := c.Request.URL.Path | |
| if !shouldLogRequest(path) { | |
| c.Next() | |
| return | |
| } | |
| loggerEnabled := logger.IsEnabled() | |
| // Capture request information | |
| requestInfo, err := captureRequestInfo(c, shouldCaptureRequestBody(loggerEnabled, c.Request)) | |
| if err != nil { | |
| // Log error but continue processing | |
| // In a real implementation, you might want to use a proper logger here | |
| c.Next() | |
| return | |
| } | |
| // Create response writer wrapper | |
| wrapper := NewResponseWriterWrapper(c.Writer, logger, requestInfo) | |
| if !loggerEnabled { | |
| wrapper.logOnErrorOnly = true | |
| } | |
| c.Writer = wrapper | |
| // Process the request | |
| c.Next() | |
| // Finalize logging after request processing | |
| if err = wrapper.Finalize(c); err != nil { | |
| // Log error but don't interrupt the response | |
| // In a real implementation, you might want to use a proper logger here | |
| } | |
| } | |
| } | |
| func shouldSkipMethodForRequestLogging(req *http.Request) bool { | |
| if req == nil { | |
| return true | |
| } | |
| if req.Method != http.MethodGet { | |
| return false | |
| } | |
| return !isResponsesWebsocketUpgrade(req) | |
| } | |
| func isResponsesWebsocketUpgrade(req *http.Request) bool { | |
| if req == nil || req.URL == nil { | |
| return false | |
| } | |
| if req.URL.Path != "/v1/responses" { | |
| return false | |
| } | |
| return strings.EqualFold(strings.TrimSpace(req.Header.Get("Upgrade")), "websocket") | |
| } | |
| func shouldCaptureRequestBody(loggerEnabled bool, req *http.Request) bool { | |
| if loggerEnabled { | |
| return true | |
| } | |
| if req == nil || req.Body == nil { | |
| return false | |
| } | |
| contentType := strings.ToLower(strings.TrimSpace(req.Header.Get("Content-Type"))) | |
| if strings.HasPrefix(contentType, "multipart/form-data") { | |
| return false | |
| } | |
| if req.ContentLength <= 0 { | |
| return false | |
| } | |
| return req.ContentLength <= maxErrorOnlyCapturedRequestBodyBytes | |
| } | |
| // captureRequestInfo extracts relevant information from the incoming HTTP request. | |
| // It captures the URL, method, headers, and body. The request body is read and then | |
| // restored so that it can be processed by subsequent handlers. | |
| func captureRequestInfo(c *gin.Context, captureBody bool) (*RequestInfo, error) { | |
| // Capture URL with sensitive query parameters masked | |
| maskedQuery := util.MaskSensitiveQuery(c.Request.URL.RawQuery) | |
| url := c.Request.URL.Path | |
| if maskedQuery != "" { | |
| url += "?" + maskedQuery | |
| } | |
| // Capture method | |
| method := c.Request.Method | |
| // Capture headers | |
| headers := make(map[string][]string) | |
| for key, values := range c.Request.Header { | |
| headers[key] = values | |
| } | |
| // Capture request body | |
| var body []byte | |
| if captureBody && c.Request.Body != nil { | |
| // Read the body | |
| bodyBytes, err := io.ReadAll(c.Request.Body) | |
| if err != nil { | |
| return nil, err | |
| } | |
| // Restore the body for the actual request processing | |
| c.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) | |
| body = bodyBytes | |
| } | |
| return &RequestInfo{ | |
| URL: url, | |
| Method: method, | |
| Headers: headers, | |
| Body: body, | |
| RequestID: logging.GetGinRequestID(c), | |
| Timestamp: time.Now(), | |
| }, nil | |
| } | |
| // shouldLogRequest determines whether the request should be logged. | |
| // It skips management endpoints to avoid leaking secrets but allows | |
| // all other routes, including module-provided ones, to honor request-log. | |
| func shouldLogRequest(path string) bool { | |
| if strings.HasPrefix(path, "/v0/management") || strings.HasPrefix(path, "/management") { | |
| return false | |
| } | |
| if strings.HasPrefix(path, "/api") { | |
| return strings.HasPrefix(path, "/api/provider") | |
| } | |
| return true | |
| } | |