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
44
45
46
47
48
49
50
51 package org.eclipse.jgit.transport;
52
53 import static java.util.stream.Collectors.joining;
54 import static java.util.stream.Collectors.toList;
55 import static org.eclipse.jgit.transport.OpenSshConfig.SSH_PORT;
56
57 import java.io.File;
58 import java.io.FileInputStream;
59 import java.io.FileNotFoundException;
60 import java.io.IOException;
61 import java.lang.reflect.InvocationTargetException;
62 import java.lang.reflect.Method;
63 import java.net.ConnectException;
64 import java.net.UnknownHostException;
65 import java.text.MessageFormat;
66 import java.util.HashMap;
67 import java.util.List;
68 import java.util.Locale;
69 import java.util.Map;
70 import java.util.concurrent.TimeUnit;
71 import java.util.stream.Stream;
72
73 import org.eclipse.jgit.errors.TransportException;
74 import org.eclipse.jgit.internal.JGitText;
75 import org.eclipse.jgit.util.FS;
76 import org.slf4j.Logger;
77 import org.slf4j.LoggerFactory;
78
79 import com.jcraft.jsch.ConfigRepository;
80 import com.jcraft.jsch.ConfigRepository.Config;
81 import com.jcraft.jsch.HostKey;
82 import com.jcraft.jsch.HostKeyRepository;
83 import com.jcraft.jsch.JSch;
84 import com.jcraft.jsch.JSchException;
85 import com.jcraft.jsch.Session;
86
87
88
89
90
91
92
93
94
95
96
97
98
99 public abstract class JschConfigSessionFactory extends SshSessionFactory {
100
101 private static final Logger LOG = LoggerFactory
102 .getLogger(JschConfigSessionFactory.class);
103
104
105
106
107
108
109
110
111
112 private final Map<String, JSch> byIdentityFile = new HashMap<>();
113
114 private JSch defaultJSch;
115
116 private OpenSshConfig config;
117
118
119 @Override
120 public synchronized RemoteSession getSession(URIish uri,
121 CredentialsProvider credentialsProvider, FS fs, int tms)
122 throws TransportException {
123
124 String user = uri.getUser();
125 final String pass = uri.getPass();
126 String host = uri.getHost();
127 int port = uri.getPort();
128
129 try {
130 if (config == null)
131 config = OpenSshConfig.get(fs);
132
133 final OpenSshConfig.Host hc = config.lookup(host);
134 if (port <= 0)
135 port = hc.getPort();
136 if (user == null)
137 user = hc.getUser();
138
139 Session session = createSession(credentialsProvider, fs, user,
140 pass, host, port, hc);
141
142 int retries = 0;
143 while (!session.isConnected()) {
144 try {
145 retries++;
146 session.connect(tms);
147 } catch (JSchException e) {
148 session.disconnect();
149 session = null;
150
151 knownHosts(getJSch(hc, fs), fs);
152
153 if (isAuthenticationCanceled(e)) {
154 throw e;
155 } else if (isAuthenticationFailed(e)
156 && credentialsProvider != null) {
157
158
159 if (retries < 3) {
160 credentialsProvider.reset(uri);
161 session = createSession(credentialsProvider, fs,
162 user, pass, host, port, hc);
163 } else
164 throw e;
165 } else if (retries >= hc.getConnectionAttempts()) {
166 throw e;
167 } else {
168 try {
169 Thread.sleep(1000);
170 session = createSession(credentialsProvider, fs,
171 user, pass, host, port, hc);
172 } catch (InterruptedException e1) {
173 throw new TransportException(
174 JGitText.get().transportSSHRetryInterrupt,
175 e1);
176 }
177 }
178 }
179 }
180
181 return new JschSession(session, uri);
182
183 } catch (JSchException je) {
184 final Throwable c = je.getCause();
185 if (c instanceof UnknownHostException) {
186 throw new TransportException(uri, JGitText.get().unknownHost,
187 je);
188 }
189 if (c instanceof ConnectException) {
190 throw new TransportException(uri, c.getMessage(), je);
191 }
192 throw new TransportException(uri, je.getMessage(), je);
193 }
194
195 }
196
197 private static boolean isAuthenticationFailed(JSchException e) {
198 return e.getCause() == null && e.getMessage().equals("Auth fail");
199 }
200
201 private static boolean isAuthenticationCanceled(JSchException e) {
202 return e.getCause() == null && e.getMessage().equals("Auth cancel");
203 }
204
205
206 Session createSession(CredentialsProvider credentialsProvider,
207 FS fs, String user, final String pass, String host, int port,
208 final OpenSshConfig.Host hc) throws JSchException {
209 final Session session = createSession(hc, user, host, port, fs);
210
211
212 setUserName(session, user);
213
214 if (port > 0 && port != session.getPort()) {
215 session.setPort(port);
216 }
217
218
219 session.setConfig("MaxAuthTries", "1");
220 if (pass != null)
221 session.setPassword(pass);
222 final String strictHostKeyCheckingPolicy = hc
223 .getStrictHostKeyChecking();
224 if (strictHostKeyCheckingPolicy != null)
225 session.setConfig("StrictHostKeyChecking",
226 strictHostKeyCheckingPolicy);
227 final String pauth = hc.getPreferredAuthentications();
228 if (pauth != null)
229 session.setConfig("PreferredAuthentications", pauth);
230 if (credentialsProvider != null
231 && (!hc.isBatchMode() || !credentialsProvider.isInteractive())) {
232 session.setUserInfo(new CredentialsProviderUserInfo(session,
233 credentialsProvider));
234 }
235 safeConfig(session, hc.getConfig());
236 if (hc.getConfig().getValue("HostKeyAlgorithms") == null) {
237 setPreferredKeyTypesOrder(session);
238 }
239 configure(hc, session);
240 return session;
241 }
242
243 private void safeConfig(Session session, Config cfg) {
244
245
246
247
248 copyConfigValueToSession(session, cfg, "Ciphers", "CheckCiphers");
249 copyConfigValueToSession(session, cfg, "KexAlgorithms", "CheckKexes");
250 copyConfigValueToSession(session, cfg, "HostKeyAlgorithms",
251 "CheckSignatures");
252 }
253
254 private static void setPreferredKeyTypesOrder(Session session) {
255 HostKeyRepository hkr = session.getHostKeyRepository();
256 List<String> known = Stream.of(hkr.getHostKey(hostName(session), null))
257 .map(HostKey::getType)
258 .collect(toList());
259
260 if (!known.isEmpty()) {
261 String serverHostKey = "server_host_key";
262 String current = session.getConfig(serverHostKey);
263 if (current == null) {
264 session.setConfig(serverHostKey, String.join(",", known));
265 return;
266 }
267
268 String knownFirst = Stream.concat(
269 known.stream(),
270 Stream.of(current.split(","))
271 .filter(s -> !known.contains(s)))
272 .collect(joining(","));
273 session.setConfig(serverHostKey, knownFirst);
274 }
275 }
276
277 private static String hostName(Session s) {
278 if (s.getPort() == SSH_PORT) {
279 return s.getHost();
280 }
281 return String.format("[%s]:%d", s.getHost(),
282 Integer.valueOf(s.getPort()));
283 }
284
285 private void copyConfigValueToSession(Session session, Config cfg,
286 String from, String to) {
287 String value = cfg.getValue(from);
288 if (value != null) {
289 session.setConfig(to, value);
290 }
291 }
292
293 private void setUserName(Session session, String userName) {
294
295
296
297 if (userName == null || userName.isEmpty()
298 || userName.equals(session.getUserName())) {
299 return;
300 }
301 try {
302 Class<?>[] parameterTypes = { String.class };
303 Method method = Session.class.getDeclaredMethod("setUserName",
304 parameterTypes);
305 method.setAccessible(true);
306 method.invoke(session, userName);
307 } catch (NullPointerException | IllegalAccessException
308 | IllegalArgumentException | InvocationTargetException
309 | NoSuchMethodException | SecurityException e) {
310 LOG.error(MessageFormat.format(JGitText.get().sshUserNameError,
311 userName, session.getUserName()), e);
312 }
313 }
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333 protected Session createSession(final OpenSshConfig.Host hc,
334 final String user, final String host, final int port, FS fs)
335 throws JSchException {
336 return getJSch(hc, fs).getSession(user, host, port);
337 }
338
339
340
341
342
343
344
345
346
347
348 protected void configureJSch(JSch jsch) {
349
350 }
351
352
353
354
355
356
357
358
359
360
361
362 protected abstract void configure(OpenSshConfig.Host hc, Session session);
363
364
365
366
367
368
369
370
371
372
373
374
375
376 protected JSch getJSch(OpenSshConfig.Host hc, FS fs) throws JSchException {
377 if (defaultJSch == null) {
378 defaultJSch = createDefaultJSch(fs);
379 if (defaultJSch.getConfigRepository() == null) {
380 defaultJSch.setConfigRepository(
381 new JschBugFixingConfigRepository(config));
382 }
383 for (Object name : defaultJSch.getIdentityNames())
384 byIdentityFile.put((String) name, defaultJSch);
385 }
386
387 final File identityFile = hc.getIdentityFile();
388 if (identityFile == null)
389 return defaultJSch;
390
391 final String identityKey = identityFile.getAbsolutePath();
392 JSch jsch = byIdentityFile.get(identityKey);
393 if (jsch == null) {
394 jsch = new JSch();
395 configureJSch(jsch);
396 if (jsch.getConfigRepository() == null) {
397 jsch.setConfigRepository(defaultJSch.getConfigRepository());
398 }
399 jsch.setHostKeyRepository(defaultJSch.getHostKeyRepository());
400 jsch.addIdentity(identityKey);
401 byIdentityFile.put(identityKey, jsch);
402 }
403 return jsch;
404 }
405
406
407
408
409
410
411
412
413
414
415
416 protected JSch createDefaultJSch(FS fs) throws JSchException {
417 final JSch jsch = new JSch();
418 JSch.setConfig("ssh-rsa", JSch.getConfig("signature.rsa"));
419 JSch.setConfig("ssh-dss", JSch.getConfig("signature.dss"));
420 configureJSch(jsch);
421 knownHosts(jsch, fs);
422 identities(jsch, fs);
423 return jsch;
424 }
425
426 private static void knownHosts(JSch sch, FS fs) throws JSchException {
427 final File home = fs.userHome();
428 if (home == null)
429 return;
430 final File known_hosts = new File(new File(home, ".ssh"), "known_hosts");
431 try (FileInputStream in = new FileInputStream(known_hosts)) {
432 sch.setKnownHosts(in);
433 } catch (FileNotFoundException none) {
434
435 } catch (IOException err) {
436
437 }
438 }
439
440 private static void identities(JSch sch, FS fs) {
441 final File home = fs.userHome();
442 if (home == null)
443 return;
444 final File sshdir = new File(home, ".ssh");
445 if (sshdir.isDirectory()) {
446 loadIdentity(sch, new File(sshdir, "identity"));
447 loadIdentity(sch, new File(sshdir, "id_rsa"));
448 loadIdentity(sch, new File(sshdir, "id_dsa"));
449 }
450 }
451
452 private static void loadIdentity(JSch sch, File priv) {
453 if (priv.isFile()) {
454 try {
455 sch.addIdentity(priv.getAbsolutePath());
456 } catch (JSchException e) {
457
458 }
459 }
460 }
461
462 private static class JschBugFixingConfigRepository
463 implements ConfigRepository {
464
465 private final ConfigRepository base;
466
467 public JschBugFixingConfigRepository(ConfigRepository base) {
468 this.base = base;
469 }
470
471 @Override
472 public Config getConfig(String host) {
473 return new JschBugFixingConfig(base.getConfig(host));
474 }
475
476
477
478
479
480
481
482
483
484
485
486
487 private static class JschBugFixingConfig implements Config {
488
489 private static final String[] NO_IDENTITIES = {};
490
491 private final Config real;
492
493 public JschBugFixingConfig(Config delegate) {
494 real = delegate;
495 }
496
497 @Override
498 public String getHostname() {
499 return real.getHostname();
500 }
501
502 @Override
503 public String getUser() {
504 return real.getUser();
505 }
506
507 @Override
508 public int getPort() {
509 return real.getPort();
510 }
511
512 @Override
513 public String getValue(String key) {
514 String k = key.toUpperCase(Locale.ROOT);
515 if ("IDENTITYFILE".equals(k)) {
516 return null;
517 }
518 String result = real.getValue(key);
519 if (result != null) {
520 if ("SERVERALIVEINTERVAL".equals(k)
521 || "CONNECTTIMEOUT".equals(k)) {
522
523
524
525
526 try {
527 int timeout = Integer.parseInt(result);
528 result = Long.toString(
529 TimeUnit.SECONDS.toMillis(timeout));
530 } catch (NumberFormatException e) {
531
532 }
533 }
534 }
535 return result;
536 }
537
538 @Override
539 public String[] getValues(String key) {
540 String k = key.toUpperCase(Locale.ROOT);
541 if ("IDENTITYFILE".equals(k)) {
542 return NO_IDENTITIES;
543 }
544 return real.getValues(key);
545 }
546 }
547 }
548
549
550
551
552
553
554
555 void setConfig(OpenSshConfig config) {
556 this.config = config;
557 }
558 }