UNPKG

hono-sess

Version:

A Simple Session Middleware for Hono

275 lines (274 loc) 10.2 kB
"use strict"; import { getSignedCookie, setSignedCookie } from "hono/cookie"; import crypto from "crypto"; import { Cookie } from "./cookie.js"; import { MemoryStore } from "./memory.js"; import { Session } from "./session.js"; import { Store } from "./store.js"; import { issecure, warning, debug, deprecate, hash, expressCookieOptionsToHonoCookieOptions, } from "./utils.js"; let env = process.env.NODE_ENV; export { Store, Cookie, Session, MemoryStore }; export * from "./types.js"; const session = ({ cookie = {}, genid = crypto.randomUUID, name = "connect.sid", store = new MemoryStore(), proxy = false, resave = false, rolling = false, saveUninitialized = true, secret = "dev-secret", unset = "keep", }) => { if (typeof genid !== "function") throw new TypeError("genid option must be a function"); if (resave === undefined) { deprecate("undefined resave option; provide resave option"); resave = true; } if (saveUninitialized === undefined) { deprecate("undefined saveUninitialized option; provide saveUninitialized option"); saveUninitialized = true; } if (unset !== "destroy" && unset !== "keep") throw new TypeError("unset option must be \"destroy\" or \"keep\""); const unsetDestroy = unset === "destroy"; if (Array.isArray(secret) && secret.length === 0) { throw new TypeError("secret option array must contain one or more strings"); } if (secret && !Array.isArray(secret)) secret = [secret]; if (!secret) deprecate("req.secret; provide secret option"); if (env === "production" && store instanceof MemoryStore) console.warn(warning); store.generate = function (req) { req.sessionID = genid(); req.session = new Session(req, null); req.session.cookie = new Cookie(cookie); req.raw.sessionID = req.sessionID; if (cookie.secure === "auto") { req.session.cookie.secure = issecure(req, proxy); } }; const storeImplementsTouch = typeof store.touch === "function"; let storeReady = true; store.on("disconnect", function ondisconnect() { storeReady = false; }); store.on("connect", function onconnect() { storeReady = true; }); return async function session(context, next) { const c = context; if (c.req.session) { return await next(); } if (!storeReady) { debug("store is disconnected"); return await next(); } if (c.req.path.indexOf(cookie.path || "/") !== 0) { debug("pathname mismatch"); return await next(); } if (!secret) { console.error("secret option required for sessions"); return await next(); } let secrets = secret; let cookieId = null; let originalHash = null; let originalId = null; let savedHash = null; let touched = false; c.req.sessionStore = store; const signedCookie = (((await getSignedCookie(c, secrets.join(" "), name)) || undefined)); cookieId = signedCookie; if (cookieId) { c.req.sessionID = cookieId; } const getNext = () => next().then(async (nextResult) => { if (shouldDestroy(c.req)) { debug("destroying"); await new Promise((resolve, reject) => { store.destroy(c.req.sessionID, (err) => { if (err) reject(err); debug("destroyed"); resolve(); }); }); } if (!c.req.session) { debug("no session at post next"); return nextResult; } if (!touched) { c.req.session.touch(); touched = true; } if (shouldSave(c.req)) { await new Promise((resolve, reject) => { c.req.session.save((err) => { if (err) { reject(err); } resolve(); }); }); } else if (storeImplementsTouch && shouldTouch(c.req)) { debug("touching"); await new Promise((resolve, reject) => { store.touch?.(c.req.sessionID, c.req.session, (err) => { if (err) { reject(err); } debug("touched"); resolve(); }); }); } await handleCookies(); return nextResult; }); const handleCookies = async () => { if (!c.req.session) { debug("no session"); return; } if (!shouldSetCookie(c.req)) { debug("should not set cookie"); return; } if (c.req.session.cookie.secure && !issecure(c.req, proxy)) { debug("not secured"); return; } if (!touched) { c.req.session.touch(); touched = true; debug("touched"); } try { debug("setting cookie"); const cookieData = c.req.session.cookie.data; await setSignedCookie(c, name, c.req.sessionID, secrets.join(" "), expressCookieOptionsToHonoCookieOptions(cookieData, c.req, proxy)); return; } catch (err) { console.error(err); return; } }; function generate() { debug("generating"); store.generate(c.req); originalId = c.req.sessionID; originalHash = hash(c.req.session); wrapmethods(c.req.session); } function inflate(req, session) { debug("inflating"); store.createSession(req, session); originalId = req.sessionID; originalHash = hash(session); if (!resave) { savedHash = originalHash; } wrapmethods(session); } function rewrapmethods(session, callback) { debug("rewrapmethods"); return function () { if (c.req.session !== session) { wrapmethods(c.req.session); } callback.apply(this, arguments); }; } function wrapmethods(session) { const _reload = session.reload; const _save = session.save; function reload(callback) { debug("reloading %s", session.id); _reload.call(session, rewrapmethods(session, callback)); } function save() { debug("saving %s", session.id); savedHash = hash(session); _save.apply(session, arguments); } Object.defineProperty(session, "reload", { configurable: true, enumerable: false, value: reload, writable: true, }); Object.defineProperty(session, "save", { configurable: true, enumerable: false, value: save, writable: true, }); } function isModified(session) { return originalId !== session.id || originalHash !== hash(session); } function isSaved(session) { return originalId === session.id && savedHash === hash(session); } function shouldDestroy(req) { return req.sessionID && unsetDestroy && req.session == null; } function shouldSave(req) { if (typeof req.sessionID !== "string") { debug("session ignored because of bogus req.sessionID %o", req.sessionID); return false; } return !saveUninitialized && !savedHash && cookieId !== req.sessionID ? isModified(req.session) : !isSaved(req.session); } function shouldTouch(req) { if (typeof req.sessionID !== "string") { debug("session ignored because of bogus req.sessionID %o", req.sessionID); return false; } return cookieId === req.sessionID && !shouldSave(req); } function shouldSetCookie(req) { if (typeof req.sessionID !== "string") { debug("session ignored because of bogus req.sessionID %o"); return false; } return cookieId !== req.sessionID ? saveUninitialized || isModified(req.session) : rolling || (req.session.cookie.expires != null && isModified(req.session)); } if (!c.req.sessionID) { debug("no SID sent, generating session"); generate(); return getNext(); } debug("fetching %s", c.req.sessionID); return new Promise((resolve) => { store.get(c.req.sessionID, (err, session) => { if (err && err.code !== "ENOENT") { debug("error %j", err); resolve(getNext()); return; } try { if (err || !session) { debug("no session found"); generate(); } else { debug("session found"); inflate(c.req, session); } } catch (e) { console.error(e); resolve(getNext()); return; } resolve(getNext()); }); }); }; }; export default session;