mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-17 04:12:17 +03:00
Merged as part of the owner batch of 2026-09-11. This PR had a live worktree in another session, so it sat outside the main 39. Merged on your explicit call, validated first rather than taken on trust: boarded with the other 10 worktree-held PRs into a consolidated worktree off `release/v3.8.51`. - ESLint over every changed file: no errors - `typecheck:core` clean; `check:dashboard-typecheck` OK; `check:changelog-integrity` OK - complexity 2821 / baseline 3218 and cognitive-complexity 1272 / baseline 1437 - 203 of 208 assertions green. The 5 remaining (`guide-settings-route` ×4, `hard-session-lease-bypass-inventory` ×1) reproduce on the pure tip with nothing from this batch applied. - `imageGeneration.ts` rebaselined 3259 → 3293 for #12945's image-only-model guard, landed separately in #13392 so nothing was pushed onto a live branch. ⚠️ base-red inherited: #12732 — provider count 356 vs 358 and `open-sse/utils/stream.ts` 3115 > frozen 3098, both reproducing on the pure tip.
This commit is contained in:
committed by
GitHub
parent
1fb7d5dff2
commit
b97bc59f4f
@@ -17,6 +17,9 @@ export interface HarvestResult {
|
||||
hasCredential: boolean;
|
||||
}
|
||||
|
||||
/** Header name the CDP bridge (docker/vnc-browser/chromium/cdp-bridge.py) requires (#12571). */
|
||||
const CDP_TOKEN_HEADER = "X-Omni-Cdp-Token";
|
||||
|
||||
interface Pending {
|
||||
resolve: (value: any) => void;
|
||||
reject: (error: Error) => void;
|
||||
@@ -36,8 +39,8 @@ class CdpClient {
|
||||
private sessionId: string | null = null;
|
||||
private closed = false;
|
||||
|
||||
constructor(wsUrl: string) {
|
||||
this.ws = new WebSocket(wsUrl);
|
||||
constructor(wsUrl: string, cdpToken: string) {
|
||||
this.ws = new WebSocket(wsUrl, { headers: { [CDP_TOKEN_HEADER]: cdpToken } });
|
||||
this.ws.on("message", (data) => this.onMessage(data));
|
||||
this.ws.on("close", () => this.rejectAll(new Error("CDP websocket closed")));
|
||||
this.ws.on("error", (error) => this.rejectAll(toError(error, "CDP websocket error")));
|
||||
@@ -252,7 +255,11 @@ class CdpClient {
|
||||
}
|
||||
}
|
||||
|
||||
export async function waitForCdpReady(cdpPort: number, timeoutMs: number): Promise<void> {
|
||||
export async function waitForCdpReady(
|
||||
cdpPort: number,
|
||||
timeoutMs: number,
|
||||
cdpToken: string
|
||||
): Promise<void> {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
let lastError: Error | null = null;
|
||||
|
||||
@@ -260,7 +267,11 @@ export async function waitForCdpReady(cdpPort: number, timeoutMs: number): Promi
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), 2_000);
|
||||
try {
|
||||
const version = await fetchJson(`http://127.0.0.1:${cdpPort}/json/version`, controller.signal);
|
||||
const version = await fetchJson(
|
||||
`http://127.0.0.1:${cdpPort}/json/version`,
|
||||
controller.signal,
|
||||
cdpToken
|
||||
);
|
||||
if (version?.webSocketDebuggerUrl) return;
|
||||
lastError = new Error("CDP endpoint did not return a websocket URL");
|
||||
} catch (error) {
|
||||
@@ -277,7 +288,8 @@ export async function waitForCdpReady(cdpPort: number, timeoutMs: number): Promi
|
||||
export async function harvestFromContainer(
|
||||
cdpPort: number,
|
||||
provider: VncProviderEntry,
|
||||
timeoutMs = 20_000
|
||||
timeoutMs = 20_000,
|
||||
cdpToken = ""
|
||||
): Promise<HarvestResult> {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
||||
@@ -286,14 +298,15 @@ export async function harvestFromContainer(
|
||||
try {
|
||||
const version = await fetchJson(
|
||||
`http://127.0.0.1:${cdpPort}/json/version`,
|
||||
controller.signal
|
||||
controller.signal,
|
||||
cdpToken
|
||||
);
|
||||
const debuggerUrl = version?.webSocketDebuggerUrl;
|
||||
if (typeof debuggerUrl !== "string" || !debuggerUrl) {
|
||||
throw new Error("No CDP websocket endpoint from browser container");
|
||||
}
|
||||
|
||||
client = new CdpClient(rewriteDebuggerUrl(debuggerUrl, cdpPort));
|
||||
client = new CdpClient(rewriteDebuggerUrl(debuggerUrl, cdpPort), cdpToken);
|
||||
await client.ready(Math.min(timeoutMs, 15_000), controller.signal);
|
||||
|
||||
const origin = new URL(provider.url).origin;
|
||||
@@ -403,8 +416,8 @@ function safeOrigin(value: string | undefined): string | null {
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchJson(url: string, signal: AbortSignal): Promise<any> {
|
||||
const response = await fetch(url, { signal });
|
||||
async function fetchJson(url: string, signal: AbortSignal, cdpToken = ""): Promise<any> {
|
||||
const response = await fetch(url, { signal, headers: { [CDP_TOKEN_HEADER]: cdpToken } });
|
||||
if (!response.ok) throw new Error(`CDP endpoint returned HTTP ${response.status}`);
|
||||
return response.json();
|
||||
}
|
||||
|
||||
@@ -86,6 +86,12 @@ export const VNC_CONFIG = {
|
||||
maxSessionMs: Number(process.env.OMNIROUTE_VNC_MAX_MS || 30 * 60 * 1000),
|
||||
maxSessions: Number(process.env.OMNIROUTE_VNC_MAX_SESSIONS || 4),
|
||||
dockerBin: process.env.OMNIROUTE_DOCKER_BIN || "docker",
|
||||
/**
|
||||
* Dedicated bridge network for browser-login containers (#12571): keeps
|
||||
* them off Docker's default bridge network so sibling containers can't
|
||||
* reach the CDP bridge port over the container-to-container path.
|
||||
*/
|
||||
network: process.env.OMNIROUTE_VNC_NETWORK || "omniroute-vnc-browser-login",
|
||||
browserReadyTimeoutMs: Number(process.env.OMNIROUTE_VNC_READY_MS || 45_000),
|
||||
harvestTimeoutMs: Number(process.env.OMNIROUTE_VNC_HARVEST_MS || 20_000),
|
||||
chromiumArgs:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { randomBytes, randomUUID } from "node:crypto";
|
||||
import { chmodSync, mkdirSync, rmSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { isConnectionUnavailableToAuxiliaryActivity } from "@/lib/exclusiveLeaseIsolation";
|
||||
@@ -17,6 +17,8 @@ export interface VncSession {
|
||||
containerName: string;
|
||||
profileDir: string;
|
||||
cdpPort: number;
|
||||
/** Shared secret the CDP bridge (docker/vnc-browser/chromium/cdp-bridge.py) requires (#12571). */
|
||||
cdpToken: string;
|
||||
vncPort: number;
|
||||
url: string;
|
||||
status: VncSessionStatus;
|
||||
@@ -138,6 +140,67 @@ function createProfileDir(connectionId: string, sessionId: string): string {
|
||||
return profileDir;
|
||||
}
|
||||
|
||||
let networkEnsured = false;
|
||||
|
||||
/**
|
||||
* Creates the dedicated browser-login bridge network (#12571) if it does not
|
||||
* already exist. Idempotent: `docker network create` failing because the
|
||||
* network is already there is not an error.
|
||||
*/
|
||||
async function ensureNetwork(): Promise<void> {
|
||||
if (networkEnsured) return;
|
||||
const result = await docker(["network", "create", VNC_CONFIG.network], { timeoutMs: 15_000 });
|
||||
if (result.code !== 0 && !/already exists/i.test(result.err)) {
|
||||
throw new Error(result.err.trim() || `Could not create Docker network ${VNC_CONFIG.network}`);
|
||||
}
|
||||
networkEnsured = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the `docker run` argument array for a browser-login container.
|
||||
* Pulled out as a pure function so the security-relevant shape (dedicated
|
||||
* network + CDP_BRIDGE_TOKEN, #12571) is directly testable without spawning
|
||||
* Docker or touching the DB.
|
||||
*/
|
||||
export function buildRunArgs(params: {
|
||||
containerName: string;
|
||||
sessionId: string;
|
||||
connectionId: string;
|
||||
profileDir: string;
|
||||
chromeCli: string;
|
||||
cdpToken: string;
|
||||
}): string[] {
|
||||
return [
|
||||
"run",
|
||||
"-d",
|
||||
"--name",
|
||||
params.containerName,
|
||||
"--restart",
|
||||
"no",
|
||||
"--network",
|
||||
VNC_CONFIG.network,
|
||||
"--label",
|
||||
`${LABEL}=true`,
|
||||
"--label",
|
||||
`${LABEL}.session-id=${params.sessionId}`,
|
||||
"--label",
|
||||
`${LABEL}.connection-id=${params.connectionId}`,
|
||||
"--shm-size",
|
||||
"1gb",
|
||||
"-p",
|
||||
`127.0.0.1::${VNC_CONFIG.containerVncPort}`,
|
||||
"-p",
|
||||
`127.0.0.1::${VNC_CONFIG.containerCdpPort}`,
|
||||
"-v",
|
||||
`${params.profileDir}:${VNC_CONFIG.containerProfileDir}`,
|
||||
"-e",
|
||||
`CHROME_CLI=${params.chromeCli}`,
|
||||
"-e",
|
||||
`CDP_BRIDGE_TOKEN=${params.cdpToken}`,
|
||||
VNC_CONFIG.image,
|
||||
];
|
||||
}
|
||||
|
||||
async function publishedPort(containerName: string, containerPort: number): Promise<number> {
|
||||
const result = await docker(["port", containerName, `${containerPort}/tcp`], {
|
||||
timeoutMs: 10_000,
|
||||
@@ -176,6 +239,7 @@ export async function startSession(connectionId: string): Promise<VncSession> {
|
||||
const sessionId = randomUUID();
|
||||
const containerName = sessionKey(sessionId);
|
||||
const profileDir = createProfileDir(connectionId, sessionId);
|
||||
const cdpToken = randomBytes(24).toString("hex");
|
||||
const state: VncSession = {
|
||||
sessionId,
|
||||
connectionId,
|
||||
@@ -183,6 +247,7 @@ export async function startSession(connectionId: string): Promise<VncSession> {
|
||||
containerName,
|
||||
profileDir,
|
||||
cdpPort: 0,
|
||||
cdpToken,
|
||||
vncPort: 0,
|
||||
url: provider.url,
|
||||
status: "starting",
|
||||
@@ -193,33 +258,10 @@ export async function startSession(connectionId: string): Promise<VncSession> {
|
||||
SESSIONS.set(sessionId, state);
|
||||
|
||||
try {
|
||||
await ensureNetwork();
|
||||
const chromeCli = `${VNC_CONFIG.chromiumArgs} ${provider.url}`;
|
||||
const result = await docker(
|
||||
[
|
||||
"run",
|
||||
"-d",
|
||||
"--name",
|
||||
containerName,
|
||||
"--restart",
|
||||
"no",
|
||||
"--label",
|
||||
`${LABEL}=true`,
|
||||
"--label",
|
||||
`${LABEL}.session-id=${sessionId}`,
|
||||
"--label",
|
||||
`${LABEL}.connection-id=${connectionId}`,
|
||||
"--shm-size",
|
||||
"1gb",
|
||||
"-p",
|
||||
`127.0.0.1::${VNC_CONFIG.containerVncPort}`,
|
||||
"-p",
|
||||
`127.0.0.1::${VNC_CONFIG.containerCdpPort}`,
|
||||
"-v",
|
||||
`${profileDir}:${VNC_CONFIG.containerProfileDir}`,
|
||||
"-e",
|
||||
`CHROME_CLI=${chromeCli}`,
|
||||
VNC_CONFIG.image,
|
||||
],
|
||||
buildRunArgs({ containerName, sessionId, connectionId, profileDir, chromeCli, cdpToken }),
|
||||
{ timeoutMs: 120_000 }
|
||||
);
|
||||
if (result.code !== 0) {
|
||||
@@ -234,7 +276,7 @@ export async function startSession(connectionId: string): Promise<VncSession> {
|
||||
|
||||
state.vncPort = await publishedPort(containerName, VNC_CONFIG.containerVncPort);
|
||||
state.cdpPort = await publishedPort(containerName, VNC_CONFIG.containerCdpPort);
|
||||
await waitForCdpReady(state.cdpPort, VNC_CONFIG.browserReadyTimeoutMs);
|
||||
await waitForCdpReady(state.cdpPort, VNC_CONFIG.browserReadyTimeoutMs, state.cdpToken);
|
||||
|
||||
state.status = "running";
|
||||
scheduleIdleSweep();
|
||||
@@ -275,7 +317,8 @@ export async function harvestSession(
|
||||
const harvest = await harvestFromContainer(
|
||||
session.cdpPort,
|
||||
provider,
|
||||
VNC_CONFIG.harvestTimeoutMs
|
||||
VNC_CONFIG.harvestTimeoutMs,
|
||||
session.cdpToken
|
||||
);
|
||||
session.lastHarvestAt = Date.now();
|
||||
if (!harvest.hasCredential) {
|
||||
|
||||
Reference in New Issue
Block a user