@wxn0brp/falcon-frame 0.0.21 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/body.d.ts +3 -0
- package/dist/body.js +52 -0
- package/dist/helpers.d.ts +2 -3
- package/dist/helpers.js +83 -61
- package/dist/index.d.ts +27 -7
- package/dist/index.js +49 -7
- package/dist/{plugins.d.ts → plugin.d.ts} +2 -1
- package/dist/{plugins.js → plugin.js} +9 -10
- package/dist/plugins/cors.d.ts +2 -1
- package/dist/plugins/cors.js +5 -0
- package/dist/plugins/rateLimit.d.ts +1 -1
- package/dist/plugins/security.d.ts +2 -0
- package/dist/plugins/security.js +14 -0
- package/dist/render.d.ts +1 -1
- package/dist/render.js +22 -15
- package/dist/req.js +15 -8
- package/dist/res.d.ts +7 -5
- package/dist/res.js +42 -9
- package/dist/router.d.ts +2 -2
- package/dist/router.js +4 -4
- package/dist/types.d.ts +11 -0
- package/dist/valid.js +4 -2
- package/package.json +5 -5
- package/dist/plugins/index.d.ts +0 -2
- package/dist/plugins/index.js +0 -2
package/dist/body.d.ts
ADDED
package/dist/body.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import querystring from "querystring";
|
|
2
|
+
const parseBodyFunctions = {
|
|
3
|
+
"application/json": async (body) => ({ body: JSON.parse(body) }),
|
|
4
|
+
"application/x-www-form-urlencoded": async (body) => ({
|
|
5
|
+
body: querystring.parse(body),
|
|
6
|
+
}),
|
|
7
|
+
};
|
|
8
|
+
export async function parseBody(req, body, FF) {
|
|
9
|
+
const funcs = Object.assign({}, parseBodyFunctions, FF.customParsers || {});
|
|
10
|
+
const limit = parseLimit(FF.opts.bodyLimit);
|
|
11
|
+
try {
|
|
12
|
+
if (limit && body.length > limit) {
|
|
13
|
+
await FF.logger.warn(`Body size exceeds limit of ${limit} bytes`);
|
|
14
|
+
return {};
|
|
15
|
+
}
|
|
16
|
+
const type = req.headers["content-type"] || "";
|
|
17
|
+
const func = funcs[type];
|
|
18
|
+
if (!func)
|
|
19
|
+
return {};
|
|
20
|
+
const data = await func(body, req, FF);
|
|
21
|
+
if (!data || typeof data !== "object")
|
|
22
|
+
return {};
|
|
23
|
+
return data;
|
|
24
|
+
}
|
|
25
|
+
catch (e) {
|
|
26
|
+
await FF.logger.warn(`Error parsing body: ${e}`);
|
|
27
|
+
return {};
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function parseLimit(limit) {
|
|
31
|
+
if (!limit)
|
|
32
|
+
return 0;
|
|
33
|
+
if (typeof limit === "number")
|
|
34
|
+
return limit;
|
|
35
|
+
if (typeof limit !== "string")
|
|
36
|
+
return 0;
|
|
37
|
+
const match = limit.match(/^(\d+)([kmg])?$/i);
|
|
38
|
+
if (!match)
|
|
39
|
+
return 0;
|
|
40
|
+
const num = parseInt(match[1], 10);
|
|
41
|
+
const unit = match[2]?.toLowerCase();
|
|
42
|
+
switch (unit) {
|
|
43
|
+
case "k":
|
|
44
|
+
return num * 1024;
|
|
45
|
+
case "m":
|
|
46
|
+
return num * 1024 * 1024;
|
|
47
|
+
case "g":
|
|
48
|
+
return num * 1024 * 1024 * 1024;
|
|
49
|
+
default:
|
|
50
|
+
return num;
|
|
51
|
+
}
|
|
52
|
+
}
|
package/dist/helpers.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Cookies, RouteHandler, StaticServeOptions } from "./types.js";
|
|
2
2
|
export declare function parseCookies(cookieHeader: string): Cookies;
|
|
3
|
-
export declare function parseBody(contentType: string, body: string): Body;
|
|
4
3
|
export declare function getContentType(filePath: string, utf8?: boolean): string;
|
|
5
|
-
export declare function handleStaticFiles(dirPath: string,
|
|
4
|
+
export declare function handleStaticFiles(dirPath: string, opts: StaticServeOptions): RouteHandler;
|
package/dist/helpers.js
CHANGED
|
@@ -1,58 +1,31 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
|
-
import querystring from "querystring";
|
|
4
3
|
export function parseCookies(cookieHeader) {
|
|
5
4
|
const cookies = {};
|
|
6
|
-
cookieHeader.split(";").forEach(cookie => {
|
|
5
|
+
cookieHeader.split(";").forEach((cookie) => {
|
|
7
6
|
const [name, ...valueParts] = cookie.split("=");
|
|
8
7
|
const value = decodeURIComponent(valueParts.join("=").trim());
|
|
9
8
|
cookies[name.trim()] = value;
|
|
10
9
|
});
|
|
11
10
|
return cookies;
|
|
12
11
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
12
|
+
const mimeTypes = {
|
|
13
|
+
".html": "text/html",
|
|
14
|
+
".css": "text/css",
|
|
15
|
+
".js": "application/javascript",
|
|
16
|
+
".json": "application/json",
|
|
17
|
+
".png": "image/png",
|
|
18
|
+
".jpg": "image/jpeg",
|
|
19
|
+
".jpeg": "image/jpeg",
|
|
20
|
+
".gif": "image/gif",
|
|
21
|
+
".svg": "image/svg+xml",
|
|
22
|
+
".ico": "image/x-icon",
|
|
23
|
+
".txt": "text/plain",
|
|
24
|
+
".pdf": "application/pdf",
|
|
25
|
+
};
|
|
27
26
|
function _getContentType(filePath) {
|
|
28
27
|
const ext = path.extname(filePath).toLowerCase();
|
|
29
|
-
|
|
30
|
-
case ".html":
|
|
31
|
-
return "text/html";
|
|
32
|
-
case ".css":
|
|
33
|
-
return "text/css";
|
|
34
|
-
case ".js":
|
|
35
|
-
return "application/javascript";
|
|
36
|
-
case ".json":
|
|
37
|
-
return "application/json";
|
|
38
|
-
case ".png":
|
|
39
|
-
return "image/png";
|
|
40
|
-
case ".jpg":
|
|
41
|
-
case ".jpeg":
|
|
42
|
-
return "image/jpeg";
|
|
43
|
-
case ".gif":
|
|
44
|
-
return "image/gif";
|
|
45
|
-
case ".svg":
|
|
46
|
-
return "image/svg+xml";
|
|
47
|
-
case ".ico":
|
|
48
|
-
return "image/x-icon";
|
|
49
|
-
case ".txt":
|
|
50
|
-
return "text/plain";
|
|
51
|
-
case ".pdf":
|
|
52
|
-
return "application/pdf";
|
|
53
|
-
default:
|
|
54
|
-
return "application/octet-stream";
|
|
55
|
-
}
|
|
28
|
+
return mimeTypes[ext] || "application/octet-stream";
|
|
56
29
|
}
|
|
57
30
|
export function getContentType(filePath, utf8 = false) {
|
|
58
31
|
let contentType = _getContentType(filePath);
|
|
@@ -60,34 +33,83 @@ export function getContentType(filePath, utf8 = false) {
|
|
|
60
33
|
contentType += "; charset=utf-8";
|
|
61
34
|
return contentType;
|
|
62
35
|
}
|
|
63
|
-
export function handleStaticFiles(dirPath,
|
|
36
|
+
export function handleStaticFiles(dirPath, opts) {
|
|
37
|
+
opts = {
|
|
38
|
+
utf8: true,
|
|
39
|
+
render: true,
|
|
40
|
+
etag: true,
|
|
41
|
+
...opts,
|
|
42
|
+
};
|
|
43
|
+
if (!fs.existsSync(dirPath)) {
|
|
44
|
+
throw new Error(`Directory ${dirPath} does not exist`);
|
|
45
|
+
}
|
|
46
|
+
const serveFile = (req, res, filePath, stats) => {
|
|
47
|
+
if (opts.etag) {
|
|
48
|
+
const etag = `W/"${stats.size}-${stats.mtime.getTime()}"`;
|
|
49
|
+
if (req.headers["if-none-match"] === etag) {
|
|
50
|
+
res.status(304).end();
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
res.setHeader("ETag", etag);
|
|
54
|
+
}
|
|
55
|
+
if (opts.render && filePath.endsWith(".html")) {
|
|
56
|
+
res.ct("text/html");
|
|
57
|
+
res.render(filePath);
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
res.ct(getContentType(filePath, opts.utf8));
|
|
61
|
+
fs.createReadStream(filePath).pipe(res);
|
|
62
|
+
return true;
|
|
63
|
+
};
|
|
64
64
|
return (req, res, next) => {
|
|
65
65
|
if (req.method.toLowerCase() !== "get")
|
|
66
66
|
return next();
|
|
67
67
|
const apiPath = req.middleware.path;
|
|
68
68
|
const filePath = path.join(dirPath, req.path.replace(apiPath, ""));
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
69
|
+
try {
|
|
70
|
+
const stats = fs.statSync(filePath);
|
|
71
|
+
if (stats.isFile()) {
|
|
72
|
+
return serveFile(req, res, filePath, stats);
|
|
73
|
+
}
|
|
74
|
+
if (stats.isDirectory()) {
|
|
75
|
+
const indexPath = path.join(filePath, "index.html");
|
|
76
|
+
try {
|
|
77
|
+
const indexStats = fs.statSync(indexPath);
|
|
78
|
+
if (indexStats.isFile()) {
|
|
79
|
+
if (!req.path.endsWith("/")) {
|
|
80
|
+
res.redirect(req.path + "/");
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
return serveFile(req, res, indexPath, indexStats);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch (e) {
|
|
87
|
+
/* index.html not found, do nothing */
|
|
88
|
+
}
|
|
89
|
+
}
|
|
73
90
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
91
|
+
catch (e) {
|
|
92
|
+
/* file/dir not found, proceed to check for .html */
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
const htmlPath = filePath + ".html";
|
|
96
|
+
const htmlStats = fs.statSync(htmlPath);
|
|
97
|
+
if (htmlStats.isFile()) {
|
|
98
|
+
if (opts.etag) {
|
|
99
|
+
const etag = `W/"${htmlStats.size}-${htmlStats.mtime.getTime()}"`;
|
|
100
|
+
if (req.headers["if-none-match"] === etag) {
|
|
101
|
+
res.status(304).end();
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
res.setHeader("ETag", etag);
|
|
80
105
|
}
|
|
81
|
-
res.ct(getContentType(
|
|
82
|
-
fs.createReadStream(
|
|
106
|
+
res.ct(getContentType(htmlPath, opts.utf8));
|
|
107
|
+
fs.createReadStream(htmlPath).pipe(res);
|
|
83
108
|
return true;
|
|
84
109
|
}
|
|
85
110
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
res.ct(getContentType(htmlPath, utf8));
|
|
89
|
-
fs.createReadStream(htmlPath).pipe(res);
|
|
90
|
-
return true;
|
|
111
|
+
catch (e) {
|
|
112
|
+
/* .html file not found, fall through to next() */
|
|
91
113
|
}
|
|
92
114
|
next();
|
|
93
115
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,18 +1,38 @@
|
|
|
1
1
|
import { Logger, LoggerOptions } from "@wxn0brp/lucerna-log";
|
|
2
2
|
import http from "http";
|
|
3
|
-
import { PluginSystem } from "./
|
|
3
|
+
import { PluginSystem } from "./plugin.js";
|
|
4
4
|
import { renderHTML } from "./render.js";
|
|
5
5
|
import { FFResponse } from "./res.js";
|
|
6
6
|
import { Router } from "./router.js";
|
|
7
|
-
import { BeforeHandleRequest, FFRequest, RouteHandler } from "./types.js";
|
|
8
|
-
export
|
|
7
|
+
import type { BeforeHandleRequest, FFRequest, ParseBodyFunction, RouteHandler } from "./types.js";
|
|
8
|
+
export type EngineCallback = (path: string, options: any, callback: (e: any, rendered?: string) => void) => void;
|
|
9
|
+
export interface Opts {
|
|
10
|
+
loggerOpts?: LoggerOptions;
|
|
11
|
+
bodyLimit?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare class FalconFrame<Vars extends Record<string, any> = any> extends Router {
|
|
9
14
|
logger: Logger;
|
|
10
|
-
|
|
15
|
+
customParsers: Record<string, ParseBodyFunction>;
|
|
16
|
+
vars: Vars;
|
|
17
|
+
opts: Opts;
|
|
18
|
+
engines: Record<string, EngineCallback>;
|
|
19
|
+
constructor(opts?: Partial<Opts>);
|
|
11
20
|
listen(port: number, callback?: (() => void) | boolean, beforeHandleRequest?: BeforeHandleRequest): http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>;
|
|
12
21
|
getApp(beforeHandleRequest?: BeforeHandleRequest): (req: any, res: any) => Promise<void>;
|
|
22
|
+
engine(ext: string, callback: EngineCallback): this;
|
|
23
|
+
setVar(key: keyof Vars, value: typeof this.vars[keyof Vars]): void;
|
|
24
|
+
getVar(key: string): typeof this.vars[keyof Vars];
|
|
25
|
+
/**
|
|
26
|
+
* Sets the allowed origins for CORS.
|
|
27
|
+
* This method is a shortcut that simplifies CORS configuration
|
|
28
|
+
* without needing to manually create and register a plugin.
|
|
29
|
+
* @param origin - An array of allowed origins.
|
|
30
|
+
* @example
|
|
31
|
+
* app.setOrigin(["http://example.com", "https://example.com"]);
|
|
32
|
+
*/
|
|
33
|
+
setOrigin(origin: string[]): void;
|
|
13
34
|
}
|
|
14
35
|
export default FalconFrame;
|
|
15
|
-
export { FFRequest, FFResponse, PluginSystem, renderHTML, RouteHandler, Router };
|
|
16
|
-
export * as Plugins from "./plugins/index.js";
|
|
17
|
-
export * as PluginsEngine from "./plugins.js";
|
|
18
36
|
export * as Helpers from "./helpers.js";
|
|
37
|
+
export * as PluginsEngine from "./plugin.js";
|
|
38
|
+
export { FFRequest, FFResponse, PluginSystem, renderHTML, RouteHandler, Router };
|
package/dist/index.js
CHANGED
|
@@ -1,18 +1,35 @@
|
|
|
1
1
|
import { Logger } from "@wxn0brp/lucerna-log";
|
|
2
2
|
import http from "http";
|
|
3
|
-
import { PluginSystem } from "./
|
|
3
|
+
import { PluginSystem } from "./plugin.js";
|
|
4
|
+
import { createCORSPlugin } from "./plugins/cors.js";
|
|
4
5
|
import { renderHTML } from "./render.js";
|
|
5
6
|
import { handleRequest } from "./req.js";
|
|
6
7
|
import { FFResponse } from "./res.js";
|
|
7
8
|
import { Router } from "./router.js";
|
|
8
|
-
import { FFRequest } from "./types.js";
|
|
9
9
|
export class FalconFrame extends Router {
|
|
10
10
|
logger;
|
|
11
|
-
|
|
11
|
+
customParsers = {};
|
|
12
|
+
vars = {};
|
|
13
|
+
opts = {};
|
|
14
|
+
engines = {};
|
|
15
|
+
constructor(opts = {}) {
|
|
12
16
|
super();
|
|
13
17
|
this.logger = new Logger({
|
|
14
18
|
loggerName: "falcon-frame",
|
|
15
|
-
...loggerOpts
|
|
19
|
+
...[opts?.loggerOpts || {}],
|
|
20
|
+
});
|
|
21
|
+
this.opts = {
|
|
22
|
+
bodyLimit: "10m",
|
|
23
|
+
...opts,
|
|
24
|
+
};
|
|
25
|
+
this.engine(".html", (path, options, callback) => {
|
|
26
|
+
try {
|
|
27
|
+
const content = renderHTML(path, options);
|
|
28
|
+
callback(null, content);
|
|
29
|
+
}
|
|
30
|
+
catch (e) {
|
|
31
|
+
callback(e);
|
|
32
|
+
}
|
|
16
33
|
});
|
|
17
34
|
}
|
|
18
35
|
listen(port, callback, beforeHandleRequest) {
|
|
@@ -38,9 +55,34 @@ export class FalconFrame extends Router {
|
|
|
38
55
|
await handleRequest(req, res, this);
|
|
39
56
|
};
|
|
40
57
|
}
|
|
58
|
+
engine(ext, callback) {
|
|
59
|
+
if (ext[0] !== ".") {
|
|
60
|
+
ext = "." + ext;
|
|
61
|
+
}
|
|
62
|
+
this.engines[ext] = callback;
|
|
63
|
+
return this;
|
|
64
|
+
}
|
|
65
|
+
setVar(key, value) {
|
|
66
|
+
// @ts-ignore
|
|
67
|
+
this.vars[key] = value;
|
|
68
|
+
}
|
|
69
|
+
getVar(key) {
|
|
70
|
+
// @ts-ignore
|
|
71
|
+
return this.vars[key];
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Sets the allowed origins for CORS.
|
|
75
|
+
* This method is a shortcut that simplifies CORS configuration
|
|
76
|
+
* without needing to manually create and register a plugin.
|
|
77
|
+
* @param origin - An array of allowed origins.
|
|
78
|
+
* @example
|
|
79
|
+
* app.setOrigin(["http://example.com", "https://example.com"]);
|
|
80
|
+
*/
|
|
81
|
+
setOrigin(origin) {
|
|
82
|
+
this.use(createCORSPlugin(origin).process);
|
|
83
|
+
}
|
|
41
84
|
}
|
|
42
85
|
export default FalconFrame;
|
|
43
|
-
export { FFRequest, FFResponse, PluginSystem, renderHTML, Router };
|
|
44
|
-
export * as Plugins from "./plugins/index.js";
|
|
45
|
-
export * as PluginsEngine from "./plugins.js";
|
|
46
86
|
export * as Helpers from "./helpers.js";
|
|
87
|
+
export * as PluginsEngine from "./plugin.js";
|
|
88
|
+
export { FFResponse, PluginSystem, renderHTML, Router };
|
|
@@ -7,30 +7,29 @@ export class PluginSystem {
|
|
|
7
7
|
* @param options - Options for positioning plugins
|
|
8
8
|
*/
|
|
9
9
|
register(plugin, options) {
|
|
10
|
-
if (this.plugins.some(p => p.id === plugin.id)) {
|
|
10
|
+
if (this.plugins.some((p) => p.id === plugin.id)) {
|
|
11
11
|
throw new Error(`Plugin with id '${plugin.id}' already registered`);
|
|
12
12
|
}
|
|
13
|
-
// Add plugin to the list
|
|
14
13
|
this.plugins.push(plugin);
|
|
15
|
-
|
|
16
|
-
this.updateExecutionOrder(plugin.id, options);
|
|
14
|
+
this.updateExecutionOrder(plugin, options);
|
|
17
15
|
}
|
|
18
16
|
/**
|
|
19
17
|
* Updates the execution order of plugins
|
|
20
18
|
* @param pluginId - ID of the plugin to position
|
|
21
19
|
* @param options - Options for positioning
|
|
22
20
|
*/
|
|
23
|
-
updateExecutionOrder(
|
|
21
|
+
updateExecutionOrder(plugin, options) {
|
|
22
|
+
const pluginId = plugin.id;
|
|
24
23
|
if (this.executionOrder.includes(pluginId))
|
|
25
24
|
return;
|
|
26
25
|
const resolveTarget = (target) => {
|
|
27
26
|
if (!target)
|
|
28
27
|
return null;
|
|
29
28
|
const list = Array.isArray(target) ? target : [target];
|
|
30
|
-
return list.find(id => this.executionOrder.includes(id)) || null;
|
|
29
|
+
return list.find((id) => this.executionOrder.includes(id)) || null;
|
|
31
30
|
};
|
|
32
|
-
const beforeTarget = resolveTarget(options?.before);
|
|
33
|
-
const afterTarget = resolveTarget(options?.after);
|
|
31
|
+
const beforeTarget = resolveTarget(options?.before || plugin.before);
|
|
32
|
+
const afterTarget = resolveTarget(options?.after || plugin.after);
|
|
34
33
|
if (beforeTarget) {
|
|
35
34
|
const index = this.executionOrder.indexOf(beforeTarget);
|
|
36
35
|
this.executionOrder.splice(index, 0, pluginId);
|
|
@@ -74,7 +73,7 @@ export class PluginSystem {
|
|
|
74
73
|
return;
|
|
75
74
|
}
|
|
76
75
|
const pluginId = this.executionOrder[index];
|
|
77
|
-
const plugin = this.plugins.find(p => p.id === pluginId);
|
|
76
|
+
const plugin = this.plugins.find((p) => p.id === pluginId);
|
|
78
77
|
if (!plugin) {
|
|
79
78
|
throw new Error(`Plugin '${pluginId}' not found`);
|
|
80
79
|
}
|
|
@@ -87,7 +86,7 @@ export class PluginSystem {
|
|
|
87
86
|
*/
|
|
88
87
|
getPluginsInOrder() {
|
|
89
88
|
return this.executionOrder
|
|
90
|
-
.map(id => this.plugins.find(p => p.id === id))
|
|
89
|
+
.map((id) => this.plugins.find((p) => p.id === id))
|
|
91
90
|
.filter((p) => p !== undefined);
|
|
92
91
|
}
|
|
93
92
|
}
|
package/dist/plugins/cors.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { Plugin } from "../
|
|
1
|
+
import { Plugin } from "../plugin.js";
|
|
2
2
|
interface Opts {
|
|
3
3
|
accessControlAllowMethods?: boolean;
|
|
4
4
|
accessControlAllowHeaders?: boolean;
|
|
5
|
+
headers?: Record<string, string>;
|
|
5
6
|
}
|
|
6
7
|
export declare function createCORSPlugin(allowedOrigins: string[], opts?: Opts): Plugin;
|
|
7
8
|
export {};
|
package/dist/plugins/cors.js
CHANGED
|
@@ -13,6 +13,11 @@ export function createCORSPlugin(allowedOrigins, opts = {}) {
|
|
|
13
13
|
return {
|
|
14
14
|
id: "cors",
|
|
15
15
|
process: (req, res, next) => {
|
|
16
|
+
if (opts.headers) {
|
|
17
|
+
for (const [key, value] of Object.entries(opts.headers)) {
|
|
18
|
+
res.setHeader(key, value);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
16
21
|
if (allowedOrigins.includes("*")) {
|
|
17
22
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
18
23
|
setHeader(res, opts);
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { Plugin } from "../
|
|
1
|
+
import { Plugin } from "../plugin.js";
|
|
2
2
|
export declare function createRateLimiterPlugin(maxRequests: number, windowMs: number): Plugin;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function createSecurityPlugin() {
|
|
2
|
+
return {
|
|
3
|
+
id: "security",
|
|
4
|
+
process: (req, res, next) => {
|
|
5
|
+
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
6
|
+
res.setHeader("X-Frame-Options", "SAMEORIGIN");
|
|
7
|
+
res.setHeader("Referrer-Policy", "no-referrer");
|
|
8
|
+
res.setHeader("X-XSS-Protection", "1; mode=block");
|
|
9
|
+
res.setHeader("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
|
|
10
|
+
next();
|
|
11
|
+
},
|
|
12
|
+
after: "cors",
|
|
13
|
+
};
|
|
14
|
+
}
|
package/dist/render.d.ts
CHANGED
package/dist/render.js
CHANGED
|
@@ -1,18 +1,25 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
|
-
export function renderHTML(templatePath, data) {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
return
|
|
16
|
-
|
|
17
|
-
|
|
3
|
+
export function renderHTML(templatePath, data, renderedPaths = []) {
|
|
4
|
+
try {
|
|
5
|
+
const realPath = path.resolve(templatePath);
|
|
6
|
+
if (renderedPaths.includes(realPath)) {
|
|
7
|
+
return `<!-- Circular dependency detected: tried to render ${templatePath} again -->`;
|
|
8
|
+
}
|
|
9
|
+
let template = fs.readFileSync(templatePath, "utf8");
|
|
10
|
+
// Inserting data, e.g. {{name}}
|
|
11
|
+
template = template.replace(/{{(.*?)}}/g, (_, key) => data[key.trim()] || "");
|
|
12
|
+
// Loading partials, e.g. <!-- include header -->
|
|
13
|
+
template = template.replace(/<!--\s*include\s*(.*?)\s*-->/g, (_, partialName) => {
|
|
14
|
+
const partialPath = path.join(path.dirname(templatePath), partialName + ".html");
|
|
15
|
+
return renderHTML(partialPath, data, [
|
|
16
|
+
...renderedPaths,
|
|
17
|
+
realPath,
|
|
18
|
+
]);
|
|
19
|
+
});
|
|
20
|
+
return template;
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
return `<!-- Template not found: ${templatePath} -->`;
|
|
24
|
+
}
|
|
18
25
|
}
|
package/dist/req.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { URL } from "url";
|
|
2
|
-
import {
|
|
2
|
+
import { parseCookies } from "./helpers.js";
|
|
3
3
|
import { FFResponse } from "./res.js";
|
|
4
4
|
import { validate } from "./valid.js";
|
|
5
5
|
import { getMiddlewares, matchMiddleware } from "./middleware.js";
|
|
6
|
+
import { parseBody } from "./body.js";
|
|
6
7
|
export function handleRequest(req, res, FF) {
|
|
7
8
|
Object.setPrototypeOf(res, FFResponse.prototype);
|
|
9
|
+
res.FF = FF;
|
|
8
10
|
const originalEnd = res.end;
|
|
9
11
|
res.end = function (...any) {
|
|
10
12
|
res._ended = true;
|
|
@@ -29,9 +31,11 @@ export function handleRequest(req, res, FF) {
|
|
|
29
31
|
logger.info(`Incoming request: ${req.method} ${req.url}`);
|
|
30
32
|
const middlewaresPath = req.path + "/";
|
|
31
33
|
const middlewares = getMiddlewares(FF.middlewares, middlewaresPath.replace(/\/+/g, "/"));
|
|
32
|
-
const matchedTypeMiddlewares = middlewares.filter(middleware => middleware.method === req.method.toLowerCase() ||
|
|
34
|
+
const matchedTypeMiddlewares = middlewares.filter((middleware) => middleware.method === req.method.toLowerCase() ||
|
|
35
|
+
middleware.method === "all");
|
|
33
36
|
const matchedMiddlewares = matchMiddleware(req.path, matchedTypeMiddlewares);
|
|
34
|
-
logger.debug("Matched middlewares: " +
|
|
37
|
+
logger.debug("Matched middlewares: " +
|
|
38
|
+
matchedMiddlewares.map((middleware) => middleware.path).join(", "));
|
|
35
39
|
if (matchedMiddlewares.length === 0) {
|
|
36
40
|
res.status(404).end("404: File had second thoughts");
|
|
37
41
|
return;
|
|
@@ -67,15 +71,18 @@ export function handleRequest(req, res, FF) {
|
|
|
67
71
|
}
|
|
68
72
|
}
|
|
69
73
|
}
|
|
70
|
-
if (req.method === "GET"
|
|
74
|
+
if (req.method === "GET" ||
|
|
75
|
+
req.method === "HEAD" ||
|
|
76
|
+
req.method === "OPTIONS") {
|
|
77
|
+
req.body = {};
|
|
71
78
|
next();
|
|
72
79
|
return;
|
|
73
80
|
}
|
|
74
81
|
let body = "";
|
|
75
|
-
req.on("data", chunk => (body += chunk.toString()));
|
|
76
|
-
req.on("end", () => {
|
|
77
|
-
const
|
|
78
|
-
|
|
82
|
+
req.on("data", (chunk) => (body += chunk.toString()));
|
|
83
|
+
req.on("end", async () => {
|
|
84
|
+
const parsedBody = await parseBody(req, body, FF);
|
|
85
|
+
Object.assign(req, parsedBody);
|
|
79
86
|
logger.debug(`Request body: ${JSON.stringify(req.body)}`);
|
|
80
87
|
next();
|
|
81
88
|
});
|
package/dist/res.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import http from "http";
|
|
2
|
+
import FalconFrame from "./index.js";
|
|
2
3
|
import { CookieOptions } from "./types.js";
|
|
3
4
|
export declare class FFResponse extends http.ServerResponse {
|
|
4
5
|
_ended: boolean;
|
|
6
|
+
FF: FalconFrame;
|
|
5
7
|
/**
|
|
6
8
|
* bind end for compatibility
|
|
7
|
-
|
|
9
|
+
*/
|
|
8
10
|
send(data: string): void;
|
|
9
11
|
/**
|
|
10
12
|
* Sets a header. This is a shortcut for setHeader.
|
|
@@ -54,13 +56,13 @@ export declare class FFResponse extends http.ServerResponse {
|
|
|
54
56
|
*/
|
|
55
57
|
sendFile(filePath: string, contentType?: string): this;
|
|
56
58
|
/**
|
|
57
|
-
* Renders
|
|
58
|
-
*
|
|
59
|
-
* @param
|
|
59
|
+
* Renders a view with the given data and sends it as the response.
|
|
60
|
+
* It uses the registered template engine.
|
|
61
|
+
* @param view The name of the view file to render.
|
|
60
62
|
* @param data An object containing data to be injected into the template.
|
|
61
63
|
* @returns The response object.
|
|
62
64
|
*/
|
|
63
|
-
render(
|
|
65
|
+
render(view: string, data?: any): this;
|
|
64
66
|
/**
|
|
65
67
|
* Initialize SSE headers to start a server-sent event stream.
|
|
66
68
|
* Sets:
|
package/dist/res.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
+
import { createReadStream } from "fs";
|
|
1
2
|
import http from "http";
|
|
3
|
+
import path from "path";
|
|
2
4
|
import { getContentType } from "./helpers.js";
|
|
3
|
-
import { createReadStream } from "fs";
|
|
4
|
-
import { renderHTML } from "./render.js";
|
|
5
5
|
export class FFResponse extends http.ServerResponse {
|
|
6
6
|
_ended = false;
|
|
7
|
+
FF;
|
|
7
8
|
/**
|
|
8
9
|
* bind end for compatibility
|
|
9
|
-
|
|
10
|
+
*/
|
|
10
11
|
send(data) {
|
|
11
12
|
this.end(data);
|
|
12
13
|
}
|
|
@@ -97,15 +98,47 @@ export class FFResponse extends http.ServerResponse {
|
|
|
97
98
|
return this;
|
|
98
99
|
}
|
|
99
100
|
/**
|
|
100
|
-
* Renders
|
|
101
|
-
*
|
|
102
|
-
* @param
|
|
101
|
+
* Renders a view with the given data and sends it as the response.
|
|
102
|
+
* It uses the registered template engine.
|
|
103
|
+
* @param view The name of the view file to render.
|
|
103
104
|
* @param data An object containing data to be injected into the template.
|
|
104
105
|
* @returns The response object.
|
|
105
106
|
*/
|
|
106
|
-
render(
|
|
107
|
-
this.
|
|
108
|
-
|
|
107
|
+
render(view, data = {}) {
|
|
108
|
+
const ff = this.FF;
|
|
109
|
+
const viewEngine = ff.getVar("view engine");
|
|
110
|
+
const viewsDir = ff.getVar("views") || ".";
|
|
111
|
+
let finalExt = path.extname(view);
|
|
112
|
+
let filePath = view;
|
|
113
|
+
if (!finalExt) {
|
|
114
|
+
const defaultExt = viewEngine ? (viewEngine.startsWith(".") ? viewEngine : "." + viewEngine) : ".html";
|
|
115
|
+
finalExt = defaultExt;
|
|
116
|
+
filePath += finalExt;
|
|
117
|
+
}
|
|
118
|
+
const engine = ff.engines[finalExt];
|
|
119
|
+
if (!engine) {
|
|
120
|
+
const errMessage = `No engine registered for extension ${finalExt}`;
|
|
121
|
+
ff.logger.error(errMessage);
|
|
122
|
+
this.status(500).end("Server Error: " + errMessage);
|
|
123
|
+
return this;
|
|
124
|
+
}
|
|
125
|
+
const fullPath = path.resolve(viewsDir, filePath);
|
|
126
|
+
try {
|
|
127
|
+
engine(fullPath, data, (err, str) => {
|
|
128
|
+
if (err) {
|
|
129
|
+
ff.logger.error(`Error rendering view: ${err}`);
|
|
130
|
+
this.status(500).end("Server Error: Failed to render view.");
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
this.setHeader("Content-Type", "text/html");
|
|
134
|
+
this.end(str);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
catch (err) {
|
|
139
|
+
ff.logger.error(`Unhandled error in template engine: ${err}`);
|
|
140
|
+
this.status(500).end("Server Error: Unhandled exception in template engine.");
|
|
141
|
+
}
|
|
109
142
|
return this;
|
|
110
143
|
}
|
|
111
144
|
/**
|
package/dist/router.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Method, Middleware, RouteHandler } from "./types.js";
|
|
1
|
+
import { Method, Middleware, RouteHandler, StaticServeOptions } from "./types.js";
|
|
2
2
|
export declare class Router {
|
|
3
3
|
middlewares: Middleware[];
|
|
4
4
|
addRoute(method: Method, path: string, ...handlers: RouteHandler[]): number;
|
|
@@ -8,6 +8,6 @@ export declare class Router {
|
|
|
8
8
|
put(path: string, ...handlers: RouteHandler[]): this;
|
|
9
9
|
delete(path: string, ...handlers: RouteHandler[]): this;
|
|
10
10
|
all(path: string, ...handlers: RouteHandler[]): this;
|
|
11
|
-
static(apiPath: string, dirPath?: string,
|
|
11
|
+
static(apiPath: string, dirPath?: string, opts?: StaticServeOptions): this;
|
|
12
12
|
sse(path: string, ...handlers: RouteHandler[]): this;
|
|
13
13
|
}
|
package/dist/router.js
CHANGED
|
@@ -3,7 +3,7 @@ export class Router {
|
|
|
3
3
|
middlewares = [];
|
|
4
4
|
addRoute(method, path, ...handlers) {
|
|
5
5
|
const handler = handlers.pop();
|
|
6
|
-
handlers.forEach(middleware => this.use(path, middleware));
|
|
6
|
+
handlers.forEach((middleware) => this.use(path, middleware));
|
|
7
7
|
return this.middlewares.push({ path, middleware: handler, method });
|
|
8
8
|
}
|
|
9
9
|
use(path = "/", middlewareFn, method = "all") {
|
|
@@ -15,7 +15,7 @@ export class Router {
|
|
|
15
15
|
path,
|
|
16
16
|
method,
|
|
17
17
|
middleware: null,
|
|
18
|
-
use: true
|
|
18
|
+
use: true,
|
|
19
19
|
};
|
|
20
20
|
if (middlewareFn instanceof Router) {
|
|
21
21
|
middleware.router = middlewareFn.middlewares;
|
|
@@ -46,12 +46,12 @@ export class Router {
|
|
|
46
46
|
this.addRoute("all", path, ...handlers);
|
|
47
47
|
return this;
|
|
48
48
|
}
|
|
49
|
-
static(apiPath, dirPath,
|
|
49
|
+
static(apiPath, dirPath, opts = {}) {
|
|
50
50
|
if (!dirPath) {
|
|
51
51
|
dirPath = apiPath;
|
|
52
52
|
apiPath = "/";
|
|
53
53
|
}
|
|
54
|
-
this.use(apiPath, handleStaticFiles(dirPath,
|
|
54
|
+
this.use(apiPath, handleStaticFiles(dirPath, opts));
|
|
55
55
|
return this;
|
|
56
56
|
}
|
|
57
57
|
sse(path, ...handlers) {
|
package/dist/types.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import FalconFrame from "./index.js";
|
|
1
2
|
import { FFResponse } from "./res.js";
|
|
2
3
|
import http from "http";
|
|
3
4
|
export type RouteHandler = (req: FFRequest, res: FFResponse, next?: () => void) => void | any;
|
|
@@ -14,6 +15,11 @@ export interface Query {
|
|
|
14
15
|
export interface Body {
|
|
15
16
|
[key: string]: any;
|
|
16
17
|
}
|
|
18
|
+
export interface ParseBody {
|
|
19
|
+
body?: Body;
|
|
20
|
+
files?: Record<string, Buffer>;
|
|
21
|
+
}
|
|
22
|
+
export type ParseBodyFunction = (body: string, req: FFRequest, FF: FalconFrame) => Promise<ParseBody>;
|
|
17
23
|
export declare class FFRequest extends http.IncomingMessage {
|
|
18
24
|
path: string;
|
|
19
25
|
query: Query;
|
|
@@ -48,3 +54,8 @@ export interface ValidationResult {
|
|
|
48
54
|
};
|
|
49
55
|
}
|
|
50
56
|
export type BeforeHandleRequest = (req: http.IncomingMessage, res: http.ServerResponse) => any;
|
|
57
|
+
export interface StaticServeOptions {
|
|
58
|
+
utf8?: boolean;
|
|
59
|
+
render?: boolean;
|
|
60
|
+
etag?: boolean;
|
|
61
|
+
}
|
package/dist/valid.js
CHANGED
|
@@ -24,7 +24,8 @@ export function validate(schema, data) {
|
|
|
24
24
|
}
|
|
25
25
|
break;
|
|
26
26
|
case "min":
|
|
27
|
-
if (typeof value === "string" &&
|
|
27
|
+
if (typeof value === "string" &&
|
|
28
|
+
value.length < parseInt(param)) {
|
|
28
29
|
fieldErrors.push(`${key} must be at least ${param} characters long`);
|
|
29
30
|
}
|
|
30
31
|
if (typeof value === "number" && value < parseInt(param)) {
|
|
@@ -32,7 +33,8 @@ export function validate(schema, data) {
|
|
|
32
33
|
}
|
|
33
34
|
break;
|
|
34
35
|
case "max":
|
|
35
|
-
if (typeof value === "string" &&
|
|
36
|
+
if (typeof value === "string" &&
|
|
37
|
+
value.length > parseInt(param)) {
|
|
36
38
|
fieldErrors.push(`${key} must not exceed ${param} characters`);
|
|
37
39
|
}
|
|
38
40
|
if (typeof value === "number" && value > parseInt(param)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wxn0brp/falcon-frame",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"author": "wxn0brP",
|
|
@@ -12,12 +12,12 @@
|
|
|
12
12
|
"url": "https://github.com/wxn0brP/FalconFrame.git"
|
|
13
13
|
},
|
|
14
14
|
"devDependencies": {
|
|
15
|
-
"@types/node": "
|
|
16
|
-
"tsc-alias": "
|
|
17
|
-
"typescript": "
|
|
15
|
+
"@types/node": "*",
|
|
16
|
+
"tsc-alias": "*",
|
|
17
|
+
"typescript": "*"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@wxn0brp/lucerna-log": "^0.
|
|
20
|
+
"@wxn0brp/lucerna-log": "^0.2.0"
|
|
21
21
|
},
|
|
22
22
|
"files": [
|
|
23
23
|
"dist"
|
package/dist/plugins/index.d.ts
DELETED
package/dist/plugins/index.js
DELETED