You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
243 lines
6.6 KiB
243 lines
6.6 KiB
3 years ago
|
'use strict'
|
||
|
|
||
|
const uid = require('uid-safe').sync
|
||
|
const fastifyPlugin = require('fastify-plugin')
|
||
|
const Store = require('./store')
|
||
|
const Session = require('./session')
|
||
|
const metadata = require('./metadata')
|
||
|
const cookieSignature = require('cookie-signature')
|
||
|
|
||
|
function session (fastify, options, next) {
|
||
|
const error = checkOptions(options)
|
||
|
if (error) {
|
||
|
return next(error)
|
||
|
}
|
||
|
|
||
|
options = ensureDefaults(options)
|
||
|
|
||
|
// Decorator function takes cookieOpts so we can customize on per-session basis.
|
||
|
// Note: method is deprecated, avoid using unless you need this functionality.
|
||
|
fastify.decorate('decryptSession', (sessionId, request, cookieOpts, callback) => {
|
||
|
if (typeof cookieOpts === 'function') {
|
||
|
callback = cookieOpts
|
||
|
cookieOpts = {}
|
||
|
}
|
||
|
|
||
|
const cookie = { ...options.cookie, ...cookieOpts }
|
||
|
decryptSession(sessionId, { ...options, cookie }, request, callback)
|
||
|
})
|
||
|
fastify.decorateRequest('sessionStore', { getter: () => options.store })
|
||
|
fastify.decorateRequest('session', null)
|
||
|
fastify.addHook('onRequest', onRequest(options))
|
||
|
fastify.addHook('onSend', onSend(options))
|
||
|
next()
|
||
|
}
|
||
|
|
||
|
function decryptSession (sessionId, options, request, done) {
|
||
|
const cookieOpts = options.cookie
|
||
|
const idGenerator = options.idGenerator
|
||
|
const secrets = options.secret
|
||
|
const secretsLength = secrets.length
|
||
|
const secret = secrets[0]
|
||
|
|
||
|
let decryptedSessionId = false
|
||
|
for (let i = 0; i < secretsLength; ++i) {
|
||
|
decryptedSessionId = cookieSignature.unsign(sessionId, secrets[i])
|
||
|
if (decryptedSessionId !== false) {
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
if (decryptedSessionId === false) {
|
||
|
newSession(secret, request, cookieOpts, idGenerator, done)
|
||
|
} else {
|
||
|
options.store.get(decryptedSessionId, (err, session) => {
|
||
|
if (err) {
|
||
|
if (err.code === 'ENOENT') {
|
||
|
newSession(secret, request, cookieOpts, idGenerator, done)
|
||
|
} else {
|
||
|
done(err)
|
||
|
}
|
||
|
return
|
||
|
}
|
||
|
if (!session) {
|
||
|
newSession(secret, request, cookieOpts, idGenerator, done)
|
||
|
return
|
||
|
}
|
||
|
if (session && session.expires && session.expires <= Date.now()) {
|
||
|
const restoredSession = Session.restore(
|
||
|
request,
|
||
|
idGenerator,
|
||
|
cookieOpts,
|
||
|
secret,
|
||
|
session
|
||
|
)
|
||
|
|
||
|
restoredSession.destroy(err => {
|
||
|
if (err) {
|
||
|
done(err)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
restoredSession.regenerate(done)
|
||
|
})
|
||
|
return
|
||
|
}
|
||
|
if (options.rolling) {
|
||
|
request.session = new Session(
|
||
|
request,
|
||
|
idGenerator,
|
||
|
cookieOpts,
|
||
|
secret,
|
||
|
session
|
||
|
)
|
||
|
} else {
|
||
|
request.session = Session.restore(
|
||
|
request,
|
||
|
idGenerator,
|
||
|
cookieOpts,
|
||
|
secret,
|
||
|
session
|
||
|
)
|
||
|
}
|
||
|
done()
|
||
|
})
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function onRequest (options) {
|
||
|
const unsignSignedCookie = options.unsignSignedCookie
|
||
|
const cookieOpts = options.cookie
|
||
|
const idGenerator = options.idGenerator
|
||
|
return function handleSession (request, reply, done) {
|
||
|
request.session = {}
|
||
|
|
||
|
const url = request.raw.url
|
||
|
if (url.indexOf(cookieOpts.path || '/') !== 0) {
|
||
|
done()
|
||
|
return
|
||
|
}
|
||
|
const sessionId = request.cookies[options.cookieName]
|
||
|
const secret = options.secret[0]
|
||
|
if (!sessionId) {
|
||
|
newSession(secret, request, cookieOpts, idGenerator, done)
|
||
|
} else {
|
||
|
let sessionToDecrypt = sessionId
|
||
|
|
||
|
if (unsignSignedCookie) {
|
||
|
const unsignedCookie = reply.unsignCookie(sessionId)
|
||
|
if (unsignedCookie.valid) {
|
||
|
sessionToDecrypt = unsignedCookie.value
|
||
|
}
|
||
|
}
|
||
|
|
||
|
decryptSession(sessionToDecrypt, options, request, done)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function onSend (options) {
|
||
|
return function saveSession (request, reply, payload, done) {
|
||
|
const session = request.session
|
||
|
if (!session || !session.sessionId) {
|
||
|
done()
|
||
|
return
|
||
|
}
|
||
|
|
||
|
if (!shouldSaveSession(request, options.cookie, options.saveUninitialized)) {
|
||
|
// if a session cookie is set, but has a different ID, clear it
|
||
|
if (request.cookies[options.cookieName] && request.cookies[options.cookieName] !== session.encryptedSessionId) {
|
||
|
reply.clearCookie(options.cookieName)
|
||
|
}
|
||
|
done()
|
||
|
return
|
||
|
}
|
||
|
session.save((err) => {
|
||
|
if (err) {
|
||
|
done(err)
|
||
|
return
|
||
|
}
|
||
|
reply.setCookie(
|
||
|
options.cookieName,
|
||
|
session.encryptedSessionId,
|
||
|
session.cookie.options(isConnectionSecure(request))
|
||
|
)
|
||
|
done()
|
||
|
})
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function newSession (secret, request, cookieOpts, idGenerator, done) {
|
||
|
request.session = new Session(request, idGenerator, cookieOpts, secret)
|
||
|
done()
|
||
|
}
|
||
|
|
||
|
function checkOptions (options) {
|
||
|
if (!options.secret) {
|
||
|
return new Error('the secret option is required!')
|
||
|
}
|
||
|
if (typeof options.secret === 'string' && options.secret.length < 32) {
|
||
|
return new Error('the secret must have length 32 or greater')
|
||
|
}
|
||
|
if (Array.isArray(options.secret) && options.secret.length === 0) {
|
||
|
return new Error('at least one secret is required')
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function idGenerator () {
|
||
|
return uid(24)
|
||
|
}
|
||
|
|
||
|
function ensureDefaults (options) {
|
||
|
options.store = options.store || new Store()
|
||
|
options.idGenerator = options.idGenerator || idGenerator
|
||
|
options.cookieName = options.cookieName || 'sessionId'
|
||
|
options.unsignSignedCookie = options.unsignSignedCookie || false
|
||
|
options.cookie = options.cookie || {}
|
||
|
options.cookie.secure = option(options.cookie, 'secure', true)
|
||
|
options.rolling = option(options, 'rolling', true)
|
||
|
options.saveUninitialized = option(options, 'saveUninitialized', true)
|
||
|
options.secret = Array.isArray(options.secret) ? options.secret : [options.secret]
|
||
|
return options
|
||
|
}
|
||
|
|
||
|
function getRequestProto (request) {
|
||
|
return request.headers['x-forwarded-proto'] || 'http'
|
||
|
}
|
||
|
|
||
|
function isConnectionSecure (request) {
|
||
|
if (isConnectionEncrypted(request)) {
|
||
|
return true
|
||
|
}
|
||
|
return getRequestProto(request) === 'https'
|
||
|
}
|
||
|
|
||
|
function isConnectionEncrypted (request) {
|
||
|
const socket = request.raw.socket
|
||
|
if (socket && socket.encrypted === true) {
|
||
|
return true
|
||
|
}
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
function shouldSaveSession (request, cookieOpts, saveUninitialized) {
|
||
|
if (!saveUninitialized && !request.session.isModified()) {
|
||
|
return false
|
||
|
}
|
||
|
if (cookieOpts.secure !== true || cookieOpts.secure === 'auto') {
|
||
|
return true
|
||
|
}
|
||
|
if (isConnectionEncrypted(request)) {
|
||
|
return true
|
||
|
}
|
||
|
const forwardedProto = getRequestProto(request)
|
||
|
return forwardedProto === 'https'
|
||
|
}
|
||
|
|
||
|
function option (options, key, def) {
|
||
|
return options[key] === undefined ? def : options[key]
|
||
|
}
|
||
|
|
||
|
exports = module.exports = fastifyPlugin(session, metadata)
|
||
|
module.exports.Store = Store
|
||
|
module.exports.MemoryStore = Store
|