Skip to content

Commit 5da5674

Browse files
authored
Merge pull request router-for-me#161 from router-for-me/aistudio
Add websocket provider
2 parents cd4706f + 7459c2c commit 5da5674

File tree

18 files changed

+1291
-13
lines changed

18 files changed

+1291
-13
lines changed

config.example.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ quota-exceeded:
4343
switch-project: true # Whether to automatically switch to another project when a quota is exceeded
4444
switch-preview-model: true # Whether to automatically switch to a preview model when a quota is exceeded
4545

46+
# When true, enable authentication for the WebSocket API (/v1/ws).
47+
ws-auth: false
48+
4649
# API keys for official Generative Language API
4750
#generative-language-api-key:
4851
# - "AIzaSy...01"

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ require (
77
github.com/gin-gonic/gin v1.10.1
88
github.com/go-git/go-git/v6 v6.0.0-20251009132922-75a182125145
99
github.com/google/uuid v1.6.0
10+
github.com/gorilla/websocket v1.5.3
1011
github.com/jackc/pgx/v5 v5.7.6
1112
github.com/joho/godotenv v1.5.1
1213
github.com/klauspost/compress v1.17.4

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
6666
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
6767
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
6868
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
69+
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
70+
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
6971
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
7072
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
7173
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
@@ -80,8 +82,6 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr
8082
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
8183
github.com/kevinburke/ssh_config v1.4.0 h1:6xxtP5bZ2E4NF5tuQulISpTO2z8XbtH8cg1PWkxoFkQ=
8284
github.com/kevinburke/ssh_config v1.4.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
83-
github.com/klauspost/compress v1.17.3 h1:qkRjuerhUU1EmXLYGkSH6EZL+vPSxIrYjLNAK4slzwA=
84-
github.com/klauspost/compress v1.17.3/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
8585
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
8686
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
8787
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=

internal/access/config_access/provider.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,12 @@ func (p *provider) Authenticate(_ context.Context, r *http.Request) (*sdkaccess.
5757
authHeaderGoogle := r.Header.Get("X-Goog-Api-Key")
5858
authHeaderAnthropic := r.Header.Get("X-Api-Key")
5959
queryKey := ""
60+
queryAuthToken := ""
6061
if r.URL != nil {
6162
queryKey = r.URL.Query().Get("key")
63+
queryAuthToken = r.URL.Query().Get("auth_token")
6264
}
63-
if authHeader == "" && authHeaderGoogle == "" && authHeaderAnthropic == "" && queryKey == "" {
65+
if authHeader == "" && authHeaderGoogle == "" && authHeaderAnthropic == "" && queryKey == "" && queryAuthToken == "" {
6466
return nil, sdkaccess.ErrNoCredentials
6567
}
6668

@@ -74,6 +76,7 @@ func (p *provider) Authenticate(_ context.Context, r *http.Request) (*sdkaccess.
7476
{authHeaderGoogle, "x-goog-api-key"},
7577
{authHeaderAnthropic, "x-api-key"},
7678
{queryKey, "query-key"},
79+
{queryAuthToken, "query-auth-token"},
7780
}
7881

7982
for _, candidate := range candidates {

internal/api/middleware/request_logging.go

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010

1111
"github.com/gin-gonic/gin"
1212
"github.com/router-for-me/CLIProxyAPI/v6/internal/logging"
13+
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
1314
)
1415

1516
// RequestLoggingMiddleware creates a Gin middleware that logs HTTP requests and responses.
@@ -63,13 +64,11 @@ func RequestLoggingMiddleware(logger logging.RequestLogger) gin.HandlerFunc {
6364
// It captures the URL, method, headers, and body. The request body is read and then
6465
// restored so that it can be processed by subsequent handlers.
6566
func captureRequestInfo(c *gin.Context) (*RequestInfo, error) {
66-
// Capture URL
67-
url := c.Request.URL.String()
68-
if c.Request.URL.Path != "" {
69-
url = c.Request.URL.Path
70-
if c.Request.URL.RawQuery != "" {
71-
url += "?" + c.Request.URL.RawQuery
72-
}
67+
// Capture URL with sensitive query parameters masked
68+
maskedQuery := util.MaskSensitiveQuery(c.Request.URL.RawQuery)
69+
url := c.Request.URL.Path
70+
if maskedQuery != "" {
71+
url += "?" + maskedQuery
7372
}
7473

7574
// Capture method

internal/api/server.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"os"
1414
"path/filepath"
1515
"strings"
16+
"sync"
1617
"sync/atomic"
1718
"time"
1819

@@ -138,6 +139,12 @@ type Server struct {
138139
// currentPath is the absolute path to the current working directory.
139140
currentPath string
140141

142+
// wsRoutes tracks registered websocket upgrade paths.
143+
wsRouteMu sync.Mutex
144+
wsRoutes map[string]struct{}
145+
wsAuthChanged func(bool, bool)
146+
wsAuthEnabled atomic.Bool
147+
141148
// management handler
142149
mgmt *managementHandlers.Handler
143150

@@ -228,7 +235,9 @@ func NewServer(cfg *config.Config, authManager *auth.Manager, accessManager *sdk
228235
configFilePath: configFilePath,
229236
currentPath: wd,
230237
envManagementSecret: envManagementSecret,
238+
wsRoutes: make(map[string]struct{}),
231239
}
240+
s.wsAuthEnabled.Store(cfg.WebsocketAuth)
232241
// Save initial YAML snapshot
233242
s.oldConfigYaml, _ = yaml.Marshal(cfg)
234243
s.applyAccessConfig(nil, cfg)
@@ -371,6 +380,43 @@ func (s *Server) setupRoutes() {
371380
// Management routes are registered lazily by registerManagementRoutes when a secret is configured.
372381
}
373382

383+
// AttachWebsocketRoute registers a websocket upgrade handler on the primary Gin engine.
384+
// The handler is served as-is without additional middleware beyond the standard stack already configured.
385+
func (s *Server) AttachWebsocketRoute(path string, handler http.Handler) {
386+
if s == nil || s.engine == nil || handler == nil {
387+
return
388+
}
389+
trimmed := strings.TrimSpace(path)
390+
if trimmed == "" {
391+
trimmed = "/v1/ws"
392+
}
393+
if !strings.HasPrefix(trimmed, "/") {
394+
trimmed = "/" + trimmed
395+
}
396+
s.wsRouteMu.Lock()
397+
if _, exists := s.wsRoutes[trimmed]; exists {
398+
s.wsRouteMu.Unlock()
399+
return
400+
}
401+
s.wsRoutes[trimmed] = struct{}{}
402+
s.wsRouteMu.Unlock()
403+
404+
authMiddleware := AuthMiddleware(s.accessManager)
405+
conditionalAuth := func(c *gin.Context) {
406+
if !s.wsAuthEnabled.Load() {
407+
c.Next()
408+
return
409+
}
410+
authMiddleware(c)
411+
}
412+
finalHandler := func(c *gin.Context) {
413+
handler.ServeHTTP(c.Writer, c.Request)
414+
c.Abort()
415+
}
416+
417+
s.engine.GET(trimmed, conditionalAuth, finalHandler)
418+
}
419+
374420
func (s *Server) registerManagementRoutes() {
375421
if s == nil || s.engine == nil || s.mgmt == nil {
376422
return
@@ -770,6 +816,10 @@ func (s *Server) UpdateClients(cfg *config.Config) {
770816

771817
s.applyAccessConfig(oldCfg, cfg)
772818
s.cfg = cfg
819+
s.wsAuthEnabled.Store(cfg.WebsocketAuth)
820+
if oldCfg != nil && s.wsAuthChanged != nil && oldCfg.WebsocketAuth != cfg.WebsocketAuth {
821+
s.wsAuthChanged(oldCfg.WebsocketAuth, cfg.WebsocketAuth)
822+
}
773823
managementasset.SetCurrentConfig(cfg)
774824
// Save YAML snapshot for next comparison
775825
s.oldConfigYaml, _ = yaml.Marshal(cfg)
@@ -810,6 +860,13 @@ func (s *Server) UpdateClients(cfg *config.Config) {
810860
)
811861
}
812862

863+
func (s *Server) SetWebsocketAuthChangeHandler(fn func(bool, bool)) {
864+
if s == nil {
865+
return
866+
}
867+
s.wsAuthChanged = fn
868+
}
869+
813870
// (management handlers moved to internal/api/handlers/management)
814871

815872
// AuthMiddleware returns a Gin middleware handler that authenticates requests

internal/config/config.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ type Config struct {
4040
// QuotaExceeded defines the behavior when a quota is exceeded.
4141
QuotaExceeded QuotaExceeded `yaml:"quota-exceeded" json:"quota-exceeded"`
4242

43+
// WebsocketAuth enables or disables authentication for the WebSocket API.
44+
WebsocketAuth bool `yaml:"ws-auth" json:"ws-auth"`
45+
4346
// GlAPIKey is the API key for the generative language API.
4447
GlAPIKey []string `yaml:"generative-language-api-key" json:"generative-language-api-key"`
4548

internal/logging/gin_logger.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"time"
1111

1212
"github.com/gin-gonic/gin"
13+
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
1314
log "github.com/sirupsen/logrus"
1415
)
1516

@@ -23,7 +24,7 @@ func GinLogrusLogger() gin.HandlerFunc {
2324
return func(c *gin.Context) {
2425
start := time.Now()
2526
path := c.Request.URL.Path
26-
raw := c.Request.URL.RawQuery
27+
raw := util.MaskSensitiveQuery(c.Request.URL.RawQuery)
2728

2829
c.Next()
2930

0 commit comments

Comments
 (0)