package middleware import ( "apiserver/common" "apiserver/controllers" "apiserver/models" "apiserver/store" logger "apiserver/utils" "bytes" "encoding/json" "errors" "io" "net/http" "strings" "github.com/gin-gonic/gin" "go.uber.org/zap" ) func ValidateHandler() gin.HandlerFunc { logger.Debug("Enter ValidateHandler() function") return func(ctx *gin.Context) { if isExcludedPath(ctx.Request.URL.Path) { ctx.Next() return } validateHeader()(ctx) if ctx.IsAborted() { return } flag := !strings.Contains("multipart/form-data", ctx.Request.Header.Get("Content-Type")) if ctx.Request.Method == http.MethodPost && flag == false { isValidBody, err := validateRequestBody(ctx) if err != nil || !isValidBody { controllers.ReturnError(ctx, common.INVALID_PARAMS_CODE, common.ERROR_READ_REQUEST_BODY_MSG+": "+err.Error(), nil) ctx.Abort() return } } if ctx.IsAborted() { return } ctx.Next() } } // jwtExcludedPaths are the routes that skip JWT validation. An entry is // matched exactly, unless its last segment is a gin catch-all ("*name", as in // the route "/swagger/*any"): then any path under the part before the "*" // matches (bug-1962). var jwtExcludedPaths = []string{ "/ping", "/xce/v1/auth/logon", "/swagger/*any", "/xce/v1/auth/logout", } func isExcludedPath(path string) bool { for _, excludedPath := range jwtExcludedPaths { if i := strings.LastIndex(excludedPath, "/*"); i >= 0 { if strings.HasPrefix(path, excludedPath[:i+1]) { return true } continue } if path == excludedPath { return true } } return false } // Token validation middleware func validateHeader() gin.HandlerFunc { logger.Debug("Enter validateHeader() function") return func(ctx *gin.Context) { //continue to validate token token := ctx.GetHeader("Authorization") if token == "" { logger.Error("Token is empty") controllers.ReturnError(ctx, common.ERROR_AUTH_CHECK_TOKEN_CODE, common.ERROR_AUTH_CHECK_TOKEN_EMPTY_MSG, nil) ctx.Abort() return } // Validate token (this is a placeholder for your actual token validation logic) isValid, err := isValidToken(strings.TrimPrefix(token, "Bearer "), ctx.ClientIP()) if err != nil { logger.Error("Validate token error", zap.Error(err)) controllers.ReturnError(ctx, common.ERROR_AUTH_CHECK_TOKEN_CODE, common.ERROR_AUTH_MSG+": "+err.Error(), nil) ctx.Abort() return } if !isValid { logger.Error("Token is invalid") controllers.ReturnError(ctx, common.ERROR_AUTH_CHECK_TOKEN_CODE, common.ERROR_AUTH_MSG, nil) ctx.Abort() return } } } // isValidToken verifies the token (signature, HS256 only, expiry, not // revoked by logout) and its IP binding. func isValidToken(tokenString string, IP string) (bool, error) { // Service-account API keys are a distinct credential type from JWTs and // carry no IP binding (they're meant for long-lived automated clients // like the scheduler-agent MCP server, which may not have a stable IP). // API keys also bypass logout-revocation (they outlive JWT sessions). if strings.HasPrefix(tokenString, models.APIKeyPrefix) { _, ok := store.ValidateAPIKey(tokenString) return ok, nil } // Their hardening (bug-1945/1946/1950): HS256 pinned, default-secret // rejected, logout revocation — all centralized in ParseTokenClaims. claims, err := controllers.ParseTokenClaims(tokenString) if err != nil { return false, err } return claims.IP == IP, nil } func validateRequestBody(ctx *gin.Context) (bool, error) { logger.Debug("Enter validateRequestBody function") // read request body and resolve EOF error body, err := ctx.GetRawData() if err != nil { logger.Error("Failed to read request body", zap.Error(err)) ctx.Abort() return false, err } if len(body) == 0 { logger.Error("Request body is empty") ctx.Abort() return false, errors.New(common.ERROR_REQUEST_BODY_EMPTY_MSG) } // reset request body ctx.Request.Body = io.NopCloser(bytes.NewBuffer(body)) // check request body is valid if !json.Valid(body) { logger.Error("Request body is not valid", zap.Error(err)) ctx.Abort() return false, errors.New(common.ERROR_REQUEST_BODY_NOT_JSON_MSG) } return true, nil }