Skip to content

Commit

Permalink
remote: ssh jump / proxy thebaselab#976
Browse files Browse the repository at this point in the history
  • Loading branch information
bummoblizard committed Jun 20, 2024
1 parent 1cc491e commit 21ed054
Show file tree
Hide file tree
Showing 18 changed files with 709 additions and 305 deletions.
6 changes: 0 additions & 6 deletions Code.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@
94196964280316C7008AAEB2 /* CloudCodeExecutionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94A77818257BC332008FE7B2 /* CloudCodeExecutionManager.swift */; };
94196965280316C7008AAEB2 /* String+toCString.swift in Sources */ = {isa = PBXBuildFile; fileRef = 949B3CC425DEA89A00BC83B5 /* String+toCString.swift */; };
94196967280316C7008AAEB2 /* Executor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 948D12212583F2A5008F877A /* Executor.swift */; };
94196968280316C7008AAEB2 /* RemoteAuthView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9437155A26C3C745000376FB /* RemoteAuthView.swift */; };
94196969280316C7008AAEB2 /* CodeApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 944EEBF32563C381009D77FE /* CodeApp.swift */; };
9419696A280316C7008AAEB2 /* openFilesApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94A777DB257B8C99008FE7B2 /* openFilesApp.swift */; };
9419696B280316C7008AAEB2 /* View+If.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94B3DDF9260526D200C4F2B1 /* View+If.swift */; };
Expand Down Expand Up @@ -473,7 +472,6 @@
94369B1325E3DE02008419A0 /* NodeRunner.mm in Sources */ = {isa = PBXBuildFile; fileRef = 94369B1225E3DE02008419A0 /* NodeRunner.mm */; };
94369B4A25EAB262008419A0 /* npm.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 94369B4925EAB175008419A0 /* npm.bundle */; };
9437153F26BF9FC3000376FB /* RemoteContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9437153E26BF9FC3000376FB /* RemoteContainer.swift */; };
9437155B26C3C745000376FB /* RemoteAuthView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9437155A26C3C745000376FB /* RemoteAuthView.swift */; };
9438C9A225CBD25F00335E82 /* EditorKeyboardToolBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9438C9A125CBD25F00335E82 /* EditorKeyboardToolBar.swift */; };
9441129D28217D6A00A8F1D7 /* TerminalProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9441129C28217D6A00A8F1D7 /* TerminalProvider.swift */; };
9441129E2821816700A8F1D7 /* TerminalProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9441129C28217D6A00A8F1D7 /* TerminalProvider.swift */; };
Expand Down Expand Up @@ -1734,7 +1732,6 @@
94369B1B25E3EDFC008419A0 /* extension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = extension.entitlements; sourceTree = "<group>"; };
94369B4925EAB175008419A0 /* npm.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = npm.bundle; sourceTree = "<group>"; };
9437153E26BF9FC3000376FB /* RemoteContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteContainer.swift; sourceTree = "<group>"; };
9437155A26C3C745000376FB /* RemoteAuthView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteAuthView.swift; sourceTree = "<group>"; };
9438C9A125CBD25F00335E82 /* EditorKeyboardToolBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditorKeyboardToolBar.swift; sourceTree = "<group>"; };
9441129C28217D6A00A8F1D7 /* TerminalProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalProvider.swift; sourceTree = "<group>"; };
944112A0282181E500A8F1D7 /* SFTPTerminalServiceProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SFTPTerminalServiceProvider.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2902,7 +2899,6 @@
94A777F2257B91CA008FE7B2 /* RemoteImage.swift */,
94A045F92804842500182275 /* RemoteConnectedSection.swift */,
94A045F3280481A900182275 /* RemoteTypeLabel.swift */,
9437155A26C3C745000376FB /* RemoteAuthView.swift */,
94A045ED280480E800182275 /* RemoteListSection.swift */,
94A045F62804822400182275 /* RemoteHostCell.swift */,
94A045F02804816000182275 /* RemoteCreateSection.swift */,
Expand Down Expand Up @@ -3591,7 +3587,6 @@
94795C4929314A0A0057C12F /* CompactSidebar.swift in Sources */,
94BEF1322B74AEBD003BBF5D /* UIFont+YYAdd.m in Sources */,
94196967280316C7008AAEB2 /* Executor.swift in Sources */,
94196968280316C7008AAEB2 /* RemoteAuthView.swift in Sources */,
94196969280316C7008AAEB2 /* CodeApp.swift in Sources */,
9FC673852AA068EE00346FD7 /* PortForwardServiceProvider.swift in Sources */,
9419696A280316C7008AAEB2 /* openFilesApp.swift in Sources */,
Expand Down Expand Up @@ -3774,7 +3769,6 @@
94795C4829314A0A0057C12F /* CompactSidebar.swift in Sources */,
94BEF1312B74AEBD003BBF5D /* UIFont+YYAdd.m in Sources */,
948D12222583F2A5008F877A /* Executor.swift in Sources */,
9437155B26C3C745000376FB /* RemoteAuthView.swift in Sources */,
944EEBF42563C381009D77FE /* CodeApp.swift in Sources */,
9FC673842AA068EE00346FD7 /* PortForwardServiceProvider.swift in Sources */,
94A777DC257B8C99008FE7B2 /* openFilesApp.swift in Sources */,
Expand Down
29 changes: 28 additions & 1 deletion CodeApp/Containers/MainScene.swift
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ struct MainScene: View {
.environmentObject(App.safariManager)
.environmentObject(App.directoryPickerManager)
.environmentObject(App.createFileSheetManager)
.environmentObject(App.authenticationRequestManager)
.onAppear {
restoreSceneState()
App.extensionManager.initializeExtensions(app: App)
Expand Down Expand Up @@ -134,6 +135,7 @@ private struct MainView: View {
@EnvironmentObject var directoryPickerManager: DirectoryPickerManager
@EnvironmentObject var createFileSheetManager: CreateFileSheetManager
@EnvironmentObject var themeManager: ThemeManager
@EnvironmentObject var authenticationRequestManager: AuthenticationRequestManager

@Environment(\.horizontalSizeClass) var horizontalSizeClass
@Environment(\.colorScheme) var colorScheme: ColorScheme
Expand Down Expand Up @@ -243,7 +245,6 @@ private struct MainView: View {

changeLogLastReadVersion = appVersion
}

.alert(
alertManager.title, isPresented: $alertManager.isShowingAlert,
actions: {
Expand All @@ -257,6 +258,32 @@ private struct MainView: View {
}
}
)
.alert(
authenticationRequestManager.title,
isPresented: $authenticationRequestManager.isShowingAlert,
actions: {
TextField(
authenticationRequestManager.usernameTitleKey ?? "common.username",
text: $authenticationRequestManager.username
)
.textContentType(.username)
.disableAutocorrection(true)
.autocapitalization(.none)

SecureField(
authenticationRequestManager.passwordTitleKey ?? "common.password",
text: $authenticationRequestManager.password
)
.textContentType(.password)
.disableAutocorrection(true)
.autocapitalization(.none)

Button(
"common.cancel", role: .cancel,
action: authenticationRequestManager.callbackOnCancel)
Button("common.continue", action: authenticationRequestManager.callback)
}
)
.sheet(isPresented: $safariManager.showsSafari) {
if let url = safariManager.urlToVisit {
SafariView(url: url)
Expand Down
211 changes: 167 additions & 44 deletions CodeApp/Containers/RemoteContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ import SwiftUI
struct RemoteContainer: View {

@EnvironmentObject var App: MainApp
@EnvironmentObject var authenticationRequestManager: AuthenticationRequestManager
@EnvironmentObject var alertManager: AlertManager

@State var hosts: [RemoteHost] = []

func onSaveCredentialsForHost(for host: RemoteHost, cred: URLCredential) throws {
Expand All @@ -38,15 +41,31 @@ struct RemoteContainer: View {
}
}

func onRemoveHost(host: RemoteHost) {
_ = KeychainAccessor.shared.removeCredentials(for: host.url)
if let keyChainId = host.privateKeyContentKeychainID {
_ = KeychainAccessor.shared.removeObjectForKey(for: keyChainId)
}
func onRemoveHost(host: RemoteHost, confirm: Bool = false) {
if !confirm
&& UserDefaults.standard.remoteHosts.contains(where: { $0.jumpServerUrl == host.url })
{
alertManager.showAlert(
title: "remote.confirm_delete_are_you_sure_to_delete",
message: "remote.one_or_more_hosts_use_this_host_as_jump_proxy",
content: AnyView(
Group {
Button("common.delete", role: .destructive) {
onRemoveHost(host: host, confirm: true)
}
Button("common.cancel", role: .cancel) {}
}
))
} else {
_ = KeychainAccessor.shared.removeCredentials(for: host.url)
if let keyChainId = host.privateKeyContentKeychainID {
_ = KeychainAccessor.shared.removeObjectForKey(for: keyChainId)
}

DispatchQueue.main.async {
hosts.removeAll(where: { $0.url == host.url })
UserDefaults.standard.remoteHosts = hosts
DispatchQueue.main.async {
hosts.removeAll(where: { $0.url == host.url })
UserDefaults.standard.remoteHosts = hosts
}
}
}

Expand All @@ -58,44 +77,79 @@ struct RemoteContainer: View {
UserDefaults.standard.remoteHosts = hosts
}

func onConnectToHost(host: RemoteHost, onRequestCredentials: () -> Void) async throws {
private func requestManualAuthenticationForHost(host: RemoteHost) async throws -> URLCredential
{
let hostPasswordPair = try await authenticationRequestManager.requestPasswordAuthentication(
title: "remote.credentials_for \(host.url)",
usernameTitleKey: "common.username",
passwordTitleKey: (host.useKeyAuth || host.privateKeyContentKeychainID != nil
|| host.privateKeyPath != nil)
? "remote.passphrase_for_private_key" : "common.password"
)
return URLCredential(
user: hostPasswordPair.0, password: hostPasswordPair.1, persistence: .none)
}

private func requestBiometricAuthenticationForHost(host: RemoteHost) async throws
-> URLCredential
{
guard let hostUrl = URL(string: host.url) else {
throw RemoteHostError.invalidUrl
}

guard KeychainAccessor.shared.hasCredentials(for: host.url) else {
onRequestCredentials()
return
}

let context = LAContext()
context.localizedCancelTitle = "Enter Credentials"
context.localizedCancelTitle = NSLocalizedString("remote.enter_credentials", comment: "")

let biometricAuthSuccess = try? await context.evaluatePolicy(
.deviceOwnerAuthenticationWithBiometrics,
localizedReason: "Authenticate to \(hostUrl.host ?? "server")")

guard biometricAuthSuccess == true else {
onRequestCredentials()
return
guard
try await context.evaluatePolicy(
.deviceOwnerAuthenticationWithBiometrics,
localizedReason: NSLocalizedString(
"remote.authenticate_to \(hostUrl.host ?? "host")", comment: ""))
else {
throw WorkSpaceStorage.FSError.AuthFailure
}

guard let cred = KeychainAccessor.shared.getCredentials(for: host.url) else {
throw WorkSpaceStorage.FSError.AuthFailure
}
return cred
}

try await onConnectToHostWithCredentials(host: host, cred: cred)
private func requestAuthenticationForHost(host: RemoteHost) async throws -> URLCredential {
if KeychainAccessor.shared.hasCredentials(for: host.url) {
do {
return try await requestBiometricAuthenticationForHost(host: host)
} catch {
return try await requestManualAuthenticationForHost(host: host)
}
} else {
return try await requestManualAuthenticationForHost(host: host)
}
}

func onConnectToHostWithCredentials(
host: RemoteHost, cred: URLCredential
) async throws {
guard let hostUrl = URL(string: host.url) else {
throw RemoteHostError.invalidUrl
func onConnectToHost(host: RemoteHost) async throws {
if let jumpServerURL = host.jumpServerUrl {
guard
let jumpHost = UserDefaults.standard.remoteHosts.first(where: {
$0.url == jumpServerURL
})
else {
throw WorkSpaceStorage.FSError.MissingJumpingServer
}
let jumpCred = try await requestAuthenticationForHost(host: jumpHost)
let cred = try await requestAuthenticationForHost(host: host)
try await connectToHostWithCredentialsUsingJumpHost(
host: host, jumpHost: jumpHost, hostCred: cred, jumpCred: jumpCred)
} else {
let cred = try await requestAuthenticationForHost(host: host)
try await onConnectToHostWithCredentials(host: host, cred: cred)
}
}

private func authenticationModeForHost(host: RemoteHost, cred: URLCredential) throws
-> RemoteAuthenticationMode
{
var authenticationMode: RemoteAuthenticationMode

if host.useKeyAuth {
// Legacy in-file id_rsa authentication
authenticationMode = .inFileSSHKey(cred, nil)
Expand All @@ -107,6 +161,88 @@ struct RemoteContainer: View {
} else {
authenticationMode = .plainUsernamePassword(cred)
}
return authenticationMode
}

private func connectionResultHandler(
hostUrl: URL, error: (any Error)?, continuation: CheckedContinuation<Void, Error>
) {
if let error {
DispatchQueue.main.async {
App.notificationManager.showErrorMessage(
error.localizedDescription)
}
continuation.resume(throwing: error)
} else {
DispatchQueue.main.async {
App.loadRepository(url: hostUrl)
App.notificationManager.showInformationMessage(
"remote.connected")
App.terminalInstance.terminalServiceProvider =
App.workSpaceStorage.terminalServiceProvider
}
continuation.resume(returning: ())
}
}

private func connectToHostWithCredentialsUsingJumpHost(
host: RemoteHost,
jumpHost: RemoteHost,
hostCred: URLCredential,
jumpCred: URLCredential
) async throws {
guard let hostUrl = URL(string: host.url),
let jumpServerUrlString = host.jumpServerUrl,
let jumpHostUrl = URL(string: jumpServerUrlString)
else {
throw RemoteHostError.invalidUrl
}

let hostAuthenticationMode = try authenticationModeForHost(host: host, cred: hostCred)
let jumpHostAuthenticationMode = try authenticationModeForHost(
host: jumpHost, cred: jumpCred)

try await App.notificationManager.withAsyncNotification(
title: "remote.connecting",
task: {
try await withCheckedThrowingContinuation {
(continuation: CheckedContinuation<Void, Error>) in
App.workSpaceStorage.connectToServer(
host: hostUrl, authenticationModeForHost: hostAuthenticationMode,
jumpServer: jumpHostUrl,
authenticationModeForJumpServer: jumpHostAuthenticationMode
) {
error in
connectionResultHandler(
hostUrl: hostUrl, error: error, continuation: continuation)
}
}
}
)
}

func onConnectToHostWithCredentials(
host: RemoteHost, cred: URLCredential
) async throws {

if host.jumpServerUrl != nil {
guard
let jumpHost = UserDefaults.standard.remoteHosts.first(where: {
$0.url == host.jumpServerUrl
})
else {
throw WorkSpaceStorage.FSError.MissingJumpingServer
}
let jumpHostCred = try await requestAuthenticationForHost(host: jumpHost)
return try await connectToHostWithCredentialsUsingJumpHost(
host: host, jumpHost: jumpHost, hostCred: cred, jumpCred: jumpHostCred)
}

guard let hostUrl = URL(string: host.url) else {
throw RemoteHostError.invalidUrl
}

let authenticationMode = try authenticationModeForHost(host: host, cred: cred)

try await App.notificationManager.withAsyncNotification(
title: "remote.connecting",
Expand All @@ -117,20 +253,8 @@ struct RemoteContainer: View {
host: hostUrl, authenticationMode: authenticationMode
) {
error in
if let error = error {
DispatchQueue.main.async {
App.notificationManager.showErrorMessage(
error.localizedDescription)
}
continuation.resume(throwing: error)
} else {
App.loadRepository(url: hostUrl)
App.notificationManager.showInformationMessage(
"remote.connected")
App.terminalInstance.terminalServiceProvider =
App.workSpaceStorage.terminalServiceProvider
continuation.resume(returning: ())
}
connectionResultHandler(
hostUrl: hostUrl, error: error, continuation: continuation)
}
}
}
Expand All @@ -145,7 +269,6 @@ struct RemoteContainer: View {
} else {
RemoteListSection(
hosts: hosts, onRemoveHost: onRemoveHost, onConnectToHost: onConnectToHost,
onConnectToHostWithCredentials: onConnectToHostWithCredentials,
onRenameHost: onRenameHost)
RemoteCreateSection(
hosts: hosts,
Expand Down
1 change: 1 addition & 0 deletions CodeApp/Errors/AppError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ enum AppError: String {
case editorIsNotReady = "errors.editor_is_not_ready"
case encodingFailed = "errors.failed_to_save_file.encoding.failed"
case fileModifiedByAnotherProcess = "errors.file_modified_by_another_process"
case operationCancelledByUser = "errors.operation_cancelled_by_user"
}

extension AppError: LocalizedError {
Expand Down
Loading

0 comments on commit 21ed054

Please sign in to comment.