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 fs = require("node:fs");4
const path = require("node:path");5
const { windowChromeOptions } = require("../bootstrap/windowChrome");7
const 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
});22
const 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'";24
class 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.localHashService = options.localHashService;33
this.getLanguage = options.getLanguage || (() => "en");34
this.getMainWindow = options.getMainWindow;35
this.isVaultUnlocked = options.isVaultUnlocked;36
this.preload = options.preload;37
this.icon = options.icon;38
this.onWindowState = options.onWindowState;39
this.logger = options.logger;40
this.contexts = new Map();41
this.windows = new Map();42
this.localHashService?.on("progress", ({ pluginId, job }) => {43
const window = this.windows.get(pluginId);44
if (window && !window.isDestroyed()) window.webContents.send("plugin:hashing:progress", job);45
});46
}48
registerIpc(ipcMain) {49
const handle = (channel, permission, action) => {50
ipcMain.handle(channel, async (event, payload = {}) => {51
const context = this._context(event.sender.id);52
if (permission && !context.manifest.permissions.includes(permission)) {53
const error = new Error(`Plugin permission denied: ${permission}`);54
error.code = "PLUGIN_PERMISSION_DENIED";55
throw error;56
}57
try {58
return await action(context, payload);59
} catch (error) {60
// Electron serializes the message, but drops custom error properties.61
if (error?.code?.startsWith("HASH_") && !error.message.startsWith(`${error.code}:`)) error.message = `${error.code}: ${error.message}`;62
throw error;63
}64
});65
};66
handle("plugin:getInfo", null, (context) => context.manifest);67
handle("plugin:ui:getLanguage", null, () => this.getLanguage());68
handle("plugin:close", null, (context) => { context.window.close(); return { ok: true }; });69
handle("plugin:window:minimize", null, (context) => { this._minimize(context); return { ok: true }; });70
handle("plugin:sessions:list", "session.read", () => this.sessionManager.list());71
handle("plugin:system:metrics", "system.metrics.read", (_context, payload) => this.sessionManager.readSystemMetrics(payload.sessionId));72
handle("plugin:keys:generate", "keys.generate", (_context, payload) => this.keyGeneratorService.generate(payload));73
handle("plugin:keys:save", "keys.generate", (_context, payload) => this.keyGeneratorService.save(payload.generationId));74
handle("plugin:logs:read", "logs.read", (_context, payload) => this.logService.readTail(payload));75
handle("plugin:users:list", "users.read", (_context, payload) => this.sessionManager.readUsers(payload.sessionId));76
handle("plugin:users:manage", "users.manage", (_context, payload) => this.sessionManager.manageUser(payload.sessionId, payload));77
handle("plugin:nginx:inspect", "nginx.read", (_context, payload) => this.sessionManager.inspectNginx(payload.sessionId));78
handle("plugin:nginx:readConfig", "nginx.read", (_context, payload) => this.sessionManager.readNginxConfig(payload.sessionId, payload));79
handle("plugin:nginx:dumpConfig", "nginx.read", (_context, payload) => this.sessionManager.dumpNginxConfig(payload.sessionId, payload));80
handle("plugin:nginx:testConfig", "nginx.read", (_context, payload) => this.sessionManager.testNginxConfig(payload.sessionId, payload));81
handle("plugin:nginx:saveConfig", "nginx.manage", (_context, payload) => this.sessionManager.saveNginxConfig(payload.sessionId, payload));82
handle("plugin:nginx:reload", "nginx.manage", (_context, payload) => this.sessionManager.reloadNginx(payload.sessionId, payload));83
handle("plugin:files:list", "files.read", (_context, payload) => this.remoteFileService.list(payload.sessionId, payload.path));84
handle("plugin:files:readText", "files.read", (context, payload) => this.remoteFileService.readText(payload.sessionId, payload.path, payload.options || {}, context.manifest.id));85
handle("plugin:files:writeText", "files.write", (context, payload) => this.remoteFileService.writeText(payload.sessionId, payload.path, payload.text, payload.options || {}, context.manifest.id));86
handle("plugin:files:saveText", "files.write", (context, payload) => this.remoteFileService.saveText(payload.sessionId, payload.path, payload.text, payload.options || {}, context.manifest.id));87
handle("plugin:files:listRecovery", "files.read", (_context, payload) => this.remoteFileService.listRecovery(payload.sessionId, payload.options || {}));88
handle("plugin:files:readRecovery", "files.read", (_context, payload) => this.remoteFileService.readRecovery(payload.sessionId, payload.recoveryId, payload.options || {}));89
handle("plugin:files:cleanupRecovery", "files.write", (_context, payload) => this.remoteFileService.cleanupRecovery(payload.sessionId, payload.recoveryId, payload.options || {}));90
handle("plugin:files:closeText", "files.read", (context, payload) => this.remoteFileService.closeText(payload.editId, context.manifest.id));91
handle("plugin:files:createFile", "files.write", (_context, payload) => this.remoteFileService.createFile(payload.sessionId, payload.path));92
handle("plugin:files:mkdir", "files.write", (_context, payload) => this.remoteFileService.mkdir(payload.sessionId, payload.path));93
handle("plugin:files:rename", "files.write", (_context, payload) => this.remoteFileService.rename(payload.sessionId, payload.from, payload.to));94
handle("plugin:files:remove", "files.write", (_context, payload) => this.remoteFileService.remove(payload.sessionId, payload.path));95
handle("plugin:files:removeMany", "files.write", (_context, payload) => this.remoteFileService.removeMany(payload.sessionId, payload.paths, payload.recursive));96
handle("plugin:files:upload", "files.transfer", (_context, payload) => this.remoteFileService.upload(payload.sessionId, payload.directory));97
handle("plugin:files:download", "files.transfer", (_context, payload) => this.remoteFileService.download(payload.sessionId, payload.path));98
handle("plugin:files:downloadMany", "files.transfer", (_context, payload) => this.remoteFileService.downloadMany(payload.sessionId, payload.paths));99
handle("plugin:files:downloadArchive", "files.transfer", (_context, payload) => this.remoteFileService.downloadArchive(payload.sessionId, payload.paths, payload.format));100
handle("plugin:hashing:algorithms", "local.hash", () => this.localHashService.getAlgorithms());101
handle("plugin:hashing:selectFiles", "local.hash", (context, payload) => this.localHashService.selectFiles(context.manifest.id, payload));102
handle("plugin:hashing:selectDirectory", "local.hash", (context) => this.localHashService.selectDirectory(context.manifest.id));103
handle("plugin:hashing:selectManifest", "local.hash", (context) => this.localHashService.selectManifest(context.manifest.id));104
handle("plugin:hashing:selectManifestDestination", "local.hash", (context, payload) => this.localHashService.selectManifestDestination(context.manifest.id, payload));105
handle("plugin:hashing:start", "local.hash", (context, payload) => this.localHashService.start(context.manifest.id, payload));106
handle("plugin:hashing:verify", "local.hash", (context, payload) => this.localHashService.verify(context.manifest.id, payload));107
handle("plugin:hashing:compare", "local.hash", (context, payload) => this.localHashService.compare(context.manifest.id, payload));108
handle("plugin:hashing:generateManifest", "local.hash", (context, payload) => this.localHashService.generateManifest(context.manifest.id, payload));109
handle("plugin:hashing:verifyManifest", "local.hash", (context, payload) => this.localHashService.verifyManifest(context.manifest.id, payload));110
handle("plugin:hashing:exportResults", "local.hash", (context, payload) => this.localHashService.exportResults(context.manifest.id, payload));111
handle("plugin:hashing:copyResult", "local.hash", (context, payload) => this.localHashService.copyResult(context.manifest.id, payload));112
handle("plugin:hashing:status", "local.hash", (context, payload) => this.localHashService.status(context.manifest.id, payload.jobId));113
handle("plugin:hashing:cancel", "local.hash", (context, payload) => this.localHashService.cancel(context.manifest.id, payload.jobId));114
}116
notifyLanguageChanged(language) {117
for (const window of this.windows.values()) {118
if (!window.isDestroyed()) window.webContents.send("plugin:ui:languageChanged", language);119
}120
}122
async handleRequest(request) {123
try {124
const url = new URL(request.url);125
const pluginId = url.hostname;126
const resolved = this.registry.resolveEntry(pluginId);127
const requested = decodeURIComponent(url.pathname.replace(/^\/+/, "") || resolved.manifest.entry).replace(/\\/g, "/");128
if (!requested || requested.startsWith(".") || requested.split("/").some((segment) => !segment || segment === ".." || segment.startsWith("."))) {129
return new Response("Not found", { status: 404 });130
}131
const isChrome = requested === "__chj_core__/window-chrome.css";132
const root = isChrome ? path.join(__dirname, "..", "bootstrap") : resolved.root;133
const filePath = isChrome ? path.join(root, "pluginWindowChrome.css") : path.join(root, ...requested.split("/"));134
const relative = path.relative(root, filePath);135
if (relative.startsWith("..") || path.isAbsolute(relative)) return new Response("Not found", { status: 404 });136
const stat = await fs.promises.stat(filePath);137
if (!stat.isFile() || stat.size > 10 * 1024 * 1024) return new Response("Not found", { status: 404 });138
const data = await fs.promises.readFile(filePath);139
return new Response(data, {140
status: 200,141
headers: {142
"Content-Type": MIME_TYPES[path.extname(filePath).toLowerCase()] || "application/octet-stream",143
"Content-Security-Policy": PLUGIN_CSP,144
"Cache-Control": "no-store",145
"X-Content-Type-Options": "nosniff"146
}147
});148
} catch (error) {149
this.logger?.warn("Plugin resource request rejected.", { url: request.url, message: error?.message || String(error) });150
return new Response("Not found", { status: 404 });151
}152
}154
open(pluginId) {155
if (!this.isVaultUnlocked()) throw new Error("Unlock the vault before opening a plugin.");156
const resolved = this.registry.resolveEntry(pluginId);157
const existing = this.windows.get(resolved.manifest.id);158
if (existing && !existing.isDestroyed()) {159
if (existing.isMinimized()) existing.restore();160
const context = this.contexts.get(existing.webContents.id);161
if (context) context.minimized = false;162
existing.show();163
existing.focus();164
this._emitWindowState();165
return resolved.manifest;166
}167
const manager = this.getMainWindow?.();168
const window = new this.BrowserWindow({169
title: `${resolved.manifest.name} · CH-J Server Manager`,170
width: 980,171
height: 700,172
minWidth: 760,173
minHeight: 520,174
backgroundColor: "#07111f",175
icon: this.icon,176
show: false,177
parent: manager && !manager.isDestroyed() ? manager : undefined,178
modal: false,179
...windowChromeOptions(),180
webPreferences: {181
preload: this.preload,182
contextIsolation: true,183
nodeIntegration: false,184
sandbox: true,185
webSecurity: true,186
allowRunningInsecureContent: false,187
spellcheck: false,188
partition: `plugin-${resolved.manifest.id}`189
}190
});191
const pluginSession = window.webContents.session;192
if (!pluginSession.protocol.isProtocolHandled("chj-plugin")) {193
pluginSession.protocol.handle("chj-plugin", (request) => this.handleRequest(request));194
}195
pluginSession.setPermissionRequestHandler((_webContents, _permission, callback) => callback(false));196
pluginSession.setPermissionCheckHandler(() => false);197
window.webContents.setWindowOpenHandler(() => ({ action: "deny" }));198
window.webContents.on("will-navigate", (event, url) => {199
if (!url.startsWith(`chj-plugin://${resolved.manifest.id}/`)) event.preventDefault();200
});201
const context = { manifest: resolved.manifest, window, minimized: false };202
const webContentsId = window.webContents.id;203
this.contexts.set(webContentsId, context);204
this.windows.set(resolved.manifest.id, window);205
window.once("ready-to-show", () => {206
if (window.isDestroyed() || context.minimized) return;207
window.show();208
window.focus();209
this._emitWindowState();210
});211
window.on("minimize", () => this._minimize(context, { focusManager: !manager?.isMinimized() }));212
const onManagerMinimize = () => this._minimize(context, { focusManager: false });213
manager?.on("minimize", onManagerMinimize);214
window.on("closed", () => {215
manager?.removeListener("minimize", onManagerMinimize);216
this.localHashService?.cleanupPlugin(resolved.manifest.id);217
this.remoteFileService?.cleanupPlugin?.(resolved.manifest.id);218
this.contexts.delete(webContentsId);219
this.windows.delete(resolved.manifest.id);220
this._emitWindowState();221
});222
const pluginUrl = `chj-plugin://${resolved.manifest.id}/${resolved.manifest.entry}`;223
void window.loadURL(pluginUrl).catch((error) => {224
this.logger?.error("Plugin window failed to load.", {225
pluginId: resolved.manifest.id,226
url: pluginUrl,227
message: error?.message || String(error)228
});229
});230
this.logger?.info("Plugin window opened.", { pluginId: resolved.manifest.id, version: resolved.manifest.version });231
return resolved.manifest;232
}234
closeAll() {235
for (const window of this.windows.values()) {236
if (!window.isDestroyed()) window.close();237
}238
}240
_minimize(context, { focusManager = true } = {}) {241
if (context.window.isDestroyed()) return;242
context.minimized = true;243
context.window.hide();244
const manager = this.getMainWindow?.();245
if (focusManager && manager && !manager.isDestroyed()) {246
if (manager.isMinimized()) manager.restore();247
manager.show();248
manager.focus();249
}250
this._emitWindowState();251
this.logger?.info("Plugin window minimized to the Core taskbar.", { pluginId: context.manifest.id });252
}254
close(pluginId) {255
const window = this.windows.get(String(pluginId || ""));256
if (window && !window.isDestroyed()) window.close();257
}259
listWindows() {260
const result = [];261
for (const [pluginId, window] of this.windows) {262
if (window.isDestroyed()) continue;263
const context = this.contexts.get(window.webContents.id);264
if (!context) continue;265
result.push({266
pluginId,267
name: context.manifest.name,268
version: context.manifest.version,269
minimized: context.minimized === true270
});271
}272
return result;273
}275
_emitWindowState() {276
try { this.onWindowState?.(this.listWindows()); }277
catch (error) { this.logger?.warn("Plugin window state could not be delivered.", { message: error?.message || String(error) }); }278
}280
_context(webContentsId) {281
const context = this.contexts.get(webContentsId);282
if (!context || context.window.isDestroyed()) {283
const error = new Error("Untrusted plugin IPC sender.");284
error.code = "UNTRUSTED_PLUGIN_SENDER";285
throw error;286
}287
return context;288
}289
}291
module.exports = { MIME_TYPES, PLUGIN_CSP, PluginRuntime };SHA-256: fecab5bdfab48d64db240f2b0ec6e00a4b4a0eab2c327dfad28b0ddd31f9f252
SHA-256 archivu: 5ac91caf4fa32a6fdb114f2430deed486fbe7489d5eea343d1f034169fafb5e0