Skip to content

Commit

Permalink
Fix #1787: Add support for optional filesystem to the static middlewa…
Browse files Browse the repository at this point in the history
…re (#1797)

* Add optional filesystem to static middleware.
  • Loading branch information
lukasdietrich authored May 8, 2021
1 parent de3f87e commit b643e68
Show file tree
Hide file tree
Showing 3 changed files with 191 additions and 24 deletions.
80 changes: 56 additions & 24 deletions middleware/static.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ type (
// the filesystem path is not doubled
// Optional. Default value false.
IgnoreBase bool `yaml:"ignoreBase"`

// Filesystem provides access to the static content.
// Optional. Defaults to http.Dir(config.Root)
Filesystem http.FileSystem `yaml:"-"`
}
)

Expand Down Expand Up @@ -146,6 +150,10 @@ func StaticWithConfig(config StaticConfig) echo.MiddlewareFunc {
if config.Index == "" {
config.Index = DefaultStaticConfig.Index
}
if config.Filesystem == nil {
config.Filesystem = http.Dir(config.Root)
config.Root = "."
}

// Index template
t, err := template.New("index").Parse(html)
Expand Down Expand Up @@ -178,49 +186,73 @@ func StaticWithConfig(config StaticConfig) echo.MiddlewareFunc {
}
}

fi, err := os.Stat(name)
file, err := openFile(config.Filesystem, name)
if err != nil {
if os.IsNotExist(err) {
if err = next(c); err != nil {
if he, ok := err.(*echo.HTTPError); ok {
if config.HTML5 && he.Code == http.StatusNotFound {
return c.File(filepath.Join(config.Root, config.Index))
}
}
return
}
if !os.IsNotExist(err) {
return err
}

if err = next(c); err == nil {
return err
}

he, ok := err.(*echo.HTTPError)
if !(ok && config.HTML5 && he.Code == http.StatusNotFound) {
return err
}

file, err = openFile(config.Filesystem, filepath.Join(config.Root, config.Index))
if err != nil {
return err
}
return
}

if fi.IsDir() {
index := filepath.Join(name, config.Index)
fi, err = os.Stat(index)
defer file.Close()

info, err := file.Stat()
if err != nil {
return err
}

if info.IsDir() {
index, err := openFile(config.Filesystem, filepath.Join(name, config.Index))
if err != nil {
if config.Browse {
return listDir(t, name, c.Response())
return listDir(t, name, file, c.Response())
}

if os.IsNotExist(err) {
return next(c)
}
return
}

return c.File(index)
defer index.Close()

info, err = index.Stat()
if err != nil {
return err
}

return serveFile(c, index, info)
}

return c.File(name)
return serveFile(c, file, info)
}
}
}

func listDir(t *template.Template, name string, res *echo.Response) (err error) {
file, err := os.Open(name)
if err != nil {
return
}
files, err := file.Readdir(-1)
func openFile(fs http.FileSystem, name string) (http.File, error) {
pathWithSlashes := filepath.ToSlash(name)
return fs.Open(pathWithSlashes)
}

func serveFile(c echo.Context, file http.File, info os.FileInfo) error {
http.ServeContent(c.Response(), c.Request(), info.Name(), info.ModTime(), file)
return nil
}

func listDir(t *template.Template, name string, dir http.File, res *echo.Response) (err error) {
files, err := dir.Readdir(-1)
if err != nil {
return
}
Expand Down
106 changes: 106 additions & 0 deletions middleware/static_1_16_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// +build go1.16

package middleware

import (
"io/fs"
"net/http"
"net/http/httptest"
"os"
"testing"
"testing/fstest"

"github.com/labstack/echo/v4"
"github.com/stretchr/testify/assert"
)

func TestStatic_CustomFS(t *testing.T) {
var testCases = []struct {
name string
filesystem fs.FS
root string
whenURL string
expectContains string
expectCode int
}{
{
name: "ok, serve index with Echo message",
whenURL: "/",
filesystem: os.DirFS("../_fixture"),
expectCode: http.StatusOK,
expectContains: "<title>Echo</title>",
},

{
name: "ok, serve index with Echo message",
whenURL: "/_fixture/",
filesystem: os.DirFS(".."),
expectCode: http.StatusOK,
expectContains: "<title>Echo</title>",
},
{
name: "ok, serve file from map fs",
whenURL: "/file.txt",
filesystem: fstest.MapFS{
"file.txt": &fstest.MapFile{Data: []byte("file.txt is ok")},
},
expectCode: http.StatusOK,
expectContains: "file.txt is ok",
},
{
name: "nok, missing file in map fs",
whenURL: "/file.txt",
expectCode: http.StatusNotFound,
filesystem: fstest.MapFS{
"file2.txt": &fstest.MapFile{Data: []byte("file2.txt is ok")},
},
},
{
name: "nok, file is not a subpath of root",
whenURL: `/../../secret.txt`,
root: "/nested/folder",
filesystem: fstest.MapFS{
"secret.txt": &fstest.MapFile{Data: []byte("this is a secret")},
},
expectCode: http.StatusNotFound,
},
{
name: "nok, backslash is forbidden",
whenURL: `/..\..\secret.txt`,
expectCode: http.StatusNotFound,
root: "/nested/folder",
filesystem: fstest.MapFS{
"secret.txt": &fstest.MapFile{Data: []byte("this is a secret")},
},
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
e := echo.New()

config := StaticConfig{
Root: ".",
Filesystem: http.FS(tc.filesystem),
}

if tc.root != "" {
config.Root = tc.root
}

middlewareFunc := StaticWithConfig(config)
e.Use(middlewareFunc)

req := httptest.NewRequest(http.MethodGet, tc.whenURL, nil)
rec := httptest.NewRecorder()

e.ServeHTTP(rec, req)

assert.Equal(t, tc.expectCode, rec.Code)
if tc.expectContains != "" {
responseBody := rec.Body.String()
assert.Contains(t, responseBody, tc.expectContains)
}
})
}
}
29 changes: 29 additions & 0 deletions middleware/static_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,32 @@ func TestStatic(t *testing.T) {
expectCode: http.StatusNotFound,
expectContains: "{\"message\":\"Not Found\"}\n",
},
{
name: "ok, do not serve file, when a handler took care of the request",
whenURL: "/regular-handler",
expectCode: http.StatusOK,
expectContains: "ok",
},
{
name: "nok, when html5 fail if the index file does not exist",
givenConfig: &StaticConfig{
Root: "../_fixture",
HTML5: true,
Index: "missing.html",
},
whenURL: "/random",
expectCode: http.StatusInternalServerError,
},
{
name: "ok, serve from http.FileSystem",
givenConfig: &StaticConfig{
Root: "_fixture",
Filesystem: http.Dir(".."),
},
whenURL: "/",
expectCode: http.StatusOK,
expectContains: "<title>Echo</title>",
},
}

for _, tc := range testCases {
Expand All @@ -115,6 +141,9 @@ func TestStatic(t *testing.T) {
} else {
// middleware is on root level
e.Use(middlewareFunc)
e.GET("/regular-handler", func(c echo.Context) error {
return c.String(http.StatusOK, "ok")
})
}

req := httptest.NewRequest(http.MethodGet, tc.whenURL, nil)
Expand Down

0 comments on commit b643e68

Please sign in to comment.