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 |