package main import ( "bufio" "crypto/subtle" "embed" "fmt" "html/template" "io/ioutil" "log" "net" "net/http" "os" "os/exec" "path/filepath" "strconv" "strings" "sync" "syscall" "time" ) var LISTEN_ADDR string var listener net.Listener //go:embed tmpls/* var tmpls embed.FS //go:embed etc/* var etc embed.FS func createFile(storePath, name string) (*os.File, string, error) { if _, err := os.Stat(storePath); os.IsNotExist(err) { os.Mkdir(storePath, 0755) } uuidBytes, err := NewV4() if err != nil { return nil, "", err } uuid := uuidBytes.String() bare_name := fmt.Sprintf("%s-%s", uuid, name) dst, err := os.Create(filepath.Join(storePath, bare_name)) if err != nil { return nil, bare_name, err } return dst, bare_name, nil } type AllowedUser struct { Name string Password string } var allowedUsers []AllowedUser var allowedUsersMutex sync.Mutex func basicAuth(handler http.HandlerFunc, realm string) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { user, pass, ok := r.BasicAuth() if !ok { return } allowedUsersMutex.Lock() for _, allowedUser := range allowedUsers { if subtle.ConstantTimeCompare([]byte(user), []byte(allowedUser.Name)) == 1 { if subtle.ConstantTimeCompare([]byte(pass), []byte(allowedUser.Password)) == 1 { goto auth_ok } else { goto auth_fail } } } goto auth_fail auth_ok: allowedUsersMutex.Unlock() handler(w, r) return auth_fail: allowedUsersMutex.Unlock() w.Header().Set("WWW-Authenticate", `Basic realm="`+realm+`"`) w.WriteHeader(401) w.Write([]byte("Unauthorised.\n")) return } } var storePath string = "./store" // https://yourbasic.org/golang/formatting-byte-size-to-human-readable-format/ func byteCountSI(b int64) string { const unit = 1000 if b < unit { return fmt.Sprintf("%d B", b) } div, exp := int64(unit), 0 for n := b / unit; n >= unit; n /= unit { div *= unit exp++ } return fmt.Sprintf("%.1f %cB", float64(b)/float64(div), "kMGTPE"[exp]) } type Timeout struct { H int64 M int64 S int64 } type Cleaner struct { Timeout Timeout SpawnTime int64 } var cleaners map[string]Cleaner = map[string]Cleaner{} var cleanersMutex sync.Mutex func handleUpload(w http.ResponseWriter, r *http.Request) { limit := int64(10737418240) // 10GiB r.ParseMultipartForm(limit) hours_str := r.FormValue("hours") mins_str := r.FormValue("mins") secs_str := r.FormValue("secs") hours, err := strconv.ParseInt(hours_str, 10, 32) if err != nil { http.Error(w, "Could not parse timeout hours", http.StatusBadRequest) return } mins, err := strconv.ParseInt(mins_str, 10, 32) if err != nil { http.Error(w, "Could not parse timeout mins", http.StatusBadRequest) return } secs, err := strconv.ParseInt(secs_str, 10, 32) if err != nil { http.Error(w, "Could not parse timeout secs", http.StatusBadRequest) return } if (hours < 0 || hours > 24) || (mins < 0 || mins > 60) || (secs < 0 || secs > 60) { http.Error(w, "Max values: 24 hours, 60 mins, 60 secs, all cannot be < 0", http.StatusBadRequest) return } total := secs + mins*60 + hours*60*60 log.Printf("h: %d m: %d s: %d, total: %d\n", hours, mins, secs, total) file, handler, err := r.FormFile("myfile") if err != nil { log.Println("Error getting file from form: ", err) http.Error(w, "Could not get the file", http.StatusBadRequest) return } defer file.Close() dst, bare_name, err := createFile(storePath, handler.Filename) if err != nil { http.Error(w, "Could not save file", http.StatusInternalServerError) return } defer dst.Close() if _, err := dst.ReadFrom(file); err != nil { http.Error(w, "Error saving file", http.StatusInternalServerError) } fmt.Fprintf(w, "Uploaded file: %s\n", handler.Filename) fmt.Fprintf(w, "File size: %s\n", byteCountSI(handler.Size)) fmt.Fprintf(w, "Lifetime: %02d:%02d:%02d total %d\n", hours, mins, secs, total); fmt.Fprintf(w, "Link: %s", fmt.Sprintf("%s/store/%s", listener.Addr().String(), bare_name)) cmd := exec.Command("./ltscleanerd", dst.Name(), strconv.FormatInt(total, 10)) cmd.SysProcAttr = &syscall.SysProcAttr{ Setsid: true, } log.Println("Started ", cmd.String()) err = cmd.Start() if err != nil { log.Println("Could not start cleaner daemon: ", err) } cleanersMutex.Lock() cleaners[bare_name] = Cleaner{ Timeout: Timeout{ H: hours, M: mins, S: secs }, SpawnTime: time.Now().Unix(), } cleanersMutex.Unlock() } func handleHome(w http.ResponseWriter, r *http.Request) { home_tmpl, _ := tmpls.ReadFile("tmpls/home.html") tmpl := template.Must(template.New("home").Parse(string(home_tmpl))) if err := tmpl.Execute(w, nil); err != nil { log.Println("Error executing template: ", err) } } type BrowseRecord struct { FileName string ReadableName string Perm string Modtime string TimeLeft string } func handleBrowse(w http.ResponseWriter, r *http.Request) { browse_tmpl, _ := tmpls.ReadFile("tmpls/browse.html") tmpl := template.Must(template.New("browse").Parse(string(browse_tmpl))) storeEntries, err := os.ReadDir(storePath) if err != nil { log.Printf("Error reading store path %s %v\n", storePath, err) return } var records []BrowseRecord for _, e := range storeEntries { filename := e.Name() readable_name := filename[37:] // 32 hex + 4 `-` + 1 `-` info, _ := e.Info() modtime := fmt.Sprintf("%d/%s/%d %02d:%02d:%02d", info.ModTime().Day(), info.ModTime().Month().String(), info.ModTime().Year(), info.ModTime().Hour(), info.ModTime().Minute(), info.ModTime().Second(), ) cleanersMutex.Lock() cleaner, _ := cleaners[e.Name()] total := cleaner.Timeout.S + cleaner.Timeout.M*60 + cleaner.Timeout.H*60*60 now := time.Now().Unix() timeleft_unix := time.Unix(cleaner.SpawnTime + total - now, 0) cleanersMutex.Unlock() timeleft := fmt.Sprintf("time left %02d:%02d:%02d", timeleft_unix.Hour() - 1, timeleft_unix.Minute(), timeleft_unix.Second(), ) records = append(records, BrowseRecord{ FileName: filename, ReadableName: readable_name, Perm: info.Mode().Perm().String(), Modtime: modtime, TimeLeft: timeleft, }) } if err := tmpl.Execute(w, records); err != nil { log.Println("Error executing template: ", err) } } func loadAllowedUsers() { log.Println("loading ALLOWED_USERS.txt") allowedUsersTxt, err := ioutil.ReadFile("./ALLOWED_USERS.txt") if err != nil { log.Println("Error reading ALLOWED_USERS.txt: ", err) return } scanner := bufio.NewScanner(strings.NewReader(string(allowedUsersTxt))) allowedUsersMutex.Lock() for scanner.Scan() { line := scanner.Text() parts := strings.Fields(line) allowedUsers = append(allowedUsers, AllowedUser{ Name: parts[0], Password: parts[1] }) } allowedUsersMutex.Unlock() } func main() { loadAllowedUsers() http.HandleFunc("/", handleHome) http.HandleFunc("/browse", handleBrowse) http.HandleFunc("/upload", basicAuth(handleUpload, "Enter username and password")) http.Handle("/etc/", http.FileServerFS(etc)) http.Handle("/store/", http.StripPrefix("/store/", http.FileServer(http.Dir(storePath)))) listener_tcp, err := net.Listen("tcp", LISTEN_ADDR) if err != nil { log.Fatal("Error starting server: ", err) } listener = listener_tcp log.Printf("Listening on %s\n", listener.Addr().String()) err = http.Serve(listener, nil) if err != nil { log.Fatal("Error in serving: ", err) } }