@@ -12,6 +12,9 @@ import SwiftUI
12
12
struct RemoteContainer : View {
13
13
14
14
@EnvironmentObject var App : MainApp
15
+ @EnvironmentObject var authenticationRequestManager : AuthenticationRequestManager
16
+ @EnvironmentObject var alertManager : AlertManager
17
+
15
18
@State var hosts : [ RemoteHost ] = [ ]
16
19
17
20
func onSaveCredentialsForHost( for host: RemoteHost , cred: URLCredential ) throws {
@@ -38,15 +41,31 @@ struct RemoteContainer: View {
38
41
}
39
42
}
40
43
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
+ }
46
64
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
+ }
50
69
}
51
70
}
52
71
@@ -58,44 +77,79 @@ struct RemoteContainer: View {
58
77
UserDefaults . standard. remoteHosts = hosts
59
78
}
60
79
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
+ {
62
96
guard let hostUrl = URL ( string: host. url) else {
63
97
throw RemoteHostError . invalidUrl
64
98
}
65
99
66
- guard KeychainAccessor . shared. hasCredentials ( for: host. url) else {
67
- onRequestCredentials ( )
68
- return
69
- }
70
-
71
100
let context = LAContext ( )
72
- context. localizedCancelTitle = " Enter Credentials "
101
+ context. localizedCancelTitle = NSLocalizedString ( " remote.enter_credentials " , comment : " " )
73
102
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
81
110
}
82
111
83
112
guard let cred = KeychainAccessor . shared. getCredentials ( for: host. url) else {
84
113
throw WorkSpaceStorage . FSError. AuthFailure
85
114
}
115
+ return cred
116
+ }
86
117
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
+ }
88
128
}
89
129
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)
95
146
}
147
+ }
96
148
149
+ private func authenticationModeForHost( host: RemoteHost , cred: URLCredential ) throws
150
+ -> RemoteAuthenticationMode
151
+ {
97
152
var authenticationMode : RemoteAuthenticationMode
98
-
99
153
if host. useKeyAuth {
100
154
// Legacy in-file id_rsa authentication
101
155
authenticationMode = . inFileSSHKey( cred, nil )
@@ -107,6 +161,88 @@ struct RemoteContainer: View {
107
161
} else {
108
162
authenticationMode = . plainUsernamePassword( cred)
109
163
}
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)
110
246
111
247
try await App . notificationManager. withAsyncNotification (
112
248
title: " remote.connecting " ,
@@ -117,20 +253,8 @@ struct RemoteContainer: View {
117
253
host: hostUrl, authenticationMode: authenticationMode
118
254
) {
119
255
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)
134
258
}
135
259
}
136
260
}
@@ -145,7 +269,6 @@ struct RemoteContainer: View {
145
269
} else {
146
270
RemoteListSection (
147
271
hosts: hosts, onRemoveHost: onRemoveHost, onConnectToHost: onConnectToHost,
148
- onConnectToHostWithCredentials: onConnectToHostWithCredentials,
149
272
onRenameHost: onRenameHost)
150
273
RemoteCreateSection (
151
274
hosts: hosts,
0 commit comments