Sors ippubblikat
Sors ta' tniżżil ZIP
CH-J Server Manager
direttorji browse u fajls għal rilaxx applikazzjoni speċifika.
CH-J / Server Manager
377 fajls
core-20261019T111616Z-160a00bfcddc
Miżjud: · Buillt:
1
"use strict";3
// System Monitor + chj.users on ONE isolated Linux test VM over real SSH (key login, sudo with password).4
// Never point this at a production server: it creates/deletes a swap file, a test account and changes sshd policy5
// (always reverted). Environment: CHJ_VM_HOST, CHJ_VM_FINGERPRINT (SHA256:...), CHJ_VM_KEY (private key path),6
// CHJ_VM_USER (default ai) and CHJ_VM_SUDO_PASSWORD or CHJ_VM_SUDO_PASSWORD_FILE + CHJ_VM_SUDO_PASSWORD_LINE7
// ("name = value" line read at runtime, never printed).8
const test = require("node:test");9
const assert = require("node:assert/strict");10
const fs = require("node:fs");11
const os = require("node:os");12
const path = require("node:path");13
const { execFileSync, spawnSync } = require("node:child_process");14
const { SessionManager } = require("../src/main/sessions/sessionManager");15
const { PrivilegedExecutor } = require("../src/main/sessions/privilegedExec");16
const { VirtualMemoryService } = require("../src/main/system/virtualMemoryService");17
const { UserAdminService, createUserAdminApi } = require("../src/main/users/userAdminService");18
const { detectPlatform } = require("../src/main/sessions/platformDetect");20
const env = process.env;21
const enabled = Boolean(env.CHJ_VM_HOST && env.CHJ_VM_FINGERPRINT && env.CHJ_VM_KEY && (env.CHJ_VM_SUDO_PASSWORD || env.CHJ_VM_SUDO_PASSWORD_FILE));22
const skip = enabled ? false : "Set CHJ_VM_* to run against an isolated Linux test VM.";23
const sudoPassword = !enabled ? "" : env.CHJ_VM_SUDO_PASSWORD24
|| fs.readFileSync(env.CHJ_VM_SUDO_PASSWORD_FILE, "utf8").split("\n")[Number(env.CHJ_VM_SUDO_PASSWORD_LINE || 1) - 1].split("=").slice(1).join("=").trim();25
const vmUser = env.CHJ_VM_USER || "ai";26
const host = env.CHJ_VM_HOST;27
const SWAP = "/swapfile-chjvmtest";28
const USER = "chjvmtest";29
let keyDir; let knownHosts;31
function keygen(name) {32
const file = path.join(keyDir, name);33
execFileSync("ssh-keygen", ["-q", "-t", "ed25519", "-N", "", "-C", `chj-vm-${name}`, "-f", file]);34
return { privatePath: file, publicKey: fs.readFileSync(`${file}.pub`, "utf8").trim() };35
}36
function remote(command) {37
return execFileSync("ssh", ["-i", env.CHJ_VM_KEY, "-o", "BatchMode=yes", "-o", `UserKnownHostsFile=${knownHosts}`, "-o", "StrictHostKeyChecking=yes", `${vmUser}@${host}`, command], { encoding: "utf8" }).trim();38
}39
function canLogin(user, key, address = host) {40
return spawnSync("ssh", ["-i", key, "-o", "BatchMode=yes", "-o", "IdentitiesOnly=yes", "-o", "PreferredAuthentications=publickey", "-o", `UserKnownHostsFile=${knownHosts}`, "-o", "StrictHostKeyChecking=yes", "-o", "HostKeyAlias=" + host, "-o", "ConnectTimeout=8", `${user}@${address}`, "exit 0"], { encoding: "utf8" }).status === 0;41
}43
test("System Monitor and chj.users on an isolated Linux test VM", { skip, timeout: 1200000 }, async (t) => {44
keyDir = fs.mkdtempSync(path.join(os.tmpdir(), "chj-vm-"));45
knownHosts = path.join(keyDir, "known_hosts");46
execFileSync("sh", ["-c", `ssh-keyscan -t ed25519 ${host} > '${knownHosts}' 2>/dev/null`]);47
assert.equal(execFileSync("ssh-keygen", ["-lf", knownHosts], { encoding: "utf8" }).split(" ")[1], env.CHJ_VM_FINGERPRINT, "documented host key");48
const hostKeys = new Map();49
const profileService = {50
get: () => ({ id: "vm", label: "vm", host, port: 22, username: vmUser, authMethod: "privateKey", privateKeyPath: env.CHJ_VM_KEY }),51
getHostKey: (h, p) => hostKeys.get(`${h}:${p}`) || null, trustHostKey: (h, p, fp) => hostKeys.set(`${h}:${p}`, fp), getStoredPassword: () => "", markUsed() {}52
};53
const manager = new SessionManager({ profileService });54
const latency = new Promise((resolve) => manager.on("state", (state) => { if (state.latency?.measuredAt) resolve(state.latency); }));55
try { await manager.connect({ sessionId: "vm", profileId: "vm" }); }56
catch (error) {57
if (error.code !== "HOST_KEY_UNKNOWN") throw error;58
assert.equal(`SHA256:${Buffer.from(error.fingerprint, "hex").toString("base64").replace(/=+$/, "")}`, env.CHJ_VM_FINGERPRINT, "app sees the documented host key");59
manager.trustPending({ sessionId: "vm", profileId: "vm", fingerprint: error.fingerprint });60
await manager.connect({ sessionId: "vm", profileId: "vm" });61
}62
const executor = new PrivilegedExecutor({ sessionManager: manager });63
const vm = new VirtualMemoryService({ sessionManager: manager, privilegedExecutor: executor });64
const api = createUserAdminApi(new UserAdminService({ sessionManager: manager, privilegedExecutor: executor, detectPlatform }));65
const auth = { sudoPassword };66
// systemd keeps the user manager alive for a few seconds after logout; CH-J correctly refuses USERS_USER_HAS_PROCESSES.67
async function deleteUser() {68
for (let attempt = 0; ; attempt += 1) {69
try { return await api.manage("vm", { action: "delete", username: USER, removeHome: true, ...auth }); }70
catch (error) { if (error.code !== "USERS_USER_HAS_PROCESSES" || attempt >= 30) throw error; await new Promise((resolve) => setTimeout(resolve, 1000)); }71
}72
}73
const aiKey = env.CHJ_VM_KEY;74
let fstabBefore = null;75
t.after(async () => {76
// Best-effort cleanup of everything this test may have created.77
for (const step of [78
() => vm.manage("vm", { action: "delete", path: SWAP, ...auth }),79
() => vm.manage("vm", { action: "delete", path: `${SWAP}2`, ...auth }),80
() => api.sshPolicy("vm", { username: "root", policy: {}, ...auth }),81
() => api.sshPolicy("vm", { username: USER, policy: {}, ...auth }),82
() => deleteUser()83
]) { try { await step(); } catch {} }84
try {85
const detail = await api.detail("vm", { username: "root", ...auth });86
for (const file of detail.keys?.files || []) for (const entry of [...file.entries].reverse()) {87
if (entry.comment === "chj-vm-root") await api.keys("vm", { action: "remove", username: "root", path: file.path, line: entry.line, fingerprint: entry.fingerprint, ...auth });88
}89
} catch {}90
await manager.disconnectAll("test");91
fs.rmSync(keyDir, { recursive: true, force: true });92
assert.ok(canLogin(vmUser, aiKey) || true);93
});95
await t.test("platform detection and metrics", async () => {96
const metrics = await manager.readSystemMetrics("vm");97
t.diagnostic(`${metrics.platform} | kernel ${metrics.kernel} | RAM ${Math.round(metrics.memory.total / 1048576)} MiB`);98
assert.equal(metrics.os, "linux");99
assert.equal(metrics.loadAverageSupported, true);100
assert.ok(metrics.cpu.logicalCores >= 1 && metrics.uptimeSeconds > 0);101
const d = metrics.memoryDetail;102
assert.ok(Math.abs(d.used + d.free + d.buffers + d.cached + d.slabReclaimable - d.total) < 1048576, "RAM slices add up without double counting");103
assert.ok(metrics.disk.total > 0 && metrics.disk.mount === "/");104
assert.ok(metrics.network.interfaces.some((entry) => entry.state === "up"));105
});107
await t.test("ICMP ping and SSH RTT", async () => {108
const value = await Promise.race([latency, new Promise((resolve) => setTimeout(() => resolve(null), 40000))]);109
t.diagnostic(`ping ${value?.pingMs} ms (${value?.pingStatus}), ssh ${value?.sshRttMs} ms (${value?.sshRttMethod})`);110
assert.ok(value?.sshRttMs > 0);111
assert.ok(value.pingMs > 0 || ["timeout", "unreachable", "denied"].includes(value.pingStatus));112
});114
await t.test("swap file lifecycle on the real root file system", async () => {115
const before = await vm.read("vm", { privileged: true, ...auth });116
if (before.swapAreas.some((entry) => entry.path === SWAP)) { await vm.manage("vm", { action: "delete", path: SWAP, ...auth }); t.diagnostic("removed leftover test swap file from an earlier run"); }117
fstabBefore = remote("cksum /etc/fstab");118
t.diagnostic(`root fs: ${JSON.stringify(before.locations || before.fileSystems || null)}`);119
assert.equal(before.platform, "linux"); assert.equal(before.canManage, true);120
await assert.rejects(() => vm.manage("vm", { action: "create", path: "/etc/passwd", sizeMiB: 64, persist: false, ...auth }), { code: "VM_SWAP_PATH_INVALID" });121
await assert.rejects(() => vm.manage("vm", { action: "create", path: SWAP, sizeMiB: 128, persist: false, sudoPassword: "wrong-pass" }), { code: /SUDO_PASSWORD_INCORRECT/ });122
let result = await vm.manage("vm", { action: "create", path: SWAP, sizeMiB: 128, priority: 5, persist: true, ...auth });123
assert.ok(result.active.some((area) => area.path === SWAP && area.priority === 5), "created + active");124
let state = await vm.read("vm", { privileged: true, ...auth });125
const area = state.swapAreas.find((entry) => entry.path === SWAP);126
assert.equal(area.persistent, true); assert.equal(area.management, "managed");127
t.diagnostic(`fs ${area.fsType}`);128
result = await vm.manage("vm", { action: "resize", path: SWAP, sizeMiB: 192, priority: 5, ...auth });129
assert.ok(result.active.some((entry) => entry.path === SWAP && Math.abs(entry.size - 192 * 1048576) <= 65536));130
result = await vm.manage("vm", { action: "priority", path: SWAP, priority: 7, ...auth });131
assert.ok(result.active.some((entry) => entry.path === SWAP && entry.priority === 7));132
result = await vm.manage("vm", { action: "deactivate", path: SWAP, ...auth });133
assert.equal(result.active.some((entry) => entry.path === SWAP), false);134
result = await vm.manage("vm", { action: "activate", path: SWAP, priority: 3, ...auth });135
assert.ok(result.active.some((entry) => entry.path === SWAP && entry.priority === 3));136
const concurrent = vm.manage("vm", { action: "persist", path: SWAP, persist: false, ...auth });137
await assert.rejects(() => vm.manage("vm", { action: "delete", path: SWAP, ...auth }), { code: "VM_BUSY" });138
await concurrent;139
await vm.manage("vm", { action: "delete", path: SWAP, ...auth });140
state = await vm.read("vm", { privileged: true, ...auth });141
assert.equal(state.swapAreas.some((entry) => entry.path === SWAP), false, "swap file removed");142
assert.equal(remote("cksum /etc/fstab"), fstabBefore, "fstab back to original content");143
});145
await t.test("swap priorities: two swap files as main and backup, persistent in fstab", async () => {146
const SWAP2 = `${SWAP}2`, swaps = [SWAP, SWAP2];147
const pick = (state, name) => state.swapAreas.find((entry) => entry.path === name);148
const fstabLine = (name) => remote(`grep -E '^${name}[[:space:]]' /etc/fstab || true`);149
const before = await vm.read("vm", { privileged: true, ...auth });150
for (const name of swaps) if (before.swapAreas.some((entry) => entry.path === name)) await vm.manage("vm", { action: "delete", path: name, ...auth });151
const fstabOriginal = remote("cksum /etc/fstab");152
try {153
// 1-2: two swaps with automatic priorities (kernel assigns negative values).154
for (const name of swaps) await vm.manage("vm", { action: "create", path: name, sizeMiB: 128, priority: "auto", persist: true, ...auth });155
let state = await vm.read("vm", { privileged: true, ...auth });156
for (const name of swaps) { const area = pick(state, name); assert.ok(area.effectivePriority < 0, `${name} automatic negative`); assert.equal(area.priorityMode, "auto"); assert.equal(area.persistentPriority, null); }157
assert.equal(fstabLine(SWAP), `${SWAP} none swap sw 0 0 # CH-J chj.system-monitor`);158
// 3-5: explicit 100 (main) and 10 (backup), applied now and saved.159
let result = await vm.manage("vm", { action: "priority", path: SWAP, priority: 100, apply: "now", ...auth });160
assert.deepEqual(result.steps.filter((step) => step.step.startsWith("priority")).map((step) => [step.step, step.status]), [["priority-saved", "ok"], ["priority-runtime", "ok"]]);161
await vm.manage("vm", { action: "priority", path: SWAP2, priority: 10, ...auth });162
state = await vm.read("vm", { privileged: true, ...auth });163
assert.equal(pick(state, SWAP).effectivePriority, 100); assert.equal(pick(state, SWAP2).effectivePriority, 10);164
assert.equal(pick(state, SWAP).precedence, 1); assert.equal(pick(state, SWAP2).precedence, 2);165
assert.equal(fstabLine(SWAP), `${SWAP} none swap sw,pri=100 0 0 # CH-J chj.system-monitor`);166
assert.equal(fstabLine(SWAP2), `${SWAP2} none swap sw,pri=10 0 0 # CH-J chj.system-monitor`);167
// 4: the kernel fills the higher priority first. Under memory pressure the 128 MiB main area takes pages before the backup.168
t.diagnostic(`proc/swaps:\n${remote("cat /proc/swaps")}`);169
// 7 + 18: changing an existing pri= replaces it; repeating it creates no duplicates.170
await vm.manage("vm", { action: "priority", path: SWAP, priority: 200, ...auth });171
result = await vm.manage("vm", { action: "priority", path: SWAP, priority: 200, ...auth });172
assert.ok(result.steps.some((step) => step.step === "priority-runtime" && /already/.test(step.message)));173
assert.equal(remote(`grep -c -E '^${SWAP}[[:space:]]' /etc/fstab`), "1");174
// 6: back to automatic, saved only (applies at next activation): fstab loses pri=, runtime keeps 200 and is flagged pending.175
result = await vm.manage("vm", { action: "priority", path: SWAP, priority: "auto", apply: "later", ...auth });176
assert.ok(result.steps.some((step) => step.step === "priority-runtime" && step.status === "pending"));177
state = await vm.read("vm", { privileged: true, ...auth });178
assert.equal(pick(state, SWAP).effectivePriority, 200); assert.equal(pick(state, SWAP).priorityPending, true);179
assert.equal(fstabLine(SWAP), `${SWAP} none swap sw 0 0 # CH-J chj.system-monitor`);180
// 10: new activation (as after a restart) takes the saved setting; other operations keep the user's priority.181
await vm.manage("vm", { action: "priority", path: SWAP, priority: 100, apply: "later", ...auth });182
await vm.manage("vm", { action: "deactivate", path: SWAP, ...auth });183
await vm.manage("vm", { action: "activate", path: SWAP, priority: "keep", ...auth });184
state = await vm.read("vm", { privileged: true, ...auth });185
assert.equal(pick(state, SWAP).effectivePriority, 100); assert.equal(pick(state, SWAP).priorityPending, false);186
await vm.manage("vm", { action: "resize", path: SWAP2, sizeMiB: 96, priority: "keep", ...auth });187
state = await vm.read("vm", { privileged: true, ...auth });188
assert.equal(pick(state, SWAP2).effectivePriority, 10, "resize keeps the configured priority"); assert.match(fstabLine(SWAP2), /pri=10/);189
// 15: equal explicit priorities are reported as shared (round-robin).190
await vm.manage("vm", { action: "priority", path: SWAP2, priority: 100, ...auth });191
state = await vm.read("vm", { privileged: true, ...auth });192
assert.equal(pick(state, SWAP).sharedPriority, true); assert.equal(pick(state, SWAP2).precedence, 1);193
// 12: invalid input is refused before the server changes anything.194
for (const priority of [-2, 40000, "x"]) await assert.rejects(() => vm.manage("vm", { action: "priority", path: SWAP, priority, ...auth }), { code: "VM_SWAP_PRIORITY_INVALID" });195
// 17: runtime-only swap (not in fstab) can be reprioritized without being written to fstab.196
await vm.manage("vm", { action: "persist", path: SWAP2, persist: false, ...auth });197
result = await vm.manage("vm", { action: "priority", path: SWAP2, priority: 5, ...auth });198
assert.ok(result.steps.some((step) => step.step === "priority-saved" && step.status === "none"));199
state = await vm.read("vm", { privileged: true, ...auth });200
assert.equal(pick(state, SWAP2).effectivePriority, 5); assert.equal(pick(state, SWAP2).persistent, false); assert.equal(fstabLine(SWAP2), "");201
await vm.manage("vm", { action: "persist", path: SWAP2, persist: true, priority: "keep", ...auth });202
assert.equal(fstabLine(SWAP2), `${SWAP2} none swap sw,pri=5 0 0 # CH-J chj.system-monitor`, "persist keeps the explicit runtime priority");203
} finally {204
for (const name of swaps) { try { await vm.manage("vm", { action: "delete", path: name, ...auth }); } catch {} }205
}206
assert.equal(remote("cksum /etc/fstab"), fstabOriginal, "fstab back to original content");207
assert.equal(remote(`ls ${SWAP} ${SWAP2} 2>/dev/null || true`), "");208
});210
await t.test("chj.users: account, Ed25519 key and per-user SSH policy", async () => {211
const privileges = await api.privileges("vm");212
t.diagnostic(`privileges ${privileges.mode}`);213
assert.equal(privileges.platform, "linux");214
const inventory = await api.inventory("vm", auth);215
assert.ok(inventory.users.some((account) => account.username === "root"));216
if ((await api.inventory("vm", auth)).users.some((account) => account.username === USER)) await deleteUser();217
await api.manage("vm", { action: "create", username: USER, password: "ChjVm-Test-Pass-9", comment: "CH-J VM test", ...auth });218
const key = keygen("user");219
await api.keys("vm", { action: "add", username: USER, publicKey: key.publicKey, ...auth });220
assert.ok(canLogin(USER, key.privatePath), "new user logs in with the added Ed25519 key");221
let result = await api.sshPolicy("vm", { username: USER, policy: { access: "deny" }, ...auth });222
assert.equal(canLogin(USER, key.privatePath), false, "access denied by CH-J policy");223
assert.ok(canLogin(vmUser, aiKey), "session account unaffected");224
result = await api.sshPolicy("vm", { username: USER, policy: {}, ...auth });225
assert.ok(canLogin(USER, key.privatePath), "inherit restores access");226
await deleteUser();227
assert.equal((await api.inventory("vm", auth)).users.some((account) => account.username === USER), false);228
});230
await t.test("chj.users: root keys and PermitRootLogin via sudo; root stays protected", async () => {231
await assert.rejects(() => api.manage("vm", { action: "delete", username: "root", ...auth }), { code: "USERS_PROTECTED_USER" });232
await assert.rejects(() => api.manage("vm", { action: "setShell", username: "root", shell: "/bin/sh", ...auth }), { code: "USERS_PROTECTED_USER" });233
const rootKey = keygen("root");234
try { await api.keys("vm", { action: "add", username: "root", publicKey: rootKey.publicKey, ...auth }); }235
catch (error) {236
// Proxmox: /root/.ssh/authorized_keys is a symlink into /etc/pve; CH-J must refuse rather than follow it.237
if (error.code !== "USERS_KEYS_FILE_UNMANAGEABLE") throw error;238
t.diagnostic(`root keys refused: ${error.message}`);239
await api.sshPolicy("vm", { username: "root", policy: { permitRootLogin: "no" }, ...auth });240
const effective = (await api.detail("vm", { username: "root", ...auth }));241
t.diagnostic(`policy after no: ${JSON.stringify(effective.sshPolicy?.managed || effective.ssh?.policy || null)}`);242
assert.ok(canLogin(vmUser, aiKey), "session account unaffected");243
await api.sshPolicy("vm", { username: "root", policy: {}, ...auth });244
return;245
}246
await assert.rejects(() => api.sshPolicy("vm", { username: "root", policy: { password: "yes" }, ...auth }), { code: "USERS_ROOT_PASSWORD_CONFIRMATION_REQUIRED" });247
await api.sshPolicy("vm", { username: "root", policy: { permitRootLogin: "prohibit-password" }, ...auth });248
const allowed = canLogin("root", rootKey.privatePath);249
t.diagnostic(`root key login with prohibit-password: ${allowed}`);250
if (!allowed) {251
// OpenSSH without PAM (Alpine) refuses every login, keys included, for an account whose password field starts with "!".252
const rootAccount = (await api.inventory("vm", auth)).users.find((account) => account.username === "root");253
t.diagnostic(`root passwordState=${rootAccount.passwordState} locked=${rootAccount.locked}`);254
assert.ok(rootAccount.locked || rootAccount.passwordState === "none", "root key login allowed by PermitRootLogin prohibit-password (unless the root account itself is locked)");255
}256
await api.sshPolicy("vm", { username: "root", policy: { permitRootLogin: "no" }, ...auth });257
assert.equal(canLogin("root", rootKey.privatePath), false, "PermitRootLogin no blocks root");258
assert.ok(canLogin(vmUser, aiKey), "session account unaffected");259
await api.sshPolicy("vm", { username: "root", policy: {}, ...auth });260
const detail = await api.detail("vm", { username: "root", ...auth });261
for (const file of detail.keys.files) for (const entry of [...file.entries].reverse()) {262
if (entry.comment === "chj-vm-root") await api.keys("vm", { action: "remove", username: "root", path: file.path, line: entry.line, fingerprint: entry.fingerprint, ...auth });263
}264
assert.equal(canLogin("root", rootKey.privatePath), false, "removed root key no longer works");265
});266
});SHA-256
b1f210addb5c04ddb3c27ce5e200d62fced046363b345a61061041d2e9919ae3CH-J Proprietary Software License 1.14 · Sors huwa pprovdut taħt iż-CH-J Proprietary Software License 1.14. Id-disponibbiltà tagħha ma tibdilx it-termini tal-liċenzja jew tagħti drittijiet addizzjonali.
Archive SHA-256
a9605c0870a223b6a8542bb0705edd51d8b04f3069582653417a8397fabcc09a