summaryrefslogtreecommitdiffstats
path: root/vendor/github.com/labstack/echo/v4/echo_fs.go
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/github.com/labstack/echo/v4/echo_fs.go')
-rw-r--r--vendor/github.com/labstack/echo/v4/echo_fs.go179
1 files changed, 146 insertions, 33 deletions
diff --git a/vendor/github.com/labstack/echo/v4/echo_fs.go b/vendor/github.com/labstack/echo/v4/echo_fs.go
index c3790545..b8526da9 100644
--- a/vendor/github.com/labstack/echo/v4/echo_fs.go
+++ b/vendor/github.com/labstack/echo/v4/echo_fs.go
@@ -1,62 +1,175 @@
-//go:build !go1.16
-// +build !go1.16
-
package echo
import (
+ "fmt"
+ "io/fs"
"net/http"
"net/url"
"os"
"path/filepath"
+ "runtime"
+ "strings"
)
type filesystem struct {
+ // Filesystem is file system used by Static and File handlers to access files.
+ // Defaults to os.DirFS(".")
+ //
+ // When dealing with `embed.FS` use `fs := echo.MustSubFS(fs, "rootDirectory") to create sub fs which uses necessary
+ // prefix for directory path. This is necessary as `//go:embed assets/images` embeds files with paths
+ // including `assets/images` as their prefix.
+ Filesystem fs.FS
}
func createFilesystem() filesystem {
- return filesystem{}
+ return filesystem{
+ Filesystem: newDefaultFS(),
+ }
}
-// Static registers a new route with path prefix to serve static files from the
-// provided root directory.
-func (e *Echo) Static(prefix, root string) *Route {
- if root == "" {
- root = "." // For security we want to restrict to CWD.
- }
- return e.static(prefix, root, e.GET)
+// Static registers a new route with path prefix to serve static files from the provided root directory.
+func (e *Echo) Static(pathPrefix, fsRoot string) *Route {
+ subFs := MustSubFS(e.Filesystem, fsRoot)
+ return e.Add(
+ http.MethodGet,
+ pathPrefix+"*",
+ StaticDirectoryHandler(subFs, false),
+ )
}
-func (common) static(prefix, root string, get func(string, HandlerFunc, ...MiddlewareFunc) *Route) *Route {
- h := func(c Context) error {
- p, err := url.PathUnescape(c.Param("*"))
- if err != nil {
- return err
+// StaticFS registers a new route with path prefix to serve static files from the provided file system.
+//
+// When dealing with `embed.FS` use `fs := echo.MustSubFS(fs, "rootDirectory") to create sub fs which uses necessary
+// prefix for directory path. This is necessary as `//go:embed assets/images` embeds files with paths
+// including `assets/images` as their prefix.
+func (e *Echo) StaticFS(pathPrefix string, filesystem fs.FS) *Route {
+ return e.Add(
+ http.MethodGet,
+ pathPrefix+"*",
+ StaticDirectoryHandler(filesystem, false),
+ )
+}
+
+// StaticDirectoryHandler creates handler function to serve files from provided file system
+// When disablePathUnescaping is set then file name from path is not unescaped and is served as is.
+func StaticDirectoryHandler(fileSystem fs.FS, disablePathUnescaping bool) HandlerFunc {
+ return func(c Context) error {
+ p := c.Param("*")
+ if !disablePathUnescaping { // when router is already unescaping we do not want to do is twice
+ tmpPath, err := url.PathUnescape(p)
+ if err != nil {
+ return fmt.Errorf("failed to unescape path variable: %w", err)
+ }
+ p = tmpPath
}
- name := filepath.Join(root, filepath.Clean("/"+p)) // "/"+ for security
- fi, err := os.Stat(name)
+ // fs.FS.Open() already assumes that file names are relative to FS root path and considers name with prefix `/` as invalid
+ name := filepath.ToSlash(filepath.Clean(strings.TrimPrefix(p, "/")))
+ fi, err := fs.Stat(fileSystem, name)
if err != nil {
- // The access path does not exist
- return NotFoundHandler(c)
+ return ErrNotFound
}
// If the request is for a directory and does not end with "/"
p = c.Request().URL.Path // path must not be empty.
- if fi.IsDir() && p[len(p)-1] != '/' {
+ if fi.IsDir() && len(p) > 0 && p[len(p)-1] != '/' {
// Redirect to ends with "/"
- return c.Redirect(http.StatusMovedPermanently, p+"/")
+ return c.Redirect(http.StatusMovedPermanently, sanitizeURI(p+"/"))
}
- return c.File(name)
- }
- // Handle added routes based on trailing slash:
- // /prefix => exact route "/prefix" + any route "/prefix/*"
- // /prefix/ => only any route "/prefix/*"
- if prefix != "" {
- if prefix[len(prefix)-1] == '/' {
- // Only add any route for intentional trailing slash
- return get(prefix+"*", h)
+ return fsFile(c, name, fileSystem)
+ }
+}
+
+// FileFS registers a new route with path to serve file from the provided file system.
+func (e *Echo) FileFS(path, file string, filesystem fs.FS, m ...MiddlewareFunc) *Route {
+ return e.GET(path, StaticFileHandler(file, filesystem), m...)
+}
+
+// StaticFileHandler creates handler function to serve file from provided file system
+func StaticFileHandler(file string, filesystem fs.FS) HandlerFunc {
+ return func(c Context) error {
+ return fsFile(c, file, filesystem)
+ }
+}
+
+// defaultFS exists to preserve pre v4.7.0 behaviour where files were open by `os.Open`.
+// v4.7 introduced `echo.Filesystem` field which is Go1.16+ `fs.Fs` interface.
+// Difference between `os.Open` and `fs.Open` is that FS does not allow opening path that start with `.`, `..` or `/`
+// etc. For example previously you could have `../images` in your application but `fs := os.DirFS("./")` would not
+// allow you to use `fs.Open("../images")` and this would break all old applications that rely on being able to
+// traverse up from current executable run path.
+// NB: private because you really should use fs.FS implementation instances
+type defaultFS struct {
+ prefix string
+ fs fs.FS
+}
+
+func newDefaultFS() *defaultFS {
+ dir, _ := os.Getwd()
+ return &defaultFS{
+ prefix: dir,
+ fs: nil,
+ }
+}
+
+func (fs defaultFS) Open(name string) (fs.File, error) {
+ if fs.fs == nil {
+ return os.Open(name)
+ }
+ return fs.fs.Open(name)
+}
+
+func subFS(currentFs fs.FS, root string) (fs.FS, error) {
+ root = filepath.ToSlash(filepath.Clean(root)) // note: fs.FS operates only with slashes. `ToSlash` is necessary for Windows
+ if dFS, ok := currentFs.(*defaultFS); ok {
+ // we need to make exception for `defaultFS` instances as it interprets root prefix differently from fs.FS.
+ // fs.Fs.Open does not like relative paths ("./", "../") and absolute paths at all but prior echo.Filesystem we
+ // were able to use paths like `./myfile.log`, `/etc/hosts` and these would work fine with `os.Open` but not with fs.Fs
+ if isRelativePath(root) {
+ root = filepath.Join(dFS.prefix, root)
}
- get(prefix, h)
+ return &defaultFS{
+ prefix: root,
+ fs: os.DirFS(root),
+ }, nil
+ }
+ return fs.Sub(currentFs, root)
+}
+
+func isRelativePath(path string) bool {
+ if path == "" {
+ return true
+ }
+ if path[0] == '/' {
+ return false
+ }
+ if runtime.GOOS == "windows" && strings.IndexByte(path, ':') != -1 {
+ // https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file?redirectedfrom=MSDN#file_and_directory_names
+ // https://docs.microsoft.com/en-us/dotnet/standard/io/file-path-formats
+ return false
+ }
+ return true
+}
+
+// MustSubFS creates sub FS from current filesystem or panic on failure.
+// Panic happens when `fsRoot` contains invalid path according to `fs.ValidPath` rules.
+//
+// MustSubFS is helpful when dealing with `embed.FS` because for example `//go:embed assets/images` embeds files with
+// paths including `assets/images` as their prefix. In that case use `fs := echo.MustSubFS(fs, "rootDirectory") to
+// create sub fs which uses necessary prefix for directory path.
+func MustSubFS(currentFs fs.FS, fsRoot string) fs.FS {
+ subFs, err := subFS(currentFs, fsRoot)
+ if err != nil {
+ panic(fmt.Errorf("can not create sub FS, invalid root given, err: %w", err))
+ }
+ return subFs
+}
+
+func sanitizeURI(uri string) string {
+ // double slash `\\`, `//` or even `\/` is absolute uri for browsers and by redirecting request to that uri
+ // we are vulnerable to open redirect attack. so replace all slashes from the beginning with single slash
+ if len(uri) > 1 && (uri[0] == '\\' || uri[0] == '/') && (uri[1] == '\\' || uri[1] == '/') {
+ uri = "/" + strings.TrimLeft(uri, `/\`)
}
- return get(prefix+"/*", h)
+ return uri
}