View Javadoc
1   /*
2    * Copyright (C) 2018, Sasa Zivkov <sasa.zivkov@sap.com>
3    * Copyright (C) 2016, Mark Ingram <markdingram@gmail.com>
4    * Copyright (C) 2009, Constantine Plotnikov <constantine.plotnikov@gmail.com>
5    * Copyright (C) 2008-2009, Google Inc.
6    * Copyright (C) 2009, Google, Inc.
7    * Copyright (C) 2009, JetBrains s.r.o.
8    * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com>
9    * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> and others
10   *
11   * This program and the accompanying materials are made available under the
12   * terms of the Eclipse Distribution License v. 1.0 which is available at
13   * https://www.eclipse.org/org/documents/edl-v10.php.
14   *
15   * SPDX-License-Identifier: BSD-3-Clause
16   */
17  
18  package org.eclipse.jgit.transport;
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.JGitText;
41  import org.eclipse.jgit.util.FS;
42  import org.slf4j.Logger;
43  import org.slf4j.LoggerFactory;
44  
45  import com.jcraft.jsch.ConfigRepository;
46  import com.jcraft.jsch.ConfigRepository.Config;
47  import com.jcraft.jsch.HostKey;
48  import com.jcraft.jsch.HostKeyRepository;
49  import com.jcraft.jsch.JSch;
50  import com.jcraft.jsch.JSchException;
51  import com.jcraft.jsch.Session;
52  
53  /**
54   * The base session factory that loads known hosts and private keys from
55   * <code>$HOME/.ssh</code>.
56   * <p>
57   * This is the default implementation used by JGit and provides most of the
58   * compatibility necessary to match OpenSSH, a popular implementation of SSH
59   * used by C Git.
60   * <p>
61   * The factory does not provide UI behavior. Override the method
62   * {@link #configure(org.eclipse.jgit.transport.OpenSshConfig.Host, Session)} to
63   * supply appropriate {@link com.jcraft.jsch.UserInfo} to the session.
64   */
65  public abstract class JschConfigSessionFactory extends SshSessionFactory {
66  
67  	private static final Logger LOG = LoggerFactory
68  			.getLogger(JschConfigSessionFactory.class);
69  
70  	/**
71  	 * We use different Jsch instances for hosts that have an IdentityFile
72  	 * configured in ~/.ssh/config. Jsch by default would cache decrypted keys
73  	 * only per session, which results in repeated password prompts. Using
74  	 * different Jsch instances, we can cache the keys on these instances so
75  	 * that they will be re-used for successive sessions, and thus the user is
76  	 * prompted for a key password only once while Eclipse runs.
77  	 */
78  	private final Map<String, JSch> byIdentityFile = new HashMap<>();
79  
80  	private JSch defaultJSch;
81  
82  	private OpenSshConfig config;
83  
84  	/** {@inheritDoc} */
85  	@Override
86  	public synchronized RemoteSession getSession(URIish uri,
87  			CredentialsProvider credentialsProvider, FS fs, int tms)
88  			throws TransportException {
89  
90  		String user = uri.getUser();
91  		final String pass = uri.getPass();
92  		String host = uri.getHost();
93  		int port = uri.getPort();
94  
95  		try {
96  			if (config == null)
97  				config = OpenSshConfig.get(fs);
98  
99  			final OpenSshConfig.Host hc = config.lookup(host);
100 			if (port <= 0)
101 				port = hc.getPort();
102 			if (user == null)
103 				user = hc.getUser();
104 
105 			Session session = createSession(credentialsProvider, fs, user,
106 					pass, host, port, hc);
107 
108 			int retries = 0;
109 			while (!session.isConnected()) {
110 				try {
111 					retries++;
112 					session.connect(tms);
113 				} catch (JSchException e) {
114 					session.disconnect();
115 					session = null;
116 					// Make sure our known_hosts is not outdated
117 					knownHosts(getJSch(hc, fs), fs);
118 
119 					if (isAuthenticationCanceled(e)) {
120 						throw e;
121 					} else if (isAuthenticationFailed(e)
122 							&& credentialsProvider != null) {
123 						// if authentication failed maybe credentials changed at
124 						// the remote end therefore reset credentials and retry
125 						if (retries < 3) {
126 							credentialsProvider.reset(uri);
127 							session = createSession(credentialsProvider, fs,
128 									user, pass, host, port, hc);
129 						} else
130 							throw e;
131 					} else if (retries >= hc.getConnectionAttempts()) {
132 						throw e;
133 					} else {
134 						try {
135 							Thread.sleep(1000);
136 							session = createSession(credentialsProvider, fs,
137 									user, pass, host, port, hc);
138 						} catch (InterruptedException e1) {
139 							throw new TransportException(
140 									JGitText.get().transportSSHRetryInterrupt,
141 									e1);
142 						}
143 					}
144 				}
145 			}
146 
147 			return new JschSession(session, uri);
148 
149 		} catch (JSchException je) {
150 			final Throwable c = je.getCause();
151 			if (c instanceof UnknownHostException) {
152 				throw new TransportException(uri, JGitText.get().unknownHost,
153 						je);
154 			}
155 			if (c instanceof ConnectException) {
156 				throw new TransportException(uri, c.getMessage(), je);
157 			}
158 			throw new TransportException(uri, je.getMessage(), je);
159 		}
160 
161 	}
162 
163 	private static boolean isAuthenticationFailed(JSchException e) {
164 		return e.getCause() == null && e.getMessage().equals("Auth fail"); //$NON-NLS-1$
165 	}
166 
167 	private static boolean isAuthenticationCanceled(JSchException e) {
168 		return e.getCause() == null && e.getMessage().equals("Auth cancel"); //$NON-NLS-1$
169 	}
170 
171 	// Package visibility for tests
172 	Session createSession(CredentialsProvider credentialsProvider,
173 			FS fs, String user, final String pass, String host, int port,
174 			final OpenSshConfig.Host hc) throws JSchException {
175 		final Session session = createSession(hc, user, host, port, fs);
176 		// Jsch will have overridden the explicit user by the one from the SSH
177 		// config file...
178 		setUserName(session, user);
179 		// Jsch will also have overridden the port.
180 		if (port > 0 && port != session.getPort()) {
181 			session.setPort(port);
182 		}
183 		// We retry already in getSession() method. JSch must not retry
184 		// on its own.
185 		session.setConfig("MaxAuthTries", "1"); //$NON-NLS-1$ //$NON-NLS-2$
186 		if (pass != null)
187 			session.setPassword(pass);
188 		final String strictHostKeyCheckingPolicy = hc
189 				.getStrictHostKeyChecking();
190 		if (strictHostKeyCheckingPolicy != null)
191 			session.setConfig("StrictHostKeyChecking", //$NON-NLS-1$
192 					strictHostKeyCheckingPolicy);
193 		final String pauth = hc.getPreferredAuthentications();
194 		if (pauth != null)
195 			session.setConfig("PreferredAuthentications", pauth); //$NON-NLS-1$
196 		if (credentialsProvider != null
197 				&& (!hc.isBatchMode() || !credentialsProvider.isInteractive())) {
198 			session.setUserInfo(new CredentialsProviderUserInfo(session,
199 					credentialsProvider));
200 		}
201 		safeConfig(session, hc.getConfig());
202 		if (hc.getConfig().getValue("HostKeyAlgorithms") == null) { //$NON-NLS-1$
203 			setPreferredKeyTypesOrder(session);
204 		}
205 		configure(hc, session);
206 		return session;
207 	}
208 
209 	private void safeConfig(Session session, Config cfg) {
210 		// Ensure that Jsch checks all configured algorithms, not just its
211 		// built-in ones. Otherwise it may propose an algorithm for which it
212 		// doesn't have an implementation, and then run into an NPE if that
213 		// algorithm ends up being chosen.
214 		copyConfigValueToSession(session, cfg, "Ciphers", "CheckCiphers"); //$NON-NLS-1$ //$NON-NLS-2$
215 		copyConfigValueToSession(session, cfg, "KexAlgorithms", "CheckKexes"); //$NON-NLS-1$ //$NON-NLS-2$
216 		copyConfigValueToSession(session, cfg, "HostKeyAlgorithms", //$NON-NLS-1$
217 				"CheckSignatures"); //$NON-NLS-1$
218 	}
219 
220 	private static void setPreferredKeyTypesOrder(Session session) {
221 		HostKeyRepository hkr = session.getHostKeyRepository();
222 		HostKey[] hostKeys = hkr.getHostKey(hostName(session), null);
223 
224 		if (hostKeys == null) {
225 			return;
226 		}
227 
228 		List<String> known = Stream.of(hostKeys)
229 				.map(HostKey::getType)
230 				.collect(toList());
231 
232 		if (!known.isEmpty()) {
233 			String serverHostKey = "server_host_key"; //$NON-NLS-1$
234 			String current = session.getConfig(serverHostKey);
235 			if (current == null) {
236 				session.setConfig(serverHostKey, String.join(",", known)); //$NON-NLS-1$
237 				return;
238 			}
239 
240 			String knownFirst = Stream.concat(
241 							known.stream(),
242 							Stream.of(current.split(",")) //$NON-NLS-1$
243 									.filter(s -> !known.contains(s)))
244 					.collect(joining(",")); //$NON-NLS-1$
245 			session.setConfig(serverHostKey, knownFirst);
246 		}
247 	}
248 
249 	private static String hostName(Session s) {
250 		if (s.getPort() == SshConstants.SSH_DEFAULT_PORT) {
251 			return s.getHost();
252 		}
253 		return String.format("[%s]:%d", s.getHost(), //$NON-NLS-1$
254 				Integer.valueOf(s.getPort()));
255 	}
256 
257 	private void copyConfigValueToSession(Session session, Config cfg,
258 			String from, String to) {
259 		String value = cfg.getValue(from);
260 		if (value != null) {
261 			session.setConfig(to, value);
262 		}
263 	}
264 
265 	private void setUserName(Session session, String userName) {
266 		// Jsch 0.1.54 picks up the user name from the ssh config, even if an
267 		// explicit user name was given! We must correct that if ~/.ssh/config
268 		// has a different user name.
269 		if (userName == null || userName.isEmpty()
270 				|| userName.equals(session.getUserName())) {
271 			return;
272 		}
273 		try {
274 			Class<?>[] parameterTypes = { String.class };
275 			Method method = Session.class.getDeclaredMethod("setUserName", //$NON-NLS-1$
276 					parameterTypes);
277 			method.setAccessible(true);
278 			method.invoke(session, userName);
279 		} catch (NullPointerException | IllegalAccessException
280 				| IllegalArgumentException | InvocationTargetException
281 				| NoSuchMethodException | SecurityException e) {
282 			LOG.error(MessageFormat.format(JGitText.get().sshUserNameError,
283 					userName, session.getUserName()), e);
284 		}
285 	}
286 
287 	/**
288 	 * Create a new remote session for the requested address.
289 	 *
290 	 * @param hc
291 	 *            host configuration
292 	 * @param user
293 	 *            login to authenticate as.
294 	 * @param host
295 	 *            server name to connect to.
296 	 * @param port
297 	 *            port number of the SSH daemon (typically 22).
298 	 * @param fs
299 	 *            the file system abstraction which will be necessary to
300 	 *            perform certain file system operations.
301 	 * @return new session instance, but otherwise unconfigured.
302 	 * @throws com.jcraft.jsch.JSchException
303 	 *             the session could not be created.
304 	 */
305 	protected Session createSession(final OpenSshConfig.Host hc,
306 			final String user, final String host, final int port, FS fs)
307 			throws JSchException {
308 		return getJSch(hc, fs).getSession(user, host, port);
309 	}
310 
311 	/**
312 	 * Provide additional configuration for the JSch instance. This method could
313 	 * be overridden to supply a preferred
314 	 * {@link com.jcraft.jsch.IdentityRepository}.
315 	 *
316 	 * @param jsch
317 	 *            jsch instance
318 	 * @since 4.5
319 	 */
320 	protected void configureJSch(JSch jsch) {
321 		// No additional configuration required.
322 	}
323 
324 	/**
325 	 * Provide additional configuration for the session based on the host
326 	 * information. This method could be used to supply
327 	 * {@link com.jcraft.jsch.UserInfo}.
328 	 *
329 	 * @param hc
330 	 *            host configuration
331 	 * @param session
332 	 *            session to configure
333 	 */
334 	protected abstract void configure(OpenSshConfig.Host hc, Session session);
335 
336 	/**
337 	 * Obtain the JSch used to create new sessions.
338 	 *
339 	 * @param hc
340 	 *            host configuration
341 	 * @param fs
342 	 *            the file system abstraction which will be necessary to
343 	 *            perform certain file system operations.
344 	 * @return the JSch instance to use.
345 	 * @throws com.jcraft.jsch.JSchException
346 	 *             the user configuration could not be created.
347 	 */
348 	protected JSch getJSch(OpenSshConfig.Host hc, FS fs) throws JSchException {
349 		if (defaultJSch == null) {
350 			defaultJSch = createDefaultJSch(fs);
351 			if (defaultJSch.getConfigRepository() == null) {
352 				defaultJSch.setConfigRepository(
353 						new JschBugFixingConfigRepository(config));
354 			}
355 			for (Object name : defaultJSch.getIdentityNames())
356 				byIdentityFile.put((String) name, defaultJSch);
357 		}
358 
359 		final File identityFile = hc.getIdentityFile();
360 		if (identityFile == null)
361 			return defaultJSch;
362 
363 		final String identityKey = identityFile.getAbsolutePath();
364 		JSch jsch = byIdentityFile.get(identityKey);
365 		if (jsch == null) {
366 			jsch = new JSch();
367 			configureJSch(jsch);
368 			if (jsch.getConfigRepository() == null) {
369 				jsch.setConfigRepository(defaultJSch.getConfigRepository());
370 			}
371 			jsch.setHostKeyRepository(defaultJSch.getHostKeyRepository());
372 			jsch.addIdentity(identityKey);
373 			byIdentityFile.put(identityKey, jsch);
374 		}
375 		return jsch;
376 	}
377 
378 	/**
379 	 * Create default instance of jsch
380 	 *
381 	 * @param fs
382 	 *            the file system abstraction which will be necessary to perform
383 	 *            certain file system operations.
384 	 * @return the new default JSch implementation.
385 	 * @throws com.jcraft.jsch.JSchException
386 	 *             known host keys cannot be loaded.
387 	 */
388 	protected JSch createDefaultJSch(FS fs) throws JSchException {
389 		final JSch jsch = new JSch();
390 		JSch.setConfig("ssh-rsa", JSch.getConfig("signature.rsa")); //$NON-NLS-1$ //$NON-NLS-2$
391 		JSch.setConfig("ssh-dss", JSch.getConfig("signature.dss")); //$NON-NLS-1$ //$NON-NLS-2$
392 		configureJSch(jsch);
393 		knownHosts(jsch, fs);
394 		identities(jsch, fs);
395 		return jsch;
396 	}
397 
398 	private static void knownHosts(JSch sch, FS fs) throws JSchException {
399 		final File home = fs.userHome();
400 		if (home == null)
401 			return;
402 		final File known_hosts = new File(new File(home, ".ssh"), "known_hosts"); //$NON-NLS-1$ //$NON-NLS-2$
403 		try (FileInputStream in = new FileInputStream(known_hosts)) {
404 			sch.setKnownHosts(in);
405 		} catch (FileNotFoundException none) {
406 			// Oh well. They don't have a known hosts in home.
407 		} catch (IOException err) {
408 			// Oh well. They don't have a known hosts in home.
409 		}
410 	}
411 
412 	private static void identities(JSch sch, FS fs) {
413 		final File home = fs.userHome();
414 		if (home == null)
415 			return;
416 		final File sshdir = new File(home, ".ssh"); //$NON-NLS-1$
417 		if (sshdir.isDirectory()) {
418 			loadIdentity(sch, new File(sshdir, "identity")); //$NON-NLS-1$
419 			loadIdentity(sch, new File(sshdir, "id_rsa")); //$NON-NLS-1$
420 			loadIdentity(sch, new File(sshdir, "id_dsa")); //$NON-NLS-1$
421 		}
422 	}
423 
424 	private static void loadIdentity(JSch sch, File priv) {
425 		if (priv.isFile()) {
426 			try {
427 				sch.addIdentity(priv.getAbsolutePath());
428 			} catch (JSchException e) {
429 				// Instead, pretend the key doesn't exist.
430 			}
431 		}
432 	}
433 
434 	private static class JschBugFixingConfigRepository
435 			implements ConfigRepository {
436 
437 		private final ConfigRepository base;
438 
439 		public JschBugFixingConfigRepository(ConfigRepository base) {
440 			this.base = base;
441 		}
442 
443 		@Override
444 		public Config getConfig(String host) {
445 			return new JschBugFixingConfig(base.getConfig(host));
446 		}
447 
448 		/**
449 		 * A {@link com.jcraft.jsch.ConfigRepository.Config} that transforms
450 		 * some values from the config file into the format Jsch 0.1.54 expects.
451 		 * This is a work-around for bugs in Jsch.
452 		 * <p>
453 		 * Additionally, this config hides the IdentityFile config entries from
454 		 * Jsch; we manage those ourselves. Otherwise Jsch would cache passwords
455 		 * (or rather, decrypted keys) only for a single session, resulting in
456 		 * multiple password prompts for user operations that use several Jsch
457 		 * sessions.
458 		 */
459 		private static class JschBugFixingConfig implements Config {
460 
461 			private static final String[] NO_IDENTITIES = {};
462 
463 			private final Config real;
464 
465 			public JschBugFixingConfig(Config delegate) {
466 				real = delegate;
467 			}
468 
469 			@Override
470 			public String getHostname() {
471 				return real.getHostname();
472 			}
473 
474 			@Override
475 			public String getUser() {
476 				return real.getUser();
477 			}
478 
479 			@Override
480 			public int getPort() {
481 				return real.getPort();
482 			}
483 
484 			@Override
485 			public String getValue(String key) {
486 				String k = key.toUpperCase(Locale.ROOT);
487 				if ("IDENTITYFILE".equals(k)) { //$NON-NLS-1$
488 					return null;
489 				}
490 				String result = real.getValue(key);
491 				if (result != null) {
492 					if ("SERVERALIVEINTERVAL".equals(k) //$NON-NLS-1$
493 							|| "CONNECTTIMEOUT".equals(k)) { //$NON-NLS-1$
494 						// These values are in seconds. Jsch 0.1.54 passes them
495 						// on as is to java.net.Socket.setSoTimeout(), which
496 						// expects milliseconds. So convert here to
497 						// milliseconds.
498 						try {
499 							int timeout = Integer.parseInt(result);
500 							result = Long.toString(
501 									TimeUnit.SECONDS.toMillis(timeout));
502 						} catch (NumberFormatException e) {
503 							// Ignore
504 						}
505 					}
506 				}
507 				return result;
508 			}
509 
510 			@Override
511 			public String[] getValues(String key) {
512 				String k = key.toUpperCase(Locale.ROOT);
513 				if ("IDENTITYFILE".equals(k)) { //$NON-NLS-1$
514 					return NO_IDENTITIES;
515 				}
516 				return real.getValues(key);
517 			}
518 		}
519 	}
520 
521 	/**
522 	 * Set the {@link OpenSshConfig} to use. Intended for use in tests.
523 	 *
524 	 * @param config
525 	 *            to use
526 	 */
527 	synchronized void setConfig(OpenSshConfig config) {
528 		this.config = config;
529 	}
530 }