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