Compare commits

..

22 Commits

Author SHA1 Message Date
c5f842b409 just remove session cookie, if its set 2023-05-14 22:13:17 +02:00
46cf6081af remove debug log 2023-05-14 22:07:51 +02:00
997fe6dc77 change middleware done result to own type 2023-05-14 22:07:08 +02:00
b335bb61bd set crypto version 2023-05-14 20:23:17 +02:00
2e6ce88e80 add exception for sessionSecret 2023-05-14 20:22:37 +02:00
8a3683d2e5 allow undefined arg for cookie, just set cookie if session has entries 2023-05-13 06:29:01 +02:00
96a73035f8 add sessionDestroy func 2023-05-13 06:25:28 +02:00
5392b032d0 add sessionExpire option 2023-05-13 04:58:01 +02:00
d519c7bf9c add sessions, add session example 2023-05-13 04:50:15 +02:00
b2e357227c add example for multiple preprocessors 2023-05-12 15:19:02 +02:00
1376fbdeba replace return with continue 2023-05-12 15:05:06 +02:00
7f1fce92b2 revert last change 2023-05-12 14:51:36 +02:00
784a34ed4d fix memory leak, close file after serve 2023-05-12 14:49:21 +02:00
4e1ac5888a better example html 2023-05-12 14:14:06 +02:00
e6f6b1ddc5 add var to identify static resource requests 2023-05-12 14:10:41 +02:00
06695c5443 add img to html static test 2023-05-12 14:01:51 +02:00
c6cb48b4ff add static example to site html test 2023-05-12 14:00:24 +02:00
452be0cecf export HTTPMethod 2023-05-12 13:57:13 +02:00
ec0df0de60 change HTTPMethod to enum 2023-05-12 13:55:26 +02:00
ca6d23f5ef fix catch block text 2023-05-12 13:34:11 +02:00
d89310bb2e remove whitespace at start of path 2023-05-12 13:09:23 +02:00
1c085b9ec5 catch issue where the connection was closed before reply 2023-05-12 13:09:09 +02:00
3 changed files with 378 additions and 144 deletions

6
example/static/style.css Normal file
View File

@@ -0,0 +1,6 @@
h1 {
color: red;
}
img {
margin-bottom: 15px;
}

View File

