CH-J Server Managerserverhåndtering over SSH
Menu
Offentliggjort kilde

CH-J Server Manager

Gennemse mapper og filer til en specifik applikationsudgivelse.

Download kilde ZIP
CH-J / Server Manager 339 filer
core-20261014T184258Z-3fbed0b93fdf Tilføjet: Dato ikke registreret · Bygget:
Filer
profileTransfer.js Tilføjet: Dato ikke registreret 6,8 KB · 135 linjer
Download fil
1"use strict";
3const crypto = require("node:crypto");
4const net = require("node:net");
5const { normalizeProfile } = require("./profileService");
7const FORMAT = "chj-server-profiles";
8const VERSION = 1;
9const MAX_BYTES = 5 * 1024 * 1024;
10const FIELDS = ["name", "host", "username", "port"];
12function portableProfile(input) {
13 if (!input || typeof input !== "object" || Array.isArray(input)) throw new Error("Invalid server profile.");
14 for (const field of ["name", "host", "username"]) {
15 if (typeof input[field] !== "string" || !input[field].trim()) throw new Error(`Missing or invalid ${field}.`);
16 }
17 if (input.name.length > 100 || /[\0\r\n]/.test(input.name)) throw new Error("Invalid server name (maximum 100 characters).");
18 if (input.port !== undefined && !/^[0-9]+$/.test(String(input.port))) throw new Error("Invalid SSH port.");
19 if (input.port !== undefined && (Number(input.port) < 1 || Number(input.port) > 65535)) throw new Error("SSH port must be between 1 and 65535.");
20 const profile = normalizeProfile({ label: input.name, host: input.host, username: input.username, port: input.port ?? 22 });
21 return { name: profile.label, host: profile.host, username: profile.username, port: profile.port };
24function identity(profile) {
25 const host = net.isIP(profile.host) === 6 ? new URL(`http://[${profile.host}]/`).hostname : profile.host.toLowerCase();
26 return JSON.stringify([host, profile.port, profile.username]);
29// RFC 4180 quoting, including embedded commas, quotes and line breaks.
30function csvRows(content) {
31 const rows = []; let row = [], cell = "", quoted = false, closed = false;
32 for (let i = 0; i < content.length; i++) {
33 const char = content[i];
34 if (quoted) {
35 if (char === '"' && content[i + 1] === '"') { cell += '"'; i++; }
36 else if (char === '"') { quoted = false; closed = true; }
37 else cell += char;
38 } else if (char === '"' && !cell && !closed) quoted = true;
39 else if (char === ",") { row.push(cell); cell = ""; closed = false; }
40 else if (char === "\n" || char === "\r") {
41 if (char === "\r" && content[i + 1] === "\n") i++;
42 row.push(cell); rows.push(row); row = []; cell = ""; closed = false;
43 } else {
44 if (closed || char === '"') throw new Error("Malformed CSV quoting.");
45 cell += char;
46 }
47 }
48 if (quoted) throw new Error("Unterminated CSV field.");
49 if (cell || row.length || closed) { row.push(cell); rows.push(row); }
50 return rows;
53function decodeCsvCell(value) {
54 return /^'(?:[=+@\-\t\r]|')/.test(value) ? value.slice(1) : value;
57function parseProfiles(content) {
58 if (typeof content !== "string" || Buffer.byteLength(content, "utf8") > MAX_BYTES) throw new Error("Profile file exceeds the 5 MiB limit.");
59 content = content.replace(/^\uFEFF/, "").trim();
60 let servers, format;
61 if (/^[{\[]/.test(content)) {
62 const document = JSON.parse(content);
63 if (document.format !== FORMAT || document.version !== VERSION || !Array.isArray(document.servers)) throw new Error("Unsupported server profile format or version.");
64 servers = document.servers; format = "json";
65 } else {
66 const [header, ...rows] = csvRows(content);
67 if (!header || header.length !== FIELDS.length || new Set(header).size !== FIELDS.length || FIELDS.some((field) => !header.includes(field))) throw new Error("CSV columns must be name,host,username,port.");
68 servers = rows.filter((row) => row.some(Boolean)).map((row) => {
69 if (row.length !== header.length) throw new Error("CSV row has an unexpected number of columns.");
70 return Object.fromEntries(header.map((field, index) => [field, decodeCsvCell(row[index])]));
71 }); format = "csv";
72 }
73 if (!servers.length || servers.length > 10000) throw new Error("Import must contain between 1 and 10000 servers.");
74 return { format, servers: servers.map((server, index) => {
75 try { return portableProfile(server); }
76 catch (error) { throw new Error(`Server ${index + 1}: ${error.message}`); }
77 }) };
80function serializeProfiles(profiles, format = "json") {
81 const servers = profiles.map((profile) => portableProfile({ name: profile.label, host: profile.host, username: profile.username, port: profile.port }));
82 if (format === "json") return JSON.stringify({ format: FORMAT, version: VERSION, servers }, null, 2) + "\n";
83 if (format !== "csv") throw new Error("Unsupported export format.");
84 const escape = (value) => {
85 let cell = String(value);
86 // Prevent spreadsheet formula execution; the importer reverses this prefix.
87 if (/^[=+@\-\t\r']/.test(cell)) cell = "'" + cell;
88 return /[",\r\n]/.test(cell) ? '"' + cell.replace(/"/g, '""') + '"' : cell;
89 };
90 return "\uFEFF" + [FIELDS.join(","), ...servers.map((server) => FIELDS.map((field) => escape(server[field])).join(","))].join("\r\n") + "\r\n";
93class ProfileTransfer {
94 constructor(profileService) { this.profiles = profileService; this.pending = null; }
96 preview(content) {
97 const parsed = parseProfiles(content);
98 const seen = new Set(this.profiles.list().map(identity));
99 const rows = parsed.servers.map((server) => {
100 const duplicate = seen.has(identity(server)); seen.add(identity(server));
101 return { ...server, duplicate };
102 });
103 const token = crypto.randomUUID();
104 this.pending = { token, servers: parsed.servers, generation: this.profiles.vaultStore.generation, expires: Date.now() + 15 * 60 * 1000 };
105 return { token, format: parsed.format, servers: rows };
106 }
108 cancel() { this.pending = null; }
110 commit(token, policy) {
111 const pending = this.pending;
112 if (!pending || pending.token !== token || pending.generation !== this.profiles.vaultStore.generation || pending.expires < Date.now()) throw new Error("Import preview expired. Select the file again.");
113 if (!["skip", "update", "create"].includes(policy)) throw new Error("Invalid duplicate policy.");
114 const result = { created: 0, updated: 0, skipped: 0 };
115 this.profiles.vaultStore.update((data) => {
116 const existing = new Map(data.profiles.map((profile) => [identity(profile), profile]));
117 for (const server of pending.servers) {
118 const match = existing.get(identity(server));
119 if (match && policy === "skip") { result.skipped++; continue; }
120 const previous = match && policy === "update" ? match : null;
121 const profile = normalizeProfile({ label: server.name, ...server, tags: previous?.tags || [] }, previous);
122 if (previous) {
123 data.profiles[data.profiles.findIndex((item) => item.id === previous.id)] = profile;
124 if (data.secrets?.profilePasswords) delete data.secrets.profilePasswords[previous.id];
125 result.updated++;
126 } else { data.profiles.push(profile); result.created++; }
127 existing.set(identity(server), profile);
128 }
129 });
130 this.cancel();
131 return result;
132 }
135module.exports = { FORMAT, VERSION, MAX_BYTES, ProfileTransfer, parseProfiles, serializeProfiles, identity };
SHA-2563ff65fa0378c89dc7d2b1be2fa514ddd1dc3657a41b456105fb87d06ea63900b

CH-J Proprietary Software License 1.14 · Kilden leveres under CH-J Proprietary Software License 1.14. Dens tilgængelighed ændrer ikke licensbetingelserne eller giver yderligere rettigheder.

Arkiv SHA-2563df08cf5e899262a6ce92d7ec2caf5c42b683f1f2b57055662b845ff7dde67a8