Compare commits
22 Commits
5626782178
...
master
Author | SHA1 | Date | |
---|---|---|---|
c5f842b409 | |||
46cf6081af | |||
997fe6dc77 | |||
b335bb61bd | |||
2e6ce88e80 | |||
8a3683d2e5 | |||
96a73035f8 | |||
5392b032d0 | |||
d519c7bf9c | |||
b2e357227c | |||
1376fbdeba | |||
7f1fce92b2 | |||
784a34ed4d | |||
4e1ac5888a | |||
e6f6b1ddc5 | |||
06695c5443 | |||
c6cb48b4ff | |||
452be0cecf | |||
ec0df0de60 | |||
ca6d23f5ef | |||
d89310bb2e | |||
1c085b9ec5 |
6
example/static/style.css
Normal file
6
example/static/style.css
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
h1 {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
106
example/test.ts
106
example/test.ts
@@ -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
|
||||||
});
|
});
|
||||||
|
410
mod.ts
410
mod.ts
@@ -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,145 +136,182 @@ export class HTTPServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async handleHttp(conn: Deno.Conn) {
|
private async handleHttp(conn: Deno.Conn) {
|
||||||
const httpConn = Deno.serveHttp(conn);
|
try {
|
||||||
for await (const requestEvent of httpConn) {
|
const httpConn = Deno.serveHttp(conn);
|
||||||
const filepath = decodeURIComponent(
|
for await (const requestEvent of httpConn) {
|
||||||
"/" + requestEvent.request.url.split("/").slice(3).join("/"),
|
const filepath = decodeURIComponent(
|
||||||
);
|
"/" + requestEvent.request.url.split("/").slice(3).join("/"),
|
||||||
const request = requestEvent.request;
|
|
||||||
const url = request.url;
|
|
||||||
const routeRequest = new RouteRequest(
|
|
||||||
request,
|
|
||||||
conn,
|
|
||||||
filepath,
|
|
||||||
url,
|
|
||||||
);
|
|
||||||
const routeReply: RouteReply = new RouteReply();
|
|
||||||
|
|
||||||
if (filepath.startsWith("/_static") || filepath.endsWith(".ico")) {
|
|
||||||
this.handleNotFound(routeRequest, routeReply, requestEvent);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.preprocessors.forEach((preProcessor) =>
|
|
||||||
preProcessor(routeRequest, routeReply)
|
|
||||||
);
|
|
||||||
|
|
||||||
let resolveAction: (value: number[]) => void = () => {};
|
|
||||||
let middlewarePromise;
|
|
||||||
const perStart = performance.now();
|
|
||||||
if (this.middlewareHandler) {
|
|
||||||
middlewarePromise = (): Promise<number[]> => {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
resolveAction = resolve;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
this.middlewareHandler(routeRequest, routeReply, middlewarePromise);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.staticServePath && filepath.startsWith(this.staticServePath)) {
|
|
||||||
const fileDir = filepath.split("/").slice(2).join("/");
|
|
||||||
const pathLoc = path.join(
|
|
||||||
Deno.cwd(),
|
|
||||||
this.staticLocalDir as string,
|
|
||||||
fileDir,
|
|
||||||
);
|
);
|
||||||
let file;
|
const request = requestEvent.request;
|
||||||
try {
|
const url = request.url;
|
||||||
file = await Deno.open(pathLoc, { read: true });
|
const routeRequest = new RouteRequest(
|
||||||
} catch {
|
this,
|
||||||
if (middlewarePromise) {
|
request,
|
||||||
const pt = performance.now() - perStart;
|
conn,
|
||||||
const hrArray: number[] = [0, Math.trunc(pt * 1000000)];
|
filepath,
|
||||||
resolveAction(hrArray);
|
url,
|
||||||
}
|
);
|
||||||
|
const routeReply: RouteReply = new RouteReply();
|
||||||
|
|
||||||
|
if (filepath.startsWith("/_static") || filepath.endsWith(".ico")) {
|
||||||
this.handleNotFound(routeRequest, routeReply, requestEvent);
|
this.handleNotFound(routeRequest, routeReply, requestEvent);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const readableStream = file.readable;
|
this.preprocessors.forEach((preProcessor) =>
|
||||||
const response = new Response(readableStream);
|
preProcessor(routeRequest, routeReply)
|
||||||
|
);
|
||||||
|
|
||||||
|
let resolveAction: (value: MiddlewareResult) => void = () => {};
|
||||||
|
let middlewarePromise;
|
||||||
|
const perStart = performance.now();
|
||||||
|
if (this.middlewareHandler) {
|
||||||
|
middlewarePromise = (): Promise<MiddlewareResult> => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
resolveAction = resolve;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
this.middlewareHandler(routeRequest, routeReply, middlewarePromise);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.staticServePath && filepath.startsWith(this.staticServePath)) {
|
||||||
|
const fileDir = filepath.split("/").slice(2).join("/");
|
||||||
|
const pathLoc = path.join(
|
||||||
|
Deno.cwd(),
|
||||||
|
this.staticLocalDir as string,
|
||||||
|
fileDir,
|
||||||
|
);
|
||||||
|
let file;
|
||||||
|
try {
|
||||||
|
file = await Deno.open(pathLoc, { read: true });
|
||||||
|
} catch {
|
||||||
|
if (middlewarePromise) {
|
||||||
|
const pt = performance.now() - perStart;
|
||||||
|
resolveAction({
|
||||||
|
processTime: pt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.processSession(routeRequest, routeReply);
|
||||||
|
this.handleNotFound(routeRequest, routeReply, requestEvent);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const readableStream = file.readable;
|
||||||
|
const response = new Response(readableStream);
|
||||||
|
if (middlewarePromise) {
|
||||||
|
const pt = performance.now() - perStart;
|
||||||
|
resolveAction({
|
||||||
|
processTime: pt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.processSession(routeRequest, routeReply);
|
||||||
|
await requestEvent.respondWith(response);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const routeName = `${requestEvent.request.method}@${
|
||||||
|
filepath.replace(/(?!\/)\W\D.*/gm, "")
|
||||||
|
}`;
|
||||||
|
let route = this.routes.get(routeName);
|
||||||
|
|
||||||
|
if (route) {
|
||||||
|
let handler = await route.handler(
|
||||||
|
routeRequest,
|
||||||
|
routeReply,
|
||||||
|
) ?? routeReply.body;
|
||||||
|
|
||||||
|
if (typeof (handler) == "object") {
|
||||||
|
handler = JSON.stringify(handler, null, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (middlewarePromise) {
|
||||||
|
const pt = performance.now() - perStart;
|
||||||
|
resolveAction({
|
||||||
|
processTime: pt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.processSession(routeRequest, routeReply);
|
||||||
|
await requestEvent.respondWith(
|
||||||
|
new Response(handler as string, {
|
||||||
|
status: routeReply.statusCode,
|
||||||
|
headers: routeReply.headers,
|
||||||
|
statusText: STATUS_TEXT[routeReply.statusCode],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
route = Array.from(this.routes.values()).find((route) =>
|
||||||
|
routeWithParamsRouteMatcher(routeRequest, route)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (route) {
|
||||||
|
const routeParamsMap: RouteParam[] = extractRouteParams(route.path);
|
||||||
|
const routeSegments: string[] = filepath.split("/");
|
||||||
|
routeRequest.pathParams = routeParamsMap.reduce(
|
||||||
|
(accum: { [key: string]: string }, curr: RouteParam) => {
|
||||||
|
return {
|
||||||
|
...accum,
|
||||||
|
[curr.paramKey]: routeSegments[curr.idx].replace(
|
||||||
|
/(?!\/)\W\D.*/gm,
|
||||||
|
"",
|
||||||
|
),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
|
const handler = await route.handler(
|
||||||
|
routeRequest,
|
||||||
|
routeReply,
|
||||||
|
);
|
||||||
|
if (middlewarePromise) {
|
||||||
|
const pt = performance.now() - perStart;
|
||||||
|
resolveAction({
|
||||||
|
processTime: pt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.processSession(routeRequest, routeReply);
|
||||||
|
await requestEvent.respondWith(
|
||||||
|
new Response(handler as string, {
|
||||||
|
status: routeReply.statusCode,
|
||||||
|
headers: routeReply.headers,
|
||||||
|
statusText: STATUS_TEXT[routeReply.statusCode],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
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,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
await requestEvent.respondWith(response);
|
this.processSession(routeRequest, routeReply);
|
||||||
return;
|
this.handleNotFound(routeRequest, routeReply, requestEvent);
|
||||||
}
|
}
|
||||||
|
} catch (_err) {
|
||||||
|
// Ignore http connections that where closed before reply was sent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const routeName = `${requestEvent.request.method}@${filepath}`;
|
private processSession(routeRequest: RouteRequest, routeReply: RouteReply) {
|
||||||
let route = this.routes.get(routeName);
|
if (this.settings?.sessionSecret) {
|
||||||
|
const sessionObject = JSON.stringify(routeRequest.session);
|
||||||
if (route) {
|
if (Object.keys(routeRequest.session).length > 0) {
|
||||||
let handler = await route.handler(
|
const encodedSession = encryptData(
|
||||||
routeRequest,
|
sessionObject,
|
||||||
routeReply,
|
this.settings?.sessionSecret,
|
||||||
) ?? routeReply.body;
|
);
|
||||||
|
routeReply.cookie("session", encodedSession, {
|
||||||
if (typeof (handler) == "object") {
|
maxAge: this.settings.sessionExpire ?? undefined,
|
||||||
handler = JSON.stringify(handler, null, 2);
|
});
|
||||||
|
} else {
|
||||||
|
if (routeRequest.cookie("session")) {
|
||||||
|
routeReply.cookie("session", undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (middlewarePromise) {
|
|
||||||
const pt = performance.now() - perStart;
|
|
||||||
const hrArray: number[] = [0, Math.trunc(pt * 1000000)];
|
|
||||||
resolveAction(hrArray);
|
|
||||||
}
|
|
||||||
await requestEvent.respondWith(
|
|
||||||
new Response(handler as string, {
|
|
||||||
status: routeReply.statusCode,
|
|
||||||
headers: routeReply.headers,
|
|
||||||
statusText: STATUS_TEXT[routeReply.statusCode],
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
route = Array.from(this.routes.values()).find((route) =>
|
|
||||||
routeWithParamsRouteMatcher(routeRequest, route)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (route) {
|
|
||||||
const routeParamsMap: RouteParam[] = extractRouteParams(route.path);
|
|
||||||
const routeSegments: string[] = filepath.split("/");
|
|
||||||
routeRequest.pathParams = routeParamsMap.reduce(
|
|
||||||
(accum: { [key: string]: string }, curr: RouteParam) => {
|
|
||||||
return {
|
|
||||||
...accum,
|
|
||||||
[curr.paramKey]: routeSegments[curr.idx].replace(
|
|
||||||
/(?!\/)\W\D.*/gm,
|
|
||||||
"",
|
|
||||||
),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
|
|
||||||
const handler = await route.handler(
|
|
||||||
routeRequest,
|
|
||||||
routeReply,
|
|
||||||
);
|
|
||||||
if (middlewarePromise) {
|
|
||||||
const pt = performance.now() - perStart;
|
|
||||||
const hrArray: number[] = [0, Math.trunc(pt * 1000000)];
|
|
||||||
resolveAction(hrArray);
|
|
||||||
}
|
|
||||||
await requestEvent.respondWith(
|
|
||||||
new Response(handler as string, {
|
|
||||||
status: routeReply.statusCode,
|
|
||||||
headers: routeReply.headers,
|
|
||||||
statusText: STATUS_TEXT[routeReply.statusCode],
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (middlewarePromise) {
|
|
||||||
const pt = performance.now() - perStart;
|
|
||||||
const hrArray: number[] = [0, Math.trunc(pt * 1000000)];
|
|
||||||
resolveAction(hrArray);
|
|
||||||
}
|
|
||||||
this.handleNotFound(routeRequest, routeReply, requestEvent);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
Reference in New Issue
Block a user