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 } type ImagePageData struct { Title string ImageURL string BaseURL string Description string FullImageURL string } var config Config var indexHTML string var imagePageTemplate string func loadIndexHTML() { content, err := os.ReadFile(config.IndexHTML) if err != nil { panic(err) } indexHTML = string(content) // HTML template for rendering images imagePageTemplate = ` {{.Title}}

{{.Title}}

{{.Title}}
Download Image
` } 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 isImageFile(filePath string) bool { file, err := os.Open(filePath) if err != nil { log.Printf("Error opening file for MIME detection: %v", err) return false } defer file.Close() buffer := make([]byte, 512) _, err = file.Read(buffer) if err != nil && err != io.EOF { log.Printf("Error reading file for MIME detection: %v", err) return false } _, err = file.Seek(0, 0) if err != nil { log.Printf("Error seeking file after MIME detection: %v", err) return false } contentType := http.DetectContentType(buffer) log.Printf("Detected content type for %s: %s", filePath, contentType) return strings.HasPrefix(contentType, "image/") } 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 == "/" { tmpl, err := template.New("index").Parse(indexHTML) if err != nil { http.Error(w, "Internal server error", http.StatusInternalServerError) return } tmpl.Execute(w, config) return } requestPath := strings.TrimPrefix(r.URL.Path, "/") filePath := path.Join(config.StorageDir, requestPath) log.Printf("Requested path: %s, File path: %s", r.URL.Path, filePath) fileInfo, err := os.Stat(filePath) if err != nil { log.Printf("File not found: %s, Error: %v", filePath, err) http.NotFound(w, r) return } if r.URL.Query().Get("raw") == "true" || r.URL.Query().Get("download") == "true" { if r.URL.Query().Get("download") == "true" { w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filepath.Base(requestPath))) } log.Printf("Serving file directly: %s", filePath) http.ServeFile(w, r, filePath) return } if !isImageFile(filePath) { log.Printf("File is not an image, serving directly: %s", filePath) http.ServeFile(w, r, filePath) return } tmpl, err := template.New("imagePage").Parse(imagePageTemplate) if err != nil { log.Printf("Template parsing error: %v", err) http.Error(w, "Internal server error", http.StatusInternalServerError) return } baseURL := strings.TrimRight(config.BaseURL, "/") data := ImagePageData{ Title: "Filename: " + filepath.Base(requestPath), ImageURL: r.URL.Path, // Use the original request path for the image URL BaseURL: baseURL, Description: fmt.Sprintf("Image shared on %s, size: %d bytes", fileInfo.ModTime().Format("Jan 2, 2006"), fileInfo.Size()), FullImageURL: baseURL + r.URL.Path + "?raw=true", // Full URL for the image with raw parameter } log.Printf("Serving image template with data: %+v", data) w.Header().Set("Content-Type", "text/html; charset=utf-8") tmpl.Execute(w, data) } func main() { flag.Parse() loadIndexHTML() if err := os.MkdirAll(config.StorageDir, 0700); 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.Printf("Storage directory: %s", config.StorageDir) log.Printf("Base URL: %s", config.BaseURL) log.Fatal(http.ListenAndServe(config.Listen, nil)) }