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 crypto = require("node:crypto");4
const fs = require("node:fs");5
const path = require("node:path");6
const AdmZip = require("adm-zip");7
const { compareVersions } = require("../../shared/version");8
const { PLUGIN_API_VERSION, supportsPluginApi, validateManifest } = require("./pluginRegistry");10
const MAX_ENTRIES = 500;11
const MAX_UNCOMPRESSED_BYTES = 100 * 1024 * 1024;12
const ALLOWED_EXTENSIONS = new Set([".css", ".gif", ".html", ".jpeg", ".jpg", ".js", ".json", ".md", ".png", ".svg", ".txt", ".ttf", ".webp", ".woff", ".woff2"]);14
function 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(/^\.\//, "");20
}22
function isSymlink(entry) {23
const unixMode = (Number(entry.attr || 0) >>> 16) & 0xffff;24
return (unixMode & 0xf000) === 0xa000;25
}27
class 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.publishedAt91
}, 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
}127
}129
module.exports = { ALLOWED_EXTENSIONS, MAX_ENTRIES, MAX_UNCOMPRESSED_BYTES, PluginInstaller, safeEntryName };SHA-256: 3682721ce0b892525300c8320048e74df664e2757e47712acee574c3dffee0da
SHA-256 archivu: 5ac91caf4fa32a6fdb114f2430deed486fbe7489d5eea343d1f034169fafb5e0