hardfiles/main.go

325 lines
7.6 KiB
Go
Raw Normal View History

2023-09-30 23:06:22 +00:00
package main
import (
"crypto/rand"
2023-09-30 23:06:22 +00:00
"io"
"net/http"
"os"
"strconv"
"time"
"github.com/BurntSushi/toml"
"github.com/gabriel-vasile/mimetype"
"github.com/gorilla/mux"
"github.com/landlock-lsm/go-landlock/landlock"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
2023-11-01 01:19:21 +00:00
bolt "go.etcd.io/bbolt"
2023-09-30 23:06:22 +00:00
)
var (
db *bolt.DB
conf Config
)
type Config struct {
Webroot string `toml:"webroot"`
LPort string `toml:"lport"`
VHost string `toml:"vhost"`
DBFile string `toml:"dbfile"`
FileLen int `toml:"filelen"`
FileFolder string `toml:"folder"`
2023-12-13 13:57:31 +00:00
DefaultTTL int `toml:"default_ttl"`
MaxTTL int `toml:"maximum_ttl"`
2023-09-30 23:06:22 +00:00
}
func LoadConf() {
if _, err := toml.DecodeFile("config.toml", &conf); err != nil {
log.Fatal().Err(err).Msg("unable to parse config.toml")
}
}
2023-11-01 13:11:34 +00:00
func Shred(path string) error {
2023-11-01 01:19:21 +00:00
fileinfo, err := os.Stat(path)
if err != nil {
return err
}
size := fileinfo.Size()
if err = Scramble(path, size); err != nil {
2023-11-01 01:19:21 +00:00
return err
}
if err = Zeros(path, size); err != nil {
2023-11-01 01:19:21 +00:00
return err
}
if err = os.Remove(path); err != nil {
2023-11-01 01:19:21 +00:00
return err
}
return nil
}
2023-11-01 13:11:34 +00:00
func Scramble(path string, size int64) error {
2023-12-27 20:11:13 +00:00
file, err := os.OpenFile(path, os.O_RDWR, 0)
if err != nil {
return err
}
defer file.Close()
for i := 0; i < 7; i++ { // 7 iterations
buff := make([]byte, size)
if _, err := rand.Read(buff); err != nil {
2023-11-01 01:19:21 +00:00
return err
}
2023-12-27 20:11:13 +00:00
if _, err := file.WriteAt(buff, 0); err != nil {
2023-11-01 01:19:21 +00:00
return err
}
}
return nil
}
2023-11-01 13:11:34 +00:00
func Zeros(path string, size int64) error {
2023-11-01 01:19:21 +00:00
file, err := os.OpenFile(path, os.O_RDWR, 0)
if err != nil {
return err
}
2023-11-01 13:11:34 +00:00
defer file.Close()
2023-11-01 01:19:21 +00:00
offset, err := file.Seek(0, 0)
if err != nil {
return err
}
buff := make([]byte, size)
file.WriteAt(buff, offset)
return nil
}
func NameGen(fileNameLength int) string {
const chars = "abcdefghjkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ0123456789"
2023-09-30 23:06:22 +00:00
ll := len(chars)
b := make([]byte, fileNameLength)
2023-09-30 23:06:22 +00:00
rand.Read(b) // generates len(b) random bytes
for i := int64(0); i < int64(fileNameLength); i++ {
2023-09-30 23:06:22 +00:00
b[i] = chars[int(b[i])%ll]
}
return string(b)
}
2023-12-12 20:04:32 +00:00
func Exists(path string) bool {
if _, err := os.Stat(path); os.IsNotExist(err) {
2023-09-30 23:06:22 +00:00
return false
}
return true
}
func UploadHandler(w http.ResponseWriter, r *http.Request) {
// expiry time
var name string
var ttl int64
var fileNameLength int
fileNameLength = 0
ttl = 0
2023-09-30 23:06:22 +00:00
file, _, err := r.FormFile("file")
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
defer file.Close()
mtype, err := mimetype.DetectReader(file)
if err != nil {
w.Write([]byte("error detecting the mime type of your file\n"))
return
}
file.Seek(0, 0)
// Check if expiry time is present and length is too long
if r.PostFormValue("expiry") != "" {
ttl, err = strconv.ParseInt(r.PostFormValue("expiry"), 10, 64)
if err != nil {
log.Error().Err(err).Msg("expiry could not be parsed")
} else {
2023-12-13 13:57:31 +00:00
// Get maximum ttl length from config and kill upload if specified ttl is too long, this can probably be handled better in the future
if ttl < 1 || ttl > int64(conf.MaxTTL) {
w.WriteHeader(http.StatusBadRequest)
return
}
}
}
// Default to conf if not present
if ttl == 0 {
2023-12-13 13:57:31 +00:00
ttl = int64(conf.DefaultTTL)
}
// Check if the file length parameter exists and also if it's too long
if r.PostFormValue("url_len") != "" {
fileNameLength, err = strconv.Atoi(r.PostFormValue("url_len"))
if err != nil {
log.Error().Err(err).Msg("url_len could not be parsed")
} else {
// if the length is < 3 and > 128 return error
if fileNameLength < 3 || fileNameLength > 128 {
w.WriteHeader(http.StatusBadRequest)
return
}
}
}
// Default to conf if not present
if fileNameLength == 0 {
fileNameLength = conf.FileLen
}
2023-09-30 23:06:22 +00:00
// generate + check name
for {
id := NameGen(fileNameLength)
2023-09-30 23:06:22 +00:00
name = id + mtype.Extension()
2023-12-12 20:04:32 +00:00
if !Exists(conf.FileFolder + "/" + name) {
2023-09-30 23:06:22 +00:00
break
}
}
err = db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte("expiry"))
err := b.Put([]byte(name), []byte(strconv.FormatInt(time.Now().Unix()+ttl, 10)))
2023-09-30 23:06:22 +00:00
return err
})
if err != nil {
log.Error().Err(err).Msg("failed to put expiry")
2023-09-30 23:06:22 +00:00
}
f, err := os.OpenFile(conf.FileFolder+"/"+name, os.O_WRONLY|os.O_CREATE, 0644)
if err != nil {
log.Error().Err(err).Msg("error opening a file for write")
2023-09-30 23:06:22 +00:00
w.WriteHeader(http.StatusInternalServerError) // change to json
return
}
defer f.Close()
io.Copy(f, file)
log.Info().Str("name", name).Int64("ttl", ttl).Msg("wrote new file")
hostedurl := "https://" + conf.VHost + "/uploads/" + name
2023-09-30 23:06:22 +00:00
w.Header().Set("Location", hostedurl)
w.WriteHeader(http.StatusSeeOther)
w.Write([]byte(hostedurl))
2023-09-30 23:06:22 +00:00
}
func Cull() {
for {
db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte("expiry"))
c := b.Cursor()
for k, v := c.First(); k != nil; k, v = c.Next() {
eol, err := strconv.ParseInt(string(v), 10, 64)
if err != nil {
log.Error().Err(err).Bytes("k", k).Bytes("v", v).Msg("expiration time could not be parsed")
continue
}
if time.Now().After(time.Unix(eol, 0)) {
2023-11-01 13:11:34 +00:00
if err := Shred(conf.FileFolder + "/" + string(k)); err != nil {
2023-11-01 01:19:21 +00:00
log.Error().Err(err).Msg("shredding failed")
} else {
log.Info().Str("name", string(k)).Msg("shredded file")
2023-11-01 01:19:21 +00:00
}
2023-09-30 23:06:22 +00:00
c.Delete()
}
}
return nil
})
time.Sleep(5 * time.Second)
}
}
func main() {
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
LoadConf()
2023-12-12 20:04:32 +00:00
if !Exists(conf.FileFolder) {
if err := os.Mkdir(conf.FileFolder, 0755); err != nil {
log.Fatal().Err(err).Msg("unable to create folder")
}
}
if !Exists(conf.DBFile) {
if _, err := os.Create(conf.DBFile); err != nil {
log.Fatal().Err(err).Msg("unable to create database file")
}
}
2023-09-30 23:06:22 +00:00
err := landlock.V2.BestEffort().RestrictPaths(
2023-11-01 13:11:34 +00:00
landlock.RWDirs(conf.FileFolder),
2023-11-01 01:19:21 +00:00
landlock.RWDirs(conf.Webroot),
2023-09-30 23:06:22 +00:00
landlock.RWFiles(conf.DBFile),
)
if err != nil {
log.Warn().Err(err).Msg("could not landlock")
}
_, err = os.Open("/etc/passwd")
if err == nil {
log.Warn().Msg("landlock failed, could open /etc/passwd, are you on a 5.13+ kernel?")
2023-09-30 23:06:22 +00:00
} else {
log.Info().Err(err).Msg("landlocked")
2023-09-30 23:06:22 +00:00
}
db, err = bolt.Open(conf.DBFile, 0600, nil)
if err != nil {
log.Fatal().Err(err).Msg("unable to open database file")
}
db.Update(func(tx *bolt.Tx) error {
_, err := tx.CreateBucketIfNotExists([]byte("expiry"))
if err != nil {
log.Fatal().Err(err).Msg("error creating expiry bucket")
return err
}
return nil
})
r := mux.NewRouter()
r.HandleFunc("/", UploadHandler).Methods("POST")
2023-11-01 13:11:34 +00:00
r.HandleFunc("/uploads/{name}", func(w http.ResponseWriter, r *http.Request) {
2023-09-30 23:06:22 +00:00
vars := mux.Vars(r)
2023-12-12 20:04:32 +00:00
if !Exists(conf.FileFolder + "/" + vars["name"]) {
2023-09-30 23:06:22 +00:00
w.WriteHeader(http.StatusNotFound)
w.Write([]byte("file not found"))
2023-09-30 23:06:22 +00:00
} else {
http.ServeFile(w, r, conf.FileFolder+"/"+vars["name"])
}
}).Methods("GET")
r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, conf.Webroot+"/index.html")
})
r.HandleFunc("/{file}", func(w http.ResponseWriter, r *http.Request) {
file := mux.Vars(r)["file"]
if _, err := os.Stat(conf.Webroot + "/" + file); os.IsNotExist(err) {
http.Redirect(w, r, "/", http.StatusSeeOther)
} else {
http.ServeFile(w, r, conf.Webroot+"/"+file)
}
2023-11-16 02:43:18 +00:00
}).Methods("GET")
2023-09-30 23:06:22 +00:00
http.Handle("/", r)
go Cull()
serv := &http.Server{
2023-11-01 13:11:34 +00:00
Addr: ":" + conf.LPort,
Handler: r,
ErrorLog: nil,
2023-09-30 23:06:22 +00:00
IdleTimeout: 20 * time.Second,
}
log.Warn().Msg("shredding is only effective on HDD volumes")
2023-11-01 13:11:34 +00:00
log.Info().Err(err).Msg("listening on port " + conf.LPort + "...")
2023-09-30 23:06:22 +00:00
if err := serv.ListenAndServe(); err != nil {
log.Fatal().Err(err).Msg("error starting server")
}
db.Close()
}