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 + + + + + + {{range .}} + + + + + + {{end}} +
{{.ReadableName}}{{.Perm}}{{.Modtime}}
+ + 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.

+
+

Upload

+

+

+ + + + +
+

+
+
+

Browse

+

+ Browse the files here +

+
+ diff --git a/uuid.go b/uuid.go new file mode 100644 index 0000000..3975562 --- /dev/null +++ b/uuid.go @@ -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 +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:]) +}