99 "net/http"
1010 "os"
1111 "path/filepath"
12+ "sort"
1213 "strings"
1314 "sync"
1415 "time"
@@ -84,7 +85,6 @@ func (cr *ctxReader) Read(p []byte) (int, error) {
8485 }
8586}
8687
87- // ── Added OnDevicesDiscovered to Interface ──
8888type BridgeCallback interface {
8989 OnDeviceDropped (ip string )
9090 OnClipboardDataReceived (data []byte , contentType string )
@@ -193,9 +193,11 @@ func DisconnectDevice(ip string) {
193193}
194194
195195func GetSessionToken (ip string ) string { return sessionManager .GetOutboundToken (ip ) }
196+
196197func ShareMobileClipboard (ip string , port string , data []byte , contentType string ) error {
197198 return clipboardManager .ShareMobileData (ip , port , data , contentType )
198199}
200+
199201func GetRemoteFilesJson (ip string , port string , path string ) (string , error ) {
200202 result , err := androidClient .GetRemoteFiles (ip , port , path )
201203 if err != nil {
@@ -204,10 +206,25 @@ func GetRemoteFilesJson(ip string, port string, path string) (string, error) {
204206 jsonBytes , err := json .Marshal (result )
205207 return string (jsonBytes ), err
206208}
209+
207210func MakeDirectory (ip string , port string , dir string , name string ) error {
208211 return androidClient .MakeDirectory (ip , port , dir , name )
209212}
210213
214+ func resolveMobilePath (reqPath string ) (string , string , error ) {
215+ if reqPath == "" {
216+ reqPath = "/"
217+ }
218+ cleanVirtual := filepath .Clean ("/" + reqPath )
219+ baseDir := getExposedDir ()
220+ absPhysical := filepath .Join (baseDir , cleanVirtual )
221+
222+ if ! strings .HasPrefix (absPhysical , baseDir ) {
223+ return "" , "" , fmt .Errorf ("path traversal denied" )
224+ }
225+ return absPhysical , cleanVirtual , nil
226+ }
227+
211228func StartMobileServer () {
212229 go func () {
213230 mux := http .NewServeMux ()
@@ -304,8 +321,16 @@ func StartMobileServer() {
304321 mux .HandleFunc ("/api/ping" , authMiddleware (func (w http.ResponseWriter , r * http.Request ) { w .WriteHeader (http .StatusOK ) }))
305322
306323 mux .HandleFunc ("/api/disconnect" , authMiddleware (func (w http.ResponseWriter , r * http.Request ) {
307- clientIP , _ , _ := net .SplitHostPort (r .RemoteAddr )
324+ clientIP , _ , err := net .SplitHostPort (r .RemoteAddr )
325+ if err != nil {
326+ clientIP = r .RemoteAddr
327+ }
308328 clientIP = strings .TrimPrefix (clientIP , "::ffff:" )
329+ if clientIP == "::1" {
330+ clientIP = "127.0.0.1"
331+ }
332+
333+ cancelTransfersForIP (clientIP )
309334 sessionManager .RemoveSession (clientIP )
310335 if cbProxy != nil && cbProxy .cb != nil {
311336 cbProxy .cb .OnDeviceDropped (clientIP )
@@ -315,60 +340,109 @@ func StartMobileServer() {
315340
316341 mux .HandleFunc ("/api/files/list" , authMiddleware (func (w http.ResponseWriter , r * http.Request ) {
317342 w .Header ().Set ("Content-Type" , "application/json" )
318- requestedPath := r .URL .Query ().Get ("path" )
319- if requestedPath == "/" {
320- requestedPath = ""
321- }
322- if strings .Contains (requestedPath , ".." ) {
343+
344+ absPhysical , cleanVirtual , err := resolveMobilePath (r .URL .Query ().Get ("path" ))
345+ if err != nil {
346+ http .Error (w , err .Error (), http .StatusBadRequest )
323347 return
324348 }
325349
326- fullPath := filepath .Join (getExposedDir (), requestedPath )
327- entries , err := os .ReadDir (fullPath )
350+ entries , err := os .ReadDir (absPhysical )
328351 if err != nil {
329- json .NewEncoder (w ).Encode (map [string ]interface {}{"path" : requestedPath , "parent" : filepath .Dir (requestedPath ), "files" : []models.FileInfo {}})
352+ json .NewEncoder (w ).Encode (map [string ]interface {}{
353+ "path" : cleanVirtual ,
354+ "parent" : filepath .Dir (cleanVirtual ),
355+ "files" : []models.FileInfo {},
356+ })
330357 return
331358 }
332359
333360 var files []models.FileInfo
334- for _ , e := range entries {
335- info , err := e . Info ()
336- if err != nil {
361+ for _ , entry := range entries {
362+ // Hide Android system dotfiles
363+ if strings . HasPrefix ( entry . Name (), "." ) {
337364 continue
338365 }
339- relPath := e .Name ()
340- if requestedPath != "" {
341- relPath = requestedPath + "/" + e .Name ()
366+
367+ info , err := entry .Info ()
368+ if err != nil {
369+ continue
342370 }
343- files = append (files , models.FileInfo {Name : e .Name (), Path : relPath , Size : info .Size (), IsDir : e .IsDir (), ModTime : info .ModTime ().Format ("2006-01-02 15:04" )})
371+
372+ relPath := filepath .ToSlash (filepath .Join (cleanVirtual , entry .Name ()))
373+
374+ files = append (files , models.FileInfo {
375+ Name : entry .Name (),
376+ Path : relPath ,
377+ Size : info .Size (),
378+ IsDir : entry .IsDir (),
379+ ModTime : info .ModTime ().Format ("2006-01-02 15:04" ),
380+ })
344381 }
345- parent := filepath .Dir (requestedPath )
346- if requestedPath == "" {
382+
383+ // Sort correctly (Folders first, then A-Z)
384+ sort .Slice (files , func (i , j int ) bool {
385+ if files [i ].IsDir != files [j ].IsDir {
386+ return files [i ].IsDir
387+ }
388+ return strings .ToLower (files [i ].Name ) < strings .ToLower (files [j ].Name )
389+ })
390+
391+ parent := filepath .Dir (cleanVirtual )
392+ if cleanVirtual == "/" {
347393 parent = ""
348394 }
349395 if files == nil {
350396 files = []models.FileInfo {}
351397 }
352- json .NewEncoder (w ).Encode (map [string ]interface {}{"path" : requestedPath , "parent" : parent , "files" : files })
398+
399+ json .NewEncoder (w ).Encode (map [string ]interface {}{
400+ "path" : cleanVirtual ,
401+ "parent" : parent ,
402+ "files" : files ,
403+ })
353404 }))
354405
355406 mux .HandleFunc ("/api/files/download" , authMiddleware (func (w http.ResponseWriter , r * http.Request ) {
356- requestedPath := r .URL .Query ().Get ("path" )
357- if strings .Contains (requestedPath , ".." ) {
407+ absPhysical , _ , err := resolveMobilePath (r .URL .Query ().Get ("path" ))
408+ if err != nil {
409+ http .Error (w , err .Error (), http .StatusBadRequest )
358410 return
359411 }
360- http .ServeFile (w , r , filepath .Join (getExposedDir (), requestedPath ))
412+
413+ info , err := os .Stat (absPhysical )
414+ if err != nil || info .IsDir () {
415+ http .Error (w , "File not found" , http .StatusNotFound )
416+ return
417+ }
418+
419+ f , err := os .Open (absPhysical )
420+ if err != nil {
421+ http .Error (w , "Cannot read file" , http .StatusInternalServerError )
422+ return
423+ }
424+ defer f .Close ()
425+
426+ w .Header ().Set ("Content-Disposition" , fmt .Sprintf (`attachment; filename="%s"` , filepath .Base (absPhysical )))
427+ w .Header ().Set ("Content-Type" , "application/octet-stream" )
428+ http .ServeContent (w , r , filepath .Base (absPhysical ), info .ModTime (), f )
361429 }))
362430
363431 mux .HandleFunc ("/api/files/upload" , authMiddleware (func (w http.ResponseWriter , r * http.Request ) {
364- dir := r .URL .Query ().Get ("dir" )
365- if strings . Contains ( dir , ".." ) {
366- http .Error (w , "Invalid directory" , http .StatusBadRequest )
432+ absPhysical , _ , err := resolveMobilePath ( r .URL .Query ().Get ("dir" ) )
433+ if err != nil {
434+ http .Error (w , err . Error () , http .StatusBadRequest )
367435 return
368436 }
369437
370- clientIP , _ , _ := net .SplitHostPort (r .RemoteAddr )
438+ clientIP , _ , err := net .SplitHostPort (r .RemoteAddr )
439+ if err != nil {
440+ clientIP = r .RemoteAddr
441+ }
371442 clientIP = strings .TrimPrefix (clientIP , "::ffff:" )
443+ if clientIP == "::1" {
444+ clientIP = "127.0.0.1"
445+ }
372446
373447 transferCtx , cancelTransfer := context .WithCancel (r .Context ())
374448 registerTransfer (clientIP , cancelTransfer )
@@ -380,8 +454,7 @@ func StartMobileServer() {
380454 return
381455 }
382456
383- destDir := filepath .Join (getExposedDir (), dir )
384- os .MkdirAll (destDir , 0755 )
457+ os .MkdirAll (absPhysical , 0755 )
385458
386459 for {
387460 part , err := reader .NextPart ()
@@ -393,12 +466,12 @@ func StartMobileServer() {
393466 }
394467
395468 if part .FormName () == "files" {
396- filename := part .FileName ()
397- if filename == "" {
469+ filename := filepath . Base ( part .FileName ()) // Strip malicious traversal attempts
470+ if filename == "" || filename == "." || filename == "/" {
398471 continue
399472 }
400473
401- dst , err := os .Create (filepath .Join (destDir , filename ))
474+ dst , err := os .Create (filepath .Join (absPhysical , filename ))
402475 if err == nil {
403476 _ , copyErr := io .Copy (dst , & ctxReader {r : part , ctx : transferCtx })
404477 dst .Close ()
@@ -413,12 +486,19 @@ func StartMobileServer() {
413486 }))
414487
415488 mux .HandleFunc ("/api/files/mkdir" , authMiddleware (func (w http.ResponseWriter , r * http.Request ) {
416- dir := r .URL .Query ().Get ("dir" )
489+ absPhysical , _ , err := resolveMobilePath (r .URL .Query ().Get ("dir" ))
490+ if err != nil {
491+ http .Error (w , err .Error (), http .StatusBadRequest )
492+ return
493+ }
494+
417495 name := r .URL .Query ().Get ("name" )
418- if strings .Contains (dir , ".." ) || strings .Contains (name , ".." ) {
496+ if name == "" || strings .ContainsAny (name , "/\\ " ) || strings .Contains (name , ".." ) {
497+ http .Error (w , "Invalid directory name" , http .StatusBadRequest )
419498 return
420499 }
421- os .MkdirAll (filepath .Join (getExposedDir (), dir , name ), 0755 )
500+
501+ os .MkdirAll (filepath .Join (absPhysical , name ), 0755 )
422502 w .WriteHeader (http .StatusOK )
423503 }))
424504
@@ -427,8 +507,15 @@ func StartMobileServer() {
427507 }))
428508
429509 mux .HandleFunc ("/api/files/cancel" , authMiddleware (func (w http.ResponseWriter , r * http.Request ) {
430- clientIP , _ , _ := net .SplitHostPort (r .RemoteAddr )
510+ clientIP , _ , err := net .SplitHostPort (r .RemoteAddr )
511+ if err != nil {
512+ clientIP = r .RemoteAddr
513+ }
431514 clientIP = strings .TrimPrefix (clientIP , "::ffff:" )
515+ if clientIP == "::1" {
516+ clientIP = "127.0.0.1"
517+ }
518+
432519 cancelTransfersForIP (clientIP )
433520 w .WriteHeader (http .StatusOK )
434521 }))
0 commit comments