Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SSH Jump / Proxy #1096

Merged
merged 1 commit into from
Jun 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading