407 lines · 12.9 KB · Go
RawDownload
// Package api provides the HTTP API server implementation for the CLI Proxy API.// It includes the main server struct, routing setup, middleware for CORS and authentication,// and integration with various AI API handlers (OpenAI, Claude, Gemini).// The server supports hot-reloading of clients and configuration.package api import (	"context"	"crypto/tls"	"errors"	"fmt"	"net"	"net/http"	"os"	"strings"	"sync"	"sync/atomic"	"time" 	"github.com/gin-gonic/gin"	managementHandlers "github.com/router-for-me/CLIProxyAPI/v7/internal/api/handlers/management"	"github.com/router-for-me/CLIProxyAPI/v7/internal/api/middleware"	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/logging"	"github.com/router-for-me/CLIProxyAPI/v7/internal/managementasset"	"github.com/router-for-me/CLIProxyAPI/v7/internal/pluginhost"	"github.com/router-for-me/CLIProxyAPI/v7/internal/redisqueue"	sdkaccess "github.com/router-for-me/CLIProxyAPI/v7/sdk/access"	"github.com/router-for-me/CLIProxyAPI/v7/sdk/api/handlers"	"github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"	log "github.com/sirupsen/logrus"	"golang.org/x/net/http2"	"gopkg.in/yaml.v3") // Server represents the main API server.// It encapsulates the Gin engine, HTTP server, handlers, and configuration.type Server struct {	// engine is the Gin web framework engine instance.	engine *gin.Engine 	// server is the underlying HTTP server.	server *http.Server 	// muxBaseListener is the shared TCP listener used to serve both HTTP and Redis protocol traffic.	muxBaseListener net.Listener 	// muxHTTPListener receives HTTP connections selected by the multiplexer.	muxHTTPListener *muxListener 	// handlers contains the API handlers for processing requests.	handlers         *handlers.BaseAPIHandler	codexLiveHandler *codexlive.Handler 	// cfg holds the current server configuration.	cfg *config.Config 	// oldConfigYaml stores a YAML snapshot of the previous configuration for change detection.	// This prevents issues when the config object is modified in place by Management API.	oldConfigYaml []byte 	// accessManager handles request authentication providers.	accessManager *sdkaccess.Manager 	// requestLogger is the request logger instance for dynamic configuration updates.	requestLogger logging.RequestLogger	loggerToggle  func(bool) 	// configFilePath is the absolute path to the YAML config file for persistence.	configFilePath string 	// currentPath is the absolute path to the current working directory.	currentPath string 	// wsRoutes tracks registered websocket upgrade paths.	wsRouteMu     sync.Mutex	wsRoutes      map[string]struct{}	wsAuthChanged func(bool, bool)	wsAuthEnabled atomic.Bool 	// management handler	mgmt *managementHandlers.Handler 	// pluginHost owns dynamic plugin Management API route dispatch.	pluginHost *pluginhost.Host 	// managementRoutesRegistered tracks whether the management routes have been attached to the engine.	managementRoutesRegistered atomic.Bool	// managementRoutesEnabled controls whether management endpoints serve real handlers.	managementRoutesEnabled atomic.Bool 	// envManagementSecret indicates whether MANAGEMENT_PASSWORD is configured.	envManagementSecret bool 	localPassword string 	keepAliveEnabled   bool	keepAliveTimeout   time.Duration	keepAliveOnTimeout func()	keepAliveHeartbeat chan struct{}	keepAliveStop      chan struct{} 	exampleAPIKeySafeModeEnabled bool	exampleAPIKeySafeModeActive  atomic.Bool} // NewServer creates and initializes a new API server instance.// It sets up the Gin engine, middleware, routes, and handlers.//// Parameters://   - cfg: The server configuration//   - authManager: core runtime auth manager//   - accessManager: request authentication manager//// Returns://   - *Server: A new server instancefunc NewServer(cfg *config.Config, authManager *auth.Manager, accessManager *sdkaccess.Manager, configFilePath string, opts ...ServerOption) *Server {	optionState := &serverOptionConfig{		requestLoggerFactory: defaultRequestLoggerFactory,	}	for i := range opts {		opts[i](optionState)	}	// Set gin mode	if !cfg.Debug {		gin.SetMode(gin.ReleaseMode)	} 	// Create gin engine	engine := gin.New()	if optionState.engineConfigurator != nil {		optionState.engineConfigurator(engine)	} 	// Add middleware	engine.Use(logging.GinLogrusLogger())	engine.Use(logging.GinLogrusRecovery())	engine.Use(logging.CPATraceIDMiddleware())	for _, mw := range optionState.extraMiddleware {		engine.Use(mw)	} 	// Add request logging middleware (positioned after recovery, before auth)	// Resolve logs directory relative to the configuration file directory.	var requestLogger logging.RequestLogger	var toggle func(bool)	if !cfg.CommercialMode {		if optionState.requestLoggerFactory != nil {			requestLogger = optionState.requestLoggerFactory(cfg, configFilePath)		}		if requestLogger != nil {			engine.Use(middleware.RequestLoggingMiddleware(requestLogger))			if setter, ok := requestLogger.(interface{ SetEnabled(bool) }); ok {				toggle = setter.SetEnabled			}		}	} 	engine.Use(corsMiddleware())	wd, err := os.Getwd()	if err != nil {		wd = configFilePath	} 	envAdminPassword, envAdminPasswordSet := os.LookupEnv("MANAGEMENT_PASSWORD")	envAdminPassword = strings.TrimSpace(envAdminPassword)	envManagementSecret := envAdminPasswordSet && envAdminPassword != "" 	// Create server instance	s := &Server{		engine:              engine,		handlers:            handlers.NewBaseAPIHandlers(effectiveSDKConfig(cfg), authManager),		cfg:                 cfg,		accessManager:       accessManager,		requestLogger:       requestLogger,		loggerToggle:        toggle,		configFilePath:      configFilePath,		currentPath:         wd,		envManagementSecret: envManagementSecret,		wsRoutes:            make(map[string]struct{}),		pluginHost:          optionState.pluginHost, 		exampleAPIKeySafeModeEnabled: optionState.exampleAPIKeySafeMode,	}	s.wsAuthEnabled.Store(cfg.WebsocketAuth)	s.exampleAPIKeySafeModeActive.Store(s.exampleAPIKeySafeModeRequired(cfg))	s.handlers.SetPluginHost(optionState.pluginHost)	if optionState.pluginHost != nil {		optionState.pluginHost.SetModelExecutor(s.handlers)		optionState.pluginHost.SetAuthManager(authManager)	}	// Save initial YAML snapshot	s.oldConfigYaml, _ = yaml.Marshal(cfg)	s.applyAccessConfig(nil, cfg)	if authManager != nil {		authManager.SetRetryConfig(cfg.RequestRetry, time.Duration(cfg.MaxRetryInterval)*time.Second, cfg.MaxRetryCredentials)	}	managementasset.SetCurrentConfig(cfg)	auth.SetQuotaCooldownDisabled(cfg.DisableCooling)	auth.SetTransientErrorCooldownSeconds(cfg.TransientErrorCooldownSeconds)	applySignatureCacheConfig(nil, cfg)	// Initialize management handler	s.mgmt = managementHandlers.NewHandler(cfg, configFilePath, authManager)	s.mgmt.SetPluginHost(optionState.pluginHost)	s.mgmt.SetConfigReloadHook(optionState.configReloadHook)	if optionState.localPassword != "" {		s.mgmt.SetLocalPassword(optionState.localPassword)	}	logDir := logging.ResolveLogDirectory(cfg)	s.mgmt.SetLogDirectory(logDir)	if optionState.postAuthHook != nil {		s.mgmt.SetPostAuthHook(optionState.postAuthHook)	}	if optionState.postAuthPersistHook != nil {		s.mgmt.SetPostAuthPersistHook(optionState.postAuthPersistHook)	}	s.localPassword = optionState.localPassword 	// Home heartbeat gate: when home is enabled, block all endpoints with 503 until the	// subscribe-config heartbeat connection is healthy.	engine.Use(s.homeHeartbeatMiddleware())	engine.Use(s.exampleAPIKeySafeModeMiddleware()) 	// Setup routes	s.setupRoutes() 	// Apply additional router configurators from options	if optionState.routerConfigurator != nil {		optionState.routerConfigurator(engine, s.handlers, cfg)	} 	// Register management routes when configuration or environment secrets are available,	// or when a local management password is provided (e.g. TUI mode).	hasManagementSecret := cfg.RemoteManagement.SecretKey != "" || envManagementSecret || s.localPassword != ""	s.managementRoutesEnabled.Store(hasManagementSecret)	redisqueue.SetEnabled(hasManagementSecret || (cfg != nil && cfg.Home.Enabled))	if hasManagementSecret {		s.registerManagementRoutes()	}	s.refreshPluginManagementRoutes()	engine.NoRoute(s.pluginManagementNoRoute) 	if optionState.keepAliveEnabled {		s.enableKeepAlive(optionState.keepAliveTimeout, optionState.keepAliveOnTimeout)	} 	// Create HTTP server	s.server = &http.Server{		Addr:    fmt.Sprintf("%s:%d", cfg.Host, cfg.Port),		Handler: engine,	} 	return s} // Handler returns the HTTP handler used by the server.func (s *Server) Handler() http.Handler {	if s == nil || s.server == nil {		return nil	}	return s.server.Handler} // Start begins listening for and serving HTTP or HTTPS requests.// It's a blocking call and will only return on an unrecoverable error.//// Returns://   - error: An error if the server fails to startfunc (s *Server) Start() error {	if s == nil || s.server == nil {		return fmt.Errorf("failed to start HTTP server: server not initialized")	} 	addr := s.server.Addr	listener, errListen := net.Listen("tcp", addr)	if errListen != nil {		return fmt.Errorf("failed to start HTTP server: %v", errListen)	} 	useTLS := s.cfg != nil && s.cfg.TLS.Enable	if useTLS {		certPath := strings.TrimSpace(s.cfg.TLS.Cert)		keyPath := strings.TrimSpace(s.cfg.TLS.Key)		if certPath == "" || keyPath == "" {			if errClose := listener.Close(); errClose != nil {				log.Errorf("failed to close listener after TLS validation failure: %v", errClose)			}			return fmt.Errorf("failed to start HTTPS server: tls.cert or tls.key is empty")		}		certPair, errLoad := tls.LoadX509KeyPair(certPath, keyPath)		if errLoad != nil {			if errClose := listener.Close(); errClose != nil {				log.Errorf("failed to close listener after TLS key pair load failure: %v", errClose)			}			return fmt.Errorf("failed to start HTTPS server: %v", errLoad)		} 		tlsConfig := &tls.Config{			Certificates: []tls.Certificate{certPair},			NextProtos:   []string{"h2", "http/1.1"},		}		s.server.TLSConfig = tlsConfig		if errHTTP2 := http2.ConfigureServer(s.server, &http2.Server{}); errHTTP2 != nil {			log.Warnf("failed to configure HTTP/2: %v", errHTTP2)		}		listener = tls.NewListener(listener, tlsConfig)		log.Debugf("Starting API server on %s with TLS", addr)	} else {		log.Debugf("Starting API server on %s", addr)	} 	httpListener := newMuxListener(listener.Addr(), 1024)	s.muxBaseListener = listener	s.muxHTTPListener = httpListener 	httpErrCh := make(chan error, 1)	acceptErrCh := make(chan error, 1) 	go func() {		httpErrCh <- s.server.Serve(httpListener)	}()	go func() {		acceptErrCh <- s.acceptMuxConnections(listener, httpListener)	}() 	select {	case errServe := <-httpErrCh:		if s.muxBaseListener != nil {			if errClose := s.muxBaseListener.Close(); errClose != nil && !errors.Is(errClose, net.ErrClosed) {				log.Debugf("failed to close shared listener after HTTP serve exit: %v", errClose)			}		}		if s.muxHTTPListener != nil {			_ = s.muxHTTPListener.Close()		}		errAccept := <-acceptErrCh		errServe = normalizeHTTPServeError(errServe)		errAccept = normalizeListenerError(errAccept)		if errServe != nil {			return fmt.Errorf("failed to start HTTP server: %v", errServe)		}		if errAccept != nil {			return fmt.Errorf("failed to start HTTP server: %v", errAccept)		}		return nil	case errAccept := <-acceptErrCh:		if s.muxHTTPListener != nil {			_ = s.muxHTTPListener.Close()		}		if s.muxBaseListener != nil {			if errClose := s.muxBaseListener.Close(); errClose != nil && !errors.Is(errClose, net.ErrClosed) {				log.Debugf("failed to close shared listener after accept loop exit: %v", errClose)			}		}		errServe := <-httpErrCh		errServe = normalizeHTTPServeError(errServe)		errAccept = normalizeListenerError(errAccept)		if errAccept != nil {			return fmt.Errorf("failed to start HTTP server: %v", errAccept)		}		if errServe != nil {			return fmt.Errorf("failed to start HTTP server: %v", errServe)		}		return nil	}} // Stop gracefully shuts down the API server without interrupting any// active connections.//// Parameters://   - ctx: The context for graceful shutdown//// Returns://   - error: An error if the server fails to stopfunc (s *Server) Stop(ctx context.Context) error {	log.Debug("Stopping API server...") 	if s.keepAliveEnabled {		select {		case s.keepAliveStop <- struct{}{}:		default:		}	} 	if s.muxHTTPListener != nil {		_ = s.muxHTTPListener.Close()	}	if s.muxBaseListener != nil {		if errClose := s.muxBaseListener.Close(); errClose != nil && !errors.Is(errClose, net.ErrClosed) {			log.Debugf("failed to close shared listener: %v", errClose)		}	} 	// Shutdown the HTTP server.	errShutdown := s.server.Shutdown(ctx)	if s.codexLiveHandler != nil {		s.codexLiveHandler.Close()	}	if errShutdown != nil {		return fmt.Errorf("failed to shutdown HTTP server: %v", errShutdown)	} 	log.Debug("API server stopped")	return nil}