mirror of
https://github.com/neon443/ShhShell.git
synced 2026-03-11 13:26:16 +00:00
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:
@@ -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>";
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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>) {
|
||||||
|
|||||||
Reference in New Issue
Block a user