Files
sshkeyvault/src/lib/ssh/optionen.ts
T
florian.wildeandClaude Opus 5.5 19edd20d72 feat(ssh): authorized_keys lesen und verwalteten Block setzen (#015)
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>
2026-10-12 10:10:54 +02:00

126 lines
4.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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(",");
}