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(), 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 }