Skip to content

Commit

Permalink
feat: only save session to storage when it changes (#80)
Browse files Browse the repository at this point in the history
* feat: only save session to storage when it changes

* use safe-stable-stringify

* stringify is prod dep
  • Loading branch information
SimenB authored Apr 7, 2022
1 parent 12f4ae6 commit bfc08dd
Show file tree
Hide file tree
Showing 6 changed files with 90 additions and 6 deletions.
6 changes: 1 addition & 5 deletions lib/fastifySession.js
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ function isConnectionEncrypted (request) {
}

function shouldSaveSession (request, cookieOpts, saveUninitialized) {
if (!saveUninitialized && !isSessionModified(request.session)) {
if (!saveUninitialized && !request.session.isModified()) {
return false
}
if (cookieOpts.secure !== true || cookieOpts.secure === 'auto') {
Expand All @@ -222,10 +222,6 @@ function shouldSaveSession (request, cookieOpts, saveUninitialized) {
return forwardedProto === 'https'
}

function isSessionModified (session) {
return (Object.keys(session).length !== 4)
}

function option (options, key, def) {
return options[key] === undefined ? def : options[key]
}
Expand Down
30 changes: 30 additions & 0 deletions lib/session.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
'use strict'

const crypto = require('crypto')

const Cookie = require('./cookie')
const cookieSignature = require('cookie-signature')
const { configure: configureStringifier } = require('safe-stable-stringify')

const stringify = configureStringifier({ bigint: false })

const maxAge = Symbol('maxAge')
const secretKey = Symbol('secretKey')
Expand All @@ -10,6 +15,8 @@ const addDataToSession = Symbol('addDataToSession')
const generateId = Symbol('generateId')
const requestKey = Symbol('request')
const cookieOptsKey = Symbol('cookieOpts')
const originalHash = Symbol('originalHash')
const hash = Symbol('hash')

module.exports = class Session {
constructor (request, idGenerator, cookieOpts, secret, prevSession = {}) {
Expand All @@ -26,6 +33,7 @@ module.exports = class Session {
this.sessionId = this[generateId](this[requestKey])
this.encryptedSessionId = this[sign]()
}
this[originalHash] = this[hash]()
}

touch () {
Expand Down Expand Up @@ -143,12 +151,34 @@ module.exports = class Session {
return cookieSignature.sign(this.sessionId, this[secretKey])
}

[hash] () {
const sess = this
const str = stringify(sess, function (key, val) {
// ignore sess.cookie property
if (this === sess && key === 'cookie') {
return
}

return val
})

return crypto
.createHash('sha1')
.update(str, 'utf8')
.digest('hex')
}

isModified () {
return this[originalHash] !== this[hash]()
}

static restore (request, idGenerator, cookieOpts, secret, prevSession) {
const restoredSession = new Session(request, idGenerator, cookieOpts, secret, prevSession)
const restoredCookie = new Cookie(cookieOpts)
restoredCookie.expires = new Date(prevSession.cookie.expires)
restoredSession.cookie = restoredCookie
restoredSession.expires = restoredCookie.expires
restoredSession[originalHash] = restoredSession[hash]()
return restoredSession
}
}
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"dependencies": {
"cookie-signature": "^1.1.0",
"fastify-plugin": "^3.0.0",
"safe-stable-stringify": "^2.3.1",
"uid-safe": "^2.1.5"
},
"engines": {
Expand All @@ -41,6 +42,7 @@
"published-session": "npm:@fastify/session@latest",
"redis": "^4.0.6",
"session-file-store": "^1.5.0",
"sinon": "^13.0.1",
"standard": "^16.0.1",
"tsd": "^0.20.0",
"typescript": "^4.0.2",
Expand Down
54 changes: 53 additions & 1 deletion test/session.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
const test = require('ava')
const Fastify = require('fastify')
const fastifyCookie = require('fastify-cookie')
const sinon = require('sinon')
const fastifySession = require('..')
const { request, testServer, DEFAULT_OPTIONS, DEFAULT_COOKIE, DEFAULT_COOKIE_VALUE } = require('./util')

Expand Down Expand Up @@ -667,7 +668,7 @@ test('save supports rejecting promises', async t => {
store: {
set (id, data, cb) { cb(new Error('no can do')) },
get (id, cb) { cb(null) },
save (id, cb) { cb(null) }
destroy (id, cb) { cb(null) }
}
})

Expand Down Expand Up @@ -706,3 +707,54 @@ test('does not clear cookie if no session cookie in request', async t => {
t.is(response.statusCode, 200)
t.is(cookie, undefined)
})

test('only save session when it changes', async t => {
t.plan(6)
const setStub = sinon.stub()
const store = new Map()

const fastify = Fastify()
fastify.register(fastifyCookie)

fastify.register(fastifySession, {
...DEFAULT_OPTIONS,
saveUninitialized: false,
cookie: { secure: false },
store: {
set (id, data, cb) {
setStub()
store.set(id, data)
cb(null)
},
get (id, cb) { cb(null, store.get(id)) },
destroy (id, cb) {
store.delete(id)
cb(null)
}
}
})

fastify.get('/', (request, reply) => {
request.session.userId = 42

reply.send(200)
})

const { statusCode: statusCode1, headers: headers1 } = await fastify.inject('/')
const setCookieHeader1 = headers1['set-cookie']

t.is(statusCode1, 200)
t.is(setStub.callCount, 1)
t.is(typeof setCookieHeader1, 'string')

const { sessionId } = fastify.parseCookie(setCookieHeader1)

const { statusCode: statusCode2, headers: headers2 } = await fastify.inject({ path: '/', headers: { cookie: `sessionId=${sessionId}` } })
const setCookieHeader2 = headers2['set-cookie']

t.is(statusCode2, 200)
// still only called once
t.is(setStub.callCount, 1)
// no set-cookie
t.is(setCookieHeader2, undefined)
})
3 changes: 3 additions & 0 deletions types/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ interface SessionData extends ExpressSessionData {

/** gets values from the session. */
get<T>(key: string): T;

/** checks if session has been modified since it was generated or loaded from the store. */
isModified(): boolean;
}

interface ExpressSessionData {
Expand Down
1 change: 1 addition & 0 deletions types/types.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ app.route({
expectType<void>(request.session.set('foo', 'bar'));
expectType<string>(request.session.get('foo'));
expectType<void>(request.session.touch());
expectType<boolean>(request.session.isModified());
expectType<void>(request.session.reload(() => {}));
expectType<void>(request.session.destroy(() => {}));
expectType<void>(request.session.regenerate(() => {}));
Expand Down

0 comments on commit bfc08dd

Please sign in to comment.