package server import ( "context" "fmt" "net/http" "strings" "sync/atomic" "github.com/gin-gonic/gin" "github.com/rs/zerolog/log" "github.com/fujin/anthropic-proxy/internal/auth" "github.com/fujin/anthropic-proxy/internal/config" "github.com/fujin/anthropic-proxy/internal/logging" "github.com/fujin/anthropic-proxy/internal/proxy" "github.com/fujin/anthropic-proxy/internal/ratelimit" "go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin" ) type Server struct { httpServer *http.Server engine *gin.Engine configPath string sanitizer atomic.Pointer[proxy.Sanitizer] apiKeys atomic.Pointer[map[string]struct{}] } func New(cfg *config.Config, pool *auth.Pool, profile *proxy.SniffedProfile, tracker *ratelimit.Tracker, metricsHandler http.Handler) *Server { s := &Server{configPath: "config.yaml"} san := proxy.NewSanitizer(cfg.Sanitize) s.sanitizer.Store(san) keys := makeKeySet(cfg.APIKeys) s.apiKeys.Store(&keys) gin.SetMode(gin.ReleaseMode) engine := gin.New() engine.Use(gin.Recovery()) engine.Use(corsMiddleware()) if cfg.Telemetry.Export.Enabled() { engine.Use(otelgin.Middleware(cfg.Telemetry.ServiceName)) } engine.Use(s.authMiddleware()) engine.Use(logging.GinRequestLogger()) handler := proxy.HandleMessages(pool, profile, func() *proxy.Sanitizer { return s.sanitizer.Load() }, tracker) engine.POST("/v1/messages", handler) engine.POST("/messages", handler) if metricsHandler != nil { engine.GET("/metrics", gin.WrapH(metricsHandler)) } engine.POST("/reload", s.handleReload()) engine.POST("/debug/refresh", handleDebugRefresh(pool)) engine.GET("/healthz", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"status": "ok"}) }) engine.NoRoute(func(c *gin.Context) { log.Warn().Str("method", c.Request.Method).Str("path", c.Request.URL.Path).Msg("unmatched route") c.JSON(http.StatusNotFound, gin.H{"error": "not found"}) }) s.engine = engine s.httpServer = &http.Server{ Addr: fmt.Sprintf(":%d", cfg.Port), Handler: engine, } return s } func (s *Server) Start() error { return s.httpServer.ListenAndServe() } func (s *Server) Shutdown(ctx context.Context) error { return s.httpServer.Shutdown(ctx) } func (s *Server) handleReload() gin.HandlerFunc { return func(c *gin.Context) { cfg, err := config.Load(s.configPath) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("load config: %v", err)}) return } san := proxy.NewSanitizer(cfg.Sanitize) s.sanitizer.Store(san) keys := makeKeySet(cfg.APIKeys) s.apiKeys.Store(&keys) log.Info().Int("tool_renames", len(cfg.Sanitize.Tools)).Int("system_rules", len(cfg.Sanitize.System)).Int("body_rules", len(cfg.Sanitize.Body)).Int("api_keys", len(cfg.APIKeys)).Msg("config reloaded") c.JSON(http.StatusOK, gin.H{ "status": "reloaded", "tool_renames": len(cfg.Sanitize.Tools), "system_rules": len(cfg.Sanitize.System), "body_rules": len(cfg.Sanitize.Body), "api_keys": len(cfg.APIKeys), }) } } func handleDebugRefresh(pool *auth.Pool) gin.HandlerFunc { return func(c *gin.Context) { results := pool.RefreshAll(c.Request.Context()) c.JSON(http.StatusOK, results) } } func makeKeySet(apiKeys []string) map[string]struct{} { keySet := make(map[string]struct{}, len(apiKeys)) for _, k := range apiKeys { keySet[k] = struct{}{} } return keySet } func corsMiddleware() gin.HandlerFunc { return func(c *gin.Context) { c.Header("Access-Control-Allow-Origin", "*") c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") c.Header("Access-Control-Allow-Headers", "Origin, Content-Type, Authorization, x-api-key, anthropic-version, anthropic-beta") if c.Request.Method == http.MethodOptions { c.AbortWithStatus(http.StatusNoContent) return } c.Next() } } // authBypassPaths lists endpoints that do not require API key authentication. var authBypassPaths = map[string]bool{ "/healthz": true, "/reload": true, "/metrics": true, } func (s *Server) authMiddleware() gin.HandlerFunc { return func(c *gin.Context) { if authBypassPaths[c.Request.URL.Path] { c.Next() return } token := "" if authHeader := c.GetHeader("Authorization"); authHeader != "" { token = strings.TrimPrefix(authHeader, "Bearer ") } if token == "" { token = c.GetHeader("x-api-key") } if token == "" { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "missing authentication"}) return } keys := s.apiKeys.Load() if _, ok := (*keys)[token]; !ok { c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "invalid api key"}) return } c.Next() } }