Skip to content

Commit

Permalink
Port the whole upload details page
Browse files Browse the repository at this point in the history
  • Loading branch information
chriskuehl committed Sep 21, 2024
1 parent 5dab2cd commit 1d4bc5e
Show file tree
Hide file tree
Showing 13 changed files with 217 additions and 141 deletions.
2 changes: 2 additions & 0 deletions TODO
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
* Add some basic logging to storages, none of them have it.
* Also some timing information to the upload view + UploadObjects function
* Test that tries to upload an HTML file and ensures it's not served as HTML.
* Consolidate StoredFile and StoredHTML somehow.
2 changes: 1 addition & 1 deletion scss/details.scss
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.page-details {
.page-upload-details {
.file-holder {
display: flex;
justify-content: center;
Expand Down
7 changes: 2 additions & 5 deletions server/assets/assets.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,10 @@ import (
"github.com/chriskuehl/fluffy/server/config"
)

var mimeExtensions = []string{}

func LoadAssets(assetsFS *embed.FS) (*config.Assets, error) {
assets := config.Assets{
FS: assetsFS,
Hashes: map[string]string{},
// MIMEExtensions is a set of all the mime extensions, without dot, e.g. "png", "jpg".
FS: assetsFS,
Hashes: map[string]string{},
MIMEExtensions: map[string]struct{}{},
}

Expand Down
12 changes: 10 additions & 2 deletions server/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,10 @@ type StorageBackend interface {
}

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

Expand All @@ -73,6 +75,9 @@ type Config struct {
HomeURL *url.URL
FileURLPattern *url.URL
HTMLURLPattern *url.URL
// ForbiddenFileExtensions is the set of file extensions that are not allowed to be uploaded.
// The extensions should not start with a dot, but may contain one if trying to match multiple
// extensions, e.g. "tar.gz".
ForbiddenFileExtensions map[string]struct{}
Host string
Port uint
Expand Down Expand Up @@ -126,6 +131,9 @@ func (conf *Config) Validate() []string {
if strings.HasPrefix(ext, ".") {
errs = append(errs, "ForbiddenFileExtensions should not start with a dot: "+ext)
}
if strings.ToLower(ext) != ext {
errs = append(errs, "ForbiddenFileExtensions should be lowercase: "+ext)
}
}
if conf.Version == "" {
errs = append(errs, "Version must not be empty")
Expand Down
21 changes: 21 additions & 0 deletions server/meta/meta.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/chriskuehl/fluffy/server/assets"
"github.com/chriskuehl/fluffy/server/config"
"github.com/chriskuehl/fluffy/server/security"
"github.com/chriskuehl/fluffy/server/utils"
)

type PageConfig struct {
Expand Down Expand Up @@ -67,3 +68,23 @@ func (m Meta) AssetURL(path string) string {
}
return url
}

func (m Meta) MIMEIcon(filename string) string {
// Try "file.tar.gz" => "tar.gz" => "gz" => "unknown".
parts := strings.Split(filename, ".")
for i := 0; i < len(parts); i++ {
ext := strings.Join(parts[i:], ".")
if _, ok := m.Conf.Assets.MIMEExtensions[ext]; ok {
return ext
}
}
return "unknown"
}

func (m Meta) MIMEIconSmallURL(iconName string) string {
return m.AssetURL("img/mime/small/" + iconName + ".png")
}

func (m Meta) FormatBytes(bytes int64) string {
return utils.FormatBytes(bytes)
}
35 changes: 0 additions & 35 deletions server/templates/upload-details-bk

This file was deleted.

38 changes: 35 additions & 3 deletions server/templates/upload-details.html
Original file line number Diff line number Diff line change
@@ -1,10 +1,42 @@
{{define "extraHead"}}{{end}}

{{define "content"}}
CONTENT HERE
{{end}}
<div id="files">
{{range .UploadedFiles}}
<div class="file-holder">
<div class="file{{if .IsImage}} image{{end}}">
<div class="filename">
<img src="{{$.Meta.MIMEIconSmallURL ($.Meta.MIMEIcon .Name)}}" />
{{.Name}}
</div>

{{if .IsImage}}
<div class="image-holder">
<a href="{{$.Meta.Conf.FileURL .Key}}">
<img src="{{$.Meta.Conf.FileURL .Key}}" />
</a>
</div>
{{end}}

<div class="metadata-bar">
<div class="filesize">
{{$.Meta.FormatBytes .Bytes}}
</div>

{{define "inlineJS"}}
<div class="buttons">
<a href="{{$.Meta.Conf.FileURL .Key}}" class="download">Direct Link</a>
<!-- TODO: implement this! -->
<a href="TODO_PASTE_URL" class="view-paste">View Text</a>
</div>

<div class="clearfix"></div>
</div>
</div>
</div>
{{end}}
</div>
{{end}}

{{define "inlineJS"}}{{end}}

{{template "base.html" .}}
46 changes: 17 additions & 29 deletions server/uploads/uploads.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,6 @@ const (
var (
ErrForbiddenExtension = fmt.Errorf("forbidden extension")

// Extensions that traditionally wrap another file extension.
wrapperExtensions = map[string]struct{}{
"bz2": {},
"gz": {},
"xz": {},
"zst": {},
}

// MIME types which are allowed to be presented as detected.
// TODO: I think we actually only need to prevent text/html (and any HTML
// variants like XHTML)?
Expand All @@ -63,6 +55,14 @@ var (
"image/",
"video/",
}
imageMIMEAllowlist = map[string]struct{}{
"image/gif": {},
"image/jpeg": {},
"image/png": {},
"image/svg+xml": {},
"image/tiff": {},
"image/webp": {},
}
)

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

func extractExtension(name string) string {
fullExt := ""
for strings.Contains(name, ".") {
ext := filepath.Ext(name)
name = strings.TrimSuffix(name, ext)
if ext == "." {
// Don't add ".", but keep processing any additional extensions.
continue
}
fullExt = ext + fullExt
if _, ok := wrapperExtensions[strings.TrimPrefix(ext, ".")]; !ok {
return fullExt
}
}
return fullExt
}

type SanitizedKey struct {
UniqueID string
Extension string
Expand All @@ -114,15 +97,15 @@ func SanitizeUploadName(name string, forbiddenExtensions map[string]struct{}) (*
if err != nil {
return nil, fmt.Errorf("generating unique object key: %w", err)
}
ext := extractExtension(name)
for _, extPart := range strings.Split(ext, ".") {
if _, ok := forbiddenExtensions[extPart]; ok {
lowercaseName := strings.ToLower(name)
for ext := range forbiddenExtensions {
if strings.HasSuffix(lowercaseName, "."+ext) || strings.Contains(lowercaseName, "."+ext+".") {
return nil, ErrForbiddenExtension
}
}
return &SanitizedKey{
UniqueID: id,
Extension: ext,
Extension: utils.HumanFileExtension(name),
}, nil
}

Expand Down Expand Up @@ -266,6 +249,11 @@ func isInlineDisplayMIME(mimeType string) bool {
return false
}

func IsImageMIME(mimeType string) bool {
_, ok := imageMIMEAllowlist[mimeType]
return ok
}

func DetermineContentDisposition(filename string, mimeType string, probablyText bool) string {
renderType := "attachment"
if probablyText || isInlineDisplayMIME(mimeType) {
Expand Down
7 changes: 6 additions & 1 deletion server/uploads/uploads_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,14 +98,19 @@ func TestSanitizeUploadName(t *testing.T) {
in: "file.exe",
wantErr: uploads.ErrForbiddenExtension,
},
{
name: "forbidden extension with caps",
in: "file.EXE",
wantErr: uploads.ErrForbiddenExtension,
},
{
name: "forbidden extension before wrapped extension",
in: "file.exe.gz",
wantErr: uploads.ErrForbiddenExtension,
},
{
name: "forbidden extension before wrapped extension with ..",
in: "file.exe..gz",
in: "file.Exe..gz",
wantErr: uploads.ErrForbiddenExtension,
},
}
Expand Down
63 changes: 0 additions & 63 deletions server/uploads/uploads_x_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,69 +15,6 @@ func TestGetUniqueObjectKey(t *testing.T) {
}
}

func TestExtractExtension(t *testing.T) {
tests := []struct {
name string
in string
want string
wantErr error
}{
{
name: "no extension",
in: "file",
want: "",
},
{
name: "regular extension",
in: "file.txt",
want: ".txt",
},
{
name: "wrapped extension only",
in: "file.gz",
want: ".gz",
},
{
name: "wrapped extension after regular extension",
in: "file.tar.gz",
want: ".tar.gz",
},
{
name: "multiple wrapped extensions",
in: "file.tar.gz.bz2",
want: ".tar.gz.bz2",
},
{
name: "multiple wrapped extensions with a regular extension",
in: "file.txt.tar.gz.bz2",
want: ".tar.gz.bz2",
},
{
// Kind of nonsense, just making sure it doesn't remove more than it should.
name: "wrapped extensions before regular extension",
in: "file.tar.gz.txt",
want: ".txt",
},
{
name: ". only",
in: ".",
want: "",
},
{
name: "multiple wrapped extensions with empty extensions",
in: "file.txt.tar.gz....bz2",
want: ".tar.gz.bz2",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := extractExtension(tt.in); got != tt.want {
t.Errorf("got extractExtension(%q) = %q, want %q", tt.in, got, tt.want)
}
})
}
}

func TestIsAllowedMIMEType(t *testing.T) {
tests := map[string]bool{
"application/javascript": true,
Expand Down
36 changes: 36 additions & 0 deletions server/utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,44 @@ package utils
import (
"fmt"
"io"
"path/filepath"
"strings"
)

// Extensions that traditionally wrap another file extension.
var wrapperExtensions = map[string]struct{}{
"bz2": {},
"gz": {},
"xz": {},
"zst": {},
}

// HumanFileExtension returns the "human" file extension of a file name.
//
// This function tries to mimic what a human would consider the file extension rather than always
// returning just the last extension. For example, "file.tar.gz" would return ".tar.gz" instead of
// just ".gz".
//
// Files with no extension will return an empty string.
//
// This function should not be used for any kind of validation purposes.
func HumanFileExtension(filename string) string {
fullExt := ""
for strings.Contains(filename, ".") {
ext := filepath.Ext(filename)
filename = strings.TrimSuffix(filename, ext)
if ext == "." {
// Don't add ".", but keep processing any additional extensions.
continue
}
fullExt = ext + fullExt
if _, ok := wrapperExtensions[strings.TrimPrefix(ext, ".")]; !ok {
return fullExt
}
}
return fullExt
}

func Pluralize(s string, n int64) string {
if n == 1 {
return s
Expand Down
Loading

0 comments on commit 1d4bc5e

Please sign in to comment.