CH-J Server Managerservera pārvaldība virs SSH
Izvēlne
Publicētais avots

CH-J Server Manager

Pārlūkot mapes un failus konkrētai lietojumprogrammas izdošanai.

Lejupielādēt avota ZIP
CH-J / Server Manager 377 faili
core-20261019T111616Z-160a00bfcddc Pievienots: · Būvēts:
pluginRuntime.js Pievienots: Datums nav reģistrēts 19,9 KB · 323 rindas
Kods Lejupielādēt failu
1"use strict";
3const fs = require("node:fs");
4const path = require("node:path");
5const { windowChromeOptions } = require("../bootstrap/windowChrome");
7const MIME_TYPES = Object.freeze({
8 ".css": "text/css; charset=utf-8",
9 ".gif": "image/gif",
10 ".html": "text/html; charset=utf-8",
11 ".jpeg": "image/jpeg",
12 ".jpg": "image/jpeg",
13 ".js": "text/javascript; charset=utf-8",
14 ".json": "application/json; charset=utf-8",
15 ".png": "image/png",
16 ".svg": "image/svg+xml",
17 ".webp": "image/webp",
18 ".woff": "font/woff",
19 ".woff2": "font/woff2",
20 ".ttf": "font/ttf"
21});
22const PLUGIN_CSP = "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; worker-src 'self'; connect-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'; frame-src 'none'";
24class PluginRuntime {
25 constructor(options) {
26 this.BrowserWindow = options.BrowserWindow;
27 this.registry = options.registry;
28 this.sessionManager = options.sessionManager;
29 this.keyGeneratorService = options.keyGeneratorService;
30 this.logService = options.logService;
31 this.remoteFileService = options.remoteFileService;
32 this.transferManager = options.transferManager;
33 this.localHashService = options.localHashService;
34 this.userAdmin = options.userAdmin;
35 this.virtualMemory = options.virtualMemory;
36 this.getLanguage = options.getLanguage || (() => "en");
37 this.getMainWindow = options.getMainWindow;
38 this.isVaultUnlocked = options.isVaultUnlocked;
39 this.preload = options.preload;
40 this.icon = options.icon;
41 this.onWindowState = options.onWindowState;
42 this.logger = options.logger;
43 this.contexts = new Map();
44 this.windows = new Map();
45 this.localHashService?.on("progress", ({ pluginId, job }) => {
46 const window = this.windows.get(pluginId);
47 if (window && !window.isDestroyed()) window.webContents.send("plugin:hashing:progress", job);
48 });
49 // Transfer progress goes only to the plugin that owns the jobs; jobs keep running while it is closed.
50 this.transferManager?.on("update", (update) => {
51 const window = this.windows.get(update.owner);
52 if (window && !window.isDestroyed()) window.webContents.send("plugin:files:transfers:update", update);
53 });
54 }
56 registerIpc(ipcMain) {
57 const handle = (channel, permission, action) => {
58 ipcMain.handle(channel, async (event, payload = {}) => {
59 const context = this._context(event.sender.id);
60 if (permission && !context.manifest.permissions.includes(permission)) {
61 const error = new Error(`Plugin permission denied: ${permission}`);
62 error.code = "PLUGIN_PERMISSION_DENIED";
63 throw error;
64 }
65 try {
66 return await action(context, payload);
67 } catch (error) {
68 // Electron serializes the message, but drops custom error properties.
69 const exposeCode = error?.code?.startsWith?.("HASH_") || ((channel.startsWith("plugin:users:") || channel.startsWith("plugin:system:") || channel.startsWith("plugin:files:")) && /^[A-Z][A-Z0-9_]{2,63}$/.test(String(error?.code || "")));
70 if (exposeCode && !error.message.startsWith(`${error.code}:`)) error.message = `${error.code}: ${error.message}`;
71 throw error;
72 }
73 });
74 };
75 handle("plugin:getInfo", null, (context) => context.manifest);
76 handle("plugin:ui:getLanguage", null, () => this.getLanguage());
77 handle("plugin:close", null, (context) => { context.window.close(); return { ok: true }; });
78 handle("plugin:window:minimize", null, (context) => { this._minimize(context); return { ok: true }; });
79 handle("plugin:sessions:list", "session.read", () => this.sessionManager.list());
80 handle("plugin:system:metrics", "system.metrics.read", (_context, payload) => this.sessionManager.readSystemMetrics(payload.sessionId));
81 handle("plugin:system:virtualMemory", "system.metrics.read", (_context, payload) => this.virtualMemory.read(payload.sessionId, { sudoPassword: payload.sudoPassword, privileged: payload.privileged === true }));
82 handle("plugin:system:manageVirtualMemory", "system.memory.manage", (_context, payload) => this.virtualMemory.manage(payload.sessionId, payload));
83 handle("plugin:keys:generate", "keys.generate", (_context, payload) => this.keyGeneratorService.generate(payload));
84 handle("plugin:keys:save", "keys.generate", (_context, payload) => this.keyGeneratorService.save(payload.generationId));
85 handle("plugin:logs:read", "logs.read", (_context, payload) => this.logService.readTail(payload));
86 handle("plugin:users:list", "users.read", (_context, payload) => this.userAdmin.list(payload.sessionId));
87 handle("plugin:users:privileges", "users.read", (_context, payload) => this.userAdmin.privileges(payload.sessionId, { refresh: payload.refresh === true }));
88 handle("plugin:users:inventory", "users.read", (_context, payload) => this.userAdmin.inventory(payload.sessionId, payload));
89 handle("plugin:users:detail", "users.read", (_context, payload) => this.userAdmin.detail(payload.sessionId, payload));
90 handle("plugin:users:manage", "users.manage", (_context, payload) => this.userAdmin.manage(payload.sessionId, payload));
91 handle("plugin:users:keys", "users.manage", (_context, payload) => this.userAdmin.keys(payload.sessionId, payload));
92 handle("plugin:users:sshPolicy", "users.manage", (_context, payload) => this.userAdmin.sshPolicy(payload.sessionId, payload));
93 handle("plugin:nginx:inspect", "nginx.read", (_context, payload) => this.sessionManager.inspectNginx(payload.sessionId));
94 handle("plugin:nginx:readConfig", "nginx.read", (_context, payload) => this.sessionManager.readNginxConfig(payload.sessionId, payload));
95 handle("plugin:nginx:dumpConfig", "nginx.read", (_context, payload) => this.sessionManager.dumpNginxConfig(payload.sessionId, payload));
96 handle("plugin:nginx:testConfig", "nginx.read", (_context, payload) => this.sessionManager.testNginxConfig(payload.sessionId, payload));
97 handle("plugin:nginx:saveConfig", "nginx.manage", (_context, payload) => this.sessionManager.saveNginxConfig(payload.sessionId, payload));
98 handle("plugin:nginx:reload", "nginx.manage", (_context, payload) => this.sessionManager.reloadNginx(payload.sessionId, payload));
99 handle("plugin:files:list", "files.read", (_context, payload) => this.remoteFileService.list(payload.sessionId, payload.path, payload.options || {}));
100 handle("plugin:files:privileges", "files.read", (_context, payload) => this.remoteFileService.privileges(payload.sessionId));
101 handle("plugin:files:readText", "files.read", (context, payload) => this.remoteFileService.readText(payload.sessionId, payload.path, payload.options || {}, context.manifest.id));
102 handle("plugin:files:writeText", "files.write", (context, payload) => this.remoteFileService.writeText(payload.sessionId, payload.path, payload.text, payload.options || {}, context.manifest.id));
103 handle("plugin:files:saveText", "files.write", (context, payload) => this.remoteFileService.saveText(payload.sessionId, payload.path, payload.text, payload.options || {}, context.manifest.id));
104 handle("plugin:files:listRecovery", "files.read", (_context, payload) => this.remoteFileService.listRecovery(payload.sessionId, payload.options || {}));
105 handle("plugin:files:readRecovery", "files.read", (_context, payload) => this.remoteFileService.readRecovery(payload.sessionId, payload.recoveryId, payload.options || {}));
106 handle("plugin:files:cleanupRecovery", "files.write", (_context, payload) => this.remoteFileService.cleanupRecovery(payload.sessionId, payload.recoveryId, payload.options || {}));
107 handle("plugin:files:closeText", "files.read", (context, payload) => this.remoteFileService.closeText(payload.editId, context.manifest.id));
108 handle("plugin:files:createFile", "files.write", (_context, payload) => this.remoteFileService.createFile(payload.sessionId, payload.path, payload.options || {}));
109 handle("plugin:files:mkdir", "files.write", (_context, payload) => this.remoteFileService.mkdir(payload.sessionId, payload.path, payload.options || {}));
110 handle("plugin:files:rename", "files.write", (_context, payload) => this.remoteFileService.rename(payload.sessionId, payload.from, payload.to, payload.options || {}));
111 handle("plugin:files:remove", "files.write", (_context, payload) => this.remoteFileService.remove(payload.sessionId, payload.path, payload.options || {}));
112 handle("plugin:files:removeMany", "files.write", (_context, payload) => this.remoteFileService.removeMany(payload.sessionId, payload.paths, payload.recursive, payload.options || {}));
113 handle("plugin:files:upload", "files.transfer", (_context, payload) => this.remoteFileService.upload(payload.sessionId, payload.directory));
114 handle("plugin:files:download", "files.transfer", (_context, payload) => this.remoteFileService.download(payload.sessionId, payload.path));
115 handle("plugin:files:downloadMany", "files.transfer", (_context, payload) => this.remoteFileService.downloadMany(payload.sessionId, payload.paths));
116 handle("plugin:files:downloadArchive", "files.transfer", (_context, payload) => this.remoteFileService.downloadArchive(payload.sessionId, payload.paths, payload.format));
117 const transfers = () => {
118 if (!this.transferManager) throw Object.assign(new Error("TRANSFERS_UNAVAILABLE: Transfer Manager is unavailable."), { code: "TRANSFERS_UNAVAILABLE" });
119 return this.transferManager;
120 };
121 const ids = (payload) => payload.ids === "all" ? "all" : (Array.isArray(payload.ids) ? payload.ids : [payload.ids]).slice(0, 20000).map(String);
122 handle("plugin:files:transfers:list", "files.transfer", (context) => transfers().list(context.manifest.id));
123 handle("plugin:files:transfers:planUpload", "files.transfer", (context, payload) => transfers().planUpload(context.manifest.id, payload.sessionId, payload.directory, payload.options || {}));
124 handle("plugin:files:transfers:planDownload", "files.transfer", (context, payload) => transfers().planDownload(context.manifest.id, payload.sessionId, payload.paths, payload.options || {}));
125 handle("plugin:files:transfers:start", "files.transfer", (context, payload) => transfers().start(context.manifest.id, payload.planId, payload.options || {}));
126 handle("plugin:files:transfers:pause", "files.transfer", (context, payload) => transfers().pause(context.manifest.id, ids(payload)));
127 handle("plugin:files:transfers:resume", "files.transfer", (context, payload) => transfers().resume(context.manifest.id, ids(payload), payload.options || {}));
128 handle("plugin:files:transfers:retry", "files.transfer", (context, payload) => transfers().retry(context.manifest.id, ids(payload), payload.options || {}));
129 handle("plugin:files:transfers:cancel", "files.transfer", (context, payload) => transfers().cancel(context.manifest.id, ids(payload)));
130 handle("plugin:files:transfers:remove", "files.transfer", (context, payload) => transfers().remove(context.manifest.id, ids(payload)));
131 handle("plugin:files:transfers:settings", "files.transfer", (context, payload) => transfers().updateSettings(context.manifest.id, payload.settings || {}));
132 handle("plugin:hashing:algorithms", "local.hash", () => this.localHashService.getAlgorithms());
133 handle("plugin:hashing:selectFiles", "local.hash", (context, payload) => this.localHashService.selectFiles(context.manifest.id, payload));
134 handle("plugin:hashing:selectDirectory", "local.hash", (context) => this.localHashService.selectDirectory(context.manifest.id));
135 handle("plugin:hashing:selectManifest", "local.hash", (context) => this.localHashService.selectManifest(context.manifest.id));
136 handle("plugin:hashing:selectManifestDestination", "local.hash", (context, payload) => this.localHashService.selectManifestDestination(context.manifest.id, payload));
137 handle("plugin:hashing:start", "local.hash", (context, payload) => this.localHashService.start(context.manifest.id, payload));
138 handle("plugin:hashing:verify", "local.hash", (context, payload) => this.localHashService.verify(context.manifest.id, payload));
139 handle("plugin:hashing:compare", "local.hash", (context, payload) => this.localHashService.compare(context.manifest.id, payload));
140 handle("plugin:hashing:generateManifest", "local.hash", (context, payload) => this.localHashService.generateManifest(context.manifest.id, payload));
141 handle("plugin:hashing:verifyManifest", "local.hash", (context, payload) => this.localHashService.verifyManifest(context.manifest.id, payload));
142 handle("plugin:hashing:exportResults", "local.hash", (context, payload) => this.localHashService.exportResults(context.manifest.id, payload));
143 handle("plugin:hashing:copyResult", "local.hash", (context, payload) => this.localHashService.copyResult(context.manifest.id, payload));
144 handle("plugin:hashing:status", "local.hash", (context, payload) => this.localHashService.status(context.manifest.id, payload.jobId));
145 handle("plugin:hashing:cancel", "local.hash", (context, payload) => this.localHashService.cancel(context.manifest.id, payload.jobId));
146 }
148 notifyLanguageChanged(language) {
149 for (const window of this.windows.values()) {
150 if (!window.isDestroyed()) window.webContents.send("plugin:ui:languageChanged", language);
151 }
152 }
154 async handleRequest(request) {
155 try {
156 const url = new URL(request.url);
157 const pluginId = url.hostname;
158 const resolved = this.registry.resolveEntry(pluginId);
159 const requested = decodeURIComponent(url.pathname.replace(/^\/+/, "") || resolved.manifest.entry).replace(/\\/g, "/");
160 if (!requested || requested.startsWith(".") || requested.split("/").some((segment) => !segment || segment === ".." || segment.startsWith("."))) {
161 return new Response("Not found", { status: 404 });
162 }
163 const isChrome = requested === "__chj_core__/window-chrome.css";
164 const root = isChrome ? path.join(__dirname, "..", "bootstrap") : resolved.root;
165 const filePath = isChrome ? path.join(root, "pluginWindowChrome.css") : path.join(root, ...requested.split("/"));
166 const relative = path.relative(root, filePath);
167 if (relative.startsWith("..") || path.isAbsolute(relative)) return new Response("Not found", { status: 404 });
168 const stat = await fs.promises.stat(filePath);
169 if (!stat.isFile() || stat.size > 10 * 1024 * 1024) return new Response("Not found", { status: 404 });
170 const data = await fs.promises.readFile(filePath);
171 return new Response(data, {
172 status: 200,
173 headers: {
174 "Content-Type": MIME_TYPES[path.extname(filePath).toLowerCase()] || "application/octet-stream",
175 "Content-Security-Policy": PLUGIN_CSP,
176 "Cache-Control": "no-store",
177 "X-Content-Type-Options": "nosniff"
178 }
179 });
180 } catch (error) {
181 this.logger?.warn("Plugin resource request rejected.", { url: request.url, message: error?.message || String(error) });
182 return new Response("Not found", { status: 404 });
183 }
184 }
186 open(pluginId) {
187 if (!this.isVaultUnlocked()) throw new Error("Unlock the vault before opening a plugin.");
188 const resolved = this.registry.resolveEntry(pluginId);
189 const existing = this.windows.get(resolved.manifest.id);
190 if (existing && !existing.isDestroyed()) {
191 if (existing.isMinimized()) existing.restore();
192 const context = this.contexts.get(existing.webContents.id);
193 if (context) context.minimized = false;
194 existing.show();
195 existing.focus();
196 this._emitWindowState();
197 return resolved.manifest;
198 }
199 const manager = this.getMainWindow?.();
200 const window = new this.BrowserWindow({
201 title: `${resolved.manifest.name} · CH-J Server Manager`,
202 width: 980,
203 height: 700,
204 minWidth: 760,
205 minHeight: 520,
206 backgroundColor: "#07111f",
207 icon: this.icon,
208 show: false,
209 parent: manager && !manager.isDestroyed() ? manager : undefined,
210 modal: false,
211 ...windowChromeOptions(),
212 webPreferences: {
213 preload: this.preload,
214 contextIsolation: true,
215 nodeIntegration: false,
216 sandbox: true,
217 webSecurity: true,
218 allowRunningInsecureContent: false,
219 spellcheck: false,
220 partition: `plugin-${resolved.manifest.id}`
221 }
222 });
223 const pluginSession = window.webContents.session;
224 if (!pluginSession.protocol.isProtocolHandled("chj-plugin")) {
225 pluginSession.protocol.handle("chj-plugin", (request) => this.handleRequest(request));
226 }
227 pluginSession.setPermissionRequestHandler((_webContents, _permission, callback) => callback(false));
228 pluginSession.setPermissionCheckHandler(() => false);
229 window.webContents.setWindowOpenHandler(() => ({ action: "deny" }));
230 window.webContents.on("will-navigate", (event, url) => {
231 if (!url.startsWith(`chj-plugin://${resolved.manifest.id}/`)) event.preventDefault();
232 });
233 const context = { manifest: resolved.manifest, window, minimized: false };
234 const webContentsId = window.webContents.id;
235 this.contexts.set(webContentsId, context);
236 this.windows.set(resolved.manifest.id, window);
237 window.once("ready-to-show", () => {
238 if (window.isDestroyed() || context.minimized) return;
239 window.show();
240 window.focus();
241 this._emitWindowState();
242 });
243 window.on("minimize", () => this._minimize(context, { focusManager: !manager?.isMinimized() }));
244 const onManagerMinimize = () => this._minimize(context, { focusManager: false });
245 manager?.on("minimize", onManagerMinimize);
246 window.on("closed", () => {
247 manager?.removeListener("minimize", onManagerMinimize);
248 this.localHashService?.cleanupPlugin(resolved.manifest.id);
249 this.remoteFileService?.cleanupPlugin?.(resolved.manifest.id);
250 this.contexts.delete(webContentsId);
251 this.windows.delete(resolved.manifest.id);
252 this._emitWindowState();
253 });
254 const pluginUrl = `chj-plugin://${resolved.manifest.id}/${resolved.manifest.entry}`;
255 void window.loadURL(pluginUrl).catch((error) => {
256 this.logger?.error("Plugin window failed to load.", {
257 pluginId: resolved.manifest.id,
258 url: pluginUrl,
259 message: error?.message || String(error)
260 });
261 });
262 this.logger?.info("Plugin window opened.", { pluginId: resolved.manifest.id, version: resolved.manifest.version });
263 return resolved.manifest;
264 }
266 closeAll() {
267 for (const window of this.windows.values()) {
268 if (!window.isDestroyed()) window.close();
269 }
270 }
272 _minimize(context, { focusManager = true } = {}) {
273 if (context.window.isDestroyed()) return;
274 context.minimized = true;
275 context.window.hide();
276 const manager = this.getMainWindow?.();
277 if (focusManager && manager && !manager.isDestroyed()) {
278 if (manager.isMinimized()) manager.restore();
279 manager.show();
280 manager.focus();
281 }
282 this._emitWindowState();
283 this.logger?.info("Plugin window minimized to the Core taskbar.", { pluginId: context.manifest.id });
284 }
286 close(pluginId) {
287 const window = this.windows.get(String(pluginId || ""));
288 if (window && !window.isDestroyed()) window.close();
289 }
291 listWindows() {
292 const result = [];
293 for (const [pluginId, window] of this.windows) {
294 if (window.isDestroyed()) continue;
295 const context = this.contexts.get(window.webContents.id);
296 if (!context) continue;
297 result.push({
298 pluginId,
299 name: context.manifest.name,
300 version: context.manifest.version,
301 minimized: context.minimized === true
302 });
303 }
304 return result;
305 }
307 _emitWindowState() {
308 try { this.onWindowState?.(this.listWindows()); }
309 catch (error) { this.logger?.warn("Plugin window state could not be delivered.", { message: error?.message || String(error) }); }
310 }
312 _context(webContentsId) {
313 const context = this.contexts.get(webContentsId);
314 if (!context || context.window.isDestroyed()) {
315 const error = new Error("Untrusted plugin IPC sender.");
316 error.code = "UNTRUSTED_PLUGIN_SENDER";
317 throw error;
318 }
319 return context;
320 }
323module.exports = { MIME_TYPES, PLUGIN_CSP, PluginRuntime };
SHA-256b757443f92d07b0488fcbf6e22b8da31bf0837ec426165b59f714999f80cf915

CH-J Proprietary Software License 1.14 · Avots ir sniegts saskaņā ar CH-J Proprietary Software License 1.14. Tā pieejamība nemaina licences noteikumus un nepiešķir papildu tiesības.

Arhīvs SHA-256a9605c0870a223b6a8542bb0705edd51d8b04f3069582653417a8397fabcc09a
Faili