CH-J Server Managerspráva serverů přes SSH
Menu
Vydané zdroje

CH-J Server Manager

Procházejte adresáře a soubory konkrétního vydání aplikace.

Stáhnout zdroje ZIP
CH-J Proprietary Software License 1.14

Zdroje jsou zveřejněny pod CH-J Proprietary Software License 1.14. Jejich dostupnost nemění licenční podmínky ani neposkytuje další oprávnění.

5,8 KB · 88 řádkůStáhnout soubor
1"use strict";
3const test = require("node:test");
4const assert = require("node:assert/strict");
5const { EventEmitter } = require("node:events");
6const { SessionManager } = require("../src/main/sessions/sessionManager");
7const marker = "\x1eCHJ_EDITOR_START\n";
9function 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 };
23test("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});
38test("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});
49test("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});
58test("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});
66test("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});
74test("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

SHA-256 archivu: 5ac91caf4fa32a6fdb114f2430deed486fbe7489d5eea343d1f034169fafb5e0