233 lines · 6.7 KB · Go
RawDownload
package api import (	"net/http"	"strings" 	"github.com/gin-gonic/gin"	codexlive "github.com/router-for-me/CLIProxyAPI/v7/internal/client/codex/live"	"github.com/router-for-me/CLIProxyAPI/v7/internal/config"	"github.com/router-for-me/CLIProxyAPI/v7/internal/home"	"github.com/router-for-me/CLIProxyAPI/v7/internal/logging"	"github.com/router-for-me/CLIProxyAPI/v7/internal/safemode"	sdkaccess "github.com/router-for-me/CLIProxyAPI/v7/sdk/access"	log "github.com/sirupsen/logrus") var corsExposedResponseHeaders = []string{	logging.CPATraceIDHeader,	"X-CPA-VERSION",	"X-CPA-COMMIT",	"X-CPA-BUILD-DATE",	"X-CPA-SUPPORT-PLUGIN",	"X-CPA-HOME-VERSION",	"X-CPA-HOME-BUILD-DATE",	"X-SERVER-VERSION",	"X-SERVER-BUILD-DATE",	"Location",	"Retry-After",	"X-Request-Id",	"OpenAI-Request-Id",} var corsExposedResponseHeadersJoined = strings.Join(corsExposedResponseHeaders, ", ") const (	exampleAPIKeyManagementPath = "/management.html"	exampleAPIKeyManagementURL  = "/management.html?safe-mode=configure") func (s *Server) homeHeartbeatMiddleware() gin.HandlerFunc {	return func(c *gin.Context) {		if s == nil || s.cfg == nil || !s.cfg.Home.Enabled {			c.Next()			return		}		if c != nil && c.Request != nil {			path := c.Request.URL.Path			if strings.HasPrefix(path, "/v0/management/") || path == "/v0/management" || strings.HasPrefix(path, "/v0/resource/plugins/") || path == "/management.html" {				c.Next()				return			}		}		client := home.Current()		if client == nil || !client.HeartbeatOK() {			c.AbortWithStatus(http.StatusServiceUnavailable)			return		}		c.Next()	}} func (s *Server) exampleAPIKeySafeModeRequired(cfg *config.Config) bool {	return s != nil && s.exampleAPIKeySafeModeEnabled && cfg != nil && safemode.HasExampleAPIKeys(cfg.APIKeys)} func (s *Server) exampleAPIKeySafeModeMiddleware() gin.HandlerFunc {	return func(c *gin.Context) {		if s == nil || !s.exampleAPIKeySafeModeActive.Load() || c == nil || c.Request == nil || c.Request.URL == nil {			c.Next()			return		} 		path := c.Request.URL.Path		if path == exampleAPIKeyManagementPath && c.Query("safe-mode") == "configure" {			c.Next()			return		}		if (path == "/" || path == exampleAPIKeyManagementPath) && (c.Request.Method == http.MethodGet || c.Request.Method == http.MethodHead) {			s.serveExampleAPIKeyWarningPage(c)			return		}		if !isExampleAPIKeySafeModeProxyPath(path) {			c.Next()			return		} 		c.Header("X-CPA-SAFE-MODE", "example-api-key")		c.AbortWithStatusJSON(http.StatusForbidden, gin.H{			"error":   "unsafe_example_api_key",			"message": "Proxy API endpoints are disabled because api-keys contains template values. Open /management.html?safe-mode=configure, update api-keys in Management, then retry.",		})	}} func (s *Server) serveExampleAPIKeyWarningPage(c *gin.Context) {	cfg := s.cfg	var keys []string	if cfg != nil {		keys = safemode.ExampleAPIKeys(cfg.APIKeys)	}	c.Header("Content-Type", "text/html; charset=utf-8")	c.Header("Cache-Control", "no-store")	if c.Request.Method == http.MethodHead {		c.Status(http.StatusOK)		c.Abort()		return	}	c.String(http.StatusOK, safemode.ExampleAPIKeyWarningPageHTML(keys, exampleAPIKeyManagementURL))	c.Abort()} func isExampleAPIKeySafeModeProxyPath(path string) bool {	switch {	case path == "/v1" || strings.HasPrefix(path, "/v1/"):		return true	case path == "/v1beta" || strings.HasPrefix(path, "/v1beta/"):		return true	case path == "/openai/v1" || strings.HasPrefix(path, "/openai/v1/"):		return true	case path == "/backend-api/codex" || strings.HasPrefix(path, "/backend-api/codex/"):		return true	default:		return false	}} // corsMiddleware returns a Gin middleware handler that adds CORS headers// to every response, allowing cross-origin requests.//// Returns://   - gin.HandlerFunc: The CORS middleware handlerfunc corsMiddleware() gin.HandlerFunc {	return func(c *gin.Context) {		c.Header("Access-Control-Allow-Origin", "*")		c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS")		c.Header("Access-Control-Allow-Headers", "*")		c.Header("Access-Control-Expose-Headers", corsExposedResponseHeadersJoined) 		if c.Request.Method == "OPTIONS" {			c.AbortWithStatus(http.StatusNoContent)			return		} 		c.Next()	}} // AuthMiddleware returns a Gin middleware handler that authenticates requests// using the configured authentication providers. When no providers are available,// it allows all requests (legacy behaviour).func AuthMiddleware(manager *sdkaccess.Manager) gin.HandlerFunc {	return accessAuthMiddleware(manager, false)} func realtimeStandardAuthMiddleware(manager *sdkaccess.Manager) gin.HandlerFunc {	return accessAuthMiddleware(manager, true)} func accessAuthMiddleware(manager *sdkaccess.Manager, realtimeError bool) gin.HandlerFunc {	return func(c *gin.Context) {		if manager == nil {			c.Next()			return		} 		result, err := manager.Authenticate(c.Request.Context(), c.Request)		if err == nil {			if result != nil {				c.Set("userApiKey", result.Principal)				c.Set("accessProvider", result.Provider)				if len(result.Metadata) > 0 {					c.Set("accessMetadata", result.Metadata)				}			}			c.Next()			return		} 		statusCode := err.HTTPStatusCode()		if statusCode >= http.StatusInternalServerError {			log.Errorf("authentication middleware error: %v", err)		}		if realtimeError {			errorType := "authentication_error"			code := "invalid_api_key"			if statusCode >= http.StatusInternalServerError {				errorType = "server_error"				code = "authentication_service_error"			}			c.AbortWithStatusJSON(statusCode, gin.H{"error": gin.H{				"message": err.Message,				"type":    errorType,				"param":   nil,				"code":    code,			}})			return		}		c.AbortWithStatusJSON(statusCode, gin.H{"error": err.Message})	}} func realtimeAuthMiddleware(manager *sdkaccess.Manager, handler *codexlive.Handler) gin.HandlerFunc {	fallback := realtimeStandardAuthMiddleware(manager)	return func(c *gin.Context) {		authorization, matched, errAuthenticate := handler.AuthenticateClientSecret(c.Request)		if !matched {			fallback(c)			return		}		if errAuthenticate != nil {			c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": gin.H{				"message": errAuthenticate.Error(),				"type":    "invalid_request_error",				"param":   nil,				"code":    "invalid_realtime_client_secret",			}})			return		}		principal := authorization.IssuerPrincipal		if principal == "" {			principal = authorization.Principal		}		provider := authorization.IssuerProvider		if provider == "" {			provider = "realtime-client-secret"		}		c.Set("userApiKey", principal)		c.Set("accessProvider", provider)		c.Set(codexlive.ClientSecretSessionContextKey, authorization.Session)		c.Set(codexlive.ClientSecretPrincipalContextKey, authorization.Principal)		c.Next()	}}