dkforest

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

handlers.go (23549B)


      1 package handlers
      2 
      3 import (
      4 	"bytes"
      5 	"dkforest/pkg/cache"
      6 	"dkforest/pkg/captcha"
      7 	"dkforest/pkg/config"
      8 	"dkforest/pkg/database"
      9 	dutils "dkforest/pkg/database/utils"
     10 	"dkforest/pkg/odometer"
     11 	"dkforest/pkg/utils"
     12 	hutils "dkforest/pkg/web/handlers/utils"
     13 	"encoding/base64"
     14 	"fmt"
     15 	humanize "github.com/dustin/go-humanize"
     16 	"github.com/labstack/echo"
     17 	"github.com/pquerna/otp"
     18 	"github.com/pquerna/otp/totp"
     19 	"github.com/sirupsen/logrus"
     20 	"golang.org/x/crypto/bcrypt"
     21 	_ "golang.org/x/image/bmp"
     22 	_ "golang.org/x/image/webp"
     23 	"image"
     24 	_ "image/gif"
     25 	"image/png"
     26 	"net/http"
     27 	"net/url"
     28 	"os"
     29 	"path/filepath"
     30 	"regexp"
     31 	"strings"
     32 	"time"
     33 )
     34 
     35 var tempLoginCache = cache.New[TempLoginCaptcha](3*time.Minute, 3*time.Minute)
     36 var tempLoginStore = captcha.NewMemoryStore(captcha.CollectNum, 3*time.Minute)
     37 
     38 type TempLoginCaptcha struct {
     39 	ID         string
     40 	Img        string
     41 	ValidUntil time.Time
     42 }
     43 
     44 // HomeHandler ...
     45 func HomeHandler(c echo.Context) error {
     46 	if config.IsFirstUse.IsTrue() {
     47 		return firstUseHandler(c)
     48 	}
     49 
     50 	// If we're logged in, render the home page
     51 	user := c.Get("authUser").(*database.User)
     52 	if user != nil {
     53 		return c.Render(http.StatusOK, "home", nil)
     54 	}
     55 
     56 	// If we protect the home page, render the special login page with time based captcha for login URL discovery
     57 	if config.ProtectHome.IsTrue() {
     58 		// return waitPageWrapper(c, protectHomeHandler, hutils.WaitCookieName)
     59 		return protectHomeHandler(c)
     60 	}
     61 
     62 	// Otherwise, render the normal login page
     63 	return loginHandler(c)
     64 }
     65 
     66 func createSessionCookie(value string, sessionDuration time.Duration) *http.Cookie {
     67 	return hutils.CreateCookie(hutils.AuthCookieName, value, int64(sessionDuration.Seconds()))
     68 }
     69 
     70 // FlashResponse ...
     71 type FlashResponse struct {
     72 	Message  string
     73 	Redirect string
     74 	Type     string
     75 }
     76 
     77 func AesNB64(in string) string {
     78 	encryptedVal, _ := utils.EncryptAESMaster([]byte(in))
     79 	return base64.URLEncoding.EncodeToString(encryptedVal)
     80 }
     81 
     82 func DAesB64(in string) ([]byte, error) {
     83 	enc, err := base64.URLEncoding.DecodeString(in)
     84 	if err != nil {
     85 		return nil, err
     86 	}
     87 	encryptedVal, err := utils.DecryptAESMaster(enc)
     88 	if err != nil {
     89 		return nil, err
     90 	}
     91 	return encryptedVal, nil
     92 }
     93 
     94 func DAesB64Str(in string) (string, error) {
     95 	encryptedVal, err := DAesB64(in)
     96 	return string(encryptedVal), err
     97 }
     98 
     99 type WaitPageCookiePayload struct {
    100 	Token string
    101 	Count int64
    102 	Now   int64
    103 	Unix  int64
    104 }
    105 
    106 func waitPageWrapper(c echo.Context, clb echo.HandlerFunc, cookieName string) error {
    107 	now := time.Now()
    108 	start := now.UnixNano()
    109 	var waitToken string
    110 
    111 	if cc, payload, err := hutils.EncCookie[WaitPageCookiePayload](c, cookieName); err != nil {
    112 		// No cookie found, we create one and display the waiting page.
    113 		waitTime := getWaitPageDuration()
    114 		waitToken = utils.GenerateToken10()
    115 		payload := WaitPageCookiePayload{
    116 			Token: waitToken,
    117 			Count: 1,
    118 			Now:   now.UnixMilli(),
    119 			Unix:  now.Unix() + waitTime - 1, // unix time at which the wait time is over
    120 		}
    121 		c.SetCookie(hutils.CreateEncCookie(cookieName, payload, utils.OneMinuteSecs*5))
    122 
    123 		var data waitData
    124 		// Generate css frames
    125 		data.Frames = generateCssFrames(waitTime, nil, true)
    126 		data.WaitTime = waitTime
    127 		data.WaitToken = waitToken
    128 		return c.Render(http.StatusOK, "standalone.wait", data)
    129 
    130 	} else {
    131 		// Cookie was found, incr counter then call callback
    132 		waitToken = payload.Token
    133 		start = payload.Now
    134 		if c.Request().Method == http.MethodGet {
    135 			// If you reload the page before the wait time is over, we kill the circuit.
    136 			if now.Unix() < payload.Unix {
    137 				hutils.KillCircuit(c)
    138 				return c.String(http.StatusFound, "DDoS filter killed your path")
    139 			}
    140 
    141 			// If the wait time is over, and you reload the protected page more than 4 times, we make you wait 1min
    142 			if payload.Count >= 4 {
    143 				c.SetCookie(hutils.CreateCookie(cookieName, cc.Value, utils.OneMinuteSecs))
    144 				return c.String(http.StatusFound, "You tried to reload the page too many times. Now you have to wait one minute.")
    145 			}
    146 			payload.Count++
    147 			payload.Now = now.UnixMilli()
    148 			c.SetCookie(hutils.CreateEncCookie(cookieName, payload, utils.OneMinuteSecs*5))
    149 		}
    150 	}
    151 	c.Set("start", start)
    152 	c.Set("signupToken", waitToken)
    153 	return clb(c)
    154 }
    155 
    156 // RecaptchaResponse ...
    157 type RecaptchaResponse struct {
    158 	Success     bool      `json:"success"`
    159 	ChallengeTS time.Time `json:"challenge_ts"`
    160 	Hostname    string    `json:"hostname"`
    161 	ErrorCodes  []string  `json:"error-codes"`
    162 }
    163 
    164 // n: how many frames to generate.
    165 // contentFn: callback to alter the content of the frames
    166 // reverse: if true, will generate the frames like so: 5 4 3 2 1 0
    167 func generateCssFrames(n int64, contentFn func(int64) string, reverse bool) (frames []string) {
    168 	step := 100.0 / float64(n)
    169 	pct := 0.0
    170 	for i := int64(0); i <= n; i++ {
    171 		num := i
    172 		if reverse {
    173 			num = n - i
    174 		}
    175 		if contentFn == nil {
    176 			contentFn = utils.FormatInt64
    177 		}
    178 		frames = append(frames, fmt.Sprintf(`%.2f%% { content: "%s"; }`, pct, contentFn(num)))
    179 		pct += step
    180 	}
    181 	return
    182 }
    183 
    184 func MemeHandler(c echo.Context) error {
    185 	slug := c.Param("slug")
    186 	db := c.Get("database").(*database.DkfDB)
    187 	meme, err := db.GetMemeBySlug(slug)
    188 	if err != nil {
    189 		return c.Redirect(http.StatusFound, "/")
    190 	}
    191 
    192 	fi, by, err := meme.GetContent()
    193 	if err != nil {
    194 		return c.Redirect(http.StatusFound, "/")
    195 	}
    196 	buf := bytes.NewReader(by)
    197 
    198 	http.ServeContent(c.Response(), c.Request(), fi.Name(), fi.ModTime(), buf)
    199 	return nil
    200 }
    201 
    202 func NewsHandler(c echo.Context) error {
    203 	db := c.Get("database").(*database.DkfDB)
    204 	var data newsData
    205 	category, _ := db.GetForumCategoryBySlug("news")
    206 	data.News, _ = db.GetForumNews(category.ID)
    207 	return c.Render(http.StatusOK, "news", data)
    208 }
    209 
    210 func BhcliHandler(c echo.Context) error {
    211 	return c.Render(http.StatusOK, "bhcli", nil)
    212 }
    213 
    214 func TorchessHandler(c echo.Context) error {
    215 	return c.Render(http.StatusOK, "torchess", nil)
    216 }
    217 
    218 func PowHelpHandler(c echo.Context) error {
    219 	var data powHelperData
    220 	data.Difficulty = config.PowDifficulty
    221 	return c.Render(http.StatusOK, "pow-help", data)
    222 }
    223 
    224 func CaptchaHelpHandler(c echo.Context) error {
    225 	return c.Render(http.StatusOK, "captcha-help", nil)
    226 }
    227 
    228 func WerewolfHandler(c echo.Context) error {
    229 	return c.Render(http.StatusOK, "werewolf", nil)
    230 }
    231 
    232 func RoomsHandler(c echo.Context) error {
    233 	authUser := c.Get("authUser").(*database.User)
    234 	db := c.Get("database").(*database.DkfDB)
    235 	var data roomsData
    236 	data.Rooms, _ = db.GetListedChatRooms(authUser.ID)
    237 	return c.Render(http.StatusOK, "rooms", data)
    238 }
    239 
    240 func getTutorialStepDuration() int64 {
    241 	secs := int64(15)
    242 	if config.Development.IsTrue() {
    243 		secs = 1
    244 	}
    245 	return secs
    246 }
    247 
    248 func getWaitPageDuration() int64 {
    249 	secs := utils.RandI64(5, 15)
    250 	if config.Development.IsTrue() {
    251 		secs = 2
    252 	}
    253 	return secs
    254 }
    255 
    256 func ExternalLink1Handler(c echo.Context) error {
    257 	original, _ := url.PathUnescape(c.Param("original"))
    258 	var data externalLink1Data
    259 	data.Link = original
    260 	return c.Render(http.StatusOK, "external-link1", data)
    261 }
    262 
    263 func ExternalLinkHandler(c echo.Context) error {
    264 	service := c.Param("service")
    265 	original, _ := url.PathUnescape(c.Param("original"))
    266 	baseURL := "/"
    267 	if service == "invidious" {
    268 		baseURL = utils.RandChoice(dutils.InvidiousURLs)
    269 	} else if service == "libreddit" {
    270 		baseURL = utils.RandChoice(dutils.LibredditURLs)
    271 	} else if service == "wikiless" {
    272 		baseURL = utils.RandChoice(dutils.WikilessURLs)
    273 	} else if service == "nitter" {
    274 		baseURL = utils.RandChoice(dutils.NitterURLs)
    275 	} else if service == "rimgo" {
    276 		baseURL = utils.RandChoice(dutils.RimgoURLs)
    277 	} else {
    278 		return c.String(http.StatusNotFound, "Not found")
    279 	}
    280 	return c.Redirect(http.StatusFound, baseURL+"/"+original)
    281 }
    282 
    283 func DonateHandler(c echo.Context) error {
    284 	return c.Render(http.StatusOK, "donate", nil)
    285 }
    286 
    287 func ShopHandler(c echo.Context) error {
    288 	getImgStr := func(img image.Image) string {
    289 		buf := bytes.NewBuffer([]byte(""))
    290 		_ = png.Encode(buf, img)
    291 		return base64.StdEncoding.EncodeToString(buf.Bytes())
    292 	}
    293 	authUser := c.Get("authUser").(*database.User)
    294 	db := c.Get("database").(*database.DkfDB)
    295 	var data shopData
    296 	invoice, err := db.CreateXmrInvoice(authUser.ID, 1)
    297 	if err != nil {
    298 		logrus.Error(err)
    299 	}
    300 	b, _ := invoice.GetImage()
    301 	data.Img = getImgStr(b)
    302 	data.Invoice = invoice
    303 
    304 	return c.Render(http.StatusOK, "shop", data)
    305 }
    306 
    307 type ValueTokenCache struct {
    308 	Value string // Either age/pgp token or msg to sign
    309 	PKey  string // age/pgp public key
    310 }
    311 
    312 var ageTokenCache = cache.NewWithKey[database.UserID, ValueTokenCache](2*time.Minute, time.Hour)
    313 var pgpTokenCache = cache.NewWithKey[database.UserID, ValueTokenCache](2*time.Minute, time.Hour)
    314 
    315 func generateTokenMsg(token string) string {
    316 	msg := "The required code is below the line.\n"
    317 	msg += "----------------------------------------------------------------------------------\n"
    318 	msg += token + "\n"
    319 	return msg
    320 }
    321 
    322 func generatePgpEncryptedTokenMessage(userID database.UserID, pkey string) (string, error) {
    323 	token := utils.GenerateToken10()
    324 	pgpTokenCache.SetD(userID, ValueTokenCache{Value: token, PKey: pkey})
    325 	msg := generateTokenMsg(token)
    326 	return utils.GeneratePgpEncryptedMessage(pkey, msg)
    327 }
    328 
    329 func generatePgpToBeSignedTokenMessage(userID database.UserID, pkey string) string {
    330 	token := utils.GenerateToken10()
    331 	msg := fmt.Sprintf("dkf_%s{%s}", time.Now().UTC().Format("2006.01.02"), token)
    332 	pgpTokenCache.SetD(userID, ValueTokenCache{Value: msg, PKey: pkey})
    333 	return msg
    334 }
    335 
    336 // twoFactorCache ...
    337 var twoFactorCache = cache.NewWithKey[database.UserID, twoFactorObj](10*time.Minute, time.Hour)
    338 
    339 type twoFactorObj struct {
    340 	key      *otp.Key
    341 	recovery string
    342 }
    343 
    344 func GpgTwoFactorAuthenticationToggleHandler(c echo.Context) error {
    345 	authUser := c.Get("authUser").(*database.User)
    346 	db := c.Get("database").(*database.DkfDB)
    347 
    348 	var data gpgTwoFactorAuthenticationVerifyData
    349 	data.IsEnabled = authUser.GpgTwoFactorEnabled
    350 	data.GpgTwoFactorMode = authUser.GpgTwoFactorMode
    351 
    352 	if c.Request().Method == http.MethodGet {
    353 		return c.Render(http.StatusOK, "two-factor-authentication-gpg", data)
    354 	}
    355 
    356 	password := c.Request().PostFormValue("password")
    357 	if !authUser.CheckPassword(db, password) {
    358 		data.ErrorPassword = "Invalid password"
    359 		return c.Render(http.StatusOK, "two-factor-authentication-gpg", data)
    360 	}
    361 
    362 	// Disable
    363 	if authUser.GpgTwoFactorEnabled {
    364 		authUser.DisableGpg2FA(db)
    365 		db.CreateSecurityLog(authUser.ID, database.Gpg2faDisabledSecurityLog)
    366 		return c.Render(http.StatusOK, "flash", FlashResponse{"GPG Two-factor authentication disabled", "/settings/account", "alert-success"})
    367 	}
    368 
    369 	// Enable
    370 	if authUser.GPGPublicKey == "" {
    371 		return c.Render(http.StatusOK, "flash", FlashResponse{"You need to setup your PGP key first", "/settings/pgp", "alert-danger"})
    372 	}
    373 	// Delete active user sessions
    374 	if err := db.DeleteUserSessions(authUser.ID); err != nil {
    375 		logrus.Error(err)
    376 	}
    377 	c.SetCookie(hutils.DeleteCookie(hutils.AuthCookieName))
    378 	authUser.GpgTwoFactorEnabled = true
    379 	authUser.GpgTwoFactorMode = utils.DoParseBool(c.Request().PostFormValue("gpg_two_factor_mode"))
    380 	authUser.DoSave(db)
    381 	db.CreateSecurityLog(authUser.ID, database.Gpg2faEnabledSecurityLog)
    382 	return c.Render(http.StatusOK, "flash", FlashResponse{"GPG Two-factor authentication enabled", "/settings/account", "alert-success"})
    383 }
    384 
    385 // TwoFactorAuthenticationVerifyHandler ...
    386 func TwoFactorAuthenticationVerifyHandler(c echo.Context) error {
    387 	getImgStr := func(img image.Image) string {
    388 		buf := bytes.NewBuffer([]byte(""))
    389 		_ = png.Encode(buf, img)
    390 		return base64.StdEncoding.EncodeToString(buf.Bytes())
    391 	}
    392 	authUser := c.Get("authUser").(*database.User)
    393 	db := c.Get("database").(*database.DkfDB)
    394 	if authUser.HasTotpEnabled() {
    395 		return c.Redirect(http.StatusFound, "/settings/account")
    396 	}
    397 	var data twoFactorAuthenticationVerifyData
    398 	if c.Request().Method == http.MethodPost {
    399 		twoFactor, found := twoFactorCache.Get(authUser.ID)
    400 		if !found {
    401 			return c.Redirect(http.StatusFound, "/two-factor-authentication/verify")
    402 		}
    403 		password := c.Request().PostFormValue("password")
    404 		if !authUser.CheckPassword(db, password) {
    405 			img, _ := twoFactor.key.Image(150, 150)
    406 			data.QRCode = getImgStr(img)
    407 			data.Secret = twoFactor.key.Secret()
    408 			data.RecoveryCode = twoFactor.recovery
    409 			data.ErrorPassword = "Invalid password"
    410 			return c.Render(http.StatusOK, "two-factor-authentication-verify", data)
    411 		}
    412 		code := c.Request().PostFormValue("code")
    413 		if !totp.Validate(code, twoFactor.key.Secret()) {
    414 			img, _ := twoFactor.key.Image(150, 150)
    415 			data.QRCode = getImgStr(img)
    416 			data.Secret = twoFactor.key.Secret()
    417 			data.RecoveryCode = twoFactor.recovery
    418 			data.Password = password
    419 			data.Error = "Two-factor code verification failed. Please try again."
    420 			return c.Render(http.StatusOK, "two-factor-authentication-verify", data)
    421 		}
    422 		h, err := bcrypt.GenerateFromPassword([]byte(twoFactor.recovery), 12)
    423 		if err != nil {
    424 			data.Error = "unable to hash recovery code: " + err.Error()
    425 			return c.Render(http.StatusOK, "two-factor-authentication-verify", data)
    426 		}
    427 		// Delete active user sessions
    428 		if err := db.DeleteUserSessions(authUser.ID); err != nil {
    429 			logrus.Error(err)
    430 		}
    431 		c.SetCookie(hutils.DeleteCookie(hutils.AuthCookieName))
    432 		authUser.TwoFactorSecret = database.EncryptedString(twoFactor.key.Secret())
    433 		authUser.TwoFactorRecovery = string(h)
    434 		authUser.DoSave(db)
    435 		db.CreateSecurityLog(authUser.ID, database.TotpEnabledSecurityLog)
    436 		return c.Render(http.StatusOK, "flash", FlashResponse{"Two-factor authentication enabled", "/", "alert-success"})
    437 	}
    438 	key, _ := totp.Generate(totp.GenerateOpts{Issuer: "DarkForest", AccountName: string(authUser.Username)})
    439 	img, _ := key.Image(150, 150)
    440 	recovery := utils.ShortDisplayID(10)
    441 	data.QRCode = getImgStr(img)
    442 	data.Secret = key.Secret()
    443 	data.RecoveryCode = recovery
    444 	twoFactorCache.SetD(authUser.ID, twoFactorObj{key, recovery})
    445 	return c.Render(http.StatusOK, "two-factor-authentication-verify", data)
    446 }
    447 
    448 // TwoFactorAuthenticationDisableHandler ...
    449 func TwoFactorAuthenticationDisableHandler(c echo.Context) error {
    450 	var data diableTotpData
    451 	if c.Request().Method == http.MethodGet {
    452 		return c.Render(http.StatusOK, "disable-totp", data)
    453 	}
    454 	authUser := c.Get("authUser").(*database.User)
    455 	db := c.Get("database").(*database.DkfDB)
    456 	password := c.Request().PostFormValue("password")
    457 	if !authUser.CheckPassword(db, password) {
    458 		data.ErrorPassword = "Invalid password"
    459 		return c.Render(http.StatusOK, "disable-totp", data)
    460 	}
    461 	authUser.DisableTotp2FA(db)
    462 	db.CreateSecurityLog(authUser.ID, database.TotpDisabledSecurityLog)
    463 	return c.Render(http.StatusOK, "flash", FlashResponse{"Two-factor authentication disabled", "/settings/account", "alert-success"})
    464 }
    465 
    466 type downloadableFileInfo struct {
    467 	Name     string
    468 	OS       string
    469 	Arch     string
    470 	Bytes    string
    471 	Checksum string
    472 }
    473 
    474 func getDownloadsBhcliFiles() (out []downloadableFileInfo) {
    475 	return getDownloadableFiles("downloads-bhcli", `bhcli`)
    476 }
    477 
    478 func getDownloadsTorchessFiles() (out []downloadableFileInfo) {
    479 	return getTorchessDownloadableFiles("downloads-torchess", `torchess`)
    480 }
    481 
    482 func getDownloadsFiles() (out []downloadableFileInfo) {
    483 	return getDownloadableFiles("downloads", `ransomware-re-challenge1`)
    484 }
    485 
    486 func distStrToFriendlyStr(os, arch string) (string, string) {
    487 	switch os {
    488 	case "darwin":
    489 		os = "macOS"
    490 	}
    491 	switch arch {
    492 	case "386":
    493 		arch = "x86"
    494 	case "amd64":
    495 		arch = "x86-64"
    496 	case "arm":
    497 		arch = "ARMv7"
    498 	}
    499 	return os, arch
    500 }
    501 
    502 func getDownloadableFiles(folder, fileNamePrefix string) (out []downloadableFileInfo) {
    503 	err := filepath.Walk(folder, func(path string, info os.FileInfo, err error) error {
    504 		if info == nil {
    505 			return nil
    506 		}
    507 		if !strings.HasSuffix(info.Name(), ".checksum") {
    508 			checksumBytes, err := os.ReadFile(path + ".checksum")
    509 			if err != nil {
    510 				return nil
    511 			}
    512 			m := regexp.MustCompile(fileNamePrefix + `\.(\w+)\.(\w+)(\.exe)?`).FindStringSubmatch(info.Name())
    513 			if len(m) < 2 {
    514 				return nil
    515 			}
    516 			osIdx := 1
    517 			archIdx := 2
    518 			osStr, archFmt := distStrToFriendlyStr(m[osIdx], m[archIdx])
    519 			out = append(out, downloadableFileInfo{
    520 				info.Name(),
    521 				osStr,
    522 				archFmt,
    523 				humanize.Bytes(uint64(info.Size())),
    524 				string(checksumBytes),
    525 			})
    526 		}
    527 		return nil
    528 	})
    529 	if err != nil {
    530 		logrus.Error(err)
    531 	}
    532 	return
    533 }
    534 
    535 func getTorchessDownloadableFiles(folder, fileNamePrefix string) (out []downloadableFileInfo) {
    536 	err := filepath.Walk(folder, func(path string, info os.FileInfo, err error) error {
    537 		if info == nil {
    538 			return nil
    539 		}
    540 		if !strings.HasSuffix(info.Name(), ".checksum") {
    541 			checksumBytes, err := os.ReadFile(path + ".checksum")
    542 			if err != nil {
    543 				return nil
    544 			}
    545 			m := regexp.MustCompile(fileNamePrefix + `\.\d+\.\d+\.\d+\.(\w+)\.(\w+)(\.exe)?`).FindStringSubmatch(info.Name())
    546 			if len(m) < 2 {
    547 				return nil
    548 			}
    549 			osIdx := 1
    550 			archIdx := 2
    551 			osStr, archFmt := distStrToFriendlyStr(m[osIdx], m[archIdx])
    552 			out = append(out, downloadableFileInfo{
    553 				info.Name(),
    554 				osStr,
    555 				archFmt,
    556 				humanize.Bytes(uint64(info.Size())),
    557 				string(checksumBytes),
    558 			})
    559 		}
    560 		return nil
    561 	})
    562 	if err != nil {
    563 		logrus.Error(err)
    564 	}
    565 	return
    566 }
    567 
    568 // BhcliDownloadsHandler ...
    569 func BhcliDownloadsHandler(c echo.Context) error {
    570 	var data bhcliDownloadsHandlerData
    571 	data.Files = getDownloadsBhcliFiles()
    572 	return c.Render(http.StatusOK, "bhcli-downloads", data)
    573 }
    574 
    575 func TorchessDownloadsHandler(c echo.Context) error {
    576 	var data bhcliDownloadsHandlerData
    577 	data.Files = getDownloadsTorchessFiles()
    578 	return c.Render(http.StatusOK, "torchess-downloads", data)
    579 }
    580 
    581 func downloadFile(c echo.Context, folder, redirect string) error {
    582 	if config.DownloadsEnabled.IsFalse() {
    583 		return c.Render(http.StatusOK, "flash", FlashResponse{Message: "Downloads are temporarily disabled", Redirect: "/", Type: "alert-danger"})
    584 	}
    585 
    586 	authUser := c.Get("authUser").(*database.User)
    587 	db := c.Get("database").(*database.DkfDB)
    588 	if authUser == nil {
    589 		return c.Redirect(http.StatusFound, "/login?redirect="+redirect)
    590 	}
    591 
    592 	filename := c.Param("filename")
    593 
    594 	if !utils.FileExists(filepath.Join(folder, filename)) {
    595 		logrus.Error(filename + " does not exists")
    596 		return c.Redirect(http.StatusFound, redirect)
    597 	}
    598 
    599 	// Keep track of user downloads
    600 	if _, err := db.CreateDownload(authUser.ID, filename); err != nil {
    601 		logrus.Error(err)
    602 	}
    603 
    604 	return c.Attachment(filepath.Join(folder, filename), filename)
    605 }
    606 
    607 func TorChessDownloadFileHandler(c echo.Context) error {
    608 	return downloadFile(c, "downloads-torchess", "/torchess/downloads")
    609 }
    610 
    611 func BhcliDownloadFileHandler(c echo.Context) error {
    612 	return downloadFile(c, "downloads-bhcli", "/bhcli/downloads")
    613 }
    614 
    615 func CaptchaRequiredHandler(c echo.Context) error {
    616 	authUser := c.Get("authUser").(*database.User)
    617 	db := c.Get("database").(*database.DkfDB)
    618 
    619 	var data captchaRequiredData
    620 	data.CaptchaDescription = "Captcha required"
    621 	data.CaptchaID, data.CaptchaImg = captcha.New()
    622 	config.CaptchaRequiredGenerated.Inc()
    623 
    624 	const captchaRequiredTmpl = "captcha-required"
    625 	if c.Request().Method == http.MethodGet {
    626 		return c.Render(http.StatusOK, captchaRequiredTmpl, data)
    627 	}
    628 
    629 	captchaID := c.Request().PostFormValue("captcha_id")
    630 	captchaInput := c.Request().PostFormValue("captcha")
    631 	if err := hutils.CaptchaVerifyString(c, captchaID, captchaInput); err != nil {
    632 		data.ErrCaptcha = err.Error()
    633 		config.CaptchaRequiredFailed.Inc()
    634 		return c.Render(http.StatusOK, captchaRequiredTmpl, data)
    635 	}
    636 	config.CaptchaRequiredSuccess.Inc()
    637 	authUser.SetCaptchaRequired(db, false)
    638 	return c.Redirect(http.StatusFound, "/chat")
    639 }
    640 
    641 func OdometerHandler(c echo.Context) error {
    642 	var data odometerData
    643 	data.Odometer = odometer.New("12345")
    644 	return c.Render(http.StatusOK, "odometer", data)
    645 }
    646 
    647 func CaptchaHandler(c echo.Context) error {
    648 	var data captchaData
    649 	if c.QueryParam("a") != "" {
    650 		data.ShowAnswer = true
    651 	}
    652 	setCaptcha := func(seed int64) {
    653 		data.CaptchaID, data.Answer, data.CaptchaImg, data.CaptchaAnswerImg = captcha.NewWithSolution(seed)
    654 		if !data.ShowAnswer {
    655 			data.CaptchaAnswerImg = ""
    656 		}
    657 	}
    658 	data.Seed = time.Now().UnixNano()
    659 	data.Ts = time.Now().UnixMilli()
    660 	//fmt.Println("Seed:", seed)
    661 
    662 	data.CaptchaSec = 120
    663 	data.Frames = generateCssFrames(data.CaptchaSec, func(i int64) string {
    664 		return fmt.Sprintf("%ds", i)
    665 	}, false)
    666 
    667 	if c.Request().Method == http.MethodGet {
    668 		setCaptcha(data.Seed)
    669 		return c.Render(http.StatusOK, "captcha", data)
    670 	}
    671 
    672 	captchaID := c.Request().PostFormValue("captcha_id")
    673 	captchaInput := c.Request().PostFormValue("captcha")
    674 	ts := utils.DoParseInt64(c.Request().PostFormValue("ts"))
    675 	delta := time.Now().UnixMilli() - ts
    676 	if err := hutils.CaptchaVerifyString(c, captchaID, captchaInput); err != nil {
    677 		data.Seed = utils.DoParseInt64(c.Request().PostFormValue("seed"))
    678 		setCaptcha(data.Seed)
    679 		data.Error = fmt.Sprintf("%s; took: %.2fs", err.Error(), float64(delta)/1000)
    680 		return c.Render(http.StatusOK, "captcha", data)
    681 	}
    682 	setCaptcha(data.Seed)
    683 	data.Success = fmt.Sprintf("Good captcha; took: %.2fs", float64(delta)/1000)
    684 	return c.Render(http.StatusOK, "captcha", data)
    685 }
    686 
    687 func PublicUserProfileHandler(c echo.Context) error {
    688 	username := database.Username(c.Param("username"))
    689 	db := c.Get("database").(*database.DkfDB)
    690 	user, err := db.GetUserByUsername(username)
    691 	if err != nil {
    692 		return c.Redirect(http.StatusFound, "/")
    693 	}
    694 	var data publicProfileData
    695 	data.User = user
    696 	data.UserStyle = user.GenerateChatStyle()
    697 	data.PublicNotes, _ = db.GetUserPublicNotes(user.ID)
    698 	data.GpgKeyExpiredTime, data.GpgKeyExpired = utils.GetKeyExpiredTime(user.GPGPublicKey)
    699 	if data.GpgKeyExpiredTime != nil {
    700 		data.GpgKeyExpiredSoon = data.GpgKeyExpiredTime.AddDate(0, -1, 0).Before(time.Now())
    701 	}
    702 	return c.Render(http.StatusOK, "public-profile", data)
    703 }
    704 
    705 func PublicUserProfilePGPHandler(c echo.Context) error {
    706 	username := database.Username(c.Param("username"))
    707 	db := c.Get("database").(*database.DkfDB)
    708 	user, err := db.GetUserByUsername(username)
    709 	if err != nil {
    710 		return c.Redirect(http.StatusFound, "/")
    711 	}
    712 	if user.GPGPublicKey == "" {
    713 		return c.NoContent(http.StatusOK)
    714 	}
    715 	return c.String(http.StatusOK, user.GPGPublicKey)
    716 }
    717 
    718 func BHCHandler(c echo.Context) error {
    719 	/*
    720 		We have a script that check BHC wait room and kick any users that has not completed the dkf captcha.
    721 		When a user is kicked by that script, they are told to come here and solve the dkf captcha to get a valid bhc username.
    722 		Once they complete the captcha, they are given a username with a suffix that prove they completed the challenge.
    723 		Using a shared secret, the script is able to verify that the suffix is valid.
    724 		A suffix is valid for 10min, after that a different suffix would be generated for the same username.
    725 	*/
    726 	var data bhcData
    727 	data.CaptchaID, data.CaptchaImg = captcha.New()
    728 	config.BHCCaptchaGenerated.Inc()
    729 
    730 	username := c.QueryParam("username")
    731 	if len(username) > 17 {
    732 		data.Error = fmt.Sprintf("Invalid username, must have 17 characters at most")
    733 		return c.Render(http.StatusOK, "bhc", data)
    734 	}
    735 
    736 	const sharedSecret = "4#yFvRpk4^rJCxjjdbrdaBzWZ"
    737 
    738 	if c.Request().Method == http.MethodGet {
    739 		return c.Render(http.StatusOK, "bhc", data)
    740 	}
    741 
    742 	captchaID := c.Request().PostFormValue("captcha_id")
    743 	captchaInput := c.Request().PostFormValue("captcha")
    744 	if err := hutils.CaptchaVerifyString(c, captchaID, captchaInput); err != nil {
    745 		data.Error = fmt.Sprintf("Invalid answer")
    746 		config.BHCCaptchaFailed.Inc()
    747 		return c.Render(http.StatusOK, "bhc", data)
    748 	}
    749 	h := utils.Sha1([]byte(fmt.Sprintf("%s_%s_%d", username, sharedSecret, time.Now().Unix()/(60*10))))
    750 	config.BHCCaptchaSuccess.Inc()
    751 	data.Success = fmt.Sprintf("Good answer, go back to BHC and use '%s' as your username", username+h[:3])
    752 	return c.Render(http.StatusOK, "bhc", data)
    753 }