push from local

This commit is contained in:
SrGooglo 2024-03-10 00:25:35 +00:00
parent c011f2353f
commit 6d553830ab
55 changed files with 899 additions and 676 deletions

0
.DS_Store vendored Normal file → Executable file
View File

0
.experimental Normal file → Executable file
View File

0
.npmignore Normal file → Executable file
View File

2
bootstrap.js vendored Normal file → Executable file
View File

@ -67,7 +67,7 @@ global.toBoolean = (value) => {
} }
async function injectEnvFromInfisical() { async function injectEnvFromInfisical() {
const envMode = global.FORCE_ENV ?? global.isProduction ? "prod" : "dev" const envMode = "dev"
console.log(`🔑 Injecting env variables from INFISICAL in [${envMode}] mode...`) console.log(`🔑 Injecting env variables from INFISICAL in [${envMode}] mode...`)

View File

@ -1,9 +0,0 @@
{
"name": "example_server",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"scripts": {
"start": "hermes-node ./src/index.js"
}
}

View File

@ -1,17 +0,0 @@
export default {
method: "get",
route: "/fail",
fn: async (req, res) => {
throw new Error("Testing catch handler")
return res.json({
message: "This is not supposed to be here!"
})
},
onCatch: async (err, req, res) => {
return res.json({
message: "Catch handler works!",
error: err.message,
})
}
}

View File

@ -1,9 +0,0 @@
export default {
method: "get",
route: "/",
fn: async (req, res) => {
return res.json({
message: "Hello world!"
})
}
}

View File

@ -1,13 +0,0 @@
export default {
method: "get",
route: "/middlewaresTest",
middlewares: ["test", (req, res, next) => {
console.log("Hello from inline middleware 2")
next()
}],
fn: async (req, res) => {
return res.json({
message: "Hello world! Look at the console for the middlewares!"
})
}
}

View File

@ -1,9 +0,0 @@
export default {
method: "get",
route: "/withoutClass",
fn: async (req, res) => {
return res.json({
message: "Im an object endpoint",
})
}
}

View File

@ -1,13 +0,0 @@
import { Endpoint } from "../../../../../src/server"
export default class TestOneEndpoint extends Endpoint {
static method = "get"
static route = "/one"
fn = async (req, res) => {
return res.json({
message: "Hello world! Im using Endpoint class!"
})
}
}

View File

@ -1,8 +0,0 @@
import { Controller } from "../../../../src/server"
import generateEndpointsFromDir from "../../../../src/server/lib/generateEndpointsFromDir"
export default class TestController extends Controller {
static useRoute = "/test"
httpEndpoints = generateEndpointsFromDir(__dirname + "/endpoints")
}

View File

@ -1 +0,0 @@
export { default as TestController } from "./TestController"

View File

@ -1,13 +0,0 @@
import { registerBaseAliases, Server } from "../../src/server"
registerBaseAliases()
const server = new Server({
name: "example_server",
listen_port: 3011,
},
require("@controllers"),
require("@middlewares"),
)
server.initialize()

View File

@ -1,6 +0,0 @@
export default {
"test": (req, res, next) => {
console.log("Hello loaded middleware 1")
next()
}
}

View File

@ -1,6 +1,6 @@
{ {
"name": "linebridge", "name": "linebridge",
"version": "0.16.0", "version": "0.18.0",
"description": "A simple, fast, and powerful REST API interface library", "description": "A simple, fast, and powerful REST API interface library",
"author": "RageStudio", "author": "RageStudio",
"main": "./dist/client/index.js", "main": "./dist/client/index.js",
@ -13,12 +13,14 @@
"access": "public" "access": "public"
}, },
"files": [ "files": [
"src/**/**",
"dist/**/**", "dist/**/**",
"./package.json" "./package.json"
], ],
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@foxify/events": "^2.1.0", "@foxify/events": "^2.1.0",
"@gullerya/object-observer": "^6.1.3",
"@socket.io/cluster-adapter": "^0.2.2", "@socket.io/cluster-adapter": "^0.2.2",
"@socket.io/redis-adapter": "^8.2.1", "@socket.io/redis-adapter": "^8.2.1",
"@socket.io/redis-emitter": "^5.1.0", "@socket.io/redis-emitter": "^5.1.0",

0
src/client/bridge.js Normal file → Executable file
View File

0
src/client/classes/WSInterface/index.js Normal file → Executable file
View File

0
src/client/classes/index.js Normal file → Executable file
View File

0
src/client/controller.js Normal file → Executable file
View File

0
src/client/lib/generateHTTPRequestDispatcher/index.js Normal file → Executable file
View File

0
src/client/lib/generateWSRequestDispatcher/index.js Normal file → Executable file
View File

0
src/client/lib/index.js Normal file → Executable file
View File

0
src/lib/event_emitter/index.js Normal file → Executable file
View File

View File

@ -0,0 +1,23 @@
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) => {
const { params } = this.ctx
return {
name: params.name ?? "unknown",
version: projectPkg.version ?? "unknown",
engine: params.useEngine ?? "unknown",
request_time: new Date().getTime(),
lb_version: defaults.version ?? "unknown",
experimental: defaults.isExperimental.toString() ?? "unknown",
}
}
}

