From b7fa51625286bda0859b86751b52e673d4f00161 Mon Sep 17 00:00:00 2001 From: Rory& Date: Wed, 29 Jul 2026 00:15:57 +0200 Subject: [PATCH] Prepare to use Express --- src/bundle/Server.ts | 2 +- src/gateway/Server.ts | 148 +++--------------- .../_spacebar/gateway/admin/introspect.ts | 133 ++++++++++++++++ 3 files changed, 153 insertions(+), 130 deletions(-) create mode 100644 src/gateway/routes/_spacebar/gateway/admin/introspect.ts diff --git a/src/bundle/Server.ts b/src/bundle/Server.ts index 9cc99369b..d76866926 100644 --- a/src/bundle/Server.ts +++ b/src/bundle/Server.ts @@ -40,7 +40,7 @@ server.on("request", app); const api = new SpacebarServer({ server, port, production, app }); const cdn = new CDNServer({ server, port, production, app }); -const gateway = new GatewayServer({ server, port, production }); +const gateway = new GatewayServer({ server, port, production, app }); const webrtc = new WebrtcServer({ server: undefined, port: wrtcWsPort, diff --git a/src/gateway/Server.ts b/src/gateway/Server.ts index ff1fe7ab3..cb2dd0d1c 100644 --- a/src/gateway/Server.ts +++ b/src/gateway/Server.ts @@ -17,150 +17,40 @@ */ import http from "node:http"; -import { setInterval } from "node:timers"; import ws from "ws"; +import { Server, ServerOptions } from "lambert-server"; import { initDatabase } from "@spacebar/database"; import { Random } from "@spacebar/extensions"; -import { checkToken, Config, initEvent, JwtKeypairManager, Rights } from "@spacebar/util"; +import { Config, initEvent, JwtKeypairManager } from "@spacebar/util"; import { ProcessLifecycle, SystemdLifecycle } from "../util/util/ProcessLifecycle"; import { Monitoring } from "../util/monitoring/Monitoring"; -import { Connection, openConnections } from "./events/Connection"; +import { Connection } from "./events/Connection"; import { cleanupOnStartup } from "./util"; -export class GatewayServer { +export class GatewayServer extends Server { public ws: ws.Server; public port: number; public server: http.Server; public production: boolean; private monitoringLoop: NodeJS.Timeout; - constructor({ port, server, production }: { port: number; server?: http.Server; production?: boolean }) { - this.port = port; - this.production = production || false; + constructor(options?: Partial) { + super(options); - if (server) this.server = server; - else { - const elu = [1, 5, 15].map(() => performance.eventLoopUtilization()); - const eluP = [1, 5, 15].map(() => performance.eventLoopUtilization()); - const cpu = [1, 5, 15].map(() => process.cpuUsage()); - let sec = 0; - const monitoringLoop = setInterval(() => { - sec += 1; - // for some reason this behaves differently from cpuUsage, so we need an absolute reference as "previous" - const eluC = performance.eventLoopUtilization(); + this.server = http.createServer(async (req, res) => { + if (!req.headers.cookie?.split("; ").find((x) => x.startsWith("__sb_sessid="))) { + res.setHeader( + "Set-Cookie", + `__sb_sessid=${Random.getString("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", 32)}; Secure; HttpOnly; SameSite=None; Path=/`, + ); + } + const requestUrl = new URL(`http://${req.headers.host}${req.url}`); + if (requestUrl.pathname === "/metrics") { + return await Monitoring.handleRawRequest(req, res); + } - cpu[0] = process.cpuUsage(cpu[0]); - elu[0] = performance.eventLoopUtilization(eluP[0]); - eluP[0] = eluC; - if (sec % 5 === 0) { - cpu[1] = process.cpuUsage(cpu[1]); - elu[1] = performance.eventLoopUtilization(eluP[1]); - eluP[1] = eluC; - } - if (sec % 15 === 0) { - cpu[2] = process.cpuUsage(cpu[2]); - elu[2] = performance.eventLoopUtilization(eluP[2]); - eluP[2] = eluC; - } - }, 1000); - - this.server = http.createServer(); - this.server = http.createServer(async (req, res) => { - if (!req.headers.cookie?.split("; ").find((x) => x.startsWith("__sb_sessid="))) { - res.setHeader( - "Set-Cookie", - `__sb_sessid=${Random.getString("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", 32)}; Secure; HttpOnly; SameSite=None; Path=/`, - ); - } - const requestUrl = new URL(`http://${req.headers.host}${req.url}`); - if (requestUrl.pathname === "/metrics") { - return await Monitoring.handleRawRequest(req, res); - } else if (requestUrl.pathname === "/_spacebar/gateway/admin/introspect") { - if (!req.headers.authorization) { - return res.writeHead(401).end("Unauthorized"); - } else { - const auth = req.headers.authorization.split(" "); - const sess = await checkToken(auth[1]); - if ((BigInt(sess.user.rights) & BigInt(Rights.FLAGS.OPERATOR)) === BigInt(0)) { - return res.writeHead(401).end("Unauthorized"); - } - } - const useFullWsObj = requestUrl.searchParams.get("fullWs") === "true"; - res.setHeader("Content-Type", "application/json") - .writeHead(200) - .end( - JSON.stringify( - { - uptime: process.uptime(), - resourceUsage: process.resourceUsage(), - eventLoop: elu, - cpu: cpu.map((x) => ({ - user: x.user / 1000, - system: x.system / 1000, - })), - socketStates: { - open: openConnections.length, - sessions: openConnections.map((x) => - // console.log(x); - useFullWsObj - ? { - ...x, - ...{ - _events: undefined, - _closeTimer: undefined, - accessToken: x.accessToken?.split(".")[0] + "." + x.accessToken?.split(".")[1] + ".***", - }, - } - : { - wsReadystate: x.readyState, - version: x.version, - user_id: x.user_id, - session_id: x.session_id, - accessToken: x.accessToken?.split(".")[0] + "." + x.accessToken?.split(".")[1] + +".***", - encoding: x.encoding, - compress: x.compress, - ipAddress: x.ipAddress, - userAgent: x.userAgent, - fingerprint: x.fingerprint, - shard_count: x.shard_count, - shard_id: x.shard_id, - deflate: x.deflate != null, - inflate: x.inflate != null, - zstdEncoder: x.zstdEncoder != null, - zstdDecoder: x.zstdDecoder != null, - heartbeatTimeout: x.heartbeatTimeout, - readyTimeout: x.readyTimeout, - intents: x.intents, - sequence: x.sequence, - permissions: x.permissions, - events: x.events, - member_events: x.member_events, - listen_options: x.listen_options, - capabilities: x.capabilities, - large_threshold: x.large_threshold, - qos: x.qos, - session: x.session, - }, - ), - }, - }, - (key, value) => { - if (value === null || value === undefined) return value; - if (Object.getPrototypeOf(value)?.constructor?.name === "Timeout") return `[Timeout] ${value._idleTimeout}ms, repeat: ${value._repeat}`; - if (Object.getPrototypeOf(value)?.constructor?.name === "BigInt") return value.toString() + "n"; - return value; - }, - 2, - ), - ); - return; - } - - res.writeHead(200).end("Online"); - }); - - ProcessLifecycle.eventEmitter.on("stopping", () => clearTimeout(monitoringLoop)); - } + res.writeHead(200).end("Online"); + }); this.server.on("upgrade", (request, socket, head) => { this.ws.handleUpgrade(request, socket, head, (socket) => { diff --git a/src/gateway/routes/_spacebar/gateway/admin/introspect.ts b/src/gateway/routes/_spacebar/gateway/admin/introspect.ts new file mode 100644 index 000000000..f6ab1e7b6 --- /dev/null +++ b/src/gateway/routes/_spacebar/gateway/admin/introspect.ts @@ -0,0 +1,133 @@ +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2026 Spacebar and Spacebar Contributors + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +import { route } from "@spacebar/api/middlewares"; +import { Request, Response, Router } from "express"; +import { ProcessLifecycle } from "@spacebar/util/util/ProcessLifecycle"; +import { openConnections } from "@spacebar/gateway/events/Connection"; + +const router: Router = Router({ mergeParams: true }); + +const elu = [1, 5, 15].map(() => performance.eventLoopUtilization()); +const eluP = [1, 5, 15].map(() => performance.eventLoopUtilization()); +const cpu = [1, 5, 15].map(() => process.cpuUsage()); +let sec = 0; +const monitoringLoop = setInterval(() => { + sec += 1; + // for some reason this behaves differently from cpuUsage, so we need an absolute reference as "previous" + const eluC = performance.eventLoopUtilization(); + + cpu[0] = process.cpuUsage(cpu[0]); + elu[0] = performance.eventLoopUtilization(eluP[0]); + eluP[0] = eluC; + if (sec % 5 === 0) { + cpu[1] = process.cpuUsage(cpu[1]); + elu[1] = performance.eventLoopUtilization(eluP[1]); + eluP[1] = eluC; + } + if (sec % 15 === 0) { + cpu[2] = process.cpuUsage(cpu[2]); + elu[2] = performance.eventLoopUtilization(eluP[2]); + eluP[2] = eluC; + } +}, 1000); + +ProcessLifecycle.eventEmitter.on("stopping", () => clearTimeout(monitoringLoop)); + +router.get( + "/", + route({ + right: "OPERATOR", + responses: { + 200: {}, + 403: { + body: "APIErrorResponse", + }, + }, + }), + (req: Request, res: Response) => { + const useFullWsObj = req.params.fullWs == "true"; + res.set("Content-Type", "application/json").send( + JSON.stringify( + { + uptime: process.uptime(), + resourceUsage: process.resourceUsage(), + eventLoop: elu, + cpu: cpu.map((x) => ({ + user: x.user / 1000, + system: x.system / 1000, + })), + socketStates: { + open: openConnections.length, + sessions: openConnections.map((x) => + // console.log(x); + useFullWsObj + ? { + ...x, + ...{ + _events: undefined, + _closeTimer: undefined, + accessToken: x.accessToken?.split(".")[0] + "." + x.accessToken?.split(".")[1] + ".***", + }, + } + : { + wsReadystate: x.readyState, + version: x.version, + user_id: x.user_id, + session_id: x.session_id, + accessToken: x.accessToken?.split(".")[0] + "." + x.accessToken?.split(".")[1] + +".***", + encoding: x.encoding, + compress: x.compress, + ipAddress: x.ipAddress, + userAgent: x.userAgent, + fingerprint: x.fingerprint, + shard_count: x.shard_count, + shard_id: x.shard_id, + deflate: x.deflate != null, + inflate: x.inflate != null, + zstdEncoder: x.zstdEncoder != null, + zstdDecoder: x.zstdDecoder != null, + heartbeatTimeout: x.heartbeatTimeout, + readyTimeout: x.readyTimeout, + intents: x.intents, + sequence: x.sequence, + permissions: x.permissions, + events: x.events, + member_events: x.member_events, + listen_options: x.listen_options, + capabilities: x.capabilities, + large_threshold: x.large_threshold, + qos: x.qos, + session: x.session, + }, + ), + }, + }, + (key, value) => { + if (value === null || value === undefined) return value; + if (Object.getPrototypeOf(value)?.constructor?.name === "Timeout") return `[Timeout] ${value._idleTimeout}ms, repeat: ${value._repeat}`; + if (Object.getPrototypeOf(value)?.constructor?.name === "BigInt") return value.toString() + "n"; + return value; + }, + 2, + ), + ); + }, +); + +export default router;