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 {