middlewares.go (18369B)
1 package middlewares 2 3 import ( 4 "dkforest/bindata" 5 "dkforest/pkg/cache" 6 "dkforest/pkg/captcha" 7 "dkforest/pkg/config" 8 "dkforest/pkg/database" 9 "dkforest/pkg/utils" 10 "dkforest/pkg/web/clientFrontends" 11 "dkforest/pkg/web/handlers" 12 hutils "dkforest/pkg/web/handlers/utils" 13 "github.com/labstack/echo" 14 "github.com/labstack/echo/middleware" 15 "github.com/nicksnyder/go-i18n/v2/i18n" 16 "github.com/ulule/limiter" 17 "github.com/ulule/limiter/drivers/store/memory" 18 "net" 19 "net/http" 20 "strings" 21 "time" 22 ) 23 24 // GzipMiddleware ... 25 func GzipMiddleware(next echo.HandlerFunc) echo.HandlerFunc { 26 return func(c echo.Context) error { 27 authUser := c.Get("authUser").(*database.User) 28 cfg := middleware.GzipConfig{ 29 Level: 5, 30 Skipper: func(c echo.Context) bool { 31 if c.Path() == "/bhcli/downloads/:filename" || 32 c.Path() == "/vip/downloads/:filename" || 33 c.Path() == "/vip/challenges/re-1/:filename" || 34 c.Path() == "/chess/:key" || 35 c.Path() == "/chess/:key/analyze" || 36 c.Path() == "/poker/:roomID/stream" || 37 c.Path() == "/poker/:roomID/logs" || 38 c.Path() == "/poker/:roomID/bet" || 39 c.Path() == "/api/v1/chat/messages/:roomName/stream" || 40 c.Path() == "/api/v1/chat/messages/:roomName/stream/menu" || 41 (c.Path() == "/api/v1/chat/top-bar/:roomName" && authUser != nil && authUser.UseStreamTopBar) || 42 c.Path() == "/uploads/:filename" || 43 c.Path() == "/" { 44 return true 45 } 46 return false 47 }, 48 } 49 return middleware.GzipWithConfig(cfg)(next)(c) 50 } 51 } 52 53 // BodyLimit ... 54 var BodyLimit = middleware.BodyLimitWithConfig(middleware.BodyLimitConfig{ 55 Limit: "1M", 56 Skipper: func(c echo.Context) bool { 57 if c.Path() == "/api/v1/chat/top-bar/:roomName" { 58 return true 59 } 60 return false 61 }, 62 }) 63 64 // CaptchaMiddleware ... 65 func CaptchaMiddleware() echo.MiddlewareFunc { 66 return func(next echo.HandlerFunc) echo.HandlerFunc { 67 return func(c echo.Context) error { 68 var data captchaMiddlewareData 69 data.CaptchaDescription = "Captcha required" 70 data.CaptchaID, data.CaptchaImg = captcha.New() 71 const captchaRequiredTmpl = "captcha-required" 72 if c.Request().Method == http.MethodPost { 73 captchaID := c.Request().PostFormValue("captcha_id") 74 captchaInput := c.Request().PostFormValue("captcha") 75 if err := hutils.CaptchaVerifyString(c, captchaID, captchaInput); err != nil { 76 data.ErrCaptcha = err.Error() 77 return c.Render(http.StatusOK, captchaRequiredTmpl, data) 78 } 79 return next(c) 80 } 81 return c.Render(http.StatusOK, captchaRequiredTmpl, data) 82 } 83 } 84 } 85 86 // GenericRateLimitMiddleware rate limit on userID if authenticated, or circuitID otherwise 87 // This rate limiter should be used for endpoints that are accessible by both unauthenticated and authenticated users. 88 func GenericRateLimitMiddleware(period time.Duration, limit int64) echo.MiddlewareFunc { 89 rate := limiter.Rate{Period: period, Limit: limit} 90 store := memory.NewStore() 91 limiterInstance := limiter.New(store, rate) 92 93 return func(next echo.HandlerFunc) echo.HandlerFunc { 94 return func(c echo.Context) error { 95 key := "ip_" + c.RealIP() 96 if authUser, ok := c.Get("authUser").(*database.User); ok && authUser != nil { 97 key = "userid_" + authUser.ID.String() 98 } else if conn, ok := c.Request().Context().Value("conn").(net.Conn); ok { 99 circuitID := config.ConnMap.Get(conn) 100 key = "circuitid_" + utils.FormatInt64(circuitID) 101 } 102 context, err := limiterInstance.Get(c.Request().Context(), key) 103 if err != nil { 104 return next(c) 105 } 106 c.Response().Header().Add("X-RateLimit-Limit", utils.FormatInt64(context.Limit)) 107 c.Response().Header().Add("X-RateLimit-Remaining", utils.FormatInt64(context.Remaining)) 108 c.Response().Header().Add("X-RateLimit-Reset", utils.FormatInt64(context.Reset)) 109 if context.Reached { 110 return c.Render(http.StatusTooManyRequests, "flash", handlers.FlashResponse{Message: "Rate limit exceeded", Redirect: c.Request().URL.String(), Type: "alert-warning"}) 111 } 112 return next(c) 113 } 114 } 115 } 116 117 func CircuitRateLimitMiddleware(period time.Duration, limit int64, kill bool) echo.MiddlewareFunc { 118 rate := limiter.Rate{Period: period, Limit: limit} 119 store := memory.NewStore() 120 limiterInstance := limiter.New(store, rate) 121 122 return func(next echo.HandlerFunc) echo.HandlerFunc { 123 return func(c echo.Context) error { 124 125 if conn, ok := c.Request().Context().Value("conn").(net.Conn); ok { 126 circuitID := config.ConnMap.Get(conn) 127 128 context, err := limiterInstance.Get(c.Request().Context(), utils.FormatInt64(circuitID)) 129 if err != nil { 130 return next(c) 131 } 132 c.Response().Header().Add("X-RateLimit-Limit", utils.FormatInt64(context.Limit)) 133 c.Response().Header().Add("X-RateLimit-Remaining", utils.FormatInt64(context.Remaining)) 134 c.Response().Header().Add("X-RateLimit-Reset", utils.FormatInt64(context.Reset)) 135 if context.Reached { 136 if kill { 137 config.ConnMap.CloseCircuit(circuitID) 138 return c.NoContent(http.StatusOK) 139 } 140 return c.Render(http.StatusTooManyRequests, "flash", handlers.FlashResponse{Message: "Rate limit exceeded", Redirect: c.Request().URL.String(), Type: "alert-warning"}) 141 } 142 } 143 return next(c) 144 } 145 } 146 } 147 148 // AuthRateLimitMiddleware ... 149 func AuthRateLimitMiddleware(period time.Duration, limit int64) echo.MiddlewareFunc { 150 rate := limiter.Rate{Period: period, Limit: limit} 151 store := memory.NewStore() 152 limiterInstance := limiter.New(store, rate) 153 154 return func(next echo.HandlerFunc) echo.HandlerFunc { 155 return func(c echo.Context) error { 156 authUser := c.Get("authUser").(*database.User) 157 context, err := limiterInstance.Get(c.Request().Context(), utils.FormatInt64(int64(authUser.ID))) 158 if err != nil { 159 // fmt.Errorf("could not get context for IP %s - %v", c.RealIP(), err) 160 return next(c) 161 } 162 c.Response().Header().Add("X-RateLimit-Limit", utils.FormatInt64(context.Limit)) 163 c.Response().Header().Add("X-RateLimit-Remaining", utils.FormatInt64(context.Remaining)) 164 c.Response().Header().Add("X-RateLimit-Reset", utils.FormatInt64(context.Reset)) 165 if context.Reached { 166 return c.Render(http.StatusTooManyRequests, "flash", handlers.FlashResponse{Message: "Rate limit exceeded", Redirect: c.Request().URL.String(), Type: "alert-warning"}) 167 //return c.JSON(429, map[string]string{"message": fmt.Sprintf("Rate limit exceeded for %s", authUser.Username)}) 168 } 169 return next(c) 170 } 171 } 172 } 173 174 // CSRFMiddleware ... 175 func CSRFMiddleware() echo.MiddlewareFunc { 176 csrfConfig := CSRFConfig{ 177 TokenLookup: "form:csrf", 178 CookieDomain: config.Global.CookieDomain.Get(), 179 CookiePath: "/", 180 CookieHTTPOnly: true, 181 CookieSecure: config.Global.CookieSecure.Get(), 182 CookieMaxAge: utils.OneMonthSecs, 183 SameSite: http.SameSiteLaxMode, 184 Skipper: func(c echo.Context) bool { 185 apiKey := c.Request().Header.Get("DKF_API_KEY") 186 return (apiKey != "" && strings.HasPrefix(c.Path(), "/api/v1/")) || 187 c.Path() == "/api/v1/battleship" || 188 c.Path() == "/api/v1/werewolf" || 189 c.Path() == "/chess/:key" || 190 c.Path() == "/poker/:roomID/sit/:pos" || 191 c.Path() == "/poker/:roomID/unsit" || 192 c.Path() == "/poker/:roomID/bet" || 193 c.Path() == "/poker/:roomID/logs" || 194 c.Path() == "/poker/:roomID/deal" 195 }, 196 } 197 return CSRFWithConfig(csrfConfig) 198 } 199 200 // I18nMiddleware ... 201 func I18nMiddleware(bundle *i18n.Bundle, defaultLang string) echo.MiddlewareFunc { 202 return func(next echo.HandlerFunc) echo.HandlerFunc { 203 return func(c echo.Context) error { 204 if strings.HasPrefix(c.Path(), "/sse/") { 205 return next(c) 206 } 207 accept := c.Request().Header.Get("Accept-Language") 208 209 // This is how the language is chosen: 210 // - User preference (if set) 211 // - App lang flag (if set) 212 // - Browser accept-language header 213 // - Default en 214 215 lang := "" 216 user, _ := c.Get("authUser").(*database.User) 217 if user != nil && user.Lang != "" { 218 lang = user.Lang 219 } else if defaultLang != "" { 220 lang = defaultLang 221 } 222 c.Set("lang", lang) 223 c.Set("accept-language", accept) 224 c.Set("bundle", bundle) 225 return next(c) 226 } 227 } 228 } 229 230 func SetDatabaseMiddleware(db *database.DkfDB) echo.MiddlewareFunc { 231 return func(next echo.HandlerFunc) echo.HandlerFunc { 232 return func(ctx echo.Context) error { 233 ctx.Set("database", db) 234 return next(ctx) 235 } 236 } 237 } 238 239 func SetClientFEMiddleware(clientFE clientFrontends.ClientFrontend) echo.MiddlewareFunc { 240 return func(next echo.HandlerFunc) echo.HandlerFunc { 241 return func(ctx echo.Context) error { 242 ctx.Set("clientFE", clientFE) 243 return next(ctx) 244 } 245 } 246 } 247 248 // SetUserMiddleware Get user and put it into echo context. 249 // - Get auth-token from cookie 250 // - If exists, get user from database 251 // - If found, set user in echo context 252 // - Otherwise, empty user will be put in context 253 func SetUserMiddleware(next echo.HandlerFunc) echo.HandlerFunc { 254 return func(ctx echo.Context) error { 255 db := ctx.Get("database").(*database.DkfDB) 256 var nilUser *database.User 257 var user database.User 258 259 if apiKey := ctx.Request().Header.Get("DKF_API_KEY"); apiKey != "" { 260 // Login using DKF_API_KEY 261 if err := db.GetUserByApiKey(&user, apiKey); err == nil { 262 ctx.Set("authUser", &user) 263 return next(ctx) 264 } 265 } else if authCookie, err := ctx.Cookie(hutils.AuthCookieName); err == nil { 266 // Login using auth cookie 267 if err := db.GetUserBySessionKey(&user, authCookie.Value); err == nil { 268 ctx.Set("authUser", &user) 269 return next(ctx) 270 } else { 271 } 272 } 273 274 ctx.Set("authUser", nilUser) 275 return next(ctx) 276 } 277 } 278 279 type RateLimit[K comparable, V any] struct { 280 cache *cache.Cache[K, V] 281 value V 282 } 283 284 func NewRateLimit[K comparable](defaultExpiration time.Duration) *RateLimit[K, struct{}] { 285 return NewRateLimitV[K, struct{}](defaultExpiration) 286 } 287 288 func NewRateLimitV[K comparable, V any](defaultExpiration time.Duration) *RateLimit[K, V] { 289 return &RateLimit[K, V]{ 290 cache: cache.NewWithKey[K, V](defaultExpiration, time.Minute), 291 } 292 } 293 294 func (l *RateLimit[K, V]) RateLimit(k K, clb func()) { 295 if !l.cache.Has(k) { 296 clb() 297 l.cache.SetD(k, l.value) 298 } 299 } 300 301 func (l *RateLimit[K, V]) RateLimitV(k K, clb func() (V, error)) (V, bool, error) { 302 var err error 303 if !l.cache.Has(k) { 304 l.value, err = clb() 305 l.cache.SetD(k, l.value) 306 return l.value, true, err 307 } 308 return l.value, false, err 309 } 310 311 var lastSeenRL = NewRateLimit[database.UserID](time.Second) 312 313 // IsAuthMiddleware will ensure user is authenticated. 314 // - Find user from context 315 // - If user is empty, redirect to home 316 func IsAuthMiddleware(next echo.HandlerFunc) echo.HandlerFunc { 317 return func(c echo.Context) error { 318 user := c.Get("authUser").(*database.User) 319 db := c.Get("database").(*database.DkfDB) 320 if user == nil { 321 if strings.HasPrefix(c.Path(), "/api/") { 322 return c.String(http.StatusUnauthorized, "unauthorized") 323 } 324 referralToken := c.QueryParam("r") 325 if strings.HasPrefix(c.Path(), "/poker") && referralToken != "" { 326 if len(referralToken) == 9 { 327 hutils.CreatePokerReferralCookie(c, referralToken) 328 } 329 } 330 return c.Redirect(http.StatusFound, "/?redirect="+c.Request().URL.String()) 331 } 332 333 c.Response().Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") 334 335 lastSeenRL.RateLimit(user.ID, func() { 336 now := time.Now() 337 db.DB().Exec("UPDATE users SET last_seen_at = ?, updated_at = ? WHERE id = ?", now, now, int64(user.ID)) 338 }) 339 340 // Prevent clickjacking by setting the header on every logged in page 341 if !strings.Contains(c.Path(), "/chess/:key/form") && 342 !strings.Contains(c.Path(), "/chess/:key/stats") && 343 !strings.Contains(c.Path(), "/api/v1/chat/messages") && 344 !strings.Contains(c.Path(), "/api/v1/chat/messages/:roomName/stream") && 345 !strings.Contains(c.Path(), "/api/v1/chat/messages/:roomName/stream/menu") && 346 !strings.Contains(c.Path(), "/api/v1/chat/top-bar") && 347 !strings.Contains(c.Path(), "/api/v1/chat/controls") && 348 !strings.Contains(c.Path(), "/poker/:roomID/stream") && 349 !strings.Contains(c.Path(), "/poker/:roomID/sit/:pos") && 350 !strings.Contains(c.Path(), "/poker/:roomID/unsit") && 351 !strings.Contains(c.Path(), "/poker/:roomID/bet") && 352 !strings.Contains(c.Path(), "/poker/:roomID/logs") && 353 !strings.Contains(c.Path(), "/poker/:roomID/deal") { 354 c.Response().Header().Set("X-Frame-Options", "DENY") 355 } 356 357 return next(c) 358 } 359 } 360 361 func ForceCaptchaMiddleware(next echo.HandlerFunc) echo.HandlerFunc { 362 return func(c echo.Context) error { 363 user := c.Get("authUser").(*database.User) 364 if user.CaptchaRequired && c.Path() != "/captcha-required" { 365 return c.Redirect(http.StatusFound, "/captcha-required") 366 } 367 return next(c) 368 } 369 } 370 371 // HellbannedCookieMiddleware if a user is HB and doesn't have the cookie, creates it. 372 // We use this cookie to auto HB new account created by this person. 373 func HellbannedCookieMiddleware(next echo.HandlerFunc) echo.HandlerFunc { 374 return func(c echo.Context) error { 375 user := c.Get("authUser").(*database.User) 376 if user != nil && user.IsHellbanned { 377 if _, err := c.Cookie(hutils.HBCookieName); err != nil { 378 cookie := hutils.CreateCookie(hutils.HBCookieName, utils.GenerateToken3(), utils.OneMonthSecs) 379 c.SetCookie(cookie) 380 } 381 } 382 return next(c) 383 } 384 } 385 386 // ClubMiddleware ... 387 func ClubMiddleware(next echo.HandlerFunc) echo.HandlerFunc { 388 return func(c echo.Context) error { 389 user := c.Get("authUser").(*database.User) 390 if user != nil && (!user.IsAdmin && !user.IsClubMember) { 391 var data unauthorizedData 392 data.Message = `To access this section, you need an official invitation from the team.` 393 return c.Render(http.StatusOK, "unauthorized", data) 394 } 395 return next(c) 396 } 397 } 398 399 // VipMiddleware ... 400 func VipMiddleware(next echo.HandlerFunc) echo.HandlerFunc { 401 return func(c echo.Context) error { 402 user := c.Get("authUser").(*database.User) 403 if user != nil && user.GPGPublicKey == "" { 404 var data unauthorizedData 405 data.Message = `To access this section, you need to have a valid PGP public key linked to your profile.<br /> 406 <a href="/settings/pgp">Add your PGP public key to your profile here</a>` 407 return c.Render(http.StatusOK, "unauthorized", data) 408 } 409 return next(c) 410 } 411 } 412 413 // IsModeratorMiddleware only moderators can access these routes. 414 func IsModeratorMiddleware(next echo.HandlerFunc) echo.HandlerFunc { 415 return func(c echo.Context) error { 416 user := c.Get("authUser").(*database.User) 417 if user == nil || !user.IsModerator() { 418 if strings.HasPrefix(c.Path(), "/api") { 419 if user == nil { 420 return c.NoContent(http.StatusUnauthorized) 421 } else if !user.IsModerator() { 422 return c.NoContent(http.StatusForbidden) 423 } 424 return c.NoContent(http.StatusInternalServerError) 425 } 426 return c.Redirect(http.StatusFound, "/") 427 } 428 return next(c) 429 } 430 } 431 432 // IsAdminMiddleware only administrators can access these routes. 433 func IsAdminMiddleware(next echo.HandlerFunc) echo.HandlerFunc { 434 return func(c echo.Context) error { 435 user := c.Get("authUser").(*database.User) 436 if user == nil || !user.IsAdmin { 437 if strings.HasPrefix(c.Path(), "/api") { 438 if user == nil { 439 return c.NoContent(http.StatusUnauthorized) 440 } else if !user.IsAdmin { 441 return c.NoContent(http.StatusForbidden) 442 } 443 return c.NoContent(http.StatusInternalServerError) 444 } 445 return c.Redirect(http.StatusFound, "/") 446 } 447 return next(c) 448 } 449 } 450 451 func AprilFoolMiddleware() echo.MiddlewareFunc { 452 return func(next echo.HandlerFunc) echo.HandlerFunc { 453 return func(c echo.Context) error { 454 if strings.HasPrefix(c.Path(), "/api/v1/") { 455 return next(c) 456 } 457 458 year, month, day := time.Now().UTC().Date() 459 if year == 2022 && month == time.April && day == 1 { 460 vv := hutils.GetAprilFoolCookie(c) 461 if vv < 3 { 462 hutils.CreateAprilFoolCookie(c, vv+1) 463 return c.Render(http.StatusOK, "seized", nil) 464 } 465 } 466 return next(c) 467 } 468 } 469 } 470 471 func DdosMiddleware(next echo.HandlerFunc) echo.HandlerFunc { 472 stopFn := func(c echo.Context) error { 473 hutils.KillCircuit(c) 474 config.RejectedReqCounter.Incr() 475 time.Sleep(utils.RandSec(5, 20)) 476 return c.NoContent(http.StatusOK) 477 } 478 return func(c echo.Context) error { 479 config.RpsCounter.Incr() 480 if authCookie, err := c.Cookie(hutils.AuthCookieName); err == nil { 481 if len(authCookie.Value) > 64 { 482 return stopFn(c) 483 } 484 } 485 if csrfCookie, err := c.Cookie("_csrf"); err == nil { 486 if len(csrfCookie.Value) > 32 { 487 return stopFn(c) 488 } 489 } 490 if len(c.QueryParam("captcha")) > 6 { 491 return stopFn(c) 492 } 493 return next(c) 494 } 495 } 496 497 // MaintenanceMiddleware ... 498 func MaintenanceMiddleware(next echo.HandlerFunc) echo.HandlerFunc { 499 return func(c echo.Context) error { 500 if config.MaintenanceAtom.IsFalse() { 501 return next(c) 502 } 503 if strings.HasPrefix(c.Path(), "/admin/") || 504 strings.HasPrefix(c.Path(), "/master-admin/") || 505 strings.HasPrefix(c.Path(), "/api/v1/master-admin") { 506 return next(c) 507 } 508 asset := bindata.MustAsset("views/pages/maintenance.gohtml") 509 return c.HTML(http.StatusOK, string(asset)) 510 } 511 } 512 513 // MaybeAuthMiddleware let un-authenticated users access the page if MaybeAuthEnabled is enabled. 514 // Otherwise, the user needs to be authenticated to access the page. 515 func MaybeAuthMiddleware(next echo.HandlerFunc) echo.HandlerFunc { 516 return func(c echo.Context) error { 517 if config.MaybeAuthEnabled.IsFalse() { 518 if user := c.Get("authUser").(*database.User); user == nil { 519 return c.Redirect(http.StatusFound, "/") 520 } 521 } 522 return next(c) 523 } 524 } 525 526 // NoAuthMiddleware redirect to / is the user is authenticated 527 func NoAuthMiddleware(next echo.HandlerFunc) echo.HandlerFunc { 528 return func(c echo.Context) error { 529 if user := c.Get("authUser").(*database.User); user != nil { 530 return c.Redirect(http.StatusFound, "/") 531 } 532 return next(c) 533 } 534 } 535 536 // FirstUseMiddleware if first use, redirect to / 537 func FirstUseMiddleware(next echo.HandlerFunc) echo.HandlerFunc { 538 return func(c echo.Context) error { 539 if config.IsFirstUse.IsTrue() && c.Path() != "/" { 540 return c.Redirect(http.StatusFound, "/") 541 } 542 return next(c) 543 } 544 } 545 546 // SecureMiddleware ... 547 var SecureMiddleware = middleware.SecureWithConfig(middleware.SecureConfig{ 548 XSSProtection: "1; mode=block", 549 ContentTypeNosniff: "nosniff", 550 XFrameOptions: "SAMEORIGIN", 551 //HSTSMaxAge: 3600, 552 //ContentSecurityPolicy: "default-src 'self'", 553 }) 554 555 func SetUselessHeadersMiddleware(next echo.HandlerFunc) echo.HandlerFunc { 556 return func(c echo.Context) error { 557 c.Response().Header().Set("X-Powered-By", "the almighty n0tr1v") 558 return next(c) 559 } 560 }