uploads.go (5450B)
1 package handlers 2 3 import ( 4 "bytes" 5 "dkforest/pkg/captcha" 6 "dkforest/pkg/config" 7 "dkforest/pkg/database" 8 hutils "dkforest/pkg/web/handlers/utils" 9 "fmt" 10 "github.com/dustin/go-humanize" 11 "github.com/labstack/echo" 12 "github.com/sirupsen/logrus" 13 "io" 14 "net/http" 15 "unicode/utf8" 16 ) 17 18 func isImageMimeType(mimeType string) bool { 19 return mimeType == "image/jpeg" || 20 mimeType == "image/png" || 21 mimeType == "image/gif" || 22 mimeType == "image/bmp" || 23 mimeType == "image/x-icon" || 24 mimeType == "image/webp" 25 } 26 27 func isAttachmentMimeType(mimeType string) bool { 28 return mimeType == "application/x-gzip" || 29 mimeType == "application/zip" || 30 mimeType == "application/x-rar-compressed" || 31 mimeType == "application/pdf" || 32 mimeType == "audio/basic" || 33 mimeType == "audio/aiff" || 34 mimeType == "audio/mpeg" || 35 mimeType == "application/ogg" || 36 mimeType == "audio/midi" || 37 mimeType == "video/avi" || 38 mimeType == "audio/wave" || 39 mimeType == "video/webm" || 40 mimeType == "font/ttf" || 41 mimeType == "font/otf" || 42 mimeType == "font/collection" || 43 mimeType == "font/woff" || 44 mimeType == "font/woff2" || 45 mimeType == "application/wasm" || 46 mimeType == "application/postscript" || 47 mimeType == "application/vnd.ms-fontobject" || 48 mimeType == "application/octet-stream" 49 } 50 51 func UploadsDownloadHandler(c echo.Context) error { 52 authUser := c.Get("authUser").(*database.User) 53 db := c.Get("database").(*database.DkfDB) 54 filename := c.Param("filename") 55 file, err := db.GetUploadByFileName(filename) 56 if err != nil { 57 return c.Render(http.StatusOK, "standalone.upload404", nil) 58 } 59 if !file.Exists() { 60 logrus.Error(filename + " does not exists") 61 return c.Render(http.StatusOK, "standalone.upload404", nil) 62 } 63 64 if file.FileSize < config.MaxFileSizeBeforeDownload { 65 fi, decFileBytes, err := file.GetContent() 66 if err != nil || fi.IsDir() { 67 return c.Render(http.StatusOK, "standalone.upload404", nil) 68 } 69 buf := bytes.NewReader(decFileBytes) 70 71 // Validate image type and determine extension 72 mimeType, err := getFileContentType(buf) 73 _, err = buf.Seek(0, io.SeekStart) 74 if err != nil { 75 return c.Render(http.StatusOK, "standalone.upload404", nil) 76 } 77 78 // Serve images 79 if isImageMimeType(mimeType) { 80 http.ServeContent(c.Response(), c.Request(), fi.Name(), fi.ModTime(), buf) 81 return nil 82 } 83 84 if mimeType == "application/octet-stream" && utf8.Valid(decFileBytes) { 85 mimeType = "text/plain; charset=utf-8" 86 } 87 88 // MimeType that always trigger a file "download" 89 if isAttachmentMimeType(mimeType) { 90 // Keep track of user downloads 91 if _, err := db.CreateDownload(authUser.ID, filename); err != nil { 92 logrus.Error(err) 93 } 94 c.Response().Header().Set(echo.HeaderContentDisposition, fmt.Sprintf("%s; filename=%q", "attachment", file.OrigFileName)) 95 http.ServeContent(c.Response(), c.Request(), fi.Name(), fi.ModTime(), buf) 96 return nil 97 } 98 99 // Serve any other file as text/plain 100 c.Response().Header().Set(echo.HeaderContentType, "text/plain") 101 c.Response().Header().Set(echo.HeaderContentDisposition, fmt.Sprintf("filename=%q", file.OrigFileName)) 102 http.ServeContent(c.Response(), c.Request(), fi.Name(), fi.ModTime(), buf) 103 return nil 104 } 105 106 userNbDownloaded := db.UserNbDownloaded(authUser.ID, filename) 107 108 // Display captcha to new users, or old users if they already downloaded the file. 109 if !authUser.AccountOldEnough() || userNbDownloaded >= 1 { 110 // Captcha for bigger files 111 var data captchaRequiredData 112 data.CaptchaDescription = "Captcha required" 113 if !authUser.AccountOldEnough() { 114 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)) 115 } else if userNbDownloaded >= 1 { 116 data.CaptchaDescription = fmt.Sprintf("For the second download onward of a file bigger than %s, you must complete the captcha", humanize.Bytes(config.MaxFileSizeBeforeDownload)) 117 } 118 data.CaptchaID, data.CaptchaImg = captcha.New() 119 const captchaRequiredTmpl = "captcha-required" 120 if c.Request().Method == http.MethodGet { 121 return c.Render(http.StatusOK, captchaRequiredTmpl, data) 122 } 123 captchaID := c.Request().PostFormValue("captcha_id") 124 captchaInput := c.Request().PostFormValue("captcha") 125 if err := hutils.CaptchaVerifyString(c, captchaID, captchaInput); err != nil { 126 data.ErrCaptcha = err.Error() 127 return c.Render(http.StatusOK, captchaRequiredTmpl, data) 128 } 129 } 130 131 // Keep track of user downloads 132 if _, err := db.CreateDownload(authUser.ID, filename); err != nil { 133 logrus.Error(err) 134 } 135 136 fi, decFileBytes, err := file.GetContent() 137 if err != nil { 138 return c.Render(http.StatusOK, "standalone.upload404", nil) 139 } 140 buf := bytes.NewReader(decFileBytes) 141 142 c.Response().Header().Set(echo.HeaderContentDisposition, fmt.Sprintf("%s; filename=%q", "attachment", file.OrigFileName)) 143 http.ServeContent(c.Response(), c.Request(), fi.Name(), fi.ModTime(), buf) 144 return nil 145 } 146 147 func getFileContentType(out io.ReadSeeker) (string, error) { 148 149 // Only the first 512 bytes are used to sniff the content type. 150 buffer := make([]byte, 512) 151 152 _, err := out.Read(buffer) 153 if err != nil { 154 return "", err 155 } 156 157 // Use the net/http package's handy DectectContentType function. Always returns a valid 158 // content-type by returning "application/octet-stream" if no others seemed to match. 159 contentType := http.DetectContentType(buffer) 160 161 return contentType, nil 162 }