Simple file upload and browsing

This commit is contained in:
kamkow1
2025-06-21 02:05:25 +02:00
parent c5f009654c
commit 622b98f40a
7 changed files with 496 additions and 7 deletions

2
.gitignore vendored
View File

@ -33,3 +33,5 @@ go.work.sum
watcher watcher
lts lts
ALLOWED_USERS.txt

4
go.mod
View File

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

4
go.sum Normal file
View File

@ -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=

240
lts.go
View File

@ -1,10 +1,20 @@
package main package main
import ( import (
"bufio"
"crypto/subtle"
"embed" "embed"
"fmt"
"html/template" "html/template"
"io/ioutil"
"log" "log"
"net/http" "net/http"
"os"
"path/filepath"
"strings"
"sync"
"github.com/fsnotify/fsnotify"
) )
//go:embed tmpls/* //go:embed tmpls/*
@ -13,14 +23,232 @@ var tmpls embed.FS
//go:embed etc/* //go:embed etc/*
var etc embed.FS var etc embed.FS
func main() { func createFile(storePath, name string) (*os.File, error) {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { if _, err := os.Stat(storePath); os.IsNotExist(err) {
tmpl := template.Must(template.ParseFS(tmpls, "tmpls/*.html")) os.Mkdir(storePath, 0755)
if err := tmpl.Execute(w, nil); err != nil { }
log.Fatal(err)
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("/etc/", http.FileServerFS(etc))
http.Handle("/store/", http.StripPrefix("/store/", http.FileServer(http.Dir(storePath))))
http.ListenAndServe(":9090", nil) http.ListenAndServe(":9090", nil)
doneWatching<-true
} }

18
tmpls/browse.html Normal file
View File

@ -0,0 +1,18 @@
<html>
<head>
<title>Lair Tremporary Storage</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="/etc/simple.css" />
</head>
<body>
<table>
{{range .}}
<tr>
<td><a href="/store/{{.FileName}}">{{.ReadableName}}</a></td>
<td><span>{{.Perm}}</span></td>
<td><span>{{.Modtime}}</span></td>
</tr>
{{end}}
</table>
</body>
</html>

View File

@ -7,10 +7,70 @@
<body> <body>
<h1>Lair Tremporary Storage</h1> <h1>Lair Tremporary Storage</h1>
<section> <section>
<h2>Welcome</h2>
<p> <p>
Welcome to The Lair's temporary storage service. Use this service to transport files via 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. <br />
ONLY WHITELISTED USERS can upload! For access, reach out to <a href="mailto:kamkow256@gmail.com">me</a>.<br />
Only 30PLN / 12MON.
</p> </p>
</section> </section>
<section>
<h2>Upload</h2>
<p>
<form id="upload-form" method="POST" enctype="multipart/form-data">
<input type="file" name="myfile" id="myfile" required />
<input type="text" name="user" id="user" required />
<input type="password" name="pass", id="pass" required />
<input type="submit" value="Upload" />
</form>
</p>
</section>
<section>
<h2>Browse</h2>
<p>
Browse the files <a href="/store">here</a>
</p>
</section>
<script>
let upload_form = document.getElementById("upload-form");
upload_form.addEventListener("submit", async function (event) {
event.preventDefault();
const file_input = document.getElementById("myfile");
const file = file_input.files[0];
if (!file) {
alert("Select a file");
return;
}
const form_data = new FormData();
form_data.append("myfile", file);
const user = document.getElementById("user").value;
const pass = document.getElementById("pass").value;
console.log(form_data);
console.log(user, pass);
const auth = "Basic " + btoa(user + ":" + pass);
try {
const host = window.location.origin;
const res = await fetch(host + "/upload", {
method: "POST",
headers: {
"Authorization": auth,
},
body: form_data,
});
const text = await res.text();
alert(text);
} catch (err) {
alert("Error: " + err);
}
});
</script>
</body> </body>
</html> </html>

173
uuid.go Normal file
View File

@ -0,0 +1,173 @@
// This package provides immutable UUID structs and the functions
// NewV3, NewV4, NewV5 and Parse() for generating versions 3, 4
// and 5 UUIDs as specified in RFC 4122.
//
// Copyright (C) 2011 by Krzysztof Kowalik <chris@nu7hat.ch>
package main
import (
"crypto/md5"
"crypto/rand"
"crypto/sha1"
"encoding/hex"
"errors"
"fmt"
"hash"
"regexp"
)
// The UUID reserved variants.
const (
ReservedNCS byte = 0x80
ReservedRFC4122 byte = 0x40
ReservedMicrosoft byte = 0x20
ReservedFuture byte = 0x00
)
// The following standard UUIDs are for use with NewV3() or NewV5().
var (
NamespaceDNS, _ = ParseHex("6ba7b810-9dad-11d1-80b4-00c04fd430c8")
NamespaceURL, _ = ParseHex("6ba7b811-9dad-11d1-80b4-00c04fd430c8")
NamespaceOID, _ = ParseHex("6ba7b812-9dad-11d1-80b4-00c04fd430c8")
NamespaceX500, _ = ParseHex("6ba7b814-9dad-11d1-80b4-00c04fd430c8")
)
// Pattern used to parse hex string representation of the UUID.
// FIXME: do something to consider both brackets at one time,
// current one allows to parse string with only one opening
// or closing bracket.
const hexPattern = "^(urn\\:uuid\\:)?\\{?([a-z0-9]{8})-([a-z0-9]{4})-" +
"([1-5][a-z0-9]{3})-([a-z0-9]{4})-([a-z0-9]{12})\\}?$"
var re = regexp.MustCompile(hexPattern)
// A UUID representation compliant with specification in
// RFC 4122 document.
type UUID [16]byte
// ParseHex creates a UUID object from given hex string
// representation. Function accepts UUID string in following
// formats:
//
// uuid.ParseHex("6ba7b814-9dad-11d1-80b4-00c04fd430c8")
// uuid.ParseHex("{6ba7b814-9dad-11d1-80b4-00c04fd430c8}")
// uuid.ParseHex("urn:uuid:6ba7b814-9dad-11d1-80b4-00c04fd430c8")
//
func ParseHex(s string) (u *UUID, err error) {
md := re.FindStringSubmatch(s)
if md == nil {
err = errors.New("Invalid UUID string")
return
}
hash := md[2] + md[3] + md[4] + md[5] + md[6]
b, err := hex.DecodeString(hash)
if err != nil {
return
}
u = new(UUID)
copy(u[:], b)
return
}
// Parse creates a UUID object from given bytes slice.
func Parse(b []byte) (u *UUID, err error) {
if len(b) != 16 {
err = errors.New("Given slice is not valid UUID sequence")
return
}
u = new(UUID)
copy(u[:], b)
return
}
// Generate a UUID based on the MD5 hash of a namespace identifier
// and a name.
func NewV3(ns *UUID, name []byte) (u *UUID, err error) {
if ns == nil {
err = errors.New("Invalid namespace UUID")
return
}
u = new(UUID)
// Set all bits to MD5 hash generated from namespace and name.
u.setBytesFromHash(md5.New(), ns[:], name)
u.setVariant(ReservedRFC4122)
u.setVersion(3)
return
}
// Generate a random UUID.
func NewV4() (u *UUID, err error) {
u = new(UUID)
// Set all bits to randomly (or pseudo-randomly) chosen values.
_, err = rand.Read(u[:])
if err != nil {
return
}
u.setVariant(ReservedRFC4122)
u.setVersion(4)
return
}
// Generate a UUID based on the SHA-1 hash of a namespace identifier
// and a name.
func NewV5(ns *UUID, name []byte) (u *UUID, err error) {
u = new(UUID)
// Set all bits to truncated SHA1 hash generated from namespace
// and name.
u.setBytesFromHash(sha1.New(), ns[:], name)
u.setVariant(ReservedRFC4122)
u.setVersion(5)
return
}
// Generate a MD5 hash of a namespace and a name, and copy it to the
// UUID slice.
func (u *UUID) setBytesFromHash(hash hash.Hash, ns, name []byte) {
hash.Write(ns[:])
hash.Write(name)
copy(u[:], hash.Sum([]byte{})[:16])
}
// Set the two most significant bits (bits 6 and 7) of the
// clock_seq_hi_and_reserved to zero and one, respectively.
func (u *UUID) setVariant(v byte) {
switch v {
case ReservedNCS:
u[8] = (u[8] | ReservedNCS) & 0xBF
case ReservedRFC4122:
u[8] = (u[8] | ReservedRFC4122) & 0x7F
case ReservedMicrosoft:
u[8] = (u[8] | ReservedMicrosoft) & 0x3F
}
}
// Variant returns the UUID Variant, which determines the internal
// layout of the UUID. This will be one of the constants: RESERVED_NCS,
// RFC_4122, RESERVED_MICROSOFT, RESERVED_FUTURE.
func (u *UUID) Variant() byte {
if u[8]&ReservedNCS == ReservedNCS {
return ReservedNCS
} else if u[8]&ReservedRFC4122 == ReservedRFC4122 {
return ReservedRFC4122
} else if u[8]&ReservedMicrosoft == ReservedMicrosoft {
return ReservedMicrosoft
}
return ReservedFuture
}
// Set the four most significant bits (bits 12 through 15) of the
// time_hi_and_version field to the 4-bit version number.
func (u *UUID) setVersion(v byte) {
u[6] = (u[6] & 0xF) | (v << 4)
}
// Version returns a version number of the algorithm used to
// generate the UUID sequence.
func (u *UUID) Version() uint {
return uint(u[6] >> 4)
}
// Returns unparsed version of the generated UUID sequence.
func (u *UUID) String() string {
return fmt.Sprintf("%x-%x-%x-%x-%x", u[0:4], u[4:6], u[6:8], u[8:10], u[10:])
}