summary refs log tree commit diff
diff options
context:
space:
mode:
authorRory& <root@rory.gay>2026-02-08 03:56:52 +0100
committerRory& <root@rory.gay>2026-02-08 03:56:52 +0100
commitd58574ee98a25f8e42de6cf9ead7f62634ce6cf7 (patch)
tree67a5585b27cc6cb509cc8bc87bf23da8c3f98744
parentSave channel on message create (diff)
downloadserver-ts-d58574ee98a25f8e42de6cf9ead7f62634ce6cf7.tar.xz
gateway: introspect endpoint
-rw-r--r--src/gateway/Server.ts112
-rw-r--r--src/gateway/events/Connection.ts8
2 files changed, 117 insertions, 3 deletions
diff --git a/src/gateway/Server.ts b/src/gateway/Server.ts

index 786d1733..7fea1a27 100644 --- a/src/gateway/Server.ts +++ b/src/gateway/Server.ts
@@ -18,12 +18,13 @@ import dotenv from "dotenv"; dotenv.config({ quiet: true }); -import { closeDatabase, Config, initDatabase, initEvent } from "@spacebar/util"; +import { checkToken, closeDatabase, Config, initDatabase, initEvent, Rights } from "@spacebar/util"; import ws from "ws"; -import { Connection } from "./events/Connection"; +import { Connection, openConnections } from "./events/Connection"; import http from "http"; import { cleanupOnStartup } from "./util/Utils"; import { randomString } from "@spacebar/api"; +import { setInterval } from "timers"; export class Server { public ws: ws.Server; @@ -37,10 +38,115 @@ export class Server { if (server) this.server = server; else { - this.server = http.createServer(function (req, res) { + const elu = [1, 5, 15].map((x) => performance.eventLoopUtilization()); + const eluP = [1, 5, 15].map((x) => performance.eventLoopUtilization()); + const cpu = [1, 5, 15].map((x) => process.cpuUsage()); + let sec = 0; + 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); + + this.server = http.createServer(async (req, res) => { if (!req.headers.cookie?.split("; ").find((x) => x.startsWith("__sb_sessid="))) { res.setHeader("Set-Cookie", `__sb_sessid=${randomString(32)}; Secure; HttpOnly; SameSite=None; Path=/`); } + const requestUrl = new URL(`http://${req.headers.host}${req.url}`); + 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); + return 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"); }); diff --git a/src/gateway/events/Connection.ts b/src/gateway/events/Connection.ts
index 709fc884..07a5706c 100644 --- a/src/gateway/events/Connection.ts +++ b/src/gateway/events/Connection.ts
@@ -42,7 +42,15 @@ try { // TODO: specify rate limit in config // TODO: check msg max size +export const openConnections: WebSocket[] = []; + export async function Connection(this: WS.Server, socket: WebSocket, request: IncomingMessage) { + openConnections.push(socket); + socket.on("close", () => { + const index = openConnections.indexOf(socket); + if (index !== -1) openConnections.splice(index, 1); + }); + const forwardedFor = Config.get().security.forwardedFor; const ipAddress = forwardedFor ? (request.headers[forwardedFor.toLowerCase()] as string) : request.socket.remoteAddress;