909c8b1894
Sanitizer renames tool names and replaces system prompt patterns that Anthropic fingerprints to detect non-Claude-Code clients. Lowercase tool names (bash, read, glob, etc.) combined together trigger rejection — renaming to PascalCase bypasses this. Configurable via YAML sanitize rules for tools, system, and body. Background OAuth token refresh every 30s with 5-minute pre-expiry lead. Uses Chrome TLS fingerprint for refresh endpoint too. Adds /messages route (without /v1 prefix) for OpenCode compat.
96 lines
2.2 KiB
Go
96 lines
2.2 KiB
Go
package server
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"github.com/fujin/anthropic-proxy/internal/auth"
|
|
"github.com/fujin/anthropic-proxy/internal/config"
|
|
"github.com/fujin/anthropic-proxy/internal/proxy"
|
|
)
|
|
|
|
type Server struct {
|
|
engine *gin.Engine
|
|
port int
|
|
}
|
|
|
|
func New(cfg *config.Config, pool *auth.Pool, profile *proxy.SniffedProfile) *Server {
|
|
gin.SetMode(gin.ReleaseMode)
|
|
engine := gin.New()
|
|
engine.Use(gin.Recovery())
|
|
engine.Use(corsMiddleware())
|
|
engine.Use(authMiddleware(cfg.APIKeys))
|
|
|
|
handler := proxy.HandleMessages(pool, profile, cfg.Sanitize)
|
|
engine.POST("/v1/messages", handler)
|
|
engine.POST("/messages", handler)
|
|
|
|
engine.GET("/healthz", func(c *gin.Context) {
|
|
c.JSON(http.StatusOK, gin.H{"status": "ok"})
|
|
})
|
|
engine.NoRoute(func(c *gin.Context) {
|
|
log.Printf("unmatched route: %s %s", c.Request.Method, c.Request.URL.Path)
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
|
})
|
|
|
|
return &Server{engine: engine, port: cfg.Port}
|
|
}
|
|
|
|
func (s *Server) Start() error {
|
|
addr := fmt.Sprintf(":%d", s.port)
|
|
return s.engine.Run(addr)
|
|
}
|
|
|
|
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()
|
|
}
|
|
}
|
|
|
|
func authMiddleware(apiKeys []string) gin.HandlerFunc {
|
|
keySet := make(map[string]struct{}, len(apiKeys))
|
|
for _, k := range apiKeys {
|
|
keySet[k] = struct{}{}
|
|
}
|
|
|
|
return func(c *gin.Context) {
|
|
if c.Request.URL.Path == "/healthz" {
|
|
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
|
|
}
|
|
|
|
if _, ok := keySet[token]; !ok {
|
|
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "invalid api key"})
|
|
return
|
|
}
|
|
|
|
c.Next()
|
|
}
|
|
}
|