@@ -1,6 +1,6 @@
import { Status } from "https://deno.land/std@0.186.0/http/http_status.ts"; import { Status } from "https://deno.land/std@0.186.0/http/http_status.ts";
import prettyTime from "npm:pretty-time"; import prettyTime from "npm:pretty-time";
import { HTTPServer } from "../mod.ts"; import { HTTPServer, SessionExpire } from "../mod.ts";
const JOKES = [ const JOKES = [
"Why do Java developers often wear glasses? They can't C#.", "Why do Java developers often wear glasses? They can't C#.",
@@ -17,13 +17,27 @@ const JOKES = [
const httpServer = new HTTPServer(); const httpServer = new HTTPServer();
// Add as many preprocessors as you want
httpServer.preprocessor((req, _rep) => {
if (req.resourceRequest) {
console.log(`Requested resource ${req.path}`);
}
});
httpServer.preprocessor((_req, rep) => { httpServer.preprocessor((_req, rep) => {
rep.header("Access-Control-Allow-Origin", "*"); rep.header("Access-Control-Allow-Origin", "*");
}); });
httpServer.middleware(async (req, _rep, done) => { httpServer.middleware(async (req, _rep, done) => {
const processTime = await done(); const result = await done();
console.log(`${req.method} - ${req.remoteIpAddr} - ${req.path} - ${prettyTime(processTime)}`); const hrArray: number[] = [0, Math.trunc(result.processTime * 1000000)];
if (!req.resourceRequest) {
console.log(
`${req.method} - ${req.remoteIpAddr} - ${req.path} - ${
prettyTime(hrArray)
}`,
);
}
}); });
httpServer.error((req, _rep) => { httpServer.error((req, _rep) => {
@@ -53,16 +67,98 @@ httpServer.get("/site", (_req, rep) => {
<html> <html>
<head> <head>
<title>HTML Test</title> <title>HTML Test</title>
<link rel="stylesheet" type="text/css" href="/assets/style.css">
</head> </head>
<body> <body>
<h1>Hello World!</h1> <h1>Hello World!</h1>
<button onclick="alert('bruh')">Useless button, do not press.</button> <img src="/assets/lucoa.gif" id="lucoa" width="150" />
<br>
<button onclick="document.getElementById('lucoa').remove(); alert('omg, you killed her you monster.')">Useless button, do not press.</button>
</body> </body>
</html> </html>
`; `;
rep.html(htmlTest); rep.html(htmlTest);
}); });
httpServer.delete("/session", (req, _rep) => {
const username = req.session.user as string ?? "";
if (username.length > 0) {
req.sessionDestroy();
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")
@@ -98,4 +194,6 @@ httpServer.listen({
port: 8080, port: 8080,
staticLocalDir: "/static", staticLocalDir: "/static",
staticServePath: "/assets", staticServePath: "/assets",
sessionSecret: "SuperDuperSecret",
sessionExpire: SessionExpire.NEVER
}); });

184
mod.ts
View File

@@ -4,14 +4,37 @@ 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@v0.10.0/aes.ts";
import {
Cbc,
Padding,
} from "https://deno.land/x/crypto@v0.10.0/block-modes.ts";
import { cryptoRandomString } from "https://deno.land/x/crypto_random_string@1.0.0/mod.ts";
type ListenOptions = { type HTTPServerOptions = {
port: number; port: number;
host?: string; host?: string;
staticLocalDir?: string; staticLocalDir?: string;
staticServePath?: string; staticServePath?: string;
sessionSecret?: string;
sessionExpire?: SessionExpire | number;
}; };
type HTTPMethod = "GET" | "POST" | "PUSH" | "DELETE";
type MiddlewareResult = {
processTime: number;
};
export enum SessionExpire {
NEVER = 2147483647,
}
export enum HTTPMethod {
GET = "GET",
POST = "POST",
PUSH = "PUSH",
DELETE = "DELETE",
}
type RouteHandler = ( type RouteHandler = (
req: RouteRequest, req: RouteRequest,
rep: RouteReply, rep: RouteReply,
@@ -22,7 +45,7 @@ type RouteHandler = (
type RouteMiddlewareHandler = ( type RouteMiddlewareHandler = (
req: RouteRequest, req: RouteRequest,
rep: RouteReply, rep: RouteReply,
done: () => Promise<number[]>, done: () => Promise<MiddlewareResult>,
) => Promise<void>; ) => Promise<void>;
type RoutePreprocessor = ( type RoutePreprocessor = (
@@ -43,8 +66,19 @@ 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) {
if (options.sessionSecret) {
if (![16, 24, 32].includes(options.sessionSecret.length)) {
const randomString = cryptoRandomString({ length: 32 });
throw new Error(
"\nInvalid key size (must be either 16, 24 or 32 bytes)\nHere is a pregenerated key: " +
randomString,
);
}
}
this.settings = options;
this.server = Deno.listen({ this.server = Deno.listen({
port: options.port, port: options.port,
hostname: options.host, hostname: options.host,
@@ -102,6 +136,7 @@ export class HTTPServer {
} }
private async handleHttp(conn: Deno.Conn) { private async handleHttp(conn: Deno.Conn) {
try {
const httpConn = Deno.serveHttp(conn); const httpConn = Deno.serveHttp(conn);
for await (const requestEvent of httpConn) { for await (const requestEvent of httpConn) {
const filepath = decodeURIComponent( const filepath = decodeURIComponent(
@@ -110,6 +145,7 @@ 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,
@@ -126,11 +162,11 @@ export class HTTPServer {
preProcessor(routeRequest, routeReply) preProcessor(routeRequest, routeReply)
); );
let resolveAction: (value: number[]) => void = () => {}; let resolveAction: (value: MiddlewareResult) => void = () => {};
let middlewarePromise; let middlewarePromise;
const perStart = performance.now(); const perStart = performance.now();
if (this.middlewareHandler) { if (this.middlewareHandler) {
middlewarePromise = (): Promise<number[]> => { middlewarePromise = (): Promise<MiddlewareResult> => {
return new Promise((resolve) => { return new Promise((resolve) => {
resolveAction = resolve; resolveAction = resolve;
}); });
@@ -151,9 +187,11 @@ export class HTTPServer {
} catch { } catch {
if (middlewarePromise) { if (middlewarePromise) {
const pt = performance.now() - perStart; const pt = performance.now() - perStart;
const hrArray: number[] = [0, Math.trunc(pt * 1000000)]; resolveAction({
resolveAction(hrArray); processTime: pt,
});
} }
this.processSession(routeRequest, routeReply);
this.handleNotFound(routeRequest, routeReply, requestEvent); this.handleNotFound(routeRequest, routeReply, requestEvent);
continue; continue;
} }
@@ -162,14 +200,18 @@ export class HTTPServer {
const response = new Response(readableStream); const response = new Response(readableStream);
if (middlewarePromise) { if (middlewarePromise) {
const pt = performance.now() - perStart; const pt = performance.now() - perStart;
const hrArray: number[] = [0, Math.trunc(pt * 1000000)]; resolveAction({
resolveAction(hrArray); processTime: pt,
});
} }
this.processSession(routeRequest, routeReply);
await requestEvent.respondWith(response); await requestEvent.respondWith(response);
return; 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) {
@@ -184,9 +226,11 @@ export class HTTPServer {
if (middlewarePromise) { if (middlewarePromise) {
const pt = performance.now() - perStart; const pt = performance.now() - perStart;
const hrArray: number[] = [0, Math.trunc(pt * 1000000)]; resolveAction({
resolveAction(hrArray); processTime: pt,
});
} }
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,
@@ -223,9 +267,12 @@ export class HTTPServer {
); );
if (middlewarePromise) { if (middlewarePromise) {
const pt = performance.now() - perStart; const pt = performance.now() - perStart;
const hrArray: number[] = [0, Math.trunc(pt * 1000000)]; resolveAction({
resolveAction(hrArray); processTime: pt,
});
} }
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,
@@ -237,11 +284,35 @@ export class HTTPServer {
} }
if (middlewarePromise) { if (middlewarePromise) {
const pt = performance.now() - perStart; const pt = performance.now() - perStart;
const hrArray: number[] = [0, Math.trunc(pt * 1000000)]; resolveAction({
resolveAction(hrArray); processTime: pt,
});
} }
this.processSession(routeRequest, routeReply);
this.handleNotFound(routeRequest, routeReply, requestEvent); this.handleNotFound(routeRequest, routeReply, requestEvent);
} }
} catch (_err) {
// Ignore http connections that where closed before reply was sent
}
}
private processSession(routeRequest: RouteRequest, routeReply: RouteReply) {
if (this.settings?.sessionSecret) {
const sessionObject = JSON.stringify(routeRequest.session);
if (Object.keys(routeRequest.session).length > 0) {
const encodedSession = encryptData(
sessionObject,
this.settings?.sessionSecret,
);
routeReply.cookie("session", encodedSession, {
maxAge: this.settings.sessionExpire ?? undefined,
});
} else {
if (routeRequest.cookie("session")) {
routeReply.cookie("session", undefined);
}
}
}
} }
close() { close() {
@@ -263,19 +334,19 @@ export class HTTPServer {
} }
get(path: string, handler: RouteHandler) { get(path: string, handler: RouteHandler) {
this.add("GET", path, handler); this.add(HTTPMethod.GET, path, handler);
} }
post(path: string, handler: RouteHandler) { post(path: string, handler: RouteHandler) {
this.add("POST", path, handler); this.add(HTTPMethod.POST, path, handler);
} }
push(path: string, handler: RouteHandler) { push(path: string, handler: RouteHandler) {
this.add("PUSH", path, handler); this.add(HTTPMethod.PUSH, path, handler);
} }
delete(path: string, handler: RouteHandler) { delete(path: string, handler: RouteHandler) {
this.add("DELETE", path, handler); this.add(HTTPMethod.DELETE, path, handler);
} }
add(method: HTTPMethod, path: string, handler: RouteHandler) { add(method: HTTPMethod, path: string, handler: RouteHandler) {
@@ -314,6 +385,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;
@@ -332,21 +431,53 @@ 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;
session: { [key: string]: unknown } = {};
constructor(request: Request, conn: Deno.Conn, path: string, url: string) { constructor(
httpServer: HTTPServer,
request: Request,
conn: Deno.Conn,
path: string,
url: 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 = httpServer.settings?.staticServePath &&
httpServer.settings?.staticServePath.length > 0
? path.startsWith(httpServer.settings?.staticServePath)
: 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) {
console.log(_err);
// Ignore if sessionCookie is not in JSON format
}
}
}
sessionDestroy(): void {
this.session = {};
} }
header(name: string): unknown { header(name: string): unknown {
@@ -357,9 +488,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 {
@@ -401,7 +531,7 @@ export class RouteReply {
return this; return this;
} }
cookie(name: string, value: string, attributes?: { cookie(name: string, value: string | undefined, attributes?: {
expires?: Date | number; expires?: Date | number;
maxAge?: number; maxAge?: number;
domain?: string; domain?: string;