diff --git a/Sources/RemminaMac/Views/EmbeddedTerminalView.swift b/Sources/RemminaMac/Views/EmbeddedTerminalView.swift index d61528a..cdbd714 100644 --- a/Sources/RemminaMac/Views/EmbeddedTerminalView.swift +++ b/Sources/RemminaMac/Views/EmbeddedTerminalView.swift @@ -11,9 +11,7 @@ struct EmbeddedTerminalView: View { @State private var input = "" @State private var secureInput = false @State private var checking = false - @State private var hostKeyInfo: HostKeyInfo? - @State private var hostKeyChange = false - @State private var showingHostKey = false + @State private var hostKeyRequest: HostKeyRequest? var body: some View { VStack(spacing: 0) { @@ -101,17 +99,14 @@ struct EmbeddedTerminalView: View { connect() // Auto-Connect beim Öffnen des Tabs } } - .sheet(isPresented: $showingHostKey) { - if let info = hostKeyInfo { - HostKeySheet(info: info, isChange: hostKeyChange, - onAccept: acceptHostKey, - onCancel: { - showingHostKey = false - checking = false - hostKeyInfo = nil - term.fail("Host-Key nicht akzeptiert — Verbindung abgebrochen.") - }) - } + .sheet(item: $hostKeyRequest) { req in + HostKeySheet(info: req.info, isChange: req.isChange, + onAccept: { acceptHostKey(req) }, + onCancel: { + hostKeyRequest = nil + checking = false + term.fail("Host-Key nicht akzeptiert — Verbindung abgebrochen.") + }) } } @@ -125,40 +120,41 @@ struct EmbeddedTerminalView: View { term.fail("Vorab-Check: \(message)") return } - // Host-Key vorab per keyscan prüfen (Yes/No-Prompt käme ohne Ctty nicht an) - let hk = HostKeyService.status(host: profile.host, port: profile.port) - switch hk { - case .known, .unreachable: - checking = false - term.start() - case .unknown(let info): - hostKeyInfo = info - hostKeyChange = false - showingHostKey = true - case .changed(let info): - hostKeyInfo = info - hostKeyChange = true - showingHostKey = true + // Host-Key vorab per keyscan prüfen (Yes/No-Prompt käme ohne Ctty nicht an). + // Läuft im Hintergrund: ssh-keyscan blockiert Sekunden (kein UI-Freeze). + let host = profile.host + let port = profile.port + DispatchQueue.global(qos: .userInitiated).async { + let hk = HostKeyService.status(host: host, port: port) + DispatchQueue.main.async { + switch hk { + case .known, .unreachable: + checking = false + term.start() + case .unknown(let info): + hostKeyRequest = HostKeyRequest(info: info, isChange: false) + case .changed(let info): + hostKeyRequest = HostKeyRequest(info: info, isChange: true) + } + } } } } } - func acceptHostKey() { - guard let info = hostKeyInfo else { return } - showingHostKey = false - if hostKeyChange { + func acceptHostKey(_ req: HostKeyRequest) { + hostKeyRequest = nil + if req.isChange { _ = HostKeyService.revoke(host: profile.host, port: profile.port) } - if HostKeyService.accept(info) { + if HostKeyService.accept(req.info) { checking = false - term.note("Host-Key akzeptiert (\(info.keyType), \(info.fingerprintSHA256.prefix(19))…).") + term.note("Host-Key akzeptiert (\(req.info.keyType), \(req.info.fingerprintSHA256.prefix(19))…).") term.start() } else { checking = false term.fail("Host-Key konnte nicht in known_hosts gespeichert werden.") } - hostKeyInfo = nil } func diagnose() { diff --git a/Sources/RemminaMac/Views/HostKeySheet.swift b/Sources/RemminaMac/Views/HostKeySheet.swift index c39b2e5..2b7c2b5 100644 --- a/Sources/RemminaMac/Views/HostKeySheet.swift +++ b/Sources/RemminaMac/Views/HostKeySheet.swift @@ -1,5 +1,13 @@ import SwiftUI +/// Sheet-Request (Identifiable): .sheet(item:) rendert IMMER mit Wert — +/// ein `if let` im Sheet kann den Empty-Zweig nehmen und bleibt dann leer. +struct HostKeyRequest: Identifiable { + let id = UUID() + var info: HostKeyInfo + var isChange: Bool +} + /// Popup bei unbekanntem/geändertem Host-Key: Fingerprint prüfen, dann /// akzeptieren (→ known_hosts + verbinden) oder abbrechen. struct HostKeySheet: View { diff --git a/docs/ERRORS.md b/docs/ERRORS.md index 18a5cdb..2265e6e 100644 --- a/docs/ERRORS.md +++ b/docs/ERRORS.md @@ -25,6 +25,7 @@ Stand: 2026-09-21 · Version 0.1.0 · macOS arm64 · Swift 5.9+ · Xcode 15+ | 16 | 2026-09-22 | Profil-Änderungen wirkungslos | Session-Cache ignorierte Host-/Passwort-/Key-Korrekturen | hoch | ✅ behoben — Fingerprint-Reconfig (9/9 Tests) | | 17 | 2026-09-22 | rzsv: Vorab-Check Timeout | Port 22 wird serverseitig gedroppt (443 ok) — kein App-Fehler | extern | ⬜ Server-Seite: fail2ban/VPN/Firewall prüfen | | 18 | 2026-09-22 | Host-Key-Yes/No unmöglich | ssh ohne Controlling-Terminal: /dev/tty ENXIO → stummer Abbruch | kritisch | ✅ ctty-run-Helper (Double-Fork) + Fingerprint-Popup | +| 19 | 2026-09-22 | Popup leer (s. Screenshot) | `if let` im Sheet nimmt Empty-Zweig ohne Update-Pfad | mittel | ✅ `.sheet(item:)` + Keyscan im Hintergrund | --- @@ -241,3 +242,9 @@ Zweite Zeile erzwingt Passwort-Login ohne Agent — scheitert sie auch im Termin - `Views/HostKeySheet.swift` + Flow: Vorab per `ssh-keyscan` Fingerprint holen → Popup (SHA256/MD5, Typ) → Akzeptieren (→ `~/.ssh/known_hosts`, geteilt mit Terminal) / Abbrechen; bei GEÄNDERTEM Key rote MITM-Warnung + `ssh-keygen -R`-Pfad. **Verifikation (lokaler paramiko-Server):** Bug reproduziert (kein Prompt, Exit 255) → nach Fix: Ctty per `ps` bewiesen (ttys001), Yes/No sichtbar + beantwortbar, Passwort akzeptiert, Shell aktiv. HostKeyService: unknown/accept/known/changed/revoke/unreachable grün. Regression (stop ohne Waisen) grün. **Status:** ✅ behoben (v0.6.0) — Fingerprint im Popup mit Terminal-Ausgabe vergleichbar. + +## #19 — Fingerprint-Popup erscheint leer (User-Screenshot) +**Bild:** Sheet-Fenster da, aber winzig und ohne Inhalt; Status hängt auf „Prüfe …". +**Ursache:** `.sheet(isPresented:)` + `if let info` im Content: Nimmt die Auswertung den Empty-Zweig (Timing beim Präsentieren), hat der leere Zweig keine Abhängigkeit auf die State-Variable → Sheet aktualisiert sich nie, bleibt leer. Klassische SwiftUI-Falle. +**Fix:** `.sheet(item: $hostKeyRequest)` mit `HostKeyRequest: Identifiable` — Content existiert strukturell immer. Zusätzlich läuft `HostKeyService.status` (ssh-keyscan blockiert Sekunden) jetzt auf Hintergrund-Queue — vorher fror die UI während „Prüfe …" ein. +**Status:** ✅ behoben (v0.6.1).