CH-J Server Managerمدیریت سرور از طریق SSH
منو
منبع منتشرشده

CH-J Server Manager

پوشه‌ها و فایل‌های یک نسخهٔ مشخص از برنامه را مرور کنید.

دانلود ZIP منبع
CH-J / Server Manager 368 فایل
core-20261017T201717Z-057348c4a70f افزوده‌شده: تاریخ ثبت نشده است · ساخته‌شده:
linuxVmIntegration.test.js افزوده‌شده: تاریخ ثبت نشده است ۱۴٫۰ KB · 200 خط
کد دانلود فایل
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 policy
5// (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_LINE
7// ("name = value" line read at runtime, never printed).
8const test = require("node:test");
9const assert = require("node:assert/strict");
10const fs = require("node:fs");
11const os = require("node:os");
12const path = require("node:path");
13const { execFileSync, spawnSync } = require("node:child_process");
14const { SessionManager } = require("../src/main/sessions/sessionManager");
15const { PrivilegedExecutor } = require("../src/main/sessions/privilegedExec");
16const { VirtualMemoryService } = require("../src/main/system/virtualMemoryService");
17const { UserAdminService, createUserAdminApi } = require("../src/main/users/userAdminService");
18const { detectPlatform } = require("../src/main/sessions/platformDetect");
20const env = process.env;
21const 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));
22const skip = enabled ? false : "Set CHJ_VM_* to run against an isolated Linux test VM.";
23const sudoPassword = !enabled ? "" : env.CHJ_VM_SUDO_PASSWORD
24 || 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();
25const vmUser = env.CHJ_VM_USER || "ai";
26const host = env.CHJ_VM_HOST;
27const SWAP = "/swapfile-chjvmtest";
28const USER = "chjvmtest";
29let keyDir; let knownHosts;
31function 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() };
36function 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();
39function 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;
43test("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 () => api.sshPolicy("vm", { username: "root", policy: {}, ...auth }),
80 () => api.sshPolicy("vm", { username: USER, policy: {}, ...auth }),
81 () => deleteUser()
82 ]) { try { await step(); } catch {} }
83 try {
84 const detail = await api.detail("vm", { username: "root", ...auth });
85 for (const file of detail.keys?.files || []) for (const entry of [...file.entries].reverse()) {
86 if (entry.comment === "chj-vm-root") await api.keys("vm", { action: "remove", username: "root", path: file.path, line: entry.line, fingerprint: entry.fingerprint, ...auth });
87 }
88 } catch {}
89 await manager.disconnectAll("test");
90 fs.rmSync(keyDir, { recursive: true, force: true });
91 assert.ok(canLogin(vmUser, aiKey) || true);
92 });
94 await t.test("platform detection and metrics", async () => {
95 const metrics = await manager.readSystemMetrics("vm");
96 t.diagnostic(`${metrics.platform} | kernel ${metrics.kernel} | RAM ${Math.round(metrics.memory.total / 1048576)} MiB`);
97 assert.equal(metrics.os, "linux");
98 assert.equal(metrics.loadAverageSupported, true);
99 assert.ok(metrics.cpu.logicalCores >= 1 && metrics.uptimeSeconds > 0);
100 const d = metrics.memoryDetail;
101 assert.ok(Math.abs(d.used + d.free + d.buffers + d.cached + d.slabReclaimable - d.total) < 1048576, "RAM slices add up without double counting");
102 assert.ok(metrics.disk.total > 0 && metrics.disk.mount === "/");
103 assert.ok(metrics.network.interfaces.some((entry) => entry.state === "up"));
104 });
106 await t.test("ICMP ping and SSH RTT", async () => {
107 const value = await Promise.race([latency, new Promise((resolve) => setTimeout(() => resolve(null), 40000))]);
108 t.diagnostic(`ping ${value?.pingMs} ms (${value?.pingStatus}), ssh ${value?.sshRttMs} ms (${value?.sshRttMethod})`);
109 assert.ok(value?.sshRttMs > 0);
110 assert.ok(value.pingMs > 0 || ["timeout", "unreachable", "denied"].includes(value.pingStatus));
111 });
113 await t.test("swap file lifecycle on the real root file system", async () => {
114 const before = await vm.read("vm", { privileged: true, ...auth });
115 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"); }
116 fstabBefore = remote("cksum /etc/fstab");
117 t.diagnostic(`root fs: ${JSON.stringify(before.locations || before.fileSystems || null)}`);
118 assert.equal(before.platform, "linux"); assert.equal(before.canManage, true);
119 await assert.rejects(() => vm.manage("vm", { action: "create", path: "/etc/passwd", sizeMiB: 64, persist: false, ...auth }), { code: "VM_SWAP_PATH_INVALID" });
120 await assert.rejects(() => vm.manage("vm", { action: "create", path: SWAP, sizeMiB: 128, persist: false, sudoPassword: "wrong-pass" }), { code: /SUDO_PASSWORD_INCORRECT/ });
121 let result = await vm.manage("vm", { action: "create", path: SWAP, sizeMiB: 128, priority: 5, persist: true, ...auth });
122 assert.ok(result.active.some((area) => area.path === SWAP && area.priority === 5), "created + active");
123 let state = await vm.read("vm", { privileged: true, ...auth });
124 const area = state.swapAreas.find((entry) => entry.path === SWAP);
125 assert.equal(area.persistent, true); assert.equal(area.management, "managed");
126 t.diagnostic(`fs ${area.fsType}`);
127 result = await vm.manage("vm", { action: "resize", path: SWAP, sizeMiB: 192, priority: 5, ...auth });
128 assert.ok(result.active.some((entry) => entry.path === SWAP && Math.abs(entry.size - 192 * 1048576) <= 65536));
129 result = await vm.manage("vm", { action: "priority", path: SWAP, priority: 7, ...auth });
130 assert.ok(result.active.some((entry) => entry.path === SWAP && entry.priority === 7));
131 result = await vm.manage("vm", { action: "deactivate", path: SWAP, ...auth });
132 assert.equal(result.active.some((entry) => entry.path === SWAP), false);
133 result = await vm.manage("vm", { action: "activate", path: SWAP, priority: 3, ...auth });
134 assert.ok(result.active.some((entry) => entry.path === SWAP && entry.priority === 3));
135 const concurrent = vm.manage("vm", { action: "persist", path: SWAP, persist: false, ...auth });
136 await assert.rejects(() => vm.manage("vm", { action: "delete", path: SWAP, ...auth }), { code: "VM_BUSY" });
137 await concurrent;
138 await vm.manage("vm", { action: "delete", path: SWAP, ...auth });
139 state = await vm.read("vm", { privileged: true, ...auth });
140 assert.equal(state.swapAreas.some((entry) => entry.path === SWAP), false, "swap file removed");
141 assert.equal(remote("cksum /etc/fstab"), fstabBefore, "fstab back to original content");
142 });
144 await t.test("chj.users: account, Ed25519 key and per-user SSH policy", async () => {
145 const privileges = await api.privileges("vm");
146 t.diagnostic(`privileges ${privileges.mode}`);
147 assert.equal(privileges.platform, "linux");
148 const inventory = await api.inventory("vm", auth);
149 assert.ok(inventory.users.some((account) => account.username === "root"));
150 if ((await api.inventory("vm", auth)).users.some((account) => account.username === USER)) await deleteUser();
151 await api.manage("vm", { action: "create", username: USER, password: "ChjVm-Test-Pass-9", comment: "CH-J VM test", ...auth });
152 const key = keygen("user");
153 await api.keys("vm", { action: "add", username: USER, publicKey: key.publicKey, ...auth });
154 assert.ok(canLogin(USER, key.privatePath), "new user logs in with the added Ed25519 key");
155 let result = await api.sshPolicy("vm", { username: USER, policy: { access: "deny" }, ...auth });
156 assert.equal(canLogin(USER, key.privatePath), false, "access denied by CH-J policy");
157 assert.ok(canLogin(vmUser, aiKey), "session account unaffected");
158 result = await api.sshPolicy("vm", { username: USER, policy: {}, ...auth });
159 assert.ok(canLogin(USER, key.privatePath), "inherit restores access");
160 await deleteUser();
161 assert.equal((await api.inventory("vm", auth)).users.some((account) => account.username === USER), false);
162 });
164 await t.test("chj.users: root keys and PermitRootLogin via sudo; root stays protected", async () => {
165 await assert.rejects(() => api.manage("vm", { action: "delete", username: "root", ...auth }), { code: "USERS_PROTECTED_USER" });
166 await assert.rejects(() => api.manage("vm", { action: "setShell", username: "root", shell: "/bin/sh", ...auth }), { code: "USERS_PROTECTED_USER" });
167 const rootKey = keygen("root");
168 try { await api.keys("vm", { action: "add", username: "root", publicKey: rootKey.publicKey, ...auth }); }
169 catch (error) {
170 // Proxmox: /root/.ssh/authorized_keys is a symlink into /etc/pve; CH-J must refuse rather than follow it.
171 if (error.code !== "USERS_KEYS_FILE_UNMANAGEABLE") throw error;
172 t.diagnostic(`root keys refused: ${error.message}`);
173 await api.sshPolicy("vm", { username: "root", policy: { permitRootLogin: "no" }, ...auth });
174 const effective = (await api.detail("vm", { username: "root", ...auth }));
175 t.diagnostic(`policy after no: ${JSON.stringify(effective.sshPolicy?.managed || effective.ssh?.policy || null)}`);
176 assert.ok(canLogin(vmUser, aiKey), "session account unaffected");
177 await api.sshPolicy("vm", { username: "root", policy: {}, ...auth });
178 return;
179 }
180 await assert.rejects(() => api.sshPolicy("vm", { username: "root", policy: { password: "yes" }, ...auth }), { code: "USERS_ROOT_PASSWORD_CONFIRMATION_REQUIRED" });
181 await api.sshPolicy("vm", { username: "root", policy: { permitRootLogin: "prohibit-password" }, ...auth });
182 const allowed = canLogin("root", rootKey.privatePath);
183 t.diagnostic(`root key login with prohibit-password: ${allowed}`);
184 if (!allowed) {
185 // OpenSSH without PAM (Alpine) refuses every login, keys included, for an account whose password field starts with "!".
186 const rootAccount = (await api.inventory("vm", auth)).users.find((account) => account.username === "root");
187 t.diagnostic(`root passwordState=${rootAccount.passwordState} locked=${rootAccount.locked}`);
188 assert.ok(rootAccount.locked || rootAccount.passwordState === "none", "root key login allowed by PermitRootLogin prohibit-password (unless the root account itself is locked)");
189 }
190 await api.sshPolicy("vm", { username: "root", policy: { permitRootLogin: "no" }, ...auth });
191 assert.equal(canLogin("root", rootKey.privatePath), false, "PermitRootLogin no blocks root");
192 assert.ok(canLogin(vmUser, aiKey), "session account unaffected");
193 await api.sshPolicy("vm", { username: "root", policy: {}, ...auth });
194 const detail = await api.detail("vm", { username: "root", ...auth });
195 for (const file of detail.keys.files) for (const entry of [...file.entries].reverse()) {
196 if (entry.comment === "chj-vm-root") await api.keys("vm", { action: "remove", username: "root", path: file.path, line: entry.line, fingerprint: entry.fingerprint, ...auth });
197 }
198 assert.equal(canLogin("root", rootKey.privatePath), false, "removed root key no longer works");
199 });
200});
SHA-256d4bdf6efac3fefdae0f0602a48edf894660563898a622c690d45ea3d035836bf

CH-J Proprietary Software License 1.14 · کد منبع تحت CH-J Proprietary Software License 1.14 ارائه می‌شود. در دسترس بودن آن شرایط مجوز را تغییر نمی‌دهد و حق اضافه‌ای اعطا نمی‌کند.

SHA-256 بایگانیcf4455b698f0f28e456a37b1834976a0194d8486acb7101052d59a838745cca3
فایل‌ها