diff --git a/ShhShell.xcodeproj/project.pbxproj b/ShhShell.xcodeproj/project.pbxproj index 0c0e213..92951b6 100644 --- a/ShhShell.xcodeproj/project.pbxproj +++ b/ShhShell.xcodeproj/project.pbxproj @@ -180,8 +180,8 @@ A92538C72DEE0742007E0A18 /* ShhShell */ = { isa = PBXGroup; children = ( - A93143C22DF61F5700FCD5DB /* ShhShell.entitlements */, A92538C62DEE0742007E0A18 /* ShhShellApp.swift */, + A93143C22DF61F5700FCD5DB /* ShhShell.entitlements */, A98554572E055398009051BD /* Keys */, A98554562E055394009051BD /* Host */, A93143C12DF61E8500FCD5DB /* SSH */, @@ -210,12 +210,11 @@ A92538D32DEE0749007E0A18 /* Views */ = { isa = PBXGroup; children = ( - A98554532E05534F009051BD /* Keys */, A92538C52DEE0742007E0A18 /* ContentView.swift */, - A98554622E0587DF009051BD /* HostsView.swift */, - A985545C2E055D4D009051BD /* ConnectionView.swift */, - A93143C52DF61FE300FCD5DB /* ViewModifiers.swift */, + A98554532E05534F009051BD /* Keys */, + A96C6B042E0C523E00F377FE /* Hosts */, A923172B2E0712F200ECE1E6 /* Terminal */, + A96C6B032E0C523600F377FE /* Misc */, ); path = Views; sourceTree = ""; @@ -248,11 +247,29 @@ path = ci_scripts; sourceTree = ""; }; + A96C6B032E0C523600F377FE /* Misc */ = { + isa = PBXGroup; + children = ( + A96C6B012E0C49E800F377FE /* CenteredLabel.swift */, + A93143C52DF61FE300FCD5DB /* ViewModifiers.swift */, + ); + path = Misc; + sourceTree = ""; + }; + A96C6B042E0C523E00F377FE /* Hosts */ = { + isa = PBXGroup; + children = ( + A985545C2E055D4D009051BD /* ConnectionView.swift */, + A98554622E0587DF009051BD /* HostsView.swift */, + ); + path = Hosts; + sourceTree = ""; + }; A98554532E05534F009051BD /* Keys */ = { isa = PBXGroup; children = ( + A96C6AFF2E0C45FE00F377FE /* KeyDetailView.swift */, A98554542E05535F009051BD /* KeyManagerView.swift */, - A96C6AFD2E0C43B600F377FE /* Keypair.swift */, ); path = Keys; sourceTree = ""; @@ -271,8 +288,7 @@ children = ( A98554582E0553AA009051BD /* KeyManager.swift */, A985545E2E056EDD009051BD /* KeychainLayer.swift */, - A96C6AFF2E0C45FE00F377FE /* KeyDetailView.swift */, - A96C6B012E0C49E800F377FE /* CenteredLabel.swift */, + A96C6AFD2E0C43B600F377FE /* Keypair.swift */, ); path = Keys; sourceTree = ""; diff --git a/ShhShell/Host/Host.swift b/ShhShell/Host/Host.swift index a014f69..66381fc 100644 --- a/ShhShell/Host/Host.swift +++ b/ShhShell/Host/Host.swift @@ -9,6 +9,7 @@ import Foundation protocol HostPr: Codable, Identifiable, Equatable { var id: UUID { get set } + var name: String { get set } var address: String { get set } var port: Int { get set } var username: String { get set } @@ -21,6 +22,7 @@ protocol HostPr: Codable, Identifiable, Equatable { struct Host: HostPr { var id = UUID() + var name: String = "" var address: String = "" var port: Int var username: String @@ -31,6 +33,7 @@ struct Host: HostPr { var key: String? init( + name: String = "", address: String, port: Int = 22, username: String = "", @@ -40,6 +43,7 @@ struct Host: HostPr { passphrase: String = "", hostkey: String? = nil ) { + self.name = name self.address = address self.port = port self.username = username @@ -57,6 +61,7 @@ extension Host { } static var debug: Host { Host( + name: "name for localhost", address: "localhost", port: 22, username: "neon443", diff --git a/ShhShell/Views/Keys/Keypair.swift b/ShhShell/Keys/Keypair.swift similarity index 100% rename from ShhShell/Views/Keys/Keypair.swift rename to ShhShell/Keys/Keypair.swift diff --git a/ShhShell/SSH/SSHError.swift b/ShhShell/SSH/SSHError.swift index 7da648e..dfcc04a 100644 --- a/ShhShell/SSH/SSHError.swift +++ b/ShhShell/SSH/SSHError.swift @@ -19,6 +19,7 @@ enum AuthError: Error { } enum KeyError: Error { + case notConnected case importPubkeyError case importPrivkeyError case pubkeyRejected diff --git a/ShhShell/SSH/SSHHandler.swift b/ShhShell/SSH/SSHHandler.swift index 3f2cfc7..3561022 100644 --- a/ShhShell/SSH/SSHHandler.swift +++ b/ShhShell/SSH/SSHHandler.swift @@ -82,7 +82,13 @@ class SSHHandler: @unchecked Sendable, ObservableObject { print(error.localizedDescription) } } - openShell() + + do { + try openShell() + } catch { + print(error.localizedDescription) + } + setTitle("\(host.username)@\(host.address)") ssh_channel_request_env(channel, "TERM", "xterm-256color") ssh_channel_request_env(channel, "LANG", "en_US.UTF-8") @@ -90,6 +96,7 @@ class SSHHandler: @unchecked Sendable, ObservableObject { } func connect() throws(SSHError) { + guard !host.address.isEmpty else { throw .connectionFailed("No address to connect to.") } withAnimation { state = .connecting } var verbosity: Int = 0 @@ -153,6 +160,14 @@ class SSHHandler: @unchecked Sendable, ObservableObject { } } + func hostInvalid() -> Bool { + if host.address.isEmpty && host.username.isEmpty { + return true + } else { + return false + } + } + func testExec() { var success = false defer { @@ -174,9 +189,7 @@ class SSHHandler: @unchecked Sendable, ObservableObject { var nbytes: CInt let testChannel = ssh_channel_new(session) - guard testChannel != nil else { - return - } + guard testChannel != nil else { return } status = ssh_channel_open_session(testChannel) guard status == SSH_OK else { @@ -220,9 +233,7 @@ class SSHHandler: @unchecked Sendable, ObservableObject { //MARK: auth func authWithPubkey(pub pubInp: Data, priv privInp: Data, pass: String) throws(KeyError) { - guard session != nil else { - return - } + guard session != nil else { throw .notConnected } let fileManager = FileManager.default let tempDir = fileManager.temporaryDirectory @@ -328,32 +339,36 @@ class SSHHandler: @unchecked Sendable, ObservableObject { } //MARK: shell - func openShell() { + func openShell() throws(SSHError) { var status: CInt channel = ssh_channel_new(session) - guard let channel else { return } + guard let channel else { throw .communicationError("Not connected") } status = ssh_channel_open_session(channel) guard status == SSH_OK else { ssh_channel_free(channel) - return + throw .communicationError("Failed opening channel") } - interactiveShellSession() + do { + try interactiveShellSession() + } catch { + print(error.localizedDescription) + } } - private func interactiveShellSession() { + private func interactiveShellSession() throws(SSHError) { var status: CInt status = ssh_channel_request_pty(self.channel) - guard status == SSH_OK else { return } + guard status == SSH_OK else { throw .communicationError("PTY request failed") } status = ssh_channel_change_pty_size(self.channel, 80, 24) - guard status == SSH_OK else { return } + guard status == SSH_OK else { throw .communicationError("Failed setting PTY size") } status = ssh_channel_request_shell(self.channel) - guard status == SSH_OK else { return } + guard status == SSH_OK else { throw .communicationError("Failed requesting shell") } withAnimation { state = .shellOpen } } @@ -398,9 +413,9 @@ class SSHHandler: @unchecked Sendable, ObservableObject { } } - func resizePTY(toRows: Int, toCols: Int) { - guard ssh_channel_is_open(channel) != 0 else { return } - guard ssh_channel_is_eof(channel) == 0 else { return } + func resizePTY(toRows: Int, toCols: Int) throws(SSHError) { + guard ssh_channel_is_open(channel) != 0 else { throw .communicationError("Channel not open") } + guard ssh_channel_is_eof(channel) == 0 else { throw .backendError("Channel is EOF") } ssh_channel_change_pty_size(channel, Int32(toCols), Int32(toRows)) // print("resized tty to \(toRows)rows and \(toCols)cols") diff --git a/ShhShell/Views/ConnectionView.swift b/ShhShell/Views/Hosts/ConnectionView.swift similarity index 90% rename from ShhShell/Views/ConnectionView.swift rename to ShhShell/Views/Hosts/ConnectionView.swift index db2c0be..7e48e78 100644 --- a/ShhShell/Views/ConnectionView.swift +++ b/ShhShell/Views/Hosts/ConnectionView.swift @@ -76,17 +76,6 @@ struct ConnectionView: View { TextField("", text: $passphrase, prompt: Text("Passphrase (Optional)")) } - if handler.host.key != nil { - Text("Hostkey: \(handler.getHostkey() ?? hostsManager.getHostMatching(handler.host)?.key ?? "nil")") - .onChange(of: handler.host.key) { _ in - guard let previousKnownHost = hostsManager.getHostMatching(handler.host) else { return } - guard handler.host.key == previousKnownHost.key else { - hostKeyChangedAlert = true - return - } - } - } - Button() { showTerminal.toggle() } label: { @@ -130,12 +119,20 @@ struct ConnectionView: View { systemImage: handler.connected ? "xmark.app.fill" : "power" ) } + .disabled(handler.hostInvalid()) } } } .fullScreenCover(isPresented: $showTerminal) { ShellView(handler: handler) } + .onChange(of: handler.host.key) { _ in + guard let previousKnownHost = hostsManager.getHostMatching(handler.host) else { return } + guard handler.host.key == previousKnownHost.key else { + hostKeyChangedAlert = true + return + } + } .onDisappear { guard hostsManager.getHostMatching(handler.host) == handler.host else { hostsManager.updateHost(handler.host) diff --git a/ShhShell/Views/HostsView.swift b/ShhShell/Views/Hosts/HostsView.swift similarity index 100% rename from ShhShell/Views/HostsView.swift rename to ShhShell/Views/Hosts/HostsView.swift diff --git a/ShhShell/Keys/KeyDetailView.swift b/ShhShell/Views/Keys/KeyDetailView.swift similarity index 100% rename from ShhShell/Keys/KeyDetailView.swift rename to ShhShell/Views/Keys/KeyDetailView.swift diff --git a/ShhShell/Views/Keys/KeyManagerView.swift b/ShhShell/Views/Keys/KeyManagerView.swift index a34d839..90a5bd0 100644 --- a/ShhShell/Views/Keys/KeyManagerView.swift +++ b/ShhShell/Views/Keys/KeyManagerView.swift @@ -19,13 +19,31 @@ struct KeyManagerView: View { NavigationLink { KeyDetailView(hostsManager: hostsManager, keypair: keypair) } label: { - Text(String(data: keypair.publicKey!, encoding: .utf8) ?? "nil") + if let publicKey = keypair.publicKey { + Text(String(data: publicKey, encoding: .utf8) ?? "nil") + } } } } Section { NavigationLink { List { + if hostsManager.savedHosts.isEmpty { + VStack(alignment: .center) { + Text("Looking empty 'round here...") + .font(.title3) + .bold() + .padding(.bottom) + VStack(alignment: .leading) { + Text("Connect to some hosts to collect more hostkeys!") + .padding(.bottom) + Text("ShhShell remembers hostkey fingerprints for you, and can alert you if they change.") + .font(.subheadline) + Text("This could be due a man in the middle attack, where a bad actor tries to impersonate your server.") + .font(.subheadline) + } + } + } ForEach(hostsManager.savedHosts) { host in VStack(alignment: .leading) { Text(host.address) diff --git a/ShhShell/Keys/CenteredLabel.swift b/ShhShell/Views/Misc/CenteredLabel.swift similarity index 100% rename from ShhShell/Keys/CenteredLabel.swift rename to ShhShell/Views/Misc/CenteredLabel.swift diff --git a/ShhShell/Views/ViewModifiers.swift b/ShhShell/Views/Misc/ViewModifiers.swift similarity index 100% rename from ShhShell/Views/ViewModifiers.swift rename to ShhShell/Views/Misc/ViewModifiers.swift diff --git a/ShhShell/Views/Terminal/SSHTerminalView.swift b/ShhShell/Views/Terminal/SSHTerminalView.swift index 1f96d4d..2c0cbfb 100644 --- a/ShhShell/Views/Terminal/SSHTerminalView.swift +++ b/ShhShell/Views/Terminal/SSHTerminalView.swift @@ -75,7 +75,7 @@ final class SSHTerminalView: TerminalView, Sendable, @preconcurrency TerminalVie } public func sizeChanged(source: TerminalView, newCols: Int, newRows: Int) { - handler?.resizePTY(toRows: newRows, toCols: newCols) + try? handler?.resizePTY(toRows: newRows, toCols: newCols) } public func send(source: TerminalView, data: ArraySlice) {