made more funcitons throw errors instead of just return, more informative

added name to host
removed hostkey from connectionview as its in keys now
fix crash on force unrap in keypair navigatoin labels
added hostinvalid() to check if the host is invalid
added case notconnected for keyerror
openshell is throwing
moved stuff round
This commit is contained in:
neon443
2025-06-25 17:17:47 +01:00
parent 2548dde85b
commit 76601e0470
12 changed files with 91 additions and 39 deletions

View File

@@ -180,8 +180,8 @@
A92538C72DEE0742007E0A18 /* ShhShell */ = { A92538C72DEE0742007E0A18 /* ShhShell */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
A93143C22DF61F5700FCD5DB /* ShhShell.entitlements */,
A92538C62DEE0742007E0A18 /* ShhShellApp.swift */, A92538C62DEE0742007E0A18 /* ShhShellApp.swift */,
A93143C22DF61F5700FCD5DB /* ShhShell.entitlements */,
A98554572E055398009051BD /* Keys */, A98554572E055398009051BD /* Keys */,
A98554562E055394009051BD /* Host */, A98554562E055394009051BD /* Host */,
A93143C12DF61E8500FCD5DB /* SSH */, A93143C12DF61E8500FCD5DB /* SSH */,
@@ -210,12 +210,11 @@
A92538D32DEE0749007E0A18 /* Views */ = { A92538D32DEE0749007E0A18 /* Views */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
A98554532E05534F009051BD /* Keys */,
A92538C52DEE0742007E0A18 /* ContentView.swift */, A92538C52DEE0742007E0A18 /* ContentView.swift */,
A98554622E0587DF009051BD /* HostsView.swift */, A98554532E05534F009051BD /* Keys */,
A985545C2E055D4D009051BD /* ConnectionView.swift */, A96C6B042E0C523E00F377FE /* Hosts */,
A93143C52DF61FE300FCD5DB /* ViewModifiers.swift */,
A923172B2E0712F200ECE1E6 /* Terminal */, A923172B2E0712F200ECE1E6 /* Terminal */,
A96C6B032E0C523600F377FE /* Misc */,
); );
path = Views; path = Views;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -248,11 +247,29 @@
path = ci_scripts; path = ci_scripts;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
A96C6B032E0C523600F377FE /* Misc */ = {
isa = PBXGroup;
children = (
A96C6B012E0C49E800F377FE /* CenteredLabel.swift */,
A93143C52DF61FE300FCD5DB /* ViewModifiers.swift */,
);
path = Misc;
sourceTree = "<group>";
};
A96C6B042E0C523E00F377FE /* Hosts */ = {
isa = PBXGroup;
children = (
A985545C2E055D4D009051BD /* ConnectionView.swift */,
A98554622E0587DF009051BD /* HostsView.swift */,
);
path = Hosts;
sourceTree = "<group>";
};
A98554532E05534F009051BD /* Keys */ = { A98554532E05534F009051BD /* Keys */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
A96C6AFF2E0C45FE00F377FE /* KeyDetailView.swift */,
A98554542E05535F009051BD /* KeyManagerView.swift */, A98554542E05535F009051BD /* KeyManagerView.swift */,
A96C6AFD2E0C43B600F377FE /* Keypair.swift */,
); );
path = Keys; path = Keys;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -271,8 +288,7 @@
children = ( children = (
A98554582E0553AA009051BD /* KeyManager.swift */, A98554582E0553AA009051BD /* KeyManager.swift */,
A985545E2E056EDD009051BD /* KeychainLayer.swift */, A985545E2E056EDD009051BD /* KeychainLayer.swift */,
A96C6AFF2E0C45FE00F377FE /* KeyDetailView.swift */, A96C6AFD2E0C43B600F377FE /* Keypair.swift */,
A96C6B012E0C49E800F377FE /* CenteredLabel.swift */,
); );
path = Keys; path = Keys;
sourceTree = "<group>"; sourceTree = "<group>";

View File

