From 37a70edbabd747919ba21f528c0ed51fd6f4efe7 Mon Sep 17 00:00:00 2001 From: x3 Date: Mon, 21 Sep 2026 23:38:52 +0200 Subject: [PATCH] Fix Permission-denied: gehaertetes Terminal-Eingabefeld ohne Text-Ersetzung, Banner-Tipp, Doku --- .../Views/EmbeddedTerminalView.swift | 25 ++-- .../RemminaMac/Views/TerminalInputField.swift | 110 ++++++++++++++++++ docs/ERRORS.md | 8 ++ docs/USAGE.md | 2 +- 4 files changed, 134 insertions(+), 11 deletions(-) create mode 100644 Sources/RemminaMac/Views/TerminalInputField.swift diff --git a/Sources/RemminaMac/Views/EmbeddedTerminalView.swift b/Sources/RemminaMac/Views/EmbeddedTerminalView.swift index c91207a..95d4ce2 100644 --- a/Sources/RemminaMac/Views/EmbeddedTerminalView.swift +++ b/Sources/RemminaMac/Views/EmbeddedTerminalView.swift @@ -41,6 +41,10 @@ struct EmbeddedTerminalView: View { Text(lastMessage) .font(.caption).foregroundStyle(Dracula.foreground) .lineLimit(3) + if lastMessage.localizedCaseInsensitiveContains("permission denied") { + Text("Tipp: Passwort im Auge-Modus eingeben, auf Tastaturlayout (z. B. Z/Y) achten, Keychain-Eintrag prüfen.") + .font(.caption).foregroundStyle(Dracula.orange) + } } Spacer() Button("Bericht kopieren") { copyReport() } @@ -61,16 +65,17 @@ struct EmbeddedTerminalView: View { // Eingabezeile HStack(spacing: 6) { Text("❯").font(.system(.body, design: .monospaced)).foregroundStyle(Dracula.green) - if secureInput { - SecureField("Eingabe (versteckt, z. B. Passwort) …", text: $input, onCommit: submit) - .textFieldStyle(.plain) - .font(.system(.body, design: .monospaced)) - .foregroundStyle(Dracula.foreground) - } else { - TextField("Eingabe …", text: $input, onCommit: submit) - .textFieldStyle(.plain) - .font(.system(.body, design: .monospaced)) - .foregroundStyle(Dracula.foreground) + // Gehärtetes Feld: keine macOS-Text-Ersetzungen (Passwort-Sonderzeichen!) + ZStack(alignment: .leading) { + if input.isEmpty { + Text(secureInput ? "Eingabe (versteckt, z. B. Passwort) …" : "Eingabe …") + .font(.system(.body, design: .monospaced)) + .foregroundStyle(Dracula.comment.opacity(0.7)) + .allowsHitTesting(false) + } + TerminalInputField(text: $input, isSecure: secureInput, onSubmit: submit) + .id(secureInput) + .frame(height: 22) } Button(action: { secureInput.toggle() }) { Image(systemName: secureInput ? "eye.slash" : "eye") diff --git a/Sources/RemminaMac/Views/TerminalInputField.swift b/Sources/RemminaMac/Views/TerminalInputField.swift new file mode 100644 index 0000000..1974dfb --- /dev/null +++ b/Sources/RemminaMac/Views/TerminalInputField.swift @@ -0,0 +1,110 @@ +import SwiftUI +import AppKit + +/// Eingabefeld ohne macOS-Text-Ersetzungen (Anführungszeichen, Gedankenstriche, +/// Auto-Korrektur, Punkt-Verdopplung, Vervollständigung, Links/Daten-Erkennung). +/// Hintergrund: Normale Textfelder ersetzen still z. B. `"` durch „“ und `--` +/// durch — (je nach Tastatur-Einstellung). Im Terminal fällt das nicht auf, aber +/// ein Passwort mit Sonderzeichen kommt falsch bei ssh an → „Permission denied", +/// obwohl das Passwort „stimmt" (docs/ERRORS.md #13). +/// NSTextField kann das nicht alles abschalten, daher NSTextView-Basis. +/// Secure-Modus nutzt NSSecureTextField (per Design ohne Ersetzungen). +struct TerminalInputField: NSViewRepresentable { + @Binding var text: String + var isSecure: Bool + var onSubmit: () -> Void + + func makeCoordinator() -> Coordinator { + Coordinator(text: $text, onSubmit: onSubmit) + } + + func makeNSView(context: Context) -> NSView { + if isSecure { + let f = NSSecureTextField() + f.font = .monospacedSystemFont(ofSize: 13, weight: .regular) + f.textColor = Dracula.nsColor(for: nil) + f.backgroundColor = .clear + f.isBordered = false + f.focusRingType = .none + f.delegate = context.coordinator + return f + } else { + let tv = NSTextView() + Self.configure(tv) + tv.delegate = context.coordinator + return tv + } + } + + func updateNSView(_ view: NSView, context: Context) { + context.coordinator.onSubmit = onSubmit + if let f = view as? NSTextField, f.stringValue != text { + f.stringValue = text + } else if let tv = view as? NSTextView, tv.string != text { + tv.string = text + } + } + + /// Alle Ersetzungen aus. Auch als Harness-Test nutzbar (reine Flags, headless). + static func configure(_ tv: NSTextView) { + tv.isRichText = false + tv.importsGraphics = false + tv.isAutomaticQuoteSubstitutionEnabled = false + tv.isAutomaticDashSubstitutionEnabled = false + tv.isAutomaticTextCompletionEnabled = false + tv.isAutomaticSpellingCorrectionEnabled = false + tv.isContinuousSpellCheckingEnabled = false + tv.isAutomaticDataDetectionEnabled = false + tv.isAutomaticLinkDetectionEnabled = false + tv.isAutomaticTextReplacementEnabled = false + tv.font = .monospacedSystemFont(ofSize: 13, weight: .regular) + tv.textColor = Dracula.nsColor(for: nil) + tv.backgroundColor = .clear + tv.drawsBackground = false + tv.isVerticallyResizable = false + tv.isHorizontallyResizable = true + tv.textContainer?.widthTracksTextView = false + tv.textContainer?.containerSize = NSSize(width: 100000, height: 24) + } + + final class Coordinator: NSObject, NSTextFieldDelegate, NSTextViewDelegate { + var text: Binding + var onSubmit: () -> Void + + init(text: Binding, onSubmit: @escaping () -> Void) { + self.text = text + self.onSubmit = onSubmit + } + + func controlTextDidChange(_ obj: Notification) { + if let f = obj.object as? NSTextField { + text.wrappedValue = f.stringValue + } + } + + func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool { + if commandSelector == #selector(NSResponder.insertNewline(_:)) { + onSubmit() + return true + } + return false + } + + func textDidChange(_ notification: Notification) { + if let tv = notification.object as? NSTextView { + // Einzeiler: harte Umbrüche sofort entfernen (Paste mit \n) + let single = tv.string.replacingOccurrences(of: "\n", with: "") + if single != tv.string { tv.string = single } + text.wrappedValue = single + } + } + + func textView(_ textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool { + if commandSelector == #selector(NSResponder.insertNewline(_:)) { + onSubmit() + return true + } + return false + } + } +} diff --git a/docs/ERRORS.md b/docs/ERRORS.md index 55648ef..885718f 100644 --- a/docs/ERRORS.md +++ b/docs/ERRORS.md @@ -19,6 +19,7 @@ Stand: 2026-09-21 · Version 0.1.0 · macOS arm64 · Swift 5.9+ · Xcode 15+ | 10 | 2026-09-21 | Icon-Generator (`RenderIcon.swift`) | 3 Swift-Fehler: `guard let x = f() ?? g()` (nicht optional), `Int * CGFloat`, falscher Preview-Pfad | klein | ✅ behoben | | 11 | 2026-09-21 | Passwort-Feature | Wunsch: Passwort + Keys speichern UND nutzen (bisher nur gespeichert, nie benutzt) | Feature | ✅ umgesetzt — Auto-Login via sshpass, sonst interaktiv | | 12 | 2026-09-21 | „Immer Verbindung falsch" | User-Report ohne Details (Ziel/Credentials unbekannt) | offen | ⬜ Teils: Editor-Validierung + kopierbarer Fehlerbericht; warte auf Bericht | +| 13 | 2026-09-21 | Permission denied trotz richtigem Passwort | Text-Ersetzung im Eingabefeld verfälscht Sonderzeichen | hoch | ✅ behoben — gehärtetes Eingabefeld (alle Ersetzungen aus, 9/9 Tests) | --- @@ -176,3 +177,10 @@ Oder mit Token als Passwort: `git push https://:@git.sysdaemon.xyz **Umgesetzt:** `Models/ProfileValidation.swift` (trimmen + Warnungen, Harness 7/7), Passwort direkt unter Benutzername, „Bericht kopieren" im Fehler-Banner (ohne Secrets). **Offen:** Bitte Banner-Text oder kopierten Bericht hierher schicken — dann steht die Ursache in einer Zeile. **Status:** ⬜ wartet auf Fehlerbericht. + +## #13 — Permission denied trotz richtigem Passwort (User-Fehlerbericht) +**Bericht:** `administrator@sysdaemon.xyz:22`, ssh fragt 3x Passwort → `Permission denied (publickey,password)`. Gleiches Passwort im Terminal.app funktioniert. +**Ursache:** Das Terminal-Eingabefeld war ein Standard-TextField. macOS ersetzt dort je nach Einstellung still Zeichen (`"` → „“, `--` → —, Auto-Korrektur). Ein Passwort mit Sonderzeichen kommt dadurch falsch bei ssh an — im Terminal fällt nichts auf, ssh sagt nur „denied". Verifiziert: Standard-`NSTextField` hat diese Schalter an bzw. `NSTextField` kann Dash-Ersetzung gar nicht abschalten (Compiler-Beweis). +**Fix (`Views/TerminalInputField.swift`):** Eigenes Eingabefeld auf `NSTextView`-Basis mit allen Ersetzungen explizit aus (Quotes, Dashes, Completion, Spelling, Data/Link-Erkennung, Text-Replacement, Plain-Text); Secure-Variante via `NSSecureTextField` (per Design ersetzungsfrei); Enter → Senden, Paste-Normalisierung (keine Umbrüche). +**Verifikation:** Harness 9/9 (alle Flags aus). Hinweis: Restrisiko Tastaturlayout (Z/Y) bleibt — Banner-Tipp ergänzt. +**Status:** ✅ behoben — bitte mit v0.5.1 erneut testen. diff --git a/docs/USAGE.md b/docs/USAGE.md index 5ab957f..83802f2 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -30,7 +30,7 @@ - Im Profil-Editor: Passwort eingeben + „in Keychain speichern“. Gespeichertes Passwort wird angezeigt (✓) und kann gelöscht werden. In `profiles.json` steht nur die Referenz, nie das Passwort. - Reihenfolge beim Verbinden: Profil-Passwort zuerst, sonst Key-Passphrase. - **Auto-Login ohne Tippen:** braucht `sshpass` (`brew install sshpass`). Dann loggt ssh via Keychain-Secret ein (Passwort nur im Prozess-Env `SSHPASS`, nie in Befehlszeile/Logs). Status zeigt „Auto-Login via Keychain". -- **Ohne sshpass:** Passwort/Key-Passphrase wird interaktiv im Terminal abgefragt (Passwort-Modus: Auge-Icon). +- **Ohne sshpass:** Passwort/Key-Passphrase wird interaktiv im Terminal abgefragt (Passwort-Modus: Auge-Icon). Bei Sonderzeichen im Passwort immer Auge-Modus nutzen (keine Text-Ersetzung). ## Quick-Connect - Mit Auswahl: verbindet direkt. Ohne Auswahl: öffnet Editor mit „Quick-Connect“-Vorlage.