Skip to content

Commit 1d4bc5e

Browse files
committed
Port the whole upload details page
1 parent 5dab2cd commit 1d4bc5e

File tree

13 files changed

+217
-141
lines changed

13 files changed

+217
-141
lines changed

TODO

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
1+
* Add some basic logging to storages, none of them have it.
2+
* Also some timing information to the upload view + UploadObjects function
13
* Test that tries to upload an HTML file and ensures it's not served as HTML.
24
* Consolidate StoredFile and StoredHTML somehow.

scss/details.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.page-details {
1+
.page-upload-details {
22
.file-holder {
33
display: flex;
44
justify-content: center;

server/assets/assets.go

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,10 @@ import (
1313
"github.com/chriskuehl/fluffy/server/config"
1414
)
1515

16-
var mimeExtensions = []string{}
17-
1816
func LoadAssets(assetsFS *embed.FS) (*config.Assets, error) {
1917
assets := config.Assets{
20-
FS: assetsFS,
21-
Hashes: map[string]string{},
22-
// MIMEExtensions is a set of all the mime extensions, without dot, e.g. "png", "jpg".
18+
FS: assetsFS,
19+
Hashes: map[string]string{},
2320
MIMEExtensions: map[string]struct{}{},
2421
}
2522

server/config/config.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,10 @@ type StorageBackend interface {
4949
}
5050

5151
type Assets struct {
52-
FS *embed.FS
53-
Hashes map[string]string
52+
FS *embed.FS
53+
// Hashes is a map of file paths to their SHA-256 hashes.
54+
Hashes map[string]string
55+
// MIMEExtensions is a set of all the mime extensions, without dot, e.g. "png", "jpg".
5456
MIMEExtensions map[string]struct{}
5557
}
5658

@@ -73,6 +75,9 @@ type Config struct {
7375
HomeURL *url.URL
7476
FileURLPattern *url.URL
7577
HTMLURLPattern *url.URL
78+
// ForbiddenFileExtensions is the set of file extensions that are not allowed to be uploaded.
79+
// The extensions should not start with a dot, but may contain one if trying to match multiple
80+
// extensions, e.g. "tar.gz".
7681
ForbiddenFileExtensions map[string]struct{}
7782
Host string
7883
Port uint
@@ -126,6 +131,9 @@ func (conf *Config) Validate() []string {
126131
if strings.HasPrefix(ext, ".") {
127132
errs = append(errs, "ForbiddenFileExtensions should not start with a dot: "+ext)
128133
}
134+
if strings.ToLower(ext) != ext {
135+
errs = append(errs, "ForbiddenFileExtensions should be lowercase: "+ext)
136+
}
129137
}
130138
if conf.Version == "" {
131139
errs = append(errs, "Version must not be empty")

server/meta/meta.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"github.com/chriskuehl/fluffy/server/assets"
1111
"github.com/chriskuehl/fluffy/server/config"
1212
"github.com/chriskuehl/fluffy/server/security"
13+
"github.com/chriskuehl/fluffy/server/utils"
1314
)
1415

1516
type PageConfig struct {
@@ -67,3 +68,23 @@ func (m Meta) AssetURL(path string) string {
6768
}
6869
return url
6970
}
71+
72+
func (m Meta) MIMEIcon(filename string) string {
73+
// Try "file.tar.gz" => "tar.gz" => "gz" => "unknown".
74+
parts := strings.Split(filename, ".")
75+
for i := 0; i < len(parts); i++ {
76+
ext := strings.Join(parts[i:], ".")
77+
if _, ok := m.Conf.Assets.MIMEExtensions[ext]; ok {
78+
return ext
79+
}
80+
}
81+
return "unknown"
82+
}
83+
84+
func (m Meta) MIMEIconSmallURL(iconName string) string {
85+
return m.AssetURL("img/mime/small/" + iconName + ".png")
86+
}
87+
88+
func (m Meta) FormatBytes(bytes int64) string {
89+
return utils.FormatBytes(bytes)
90+
}

server/templates/upload-details-bk

Lines changed: 0 additions & 35 deletions
This file was deleted.

server/templates/upload-details.html

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,42 @@
11
{{define "extraHead"}}{{end}}
22

33
{{define "content"}}
4-
CONTENT HERE
5-
{{end}}
4+
<div id="files">
5+
{{range .UploadedFiles}}
6+
<div class="file-holder">
7+
<div class="file{{if .IsImage}} image{{end}}">
8+
<div class="filename">
9+
<img src="{{$.Meta.MIMEIconSmallURL ($.Meta.MIMEIcon .Name)}}" />
10+
{{.Name}}
11+
</div>
12+
13+
{{if .IsImage}}
14+
<div class="image-holder">
15+
<a href="{{$.Meta.Conf.FileURL .Key}}">
16+
<img src="{{$.Meta.Conf.FileURL .Key}}" />
17+
</a>
18+
</div>
19+
{{end}}
20+
21+
<div class="metadata-bar">
22+
<div class="filesize">
23+
{{$.Meta.FormatBytes .Bytes}}
24+
</div>
625

7-
{{define "inlineJS"}}
26+
<div class="buttons">
27+
<a href="{{$.Meta.Conf.FileURL .Key}}" class="download">Direct Link</a>
28+
<!-- TODO: implement this! -->
29+
<a href="TODO_PASTE_URL" class="view-paste">View Text</a>
30+
</div>
31+
32+
<div class="clearfix"></div>
33+
</div>
34+
</div>
35+
</div>
36+
{{end}}
37+
</div>
838
{{end}}
939

40+
{{define "inlineJS"}}{{end}}
41+
1042
{{template "base.html" .}}

server/uploads/uploads.go

Lines changed: 17 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,6 @@ const (
2929
var (
3030
ErrForbiddenExtension = fmt.Errorf("forbidden extension")
3131

32-
// Extensions that traditionally wrap another file extension.
33-
wrapperExtensions = map[string]struct{}{
34-
"bz2": {},
35-
"gz": {},
36-
"xz": {},
37-
"zst": {},
38-
}
39-
4032
// MIME types which are allowed to be presented as detected.
4133
// TODO: I think we actually only need to prevent text/html (and any HTML
4234
// variants like XHTML)?
@@ -63,6 +55,14 @@ var (
6355
"image/",
6456
"video/",
6557
}
58+
imageMIMEAllowlist = map[string]struct{}{
59+
"image/gif": {},
60+
"image/jpeg": {},
61+
"image/png": {},
62+
"image/svg+xml": {},
63+
"image/tiff": {},
64+
"image/webp": {},
65+
}
6666
)
6767

6868
// GenUniqueObjectKey returns a random string for use as object key.
@@ -81,23 +81,6 @@ func GenUniqueObjectKey() (string, error) {
8181
return s.String(), nil
8282
}
8383

84-
func extractExtension(name string) string {
85-
fullExt := ""
86-
for strings.Contains(name, ".") {
87-
ext := filepath.Ext(name)
88-
name = strings.TrimSuffix(name, ext)
89-
if ext == "." {
90-
// Don't add ".", but keep processing any additional extensions.
91-
continue
92-
}
93-
fullExt = ext + fullExt
94-
if _, ok := wrapperExtensions[strings.TrimPrefix(ext, ".")]; !ok {
95-
return fullExt
96-
}
97-
}
98-
return fullExt
99-
}
100-
10184
type SanitizedKey struct {
10285
UniqueID string
10386
Extension string
@@ -114,15 +97,15 @@ func SanitizeUploadName(name string, forbiddenExtensions map[string]struct{}) (*
11497
if err != nil {
11598
return nil, fmt.Errorf("generating unique object key: %w", err)
11699
}
117-
ext := extractExtension(name)
118-
for _, extPart := range strings.Split(ext, ".") {
119-
if _, ok := forbiddenExtensions[extPart]; ok {
100+
lowercaseName := strings.ToLower(name)
101+
for ext := range forbiddenExtensions {
102+
if strings.HasSuffix(lowercaseName, "."+ext) || strings.Contains(lowercaseName, "."+ext+".") {
120103
return nil, ErrForbiddenExtension
121104
}
122105
}
123106
return &SanitizedKey{
124107
UniqueID: id,
125-
Extension: ext,
108+
Extension: utils.HumanFileExtension(name),
126109
}, nil
127110
}
128111

@@ -266,6 +249,11 @@ func isInlineDisplayMIME(mimeType string) bool {
266249
return false
267250
}
268251

252+
func IsImageMIME(mimeType string) bool {
253+
_, ok := imageMIMEAllowlist[mimeType]
254+
return ok
255+
}
256+
269257
func DetermineContentDisposition(filename string, mimeType string, probablyText bool) string {
270258
renderType := "attachment"
271259
if probablyText || isInlineDisplayMIME(mimeType) {

server/uploads/uploads_test.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,14 +98,19 @@ func TestSanitizeUploadName(t *testing.T) {
9898
in: "file.exe",
9999
wantErr: uploads.ErrForbiddenExtension,
100100
},
101+
{
102+
name: "forbidden extension with caps",
103+
in: "file.EXE",
104+
wantErr: uploads.ErrForbiddenExtension,
105+
},
101106
{
102107
name: "forbidden extension before wrapped extension",
103108
in: "file.exe.gz",
104109
wantErr: uploads.ErrForbiddenExtension,
105110
},
106111
{
107112
name: "forbidden extension before wrapped extension with ..",
108-
in: "file.exe..gz",
113+
in: "file.Exe..gz",
109114
wantErr: uploads.ErrForbiddenExtension,
110115
},
111116
}

server/uploads/uploads_x_test.go

Lines changed: 0 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -15,69 +15,6 @@ func TestGetUniqueObjectKey(t *testing.T) {
1515
}
1616
}
1717

18-
func TestExtractExtension(t *testing.T) {
19-
tests := []struct {
20-
name string
21-
in string
22-
want string
23-
wantErr error
24-
}{
25-
{
26-
name: "no extension",
27-
in: "file",
28-
want: "",
29-
},
30-
{
31-
name: "regular extension",
32-
in: "file.txt",
33-
want: ".txt",
34-
},
35-
{
36-
name: "wrapped extension only",
37-
in: "file.gz",
38-
want: ".gz",
39-
},
40-
{
41-
name: "wrapped extension after regular extension",
42-
in: "file.tar.gz",
43-
want: ".tar.gz",
44-
},
45-
{
46-
name: "multiple wrapped extensions",
47-
in: "file.tar.gz.bz2",
48-
want: ".tar.gz.bz2",
49-
},
50-
{
51-
name: "multiple wrapped extensions with a regular extension",
52-
in: "file.txt.tar.gz.bz2",
53-
want: ".tar.gz.bz2",
54-
},
55-
{
56-
// Kind of nonsense, just making sure it doesn't remove more than it should.
57-
name: "wrapped extensions before regular extension",
58-
in: "file.tar.gz.txt",
59-
want: ".txt",
60-
},
61-
{
62-
name: ". only",
63-
in: ".",
64-
want: "",
65-
},
66-
{
67-
name: "multiple wrapped extensions with empty extensions",
68-
in: "file.txt.tar.gz....bz2",
69-
want: ".tar.gz.bz2",
70-
},
71-
}
72-
for _, tt := range tests {
73-
t.Run(tt.name, func(t *testing.T) {
74-
if got := extractExtension(tt.in); got != tt.want {
75-
t.Errorf("got extractExtension(%q) = %q, want %q", tt.in, got, tt.want)
76-
}
77-
})
78-
}
79-
}
80-
8118
func TestIsAllowedMIMEType(t *testing.T) {
8219
tests := map[string]bool{
8320
"application/javascript": true,

0 commit comments

Comments
 (0)