@@ -12,6 +12,9 @@ import SwiftUI
1212struct RemoteContainer : View {
1313
1414 @EnvironmentObject var App : MainApp
15+ @EnvironmentObject var authenticationRequestManager : AuthenticationRequestManager
16+ @EnvironmentObject var alertManager : AlertManager
17+
1518 @State var hosts : [ RemoteHost ] = [ ]
1619
1720 func onSaveCredentialsForHost( for host: RemoteHost , cred: URLCredential ) throws {
@@ -38,15 +41,31 @@ struct RemoteContainer: View {
3841 }
3942 }
4043
41- func onRemoveHost( host: RemoteHost ) {
42- _ = KeychainAccessor . shared. removeCredentials ( for: host. url)
43- if let keyChainId = host. privateKeyContentKeychainID {
44- _ = KeychainAccessor . shared. removeObjectForKey ( for: keyChainId)
45- }
44+ func onRemoveHost( host: RemoteHost , confirm: Bool = false ) {
45+ if !confirm
46+ && UserDefaults . standard. remoteHosts. contains ( where: { $0. jumpServerUrl == host. url } )
47+ {
48+ alertManager. showAlert (
49+ title: " remote.confirm_delete_are_you_sure_to_delete " ,
50+ message: " remote.one_or_more_hosts_use_this_host_as_jump_proxy " ,
51+ content: AnyView (
52+ Group {
53+ Button ( " common.delete " , role: . destructive) {
54+ onRemoveHost ( host: host, confirm: true )
55+ }
56+ Button ( " common.cancel " , role: . cancel) { }
57+ }
58+ ) )
59+ } else {
60+ _ = KeychainAccessor . shared. removeCredentials ( for: host. url)
61+ if let keyChainId = host. privateKeyContentKeychainID {
62+ _ = KeychainAccessor . shared. removeObjectForKey ( for: keyChainId)
63+ }
4664
47- DispatchQueue . main. async {
48- hosts. removeAll ( where: { $0. url == host. url } )
49- UserDefaults . standard. remoteHosts = hosts
65+ DispatchQueue . main. async {
66+ hosts. removeAll ( where: { $0. url == host. url } )
67+ UserDefaults . standard. remoteHosts = hosts
68+ }
5069 }
5170 }
5271
@@ -58,44 +77,79 @@ struct RemoteContainer: View {
5877 UserDefaults . standard. remoteHosts = hosts
5978 }
6079
61- func onConnectToHost( host: RemoteHost , onRequestCredentials: ( ) -> Void ) async throws {
80+ private func requestManualAuthenticationForHost( host: RemoteHost ) async throws -> URLCredential
81+ {
82+ let hostPasswordPair = try await authenticationRequestManager. requestPasswordAuthentication (
83+ title: " remote.credentials_for \( host. url) " ,
84+ usernameTitleKey: " common.username " ,
85+ passwordTitleKey: ( host. useKeyAuth || host. privateKeyContentKeychainID != nil
86+ || host. privateKeyPath != nil )
87+ ? " remote.passphrase_for_private_key " : " common.password "
88+ )
89+ return URLCredential (
90+ user: hostPasswordPair. 0 , password: hostPasswordPair. 1 , persistence: . none)
91+ }
92+
93+ private func requestBiometricAuthenticationForHost( host: RemoteHost ) async throws
94+ -> URLCredential
95+ {
6296 guard let hostUrl = URL ( string: host. url) else {
6397 throw RemoteHostError . invalidUrl
6498 }
6599
66- guard KeychainAccessor . shared. hasCredentials ( for: host. url) else {
67- onRequestCredentials ( )
68- return
69- }
70-
71100 let context = LAContext ( )
72- context. localizedCancelTitle = " Enter Credentials "
101+ context. localizedCancelTitle = NSLocalizedString ( " remote.enter_credentials " , comment : " " )
73102
74- let biometricAuthSuccess = try ? await context . evaluatePolicy (
75- . deviceOwnerAuthenticationWithBiometrics ,
76- localizedReason : " Authenticate to \( hostUrl . host ?? " server " ) " )
77-
78- guard biometricAuthSuccess == true else {
79- onRequestCredentials ( )
80- return
103+ guard
104+ try await context . evaluatePolicy (
105+ . deviceOwnerAuthenticationWithBiometrics ,
106+ localizedReason : NSLocalizedString (
107+ " remote.authenticate_to \( hostUrl . host ?? " host " ) " , comment : " " ) )
108+ else {
109+ throw WorkSpaceStorage . FSError . AuthFailure
81110 }
82111
83112 guard let cred = KeychainAccessor . shared. getCredentials ( for: host. url) else {
84113 throw WorkSpaceStorage . FSError. AuthFailure
85114 }
115+ return cred
116+ }
86117
87- try await onConnectToHostWithCredentials ( host: host, cred: cred)
118+ private func requestAuthenticationForHost( host: RemoteHost ) async throws -> URLCredential {
119+ if KeychainAccessor . shared. hasCredentials ( for: host. url) {
120+ do {
121+ return try await requestBiometricAuthenticationForHost ( host: host)
122+ } catch {
123+ return try await requestManualAuthenticationForHost ( host: host)
124+ }
125+ } else {
126+ return try await requestManualAuthenticationForHost ( host: host)
127+ }
88128 }
89129
90- func onConnectToHostWithCredentials(
91- host: RemoteHost , cred: URLCredential
92- ) async throws {
93- guard let hostUrl = URL ( string: host. url) else {
94- throw RemoteHostError . invalidUrl
130+ func onConnectToHost( host: RemoteHost ) async throws {
131+ if let jumpServerURL = host. jumpServerUrl {
132+ guard
133+ let jumpHost = UserDefaults . standard. remoteHosts. first ( where: {
134+ $0. url == jumpServerURL
135+ } )
136+ else {
137+ throw WorkSpaceStorage . FSError. MissingJumpingServer
138+ }
139+ let jumpCred = try await requestAuthenticationForHost ( host: jumpHost)
140+ let cred = try await requestAuthenticationForHost ( host: host)
141+ try await connectToHostWithCredentialsUsingJumpHost (
142+ host: host, jumpHost: jumpHost, hostCred: cred, jumpCred: jumpCred)
143+ } else {
144+ let cred = try await requestAuthenticationForHost ( host: host)
145+ try await onConnectToHostWithCredentials ( host: host, cred: cred)
95146 }
147+ }
96148
149+ private func authenticationModeForHost( host: RemoteHost , cred: URLCredential ) throws
150+ -> RemoteAuthenticationMode
151+ {
97152 var authenticationMode : RemoteAuthenticationMode
98-
99153 if host. useKeyAuth {
100154 // Legacy in-file id_rsa authentication
101155 authenticationMode = . inFileSSHKey( cred, nil )
@@ -107,6 +161,88 @@ struct RemoteContainer: View {
107161 } else {
108162 authenticationMode = . plainUsernamePassword( cred)
109163 }
164+ return authenticationMode
165+ }
166+
167+ private func connectionResultHandler(
168+ hostUrl: URL , error: ( any Error ) ? , continuation: CheckedContinuation < Void , Error >
169+ ) {
170+ if let error {
171+ DispatchQueue . main. async {
172+ App . notificationManager. showErrorMessage (
173+ error. localizedDescription)
174+ }
175+ continuation. resume ( throwing: error)
176+ } else {
177+ DispatchQueue . main. async {
178+ App . loadRepository ( url: hostUrl)
179+ App . notificationManager. showInformationMessage (
180+ " remote.connected " )
181+ App . terminalInstance. terminalServiceProvider =
182+ App . workSpaceStorage. terminalServiceProvider
183+ }
184+ continuation. resume ( returning: ( ) )
185+ }
186+ }
187+
188+ private func connectToHostWithCredentialsUsingJumpHost(
189+ host: RemoteHost ,
190+ jumpHost: RemoteHost ,
191+ hostCred: URLCredential ,
192+ jumpCred: URLCredential
193+ ) async throws {
194+ guard let hostUrl = URL ( string: host. url) ,
195+ let jumpServerUrlString = host. jumpServerUrl,
196+ let jumpHostUrl = URL ( string: jumpServerUrlString)
197+ else {
198+ throw RemoteHostError . invalidUrl
199+ }
200+
201+ let hostAuthenticationMode = try authenticationModeForHost ( host: host, cred: hostCred)
202+ let jumpHostAuthenticationMode = try authenticationModeForHost (
203+ host: jumpHost, cred: jumpCred)
204+
205+ try await App . notificationManager. withAsyncNotification (
206+ title: " remote.connecting " ,
207+ task: {
208+ try await withCheckedThrowingContinuation {
209+ ( continuation: CheckedContinuation < Void , Error > ) in
210+ App . workSpaceStorage. connectToServer (
211+ host: hostUrl, authenticationModeForHost: hostAuthenticationMode,
212+ jumpServer: jumpHostUrl,
213+ authenticationModeForJumpServer: jumpHostAuthenticationMode
214+ ) {
215+ error in
216+ connectionResultHandler (
217+ hostUrl: hostUrl, error: error, continuation: continuation)
218+ }
219+ }
220+ }
221+ )
222+ }
223+
224+ func onConnectToHostWithCredentials(
225+ host: RemoteHost , cred: URLCredential
226+ ) async throws {
227+
228+ if host. jumpServerUrl != nil {
229+ guard
230+ let jumpHost = UserDefaults . standard. remoteHosts. first ( where: {
231+ $0. url == host. jumpServerUrl
232+ } )
233+ else {
234+ throw WorkSpaceStorage . FSError. MissingJumpingServer
235+ }
236+ let jumpHostCred = try await requestAuthenticationForHost ( host: jumpHost)
237+ return try await connectToHostWithCredentialsUsingJumpHost (
238+ host: host, jumpHost: jumpHost, hostCred: cred, jumpCred: jumpHostCred)
239+ }
240+
241+ guard let hostUrl = URL ( string: host. url) else {
242+ throw RemoteHostError . invalidUrl
243+ }
244+
245+ let authenticationMode = try authenticationModeForHost ( host: host, cred: cred)
110246
111247 try await App . notificationManager. withAsyncNotification (
112248 title: " remote.connecting " ,
@@ -117,20 +253,8 @@ struct RemoteContainer: View {
117253 host: hostUrl, authenticationMode: authenticationMode
118254 ) {
119255 error in
120- if let error = error {
121- DispatchQueue . main. async {
122- App . notificationManager. showErrorMessage (
123- error. localizedDescription)
124- }
125- continuation. resume ( throwing: error)
126- } else {
127- App . loadRepository ( url: hostUrl)
128- App . notificationManager. showInformationMessage (
129- " remote.connected " )
130- App . terminalInstance. terminalServiceProvider =
131- App . workSpaceStorage. terminalServiceProvider
132- continuation. resume ( returning: ( ) )
133- }
256+ connectionResultHandler (
257+ hostUrl: hostUrl, error: error, continuation: continuation)
134258 }
135259 }
136260 }
@@ -145,7 +269,6 @@ struct RemoteContainer: View {
145269 } else {
146270 RemoteListSection (
147271 hosts: hosts, onRemoveHost: onRemoveHost, onConnectToHost: onConnectToHost,
148- onConnectToHostWithCredentials: onConnectToHostWithCredentials,
149272 onRenameHost: onRenameHost)
150273 RemoteCreateSection (
151274 hosts: hosts,
0 commit comments