hardfiles

- EZPZ File Sharing Service
git clone git://git.acid.vegas/hardfiles.git
Log | Files | Refs | Archive | README | LICENSE

main.go (7663B)

      1 package main
      2 
      3 import (
      4 	"crypto/rand"
      5 	"io"
      6 	"net/http"
      7 	"os"
      8 	"strconv"
      9 	"time"
     10 
     11 	"github.com/BurntSushi/toml"
     12 	"github.com/gabriel-vasile/mimetype"
     13 	"github.com/gorilla/mux"
     14 	"github.com/landlock-lsm/go-landlock/landlock"
     15 	"github.com/rs/zerolog"
     16 	"github.com/rs/zerolog/log"
     17 	bolt "go.etcd.io/bbolt"
     18 )
     19 
     20 var (
     21 	db   *bolt.DB
     22 	conf Config
     23 )
     24 
     25 type Config struct {
     26 	Webroot    string `toml:"webroot"`
     27 	LPort      string `toml:"lport"`
     28 	VHost      string `toml:"vhost"`
     29 	DBFile     string `toml:"dbfile"`
     30 	FileLen    int    `toml:"filelen"`
     31 	FileFolder string `toml:"folder"`
     32 	DefaultTTL int    `toml:"default_ttl"`
     33 	MaxTTL     int    `toml:"maximum_ttl"`
     34 }
     35 
     36 func LoadConf() {
     37 	if _, err := toml.DecodeFile("config.toml", &conf); err != nil {
     38 		log.Fatal().Err(err).Msg("unable to parse config.toml")
     39 	}
     40 }
     41 
     42 func Shred(path string) error {
     43 	fileinfo, err := os.Stat(path)
     44 	if err != nil {
     45 		return err
     46 	}
     47 	size := fileinfo.Size()
     48 	if err = Scramble(path, size); err != nil {
     49 		return err
     50 	}
     51 
     52 	if err = Zeros(path, size); err != nil {
     53 		return err
     54 	}
     55 
     56 	if err = os.Remove(path); err != nil {
     57 		return err
     58 	}
     59 
     60 	return nil
     61 }
     62 
     63 func Scramble(path string, size int64) error {
     64 	file, err := os.OpenFile(path, os.O_RDWR, 0)
     65 	if err != nil {
     66 		return err
     67 	}
     68 	defer file.Close()
     69 
     70 	for i := 0; i < 7; i++ { // 7 iterations
     71 		buff := make([]byte, size)
     72 		if _, err := rand.Read(buff); err != nil {
     73 			return err
     74 		}
     75 		if _, err := file.WriteAt(buff, 0); err != nil {
     76 			return err
     77 		}
     78 	}
     79 	return nil
     80 }
     81 
     82 func Zeros(path string, size int64) error {
     83 	file, err := os.OpenFile(path, os.O_RDWR, 0)
     84 	if err != nil {
     85 		return err
     86 	}
     87 	defer file.Close()
     88 
     89 	buff := make([]byte, size)
     90 	file.WriteAt(buff, 0)
     91 	return nil
     92 }
     93 
     94 func NameGen(fileNameLength int) string {
     95 	const chars = "abcdefghjkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ0123456789"
     96 	ll := len(chars)
     97 	b := make([]byte, fileNameLength)
     98 	rand.Read(b) // generates len(b) random bytes
     99 	for i := int64(0); i < int64(fileNameLength); i++ {
    100 		b[i] = chars[int(b[i])%ll]
    101 	}
    102 	return string(b)
    103 }
    104 
    105 func Exists(path string) bool {
    106 	if _, err := os.Stat(path); os.IsNotExist(err) {
    107 		return false
    108 	}
    109 	return true
    110 }
    111 
    112 func UploadHandler(w http.ResponseWriter, r *http.Request) {
    113 	// expiry time
    114 	var name string
    115 	var ttl int64
    116 	var fileNameLength int
    117 
    118 	fileNameLength = 0
    119 	ttl = 0
    120 
    121 	file, _, err := r.FormFile("file")
    122 	if err != nil {
    123 		w.WriteHeader(http.StatusBadRequest)
    124 		return
    125 	}
    126 	defer file.Close()
    127 
    128 	mtype, err := mimetype.DetectReader(file)
    129 	if err != nil {
    130 		w.Write([]byte("error detecting the mime type of your file\n"))
    131 		return
    132 	}
    133 	file.Seek(0, 0)
    134 
    135 	// Check if expiry time is present and length is too long
    136 	if r.PostFormValue("expiry") != "" {
    137 		ttl, err = strconv.ParseInt(r.PostFormValue("expiry"), 10, 64)
    138 		if err != nil {
    139 			log.Error().Err(err).Msg("expiry could not be parsed")
    140 		} else {
    141 			// Get maximum ttl length from config and kill upload if specified ttl is too long, this can probably be handled better in the future
    142 			if ttl < 1 || ttl > int64(conf.MaxTTL) {
    143 				w.WriteHeader(http.StatusBadRequest)
    144 				return
    145 			}
    146 		}
    147 	}
    148 
    149 	// Default to conf if not present
    150 	if ttl == 0 {
    151 		ttl = int64(conf.DefaultTTL)
    152 	}
    153 
    154 	// Check if the file length parameter exists and also if it's too long
    155 	if r.PostFormValue("url_len") != "" {
    156 		fileNameLength, err = strconv.Atoi(r.PostFormValue("url_len"))
    157 		if err != nil {
    158 			log.Error().Err(err).Msg("url_len could not be parsed")
    159 		} else {
    160 			// if the length is < 3 and > 128 return error
    161 			if fileNameLength < 3 || fileNameLength > 128 {
    162 				w.WriteHeader(http.StatusBadRequest)
    163 				return
    164 			}
    165 		}
    166 	}
    167 
    168 	// Default to conf if not present
    169 	if fileNameLength == 0 {
    170 		fileNameLength = conf.FileLen
    171 	}
    172 
    173 	// generate + check name
    174 	for {
    175 		id := NameGen(fileNameLength)
    176 		name = id + mtype.Extension()
    177 		if !Exists(conf.FileFolder + "/" + name) {
    178 			break
    179 		}
    180 	}
    181 
    182 	err = db.Update(func(tx *bolt.Tx) error {
    183 		b := tx.Bucket([]byte("expiry"))
    184 		err := b.Put([]byte(name), []byte(strconv.FormatInt(time.Now().Unix()+ttl, 10)))
    185 		return err
    186 	})
    187 	if err != nil {
    188 		log.Error().Err(err).Msg("failed to put expiry")
    189 	}
    190 
    191 	f, err := os.OpenFile(conf.FileFolder+"/"+name, os.O_WRONLY|os.O_CREATE, 0644)
    192 	if err != nil {
    193 		log.Error().Err(err).Msg("error opening a file for write")
    194 		w.WriteHeader(http.StatusInternalServerError) // change to json
    195 		return
    196 	}
    197 	defer f.Close()
    198 
    199 	io.Copy(f, file)
    200 	log.Info().Str("name", name).Int64("ttl", ttl).Msg("wrote new file")
    201 
    202 	hostedurl := "https://" + conf.VHost + "/uploads/" + name
    203 
    204 	w.Header().Set("Location", hostedurl)
    205 	w.WriteHeader(http.StatusSeeOther)
    206 	w.Write([]byte(hostedurl))
    207 }
    208 
    209 func Cull() {
    210 	for {
    211 		db.Update(func(tx *bolt.Tx) error {
    212 			b := tx.Bucket([]byte("expiry"))
    213 			c := b.Cursor()
    214 			for k, v := c.First(); k != nil; k, v = c.Next() {
    215 				eol, err := strconv.ParseInt(string(v), 10, 64)
    216 				if err != nil {
    217 					log.Error().Err(err).Bytes("k", k).Bytes("v", v).Msg("expiration time could not be parsed")
    218 					continue
    219 				}
    220 				if time.Now().After(time.Unix(eol, 0)) {
    221 					if err := Shred(conf.FileFolder + "/" + string(k)); err != nil {
    222 						log.Error().Err(err).Msg("shredding failed")
    223 					} else {
    224 						log.Info().Str("name", string(k)).Msg("shredded file")
    225 					}
    226 					c.Delete()
    227 				}
    228 			}
    229 			return nil
    230 		})
    231 		time.Sleep(5 * time.Second)
    232 	}
    233 }
    234 
    235 func main() {
    236 	log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
    237 	LoadConf()
    238 
    239 	if !Exists(conf.FileFolder) {
    240 		if err := os.Mkdir(conf.FileFolder, 0755); err != nil {
    241 			log.Fatal().Err(err).Msg("unable to create folder")
    242 		}
    243 	}
    244 	if !Exists(conf.DBFile) {
    245 		if _, err := os.Create(conf.DBFile); err != nil {
    246 			log.Fatal().Err(err).Msg("unable to create database file")
    247 		}
    248 	}
    249 	err := landlock.V2.BestEffort().RestrictPaths(
    250 		landlock.RWDirs(conf.FileFolder),
    251 		landlock.RWDirs(conf.Webroot),
    252 		landlock.RWFiles(conf.DBFile),
    253 	)
    254 
    255 	if err != nil {
    256 		log.Warn().Err(err).Msg("could not landlock")
    257 	}
    258 
    259 	_, err = os.Open("/etc/passwd")
    260 	if err == nil {
    261 		log.Warn().Msg("landlock failed, could open /etc/passwd, are you on a 5.13+ kernel?")
    262 	} else {
    263 		log.Info().Err(err).Msg("landlocked")
    264 	}
    265 
    266 	db, err = bolt.Open(conf.DBFile, 0600, nil)
    267 	if err != nil {
    268 		log.Fatal().Err(err).Msg("unable to open database file")
    269 	}
    270 	db.Update(func(tx *bolt.Tx) error {
    271 		_, err := tx.CreateBucketIfNotExists([]byte("expiry"))
    272 		if err != nil {
    273 			log.Fatal().Err(err).Msg("error creating expiry bucket")
    274 			return err
    275 		}
    276 		return nil
    277 	})
    278 
    279 	r := mux.NewRouter()
    280 	r.HandleFunc("/", UploadHandler).Methods("POST")
    281 	r.HandleFunc("/uploads/{name}", func(w http.ResponseWriter, r *http.Request) {
    282 		vars := mux.Vars(r)
    283 		if !Exists(conf.FileFolder + "/" + vars["name"]) {
    284 			w.WriteHeader(http.StatusNotFound)
    285 			w.Write([]byte("file not found"))
    286 		} else {
    287 			http.ServeFile(w, r, conf.FileFolder+"/"+vars["name"])
    288 		}
    289 	}).Methods("GET")
    290 	r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    291 		http.ServeFile(w, r, conf.Webroot+"/index.html")
    292 	})
    293 	r.HandleFunc("/{file}", func(w http.ResponseWriter, r *http.Request) {
    294 		file := mux.Vars(r)["file"]
    295 		if _, err := os.Stat(conf.Webroot + "/" + file); os.IsNotExist(err) {
    296 			http.Redirect(w, r, "/", http.StatusSeeOther)
    297 		} else {
    298 			http.ServeFile(w, r, conf.Webroot+"/"+file)
    299 		}
    300 	}).Methods("GET")
    301 	http.Handle("/", r)
    302 
    303 	go Cull()
    304 
    305 	serv := &http.Server{
    306 		Addr:        ":" + conf.LPort,
    307 		Handler:     r,
    308 		ErrorLog:    nil,
    309 		IdleTimeout: 20 * time.Second,
    310 	}
    311 
    312 	log.Warn().Msg("shredding is only effective on HDD volumes")
    313 	log.Info().Err(err).Msg("listening on port " + conf.LPort + "...")
    314 
    315 	if err := serv.ListenAndServe(); err != nil {
    316 		log.Fatal().Err(err).Msg("error starting server")
    317 	}
    318 
    319 	db.Close()
    320 }