Compare commits

..

16 Commits

Author SHA1 Message Date
571ffca339 Remove ALLOWED_USERS.txt watcher 2025-06-22 20:43:51 +02:00
9b0d448698 Mutex around cleaners map 2025-06-22 20:42:18 +02:00
35958f2ade Install ltscleanerd to lts work dir 2025-06-22 20:26:28 +02:00
71fed69dd7 Change gebs path 2025-06-22 19:55:45 +02:00
339d8ed9a7 Fix path for gebs 2025-06-22 19:54:39 +02:00
0f317edb28 Change RELEASE port 2025-06-22 19:52:00 +02:00
d4906360e1 chmod +x 2025-06-22 19:48:20 +02:00
df6d009ce7 update-release script 2025-06-22 19:47:35 +02:00
cd29f363fa systemd service 2025-06-22 19:44:27 +02:00
110a7387b5 Display remaining time until removal 2025-06-22 19:41:38 +02:00
5578822c3f Start cleaner daemon after upload 2025-06-21 18:22:26 +02:00
099f4777cd Storage cleaner daemon 2025-06-21 17:50:59 +02:00
8d470cbe68 Generate upload link 2025-06-21 16:12:16 +02:00
8ce2789588 We can now get the serve address 2025-06-21 15:14:21 +02:00
61e218e58f Improved makefile 2025-06-21 15:03:19 +02:00
59bb805ecd Configurable listen port 2025-06-21 14:55:24 +02:00
12 changed files with 254 additions and 77 deletions

1
.gitignore vendored
View File

@ -35,3 +35,4 @@ watcher
lts
store
ALLOWED_USERS.txt
ltscleanerd

2
.gitmodules vendored
View File

@ -1,3 +1,3 @@
[submodule "gebs"]
path = gebs
url = http://git.kamkow1lair/kamkow1/gebs.git
url = http://git.kamkow1lair.pl/kamkow1/gebs.git

View File

@ -1 +1,2 @@
./lts
./ltscleanerd

View File

@ -1,12 +1,34 @@
all:
go build
BUILD_MODE ?= DEBUG
watcher:
cc -o watcher watcher.c
ifeq ($(BUILD_MODE),DEBUG)
LISTEN_ADDR = "localhost:9090"
else ifeq ($(BUILD_MODE),RELEASE)
LISTEN_ADDR = "0.0.0.0:4000"
else
$(error Unknown build mode)
endif
all: lts watcher
lts: ltscleanerd lts.go uuid.go
go build -ldflags="-X main.LISTEN_ADDR=$(LISTEN_ADDR)"
ltscleanerd: ltscleanerd.c
cc -o ltscleanerd ltscleanerd.c
watcher: watcher.c
cc -o $@ $<
clean:
go clean
rm watcher
rm -f watcher
rm -f ltscleanerd
.PHONY: all clean watcher
watch: all
./watcher . sh -c "make BUILD_MODE=$(BUILD_MODE) && ./lts"
run: lts
./lts
.PHONY: all clean watch run

5
go.mod
View File

@ -1,8 +1,3 @@
module lts
go 1.24.3
require (
github.com/fsnotify/fsnotify v1.9.0 // indirect
golang.org/x/sys v0.13.0 // indirect
)

4
go.sum
View File

@ -1,4 +0,0 @@
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=

169
lts.go
View File

