From 4d3b3d17400cfc032f45015c5bd7011a7779b042 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 4 Aug 2024 12:00:10 -0500 Subject: [PATCH] Update:Replace default express-session MemoryStore with stable MemoryStore #2538 --- server/Server.js | 8 +- server/libs/memorystore/LICENSE | 21 +++ server/libs/memorystore/index.js | 303 +++++++++++++++++++++++++++++++ 3 files changed, 331 insertions(+), 1 deletion(-) create mode 100644 server/libs/memorystore/LICENSE create mode 100644 server/libs/memorystore/index.js diff --git a/server/Server.js b/server/Server.js index 7a2761bd..ed1f5770 100644 --- a/server/Server.js +++ b/server/Server.js @@ -41,6 +41,7 @@ const LibraryScanner = require('./scanner/LibraryScanner') //Import the main Passport and Express-Session library const passport = require('passport') const expressSession = require('express-session') +const MemoryStore = require('./libs/memorystore')(expressSession) class Server { constructor(SOURCE, PORT, HOST, CONFIG_PATH, METADATA_PATH, ROUTER_BASE_PATH) { @@ -216,7 +217,12 @@ class Server { cookie: { // also send the cookie if were are not on https (not every use has https) secure: false - } + }, + store: new MemoryStore({ + checkPeriod: 86400000, // prune expired entries every 24h + ttl: 86400000, // 24h + max: 1000 + }) }) ) // init passport.js diff --git a/server/libs/memorystore/LICENSE b/server/libs/memorystore/LICENSE new file mode 100644 index 00000000..f8855e7e --- /dev/null +++ b/server/libs/memorystore/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Rocco Musolino + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/server/libs/memorystore/index.js b/server/libs/memorystore/index.js new file mode 100644 index 00000000..b17e8813 --- /dev/null +++ b/server/libs/memorystore/index.js @@ -0,0 +1,303 @@ +/*! + * memorystore + * Copyright(c) 2020 Rocco Musolino <@roccomuso> + * MIT Licensed + */ +// +// modified for audiobookshelf (update to lru-cache 10) +// SOURCE: https://github.com/roccomuso/memorystore +// + +var debug = require('debug')('memorystore') +const { LRUCache } = require('lru-cache') +var util = require('util') + +/** + * One day in milliseconds. + */ + +var oneDay = 86400000 + +function getTTL(options, sess, sid) { + if (typeof options.ttl === 'number') return options.ttl + if (typeof options.ttl === 'function') return options.ttl(options, sess, sid) + if (options.ttl) throw new TypeError('`options.ttl` must be a number or function.') + + var maxAge = sess?.cookie?.maxAge || null + return typeof maxAge === 'number' ? Math.floor(maxAge) : oneDay +} + +function prune(store) { + debug('Pruning expired entries') + store.forEach(function (value, key) { + store.get(key) + }) +} + +var defer = + typeof setImmediate === 'function' + ? setImmediate + : function (fn) { + process.nextTick(fn.bind.apply(fn, arguments)) + } + +/** + * Return the `MemoryStore` extending `express`'s session Store. + * + * @param {object} express session + * @return {Function} + * @api public + */ + +module.exports = function (session) { + /** + * Express's session Store. + */ + + var Store = session.Store + + /** + * Initialize MemoryStore with the given `options`. + * + * @param {Object} options + * @api public + */ + + function MemoryStore(options) { + if (!(this instanceof MemoryStore)) { + throw new TypeError('Cannot call MemoryStore constructor as a function') + } + + options = options || {} + Store.call(this, options) + + this.options = {} + this.options.checkPeriod = options.checkPeriod + this.options.max = options.max + this.options.ttl = options.ttl + this.options.dispose = options.dispose + this.options.stale = options.stale + + this.serializer = options.serializer || JSON + this.store = new LRUCache(this.options) + debug('Init MemoryStore') + + this.startInterval() + } + + /** + * Inherit from `Store`. + */ + + util.inherits(MemoryStore, Store) + + /** + * Attempt to fetch session by the given `sid`. + * + * @param {String} sid + * @param {Function} fn + * @api public + */ + + MemoryStore.prototype.get = function (sid, fn) { + var store = this.store + + debug('GET "%s"', sid) + + var data = store.get(sid) + if (!data) return fn() + + debug('GOT %s', data) + var err = null + var result + try { + result = this.serializer.parse(data) + } catch (er) { + err = er + } + + fn && defer(fn, err, result) + } + + /** + * Commit the given `sess` object associated with the given `sid`. + * + * @param {String} sid + * @param {Session} sess + * @param {Function} fn + * @api public + */ + + MemoryStore.prototype.set = function (sid, sess, fn) { + var store = this.store + + var ttl = getTTL(this.options, sess, sid) + try { + var jsess = this.serializer.stringify(sess) + } catch (err) { + fn && defer(fn, err) + } + + store.set(sid, jsess, { + ttl + }) + debug('SET "%s" %s ttl:%s', sid, jsess, ttl) + fn && defer(fn, null) + } + + /** + * Destroy the session associated with the given `sid`. + * + * @param {String} sid + * @api public + */ + + MemoryStore.prototype.destroy = function (sid, fn) { + var store = this.store + + if (Array.isArray(sid)) { + sid.forEach(function (s) { + debug('DEL "%s"', s) + store.delete(s) + }) + } else { + debug('DEL "%s"', sid) + store.delete(sid) + } + fn && defer(fn, null) + } + + /** + * Refresh the time-to-live for the session with the given `sid`. + * + * @param {String} sid + * @param {Session} sess + * @param {Function} fn + * @api public + */ + + MemoryStore.prototype.touch = function (sid, sess, fn) { + var store = this.store + + var ttl = getTTL(this.options, sess, sid) + + debug('EXPIRE "%s" ttl:%s', sid, ttl) + var err = null + if (store.get(sid) !== undefined) { + try { + var s = this.serializer.parse(store.get(sid)) + s.cookie = sess.cookie + store.set(sid, this.serializer.stringify(s), { + ttl + }) + } catch (e) { + err = e + } + } + fn && defer(fn, err) + } + + /** + * Fetch all sessions' ids + * + * @param {Function} fn + * @api public + */ + + MemoryStore.prototype.ids = function (fn) { + var store = this.store + + var Ids = store.keys() + debug('Getting IDs: %s', Ids) + fn && defer(fn, null, Ids) + } + + /** + * Fetch all sessions + * + * @param {Function} fn + * @api public + */ + + MemoryStore.prototype.all = function (fn) { + var store = this.store + var self = this + + debug('Fetching all sessions') + var err = null + var result = {} + try { + store.forEach(function (val, key) { + result[key] = self.serializer.parse(val) + }) + } catch (e) { + err = e + } + fn && defer(fn, err, result) + } + + /** + * Delete all sessions from the store + * + * @param {Function} fn + * @api public + */ + + MemoryStore.prototype.clear = function (fn) { + var store = this.store + debug('delete all sessions from the store') + store.clear() + fn && defer(fn, null) + } + + /** + * Get the count of all sessions in the store + * + * @param {Function} fn + * @api public + */ + + MemoryStore.prototype.length = function (fn) { + var store = this.store + debug('getting length', store.size) + fn && defer(fn, null, store.size) + } + + /** + * Start the check interval + * @api public + */ + + MemoryStore.prototype.startInterval = function () { + var self = this + var ms = this.options.checkPeriod + if (ms && typeof ms === 'number') { + clearInterval(this._checkInterval) + debug('starting periodic check for expired sessions') + this._checkInterval = setInterval(function () { + prune(self.store) // iterates over the entire cache proactively pruning old entries + }, Math.floor(ms)).unref() + } + } + + /** + * Stop the check interval + * @api public + */ + + MemoryStore.prototype.stopInterval = function () { + debug('stopping periodic check for expired sessions') + clearInterval(this._checkInterval) + } + + /** + * Remove only expired entries from the store + * @api public + */ + + MemoryStore.prototype.prune = function () { + prune(this.store) + } + + return MemoryStore +}