Skip to content
Merged
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
.DS_Store
*.log
main
release
testCoverage*
12 changes: 10 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,10 @@ BUILDFLAG = \
-X "$(BUILDPKG).Version=$(VERSION)"
BUILDCMD = $(GO) build -ldflags '-s -w $(BUILDFLAG)'
BINNAME = $(APPNAME)-$(BUILDOS)-$(BUILDARCH)-$(VERSION)
GOBUILD = GOOS=$(BUILDOS) GOARCH=$(BUILDARCH) \
$(BUILDCMD) -o $(OUT)/$(BINNAME) $(SRC)
GOBUILD = GOOS=$(BUILDOS) GOARCH=$(BUILDARCH) $(BUILDCMD) \
-o "$(OUT)/$(BINNAME)" "$(SRC)"
GORACE = GOOS=$(BUILDOS) GOARCH=$(BUILDARCH) $(BUILDCMD) \
-race -o "$(OUT)/$(BINNAME)-race" "$(SRC)"

SERVICE = $(APPNAME).service

Expand Down Expand Up @@ -123,6 +125,12 @@ lint:
lint-verbose:
@$(GOLINT) run -v ./...

build-race: prep
@$(GORACE)

race: build-race
@$(OUT)/$(BINNAME)-race -debug

cover: test-cover
@$(GO) tool cover -html=$(TESTCOVER) -o $(TESTCOVER).html
@printf "cover: %s\n" "$$(file $(TESTCOVER).html)"
Expand Down
6 changes: 6 additions & 0 deletions handlers/form_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,16 @@ func TestParseFormDuration(t *testing.T) {
def, def, maximum},
{"fraction", "/?duration=1.5h", "duration",
def, 90 * time.Minute, maximum},
{"no-unit", "/?duration=3333", "duration",
def, def, maximum},
{"bad-unit", "/duration=8h", "duration",
def, def, maximum},
{"large", "/?duration=9999h", "duration",
def, maximum, maximum},
{"xlarge", "/?duration=99999999999h", "duration",
def, def, maximum}, // overflows int64
{"encoded", "/?duration=%32%34%68", "duration",
def, 24 * time.Hour, maximum}, // "24h" encoded
}
for _, tc := range tests {
tc := tc
Expand Down
30 changes: 30 additions & 0 deletions handlers/json.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package handlers

import (
"encoding/json"
"net/http"
)

// deny serves a JSON response for disallowed requests.
func deny(w http.ResponseWriter, code int, reason string, r *Request) {
writeJSON(w, code, errorJSON(reason))
}

// errorJSON returns an error string map containing the string.
func errorJSON(s string) map[string]string {
return map[string]string{
"error": s,
}
}

// writeJSON serves a JSON response with data.
func writeJSON(w http.ResponseWriter, code int, data interface{}) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(code)
err := json.NewEncoder(w).Encode(data)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
_ = json.NewEncoder(w).Encode(errorJSON(err.Error()))
return
}
}
1 change: 0 additions & 1 deletion handlers/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ func List(app *config.App) http.HandlerFunc {
if req == nil {
return
}
app.UpdateTimeRemaining()
files := app.ListFiles()
app.Log.Info("serving file list",
"files", len(files), "user", req)
Expand Down
5 changes: 1 addition & 4 deletions handlers/message.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,6 @@ func Message(app *config.App) http.HandlerFunc {
if formContent != "" {
message.Count++
message.Data = formContent
app.Log.Debug("adding message",
"count", message.Count,
"content", message.Data, "user", req)
app.Messages[message.Count] = &message
app.Log.Info("added message", "user", req)
}
Expand All @@ -52,7 +49,7 @@ func Message(app *config.App) http.HandlerFunc {
}

if r.URL.Query().Get("download") == "all" {
app.Log.Debug("serving all messages",
app.Log.Debug("downloading messages",
"count", app.NumMessages, "user", req)
app.ServeMessages(w)
return
Expand Down
20 changes: 20 additions & 0 deletions handlers/param.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package handlers

import "net/http"

// getRequestParameter returns a parameter from the request
// URL or a form value.
func getRequestParameter(
r *http.Request, pathLen int, fieldName string) string {
if pathLen > len(r.URL.Path) {
return ""
}
p := r.URL.Path[pathLen:]
if p != "" {
return p
}
if queryValue := r.URL.Query().Get(fieldName); queryValue != "" {
return queryValue
}
return r.FormValue(fieldName)
}
70 changes: 70 additions & 0 deletions handlers/param_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package handlers

import (
"net/http"
"net/url"
"testing"
)

// TestGetRequestParameter validates the parameter value
// is read from the URL or form.
func TestGetRequestParameter(t *testing.T) {
tests := []struct {
name string
request *http.Request
pathLen int
field string
want string
}{
{
name: "URL parameter",
request: &http.Request{
URL: &url.URL{Path: "/download/id1"},
},
pathLen: 10,
field: "",
want: "id1",
},
{
name: "Query parameter",
request: &http.Request{
URL: &url.URL{
Path: "/download/",
RawQuery: "field=id2",
},
},
pathLen: 10,
field: "field",
want: "id2",
},
{
name: "Form parameter",
request: &http.Request{
Body: http.NoBody,
Form: url.Values{"field": {"id3"}},
URL: &url.URL{Path: "/download/"},
},
pathLen: 10,
field: "field",
want: "id3",
},
{
name: "No parameter",
request: &http.Request{
Body: http.NoBody,
URL: &url.URL{Path: "/download/"},
},
pathLen: 10,
field: "",
want: "",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result := getRequestParameter(test.request, test.pathLen, test.field)
if result != test.want {
t.Fatalf("%s: expect '%q'; got '%q'", test.name, test.want, result)
}
})
}
}
40 changes: 40 additions & 0 deletions handlers/theme.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package handlers

import (
"net/http"
"slices"
"time"

"github.com/drduh/gone/auth"
"github.com/drduh/gone/util"
)

const autoTheme = "auto"

// getDefaultTheme returns a default theme, based on
// the current time if set to automatically theme.
func getDefaultTheme(theme string) string {
if theme != autoTheme {
return theme
}
if util.IsDaytime() {
return "light"
}
return "dark"
}

// getTheme returns the CSS theme based on cookie preference,
// setting the cookie value if none exists, or is invalid.
func getTheme(w http.ResponseWriter, r *http.Request,
defaultTheme, id string, t time.Duration, themes []string) string {
formContent := r.FormValue(formFieldTheme)
if formContent != "" {
theme := formContent
if !slices.Contains(themes, theme) {
theme = getDefaultTheme(autoTheme)
}
http.SetCookie(w, auth.NewCookie(theme, id, t))
return theme
}
return auth.GetCookie(w, r, defaultTheme, id, t)
}
5 changes: 4 additions & 1 deletion handlers/upload.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ func Upload(app *config.App) http.HandlerFunc {

var upload storage.File
var uploads []storage.File
var mu sync.Mutex
var wg sync.WaitGroup

formFileContent := r.MultipartForm.File["file"]
Expand All @@ -59,6 +60,8 @@ func Upload(app *config.App) http.HandlerFunc {
for _, fileHeader := range formFileContent {
go func(fileHeader *multipart.FileHeader) {
defer wg.Done()
mu.Lock()
defer mu.Unlock()
file, err := fileHeader.Open()
if err != nil {
app.Log.Error(app.Copy, "error", err.Error(), "user", req)
Expand All @@ -78,7 +81,7 @@ func Upload(app *config.App) http.HandlerFunc {
}

filename := storage.SanitizeName(fileHeader.Filename,
app.MaxSizeName, app.FilenameExtraChars)
app.FilenameExtraChars, app.MaxSizeName)
f := &storage.File{
Name: filename,
Data: buf.Bytes(),
Expand Down
79 changes: 4 additions & 75 deletions handlers/util.go
Original file line number Diff line number Diff line change
@@ -1,26 +1,14 @@
package handlers

import (
"encoding/json"
"net"
"net/http"
"slices"
"time"

"github.com/drduh/gone/auth"
"github.com/drduh/gone/config"
"github.com/drduh/gone/util"
)

const autoTheme = "auto"

// deny serves a JSON response for disallowed requests.
func deny(w http.ResponseWriter, httpCode int, reason string,
app *config.App, r *Request) {
writeJSON(w, httpCode, errorJSON(reason))
app.Log.Error(reason, "user", r)
}

// toRoot redirects an HTTP request to the root/index handler.
func toRoot(w http.ResponseWriter, r *http.Request, rootPath string) {
http.Redirect(w, r, rootPath, http.StatusSeeOther)
Expand Down Expand Up @@ -54,25 +42,6 @@ func isAuthenticated(app *config.App, r *http.Request) bool {
return auth.Basic(app.Basic.Field, app.Basic.Token, r)
}

// errorJSON returns an error string map containing the string.
func errorJSON(s string) map[string]string {
return map[string]string{
"error": s,
}
}

// getDefaultTheme returns a default theme, based on
// the current time if set to automatically theme.
func getDefaultTheme(theme string) string {
if theme != autoTheme {
return theme
}
if util.IsDaytime() {
return "light"
}
return "dark"
}

// parseRequest returns a Request with masked address.
func parseRequest(r *http.Request) *Request {
address, _, err := net.SplitHostPort(r.RemoteAddr)
Expand All @@ -93,54 +62,14 @@ func authRequest(w http.ResponseWriter,
r *http.Request, app *config.App) *Request {
req := parseRequest(r)
if !isAuthenticated(app, r) {
deny(w, http.StatusForbidden, app.Deny, app, req)
deny(w, http.StatusForbidden, app.Deny, req)
app.Log.Error(app.Deny, "user", r)
return nil
}
if !app.Authorize(app.ReqsPerMinute) {
deny(w, http.StatusTooManyRequests, app.RateLimit, app, req)
deny(w, http.StatusTooManyRequests, app.RateLimit, req)
app.Log.Error(app.RateLimit, "user", r)
return nil
}
return req
}

// writeJSON serves a JSON response with data.
func writeJSON(w http.ResponseWriter, code int, data interface{}) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(code)
err := json.NewEncoder(w).Encode(data)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
_ = json.NewEncoder(w).Encode(errorJSON(err.Error()))
return
}
}

// getRequestParameter returns a request parameter from the
// URL or a form value.
func getRequestParameter(r *http.Request,
pathLen int, fieldName string) string {
p := r.URL.Path[pathLen:]
if p == "" {
p = r.URL.Query().Get(fieldName)
}
if p == "" {
p = r.FormValue(fieldName)
}
return p
}

// getTheme returns the CSS theme based on cookie preference,
// setting the cookie value if none exists, or is invalid.
func getTheme(w http.ResponseWriter, r *http.Request,
defaultTheme, id string, t time.Duration, themes []string) string {
formContent := r.FormValue(formFieldTheme)
if formContent != "" {
theme := formContent
if !slices.Contains(themes, theme) {
theme = getDefaultTheme(autoTheme)
}
http.SetCookie(w, auth.NewCookie(theme, id, t))
return theme
}
return auth.GetCookie(w, r, defaultTheme, id, t)
}
2 changes: 1 addition & 1 deletion handlers/wall.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ func Wall(app *config.App) http.HandlerFunc {
}

if r.URL.Query().Get("download") == "all" {
app.Log.Debug("serving wall content", "user", req)
app.ServeWall(w)
app.Log.Info("downloaded wall", "user", req)
return
}

Expand Down
2 changes: 1 addition & 1 deletion settings/defaultSettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@
},
"limit": {
"expiryTicker": "30s",
"filenameChars": "_.-",
"filenameChars": "_.- ",
"maxDownloads": 100,
"maxDuration": "192h",
"maxSizeMsg": 128,
Expand Down
Loading
Loading