CH-J Server Managerspráva serverů přes SSH
Menu
Vydané zdroje

CH-J Server Manager

Procházejte adresáře a soubory konkrétního vydání aplikace.

Stáhnout zdroje ZIP
CH-J Proprietary Software License 1.14

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í.

6,9 KB · 159 řádkůStáhnout soubor
1"use strict";
3const fs = require("node:fs");
4const path = require("node:path");
5const { compareVersions, parseVersion } = require("../../shared/version");
7const PLUGIN_ID_PATTERN = /^[a-z0-9][a-z0-9.-]{2,63}$/;
8const PLUGIN_API_VERSION = "1.2.0";
9const ALLOWED_PERMISSIONS = new Set([
10 "session.read",
11 "system.metrics.read",
12 "keys.generate",
13 "files.read",
14 "files.write",
15 "files.transfer",
16 "users.read",
17 "users.manage",
18 "nginx.read",
19 "nginx.manage",
20 "certbot.manage",
21 "mail.read",
22 "mail.manage",
23 "logs.read",
24 "remote.exec",
25 "local.hash"
26]);
28function validateManifest(input) {
29 if (!input || typeof input !== "object" || Array.isArray(input)) throw new Error("Plugin manifest must be an object.");
30 const id = String(input.id || "").trim();
31 if (!PLUGIN_ID_PATTERN.test(id)) throw new Error("Invalid plugin ID.");
32 const version = String(input.version || "").trim();
33 parseVersion(version);
34 const entry = String(input.entry || "").replace(/\\/g, "/");
35 if (!entry || entry.startsWith("/") || entry.split("/").includes("..")) throw new Error("Invalid plugin entry path.");
36 const permissions = Array.isArray(input.permissions) ? [...new Set(input.permissions.map(String))] : [];
37 for (const permission of permissions) {
38 if (!ALLOWED_PERMISSIONS.has(permission)) throw new Error(`Unsupported plugin permission: ${permission}`);
39 }
40 const schemaVersion = Number(input.schemaVersion || 1);
41 if (schemaVersion !== 1) throw new Error(`Unsupported plugin manifest schema: ${schemaVersion}`);
42 const minAppVersion = String(input.minAppVersion || "0.0.1");
43 parseVersion(minAppVersion);
44 const pluginApi = String(input.pluginApi || "^1.0.0");
45 if (!/^\^1\.\d+\.\d+$/.test(pluginApi)) throw new Error(`Unsupported plugin API: ${pluginApi}`);
46 return {
47 schemaVersion,
48 id,
49 name: String(input.name || id).trim().slice(0, 100),
50 description: String(input.description || "").trim().slice(0, 500),
51 version,
52 publisher: String(input.publisher || "Unknown").trim().slice(0, 100),
53 entry,
54 pluginApi,
55 minAppVersion,
56 permissions
57 };
60function supportsPluginApi(range, supportedVersion = PLUGIN_API_VERSION) {
61 const required = String(range || "").match(/^\^1\.(\d+)\.(\d+)$/);
62 const supported = String(supportedVersion || "").match(/^1\.(\d+)\.(\d+)$/);
63 return Boolean(required && supported && (Number(required[1]) < Number(supported[1]) || (Number(required[1]) === Number(supported[1]) && Number(required[2]) <= Number(supported[2]))));
66class PluginRegistry {
67 constructor(rootDir, logger, options = {}) {
68 this.rootDir = path.join(rootDir, "plugins");
69 this.bundledRoot = options.bundledRoot || null;
70 this.logger = logger;
71 this.locations = new Map();
72 }
74 listInstalled() {
75 fs.mkdirSync(this.rootDir, { recursive: true });
76 this.locations.clear();
77 const plugins = [];
78 const roots = [{ root: this.rootDir, bundled: false }];
79 if (this.bundledRoot && fs.statSync(this.bundledRoot, { throwIfNoEntry: false })?.isDirectory()) roots.push({ root: this.bundledRoot, bundled: true });
80 const candidatesById = new Map();
81 for (const source of roots) for (const idEntry of fs.readdirSync(source.root, { withFileTypes: true })) {
82 if (!idEntry.isDirectory() || !PLUGIN_ID_PATTERN.test(idEntry.name)) continue;
83 const idRoot = path.join(source.root, idEntry.name);
84 const candidates = [];
85 for (const versionEntry of fs.readdirSync(idRoot, { withFileTypes: true })) {
86 if (!versionEntry.isDirectory() || versionEntry.name.startsWith(".")) continue;
87 const manifestPath = path.join(idRoot, versionEntry.name, "manifest.json");
88 try {
89 const manifest = validateManifest(JSON.parse(fs.readFileSync(manifestPath, "utf8")));
90 if (manifest.id !== idEntry.name || manifest.version !== versionEntry.name) {
91 throw new Error("Plugin directory does not match its manifest.");
92 }
93 const entryPath = path.join(idRoot, versionEntry.name, manifest.entry);
94 if (!fs.statSync(entryPath).isFile()) throw new Error("Plugin entry file is missing.");
95 let installation = {};
96 try { installation = JSON.parse(fs.readFileSync(path.join(idRoot, versionEntry.name, ".installation.json"), "utf8")); } catch {}
97 candidates.push({
98 ...manifest,
99 bundled: source.bundled,
100 installedReleaseId: typeof installation.releaseId === "string" ? installation.releaseId : null,
101 installedSha512: /^[a-f0-9]{128}$/i.test(String(installation.sha512 || "")) ? String(installation.sha512).toLowerCase() : null,
102 installedAt: typeof installation.installedAt === "string" ? installation.installedAt : null,
103 installedPublishedAt: typeof installation.publishedAt === "string" ? installation.publishedAt : null,
104 _root: path.join(idRoot, versionEntry.name)
105 });
106 } catch (error) {
107 this.logger?.warn("Ignoring invalid installed plugin.", {
108 pluginId: idEntry.name,
109 version: versionEntry.name,
110 message: error?.message || String(error)
111 });
112 }
113 }
114 if (!candidatesById.has(idEntry.name)) candidatesById.set(idEntry.name, []);
115 candidatesById.get(idEntry.name).push(...candidates);
116 }
117 for (const candidates of candidatesById.values()) {
118 candidates.sort((left, right) => compareVersions(right.version, left.version) || Number(left.bundled) - Number(right.bundled));
119 if (!candidates[0]) continue;
120 const selected = candidates[0];
121 this.locations.set(`${selected.id}@${selected.version}`, selected._root);
122 const { _root, ...publicManifest } = selected;
123 plugins.push(publicManifest);
124 }
125 return plugins.sort((left, right) => left.name.localeCompare(right.name));
126 }
128 getInstalled(pluginId) {
129 const id = String(pluginId || "");
130 if (!PLUGIN_ID_PATTERN.test(id)) throw new Error("Invalid plugin ID.");
131 return this.listInstalled().find((plugin) => plugin.id === id) || null;
132 }
134 resolveEntry(pluginId) {
135 const manifest = this.getInstalled(pluginId);
136 if (!manifest) throw new Error("Plugin is not installed.");
137 const root = this.locations.get(`${manifest.id}@${manifest.version}`) || path.join(this.rootDir, manifest.id, manifest.version);
138 return { manifest, root, entryPath: path.join(root, ...manifest.entry.split("/")) };
139 }
141 removeInstalled(pluginId) {
142 const id = String(pluginId || "");
143 if (!PLUGIN_ID_PATTERN.test(id)) throw new Error("Invalid plugin ID.");
144 const directory = path.join(this.rootDir, id);
145 if (!fs.existsSync(directory)) return { removed: false, pluginId: id };
146 fs.rmSync(directory, { recursive: true, force: true });
147 this.logger?.info("Plugin removed.", { pluginId: id });
148 return { removed: true, pluginId: id };
149 }
152module.exports = {
153 ALLOWED_PERMISSIONS,
154 PLUGIN_API_VERSION,
155 PLUGIN_ID_PATTERN,
156 PluginRegistry,
157 supportsPluginApi,
158 validateManifest
159};

SHA-256: 8de632e09ca2511335ae7798b6e21d04865e275ae1f26a23a845704fd4666289

SHA-256 archivu: 5ac91caf4fa32a6fdb114f2430deed486fbe7489d5eea343d1f034169fafb5e0