diff --git a/Dockerfile b/Dockerfile index 84ea3f1..b4fcd0c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,6 @@ FROM golang:1.26-alpine AS builder WORKDIR /build COPY go.mod ./ -RUN go mod download COPY index.html.tmpl main.go ./ @@ -11,7 +10,10 @@ RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o beam . FROM alpine:latest -RUN adduser -D -u 1000 beam +RUN apk add mailcap \ # for mime types + && adduser -D -u 1000 beam \ + && mkdir /uploads \ + && chown beam /uploads USER beam WORKDIR /app diff --git a/main.go b/main.go index d195c46..985ee70 100644 --- a/main.go +++ b/main.go @@ -4,21 +4,24 @@ import ( "crypto/rand" "encoding/json" "fmt" + "html/template" "io" "log" + "mime" "net/http" "os" "path/filepath" "strings" "time" - "html/template" ) var uploadDir string const idLength = 8 const idCharset = "abcdefghijklmnopqrstuvwxyz0123456789" + var indexHTML *template.Template + func main() { uploadDir = os.Getenv("UPLOAD_DIR") if uploadDir == "" { @@ -60,7 +63,7 @@ func handleRequest(w http.ResponseWriter, r *http.Request) { } if r.Method == "PUT" { - handleCurlUpload(w, r, start) + handleUpload(w, r, start) return } @@ -69,11 +72,15 @@ func handleRequest(w http.ResponseWriter, r *http.Request) { return } + if r.Method == "HEAD" { + return + } + w.WriteHeader(http.StatusMethodNotAllowed) logRequest(r, 405, time.Since(start)) } -func handleCurlUpload(w http.ResponseWriter, r *http.Request, start time.Time) { +func handleUpload(w http.ResponseWriter, r *http.Request, start time.Time) { filename := strings.TrimPrefix(r.URL.Path, "/") if filename == "" { http.Error(w, "Filename required", http.StatusBadRequest) @@ -192,6 +199,12 @@ func handleDownload(w http.ResponseWriter, r *http.Request, start time.Time) { logRequest(r, 404, time.Since(start)) return } + if !strings.HasPrefix(filePath, uploadDir) { + http.NotFound(w, r) + logRequest(r, 404, time.Since(start)) + return + } + defer file.Close() stat, err := file.Stat() @@ -212,7 +225,7 @@ func handleDownload(w http.ResponseWriter, r *http.Request, start time.Time) { func generateUniqueID() (string, error) { const maxRetries = 100 - for i := 0; i < maxRetries; i++ { + for range maxRetries { id := generateID() targetDir := filepath.Join(uploadDir, id) @@ -249,11 +262,11 @@ func getHost(r *http.Request) string { // Check standard Forwarded header (RFC 7239) if forwarded := r.Header.Get("Forwarded"); forwarded != "" { // Parse "Forwarded: for=...; host=example.com; proto=https" - parts := strings.Split(forwarded, ";") - for _, part := range parts { + parts := strings.SplitSeq(forwarded, ";") + for part := range parts { part = strings.TrimSpace(part) - if strings.HasPrefix(part, "host=") { - return strings.TrimPrefix(part, "host=") + if host, found := strings.CutPrefix(part, "host="); found { + return host } } } @@ -267,29 +280,23 @@ func getHost(r *http.Request) string { } func getProtocol(r *http.Request) string { - // Check X-Forwarded-Proto (common proxy header) if proto := r.Header.Get("X-Forwarded-Proto"); proto != "" { return proto } - // Check standard Forwarded header (RFC 7239) if forwarded := r.Header.Get("Forwarded"); forwarded != "" { - // Parse "Forwarded: for=...; proto=https; host=..." - parts := strings.Split(forwarded, ";") - for _, part := range parts { + parts := strings.SplitSeq(forwarded, ";") + for part := range parts { part = strings.TrimSpace(part) - if strings.HasPrefix(part, "proto=") { - return strings.TrimPrefix(part, "proto=") + if proto, found := strings.CutPrefix(part, "proto="); found { + return proto } } } - // Check X-Forwarded-SSL (some proxies use this) if ssl := r.Header.Get("X-Forwarded-SSL"); ssl == "on" { return "https" } - - // Check if connection is TLS if r.TLS != nil { return "https" } @@ -298,28 +305,10 @@ func getProtocol(r *http.Request) string { } func detectContentType(filename string) string { - ext := strings.ToLower(filepath.Ext(filename)) - types := map[string]string{ - ".txt": "text/plain", - ".html": "text/html", - ".css": "text/css", - ".js": "application/javascript", - ".json": "application/json", - ".xml": "application/xml", - ".pdf": "application/pdf", - ".zip": "application/zip", - ".jpg": "image/jpeg", - ".jpeg": "image/jpeg", - ".png": "image/png", - ".gif": "image/gif", - ".svg": "image/svg+xml", - ".mp3": "audio/mpeg", - ".mp4": "video/mp4", - ".webm": "video/webm", - } - - if ct, ok := types[ext]; ok { - return ct + ext := filepath.Ext(filename) + mimeType := mime.TypeByExtension(ext) + if mimeType == "" { + return mimeType } return "application/octet-stream" } @@ -331,7 +320,7 @@ func logRequest(r *http.Request, status int, duration time.Duration) { func serveFrontpage(w http.ResponseWriter, r *http.Request) { proto := getProtocol(r) host := getHost(r) - pageData := map[string]string {"Proto": proto, "Host":host} + pageData := map[string]string{"Proto": proto, "Host": host} w.Header().Set("Content-Type", "text/html; charset=utf-8") indexHTML.Execute(w, pageData)