mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge pull request #18848 from overleaf/bg-cookie-session-metrics-for-real-time
add cookie session metrics for real-time GitOrigin-RevId: 6b482dafc19fca46a375ea00a9b2297e20d915ce
This commit is contained in:
parent
38ac00ba13
commit
49dc94192a
5 changed files with 186 additions and 49 deletions
|
@ -1,3 +1,4 @@
|
||||||
|
const metrics = require('@overleaf/metrics')
|
||||||
const OError = require('@overleaf/o-error')
|
const OError = require('@overleaf/o-error')
|
||||||
const { EventEmitter } = require('events')
|
const { EventEmitter } = require('events')
|
||||||
const { MissingSessionError } = require('./Errors')
|
const { MissingSessionError } = require('./Errors')
|
||||||
|
@ -15,18 +16,26 @@ module.exports = function (io, sessionStore, cookieParser, cookieName) {
|
||||||
cookieParser(req, {}, function () {
|
cookieParser(req, {}, function () {
|
||||||
const sessionId = req.signedCookies && req.signedCookies[cookieName]
|
const sessionId = req.signedCookies && req.signedCookies[cookieName]
|
||||||
if (!sessionId) {
|
if (!sessionId) {
|
||||||
|
metrics.inc('session.cookie', 1, {
|
||||||
|
// the cookie-parser middleware sets the signed cookie to false if the
|
||||||
|
// signature is invalid, so we can use this to detect bad signatures
|
||||||
|
status: sessionId === false ? 'bad-signature' : 'none',
|
||||||
|
})
|
||||||
return next(missingSessionError, socket)
|
return next(missingSessionError, socket)
|
||||||
}
|
}
|
||||||
sessionStore.get(sessionId, function (error, session) {
|
sessionStore.get(sessionId, function (error, session) {
|
||||||
if (error) {
|
if (error) {
|
||||||
|
metrics.inc('session.cookie', 1, { status: 'error' })
|
||||||
OError.tag(error, 'error getting session from sessionStore', {
|
OError.tag(error, 'error getting session from sessionStore', {
|
||||||
sessionId,
|
sessionId,
|
||||||
})
|
})
|
||||||
return next(error, socket)
|
return next(error, socket)
|
||||||
}
|
}
|
||||||
if (!session) {
|
if (!session) {
|
||||||
|
metrics.inc('session.cookie', 1, { status: 'missing' })
|
||||||
return next(missingSessionError, socket)
|
return next(missingSessionError, socket)
|
||||||
}
|
}
|
||||||
|
metrics.inc('session.cookie', 1, { status: 'signed' })
|
||||||
next(null, socket, session)
|
next(null, socket, session)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -6,5 +6,6 @@ module.exports = {
|
||||||
security: {
|
security: {
|
||||||
sessionSecret: 'static-secret-for-tests',
|
sessionSecret: 'static-secret-for-tests',
|
||||||
sessionSecretFallback: 'static-secret-fallback-for-tests',
|
sessionSecretFallback: 'static-secret-fallback-for-tests',
|
||||||
|
sessionSecretUpcoming: 'static-secret-upcoming-for-tests',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,7 @@
|
||||||
/* eslint-disable
|
|
||||||
no-return-assign,
|
|
||||||
*/
|
|
||||||
// TODO: This file was created by bulk-decaffeinate.
|
|
||||||
// Fix any style issues and re-enable lint.
|
|
||||||
/*
|
|
||||||
* decaffeinate suggestions:
|
|
||||||
* DS102: Remove unnecessary code created because of implicit returns
|
|
||||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
|
||||||
*/
|
|
||||||
const RealTimeClient = require('./helpers/RealTimeClient')
|
const RealTimeClient = require('./helpers/RealTimeClient')
|
||||||
const FixturesManager = require('./helpers/FixturesManager')
|
const FixturesManager = require('./helpers/FixturesManager')
|
||||||
const Settings = require('@overleaf/settings')
|
const Settings = require('@overleaf/settings')
|
||||||
|
const signature = require('cookie-signature')
|
||||||
const { expect } = require('chai')
|
const { expect } = require('chai')
|
||||||
|
|
||||||
describe('SessionSockets', function () {
|
describe('SessionSockets', function () {
|
||||||
|
@ -32,28 +23,28 @@ describe('SessionSockets', function () {
|
||||||
|
|
||||||
describe('without cookies', function () {
|
describe('without cookies', function () {
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
return (RealTimeClient.cookie = null)
|
RealTimeClient.cookie = null
|
||||||
})
|
})
|
||||||
|
|
||||||
return it('should return a lookup error', function (done) {
|
it('should return a lookup error', function (done) {
|
||||||
return this.checkSocket(error => {
|
this.checkSocket(error => {
|
||||||
expect(error).to.exist
|
expect(error).to.exist
|
||||||
expect(error.message).to.equal('invalid session')
|
expect(error.message).to.equal('invalid session')
|
||||||
return done()
|
done()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('with a different cookie', function () {
|
describe('with a different cookie', function () {
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
return (RealTimeClient.cookie = 'some.key=someValue')
|
RealTimeClient.cookie = 'some.key=someValue'
|
||||||
})
|
})
|
||||||
|
|
||||||
return it('should return a lookup error', function (done) {
|
it('should return a lookup error', function (done) {
|
||||||
return this.checkSocket(error => {
|
this.checkSocket(error => {
|
||||||
expect(error).to.exist
|
expect(error).to.exist
|
||||||
expect(error.message).to.equal('invalid session')
|
expect(error.message).to.equal('invalid session')
|
||||||
return done()
|
done()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -67,39 +58,82 @@ describe('SessionSockets', function () {
|
||||||
RealTimeClient.cookie = `${
|
RealTimeClient.cookie = `${
|
||||||
Settings.cookieName
|
Settings.cookieName
|
||||||
}=${RealTimeClient.cookie.slice(17, 49)}`
|
}=${RealTimeClient.cookie.slice(17, 49)}`
|
||||||
return done()
|
done()
|
||||||
})
|
})
|
||||||
return null
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return it('should return a lookup error', function (done) {
|
it('should return a lookup error', function (done) {
|
||||||
return this.checkSocket(error => {
|
this.checkSocket(error => {
|
||||||
expect(error).to.exist
|
expect(error).to.exist
|
||||||
expect(error.message).to.equal('invalid session')
|
expect(error.message).to.equal('invalid session')
|
||||||
return done()
|
done()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('with a valid cookie and no matching session', function () {
|
describe('with a valid cookie and no matching session', function () {
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
return (RealTimeClient.cookie = `${Settings.cookieName}=unknownId`)
|
RealTimeClient.cookie = `${Settings.cookieName}=unknownId`
|
||||||
})
|
})
|
||||||
|
|
||||||
return it('should return a lookup error', function (done) {
|
it('should return a lookup error', function (done) {
|
||||||
return this.checkSocket(error => {
|
this.checkSocket(error => {
|
||||||
expect(error).to.exist
|
expect(error).to.exist
|
||||||
expect(error.message).to.equal('invalid session')
|
expect(error.message).to.equal('invalid session')
|
||||||
return done()
|
done()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
return describe('with a valid cookie and a matching session', function () {
|
describe('with a valid cookie and a matching session', function () {
|
||||||
return it('should not return an error', function (done) {
|
it('should not return an error', function (done) {
|
||||||
return this.checkSocket(error => {
|
this.checkSocket(error => {
|
||||||
expect(error).to.not.exist
|
expect(error).to.not.exist
|
||||||
return done()
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with a cookie signed by the fallback key and a matching session', function () {
|
||||||
|
beforeEach(function () {
|
||||||
|
RealTimeClient.cookie =
|
||||||
|
RealTimeClient.cookieSignedWith.sessionSecretFallback
|
||||||
|
})
|
||||||
|
it('should not return an error', function (done) {
|
||||||
|
this.checkSocket(error => {
|
||||||
|
expect(error).to.not.exist
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with a cookie signed by the upcoming key and a matching session', function () {
|
||||||
|
beforeEach(function () {
|
||||||
|
RealTimeClient.cookie =
|
||||||
|
RealTimeClient.cookieSignedWith.sessionSecretUpcoming
|
||||||
|
})
|
||||||
|
it('should not return an error', function (done) {
|
||||||
|
this.checkSocket(error => {
|
||||||
|
expect(error).to.not.exist
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with a cookie signed with an unrecognized secret and a matching session', function () {
|
||||||
|
beforeEach(function () {
|
||||||
|
const [sessionKey] = RealTimeClient.cookie.split('.')
|
||||||
|
// sign the session key with a unrecognized secret
|
||||||
|
RealTimeClient.cookie = signature.sign(
|
||||||
|
sessionKey,
|
||||||
|
'unrecognised-session-secret'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
it('should return a lookup error', function (done) {
|
||||||
|
this.checkSocket(error => {
|
||||||
|
expect(error).to.exist
|
||||||
|
expect(error.message).to.equal('invalid session')
|
||||||
|
done()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -47,9 +47,20 @@ module.exports = Client = {
|
||||||
if (error != null) {
|
if (error != null) {
|
||||||
return callback(error)
|
return callback(error)
|
||||||
}
|
}
|
||||||
const secret = Settings.security.sessionSecret
|
Client.cookieSignedWith = {}
|
||||||
const cookieKey = 's:' + signature.sign(sessionId, secret)
|
// prepare cookie strings for all supported session secrets
|
||||||
Client.cookie = `${Settings.cookieName}=${cookieKey}`
|
for (const secretName of [
|
||||||
|
'sessionSecret',
|
||||||
|
'sessionSecretFallback',
|
||||||
|
'sessionSecretUpcoming',
|
||||||
|
]) {
|
||||||
|
const secret = Settings.security[secretName]
|
||||||
|
const cookieKey = 's:' + signature.sign(sessionId, secret)
|
||||||
|
Client.cookieSignedWith[secretName] =
|
||||||
|
`${Settings.cookieName}=${cookieKey}`
|
||||||
|
}
|
||||||
|
// default to the current session secret
|
||||||
|
Client.cookie = Client.cookieSignedWith.sessionSecret
|
||||||
return callback()
|
return callback()
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
|
@ -15,8 +15,13 @@ const modulePath = '../../../app/js/SessionSockets'
|
||||||
const sinon = require('sinon')
|
const sinon = require('sinon')
|
||||||
|
|
||||||
describe('SessionSockets', function () {
|
describe('SessionSockets', function () {
|
||||||
before(function () {
|
beforeEach(function () {
|
||||||
this.SessionSocketsModule = SandboxedModule.require(modulePath)
|
this.metrics = { inc: sinon.stub() }
|
||||||
|
this.SessionSocketsModule = SandboxedModule.require(modulePath, {
|
||||||
|
requires: {
|
||||||
|
'@overleaf/metrics': this.metrics,
|
||||||
|
},
|
||||||
|
})
|
||||||
this.io = new EventEmitter()
|
this.io = new EventEmitter()
|
||||||
this.id1 = Math.random().toString()
|
this.id1 = Math.random().toString()
|
||||||
this.id2 = Math.random().toString()
|
this.id2 = Math.random().toString()
|
||||||
|
@ -49,7 +54,7 @@ describe('SessionSockets', function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('without cookies', function () {
|
describe('without cookies', function () {
|
||||||
before(function () {
|
beforeEach(function () {
|
||||||
return (this.socket = { handshake: {} })
|
return (this.socket = { handshake: {} })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -61,16 +66,25 @@ describe('SessionSockets', function () {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
return it('should not query redis', function (done) {
|
it('should not query redis', function (done) {
|
||||||
return this.checkSocket(this.socket, () => {
|
return this.checkSocket(this.socket, () => {
|
||||||
expect(this.sessionStore.get.called).to.equal(false)
|
expect(this.sessionStore.get.called).to.equal(false)
|
||||||
return done()
|
return done()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should increment the session.cookie metric with status "none"', function (done) {
|
||||||
|
return this.checkSocket(this.socket, () => {
|
||||||
|
expect(this.metrics.inc).to.be.calledWith('session.cookie', 1, {
|
||||||
|
status: 'none',
|
||||||
|
})
|
||||||
|
return done()
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('with a different cookie', function () {
|
describe('with a different cookie', function () {
|
||||||
before(function () {
|
beforeEach(function () {
|
||||||
return (this.socket = { handshake: { _signedCookies: { other: 1 } } })
|
return (this.socket = { handshake: { _signedCookies: { other: 1 } } })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -82,7 +96,7 @@ describe('SessionSockets', function () {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
return it('should not query redis', function (done) {
|
it('should not query redis', function (done) {
|
||||||
return this.checkSocket(this.socket, () => {
|
return this.checkSocket(this.socket, () => {
|
||||||
expect(this.sessionStore.get.called).to.equal(false)
|
expect(this.sessionStore.get.called).to.equal(false)
|
||||||
return done()
|
return done()
|
||||||
|
@ -90,8 +104,40 @@ describe('SessionSockets', function () {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('with a cookie with an invalid signature', function () {
|
||||||
|
beforeEach(function () {
|
||||||
|
return (this.socket = {
|
||||||
|
handshake: { _signedCookies: { 'ol.sid': false } },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return a lookup error', function (done) {
|
||||||
|
return this.checkSocket(this.socket, error => {
|
||||||
|
expect(error).to.exist
|
||||||
|
expect(error.message).to.equal('could not look up session by key')
|
||||||
|
return done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not query redis', function (done) {
|
||||||
|
return this.checkSocket(this.socket, () => {
|
||||||
|
expect(this.sessionStore.get.called).to.equal(false)
|
||||||
|
return done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should increment the session.cookie metric with status=bad-signature', function (done) {
|
||||||
|
return this.checkSocket(this.socket, () => {
|
||||||
|
expect(this.metrics.inc).to.be.calledWith('session.cookie', 1, {
|
||||||
|
status: 'bad-signature',
|
||||||
|
})
|
||||||
|
return done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('with a valid cookie and a failing session lookup', function () {
|
describe('with a valid cookie and a failing session lookup', function () {
|
||||||
before(function () {
|
beforeEach(function () {
|
||||||
return (this.socket = {
|
return (this.socket = {
|
||||||
handshake: { _signedCookies: { 'ol.sid': 'error' } },
|
handshake: { _signedCookies: { 'ol.sid': 'error' } },
|
||||||
})
|
})
|
||||||
|
@ -104,17 +150,26 @@ describe('SessionSockets', function () {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
return it('should return a redis error', function (done) {
|
it('should return a redis error', function (done) {
|
||||||
return this.checkSocket(this.socket, error => {
|
return this.checkSocket(this.socket, error => {
|
||||||
expect(error).to.exist
|
expect(error).to.exist
|
||||||
expect(error.message).to.equal('Redis: something went wrong')
|
expect(error.message).to.equal('Redis: something went wrong')
|
||||||
return done()
|
return done()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should increment the session.cookie metric with status=error', function (done) {
|
||||||
|
return this.checkSocket(this.socket, () => {
|
||||||
|
expect(this.metrics.inc).to.be.calledWith('session.cookie', 1, {
|
||||||
|
status: 'error',
|
||||||
|
})
|
||||||
|
return done()
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('with a valid cookie and no matching session', function () {
|
describe('with a valid cookie and no matching session', function () {
|
||||||
before(function () {
|
beforeEach(function () {
|
||||||
return (this.socket = {
|
return (this.socket = {
|
||||||
handshake: { _signedCookies: { 'ol.sid': 'unknownId' } },
|
handshake: { _signedCookies: { 'ol.sid': 'unknownId' } },
|
||||||
})
|
})
|
||||||
|
@ -127,17 +182,26 @@ describe('SessionSockets', function () {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
return it('should return a lookup error', function (done) {
|
it('should return a lookup error', function (done) {
|
||||||
return this.checkSocket(this.socket, error => {
|
return this.checkSocket(this.socket, error => {
|
||||||
expect(error).to.exist
|
expect(error).to.exist
|
||||||
expect(error.message).to.equal('could not look up session by key')
|
expect(error.message).to.equal('could not look up session by key')
|
||||||
return done()
|
return done()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should increment the session.cookie metric with status=missing', function (done) {
|
||||||
|
return this.checkSocket(this.socket, () => {
|
||||||
|
expect(this.metrics.inc).to.be.calledWith('session.cookie', 1, {
|
||||||
|
status: 'missing',
|
||||||
|
})
|
||||||
|
return done()
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('with a valid cookie and a matching session', function () {
|
describe('with a valid cookie and a matching session', function () {
|
||||||
before(function () {
|
beforeEach(function () {
|
||||||
return (this.socket = {
|
return (this.socket = {
|
||||||
handshake: { _signedCookies: { 'ol.sid': this.id1 } },
|
handshake: { _signedCookies: { 'ol.sid': this.id1 } },
|
||||||
})
|
})
|
||||||
|
@ -157,17 +221,26 @@ describe('SessionSockets', function () {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
return it('should return the session', function (done) {
|
it('should return the session', function (done) {
|
||||||
return this.checkSocket(this.socket, (error, s, session) => {
|
return this.checkSocket(this.socket, (error, s, session) => {
|
||||||
if (error) return done(error)
|
if (error) return done(error)
|
||||||
expect(session).to.deep.equal({ user: { _id: '123' } })
|
expect(session).to.deep.equal({ user: { _id: '123' } })
|
||||||
return done()
|
return done()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should increment the session.cookie metric with status=signed', function (done) {
|
||||||
|
return this.checkSocket(this.socket, () => {
|
||||||
|
expect(this.metrics.inc).to.be.calledWith('session.cookie', 1, {
|
||||||
|
status: 'signed',
|
||||||
|
})
|
||||||
|
return done()
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
return describe('with a different valid cookie and matching session', function () {
|
describe('with a different valid cookie and matching session', function () {
|
||||||
before(function () {
|
beforeEach(function () {
|
||||||
return (this.socket = {
|
return (this.socket = {
|
||||||
handshake: { _signedCookies: { 'ol.sid': this.id2 } },
|
handshake: { _signedCookies: { 'ol.sid': this.id2 } },
|
||||||
})
|
})
|
||||||
|
@ -187,12 +260,21 @@ describe('SessionSockets', function () {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
return it('should return the other session', function (done) {
|
it('should return the other session', function (done) {
|
||||||
return this.checkSocket(this.socket, (error, s, session) => {
|
return this.checkSocket(this.socket, (error, s, session) => {
|
||||||
if (error) return done(error)
|
if (error) return done(error)
|
||||||
expect(session).to.deep.equal({ user: { _id: 'abc' } })
|
expect(session).to.deep.equal({ user: { _id: 'abc' } })
|
||||||
return done()
|
return done()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should increment the session.cookie metric with status=error', function (done) {
|
||||||
|
return this.checkSocket(this.socket, () => {
|
||||||
|
expect(this.metrics.inc).to.be.calledWith('session.cookie', 1, {
|
||||||
|
status: 'signed',
|
||||||
|
})
|
||||||
|
return done()
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in a new issue