fileDrop.go (11982B)
1 package handlers 2 3 import ( 4 "bytes" 5 cryptoRand "crypto/rand" 6 "crypto/sha256" 7 "dkforest/pkg/captcha" 8 "dkforest/pkg/config" 9 "dkforest/pkg/database" 10 "dkforest/pkg/utils" 11 "dkforest/pkg/utils/crypto" 12 hutils "dkforest/pkg/web/handlers/utils" 13 "encoding/base64" 14 "encoding/hex" 15 "errors" 16 "fmt" 17 "github.com/asaskevich/govalidator" 18 "github.com/dustin/go-humanize" 19 "github.com/labstack/echo" 20 "github.com/sirupsen/logrus" 21 "io" 22 "math" 23 "net/http" 24 "os" 25 "path/filepath" 26 "sort" 27 "strconv" 28 "strings" 29 ) 30 31 func FileDropHandler(c echo.Context) error { 32 const filedropTmplName = "standalone.filedrop" 33 uuidParam := c.Param("uuid") 34 db := c.Get("database").(*database.DkfDB) 35 //if c.Request().ContentLength > config.MaxUserFileUploadSize { 36 // data.Error = fmt.Sprintf("The maximum file size is %s", humanize.Bytes(config.MaxUserFileUploadSize)) 37 // return c.Render(http.StatusOK, "chat-top-bar", data) 38 //} 39 40 filedrop, err := db.GetFiledropByUUID(uuidParam) 41 if err != nil { 42 return c.Redirect(http.StatusFound, "/") 43 } 44 if filedrop.FileSize > 0 { 45 return c.Redirect(http.StatusFound, "/") 46 } 47 48 var data fileDropData 49 data.Filedrop = filedrop 50 51 if c.Request().Method == http.MethodGet { 52 return c.Render(http.StatusOK, filedropTmplName, data) 53 } 54 55 file, handler, uploadErr := c.Request().FormFile("file") 56 if uploadErr != nil { 57 data.Error = uploadErr.Error() 58 return c.Render(http.StatusOK, filedropTmplName, data) 59 } 60 61 defer file.Close() 62 origFileName := handler.Filename 63 //if handler.Size > config.MaxUserFileUploadSize { 64 // return nil, html, fmt.Errorf("the maximum file size is %s", humanize.Bytes(config.MaxUserFileUploadSize)) 65 //} 66 if !govalidator.StringLength(origFileName, "3", "50") { 67 data.Error = "invalid file name, 3-50 characters" 68 return c.Render(http.StatusOK, filedropTmplName, data) 69 } 70 71 password := make([]byte, 16) 72 _, _ = cryptoRand.Read(password) 73 74 encrypter, err := utils.EncryptStream(password, file) 75 if err != nil { 76 data.Error = err.Error() 77 return c.Render(http.StatusOK, filedropTmplName, data) 78 } 79 80 outFile, err := os.OpenFile(filepath.Join(config.Global.ProjectFiledropPath.Get(), filedrop.FileName), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) 81 if err != nil { 82 data.Error = err.Error() 83 return c.Render(http.StatusOK, filedropTmplName, data) 84 } 85 defer outFile.Close() 86 written, err := io.Copy(outFile, encrypter) 87 if err != nil { 88 data.Error = err.Error() 89 return c.Render(http.StatusOK, filedropTmplName, data) 90 } 91 92 filedrop.Password = database.EncryptedString(password) 93 filedrop.IV = encrypter.Meta().IV 94 filedrop.OrigFileName = origFileName 95 filedrop.FileSize = written 96 filedrop.DoSave(db) 97 98 data.Success = "File uploaded successfully" 99 return c.Render(http.StatusOK, filedropTmplName, data) 100 } 101 102 func FileDropDkfUploadHandler(c echo.Context) error { 103 db := c.Get("database").(*database.DkfDB) 104 // Init 105 if c.Request().PostFormValue("init") != "" { 106 filedropUUID := c.Param("uuid") 107 _, err := db.GetFiledropByUUID(filedropUUID) 108 if err != nil { 109 return c.Redirect(http.StatusFound, "/") 110 } 111 112 _ = os.Mkdir(filepath.Join(config.Global.ProjectFiledropPath.Get(), filedropUUID), 0755) 113 metadataPath := filepath.Join(config.Global.ProjectFiledropPath.Get(), filedropUUID, "metadata") 114 115 fileName := c.Request().PostFormValue("fileName") 116 fileSize := c.Request().PostFormValue("fileSize") 117 fileSha256 := c.Request().PostFormValue("fileSha256") 118 chunkSize := c.Request().PostFormValue("chunkSize") 119 nbChunks := c.Request().PostFormValue("nbChunks") 120 data := []byte(fileName + "\n" + fileSize + "\n" + fileSha256 + "\n" + chunkSize + "\n" + nbChunks + "\n") 121 122 if _, err := os.Stat(metadataPath); err != nil { 123 if err := os.WriteFile(metadataPath, data, 0644); err != nil { 124 logrus.Error(err) 125 return c.String(http.StatusInternalServerError, err.Error()) 126 } 127 } else { 128 by, err := os.ReadFile(metadataPath) 129 if err != nil { 130 logrus.Error(err) 131 return c.String(http.StatusInternalServerError, err.Error()) 132 } 133 if bytes.Compare(by, data) != 0 { 134 err := errors.New("metadata file already exists with different configuration") 135 logrus.Error(err) 136 return c.String(http.StatusInternalServerError, err.Error()) 137 } 138 } 139 140 return c.NoContent(http.StatusOK) 141 } 142 143 // completed 144 if c.Request().PostFormValue("completed") != "" { 145 filedropUUID := c.Param("uuid") 146 147 filedrop, err := db.GetFiledropByUUID(filedropUUID) 148 if err != nil { 149 return c.Redirect(http.StatusFound, "/") 150 } 151 152 dirEntries, _ := os.ReadDir(filepath.Join(config.Global.ProjectFiledropPath.Get(), filedropUUID)) 153 fileNames := make([]string, 0) 154 for _, dirEntry := range dirEntries { 155 if !strings.HasPrefix(dirEntry.Name(), "part_") { 156 continue 157 } 158 fileNames = append(fileNames, dirEntry.Name()) 159 } 160 sort.Slice(fileNames, func(i, j int) bool { 161 a := strings.Split(fileNames[i], "_")[1] 162 b := strings.Split(fileNames[j], "_")[1] 163 numA, _ := strconv.Atoi(a) 164 numB, _ := strconv.Atoi(b) 165 return numA < numB 166 }) 167 168 metadata, err := os.ReadFile(filepath.Join(config.Global.ProjectFiledropPath.Get(), filedropUUID, "metadata")) 169 if err != nil { 170 logrus.Error(err) 171 return c.NoContent(http.StatusInternalServerError) 172 } 173 lines := strings.Split(string(metadata), "\n") 174 origFileName := lines[0] 175 fileSha256 := lines[2] 176 177 f, err := os.OpenFile(filepath.Join(config.Global.ProjectFiledropPath.Get(), filedrop.FileName), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) 178 if err != nil { 179 logrus.Error(err) 180 return c.NoContent(http.StatusInternalServerError) 181 } 182 defer f.Close() 183 h := sha256.New() 184 185 password := make([]byte, 16) 186 _, _ = cryptoRand.Read(password) 187 188 stream, _, iv, err := crypto.NewCtrStram(password) 189 if err != nil { 190 logrus.Error(err) 191 return c.NoContent(http.StatusInternalServerError) 192 } 193 written := int64(0) 194 for _, fileName := range fileNames { 195 by, err := os.ReadFile(filepath.Join(config.Global.ProjectFiledropPath.Get(), filedropUUID, fileName)) 196 if err != nil { 197 logrus.Error(err) 198 return c.NoContent(http.StatusInternalServerError) 199 } 200 201 dst := make([]byte, len(by)) 202 _, _ = h.Write(by) 203 stream.XORKeyStream(dst, by) 204 _, err = f.Write(dst) 205 if err != nil { 206 logrus.Error(err) 207 return c.NoContent(http.StatusInternalServerError) 208 } 209 written += int64(len(by)) 210 } 211 212 newFileSha256 := hex.EncodeToString(h.Sum(nil)) 213 214 if newFileSha256 != fileSha256 { 215 logrus.Errorf("%s != %s", newFileSha256, fileSha256) 216 return c.NoContent(http.StatusInternalServerError) 217 } 218 219 // Cleanup 220 _ = os.RemoveAll(filepath.Join(config.Global.ProjectFiledropPath.Get(), filedropUUID)) 221 222 filedrop.Password = database.EncryptedString(password) 223 filedrop.IV = iv 224 filedrop.OrigFileName = origFileName 225 filedrop.FileSize = written 226 filedrop.DoSave(db) 227 228 return c.NoContent(http.StatusOK) 229 } 230 231 filedropUUID := c.Param("uuid") 232 233 { 234 chunkFileName := c.Request().PostFormValue("chunkFileName") 235 if chunkFileName != "" { 236 if _, err := os.Stat(filepath.Join(config.Global.ProjectFiledropPath.Get(), filedropUUID, chunkFileName)); err != nil { 237 return c.NoContent(http.StatusOK) 238 } 239 // Let's use the teapot response (because why not) to say that we already have the file 240 return c.NoContent(http.StatusTeapot) 241 } 242 } 243 244 _, err := db.GetFiledropByUUID(filedropUUID) 245 if err != nil { 246 return c.Redirect(http.StatusFound, "/") 247 } 248 249 file, handler, err := c.Request().FormFile("file") 250 if err != nil { 251 logrus.Error(err) 252 return c.NoContent(http.StatusInternalServerError) 253 } 254 defer file.Close() 255 fileName := handler.Filename 256 by, _ := io.ReadAll(file) 257 p := filepath.Join(config.Global.ProjectFiledropPath.Get(), filedropUUID, fileName) 258 if err := os.WriteFile(p, by, 0644); err != nil { 259 logrus.Error(err) 260 return c.NoContent(http.StatusInternalServerError) 261 } 262 return c.NoContent(http.StatusOK) 263 } 264 265 func FileDropDkfDownloadHandler(c echo.Context) error { 266 filedropUUID := c.Param("uuid") 267 db := c.Get("database").(*database.DkfDB) 268 filedrop, err := db.GetFiledropByUUID(filedropUUID) 269 if err != nil { 270 return c.NoContent(http.StatusNotFound) 271 } 272 273 maxChunkSize := int64(2 << 20) // 2MB 274 f, err := os.Open(filepath.Join(config.Global.ProjectFiledropPath.Get(), filedrop.FileName)) 275 if err != nil { 276 logrus.Error(err) 277 return c.NoContent(http.StatusInternalServerError) 278 } 279 defer f.Close() 280 281 init := c.Request().PostFormValue("init") 282 if init != "" { 283 fs, err := f.Stat() 284 if err != nil { 285 logrus.Error(err.Error()) 286 return c.NoContent(http.StatusInternalServerError) 287 } 288 289 // Calculate sha256 of file 290 h := sha256.New() 291 if _, err := io.Copy(h, f); err != nil { 292 logrus.Error(err.Error()) 293 return c.NoContent(http.StatusInternalServerError) 294 } 295 fileSha256 := hex.EncodeToString(h.Sum(nil)) 296 297 fileSize := fs.Size() 298 nbChunks := int64(math.Ceil(float64(fileSize) / float64(maxChunkSize))) 299 b64Password := base64.StdEncoding.EncodeToString([]byte(filedrop.Password)) 300 b64IV := base64.StdEncoding.EncodeToString(filedrop.IV) 301 body := fmt.Sprintf("%s\n%s\n%s\n%s\n%d\n%d\n", filedrop.OrigFileName, b64Password, b64IV, fileSha256, fileSize, nbChunks) 302 return c.String(http.StatusOK, body) 303 } 304 305 chunkNum := utils.DoParseInt64(c.Request().PostFormValue("chunk")) 306 307 buf := make([]byte, maxChunkSize) 308 n, err := f.ReadAt(buf, chunkNum*maxChunkSize) 309 if err != nil { 310 if !errors.Is(err, io.EOF) { 311 logrus.Error(err) 312 return c.NoContent(http.StatusInternalServerError) 313 } 314 } 315 316 c.Response().Header().Set(echo.HeaderContentDisposition, fmt.Sprintf("attachment; filename=chunk_%d", chunkNum)) 317 if _, err := io.Copy(c.Response().Writer, bytes.NewReader(buf[:n])); err != nil { 318 logrus.Error(err) 319 return c.NoContent(http.StatusInternalServerError) 320 } 321 c.Response().Flush() 322 return nil 323 } 324 325 func FileDropDownloadHandler(c echo.Context) error { 326 authUser, ok := c.Get("authUser").(*database.User) 327 db := c.Get("database").(*database.DkfDB) 328 if !ok { 329 return c.Redirect(http.StatusFound, "/") 330 } 331 332 fileName := c.Param("fileName") 333 if !utils.FileExists(filepath.Join(config.Global.ProjectDownloadsPath.Get(), fileName)) { 334 logrus.Error(fileName + " does not exists") 335 return c.Redirect(http.StatusFound, "/") 336 } 337 338 userNbDownloaded := db.UserNbDownloaded(authUser.ID, fileName) 339 340 // Display captcha to new users, or old users if they already downloaded the file. 341 if !authUser.AccountOldEnough() || userNbDownloaded >= 1 { 342 // Captcha for bigger files 343 var data captchaRequiredData 344 data.CaptchaDescription = "Captcha required" 345 if !authUser.AccountOldEnough() { 346 data.CaptchaDescription = fmt.Sprintf("Account that are less than 3 days old must complete the captcha to download files bigger than %s", humanize.Bytes(config.MaxFileSizeBeforeDownload)) 347 } else if userNbDownloaded >= 1 { 348 data.CaptchaDescription = fmt.Sprintf("For the second download onward of a file bigger than %s, you must complete the captcha", humanize.Bytes(config.MaxFileSizeBeforeDownload)) 349 } 350 data.CaptchaID, data.CaptchaImg = captcha.New() 351 const captchaRequiredTmpl = "captcha-required" 352 if c.Request().Method == http.MethodGet { 353 return c.Render(http.StatusOK, captchaRequiredTmpl, data) 354 } 355 captchaID := c.Request().PostFormValue("captcha_id") 356 captchaInput := c.Request().PostFormValue("captcha") 357 if err := hutils.CaptchaVerifyString(c, captchaID, captchaInput); err != nil { 358 data.ErrCaptcha = err.Error() 359 return c.Render(http.StatusOK, captchaRequiredTmpl, data) 360 } 361 } 362 363 // Keep track of user downloads 364 if _, err := db.CreateDownload(authUser.ID, fileName); err != nil { 365 logrus.Error(err) 366 } 367 368 f, err := os.Open(filepath.Join(config.Global.ProjectDownloadsPath.Get(), fileName)) 369 if err != nil { 370 return c.Redirect(http.StatusFound, "/") 371 } 372 fi, err := f.Stat() 373 if err != nil { 374 return c.Redirect(http.StatusFound, "/") 375 } 376 377 c.Response().Header().Set(echo.HeaderContentDisposition, fmt.Sprintf("%s; filename=%q", "attachment", fileName)) 378 http.ServeContent(c.Response(), c.Request(), fi.Name(), fi.ModTime(), f) 379 return nil 380 }