Published source
Download source ZIP
CH-J Server Manager
Browse directories and files for a specific application release.
Source is provided under the CH-J Proprietary Software License 1.14. Its availability does not change the license terms or grant additional rights.
1
"use strict";3
const test = require("node:test");4
const assert = require("node:assert/strict");5
const { EventEmitter } = require("node:events");6
const { SessionManager } = require("../src/main/sessions/sessionManager");7
const marker = "\x1eCHJ_EDITOR_START\n";9
function fixture({ username = "user", stdout = marker + JSON.stringify({ ok: true, value: { status: "saved" } }), code = 0, stderr = "" } = {}) {10
const commands = [], input = [];11
const manager = new SessionManager({});12
manager.sessions.set("s", { sessionId: "s", username, state: "connected", stream: {}, client: {13
exec(command, callback) {14
commands.push(command);15
const stream = new EventEmitter(); stream.stderr = new EventEmitter(); stream.end = (value) => input.push(value);16
callback(null, stream);17
queueMicrotask(() => { stream.emit("data", Buffer.from(stdout)); stream.stderr.emit("data", Buffer.from(stderr)); stream.emit("close", code); });18
}19
} });20
return { manager, commands, input };21
}23
test("editor sudo supports passwordless, password stdin and root without exposing credentials in commands", async () => {24
const request = { operation: "finalize", path: "/etc/test 'quoted' ; $(echo unsafe)" };25
const password = "sudo-secret-'$value";26
const f = fixture(); await f.manager.remoteEditor("s", request, { sudo: true, sudoPassword: password });27
assert.match(f.commands[0], /^sudo -S -p '' -- sh -c /);28
assert.equal(f.commands[0].includes(password), false); assert.deepEqual(f.input, [password + "\n"]);29
const passwordless = fixture(); await passwordless.manager.remoteEditor("s", request, { sudo: true });30
assert.match(passwordless.commands[0], /^sudo -n -- sh -c /);31
const root = fixture({ username: "root" }); await root.manager.remoteEditor("s", request, { sudo: true });32
assert.match(root.commands[0], /^sh -c /); assert.doesNotMatch(root.commands[0], /^sudo/);33
const plain = fixture(); await plain.manager.remoteEditor("s", request);34
assert.match(plain.commands[0], /^printf /); assert.doesNotMatch(plain.commands[0], /^sudo/);35
await assert.rejects(() => f.manager.remoteEditor("s", request, { sudoPassword: password }), { code: "FILE_SUDO_AUTHORIZATION_REQUIRED" });36
});38
test("sudo denial is distinguished from an uncertain command failure after sudo authorization", async () => {39
const denied = fixture({ stdout: "", code: 1, stderr: "sudo-secret: unauthorized" });40
await assert.rejects(() => denied.manager.remoteEditor("s", { operation: "finalize" }, { sudo: true, sudoPassword: "sudo-secret" }), (error) => {41
assert.equal(error.code, "FILE_SUDO_FAILED"); assert.doesNotMatch(error.message, /sudo-secret/); return true;42
});43
const started = fixture({ stdout: marker, code: 1 });44
await assert.rejects(() => started.manager.remoteEditor("s", { operation: "finalize" }, { sudo: true }), { code: "FILE_RESULT_UNKNOWN" });45
const noPython = fixture({ stdout: marker, code: 127 });46
await assert.rejects(() => noPython.manager.remoteEditor("s", { operation: "read" }), { code: "FILE_DEPENDENCY_UNAVAILABLE" });47
});49
test("invalid replies and missing remote exit status fail closed", async () => {50
for (const stdout of ["", "{}", marker + "null", marker + "unexpected"]) {51
const f = fixture({ stdout });52
await assert.rejects(() => f.manager.remoteEditor("s", { operation: "finalize" }));53
}54
const f = fixture({ code: null });55
await assert.rejects(() => f.manager.remoteEditor("s", { operation: "finalize" }), { code: "FILE_RESULT_UNKNOWN" });56
});58
test("synchronous exec failure releases timers and pending requests", async () => {59
const manager = new SessionManager({}); const record = { client: { exec() { throw new Error("Disconnected"); } } };60
await assert.rejects(() => manager._execFixed(record, "true", 5000));61
assert.equal(record.pendingExec.size, 0);62
await assert.rejects(() => manager._execSudo(record, "true", "", 5000));63
assert.equal(record.pendingExec.size, 0);64
});66
test("SSH loss settles a pending editor save as unknown immediately", async () => {67
const f = fixture(); const record = f.manager.sessions.get("s");68
record.decoder = { end: () => "" }; record.client.end = () => {}; record.client.exec = () => {};69
const pending = assert.rejects(() => f.manager.remoteEditor("s", { operation: "finalize" }), { code: "FILE_RESULT_UNKNOWN" });70
await f.manager.disconnect("s"); await pending;71
assert.equal(record.pendingExec.size, 0);72
});74
test("host shell receives hostile paths as an exact JSON argument with no expansion", async (t) => {75
const fs = require("node:fs"), os = require("node:os"), path = require("node:path"), { execFile } = require("node:child_process");76
if (process.platform === "win32") { t.skip("POSIX quoting belongs to the Linux server shell"); return; }77
const root = fs.mkdtempSync(path.join(os.tmpdir(), "chj-editor-shell-")); t.after(() => fs.rmSync(root, { recursive: true, force: true }));78
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 });79
const request = { operation: "read", path: "/etc/trailing ' ; $(touch " + root + "/EXPANDED) `echo bad` \\" };80
const manager = new SessionManager({});81
const record = { state: "connected", stream: {}, client: {} }; manager.sessions.set("s", record);82
// The real shell parses exactly the fixed command handed to ssh2.exec.83
manager._execFixed = (_record, command) => new Promise((resolve, reject) => {84
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 }));85
});86
assert.deepEqual(await manager.remoteEditor("s", request), request);87
assert.equal(fs.existsSync(path.join(root, "EXPANDED")), false);88
});SHA-256: f60c8921d93cde6713b67005a0b1e4e85ea1cd585a3959e879214c7f2259c1d3
Archive SHA-256: 5ac91caf4fa32a6fdb114f2430deed486fbe7489d5eea343d1f034169fafb5e0