CH-J Server ManagerSerververwaltung über SSH
Menü
Veröffentlichte Quellen

CH-J Server Manager

Durchsuchen Sie Verzeichnisse und Dateien einer bestimmten Anwendungsausgabe.

Quellen als ZIP herunterladen
CH-J Proprietary Software License 1.14

Die Quellen werden unter der CH-J Proprietary Software License 1.14 bereitgestellt. Ihre Verfügbarkeit ändert die Lizenzbedingungen nicht und gewährt keine zusätzlichen Rechte.

16,9 KB · 329 ZeilenDatei herunterladen
1"use strict";
3const test = require("node:test");
4const assert = require("node:assert/strict");
5const { EventEmitter } = require("node:events");
6const { SessionManager, normalizeNginxConfigPath, parseNginxInspectionOutput, parseSystemMetricsOutput } = require("../src/main/sessions/sessionManager");
7const lookupHost = async () => [{ address: "192.0.2.10", family: 4 }];
9class FakeStream extends EventEmitter {
10 constructor() {
11 super();
12 this.stderr = new EventEmitter();
13 this.writes = [];
14 this.window = null;
15 }
16 write(value) { this.writes.push(value); }
17 setWindow(rows, cols) { this.window = { rows, cols }; }
18 end() {}
21class FakeClient extends EventEmitter {
22 constructor(fingerprint) {
23 super();
24 this.fingerprint = fingerprint;
25 this.stream = new FakeStream();
26 }
27 connect(config) {
28 this.config = config;
29 setImmediate(() => {
30 if (!config.hostVerifier(this.fingerprint)) this.emit("error", new Error("Host key verification failed"));
31 else this.emit("ready");
32 });
33 }
34 shell(_options, callback) { setImmediate(() => callback(null, this.stream)); }
35 end() {}
38test("session manager requires host-key trust before opening a shell", async () => {
39 const fingerprint = "cd".repeat(32);
40 let knownFingerprint = null;
41 let latestClient;
42 const profile = { id: "profile-1", label: "Test", host: "server.local", port: 22, username: "root", authMethod: "password" };
43 const profileService = {
44 get: () => profile,
45 getHostKey: () => knownFingerprint,
46 trustHostKey: (_host, _port, value) => { knownFingerprint = value; return { fingerprint: value }; },
47 markUsed: () => {}
48 };
49 const manager = new SessionManager({ latencyMonitor: { start: () => () => {} },
50 profileService,
51 lookupHost,
52 clientFactory: () => { latestClient = new FakeClient(fingerprint); return latestClient; }
53 });
55 await assert.rejects(
56 () => manager.connect({ sessionId: "terminal-1", profileId: profile.id, password: "secret" }),
57 { code: "HOST_KEY_UNKNOWN", fingerprint }
58 );
59 manager.trustPending({ sessionId: "terminal-1", profileId: profile.id, fingerprint });
60 const connected = await manager.connect({ sessionId: "terminal-1", profileId: profile.id, password: "secret", cols: 120, rows: 40 });
61 assert.equal(connected.state, "connected");
63 let output = "";
64 manager.on("data", (payload) => { output += payload.data; });
65 latestClient.stream.emit("data", Buffer.from("ready\n"));
66 manager.write("terminal-1", "uptime\r");
67 assert.deepEqual(latestClient.stream.writes, ["uptime\r"]);
68 assert.deepEqual(manager.resize("terminal-1", 140, 50), { cols: 140, rows: 50 });
69 assert.deepEqual(latestClient.stream.window, { rows: 50, cols: 140 });
70 assert.equal(output, "ready\n");
71 assert.equal((await manager.disconnect("terminal-1")).disconnected, true);
72});
74test("session manager uses the encrypted stored password when no temporary password is entered", async () => {
75 const fingerprint = "ab".repeat(32);
76 let client;
77 const profile = { id: "profile-1", label: "Saved", host: "server.local", port: 22, username: "root", authMethod: "password" };
78 const manager = new SessionManager({ latencyMonitor: { start: () => () => {} },
79 profileService: {
80 get: () => profile,
81 getHostKey: () => fingerprint,
82 getStoredPassword: () => "stored-secret",
83 markUsed: () => {}
84 },
85 lookupHost,
86 clientFactory: () => { client = new FakeClient(fingerprint); return client; }
87 });
88 assert.equal((await manager.connect({ sessionId: "terminal-1", profileId: profile.id })).state, "connected");
89 assert.equal(client.config.password, "stored-secret");
90 await manager.disconnect("terminal-1");
91});
93test("session manager replaces a changed host key only after explicit confirmation of both fingerprints", async () => {
94 const known = "ab".repeat(32);
95 const changed = "cd".repeat(32);
96 let knownFingerprint = known;
97 const profile = { id: "profile-1", label: "Test", host: "server.local", port: 22, username: "root", authMethod: "password" };
98 const profileService = {
99 get: () => profile,
100 getHostKey: () => knownFingerprint,
101 trustHostKey: (_host, _port, value) => { knownFingerprint = value; return { fingerprint: value }; },
102 markUsed: () => {}
103 };
104 const manager = new SessionManager({ latencyMonitor: { start: () => () => {} }, profileService, lookupHost, clientFactory: () => new FakeClient(changed) });
106 await assert.rejects(
107 () => manager.connect({ sessionId: "terminal-1", profileId: profile.id, password: "secret" }),
108 { code: "HOST_KEY_MISMATCH", fingerprint: changed, knownFingerprint: known }
109 );
110 assert.throws(
111 () => manager.trustPending({ sessionId: "terminal-1", profileId: profile.id, fingerprint: changed }),
112 { code: "HOST_KEY_REPLACEMENT_CONFIRMATION_REQUIRED" }
113 );
114 assert.throws(
115 () => manager.trustPending({ sessionId: "terminal-1", profileId: profile.id, fingerprint: changed, knownFingerprint: "ef".repeat(32), replaceKnown: true }),
116 { code: "HOST_KEY_REPLACEMENT_CONFIRMATION_REQUIRED" }
117 );
118 manager.trustPending({ sessionId: "terminal-1", profileId: profile.id, fingerprint: changed, knownFingerprint: known, replaceKnown: true });
119 assert.equal(knownFingerprint, changed);
120 assert.equal((await manager.connect({ sessionId: "terminal-1", profileId: profile.id, password: "secret" })).state, "connected");
121 await manager.disconnect("terminal-1");
122});
124test("session manager resolves DNS names, prefers IPv4 and reports missing DNS records", async () => {
125 const fingerprint = "ef".repeat(32);
126 let client;
127 const profile = { id: "profile-dns", label: "DNS", host: "server.example.test", port: 22, username: "root", authMethod: "password" };
128 const profileService = {
129 get: () => profile,
130 getHostKey: () => fingerprint,
131 getStoredPassword: () => null,
132 markUsed: () => {}
133 };
134 const manager = new SessionManager({ latencyMonitor: { start: () => () => {} },
135 profileService,
136 lookupHost: async () => [{ address: "2001:db8::10", family: 6 }, { address: "192.0.2.10", family: 4 }],
137 clientFactory: () => { client = new FakeClient(fingerprint); return client; }
138 });
139 await manager.connect({ sessionId: "terminal-dns", profileId: profile.id, password: "secret" });
140 assert.equal(client.config.host, "192.0.2.10");
141 await manager.disconnect("terminal-dns");
143 const fallback = new SessionManager({ latencyMonitor: { start: () => () => {} },
144 profileService,
145 lookupHost: async () => { const error = new Error("system resolver failed"); error.code = "ENOTFOUND"; throw error; },
146 resolve4: async () => ["198.51.100.20"],
147 resolve6: async () => ["2001:db8::20"],
148 clientFactory: () => { client = new FakeClient(fingerprint); return client; }
149 });
150 await fallback.connect({ sessionId: "terminal-fallback", profileId: profile.id, password: "secret" });
151 assert.equal(client.config.host, "198.51.100.20");
152 await fallback.disconnect("terminal-fallback");
154 const notFound = async () => { const error = new Error("not found"); error.code = "ENOTFOUND"; throw error; };
155 const missing = new SessionManager({ latencyMonitor: { start: () => () => {} }, profileService, lookupHost: notFound, resolve4: notFound, resolve6: notFound });
156 await assert.rejects(() => missing.connect({ sessionId: "terminal-missing", profileId: profile.id, password: "secret" }), {
157 code: "SSH_DNS_RESOLUTION_FAILED",
158 host: profile.host
159 });
160});
162test("system metrics use a fixed command on an existing SSH session", async () => {
163 const manager = new SessionManager({ latencyMonitor: { start: () => () => {} }, profileService: {} });
164 const stream = new EventEmitter();
165 stream.stderr = new EventEmitter();
166 const client = {
167 exec(command, callback) {
168 assert.match(command, /\/proc\/meminfo/);
169 assert.match(command, /\/proc\/stat/);
170 assert.match(command, /\/sys\/class\/net/);
171 assert.match(command, /hw\.logicalcpu/);
172 assert.match(command, /networkInterface=/);
173 callback(null, stream);
174 queueMicrotask(() => {
175 stream.emit("data", Buffer.from([
176 "platform=Linux", "kernel=6.8", "hostname=test", "uptimeSeconds=3600", "load1=0.25",
177 "memoryTotal=1000", "memoryAvailable=400", "swapTotal=800", "swapFree=300", "disk=2000:500:1500",
178 "cpuUsage=37.5", "cpuArchitecture=x86_64", "cpuModel=AMD EPYC Test", "cpuLogical=8", "cpuPhysical=4",
179 "cpuSockets=1", "cpuCoreTypes=P-core:2,E-core:2", "networkDefault=eth0",
180 "networkInterface=eth0|up|aa:bb:cc:dd:ee:ff|1000|2000|192.168.10.54/24,fe80::1/64",
181 "networkInterface=eth1|down||0|0|", ""
182 ].join("\n")));
183 stream.emit("close", 0);
184 });
185 }
186 };
187 manager.sessions.set("terminal-main", { sessionId: "terminal-main", profileId: "p1", host: "test", state: "connected", stream: {}, client });
188 const metrics = await manager.readSystemMetrics("terminal-main");
189 assert.equal(metrics.platform, "Linux");
190 assert.equal(metrics.memory.available, 400);
191 assert.deepEqual(metrics.swap, { total: 800, used: 500, free: 300 });
192 assert.equal(metrics.disk.used, 500);
193 assert.deepEqual(metrics.cpu, {
194 usagePercent: 37.5,
195 architecture: "x86_64",
196 model: "AMD EPYC Test",
197 logicalCores: 8,
198 physicalCores: 4,
199 sockets: 1,
200 coreTypes: [{ name: "P-core", count: 2 }, { name: "E-core", count: 2 }]
201 });
202 assert.equal(metrics.network.defaultInterface, "eth0");
203 assert.equal(metrics.network.interfaces.length, 2);
204 assert.deepEqual(metrics.network.interfaces[0], {
205 name: "eth0", state: "up", mac: "aa:bb:cc:dd:ee:ff", rxBytes: 1000, txBytes: 2000,
206 addresses: ["192.168.10.54/24", "fe80::1/64"], isDefault: true
207 });
208});
210test("legacy system metrics remain compatible without CPU and network fields", () => {
211 const metrics = parseSystemMetricsOutput("platform=Linux\nhostname=legacy\nmemoryTotal=100\nmemoryAvailable=40\ndisk=200:50:150\n");
212 assert.equal(metrics.cpu.logicalCores, 0);
213 assert.equal(metrics.cpu.model, "unknown");
214 assert.deepEqual(metrics.network, { defaultInterface: "", interfaces: [] });
215});
217test("user capability parses accounts and allows only bounded sudo actions", async () => {
218 const manager = new SessionManager({ latencyMonitor: { start: () => () => {} }, profileService: {} });
219 const commands = [];
220 const inputs = [];
221 const usersOutput = [
222 "root:x:0:0:root:/root:/bin/bash",
223 "admin:x:1000:1000:Admin User:/home/admin:/bin/bash",
224 "bob:x:1001:1001:Bob User:/home/bob:/bin/bash",
225 "\x1eCHJ_GROUPS",
226 "sudo:x:27:admin",
227 "\x1eCHJ_STATUS",
228 "root P 01/01/2026 0 99999 7 -1",
229 "admin P 01/01/2026 0 99999 7 -1",
230 "bob L 01/01/2026 0 99999 7 -1",
231 ""
232 ].join("\n");
233 const client = {
234 exec(command, callback) {
235 commands.push(command);
236 const stream = new EventEmitter();
237 stream.stderr = new EventEmitter();
238 stream.end = (input = "") => inputs.push(input);
239 callback(null, stream);
240 queueMicrotask(() => {
241 if (!command.startsWith("sudo ")) stream.emit("data", Buffer.from(usersOutput));
242 stream.emit("close", 0);
243 });
244 }
245 };
246 manager.sessions.set("terminal-users", { sessionId: "terminal-users", profileId: "p1", host: "server.test", username: "admin", state: "connected", stream: {}, client });
247 const users = await manager.readUsers("terminal-users");
248 assert.deepEqual(users.map((user) => [user.username, user.admin, user.locked]), [["root", true, false], ["admin", true, false], ["bob", false, true]]);
249 const result = await manager.manageUser("terminal-users", { action: "unlock", username: "bob", sudoPassword: "sudo-secret" });
250 assert.equal(result.users[2].username, "bob");
251 assert.match(commands[1], /^sudo -S -p '' -- sh -c 'usermod --unlock -- bob'$/);
252 assert.equal(inputs[0], "sudo-secret\n");
253 await assert.rejects(() => manager.manageUser("terminal-users", { action: "delete", username: "admin" }), { code: "PROTECTED_USER" });
254 await assert.rejects(() => manager.manageUser("terminal-users", { action: "lock", username: "bob;id" }), { code: "USERNAME_INVALID" });
255});
257test("NGINX capability restricts paths, validates before reload and hides configuration from shell syntax", async () => {
258 assert.equal(normalizeNginxConfigPath("/etc/nginx/sites-enabled/default"), "/etc/nginx/sites-enabled/default");
259 assert.throws(() => normalizeNginxConfigPath("/etc/nginx/../../etc/shadow"), { code: "NGINX_CONFIG_PATH_INVALID" });
260 assert.throws(() => normalizeNginxConfigPath("/etc/nginx/sites-enabled/default;id"), { code: "NGINX_CONFIG_PATH_INVALID" });
261 const parsed = parseNginxInspectionOutput("installed=1\nversion=nginx/1.24.0\nserviceState=active\nconfigPath=/etc/nginx/nginx.conf\nconfig=file|/etc/nginx/nginx.conf\nconfig=symlink|/etc/nginx/sites-enabled/default\n");
262 assert.equal(parsed.installed, true);
263 assert.deepEqual(parsed.configs.map((item) => [item.path, item.writable]), [["/etc/nginx/nginx.conf", true], ["/etc/nginx/sites-enabled/default", false]]);
265 const manager = new SessionManager({ latencyMonitor: { start: () => () => {} }, profileService: {} });
266 const commands = [];
267 const inputs = [];
268 const client = {
269 exec(command, callback) {
270 commands.push(command);
271 const stream = new EventEmitter();
272 stream.stderr = new EventEmitter();
273 stream.end = (input = "") => inputs.push(input);
274 callback(null, stream);
275 queueMicrotask(() => {
276 let stdout = "";
277 if (command.includes("nginx_bin=$(command -v nginx")) stdout = "installed=1\nversion=nginx/1.24.0\nserviceState=active\nconfigPath=/etc/nginx/nginx.conf\nconfig=file|/etc/nginx/nginx.conf\n";
278 else if (command.includes("cat --")) stdout = "events {}\nhttp {}\n";
279 else if (command.includes("CHJ_EXIT")) stdout = "nginx: configuration file test is successful\n\x1eCHJ_EXIT=0\n";
280 else if (command.includes("CHJ_BACKUP")) stdout = "nginx: configuration file test is successful\n\x1eCHJ_BACKUP=/etc/nginx/nginx.conf.chj-backup-20260809T010203\n";
281 else if (command.includes("systemctl reload nginx")) stdout = "nginx: configuration file test is successful\nNGINX configuration reloaded gracefully.\n";
282 stream.emit("data", Buffer.from(stdout));
283 stream.emit("close", 0);
284 });
285 }
286 };
287 manager.sessions.set("terminal-nginx", { sessionId: "terminal-nginx", profileId: "p1", host: "web.test", username: "admin", state: "connected", stream: {}, client });
289 const inspection = await manager.inspectNginx("terminal-nginx");
290 assert.equal(inspection.serviceState, "active");
291 const config = await manager.readNginxConfig("terminal-nginx", { path: "/etc/nginx/nginx.conf" });
292 assert.equal(config.text, "events {}\nhttp {}\n");
293 assert.equal((await manager.testNginxConfig("terminal-nginx", { sudoPassword: "secret" })).ok, true);
294 const source = "events {}\nhttp { server { listen 80; } }\n";
295 const saved = await manager.saveNginxConfig("terminal-nginx", { path: "/etc/nginx/nginx.conf", text: source, sudoPassword: "secret" });
296 assert.match(saved.backupPath, /\.chj-backup-/);
297 assert.ok(!commands.at(-1).includes(source));
298 assert.match(commands.at(-1), /base64 --decode/);
299 assert.match(commands.at(-1), /nginx -t/);
300 assert.match(commands.at(-1), /\[ ! -L/);
301 await assert.rejects(() => manager.reloadNginx("terminal-nginx", { sudoPassword: "secret" }), { code: "NGINX_RELOAD_CONFIRMATION_REQUIRED" });
302 assert.equal((await manager.reloadNginx("terminal-nginx", { sudoPassword: "secret", confirm: true })).ok, true);
303 assert.equal(inputs.filter((value) => value === "secret\n").length, 3);
304});
306test("privileged bounded operations run directly in an authenticated root SSH session", async () => {
307 const manager = new SessionManager({ latencyMonitor: { start: () => () => {} }, profileService: {} });
308 const commands = [];
309 const client = { exec(command, callback) {
310 commands.push(command);
311 const stream = new EventEmitter(); stream.stderr = new EventEmitter(); stream.end = () => {};
312 callback(null, stream);
313 queueMicrotask(() => { stream.emit("data", Buffer.from("ok\n\x1eCHJ_EXIT=0\n")); stream.emit("close", 0); });
314 } };
315 manager.sessions.set("root-nginx", { sessionId: "root-nginx", host: "web.test", username: "root", state: "connected", stream: {}, client });
316 assert.equal((await manager.testNginxConfig("root-nginx")).ok, true);
317 assert.match(commands[0], /^sh -c /);
318 assert.doesNotMatch(commands[0], /sudo/);
319});
321test("macOS route and permission errors retain details and get local-network guidance codes", () => {
322 const manager = new SessionManager({ profileService: {} });
323 const record = { sessionId: "test", profileId: "test", host: "192.168.10.138", port: 22 };
324 for (const [code, macCode] of [["EHOSTUNREACH", "SSH_MAC_NETWORK_UNREACHABLE"], ["ENETUNREACH", "SSH_MAC_NETWORK_UNREACHABLE"], ["EPERM", "SSH_LOCAL_NETWORK_DENIED"]]) {
325 const error = manager._normalizeConnectionError(Object.assign(new Error(`connect ${code} 192.168.10.138:22`), { code }), record);
326 assert.equal(error.code, process.platform === "darwin" ? macCode : "SSH_CONNECTION_FAILED");
327 assert.match(error.message, new RegExp(code)); assert.equal(error.host, record.host);
328 }
329});

SHA-256: 60771ff7ea5b696c5065b8e3fba190cbf4206ab9648b24c30a0361ee88c73b1e

SHA-256 des Archivs: 5ac91caf4fa32a6fdb114f2430deed486fbe7489d5eea343d1f034169fafb5e0