Skip to content

Commit

Permalink
Fix e.Static, .File(), c.Attachment() being picky with paths st…
Browse files Browse the repository at this point in the history
…arting with `./`, `../` and `/` after 4.7.0 introduced echo.Filesystem support (Go1.16+) (#2123)

* Fix `e.Static`, `.File()`, `c.Attachment()` being picky with paths starting with `./`, `../` and `/` after 4.7.0 introduced echo.Filesystem support (Go1.16+)
  • Loading branch information
aldas committed Mar 13, 2022
1 parent 5ebed44 commit 3f5b733
Show file tree
Hide file tree
Showing 2 changed files with 88 additions and 13 deletions.
40 changes: 32 additions & 8 deletions echo_fs_go1.16.go
Expand Up @@ -10,6 +10,7 @@ import (
"net/url"
"os"
"path/filepath"
"runtime"
"strings"
)

Expand Down Expand Up @@ -94,10 +95,12 @@ func StaticFileHandler(file string, filesystem fs.FS) HandlerFunc {
}
}

// defaultFS emulates os.Open behaviour with filesystem opened by `os.DirFs`. Difference between `os.Open` and `fs.Open`
// is that FS does not allow to open 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.
// 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
Expand All @@ -108,20 +111,26 @@ func newDefaultFS() *defaultFS {
dir, _ := os.Getwd()
return &defaultFS{
prefix: dir,
fs: os.DirFS(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 to
// allow cases when root is given as `../somepath` which is not valid for fs.FS
root = filepath.Join(dFS.prefix, root)
// 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)
}
return &defaultFS{
prefix: root,
fs: os.DirFS(root),
Expand All @@ -130,6 +139,21 @@ func subFS(currentFs fs.FS, root string) (fs.FS, error) {
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.
//
Expand Down
61 changes: 56 additions & 5 deletions echo_test.go
Expand Up @@ -84,6 +84,14 @@ func TestEchoStatic(t *testing.T) {
expectStatus: http.StatusOK,
expectBodyStartsWith: string([]byte{0x89, 0x50, 0x4e, 0x47}),
},
{
name: "ok with relative path for root points to directory",
givenPrefix: "/images",
givenRoot: "./_fixture/images",
whenURL: "/images/walle.png",
expectStatus: http.StatusOK,
expectBodyStartsWith: string([]byte{0x89, 0x50, 0x4e, 0x47}),
},
{
name: "No file",
givenPrefix: "/images",
Expand Down Expand Up @@ -246,11 +254,54 @@ func TestEchoStaticRedirectIndex(t *testing.T) {
}

func TestEchoFile(t *testing.T) {
e := New()
e.File("/walle", "_fixture/images/walle.png")
c, b := request(http.MethodGet, "/walle", e)
assert.Equal(t, http.StatusOK, c)
assert.NotEmpty(t, b)
var testCases = []struct {
name string
givenPath string
givenFile string
whenPath string
expectCode int
expectStartsWith string
}{
{
name: "ok",
givenPath: "/walle",
givenFile: "_fixture/images/walle.png",
whenPath: "/walle",
expectCode: http.StatusOK,
expectStartsWith: string([]byte{0x89, 0x50, 0x4e}),
},
{
name: "ok with relative path",
givenPath: "/",
givenFile: "./go.mod",
whenPath: "/",
expectCode: http.StatusOK,
expectStartsWith: "module github.com/labstack/echo/v",
},
{
name: "nok file does not exist",
givenPath: "/",
givenFile: "./this-file-does-not-exist",
whenPath: "/",
expectCode: http.StatusNotFound,
expectStartsWith: "{\"message\":\"Not Found\"}\n",
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
e := New() // we are using echo.defaultFS instance
e.File(tc.givenPath, tc.givenFile)

c, b := request(http.MethodGet, tc.whenPath, e)
assert.Equal(t, tc.expectCode, c)

if len(b) > len(tc.expectStartsWith) {
b = b[:len(tc.expectStartsWith)]
}
assert.Equal(t, tc.expectStartsWith, b)
})
}
}

func TestEchoMiddleware(t *testing.T) {
Expand Down

0 comments on commit 3f5b733

Please sign in to comment.