View File

@ -0,0 +1,24 @@
import Endpoint from "../classes/endpoint"
export default class MainEndpoint extends Endpoint {
route = "/_map"
get = async (req, res) => {
const httpMap = Object.entries(this.ctx.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: []
})
}
}

View File

@ -0,0 +1,155 @@
import { EventEmitter } from "@foxify/events"
export default class IPCClient {
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
}
const { event, payload } = msg
if (!event || !event.startsWith("ipc:")) {
return false
}
if (event.startsWith("ipc:exec")) {
return this.handleExecution(payload)
}
if (event.startsWith("ipc:akn")) {
return this.handleAcknowledgement(payload)
}
})
}
eventBus = new EventEmitter()
handleExecution = async (payload) => {
let { id, command, args, from } = payload
let fn = this.self.ipcEvents[command]
if (!fn) {
this.process.send({
event: `ipc:akn:${id}`,
payload: {
target: from,
from: this.self.constructor.refName,
id: id,
error: `IPC: Command [${command}] not found`,
}
})
return false
}
try {
let result = await fn(this.self.contexts, ...args)
this.process.send({
event: `ipc:akn:${id}`,
payload: {
target: from,
from: this.self.constructor.refName,
id: id,
result: result,
}
})
} catch (error) {
this.process.send({
event: `ipc:akn:${id}`,
payload: {
target: from,
from: this.self.constructor.refName,
id: id,
error: error.message,
}
})
}
}
handleAcknowledgement = async (payload) => {
let { id, result, error } = payload
this.eventBus.emit(`ipc:akn:${id}`, {
id: id,
result: result,
error: error,
})
}
// 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 {
this.process.send({
event: "ipc:exec",
payload: {
target: to_service_id,
from: this.self.constructor.refName,
id: remote_call_id,
command,
args,
}
})
this.eventBus.once(`ipc:akn:${remote_call_id}`, resolve)
} catch (error) {
console.error(error)
reject(error)
}
}).catch((error) => {
return {
error: error
}
})
if (response.error) {
throw new OperationError(500, response.error)
}
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()
try {
this.process.send({
event: "ipc:exec",
payload: {
target: to_service_id,
from: this.self.constructor.refName,
id: remote_call_id,
command,
args,
}
})
return {
id: remote_call_id
}
} catch (error) {
console.error(error)
return {
error: error
}
}
}
}

View File

@ -0,0 +1,54 @@
export default class IPCRouter {
processes = []
register = (service) => {
service.instance.on("message", (msg) => {
if (typeof msg !== "object") {
// not an IPC message, ignore
return false
}
const { event, payload } = msg
if (!event || !event.startsWith("ipc:")) {
// not an IPC message, ignore
return false
}
const { target } = payload
if (!target) {
return false
}
if (event.startsWith("ipc:")) {
return this.route(event, payload)
}
})
this.processes.push(service)
}
unregister = (service) => {
this.processes = this.processes.filter((_process) => _process.id !== service.id)
}
route = (event, payload) => {
const { target, from } = payload
// first search service
let targetService = this.processes.find((_process) => _process.id === target)
if (!targetService) {
// TODO: respond with error
console.error(`[IPC:ROUTER] Service [${destinationId}] not found`)
return false
}
targetService.instance.send({
event: event,
payload: payload
})
}
}

0
src/server/classes/controller/index.js Normal file → Executable file
View File

73
src/server/classes/endpoint/index.js Normal file → Executable file
View File

