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 }