add sessions, add session example

This commit is contained in:
HorizonCode 2023-05-13 04:50:15 +02:00
parent b2e357227c
commit d519c7bf9c
2 changed files with 158 additions and 10 deletions

View File

@ -79,6 +79,85 @@ httpServer.get("/site", (_req, rep) => {
rep.html(htmlTest); 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 = `
<html>
<head>
<title>Session Example</title>
</head>
<body>
<h1>${headerText}</h1>
<input type="text" placeholder="Username" id="username" style="margin-bottom: 15px;" ${req.session.user ? "value='" + req.session.user + "' disabled" : ""}/>
<br>
<button onclick="${req.session.user ? "doLogout" : "doLogin"}()">${
req.session.user ? "Logout" : "Login"
}</button>
</body>
<script type="">
async function doLogout() {
const fetchResult = await fetch("/session", { method: 'DELETE'});
const jsonResult = await fetchResult.json();
if("code" in jsonResult){
if(jsonResult.code == 200){
document.location.reload(true)
}else{
alert(jsonResult.message);
}
}
}
async function doLogin() {
const username = document.getElementById('username').value;
const fetchResult = await fetch("/session?username=" + username, { method: 'POST'});
const jsonResult = await fetchResult.json();
if("code" in jsonResult){
if(jsonResult.code == 200){
document.location.reload(true)
}else{
alert(jsonResult.message);
}
}
}
</script>
</html>
`;
rep.html(htmlTest);
});
httpServer.get("/", (req, rep) => { httpServer.get("/", (req, rep) => {
rep.status(Status.Teapot) rep.status(Status.Teapot)
.header("working", "true") .header("working", "true")
@ -114,4 +193,5 @@ httpServer.listen({
port: 8080, port: 8080,
staticLocalDir: "/static", staticLocalDir: "/static",
staticServePath: "/assets", staticServePath: "/assets",
sessionSecret: "SuperDuperSecret",
}); });

88
mod.ts
View File

@ -4,12 +4,15 @@ import {
} from "https://deno.land/std@0.186.0/http/http_status.ts"; } 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 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 * 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; port: number;
host?: string; host?: string;
staticLocalDir?: string; staticLocalDir?: string;
staticServePath?: string; staticServePath?: string;
sessionSecret?: string;
}; };
export enum HTTPMethod { export enum HTTPMethod {
@ -50,8 +53,10 @@ export class HTTPServer {
private notFoundHandler?: RouteHandler; private notFoundHandler?: RouteHandler;
private preprocessors: RoutePreprocessor[] = []; private preprocessors: RoutePreprocessor[] = [];
private middlewareHandler?: RouteMiddlewareHandler; private middlewareHandler?: RouteMiddlewareHandler;
settings?: HTTPServerOptions;
async listen(options: ListenOptions) { async listen(options: HTTPServerOptions) {
this.settings = options;
this.server = Deno.listen({ this.server = Deno.listen({
port: options.port, port: options.port,
hostname: options.host, hostname: options.host,
@ -118,11 +123,11 @@ export class HTTPServer {
const request = requestEvent.request; const request = requestEvent.request;
const url = request.url; const url = request.url;
const routeRequest = new RouteRequest( const routeRequest = new RouteRequest(
this,
request, request,
conn, conn,
filepath, filepath,
url, url,
this.staticServePath ?? "",
); );
const routeReply: RouteReply = new RouteReply(); const routeReply: RouteReply = new RouteReply();
@ -163,6 +168,7 @@ export class HTTPServer {
const hrArray: number[] = [0, Math.trunc(pt * 1000000)]; const hrArray: number[] = [0, Math.trunc(pt * 1000000)];
resolveAction(hrArray); resolveAction(hrArray);
} }
this.processSession(routeRequest, routeReply);
this.handleNotFound(routeRequest, routeReply, requestEvent); this.handleNotFound(routeRequest, routeReply, requestEvent);
continue; continue;
} }
@ -174,11 +180,14 @@ export class HTTPServer {
const hrArray: number[] = [0, Math.trunc(pt * 1000000)]; const hrArray: number[] = [0, Math.trunc(pt * 1000000)];
resolveAction(hrArray); resolveAction(hrArray);
} }
this.processSession(routeRequest, routeReply);
await requestEvent.respondWith(response); await requestEvent.respondWith(response);
continue; continue;
} }
const routeName = `${requestEvent.request.method}@${filepath}`; const routeName = `${requestEvent.request.method}@${
filepath.replace(/(?!\/)\W\D.*/gm, "")
}`;
let route = this.routes.get(routeName); let route = this.routes.get(routeName);
if (route) { if (route) {
@ -196,6 +205,7 @@ export class HTTPServer {
const hrArray: number[] = [0, Math.trunc(pt * 1000000)]; const hrArray: number[] = [0, Math.trunc(pt * 1000000)];
resolveAction(hrArray); resolveAction(hrArray);
} }
this.processSession(routeRequest, routeReply);
await requestEvent.respondWith( await requestEvent.respondWith(
new Response(handler as string, { new Response(handler as string, {
status: routeReply.statusCode, status: routeReply.statusCode,
@ -235,6 +245,8 @@ export class HTTPServer {
const hrArray: number[] = [0, Math.trunc(pt * 1000000)]; const hrArray: number[] = [0, Math.trunc(pt * 1000000)];
resolveAction(hrArray); resolveAction(hrArray);
} }
this.processSession(routeRequest, routeReply);
await requestEvent.respondWith( await requestEvent.respondWith(
new Response(handler as string, { new Response(handler as string, {
status: routeReply.statusCode, status: routeReply.statusCode,
@ -249,6 +261,7 @@ export class HTTPServer {
const hrArray: number[] = [0, Math.trunc(pt * 1000000)]; const hrArray: number[] = [0, Math.trunc(pt * 1000000)];
resolveAction(hrArray); resolveAction(hrArray);
} }
this.processSession(routeRequest, routeReply);
this.handleNotFound(routeRequest, routeReply, requestEvent); this.handleNotFound(routeRequest, routeReply, requestEvent);
} }
} catch (_err) { } 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() { close() {
if (this.server) { if (this.server) {
this.server.close(); this.server.close();
@ -327,6 +351,34 @@ export const extractRouteParams: (route: string) => RouteParam[] = (route) =>
return accum; 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 { export class Route {
routeName: string; routeName: string;
path: string; path: string;
@ -345,31 +397,48 @@ export class RouteRequest {
url: string; url: string;
path: string; path: string;
headers: Headers; headers: Headers;
cookies: Record<string, string>;
method: HTTPMethod; method: HTTPMethod;
queryParams: { [key: string]: string }; queryParams: { [key: string]: string };
pathParams: { [key: string]: string }; pathParams: { [key: string]: string };
remoteIpAddr: string; remoteIpAddr: string;
resourceRequest: boolean; resourceRequest: boolean;
session: { [key: string]: unknown } = {};
constructor( constructor(
httpServer: HTTPServer,
request: Request, request: Request,
conn: Deno.Conn, conn: Deno.Conn,
path: string, path: string,
url: string, url: string,
staticServePath: string,
) { ) {
this.url = url; this.url = url;
this.path = decodeURIComponent(path); this.path = decodeURIComponent(path);
this.headers = request.headers; this.headers = request.headers;
this.method = request.method as HTTPMethod; this.method = request.method as HTTPMethod;
this.pathParams = {}; this.pathParams = {};
this.resourceRequest = staticServePath.length > 0 this.resourceRequest = httpServer.settings?.staticServePath &&
? path.startsWith(staticServePath) httpServer.settings?.staticServePath.length > 0
? path.startsWith(httpServer.settings?.staticServePath)
: false; : false;
this.queryParams = Object.fromEntries(new URL(url).searchParams.entries()); this.queryParams = Object.fromEntries(new URL(url).searchParams.entries());
this.cookies = cookie.getCookies(this.headers);
this.remoteIpAddr = "hostname" in conn.remoteAddr this.remoteIpAddr = "hostname" in conn.remoteAddr
? conn.remoteAddr["hostname"] ? conn.remoteAddr["hostname"]
: "127.0.0.1"; : "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 { header(name: string): unknown {
@ -380,9 +449,8 @@ export class RouteRequest {
} }
cookie(name: string): unknown { cookie(name: string): unknown {
const allCookies = cookie.getCookies(this.headers); const allCookieNames = Object.keys(this.cookies);
const allCookieNames = Object.keys(allCookies); return allCookieNames.includes(name) ? this.cookies[name] : undefined;
return allCookieNames.includes(name) ? allCookies[name] : undefined;
} }
pathParam(name: string): string { pathParam(name: string): string {