diff --git a/src/server/index.js b/src/server/index.js index 99c30ae..abe5c69 100755 --- a/src/server/index.js +++ b/src/server/index.js @@ -1,19 +1,10 @@ const path = require("path") -const fs = require("fs") const net = require("corenode/net") - -const HyperExpress = require("hyper-express") -const io = require("socket.io") - const packageJSON = require(path.resolve(module.path, "../../package.json")) + +// set globals variables global.LINEBRIDGE_SERVER_VERSION = packageJSON.version -const tokenizer = require("corenode/libs/tokenizer") -const { randomWord } = require("@corenode/utils") - -const { serverManifest } = require("../lib") - -// set globals default variables global.LOCALHOST_ADDRESS = net.ip.getHostAddress() ?? "localhost" global.FIXED_HTTP_METHODS = { @@ -46,373 +37,7 @@ if (process.env.NODE_ENV !== "production") { global.DEFAULT_MIDDLEWARES.push(require("morgan")("dev")) } -function outputServerError({ - message = "Unexpected error", - description, - ref = "SERVER", -}) { - console.error(`\n\x1b[41m\x1b[37m🆘 [${ref}] ${message}\x1b[0m ${description ? `\n ${description}` : ""} \n`) +module.exports = { + Server: require("./server.js"), + Controller: require("./classes/controller"), } - -class Server { - constructor(params = {}, controllers = [], middlewares = {}) { - this.params = { - ...global.DEFAULT_SERVER_PARAMS, - ...params - } - this.controllers = [ - ...controllers - ] - this.middlewares = { - ...middlewares - } - this.headers = { - ...global.DEFAULT_HEADERS, - ...this.params.headers - } - this.endpointsMap = {} - - this.WSListenPort = this.params.wsPort ?? 3020 - this.HTTPlistenPort = this.params.port ?? 3010 - - // TODO: Handle HTTPS and WSS - this.HTTPAddress = `http://${global.LOCALHOST_ADDRESS}:${this.HTTPlistenPort}` - this.WSAddress = `ws://${global.LOCALHOST_ADDRESS}:${this.WSListenPort}` - - //* set server basics - this.httpInterface = global.httpInterface = new HyperExpress.Server() - this.wsInterface = global.wsInterface = { - io: new io.Server(this.WSListenPort), - map: {}, - eventsChannels: [], - } - - //? check if origin.server exists - if (!fs.existsSync(serverManifest.filepath)) { - serverManifest.create() - } - - //? check origin.server integrity - const MANIFEST_DATA = global.MANIFEST_DATA = serverManifest.get() - const MANIFEST_STAT = global.MANIFEST_STAT = serverManifest.stat() - - if (typeof MANIFEST_DATA.created === "undefined") { - console.warn("Server generation file not contains an creation date") - serverManifest.write({ created: Date.parse(MANIFEST_STAT.birthtime) }) - } - - if (typeof MANIFEST_DATA.serverToken === "undefined") { - console.warn("Missing server token!") - serverManifest.create() - } - - this.id = this.params.id ?? randomWord.generate() ?? "unavailable" - this.usid = tokenizer.generateUSID() - this.oskid = serverManifest.get("serverToken") - - serverManifest.write({ lastStart: Date.now() }) - - // patch request model with a middleware - this.httpInterface.use(async (req, res) => { - // make sure req has an body and query - if (typeof req.body === "undefined") { - req.body = {} - } - if (typeof req.query === "undefined") { - req.query = {} - } - - // if server has enabled urlencoded parser, parse the body - if (this.params.urlencoded) { - req.body = await req.urlencoded() - } - }) - - return this - } - - initialize = async () => { - //* set server defined headers - this.initializeHeaders() - - //* set server defined middlewares - this.initializeMiddlewares() - - //* register main index endpoint `/` - await this.registerBaseEndpoints() - - //* register controllers - await this.initializeControllers() - - // initialize socket.io - this.wsInterface.io.on("connection", this.handleWSClientConnection) - - // initialize http server - await this.httpInterface.listen(this.HTTPlistenPort, this.params.listen ?? "0.0.0.0") - - // output server info - console.log(`✅ Server is up and running!`) - this.consoleOutputServerInfo() - - // handle exit events - process.on("SIGTERM", this.cleanupProcess) - process.on("SIGINT", this.cleanupProcess) - } - - initializeHeaders = () => { - this.httpInterface.use((req, res, next) => { - Object.keys(this.headers).forEach((key) => { - res.setHeader(key, this.headers[key]) - }) - - next() - }) - } - - initializeMiddlewares = () => { - const useMiddlewares = [...global.DEFAULT_MIDDLEWARES, ...(this.params.middlewares ?? [])] - - useMiddlewares.forEach((middleware) => { - if (typeof middleware === "function") { - this.httpInterface.use(middleware) - } - }) - } - - initializeControllers = async () => { - for await (let controller of this.controllers) { - if (typeof controller !== "function") { - throw new Error(`Controller must use the controller class!`) - } - - if (controller.disabled) { - console.warn(`⏩ Controller [${controller.name}] is disabled! Initialization skipped...`) - continue - } - - try { - const ControllerInstance = new controller() - - // get endpoints from controller (ComplexController) - const HTTPEndpoints = ControllerInstance.getEndpoints() - const WSEndpoints = ControllerInstance.getWSEndpoints() - - HTTPEndpoints.forEach((endpoint) => { - this.registerHTTPEndpoint(endpoint, ...this.resolveMiddlewares(controller.useMiddlewares)) - }) - - WSEndpoints.forEach((endpoint) => { - this.registerWSEndpoint(endpoint) - }) - } catch (error) { - if (!global.silentOutputServerErrors) { - outputServerError({ - message: "Controller initialization failed:", - description: error.stack, - ref: controller.refName ?? controller.name, - }) - } - } - } - } - - registerHTTPEndpoint = (endpoint, ...execs) => { - // check and fix method - endpoint.method = endpoint.method?.toLowerCase() ?? "get" - - if (global.FIXED_HTTP_METHODS[endpoint.method]) { - endpoint.method = global.FIXED_HTTP_METHODS[endpoint.method] - } - - // check if method is supported - if (typeof this.httpInterface[endpoint.method] !== "function") { - throw new Error(`Method [${endpoint.method}] is not supported!`) - } - - // grab the middlewares - let middlewares = [...execs] - - if (endpoint.middlewares) { - middlewares = [...middlewares, ...this.resolveMiddlewares(endpoint.middlewares)] - } - - // make sure method has root object on endpointsMap - if (typeof this.endpointsMap[endpoint.method] !== "object") { - this.endpointsMap[endpoint.method] = {} - } - - // create model for http interface router - const routeModel = [endpoint.route, ...middlewares, this.createHTTPRequestHandler(endpoint)] - - // register endpoint to http interface router - this.httpInterface[endpoint.method](...routeModel) - - // extend to map - this.endpointsMap[endpoint.method] = { - ...this.endpointsMap[endpoint.method], - [endpoint.route]: { - route: endpoint.route, - enabled: endpoint.enabled ?? true, - }, - } - } - - registerWSEndpoint = (endpoint, ...execs) => { - endpoint.nsp = endpoint.nsp ?? "/main" - - this.wsInterface.eventsChannels.push([endpoint.nsp, endpoint.on, endpoint.dispatch]) - - this.wsInterface.map[endpoint.on] = { - nsp: endpoint.nsp, - channel: endpoint.on, - } - } - - registerBaseEndpoints() { - //* register main index endpoint `/` - // this is the default endpoint, should return the server info and the map of all endpoints (http & ws) - this.registerHTTPEndpoint({ - method: "get", - route: "/", - fn: (req, res) => { - return res.json({ - LINEBRIDGE_SERVER_VERSION: LINEBRIDGE_SERVER_VERSION, - id: this.id, - usid: this.usid, - oskid: this.oskid, - requestTime: new Date().getTime(), - endpointsMap: this.endpointsMap, - wsEndpointsMap: this.wsInterface.map, - }) - } - }) - } - - //* resolvers - resolveMiddlewares = (middlewares) => { - middlewares = Array.isArray(middlewares) ? middlewares : [middlewares] - const middlewaresArray = [] - - middlewares.forEach((middleware) => { - if (typeof middleware === "string") { - if (typeof this.middlewares[middleware] !== "function") { - throw new Error(`Middleware ${middleware} not found!`) - } - - middlewaresArray.push(this.middlewares[middleware]) - } - - if (typeof middleware === "function") { - middlewaresArray.push(middleware) - } - }) - - return middlewaresArray - } - - cleanupProcess = () => { - console.log("🛑 Stopping server...") - - this.httpInterface.close() - this.wsInterface.io.close() - - process.exit(1) - } - - // handlers - createHTTPRequestHandler = (endpoint) => { - return async (req, res) => { - try { - // check if endpoint is disabled - if (!this.endpointsMap[endpoint.method][endpoint.route].enabled) { - throw new Error("Endpoint is disabled!") - } - - // return the returning call of the endpoint function - return await endpoint.fn(req, res) - } catch (error) { - if (typeof this.params.onRouteError === "function") { - return this.params.onRouteError(req, res, error) - } else { - if (!global.silentOutputServerErrors) { - outputServerError({ - message: "Unhandled route error:", - description: error.stack, - ref: [endpoint.method, endpoint.route].join("|"), - }) - } - - return res.status(500).json({ - "error": error.message - }) - } - } - } - } - - handleWSClientConnection = async (socket) => { - socket.res = (...args) => { - socket.emit("response", ...args) - } - socket.err = (...args) => { - socket.emit("responseError", ...args) - } - - if (typeof this.params.onWSClientConnection === "function") { - await this.params.onWSClientConnection(socket) - } - - for await (const [nsp, on, dispatch] of this.wsInterface.eventsChannels) { - socket.on(on, async (...args) => { - try { - await dispatch(socket, ...args).catch((error) => { - socket.err({ - message: error.message, - }) - }) - } catch (error) { - socket.err({ - message: error.message, - }) - } - }) - } - - socket.on("ping", () => { - socket.emit("pong") - }) - - socket.on("disconnect", async () => { - if (typeof this.params.onWSClientDisconnect === "function") { - await this.params.onWSClientDisconnect(socket) - } - }) - } - - // public methods - consoleOutputServerInfo = () => { - console.log(`🌐 Server info:`) - console.table({ - "ID": this.id, - "Version": LINEBRIDGE_SERVER_VERSION, - "HTTP address": this.HTTPAddress, - "WS address": this.WSAddress, - "WS port": this.WSListenPort, - "HTTP port": this.HTTPlistenPort, - }) - } - - toogleEndpointReachability = (method, route, enabled) => { - if (typeof this.endpointsMap[method] !== "object") { - throw new Error(`Cannot toogle endpoint, method [${method}] not set!`) - } - - if (typeof this.endpointsMap[method][route] !== "object") { - throw new Error(`Cannot toogle endpoint [${route}], is not registered!`) - } - - this.endpointsMap[method][route].enabled = enabled ?? !this.endpointsMap[method][route].enabled - } -} - -module.exports = Server \ No newline at end of file diff --git a/src/server/server.js b/src/server/server.js new file mode 100644 index 0000000..8d490e8 --- /dev/null +++ b/src/server/server.js @@ -0,0 +1,373 @@ +const fs = require("fs") +const path = require("path") + +const HyperExpress = require("hyper-express") +const io = require("socket.io") + +const tokenizer = require("corenode/libs/tokenizer") +const { randomWord } = require("@corenode/utils") + +const { serverManifest, outputServerError } = require("../lib") + +class Server { + constructor(params = {}, controllers = [], middlewares = {}) { + this.params = { + ...global.DEFAULT_SERVER_PARAMS, + ...params + } + this.controllers = [ + ...controllers + ] + this.middlewares = { + ...middlewares + } + this.headers = { + ...global.DEFAULT_HEADERS, + ...this.params.headers + } + this.endpointsMap = {} + + this.WSListenPort = this.params.wsPort ?? 3020 + this.HTTPlistenPort = this.params.port ?? 3010 + + // TODO: Handle HTTPS and WSS + this.HTTPAddress = `http://${global.LOCALHOST_ADDRESS}:${this.HTTPlistenPort}` + this.WSAddress = `ws://${global.LOCALHOST_ADDRESS}:${this.WSListenPort}` + + //* set server basics + this.httpInterface = global.httpInterface = new HyperExpress.Server() + this.wsInterface = global.wsInterface = { + io: new io.Server(this.WSListenPort), + map: {}, + eventsChannels: [], + } + + //? check if origin.server exists + if (!fs.existsSync(serverManifest.filepath)) { + serverManifest.create() + } + + //? check origin.server integrity + const MANIFEST_DATA = global.MANIFEST_DATA = serverManifest.get() + const MANIFEST_STAT = global.MANIFEST_STAT = serverManifest.stat() + + if (typeof MANIFEST_DATA.created === "undefined") { + console.warn("Server generation file not contains an creation date") + serverManifest.write({ created: Date.parse(MANIFEST_STAT.birthtime) }) + } + + if (typeof MANIFEST_DATA.serverToken === "undefined") { + console.warn("Missing server token!") + serverManifest.create() + } + + this.id = this.params.id ?? randomWord.generate() ?? "unavailable" + this.usid = tokenizer.generateUSID() + this.oskid = serverManifest.get("serverToken") + + serverManifest.write({ lastStart: Date.now() }) + + // patch request model with a middleware + this.httpInterface.use(async (req, res) => { + // make sure req has an body and query + if (typeof req.body === "undefined") { + req.body = {} + } + if (typeof req.query === "undefined") { + req.query = {} + } + + // if server has enabled urlencoded parser, parse the body + if (this.params.urlencoded) { + req.body = await req.urlencoded() + } + }) + + return this + } + + initialize = async () => { + //* set server defined headers + this.initializeHeaders() + + //* set server defined middlewares + this.initializeMiddlewares() + + //* register main index endpoint `/` + await this.registerBaseEndpoints() + + //* register controllers + await this.initializeControllers() + + // initialize socket.io + this.wsInterface.io.on("connection", this.handleWSClientConnection) + + // initialize http server + await this.httpInterface.listen(this.HTTPlistenPort, this.params.listen ?? "0.0.0.0") + + // output server info + console.log(`✅ Server is up and running!`) + this.consoleOutputServerInfo() + + // handle exit events + process.on("SIGTERM", this.cleanupProcess) + process.on("SIGINT", this.cleanupProcess) + } + + initializeHeaders = () => { + this.httpInterface.use((req, res, next) => { + Object.keys(this.headers).forEach((key) => { + res.setHeader(key, this.headers[key]) + }) + + next() + }) + } + + initializeMiddlewares = () => { + const useMiddlewares = [...global.DEFAULT_MIDDLEWARES, ...(this.params.middlewares ?? [])] + + useMiddlewares.forEach((middleware) => { + if (typeof middleware === "function") { + this.httpInterface.use(middleware) + } + }) + } + + initializeControllers = async () => { + for await (let controller of this.controllers) { + if (typeof controller !== "function") { + throw new Error(`Controller must use the controller class!`) + } + + if (controller.disabled) { + console.warn(`⏩ Controller [${controller.name}] is disabled! Initialization skipped...`) + continue + } + + try { + const ControllerInstance = new controller() + + // get endpoints from controller (ComplexController) + const HTTPEndpoints = ControllerInstance.getEndpoints() + const WSEndpoints = ControllerInstance.getWSEndpoints() + + HTTPEndpoints.forEach((endpoint) => { + this.registerHTTPEndpoint(endpoint, ...this.resolveMiddlewares(controller.useMiddlewares)) + }) + + WSEndpoints.forEach((endpoint) => { + this.registerWSEndpoint(endpoint) + }) + } catch (error) { + if (!global.silentOutputServerErrors) { + outputServerError({ + message: "Controller initialization failed:", + description: error.stack, + ref: controller.refName ?? controller.name, + }) + } + } + } + } + + registerHTTPEndpoint = (endpoint, ...execs) => { + // check and fix method + endpoint.method = endpoint.method?.toLowerCase() ?? "get" + + if (global.FIXED_HTTP_METHODS[endpoint.method]) { + endpoint.method = global.FIXED_HTTP_METHODS[endpoint.method] + } + + // check if method is supported + if (typeof this.httpInterface[endpoint.method] !== "function") { + throw new Error(`Method [${endpoint.method}] is not supported!`) + } + + // grab the middlewares + let middlewares = [...execs] + + if (endpoint.middlewares) { + middlewares = [...middlewares, ...this.resolveMiddlewares(endpoint.middlewares)] + } + + // make sure method has root object on endpointsMap + if (typeof this.endpointsMap[endpoint.method] !== "object") { + this.endpointsMap[endpoint.method] = {} + } + + // create model for http interface router + const routeModel = [endpoint.route, ...middlewares, this.createHTTPRequestHandler(endpoint)] + + // register endpoint to http interface router + this.httpInterface[endpoint.method](...routeModel) + + // extend to map + this.endpointsMap[endpoint.method] = { + ...this.endpointsMap[endpoint.method], + [endpoint.route]: { + route: endpoint.route, + enabled: endpoint.enabled ?? true, + }, + } + } + + registerWSEndpoint = (endpoint, ...execs) => { + endpoint.nsp = endpoint.nsp ?? "/main" + + this.wsInterface.eventsChannels.push([endpoint.nsp, endpoint.on, endpoint.dispatch]) + + this.wsInterface.map[endpoint.on] = { + nsp: endpoint.nsp, + channel: endpoint.on, + } + } + + registerBaseEndpoints() { + //* register main index endpoint `/` + // this is the default endpoint, should return the server info and the map of all endpoints (http & ws) + this.registerHTTPEndpoint({ + method: "get", + route: "/", + fn: (req, res) => { + return res.json({ + LINEBRIDGE_SERVER_VERSION: LINEBRIDGE_SERVER_VERSION, + id: this.id, + usid: this.usid, + oskid: this.oskid, + requestTime: new Date().getTime(), + endpointsMap: this.endpointsMap, + wsEndpointsMap: this.wsInterface.map, + }) + } + }) + } + + //* resolvers + resolveMiddlewares = (middlewares) => { + middlewares = Array.isArray(middlewares) ? middlewares : [middlewares] + const middlewaresArray = [] + + middlewares.forEach((middleware) => { + if (typeof middleware === "string") { + if (typeof this.middlewares[middleware] !== "function") { + throw new Error(`Middleware ${middleware} not found!`) + } + + middlewaresArray.push(this.middlewares[middleware]) + } + + if (typeof middleware === "function") { + middlewaresArray.push(middleware) + } + }) + + return middlewaresArray + } + + cleanupProcess = () => { + console.log("🛑 Stopping server...") + + this.httpInterface.close() + this.wsInterface.io.close() + + process.exit(1) + } + + // handlers + createHTTPRequestHandler = (endpoint) => { + return async (req, res) => { + try { + // check if endpoint is disabled + if (!this.endpointsMap[endpoint.method][endpoint.route].enabled) { + throw new Error("Endpoint is disabled!") + } + + // return the returning call of the endpoint function + return await endpoint.fn(req, res) + } catch (error) { + if (typeof this.params.onRouteError === "function") { + return this.params.onRouteError(req, res, error) + } else { + if (!global.silentOutputServerErrors) { + outputServerError({ + message: "Unhandled route error:", + description: error.stack, + ref: [endpoint.method, endpoint.route].join("|"), + }) + } + + return res.status(500).json({ + "error": error.message + }) + } + } + } + } + + handleWSClientConnection = async (socket) => { + socket.res = (...args) => { + socket.emit("response", ...args) + } + socket.err = (...args) => { + socket.emit("responseError", ...args) + } + + if (typeof this.params.onWSClientConnection === "function") { + await this.params.onWSClientConnection(socket) + } + + for await (const [nsp, on, dispatch] of this.wsInterface.eventsChannels) { + socket.on(on, async (...args) => { + try { + await dispatch(socket, ...args).catch((error) => { + socket.err({ + message: error.message, + }) + }) + } catch (error) { + socket.err({ + message: error.message, + }) + } + }) + } + + socket.on("ping", () => { + socket.emit("pong") + }) + + socket.on("disconnect", async () => { + if (typeof this.params.onWSClientDisconnect === "function") { + await this.params.onWSClientDisconnect(socket) + } + }) + } + + // public methods + consoleOutputServerInfo = () => { + console.log(`🌐 Server info:`) + console.table({ + "ID": this.id, + "Version": LINEBRIDGE_SERVER_VERSION, + "HTTP address": this.HTTPAddress, + "WS address": this.WSAddress, + "WS port": this.WSListenPort, + "HTTP port": this.HTTPlistenPort, + }) + } + + toogleEndpointReachability = (method, route, enabled) => { + if (typeof this.endpointsMap[method] !== "object") { + throw new Error(`Cannot toogle endpoint, method [${method}] not set!`) + } + + if (typeof this.endpointsMap[method][route] !== "object") { + throw new Error(`Cannot toogle endpoint [${route}], is not registered!`) + } + + this.endpointsMap[method][route].enabled = enabled ?? !this.endpointsMap[method][route].enabled + } +} + +module.exports = Server \ No newline at end of file