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