From 338b9be506a806470ca161d417cc82508735a449 Mon Sep 17 00:00:00 2001 From: garrettmills Date: Sun, 7 Mar 2021 13:26:14 -0600 Subject: [PATCH] Add basic memory-based session driver --- .../kernel/module/InjectSessionHTTPModule.ts | 30 +++++++++ .../module/PersistSessionHTTPMiddleware.ts | 18 ++++++ .../module/SetSessionCookieHTTPModule.ts | 2 +- src/http/session/MemorySession.ts | 61 +++++++++++++++++++ src/http/session/Session.ts | 39 ++++++++++++ src/http/session/SessionFactory.ts | 49 +++++++++++++++ src/service/HTTPServer.ts | 4 ++ 7 files changed, 202 insertions(+), 1 deletion(-) create mode 100644 src/http/kernel/module/InjectSessionHTTPModule.ts create mode 100644 src/http/kernel/module/PersistSessionHTTPMiddleware.ts create mode 100644 src/http/session/MemorySession.ts create mode 100644 src/http/session/Session.ts create mode 100644 src/http/session/SessionFactory.ts diff --git a/src/http/kernel/module/InjectSessionHTTPModule.ts b/src/http/kernel/module/InjectSessionHTTPModule.ts new file mode 100644 index 0000000..bd92ab9 --- /dev/null +++ b/src/http/kernel/module/InjectSessionHTTPModule.ts @@ -0,0 +1,30 @@ +import {HTTPKernelModule} from "../HTTPKernelModule"; +import {Injectable} from "@extollo/di" +import {ErrorWithContext} from "@extollo/util" +import {HTTPKernel} from "../HTTPKernel"; +import {Request} from "../../lifecycle/Request"; +import {SetSessionCookieHTTPModule} from "./SetSessionCookieHTTPModule"; +import {SessionFactory} from "../../session/SessionFactory"; +import {Session} from "../../session/Session"; + +@Injectable() +export class InjectSessionHTTPModule extends HTTPKernelModule { + public static register(kernel: HTTPKernel) { + kernel.register(this).after(SetSessionCookieHTTPModule) + } + + public async apply(request: Request) { + request.registerFactory(new SessionFactory()) + + const session = request.make(Session) + const id = request.cookies.get('extollo.session') + if ( !id ) { + throw new ErrorWithContext('Session ID has not been set. Cannot inject session!') + } + + session.setKey(id.value) + await session.load() + + return request + } +} diff --git a/src/http/kernel/module/PersistSessionHTTPMiddleware.ts b/src/http/kernel/module/PersistSessionHTTPMiddleware.ts new file mode 100644 index 0000000..b9d7772 --- /dev/null +++ b/src/http/kernel/module/PersistSessionHTTPMiddleware.ts @@ -0,0 +1,18 @@ +import {HTTPKernelModule} from "../HTTPKernelModule"; +import {Injectable} from "@extollo/di" +import {HTTPKernel} from "../HTTPKernel"; +import {Request} from "../../lifecycle/Request"; +import {Session} from "../../session/Session"; + +@Injectable() +export class PersistSessionHTTPMiddleware extends HTTPKernelModule { + public static register(kernel: HTTPKernel) { + kernel.register(this).last() + } + + public async apply(request: Request): Promise { + const session = request.make(Session) + await session.persist() + return request + } +} diff --git a/src/http/kernel/module/SetSessionCookieHTTPModule.ts b/src/http/kernel/module/SetSessionCookieHTTPModule.ts index cfb8f55..f3570e2 100644 --- a/src/http/kernel/module/SetSessionCookieHTTPModule.ts +++ b/src/http/kernel/module/SetSessionCookieHTTPModule.ts @@ -11,7 +11,7 @@ export class SetSessionCookieHTTPModule extends HTTPKernelModule { protected readonly logging!: Logging public static register(kernel: HTTPKernel) { - kernel.register(this).first() // TODO make this before inject session? + kernel.register(this).first() } public async apply(request: Request) { diff --git a/src/http/session/MemorySession.ts b/src/http/session/MemorySession.ts new file mode 100644 index 0000000..c28cae1 --- /dev/null +++ b/src/http/session/MemorySession.ts @@ -0,0 +1,61 @@ +import {NoSessionKeyError, Session, SessionData, SessionNotLoadedError} from "./Session"; +import {Injectable} from "@extollo/di"; + +@Injectable() +export class MemorySession extends Session { + private static sessionsByID: {[key: string]: SessionData} = {} + + private static getSession(id: string) { + if ( !this.sessionsByID[id] ) { + this.sessionsByID[id] = {} as SessionData + } + + return this.sessionsByID[id] + } + + private static setSession(id: string, data: SessionData) { + this.sessionsByID[id] = data + } + + protected sessionID?: string + protected data?: SessionData + + public getKey(): string { + if ( !this.sessionID ) throw new NoSessionKeyError() + return this.sessionID + } + + public setKey(key: string) { + this.sessionID = key + } + + public load() { + if ( !this.sessionID ) throw new NoSessionKeyError() + this.data = MemorySession.getSession(this.sessionID) + } + + public persist() { + if ( !this.sessionID ) throw new NoSessionKeyError() + if ( !this.data ) throw new SessionNotLoadedError() + MemorySession.setSession(this.sessionID, this.data) + } + + public getData(): SessionData { + if ( !this.data ) throw new SessionNotLoadedError() + return this.data + } + + public setData(data: SessionData) { + this.data = data + } + + public get(key: string, fallback?: any): any { + if ( !this.data ) throw new SessionNotLoadedError() + return this.data?.[key] ?? fallback + } + + public set(key: string, value: any) { + if ( !this.data ) throw new SessionNotLoadedError() + this.data[key] = value + } +} diff --git a/src/http/session/Session.ts b/src/http/session/Session.ts new file mode 100644 index 0000000..2c3c41d --- /dev/null +++ b/src/http/session/Session.ts @@ -0,0 +1,39 @@ +import {Injectable, Inject} from "@extollo/di" +import {ErrorWithContext} from "@extollo/util" +import {Request} from "../lifecycle/Request" + +export type SessionData = {[key: string]: any} + +export class NoSessionKeyError extends ErrorWithContext { + constructor() { + super('No session ID has been set.') + } +} + +export class SessionNotLoadedError extends ErrorWithContext { + constructor() { + super('Cannot access session data; data is not loaded.') + } +} + +@Injectable() +export abstract class Session { + @Inject() + protected readonly request!: Request + + public abstract getKey(): string + + public abstract setKey(key: string): void + + public abstract load(): void | Promise + + public abstract persist(): void | Promise + + public abstract getData(): SessionData + + public abstract setData(data: SessionData): void + + public abstract get(key: string, fallback?: any): any + + public abstract set(key: string, value: any): void +} diff --git a/src/http/session/SessionFactory.ts b/src/http/session/SessionFactory.ts new file mode 100644 index 0000000..36dfbbd --- /dev/null +++ b/src/http/session/SessionFactory.ts @@ -0,0 +1,49 @@ +import { + AbstractFactory, + Container, + DependencyRequirement, + PropertyDependency, + DEPENDENCY_KEYS_METADATA_KEY, + DEPENDENCY_KEYS_PROPERTY_METADATA_KEY +} from "@extollo/di" +import {Collection} from "@extollo/util" +import {MemorySession} from "./MemorySession"; +import {Session} from "./Session"; +import {Logging} from "../../service/Logging"; + +export class SessionFactory extends AbstractFactory { + protected readonly logging: Logging + + constructor() { + super({}) + this.logging = Container.getContainer().make(Logging) + } + + produce(dependencies: any[], parameters: any[]): any { + this.logging.warn(`You are using the default memory-based session driver. It is recommended you configure a persistent session driver instead.`) + return new MemorySession() // FIXME allow configuring + } + + match(something: any) { + return something === Session + } + + getDependencyKeys(): Collection { + const meta = Reflect.getMetadata(DEPENDENCY_KEYS_METADATA_KEY, this.token) + if ( meta ) return meta + return new Collection() + } + + getInjectedProperties(): Collection { + const meta = new Collection() + let currentToken = MemorySession // FIXME allow configuring + + do { + const loadedMeta = Reflect.getMetadata(DEPENDENCY_KEYS_PROPERTY_METADATA_KEY, currentToken) + if ( loadedMeta ) meta.concat(loadedMeta) + currentToken = Object.getPrototypeOf(currentToken) + } while (Object.getPrototypeOf(currentToken) !== Function.prototype && Object.getPrototypeOf(currentToken) !== Object.prototype) + + return meta + } +} diff --git a/src/service/HTTPServer.ts b/src/service/HTTPServer.ts index f828848..3707767 100644 --- a/src/service/HTTPServer.ts +++ b/src/service/HTTPServer.ts @@ -6,6 +6,8 @@ import {Request} from "../http/lifecycle/Request"; import {HTTPKernel} from "../http/kernel/HTTPKernel"; import {PoweredByHeaderInjectionHTTPModule} from "../http/kernel/module/PoweredByHeaderInjectionHTTPModule"; import {SetSessionCookieHTTPModule} from "../http/kernel/module/SetSessionCookieHTTPModule"; +import {InjectSessionHTTPModule} from "../http/kernel/module/InjectSessionHTTPModule"; +import {PersistSessionHTTPMiddleware} from "../http/kernel/module/PersistSessionHTTPMiddleware"; @Singleton() export class HTTPServer extends Unit { @@ -23,6 +25,8 @@ export class HTTPServer extends Unit { // TODO register these by config PoweredByHeaderInjectionHTTPModule.register(this.kernel) SetSessionCookieHTTPModule.register(this.kernel) + InjectSessionHTTPModule.register(this.kernel) + PersistSessionHTTPMiddleware.register(this.kernel) await new Promise((res, rej) => { this.server = createServer(this.handler)