electron/device-chrome.node-test.mjs
Rama main · réplica de lectura del repositorio original.
import assert from "node:assert/strict";
import { mkdtemp, writeFile, rm } from "node:fs/promises";
import { createServer as createHttpServer } from "node:http";
import { createServer } from "node:net";
import { createRequire } from "node:module";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { test } from "node:test";
import { setTimeout as delay } from "node:timers/promises";
import { chromeDataDirs, createChromeDoor, MAX_HEAD, parseDevtoolsActivePort, rewriteHandshake } from "./device-chrome.mjs";
const { WebSocketServer, WebSocket } = createRequire(import.meta.url)("ws");
const until = async (check, what, ms = 5000) => {
const deadline = Date.now() + ms;
while (Date.now() < deadline) { if (await check()) return; await delay(10); }
assert.fail(`timed out waiting for ${what}`);
};
// A stand-in for Chrome: upgrades only its own browser path. `seen` records every request, upgrade or not, and `bytes`
// every byte any connection delivered, so "never reaches Chrome" means nothing arrived at all.
async function fakeChrome({ onUpgrade } = {}) {
const seen = [];
const state = { bytes: 0 };
const wss = new WebSocketServer({ noServer: true });
const record = (req) => seen.push({ url: req.url, host: req.headers.host, origin: req.headers.origin });
const server = createHttpServer((req, res) => { record(req); res.statusCode = 404; res.end(); });
server.on("connection", (socket) => socket.on("data", (data) => { state.bytes += data.length; }));
server.on("upgrade", (req, socket, head) => {
record(req);
if (req.url !== "/devtools/browser/REAL-ID") { socket.resume(); socket.end("HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\n\r\n"); return; }
socket.resume(); // http stops reading once it emits `upgrade`; keep counting
if (onUpgrade) { onUpgrade(socket); return; }
wss.handleUpgrade(req, socket, head, (ws) => ws.on("message", (data) => ws.send(data.toString())));
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
return { seen, state, port: server.address().port, close: () => { wss.clients.forEach((c) => c.terminate()); server.closeAllConnections(); server.close(); } };
}
// A regression should fail a test, not hang the file.
const bounded = (promise, what, ms = 5000) => Promise.race([promise, delay(ms).then(() => assert.fail(`timed out waiting for ${what}`))]);
const withDir = async (text, body) => {
const dir = await mkdtemp(join(tmpdir(), "chrome-door-"));
try { if (text !== null) await writeFile(join(dir, "DevToolsActivePort"), text); return await body(dir); } finally { await rm(dir, { recursive: true, force: true }); }
};
test("reads DevToolsActivePort: a port and Chrome's browser path, nothing else", () => {
assert.deepEqual(parseDevtoolsActivePort("9222\n/devtools/browser/abc-1\n"), { port: 9222, path: "/devtools/browser/abc-1" });
for (const text of ["", "0\n/devtools/browser/x", "70000\n/devtools/browser/x", "9222\n/json/new", "9222\n/devtools/browser/../x"]) assert.equal(parseDevtoolsActivePort(text), null, JSON.stringify(text));
});
test("looks only in Chrome's own folders, in agent-browser's order", () => {
assert.deepEqual(chromeDataDirs({ platform: "darwin", home: "/U" }), ["/U/Library/Application Support/Google/Chrome", "/U/Library/Application Support/Google/Chrome Canary", "/U/Library/Application Support/BraveSoftware/Brave-Browser"]);
assert.deepEqual(chromeDataDirs({ platform: "win32", localAppData: "C:\\L" }), ["C:\\L\\Google\\Chrome\\User Data", "C:\\L\\Google\\Chrome SxS\\User Data", "C:\\L\\BraveSoftware\\Brave-Browser\\User Data"]);
assert.deepEqual(chromeDataDirs({ platform: "linux", home: "/h" }), ["/h/.config/google-chrome", "/h/.config/google-chrome-unstable", "/h/.config/BraveSoftware/Brave-Browser"]);
for (const platform of ["win32", "darwin", "linux"]) assert.deepEqual(chromeDataDirs({ platform }), [], `${platform} without its base folder`);
});
test("rewrites only a WebSocket upgrade to Chrome's browser or a page, with Chrome's own host and no Origin", () => {
const target = { port: 9222, path: "/devtools/browser/REAL-ID" };
const head = (line, ...rest) => [line, "Host: 127.0.0.1:5555", "Origin: https://evil.test", ...rest].join("\r\n");
const upgrade = ["Upgrade: websocket", "Connection: Upgrade"];
assert.equal(rewriteHandshake(head("GET /devtools/browser/TOKEN HTTP/1.1", ...upgrade), target),
"GET /devtools/browser/REAL-ID HTTP/1.1\r\nHost: 127.0.0.1:9222\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n\r\n");
assert.match(rewriteHandshake(head("GET /devtools/browser?x=1 HTTP/1.1", "upgrade: WebSocket", "connection: keep-alive, Upgrade"), target), /^GET \/devtools\/browser\/REAL-ID HTTP/);
assert.match(rewriteHandshake(head("GET /devtools/page/AB12 HTTP/1.1", ...upgrade), target), /^GET \/devtools\/page\/AB12 HTTP/);
assert.equal(rewriteHandshake(head("GET /json/new?https://x HTTP/1.1", ...upgrade), target), null);
assert.equal(rewriteHandshake(head("POST /devtools/browser/x HTTP/1.1", ...upgrade), target), null);
assert.equal(rewriteHandshake(head("GET /devtools/browser/x HTTP/1.1"), target), null);
assert.equal(rewriteHandshake(head("GET /devtools/browser/x HTTP/1.1", "Upgrade: websocket"), target), null, "no Connection: upgrade");
assert.equal(rewriteHandshake(head("GET /devtools/browser/x HTTP/1.1", "Upgrade: websocket", "Connection: close"), target), null, "Connection without upgrade");
assert.equal(rewriteHandshake(head("GET /devtools/browser/x HTTP/1.1", ...upgrade, "X-A: 1\nOrigin: https://evil.test"), target), null, "a bare LF inside a line");
assert.equal(rewriteHandshake(head("GET /devtools/browser/x HTTP/1.1", ...upgrade, "X-A: 1\rOrigin: https://evil.test"), target), null, "a bare CR inside a line");
});
test("a door to a running Chrome upgrades end to end and passes frames unchanged", async (t) => {
const chrome = await fakeChrome();
t.after(() => chrome.close());
await withDir(`${chrome.port}\n/devtools/browser/REAL-ID\n`, async (dir) => {
const opened = await createChromeDoor({ dirs: [dir] }).open();
assert.ok(opened.socket);
const local = createServer((client) => { client.pipe(opened.socket); opened.socket.pipe(client); });
await new Promise((resolve) => local.listen(0, "127.0.0.1", resolve));
const ws = new WebSocket(`ws://127.0.0.1:${local.address().port}/devtools/browser/TOKEN`, { origin: "https://evil.test" });
const reply = await new Promise((resolve, reject) => { ws.on("open", () => ws.send('{"id":1,"method":"Browser.getVersion"}')); ws.on("message", (d) => resolve(d.toString())); ws.on("error", reject); });
assert.equal(reply, '{"id":1,"method":"Browser.getVersion"}');
assert.deepEqual(chrome.seen, [{ url: "/devtools/browser/REAL-ID", host: `127.0.0.1:${chrome.port}`, origin: undefined }]);
ws.terminate(); local.close();
});
});
test("a request that is not a WebSocket upgrade gets 403 and never reaches Chrome", async (t) => {
const chrome = await fakeChrome();
t.after(() => chrome.close());
await withDir(`${chrome.port}\n/devtools/browser/REAL-ID\n`, async (dir) => {
const { socket } = await createChromeDoor({ dirs: [dir] }).open();
const chunks = [];
socket.on("data", (c) => chunks.push(c));
const closed = new Promise((resolve) => socket.on("close", resolve));
await new Promise((resolve) => socket.write("GET /json/new?https://x HTTP/1.1\r\nHost: a\r\n\r\n", resolve));
await bounded(closed, "closed");
assert.match(Buffer.concat(chunks).toString(), /^HTTP\/1\.1 403 Forbidden\r\n/);
assert.deepEqual(chrome.seen, []);
assert.equal(chrome.state.bytes, 0);
});
});
test("an oversized head is dropped", async (t) => {
const chrome = await fakeChrome();
t.after(() => chrome.close());
await withDir(`${chrome.port}\n/devtools/browser/REAL-ID\n`, async (dir) => {
const { socket } = await createChromeDoor({ dirs: [dir] }).open();
socket.on("error", () => {});
socket.write(Buffer.alloc(MAX_HEAD + 1, 0x61));
await until(() => socket.destroyed, "the guard to be destroyed");
await delay(50);
assert.deepEqual(chrome.seen, []);
assert.equal(chrome.state.bytes, 0);
});
});
test("no DevToolsActivePort, or a stale one whose port refuses, is no-chrome", async () => {
await withDir(null, async (dir) => assert.deepEqual(await createChromeDoor({ dirs: [dir] }).open(), { failed: "no-chrome" }));
const closed = createServer();
await new Promise((resolve) => closed.listen(0, "127.0.0.1", resolve));
const { port } = closed.address();
await new Promise((resolve) => closed.close(resolve));
await withDir(`${port}\n/devtools/browser/REAL-ID\n`, async (dir) => assert.deepEqual(await createChromeDoor({ dirs: [dir] }).open(), { failed: "no-chrome" }));
});
const UPGRADE = "GET /devtools/browser/TOKEN HTTP/1.1\r\nHost: a\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\nSec-WebSocket-Version: 13\r\n\r\n";
test("a second request pipelined behind the upgrade never reaches Chrome", async (t) => {
const chrome = await fakeChrome({ onUpgrade: (socket) => { socket.write("HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\n\r\n"); } });
t.after(() => chrome.close());
await withDir(`${chrome.port}\n/devtools/browser/REAL-ID\n`, async (dir) => {
const { socket } = await createChromeDoor({ dirs: [dir] }).open();
socket.on("data", () => {});
const closed = new Promise((resolve) => socket.on("close", resolve));
socket.write(`${UPGRADE}GET /json/list HTTP/1.1\r\nHost: localhost\r\n\r\n`);
await bounded(closed, "closed");
await delay(100);
assert.deepEqual(chrome.seen.map((r) => r.url), ["/devtools/browser/REAL-ID"]);
assert.equal(chrome.state.bytes, UPGRADE.replace("TOKEN", "REAL-ID").length - "Host: a".length + `Host: 127.0.0.1:${chrome.port}`.length);
});
});
test("when Chrome answers anything but 101, held bytes are not sent and the guard ends after delivering the answer", async (t) => {
const chrome = await fakeChrome();
t.after(() => chrome.close());
await withDir(`${chrome.port}\n/devtools/browser/NOT-REAL\n`, async (dir) => {
const { socket } = await createChromeDoor({ dirs: [dir] }).open();
const chunks = [];
socket.on("data", (c) => chunks.push(c));
const closed = new Promise((resolve) => socket.on("close", resolve));
socket.write(UPGRADE);
const second = new Promise((resolve) => socket.write("GET /json/list HTTP/1.1\r\nHost: x\r\n\r\n", resolve));
await bounded(closed, "closed");
await bounded(second, "second");
await delay(100); // anything wrongly sent after the answer would land by now
assert.match(Buffer.concat(chunks).toString(), /^HTTP\/1\.1 404 /);
assert.equal(chrome.seen.length, 1, "only the upgrade, never the held request");
assert.equal(chrome.state.bytes, UPGRADE.replace("TOKEN", "NOT-REAL").length - "Host: a".length + `Host: 127.0.0.1:${chrome.port}`.length, "only the head reached Chrome, never the held request");
});
});
test("bytes Chrome sends just before it closes still arrive when the guard was paused", async (t) => {
const chrome = await fakeChrome({ onUpgrade: (socket) => socket.end("HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n\r\nTAIL") });
t.after(() => chrome.close());
await withDir(`${chrome.port}\n/devtools/browser/REAL-ID\n`, async (dir) => {
const { socket } = await createChromeDoor({ dirs: [dir] }).open();
const chunks = [];
socket.on("data", (c) => chunks.push(c));
const ended = new Promise((resolve) => socket.on("end", resolve));
socket.pause();
socket.write(UPGRADE);
await delay(150);
socket.resume();
await bounded(ended, "ended");
assert.match(Buffer.concat(chunks).toString(), /^HTTP\/1\.1 101 [\s\S]*TAIL$/);
});
});
test("ending the guard before the head is complete closes it and Chrome's side", async (t) => {
const chrome = await fakeChrome();
t.after(() => chrome.close());
await withDir(`${chrome.port}\n/devtools/browser/REAL-ID\n`, async (dir) => {
const { socket } = await createChromeDoor({ dirs: [dir] }).open();
socket.resume();
const ended = new Promise((resolve) => socket.on("end", resolve));
const closed = new Promise((resolve) => socket.on("close", resolve));
await new Promise((resolve) => socket.write("GET /devtools/browser/x HTT", resolve));
socket.end();
await bounded(ended, "ended");
await bounded(closed, "closed");
assert.deepEqual(chrome.seen, []);
assert.equal(chrome.state.bytes, 0);
});
});
test("Chrome closing before it answers closes the guard, with later writes still pending", async (t) => {
const chrome = await fakeChrome({ onUpgrade: (socket) => socket.end() });
t.after(() => chrome.close());
await withDir(`${chrome.port}\n/devtools/browser/REAL-ID\n`, async (dir) => {
const { socket } = await createChromeDoor({ dirs: [dir] }).open();
socket.resume();
const closed = new Promise((resolve) => socket.on("close", resolve));
socket.write(UPGRADE);
socket.write("one");
socket.write("two");
socket.end();
await bounded(closed, "the guard to close");
});
});