dkforest

A forum and chat platform (onion)
git clone https://git.dasho.dev/n0tr1v/dkforest.git
Log | Files | Refs | LICENSE

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 }