/** * 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(","); }