diff --git a/example/test.ts b/example/test.ts index 1b1ed92..b65eb7c 100644 --- a/example/test.ts +++ b/example/test.ts @@ -79,6 +79,85 @@ httpServer.get("/site", (_req, rep) => { rep.html(htmlTest); }); +httpServer.delete("/session", (req, _rep) => { + const username = req.session.user as string ?? ""; + if (username.length > 0) { + delete req.session.user; + return { + code: 200, + message: "Logged out!", + }; + } else { + return { + code: 403, + message: "Not logged in!", + }; + } +}); + +httpServer.post("/session", (req, _rep) => { + const username = req.queryParam("username") ?? ""; + if (username.length > 0) { + req.session.user = username; + return { + code: 200, + message: "Logged in!", + }; + } else { + return { + code: 403, + message: "Please enter a Username", + }; + } +}); + +httpServer.get("/session", (req, rep) => { + const headerText = req.session.user + ? `Hello, ${req.session.user}!` + : `Please login!`; + const htmlTest = ` + + + Session Example + + +

${headerText}

+ +
+ + + + + `; + rep.html(htmlTest); +}); + httpServer.get("/", (req, rep) => { rep.status(Status.Teapot) .header("working", "true") @@ -114,4 +193,5 @@ httpServer.listen({ port: 8080, staticLocalDir: "/static", staticServePath: "/assets", + sessionSecret: "SuperDuperSecret", }); diff --git a/mod.ts b/mod.ts index d42634b..23c8e81 100644 --- a/mod.ts +++ b/mod.ts @@ -4,12 +4,15 @@ import { } from "https://deno.land/std@0.186.0/http/http_status.ts"; import * as path from "https://deno.land/std@0.185.0/path/mod.ts"; import * as cookie from "https://deno.land/std@0.185.0/http/cookie.ts"; +import { Aes } from "https://deno.land/x/crypto/aes.ts"; +import { Cbc, Padding } from "https://deno.land/x/crypto/block-modes.ts"; -type ListenOptions = { +type HTTPServerOptions = { port: number; host?: string; staticLocalDir?: string; staticServePath?: string; + sessionSecret?: string; }; export enum HTTPMethod { @@ -50,8 +53,10 @@ export class HTTPServer { private notFoundHandler?: RouteHandler; private preprocessors: RoutePreprocessor[] = []; private middlewareHandler?: RouteMiddlewareHandler; + settings?: HTTPServerOptions; - async listen(options: ListenOptions) { + async listen(options: HTTPServerOptions) { + this.settings = options; this.server = Deno.listen({ port: options.port, hostname: options.host, @@ -118,11 +123,11 @@ export class HTTPServer { const request = requestEvent.request; const url = request.url; const routeRequest = new RouteRequest( + this, request, conn, filepath, url, - this.staticServePath ?? "", ); const routeReply: RouteReply = new RouteReply(); @@ -163,6 +168,7 @@ export class HTTPServer { const hrArray: number[] = [0, Math.trunc(pt * 1000000)]; resolveAction(hrArray); } + this.processSession(routeRequest, routeReply); this.handleNotFound(routeRequest, routeReply, requestEvent); continue; } @@ -174,11 +180,14 @@ export class HTTPServer { const hrArray: number[] = [0, Math.trunc(pt * 1000000)]; resolveAction(hrArray); } + this.processSession(routeRequest, routeReply); await requestEvent.respondWith(response); continue; } - const routeName = `${requestEvent.request.method}@${filepath}`; + const routeName = `${requestEvent.request.method}@${ + filepath.replace(/(?!\/)\W\D.*/gm, "") + }`; let route = this.routes.get(routeName); if (route) { @@ -196,6 +205,7 @@ export class HTTPServer { const hrArray: number[] = [0, Math.trunc(pt * 1000000)]; resolveAction(hrArray); } + this.processSession(routeRequest, routeReply); await requestEvent.respondWith( new Response(handler as string, { status: routeReply.statusCode, @@ -235,6 +245,8 @@ export class HTTPServer { const hrArray: number[] = [0, Math.trunc(pt * 1000000)]; resolveAction(hrArray); } + + this.processSession(routeRequest, routeReply); await requestEvent.respondWith( new Response(handler as string, { status: routeReply.statusCode, @@ -249,6 +261,7 @@ export class HTTPServer { const hrArray: number[] = [0, Math.trunc(pt * 1000000)]; resolveAction(hrArray); } + this.processSession(routeRequest, routeReply); this.handleNotFound(routeRequest, routeReply, requestEvent); } } catch (_err) { @@ -257,6 +270,17 @@ export class HTTPServer { } } + private processSession(routeRequest: RouteRequest, routeReply: RouteReply) { + if (this.settings?.sessionSecret) { + const sessionObject = JSON.stringify(routeRequest.session); + const encodedSession = encryptData( + sessionObject, + this.settings?.sessionSecret, + ); + routeReply.cookie("session", encodedSession); + } + } + close() { if (this.server) { this.server.close(); @@ -327,6 +351,34 @@ export const extractRouteParams: (route: string) => RouteParam[] = (route) => return accum; }, []); +function encryptData(data: string, key: string) { + const te = new TextEncoder(); + const aeskey = te.encode(key); + const encodeddata = te.encode(data); + const iv = new Uint8Array(16); + const cipher = new Cbc(Aes, aeskey, iv, Padding.PKCS7); + const encrypted = cipher.encrypt(encodeddata); + const hexed = Array.from(encrypted).map((b) => + b.toString(16).padStart(2, "0") + ).join(""); + return hexed; +} + +function decryptHex(data: string, key: string) { + const te = new TextEncoder(); + const td = new TextDecoder(); + const byteArray = new Uint8Array(data.length / 2); + for (let i = 0; i < data.length; i += 2) { + const byte = parseInt(data.substring(i, i + 2), 16); + byteArray[Math.floor(i / 2)] = byte; + } + const aeskey = te.encode(key); + const iv = new Uint8Array(16); + const decipher = new Cbc(Aes, aeskey, iv, Padding.PKCS7); + const decrypted = decipher.decrypt(byteArray); + return td.decode(decrypted); +} + export class Route { routeName: string; path: string; @@ -345,31 +397,48 @@ export class RouteRequest { url: string; path: string; headers: Headers; + cookies: Record; method: HTTPMethod; queryParams: { [key: string]: string }; pathParams: { [key: string]: string }; remoteIpAddr: string; resourceRequest: boolean; + session: { [key: string]: unknown } = {}; constructor( + httpServer: HTTPServer, request: Request, conn: Deno.Conn, path: string, url: string, - staticServePath: string, ) { this.url = url; this.path = decodeURIComponent(path); this.headers = request.headers; this.method = request.method as HTTPMethod; this.pathParams = {}; - this.resourceRequest = staticServePath.length > 0 - ? path.startsWith(staticServePath) + this.resourceRequest = httpServer.settings?.staticServePath && + httpServer.settings?.staticServePath.length > 0 + ? path.startsWith(httpServer.settings?.staticServePath) : false; this.queryParams = Object.fromEntries(new URL(url).searchParams.entries()); + this.cookies = cookie.getCookies(this.headers); this.remoteIpAddr = "hostname" in conn.remoteAddr ? conn.remoteAddr["hostname"] : "127.0.0.1"; + + const sessionCookie = this.cookie("session") as string; + if (sessionCookie && httpServer.settings?.sessionSecret) { + const decodedSessionCookie = decryptHex( + sessionCookie, + httpServer.settings.sessionSecret, + ); + try{ + this.session = JSON.parse(decodedSessionCookie); + }catch(_err){ + // Ignore if sessionCookie is not in JSON format + } + } } header(name: string): unknown { @@ -380,9 +449,8 @@ export class RouteRequest { } cookie(name: string): unknown { - const allCookies = cookie.getCookies(this.headers); - const allCookieNames = Object.keys(allCookies); - return allCookieNames.includes(name) ? allCookies[name] : undefined; + const allCookieNames = Object.keys(this.cookies); + return allCookieNames.includes(name) ? this.cookies[name] : undefined; } pathParam(name: string): string {