@ -8,37 +8,45 @@ import (
"html/template"
"io/ioutil"
"log"
"net"
"net/http"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"sync"
"github.com/fsnotify/fsnotify"
"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, error) {
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
return nil, "", err
}
uuid := uuidBytes.String()
dst, err := os.Create(filepath.Join(storePath, fmt.Sprintf("%s-%s", uuid, name)))
bare_name := fmt.Sprintf("%s-%s", uuid, name)
dst, err := os.Create(filepath.Join(storePath, bare_name))
if err != nil {
return nil, err
return nil, bare_name, err
}
return dst, nil
return dst, bare_name, nil
}
type AllowedUser struct {
@ -100,22 +108,61 @@ func byteCountSI(b int64) string {
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(err)
log.Println("Error getting file from form: ", 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)
dst, bare_name, err := createFile(storePath, handler.Filename)
if err != nil {
http.Error(w, "Could not save file", http.StatusInternalServerError)
return
@ -125,13 +172,34 @@ func handleUpload(w http.ResponseWriter, r *http.Request) {
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(err)
log.Println("Error executing template: ", err)
}
}
@ -140,6 +208,7 @@ type BrowseRecord struct {
ReadableName string
Perm string
Modtime string
TimeLeft string
}
func handleBrowse(w http.ResponseWriter, r *http.Request) {
@ -148,7 +217,7 @@ func handleBrowse(w http.ResponseWriter, r *http.Request) {
storeEntries, err := os.ReadDir(storePath)
if err != nil {
log.Println(err)
log.Printf("Error reading store path %s %v\n", storePath, err)
return
}
@ -163,25 +232,35 @@ func handleBrowse(w http.ResponseWriter, r *http.Request) {
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(err)
log.Println("Error executing template: ", err)
}
}
func reloadAllowedUsers() {
func loadAllowedUsers() {
log.Println("loading ALLOWED_USERS.txt")
allowedUsersTxt, err := ioutil.ReadFile("./ALLOWED_USERS.txt")
if err != nil {
log.Println(err)
log.Println("Error reading ALLOWED_USERS.txt: ", err)
return
}
scanner := bufio.NewScanner(strings.NewReader(string(allowedUsersTxt)))
@ -194,47 +273,8 @@ func reloadAllowedUsers() {
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() {
reloadAllowedUsers()
doneWatching := watchAllowedUsers()
loadAllowedUsers()
http.HandleFunc("/", handleHome)
http.HandleFunc("/browse", handleBrowse)
@ -242,7 +282,16 @@ func main() {
http.Handle("/etc/", http.FileServerFS(etc))
http.Handle("/store/", http.StripPrefix("/store/", http.FileServer(http.Dir(storePath))))
http.ListenAndServe(":9090", nil)
doneWatching<-true
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)
}
}

75
ltscleanerd.c Normal file
View File

@ -0,0 +1,75 @@
//go:build exclude
#include <time.h>
#include <signal.h>
#define GEBS_NO_PREFIX
#define GEBS_IMPLEMENTATION
#include "gebs/gebs.h"
char *prog = nil;
char *file_path = nil;
void timer_handler(int sig, siginfo_t *si, void *uc)
{
if (file_path != nil) {
remove1(file_path);
exit(0);
}
}
void make_timer(char *name, long seconds)
{
struct sigevent timer_event;
struct itimerspec timer_spec;
struct sigaction action;
int sig_no = SIGRTMIN;
action.sa_flags = SA_SIGINFO;
action.sa_sigaction = &timer_handler;
sigemptyset(&action.sa_mask);
if (sigaction(sig_no, &action, nil) == -1) {
LOGE("Could not create timer signal handler\n");
exit(1);
}
timer_event.sigev_notify = SIGEV_SIGNAL;
timer_event.sigev_signo = sig_no;
timer_event.sigev_value.sival_ptr = name;
timer_t timer_id;
if (timer_create(CLOCK_REALTIME, &timer_event, &timer_id) == -1) {
LOGE("Could not create timer\n");
exit(1);
}
memset(&timer_spec, 0, sizeof(timer_spec));
timer_spec.it_value.tv_sec = seconds;
if (timer_settime(timer_id, 0, &timer_spec, nil) == -1) {
LOGE("Could not set the timeout\n");
exit(1);
}
}
int main(int argc, char ** argv)
{
prog = SHIFT(&argc, &argv);
file_path = SHIFT(&argc, &argv);
char *timeout_string = SHIFT(&argc, &argv);
if (!exists1(file_path)) {
LOGI("Path %s does not exist\n", file_path);
return 1;
}
char *endp;
long timeout = strtol(timeout_string, &endp, 10);
make_timer("cleaner", timeout);
pause();
return 0;
}

9
scripts/update-release Executable file
View File

@ -0,0 +1,9 @@
#!/bin/sh
systemctl stop lts.service
make clean
make -B BUILD_MODE=RELEASE
cp ./lts /usr/local/bin/lts
cp ./ltscleanerd /var/lib/lts
systemctl start lts.service
systemctl status lts.service

13
systemd/lts.service Normal file
View File

@ -0,0 +1,13 @@
[Unit]
Description=Lair Temporary Storage server
After=network.target
[Service]
Type=simple
Restart=always
User=root
WorkingDirectory=/var/lib/lts
ExecStart=/usr/local/bin/lts
[Install]
WantedBy=multi-user.target

View File

@ -11,6 +11,7 @@
<td><a href="/store/{{.FileName}}">{{.ReadableName}}</a></td>
<td><span>{{.Perm}}</span></td>
<td><span>{{.Modtime}}</span></td>
<td><span>{{.TimeLeft}}</span></td>
</tr>
{{end}}
</table>

View File

@ -11,17 +11,25 @@
<p>
Welcome to The Lair's temporary storage service. Use this service to transport files via
a temporary remote storage. <br />
ONLY WHITELISTED USERS can upload! For access, reach out to <a href="mailto:kamkow256@gmail.com">me</a>.<br />
Only 30PLN / 12MON.
ONLY WHITELISTED USERS can upload! For access, reach out to <a href="mailto:kamkow256@gmail.com">me</a>.
</p>
</section>
<section>
<h2>Upload</h2>
<p>
<form id="upload-form" method="POST" enctype="multipart/form-data">
<label for="myfile">File:</label>
<input type="file" name="myfile" id="myfile" required />
<label for="user">User:</label>
<input type="text" name="user" id="user" required />
<label for="pass">Password:</label>
<input type="password" name="pass", id="pass" required />
<label for="hours">Hours:</label>
<input type="number" name="hours" id="hours" min="0" max="24" step="1" value="0" required />
<label for="mins">Minutes:</label>
<input type="number" name="mins" id="mins" min="0" max="60" step="1" value="0" required />
<label for="secs">Seconds:</label>
<input type="number" name="secs" id="secs" min="0" max="60" step="1" value="0" required />
<input type="submit" value="Upload" />
</form>
</p>
@ -44,8 +52,15 @@ upload_form.addEventListener("submit", async function (event) {
return;
}
const hours = document.getElementById("hours").value;
const mins = document.getElementById("mins").value;
const secs = document.getElementById("secs").value;
const form_data = new FormData();
form_data.append("myfile", file);
form_data.append("hours", hours);
form_data.append("mins", mins);
form_data.append("secs", secs);
const user = document.getElementById("user").value;
const pass = document.getElementById("pass").value;