| |
| |
| |
| 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 |
|
|
| |
| |
| |
| |
| 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() |
|
|
| |
| requestInfo, err := captureRequestInfo(c, shouldCaptureRequestBody(loggerEnabled, c.Request)) |
| if err != nil { |
| |
| |
| c.Next() |
| return |
| } |
|
|
| |
| wrapper := NewResponseWriterWrapper(c.Writer, logger, requestInfo) |
| if !loggerEnabled { |
| wrapper.logOnErrorOnly = true |
| } |
| c.Writer = wrapper |
|
|
| |
| c.Next() |
|
|
| |
| if err = wrapper.Finalize(c); err != nil { |
| |
| |
| } |
| } |
| } |
|
|
| 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 |
| } |
|
|
| |
| |
| |
| func captureRequestInfo(c *gin.Context, captureBody bool) (*RequestInfo, error) { |
| |
| maskedQuery := util.MaskSensitiveQuery(c.Request.URL.RawQuery) |
| url := c.Request.URL.Path |
| if maskedQuery != "" { |
| url += "?" + maskedQuery |
| } |
|
|
| |
| method := c.Request.Method |
|
|
| |
| headers := make(map[string][]string) |
| for key, values := range c.Request.Header { |
| headers[key] = values |
| } |
|
|
| |
| var body []byte |
| if captureBody && c.Request.Body != nil { |
| |
| bodyBytes, err := io.ReadAll(c.Request.Body) |
| if err != nil { |
| return nil, err |
| } |
|
|
| |
| 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 |
| } |
|
|
| |
| |
| |
| 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 |
| } |
|
|