UNPKG

5.48 kBJavaScriptView Raw
1/*!
2 * cookie-session
3 * Copyright(c) 2013 Jonathan Ong
4 * Copyright(c) 2014-2017 Douglas Christopher Wilson
5 * MIT Licensed
6 */
7
8'use strict'
9
10/**
11 * Module dependencies.
12 * @private
13 */
14
15var Buffer = require('safe-buffer').Buffer
16var debug = require('debug')('cookie-session')
17var Cookies = require('cookies')
18var onHeaders = require('on-headers')
19
20/**
21 * Module exports.
22 * @public
23 */
24
25module.exports = cookieSession
26
27/**
28 * Create a new cookie session middleware.
29 *
30 * @param {object} [options]
31 * @param {boolean} [options.httpOnly=true]
32 * @param {array} [options.keys]
33 * @param {string} [options.name=session] Name of the cookie to use
34 * @param {boolean} [options.overwrite=true]
35 * @param {string} [options.secret]
36 * @param {boolean} [options.signed=true]
37 * @return {function} middleware
38 * @public
39 */
40
41function cookieSession (options) {
42 var opts = options || {}
43
44 // cookie name
45 var name = opts.name || 'session'
46
47 // secrets
48 var keys = opts.keys
49 if (!keys && opts.secret) keys = [opts.secret]
50
51 // defaults
52 if (opts.overwrite == null) opts.overwrite = true
53 if (opts.httpOnly == null) opts.httpOnly = true
54 if (opts.signed == null) opts.signed = true
55
56 if (!keys && opts.signed) throw new Error('.keys required.')
57
58 debug('session options %j', opts)
59
60 return function _cookieSession (req, res, next) {
61 var cookies = new Cookies(req, res, {
62 keys: keys
63 })
64 var sess
65
66 // for overriding
67 req.sessionOptions = Object.create(opts)
68
69 // define req.session getter / setter
70 Object.defineProperty(req, 'session', {
71 configurable: true,
72 enumerable: true,
73 get: getSession,
74 set: setSession
75 })
76
77 function getSession () {
78 // already retrieved
79 if (sess) {
80 return sess
81 }
82
83 // unset
84 if (sess === false) {
85 return null
86 }
87
88 // get session
89 if ((sess = tryGetSession(cookies, name, req.sessionOptions))) {
90 return sess
91 }
92
93 // create session
94 debug('new session')
95 return (sess = Session.create())
96 }
97
98 function setSession (val) {
99 if (val == null) {
100 // unset session
101 sess = false
102 return val
103 }
104
105 if (typeof val === 'object') {
106 // create a new session
107 sess = Session.create(val)
108 return sess
109 }
110
111 throw new Error('req.session can only be set as null or an object.')
112 }
113
114 onHeaders(res, function setHeaders () {
115 if (sess === undefined) {
116 // not accessed
117 return
118 }
119
120 try {
121 if (sess === false) {
122 // remove
123 debug('remove %s', name)
124 cookies.set(name, '', req.sessionOptions)
125 } else if ((!sess.isNew || sess.isPopulated) && sess.isChanged) {
126 // save populated or non-new changed session
127 debug('save %s', name)
128 cookies.set(name, Session.serialize(sess), req.sessionOptions)
129 }
130 } catch (e) {
131 debug('error saving session %s', e.message)
132 }
133 })
134
135 next()
136 }
137};
138
139/**
140 * Session model.
141 *
142 * @param {Context} ctx
143 * @param {Object} obj
144 * @private
145 */
146
147function Session (ctx, obj) {
148 Object.defineProperty(this, '_ctx', {
149 value: ctx
150 })
151
152 if (obj) {
153 for (var key in obj) {
154 if (!(key in this)) {
155 this[key] = obj[key]
156 }
157 }
158 }
159}
160
161/**
162 * Create new session.
163 * @private
164 */
165
166Session.create = function create (obj) {
167 var ctx = new SessionContext()
168 return new Session(ctx, obj)
169}
170
171/**
172 * Create session from serialized form.
173 * @private
174 */
175
176Session.deserialize = function deserialize (str) {
177 var ctx = new SessionContext()
178 var obj = decode(str)
179
180 ctx._new = false
181 ctx._val = str
182
183 return new Session(ctx, obj)
184}
185
186/**
187 * Serialize a session to a string.
188 * @private
189 */
190
191Session.serialize = function serialize (sess) {
192 return encode(sess)
193}
194
195/**
196 * Return if the session is changed for this request.
197 *
198 * @return {Boolean}
199 * @public
200 */
201
202Object.defineProperty(Session.prototype, 'isChanged', {
203 get: function getIsChanged () {
204 return this._ctx._new || this._ctx._val !== Session.serialize(this)
205 }
206})
207
208/**
209 * Return if the session is new for this request.
210 *
211 * @return {Boolean}
212 * @public
213 */
214
215Object.defineProperty(Session.prototype, 'isNew', {
216 get: function getIsNew () {
217 return this._ctx._new
218 }
219})
220
221/**
222 * populated flag, which is just a boolean alias of .length.
223 *
224 * @return {Boolean}
225 * @public
226 */
227
228Object.defineProperty(Session.prototype, 'isPopulated', {
229 get: function getIsPopulated () {
230 return Object.keys(this).length > 0
231 }
232})
233
234/**
235 * Session context to store metadata.
236 *
237 * @private
238 */
239
240function SessionContext () {
241 this._new = true
242 this._val = undefined
243}
244
245/**
246 * Decode the base64 cookie value to an object.
247 *
248 * @param {String} string
249 * @return {Object}
250 * @private
251 */
252
253function decode (string) {
254 var body = Buffer.from(string, 'base64').toString('utf8')
255 return JSON.parse(body)
256}
257
258/**
259 * Encode an object into a base64-encoded JSON string.
260 *
261 * @param {Object} body
262 * @return {String}
263 * @private
264 */
265
266function encode (body) {
267 var str = JSON.stringify(body)
268 return Buffer.from(str).toString('base64')
269}
270
271/**
272 * Try getting a session from a cookie.
273 * @private
274 */
275
276function tryGetSession (cookies, name, opts) {
277 var str = cookies.get(name, opts)
278
279 if (!str) {
280 return undefined
281 }
282
283 debug('parse %s', str)
284
285 try {
286 return Session.deserialize(str)
287 } catch (err) {
288 return undefined
289 }
290}