Skip to content

Commit 3f5b733

Browse files
authored
Fix e.Static, .File(), c.Attachment() being picky with paths starting 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+)
1 parent 5ebed44 commit 3f5b733

File tree

2 files changed

+88
-13
lines changed

2 files changed

+88
-13
lines changed

echo_fs_go1.16.go

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"net/url"
1111
"os"
1212
"path/filepath"
13+
"runtime"
1314
"strings"
1415
)
1516

@@ -94,10 +95,12 @@ func StaticFileHandler(file string, filesystem fs.FS) HandlerFunc {
9495
}
9596
}
9697

97-
// defaultFS emulates os.Open behaviour with filesystem opened by `os.DirFs`. Difference between `os.Open` and `fs.Open`
98-
// is that FS does not allow to open path that start with `..` or `/` etc. For example previously you could have `../images`
99-
// in your application but `fs := os.DirFS("./")` would not allow you to use `fs.Open("../images")` and this would break
100-
// all old applications that rely on being able to traverse up from current executable run path.
98+
// defaultFS exists to preserve pre v4.7.0 behaviour where files were open by `os.Open`.
99+
// v4.7 introduced `echo.Filesystem` field which is Go1.16+ `fs.Fs` interface.
100+
// Difference between `os.Open` and `fs.Open` is that FS does not allow opening path that start with `.`, `..` or `/`
101+
// etc. For example previously you could have `../images` in your application but `fs := os.DirFS("./")` would not
102+
// allow you to use `fs.Open("../images")` and this would break all old applications that rely on being able to
103+
// traverse up from current executable run path.
101104
// NB: private because you really should use fs.FS implementation instances
102105
type defaultFS struct {
103106
prefix string
@@ -108,20 +111,26 @@ func newDefaultFS() *defaultFS {
108111
dir, _ := os.Getwd()
109112
return &defaultFS{
110113
prefix: dir,
111-
fs: os.DirFS(dir),
114+
fs: nil,
112115
}
113116
}
114117

115118
func (fs defaultFS) Open(name string) (fs.File, error) {
119+
if fs.fs == nil {
120+
return os.Open(name)
121+
}
116122
return fs.fs.Open(name)
117123
}
118124

119125
func subFS(currentFs fs.FS, root string) (fs.FS, error) {
120126
root = filepath.ToSlash(filepath.Clean(root)) // note: fs.FS operates only with slashes. `ToSlash` is necessary for Windows
121127
if dFS, ok := currentFs.(*defaultFS); ok {
122-
// we need to make exception for `defaultFS` instances as it interprets root prefix differently from fs.FS to
123-
// allow cases when root is given as `../somepath` which is not valid for fs.FS
124-
root = filepath.Join(dFS.prefix, root)
128+
// we need to make exception for `defaultFS` instances as it interprets root prefix differently from fs.FS.
129+
// fs.Fs.Open does not like relative paths ("./", "../") and absolute paths at all but prior echo.Filesystem we
130+
// were able to use paths like `./myfile.log`, `/etc/hosts` and these would work fine with `os.Open` but not with fs.Fs
131+
if isRelativePath(root) {
132+
root = filepath.Join(dFS.prefix, root)
133+
}
125134
return &defaultFS{
126135
prefix: root,
127136
fs: os.DirFS(root),
@@ -130,6 +139,21 @@ func subFS(currentFs fs.FS, root string) (fs.FS, error) {
130139
return fs.Sub(currentFs, root)
131140
}
132141

142+
func isRelativePath(path string) bool {
143+
if path == "" {
144+
return true
145+
}
146+
if path[0] == '/' {
147+
return false
148+
}
149+
if runtime.GOOS == "windows" && strings.IndexByte(path, ':') != -1 {
150+
// https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file?redirectedfrom=MSDN#file_and_directory_names
151+
// https://docs.microsoft.com/en-us/dotnet/standard/io/file-path-formats
152+
return false
153+
}
154+
return true
155+
}
156+
133157
// MustSubFS creates sub FS from current filesystem or panic on failure.
134158
// Panic happens when `fsRoot` contains invalid path according to `fs.ValidPath` rules.
135159
//

echo_test.go

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,14 @@ func TestEchoStatic(t *testing.T) {
8484
expectStatus: http.StatusOK,
8585
expectBodyStartsWith: string([]byte{0x89, 0x50, 0x4e, 0x47}),
8686
},
87+
{
88+
name: "ok with relative path for root points to directory",
89+
givenPrefix: "/images",
90+
givenRoot: "./_fixture/images",
91+
whenURL: "/images/walle.png",
92+
expectStatus: http.StatusOK,
93+
expectBodyStartsWith: string([]byte{0x89, 0x50, 0x4e, 0x47}),
94+
},
8795
{
8896
name: "No file",
8997
givenPrefix: "/images",
@@ -246,11 +254,54 @@ func TestEchoStaticRedirectIndex(t *testing.T) {
246254
}
247255

248256
func TestEchoFile(t *testing.T) {
249-
e := New()
250-
e.File("/walle", "_fixture/images/walle.png")
251-
c, b := request(http.MethodGet, "/walle", e)
252-
assert.Equal(t, http.StatusOK, c)
253-
assert.NotEmpty(t, b)
257+
var testCases = []struct {
258+
name string
259+
givenPath string
260+
givenFile string
261+
whenPath string
262+
expectCode int
263+
expectStartsWith string
264+
}{
265+
{
266+
name: "ok",
267+
givenPath: "/walle",
268+
givenFile: "_fixture/images/walle.png",
269+
whenPath: "/walle",
270+
expectCode: http.StatusOK,
271+
expectStartsWith: string([]byte{0x89, 0x50, 0x4e}),
272+
},
273+
{
274+
name: "ok with relative path",
275+
givenPath: "/",
276+
givenFile: "./go.mod",
277+
whenPath: "/",
278+
expectCode: http.StatusOK,
279+
expectStartsWith: "module github.com/labstack/echo/v",
280+
},
281+
{
282+
name: "nok file does not exist",
283+
givenPath: "/",
284+
givenFile: "./this-file-does-not-exist",
285+
whenPath: "/",
286+
expectCode: http.StatusNotFound,
287+
expectStartsWith: "{\"message\":\"Not Found\"}\n",
288+
},
289+
}
290+
291+
for _, tc := range testCases {
292+
t.Run(tc.name, func(t *testing.T) {
293+
e := New() // we are using echo.defaultFS instance
294+
e.File(tc.givenPath, tc.givenFile)
295+
296+
c, b := request(http.MethodGet, tc.whenPath, e)
297+
assert.Equal(t, tc.expectCode, c)
298+
299+
if len(b) > len(tc.expectStartsWith) {
300+
b = b[:len(tc.expectStartsWith)]
301+
}
302+
assert.Equal(t, tc.expectStartsWith, b)
303+
})
304+
}
254305
}
255306

256307
func TestEchoMiddleware(t *testing.T) {

0 commit comments

Comments
 (0)