Vydané zdroje
Stáhnout zdroje ZIP
CH-J Server Manager
Procházejte adresáře a soubory konkrétního vydání aplikace.
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í.
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, normalizeNginxConfigPath, parseNginxInspectionOutput, parseSystemMetricsOutput } = require("../src/main/sessions/sessionManager");7
const lookupHost = async () => [{ address: "192.0.2.10", family: 4 }];9
class 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() {}19
}21
class 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() {}36
}38
test("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
});74
test("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
});93
test("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
});124
test("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.host159
});160
});162
test("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: true207
});208
});210
test("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
});217
test("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
});257
test("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
});306
test("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
});321
test("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 archivu: 5ac91caf4fa32a6fdb114f2430deed486fbe7489d5eea343d1f034169fafb5e0