From 87d416e829d692e757dec6c61f154cfa4fea1b08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nils=20O=2E=20Sel=C3=A5sdal?= Date: Thu, 28 May 2026 19:55:12 +0200 Subject: [PATCH] initial version --- .dockerignore | 6 + .gitignore | 6 + Dockerfile | 23 +++ go.mod | 3 + main.go | 530 ++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 568 insertions(+) create mode 100644 .dockerignore create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 go.mod create mode 100644 main.go diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..4a0b2a7 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +uploads/ +*.md +.git/ +.gitignore +Dockerfile +.dockerignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4e23bf4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +uploads/ +beam +*.exe +*.dll +*.so +*.dylib diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..0926c10 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,23 @@ +FROM golang:1.26-alpine AS builder + +WORKDIR /build + +COPY go.mod ./ +RUN go mod download + +COPY main.go ./ + +RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o beam . + +FROM alpine:latest + +RUN adduser -D -u 1000 beam + +USER beam +WORKDIR /app + +COPY --from=builder /build/beam . + +EXPOSE 8080 + +CMD ["./beam"] diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..bf28d04 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module beam + +go 1.26 diff --git a/main.go b/main.go new file mode 100644 index 0000000..d07f11e --- /dev/null +++ b/main.go @@ -0,0 +1,530 @@ +package main + +import ( + "crypto/rand" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "os" + "path/filepath" + "strings" + "time" +) + +var uploadDir string + +const idLength = 8 +const idCharset = "abcdefghijklmnopqrstuvwxyz0123456789" + +func main() { + uploadDir = os.Getenv("UPLOAD_DIR") + if uploadDir == "" { + uploadDir = "/uploads" + } + + port := os.Getenv("PORT") + if port == "" { + port = "8080" + } + + if err := os.MkdirAll(uploadDir, 0755); err != nil { + log.Fatalf("Failed to create upload directory: %v", err) + } + + http.HandleFunc("/", handleRequest) + + log.Printf("Starting server on port %s, upload directory: %s", port, uploadDir) + if err := http.ListenAndServe(":"+port, nil); err != nil { + log.Fatalf("Server failed: %v", err) + } +} + +func handleRequest(w http.ResponseWriter, r *http.Request) { + start := time.Now() + + if r.Method == "GET" && r.URL.Path == "/" { + serveFrontpage(w, r) + logRequest(r, 200, time.Since(start)) + return + } + + if r.Method == "POST" && r.URL.Path == "/upload" { + handleMultipartUpload(w, r, start) + return + } + + if r.Method == "PUT" { + handleCurlUpload(w, r, start) + return + } + + if r.Method == "GET" { + handleDownload(w, r, start) + return + } + + w.WriteHeader(http.StatusMethodNotAllowed) + logRequest(r, 405, time.Since(start)) +} + +func handleCurlUpload(w http.ResponseWriter, r *http.Request, start time.Time) { + filename := strings.TrimPrefix(r.URL.Path, "/") + if filename == "" { + http.Error(w, "Filename required", http.StatusBadRequest) + logRequest(r, 400, time.Since(start)) + return + } + + filename = filepath.Base(filename) + + id, err := generateUniqueID() + if err != nil { + http.Error(w, "Failed to generate unique ID", http.StatusInternalServerError) + logRequest(r, 500, time.Since(start)) + return + } + + targetPath := filepath.Join(uploadDir, id, filename) + file, err := os.Create(targetPath) + if err != nil { + http.Error(w, "Failed to create file", http.StatusInternalServerError) + logRequest(r, 500, time.Since(start)) + return + } + defer file.Close() + + written, err := io.Copy(file, r.Body) + if err != nil { + http.Error(w, "Failed to write file", http.StatusInternalServerError) + logRequest(r, 500, time.Since(start)) + return + } + + proto := getProtocol(r) + host := getHost(r) + downloadURL := fmt.Sprintf("%s://%s/%s/%s\n", proto, host, id, filename) + + w.WriteHeader(http.StatusOK) + w.Write([]byte(downloadURL)) + + log.Printf("Uploaded %s (%d bytes) -> %s", filename, written, downloadURL) + logRequest(r, 200, time.Since(start)) +} + +func handleMultipartUpload(w http.ResponseWriter, r *http.Request, start time.Time) { + if err := r.ParseMultipartForm(32 << 20); err != nil { + http.Error(w, "Failed to parse multipart form", http.StatusBadRequest) + logRequest(r, 400, time.Since(start)) + return + } + + files := r.MultipartForm.File["files"] + if len(files) == 0 { + http.Error(w, "No files provided", http.StatusBadRequest) + logRequest(r, 400, time.Since(start)) + return + } + + proto := getProtocol(r) + host := getHost(r) + urls := make([]string, 0, len(files)) + + for _, fileHeader := range files { + src, err := fileHeader.Open() + if err != nil { + continue + } + + id, err := generateUniqueID() + if err != nil { + src.Close() + continue + } + + filename := filepath.Base(fileHeader.Filename) + targetPath := filepath.Join(uploadDir, id, filename) + + dst, err := os.Create(targetPath) + if err != nil { + src.Close() + continue + } + + written, err := io.Copy(dst, src) + dst.Close() + src.Close() + + if err != nil { + continue + } + + downloadURL := fmt.Sprintf("%s://%s/%s/%s", proto, host, id, filename) + urls = append(urls, downloadURL) + log.Printf("Uploaded %s (%d bytes) -> %s", filename, written, downloadURL) + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string][]string{"urls": urls}) + logRequest(r, 200, time.Since(start)) +} + +func handleDownload(w http.ResponseWriter, r *http.Request, start time.Time) { + parts := strings.Split(strings.TrimPrefix(r.URL.Path, "/"), "/") + if len(parts) != 2 { + http.NotFound(w, r) + logRequest(r, 404, time.Since(start)) + return + } + + id := parts[0] + filename := parts[1] + + filePath := filepath.Join(uploadDir, id, filename) + file, err := os.Open(filePath) + if err != nil { + http.NotFound(w, r) + logRequest(r, 404, time.Since(start)) + return + } + defer file.Close() + + stat, err := file.Stat() + if err != nil { + http.Error(w, "Failed to stat file", http.StatusInternalServerError) + logRequest(r, 500, time.Since(start)) + return + } + + w.Header().Set("Content-Type", detectContentType(filename)) + w.Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=\"%s\"", filename)) + w.Header().Set("Content-Length", fmt.Sprintf("%d", stat.Size())) + + io.Copy(w, file) + logRequest(r, 200, time.Since(start)) +} + +func generateUniqueID() (string, error) { + const maxRetries = 100 + + for i := 0; i < maxRetries; i++ { + id := generateID() + targetDir := filepath.Join(uploadDir, id) + + if _, err := os.Stat(targetDir); os.IsNotExist(err) { + if err := os.MkdirAll(targetDir, 0755); err != nil { + return "", err + } + return id, nil + } + } + + return "", fmt.Errorf("failed to generate unique ID after %d retries", maxRetries) +} + +func generateID() string { + bytes := make([]byte, idLength) + if _, err := rand.Read(bytes); err != nil { + panic(err) + } + + for i, b := range bytes { + bytes[i] = idCharset[int(b)%len(idCharset)] + } + + return string(bytes) +} + +func getHost(r *http.Request) string { + // Check X-Forwarded-Host (common proxy header) + if host := r.Header.Get("X-Forwarded-Host"); host != "" { + return host + } + + // 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 { + part = strings.TrimSpace(part) + if strings.HasPrefix(part, "host=") { + return strings.TrimPrefix(part, "host=") + } + } + } + + // Fallback to Host header + if host := r.Header.Get("Host"); host != "" { + return host + } + + return "localhost:8080" +} + +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 { + part = strings.TrimSpace(part) + if strings.HasPrefix(part, "proto=") { + return strings.TrimPrefix(part, "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" + } + + return "http" +} + +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 + } + return "application/octet-stream" +} + +func logRequest(r *http.Request, status int, duration time.Duration) { + log.Printf("[%s] %s %s %d %v", time.Now().Format("2006-01-02 15:04:05"), r.Method, r.URL.Path, status, duration) +} + +func serveFrontpage(w http.ResponseWriter, r *http.Request) { + proto := getProtocol(r) + host := getHost(r) + html := fmt.Sprintf(` + + + + + Beam - File Upload Service + + + +

🚀 Beam - File Upload Service

+ +
+

Upload with curl (Recommended)

+ curl --upload-file myfile.txt %s://%s/myfile.txt +

The server will respond with a download URL.

+
+ +
+

Download a file

+ curl %s://%s/xxxxxxxx/myfile.txt -o myfile.txt +

Or simply open the URL in your browser.

+
+ +
+

Web Upload

+
+

📁 Click to select files or drag and drop here

+ + +
+
+
+
+
+
+ + + +`, proto, host, proto, host) + + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.Write([]byte(html)) +}