@ -1,3 +1,74 @@
module.exports = class Endpoint { export default class Endpoint {
constructor(ctx, params = {}) {
this.ctx = ctx
this.params = params
this.route = this.constructor.route ?? this.params.route
this.enabled = this.constructor.enabled ?? this.params.enabled ?? true
this.middlewares = [
...this.middlewares ?? [],
...this.params.middlewares ?? [],
]
if (this.params.handlers) {
for (const method of this.ctx.valid_http_methods) {
if (typeof this.params.handlers[method] === "function") {
this[method] = this.params.handlers[method]
}
}
}
this.selfRegister()
return this
}
createHandler(fn) {
return async (req, res) => {
try {
const result = await fn(req, res)
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 () => {
const validMethods = this.ctx.valid_http_methods
for await (const method of validMethods) {
const methodHandler = this[method]
if (typeof methodHandler !== "undefined") {
const fn = this.createHandler(this[method].fn ?? this[method])
this.ctx.register.http(
{
method,
route: this.route,
middlewares: this.middlewares,
fn: fn,
},
)
}
}
}
} }

0
src/server/classes/index.js Normal file → Executable file
View File

View File

@ -0,0 +1,6 @@
export default class OperationError {
constructor(code, message) {
this.code = code ?? 500
this.message = message
}
}

0
src/server/classes/rtengine/index.js Normal file → Executable file
View File

65
src/server/defaults.js Normal file
View File

@ -0,0 +1,65 @@
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",
],
}

View File

@ -0,0 +1,35 @@
import { createServer } from "node:http"
import express from "express"
import socketio from "socket.io"
import rtengine from "../../classes/rtengine"
export default class Engine {
constructor(params) {
this.params = params
}
http = null
app = null
io = null
ws = null
router = express.Router()
init = async (params) => {
this.app = express()
this.http = createServer(this.app)
this.io = new socketio.Server(this.http)
this.ws = new rtengine({
...params,
io: this.io,
http: false,
})
this.app.use(express.json())
this.app.use(express.urlencoded({ extended: true }))
}
listen = async () => {
await this.http.listen(this.params.listen_port)
}
}

View File

@ -0,0 +1,30 @@
import he from "hyper-express"
export default class Engine {
constructor(params) {
this.params = params
}
app = new he.Server()
router = new he.Router()
init = async (params) => {
// register 404
await this.router.any("*", (req, res) => {
return res.status(404).json({
code: 404,
message: "Not found"
})
})
// register body parser
await this.app.use(async (req, res, next) => {
req.body = await req.urlencoded()
})
}
listen = async () => {
await this.app.listen(this.params.listen_port)
}
}

View File

@ -0,0 +1,126 @@
import { EventEmitter } from "@foxify/events"
class WorkerEngineRouter {
routes = []
get = (path, ...execs) => {
}
post = (path, ...execs) => {
}
delete = (path, ...execs) => {
}
put = (path, ...execs) => {
}
patch = (path, ...execs) => {
}
head = (path, ...execs) => {
}
options = (path, ...execs) => {
}
any = (path, ...execs) => {
}
use = (path, ...execs) => {
}
}
class WorkerEngine {
static ipcPrefix = "rail:"
selfId = process.env.lb_service.id
router = new WorkerEngineRouter()
eventBus = new EventEmitter()
perExecTail = []
initialize = async () => {
console.error(`[WorkerEngine] Worker engine its not implemented yet...`)
process.on("message", this.handleIPCMessage)
}
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,
}
})
}
handleIPCMessage = async (msg) => {
if (typeof msg !== "object") {
// ignore, its not for us
return false
}
if (!msg.event || !msg.event.startsWith(WorkerEngine.ipcPrefix)) {
return false
}
const { event, payload } = msg
switch (event) {
case "rail:request": {
const { req } = payload
break
}
case "rail:response": {
}
}
}
use = (fn) => {
if (fn instanceof WorkerEngineRouter) {
this.router = fn
return
}
if (fn instanceof Function) {
this.perExecTail.push(fn)
return
}
}
}
export default class Engine {
constructor(params) {
this.params = params
}
app = new WorkerEngine()
router = new WorkerEngineRouter()
init = async () => {
await this.app.initialize()
}
listen = async () => {
await this.app.listen()
}
}

View File

@ -1,115 +1,7 @@
const path = require("path")
const fs = require("fs")
const net = require("corenode/net")
const experimentalFlag = path.resolve(module.path, "../../.experimental")
const packageJSON = require(path.resolve(module.path, "../../package.json"))
const moduleAlias = require("module-alias")
// set globals variables
global.LINEBRIDGE_SERVER_EXPERIMENTAL = fs.existsSync(experimentalFlag)
global.LINEBRIDGE_SERVER_VERSION = packageJSON.version
global.LOCALHOST_ADDRESS = net.ip.getHostAddress() ?? "localhost"
global.FIXED_HTTP_METHODS = {
"del": "delete"
}
global.VALID_HTTP_METHODS = [
"get",
"post",
"put",
"patch",
"del",
"delete",
"trace",
"head",
"any",
"options",
"ws"
]
global.DEFAULT_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",
}
global.DEFAULT_SERVER_PARAMS = {
urlencoded: true,
engine: "express",
http_protocol: "http",
ws_protocol: "ws",
}
global.DEFAULT_MIDDLEWARES = [
require("cors")({
"origin": "*",
"methods": DEFAULT_HEADERS["Access-Control-Allow-Methods"],
"preflightContinue": false,
"optionsSuccessStatus": 204
}),
require("morgan")(process.env.MORGAN_FORMAT ?? ":method :status :url - :response-time ms")
]
// patches
const { Buffer } = require("buffer")
global.b64Decode = (data) => {
return Buffer.from(data, "base64").toString("utf-8")
}
global.b64Encode = (data) => {
return Buffer.from(data, "utf-8").toString("base64")
}
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 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"),
})
}
module.exports = { module.exports = {
registerBaseAliases: registerBaseAliases,
Server: require("./server.js"), Server: require("./server.js"),
Controller: require("./classes/controller"), Controller: require("./classes/controller"),
Endpoint: require("./classes/endpoint"), Endpoint: require("./classes/endpoint"),
registerBaseAliases: require("./registerAliases"),
version: require("../../package.json").version, version: require("../../package.json").version,
} }

0
src/server/lib/generateController/index.js Normal file → Executable file
View File

0
src/server/lib/generateEndpointsFromDir/index.js Normal file → Executable file
View File

0
src/server/lib/internalConsole/index.js Normal file → Executable file
View File

0
src/server/lib/loadEndpointsFromDir/index.js Normal file → Executable file
View File

