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