package main import ( "bufio" "crypto/subtle" "embed" "fmt" "html/template" "io/ioutil" "log" "net/http" "os" "path/filepath" "strings" "sync" "github.com/fsnotify/fsnotify" ) var LISTEN_PORT string //go:embed tmpls/* var tmpls embed.FS //go:embed etc/* var etc embed.FS 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 = "./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]) } 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)) fmt.Fprintf(w, "Link: ") 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() { 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(LISTEN_PORT, nil) doneWatching<-true }