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