298 lines
7.3 KiB
Go
298 lines
7.3 KiB
Go
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)
|
|
}
|
|
}
|