totpViewRoutes.go 2.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172
  1. package routes
  2. import (
  3. "crypto/subtle"
  4. "embed"
  5. "encoding/base64"
  6. "fwh-toolbox-server/repositories/api/fwh_pro"
  7. "strings"
  8. "time"
  9. "github.com/gofiber/fiber/v2"
  10. )
  11. //go:embed totp_view.html
  12. var totpViewFiles embed.FS
  13. // TotpViewSetup registers the protected page on a dedicated Fiber app.
  14. // An unset password or TOTP secret disables both endpoints.
  15. func TotpViewSetup(app *fiber.App, secret, password string) {
  16. if secret == "" || password == "" {
  17. return
  18. }
  19. view := app.Group("/fwh-totp", func(c *fiber.Ctx) error {
  20. c.Set("Cache-Control", "no-store, max-age=0")
  21. c.Set("Pragma", "no-cache")
  22. c.Set("X-Content-Type-Options", "nosniff")
  23. c.Set("X-Frame-Options", "DENY")
  24. c.Set("Referrer-Policy", "no-referrer")
  25. if !validTotpViewAuth(c.Get("Authorization"), password) {
  26. c.Set("WWW-Authenticate", `Basic realm="FWH TOTP", charset="UTF-8"`)
  27. return c.SendStatus(fiber.StatusUnauthorized)
  28. }
  29. return c.Next()
  30. })
  31. view.Get("/", func(c *fiber.Ctx) error {
  32. page, err := totpViewFiles.ReadFile("totp_view.html")
  33. if err != nil {
  34. return fiber.ErrInternalServerError
  35. }
  36. c.Type("html", "utf-8")
  37. return c.Send(page)
  38. })
  39. view.Get("/code", func(c *fiber.Ctx) error {
  40. now := time.Now()
  41. code, err := fwh_pro.GenerateTOTP(secret, now)
  42. if err != nil {
  43. return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "TOTP 配置无效"})
  44. }
  45. return c.JSON(fiber.Map{
  46. "code": code,
  47. "expiresAt": (now.Unix()/30 + 1) * 30000,
  48. "serverTime": now.UnixMilli(),
  49. })
  50. })
  51. }
  52. func validTotpViewAuth(header, password string) bool {
  53. const prefix = "Basic "
  54. if !strings.HasPrefix(header, prefix) {
  55. return false
  56. }
  57. decoded, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(header, prefix))
  58. if err != nil {
  59. return false
  60. }
  61. username, candidate, ok := strings.Cut(string(decoded), ":")
  62. return ok && subtle.ConstantTimeCompare([]byte(username), []byte("fwh")) == 1 &&
  63. subtle.ConstantTimeCompare([]byte(candidate), []byte(password)) == 1
  64. }