From 622b98f40afa4c30f51d067ce5f7aabb040eeb8b Mon Sep 17 00:00:00 2001
From: kamkow1
Date: Sat, 21 Jun 2025 02:05:25 +0200
Subject: [PATCH] Simple file upload and browsing
---
.gitignore | 2 +
go.mod | 4 +
go.sum | 4 +
lts.go | 240 ++++++++++++++++++++++++++++++++++++++++++++--
tmpls/browse.html | 18 ++++
tmpls/home.html | 62 +++++++++++-
uuid.go | 173 +++++++++++++++++++++++++++++++++
7 files changed, 496 insertions(+), 7 deletions(-)
create mode 100644 go.sum
create mode 100644 tmpls/browse.html
create mode 100644 uuid.go
diff --git a/.gitignore b/.gitignore
index ab06424..9c916d6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -33,3 +33,5 @@ go.work.sum
watcher
lts
+
+ALLOWED_USERS.txt
diff --git a/go.mod b/go.mod
index 5b39b6e..aef7a16 100644
--- a/go.mod
+++ b/go.mod
@@ -2,3 +2,7 @@ module lts
go 1.24.3
+require (
+ github.com/fsnotify/fsnotify v1.9.0 // indirect
+ golang.org/x/sys v0.13.0 // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..c1e3272
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,4 @@
+github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
+github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
+golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
+golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
diff --git a/lts.go b/lts.go
index 8d6a65d..0aa2062 100644
--- a/lts.go
+++ b/lts.go
@@ -1,10 +1,20 @@
package main
import (
+ "bufio"
+ "crypto/subtle"
"embed"
+ "fmt"
"html/template"
+ "io/ioutil"
"log"
"net/http"
+ "os"
+ "path/filepath"
+ "strings"
+ "sync"
+
+ "github.com/fsnotify/fsnotify"
)
//go:embed tmpls/*
@@ -13,14 +23,232 @@ var tmpls embed.FS
//go:embed etc/*
var etc embed.FS
-func main() {
- http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
- tmpl := template.Must(template.ParseFS(tmpls, "tmpls/*.html"))
- if err := tmpl.Execute(w, nil); err != nil {
- log.Fatal(err)
+func createFile(storePath, name string) (*os.File, 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()
+
+ dst, err := os.Create(filepath.Join(storePath, fmt.Sprintf("%s-%s", uuid, name)))
+ if err != nil {
+ return nil, err
+ }
+ return dst, 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
+
+// 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])
+}
+
+func handleUpload(w http.ResponseWriter, r *http.Request) {
+ limit := int64(10737418240) // 10GiB
+ r.ParseMultipartForm(limit)
+
+ file, handler, err := r.FormFile("myfile")
+ if err != nil {
+ log.Println(err)
+ http.Error(w, "Could not get the file", http.StatusBadRequest)
+ return
+ }
+ defer file.Close()
+
+ fmt.Fprintf(w, "Uploaded file: %s\n", handler.Filename)
+ fmt.Fprintf(w, "File size: %s\n", byteCountSI(handler.Size))
+
+ dst, 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)
+ }
+}
+
+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(err)
+ }
+}
+
+type BrowseRecord struct {
+ FileName string
+ ReadableName string
+ Perm string
+ Modtime 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.Println(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(),
+ )
+
+ records = append(records, BrowseRecord{
+ FileName: filename,
+ ReadableName: readable_name,
+ Perm: info.Mode().Perm().String(),
+ Modtime: modtime,
+ })
+ }
+
+ if err := tmpl.Execute(w, records); err != nil {
+ log.Println(err)
+ }
+}
+
+func reloadAllowedUsers() {
+ log.Println("loading ALLOWED_USERS.txt")
+ allowedUsersTxt, err := ioutil.ReadFile("./ALLOWED_USERS.txt")
+ if err != nil {
+ log.Println(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 watchAllowedUsers() chan bool {
+ watcher, err := fsnotify.NewWatcher()
+ if err != nil {
+ log.Fatal(err)
+ }
+ defer watcher.Close()
+
+ done := make(chan bool)
+
+ go func() {
+ for {
+ select {
+ case event, ok := <-watcher.Events:
+ if !ok {
+ return
+ }
+ if event.Has(fsnotify.Chmod | fsnotify.Rename) {
+ reloadAllowedUsers()
+ }
+ case err, ok := <-watcher.Errors:
+ if !ok {
+ return
+ }
+ log.Println(err)
+ case <-done:
+ break
+ }
+ }
+ }()
+
+ err = watcher.Add("./ALLOWED_USERS.txt")
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ return done
+}
+
+func main() {
+ storePathValue, ok := os.LookupEnv("LTS_STORE_PATH")
+ if !ok {
+ log.Fatal("LTS_STORE_PATH not set")
+ }
+ storePath = storePathValue
+
+ reloadAllowedUsers()
+ doneWatching := watchAllowedUsers()
+
+ 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))))
http.ListenAndServe(":9090", nil)
+
+ doneWatching<-true
}
diff --git a/tmpls/browse.html b/tmpls/browse.html
new file mode 100644
index 0000000..b6f8d9e
--- /dev/null
+++ b/tmpls/browse.html
@@ -0,0 +1,18 @@
+
+
+ Lair Tremporary Storage
+
+
+
+
+
+
+
diff --git a/tmpls/home.html b/tmpls/home.html
index 729caf8..35432df 100644
--- a/tmpls/home.html
+++ b/tmpls/home.html
@@ -7,10 +7,70 @@
Lair Tremporary Storage
+ Welcome
Welcome to The Lair's temporary storage service. Use this service to transport files via
- a temporary remote storage. ONLY WHITELISTED IPs CAN UPLOAD!!
+ a temporary remote storage.
+ ONLY WHITELISTED USERS can upload! For access, reach out to me .
+ Only 30PLN / 12MON.
+
+
+
+ Browse
+
+ Browse the files here
+
+
+