CH-J ToolsSSH를 통한 서버 관리
메뉴
공개된 소스

CH-J Tools

특정 애플리케이션 릴리스의 디렉터리와 파일을 살펴보세요.

소스 ZIP 다운로드
CH-J / Server Manager 406 개 파일
core-20261024T135205Z-7c487d719de2 추가됨: · 빌드됨:
pluginRuntime.js 추가됨: 날짜 기록 없음 21.4 KB · 338 줄
코드 파일 다운로드
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.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 === true
317 });
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 }
338module.exports = { MIME_TYPES, PLUGIN_CSP, PluginRuntime };
SHA-25625ad1c12234bdd885fcedf9639efadaaffe2b5919497656f06ca0c8dc3471475

CH-J Proprietary Software License 1.14 · 소스는 CH-J Proprietary Software License 1.14에 따라 제공됩니다. 소스를 공개한다고 해서 라이선스 조항이 바뀌거나 추가 권리가 부여되지는 않습니다.

압축 파일 SHA-256476ed8aef0f1e96a5acdf223127b6692acdf4492eeaf5d0816b6e2a634fbbda6
파일