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
							 |