0
src/server/lib/redis_map/index.js Normal file → Executable file
View File

View File

@ -0,0 +1,8 @@
import cors from "cors"
export default cors({
origin: "*",
methods: ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD", "CONNECT", "TRACE"],
preflightContinue: false,
optionsSuccessStatus: 204,
})

View File

@ -0,0 +1,19 @@
export default (req, res, next) => {
const startHrTime = process.hrtime()
res.on("finish", () => {
const elapsedHrTime = process.hrtime(startHrTime)
const elapsedTimeInMs = elapsedHrTime[0] * 1000 + elapsedHrTime[1] / 1e6
res._responseTimeMs = elapsedTimeInMs
// cut req.url if is too long
if (req.url.length > 100) {
req.url = req.url.substring(0, 100) + "..."
}
console.log(`${req.method} ${res._status_code ?? res.statusCode ?? 200} ${req.url} ${elapsedTimeInMs}ms`)
})
next()
}

3
src/server/patches.js Normal file
View File

@ -0,0 +1,3 @@
import OperationError from "./classes/operation_error"
global.OperationError = OperationError

View File

@ -0,0 +1,21 @@
const moduleAlias = require("module-alias")
export default (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"),
})
}

426
src/server/server.js Normal file → Executable file
View File

@ -1,105 +1,82 @@
const fs = require("fs") import("./patches")
const path = require("path")
const rtengine = require("./classes/rtengine").default
const { EventEmitter } = require("@foxify/events") import fs from "node:fs"
import path from "node:path"
import { EventEmitter } from "@foxify/events"
const tokenizer = require("corenode/libs/tokenizer") import Endpoint from "./classes/endpoint"
const { serverManifest, internalConsole } = require("./lib")
const pkgjson = require(path.resolve(process.cwd(), "package.json")) import defaults from "./defaults"
const Engines = { import IPCClient from "./classes/IPCClient"
"hyper-express": () => {
console.warn("HyperExpress is not fully supported yet!")
const engine = require("hyper-express") async function loadEngine(engine) {
const enginesPath = path.resolve(__dirname, "engines")
return new engine.Server() const selectedEnginePath = path.resolve(enginesPath, engine)
},
"express": (params) => {
const { createServer } = require("node:http")
const express = require("express")
const socketio = require("socket.io")
const app = express() if (!fs.existsSync(selectedEnginePath)) {
const http = createServer(app) throw new Error(`Engine ${engine} not found!`)
const io = new socketio.Server(http)
const ws = new rtengine({
...params,
io: io,
http: false,
})
app.use(express.json())
app.use(express.urlencoded({ extended: true }))
return {
ws,
http,
app,
} }
},
return require(selectedEnginePath).default
} }
class Server { class Server {
eventBus = new EventEmitter()
constructor(params = {}, controllers = {}, middlewares = {}, headers = {}) { constructor(params = {}, controllers = {}, middlewares = {}, headers = {}) {
if (global.LINEBRIDGE_SERVER_EXPERIMENTAL) { this.isExperimental = defaults.isExperimental ?? false
console.warn("🚧🚧🚧 This version of Linebridge is experimental!")
if (this.isExperimental) {
console.warn("🚧 This version of Linebridge is experimental! 🚧")
} }
// register aliases
this.params = { this.params = {
...global.DEFAULT_SERVER_PARAMS, ...defaults.params,
...params, ...params.default ?? params,
} }
this.controllers = { this.controllers = {
...controllers ...controllers.default ?? controllers,
} }
this.middlewares = { this.middlewares = {
...middlewares.default ?? middlewares, ...middlewares.default ?? middlewares,
} }
this.headers = { this.headers = {
...global.DEFAULT_SERVER_HEADERS, ...defaults.headers,
...headers ...headers.default ?? headers,
} }
this.endpoints_map = {} this.valid_http_methods = defaults.valid_http_methods
// fix and fulfill params // fix and fulfill params
this.params.useMiddlewares = this.params.useMiddlewares ?? []
this.params.name = this.constructor.refName ?? this.params.refName
this.params.useEngine = this.constructor.useEngine ?? this.params.useEngine ?? "express"
this.params.listen_ip = this.params.listen_ip ?? "0.0.0.0" this.params.listen_ip = this.params.listen_ip ?? "0.0.0.0"
this.params.listen_port = this.constructor.listen_port ?? this.params.listen_port ?? 3000 this.params.listen_port = this.constructor.listen_port ?? this.params.listen_port ?? 3000
this.params.http_protocol = this.params.http_protocol ?? "http" this.params.http_protocol = this.params.http_protocol ?? "http"
this.params.http_address = `${this.params.http_protocol}://${global.LOCALHOST_ADDRESS}:${this.params.listen_port}` this.params.http_address = `${this.params.http_protocol}://${defaults.localhost_address}:${this.params.listen_port}`
this.params.routesPath = this.constructor.routesPath ?? this.params.routesPath
this.engine = null
this.InternalConsole = new internalConsole({
server_name: this.params.name
})
this.initializeManifest()
// handle silent mode
global.consoleSilent = this.params.silent
if (global.consoleSilent) {
// find morgan middleware and remove it
const morganMiddleware = global.DEFAULT_MIDDLEWARES.find(middleware => middleware.name === "logger")
if (morganMiddleware) {
global.DEFAULT_MIDDLEWARES.splice(global.DEFAULT_MIDDLEWARES.indexOf(morganMiddleware), 1)
}
}
return this return this
} }
engine = null
events = null
ipc = null
ipcEvents = null
eventBus = new EventEmitter()
initialize = async () => { initialize = async () => {
const startHrTime = process.hrtime()
// register events
if (this.events) { if (this.events) {
if (this.events.default) { if (this.events.default) {
this.events = this.events.default this.events = this.events.default
@ -111,73 +88,80 @@ class Server {
} }
// initialize engine // initialize engine
this.engine = global.engine = Engines[this.params.engine]({ this.engine = await loadEngine(this.params.useEngine)
this.engine = new this.engine({
...this.params, ...this.params,
handleAuth: this.handleWsAuth, handleAuth: this.handleHttpAuth,
requireAuth: this.constructor.requireWSAuth, requireAuth: this.constructor.requireHttpAuth,
}) })
// before initialize headers, middlewares and controllers, try to execute onInitialize hook. if (typeof this.engine.init === "function") {
await this.engine.init(this.params)
}
// create a router map
if (typeof this.engine.router.map !== "object") {
this.engine.router.map = []
}
// try to execute onInitialize hook
if (typeof this.onInitialize === "function") { if (typeof this.onInitialize === "function") {
await this.onInitialize() await this.onInitialize()
} }
// set server defined headers // set server defined headers
this.initializeHeaders() this.useDefaultHeaders()
// set server defined middlewares // set server defined middlewares
this.initializeRequiredMiddlewares() this.useDefaultMiddlewares()
// register controllers // register controllers
await this.initializeControllers() await this.initializeControllers()
// register routes
await this.initializeRoutes()
// register main index endpoint `/` // register main index endpoint `/`
await this.registerBaseEndpoints() await this.registerBaseEndpoints()
if (typeof this.engine.ws?.initialize !== "function") { // use main router
this.InternalConsole.warn("❌ WebSocket is not supported!") await this.engine.app.use(this.engine.router)
} else {
// initialize websocket init hook if needed
if (typeof this.engine.ws?.initialize == "function") {
await this.engine.ws.initialize({ await this.engine.ws.initialize({
redisInstance: this.redis redisInstance: this.redis
}) })
} }
if (typeof this.beforeInitialize === "function") { // if is a linebridge service then initialize IPC Channels
await this.beforeInitialize() if (process.env.lb_service) {
await this.initializeIpc()
} }
await this.engine.http.listen(this.params.listen_port) // try to execute beforeInitialize hook.
if (typeof this.afterInitialize === "function") {
this.InternalConsole.info(`✅ Server ready on => ${this.params.listen_ip}:${this.params.listen_port}`) await this.afterInitialize()
} }
initializeManifest = () => { // listen
// check if origin.server exists await this.engine.listen()
if (!fs.existsSync(serverManifest.filepath)) {
serverManifest.create() // 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`)
} }
// check origin.server integrity initializeIpc = async () => {
const MANIFEST_DATA = global.MANIFEST_DATA = serverManifest.get() console.info("🚄 Starting IPC client")
const MANIFEST_STAT = global.MANIFEST_STAT = serverManifest.stat()
if (typeof MANIFEST_DATA.created === "undefined") { this.ipc = global.ipc = new IPCClient(this, process)
this.InternalConsole.warn("Server generation file not contains an creation date")
serverManifest.write({ created: Date.parse(MANIFEST_STAT.birthtime) })
} }
if (typeof MANIFEST_DATA.server_token === "undefined") { useDefaultHeaders = () => {
this.InternalConsole.warn("Missing server token!")
serverManifest.create()
}
this.usid = tokenizer.generateUSID()
this.server_token = serverManifest.get("server_token")
serverManifest.write({ last_start: Date.now() })
}
initializeHeaders = () => {
this.engine.app.use((req, res, next) => { this.engine.app.use((req, res, next) => {
Object.keys(this.headers).forEach((key) => { Object.keys(this.headers).forEach((key) => {
res.setHeader(key, this.headers[key]) res.setHeader(key, this.headers[key])
@ -187,13 +171,14 @@ class Server {
}) })
} }
initializeRequiredMiddlewares = () => { useDefaultMiddlewares = async () => {
const useMiddlewares = [...this.params.useMiddlewares ?? [], ...global.DEFAULT_MIDDLEWARES] const middlewares = await this.resolveMiddlewares([
...this.params.useMiddlewares,
...defaults.useMiddlewares,
])
useMiddlewares.forEach((middleware) => { middlewares.forEach((middleware) => {
if (typeof middleware === "function") {
this.engine.app.use(middleware) this.engine.app.use(middleware)
}
}) })
} }
@ -206,7 +191,7 @@ class Server {
} }
if (controller.disabled) { if (controller.disabled) {
this.InternalConsole.warn(`⏩ Controller [${controller.name}] is disabled! Initialization skipped...`) console.warn(`⏩ Controller [${controller.name}] is disabled! Initialization skipped...`)
continue continue
} }
@ -218,62 +203,142 @@ class Server {
const WSEndpoints = ControllerInstance.__get_ws_endpoints() const WSEndpoints = ControllerInstance.__get_ws_endpoints()
HTTPEndpoints.forEach((endpoint) => { HTTPEndpoints.forEach((endpoint) => {
this.registerHTTPEndpoint(endpoint, ...this.resolveMiddlewares(controller.useMiddlewares)) this.register.http(endpoint, ...this.resolveMiddlewares(controller.useMiddlewares))
}) })
// WSEndpoints.forEach((endpoint) => { // WSEndpoints.forEach((endpoint) => {
// this.registerWSEndpoint(endpoint) // this.registerWSEndpoint(endpoint)
// }) // })
} catch (error) { } catch (error) {
if (!global.silentOutputServerErrors) { console.error(`\n\x1b[41m\x1b[37m🆘 [${controller.refName ?? controller.name}] Controller initialization failed:\x1b[0m ${error.stack} \n`)
this.InternalConsole.error(`\n\x1b[41m\x1b[37m🆘 [${controller.refName ?? controller.name}] Controller initialization failed:\x1b[0m ${error.stack} \n`)
}
} }
} }
} }
registerHTTPEndpoint = (endpoint, ...execs) => { initializeRoutes = async (filePath) => {
if (!this.params.routesPath) {
return false
}
const scanPath = filePath ?? this.params.routesPath
const files = fs.readdirSync(scanPath)
for await (const file of files) {
const filePath = `${scanPath}/${file}`
const stat = fs.statSync(filePath)
if (stat.isDirectory()) {
await this.initializeRoutes(filePath)
continue
} else if (file.endsWith(".js") || file.endsWith(".jsx") || file.endsWith(".ts") || file.endsWith(".tsx")) {
let splitedFilePath = filePath.split("/")
splitedFilePath = splitedFilePath.slice(splitedFilePath.indexOf("routes") + 1)
const method = splitedFilePath[splitedFilePath.length - 1].split(".")[0].toLocaleLowerCase()
splitedFilePath = splitedFilePath.slice(0, splitedFilePath.length - 1)
// parse parametrized routes
const parametersRegex = /\[([a-zA-Z0-9_]+)\]/g
splitedFilePath = splitedFilePath.map((route) => {
if (route.match(parametersRegex)) {
route = route.replace(parametersRegex, ":$1")
}
route = route.replace("[$]", "*")
return route
})
let route = splitedFilePath.join("/")
route = route.replace(".jsx", "")
route = route.replace(".js", "")
route = route.replace(".ts", "")
route = route.replace(".tsx", "")
if (route.endsWith("/index")) {
route = route.replace("/index", "")
}
route = `/${route}`
// import route
let routeFile = require(filePath)
routeFile = routeFile.default ?? routeFile
if (typeof routeFile !== "function") {
if (!routeFile.fn) {
console.warn(`Missing fn handler in [${method}][${route}]`)
continue
}
if (Array.isArray(routeFile.useContext)) {
let contexts = {}
for (const context of routeFile.useContext) {
contexts[context] = this.contexts[context]
}
routeFile.contexts = contexts
routeFile.fn.bind({ contexts })
}
}
new Endpoint(
this,
{
route: route,
enabled: true,
middlewares: routeFile.middlewares,
handlers: {
[method]: routeFile.fn ?? routeFile,
}
}
)
continue
}
}
}
register = {
http: (endpoint, ..._middlewares) => {
// check and fix method // check and fix method
endpoint.method = endpoint.method?.toLowerCase() ?? "get" endpoint.method = endpoint.method?.toLowerCase() ?? "get"
if (global.FIXED_HTTP_METHODS[endpoint.method]) { if (defaults.fixed_http_methods[endpoint.method]) {
endpoint.method = global.FIXED_HTTP_METHODS[endpoint.method] endpoint.method = defaults.fixed_http_methods[endpoint.method]
} }
// check if method is supported // check if method is supported
if (typeof this.engine.app[endpoint.method] !== "function") { if (typeof this.engine.router[endpoint.method] !== "function") {
throw new Error(`Method [${endpoint.method}] is not supported!`) throw new Error(`Method [${endpoint.method}] is not supported!`)
} }
// grab the middlewares // grab the middlewares
let middlewares = [...execs] let middlewares = [..._middlewares]
if (endpoint.middlewares) { if (endpoint.middlewares) {
middlewares = [...middlewares, ...this.resolveMiddlewares(endpoint.middlewares)] middlewares = [...middlewares, ...this.resolveMiddlewares(endpoint.middlewares)]
} }
// make sure method has root object on endpointsMap this.engine.router.map[endpoint.route] = {
if (typeof this.endpoints_map[endpoint.method] !== "object") { method: endpoint.method,
this.endpoints_map[endpoint.method] = {} path: endpoint.route,
} }
// create model for http interface router
const routeModel = [endpoint.route, ...middlewares, this.createHTTPRequestHandler(endpoint)]
// register endpoint to http interface router // register endpoint to http interface router
this.engine.app[endpoint.method](...routeModel) this.engine.router[endpoint.method](endpoint.route, ...middlewares, endpoint.fn)
// extend to map
this.endpoints_map[endpoint.method] = {
...this.endpoints_map[endpoint.method],
[endpoint.route]: {
route: endpoint.route,
enabled: endpoint.enabled ?? true,
}, },
} ws: (endpoint, ...execs) => {
}
registerWSEndpoint = (endpoint, ...execs) => {
endpoint.nsp = endpoint.nsp ?? "/main" endpoint.nsp = endpoint.nsp ?? "/main"
this.websocket_instance.eventsChannels.push([endpoint.nsp, endpoint.on, endpoint.dispatch]) this.websocket_instance.eventsChannels.push([endpoint.nsp, endpoint.on, endpoint.dispatch])
@ -282,51 +347,46 @@ class Server {
nsp: endpoint.nsp, nsp: endpoint.nsp,
channel: endpoint.on, channel: endpoint.on,
} }
},
} }
registerBaseEndpoints() { async registerBaseEndpoints() {
if (this.params.disableBaseEndpoint) { if (this.params.disableBaseEndpoint) {
this.InternalConsole.warn("‼️ [disableBaseEndpoint] Base endpoint is disabled! Endpoints mapping will not be available, so linebridge client bridges will not work! ‼️") console.warn("‼️ [disableBaseEndpoint] Base endpoint is disabled! Endpoints mapping will not be available, so linebridge client bridges will not work! ‼️")
return false return false
} }
this.registerHTTPEndpoint({ const scanPath = path.join(__dirname, "baseEndpoints")
method: "get", const files = fs.readdirSync(scanPath)
route: "/",
fn: (req, res) => {
return res.json({
LINEBRIDGE_SERVER_VERSION: LINEBRIDGE_SERVER_VERSION,
version: pkgjson.version ?? "unknown",
usid: this.usid,
requestTime: new Date().getTime(),
})
}
})
this.registerHTTPEndpoint({ for await (const file of files) {
method: "GET", if (file === "index.js") {
route: "/__http_map", continue
fn: (req, res) => { }
return res.json({
endpointsMap: this.endpoints_map, let endpoint = require(path.join(scanPath, file)).default
})
} new endpoint(this)
}) }
} }
//* resolvers
resolveMiddlewares = (requestedMiddlewares) => { resolveMiddlewares = (requestedMiddlewares) => {
const middlewares = {
...this.middlewares,
...defaults.middlewares,
}
requestedMiddlewares = Array.isArray(requestedMiddlewares) ? requestedMiddlewares : [requestedMiddlewares] requestedMiddlewares = Array.isArray(requestedMiddlewares) ? requestedMiddlewares : [requestedMiddlewares]
const execs = [] const execs = []
requestedMiddlewares.forEach((middlewareKey) => { requestedMiddlewares.forEach((middlewareKey) => {
if (typeof middlewareKey === "string") { if (typeof middlewareKey === "string") {
if (typeof this.middlewares[middlewareKey] !== "function") { if (typeof middlewares[middlewareKey] !== "function") {
throw new Error(`Middleware ${middlewareKey} not found!`) throw new Error(`Middleware ${middlewareKey} not found!`)
} }
execs.push(this.middlewares[middlewareKey]) execs.push(middlewares[middlewareKey])
} }
if (typeof middlewareKey === "function") { if (typeof middlewareKey === "function") {
@ -337,47 +397,7 @@ class Server {
return execs return execs
} }
cleanupProcess = () => { // Utilities
this.InternalConsole.log("🛑 Stopping server...")
if (typeof this.engine.app.close === "function") {
this.engine.app.close()
}
this.engine.io.close()
}
// handlers
createHTTPRequestHandler = (endpoint) => {
return async (req, res) => {
try {
// check if endpoint is disabled
if (!this.endpoints_map[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) {
this.InternalConsole.error({
message: "Unhandled route error:",
description: error.stack,
ref: [endpoint.method, endpoint.route].join("|"),
})
}
return res.status(500).json({
"error": error.message
})
}
}
}
}
toogleEndpointReachability = (method, route, enabled) => { toogleEndpointReachability = (method, route, enabled) => {
if (typeof this.endpoints_map[method] !== "object") { if (typeof this.endpoints_map[method] !== "object") {
throw new Error(`Cannot toogle endpoint, method [${method}] not set!`) throw new Error(`Cannot toogle endpoint, method [${method}] not set!`)

0
src/utils/index.js Normal file → Executable file
View File

0
src/utils/linebridge_ascii.js Normal file → Executable file
View File

0
src/utils/nanoid/index.js Normal file → Executable file
View File

0
src/utils/schematized/index.js Normal file → Executable file
View File

View File

@ -1,30 +0,0 @@
const assert = require("assert")
const linebridgeClient = require("../dist/client/index.js")
const exampleTestHTTPPort = 3010
let exampleBridge = null
describe("[Client]", async function () {
it("exports fine", function () {
assert.equal(typeof linebridgeClient, "object")
console.log(linebridgeClient)
})
it("create test bridge", async () => {
exampleBridge = new linebridgeClient.Bridge({
origin: `http://0.0.0.0:${exampleTestHTTPPort}`,
})
await exampleBridge.initialize()
})
it("bridge.endpoints is an object", async () => {
assert.equal(typeof exampleBridge.endpoints, "object")
})
it("test endpoint should correctly respond", async () => {
let response = await exampleBridge.endpoints.get.test()
assert.equal(response.test, "testing")
})
})

View File

@ -1,60 +0,0 @@
const assert = require("assert")
const linebridgeServer = require("../dist/server/index.js")
const exampleTestHTTPPort = 3010
const testEndpoints = [
{
route: "/test",
method: "GET",
fn: function (req, res) {
return res.send({ test: "testing" })
}
},
{
route: "/disabledByDefault",
method: "GET",
fn: function (req, res) {
return res.send("This must not be sended")
},
enabled: false,
},
{
route: "/shouldBeDisabled",
method: "GET",
fn: function (req, res) {
return res.send("This must not be sended after use `toogleEndpointReachability`")
}
}
]
let exampleServer = null
describe("[Server]", async function () {
it("should export", function () {
assert.equal(typeof linebridgeServer, "function")
})
it("create server", async function () {
exampleServer = new linebridgeServer({
port: exampleTestHTTPPort,
})
})
it("register test controllers", async function () {
testEndpoints.forEach((endpoint) => {
exampleServer.registerHTTPEndpoint(endpoint)
})
})
it("initialize server", async function () {
await exampleServer.initialize()
})
it("toogleEndpointReachability", async function () {
exampleServer.toogleEndpointReachability("get", "/shouldBeDisabled", false)
// check if endpoint is disabled
assert.equal(exampleServer.endpointsMap["get"]["/shouldBeDisabled"].enabled, false)
})
})

View File

@ -1,32 +0,0 @@
import { Server, Controller } from "../src/server"
const TestControllers = [
class AController extends Controller {
get = {
"/test": (req, res) => {
return res.send("Hello World!")
}
}
},
class BController extends Controller {
static useRoute = "/bcontroller"
get = {
"/test": (req, res) => {
return res.send("Hello World!")
}
}
}
]
async function _main() {
const server = new Server({
httpEngine: "express"
}, TestControllers)
await server.initialize()
}
_main().catch((error) => {
console.error(`[MAIN_ERROR] ${error}`)
})

View File

@ -1,111 +0,0 @@
import { Bridge } from "../src/client"
import { Server, Controller } from "../src/server"
const Middlewares = {
"test": (req, res, next) => {
console.log("test middleware, it should run on every endpoint of a controller")
return next()
},
"test2": (req, res, next) => {
console.log("test2 middleware, it should run on request of a endpoint")
return next()
}
}
const Controllers = [
class DisabledController extends Controller {
static disabled = true
get = {
"/unreachable": (req, res) => {
return res.send("this must not be reachable")
}
}
},
class TestController extends Controller {
static useMiddlewares = ["test"]
channels = {
"epicEvent": (socket, ...args) => {
console.log(`[SERVER WS EVENT] > ${socket.id} > `, ...args)
return socket.res("elo")
}
}
get = {
"/test/:name": {
fn: (req, res) => {
const name = req.params.name
return res.json({
message: name ? `Hello ${name}!` : "Hello World!"
})
},
},
"/crashTest": (req, res) => {
throw new Error("Boom!")
},
"/test": (req, res) => {
return res.send("Hello World!")
}
}
delete = {
"/test": (req, res) => {
return res.send(`Deleting ${req.body.a}`)
}
}
},
]
async function _main() {
const server = new Server({
onWSClientConnection: (socket) => {
const authToken = socket.handshake.auth?.token
console.log(`AUTH TOKEN: ${authToken}`)
if (!authToken) {
socket.emit("unauthorized", "No auth token provided!")
return socket.disconnect()
}
if (authToken !== "123") {
socket.emit("unauthorized", "invalid auth token!")
return socket.disconnect()
}
}
}, Controllers, Middlewares)
const clientBridge = new Bridge({
origin: server.HTTPAddress,
wsOrigin: server.WSAddress,
wsMainSocketOptions: {
auth: {
token: "123"
}
},
}, {
onUnauthorized: (reason) => {
console.log(reason)
}
})
await server.initialize()
await clientBridge.initialize()
const test = await clientBridge.endpoints.get.test()
const crashTest = await clientBridge.endpoints.get.crashtest().catch(error => {
console.log(error)
return "Crash test passed!"
})
const wsEpicEvent = await clientBridge.wsEndpoints.epicEvent("Hello", "World")
console.log(`[get.test] > ${test}`)
console.log(`[get.crashtest] > ${crashTest}`)
console.log(`[ws.epicEvent] > ${wsEpicEvent}`)
}
_main().catch((error) => {
console.error(`[MAIN_ERROR] ${error}`)
})