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