"use strict"; const test = require("node:test"); const assert = require("node:assert/strict"); const { EventEmitter } = require("node:events"); const { SessionManager } = require("../src/main/sessions/sessionManager"); const marker = "\x1eCHJ_EDITOR_START\n"; function fixture({ username = "user", stdout = marker + JSON.stringify({ ok: true, value: { status: "saved" } }), code = 0, stderr = "" } = {}) { const commands = [], input = []; const manager = new SessionManager({}); manager.sessions.set("s", { sessionId: "s", username, state: "connected", stream: {}, platform: { os: "linux" }, client: { exec(command, callback) { commands.push(command); const stream = new EventEmitter(); stream.stderr = new EventEmitter(); stream.end = (value) => input.push(value); callback(null, stream); queueMicrotask(() => { stream.emit("data", Buffer.from(stdout)); stream.stderr.emit("data", Buffer.from(stderr)); stream.emit("close", code); }); } } }); return { manager, commands, input }; } const READY = "\x1eCHJ_READY\n"; const PROMPT = "[chj-sudo-auth]"; // Simulated server for the shared PrivilegedExecutor protocol: probe, sudo prompt, READY, stdin frame. function privilegedFixture({ username = "user", sudo = "password", correct = "sudo-secret-'$value", python = true, reply = { ok: true, value: { status: "saved" } }, dropAfterRequest = false } = {}) { const commands = [], writes = [], requests = []; const manager = new SessionManager({}); const record = { sessionId: "s", username, state: "connected", stream: {}, platform: { os: "linux" }, client: { exec(command, callback) { commands.push(command); const stream = new EventEmitter(); stream.stderr = new EventEmitter(); let ready = false; let closed = false; const out = (text) => queueMicrotask(() => stream.emit("data", Buffer.from(text))); const err = (text) => queueMicrotask(() => stream.stderr.emit("data", Buffer.from(text))); const close = (code) => { if (closed) return; closed = true; queueMicrotask(() => stream.emit("close", code)); }; const becomeReady = () => { ready = true; out(READY); }; stream.write = (data) => { const text = String(data); writes.push(text); if (!ready) { if (text === correct + "\n") becomeReady(); else { err("Sorry, try again.\n" + PROMPT); } return; } const frame = /^CHJREQ:(.*)\n$/.exec(text); if (!frame) return; requests.push(JSON.parse(Buffer.from(frame[1], "base64").toString("utf8"))); if (dropAfterRequest) { manager.disconnect("s"); return; } if (!python) { out("\x1eCHJ_ERR=DEPENDENCY_UNAVAILABLE\n"); close(127); return; } out("\x1eCHJ_EDITOR_START\n" + JSON.stringify(reply)); close(0); }; stream.end = () => { if (!ready) close(1); }; stream.close = () => close(1); callback(null, stream); if (command.includes("sudoNonInteractive")) { const nopass = sudo === "nopasswd"; out(`user=${username}\nuid=${username === "root" ? 0 : 1000}\ngroups=${username}\nclient=10.0.0.1 1 22\nsudoInstalled=1\nsudoNonInteractive=${nopass ? 0 : 1}\nsudoMessage=${nopass ? "" : sudo === "none" ? "Sorry, user user may not run sudo on host." : "sudo: a password is required"}\n`); close(0); } else if (command.startsWith("printf ")) { out("\x1eCHJ_EDITOR_START\n" + JSON.stringify(reply)); close(0); } else if (command.startsWith("LC_ALL=C sudo -S -p ")) { if (sudo === "nopasswd") becomeReady(); else err(PROMPT); } else if (command.startsWith("LC_ALL=C sh -c ")) becomeReady(); } } }; record.decoder = { end: () => "" }; record.client.end = () => {}; manager.sessions.set("s", record); return { manager, record, commands, writes, requests }; } test("editor sudo uses the shared PrivilegedExecutor: password only after the prompt, request as stdin frame", async () => { const request = { operation: "finalize", path: "/etc/test 'quoted' ; $(echo unsafe)" }; const password = "sudo-secret-'$value"; const f = privilegedFixture(); assert.deepEqual(await f.manager.remoteEditor("s", request, { sudo: true, sudoPassword: password }), { status: "saved" }); const editorCommand = f.commands.find((command) => command.startsWith("LC_ALL=C sudo")); assert.match(editorCommand, /^LC_ALL=C sudo -S -p '\[chj-sudo-auth\]' -- sh -c /); assert.equal(f.commands.some((command) => command.includes(password)), false, "password never in argv"); assert.equal(f.commands.some((command) => command.includes("echo unsafe")), false, "request travels on stdin, not argv"); assert.equal(f.writes[0], password + "\n"); assert.deepEqual(f.requests, [request]); const passwordless = privilegedFixture({ sudo: "nopasswd" }); await passwordless.manager.remoteEditor("s", request, { sudo: true, sudoPassword: password }); assert.equal(passwordless.writes.some((text) => text.includes(password)), false, "NOPASSWD sudo never receives the password"); const root = privilegedFixture({ username: "root" }); await root.manager.remoteEditor("s", request, { sudo: true }); assert.equal(root.commands.some((command) => /\bsudo -S\b/.test(command)), false, "root sessions never call sudo"); assert.deepEqual(root.requests, [request]); const plain = privilegedFixture(); await plain.manager.remoteEditor("s", request); assert.match(plain.commands[0], /^printf /); await assert.rejects(() => f.manager.remoteEditor("s", request, { sudoPassword: password }), { code: "FILE_SUDO_AUTHORIZATION_REQUIRED" }); }); test("sudo refusals are definite and classified; credentials never reach error messages", async () => { const wrong = privilegedFixture(); await assert.rejects(() => wrong.manager.remoteEditor("s", { operation: "finalize" }, { sudo: true, sudoPassword: "wrong-secret" }), (error) => { assert.equal(error.code, "SUDO_PASSWORD_INCORRECT"); assert.doesNotMatch(error.message + JSON.stringify(error.details || {}), /wrong-secret/); return true; }); assert.equal(wrong.requests.length, 0, "helper never started"); await assert.rejects(() => privilegedFixture().manager.remoteEditor("s", { operation: "read" }, { sudo: true }), { code: "SUDO_PASSWORD_REQUIRED" }); await assert.rejects(() => privilegedFixture({ sudo: "none" }).manager.remoteEditor("s", { operation: "read" }, { sudo: true, sudoPassword: "x" }), { code: "SUDO_NOT_PERMITTED" }); await assert.rejects(() => privilegedFixture({ python: false }).manager.remoteEditor("s", { operation: "read" }, { sudo: true, sudoPassword: "sudo-secret-'$value" }), { code: "FILE_DEPENDENCY_UNAVAILABLE" }); const started = fixture({ stdout: marker, code: 1 }); await assert.rejects(() => started.manager.remoteEditor("s", { operation: "finalize" }), { code: "FILE_RESULT_UNKNOWN" }); const noPython = fixture({ stdout: marker, code: 127 }); await assert.rejects(() => noPython.manager.remoteEditor("s", { operation: "read" }), { code: "FILE_DEPENDENCY_UNAVAILABLE" }); }); test("a remembered sudo password is reused only in memory for the same live session", async () => { const f = privilegedFixture(); await f.manager.remoteEditor("s", { operation: "read" }, { sudo: true, sudoPassword: "sudo-secret-'$value", rememberSudo: true }); assert.equal(f.manager.privilegedExecutor.hasCachedPassword("s"), true); await f.manager.remoteEditor("s", { operation: "read" }, { sudo: true }); assert.equal(f.writes.filter((text) => text === "sudo-secret-'$value\n").length, 2, "cached password answered the second prompt"); await assert.rejects(() => f.manager.remoteEditor("s", { operation: "read" }, { sudo: true, useCachedSudo: false }), { code: "SUDO_PASSWORD_REQUIRED" }); const notRemembered = privilegedFixture(); await notRemembered.manager.remoteEditor("s", { operation: "read" }, { sudo: true, sudoPassword: "sudo-secret-'$value" }); assert.equal(notRemembered.manager.privilegedExecutor.hasCachedPassword("s"), false); await f.manager.disconnect("s"); assert.equal(f.manager.privilegedExecutor.hasCachedPassword("s"), false, "disconnect drops the cache"); }); test("SSH loss after a privileged finalize was delivered is reported as unknown", async () => { const f = privilegedFixture({ dropAfterRequest: true }); await assert.rejects(() => f.manager.remoteEditor("s", { operation: "finalize" }, { sudo: true, sudoPassword: "sudo-secret-'$value" }), { code: "FILE_RESULT_UNKNOWN" }); assert.equal(f.record.pendingExec.size, 0); }); test("invalid replies and missing remote exit status fail closed", async () => { for (const stdout of ["", "{}", marker + "null", marker + "unexpected"]) { const f = fixture({ stdout }); await assert.rejects(() => f.manager.remoteEditor("s", { operation: "finalize" })); } const f = fixture({ code: null }); await assert.rejects(() => f.manager.remoteEditor("s", { operation: "finalize" }), { code: "FILE_RESULT_UNKNOWN" }); }); test("synchronous exec failure releases timers and pending requests", async () => { const manager = new SessionManager({}); const record = { client: { exec() { throw new Error("Disconnected"); } } }; await assert.rejects(() => manager._execFixed(record, "true", 5000)); assert.equal(record.pendingExec.size, 0); await assert.rejects(() => manager._execSudo(record, "true", "", 5000)); assert.equal(record.pendingExec.size, 0); }); test("SSH loss settles a pending editor save as unknown immediately", async () => { const f = fixture(); const record = f.manager.sessions.get("s"); record.decoder = { end: () => "" }; record.client.end = () => {}; record.client.exec = () => {}; const pending = assert.rejects(() => f.manager.remoteEditor("s", { operation: "finalize" }), { code: "FILE_RESULT_UNKNOWN" }); await f.manager.disconnect("s"); await pending; assert.equal(record.pendingExec.size, 0); }); test("host shell receives hostile paths as an exact JSON argument with no expansion", async (t) => { const fs = require("node:fs"), os = require("node:os"), path = require("node:path"), { execFile } = require("node:child_process"); if (process.platform === "win32") { t.skip("POSIX quoting belongs to the Linux server shell"); return; } const root = fs.mkdtempSync(path.join(os.tmpdir(), "chj-editor-shell-")); t.after(() => fs.rmSync(root, { recursive: true, force: true })); fs.writeFileSync(path.join(root, "python3"), '#!' + process.execPath + '\nconst value=JSON.parse(process.argv.at(-1)); process.stdout.write(JSON.stringify({ok:true,value}));\n', { mode: 0o700 }); const request = { operation: "read", path: "/etc/trailing ' ; $(touch " + root + "/EXPANDED) `echo bad` \\" }; const manager = new SessionManager({}); const record = { state: "connected", stream: {}, client: {}, platform: { os: "linux" } }; manager.sessions.set("s", record); // The real shell parses exactly the fixed command handed to ssh2.exec. manager._execFixed = (_record, command) => new Promise((resolve, reject) => { execFile("/bin/sh", ["-c", command], { shell: false, env: { ...process.env, PATH: root + ":" + process.env.PATH }, maxBuffer: 1024 * 1024 }, (error, stdout, stderr) => error ? reject(error) : resolve({ stdout, stderr })); }); assert.deepEqual(await manager.remoteEditor("s", request), request); assert.equal(fs.existsSync(path.join(root, "EXPANDED")), false); });