Sumber yang diterbitkan
Muat turun ZIP sumber
CH-J Server Manager
Layari direktori dan fail untuk keluaran aplikasi tertentu.
CH-J / Server Manager
377 fail
core-20261019T111616Z-160a00bfcddc
Ditambah: · Dibina:
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: {}, platform: { os: "linux" }, 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
const READY = "\x1eCHJ_READY\n";24
const PROMPT = "[chj-sudo-auth]";25
// Simulated server for the shared PrivilegedExecutor protocol: probe, sudo prompt, READY, stdin frame.26
function privilegedFixture({ username = "user", sudo = "password", correct = "sudo-secret-'$value", python = true, reply = { ok: true, value: { status: "saved" } }, dropAfterRequest = false } = {}) {27
const commands = [], writes = [], requests = [];28
const manager = new SessionManager({});29
const record = { sessionId: "s", username, state: "connected", stream: {}, platform: { os: "linux" }, client: {30
exec(command, callback) {31
commands.push(command);32
const stream = new EventEmitter(); stream.stderr = new EventEmitter();33
let ready = false; let closed = false;34
const out = (text) => queueMicrotask(() => stream.emit("data", Buffer.from(text)));35
const err = (text) => queueMicrotask(() => stream.stderr.emit("data", Buffer.from(text)));36
const close = (code) => { if (closed) return; closed = true; queueMicrotask(() => stream.emit("close", code)); };37
const becomeReady = () => { ready = true; out(READY); };38
stream.write = (data) => {39
const text = String(data); writes.push(text);40
if (!ready) {41
if (text === correct + "\n") becomeReady(); else { err("Sorry, try again.\n" + PROMPT); }42
return;43
}44
const frame = /^CHJREQ:(.*)\n$/.exec(text);45
if (!frame) return;46
requests.push(JSON.parse(Buffer.from(frame[1], "base64").toString("utf8")));47
if (dropAfterRequest) { manager.disconnect("s"); return; }48
if (!python) { out("\x1eCHJ_ERR=DEPENDENCY_UNAVAILABLE\n"); close(127); return; }49
out("\x1eCHJ_EDITOR_START\n" + JSON.stringify(reply)); close(0);50
};51
stream.end = () => { if (!ready) close(1); };52
stream.close = () => close(1);53
callback(null, stream);54
if (command.includes("sudoNonInteractive")) {55
const nopass = sudo === "nopasswd";56
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`);57
close(0);58
} else if (command.startsWith("printf ")) { out("\x1eCHJ_EDITOR_START\n" + JSON.stringify(reply)); close(0); }59
else if (command.startsWith("LC_ALL=C sudo -S -p ")) { if (sudo === "nopasswd") becomeReady(); else err(PROMPT); }60
else if (command.startsWith("LC_ALL=C sh -c ")) becomeReady();61
}62
} };63
record.decoder = { end: () => "" }; record.client.end = () => {};64
manager.sessions.set("s", record);65
return { manager, record, commands, writes, requests };66
}68
test("editor sudo uses the shared PrivilegedExecutor: password only after the prompt, request as stdin frame", async () => {69
const request = { operation: "finalize", path: "/etc/test 'quoted' ; $(echo unsafe)" };70
const password = "sudo-secret-'$value";71
const f = privilegedFixture();72
assert.deepEqual(await f.manager.remoteEditor("s", request, { sudo: true, sudoPassword: password }), { status: "saved" });73
const editorCommand = f.commands.find((command) => command.startsWith("LC_ALL=C sudo"));74
assert.match(editorCommand, /^LC_ALL=C sudo -S -p '\[chj-sudo-auth\]' -- sh -c /);75
assert.equal(f.commands.some((command) => command.includes(password)), false, "password never in argv");76
assert.equal(f.commands.some((command) => command.includes("echo unsafe")), false, "request travels on stdin, not argv");77
assert.equal(f.writes[0], password + "\n");78
assert.deepEqual(f.requests, [request]);80
const passwordless = privilegedFixture({ sudo: "nopasswd" });81
await passwordless.manager.remoteEditor("s", request, { sudo: true, sudoPassword: password });82
assert.equal(passwordless.writes.some((text) => text.includes(password)), false, "NOPASSWD sudo never receives the password");84
const root = privilegedFixture({ username: "root" });85
await root.manager.remoteEditor("s", request, { sudo: true });86
assert.equal(root.commands.some((command) => /\bsudo -S\b/.test(command)), false, "root sessions never call sudo");87
assert.deepEqual(root.requests, [request]);89
const plain = privilegedFixture();90
await plain.manager.remoteEditor("s", request);91
assert.match(plain.commands[0], /^printf /);92
await assert.rejects(() => f.manager.remoteEditor("s", request, { sudoPassword: password }), { code: "FILE_SUDO_AUTHORIZATION_REQUIRED" });93
});95
test("sudo refusals are definite and classified; credentials never reach error messages", async () => {96
const wrong = privilegedFixture();97
await assert.rejects(() => wrong.manager.remoteEditor("s", { operation: "finalize" }, { sudo: true, sudoPassword: "wrong-secret" }), (error) => {98
assert.equal(error.code, "SUDO_PASSWORD_INCORRECT"); assert.doesNotMatch(error.message + JSON.stringify(error.details || {}), /wrong-secret/); return true;99
});100
assert.equal(wrong.requests.length, 0, "helper never started");101
await assert.rejects(() => privilegedFixture().manager.remoteEditor("s", { operation: "read" }, { sudo: true }), { code: "SUDO_PASSWORD_REQUIRED" });102
await assert.rejects(() => privilegedFixture({ sudo: "none" }).manager.remoteEditor("s", { operation: "read" }, { sudo: true, sudoPassword: "x" }), { code: "SUDO_NOT_PERMITTED" });103
await assert.rejects(() => privilegedFixture({ python: false }).manager.remoteEditor("s", { operation: "read" }, { sudo: true, sudoPassword: "sudo-secret-'$value" }), { code: "FILE_DEPENDENCY_UNAVAILABLE" });104
const started = fixture({ stdout: marker, code: 1 });105
await assert.rejects(() => started.manager.remoteEditor("s", { operation: "finalize" }), { code: "FILE_RESULT_UNKNOWN" });106
const noPython = fixture({ stdout: marker, code: 127 });107
await assert.rejects(() => noPython.manager.remoteEditor("s", { operation: "read" }), { code: "FILE_DEPENDENCY_UNAVAILABLE" });108
});110
test("a remembered sudo password is reused only in memory for the same live session", async () => {111
const f = privilegedFixture();112
await f.manager.remoteEditor("s", { operation: "read" }, { sudo: true, sudoPassword: "sudo-secret-'$value", rememberSudo: true });113
assert.equal(f.manager.privilegedExecutor.hasCachedPassword("s"), true);114
await f.manager.remoteEditor("s", { operation: "read" }, { sudo: true });115
assert.equal(f.writes.filter((text) => text === "sudo-secret-'$value\n").length, 2, "cached password answered the second prompt");116
await assert.rejects(() => f.manager.remoteEditor("s", { operation: "read" }, { sudo: true, useCachedSudo: false }), { code: "SUDO_PASSWORD_REQUIRED" });117
const notRemembered = privilegedFixture();118
await notRemembered.manager.remoteEditor("s", { operation: "read" }, { sudo: true, sudoPassword: "sudo-secret-'$value" });119
assert.equal(notRemembered.manager.privilegedExecutor.hasCachedPassword("s"), false);120
await f.manager.disconnect("s");121
assert.equal(f.manager.privilegedExecutor.hasCachedPassword("s"), false, "disconnect drops the cache");122
});124
test("SSH loss after a privileged finalize was delivered is reported as unknown", async () => {125
const f = privilegedFixture({ dropAfterRequest: true });126
await assert.rejects(() => f.manager.remoteEditor("s", { operation: "finalize" }, { sudo: true, sudoPassword: "sudo-secret-'$value" }), { code: "FILE_RESULT_UNKNOWN" });127
assert.equal(f.record.pendingExec.size, 0);128
});130
test("invalid replies and missing remote exit status fail closed", async () => {131
for (const stdout of ["", "{}", marker + "null", marker + "unexpected"]) {132
const f = fixture({ stdout });133
await assert.rejects(() => f.manager.remoteEditor("s", { operation: "finalize" }));134
}135
const f = fixture({ code: null });136
await assert.rejects(() => f.manager.remoteEditor("s", { operation: "finalize" }), { code: "FILE_RESULT_UNKNOWN" });137
});139
test("synchronous exec failure releases timers and pending requests", async () => {140
const manager = new SessionManager({}); const record = { client: { exec() { throw new Error("Disconnected"); } } };141
await assert.rejects(() => manager._execFixed(record, "true", 5000));142
assert.equal(record.pendingExec.size, 0);143
await assert.rejects(() => manager._execSudo(record, "true", "", 5000));144
assert.equal(record.pendingExec.size, 0);145
});147
test("SSH loss settles a pending editor save as unknown immediately", async () => {148
const f = fixture(); const record = f.manager.sessions.get("s");149
record.decoder = { end: () => "" }; record.client.end = () => {}; record.client.exec = () => {};150
const pending = assert.rejects(() => f.manager.remoteEditor("s", { operation: "finalize" }), { code: "FILE_RESULT_UNKNOWN" });151
await f.manager.disconnect("s"); await pending;152
assert.equal(record.pendingExec.size, 0);153
});155
test("host shell receives hostile paths as an exact JSON argument with no expansion", async (t) => {156
const fs = require("node:fs"), os = require("node:os"), path = require("node:path"), { execFile } = require("node:child_process");157
if (process.platform === "win32") { t.skip("POSIX quoting belongs to the Linux server shell"); return; }158
const root = fs.mkdtempSync(path.join(os.tmpdir(), "chj-editor-shell-")); t.after(() => fs.rmSync(root, { recursive: true, force: true }));159
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 });160
const request = { operation: "read", path: "/etc/trailing ' ; $(touch " + root + "/EXPANDED) `echo bad` \\" };161
const manager = new SessionManager({});162
const record = { state: "connected", stream: {}, client: {}, platform: { os: "linux" } }; manager.sessions.set("s", record);163
// The real shell parses exactly the fixed command handed to ssh2.exec.164
manager._execFixed = (_record, command) => new Promise((resolve, reject) => {165
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 }));166
});167
assert.deepEqual(await manager.remoteEditor("s", request), request);168
assert.equal(fs.existsSync(path.join(root, "EXPANDED")), false);169
});SHA-256
099499d0fdc2587f95c2e50707444e43ad2ca3fad46903d680f37e3109921954CH-J Proprietary Software License 1.14 · Kod sumber disediakan di bawah CH-J Proprietary Software License 1.14. Ketersediaannya tidak mengubah terma lesen atau memberikan hak tambahan.
SHA-256 arkib
a9605c0870a223b6a8542bb0705edd51d8b04f3069582653417a8397fabcc09a