@@ -9,6 +9,7 @@ import Foundation
protocol HostPr: Codable, Identifiable, Equatable { protocol HostPr: Codable, Identifiable, Equatable {
var id: UUID { get set } var id: UUID { get set }
var name: String { get set }
var address: String { get set } var address: String { get set }
var port: Int { get set } var port: Int { get set }
var username: String { get set } var username: String { get set }
@@ -21,6 +22,7 @@ protocol HostPr: Codable, Identifiable, Equatable {
struct Host: HostPr { struct Host: HostPr {
var id = UUID() var id = UUID()
var name: String = ""
var address: String = "" var address: String = ""
var port: Int var port: Int
var username: String var username: String
@@ -31,6 +33,7 @@ struct Host: HostPr {
var key: String? var key: String?
init( init(
name: String = "",
address: String, address: String,
port: Int = 22, port: Int = 22,
username: String = "", username: String = "",
@@ -40,6 +43,7 @@ struct Host: HostPr {
passphrase: String = "", passphrase: String = "",
hostkey: String? = nil hostkey: String? = nil
) { ) {
self.name = name
self.address = address self.address = address
self.port = port self.port = port
self.username = username self.username = username
@@ -57,6 +61,7 @@ extension Host {
} }
static var debug: Host { static var debug: Host {
Host( Host(
name: "name for localhost",
address: "localhost", address: "localhost",
port: 22, port: 22,
username: "neon443", username: "neon443",

View File

@@ -19,6 +19,7 @@ enum AuthError: Error {
} }
enum KeyError: Error { enum KeyError: Error {
case notConnected
case importPubkeyError case importPubkeyError
case importPrivkeyError case importPrivkeyError
case pubkeyRejected case pubkeyRejected

View File

@@ -82,7 +82,13 @@ class SSHHandler: @unchecked Sendable, ObservableObject {
print(error.localizedDescription) print(error.localizedDescription)
} }
} }
openShell()
do {
try openShell()
} catch {
print(error.localizedDescription)
}
setTitle("\(host.username)@\(host.address)") setTitle("\(host.username)@\(host.address)")
ssh_channel_request_env(channel, "TERM", "xterm-256color") ssh_channel_request_env(channel, "TERM", "xterm-256color")
ssh_channel_request_env(channel, "LANG", "en_US.UTF-8") ssh_channel_request_env(channel, "LANG", "en_US.UTF-8")
@@ -90,6 +96,7 @@ class SSHHandler: @unchecked Sendable, ObservableObject {
} }
func connect() throws(SSHError) { func connect() throws(SSHError) {
guard !host.address.isEmpty else { throw .connectionFailed("No address to connect to.") }
withAnimation { state = .connecting } withAnimation { state = .connecting }
var verbosity: Int = 0 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() { func testExec() {
var success = false var success = false
defer { defer {
@@ -174,9 +189,7 @@ class SSHHandler: @unchecked Sendable, ObservableObject {
var nbytes: CInt var nbytes: CInt
let testChannel = ssh_channel_new(session) let testChannel = ssh_channel_new(session)
guard testChannel != nil else { guard testChannel != nil else { return }
return
}
status = ssh_channel_open_session(testChannel) status = ssh_channel_open_session(testChannel)
guard status == SSH_OK else { guard status == SSH_OK else {
@@ -220,9 +233,7 @@ class SSHHandler: @unchecked Sendable, ObservableObject {
//MARK: auth //MARK: auth
func authWithPubkey(pub pubInp: Data, priv privInp: Data, pass: String) throws(KeyError) { func authWithPubkey(pub pubInp: Data, priv privInp: Data, pass: String) throws(KeyError) {
guard session != nil else { guard session != nil else { throw .notConnected }
return
}
let fileManager = FileManager.default let fileManager = FileManager.default
let tempDir = fileManager.temporaryDirectory let tempDir = fileManager.temporaryDirectory
@@ -328,32 +339,36 @@ class SSHHandler: @unchecked Sendable, ObservableObject {
} }
//MARK: shell //MARK: shell
func openShell() { func openShell() throws(SSHError) {
var status: CInt var status: CInt
channel = ssh_channel_new(session) channel = ssh_channel_new(session)
guard let channel else { return } guard let channel else { throw .communicationError("Not connected") }
status = ssh_channel_open_session(channel) status = ssh_channel_open_session(channel)
guard status == SSH_OK else { guard status == SSH_OK else {
ssh_channel_free(channel) 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 var status: CInt
status = ssh_channel_request_pty(self.channel) 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) 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) 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 } withAnimation { state = .shellOpen }
} }
@@ -398,9 +413,9 @@ class SSHHandler: @unchecked Sendable, ObservableObject {
} }
} }
func resizePTY(toRows: Int, toCols: Int) { func resizePTY(toRows: Int, toCols: Int) throws(SSHError) {
guard ssh_channel_is_open(channel) != 0 else { return } guard ssh_channel_is_open(channel) != 0 else { throw .communicationError("Channel not open") }
guard ssh_channel_is_eof(channel) == 0 else { return } guard ssh_channel_is_eof(channel) == 0 else { throw .backendError("Channel is EOF") }
ssh_channel_change_pty_size(channel, Int32(toCols), Int32(toRows)) ssh_channel_change_pty_size(channel, Int32(toCols), Int32(toRows))
// print("resized tty to \(toRows)rows and \(toCols)cols") // print("resized tty to \(toRows)rows and \(toCols)cols")

View File

@@ -76,17 +76,6 @@ struct ConnectionView: View {
TextField("", text: $passphrase, prompt: Text("Passphrase (Optional)")) 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() { Button() {
showTerminal.toggle() showTerminal.toggle()
} label: { } label: {
@@ -130,12 +119,20 @@ struct ConnectionView: View {
systemImage: handler.connected ? "xmark.app.fill" : "power" systemImage: handler.connected ? "xmark.app.fill" : "power"
) )
} }
.disabled(handler.hostInvalid())
} }
} }
} }
.fullScreenCover(isPresented: $showTerminal) { .fullScreenCover(isPresented: $showTerminal) {
ShellView(handler: handler) 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 { .onDisappear {
guard hostsManager.getHostMatching(handler.host) == handler.host else { guard hostsManager.getHostMatching(handler.host) == handler.host else {
hostsManager.updateHost(handler.host) hostsManager.updateHost(handler.host)

View File

@@ -19,13 +19,31 @@ struct KeyManagerView: View {
NavigationLink { NavigationLink {
KeyDetailView(hostsManager: hostsManager, keypair: keypair) KeyDetailView(hostsManager: hostsManager, keypair: keypair)
} label: { } label: {
Text(String(data: keypair.publicKey!, encoding: .utf8) ?? "nil") if let publicKey = keypair.publicKey {
Text(String(data: publicKey, encoding: .utf8) ?? "nil")
}
} }
} }
} }
Section { Section {
NavigationLink { NavigationLink {
List { 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 ForEach(hostsManager.savedHosts) { host in
VStack(alignment: .leading) { VStack(alignment: .leading) {
Text(host.address) Text(host.address)

View File

@@ -75,7 +75,7 @@ final class SSHTerminalView: TerminalView, Sendable, @preconcurrency TerminalVie
} }
public func sizeChanged(source: TerminalView, newCols: Int, newRows: Int) { 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<UInt8>) { public func send(source: TerminalView, data: ArraySlice<UInt8>) {