@@ -33,7 +33,10 @@ enum GenSSHClientStatus { socket, key, pwd }
3333String getPrivateKey (String id) {
3434 final pki = Stores .key.fetchOne (id);
3535 if (pki == null ) {
36- throw SSHErr (type: SSHErrType .noPrivateKey, message: l10n.privateKeyNotFoundFmt (id));
36+ throw SSHErr (
37+ type: SSHErrType .noPrivateKey,
38+ message: l10n.privateKeyNotFoundFmt (id),
39+ );
3740 }
3841 return pki.key;
3942}
@@ -47,6 +50,12 @@ Future<SSHClient> genClient(
4750
4851 /// Only pass this param if using multi-threading and key login
4952 String ? jumpPrivateKey,
53+
54+ /// Prefer this map in isolate mode, fallback to [Stores.key] otherwise.
55+ Map <String , String >? privateKeysByKeyId,
56+
57+ /// Prefer this map in isolate mode, fallback to [Stores.server] otherwise.
58+ Map <String , Spi >? jumpSpisById,
5059 Duration timeout = const Duration (seconds: 5 ),
5160
5261 /// [Spi] of the jump server
@@ -59,10 +68,23 @@ Future<SSHClient> genClient(
5968 Map <String , String >? knownHostFingerprints,
6069 void Function (String storageKey, String fingerprintHex)? onHostKeyAccepted,
6170 Future <bool > Function (HostKeyPromptInfo info)? onHostKeyPrompt,
71+ Set <String >? visitedServerIds,
6272}) async {
73+ final chainVisitedServerIds = visitedServerIds ?? < String > {};
74+ final currentServerId = _hostIdentifier (spi);
75+ if (! chainVisitedServerIds.add (currentServerId)) {
76+ throw SSHErr (
77+ type: SSHErrType .connect,
78+ message:
79+ 'Invalid jump chain: cycle detected at ${spi .name } ($currentServerId )' ,
80+ );
81+ }
82+
6383 onStatus? .call (GenSSHClientStatus .socket);
6484
65- final hostKeyCache = Map <String , String >.from (knownHostFingerprints ?? _loadKnownHostFingerprints ());
85+ final hostKeyCache = Map <String , String >.from (
86+ knownHostFingerprints ?? _loadKnownHostFingerprints (),
87+ );
6688 final hostKeyPersist = onHostKeyAccepted ?? _persistHostKeyFingerprint;
6789 final hostKeyPrompt = onHostKeyPrompt ?? _defaultHostKeyPrompt;
6890
@@ -74,16 +96,33 @@ Future<SSHClient> genClient(
7496 // Multi-thread or key login
7597 if (jumpSpi != null ) return jumpSpi;
7698 // Main thread
77- if (spi.jumpId != null ) return Stores .server.box.get (spi.jumpId);
99+ final jumpId = spi.jumpId;
100+ if (jumpId != null ) {
101+ return jumpSpisById? [jumpId] ?? Stores .server.box.get (jumpId);
102+ }
78103 }();
79104 if (jumpSpi_ != null ) {
105+ String ? nextJumpPrivateKey;
106+ final jumpSpiKeyId = jumpSpi_.keyId;
107+ if (jumpSpi != null &&
108+ jumpSpi.id == jumpSpi_.id &&
109+ jumpPrivateKey != null ) {
110+ // Isolate mode may preload first-hop key and pass it via [jumpPrivateKey].
111+ nextJumpPrivateKey = jumpPrivateKey;
112+ } else if (jumpSpiKeyId != null ) {
113+ nextJumpPrivateKey = privateKeysByKeyId? [jumpSpiKeyId];
114+ }
115+
80116 final jumpClient = await genClient (
81117 jumpSpi_,
82- privateKey: jumpPrivateKey,
118+ privateKey: nextJumpPrivateKey,
119+ privateKeysByKeyId: privateKeysByKeyId,
120+ jumpSpisById: jumpSpisById,
83121 timeout: timeout,
84122 knownHostFingerprints: hostKeyCache,
85123 onHostKeyAccepted: hostKeyPersist,
86- onHostKeyPrompt: onHostKeyPrompt,
124+ onHostKeyPrompt: hostKeyPrompt,
125+ visitedServerIds: chainVisitedServerIds,
87126 );
88127
89128 return await jumpClient.forwardLocal (spi.ip, spi.port);
@@ -126,7 +165,7 @@ Future<SSHClient> genClient(
126165 // printTrace: debugPrint,
127166 );
128167 }
129- privateKey ?? = getPrivateKey (keyId);
168+ privateKey ?? = privateKeysByKeyId ? [keyId] ?? getPrivateKey (keyId);
130169
131170 onStatus? .call (GenSSHClientStatus .key);
132171 return SSHClient (
@@ -141,7 +180,8 @@ Future<SSHClient> genClient(
141180 );
142181}
143182
144- typedef _HostKeyPersistCallback = void Function (String storageKey, String fingerprintHex);
183+ typedef _HostKeyPersistCallback =
184+ void Function (String storageKey, String fingerprintHex);
145185
146186class HostKeyPromptInfo {
147187 HostKeyPromptInfo ({
@@ -191,7 +231,9 @@ class _HostKeyVerifier {
191231 ),
192232 );
193233 if (! accepted) {
194- Loggers .app.warning ('User rejected new SSH host key for ${spi .name } ($keyType ).' );
234+ Loggers .app.warning (
235+ 'User rejected new SSH host key for ${spi .name } ($keyType ).' ,
236+ );
195237 return false ;
196238 }
197239 _cache[storageKey] = fingerprintHex;
@@ -224,7 +266,9 @@ class _HostKeyVerifier {
224266
225267 _cache[storageKey] = fingerprintHex;
226268 persistCallback? .call (storageKey, fingerprintHex);
227- Loggers .app.warning ('Updated stored SSH host key for ${spi .name } ($keyType ) after user confirmation.' );
269+ Loggers .app.warning (
270+ 'Updated stored SSH host key for ${spi .name } ($keyType ) after user confirmation.' ,
271+ );
228272 return true ;
229273 }
230274}
@@ -257,7 +301,9 @@ void _persistHostKeyFingerprint(String storageKey, String fingerprintHex) {
257301Future <bool > _defaultHostKeyPrompt (HostKeyPromptInfo info) async {
258302 final ctx = AppNavigator .context;
259303 if (ctx == null ) {
260- Loggers .app.warning ('Host key prompt skipped: navigator context unavailable.' );
304+ Loggers .app.warning (
305+ 'Host key prompt skipped: navigator context unavailable.' ,
306+ );
261307 return false ;
262308 }
263309
@@ -279,10 +325,14 @@ Future<bool> _defaultHostKeyPrompt(HostKeyPromptInfo info) async {
279325 SelectableText ('${libL10n .addr }: $hostLine ' ),
280326 SelectableText ('${l10n .sshHostKeyType }: ${info .keyType }' ),
281327 SelectableText (l10n.sshHostKeyFingerprintMd5Hex (info.fingerprintHex)),
282- SelectableText (l10n.sshHostKeyFingerprintMd5Base64 (info.fingerprintBase64)),
328+ SelectableText (
329+ l10n.sshHostKeyFingerprintMd5Base64 (info.fingerprintBase64),
330+ ),
283331 if (info.previousFingerprintHex != null ) ...[
284332 const SizedBox (height: 12 ),
285- SelectableText (l10n.sshHostKeyStoredFingerprint (info.previousFingerprintHex! )),
333+ SelectableText (
334+ l10n.sshHostKeyStoredFingerprint (info.previousFingerprintHex! ),
335+ ),
286336 ],
287337 ],
288338 ),
@@ -299,18 +349,35 @@ Future<void> ensureKnownHostKey(
299349 Spi spi, {
300350 Duration timeout = const Duration (seconds: 5 ),
301351 SSHUserInfoRequestHandler ? onKeyboardInteractive,
352+ Map <String , Spi >? jumpSpisById,
353+ Set <String >? visitedServerIds,
302354}) async {
355+ final chainVisitedServerIds = visitedServerIds ?? < String > {};
356+ final currentServerId = _hostIdentifier (spi);
357+ if (! chainVisitedServerIds.add (currentServerId)) {
358+ throw SSHErr (
359+ type: SSHErrType .connect,
360+ message:
361+ 'Invalid jump chain: cycle detected at ${spi .name } ($currentServerId )' ,
362+ );
363+ }
364+
303365 final cache = _loadKnownHostFingerprints ();
304366 if (_hasKnownHostFingerprintForSpi (spi, cache)) {
305367 return ;
306368 }
307369
308- final jumpSpi = spi.jumpId != null ? Stores .server.box.get (spi.jumpId) : null ;
370+ final jumpId = spi.jumpId;
371+ final jumpSpi = jumpId != null
372+ ? (jumpSpisById? [jumpId] ?? Stores .server.box.get (jumpId))
373+ : null ;
309374 if (jumpSpi != null && ! _hasKnownHostFingerprintForSpi (jumpSpi, cache)) {
310375 await ensureKnownHostKey (
311376 jumpSpi,
312377 timeout: timeout,
313378 onKeyboardInteractive: onKeyboardInteractive,
379+ jumpSpisById: jumpSpisById,
380+ visitedServerIds: chainVisitedServerIds,
314381 );
315382 cache.addAll (_loadKnownHostFingerprints ());
316383 if (_hasKnownHostFingerprintForSpi (spi, cache)) return ;
@@ -351,4 +418,5 @@ String _fingerprintToHex(Uint8List fingerprint) {
351418 return buffer.toString ();
352419}
353420
354- String _fingerprintToBase64 (Uint8List fingerprint) => base64.encode (fingerprint);
421+ String _fingerprintToBase64 (Uint8List fingerprint) =>
422+ base64.encode (fingerprint);
0 commit comments