mirror of
https://github.com/ragestudio/linebridge.git
synced 2025-06-09 10:34:17 +00:00
433 lines
14 KiB
JavaScript
433 lines
14 KiB
JavaScript
const fs = require("fs")
|
|
const path = require("path")
|
|
const http = require("http")
|
|
const https = require("https")
|
|
const io = require("socket.io")
|
|
|
|
const pkgjson = require(path.resolve(process.cwd(), "package.json"))
|
|
|
|
const tokenizer = require("corenode/libs/tokenizer")
|
|
const { serverManifest, outputServerError, internalConsole } = require("./lib")
|
|
|
|
const HTTPProtocolsInstances = {
|
|
http: http,
|
|
https: https,
|
|
}
|
|
|
|
const HTTPEngines = {
|
|
"hyper-express": () => {
|
|
global.InternalConsole.warn("Hyper-Express is not fully supported yet")
|
|
|
|
const engine = require("hyper-express")
|
|
return new engine.Server()
|
|
},
|
|
"express": () => {
|
|
return require("express")()
|
|
},
|
|
}
|
|
|
|
const linebridge_ascii = require("./linebridge_ascii.js")
|
|
|
|
class Server {
|
|
constructor(params = {}, controllers = {}, middlewares = {}, headers = {}) {
|
|
// register aliases
|
|
this.params = {
|
|
minimal: false,
|
|
no_brand: false,
|
|
...global.DEFAULT_SERVER_PARAMS,
|
|
...params,
|
|
}
|
|
|
|
this.controllers = {
|
|
...controllers
|
|
}
|
|
this.middlewares = {
|
|
...middlewares.default ?? middlewares,
|
|
}
|
|
this.headers = {
|
|
...global.DEFAULT_SERVER_HEADERS,
|
|
...headers
|
|
}
|
|
|
|
this.endpoints_map = {}
|
|
|
|
// fix and fulfill params
|
|
this.params.listen_ip = this.params.listen_ip ?? "0.0.0.0"
|
|
this.params.listen_port = this.params.listen_port ?? 3000
|
|
this.params.http_protocol = this.params.http_protocol ?? "http"
|
|
this.params.ws_protocol = this.params.ws_protocol ?? "ws"
|
|
|
|
this.params.http_address = `${this.params.http_protocol}://${global.LOCALHOST_ADDRESS}:${this.params.listen_port}`
|
|
this.params.ws_address = `${this.params.ws_protocol}://${global.LOCALHOST_ADDRESS}:${this.params.listen_port}`
|
|
|
|
// check if engine is supported
|
|
if (typeof HTTPProtocolsInstances[this.params.http_protocol]?.createServer !== "function") {
|
|
throw new Error("Invalid HTTP protocol (Missing createServer function)")
|
|
}
|
|
|
|
// create instances the 3 main instances of the server (Engine, HTTP, WebSocket)
|
|
this.engine_instance = global.engine_instance = HTTPEngines[this.params.engine]()
|
|
|
|
this.http_instance = global.http_instance = HTTPProtocolsInstances[this.params.http_protocol].createServer({
|
|
...this.params.httpOptions ?? {},
|
|
}, this.engine_instance)
|
|
|
|
this.websocket_instance = global.websocket_instance = {
|
|
io: new io.Server(this.http_instance),
|
|
map: {},
|
|
eventsChannels: [],
|
|
}
|
|
|
|
this.internalConsole = global.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)
|
|
}
|
|
}
|
|
|
|
// handle exit events
|
|
process.on("SIGTERM", this.cleanupProcess)
|
|
process.on("SIGINT", this.cleanupProcess)
|
|
|
|
return this
|
|
}
|
|
|
|
initializeManifest = () => {
|
|
// 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") {
|
|
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") {
|
|
InternalConsole.warn("Missing server token!")
|
|
serverManifest.create()
|
|
}
|
|
|
|
this.usid = tokenizer.generateUSID()
|
|
this.server_token = serverManifest.get("server_token")
|
|
|
|
serverManifest.write({ last_start: Date.now() })
|
|
}
|
|
|
|
initialize = async () => {
|
|
if (!this.params.no_brand) {
|
|
if (!this.params.minimal) {
|
|
InternalConsole.log(linebridge_ascii)
|
|
}
|
|
}
|
|
|
|
if (!this.params.minimal) {
|
|
InternalConsole.info(`🚀 Starting server...`)
|
|
}
|
|
|
|
//* set server defined headers
|
|
this.initializeHeaders()
|
|
|
|
//* set server defined middlewares
|
|
this.initializeRequiredMiddlewares()
|
|
|
|
//* register controllers
|
|
await this.initializeControllers()
|
|
|
|
//* register main index endpoint `/`
|
|
await this.registerBaseEndpoints()
|
|
|
|
// initialize main socket
|
|
this.websocket_instance.io.on("connection", this.handleWSClientConnection)
|
|
|
|
// initialize http server
|
|
await this.http_instance.listen(this.params.listen_port, this.params.listen_ip ?? "0.0.0.0", () => {
|
|
InternalConsole.info(`✅ Server ready on => ${this.params.listen_ip}:${this.params.listen_port}`)
|
|
|
|
if (!this.params.minimal) {
|
|
this.outputServerInfo()
|
|
}
|
|
})
|
|
}
|
|
|
|
initializeHeaders = () => {
|
|
this.engine_instance.use((req, res, next) => {
|
|
Object.keys(this.headers).forEach((key) => {
|
|
res.setHeader(key, this.headers[key])
|
|
})
|
|
|
|
next()
|
|
})
|
|
}
|
|
|
|
initializeRequiredMiddlewares = () => {
|
|
const useMiddlewares = [...this.params.useMiddlewares ?? [], ...global.DEFAULT_MIDDLEWARES]
|
|
|
|
useMiddlewares.forEach((middleware) => {
|
|
if (typeof middleware === "function") {
|
|
this.engine_instance.use(middleware)
|
|
}
|
|
})
|
|
}
|
|
|
|
initializeControllers = async () => {
|
|
const controllers = Object.entries(this.controllers)
|
|
|
|
for await (let [key, controller] of controllers) {
|
|
if (typeof controller !== "function") {
|
|
throw new Error(`Controller must use the controller class!`)
|
|
}
|
|
|
|
if (controller.disabled) {
|
|
InternalConsole.warn(`⏩ Controller [${controller.name}] is disabled! Initialization skipped...`)
|
|
continue
|
|
}
|
|
|
|
try {
|
|
const ControllerInstance = new controller()
|
|
|
|
// get endpoints from controller (ComplexController)
|
|
const HTTPEndpoints = ControllerInstance.__get_http_endpoints()
|
|
const WSEndpoints = ControllerInstance.__get_ws_endpoints()
|
|
|
|
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.engine_instance[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.endpoints_map[endpoint.method] !== "object") {
|
|
this.endpoints_map[endpoint.method] = {}
|
|
}
|
|
|
|
// create model for http interface router
|
|
const routeModel = [endpoint.route, ...middlewares, this.createHTTPRequestHandler(endpoint)]
|
|
|
|
// register endpoint to http interface router
|
|
this.engine_instance[endpoint.method](...routeModel)
|
|
|
|
// extend to map
|
|
this.endpoints_map[endpoint.method] = {
|
|
...this.endpoints_map[endpoint.method],
|
|
[endpoint.route]: {
|
|
route: endpoint.route,
|
|
enabled: endpoint.enabled ?? true,
|
|
},
|
|
}
|
|
}
|
|
|
|
registerWSEndpoint = (endpoint, ...execs) => {
|
|
endpoint.nsp = endpoint.nsp ?? "/main"
|
|
|
|
this.websocket_instance.eventsChannels.push([endpoint.nsp, endpoint.on, endpoint.dispatch])
|
|
|
|
this.websocket_instance.map[endpoint.on] = {
|
|
nsp: endpoint.nsp,
|
|
channel: endpoint.on,
|
|
}
|
|
}
|
|
|
|
registerBaseEndpoints() {
|
|
if (this.params.disableBaseEndpoint) {
|
|
InternalConsole.warn("‼️ [disableBaseEndpoint] Base endpoint is disabled! Endpoints mapping will not be available, so linebridge client bridges will not work! ‼️")
|
|
return false
|
|
}
|
|
|
|
//* 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,
|
|
version: pkgjson.version ?? "unknown",
|
|
usid: this.usid,
|
|
requestTime: new Date().getTime(),
|
|
endpointsMap: this.endpoints_map,
|
|
wsEndpointsMap: this.websocket_instance.map,
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
//* resolvers
|
|
resolveMiddlewares = (requestedMiddlewares) => {
|
|
requestedMiddlewares = Array.isArray(requestedMiddlewares) ? requestedMiddlewares : [requestedMiddlewares]
|
|
|
|
const execs = []
|
|
|
|
requestedMiddlewares.forEach((middlewareKey) => {
|
|
if (typeof middlewareKey === "string") {
|
|
if (typeof this.middlewares[middlewareKey] !== "function") {
|
|
throw new Error(`Middleware ${middlewareKey} not found!`)
|
|
}
|
|
|
|
execs.push(this.middlewares[middlewareKey])
|
|
}
|
|
|
|
if (typeof middlewareKey === "function") {
|
|
execs.push(middlewareKey)
|
|
}
|
|
})
|
|
|
|
return execs
|
|
}
|
|
|
|
|
|
cleanupProcess = () => {
|
|
InternalConsole.log("🛑 Stopping server...")
|
|
|
|
if (typeof this.engine_instance.close === "function") {
|
|
this.engine_instance.close()
|
|
}
|
|
|
|
this.websocket_instance.io.close()
|
|
|
|
process.exit(1)
|
|
}
|
|
|
|
// 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) {
|
|
outputServerError({
|
|
message: "Unhandled route error:",
|
|
description: error.stack,
|
|
ref: [endpoint.method, endpoint.route].join("|"),
|
|
})
|
|
}
|
|
|
|
return res.status(500).json({
|
|
"error": error.message
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
handleWSClientConnection = async (client) => {
|
|
client.res = (...args) => {
|
|
client.emit("response", ...args)
|
|
}
|
|
client.err = (...args) => {
|
|
client.emit("responseError", ...args)
|
|
}
|
|
|
|
if (typeof this.params.onWSClientConnection === "function") {
|
|
await this.params.onWSClientConnection(client)
|
|
}
|
|
|
|
for await (const [nsp, on, dispatch] of this.websocket_instance.eventsChannels) {
|
|
client.on(on, async (...args) => {
|
|
try {
|
|
await dispatch(client, ...args).catch((error) => {
|
|
client.err({
|
|
message: error.message,
|
|
})
|
|
})
|
|
} catch (error) {
|
|
client.err({
|
|
message: error.message,
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
client.on("ping", () => {
|
|
client.emit("pong")
|
|
})
|
|
|
|
client.on("disconnect", async () => {
|
|
if (typeof this.params.onWSClientDisconnect === "function") {
|
|
await this.params.onWSClientDisconnect(client)
|
|
}
|
|
})
|
|
}
|
|
|
|
// public methods
|
|
outputServerInfo = () => {
|
|
InternalConsole.table({
|
|
"linebridge_version": LINEBRIDGE_SERVER_VERSION,
|
|
"engine": this.params.engine,
|
|
"http_address": this.params.http_address,
|
|
"websocket_address": this.params.ws_address,
|
|
"listen_port": this.params.listen_port,
|
|
})
|
|
}
|
|
|
|
toogleEndpointReachability = (method, route, enabled) => {
|
|
if (typeof this.endpoints_map[method] !== "object") {
|
|
throw new Error(`Cannot toogle endpoint, method [${method}] not set!`)
|
|
}
|
|
|
|
if (typeof this.endpoints_map[method][route] !== "object") {
|
|
throw new Error(`Cannot toogle endpoint [${route}], is not registered!`)
|
|
}
|
|
|
|
this.endpoints_map[method][route].enabled = enabled ?? !this.endpoints_map[method][route].enabled
|
|
}
|
|
}
|
|
|
|
module.exports = Server |