@@ -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+
374420func (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
0 commit comments