Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix e.File() being picky with relative paths after 4.7.0 introduced echo.Fs support #2123

Merged
merged 4 commits into from Mar 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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