CH-J Server Managergestione server su SSH
Menu
Sorgente pubblicata

CH-J Server Manager

Sfoglia directory e file per una specifica release di applicazioni.

Scarica sorgente ZIP
CH-J / Server Manager 339 file
core-20261014T184258Z-3fbed0b93fdf Aggiunto: Data non registrata · Costruito:
File
registerCoreIpc.js Aggiunto: Data non registrata 14,5 KB · 278 righe
Scarica file
1"use strict";
3const path = require("node:path");
4const fs = require("node:fs");
5const { ProfileTransfer, serializeProfiles, MAX_BYTES } = require("../profiles/profileTransfer");
6const { LICENSE_NAME, LICENSE_VERSION } = require("../legal/licenseAcceptance");
8const PRIVATE_KEY_DIALOG_COPY = Object.freeze({
9 cs: { title: "Vybrat soukromý SSH klíč", keys: "Soukromé SSH klíče", all: "Všechny soubory" },
10 de: { title: "Privaten SSH-Schlüssel auswählen", keys: "Private SSH-Schlüssel", all: "Alle Dateien" },
11 en: { title: "Select an SSH private key", keys: "SSH private keys", all: "All files" }
12});
14function registerCoreIpc(options) {
15 const {
16 ipcMain, shell, dialog, clipboard, app, configStore, pluginRegistry, pluginService, pluginRuntime, profileService,
17 sessionManager, vaultStore, biometricService, vaultLockController, updateService, updateInstallerLauncher, diagnosticsService, getMainWindow, logger
18 } = options;
19 const profileTransfer = new ProfileTransfer(profileService);
21 function assertTrustedSender(event) {
22 const mainWindow = getMainWindow();
23 if (!mainWindow || mainWindow.isDestroyed() || event.sender.id !== mainWindow.webContents.id ||
24 (mainWindow.webContents.mainFrame && event.senderFrame !== mainWindow.webContents.mainFrame)) {
25 const error = new Error("Untrusted IPC sender.");
26 error.code = "UNTRUSTED_IPC_SENDER";
27 throw error;
28 }
29 }
31 function handle(channel, handler) {
32 ipcMain.handle(channel, async (event, payload) => {
33 assertTrustedSender(event);
34 try {
35 return await handler(payload);
36 } catch (error) {
37 logger?.error("IPC operation failed.", { channel, message: error?.message || String(error), code: error?.code });
38 throw error;
39 }
40 });
41 }
43 function send(channel, handler) {
44 ipcMain.on(channel, (event, payload) => {
45 try {
46 assertTrustedSender(event);
47 handler(payload);
48 } catch (error) {
49 logger?.warn("IPC event was rejected.", { channel, message: error?.message || String(error), code: error?.code });
50 }
51 });
52 }
54 function sessionResult(action) {
55 return async (payload) => {
56 try {
57 return { ok: true, value: await action(payload || {}) };
58 } catch (error) {
59 return {
60 ok: false,
61 error: typeof error?.toJSON === "function"
62 ? error.toJSON()
63 : { code: error?.code || "SSH_OPERATION_FAILED", message: error?.message || String(error) }
64 };
65 }
66 };
67 }
69 handle("core:getInfo", async () => ({
70 name: app.getName(),
71 version: app.getVersion(),
72 platform: process.platform,
73 arch: process.arch,
74 coreApi: "1.0.0",
75 pluginApi: options.pluginApiVersion || require("../plugins/pluginRegistry").PLUGIN_API_VERSION,
76 dataSchema: 1,
77 license: `${LICENSE_NAME} ${LICENSE_VERSION}`,
78 copyright: "Copyright 2026 Josef Chudy"
79 }));
80 handle("legal:open", async (payload = {}) => {
81 const documents = {
82 documentation: "README.md",
83 safetyCzech: "provozni-bezpecnost-a-zalohovani.md",
84 safetyEnglish: "operational-safety-and-backup-guidance.md",
85 safetyGerman: "betriebssicherheit-und-datensicherung.md"
86 };
87 const documentKey = String(payload.document || "");
88 if (Object.hasOwn(documents, documentKey)) {
89 const docsRoot = path.join(app.isPackaged ? process.resourcesPath : app.getAppPath(), "docs");
90 const error = await shell.openPath(path.join(docsRoot, documents[documentKey]));
91 if (error) throw new Error(`The documentation could not be opened: ${error}`);
92 return { ok: true };
93 }
94 const names = {
95 application: "APPLICATION_LICENSE.txt",
96 notice: "NOTICE.txt",
97 thirdParty: "THIRD_PARTY_NOTICES.txt",
98 thirdPartyBundle: "THIRD_PARTY_LICENSES.zip",
99 electron: "ELECTRON_LICENSE.txt",
100 chromium: "CHROMIUM_LICENSES.html"
101 };
102 const filename = Object.hasOwn(names, documentKey) ? names[documentKey] : null;
103 if (!filename) throw new Error("Unknown legal document.");
104 const root = app.isPackaged ? path.join(process.resourcesPath, "licenses") : app.getAppPath();
105 const developmentFiles = {
106 APPLICATION_LICENSE: "LICENSE",
107 NOTICE: "NOTICE",
108 THIRD_PARTY_NOTICES: "THIRD_PARTY_NOTICES.txt",
109 THIRD_PARTY_LICENSES: "THIRD_PARTY_LICENSES.zip",
110 ELECTRON_LICENSE: path.join("node_modules", "electron", "dist", "LICENSE"),
111 CHROMIUM_LICENSES: path.join("node_modules", "electron", "dist", "LICENSES.chromium.html")
112 };
113 const stem = path.parse(filename).name;
114 const filePath = app.isPackaged ? path.join(root, filename) : path.join(root, developmentFiles[stem]);
115 const error = await shell.openPath(filePath);
116 if (error) throw new Error(`The legal document could not be opened: ${error}`);
117 return { ok: true };
118 });
119 handle("config:get", async () => configStore.get());
120 handle("config:update", async (payload = {}) => {
121 if (payload.security) {
122 await vaultStore.verifyPassword(payload.masterPassword);
123 if (payload.security.requireSystemAuthentication && !(await biometricService.getStatus()).available) {
124 throw new Error("BIOMETRIC_UNAVAILABLE");
125 }
126 }
127 const config = configStore.update(payload);
128 pluginRuntime.notifyLanguageChanged(config.ui.language);
129 return config;
130 });
131 handle("plugins:list", async () => pluginRegistry.listInstalled());
132 handle("plugins:getState", async () => pluginService.getState());
133 handle("plugins:checkCatalog", async () => pluginService.checkCatalog());
134 handle("plugins:install", async (payload = {}) => { await biometricService?.requireSensitiveAction(); return pluginService.install(payload.releaseId); });
135 handle("plugins:uninstall", async (payload = {}) => { await biometricService?.requireSensitiveAction(); return pluginService.uninstall(payload.pluginId); });
136 handle("plugins:open", async (payload = {}) => pluginService.open(payload.pluginId));
137 handle("vault:status", async () => vaultStore.status());
138 handle("vault:create", async (payload = {}) => { const status = await vaultStore.create(payload.password); vaultLockController?.activity(); return status; });
139 handle("vault:unlock", async (payload = {}) => { biometricService?.cancel(); const status = await vaultStore.unlock(payload.password); vaultLockController?.activity(); return status; });
140 handle("biometrics:status", sessionResult(() => biometricService.getStatus()));
141 handle("biometrics:enable", sessionResult(() => biometricService.enable()));
142 handle("biometrics:disable", sessionResult(() => biometricService.disable()));
143 handle("biometrics:unlock", sessionResult(async () => {
144 const status = await biometricService.unlock(); vaultLockController?.activity(); return status;
145 }));
146 handle("biometrics:authenticate", sessionResult(() => biometricService.authenticateSensitiveAction()));
147 handle("vault:lock", async () => {
148 if (vaultLockController) return vaultLockController.lock();
149 pluginRuntime.closeAll();
150 await sessionManager.disconnectAll("vault-lock");
151 return vaultStore.lock();
152 });
153 handle("vault:reset", async (payload = {}) => {
154 if (payload.confirmation !== "SMAZAT") throw Object.assign(new Error("Vault reset confirmation is invalid."), { code: "VAULT_RESET_CONFIRMATION_REQUIRED" });
155 if (vaultStore.status().unlocked) await biometricService?.requireSensitiveAction();
156 await biometricService?.disable({ reset: true });
157 pluginRuntime.closeAll();
158 await sessionManager.disconnectAll("vault-reset");
159 const status = vaultStore.reset(payload.confirmation);
160 logger?.warn("Encrypted user vault was reset by the user.");
161 return status;
162 });
163 handle("profiles:list", async () => profileService.list());
164 handle("profiles:export", async (payload = {}) => {
165 const format = payload.format || "json";
166 const profiles = profileService.list();
167 const selected = payload.ids === undefined ? profiles : profiles.filter((profile) => Array.isArray(payload.ids) && payload.ids.includes(profile.id));
168 if (!selected.length) throw new Error("Select at least one server to export.");
169 const content = serializeProfiles(selected, format);
170 const result = await dialog.showSaveDialog(getMainWindow(), {
171 defaultPath: `chj-server-profiles.${format}`,
172 filters: [{ name: format.toUpperCase(), extensions: [format] }]
173 });
174 if (result.canceled || !result.filePath) return { canceled: true };
175 await fs.promises.writeFile(result.filePath, content, { encoding: "utf8", mode: 0o600 });
176 return { canceled: false, count: selected.length };
177 });
178 handle("profiles:previewImport", async () => {
179 profileService.list(); // Require an unlocked vault before opening the picker.
180 profileTransfer.cancel();
181 const result = await dialog.showOpenDialog(getMainWindow(), {
182 properties: ["openFile"], filters: [{ name: "JSON / CSV", extensions: ["json", "csv"] }]
183 });
184 if (result.canceled || !result.filePaths[0]) return { canceled: true };
185 const file = await fs.promises.open(result.filePaths[0], "r");
186 try {
187 const buffer = Buffer.alloc(MAX_BYTES + 1);
188 let length = 0;
189 while (length < buffer.length) {
190 const read = await file.read(buffer, length, buffer.length - length, null);
191 if (!read.bytesRead) break;
192 length += read.bytesRead;
193 }
194 if (length > MAX_BYTES) throw new Error("Profile file exceeds the 5 MiB limit.");
195 const content = new TextDecoder("utf-8", { fatal: true }).decode(buffer.subarray(0, length));
196 return { canceled: false, ...profileTransfer.preview(content) };
197 } finally { await file.close(); }
198 });
199 handle("profiles:commitImport", async (payload = {}) => {
200 await biometricService?.requireSensitiveAction();
201 return profileTransfer.commit(payload.token, payload.policy);
202 });
203 handle("profiles:cancelImport", async () => { profileTransfer.cancel(); return { canceled: true }; });
204 handle("profiles:save", async (payload = {}) => profileService.save(payload));
205 handle("profiles:delete", async (payload = {}) => { await biometricService?.requireSensitiveAction(); return profileService.delete(String(payload.id || "")); });
206 handle("profiles:selectPrivateKey", async () => {
207 const mainWindow = getMainWindow();
208 const copy = PRIVATE_KEY_DIALOG_COPY[configStore.get().ui.language] || PRIVATE_KEY_DIALOG_COPY.cs;
209 const result = await dialog.showOpenDialog(mainWindow, {
210 title: copy.title,
211 properties: ["openFile"],
212 filters: [{ name: copy.keys, extensions: ["pem", "key", "ppk"] }, { name: copy.all, extensions: ["*"] }]
213 });
214 return { canceled: result.canceled, path: result.filePaths[0] || null };
215 });
216 handle("ssh:list", async () => sessionManager.list());
217 const diagnosticsResult = (action) => async (payload = {}) => {
218 try { return { ok: true, value: await action(payload) }; }
219 catch (error) { return { ok: false, error: { code: error.code || "DIAGNOSTICS_ERROR", message: String(error.message || error).slice(0, 400) } }; }
220 };
221 handle("diagnostics:resolve", diagnosticsResult((payload) => diagnosticsService.resolve(payload)));
222 handle("diagnostics:start", diagnosticsResult((payload) => diagnosticsService.start(payload)));
223 handle("diagnostics:cancel", diagnosticsResult((payload) => diagnosticsService.cancel(payload.id, payload.tool)));
224 handle("diagnostics:get", diagnosticsResult((payload) => diagnosticsService.get(payload.id)));
225 handle("diagnostics:history", diagnosticsResult(() => diagnosticsService.history.list()));
226 handle("diagnostics:removeHistory", diagnosticsResult((payload) => diagnosticsService.history.remove(payload.id)));
227 handle("diagnostics:copy", diagnosticsResult((payload) => {
228 clipboard.writeText(diagnosticsService.export(payload.id, payload.format || "txt"));
229 return { copied: true };
230 }));
231 handle("diagnostics:export", diagnosticsResult(async (payload) => {
232 const content = diagnosticsService.export(payload.id, payload.format);
233 const result = await dialog.showSaveDialog(getMainWindow(), { defaultPath: `diagnostics.${payload.format}`, filters: [{ name: payload.format.toUpperCase(), extensions: [payload.format] }] });
234 if (result.canceled || !result.filePath) return { canceled: true };
235 await require("node:fs").promises.writeFile(result.filePath, content, { mode: 0o600 });
236 return { canceled: false };
237 }));
238 handle("diagnostics:import", diagnosticsResult(async () => {
239 const result = await dialog.showOpenDialog(getMainWindow(), { properties: ["openFile"], filters: [{ name: "Diagnostics JSON", extensions: ["json"] }] });
240 if (result.canceled || !result.filePaths[0]) return { canceled: true };
241 const fs = require("node:fs"); const stat = await fs.promises.lstat(result.filePaths[0]);
242 if (!stat.isFile() || stat.size > 2 * 1024 * 1024) throw Object.assign(new Error("IMPORT_LIMIT"), { code: "IMPORT_LIMIT" });
243 const id = diagnosticsService.history.import(await fs.promises.readFile(result.filePaths[0], "utf8"));
244 return { id };
245 }));
246 handle("ssh:connect", sessionResult((payload) => sessionManager.connect(payload)));
247 handle("ssh:trustHostKey", sessionResult((payload) => sessionManager.trustPending(payload)));
248 handle("ssh:disconnect", sessionResult((payload) => sessionManager.disconnect(payload.sessionId, "user")));
249 handle("ssh:resize", sessionResult((payload) => sessionManager.resize(payload.sessionId, payload.cols, payload.rows)));
250 send("ssh:input", (payload = {}) => sessionManager.write(payload.sessionId, payload.data));
251 handle("updates:getState", async () => updateService.getState());
252 handle("updates:check", async () => updateService.check());
253 handle("updates:select", async (payload = {}) => updateService.select(payload.id));
254 handle("updates:download", async () => updateService.download());
255 handle("updates:install", async () => {
256 await biometricService?.requireSensitiveAction();
257 // The renderer supplies neither a path nor a verification flag. The main
258 // process re-hashes and re-verifies its internally tracked artifact here.
259 const filePath = await updateService.prepareInstallerLaunch();
260 const result = await updateInstallerLauncher.install(filePath);
261 updateService.markInstallerLaunched();
262 if (result.restartApplication) {
263 setTimeout(() => {
264 app.relaunch();
265 app.quit();
266 }, 250).unref?.();
267 }
268 return { ok: true, installed: result.installed === true, restarting: result.restartApplication === true };
269 });
270 handle("updates:reveal", async () => {
271 const filePath = updateService.getVerifiedDownloadPath();
272 if (!filePath) throw new Error("No verified update has been downloaded.");
273 shell.showItemInFolder(filePath);
274 return { ok: true };
275 });
278module.exports = { registerCoreIpc };
SHA-256d6d0ef10312fdde72e527e867815622b5e9b46f3b79d1316a7aa380e23ee6238

CH-J Proprietary Software License 1.14 · La sorgente è fornita sotto CH-J Proprietary Software License 1.14. La sua disponibilità non modifica i termini di licenza o concede diritti aggiuntivi.

Archivio SHA-2563df08cf5e899262a6ce92d7ec2caf5c42b683f1f2b57055662b845ff7dde67a8