@@ -410,6 +410,15 @@ func (vb *Bridge) handleMessage(msg *BridgeMessage) error {
410410 }(* msg )
411411 return nil
412412
413+ case "dir:list" :
414+ // Handle directory list requests in parallel
415+ go func (dirListMsg BridgeMessage ) {
416+ if err := vb .handleDirListRequest (& dirListMsg ); err != nil {
417+ log .Errorf (vb .ctx , "[vite_bridge] Error handling dir list request for %s: %v" , dirListMsg .Path , err )
418+ }
419+ }(* msg )
420+ return nil
421+
413422 case "hmr:message" :
414423 return vb .handleHMRMessage (msg )
415424
@@ -596,6 +605,88 @@ func (vb *Bridge) handleFileReadRequest(msg *BridgeMessage) error {
596605 return nil
597606}
598607
608+ func (vb * Bridge ) handleDirListRequest (msg * BridgeMessage ) error {
609+ log .Debugf (vb .ctx , "[vite_bridge] Dir list request: %s" , msg .Path )
610+
611+ if err := ValidateDirPath (msg .Path ); err != nil {
612+ log .Warnf (vb .ctx , "[vite_bridge] Dir validation failed for %s: %v" , msg .Path , err )
613+ return vb .sendDirListError (msg .RequestID , fmt .Sprintf ("Invalid directory path: %v" , err ))
614+ }
615+
616+ entries , err := os .ReadDir (msg .Path )
617+
618+ response := BridgeMessage {
619+ Type : "dir:list:response" ,
620+ RequestID : msg .RequestID ,
621+ }
622+
623+ if err != nil {
624+ log .Errorf (vb .ctx , "[vite_bridge] Failed to read directory %s: %v" , msg .Path , err )
625+ response .Error = err .Error ()
626+ } else {
627+ files := make ([]string , 0 , len (entries ))
628+ for _ , entry := range entries {
629+ if ! entry .IsDir () && filepath .Ext (entry .Name ()) == allowedExtension {
630+ files = append (files , entry .Name ())
631+ }
632+ }
633+ log .Debugf (vb .ctx , "[vite_bridge] Listed directory %s (%d SQL files)" , msg .Path , len (files ))
634+ // Client expects files as JSON string in content field
635+ filesJSON , err := json .Marshal (files )
636+ if err != nil {
637+ log .Errorf (vb .ctx , "[vite_bridge] Failed to marshal file list: %v" , err )
638+ response .Error = fmt .Sprintf ("Failed to marshal file list: %v" , err )
639+ } else {
640+ response .Content = string (filesJSON )
641+ }
642+ }
643+
644+ responseData , err := json .Marshal (response )
645+ if err != nil {
646+ return fmt .Errorf ("failed to marshal dir list response: %w" , err )
647+ }
648+
649+ log .Debugf (vb .ctx , "[vite_bridge] Sending dir list response: %s" , string (responseData ))
650+
651+ select {
652+ case vb .tunnelWriteChan <- prioritizedMessage {
653+ messageType : websocket .TextMessage ,
654+ data : responseData ,
655+ priority : 1 ,
656+ }:
657+ log .Debugf (vb .ctx , "[vite_bridge] Dir list response sent successfully" )
658+ case <- time .After (wsWriteTimeout ):
659+ return errors .New ("timeout sending dir list response" )
660+ }
661+
662+ return nil
663+ }
664+
665+ func (vb * Bridge ) sendDirListError (requestID , errMsg string ) error {
666+ response := BridgeMessage {
667+ Type : "dir:list:response" ,
668+ RequestID : requestID ,
669+ Error : errMsg ,
670+ }
671+
672+ responseData , err := json .Marshal (response )
673+ if err != nil {
674+ return fmt .Errorf ("failed to marshal dir list error response: %w" , err )
675+ }
676+
677+ select {
678+ case vb .tunnelWriteChan <- prioritizedMessage {
679+ messageType : websocket .TextMessage ,
680+ data : responseData ,
681+ priority : 1 ,
682+ }:
683+ case <- time .After (wsWriteTimeout ):
684+ return errors .New ("timeout sending dir list error response" )
685+ }
686+
687+ return nil
688+ }
689+
599690func ValidateFilePath (requestedPath string ) error {
600691 // Clean the path to resolve any ../ or ./ components
601692 cleanPath := filepath .Clean (requestedPath )
@@ -635,6 +726,50 @@ func ValidateFilePath(requestedPath string) error {
635726 return nil
636727}
637728
729+ // ValidateDirPath validates that a directory path is within the allowed directory.
730+ func ValidateDirPath (requestedPath string ) error {
731+ // Clean the path to resolve any ../ or ./ components
732+ cleanPath := filepath .Clean (requestedPath )
733+
734+ // Get absolute path
735+ absPath , err := filepath .Abs (cleanPath )
736+ if err != nil {
737+ return fmt .Errorf ("failed to resolve absolute path: %w" , err )
738+ }
739+
740+ // Get the working directory
741+ cwd , err := os .Getwd ()
742+ if err != nil {
743+ return fmt .Errorf ("failed to get working directory: %w" , err )
744+ }
745+
746+ // Construct the allowed base directory (absolute path)
747+ allowedDir := filepath .Join (cwd , allowedBasePath )
748+
749+ // Ensure the resolved path is within the allowed directory
750+ // Add trailing separator to prevent prefix attacks (e.g., queries-malicious/)
751+ allowedDirWithSep := allowedDir + string (filepath .Separator )
752+ if absPath != allowedDir && ! strings .HasPrefix (absPath , allowedDirWithSep ) {
753+ return fmt .Errorf ("path %s is outside allowed directory %s" , absPath , allowedBasePath )
754+ }
755+
756+ // Additional check: no hidden directories
757+ if strings .HasPrefix (filepath .Base (absPath ), "." ) {
758+ return errors .New ("hidden directories are not allowed" )
759+ }
760+
761+ // Verify it's actually a directory
762+ info , err := os .Stat (absPath )
763+ if err != nil {
764+ return fmt .Errorf ("failed to stat path: %w" , err )
765+ }
766+ if ! info .IsDir () {
767+ return fmt .Errorf ("path %s is not a directory" , requestedPath )
768+ }
769+
770+ return nil
771+ }
772+
638773// Helper to send error response
639774func (vb * Bridge ) sendFileReadError (requestID , errorMsg string ) error {
640775 response := BridgeMessage {
0 commit comments