From 11d8a862b4e4a3e5470b40156dde254a309e1aa9 Mon Sep 17 00:00:00 2001 From: SrGooglo Date: Mon, 14 Apr 2025 14:54:18 +0000 Subject: [PATCH] Merge from local --- server/bin/boot.js | 182 -------- server/bootloader/bin | 49 ++ server/bootloader/boot.js | 55 +++ server/bootloader/bootWrapper.js | 48 ++ server/bootloader/injectEnvFromInfisical.js | 32 ++ server/bootloader/patches.js | 49 ++ server/bootloader/registerBaseAliases.js | 14 + server/bootloader/startFileWatcher.js | 29 ++ server/package.json | 24 +- server/src/baseEndpoints/main.js | 21 - server/src/baseEndpoints/map.js | 24 - server/src/baseRoutes/main.js | 20 + server/src/baseRoutes/map.js | 27 ++ server/src/classes/Endpoint/index.js | 17 + server/src/classes/Handler/index.js | 55 +++ server/src/classes/IPCClient/index.js | 251 +++++------ .../index.js | 0 server/src/classes/Route/index.js | 58 +++ .../{rtengineng => RtEngine}/client.js | 0 .../classes/{rtengineng => RtEngine}/event.js | 0 .../{rtengineng => RtEngine}/events.js | 0 .../classes/{rtengineng => RtEngine}/index.js | 2 +- server/src/classes/endpoint/index.js | 87 ---- server/src/classes/index.js | 5 - server/src/defaults.js | 65 --- server/src/engines/he-legacy/index.js | 80 ++++ .../he-legacy}/rtengine/index.js | 2 +- .../he-legacy/rtengine/redis_map.js} | 0 server/src/engines/he/index.js | 81 ++++ server/src/engines/hyper-express-ng/index.js | 100 ----- server/src/engines/hyper-express/index.js | 99 ----- server/src/engines/index.js | 8 +- server/src/engines/worker/index.js | 170 ++++--- server/src/index.js | 8 +- .../registerBaseEndpoints/index.js | 17 - .../registerHttpRoutes/index.js | 79 ---- .../registerWebsocketsEvents/index.js | 28 -- server/src/lib/generateController/index.js | 18 - .../src/lib/generateEndpointsFromDir/index.js | 23 - server/src/lib/loadEndpointsFromDir/index.js | 41 -- server/src/middlewares/cors/index.js | 8 - server/src/patches.js | 16 +- server/src/registers/baseHeaders.js | 22 + server/src/registers/baseMiddlewares.js | 14 + server/src/registers/baseRoutes.js | 21 + server/src/registers/bypassCorsHeaders.js | 6 + server/src/registers/httpFileRoutes.js | 64 +++ server/src/registers/ipcService.js | 47 ++ server/src/registers/websocketFileEvents.js | 28 ++ server/src/server.js | 417 +++++++----------- server/src/utils/composeMiddlewares.js | 29 ++ server/src/utils/getHostAddress.js | 23 + server/src/utils/index.js | 1 - server/src/utils/isExperimental.js | 8 + .../src/utils/{nanoid/index.js => nanoid.js} | 0 .../index.js => utils/recursiveRegister.js} | 0 .../{schematized/index.js => schematized.js} | 0 server/src/vars.js | 48 ++ 58 files changed, 1320 insertions(+), 1300 deletions(-) delete mode 100755 server/bin/boot.js create mode 100755 server/bootloader/bin create mode 100644 server/bootloader/boot.js create mode 100644 server/bootloader/bootWrapper.js create mode 100644 server/bootloader/injectEnvFromInfisical.js create mode 100644 server/bootloader/patches.js create mode 100644 server/bootloader/registerBaseAliases.js create mode 100644 server/bootloader/startFileWatcher.js delete mode 100755 server/src/baseEndpoints/main.js delete mode 100755 server/src/baseEndpoints/map.js create mode 100755 server/src/baseRoutes/main.js create mode 100755 server/src/baseRoutes/map.js create mode 100644 server/src/classes/Endpoint/index.js create mode 100644 server/src/classes/Handler/index.js rename server/src/classes/{operation_error => OperationError}/index.js (100%) create mode 100755 server/src/classes/Route/index.js rename server/src/classes/{rtengineng => RtEngine}/client.js (100%) rename server/src/classes/{rtengineng => RtEngine}/event.js (100%) rename server/src/classes/{rtengineng => RtEngine}/events.js (100%) rename server/src/classes/{rtengineng => RtEngine}/index.js (99%) delete mode 100755 server/src/classes/endpoint/index.js delete mode 100755 server/src/classes/index.js delete mode 100755 server/src/defaults.js create mode 100755 server/src/engines/he-legacy/index.js rename server/src/{classes => engines/he-legacy}/rtengine/index.js (99%) rename server/src/{lib/redis_map/index.js => engines/he-legacy/rtengine/redis_map.js} (100%) create mode 100755 server/src/engines/he/index.js delete mode 100755 server/src/engines/hyper-express-ng/index.js delete mode 100755 server/src/engines/hyper-express/index.js delete mode 100755 server/src/initializators/registerBaseEndpoints/index.js delete mode 100755 server/src/initializators/registerHttpRoutes/index.js delete mode 100755 server/src/initializators/registerWebsocketsEvents/index.js delete mode 100755 server/src/lib/generateController/index.js delete mode 100755 server/src/lib/generateEndpointsFromDir/index.js delete mode 100755 server/src/lib/loadEndpointsFromDir/index.js delete mode 100755 server/src/middlewares/cors/index.js create mode 100644 server/src/registers/baseHeaders.js create mode 100644 server/src/registers/baseMiddlewares.js create mode 100755 server/src/registers/baseRoutes.js create mode 100644 server/src/registers/bypassCorsHeaders.js create mode 100755 server/src/registers/httpFileRoutes.js create mode 100644 server/src/registers/ipcService.js create mode 100755 server/src/registers/websocketFileEvents.js create mode 100644 server/src/utils/composeMiddlewares.js create mode 100644 server/src/utils/getHostAddress.js delete mode 100755 server/src/utils/index.js create mode 100644 server/src/utils/isExperimental.js rename server/src/utils/{nanoid/index.js => nanoid.js} (100%) rename server/src/{lib/recursiveRegister/index.js => utils/recursiveRegister.js} (100%) rename server/src/utils/{schematized/index.js => schematized.js} (100%) create mode 100755 server/src/vars.js diff --git a/server/bin/boot.js b/server/bin/boot.js deleted file mode 100755 index 3b4608c..0000000 --- a/server/bin/boot.js +++ /dev/null @@ -1,182 +0,0 @@ -#!/usr/bin/env node -require("dotenv").config() -require("sucrase/register") - -const path = require("node:path") -const Module = require("node:module") -const { Buffer } = require("node:buffer") -const { webcrypto: crypto } = require("node:crypto") -const { InfisicalClient } = require("@infisical/sdk") -const moduleAlias = require("module-alias") -const { onExit } = require("signal-exit") - -// Override file execution arg -process.argv.splice(1, 1) -process.argv[1] = path.resolve(process.argv[1]) - -// Expose boot function to global -global.Boot = Boot -global.isProduction = process.env.NODE_ENV === "production" - -global["__root"] = path.resolve(process.cwd()) -global["__src"] = path.resolve(globalThis["__root"], path.dirname(process.argv[1])) - -global["aliases"] = { - "root": global["__root"], - "src": global["__src"], - - // expose shared resources - "@db": path.resolve(process.cwd(), "db_models"), - "@db_models": path.resolve(process.cwd(), "db_models"), - "@shared-utils": path.resolve(process.cwd(), "utils"), - "@shared-classes": path.resolve(process.cwd(), "classes"), - "@shared-lib": path.resolve(process.cwd(), "lib"), - "@shared-middlewares": path.resolve(process.cwd(), "middlewares"), - - // expose internal resources - "@lib": path.resolve(global["__src"], "lib"), - "@middlewares": path.resolve(global["__src"], "middlewares"), - "@controllers": path.resolve(global["__src"], "controllers"), - "@config": path.resolve(global["__src"], "config"), - "@shared": path.resolve(global["__src"], "shared"), - "@classes": path.resolve(global["__src"], "classes"), - "@models": path.resolve(global["__src"], "models"), - "@services": path.resolve(global["__src"], "services"), - "@utils": path.resolve(global["__src"], "utils"), -} - -function registerBaseAliases(fromPath, customAliases = {}) { - if (typeof fromPath === "undefined") { - if (module.parent.filename.includes("dist")) { - fromPath = path.resolve(process.cwd(), "dist") - } else { - fromPath = path.resolve(process.cwd(), "src") - } - } - - moduleAlias.addAliases({ - ...customAliases, - "@controllers": path.resolve(fromPath, "controllers"), - "@middlewares": path.resolve(fromPath, "middlewares"), - "@models": path.resolve(fromPath, "models"), - "@classes": path.resolve(fromPath, "classes"), - "@lib": path.resolve(fromPath, "lib"), - "@utils": path.resolve(fromPath, "utils"), - }) -} - -function registerPatches() { - global.b64Decode = (data) => { - return Buffer.from(data, "base64").toString("utf-8") - } - global.b64Encode = (data) => { - return Buffer.from(data, "utf-8").toString("base64") - } - - global.nanoid = (t = 21) => crypto.getRandomValues(new Uint8Array(t)).reduce(((t, e) => t += (e &= 63) < 36 ? e.toString(36) : e < 62 ? (e - 26).toString(36).toUpperCase() : e > 62 ? "-" : "_"), ""); - - Array.prototype.updateFromObjectKeys = function (obj) { - this.forEach((value, index) => { - if (obj[value] !== undefined) { - this[index] = obj[value] - } - }) - - return this - } - - global.ToBoolean = (value) => { - if (typeof value === "boolean") { - return value - } - - if (typeof value === "string") { - return value.toLowerCase() === "true" - } - - return false - } -} - -function registerAliases() { - registerBaseAliases(global["__src"], global["aliases"]) -} - -async function injectEnvFromInfisical() { - const envMode = global.FORCE_ENV ?? global.isProduction ? "prod" : "dev" - - console.log(`[BOOT] šŸ”‘ Injecting env variables from INFISICAL in [${envMode}] mode...`) - - const client = new InfisicalClient({ - auth: { - universalAuth: { - clientId: process.env.INFISICAL_CLIENT_ID, - clientSecret: process.env.INFISICAL_CLIENT_SECRET, - } - }, - }) - - const secrets = await client.listSecrets({ - environment: envMode, - path: process.env.INFISICAL_PATH ?? "/", - projectId: process.env.INFISICAL_PROJECT_ID ?? null, - includeImports: false, - }) - - //inject to process.env - secrets.forEach((secret) => { - if (!(process.env[secret.secretKey])) { - process.env[secret.secretKey] = secret.secretValue - } - }) -} - -async function Boot(main) { - if (!main) { - throw new Error("main class is not defined") - } - - console.log(`[BOOT] Booting in [${global.isProduction ? "production" : "development"}] mode...`) - - if (process.env.INFISICAL_CLIENT_ID && process.env.INFISICAL_CLIENT_SECRET) { - console.log(`[BOOT] INFISICAL Credentials found, injecting env variables from INFISICAL...`) - await injectEnvFromInfisical() - } - - const instance = new main() - - onExit((code, signal) => { - console.log(`[BOOT] Cleaning up...`) - - if (typeof instance.onClose === "function") { - instance.onClose() - } - - instance.engine.close() - }, { - alwaysLast: true, - }) - - await instance.initialize() - - if (process.env.lb_service && process.send) { - process.send({ - status: "ready" - }) - } - - return instance -} - -try { - // Apply patches - registerPatches() - - // Apply aliases - registerAliases() - - // execute main - Module.runMain() -} catch (error) { - console.error("[BOOT] āŒ Boot error: ", error) -} \ No newline at end of file diff --git a/server/bootloader/bin b/server/bootloader/bin new file mode 100755 index 0000000..1572df5 --- /dev/null +++ b/server/bootloader/bin @@ -0,0 +1,49 @@ +#!/usr/bin/env node +const path = require("node:path") +const childProcess = require("node:child_process") +const startFileWatcher = require("./startFileWatcher.js") + +const bootloaderPath = path.resolve(__dirname, "boot.js") +const mainModulePath = process.argv[2] +const mainModuleSrc = path.resolve(process.cwd(), path.dirname(mainModulePath)) + +let childProcessInstance = null +let reloadTimeout = null + +function selfReload() { + if (!childProcessInstance) { + console.error( + "[BOOT] Cannot self-reload. Missing childProcessInstance.", + ) + return process.exit(0) + } + + console.log("[BOOT] Reloading...") + + childProcessInstance.kill() + + boot() +} + +function selfReloadDebounce() { + if (reloadTimeout) { + clearTimeout(reloadTimeout) + } + + reloadTimeout = setTimeout(selfReload, 300) +} + +function boot() { + childProcessInstance = childProcess.fork(bootloaderPath, [mainModulePath], { + stdio: "inherit", + }) +} + +// if --watch flag exist, start file watcher +if (process.argv.includes("--watch")) { + startFileWatcher(mainModuleSrc, { + onReload: selfReloadDebounce, + }) +} + +boot() diff --git a/server/bootloader/boot.js b/server/bootloader/boot.js new file mode 100644 index 0000000..8e2c1cc --- /dev/null +++ b/server/bootloader/boot.js @@ -0,0 +1,55 @@ +require("dotenv").config() +require("sucrase/register") + +const path = require("node:path") +const Module = require("node:module") +const registerBaseAliases = require("./registerBaseAliases") + +// Override file execution arg +process.argv.splice(1, 1) +process.argv[1] = path.resolve(process.argv[1]) + +// Expose to global +global.paths = { + root: process.cwd(), + __src: path.resolve(process.cwd(), path.dirname(process.argv[1])), +} + +global["aliases"] = { + // expose src + "@": global.paths.__src, + + // expose shared resources + "@db": path.resolve(process.cwd(), "db_models"), + "@db_models": path.resolve(process.cwd(), "db_models"), + "@shared-utils": path.resolve(process.cwd(), "utils"), + "@shared-classes": path.resolve(process.cwd(), "classes"), + "@shared-lib": path.resolve(process.cwd(), "lib"), + "@shared-middlewares": path.resolve(process.cwd(), "middlewares"), + + // expose internal resources + "@routes": path.resolve(paths.__src, "routes"), + "@models": path.resolve(paths.__src, "models"), + "@middlewares": path.resolve(paths.__src, "middlewares"), + "@classes": path.resolve(paths.__src, "classes"), + "@services": path.resolve(paths.__src, "services"), + "@config": path.resolve(paths.__src, "config"), + "@utils": path.resolve(paths.__src, "utils"), + "@lib": path.resolve(paths.__src, "lib"), +} + +// expose bootwrapper to global +global.Boot = require("./bootWrapper") + +try { + // apply patches + require("./patches.js") + + // Apply aliases + registerBaseAliases(global.paths.__src, global["aliases"]) + + // execute main + Module.runMain() +} catch (error) { + console.error("[BOOT] āŒ Boot error: ", error) +} diff --git a/server/bootloader/bootWrapper.js b/server/bootloader/bootWrapper.js new file mode 100644 index 0000000..aa2cb0c --- /dev/null +++ b/server/bootloader/bootWrapper.js @@ -0,0 +1,48 @@ +const { onExit } = require("signal-exit") +const injectEnvFromInfisical = require("./injectEnvFromInfisical") + +module.exports = async function Boot(main) { + if (!main) { + throw new Error("main class is not defined") + } + + console.log( + `[BOOT] Booting in [${global.isProduction ? "production" : "development"}] mode...`, + ) + + if ( + process.env.INFISICAL_CLIENT_ID && + process.env.INFISICAL_CLIENT_SECRET + ) { + console.log( + `[BOOT] INFISICAL Credentials found, injecting env variables from INFISICAL...`, + ) + await injectEnvFromInfisical() + } + + const instance = new main() + + process.on("exit", (code) => { + console.log(`[BOOT] Closing ...`) + + instance._fireClose() + }) + + process.on("SIGTERM", () => { + process.exit(0) + }) + + process.on("SIGINT", () => { + process.exit(0) + }) + + await instance.initialize() + + if (process.env.lb_service && process.send) { + process.send({ + status: "ready", + }) + } + + return instance +} diff --git a/server/bootloader/injectEnvFromInfisical.js b/server/bootloader/injectEnvFromInfisical.js new file mode 100644 index 0000000..25b609a --- /dev/null +++ b/server/bootloader/injectEnvFromInfisical.js @@ -0,0 +1,32 @@ +const { InfisicalClient } = require("@infisical/sdk") + +module.exports = async function injectEnvFromInfisical() { + const envMode = (global.FORCE_ENV ?? global.isProduction) ? "prod" : "dev" + + console.log( + `[BOOT] šŸ”‘ Injecting env variables from INFISICAL in [${envMode}] mode...`, + ) + + const client = new InfisicalClient({ + auth: { + universalAuth: { + clientId: process.env.INFISICAL_CLIENT_ID, + clientSecret: process.env.INFISICAL_CLIENT_SECRET, + }, + }, + }) + + const secrets = await client.listSecrets({ + environment: envMode, + path: process.env.INFISICAL_PATH ?? "/", + projectId: process.env.INFISICAL_PROJECT_ID ?? null, + includeImports: false, + }) + + //inject to process.env + secrets.forEach((secret) => { + if (!process.env[secret.secretKey]) { + process.env[secret.secretKey] = secret.secretValue + } + }) +} diff --git a/server/bootloader/patches.js b/server/bootloader/patches.js new file mode 100644 index 0000000..f715802 --- /dev/null +++ b/server/bootloader/patches.js @@ -0,0 +1,49 @@ +const { webcrypto: crypto } = require("node:crypto") +const { Buffer } = require("node:buffer") + +global.isProduction = process.env.NODE_ENV === "production" + +global.b64Decode = (data) => { + return Buffer.from(data, "base64").toString("utf-8") +} +global.b64Encode = (data) => { + return Buffer.from(data, "utf-8").toString("base64") +} + +global.nanoid = (t = 21) => + crypto + .getRandomValues(new Uint8Array(t)) + .reduce( + (t, e) => + (t += + (e &= 63) < 36 + ? e.toString(36) + : e < 62 + ? (e - 26).toString(36).toUpperCase() + : e > 62 + ? "-" + : "_"), + "", + ) + +Array.prototype.updateFromObjectKeys = function (obj) { + this.forEach((value, index) => { + if (obj[value] !== undefined) { + this[index] = obj[value] + } + }) + + return this +} + +global.ToBoolean = (value) => { + if (typeof value === "boolean") { + return value + } + + if (typeof value === "string") { + return value.toLowerCase() === "true" + } + + return false +} diff --git a/server/bootloader/registerBaseAliases.js b/server/bootloader/registerBaseAliases.js new file mode 100644 index 0000000..8c7b976 --- /dev/null +++ b/server/bootloader/registerBaseAliases.js @@ -0,0 +1,14 @@ +const path = require("node:path") +const moduleAlias = require("module-alias") + +module.exports = function registerBaseAliases(fromPath, customAliases = {}) { + if (typeof fromPath === "undefined") { + if (module.parent.filename.includes("dist")) { + fromPath = path.resolve(process.cwd(), "dist") + } else { + fromPath = path.resolve(process.cwd(), "src") + } + } + + moduleAlias.addAliases(customAliases) +} diff --git a/server/bootloader/startFileWatcher.js b/server/bootloader/startFileWatcher.js new file mode 100644 index 0000000..2c453ef --- /dev/null +++ b/server/bootloader/startFileWatcher.js @@ -0,0 +1,29 @@ +const chokidar = require("chokidar") +const { minimatch } = require("minimatch") + +const defaultIgnored = [ + "**/.cache/**", + "**/node_modules/**", + "**/dist/**", + "**/build/**", +] + +module.exports = async (fromPath, { onReload }) => { + console.log("[WATCHER] Starting watching path >", fromPath) + + global._watcher = chokidar.watch(fromPath, { + ignored: (path) => + defaultIgnored.some((pattern) => minimatch(path, pattern)), + persistent: true, + ignoreInitial: true, + awaitWriteFinish: true, + }) + + global._watcher.on("all", (event, filePath) => { + console.log(`[WATCHER] Event [${event}] > ${filePath}`) + + if (typeof onReload === "function") { + onReload() + } + }) +} diff --git a/server/package.json b/server/package.json index ffcf682..cfb4a88 100755 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "linebridge", - "version": "0.26.0", + "version": "1.0.0", "description": "Multiproposal framework to build fast, scalable, and secure servers.", "author": "RageStudio ", "bugs": { @@ -9,7 +9,7 @@ "license": "MIT", "main": "./dist/index.js", "bin": { - "linebridge-boot": "./bin/boot.js" + "linebridge-boot": "./bootloader/bin" }, "publishConfig": { "access": "public" @@ -20,32 +20,26 @@ "./package.json" ], "scripts": { - "start": "hermes-node ./src/bin/server.js", "build": "hermes build --parallel --clean", "test": "mocha" }, "dependencies": { "@foxify/events": "^2.1.0", - "@gullerya/object-observer": "^6.1.3", "@infisical/sdk": "^2.1.8", "@socket.io/cluster-adapter": "^0.2.2", "@socket.io/redis-adapter": "^8.2.1", "@socket.io/redis-emitter": "^5.1.0", "@socket.io/sticky": "^1.0.4", - "axios": "^1.6.7", - "axios-retry": "3.4.0", - "cors": "2.8.5", - "dotenv": "^16.4.4", + "axios": "^1.8.4", + "chokidar": "^4.0.3", + "dotenv": "^16.5.0", "hyper-express": "^6.17.3", - "ioredis": "^5.3.2", - "md5": "^2.3.0", - "module-alias": "2.2.2", - "morgan": "1.10.0", + "ioredis": "^5.6.1", + "minimatch": "^10.0.1", + "module-alias": "^2.2.2", "signal-exit": "^4.1.0", "socket.io": "^4.8.1", - "socket.io-client": "^4.5.4", - "sucrase": "^3.35.0", - "uuid": "^9.0.1" + "sucrase": "^3.35.0" }, "devDependencies": { "@ragestudio/hermes": "^1.0.0", diff --git a/server/src/baseEndpoints/main.js b/server/src/baseEndpoints/main.js deleted file mode 100755 index fc5f044..0000000 --- a/server/src/baseEndpoints/main.js +++ /dev/null @@ -1,21 +0,0 @@ -import path from "node:path" - -import Endpoint from "../classes/endpoint" -import defaults from "../defaults" - -const projectPkg = require(path.resolve(process.cwd(), "package.json")) - -export default class MainEndpoint extends Endpoint { - route = "/" - - get = async (req, res) => { - return { - name: globalThis._linebridge.name ?? "unknown", - version: projectPkg.version ?? "unknown", - engine: globalThis._linebridge.useEngine ?? "unknown", - request_time: new Date().getTime(), - lb_version: defaults.version ?? "unknown", - experimental: defaults.isExperimental.toString() ?? "unknown", - } - } -} \ No newline at end of file diff --git a/server/src/baseEndpoints/map.js b/server/src/baseEndpoints/map.js deleted file mode 100755 index 13ce787..0000000 --- a/server/src/baseEndpoints/map.js +++ /dev/null @@ -1,24 +0,0 @@ -import Endpoint from "../classes/endpoint" - -export default class MainEndpoint extends Endpoint { - route = "/_map" - - get = async (req, res) => { - const httpMap = Object.entries(this.server.engine.router.map).reduce((acc, [route, { method, path }]) => { - if (!acc[method]) { - acc[method] = [] - } - - acc[method].push({ - route: path - }) - - return acc - }, {}) - - return res.json({ - http: httpMap, - websocket: [] - }) - } -} \ No newline at end of file diff --git a/server/src/baseRoutes/main.js b/server/src/baseRoutes/main.js new file mode 100755 index 0000000..575bc08 --- /dev/null +++ b/server/src/baseRoutes/main.js @@ -0,0 +1,20 @@ +import path from "node:path" + +import Route from "../classes/Route" +import Vars from "../vars" + +export default class MainRoute extends Route { + static route = "/" + static useContexts = ["server"] + + get = async (req, res, ctx) => { + return { + name: ctx.server.params.refName ?? "unknown", + version: Vars.projectPkg.version ?? "unknown", + engine: ctx.server.params.useEngine ?? "unknown", + lb_version: Vars.libPkg.version ?? "unknown", + experimental: ctx.server.isExperimental ?? "unknown", + request_time: new Date().getTime(), + } + } +} diff --git a/server/src/baseRoutes/map.js b/server/src/baseRoutes/map.js new file mode 100755 index 0000000..444d3a6 --- /dev/null +++ b/server/src/baseRoutes/map.js @@ -0,0 +1,27 @@ +import Route from "../classes/Route" + +export default class MapRoute extends Route { + static route = "/_map" + + get = async (req, res) => { + const httpMap = Array.from(this.server.engine.map.entries()).reduce( + (acc, [route, { method, path }]) => { + if (!acc[method]) { + acc[method] = [] + } + + acc[method].push({ + route: path, + }) + + return acc + }, + {}, + ) + + return res.json({ + http: httpMap, + websocket: [], + }) + } +} diff --git a/server/src/classes/Endpoint/index.js b/server/src/classes/Endpoint/index.js new file mode 100644 index 0000000..8fb30a2 --- /dev/null +++ b/server/src/classes/Endpoint/index.js @@ -0,0 +1,17 @@ +import { HttpRequestHandler } from "../Handler" + +export default class Endpoint { + static _constructed = false + static _class = true + + constructor(method, context) { + this._constructed = true + this.context = context + + if (typeof method === "function") { + this.run = method + } + + this.handler = new HttpRequestHandler(this.run, this.context) + } +} diff --git a/server/src/classes/Handler/index.js b/server/src/classes/Handler/index.js new file mode 100644 index 0000000..4f37a9c --- /dev/null +++ b/server/src/classes/Handler/index.js @@ -0,0 +1,55 @@ +export class Handler { + constructor(fn, ctx) { + this.fn = fn ?? (() => Promise.resolve()) + this.ctx = ctx ?? {} + + this.fn = this.fn.bind({ + contexts: this.ctx, + }) + } +} + +export class HttpRequestHandler extends Handler { + constructor(fn, ctx) { + super(fn, ctx) + + return this.exec + } + + exec = async (req, res) => { + try { + req.ctx = this.ctx + const result = await this.fn(req, res, this.ctx) + + if (result) { + return res.json(result) + } + } catch (error) { + // handle if is a operation error + if (error instanceof OperationError) { + return res.status(error.code).json({ + error: error.message, + }) + } + + // if is not a operation error, that is a exception. + // gonna handle like a generic 500 error + console.error({ + message: "Unhandled route error:", + description: error.stack, + }) + + return res.status(500).json({ + error: error.message, + }) + } + } +} + +// TODO: Implement MiddlewareHandler +export class MiddlewareHandler extends Handler {} + +// TODO: Implement WebsocketRequestHandler +export class WebsocketRequestHandler extends Handler {} + +export default Handler diff --git a/server/src/classes/IPCClient/index.js b/server/src/classes/IPCClient/index.js index 39f4495..8419376 100755 --- a/server/src/classes/IPCClient/index.js +++ b/server/src/classes/IPCClient/index.js @@ -1,159 +1,162 @@ import { EventEmitter } from "@foxify/events" export default class IPCClient { - constructor(self, _process) { - this.self = self - this.process = _process + constructor(self, _process) { + this.self = self + this.process = _process - this.process.on("message", (msg) => { - if (typeof msg !== "object") { - // not an IPC message, ignore - return false - } + this.process.on("message", (msg) => { + if (typeof msg !== "object") { + // not an IPC message, ignore + return false + } - const { event, payload } = msg + const { event, payload } = msg - if (!event || !event.startsWith("ipc:")) { - return false - } + if (!event || !event.startsWith("ipc:")) { + return false + } - //console.log(`[IPC:CLIENT] Received event [${event}] from [${payload.from}]`) + //console.log(`[IPC:CLIENT] Received event [${event}] from [${payload.from}]`) - if (event.startsWith("ipc:exec")) { - return this.handleExecution(payload) - } + if (event.startsWith("ipc:exec")) { + return this.handleExecution(payload) + } - if (event.startsWith("ipc:akn")) { - return this.handleAcknowledgement(payload) - } - }) - } + if (event.startsWith("ipc:akn")) { + return this.handleAcknowledgement(payload) + } + }) + } - eventBus = new EventEmitter() + eventBus = new EventEmitter() - handleExecution = async (payload) => { - let { id, command, args, from } = payload + handleExecution = async (payload) => { + if (typeof this.self.ipcEvents !== "object") { + return null + } - let fn = this.self.ipcEvents[command] + let { id, command, args, from } = payload - if (!fn) { - this.process.send({ - event: `ipc:akn:${id}`, - payload: { - target: from, - from: this.self.constructor.refName, + let fn = this.self.ipcEvents[command] - id: id, - error: `IPC: Command [${command}] not found`, - } - }) + if (typeof fn !== "function") { + this.process.send({ + event: `ipc:akn:${id}`, + payload: { + target: from, + from: this.self.params.refName, - return false - } + id: id, + error: `IPC: Command [${command}] not found`, + }, + }) - try { - let result = await fn(this.self.contexts, ...args) + return false + } - this.process.send({ - event: `ipc:akn:${id}`, - payload: { - target: from, - from: this.self.constructor.refName, + try { + let result = await fn(this.self.contexts, ...args) - id: id, - result: result, - } - }) - } catch (error) { - this.process.send({ - event: `ipc:akn:${id}`, - payload: { - target: from, - from: this.self.constructor.refName, + this.process.send({ + event: `ipc:akn:${id}`, + payload: { + target: from, + from: this.self.params.refName, - id: id, - error: error.message, - } - }) - } - } + id: id, + result: result, + }, + }) + } catch (error) { + this.process.send({ + event: `ipc:akn:${id}`, + payload: { + target: from, + from: this.self.params.refName, - handleAcknowledgement = async (payload) => { - let { id, result, error } = payload + id: id, + error: error.message, + }, + }) + } + } - this.eventBus.emit(`ipc:akn:${id}`, { - id: id, - result: result, - error: error, - }) - } + handleAcknowledgement = async (payload) => { + let { id, result, error } = payload - // call a command on a remote service, and waits to get a response from akn (async) - call = async (to_service_id, command, ...args) => { - const remote_call_id = Date.now() + this.eventBus.emit(`ipc:akn:${id}`, { + id: id, + result: result, + error: error, + }) + } - //console.debug(`[IPC:CLIENT] Invoking command [${command}] on service [${to_service_id}]`) + // call a command on a remote service, and waits to get a response from akn (async) + call = async (to_service_id, command, ...args) => { + const remote_call_id = Date.now() - const response = await new Promise((resolve, reject) => { - try { + //console.debug(`[IPC:CLIENT] Invoking command [${command}] on service [${to_service_id}]`) - this.process.send({ - event: "ipc:exec", - payload: { - target: to_service_id, - from: this.self.constructor.refName, + const response = await new Promise((resolve, reject) => { + try { + this.process.send({ + event: "ipc:exec", + payload: { + target: to_service_id, + from: this.self.params.refName, - id: remote_call_id, - command, - args, - } - }) + id: remote_call_id, + command, + args, + }, + }) - this.eventBus.once(`ipc:akn:${remote_call_id}`, resolve) - } catch (error) { - console.error(error) + this.eventBus.once(`ipc:akn:${remote_call_id}`, resolve) + } catch (error) { + console.error(error) - reject(error) - } - }).catch((error) => { - return { - error: error - } - }) + reject(error) + } + }).catch((error) => { + return { + error: error, + } + }) - if (response.error) { - throw new OperationError(500, response.error) - } + if (response.error) { + throw new OperationError(500, response.error) + } - return response.result - } + return response.result + } - // call a command on a remote service, but return it immediately - invoke = async (to_service_id, command, ...args) => { - const remote_call_id = Date.now() + // call a command on a remote service, but return it immediately + invoke = async (to_service_id, command, ...args) => { + const remote_call_id = Date.now() - try { - this.process.send({ - event: "ipc:exec", - payload: { - target: to_service_id, - from: this.self.constructor.refName, + try { + this.process.send({ + event: "ipc:exec", + payload: { + target: to_service_id, + from: this.self.params.refName, - id: remote_call_id, - command, - args, - } - }) + id: remote_call_id, + command, + args, + }, + }) - return { - id: remote_call_id - } - } catch (error) { - console.error(error) + return { + id: remote_call_id, + } + } catch (error) { + console.error(error) - return { - error: error - } - } - } -} \ No newline at end of file + return { + error: error, + } + } + } +} diff --git a/server/src/classes/operation_error/index.js b/server/src/classes/OperationError/index.js similarity index 100% rename from server/src/classes/operation_error/index.js rename to server/src/classes/OperationError/index.js diff --git a/server/src/classes/Route/index.js b/server/src/classes/Route/index.js new file mode 100755 index 0000000..0f3ca8d --- /dev/null +++ b/server/src/classes/Route/index.js @@ -0,0 +1,58 @@ +import Endpoint from "../Endpoint" + +export default class Route { + constructor(server, params = {}) { + if (!server) { + throw new Error("server is not defined") + } + + this.server = server + this.params = { + route: this.constructor.route ?? "/", + useContexts: this.constructor.useContexts ?? [], + useMiddlewares: this.constructor.useMiddlewares ?? [], + ...params, + } + + if (typeof this.params.handlers === "object") { + for (const method of global._linebridge.params.httpMethods) { + if (typeof this.params.handlers[method] !== "function") { + continue + } + + this[method] = this.params.handlers[method] + } + } + + if (this.server.contexts && Array.isArray(this.params.useContexts)) { + for (const key of this.params.useContexts) { + this.ctx[key] = this.server.contexts[key] + } + } + } + + ctx = {} + + register = () => { + for (const method of global._linebridge.params.httpMethods) { + if (typeof this[method] === "undefined") { + continue + } + + if (!(this[method] instanceof Endpoint)) { + if (this[method]._class && !this[method]._constructed) { + this[method] = new this[method](undefined, this.ctx) + } else { + this[method] = new Endpoint(this[method], this.ctx) + } + } + + this.server.register.http({ + method: method, + route: this.params.route, + middlewares: this.params.useMiddlewares, + fn: this[method].handler, + }) + } + } +} diff --git a/server/src/classes/rtengineng/client.js b/server/src/classes/RtEngine/client.js similarity index 100% rename from server/src/classes/rtengineng/client.js rename to server/src/classes/RtEngine/client.js diff --git a/server/src/classes/rtengineng/event.js b/server/src/classes/RtEngine/event.js similarity index 100% rename from server/src/classes/rtengineng/event.js rename to server/src/classes/RtEngine/event.js diff --git a/server/src/classes/rtengineng/events.js b/server/src/classes/RtEngine/events.js similarity index 100% rename from server/src/classes/rtengineng/events.js rename to server/src/classes/RtEngine/events.js diff --git a/server/src/classes/rtengineng/index.js b/server/src/classes/RtEngine/index.js similarity index 99% rename from server/src/classes/rtengineng/index.js rename to server/src/classes/RtEngine/index.js index 8de4dfc..1600751 100755 --- a/server/src/classes/rtengineng/index.js +++ b/server/src/classes/RtEngine/index.js @@ -167,7 +167,7 @@ class RTEngineNG { } } - attach = async (engine) => { + attach = (engine) => { this.engine = engine this.engine.app.ws(this.config.path ?? `/`, this.handleConnection) diff --git a/server/src/classes/endpoint/index.js b/server/src/classes/endpoint/index.js deleted file mode 100755 index eefedaf..0000000 --- a/server/src/classes/endpoint/index.js +++ /dev/null @@ -1,87 +0,0 @@ -export default class Endpoint { - constructor(server, params = {}, ctx = {}) { - this.server = server - this.params = params - this.ctx = ctx - - if (!server) { - throw new Error("Server is not defined") - } - - this.route = this.route ?? this.constructor.route ?? this.params.route - this.enabled = this.enabled ?? this.constructor.enabled ?? this.params.enabled ?? true - - this.middlewares = [ - ...this.middlewares ?? [], - ...this.params.middlewares ?? [], - ] - - if (this.params.handlers) { - for (const method of globalThis._linebridge.validHttpMethods) { - if (typeof this.params.handlers[method] === "function") { - this[method] = this.params.handlers[method] - } - } - } - - this.selfRegister() - - if (Array.isArray(this.params.useContexts)) { - for (const contextRef of this.params.useContexts) { - this.endpointContext[contextRef] = this.server.contexts[contextRef] - } - } - - return this - } - - endpointContext = {} - - createHandler(fn) { - fn = fn.bind(this.server) - - return async (req, res) => { - try { - const result = await fn(req, res, this.endpointContext) - - if (result) { - return res.json(result) - } - } catch (error) { - if (error instanceof OperationError) { - return res.status(error.code).json({ - "error": error.message - }) - } - - console.error({ - message: "Unhandled route error:", - description: error.stack, - }) - - return res.status(500).json({ - "error": error.message - }) - } - } - } - - selfRegister = async () => { - for await (const method of globalThis._linebridge.validHttpMethods) { - const methodHandler = this[method] - - if (typeof methodHandler !== "undefined") { - const fn = this.createHandler(this[method].fn ?? this[method]) - - this.server.register.http( - { - method, - route: this.route, - middlewares: this.middlewares, - fn: fn, - }, - ) - } - } - } -} \ No newline at end of file diff --git a/server/src/classes/index.js b/server/src/classes/index.js deleted file mode 100755 index 1b9fc52..0000000 --- a/server/src/classes/index.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = { - Controller: require("./controller"), - Endpoint: require("./endpoint"), - RTEngine: require("./rtengine"), -} \ No newline at end of file diff --git a/server/src/defaults.js b/server/src/defaults.js deleted file mode 100755 index 11a2ba2..0000000 --- a/server/src/defaults.js +++ /dev/null @@ -1,65 +0,0 @@ -const path = require("path") -const fs = require("fs") -const os = require("os") -const packageJSON = require(path.resolve(module.path, "../package.json")) - -function getHostAddress() { - const interfaces = os.networkInterfaces() - - for (const key in interfaces) { - const iface = interfaces[key] - - for (let index = 0; index < iface.length; index++) { - const alias = iface[index] - - if (alias.family === "IPv4" && alias.address !== "127.0.0.1" && !alias.internal) { - return alias.address - } - } - } - - return "0.0.0.0" -} - -export default { - isExperimental: fs.existsSync(path.resolve(module.path, "../.experimental")), - version: packageJSON.version, - localhost_address: getHostAddress() ?? "localhost", - params: { - urlencoded: true, - engine: "express", - http_protocol: "http", - ws_protocol: "ws", - }, - headers: { - "Access-Control-Allow-Headers": "Origin, X-Requested-With, Content-Type, Accept, Authorization", - "Access-Control-Allow-Origin": "*", - "Access-Control-Allow-Methods": "GET, POST, OPTIONS, PUT, PATCH, DELETE, DEL", - "Access-Control-Allow-Credentials": "true", - }, - middlewares: { - cors: require("./middlewares/cors").default, - logs: require("./middlewares/logger").default, - }, - useMiddlewares: [ - //"cors", - "logs", - ], - controllers: [], - fixed_http_methods: { - "del": "delete", - }, - valid_http_methods: [ - "get", - "post", - "put", - "patch", - "del", - "delete", - "trace", - "head", - "any", - "options", - "ws", - ], -} \ No newline at end of file diff --git a/server/src/engines/he-legacy/index.js b/server/src/engines/he-legacy/index.js new file mode 100755 index 0000000..9d1f46d --- /dev/null +++ b/server/src/engines/he-legacy/index.js @@ -0,0 +1,80 @@ +import he from "hyper-express" +import rtengine from "./rtengine" + +export default class Engine { + constructor(server) { + this.server = server + } + + static heDefaultParams = { + max_body_length: 50 * 1024 * 1024, //50MB in bytes, + } + + app = null + ws = null + router = new he.Router() + map = new Map() + + initialize = async () => { + this.app = new he.Server({ + ...Engine.heDefaultParams, + key_file_name: this.server.ssl?.key ?? undefined, + cert_file_name: this.server.ssl?.cert ?? undefined, + }) + + this.router.any("*", this.defaultResponse) + this.app.use(this.mainMiddleware) + this.app.use(this.router) + + if (this.server.params.websockets === true) { + this.ws = new rtengine({ + requireAuth: this.server.constructor.requiredWsAuth, + handleAuth: this.server.handleWsAuth, + root: `/${this.server.params.refName}`, + }) + + this.ws.initialize() + + global.websockets = this.ws + + await this.ws.io.attachApp(this.app.uws_instance) + } + } + + mainMiddleware = async (req, res, next) => { + if (req.method === "OPTIONS") { + return res.status(204).end() + } + + // register body parser + if (req.headers["content-type"]) { + if ( + !req.headers["content-type"].startsWith("multipart/form-data") + ) { + req.body = await req.urlencoded() + req.body = await req.json(req.body) + } + } + } + + defaultResponse = (req, res) => { + return res.status(404).json({ + error: "Not found", + }) + } + + listen = async () => { + await this.app.listen(this.server.params.listenPort) + } + + // close must be synchronous + close = () => { + if (this.ws && typeof this.ws.close === "function") { + this.ws.close() + } + + if (this.app && typeof this.app.close === "function") { + this.app.close() + } + } +} diff --git a/server/src/classes/rtengine/index.js b/server/src/engines/he-legacy/rtengine/index.js similarity index 99% rename from server/src/classes/rtengine/index.js rename to server/src/engines/he-legacy/rtengine/index.js index 8051165..9b39671 100755 --- a/server/src/classes/rtengine/index.js +++ b/server/src/engines/he-legacy/rtengine/index.js @@ -3,7 +3,7 @@ import redis from "ioredis" import SocketIO from "socket.io" import { EventEmitter } from "@foxify/events" -import RedisMap from "../../lib/redis_map" +import RedisMap from "./redis_map.js" export default class RTEngineServer { constructor(params = {}) { diff --git a/server/src/lib/redis_map/index.js b/server/src/engines/he-legacy/rtengine/redis_map.js similarity index 100% rename from server/src/lib/redis_map/index.js rename to server/src/engines/he-legacy/rtengine/redis_map.js diff --git a/server/src/engines/he/index.js b/server/src/engines/he/index.js new file mode 100755 index 0000000..48dbcf9 --- /dev/null +++ b/server/src/engines/he/index.js @@ -0,0 +1,81 @@ +import he from "hyper-express" +import RtEngine from "../../classes/RtEngine" + +export default class Engine { + constructor(server) { + this.server = server + } + + static heDefaultParams = { + max_body_length: 50 * 1024 * 1024, //50MB in bytes, + } + + app = null + ws = null + router = new he.Router() + map = new Map() + + initialize = async () => { + this.app = new he.Server({ + ...Engine.heDefaultParams, + key_file_name: this.server.ssl?.key ?? undefined, + cert_file_name: this.server.ssl?.cert ?? undefined, + }) + + this.router.any("*", this.defaultResponse) + this.app.use(this.mainMiddleware) + this.app.use(this.router) + + if (this.server.params.websockets === true) { + this.ws = new RtEngine({ + path: + this.server.params.wsPath ?? + `/${this.server.params.refName}`, + onUpgrade: this.server.handleWsUpgrade, + onConnection: this.server.handleWsConnection, + onDisconnect: this.server.handleWsDisconnect, + }) + + global.websockets = this.ws + + this.ws.attach(this) + } + } + + mainMiddleware = async (req, res, next) => { + if (req.method === "OPTIONS") { + return res.status(204).end() + } + + // register body parser + if (req.headers["content-type"]) { + if ( + !req.headers["content-type"].startsWith("multipart/form-data") + ) { + req.body = await req.urlencoded() + req.body = await req.json(req.body) + } + } + } + + defaultResponse = (req, res) => { + return res.status(404).json({ + error: "Not found", + }) + } + + listen = async () => { + await this.app.listen(this.server.params.listenPort) + } + + // close must be synchronous + close = () => { + if (this.ws && typeof this.ws.close === "function") { + this.ws.close() + } + + if (this.app && typeof this.app.close === "function") { + this.app.close() + } + } +} diff --git a/server/src/engines/hyper-express-ng/index.js b/server/src/engines/hyper-express-ng/index.js deleted file mode 100755 index ec566a3..0000000 --- a/server/src/engines/hyper-express-ng/index.js +++ /dev/null @@ -1,100 +0,0 @@ -import he from "hyper-express" -import rtengineng from "../../classes/rtengineng" - -export default class HyperExpressEngineNG { - constructor(ctx) { - this.ctx = ctx - } - - app = null - ws = null - router = null - - initialize = async () => { - console.warn( - `hyper-express-ng is a experimental engine, some features may not be available or work properly!`, - ) - - const appParams = { - max_body_length: 50 * 1024 * 1024, //50MB in bytes, - } - - if (this.ctx.ssl) { - appParams.key_file_name = this.ctx.ssl?.key ?? null - appParams.cert_file_name = this.ctx.ssl?.cert ?? null - } - - this.app = new he.Server(appParams) - - this.router = new he.Router() - - // create a router map - if (typeof this.router.map !== "object") { - this.router.map = {} - } - - await this.router.any("*", (req, res) => { - return res.status(404).json({ - code: 404, - message: "Not found", - }) - }) - - await this.app.use(async (req, res, next) => { - if (req.method === "OPTIONS") { - // handle cors - if (this.ctx.constructor.ignoreCors) { - res.setHeader("Access-Control-Allow-Methods", "*") - res.setHeader("Access-Control-Allow-Origin", "*") - res.setHeader("Access-Control-Allow-Headers", "*") - } - - return res.status(204).end() - } - - // register body parser - if (req.headers["content-type"]) { - if ( - !req.headers["content-type"].startsWith( - "multipart/form-data", - ) - ) { - req.body = await req.urlencoded() - req.body = await req.json(req.body) - } - } - }) - - if (this.ctx.constructor.enableWebsockets === true) { - this.ws = global.websocket = new rtengineng({ - path: - this.ctx.constructor.wsPath ?? - `/${this.ctx.constructor.refName}`, - onUpgrade: this.ctx.handleWsUpgrade, - onConnection: this.ctx.handleWsConnection, - onDisconnect: this.ctx.handleWsDisconnect, - }) - - await this.ws.attach(this) - } - } - - listen = async () => { - await this.app.listen(this.ctx.constructor.listen_port) - } - - // close must be synchronous - close = () => { - if (this.ws && typeof this.ws.close === "function") { - this.ws.close() - } - - if (typeof this.app.close === "function") { - this.app.close() - } - - if (typeof this.ctx.onClose === "function") { - this.ctx.onClose() - } - } -} diff --git a/server/src/engines/hyper-express/index.js b/server/src/engines/hyper-express/index.js deleted file mode 100755 index dae9423..0000000 --- a/server/src/engines/hyper-express/index.js +++ /dev/null @@ -1,99 +0,0 @@ -import he from "hyper-express" -import rtengine from "../../classes/rtengine" - -export default class Engine { - constructor(ctx) { - this.ctx = ctx - } - - app = null - router = null - ws = null - - initialize = async () => { - const serverParams = { - max_body_length: 50 * 1024 * 1024, //50MB in bytes, - } - - if (this.ctx.ssl) { - serverParams.key_file_name = this.ctx.ssl?.key ?? null - serverParams.cert_file_name = this.ctx.ssl?.cert ?? null - } - - this.app = new he.Server(serverParams) - - this.router = new he.Router() - - // create a router map - if (typeof this.router.map !== "object") { - this.router.map = {} - } - - await this.router.any("*", (req, res) => { - return res.status(404).json({ - code: 404, - message: "Not found", - }) - }) - - await this.app.use(async (req, res, next) => { - if (req.method === "OPTIONS") { - // handle cors - if (this.ctx.constructor.ignoreCors) { - res.setHeader("Access-Control-Allow-Methods", "*") - res.setHeader("Access-Control-Allow-Origin", "*") - res.setHeader("Access-Control-Allow-Headers", "*") - } - - return res.status(204).end() - } - - // register body parser - if (req.headers["content-type"]) { - if ( - !req.headers["content-type"].startsWith( - "multipart/form-data", - ) - ) { - req.body = await req.urlencoded() - req.body = await req.json(req.body) - } - } - }) - - if (this.ctx.constructor.enableWebsockets) { - this.ws = global.websocket = new rtengine({ - requireAuth: this.ctx.constructor.requiredWsAuth, - handleAuth: this.ctx.handleWsAuth, - root: `/${this.ctx.constructor.refName}`, - }) - - this.ws.initialize() - - await this.ws.io.attachApp(this.app.uws_instance) - } - } - - listen = async () => { - await this.app.listen(this.ctx.constructor.listen_port) - } - - // close should be synchronous - close = () => { - if (this.ws) { - this.ws.clear() - - if (typeof this.ws?.close === "function") { - this.ws.close() - } - } - - if (typeof this.app?.close === "function") { - this.app.close() - } - - if (typeof this.ctx.onClose === "function") { - this.ctx.onClose() - } - } -} diff --git a/server/src/engines/index.js b/server/src/engines/index.js index 0bf2110..beee0fa 100755 --- a/server/src/engines/index.js +++ b/server/src/engines/index.js @@ -1,9 +1,9 @@ -import HyperExpress from "./hyper-express" -import HyperExpressNG from "./hyper-express-ng" +import HeLegacy from "./he-legacy" +import He from "./he" import Worker from "./worker" export default { - "hyper-express": HyperExpress, - "hyper-express-ng": HyperExpressNG, + "he-legacy": HeLegacy, + he: He, worker: Worker, } diff --git a/server/src/engines/worker/index.js b/server/src/engines/worker/index.js index a06600f..dd2e78b 100755 --- a/server/src/engines/worker/index.js +++ b/server/src/engines/worker/index.js @@ -1,126 +1,118 @@ import { EventEmitter } from "@foxify/events" class WorkerEngineRouter { - routes = [] + routes = [] - get = (path, ...execs) => { + get = (path, ...execs) => {} - } + post = (path, ...execs) => {} - post = (path, ...execs) => { + delete = (path, ...execs) => {} - } + put = (path, ...execs) => {} - delete = (path, ...execs) => { + patch = (path, ...execs) => {} - } + head = (path, ...execs) => {} - put = (path, ...execs) => { + options = (path, ...execs) => {} - } + any = (path, ...execs) => {} - patch = (path, ...execs) => { - - } - - head = (path, ...execs) => { - - } - - options = (path, ...execs) => { - - } - - any = (path, ...execs) => { - - } - - use = (path, ...execs) => { - - } + use = (path, ...execs) => {} } class WorkerEngine { - static ipcPrefix = "rail:" + static ipcPrefix = "rail:" - selfId = process.env.lb_service.id + selfId = process.env.lb_service.id - router = new WorkerEngineRouter() + router = new WorkerEngineRouter() - eventBus = new EventEmitter() + eventBus = new EventEmitter() - perExecTail = [] + perExecTail = [] - initialize = async () => { - console.error(`[WorkerEngine] Worker engine its not implemented yet...`) + initialize = async () => { + console.error(`[WorkerEngine] Worker engine its not implemented yet...`) - process.on("message", this.handleIPCMessage) - } + process.on("message", this.handleIPCMessage) + } - listen = async () => { - console.log(`Sending Rail Register`) + listen = async () => { + console.log(`Sending Rail Register`) - process.send({ - type: "rail:register", - data: { - id: process.env.lb_service.id, - pid: process.pid, - routes: this.router.routes, - } - }) - } + process.send({ + type: "rail:register", + data: { + id: process.env.lb_service.id, + pid: process.pid, + routes: this.router.routes, + }, + }) + } - handleIPCMessage = async (msg) => { - if (typeof msg !== "object") { - // ignore, its not for us - return false - } + handleIPCMessage = async (msg) => { + if (typeof msg !== "object") { + // ignore, its not for us + return false + } - if (!msg.event || !msg.event.startsWith(WorkerEngine.ipcPrefix)) { - return false - } + if (!msg.event || !msg.event.startsWith(WorkerEngine.ipcPrefix)) { + return false + } - const { event, payload } = msg + const { event, payload } = msg - switch (event) { - case "rail:request": { - const { req } = payload + switch (event) { + case "rail:request": { + const { req } = payload - break - } - case "rail:response": { + break + } + case "rail:response": { + } + } + } - } - } - } + use = (fn) => { + if (fn instanceof WorkerEngineRouter) { + this.router = fn + return + } - use = (fn) => { - if (fn instanceof WorkerEngineRouter) { - this.router = fn - return - } - - if (fn instanceof Function) { - this.perExecTail.push(fn) - return - } - } + if (fn instanceof Function) { + this.perExecTail.push(fn) + return + } + } } export default class Engine { - constructor(params) { - this.params = params - } + constructor(params) { + this.params = params + } - app = new WorkerEngine() + app = null + router = new WorkerEngineRouter() + map = new Map() - router = new WorkerEngineRouter() + initialize = async () => { + if ( + !process.env.lb_service || + process.env.lb_service?.type !== "worker" + ) { + throw new Error( + "No worker environment detected!\nThis engine is only meant to be used in a worker environment\n", + ) + } - init = async () => { - await this.app.initialize() - } + this.app = new WorkerEngine() - listen = async () => { - await this.app.listen() - } -} \ No newline at end of file + await this.app.initialize() + } + + listen = async () => { + await this.app.listen() + } +} diff --git a/server/src/index.js b/server/src/index.js index 5978cf8..1688dfc 100755 --- a/server/src/index.js +++ b/server/src/index.js @@ -1,6 +1,6 @@ module.exports = { - Server: require("./server.js"), - Endpoint: require("./classes/endpoint"), - registerBaseAliases: require("./registerAliases"), - version: require("../package.json").version, + Server: require("./server"), + Route: require("./classes/Route"), + registerBaseAliases: require("./registerAliases"), + version: require("../package.json").version, } diff --git a/server/src/initializators/registerBaseEndpoints/index.js b/server/src/initializators/registerBaseEndpoints/index.js deleted file mode 100755 index a2c0bca..0000000 --- a/server/src/initializators/registerBaseEndpoints/index.js +++ /dev/null @@ -1,17 +0,0 @@ -import fs from "node:fs" -import path from "node:path" - -export default async (server) => { - const scanPath = path.join(__dirname, "../../", "baseEndpoints") - const files = fs.readdirSync(scanPath) - - for await (const file of files) { - if (file === "index.js") { - continue - } - - let endpoint = require(path.join(scanPath, file)).default - - new endpoint(server) - } -} \ No newline at end of file diff --git a/server/src/initializators/registerHttpRoutes/index.js b/server/src/initializators/registerHttpRoutes/index.js deleted file mode 100755 index 193c2bb..0000000 --- a/server/src/initializators/registerHttpRoutes/index.js +++ /dev/null @@ -1,79 +0,0 @@ -import fs from "node:fs" - -import Endpoint from "../../classes/endpoint" -import RecursiveRegister from "../../lib/recursiveRegister" - -const parametersRegex = /\[([a-zA-Z0-9_]+)\]/g - -export default async (startDir, engine, server) => { - if (!fs.existsSync(startDir)) { - return engine - } - - await RecursiveRegister({ - start: startDir, - match: async (filePath) => { - return filePath.endsWith(".js") || filePath.endsWith(".ts") - }, - onMatch: async ({ absolutePath, relativePath }) => { - const paths = relativePath.split("/") - - let method = paths[paths.length - 1].split(".")[0].toLocaleLowerCase() - let route = paths.slice(0, paths.length - 1).join("/") - - // parse parametrized routes - route = route.replace(parametersRegex, ":$1") - route = route.replace("[$]", "*") - - // clean up - route = route.replace(".js", "") - route = route.replace(".ts", "") - - // check if route ends with index - if (route.endsWith("/index")) { - route = route.replace("/index", "") - } - - // add leading slash - route = `/${route}` - - // import route - let fn = require(absolutePath) - - fn = fn.default ?? fn - - if (typeof fn !== "function") { - if (!fn.fn) { - console.warn(`Missing fn handler in [${method}][${route}]`) - return false - } - - if (Array.isArray(fn.useContext)) { - let contexts = {} - - for (const context of fn.useContext) { - contexts[context] = server.contexts[context] - } - - fn.contexts = contexts - - fn.fn.bind({ contexts }) - } - } - - new Endpoint( - server, - { - route: route, - enabled: true, - middlewares: fn.middlewares, - handlers: { - [method]: fn.fn ?? fn, - } - } - ) - } - }) - - return engine -} \ No newline at end of file diff --git a/server/src/initializators/registerWebsocketsEvents/index.js b/server/src/initializators/registerWebsocketsEvents/index.js deleted file mode 100755 index 77e36c9..0000000 --- a/server/src/initializators/registerWebsocketsEvents/index.js +++ /dev/null @@ -1,28 +0,0 @@ -import fs from "node:fs" - -import getRouteredFunctions from "../../utils/getRouteredFunctions" -import flatRouteredFunctions from "../../utils/flatRouteredFunctions" - -export default async (startDir, engine) => { - if (!engine.ws || !fs.existsSync(startDir)) { - return engine - } - - let events = await getRouteredFunctions(startDir) - - events = flatRouteredFunctions(events) - - if (typeof events !== "object") { - return engine - } - - if (typeof engine.ws.registerEvents === "function") { - await engine.ws.registerEvents(events) - } else { - for (const eventKey of Object.keys(events)) { - engine.ws.events.set(eventKey, events[eventKey]) - } - } - - return engine -} diff --git a/server/src/lib/generateController/index.js b/server/src/lib/generateController/index.js deleted file mode 100755 index a8c2366..0000000 --- a/server/src/lib/generateController/index.js +++ /dev/null @@ -1,18 +0,0 @@ -const { Controller } = require("../../classes/controller") -const generateEndpointsFromDir = require("../generateEndpointsFromDir") - -function generateControllerFromEndpointsDir(dir, controllerName) { - const endpoints = generateEndpointsFromDir(dir) - - return class extends Controller { - static refName = controllerName - - get = endpoints.get - post = endpoints.post - put = endpoints.put - patch = endpoints.patch - delete = endpoints.delete - } -} - -module.exports = generateControllerFromEndpointsDir \ No newline at end of file diff --git a/server/src/lib/generateEndpointsFromDir/index.js b/server/src/lib/generateEndpointsFromDir/index.js deleted file mode 100755 index c1b1788..0000000 --- a/server/src/lib/generateEndpointsFromDir/index.js +++ /dev/null @@ -1,23 +0,0 @@ -const loadEndpointsFromDir = require("../loadEndpointsFromDir") - -function generateEndpointsFromDir(dir) { - const loadedEndpoints = loadEndpointsFromDir(dir) - - // filter by methods - const endpointsByMethods = Object() - - for (const endpointKey in loadedEndpoints) { - const endpoint = loadedEndpoints[endpointKey] - const method = endpoint.method.toLowerCase() - - if (!endpointsByMethods[method]) { - endpointsByMethods[method] = {} - } - - endpointsByMethods[method][endpoint.route] = loadedEndpoints[endpointKey] - } - - return endpointsByMethods -} - -module.exports = generateEndpointsFromDir \ No newline at end of file diff --git a/server/src/lib/loadEndpointsFromDir/index.js b/server/src/lib/loadEndpointsFromDir/index.js deleted file mode 100755 index 79c0843..0000000 --- a/server/src/lib/loadEndpointsFromDir/index.js +++ /dev/null @@ -1,41 +0,0 @@ -const fs = require("node:fs") -const path = require("node:path") - -function loadEndpointsFromDir(dir) { - if (!dir) { - throw new Error("No directory provided") - } - - if (!fs.existsSync(dir)) { - throw new Error(`Directory [${dir}] does not exist`) - } - - // scan the directory for files - const files = fs.readdirSync(dir) - - // create an object to store the endpoints - const endpoints = {} - - // loop through the files - for (const file of files) { - // get the full path of the file - const filePath = path.join(dir, file) - - // get the file stats - const stats = fs.statSync(filePath) - - // if the file is a directory, recursively call this function - if (stats.isDirectory()) { - endpoints[file] = loadEndpointsFromDir(filePath) - } - - // if the file is a javascript file, require it and add it to the endpoints object - if (stats.isFile() && path.extname(filePath) === ".js") { - endpoints[path.basename(file, ".js")] = require(filePath).default - } - } - - return endpoints -} - -module.exports = loadEndpointsFromDir \ No newline at end of file diff --git a/server/src/middlewares/cors/index.js b/server/src/middlewares/cors/index.js deleted file mode 100755 index 7ebac0f..0000000 --- a/server/src/middlewares/cors/index.js +++ /dev/null @@ -1,8 +0,0 @@ -import cors from "cors" - -export default cors({ - origin: "*", - methods: ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD", "CONNECT", "TRACE"], - preflightContinue: false, - optionsSuccessStatus: 204, -}) \ No newline at end of file diff --git a/server/src/patches.js b/server/src/patches.js index 953e4e7..34dfa3e 100755 --- a/server/src/patches.js +++ b/server/src/patches.js @@ -1,3 +1,15 @@ -import OperationError from "./classes/operation_error" +import OperationError from "./classes/OperationError" +import Endpoint from "./classes/Endpoint" +import { + Handler, + HttpRequestHandler, + MiddlewareHandler, + WebsocketRequestHandler, +} from "./classes/Handler" -global.OperationError = OperationError \ No newline at end of file +global.OperationError = OperationError +global.Endpoint = Endpoint +global.Handler = Handler +global.HttpRequestHandler = HttpRequestHandler +global.MiddlewareHandler = MiddlewareHandler +global.WebsocketRequestHandler = WebsocketRequestHandler diff --git a/server/src/registers/baseHeaders.js b/server/src/registers/baseHeaders.js new file mode 100644 index 0000000..cfbd39f --- /dev/null +++ b/server/src/registers/baseHeaders.js @@ -0,0 +1,22 @@ +import Vars from "../vars" + +export default (server) => { + if (!server || !server.headers || !server.engine || !server.engine.app) { + return null + } + + let headers = { + ...server.headers, + ...Vars.baseHeaders, + } + + headers = Object.entries(headers) + + server.engine.app.use((req, res, next) => { + for (let i = 0; i < headers.length; i++) { + res.setHeader(headers[i][0], headers[i][1]) + } + + next() + }) +} diff --git a/server/src/registers/baseMiddlewares.js b/server/src/registers/baseMiddlewares.js new file mode 100644 index 0000000..aa04d56 --- /dev/null +++ b/server/src/registers/baseMiddlewares.js @@ -0,0 +1,14 @@ +import composeMiddlewares from "../utils/composeMiddlewares" +import Vars from "../vars" + +export default async (server) => { + const middlewares = composeMiddlewares( + { ...server.middlewares, ...Vars.baseMiddlewares }, + server.params.useMiddlewares, + "/*", + ) + + middlewares.forEach((middleware) => { + server.engine.app.use(middleware) + }) +} diff --git a/server/src/registers/baseRoutes.js b/server/src/registers/baseRoutes.js new file mode 100755 index 0000000..7de57de --- /dev/null +++ b/server/src/registers/baseRoutes.js @@ -0,0 +1,21 @@ +import fs from "node:fs" +import path from "node:path" + +import Vars from "../vars" + +export default async (server) => { + const scanPath = path.resolve(Vars.libPath, "baseRoutes") + const files = fs.readdirSync(scanPath) + + for await (const file of files) { + if (file === "index.js") { + continue + } + + let RouteModule = await import(path.join(scanPath, file)) + + RouteModule = RouteModule.default + + new RouteModule(server).register() + } +} diff --git a/server/src/registers/bypassCorsHeaders.js b/server/src/registers/bypassCorsHeaders.js new file mode 100644 index 0000000..428a2ac --- /dev/null +++ b/server/src/registers/bypassCorsHeaders.js @@ -0,0 +1,6 @@ +export default (server) => { + server.headers["Access-Control-Allow-Origin"] = "*" + server.headers["Access-Control-Allow-Methods"] = "*" + server.headers["Access-Control-Allow-Headers"] = "*" + server.headers["Access-Control-Allow-Credentials"] = "true" +} diff --git a/server/src/registers/httpFileRoutes.js b/server/src/registers/httpFileRoutes.js new file mode 100755 index 0000000..2767bde --- /dev/null +++ b/server/src/registers/httpFileRoutes.js @@ -0,0 +1,64 @@ +import fs from "node:fs" + +import Route from "../classes/Route" +import RecursiveRegister from "../utils/recursiveRegister" + +const parametersRegex = /\[([a-zA-Z0-9_]+)\]/g + +export default async (startDir, server) => { + if (!fs.existsSync(startDir)) { + return null + } + + await RecursiveRegister({ + start: startDir, + match: async (filePath) => { + return filePath.endsWith(".js") || filePath.endsWith(".ts") + }, + onMatch: async ({ absolutePath, relativePath }) => { + const paths = relativePath.split("/") + + let method = paths[paths.length - 1] + .split(".")[0] + .toLocaleLowerCase() + let route = paths.slice(0, paths.length - 1).join("/") + + // parse parametrized routes + route = route.replace(parametersRegex, ":$1") + route = route.replace("[$]", "*") + + // clean up + route = route.replace(".js", "") + route = route.replace(".ts", "") + + // check if route ends with index + if (route.endsWith("/index")) { + route = route.replace("/index", "") + } + + // add leading slash + route = `/${route}` + + // import endpoint + let fileObj = await import(absolutePath) + + fileObj = fileObj.default ?? fileObj + + if (typeof fileObj !== "function") { + if (typeof fileObj.fn !== "function") { + console.warn(`Missing fn handler in [${method}][${route}]`) + return false + } + } + + new Route(server, { + route: route, + useMiddlewares: fileObj.useMiddlewares, + useContexts: fileObj.useContexts, + handlers: { + [method]: fileObj.fn ?? fileObj, + }, + }).register() + }, + }) +} diff --git a/server/src/registers/ipcService.js b/server/src/registers/ipcService.js new file mode 100644 index 0000000..1d9e178 --- /dev/null +++ b/server/src/registers/ipcService.js @@ -0,0 +1,47 @@ +export default async (server) => { + if (!process.env.lb_service || !process.send) { + console.error("IPC not available") + return null + } + + // get only the root paths + let paths = Array.from(server.engine.map.keys()).map((key) => { + const root = key.split("/")[1] + + return "/" + root + }) + + // remove duplicates + paths = [...new Set(paths)] + + // remove "" and _map + paths = paths.filter((key) => { + if (key === "/" || key === "/_map") { + return false + } + + return true + }) + + process.send({ + type: "service:register", + id: process.env.lb_service.id, + index: process.env.lb_service.index, + register: { + namespace: server.params.refName, + http: { + enabled: true, + paths: paths, + proto: server.hasSSL ? "https" : "http", + }, + websocket: { + enabled: server.params.websockets, + path: server.params.refName ?? `/${server.params.refName}`, + }, + listen: { + ip: server.params.listenIp, + port: server.params.listenPort, + }, + }, + }) +} diff --git a/server/src/registers/websocketFileEvents.js b/server/src/registers/websocketFileEvents.js new file mode 100755 index 0000000..5b71032 --- /dev/null +++ b/server/src/registers/websocketFileEvents.js @@ -0,0 +1,28 @@ +import fs from "node:fs" + +import getRouteredFunctions from "../utils/getRouteredFunctions" +import flatRouteredFunctions from "../utils/flatRouteredFunctions" + +export default async (startDir, server) => { + if (!server.engine.ws || !fs.existsSync(startDir)) { + return null + } + + let events = await getRouteredFunctions(startDir) + + events = flatRouteredFunctions(events) + + if (typeof events !== "object") { + return null + } + + if (typeof server.engine.ws.registerEvents === "function") { + await server.engine.ws.registerEvents(events) + } else { + for (const eventKey of Object.keys(events)) { + server.engine.ws.events.set(eventKey, events[eventKey]) + } + } + + return server +} diff --git a/server/src/server.js b/server/src/server.js index a52c83d..c9fc4d5 100755 --- a/server/src/server.js +++ b/server/src/server.js @@ -1,136 +1,126 @@ import("./patches") -import fs from "node:fs" -import path from "node:path" import { EventEmitter } from "@foxify/events" -import defaults from "./defaults" - import IPCClient from "./classes/IPCClient" -import Endpoint from "./classes/endpoint" +import Route from "./classes/Route" -import registerBaseEndpoints from "./initializators/registerBaseEndpoints" -import registerWebsocketsEvents from "./initializators/registerWebsocketsEvents" -import registerHttpRoutes from "./initializators/registerHttpRoutes" +import registerBaseRoutes from "./registers/baseRoutes" +import registerBaseMiddlewares from "./registers/baseMiddlewares" +import registerBaseHeaders from "./registers/baseHeaders" +import registerWebsocketsFileEvents from "./registers/websocketFileEvents" +import registerHttpFileRoutes from "./registers/httpFileRoutes" +import registerServiceToIPC from "./registers/ipcService" +import bypassCorsHeaders from "./registers/bypassCorsHeaders" +import isExperimental from "./utils/isExperimental" +import getHostAddress from "./utils/getHostAddress" +import composeMiddlewares from "./utils/composeMiddlewares" + +import Vars from "./vars" import Engines from "./engines" class Server { - constructor(params = {}, controllers = {}, middlewares = {}, headers = {}) { - this.isExperimental = defaults.isExperimental ?? false - + constructor(params = {}) { if (this.isExperimental) { console.warn("\n🚧 This version of Linebridge is experimental! 🚧") - console.warn(`Version: ${defaults.version}\n`) + console.warn(`Version: ${Vars.libPkg.version}\n`) } this.params = { - ...defaults.params, - ...(params.default ?? params), + ...Vars.defaultParams, + ...params, } - this.controllers = { - ...(controllers.default ?? controllers), + // overrides some params with constructor values + if (typeof this.constructor.refName === "string") { + this.params.refName = this.constructor.refName } - this.middlewares = { - ...(middlewares.default ?? middlewares), + if (typeof this.constructor.useEngine === "string") { + this.params.useEngine = this.constructor.useEngine } - this.headers = { - ...defaults.headers, - ...(headers.default ?? headers), + if (typeof this.constructor.listenIp === "string") { + this.params.listenIp = this.constructor.listenIp } - // fix and fulfill params - this.params.useMiddlewares = this.params.useMiddlewares ?? [] + if ( + typeof this.constructor.listenPort === "string" || + typeof this.constructor.listenPort === "number" + ) { + this.params.listenPort = this.constructor.listenPort + } - this.params.name = this.constructor.refName ?? this.params.refName + if (typeof this.constructor.websockets === "boolean") { + this.params.websockets = this.constructor.websockets + } - this.params.useEngine = - this.constructor.useEngine ?? - this.params.useEngine ?? - "hyper-express" + if (typeof this.constructor.bypassCors === "boolean") { + this.params.bypassCors = this.constructor.bypassCors + } - this.params.listen_ip = - this.constructor.listenIp ?? - this.constructor.listen_ip ?? - this.params.listen_ip ?? - "0.0.0.0" + if (typeof this.constructor.baseRoutes === "boolean") { + this.params.baseRoutes = this.constructor.baseRoutes + } - this.params.listen_port = - this.constructor.listenPort ?? - this.constructor.listen_port ?? - this.params.listen_port ?? - 3000 + if (typeof this.constructor.routesPath === "string") { + this.params.routesPath = this.constructor.routesPath + } - this.params.http_protocol = this.params.http_protocol ?? "http" + if (typeof this.constructor.wsRoutesPath === "string") { + this.params.wsRoutesPath = this.constructor.wsRoutesPath + } - this.params.http_address = `${this.params.http_protocol}://${defaults.localhost_address}:${this.params.listen_port}` + if (typeof this.constructor.useMiddlewares !== "undefined") { + if (!Array.isArray(this.constructor.useMiddlewares)) { + this.constructor.useMiddlewares = [ + this.constructor.useMiddlewares, + ] + } - this.params.enableWebsockets = - this.constructor.enableWebsockets ?? - this.params.enableWebsockets ?? - false + this.params.useMiddlewares = this.constructor.useMiddlewares + } - this.params.ignoreCors = - this.constructor.ignoreCors ?? this.params.ignoreCors ?? true - - this.params.disableBaseEndpoints = - this.constructor.disableBaseEndpoints ?? - this.params.disableBaseEndpoints ?? - false - - this.params.routesPath = - this.constructor.routesPath ?? - this.params.routesPath ?? - path.resolve(process.cwd(), "routes") - - this.params.wsRoutesPath = - this.constructor.wsRoutesPath ?? - this.params.wsRoutesPath ?? - path.resolve(process.cwd(), "routes_ws") - - globalThis._linebridge = { - name: this.params.name, - useEngine: this.params.useEngine, - listenIp: this.params.listen_ip, - listenPort: this.params.listen_port, - httpProtocol: this.params.http_protocol, - httpAddress: this.params.http_address, - enableWebsockets: this.params.enableWebsockets, - ignoreCors: this.params.ignoreCors, - routesPath: this.params.routesPath, - validHttpMethods: defaults.valid_http_methods, + global._linebridge = { + vars: Vars, + params: this.params, } return this } + eventBus = new EventEmitter() + middlewares = {} + headers = {} + events = {} + contexts = {} + engine = null - events = null + get hasSSL() { + if (!this.ssl) { + return false + } - ipc = null + return this.ssl.key && this.ssl.cert + } - ipcEvents = null - - eventBus = new EventEmitter() + get isExperimental() { + return isExperimental() + } initialize = async () => { const startHrTime = process.hrtime() - // register events - if (this.events) { - if (this.events.default) { - this.events = this.events.default - } + // resolve current local private address of the host + this.localAddress = getHostAddress() - for (const [eventName, eventHandler] of Object.entries( - this.events, - )) { - this.eventBus.on(eventName, eventHandler) - } + this.contexts["server"] = this + + // register declared events to eventBus + for (const [eventName, eventHandler] of Object.entries(this.events)) { + this.eventBus.on(eventName, eventHandler) } // initialize engine @@ -140,28 +130,19 @@ class Server { throw new Error(`Engine ${this.params.useEngine} not found`) } + // construct engine instance + // important, pass this instance to the engine constructor this.engine = new this.engine(this) + // fire engine initialization if (typeof this.engine.initialize === "function") { await this.engine.initialize() } - // check if ws events are defined - if (typeof this.wsEvents !== "undefined") { - if (!this.engine.ws) { - console.warn( - "`wsEvents` detected, but Websockets are not enabled! Ignoring...", - ) - } else { - for (const [eventName, eventHandler] of Object.entries( - this.wsEvents, - )) { - this.engine.ws.events.set(eventName, eventHandler) - } - } - } + // at this point, we wanna to pass to onInitialize hook, + // a simple base context, without any registers extra - // try to execute onInitialize hook + // fire onInitialize hook if (typeof this.onInitialize === "function") { try { await this.onInitialize() @@ -171,211 +152,123 @@ class Server { } } - // set defaults - this.useDefaultHeaders() - this.useDefaultMiddlewares() + // Now gonna initialize the final steps & registers - if (this.routes) { + // bypassCors if needed + if (this.params.bypassCors) { + bypassCorsHeaders(this) + } + + // register base headers & middlewares + registerBaseHeaders(this) + registerBaseMiddlewares(this) + + // if websocket enabled, lets do some work + if (typeof this.engine.ws === "object") { + // register declared ws events + if (typeof this.wsEvents === "object") { + for (const [eventName, eventHandler] of Object.entries( + this.wsEvents, + )) { + this.engine.ws.events.set(eventName, eventHandler) + } + } + } + + // now, initialize declared routes with Endpoint class + if (typeof this.routes === "object") { for (const [route, endpoint] of Object.entries(this.routes)) { - this.engine.router.map[route] = new Endpoint(this, { + new Route(this, { ...endpoint, route: route, handlers: { [endpoint.method.toLowerCase()]: endpoint.fn, }, - }) + }).register() } } - // register http & ws routes - this.engine = await registerHttpRoutes( - this.params.routesPath, - this.engine, - this, - ) - this.engine = await registerWebsocketsEvents( - this.params.wsRoutesPath, - this.engine, - ) + // register http file routes + await registerHttpFileRoutes(this.params.routesPath, this) - // register base endpoints if enabled - if (!this.params.disableBaseEndpoints) { - await registerBaseEndpoints(this) + // register ws file routes + await registerWebsocketsFileEvents(this.params.wsRoutesPath, this) + + // register base routes if enabled + if (this.params.baseRoutes == true) { + await registerBaseRoutes(this) } - // use main router - await this.engine.app.use(this.engine.router) - - // if is a linebridge service then initialize IPC Channels + // if is a linebridge service, then initialize IPC Channels if (process.env.lb_service) { - await this.initializeIpc() - await this.registerServiceToIPC() - } + console.info("šŸš„ Starting IPC client") + this.ipc = global.ipc = new IPCClient(this, process) - // try to execute beforeInitialize hook. - if (typeof this.afterInitialize === "function") { - await this.afterInitialize() + await registerServiceToIPC(this) } // listen await this.engine.listen() + // execute afterInitialize hook. + if (typeof this.afterInitialize === "function") { + await this.afterInitialize() + } + // calculate elapsed time on ms, to fixed 2 const elapsedHrTime = process.hrtime(startHrTime) const elapsedTimeInMs = elapsedHrTime[0] * 1e3 + elapsedHrTime[1] / 1e6 console.info( - `šŸ›° Server ready!\n\t - ${this.params.http_protocol}://${this.params.listen_ip}:${this.params.listen_port} \n\t - Tooks ${elapsedTimeInMs.toFixed(2)}ms \n\t - Websocket: ${this.engine.ws ? "Enabled" : "Disabled"}`, + `šŸ›° Server ready!\n\t - ${this.hasSSL ? "https" : "http"}://${this.params.listenIp}:${this.params.listenPort} \n\t - Websocket: ${this.engine.ws ? "Enabled" : "Disabled"} \n\t - Routes: ${this.engine.map.size} \n\t - Tooks: ${elapsedTimeInMs.toFixed(2)}ms \n\t `, ) } - initializeIpc = async () => { - console.info("šŸš„ Starting IPC client") - - this.ipc = global.ipc = new IPCClient(this, process) - } - - useDefaultHeaders = () => { - this.engine.app.use((req, res, next) => { - Object.keys(this.headers).forEach((key) => { - res.setHeader(key, this.headers[key]) - }) - - next() - }) - } - - useDefaultMiddlewares = async () => { - const middlewares = await this.resolveMiddlewares([ - ...this.params.useMiddlewares, - ...(this.useMiddlewares ?? []), - ...defaults.useMiddlewares, - ]) - - middlewares.forEach((middleware) => { - this.engine.app.use(middleware) - }) - } - register = { - http: (endpoint, ..._middlewares) => { + http: (obj) => { // check and fix method - endpoint.method = endpoint.method?.toLowerCase() ?? "get" + obj.method = obj.method?.toLowerCase() ?? "get" - if (defaults.fixed_http_methods[endpoint.method]) { - endpoint.method = defaults.fixed_http_methods[endpoint.method] + if (Vars.fixedHttpMethods[obj.method]) { + obj.method = Vars.fixedHttpMethods[obj.method] } // check if method is supported - if (typeof this.engine.router[endpoint.method] !== "function") { - throw new Error(`Method [${endpoint.method}] is not supported!`) + if (typeof this.engine.router[obj.method] !== "function") { + throw new Error(`Method [${obj.method}] is not supported!`) } - // grab the middlewares - let middlewares = [..._middlewares] + // compose the middlewares + obj.middlewares = composeMiddlewares( + { ...this.middlewares, ...Vars.baseMiddlewares }, + obj.middlewares, + `[${obj.method.toUpperCase()}] ${obj.route}`, + ) - if (endpoint.middlewares) { - if (!Array.isArray(endpoint.middlewares)) { - endpoint.middlewares = [endpoint.middlewares] - } - - middlewares = [ - ...middlewares, - ...this.resolveMiddlewares(endpoint.middlewares), - ] - } - - this.engine.router.map[endpoint.route] = { - method: endpoint.method, - path: endpoint.route, - } + // set to the endpoints map, used by _map + this.engine.map.set(obj.route, { + method: obj.method, + path: obj.route, + }) // register endpoint to http interface router - this.engine.router[endpoint.method]( - endpoint.route, - ...middlewares, - endpoint.fn, + this.engine.router[obj.method]( + obj.route, + ...obj.middlewares, + obj.fn, ) }, + ws: (wsEndpointObj) => {}, } - resolveMiddlewares = (requestedMiddlewares) => { - const middlewares = { - ...this.middlewares, - ...defaults.middlewares, + _fireClose = () => { + if (typeof this.onClose === "function") { + this.onClose() } - if (typeof requestedMiddlewares === "string") { - requestedMiddlewares = [requestedMiddlewares] + if (typeof this.engine.close === "function") { + this.engine.close() } - - const execs = [] - - requestedMiddlewares.forEach((middlewareKey) => { - if (typeof middlewareKey === "string") { - if (typeof middlewares[middlewareKey] !== "function") { - throw new Error(`Middleware ${middlewareKey} not found!`) - } - - execs.push(middlewares[middlewareKey]) - } - - if (typeof middlewareKey === "function") { - execs.push(middlewareKey) - } - }) - - return execs - } - - registerServiceToIPC = () => { - if (!process.env.lb_service || !process.send) { - console.error("IPC not available") - return null - } - - // get only the root paths - let paths = Object.keys(this.engine.router.map).map((key) => { - const root = key.split("/")[1] - - return "/" + root - }) - - // remove duplicates - paths = [...new Set(paths)] - - // remove "" and _map - paths = paths.filter((key) => { - if (key === "/" || key === "/_map") { - return false - } - - return true - }) - - process.send({ - type: "service:register", - id: process.env.lb_service.id, - index: process.env.lb_service.index, - register: { - namespace: this.constructor.refName, - http: { - enabled: true, - paths: paths, - proto: this.ssl?.key && this.ssl?.cert ? "https" : "http", - }, - websocket: { - enabled: this.constructor.enableWebsockets, - path: - this.constructor.wsPath ?? - `/${this.constructor.refName}`, - }, - listen: { - ip: this.params.listen_ip, - port: this.params.listen_port, - }, - }, - }) } } diff --git a/server/src/utils/composeMiddlewares.js b/server/src/utils/composeMiddlewares.js new file mode 100644 index 0000000..2090b53 --- /dev/null +++ b/server/src/utils/composeMiddlewares.js @@ -0,0 +1,29 @@ +export default (middlewares, selectors, endpointRef) => { + if (!middlewares || !selectors) { + return [] + } + + if (typeof selectors === "string") { + selectors = [selectors] + } + + const execs = [] + + selectors.forEach((middlewareKey) => { + if (typeof middlewareKey === "string") { + if (typeof middlewares[middlewareKey] !== "function") { + throw new Error( + `Required middleware [${middlewareKey}] not found!\n\t- Required by endpoint > ${endpointRef}\n\n`, + ) + } + + execs.push(middlewares[middlewareKey]) + } + + if (typeof middlewareKey === "function") { + execs.push(middlewareKey) + } + }) + + return execs +} diff --git a/server/src/utils/getHostAddress.js b/server/src/utils/getHostAddress.js new file mode 100644 index 0000000..05b07ea --- /dev/null +++ b/server/src/utils/getHostAddress.js @@ -0,0 +1,23 @@ +import os from "node:os" + +export default function getHostAddress() { + const interfaces = os.networkInterfaces() + + for (const key in interfaces) { + const iface = interfaces[key] + + for (let index = 0; index < iface.length; index++) { + const alias = iface[index] + + if ( + alias.family === "IPv4" && + alias.address !== "127.0.0.1" && + !alias.internal + ) { + return alias.address + } + } + } + + return "0.0.0.0" +} diff --git a/server/src/utils/index.js b/server/src/utils/index.js deleted file mode 100755 index 36176b1..0000000 --- a/server/src/utils/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default as Schematized } from "./schematized" \ No newline at end of file diff --git a/server/src/utils/isExperimental.js b/server/src/utils/isExperimental.js new file mode 100644 index 0000000..eb43333 --- /dev/null +++ b/server/src/utils/isExperimental.js @@ -0,0 +1,8 @@ +import fs from "node:fs" +import path from "node:path" + +import Vars from "../vars" + +export default () => { + return fs.existsSync(path.resolve(Vars.rootLibPath, ".experimental")) +} diff --git a/server/src/utils/nanoid/index.js b/server/src/utils/nanoid.js similarity index 100% rename from server/src/utils/nanoid/index.js rename to server/src/utils/nanoid.js diff --git a/server/src/lib/recursiveRegister/index.js b/server/src/utils/recursiveRegister.js similarity index 100% rename from server/src/lib/recursiveRegister/index.js rename to server/src/utils/recursiveRegister.js diff --git a/server/src/utils/schematized/index.js b/server/src/utils/schematized.js similarity index 100% rename from server/src/utils/schematized/index.js rename to server/src/utils/schematized.js diff --git a/server/src/vars.js b/server/src/vars.js new file mode 100755 index 0000000..e27f4c1 --- /dev/null +++ b/server/src/vars.js @@ -0,0 +1,48 @@ +import path from "node:path" + +const rootLibPath = path.resolve(__dirname, "../") +const packageJSON = require(rootLibPath, "../package.json") +const projectPkg = require(path.resolve(process.cwd(), "package.json")) + +export default { + libPath: __dirname, + rootLibPath: rootLibPath, + libPkg: packageJSON, + projectCwd: process.cwd(), + projectPkg: projectPkg, + defaultParams: { + refName: "linebridge", + listenIp: "0.0.0.0", + listenPort: 3000, + useEngine: "he", + websockets: false, + bypassCors: false, + baseRoutes: true, + routesPath: path.resolve(process.cwd(), "routes"), + wsRoutesPath: path.resolve(process.cwd(), "ws_routes"), + useMiddlewares: [], + httpMethods: [ + "get", + "post", + "put", + "patch", + "del", + "delete", + "trace", + "head", + "any", + "options", + "ws", + ], + }, + baseHeaders: { + server: "linebridge", + "lb-version": packageJSON.version, + }, + baseMiddlewares: { + logs: require("./middlewares/logger").default, + }, + fixedHttpMethods: { + del: "delete", + }, +}