Add configurable MAX_UPLOAD_SIZE limit with clear error responses
This commit is contained in:
@@ -15,6 +15,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
@@ -25,7 +26,11 @@ var uploadDir string
|
|||||||
const idLength = 8
|
const idLength = 8
|
||||||
const idCharset = "abcdefghijklmnopqrstuvwxyz0123456789"
|
const idCharset = "abcdefghijklmnopqrstuvwxyz0123456789"
|
||||||
|
|
||||||
|
// defaultMaxUploadSize is used when MAX_UPLOAD_SIZE is not set.
|
||||||
|
const defaultMaxUploadSize int64 = 32 << 20 // 32MB
|
||||||
|
|
||||||
var indexHTML *template.Template
|
var indexHTML *template.Template
|
||||||
|
var maxUploadSize int64 = defaultMaxUploadSize
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
uploadDir = os.Getenv("UPLOAD_DIR")
|
uploadDir = os.Getenv("UPLOAD_DIR")
|
||||||
@@ -44,6 +49,14 @@ func main() {
|
|||||||
fileExpires = expires
|
fileExpires = expires
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if maxUploadSizeEnv := os.Getenv("MAX_UPLOAD_SIZE"); maxUploadSizeEnv != "" {
|
||||||
|
size, err := strconv.ParseInt(maxUploadSizeEnv, 10, 64)
|
||||||
|
if err != nil || size <= 0 {
|
||||||
|
log.Fatalf("MAX_UPLOAD_SIZE %s is invalid: %v", maxUploadSizeEnv, err)
|
||||||
|
}
|
||||||
|
maxUploadSize = size
|
||||||
|
}
|
||||||
|
|
||||||
port := os.Getenv("PORT")
|
port := os.Getenv("PORT")
|
||||||
if port == "" {
|
if port == "" {
|
||||||
port = "8080"
|
port = "8080"
|
||||||
@@ -57,6 +70,7 @@ func main() {
|
|||||||
|
|
||||||
log.Printf("Starting server on port %s, upload directory: %s", port, uploadDir)
|
log.Printf("Starting server on port %s, upload directory: %s", port, uploadDir)
|
||||||
log.Printf("Files expires after: %v (DRYRUN_CLEAN=%v)", fileExpires, dryRun)
|
log.Printf("Files expires after: %v (DRYRUN_CLEAN=%v)", fileExpires, dryRun)
|
||||||
|
log.Printf("Max upload size: %d bytes", maxUploadSize)
|
||||||
if fileExpires != 0 {
|
if fileExpires != 0 {
|
||||||
go cleanupProcedure(fileExpires, dryRun)
|
go cleanupProcedure(fileExpires, dryRun)
|
||||||
} else {
|
} else {
|
||||||
@@ -169,8 +183,16 @@ func handleUpload(w http.ResponseWriter, r *http.Request, start time.Time) {
|
|||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
|
||||||
written, err := io.Copy(file, r.Body)
|
limitedBody := http.MaxBytesReader(w, r.Body, maxUploadSize)
|
||||||
|
written, err := io.Copy(file, limitedBody)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
file.Close()
|
||||||
|
os.Remove(targetPath)
|
||||||
|
if isMaxBytesError(err) {
|
||||||
|
http.Error(w, fmt.Sprintf("File exceeds maximum upload size of %d bytes", maxUploadSize), http.StatusRequestEntityTooLarge)
|
||||||
|
logRequest(r, 413, time.Since(start))
|
||||||
|
return
|
||||||
|
}
|
||||||
http.Error(w, "Failed to write file", http.StatusInternalServerError)
|
http.Error(w, "Failed to write file", http.StatusInternalServerError)
|
||||||
logRequest(r, 500, time.Since(start))
|
logRequest(r, 500, time.Since(start))
|
||||||
return
|
return
|
||||||
@@ -187,16 +209,34 @@ func handleUpload(w http.ResponseWriter, r *http.Request, start time.Time) {
|
|||||||
logRequest(r, 200, time.Since(start))
|
logRequest(r, 200, time.Since(start))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func writeMultipartError(w http.ResponseWriter, status int, message string) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(status)
|
||||||
|
json.NewEncoder(w).Encode(map[string]string{"error": message})
|
||||||
|
}
|
||||||
|
|
||||||
func handleMultipartUpload(w http.ResponseWriter, r *http.Request, start time.Time) {
|
func handleMultipartUpload(w http.ResponseWriter, r *http.Request, start time.Time) {
|
||||||
if err := r.ParseMultipartForm(32 << 20); err != nil {
|
r.Body = http.MaxBytesReader(w, r.Body, maxUploadSize)
|
||||||
http.Error(w, "Failed to parse multipart form", http.StatusBadRequest)
|
|
||||||
|
memoryLimit := maxUploadSize
|
||||||
|
if memoryLimit > defaultMaxUploadSize {
|
||||||
|
memoryLimit = defaultMaxUploadSize
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.ParseMultipartForm(memoryLimit); err != nil {
|
||||||
|
if isMaxBytesError(err) {
|
||||||
|
writeMultipartError(w, http.StatusRequestEntityTooLarge, fmt.Sprintf("Upload exceeds maximum size of %d bytes", maxUploadSize))
|
||||||
|
logRequest(r, 413, time.Since(start))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeMultipartError(w, http.StatusBadRequest, "Failed to parse multipart form")
|
||||||
logRequest(r, 400, time.Since(start))
|
logRequest(r, 400, time.Since(start))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
files := r.MultipartForm.File["files"]
|
files := r.MultipartForm.File["files"]
|
||||||
if len(files) == 0 {
|
if len(files) == 0 {
|
||||||
http.Error(w, "No files provided", http.StatusBadRequest)
|
writeMultipartError(w, http.StatusBadRequest, "No files provided")
|
||||||
logRequest(r, 400, time.Since(start))
|
logRequest(r, 400, time.Since(start))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -231,6 +271,7 @@ func handleMultipartUpload(w http.ResponseWriter, r *http.Request, start time.Ti
|
|||||||
src.Close()
|
src.Close()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
os.Remove(targetPath)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -366,6 +407,13 @@ func getProtocol(r *http.Request) string {
|
|||||||
return "http"
|
return "http"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isMaxBytesError reports whether err was caused by exceeding the limit
|
||||||
|
// set on an http.MaxBytesReader.
|
||||||
|
func isMaxBytesError(err error) bool {
|
||||||
|
var maxBytesErr *http.MaxBytesError
|
||||||
|
return errors.As(err, &maxBytesErr)
|
||||||
|
}
|
||||||
|
|
||||||
func detectContentType(filename string) string {
|
func detectContentType(filename string) string {
|
||||||
ext := filepath.Ext(filename)
|
ext := filepath.Ext(filename)
|
||||||
mimeType := mime.TypeByExtension(ext)
|
mimeType := mime.TypeByExtension(ext)
|
||||||
|
|||||||
Reference in New Issue
Block a user