公開済みのソース
ソースの ZIP をダウンロード
CH-J Tools
特定のアプリケーションリリースのディレクトリとファイルを閲覧できます。
CH-J / Server Manager
406 ファイル
core-20261024T135205Z-7c487d719de2
追加日時: · ビルド日時:
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.transferManager = options.transferManager;33
this.localHashService = options.localHashService;34
this.userAdmin = options.userAdmin;35
this.virtualMemory = options.virtualMemory;36
this.diskService = options.diskService;37
this.getLanguage = options.getLanguage || (() => "en");38
this.getMainWindow = options.getMainWindow;39
this.isVaultUnlocked = options.isVaultUnlocked;40
this.preload = options.preload;41
this.icon = options.icon;42
this.onWindowState = options.onWindowState;43
this.logger = options.logger;44
this.contexts = new Map();45
this.windows = new Map();46
this.localHashService?.on("progress", ({ pluginId, job }) => {47
const window = this.windows.get(pluginId);48
if (window && !window.isDestroyed()) window.webContents.send("plugin:hashing:progress", job);49
});50
// Transfer progress goes only to the plugin that owns the jobs; jobs keep running while it is closed.51
this.transferManager?.on("update", (update) => {52
const window = this.windows.get(update.owner);53
if (window && !window.isDestroyed()) window.webContents.send("plugin:files:transfers:update", update);54
});55
}57
registerIpc(ipcMain) {58
const handle = (channel, permission, action) => {59
ipcMain.handle(channel, async (event, payload = {}) => {60
const context = this._context(event.sender.id);61
if (permission && !context.manifest.permissions.includes(permission)) {62
const error = new Error(`Plugin permission denied: ${permission}`);63
error.code = "PLUGIN_PERMISSION_DENIED";64
throw error;65
}66
try {67
return await action(context, payload);68
} catch (error) {69
// Electron serializes the message, but drops custom error properties.70
const exposeCode = error?.code?.startsWith?.("HASH_") || ((channel.startsWith("plugin:users:") || channel.startsWith("plugin:system:") || channel.startsWith("plugin:files:") || channel.startsWith("plugin:disks:")) && /^[A-Z][A-Z0-9_]{2,63}$/.test(String(error?.code || "")));71
if (exposeCode && !error.message.startsWith(`${error.code}:`)) error.message = `${error.code}: ${error.message}`;72
throw error;73
}74
});75
};76
handle("plugin:getInfo", null, (context) => context.manifest);77
handle("plugin:ui:getLanguage", null, () => this.getLanguage());78
handle("plugin:close", null, (context) => { context.window.close(); return { ok: true }; });79
handle("plugin:window:minimize", null, (context) => { this._minimize(context); return { ok: true }; });80
handle("plugin:sessions:list", "session.read", () => this.sessionManager.list());81
handle("plugin:system:metrics", "system.metrics.read", (_context, payload) => this.sessionManager.readSystemMetrics(payload.sessionId));82
handle("plugin:system:virtualMemory", "system.metrics.read", (_context, payload) => this.virtualMemory.read(payload.sessionId, { sudoPassword: payload.sudoPassword, privileged: payload.privileged === true }));83
handle("plugin:system:manageVirtualMemory", "system.memory.manage", (_context, payload) => this.virtualMemory.manage(payload.sessionId, payload));84
const disks = () => {85
if (!this.diskService) throw Object.assign(new Error("DISKS_UNAVAILABLE: The disk service is unavailable."), { code: "DISKS_UNAVAILABLE" });86
return this.diskService;87
};88
handle("plugin:disks:inspect", "disks.read", (_context, payload) => disks().inspect(payload.sessionId, { privileged: payload.privileged === true, sudoPassword: payload.sudoPassword, rememberSudo: payload.rememberSudo === true }));89
handle("plugin:disks:smart", "disks.read", (_context, payload) => disks().smart(payload.sessionId, payload));90
handle("plugin:disks:jobs", "disks.read", (_context, payload) => disks().jobs(payload.sessionId, payload));91
handle("plugin:disks:jobStatus", "disks.read", (_context, payload) => disks().jobStatus(payload.sessionId, payload));92
handle("plugin:disks:plan", "disks.manage", (_context, payload) => disks().plan(payload.sessionId, payload));93
handle("plugin:disks:autoExpand", "disks.manage", (_context, payload) => disks().autoExpand(payload.sessionId, payload));94
handle("plugin:disks:cancelPlan", "disks.manage", (_context, payload) => disks().cancelPlan(payload.planId));95
handle("plugin:disks:apply", "disks.manage", (_context, payload) => disks().apply(payload.sessionId, payload));96
handle("plugin:keys:generate", "keys.generate", (_context, payload) => this.keyGeneratorService.generate(payload));97
handle("plugin:keys:save", "keys.generate", (_context, payload) => this.keyGeneratorService.save(payload.generationId));98
handle("plugin:keys:password", "keys.generate", (_context, payload) => this.keyGeneratorService.generatePassword(payload));99
handle("plugin:keys:copyPassword", "keys.generate", (_context, payload) => this.keyGeneratorService.copyPassword(payload.passwordId));100
handle("plugin:logs:read", "logs.read", (_context, payload) => this.logService.readTail(payload));101
handle("plugin:users:list", "users.read", (_context, payload) => this.userAdmin.list(payload.sessionId));102
handle("plugin:users:privileges", "users.read", (_context, payload) => this.userAdmin.privileges(payload.sessionId, { refresh: payload.refresh === true }));103
handle("plugin:users:inventory", "users.read", (_context, payload) => this.userAdmin.inventory(payload.sessionId, payload));104
handle("plugin:users:detail", "users.read", (_context, payload) => this.userAdmin.detail(payload.sessionId, payload));105
handle("plugin:users:manage", "users.manage", (_context, payload) => this.userAdmin.manage(payload.sessionId, payload));106
handle("plugin:users:keys", "users.manage", (_context, payload) => this.userAdmin.keys(payload.sessionId, payload));107
handle("plugin:users:sshPolicy", "users.manage", (_context, payload) => this.userAdmin.sshPolicy(payload.sessionId, payload));108
handle("plugin:nginx:inspect", "nginx.read", (_context, payload) => this.sessionManager.inspectNginx(payload.sessionId));109
handle("plugin:nginx:readConfig", "nginx.read", (_context, payload) => this.sessionManager.readNginxConfig(payload.sessionId, payload));110
handle("plugin:nginx:dumpConfig", "nginx.read", (_context, payload) => this.sessionManager.dumpNginxConfig(payload.sessionId, payload));111
handle("plugin:nginx:testConfig", "nginx.read", (_context, payload) => this.sessionManager.testNginxConfig(payload.sessionId, payload));112
handle("plugin:nginx:saveConfig", "nginx.manage", (_context, payload) => this.sessionManager.saveNginxConfig(payload.sessionId, payload));113
handle("plugin:nginx:reload", "nginx.manage", (_context, payload) => this.sessionManager.reloadNginx(payload.sessionId, payload));114
handle("plugin:files:list", "files.read", (_context, payload) => this.remoteFileService.list(payload.sessionId, payload.path, payload.options || {}));115
handle("plugin:files:privileges", "files.read", (_context, payload) => this.remoteFileService.privileges(payload.sessionId));116
handle("plugin:files:readText", "files.read", (context, payload) => this.remoteFileService.readText(payload.sessionId, payload.path, payload.options || {}, context.manifest.id));117
handle("plugin:files:writeText", "files.write", (context, payload) => this.remoteFileService.writeText(payload.sessionId, payload.path, payload.text, payload.options || {}, context.manifest.id));118
handle("plugin:files:saveText", "files.write", (context, payload) => this.remoteFileService.saveText(payload.sessionId, payload.path, payload.text, payload.options || {}, context.manifest.id));119
handle("plugin:files:listRecovery", "files.read", (_context, payload) => this.remoteFileService.listRecovery(payload.sessionId, payload.options || {}));120
handle("plugin:files:readRecovery", "files.read", (_context, payload) => this.remoteFileService.readRecovery(payload.sessionId, payload.recoveryId, payload.options || {}));121
handle("plugin:files:cleanupRecovery", "files.write", (_context, payload) => this.remoteFileService.cleanupRecovery(payload.sessionId, payload.recoveryId, payload.options || {}));122
handle("plugin:files:closeText", "files.read", (context, payload) => this.remoteFileService.closeText(payload.editId, context.manifest.id));123
handle("plugin:files:createFile", "files.write", (_context, payload) => this.remoteFileService.createFile(payload.sessionId, payload.path, payload.options || {}));124
handle("plugin:files:mkdir", "files.write", (_context, payload) => this.remoteFileService.mkdir(payload.sessionId, payload.path, payload.options || {}));125
handle("plugin:files:rename", "files.write", (_context, payload) => this.remoteFileService.rename(payload.sessionId, payload.from, payload.to, payload.options || {}));126
handle("plugin:files:remove", "files.write", (_context, payload) => this.remoteFileService.remove(payload.sessionId, payload.path, payload.options || {}));127
handle("plugin:files:removeMany", "files.write", (_context, payload) => this.remoteFileService.removeMany(payload.sessionId, payload.paths, payload.recursive, payload.options || {}));128
handle("plugin:files:upload", "files.transfer", (_context, payload) => this.remoteFileService.upload(payload.sessionId, payload.directory));129
handle("plugin:files:download", "files.transfer", (_context, payload) => this.remoteFileService.download(payload.sessionId, payload.path));130
handle("plugin:files:downloadMany", "files.transfer", (_context, payload) => this.remoteFileService.downloadMany(payload.sessionId, payload.paths));131
handle("plugin:files:downloadArchive", "files.transfer", (_context, payload) => this.remoteFileService.downloadArchive(payload.sessionId, payload.paths, payload.format));132
const transfers = () => {133
if (!this.transferManager) throw Object.assign(new Error("TRANSFERS_UNAVAILABLE: Transfer Manager is unavailable."), { code: "TRANSFERS_UNAVAILABLE" });134
return this.transferManager;135
};136
const ids = (payload) => payload.ids === "all" ? "all" : (Array.isArray(payload.ids) ? payload.ids : [payload.ids]).slice(0, 20000).map(String);137
handle("plugin:files:transfers:list", "files.transfer", (context) => transfers().list(context.manifest.id));138
handle("plugin:files:transfers:planUpload", "files.transfer", (context, payload) => transfers().planUpload(context.manifest.id, payload.sessionId, payload.directory, payload.options || {}));139
handle("plugin:files:transfers:planDownload", "files.transfer", (context, payload) => transfers().planDownload(context.manifest.id, payload.sessionId, payload.paths, payload.options || {}));140
handle("plugin:files:transfers:start", "files.transfer", (context, payload) => transfers().start(context.manifest.id, payload.planId, payload.options || {}));141
handle("plugin:files:transfers:pause", "files.transfer", (context, payload) => transfers().pause(context.manifest.id, ids(payload)));142
handle("plugin:files:transfers:resume", "files.transfer", (context, payload) => transfers().resume(context.manifest.id, ids(payload), payload.options || {}));143
handle("plugin:files:transfers:retry", "files.transfer", (context, payload) => transfers().retry(context.manifest.id, ids(payload), payload.options || {}));144
handle("plugin:files:transfers:cancel", "files.transfer", (context, payload) => transfers().cancel(context.manifest.id, ids(payload)));145
handle("plugin:files:transfers:remove", "files.transfer", (context, payload) => transfers().remove(context.manifest.id, ids(payload)));146
handle("plugin:files:transfers:settings", "files.transfer", (context, payload) => transfers().updateSettings(context.manifest.id, payload.settings || {}));147
handle("plugin:hashing:algorithms", "local.hash", () => this.localHashService.getAlgorithms());148
handle("plugin:hashing:selectFiles", "local.hash", (context, payload) => this.localHashService.selectFiles(context.manifest.id, payload));149
handle("plugin:hashing:selectDirectory", "local.hash", (context) => this.localHashService.selectDirectory(context.manifest.id));150
handle("plugin:hashing:selectManifest", "local.hash", (context) => this.localHashService.selectManifest(context.manifest.id));151
handle("plugin:hashing:selectManifestDestination", "local.hash", (context, payload) => this.localHashService.selectManifestDestination(context.manifest.id, payload));152
handle("plugin:hashing:start", "local.hash", (context, payload) => this.localHashService.start(context.manifest.id, payload));153
handle("plugin:hashing:verify", "local.hash", (context, payload) => this.localHashService.verify(context.manifest.id, payload));154
handle("plugin:hashing:compare", "local.hash", (context, payload) => this.localHashService.compare(context.manifest.id, payload));155
handle("plugin:hashing:generateManifest", "local.hash", (context, payload) => this.localHashService.generateManifest(context.manifest.id, payload));156
handle("plugin:hashing:verifyManifest", "local.hash", (context, payload) => this.localHashService.verifyManifest(context.manifest.id, payload));157
handle("plugin:hashing:exportResults", "local.hash", (context, payload) => this.localHashService.exportResults(context.manifest.id, payload));158
handle("plugin:hashing:copyResult", "local.hash", (context, payload) => this.localHashService.copyResult(context.manifest.id, payload));159
handle("plugin:hashing:status", "local.hash", (context, payload) => this.localHashService.status(context.manifest.id, payload.jobId));160
handle("plugin:hashing:cancel", "local.hash", (context, payload) => this.localHashService.cancel(context.manifest.id, payload.jobId));161
}163
notifyLanguageChanged(language) {164
for (const window of this.windows.values()) {165
if (!window.isDestroyed()) window.webContents.send("plugin:ui:languageChanged", language);166
}167
}169
async handleRequest(request) {170
try {171
const url = new URL(request.url);172
const pluginId = url.hostname;173
const resolved = this.registry.resolveEntry(pluginId);174
const requested = decodeURIComponent(url.pathname.replace(/^\/+/, "") || resolved.manifest.entry).replace(/\\/g, "/");175
if (!requested || requested.startsWith(".") || requested.split("/").some((segment) => !segment || segment === ".." || segment.startsWith("."))) {176
return new Response("Not found", { status: 404 });177
}178
const isChrome = requested === "__chj_core__/window-chrome.css";179
const root = isChrome ? path.join(__dirname, "..", "bootstrap") : resolved.root;180
const filePath = isChrome ? path.join(root, "pluginWindowChrome.css") : path.join(root, ...requested.split("/"));181
const relative = path.relative(root, filePath);182
if (relative.startsWith("..") || path.isAbsolute(relative)) return new Response("Not found", { status: 404 });183
const stat = await fs.promises.stat(filePath);184
if (!stat.isFile() || stat.size > 10 * 1024 * 1024) return new Response("Not found", { status: 404 });185
const data = await fs.promises.readFile(filePath);186
return new Response(data, {187
status: 200,188
headers: {189
"Content-Type": MIME_TYPES[path.extname(filePath).toLowerCase()] || "application/octet-stream",190
"Content-Security-Policy": PLUGIN_CSP,191
"Cache-Control": "no-store",192
"X-Content-Type-Options": "nosniff"193
}194
});195
} catch (error) {196
this.logger?.warn("Plugin resource request rejected.", { url: request.url, message: error?.message || String(error) });197
return new Response("Not found", { status: 404 });198
}199
}201
open(pluginId) {202
if (!this.isVaultUnlocked()) throw new Error("Unlock the vault before opening a plugin.");203
const resolved = this.registry.resolveEntry(pluginId);204
const existing = this.windows.get(resolved.manifest.id);205
if (existing && !existing.isDestroyed()) {206
if (existing.isMinimized()) existing.restore();207
const context = this.contexts.get(existing.webContents.id);208
if (context) context.minimized = false;209
existing.show();210
existing.focus();211
this._emitWindowState();212
return resolved.manifest;213
}214
const manager = this.getMainWindow?.();215
const window = new this.BrowserWindow({216
title: `${resolved.manifest.name} · CH-J Server Manager`,217
width: 980,218
height: 700,219
minWidth: 760,220
minHeight: 520,221
backgroundColor: "#07111f",222
icon: this.icon,223
show: false,224
parent: manager && !manager.isDestroyed() ? manager : undefined,225
modal: false,226
...windowChromeOptions(),227
webPreferences: {228
preload: this.preload,229
contextIsolation: true,230
nodeIntegration: false,231
sandbox: true,232
webSecurity: true,233
allowRunningInsecureContent: false,234
spellcheck: false,235
partition: `plugin-${resolved.manifest.id}`236
}237
});238
const pluginSession = window.webContents.session;239
if (!pluginSession.protocol.isProtocolHandled("chj-plugin")) {240
pluginSession.protocol.handle("chj-plugin", (request) => this.handleRequest(request));241
}242
pluginSession.setPermissionRequestHandler((_webContents, _permission, callback) => callback(false));243
pluginSession.setPermissionCheckHandler(() => false);244
window.webContents.setWindowOpenHandler(() => ({ action: "deny" }));245
window.webContents.on("will-navigate", (event, url) => {246
if (!url.startsWith(`chj-plugin://${resolved.manifest.id}/`)) event.preventDefault();247
});248
const context = { manifest: resolved.manifest, window, minimized: false };249
const webContentsId = window.webContents.id;250
this.contexts.set(webContentsId, context);251
this.windows.set(resolved.manifest.id, window);252
window.once("ready-to-show", () => {253
if (window.isDestroyed() || context.minimized) return;254
window.show();255
window.focus();256
this._emitWindowState();257
});258
window.on("minimize", () => this._minimize(context, { focusManager: !manager?.isMinimized() }));259
const onManagerMinimize = () => this._minimize(context, { focusManager: false });260
manager?.on("minimize", onManagerMinimize);261
window.on("closed", () => {262
manager?.removeListener("minimize", onManagerMinimize);263
this.localHashService?.cleanupPlugin(resolved.manifest.id);264
this.remoteFileService?.cleanupPlugin?.(resolved.manifest.id);265
this.contexts.delete(webContentsId);266
this.windows.delete(resolved.manifest.id);267
this._emitWindowState();268
});269
const pluginUrl = `chj-plugin://${resolved.manifest.id}/${resolved.manifest.entry}`;270
void window.loadURL(pluginUrl).catch((error) => {271
this.logger?.error("Plugin window failed to load.", {272
pluginId: resolved.manifest.id,273
url: pluginUrl,274
message: error?.message || String(error)275
});276
});277
this.logger?.info("Plugin window opened.", { pluginId: resolved.manifest.id, version: resolved.manifest.version });278
return resolved.manifest;279
}281
closeAll() {282
for (const window of this.windows.values()) {283
if (!window.isDestroyed()) window.close();284
}285
}287
_minimize(context, { focusManager = true } = {}) {288
if (context.window.isDestroyed()) return;289
context.minimized = true;290
context.window.hide();291
const manager = this.getMainWindow?.();292
if (focusManager && manager && !manager.isDestroyed()) {293
if (manager.isMinimized()) manager.restore();294
manager.show();295
manager.focus();296
}297
this._emitWindowState();298
this.logger?.info("Plugin window minimized to the Core taskbar.", { pluginId: context.manifest.id });299
}301
close(pluginId) {302
const window = this.windows.get(String(pluginId || ""));303
if (window && !window.isDestroyed()) window.close();304
}306
listWindows() {307
const result = [];308
for (const [pluginId, window] of this.windows) {309
if (window.isDestroyed()) continue;310
const context = this.contexts.get(window.webContents.id);311
if (!context) continue;312
result.push({313
pluginId,314
name: context.manifest.name,315
version: context.manifest.version,316
minimized: context.minimized === true317
});318
}319
return result;320
}322
_emitWindowState() {323
try { this.onWindowState?.(this.listWindows()); }324
catch (error) { this.logger?.warn("Plugin window state could not be delivered.", { message: error?.message || String(error) }); }325
}327
_context(webContentsId) {328
const context = this.contexts.get(webContentsId);329
if (!context || context.window.isDestroyed()) {330
const error = new Error("Untrusted plugin IPC sender.");331
error.code = "UNTRUSTED_PLUGIN_SENDER";332
throw error;333
}334
return context;335
}336
}338
module.exports = { MIME_TYPES, PLUGIN_CSP, PluginRuntime };SHA-256
25ad1c12234bdd885fcedf9639efadaaffe2b5919497656f06ca0c8dc3471475CH-J Proprietary Software License 1.14 · ソースは CH-J Proprietary Software License 1.14 に基づいて提供されています。公開されていても、ライセンス条項が変わったり、追加の権利が付与されたりすることはありません。
アーカイブの SHA-256
476ed8aef0f1e96a5acdf223127b6692acdf4492eeaf5d0816b6e2a634fbbda6