Reine Funktionen fuer Dry-Run und Ausrollen. Alles ausserhalb des Blocks bleibt bytegenau stehen, auch CRLF, BOM und kaputte Zeilen; der Block entsteht deterministisch aus dem Soll. Ein beschaedigter Block wird nie automatisch ueberschrieben, weil genau dort fremde Zeilen verschwinden koennten. Verwaltete Keys erkennt sshkv am Fingerprint, nicht am Kommentar. Der Deployment-Key kann nicht aus dem Block fallen (Aussperrschutz). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
126 lines
4.0 KiB
TypeScript
126 lines
4.0 KiB
TypeScript
/**
|
||
* Optionen einer authorized_keys-Zeile (#015): `restrict,from="10.0.0.1",command="…"`.
|
||
*
|
||
* Geparst wird wie in sshd (auth-options.c): Komma trennt, ausser in doppelten
|
||
* Anfuehrungszeichen; dort maskiert `\"` ein Anfuehrungszeichen. Die Optionen stehen am
|
||
* Zeilenanfang und enden am ersten Leerraum ausserhalb von Anfuehrungszeichen.
|
||
*/
|
||
export class OptionenFehler extends Error {
|
||
constructor(nachricht: string) {
|
||
super(nachricht);
|
||
this.name = "OptionenFehler";
|
||
}
|
||
}
|
||
|
||
export type Option = { readonly name: string; readonly wert?: string };
|
||
|
||
/** Optionen ohne Wert, die sshd kennt. */
|
||
const SCHALTER = new Set([
|
||
"restrict",
|
||
"cert-authority",
|
||
"no-port-forwarding",
|
||
"no-agent-forwarding",
|
||
"no-x11-forwarding",
|
||
"no-pty",
|
||
"no-user-rc",
|
||
"no-touch-required",
|
||
"verify-required",
|
||
"port-forwarding",
|
||
"agent-forwarding",
|
||
"x11-forwarding",
|
||
"pty",
|
||
"user-rc",
|
||
"touch-required",
|
||
]);
|
||
|
||
/** Optionen mit Wert in Anfuehrungszeichen. */
|
||
const MIT_WERT = new Set([
|
||
"from",
|
||
"command",
|
||
"environment",
|
||
"permitopen",
|
||
"permitlisten",
|
||
"principals",
|
||
"tunnel",
|
||
"expiry-time",
|
||
]);
|
||
|
||
/**
|
||
* Liest den Optionen-Teil am Anfang einer Zeile. Liefert die Optionen und die Laenge des
|
||
* gelesenen Textes (bis zum ersten Leerraum ausserhalb von Anfuehrungszeichen).
|
||
*/
|
||
export function optionenAmAnfangLesen(text: string): { optionen: Option[]; laenge: number } {
|
||
const optionen: Option[] = [];
|
||
let index = 0;
|
||
while (index < text.length) {
|
||
const nameAnfang = index;
|
||
while (index < text.length && /[A-Za-z0-9-]/.test(text[index]!)) index += 1;
|
||
const name = text.slice(nameAnfang, index).toLowerCase();
|
||
if (!name) throw new OptionenFehler(`Unerwartetes Zeichen in den Optionen an Stelle ${index + 1}.`);
|
||
let wert: string | undefined;
|
||
if (text[index] === "=") {
|
||
index += 1;
|
||
if (text[index] !== '"')
|
||
throw new OptionenFehler(`Der Wert von „${name}“ muss in Anführungszeichen stehen.`);
|
||
index += 1;
|
||
let inhalt = "";
|
||
let geschlossen = false;
|
||
while (index < text.length) {
|
||
const zeichen = text[index]!;
|
||
if (zeichen === "\\" && text[index + 1] === '"') {
|
||
inhalt += '"';
|
||
index += 2;
|
||
continue;
|
||
}
|
||
if (zeichen === '"') {
|
||
geschlossen = true;
|
||
index += 1;
|
||
break;
|
||
}
|
||
inhalt += zeichen;
|
||
index += 1;
|
||
}
|
||
if (!geschlossen) throw new OptionenFehler(`Fehlendes schließendes Anführungszeichen bei „${name}“.`);
|
||
wert = inhalt;
|
||
}
|
||
optionen.push(wert === undefined ? { name } : { name, wert });
|
||
if (text[index] === ",") {
|
||
index += 1;
|
||
continue;
|
||
}
|
||
break;
|
||
}
|
||
return { optionen, laenge: index };
|
||
}
|
||
|
||
function optionSchreiben(option: Option): string {
|
||
return option.wert === undefined ? option.name : `${option.name}="${option.wert.replace(/"/g, '\\"')}"`;
|
||
}
|
||
|
||
/**
|
||
* Prueft vom Benutzer eingegebene Optionen und liefert sie normalisiert zurueck. Strenger
|
||
* als sshd: unbekannte Optionen und Steuerzeichen werden abgewiesen – ein Tippfehler soll
|
||
* nicht still dazu fuehren, dass eine Einschraenkung fehlt.
|
||
*/
|
||
export function optionenPruefen(eingabe: string): string {
|
||
const text = eingabe.trim();
|
||
if (!text) return "";
|
||
if (/[\x00-\x1f\x7f]/.test(text)) throw new OptionenFehler("Die Optionen enthalten Steuerzeichen.");
|
||
const { optionen, laenge } = optionenAmAnfangLesen(text);
|
||
if (laenge !== text.length)
|
||
throw new OptionenFehler(
|
||
"Die Optionen dürfen keine Leerzeichen außerhalb von Anführungszeichen enthalten.",
|
||
);
|
||
for (const option of optionen) {
|
||
if (SCHALTER.has(option.name)) {
|
||
if (option.wert !== undefined) throw new OptionenFehler(`„${option.name}“ hat keinen Wert.`);
|
||
} else if (MIT_WERT.has(option.name)) {
|
||
if (option.wert === undefined || option.wert === "")
|
||
throw new OptionenFehler(`„${option.name}“ braucht einen Wert.`);
|
||
} else {
|
||
throw new OptionenFehler(`Unbekannte Option „${option.name}“.`);
|
||
}
|
||
}
|
||
return optionen.map(optionSchreiben).join(",");
|
||
}
|