[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
This commit is contained in:
319
electron/bridges/x11Forwarding.cjs
Normal file
319
electron/bridges/x11Forwarding.cjs
Normal file
@@ -0,0 +1,319 @@
|
||||
const net = require("node:net");
|
||||
const fs = require("node:fs");
|
||||
const { Transform } = require("node:stream");
|
||||
const { execFileSync } = require("node:child_process");
|
||||
|
||||
const X11_PORT_BASE = 6000;
|
||||
const MIT_MAGIC_COOKIE_PROTOCOL = "MIT-MAGIC-COOKIE-1";
|
||||
|
||||
function resolveX11DisplaySpec(spec, options = {}) {
|
||||
const platform = options.platform || process.platform;
|
||||
let raw = String(spec || options.envDisplay || process.env.DISPLAY || (platform === "win32" ? "localhost:0" : ":0")).trim();
|
||||
|
||||
if (!raw) {
|
||||
return resolveX11DisplaySpec(undefined, { ...options, envDisplay: platform === "win32" ? "localhost:0" : ":0" });
|
||||
}
|
||||
if (raw === ":") {
|
||||
raw = ":0";
|
||||
}
|
||||
|
||||
if (raw.startsWith("/")) {
|
||||
return { path: raw };
|
||||
}
|
||||
|
||||
const match = raw.match(/^(.*):(\d+)(?:\.(\d+))?$/);
|
||||
if (!match) {
|
||||
return platform === "win32"
|
||||
? { host: raw, port: X11_PORT_BASE }
|
||||
: { path: raw };
|
||||
}
|
||||
|
||||
const host = match[1] || "";
|
||||
const display = Number.parseInt(match[2], 10);
|
||||
const port = display >= 100 ? display : X11_PORT_BASE + display;
|
||||
|
||||
if (host.toLowerCase() === "unix" && platform !== "win32") {
|
||||
return { path: `/tmp/.X11-unix/X${display}` };
|
||||
}
|
||||
|
||||
if (!host) {
|
||||
if (platform === "win32") {
|
||||
return { host: "localhost", port };
|
||||
}
|
||||
return { path: `/tmp/.X11-unix/X${display}` };
|
||||
}
|
||||
|
||||
if (host.startsWith("/")) {
|
||||
return { path: host };
|
||||
}
|
||||
|
||||
return { host, port };
|
||||
}
|
||||
|
||||
function formatDisplayTarget(target) {
|
||||
if (target.path) return target.path;
|
||||
return `${target.host}:${target.port}`;
|
||||
}
|
||||
|
||||
function platformHint(platform) {
|
||||
if (platform === "win32") {
|
||||
return "Install and start VcXsrv or Xming, then try again.";
|
||||
}
|
||||
if (platform === "darwin") {
|
||||
return "Install and start XQuartz, then try again.";
|
||||
}
|
||||
return "Check DISPLAY and make sure Xorg, Xwayland, or your X server is running.";
|
||||
}
|
||||
|
||||
function connectSocket(socket, target) {
|
||||
if (target.path) {
|
||||
return socket.connect(target.path);
|
||||
}
|
||||
return socket.connect(target.port, target.host);
|
||||
}
|
||||
|
||||
function destroyStream(stream) {
|
||||
try {
|
||||
stream.destroy();
|
||||
} catch {
|
||||
// best effort cleanup
|
||||
}
|
||||
}
|
||||
|
||||
function pad4(n) {
|
||||
return (n + 3) & ~3;
|
||||
}
|
||||
|
||||
function readUInt16(buf, offset, littleEndian) {
|
||||
return littleEndian ? buf.readUInt16LE(offset) : buf.readUInt16BE(offset);
|
||||
}
|
||||
|
||||
function normalizeCookieBuffer(cookie) {
|
||||
if (!cookie) return null;
|
||||
if (Buffer.isBuffer(cookie)) return cookie;
|
||||
const value = String(cookie).trim();
|
||||
if (/^[0-9a-fA-F]+$/.test(value) && value.length % 2 === 0) {
|
||||
return Buffer.from(value, "hex");
|
||||
}
|
||||
return Buffer.from(value, "binary");
|
||||
}
|
||||
|
||||
function rewriteX11AuthSetupPacket(buffer, options = {}) {
|
||||
const fakeCookie = normalizeCookieBuffer(options.fakeCookie);
|
||||
const realCookie = normalizeCookieBuffer(options.realCookie);
|
||||
if (!realCookie || buffer.length < 12) {
|
||||
return { buffer, complete: buffer.length >= 12, rewritten: false };
|
||||
}
|
||||
|
||||
const byteOrder = buffer[0];
|
||||
const littleEndian = byteOrder === 0x6c; // 'l'
|
||||
if (!littleEndian && byteOrder !== 0x42) { // 'B'
|
||||
return { buffer, complete: true, rewritten: false };
|
||||
}
|
||||
|
||||
const protocolLength = readUInt16(buffer, 6, littleEndian);
|
||||
const dataLength = readUInt16(buffer, 8, littleEndian);
|
||||
const protocolStart = 12;
|
||||
const dataStart = protocolStart + pad4(protocolLength);
|
||||
const totalLength = dataStart + pad4(dataLength);
|
||||
|
||||
if (buffer.length < totalLength) {
|
||||
return { buffer, complete: false, rewritten: false };
|
||||
}
|
||||
|
||||
const protocol = buffer.subarray(protocolStart, protocolStart + protocolLength).toString("ascii");
|
||||
if (protocol !== MIT_MAGIC_COOKIE_PROTOCOL || dataLength !== realCookie.length) {
|
||||
return { buffer, complete: true, rewritten: false };
|
||||
}
|
||||
|
||||
const dataEnd = dataStart + dataLength;
|
||||
const currentCookie = buffer.subarray(dataStart, dataEnd);
|
||||
if (fakeCookie && currentCookie.length === fakeCookie.length && !currentCookie.equals(fakeCookie)) {
|
||||
return { buffer, complete: true, rewritten: false };
|
||||
}
|
||||
|
||||
const next = Buffer.from(buffer);
|
||||
realCookie.copy(next, dataStart);
|
||||
return { buffer: next, complete: true, rewritten: true };
|
||||
}
|
||||
|
||||
function createX11AuthTransform(options = {}) {
|
||||
let pending = Buffer.alloc(0);
|
||||
let done = false;
|
||||
|
||||
return new Transform({
|
||||
transform(chunk, _encoding, callback) {
|
||||
if (done) {
|
||||
callback(null, chunk);
|
||||
return;
|
||||
}
|
||||
|
||||
pending = Buffer.concat([pending, Buffer.from(chunk)]);
|
||||
const result = rewriteX11AuthSetupPacket(pending, options);
|
||||
if (!result.complete) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
done = true;
|
||||
callback(null, result.buffer);
|
||||
},
|
||||
flush(callback) {
|
||||
if (!done && pending.length > 0) {
|
||||
callback(null, pending);
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function resolveXauthCommand(platform) {
|
||||
if (platform === "darwin" && fs.existsSync("/opt/X11/bin/xauth")) {
|
||||
return "/opt/X11/bin/xauth";
|
||||
}
|
||||
return "xauth";
|
||||
}
|
||||
|
||||
function getDisplayNumber(display) {
|
||||
const value = String(display || process.env.DISPLAY || ":0").trim() || ":0";
|
||||
const normalized = value === ":" ? ":0" : value;
|
||||
const match = normalized.match(/:(\d+)(?:\.\d+)?$/) || normalized.match(/\/X(\d+)$/);
|
||||
if (!match) return null;
|
||||
|
||||
const displayNumber = Number.parseInt(match[1], 10);
|
||||
if (!Number.isFinite(displayNumber)) return null;
|
||||
return displayNumber >= X11_PORT_BASE ? displayNumber - X11_PORT_BASE : displayNumber;
|
||||
}
|
||||
|
||||
function parseXauthCookie(output, display) {
|
||||
const requestedDisplay = getDisplayNumber(display);
|
||||
const cookiePattern = new RegExp(`\\b${MIT_MAGIC_COOKIE_PROTOCOL}\\b\\s+([0-9a-fA-F]+)`);
|
||||
|
||||
for (const entry of String(output || "").split(/\r?\n/)) {
|
||||
const match = entry.match(cookiePattern);
|
||||
if (!match) continue;
|
||||
|
||||
const target = entry.trim().split(/\s+/, 1)[0];
|
||||
if (requestedDisplay !== null && getDisplayNumber(target) !== requestedDisplay) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return Buffer.from(match[1], "hex");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function readLocalX11AuthCookie(options = {}) {
|
||||
const platform = options.platform || process.platform;
|
||||
const command = options.xauthCommand || resolveXauthCommand(platform);
|
||||
const display = String(options.display || process.env.DISPLAY || ":0").trim() || ":0";
|
||||
try {
|
||||
const normalizedDisplay = display === ":" ? ":0" : display;
|
||||
const output = typeof options.readXauthOutput === "function"
|
||||
? options.readXauthOutput({ command, display: normalizedDisplay })
|
||||
: execFileSync(command, ["list"], {
|
||||
encoding: "utf8",
|
||||
env: {
|
||||
...process.env,
|
||||
DISPLAY: normalizedDisplay,
|
||||
},
|
||||
stdio: ["ignore", "pipe", "ignore"],
|
||||
timeout: 2000,
|
||||
});
|
||||
return parseXauthCookie(output, normalizedDisplay);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function attachX11Forwarding(conn, options = {}) {
|
||||
const createSocket = options.createSocket || (() => new net.Socket());
|
||||
const sendMessage = typeof options.sendMessage === "function" ? options.sendMessage : () => {};
|
||||
const logger = options.logger || console;
|
||||
const platform = options.platform || process.platform;
|
||||
const display = options.display;
|
||||
const fakeCookie = options.fakeCookie;
|
||||
const fixedLocalAuthCookie = normalizeCookieBuffer(options.localAuthCookie);
|
||||
let localAuthCookie = fixedLocalAuthCookie;
|
||||
let localAuthCookieResolved = Boolean(fixedLocalAuthCookie);
|
||||
|
||||
const resolveLocalAuthCookie = () => {
|
||||
if (localAuthCookieResolved) return localAuthCookie;
|
||||
localAuthCookieResolved = true;
|
||||
const cookie = typeof options.readLocalAuthCookie === "function"
|
||||
? options.readLocalAuthCookie({ display, platform })
|
||||
: readLocalX11AuthCookie({ display, platform });
|
||||
localAuthCookie = normalizeCookieBuffer(cookie);
|
||||
return localAuthCookie;
|
||||
};
|
||||
|
||||
const onX11 = (info, accept, reject) => {
|
||||
const target = resolveX11DisplaySpec(display, { platform });
|
||||
const localSocket = createSocket();
|
||||
let acceptedChannel = null;
|
||||
let settled = false;
|
||||
|
||||
const cleanup = () => {
|
||||
if (acceptedChannel) destroyStream(acceptedChannel);
|
||||
destroyStream(localSocket);
|
||||
};
|
||||
|
||||
localSocket.once("connect", () => {
|
||||
if (settled) return;
|
||||
try {
|
||||
acceptedChannel = accept();
|
||||
settled = true;
|
||||
} catch (err) {
|
||||
logger.warn?.("[X11] Failed to accept forwarded channel", err);
|
||||
cleanup();
|
||||
return;
|
||||
}
|
||||
|
||||
acceptedChannel.on("error", () => cleanup());
|
||||
localSocket.on("error", () => cleanup());
|
||||
acceptedChannel.on("close", () => destroyStream(localSocket));
|
||||
localSocket.on("close", () => destroyStream(acceptedChannel));
|
||||
const realCookie = resolveLocalAuthCookie();
|
||||
if (realCookie && fakeCookie) {
|
||||
acceptedChannel
|
||||
.pipe(createX11AuthTransform({ fakeCookie, realCookie }))
|
||||
.pipe(localSocket)
|
||||
.pipe(acceptedChannel);
|
||||
} else {
|
||||
acceptedChannel.pipe(localSocket).pipe(acceptedChannel);
|
||||
}
|
||||
});
|
||||
|
||||
localSocket.once("error", (err) => {
|
||||
if (!settled) {
|
||||
settled = true;
|
||||
try { reject(); } catch { /* ignore reject errors */ }
|
||||
sendMessage(`\r\n[X11] Could not connect to the local X11 server: ${err?.message || err}\r\n`);
|
||||
sendMessage(`[X11] Display target: ${formatDisplayTarget(target)}\r\n`);
|
||||
sendMessage(`[X11] ${platformHint(platform)}\r\n`);
|
||||
}
|
||||
destroyStream(localSocket);
|
||||
});
|
||||
|
||||
try {
|
||||
connectSocket(localSocket, target);
|
||||
} catch (err) {
|
||||
localSocket.emit("error", err);
|
||||
}
|
||||
};
|
||||
|
||||
conn.on("x11", onX11);
|
||||
return () => {
|
||||
if (typeof conn.off === "function") conn.off("x11", onX11);
|
||||
else conn.removeListener("x11", onX11);
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
attachX11Forwarding,
|
||||
readLocalX11AuthCookie,
|
||||
rewriteX11AuthSetupPacket,
|
||||
resolveX11DisplaySpec,
|
||||
};
|
||||
Reference in New Issue
Block a user