165 lines · 4.5 KB · Go
RawDownload
// 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}