functions to get hosts and indexes matching input, and to update a host

added a hostkey checker - fatalerror rn
reordered ui in connection view to make more sense
added sections
moved connect button to toolbar, disconect is there too!
hostsview update
This commit is contained in:
neon443
2025-06-21 11:03:30 +01:00
parent 57c6b6005b
commit d982d018b8
4 changed files with 162 additions and 125 deletions

View File

@@ -16,6 +16,30 @@ class HostsManager: ObservableObject {
loadSavedHosts()
}
/// get the index of a matching host in saved hosts
/// - Parameter host: input a host
/// - Returns: if an item in savedHosts has a matching uuid to the parameter, it returns the index
/// else returns nil
func getHostIndexMatching(_ hostSearchingFor: Host) -> Int? {
if let index = savedHosts.firstIndex(where: { $0.id == hostSearchingFor.id }) {
return index
} else {
return nil
}
}
func getHostMatching(_ HostSearchingFor: Host) -> Host? {
guard let index = getHostIndexMatching(HostSearchingFor) else { return nil }
return savedHosts[index]
}
func updateHost(_ updatedHost: Host) {
if let index = savedHosts.firstIndex(where: { $0.id == updatedHost.id }) {
savedHosts[index] = updatedHost
saveSavedHosts()
}
}
func loadSavedHosts() {
let decoder = JSONDecoder()
guard let data = userDefaults.data(forKey: "savedHosts") else { return }

View File

@@ -17,7 +17,7 @@ class SSHHandler: ObservableObject {
@Published var connected: Bool = false
@Published var authorized: Bool = false
@Published var testSuceeded: Bool = false
@Published var testSuceeded: Bool? = nil
@Published var host: Host
@Published var terminal: String = ""
@@ -86,8 +86,9 @@ class SSHHandler: ObservableObject {
ssh_free(session)
withAnimation { authorized = false }
withAnimation { connected = false }
withAnimation { testSuceeded = nil }
session = nil
host.key = nil
// host.key = nil
}
func testExec() {

View File

@@ -26,103 +26,96 @@ struct ConnectionView: View {
var body: some View {
NavigationStack {
List {
HStack {
TextField("", text: $pubkeyStr, prompt: Text("Public Key"))
.onSubmit {
pubkey = Data(pubkeyStr.utf8)
}
Button() {
pubPickerPresented.toggle()
} label: {
Image(systemName: "folder")
Section {
HStack {
Text(handler.connected ? "connected" : "not connected")
.modifier(foregroundColorStyle(handler.connected ? .green : .red))
Text(handler.authorized ? "authorized" : "unauthorized")
.modifier(foregroundColorStyle(handler.authorized ? .green : .red))
}
.buttonStyle(.plain)
.fileImporter(isPresented: $pubPickerPresented, allowedContentTypes: [.item, .content, .data]) { (Result) in
do {
let fileURL = try Result.get()
pubkey = try! Data(contentsOf: fileURL)
print(fileURL)
} catch {
print(error.localizedDescription)
}
}
}
HStack {
TextField("", text: $privkeyStr, prompt: Text("Private Key"))
.onSubmit {
privkey = Data(privkeyStr.utf8)
}
Button() {
privPickerPresented.toggle()
} label: {
Image(systemName: "folder")
}
.buttonStyle(.plain)
.fileImporter(isPresented: $privPickerPresented, allowedContentTypes: [.item, .content, .data]) { (Result) in
do {
let fileURL = try Result.get()
privkey = try! Data(contentsOf: fileURL)
print(fileURL)
} catch {
print(error.localizedDescription)
}
}
}
TextField("", text: $passphrase)
HStack {
Text(handler.connected ? "connected" : "not connected")
.modifier(foregroundColorStyle(handler.connected ? .green : .red))
TextField("address", text: $handler.host.address)
.textFieldStyle(.roundedBorder)
Text(handler.authorized ? "authorized" : "unauthorized")
.modifier(foregroundColorStyle(handler.authorized ? .green : .red))
TextField(
"port",
text: Binding(
get: { String(handler.host.port) },
set: {
if let input = Int($0) {
handler.host.port = input
}
}
)
)
.keyboardType(.numberPad)
.textFieldStyle(.roundedBorder)
}
Section {
TextField("Username", text: $handler.host.username)
.textFieldStyle(.roundedBorder)
SecureField("Password", text: $handler.host.password)
.textFieldStyle(.roundedBorder)
HStack {
TextField("", text: $pubkeyStr, prompt: Text("Public Key"))
.onSubmit {
pubkey = Data(pubkeyStr.utf8)
}
Button() {
pubPickerPresented.toggle()
} label: {
Image(systemName: "folder")
}
.buttonStyle(.plain)
.fileImporter(isPresented: $pubPickerPresented, allowedContentTypes: [.item, .content, .data]) { (Result) in
do {
let fileURL = try Result.get()
pubkey = try! Data(contentsOf: fileURL)
print(fileURL)
} catch {
print(error.localizedDescription)
}
}
}
HStack {
TextField("", text: $privkeyStr, prompt: Text("Private Key"))
.onSubmit {
privkey = Data(privkeyStr.utf8)
}
Button() {
privPickerPresented.toggle()
} label: {
Image(systemName: "folder")
}
.buttonStyle(.plain)
.fileImporter(isPresented: $privPickerPresented, allowedContentTypes: [.item, .content, .data]) { (Result) in
do {
let fileURL = try Result.get()
privkey = try! Data(contentsOf: fileURL)
print(fileURL)
} catch {
print(error.localizedDescription)
}
}
}
TextField("", text: $passphrase, prompt: Text("Passphrase (Optional)"))
}
if handler.host.key != nil {
Text("Hostkey: \(handler.host.key!.base64EncodedString())")
}
TextField("address", text: $handler.host.address)
.textFieldStyle(.roundedBorder)
TextField(
"port",
text: Binding(
get: { String(handler.host.port) },
set: { handler.host.port = Int($0) ?? 22} )
)
.keyboardType(.numberPad)
.textFieldStyle(.roundedBorder)
TextField("username", text: $handler.host.username)
.textFieldStyle(.roundedBorder)
SecureField("password", text: $handler.host.password)
.textFieldStyle(.roundedBorder)
if handler.connected {
Button() {
handler.disconnect()
} label: {
Label("Disconnect", systemImage: "xmark.app.fill")
}
} else {
Button() {
handler.connect()
if pubkey != nil && privkey != nil {
handler.authWithPubkey(pub: pubkey!, priv: privkey!, pass: passphrase)
} else {
let _ = handler.authWithPw()
.onChange(of: handler.host.key) { _ in
guard let matchedIndex = hostsManager.getHostIndexMatching(handler.host) else { return }
guard handler.host.key == hostsManager.savedHosts[matchedIndex].key else {
let hostkeyBefore = hostsManager.savedHosts[matchedIndex].key
let currentHostkey = handler.host.key
print("hiiii")
fatalError()
}
}
handler.openShell()
} label: {
Label("Connect", systemImage: "powerplug.portrait")
}
.disabled(
pubkey == nil && privkey == nil &&
handler.host.username.isEmpty && handler.host.password.isEmpty
)
}
NavigationLink() {
@@ -135,21 +128,46 @@ struct ConnectionView: View {
Button() {
withAnimation { handler.testExec() }
} label: {
if handler.testSuceeded {
Image(systemName: handler.testSuceeded ? "checkmark.circle" : "xmark.circle")
.modifier(foregroundColorStyle(handler.testSuceeded ? .green : .red))
if let testResult = handler.testSuceeded {
Image(systemName: testResult ? "checkmark.circle" : "xmark.circle")
.modifier(foregroundColorStyle(testResult ? .green : .red))
} else {
Label("Test Connection", systemImage: "checkmark")
}
}
// .disabled(!(handler.connected && handler.authorized))
}
.transition(.opacity)
.toolbar {
ToolbarItem() {
if handler.connected {
Button() {
handler.disconnect()
} label: {
Label("Disconnect", systemImage: "xmark.app.fill")
}
} else {
Button() {
handler.connect()
if pubkey != nil && privkey != nil {
handler.authWithPubkey(pub: pubkey!, priv: privkey!, pass: passphrase)
} else {
let _ = handler.authWithPw()
}
handler.openShell()
} label: {
Label("Connect", systemImage: "powerplug.portrait")
}
.disabled(
pubkey == nil && privkey == nil &&
handler.host.username.isEmpty && handler.host.password.isEmpty
)
}
}
}
}
.onDisappear {
if let index = hostsManager.savedHosts.firstIndex(where: { $0.id == handler.host.id }) {
hostsManager.savedHosts[index] = handler.host
hostsManager.saveSavedHosts()
if hostsManager.getHostMatching(handler.host) != handler.host {
hostsManager.updateHost(handler.host)
}
}
}

View File

@@ -25,25 +25,20 @@ struct HostsView: View {
.buttonStyle(.borderedProminent)
}
ForEach(hostsManager.savedHosts) { host in
HStack {
NavigationLink() {
ConnectionView(
handler: SSHHandler(host: host),
keyManager: keyManager,
hostsManager: hostsManager
)
} label: {
if host.address.isEmpty {
Text(host.id.uuidString)
} else {
Text(host.address)
}
NavigationLink() {
ConnectionView(
handler: SSHHandler(host: host),
keyManager: keyManager,
hostsManager: hostsManager
)
} label: {
Image(systemName: "info.circle")
}
.onChange(of: host) { _ in
hostsManager.saveSavedHosts()
}
}
.animation(.default, value: host)
.swipeActions(edge: .trailing) {
Button(role: .destructive) {
if let index = hostsManager.savedHosts.firstIndex(where: { $0.id == host.id }) {
@@ -56,22 +51,21 @@ struct HostsView: View {
}
}
}
.transition(.scale)
.transition(.opacity)
.toolbar {
ToolbarItem(placement: .confirmationAction) {
if !hostsManager.savedHosts.isEmpty {
NavigationLink {
ConnectionView(
handler: SSHHandler(host: hostsManager.savedHosts.last!),
keyManager: keyManager,
hostsManager: hostsManager
)
.onAppear() {
withAnimation { hostsManager.savedHosts.append(Host.blank) }
}
} label: {
Label("Add", systemImage: "plus")
let host = Host.blank
NavigationLink {
ConnectionView(
handler: SSHHandler(host: host),
keyManager: keyManager,
hostsManager: hostsManager
)
.task(priority: .userInitiated) {
withAnimation { hostsManager.savedHosts.append(host) }
}
} label: {
Label("Add", systemImage: "plus")
}
}
}