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.

12,9 KB · 231 ZeilenDatei herunterladen
1"use strict";
3const test = require("node:test");
4const assert = require("node:assert/strict");
5const { EventEmitter } = require("node:events");
6const { PluginRuntime } = require("../src/main/plugins/pluginRuntime");
8class FakeWindow extends EventEmitter {
9 constructor() {
10 super();
11 let protocolHandler = null;
12 this.protocolRegistrations = 0;
13 this.webContents = new EventEmitter();
14 this.webContents.id = 42;
15 this.webContents.session = {
16 protocol: {
17 isProtocolHandled: () => Boolean(protocolHandler),
18 handle: (scheme, handler) => {
19 assert.equal(scheme, "chj-plugin");
20 protocolHandler = handler;
21 this.protocolRegistrations += 1;
22 }
23 },
24 setPermissionRequestHandler: () => {},
25 setPermissionCheckHandler: () => {}
26 };
27 this.webContents.setWindowOpenHandler = () => {};
28 this.destroyed = false;
29 this.visible = false;
30 this.loadedUrl = null;
31 }
33 isDestroyed() { return this.destroyed; }
34 isMinimized() { return false; }
35 show() { this.visible = true; }
36 hide() { this.visible = false; }
37 focus() { this.focused = true; }
38 loadURL(url) { this.loadedUrl = url; return Promise.resolve(); }
39 close() { this.destroyed = true; this.emit("closed"); }
42test("Core chrome CSS is available to strict self-only plugin CSP without exposing other Core files", async () => {
43 const runtime = new PluginRuntime({ registry: { resolveEntry: () => ({ manifest: { entry: "ui/index.html" }, root: "/missing-plugin" }) } });
44 const response = await runtime.handleRequest({ url: "chj-plugin://chj.key-generator/__chj_core__/window-chrome.css" });
45 assert.equal(response.status, 200);
46 assert.equal(response.headers.get("Content-Type"), "text/css; charset=utf-8");
47 assert.match(await response.text(), /#chj-chrome-collapse/);
48 const denied = await runtime.handleRequest({ url: "chj-plugin://chj.key-generator/__chj_core__/windowChrome.js" });
49 assert.equal(denied.status, 404);
50});
52test("plugin runtime registers the custom protocol in the isolated plugin session", () => {
53 let pluginWindow;
54 let pluginWindowOptions;
55 const windowStates = [];
56 const managerWindow = Object.assign(new EventEmitter(), { destroyed: false, minimized: false, shown: false, focused: false, isDestroyed() { return this.destroyed; }, isMinimized() { return this.minimized; }, restore() { this.minimized = false; }, show() { this.shown = true; }, focus() { this.focused = true; } });
57 const manifest = {
58 id: "chj.system-monitor",
59 name: "System Monitor",
60 version: "0.0.1",
61 entry: "ui/index.html",
62 permissions: ["session.read", "system.metrics.read"]
63 };
64 const runtime = new PluginRuntime({
65 BrowserWindow: class extends FakeWindow { constructor(options) { super(); pluginWindow = this; pluginWindowOptions = options; } },
66 registry: { resolveEntry: () => ({ manifest, root: "/plugin", entryPath: "/plugin/ui/index.html" }) },
67 sessionManager: {},
68 getMainWindow: () => managerWindow,
69 onWindowState: (windows) => windowStates.push(windows),
70 isVaultUnlocked: () => true,
71 preload: "/pluginPreload.js"
72 });
74 runtime.open(manifest.id);
75 assert.equal(pluginWindow.protocolRegistrations, 1);
76 assert.equal(pluginWindow.loadedUrl, "chj-plugin://chj.system-monitor/ui/index.html");
77 assert.equal(pluginWindowOptions.parent, managerWindow);
78 assert.equal(pluginWindowOptions.modal, false);
79 pluginWindow.emit("ready-to-show");
80 pluginWindow.emit("minimize");
81 assert.equal(pluginWindow.visible, false);
82 assert.equal(managerWindow.focused, true);
83 assert.equal(windowStates.at(-1)[0].minimized, true);
84 runtime.open(manifest.id);
85 assert.equal(pluginWindow.protocolRegistrations, 1);
86 assert.equal(pluginWindow.visible, true);
87 assert.equal(runtime.listWindows()[0].minimized, false);
88 managerWindow.minimized = true;
89 managerWindow.focused = false;
90 managerWindow.emit("minimize");
91 assert.equal(pluginWindow.visible, false);
92 assert.equal(managerWindow.minimized, true);
93 assert.equal(managerWindow.focused, false);
94 assert.equal(runtime.listWindows()[0].minimized, true);
95 pluginWindow.close();
96 assert.equal(managerWindow.listenerCount("minimize"), 0);
97});
99test("plugin collapse IPC docks only the sender's window and restores it on reopen", async () => {
100 const handlers = new Map();
101 let pluginWindow;
102 const manifest = { id: "chj.hash-checksum", name: "Hash & Checksum", entry: "ui/index.html", permissions: [] };
103 const runtime = new PluginRuntime({
104 BrowserWindow: class extends FakeWindow { constructor() { super(); pluginWindow = this; } },
105 registry: { resolveEntry: () => ({ manifest, root: "/plugin" }) },
106 isVaultUnlocked: () => true
107 });
108 runtime.registerIpc({ handle: (channel, handler) => handlers.set(channel, handler) });
109 runtime.open(manifest.id);
110 pluginWindow.emit("ready-to-show");
111 const collapse = handlers.get("plugin:window:minimize");
112 await assert.rejects(() => collapse({ sender: { id: 999 } }), { code: "UNTRUSTED_PLUGIN_SENDER" });
113 await collapse({ sender: { id: pluginWindow.webContents.id } });
114 assert.equal(pluginWindow.visible, false);
115 assert.equal(runtime.listWindows()[0].minimized, true);
116 runtime.open(manifest.id);
117 assert.equal(pluginWindow.visible, true);
118 assert.equal(runtime.listWindows()[0].minimized, false);
119});
121test("plugin runtime routes user capabilities by sender identity and manifest permission", async () => {
122 const handlers = new Map();
123 const calls = [];
124 const runtime = new PluginRuntime({
125 BrowserWindow: FakeWindow,
126 registry: {},
127 sessionManager: {
128 readUsers: async (sessionId) => { calls.push(["read", sessionId]); return [{ username: "test" }]; },
129 manageUser: async (sessionId, payload) => { calls.push(["manage", sessionId, payload.action]); return { action: payload.action }; }
130 },
131 logService: { readTail: async () => ({ text: "forbidden" }) },
132 keyGeneratorService: {},
133 isVaultUnlocked: () => true
134 });
135 runtime.registerIpc({ handle: (channel, handler) => handlers.set(channel, handler) });
136 const window = { isDestroyed: () => false, close: () => {} };
137 runtime.contexts.set(77, { manifest: { permissions: ["users.read", "users.manage"] }, window });
138 const event = { sender: { id: 77 } };
139 assert.deepEqual(await handlers.get("plugin:users:list")(event, { sessionId: "terminal-users" }), [{ username: "test" }]);
140 assert.deepEqual(await handlers.get("plugin:users:manage")(event, { sessionId: "terminal-users", action: "lock", username: "bob" }), { action: "lock" });
141 assert.deepEqual(calls, [["read", "terminal-users"], ["manage", "terminal-users", "lock"]]);
142 await assert.rejects(() => handlers.get("plugin:logs:read")(event, {}), { code: "PLUGIN_PERMISSION_DENIED" });
143});
145test("plugin runtime routes file capabilities and keeps permissions separate", async () => {
146 const handlers = new Map();
147 const calls = [];
148 const remoteFileService = {
149 list: async (sessionId, path) => { calls.push(["list", sessionId, path]); return [{ name: "a.txt" }]; },
150 readText: async (sessionId, path) => { calls.push(["readText", sessionId, path]); return { text: "ahoj" }; },
151 writeText: async (sessionId, path, text) => { calls.push(["writeText", sessionId, path, text]); return { size: 4 }; },
152 createFile: async () => ({}), mkdir: async () => ({}), rename: async () => ({}), remove: async () => ({}), removeMany: async () => ({}),
153 upload: async () => ({}), download: async () => ({}), downloadMany: async () => ({}), downloadArchive: async () => ({})
154 };
155 const runtime = new PluginRuntime({ BrowserWindow: FakeWindow, registry: {}, sessionManager: {}, remoteFileService, isVaultUnlocked: () => true });
156 runtime.registerIpc({ handle: (channel, handler) => handlers.set(channel, handler) });
157 const window = { isDestroyed: () => false, close: () => {} };
158 runtime.contexts.set(88, { manifest: { permissions: ["files.read"] }, window });
159 const event = { sender: { id: 88 } };
161 assert.deepEqual(await handlers.get("plugin:files:list")(event, { sessionId: "sftp-1", path: "/home/test" }), [{ name: "a.txt" }]);
162 assert.deepEqual(await handlers.get("plugin:files:readText")(event, { sessionId: "sftp-1", path: "/home/test/a.txt" }), { text: "ahoj" });
163 await assert.rejects(() => handlers.get("plugin:files:writeText")(event, { sessionId: "sftp-1", path: "/home/test/a.txt", text: "nově" }), { code: "PLUGIN_PERMISSION_DENIED" });
164 await assert.rejects(() => handlers.get("plugin:files:download")(event, { sessionId: "sftp-1", path: "/home/test/a.txt" }), { code: "PLUGIN_PERMISSION_DENIED" });
165 for (const capability of ["saveText", "cleanupRecovery"]) {
166 await assert.rejects(() => handlers.get(`plugin:files:${capability}`)(event, { sessionId: "sftp-1" }), { code: "PLUGIN_PERMISSION_DENIED" });
167 }
168 assert.deepEqual(calls, [["list", "sftp-1", "/home/test"], ["readText", "sftp-1", "/home/test/a.txt"]]);
169});
171test("editor IPC forwards structured options, owns document handles and protects recovery reads", async () => {
172 const handlers = new Map(), calls = [];
173 const remoteFileService = {};
174 for (const method of ["readText", "saveText", "listRecovery", "readRecovery", "cleanupRecovery", "closeText"]) {
175 remoteFileService[method] = (...args) => { calls.push([method, ...args]); return { ok: true }; };
176 }
177 const runtime = new PluginRuntime({ BrowserWindow: FakeWindow, registry: {}, remoteFileService, isVaultUnlocked: () => true });
178 runtime.registerIpc({ handle: (channel, handler) => handlers.set(channel, handler) });
179 const window = { isDestroyed: () => false };
180 runtime.contexts.set(91, { manifest: { id: "editor", permissions: ["files.read", "files.write"] }, window });
181 runtime.contexts.set(92, { manifest: { id: "other", permissions: [] }, window });
182 const options = { editId: "opaque", sudo: true, sudoPassword: "secret" };
183 await handlers.get("plugin:files:saveText")({ sender: { id: 91 } }, { sessionId: "s", path: "/etc/config", text: "content", options });
184 assert.deepEqual(calls[0], ["saveText", "s", "/etc/config", "content", options, "editor"]);
185 await handlers.get("plugin:files:closeText")({ sender: { id: 91 } }, { editId: "opaque" });
186 assert.deepEqual(calls[1], ["closeText", "opaque", "editor"]);
187 for (const method of ["listRecovery", "readRecovery", "closeText"]) {
188 await assert.rejects(() => handlers.get(`plugin:files:${method}`)({ sender: { id: 92 } }), { code: "PLUGIN_PERMISSION_DENIED" });
189 }
190});
192test("plugin runtime separates NGINX read and manage capabilities", async () => {
193 const handlers = new Map();
194 const calls = [];
195 const sessionManager = {
196 inspectNginx: async (sessionId) => { calls.push(["inspect", sessionId]); return { installed: true }; },
197 readNginxConfig: async () => ({}), dumpNginxConfig: async () => ({}), testNginxConfig: async () => ({ ok: true }),
198 saveNginxConfig: async (sessionId, payload) => { calls.push(["save", sessionId, payload.path]); return { path: payload.path }; },
199 reloadNginx: async () => ({ ok: true })
200 };
201 const runtime = new PluginRuntime({ BrowserWindow: FakeWindow, registry: {}, sessionManager, isVaultUnlocked: () => true });
202 runtime.registerIpc({ handle: (channel, handler) => handlers.set(channel, handler) });
203 const window = { isDestroyed: () => false, close: () => {} };
204 const event = { sender: { id: 99 } };
205 runtime.contexts.set(99, { manifest: { permissions: ["nginx.read"] }, window });
206 assert.deepEqual(await handlers.get("plugin:nginx:inspect")(event, { sessionId: "nginx-1" }), { installed: true });
207 await assert.rejects(() => handlers.get("plugin:nginx:saveConfig")(event, { sessionId: "nginx-1", path: "/etc/nginx/nginx.conf" }), { code: "PLUGIN_PERMISSION_DENIED" });
208 runtime.contexts.set(99, { manifest: { permissions: ["nginx.read", "nginx.manage"] }, window });
209 assert.deepEqual(await handlers.get("plugin:nginx:saveConfig")(event, { sessionId: "nginx-1", path: "/etc/nginx/nginx.conf" }), { path: "/etc/nginx/nginx.conf" });
210 assert.deepEqual(calls, [["inspect", "nginx-1"], ["save", "nginx-1", "/etc/nginx/nginx.conf"]]);
211});
213test("closing a destroyed plugin window releases its context without reading webContents", () => {
214 let pluginWindow;
215 const cleanup = [];
216 const manifest = { id: "chj.hash-checksum", name: "Hash & Checksum", version: "0.0.1", entry: "ui/index.html", permissions: ["local.hash"] };
217 const runtime = new PluginRuntime({
218 BrowserWindow: class extends FakeWindow { constructor() { super(); pluginWindow = this; } },
219 registry: { resolveEntry: () => ({ manifest, root: "/plugin" }) },
220 localHashService: { on: () => {}, cleanupPlugin: (id) => cleanup.push(id) },
221 isVaultUnlocked: () => true
222 });
223 runtime.open(manifest.id);
224 const senderId = pluginWindow.webContents.id;
225 Object.defineProperty(pluginWindow, "webContents", { get() { throw new Error("Object has been destroyed"); } });
226 pluginWindow.destroyed = true;
227 assert.doesNotThrow(() => pluginWindow.emit("closed"));
228 assert.equal(runtime.contexts.has(senderId), false);
229 assert.equal(runtime.windows.has(manifest.id), false);
230 assert.deepEqual(cleanup, [manifest.id]);
231});

SHA-256: 0575cb8e60be5ac2c60e5024716b1a0829a68c877c1a46c2abaff2ff74ff2710

SHA-256 des Archivs: 5ac91caf4fa32a6fdb114f2430deed486fbe7489d5eea343d1f034169fafb5e0