CH-J Server ManagerSerververwaltung über SSH
Menü
Veröffentlichte Quellen

CH-J Server Manager

Durchsuchen Sie Verzeichnisse und Dateien einer bestimmten Anwendungsausgabe.

Quellen als ZIP herunterladen
CH-J Proprietary Software License 1.14

Die Quellen werden unter der CH-J Proprietary Software License 1.14 bereitgestellt. Ihre Verfügbarkeit ändert die Lizenzbedingungen nicht und gewährt keine zusätzlichen Rechte.

6,7 KB · 129 ZeilenDatei herunterladen
1"use strict";
3const crypto = require("node:crypto");
4const fs = require("node:fs");
5const path = require("node:path");
6const AdmZip = require("adm-zip");
7const { compareVersions } = require("../../shared/version");
8const { PLUGIN_API_VERSION, supportsPluginApi, validateManifest } = require("./pluginRegistry");
10const MAX_ENTRIES = 500;
11const MAX_UNCOMPRESSED_BYTES = 100 * 1024 * 1024;
12const ALLOWED_EXTENSIONS = new Set([".css", ".gif", ".html", ".jpeg", ".jpg", ".js", ".json", ".md", ".png", ".svg", ".txt", ".ttf", ".webp", ".woff", ".woff2"]);
14function safeEntryName(value) {
15 const name = String(value || "").replace(/\\/g, "/");
16 if (!name || name.includes("\0") || name.startsWith("/") || /^[A-Za-z]:\//.test(name) || name.split("/").includes("..")) {
17 throw new Error(`Unsafe plugin archive path: ${name || "<empty>"}`);
18 }
19 return name.replace(/^\.\//, "");
22function isSymlink(entry) {
23 const unixMode = (Number(entry.attr || 0) >>> 16) & 0xffff;
24 return (unixMode & 0xf000) === 0xa000;
27class PluginInstaller {
28 constructor(options) {
29 this.rootDir = path.join(options.storageRoot, "plugins");
30 this.appVersion = options.appVersion;
31 this.pluginApiVersion = options.pluginApiVersion || PLUGIN_API_VERSION;
32 this.logger = options.logger;
33 }
35 install(packagePath, catalogEntry) {
36 const zip = new AdmZip(packagePath);
37 const entries = zip.getEntries();
38 if (!entries.length || entries.length > MAX_ENTRIES) throw new Error("Plugin archive has an invalid number of entries.");
39 let totalBytes = 0;
40 const normalized = [];
41 for (const entry of entries) {
42 const name = safeEntryName(entry.entryName);
43 if (isSymlink(entry)) throw new Error(`Plugin archive contains a symbolic link: ${name}`);
44 const size = Number(entry.header?.size || 0);
45 if (!Number.isSafeInteger(size) || size < 0) throw new Error(`Invalid plugin entry size: ${name}`);
46 totalBytes += size;
47 if (totalBytes > MAX_UNCOMPRESSED_BYTES) throw new Error("Plugin archive exceeds the uncompressed size limit.");
48 if (!entry.isDirectory) {
49 const extension = path.posix.extname(name).toLowerCase();
50 if (!ALLOWED_EXTENSIONS.has(extension)) throw new Error(`Unsupported plugin file type: ${name}`);
51 }
52 normalized.push({ entry, name });
53 }
54 const manifestEntry = normalized.find((item) => item.name === "manifest.json" && !item.entry.isDirectory);
55 if (!manifestEntry) throw new Error("Plugin archive has no root manifest.json.");
56 if (Number(manifestEntry.entry.header?.size || 0) > 1024 * 1024) throw new Error("Plugin manifest is too large.");
57 let manifest;
58 try { manifest = validateManifest(JSON.parse(manifestEntry.entry.getData().toString("utf8"))); }
59 catch (error) { throw new Error(`Invalid packaged plugin manifest: ${error?.message || String(error)}`); }
60 this._assertCatalogMatch(manifest, catalogEntry);
61 if (!supportsPluginApi(manifest.pluginApi, this.pluginApiVersion)) throw new Error(`Plugin requires unsupported Plugin API ${manifest.pluginApi}.`);
62 if (compareVersions(this.appVersion, manifest.minAppVersion) < 0) throw new Error(`Plugin requires application ${manifest.minAppVersion} or newer.`);
64 const idRoot = path.join(this.rootDir, manifest.id);
65 const stagingRoot = path.join(this.rootDir, ".staging", `${manifest.id}-${crypto.randomBytes(8).toString("hex")}`);
66 const contentRoot = path.join(stagingRoot, manifest.version);
67 fs.mkdirSync(contentRoot, { recursive: true, mode: 0o700 });
68 try {
69 let extractedBytes = 0;
70 for (const item of normalized) {
71 const destination = path.join(contentRoot, ...item.name.split("/"));
72 const relative = path.relative(contentRoot, destination);
73 if (relative.startsWith("..") || path.isAbsolute(relative)) throw new Error(`Plugin path escaped staging: ${item.name}`);
74 if (item.entry.isDirectory) fs.mkdirSync(destination, { recursive: true, mode: 0o700 });
75 else {
76 const data = item.entry.getData();
77 extractedBytes += data.length;
78 if (extractedBytes > MAX_UNCOMPRESSED_BYTES) throw new Error("Plugin archive exceeds the extracted size limit.");
79 fs.mkdirSync(path.dirname(destination), { recursive: true, mode: 0o700 });
80 fs.writeFileSync(destination, data, { mode: 0o600, flag: "wx" });
81 }
82 }
83 const entryPath = path.join(contentRoot, ...manifest.entry.split("/"));
84 if (!fs.statSync(entryPath).isFile()) throw new Error("Packaged plugin entry file is missing.");
85 fs.writeFileSync(path.join(contentRoot, ".installation.json"), JSON.stringify({
86 schemaVersion: 1,
87 releaseId: catalogEntry.releaseId,
88 sha512: catalogEntry.sha512,
89 installedAt: new Date().toISOString(),
90 publishedAt: catalogEntry.publishedAt
91 }, null, 2), { mode: 0o600, flag: "wx" });
93 fs.mkdirSync(idRoot, { recursive: true, mode: 0o700 });
94 const destination = path.join(idRoot, manifest.version);
95 if (fs.existsSync(destination)) {
96 const existing = this._installation(destination);
97 if (existing?.sha512 === catalogEntry.sha512) {
98 fs.rmSync(stagingRoot, { recursive: true, force: true });
99 return { manifest, reused: true, path: destination };
100 }
101 const rollbackRoot = path.join(idRoot, ".rollback");
102 fs.mkdirSync(rollbackRoot, { recursive: true, mode: 0o700 });
103 fs.renameSync(destination, path.join(rollbackRoot, `${manifest.version}-${Date.now()}`));
104 }
105 fs.renameSync(contentRoot, destination);
106 fs.rmSync(stagingRoot, { recursive: true, force: true });
107 this.logger?.info("Plugin installed.", { pluginId: manifest.id, version: manifest.version, permissions: manifest.permissions });
108 return { manifest, reused: false, path: destination };
109 } catch (error) {
110 fs.rmSync(stagingRoot, { recursive: true, force: true });
111 throw error;
112 }
113 }
115 _assertCatalogMatch(manifest, catalog) {
116 if (manifest.id !== catalog.id || manifest.version !== catalog.version) throw new Error("Plugin manifest identity does not match the catalog.");
117 if (manifest.pluginApi !== catalog.pluginApi || manifest.minAppVersion !== catalog.minAppVersion) throw new Error("Plugin compatibility metadata does not match the catalog.");
118 const packaged = [...manifest.permissions].sort();
119 const declared = [...catalog.permissions].sort();
120 if (JSON.stringify(packaged) !== JSON.stringify(declared)) throw new Error("Plugin permissions do not match the catalog.");
121 }
123 _installation(directory) {
124 try { return JSON.parse(fs.readFileSync(path.join(directory, ".installation.json"), "utf8")); }
125 catch { return null; }
126 }
129module.exports = { ALLOWED_EXTENSIONS, MAX_ENTRIES, MAX_UNCOMPRESSED_BYTES, PluginInstaller, safeEntryName };

SHA-256: 3682721ce0b892525300c8320048e74df664e2757e47712acee574c3dffee0da

SHA-256 des Archivs: 5ac91caf4fa32a6fdb114f2430deed486fbe7489d5eea343d1f034169fafb5e0