Просмотр исходного кода

Merge pull request #17675 from overleaf/bg-session-mitigation-redis-store-metrics

add CustomSessionStore class to handle session metrics and logging

GitOrigin-RevId: 49d4cda9fd94a8801adb33e894be239dc38ad544
Brian Gough 2 лет назад
Родитель
Сommit
cdf31ed91c

+ 84 - 0
services/web/app/src/infrastructure/CustomSessionStore.js

@@ -0,0 +1,84 @@
+const session = require('express-session')
+const RedisStore = require('connect-redis')(session)
+const metrics = require('@overleaf/metrics')
+const logger = require('@overleaf/logger')
+const SessionManager = require('../Features/Authentication/SessionManager')
+
+const MAX_SESSION_SIZE_THRESHOLD = 4096
+
+// Define a custom session store to record session metrics and log large
+// anonymous sessions for debugging purposes
+class CustomSessionStore extends RedisStore {
+  static largestSessionSize = 3 * 1024 // ignore sessions smaller than 3KB
+
+  static metric(method, sess) {
+    let type // type of session: 'logged-in', 'anonymous', or 'na' (not available)
+    if (sess) {
+      type = SessionManager.isUserLoggedIn(sess) ? 'logged-in' : 'anonymous'
+    } else {
+      type = 'na'
+    }
+    const size = sess ? JSON.stringify(sess).length : 0
+    // record the number of redis session operations
+    metrics.inc('session.store.count', 1, {
+      method,
+      type,
+      status: size > MAX_SESSION_SIZE_THRESHOLD ? 'oversize' : 'normal',
+    })
+    // record the redis session bandwidth for get/set operations
+    if (method === 'get' || method === 'set') {
+      metrics.count('session.store.bytes', size, { method, type })
+    }
+    // log the largest anonymous session seen so far
+    if (type === 'anonymous' && size > CustomSessionStore.largestSessionSize) {
+      CustomSessionStore.largestSessionSize = size
+      logger.warn(
+        { redactedSession: redactSession(sess), largestSessionSize: size },
+        'largest session size seen'
+      )
+    }
+  }
+
+  // Override the get, set, touch, and destroy methods to record metrics
+  get(sid, cb) {
+    super.get(sid, (err, ...args) => {
+      if (args[0]) {
+        CustomSessionStore.metric('get', args[0])
+      }
+      cb(err, ...args)
+    })
+  }
+
+  set(sid, sess, cb) {
+    CustomSessionStore.metric('set', sess)
+    super.set(sid, sess, cb)
+  }
+
+  touch(sid, sess, cb) {
+    CustomSessionStore.metric('touch', sess)
+    super.touch(sid, sess, cb)
+  }
+
+  destroy(sid, cb) {
+    // for the destroy method we don't have access to the session object itself
+    CustomSessionStore.metric('destroy')
+    super.destroy(sid, cb)
+  }
+}
+
+// Helper function to return a redacted version of session object
+// so we can identify the largest keys without exposing sensitive
+// data
+function redactSession(sess) {
+  // replace all string values with '***' of the same length
+  return JSON.parse(
+    JSON.stringify(sess, (key, value) => {
+      if (typeof value === 'string') {
+        return '*'.repeat(value.length)
+      }
+      return value
+    })
+  )
+}
+
+module.exports = CustomSessionStore

+ 2 - 36
services/web/app/src/infrastructure/Server.js

@@ -16,7 +16,7 @@ const SessionAutostartMiddleware = require('./SessionAutostartMiddleware')
 const SessionStoreManager = require('./SessionStoreManager')
 const AnalyticsManager = require('../Features/Analytics/AnalyticsManager')
 const session = require('express-session')
-const RedisStore = require('connect-redis')(session)
+const CustomSessionStore = require('./CustomSessionStore')
 const bodyParser = require('./BodyParserWrapper')
 const methodOverride = require('method-override')
 const cookieParser = require('cookie-parser')
@@ -48,42 +48,8 @@ const STATIC_CACHE_AGE = Settings.cacheStaticAssets
   ? oneDayInMilliseconds * 365
   : 0
 
-// Define a custom session store to record the largest session sizes
-// seen for anonymous users
-class CustomSessionStore extends RedisStore {
-  static largestSessionSize = 2048 // ignore sessions smaller than 2KB
-
-  static trackAnonymousSessionSize(sess) {
-    const isLoggedIn = SessionManager.isUserLoggedIn(sess)
-    if (!isLoggedIn) {
-      const len = JSON.stringify(sess, (key, value) => {
-        if (key === 'hashedPassword' && value?.length > 0) {
-          return '*'.repeat(value.length)
-        }
-        return value
-      }).length
-      if (len > CustomSessionStore.largestSessionSize) {
-        CustomSessionStore.largestSessionSize = len
-        logger.warn({ sess, sessionSize: len }, 'largest session size seen')
-      }
-    }
-  }
-
-  set(sid, sess, cb) {
-    CustomSessionStore.trackAnonymousSessionSize(sess)
-    super.set(sid, sess, cb)
-  }
-
-  touch(sid, sess, cb) {
-    CustomSessionStore.trackAnonymousSessionSize(sess)
-    super.touch(sid, sess, cb)
-  }
-}
-
 // Init the session store
-const sessionStore = new CustomSessionStore(
-  new RedisStore({ client: sessionsRedisClient })
-)
+const sessionStore = new CustomSessionStore({ client: sessionsRedisClient })
 
 const app = express()