CH-J Server Managerуправление на сървъри над SSH
Меню
Публикуван източник

CH-J Server Manager

Преглед на директории и файлове за конкретна версия на приложението.

Изтегляне на източник ZIP
CH-J / Server Manager 368 Файлове
core-20261017T201717Z-057348c4a70f Добавено: Дата, която не е регистрирана · Построена:
authorizedKeys.js Добавено: Дата, която не е регистрирана 6.3 KB · 146 линии
Код Изтегляне на файл
1"use strict";
3const crypto = require("node:crypto");
5// authorized_keys handling works on latin1 strings so every byte of unrelated lines,
6// including options, comments and line endings, is written back unchanged.
7const KEY_TYPE_PATTERN = /^(?:ssh-|ecdsa-|sk-)[A-Za-z0-9@._-]{1,80}$/;
8const ADDABLE_TYPES = new Set(["ssh-ed25519", "sk-ssh-ed25519@openssh.com"]);
9const MAX_KEYS_FILE_BYTES = 256 * 1024;
10const MAX_COMMENT_LENGTH = 200;
12class KeyError extends Error {
13 constructor(code, message) {
14 super(message);
15 this.code = code;
16 }
19function fingerprint(blob) {
20 return `SHA256:${crypto.createHash("sha256").update(blob).digest("base64").replace(/=+$/, "")}`;
23function readSshString(buffer, offset) {
24 if (offset + 4 > buffer.length) return null;
25 const length = buffer.readUInt32BE(offset);
26 if (length > buffer.length - offset - 4) return null;
27 return { value: buffer.subarray(offset + 4, offset + 4 + length), next: offset + 4 + length };
30function decodeBlob(type, base64) {
31 if (!/^[A-Za-z0-9+/]+={0,2}$/.test(base64) || base64.length % 4 !== 0) return null;
32 const blob = Buffer.from(base64, "base64");
33 const embedded = readSshString(blob, 0);
34 if (!embedded || embedded.value.toString("latin1") !== type) return null;
35 if (type === "ssh-ed25519") {
36 const key = readSshString(blob, embedded.next);
37 if (!key || key.value.length !== 32 || key.next !== blob.length) return null;
38 }
39 return blob;
42function skipOptions(text) {
43 let quoted = false;
44 for (let index = 0; index < text.length; index += 1) {
45 const character = text[index];
46 if (character === "\\" && quoted) { index += 1; continue; }
47 if (character === "\"") quoted = !quoted;
48 else if (!quoted && (character === " " || character === "\t")) return index;
49 }
50 return -1;
53function utf8(text) {
54 return Buffer.from(text, "latin1").toString("utf8").replace(/[\x00-\x1f\x7f]/g, " ").trim();
57function parseLine(raw, lineNumber) {
58 const text = raw.replace(/\r$/, "").trim();
59 if (!text || text.startsWith("#")) return null;
60 let options = "";
61 let rest = text;
62 const firstToken = text.split(/[ \t]/, 1)[0];
63 if (!KEY_TYPE_PATTERN.test(firstToken)) {
64 const end = skipOptions(text);
65 if (end < 0) return { line: lineNumber, valid: false, reason: "unparsed" };
66 options = text.slice(0, end);
67 rest = text.slice(end).trim();
68 }
69 const [type = "", data = "", ...commentParts] = rest.split(/[ \t]+/);
70 if (!KEY_TYPE_PATTERN.test(type)) return { line: lineNumber, valid: false, reason: "type" };
71 const blob = decodeBlob(type, data);
72 if (!blob) return { line: lineNumber, valid: false, type, reason: "data" };
73 return {
74 line: lineNumber,
75 valid: true,
76 type,
77 ed25519: type === "ssh-ed25519" || type === "sk-ssh-ed25519@openssh.com",
78 fingerprint: fingerprint(blob),
79 comment: utf8(commentParts.join(" ")).slice(0, MAX_COMMENT_LENGTH),
80 options: utf8(options).slice(0, 500),
81 blob: blob.toString("base64")
82 };
85function parseAuthorizedKeys(content) {
86 const text = Buffer.isBuffer(content) ? content.toString("latin1") : String(content || "");
87 return text.split("\n").map((raw, index) => parseLine(raw, index + 1)).filter(Boolean);
90// Accepts exactly one OpenSSH public key line without options. Only Ed25519 keys may be added.
91function normalizePublicKey(input, comment) {
92 const text = String(input || "").replace(/^/, "").trim();
93 if (!text || text.length > 16 * 1024) throw new KeyError("USERS_KEY_INVALID", "Paste exactly one OpenSSH public key.");
94 const lines = text.split(/\r?\n/).map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
95 if (lines.length !== 1) throw new KeyError("USERS_KEY_INVALID", "Paste exactly one OpenSSH public key.");
96 if (/PRIVATE KEY/.test(text)) throw new KeyError("USERS_KEY_PRIVATE", "This is a private key. Only public keys are uploaded to the server.");
97 const [type = "", data = "", ...commentParts] = lines[0].split(/\s+/);
98 if (!KEY_TYPE_PATTERN.test(type)) throw new KeyError("USERS_KEY_INVALID", "Key options are not accepted; paste the plain public key.");
99 if (!ADDABLE_TYPES.has(type)) throw new KeyError("USERS_KEY_TYPE_UNSUPPORTED", "Only Ed25519 public keys can be added.");
100 const blob = decodeBlob(type, data);
101 if (!blob) throw new KeyError("USERS_KEY_INVALID", "The public key data is invalid.");
102 const finalComment = String(comment ?? commentParts.join(" ")).replace(/[\x00-\x1f\x7f]/g, " ").replace(/\s+/g, " ").trim();
103 if (finalComment.length > MAX_COMMENT_LENGTH) throw new KeyError("USERS_KEY_INVALID", "The key comment is too long.");
104 return { type, data, comment: finalComment, fingerprint: fingerprint(blob), blob: blob.toString("base64") };
107function appendKey(content, key) {
108 const text = Buffer.isBuffer(content) ? content.toString("latin1") : String(content || "");
109 if (parseAuthorizedKeys(text).some((entry) => entry.valid && entry.blob === key.blob)) {
110 throw new KeyError("USERS_KEY_DUPLICATE", "This public key is already authorized for the account.");
111 }
112 const comment = key.comment ? ` ${Buffer.from(key.comment, "utf8").toString("latin1")}` : "";
113 const separator = text && !text.endsWith("\n") ? "\n" : "";
114 const result = Buffer.from(`${text}${separator}${key.type} ${key.data}${comment}\n`, "latin1");
115 if (result.length > MAX_KEYS_FILE_BYTES) throw new KeyError("USERS_KEYS_FILE_TOO_LARGE", "The authorized_keys file would exceed 256 KiB.");
116 return result;
119// Removes exactly one line, identified by line number and fingerprint, leaving all other bytes untouched.
120function removeKey(content, lineNumber, expectedFingerprint) {
121 const text = Buffer.isBuffer(content) ? content.toString("latin1") : String(content || "");
122 const lines = text.split("\n");
123 const index = Number(lineNumber) - 1;
124 const entry = Number.isInteger(index) && index >= 0 && index < lines.length ? parseLine(lines[index], index + 1) : null;
125 if (!entry?.valid || entry.fingerprint !== expectedFingerprint) {
126 throw new KeyError("USERS_KEY_NOT_FOUND", "The selected key changed or no longer exists. Reload the key list.");
127 }
128 lines.splice(index, 1);
129 return Buffer.from(lines.join("\n"), "latin1");
132function sha256Hex(content) {
133 return crypto.createHash("sha256").update(content).digest("hex");
136module.exports = {
137 ADDABLE_TYPES,
138 KeyError,
139 MAX_KEYS_FILE_BYTES,
140 appendKey,
141 fingerprint,
142 normalizePublicKey,
143 parseAuthorizedKeys,
144 removeKey,
145 sha256Hex
146};
SHA-256c5e2d7c0ef0c485f87af21732fb8ed2f43d55cc6d625ec129b46adefbcbffe97

CH-J Proprietary Software License 1.14 · Източникът е предоставен под CH-J Proprietary Software License 1.14. Неговата наличност не променя лицензионните условия или не предоставя допълнителни права.

Архив SHA-256cf4455b698f0f28e456a37b1834976a0194d8486acb7101052d59a838745cca3
Файлове