1
2
3
4
5
6
7
8
9
10 package org.eclipse.jgit.transport.sshd;
11
12 import java.io.Closeable;
13 import java.io.File;
14 import java.io.IOException;
15 import java.nio.file.Files;
16 import java.nio.file.InvalidPathException;
17 import java.nio.file.Path;
18 import java.security.KeyPair;
19 import java.time.Duration;
20 import java.util.ArrayList;
21 import java.util.Arrays;
22 import java.util.Collections;
23 import java.util.HashSet;
24 import java.util.List;
25 import java.util.Map;
26 import java.util.Set;
27 import java.util.concurrent.ConcurrentHashMap;
28 import java.util.concurrent.atomic.AtomicBoolean;
29 import java.util.function.Supplier;
30 import java.util.stream.Collectors;
31
32 import org.apache.sshd.client.ClientBuilder;
33 import org.apache.sshd.client.SshClient;
34 import org.apache.sshd.client.auth.UserAuthFactory;
35 import org.apache.sshd.client.auth.keyboard.UserAuthKeyboardInteractiveFactory;
36 import org.apache.sshd.client.config.hosts.HostConfigEntryResolver;
37 import org.apache.sshd.common.NamedFactory;
38 import org.apache.sshd.common.compression.BuiltinCompressions;
39 import org.apache.sshd.common.config.keys.FilePasswordProvider;
40 import org.apache.sshd.common.config.keys.loader.openssh.kdf.BCryptKdfOptions;
41 import org.apache.sshd.common.keyprovider.KeyIdentityProvider;
42 import org.apache.sshd.common.signature.BuiltinSignatures;
43 import org.apache.sshd.common.signature.Signature;
44 import org.eclipse.jgit.annotations.NonNull;
45 import org.eclipse.jgit.errors.TransportException;
46 import org.eclipse.jgit.internal.transport.ssh.OpenSshConfigFile;
47 import org.eclipse.jgit.internal.transport.sshd.CachingKeyPairProvider;
48 import org.eclipse.jgit.internal.transport.sshd.GssApiWithMicAuthFactory;
49 import org.eclipse.jgit.internal.transport.sshd.JGitPasswordAuthFactory;
50 import org.eclipse.jgit.internal.transport.sshd.JGitPublicKeyAuthFactory;
51 import org.eclipse.jgit.internal.transport.sshd.JGitServerKeyVerifier;
52 import org.eclipse.jgit.internal.transport.sshd.JGitSshClient;
53 import org.eclipse.jgit.internal.transport.sshd.JGitSshConfig;
54 import org.eclipse.jgit.internal.transport.sshd.JGitUserInteraction;
55 import org.eclipse.jgit.internal.transport.sshd.OpenSshServerKeyDatabase;
56 import org.eclipse.jgit.internal.transport.sshd.PasswordProviderWrapper;
57 import org.eclipse.jgit.internal.transport.sshd.SshdText;
58 import org.eclipse.jgit.internal.transport.sshd.agent.JGitSshAgentFactory;
59 import org.eclipse.jgit.transport.CredentialsProvider;
60 import org.eclipse.jgit.transport.SshConfigStore;
61 import org.eclipse.jgit.transport.SshConstants;
62 import org.eclipse.jgit.transport.SshSessionFactory;
63 import org.eclipse.jgit.transport.URIish;
64 import org.eclipse.jgit.transport.sshd.agent.Connector;
65 import org.eclipse.jgit.transport.sshd.agent.ConnectorFactory;
66 import org.eclipse.jgit.util.FS;
67
68
69
70
71
72
73
74
75 public class SshdSessionFactory extends SshSessionFactory implements Closeable {
76
77 private static final String MINA_SSHD = "mina-sshd";
78
79 private final AtomicBoolean closing = new AtomicBoolean();
80
81 private final Set<SshdSession> sessions = new HashSet<>();
82
83 private final Map<Tuple, HostConfigEntryResolver> defaultHostConfigEntryResolver = new ConcurrentHashMap<>();
84
85 private final Map<Tuple, ServerKeyDatabase> defaultServerKeyDatabase = new ConcurrentHashMap<>();
86
87 private final Map<Tuple, Iterable<KeyPair>> defaultKeys = new ConcurrentHashMap<>();
88
89 private final KeyCache keyCache;
90
91 private final ProxyDataFactory proxies;
92
93 private File sshDirectory;
94
95 private File homeDirectory;
96
97
98
99
100
101 public SshdSessionFactory() {
102 this(null, new DefaultProxyDataFactory());
103 }
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143 public SshdSessionFactory(KeyCache keyCache, ProxyDataFactory proxies) {
144 super();
145 this.keyCache = keyCache;
146 this.proxies = proxies;
147
148
149
150
151 BCryptKdfOptions.setMaxAllowedRounds(16384);
152 }
153
154 @Override
155 public String getType() {
156 return MINA_SSHD;
157 }
158
159
160 private static final class Tuple {
161 private Object[] objects;
162
163 public Tuple(Object[] objects) {
164 this.objects = objects;
165 }
166
167 @Override
168 public boolean equals(Object obj) {
169 if (obj == this) {
170 return true;
171 }
172 if (obj != null && obj.getClass() == Tuple.class) {
173 Tuple other = (Tuple) obj;
174 return Arrays.equals(objects, other.objects);
175 }
176 return false;
177 }
178
179 @Override
180 public int hashCode() {
181 return Arrays.hashCode(objects);
182 }
183 }
184
185
186
187
188
189
190 @Override
191 public SshdSession getSession(URIish uri,
192 CredentialsProvider credentialsProvider, FS fs, int tms)
193 throws TransportException {
194 SshdSession session = null;
195 try {
196 session = new SshdSession(uri, () -> {
197 File home = getHomeDirectory();
198 if (home == null) {
199
200
201
202
203 home = FS.DETECTED.userHome();
204 }
205 File sshDir = getSshDirectory();
206 if (sshDir == null) {
207 sshDir = new File(home, SshConstants.SSH_DIR);
208 }
209 HostConfigEntryResolver configFile = getHostConfigEntryResolver(
210 home, sshDir);
211 KeyIdentityProvider defaultKeysProvider = toKeyIdentityProvider(
212 getDefaultKeys(sshDir));
213 SshClient client = ClientBuilder.builder()
214 .factory(JGitSshClient::new)
215 .filePasswordProvider(createFilePasswordProvider(
216 () -> createKeyPasswordProvider(
217 credentialsProvider)))
218 .hostConfigEntryResolver(configFile)
219 .serverKeyVerifier(new JGitServerKeyVerifier(
220 getServerKeyDatabase(home, sshDir)))
221 .signatureFactories(getSignatureFactories())
222 .compressionFactories(
223 new ArrayList<>(BuiltinCompressions.VALUES))
224 .build();
225 client.setUserInteraction(
226 new JGitUserInteraction(credentialsProvider));
227 client.setUserAuthFactories(getUserAuthFactories());
228 client.setKeyIdentityProvider(defaultKeysProvider);
229 ConnectorFactory connectors = getConnectorFactory();
230 if (connectors != null) {
231 client.setAgentFactory(
232 new JGitSshAgentFactory(connectors, home));
233 }
234
235 JGitSshClient jgitClient = (JGitSshClient) client;
236 jgitClient.setKeyCache(getKeyCache());
237 jgitClient.setCredentialsProvider(credentialsProvider);
238 jgitClient.setProxyDatabase(proxies);
239 String defaultAuths = getDefaultPreferredAuthentications();
240 if (defaultAuths != null) {
241 jgitClient.setAttribute(
242 JGitSshClient.PREFERRED_AUTHENTICATIONS,
243 defaultAuths);
244 }
245 try {
246 jgitClient.setAttribute(JGitSshClient.HOME_DIRECTORY,
247 home.getAbsoluteFile().toPath());
248 } catch (SecurityException | InvalidPathException e) {
249
250 }
251
252 return client;
253 });
254 session.addCloseListener(s -> unregister(s));
255 register(session);
256 session.connect(Duration.ofMillis(tms));
257 return session;
258 } catch (Exception e) {
259 unregister(session);
260 if (e instanceof TransportException) {
261 throw (TransportException) e;
262 }
263 throw new TransportException(uri, e.getMessage(), e);
264 }
265 }
266
267 @Override
268 public void close() {
269 closing.set(true);
270 boolean cleanKeys = false;
271 synchronized (this) {
272 cleanKeys = sessions.isEmpty();
273 }
274 if (cleanKeys) {
275 KeyCache cache = getKeyCache();
276 if (cache != null) {
277 cache.close();
278 }
279 }
280 }
281
282 private void register(SshdSession newSession) throws IOException {
283 if (newSession == null) {
284 return;
285 }
286 if (closing.get()) {
287 throw new IOException(SshdText.get().sshClosingDown);
288 }
289 synchronized (this) {
290 sessions.add(newSession);
291 }
292 }
293
294 private void unregister(SshdSession oldSession) {
295 boolean cleanKeys = false;
296 synchronized (this) {
297 sessions.remove(oldSession);
298 cleanKeys = closing.get() && sessions.isEmpty();
299 }
300 if (cleanKeys) {
301 KeyCache cache = getKeyCache();
302 if (cache != null) {
303 cache.close();
304 }
305 }
306 }
307
308
309
310
311
312
313
314 public void setHomeDirectory(@NonNull File homeDir) {
315 if (homeDir.isAbsolute()) {
316 homeDirectory = homeDir;
317 } else {
318 homeDirectory = homeDir.getAbsoluteFile();
319 }
320 }
321
322
323
324
325
326
327 public File getHomeDirectory() {
328 return homeDirectory;
329 }
330
331
332
333
334
335
336
337 public void setSshDirectory(@NonNull File sshDir) {
338 if (sshDir.isAbsolute()) {
339 sshDirectory = sshDir;
340 } else {
341 sshDirectory = sshDir.getAbsoluteFile();
342 }
343 }
344
345
346
347
348
349
350 public File getSshDirectory() {
351 return sshDirectory;
352 }
353
354
355
356
357
358
359
360
361
362
363
364 @NonNull
365 private HostConfigEntryResolver getHostConfigEntryResolver(
366 @NonNull File homeDir, @NonNull File sshDir) {
367 return defaultHostConfigEntryResolver.computeIfAbsent(
368 new Tuple(new Object[] { homeDir, sshDir }),
369 t -> new JGitSshConfig(createSshConfigStore(homeDir,
370 getSshConfig(sshDir), getLocalUserName())));
371 }
372
373
374
375
376
377
378
379
380
381
382
383
384 protected File getSshConfig(@NonNull File sshDir) {
385 return new File(sshDir, SshConstants.CONFIG);
386 }
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404 protected SshConfigStore createSshConfigStore(@NonNull File homeDir,
405 File configFile, String localUserName) {
406 return configFile == null ? null
407 : new OpenSshConfigFile(homeDir, configFile, localUserName);
408 }
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424 @NonNull
425 protected ServerKeyDatabase getServerKeyDatabase(@NonNull File homeDir,
426 @NonNull File sshDir) {
427 return defaultServerKeyDatabase.computeIfAbsent(
428 new Tuple(new Object[] { homeDir, sshDir }),
429 t -> createServerKeyDatabase(homeDir, sshDir));
430
431 }
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447 @NonNull
448 protected ServerKeyDatabase createServerKeyDatabase(@NonNull File homeDir,
449 @NonNull File sshDir) {
450 return new OpenSshServerKeyDatabase(true,
451 getDefaultKnownHostsFiles(sshDir));
452 }
453
454
455
456
457
458
459
460
461
462
463
464 protected ConnectorFactory getConnectorFactory() {
465 return ConnectorFactory.getDefault();
466 }
467
468
469
470
471
472
473
474
475
476 @NonNull
477 protected List<Path> getDefaultKnownHostsFiles(@NonNull File sshDir) {
478 return Arrays.asList(sshDir.toPath().resolve(SshConstants.KNOWN_HOSTS),
479 sshDir.toPath().resolve(SshConstants.KNOWN_HOSTS + '2'));
480 }
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513 @NonNull
514 protected Iterable<KeyPair> getDefaultKeys(@NonNull File sshDir) {
515 List<Path> defaultIdentities = getDefaultIdentities(sshDir);
516 return defaultKeys.computeIfAbsent(
517 new Tuple(defaultIdentities.toArray(new Path[0])),
518 t -> new CachingKeyPairProvider(defaultIdentities,
519 getKeyCache()));
520 }
521
522
523
524
525
526
527
528
529
530
531 private KeyIdentityProvider toKeyIdentityProvider(Iterable<KeyPair> keys) {
532 if (keys instanceof KeyIdentityProvider) {
533 return (KeyIdentityProvider) keys;
534 }
535 return (session) -> keys;
536 }
537
538
539
540
541
542
543
544
545
546
547
548
549 @NonNull
550 protected List<Path> getDefaultIdentities(@NonNull File sshDir) {
551 return Arrays
552 .asList(SshConstants.DEFAULT_IDENTITIES).stream()
553 .map(s -> new File(sshDir, s).toPath()).filter(Files::exists)
554 .collect(Collectors.toList());
555 }
556
557
558
559
560
561
562 protected final KeyCache getKeyCache() {
563 return keyCache;
564 }
565
566
567
568
569
570
571
572
573
574 @NonNull
575 protected KeyPasswordProvider createKeyPasswordProvider(
576 CredentialsProvider provider) {
577 return new IdentityPasswordProvider(provider);
578 }
579
580
581
582
583
584
585
586
587 @NonNull
588 private FilePasswordProvider createFilePasswordProvider(
589 Supplier<KeyPasswordProvider> providerFactory) {
590 return new PasswordProviderWrapper(providerFactory);
591 }
592
593
594
595
596
597
598
599
600
601
602 @NonNull
603 private List<UserAuthFactory> getUserAuthFactories() {
604
605
606
607 return Collections.unmodifiableList(
608 Arrays.asList(GssApiWithMicAuthFactory.INSTANCE,
609 JGitPublicKeyAuthFactory.FACTORY,
610 JGitPasswordAuthFactory.INSTANCE,
611 UserAuthKeyboardInteractiveFactory.INSTANCE));
612 }
613
614
615
616
617
618
619
620
621
622
623 protected String getDefaultPreferredAuthentications() {
624 return null;
625 }
626
627
628
629
630
631
632
633 @SuppressWarnings("deprecation")
634 private static List<NamedFactory<Signature>> getSignatureFactories() {
635
636 return Arrays.asList(
637 BuiltinSignatures.nistp256_cert,
638 BuiltinSignatures.nistp384_cert,
639 BuiltinSignatures.nistp521_cert,
640 BuiltinSignatures.ed25519_cert,
641 BuiltinSignatures.rsaSHA512_cert,
642 BuiltinSignatures.rsaSHA256_cert,
643 BuiltinSignatures.rsa_cert,
644 BuiltinSignatures.nistp256,
645 BuiltinSignatures.nistp384,
646 BuiltinSignatures.nistp521,
647 BuiltinSignatures.ed25519,
648 BuiltinSignatures.sk_ecdsa_sha2_nistp256,
649 BuiltinSignatures.sk_ssh_ed25519,
650 BuiltinSignatures.rsaSHA512,
651 BuiltinSignatures.rsaSHA256,
652 BuiltinSignatures.rsa,
653 BuiltinSignatures.dsa_cert,
654 BuiltinSignatures.dsa);
655
656 }
657 }