@@ -43,6 +43,7 @@ import (
43
43
"errors"
44
44
"fmt"
45
45
"io"
46
+ "io/fs"
46
47
"io/ioutil"
47
48
stdLog "log"
48
49
"net"
@@ -52,6 +53,7 @@ import (
52
53
"path/filepath"
53
54
"reflect"
54
55
"runtime"
56
+ "strings"
55
57
"sync"
56
58
"time"
57
59
96
98
Logger Logger
97
99
IPExtractor IPExtractor
98
100
ListenerNetwork string
101
+ // Filesystem is file system used by Static and File handlers to access files.
102
+ // Defaults to os.DirFS(".")
103
+ Filesystem fs.FS
99
104
}
100
105
101
106
// Route contains a handler and information for matching against requests.
@@ -328,6 +333,7 @@ func New() (e *Echo) {
328
333
colorer : color .New (),
329
334
maxParam : new (int ),
330
335
ListenerNetwork : "tcp" ,
336
+ Filesystem : newDefaultFS (),
331
337
}
332
338
e .Server .Handler = e
333
339
e .TLSServer .Handler = e
@@ -499,48 +505,57 @@ func (e *Echo) Match(methods []string, path string, handler HandlerFunc, middlew
499
505
return routes
500
506
}
501
507
502
- // Static registers a new route with path prefix to serve static files from the
503
- // provided root directory.
504
- func (e * Echo ) Static (prefix , root string ) * Route {
505
- if root == "" {
506
- root = "." // For security we want to restrict to CWD.
508
+ // Static registers a new route with path prefix to serve static files from the provided root directory.
509
+ func (e * Echo ) Static (pathPrefix , root string ) * Route {
510
+ subFs , err := subFS (e .Filesystem , root )
511
+ if err != nil {
512
+ // happens when `root` contains invalid path according to `fs.ValidPath` rules and we are unable to create FS
513
+ panic (fmt .Errorf ("invalid root given to echo.Static, err %w" , err ))
507
514
}
508
- return e .static (prefix , root , e .GET )
515
+ return e .Add (
516
+ http .MethodGet ,
517
+ pathPrefix + "*" ,
518
+ StaticDirectoryHandler (subFs , false ),
519
+ )
509
520
}
510
521
511
- func (common ) static (prefix , root string , get func (string , HandlerFunc , ... MiddlewareFunc ) * Route ) * Route {
512
- h := func (c Context ) error {
513
- p , err := url .PathUnescape (c .Param ("*" ))
514
- if err != nil {
515
- return err
522
+ // StaticFS registers a new route with path prefix to serve static files from the provided file system.
523
+ func (e * Echo ) StaticFS (pathPrefix string , fileSystem fs.FS ) * Route {
524
+ return e .Add (
525
+ http .MethodGet ,
526
+ pathPrefix + "*" ,
527
+ StaticDirectoryHandler (fileSystem , false ),
528
+ )
529
+ }
530
+
531
+ // StaticDirectoryHandler creates handler function to serve files from provided file system
532
+ // When disablePathUnescaping is set then file name from path is not unescaped and is served as is.
533
+ func StaticDirectoryHandler (fileSystem fs.FS , disablePathUnescaping bool ) HandlerFunc {
534
+ return func (c Context ) error {
535
+ p := c .Param ("*" )
536
+ if ! disablePathUnescaping { // when router is already unescaping we do not want to do is twice
537
+ tmpPath , err := url .PathUnescape (p )
538
+ if err != nil {
539
+ return fmt .Errorf ("failed to unescape path variable: %w" , err )
540
+ }
541
+ p = tmpPath
516
542
}
517
543
518
- name := filepath .Join (root , filepath .Clean ("/" + p )) // "/"+ for security
519
- fi , err := os .Stat (name )
544
+ // fs.FS.Open() already assumes that file names are relative to FS root path and considers name with prefix `/` as invalid
545
+ name := filepath .Clean (strings .TrimPrefix (p , "/" ))
546
+ fi , err := fs .Stat (fileSystem , name )
520
547
if err != nil {
521
- // The access path does not exist
522
- return NotFoundHandler (c )
548
+ return ErrNotFound
523
549
}
524
550
525
551
// If the request is for a directory and does not end with "/"
526
552
p = c .Request ().URL .Path // path must not be empty.
527
- if fi .IsDir () && p [len (p )- 1 ] != '/' {
553
+ if fi .IsDir () && len ( p ) > 0 && p [len (p )- 1 ] != '/' {
528
554
// Redirect to ends with "/"
529
555
return c .Redirect (http .StatusMovedPermanently , p + "/" )
530
556
}
531
- return c .File (name )
532
- }
533
- // Handle added routes based on trailing slash:
534
- // /prefix => exact route "/prefix" + any route "/prefix/*"
535
- // /prefix/ => only any route "/prefix/*"
536
- if prefix != "" {
537
- if prefix [len (prefix )- 1 ] == '/' {
538
- // Only add any route for intentional trailing slash
539
- return get (prefix + "*" , h )
540
- }
541
- get (prefix , h )
557
+ return fsFile (c , name , fileSystem )
542
558
}
543
- return get (prefix + "/*" , h )
544
559
}
545
560
546
561
func (common ) file (path , file string , get func (string , HandlerFunc , ... MiddlewareFunc ) * Route ,
@@ -555,6 +570,18 @@ func (e *Echo) File(path, file string, m ...MiddlewareFunc) *Route {
555
570
return e .file (path , file , e .GET , m ... )
556
571
}
557
572
573
+ // FileFS registers a new route with path to serve file from the provided file system.
574
+ func (e * Echo ) FileFS (path , file string , filesystem fs.FS , m ... MiddlewareFunc ) * Route {
575
+ return e .GET (path , StaticFileHandler (file , filesystem ), m ... )
576
+ }
577
+
578
+ // StaticFileHandler creates handler function to serve file from provided file system
579
+ func StaticFileHandler (file string , filesystem fs.FS ) HandlerFunc {
580
+ return func (c Context ) error {
581
+ return fsFile (c , file , filesystem )
582
+ }
583
+ }
584
+
558
585
func (e * Echo ) add (host , method , path string , handler HandlerFunc , middleware ... MiddlewareFunc ) * Route {
559
586
name := handlerName (handler )
560
587
router := e .findRouter (host )
@@ -1007,3 +1034,38 @@ func applyMiddleware(h HandlerFunc, middleware ...MiddlewareFunc) HandlerFunc {
1007
1034
}
1008
1035
return h
1009
1036
}
1037
+
1038
+ // defaultFS emulates os.Open behaviour with filesystem opened by `os.DirFs`. Difference between `os.Open` and `fs.Open`
1039
+ // is that FS does not allow to open path that start with `..` or `/` etc. For example previously you could have `../images`
1040
+ // in your application but `fs := os.DirFS("./")` would not allow you to use `fs.Open("../images")` and this would break
1041
+ // all old applications that rely on being able to traverse up from current executable run path.
1042
+ // NB: private because you really should use fs.FS implementation instances
1043
+ type defaultFS struct {
1044
+ prefix string
1045
+ fs fs.FS
1046
+ }
1047
+
1048
+ func newDefaultFS () * defaultFS {
1049
+ dir , _ := os .Getwd ()
1050
+ return & defaultFS {
1051
+ prefix : dir ,
1052
+ fs : os .DirFS (dir ),
1053
+ }
1054
+ }
1055
+
1056
+ func (fs defaultFS ) Open (name string ) (fs.File , error ) {
1057
+ return fs .fs .Open (name )
1058
+ }
1059
+
1060
+ func subFS (currentFs fs.FS , root string ) (fs.FS , error ) {
1061
+ if dFS , ok := currentFs .(* defaultFS ); ok {
1062
+ // we need to make exception for `defaultFS` instances as it interprets root prefix differently from fs.FS to
1063
+ // allow cases when root is given as `../somepath` which is not valid for fs.FS
1064
+ root = filepath .Join (dFS .prefix , root )
1065
+ return & defaultFS {
1066
+ prefix : root ,
1067
+ fs : os .DirFS (root ),
1068
+ }, nil
1069
+ }
1070
+ return fs .Sub (currentFs , filepath .Clean (root ))
1071
+ }
0 commit comments