From 70169cf8627338c9e26997cc80c64494e645414c Mon Sep 17 00:00:00 2001 From: nameless Date: Sun, 12 Jan 2025 22:52:47 -0500 Subject: [PATCH] Initial commit --- README.md | 23 +++++++ go.mod | 5 ++ go.sum | 2 + main.go | 178 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 208 insertions(+) create mode 100644 README.md create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go diff --git a/README.md b/README.md new file mode 100644 index 0000000..ed35917 --- /dev/null +++ b/README.md @@ -0,0 +1,23 @@ +# goshared + +A lightweight file-hosting server for pubnixes or similar shared-hosting +environments, written in Go. Uses PAM for authentication. + +## Usage +The server can be configured using the following command-line flags: + +``` +-baseurl Base URL for generated file links (default "http://localhost:8080") +-expire Number of hours before files are deleted (default 24) +-index Path to html file to serve as index (default "index.html") +-listen Address to listen on (default ":8080") +-maxsize Maximum allowed file size in bytes (default 10MB) +-storage Directory to store uploaded files (default "/tmp/share") +``` + +## Build +```bash +git clone https://git.tilde.horse/nameless/goshared +cd goshared +go build +``` diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..4f2be8a --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module goshared + +go 1.23.4 + +require github.com/msteinert/pam v1.2.0 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..ce879bb --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/msteinert/pam v1.2.0 h1:mYfjlvN2KYs2Pb9G6nb/1f/nPfAttT/Jee5Sq9r3bGE= +github.com/msteinert/pam v1.2.0/go.mod h1:d2n0DCUK8rGecChV3JzvmsDjOY4R7AYbsNxAT+ftQl0= diff --git a/main.go b/main.go new file mode 100644 index 0000000..7fe5410 --- /dev/null +++ b/main.go @@ -0,0 +1,178 @@ +package main + +import ( + "crypto/rand" + "encoding/hex" + "flag" + "fmt" + "html/template" + "io" + "log" + "mime" + "net/http" + "os" + "path" + "path/filepath" + "strings" + "time" + + "github.com/msteinert/pam" +) + +type Config struct { + Listen string + StorageDir string + BaseURL string + MaxFileSize int64 + ExpireHours int + IndexHTML string +} + +var config Config + +var indexHTML string + +func loadIndexHTML() { + content, err := os.ReadFile(config.IndexHTML) + if err != nil { + panic(err) + } + indexHTML = string(content) +} + +func init() { + flag.StringVar(&config.Listen, "listen", ":8080", "Address to listen on") + flag.StringVar(&config.StorageDir, "storage", "/tmp/share", "Directory to store uploaded files") + flag.StringVar(&config.BaseURL, "baseurl", "http://localhost:8080", "Base URL for generated file links") + flag.Int64Var(&config.MaxFileSize, "maxsize", 10*1024*1024, "Maximum allowed file size in bytes") + flag.IntVar(&config.ExpireHours, "expire", 24, "Number of hours before files are deleted") + flag.StringVar(&config.IndexHTML, "index", "index.html", "Path to html file to serve as index") +} + +func authenticateUser(username, password string) bool { + t, err := pam.StartFunc("system-auth", username, func(s pam.Style, msg string) (string, error) { + switch s { + case pam.PromptEchoOff: + return password, nil + } + return "", fmt.Errorf("unsupported PAM style") + }) + if err != nil { + return false + } + err = t.Authenticate(0) + return err == nil +} + +func generateRandomFilename() (string, error) { + bytes := make([]byte, 16) + if _, err := rand.Read(bytes); err != nil { + return "", err + } + return hex.EncodeToString(bytes), nil +} + +func cleanupOldFiles() { + ticker := time.NewTicker(1 * time.Hour) + for range ticker.C { + now := time.Now() + err := filepath.Walk(config.StorageDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if !info.IsDir() && now.Sub(info.ModTime()) > time.Duration(config.ExpireHours)*time.Hour { + os.Remove(path) + } + return nil + }) + if err != nil { + log.Printf("Cleanup error: %v", err) + } + } +} + +func handleUpload(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPut { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + username, password, ok := r.BasicAuth() + if !ok || !authenticateUser(username, password) { + w.Header().Set("WWW-Authenticate", `Basic realm="Upload"`) + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + + if r.ContentLength > config.MaxFileSize { + http.Error(w, "File too large", http.StatusRequestEntityTooLarge) + return + } + + ext := path.Ext(r.URL.Path) + if ext == "" { + contentType := r.Header.Get("Content-Type") + exts, _ := mime.ExtensionsByType(contentType) + if len(exts) > 0 { + ext = exts[0] + } + } + + randomName, err := generateRandomFilename() + if err != nil { + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + + filename := randomName + ext + filepath := path.Join(config.StorageDir, filename) + + f, err := os.Create(filepath) + if err != nil { + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + defer f.Close() + + _, err = io.Copy(f, r.Body) + if err != nil { + os.Remove(filepath) + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + + fileURL := strings.TrimRight(config.BaseURL, "/") + "/" + filename + fmt.Fprintf(w, "%s\n", fileURL) +} + +func handleIndex(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/" { + http.ServeFile(w, r, path.Join(config.StorageDir, r.URL.Path)) + return + } + + tmpl, err := template.New("index").Parse(indexHTML) + if err != nil { + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + + tmpl.Execute(w, config) +} + +func main() { + flag.Parse() + loadIndexHTML() + + if err := os.MkdirAll(config.StorageDir, 0755); err != nil { + log.Fatalf("Failed to create storage directory: %v", err) + } + + go cleanupOldFiles() + + http.HandleFunc("/", handleIndex) + http.HandleFunc("/upload", handleUpload) + + log.Printf("Starting server on %s", config.Listen) + log.Fatal(http.ListenAndServe(config.Listen, nil)) +}