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
						
					
					
				| '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
 |