Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions middleware/static.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ type StaticConfig struct {
EnablePathUnescaping bool

// DirectoryListTemplate is template to list directory contents
// File entries expose Name, URL, Dir and Size; URL is the escaped relative link.
// Optional. Default to `directoryListHTMLTemplate` constant below.
DirectoryListTemplate string
}
Expand Down Expand Up @@ -144,9 +145,9 @@ const directoryListHTMLTemplate = `
<li>
{{ if .Dir }}
{{ $name := print .Name "/" }}
<a class="dir" href="{{ $name }}">{{ $name }}</a>
<a class="dir" href="{{ .URL }}">{{ $name }}</a>
{{ else }}
<a class="file" href="{{ .Name }}">{{ .Name }}</a>
<a class="file" href="{{ .URL }}">{{ .Name }}</a>
<span>{{ .Size }}</span>
{{ end }}
</li>
Expand Down Expand Up @@ -371,11 +372,16 @@ func listDir(t *template.Template, pathInFs string, filesystem fs.FS, res http.R
size = info.Size()
}

link := &url.URL{Path: f.Name()}
if f.IsDir() {
link.Path += "/"
}
data.Files = append(data.Files, struct {
Name string
URL string
Dir bool
Size string
}{f.Name(), f.IsDir(), format(size)})
}{f.Name(), link.String(), f.IsDir(), format(size)})
}

return t.Execute(res, data)
Expand Down
49 changes: 49 additions & 0 deletions middleware/static_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package middleware

import (
"html"
"io/fs"
"net/http"
"net/http/httptest"
Expand Down Expand Up @@ -108,6 +109,54 @@ func TestStaticHTML5PreservesMatchedHandlerNotFound(t *testing.T) {
}
}

func TestStaticBrowseEscapesNames(t *testing.T) {
for _, tc := range []struct {
name string
dir bool
}{
{name: "plain.txt"},
{name: "hash#file.txt"},
{name: "query?.txt"},
{name: "percent%.txt"},
{name: "with space.txt"},
{name: "colon:name.txt"},
{name: "amp&file.txt"},
{name: "hash#dir", dir: true},
} {
t.Run(tc.name, func(t *testing.T) {
fileName := tc.name
if tc.dir {
fileName += "/index.html"
}
e := echo.New()
e.Use(StaticWithConfig(StaticConfig{
Filesystem: fstest.MapFS{fileName: {Data: []byte("file contents")}},
Browse: true,
}))

req := httptest.NewRequest(http.MethodGet, "http://example.com/", nil)
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)
require.Equal(t, http.StatusOK, rec.Code)

_, rest, found := strings.Cut(rec.Body.String(), `href="`)
require.True(t, found)
href, _, found := strings.Cut(rest, `"`)
require.True(t, found)
target, err := req.URL.Parse(html.UnescapeString(href))
require.NoError(t, err)
// Browsers do not send URL fragments to the server.
target.Fragment = ""
target.RawFragment = ""

rec = httptest.NewRecorder()
e.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, target.String(), nil))
assert.Equal(t, http.StatusOK, rec.Code)
assert.Equal(t, "file contents", rec.Body.String())
})
}
}

func TestStatic(t *testing.T) {
var testCases = []struct {
name string
Expand Down