mirror of
https://github.com/neon443/ShhShell.git
synced 2026-03-11 13:26:16 +00:00
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:
@@ -16,6 +16,30 @@ class HostsManager: ObservableObject {
|
|||||||
loadSavedHosts()
|
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() {
|
func loadSavedHosts() {
|
||||||
let decoder = JSONDecoder()
|
let decoder = JSONDecoder()
|
||||||
guard let data = userDefaults.data(forKey: "savedHosts") else { return }
|
guard let data = userDefaults.data(forKey: "savedHosts") else { return }
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ class SSHHandler: ObservableObject {
|
|||||||
|
|
||||||
@Published var connected: Bool = false
|
@Published var connected: Bool = false
|
||||||
@Published var authorized: Bool = false
|
@Published var authorized: Bool = false
|
||||||
@Published var testSuceeded: Bool = false
|
@Published var testSuceeded: Bool? = nil
|
||||||
|
|
||||||
@Published var host: Host
|
@Published var host: Host
|
||||||
@Published var terminal: String = ""
|
@Published var terminal: String = ""
|
||||||
@@ -86,8 +86,9 @@ class SSHHandler: ObservableObject {
|
|||||||
ssh_free(session)
|
ssh_free(session)
|
||||||
withAnimation { authorized = false }
|
withAnimation { authorized = false }
|
||||||
withAnimation { connected = false }
|
withAnimation { connected = false }
|
||||||
|
withAnimation { testSuceeded = nil }
|
||||||
session = nil
|
session = nil
|
||||||
host.key = nil
|
// host.key = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func testExec() {
|
func testExec() {
|
||||||
|
|||||||
@@ -26,103 +26,96 @@ struct ConnectionView: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
List {
|
List {
|
||||||
HStack {
|
Section {
|
||||||
TextField("", text: $pubkeyStr, prompt: Text("Public Key"))
|
HStack {
|
||||||
.onSubmit {
|
Text(handler.connected ? "connected" : "not connected")
|
||||||
pubkey = Data(pubkeyStr.utf8)
|
.modifier(foregroundColorStyle(handler.connected ? .green : .red))
|
||||||
}
|
|
||||||
Button() {
|
Text(handler.authorized ? "authorized" : "unauthorized")
|
||||||
pubPickerPresented.toggle()
|
.modifier(foregroundColorStyle(handler.authorized ? .green : .red))
|
||||||
} label: {
|
|
||||||
Image(systemName: "folder")
|
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
TextField("address", text: $handler.host.address)
|
||||||
.fileImporter(isPresented: $pubPickerPresented, allowedContentTypes: [.item, .content, .data]) { (Result) in
|
.textFieldStyle(.roundedBorder)
|
||||||
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))
|
|
||||||
|
|
||||||
Text(handler.authorized ? "authorized" : "unauthorized")
|
TextField(
|
||||||
.modifier(foregroundColorStyle(handler.authorized ? .green : .red))
|
"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 {
|
if handler.host.key != nil {
|
||||||
Text("Hostkey: \(handler.host.key!.base64EncodedString())")
|
Text("Hostkey: \(handler.host.key!.base64EncodedString())")
|
||||||
}
|
.onChange(of: handler.host.key) { _ in
|
||||||
|
guard let matchedIndex = hostsManager.getHostIndexMatching(handler.host) else { return }
|
||||||
TextField("address", text: $handler.host.address)
|
guard handler.host.key == hostsManager.savedHosts[matchedIndex].key else {
|
||||||
.textFieldStyle(.roundedBorder)
|
let hostkeyBefore = hostsManager.savedHosts[matchedIndex].key
|
||||||
|
let currentHostkey = handler.host.key
|
||||||
TextField(
|
print("hiiii")
|
||||||
"port",
|
fatalError()
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
handler.openShell()
|
|
||||||
} label: {
|
|
||||||
Label("Connect", systemImage: "powerplug.portrait")
|
|
||||||
}
|
|
||||||
.disabled(
|
|
||||||
pubkey == nil && privkey == nil &&
|
|
||||||
handler.host.username.isEmpty && handler.host.password.isEmpty
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
NavigationLink() {
|
NavigationLink() {
|
||||||
@@ -135,21 +128,46 @@ struct ConnectionView: View {
|
|||||||
Button() {
|
Button() {
|
||||||
withAnimation { handler.testExec() }
|
withAnimation { handler.testExec() }
|
||||||
} label: {
|
} label: {
|
||||||
if handler.testSuceeded {
|
if let testResult = handler.testSuceeded {
|
||||||
Image(systemName: handler.testSuceeded ? "checkmark.circle" : "xmark.circle")
|
Image(systemName: testResult ? "checkmark.circle" : "xmark.circle")
|
||||||
.modifier(foregroundColorStyle(handler.testSuceeded ? .green : .red))
|
.modifier(foregroundColorStyle(testResult ? .green : .red))
|
||||||
} else {
|
} else {
|
||||||
Label("Test Connection", systemImage: "checkmark")
|
Label("Test Connection", systemImage: "checkmark")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// .disabled(!(handler.connected && handler.authorized))
|
|
||||||
}
|
}
|
||||||
.transition(.opacity)
|
.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 {
|
.onDisappear {
|
||||||
if let index = hostsManager.savedHosts.firstIndex(where: { $0.id == handler.host.id }) {
|
if hostsManager.getHostMatching(handler.host) != handler.host {
|
||||||
hostsManager.savedHosts[index] = handler.host
|
hostsManager.updateHost(handler.host)
|
||||||
hostsManager.saveSavedHosts()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,25 +25,20 @@ struct HostsView: View {
|
|||||||
.buttonStyle(.borderedProminent)
|
.buttonStyle(.borderedProminent)
|
||||||
}
|
}
|
||||||
ForEach(hostsManager.savedHosts) { host in
|
ForEach(hostsManager.savedHosts) { host in
|
||||||
HStack {
|
NavigationLink() {
|
||||||
|
ConnectionView(
|
||||||
|
handler: SSHHandler(host: host),
|
||||||
|
keyManager: keyManager,
|
||||||
|
hostsManager: hostsManager
|
||||||
|
)
|
||||||
|
} label: {
|
||||||
if host.address.isEmpty {
|
if host.address.isEmpty {
|
||||||
Text(host.id.uuidString)
|
Text(host.id.uuidString)
|
||||||
} else {
|
} else {
|
||||||
Text(host.address)
|
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) {
|
.swipeActions(edge: .trailing) {
|
||||||
Button(role: .destructive) {
|
Button(role: .destructive) {
|
||||||
if let index = hostsManager.savedHosts.firstIndex(where: { $0.id == host.id }) {
|
if let index = hostsManager.savedHosts.firstIndex(where: { $0.id == host.id }) {
|
||||||
@@ -56,22 +51,21 @@ struct HostsView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.transition(.scale)
|
.transition(.opacity)
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem(placement: .confirmationAction) {
|
ToolbarItem(placement: .confirmationAction) {
|
||||||
if !hostsManager.savedHosts.isEmpty {
|
let host = Host.blank
|
||||||
NavigationLink {
|
NavigationLink {
|
||||||
ConnectionView(
|
ConnectionView(
|
||||||
handler: SSHHandler(host: hostsManager.savedHosts.last!),
|
handler: SSHHandler(host: host),
|
||||||
keyManager: keyManager,
|
keyManager: keyManager,
|
||||||
hostsManager: hostsManager
|
hostsManager: hostsManager
|
||||||
)
|
)
|
||||||
.onAppear() {
|
.task(priority: .userInitiated) {
|
||||||
withAnimation { hostsManager.savedHosts.append(Host.blank) }
|
withAnimation { hostsManager.savedHosts.append(host) }
|
||||||
}
|
|
||||||
} label: {
|
|
||||||
Label("Add", systemImage: "plus")
|
|
||||||
}
|
}
|
||||||
|
} label: {
|
||||||
|
Label("Add", systemImage: "plus")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user