Opublikowane źródło
Pobierz źródło ZIP
CH-J Server Manager
Przeglądaj katalogi i pliki dla konkretnego wydania aplikacji.
CH-J / Server Manager
377 pliki
core-20261019T111616Z-160a00bfcddc
Dodano: · Built:
1
"use strict";3
const 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.7
const KEY_TYPE_PATTERN = /^(?:ssh-|ecdsa-|sk-)[A-Za-z0-9@._-]{1,80}$/;8
const ADDABLE_TYPES = new Set(["ssh-ed25519", "sk-ssh-ed25519@openssh.com"]);9
const MAX_KEYS_FILE_BYTES = 256 * 1024;10
const MAX_COMMENT_LENGTH = 200;12
class KeyError extends Error {13
constructor(code, message) {14
super(message);15
this.code = code;16
}17
}19
function fingerprint(blob) {20
return `SHA256:${crypto.createHash("sha256").update(blob).digest("base64").replace(/=+$/, "")}`;21
}23
function 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 };28
}30
function 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;40
}42
function 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;51
}53
function utf8(text) {54
return Buffer.from(text, "latin1").toString("utf8").replace(/[\x00-\x1f\x7f]/g, " ").trim();55
}57
function 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
};83
}85
function 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);88
}90
// Accepts exactly one OpenSSH public key line without options. Only Ed25519 keys may be added.91
function 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") };105
}107
function 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;117
}119
// Removes exactly one line, identified by line number and fingerprint, leaving all other bytes untouched.120
function 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");130
}132
function sha256Hex(content) {133
return crypto.createHash("sha256").update(content).digest("hex");134
}136
module.exports = {137
ADDABLE_TYPES,138
KeyError,139
MAX_KEYS_FILE_BYTES,140
appendKey,141
fingerprint,142
normalizePublicKey,143
parseAuthorizedKeys,144
removeKey,145
sha256Hex146
};SHA-256
c5e2d7c0ef0c485f87af21732fb8ed2f43d55cc6d625ec129b46adefbcbffe97CH-J Proprietary Software License 1.14 · Źródło jest dostarczane pod CH-J Proprietary Software License 1.14. Jego dostępność nie zmienia warunków licencji ani nie przyznaje dodatkowych praw.
SHA-256
a9605c0870a223b6a8542bb0705edd51d8b04f3069582653417a8397fabcc09a