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 }