Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
2 changes: 0 additions & 2 deletions handlers/clear.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,8 @@ func Clear(app *config.App) http.HandlerFunc {
if req == nil {
return
}

app.ClearStorage()
app.Log.Info("storage cleared", "user", req)

toRoot(w, r, app.Root)
}
}
26 changes: 22 additions & 4 deletions handlers/form.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,43 @@
import (
"net/http"
"strconv"
"strings"
"time"

"github.com/drduh/gone/settings"
)

// parseFormInt reads an integer form value or returns the default.
func parseFormInt(r *http.Request, field string, def int) int {
func parseFormInt(r *http.Request, field string, def int, maximum int) int {
input := r.FormValue(field)
if input != "" {
if v, err := strconv.Atoi(input); err == nil {
return v
input = strings.TrimSpace(input)
if v, err := strconv.ParseUint(input, 10, 64); err == nil {
if v == 0 {
return def
}
if v > uint64(maximum) {
return maximum
}
return int(v)
}
}
return def
}

// parseFormDuration reads a duration form value or returns the default.
func parseFormDuration(r *http.Request, field string, def time.Duration) time.Duration {
func parseFormDuration(r *http.Request, field string,
def time.Duration, maximum settings.Duration) time.Duration {
input := r.FormValue(field)
if input != "" {
input = strings.TrimSpace(input)
if d, err := time.ParseDuration(input); err == nil {
if d < time.Second {
return def
}
if d > maximum.GetDuration() {
return maximum.GetDuration()
}
return d
}
}
Expand Down
74 changes: 54 additions & 20 deletions handlers/form_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,40 @@ import (
"net/http"
"testing"
"time"

"github.com/drduh/gone/settings"
)

// TestParseFormInt tests integer form values are parsed.
func TestParseFormInt(t *testing.T) {
def := 1
maximum := 100
tests := []struct {
name string
query string
field string
def int
want int
name string
query string
field string
def int
want int
maximum int
}{
{"valid", "/?downloads=5", "downloads", 10, 5},
{"missing", "/", "downloads", 10, 10},
{"invalid", "/?downloads=none", "downloads", 10, 10},
{"valid", "/?downloads=5", "downloads",
def, 5, maximum},
{"space", "/?downloads= 5 ", "downloads",
def, 5, maximum},
{"missing", "/", "downloads",
def, 1, maximum},
{"invalid", "/?downloads=none", "downloads",
def, 1, maximum},
{"zero", "/?downloads=0", "downloads",
def, def, maximum},
{"negative", "/?downloads=-1", "downloads",
def, def, maximum},
{"fraction", "/?downloads=3.5", "downloads",
def, def, maximum},
{"large", "/?downloads=101", "downloads",
def, maximum, maximum},
{"xlarge", "/?downloads=999999999999999999999",
"downloads", def, def, maximum}, // overflows int64
}
for _, tc := range tests {
tc := tc
Expand All @@ -27,7 +47,7 @@ func TestParseFormInt(t *testing.T) {
if err != nil {
t.Fatal(err)
}
got := parseFormInt(req, tc.field, tc.def)
got := parseFormInt(req, tc.field, tc.def, tc.maximum)
if got != tc.want {
t.Fatalf("parseFormInt(%q) = %d; want %d",
tc.query, got, tc.want)
Expand All @@ -38,21 +58,34 @@ func TestParseFormInt(t *testing.T) {

// TestParseFormDuration tests duration form values are parsed.
func TestParseFormDuration(t *testing.T) {
def := 1 * time.Hour
maximum := 8 * 24 * time.Hour
tests := []struct {
name string
query string
field string
def time.Duration
want time.Duration
name string
query string
field string
def time.Duration
want time.Duration
maximum time.Duration
}{
{"valid", "/?duration=1h30m", "duration",
2 * time.Hour, 90 * time.Minute},
def, 90 * time.Minute, maximum},
{"space", "/?duration= 15m ", "duration",
def, 15 * time.Minute, maximum},
{"missing", "/", "duration",
2 * time.Hour, 2 * time.Hour},
def, def, maximum},
{"invalid", "/?duration=none", "duration",
2 * time.Hour, 2 * time.Hour},
{"seconds", "/?duration=123s", "duration",
2 * time.Hour, 2*time.Minute + 3*time.Second},
def, def, maximum},
{"zero", "/?duration=0s", "duration",
def, def, maximum},
{"negative", "/?duration=-1h", "duration",
def, def, maximum},
{"fraction", "/?duration=1.5h", "duration",
def, 90 * time.Minute, maximum},
{"large", "/?duration=9999h", "duration",
def, maximum, maximum},
{"xlarge", "/?duration=99999999999h", "duration",
def, def, maximum}, // overflows int64
}
for _, tc := range tests {
tc := tc
Expand All @@ -62,7 +95,8 @@ func TestParseFormDuration(t *testing.T) {
if err != nil {
t.Fatal(err)
}
got := parseFormDuration(req, tc.field, tc.def)
got := parseFormDuration(req, tc.field, tc.def,
settings.Duration{Duration: tc.maximum})
if got != tc.want {
t.Fatalf("parseFormDuration(%q) = %v; want %v",
tc.query, got, tc.want)
Expand Down
43 changes: 1 addition & 42 deletions handlers/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"net/http"

"github.com/drduh/gone/config"
"github.com/drduh/gone/storage"
)

// List handles requests to list Files in Storage.
Expand All @@ -15,49 +14,9 @@ func List(app *config.App) http.HandlerFunc {
return
}
app.UpdateTimeRemaining()
files := getFiles(app)
files := app.ListFiles()
app.Log.Info("serving file list",
"files", len(files), "user", req)
writeJSON(w, http.StatusOK, files)
}
}

// getFiles returns a list of non-expired Files in Storage,
// and removes expired Files.
func getFiles(app *config.App) []storage.File {
files := make([]storage.File, 0, len(app.Files))
for _, file := range app.Files {
reason := file.IsExpired()
if reason != "" {
app.Expire(file)
app.Log.Info("removed file", "reason", reason,
"id", file.Id, "name", file.Name,
"downloads", file.Total)
break
}

f := storage.File{
Id: file.Id,
Name: file.Name,
Size: file.Size,
Sum: file.Sum,
Type: file.Type,
Owner: storage.Owner{
Agent: file.Agent,
Mask: file.Mask,
},
Time: storage.Time{
Remain: file.Time.Remain,
Upload: file.Upload,
},
Downloads: storage.Downloads{
Allow: file.Downloads.Allow,
Remain: file.NumRemaining(),
Total: file.Total,
},
}
files = append(files, f)
}

return files
}
8 changes: 5 additions & 3 deletions handlers/upload.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,11 @@ func Upload(app *config.App) http.HandlerFunc {
}

downloadLimit := parseFormInt(r,
formFieldDownloads, app.Downloads)
formFieldDownloads, app.Downloads, app.MaxDownloads)
app.Log.Debug("got form value", formFieldDownloads, downloadLimit)

durationLimit := parseFormDuration(r,
formFieldDuration, app.Expiration.Duration)
formFieldDuration, app.Expiration.Duration, app.MaxDuration)
app.Log.Debug("got form value", formFieldDuration, durationLimit)

var upload storage.File
Expand Down Expand Up @@ -77,8 +77,10 @@ func Upload(app *config.App) http.HandlerFunc {
return
}

filename := storage.SanitizeName(fileHeader.Filename,
app.MaxSizeName, app.FilenameExtraChars)
f := &storage.File{
Name: fileHeader.Filename,
Name: filename,
Data: buf.Bytes(),
Owner: storage.Owner{
Address: req.Address,
Expand Down
12 changes: 12 additions & 0 deletions settings/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,9 +185,21 @@ type Default struct {
// Content represents limits on content sharing.
type Limit struct {

// Extra characters allowed in file names
FilenameExtraChars string `json:"filenameChars,omitempty"`

// Maximum number of allowed downloads
MaxDownloads int `json:"maxDownloads,omitempty"`

// Maximum expiration duration
MaxDuration Duration `json:"maxDuration,omitempty"`

// Maximum text message size
MaxSizeMsg int `json:"maxSizeMsg,omitempty"`

// Maximum file name length
MaxSizeName int `json:"maxSizeName,omitempty"`

// Maximum wall content size
MaxSizeWall int `json:"maxSizeWall,omitempty"`

Expand Down
8 changes: 6 additions & 2 deletions settings/defaultSettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,15 @@
}
},
"limit": {
"expiryTicker": "30s",
"filenameChars": "_.-",
"maxDownloads": 100,
"maxDuration": "192h",
"maxSizeMsg": 128,
"maxSizeName": 64,
"maxSizeWall": 1280,
"maxSizeFileMb": 256,
"reqsPerMinute": 30,
"expiryTicker": "30s"
"reqsPerMinute": 30
},
"paths": {
"assets": "/assets/",
Expand Down
2 changes: 1 addition & 1 deletion storage/config.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Package storage defines uploaded content.
// Package storage provides content storage and functions.
package storage

import (
Expand Down
38 changes: 38 additions & 0 deletions storage/list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package storage

// ListFiles returns a list of non-expired Files in Storage,
// removing expired Files in the process.
func (s *Storage) ListFiles() []File {
files := make([]File, 0, len(s.Files))
for _, file := range s.Files {
reason := file.IsExpired()
if reason != "" {
s.Expire(file)
break
}

f := File{
Id: file.Id,
Name: file.Name,
Size: file.Size,
Sum: file.Sum,
Type: file.Type,
Owner: Owner{
Agent: file.Agent,
Mask: file.Mask,
},
Time: Time{
Remain: file.Time.Remain,
Upload: file.Upload,
},
Downloads: Downloads{
Allow: file.Downloads.Allow,
Remain: file.NumRemaining(),
Total: file.Total,
},
}
files = append(files, f)
}

return files
}
57 changes: 57 additions & 0 deletions storage/sanitize.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package storage

import (
"path/filepath"
"strings"
"unicode"
)

const defaultName = "default"

// SanitizeName validates strings for use as filename.
func SanitizeName(input string, maxLength int, extraChars string) string {
f := filepath.Base(input)
f = removeInvalidChars(f, extraChars)
ext := filepath.Ext(f)
base := strings.TrimSuffix(f, ext)
if base == "" {
base = defaultName
}
return truncateName(base, ext, maxLength)
}

// removeInvalidChars removes all invalid characters.
func removeInvalidChars(filename string, allowed string) string {
var result strings.Builder
for _, char := range filename {
if unicode.IsDigit(char) ||
unicode.IsLetter(char) ||
isAllowedChar(char, allowed) {
result.WriteRune(char)
}
}
return result.String()
}

// truncateName trims a filename string to max size,
// preserving reasonably-sized original file extensions.
func truncateName(base string, ext string, maxLength int) string {
const maxExtensionLength = 5
if len(ext) > maxExtensionLength {
ext = ext[:maxExtensionLength]
}
totalLength := len(base) + len(ext)
if totalLength <= maxLength {
return base + ext
}
allowedBaseLength := maxLength - len(ext)
if allowedBaseLength > 0 {
return base[:allowedBaseLength] + ext
}
return ext[:maxLength]
}

// isAllowedChar returns true if a character is allowed.
func isAllowedChar(r rune, allowed string) bool {
return strings.ContainsRune(allowed, r)
}
Loading
Loading