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>
10   * and other copyright owners as documented in the project's IP log.
11   *
12   * This program and the accompanying materials are made available
13   * under the terms of the Eclipse Distribution License v1.0 which
14   * accompanies this distribution, is reproduced below, and is
15   * available at http://www.eclipse.org/org/documents/edl-v10.php
16   *
17   * All rights reserved.
18   *
19   * Redistribution and use in source and binary forms, with or
20   * without modification, are permitted provided that the following
21   * conditions are met:
22   *
23   * - Redistributions of source code must retain the above copyright
24   *   notice, this list of conditions and the following disclaimer.
25   *
26   * - Redistributions in binary form must reproduce the above
27   *   copyright notice, this list of conditions and the following
28   *   disclaimer in the documentation and/or other materials provided
29   *   with the distribution.
30   *
31   * - Neither the name of the Eclipse Foundation, Inc. nor the
32   *   names of its contributors may be used to endorse or promote
33   *   products derived from this software without specific prior
34   *   written permission.
35   *
36   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
37   * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
38   * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
39   * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
40   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
41   * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
42   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
43   * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
44   * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
45   * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
46   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
47   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
48   * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
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   * The base session factory that loads known hosts and private keys from
88   * <code>$HOME/.ssh</code>.
89   * <p>
90   * This is the default implementation used by JGit and provides most of the
91   * compatibility necessary to match OpenSSH, a popular implementation of SSH
92   * used by C Git.
93   * <p>
94   * The factory does not provide UI behavior. Override the method
95   * {@link #configure(org.eclipse.jgit.transport.OpenSshConfig.Host, Session)} to
96   * supply appropriate {@link com.jcraft.jsch.UserInfo} to the session.
97   */
98  public abstract class JschConfigSessionFactory extends SshSessionFactory {
99  
100 	private static final Logger LOG = LoggerFactory
101 			.getLogger(JschConfigSessionFactory.class);
102 
103 	/**
104 	 * We use different Jsch instances for hosts that have an IdentityFile
105 	 * configured in ~/.ssh/config. Jsch by default would cache decrypted keys
106 	 * only per session, which results in repeated password prompts. Using
107 	 * different Jsch instances, we can cache the keys on these instances so
108 	 * that they will be re-used for successive sessions, and thus the user is
109 	 * prompted for a key password only once while Eclipse runs.
110 	 */
111 	private final Map<String, JSch> byIdentityFile = new HashMap<>();
112 
113 	private JSch defaultJSch;
114 
115 	private OpenSshConfig config;
116 
117 	/** {@inheritDoc} */
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 					// Make sure our known_hosts is not outdated
150 					knownHosts(getJSch(hc, fs), fs);
151 
152 					if (isAuthenticationCanceled(e)) {
153 						throw e;
154 					} else if (isAuthenticationFailed(e)
155 							&& credentialsProvider != null) {
156 						// if authentication failed maybe credentials changed at
157 						// the remote end therefore reset credentials and retry
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"); //$NON-NLS-1$
198 	}
199 
200 	private static boolean isAuthenticationCanceled(JSchException e) {
201 		return e.getCause() == null && e.getMessage().equals("Auth cancel"); //$NON-NLS-1$
202 	}
203 
204 	// Package visibility for tests
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 		// Jsch will have overridden the explicit user by the one from the SSH
210 		// config file...
211 		setUserName(session, user);
212 		// Jsch will also have overridden the port.
213 		if (port > 0 && port != session.getPort()) {
214 			session.setPort(port);
215 		}
216 		// We retry already in getSession() method. JSch must not retry
217 		// on its own.
218 		session.setConfig("MaxAuthTries", "1"); //$NON-NLS-1$ //$NON-NLS-2$
219 		if (pass != null)
220 			session.setPassword(pass);
221 		final String strictHostKeyCheckingPolicy = hc
222 				.getStrictHostKeyChecking();
223 		if (strictHostKeyCheckingPolicy != null)
224 			session.setConfig("StrictHostKeyChecking", //$NON-NLS-1$
225 					strictHostKeyCheckingPolicy);
226 		final String pauth = hc.getPreferredAuthentications();
227 		if (pauth != null)
228 			session.setConfig("PreferredAuthentications", pauth); //$NON-NLS-1$
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) { //$NON-NLS-1$
236 			setPreferredKeyTypesOrder(session);
237 		}
238 		configure(hc, session);
239 		return session;
240 	}
241 
242 	private void safeConfig(Session session, Config cfg) {
243 		// Ensure that Jsch checks all configured algorithms, not just its
244 		// built-in ones. Otherwise it may propose an algorithm for which it
245 		// doesn't have an implementation, and then run into an NPE if that
246 		// algorithm ends up being chosen.
247 		copyConfigValueToSession(session, cfg, "Ciphers", "CheckCiphers"); //$NON-NLS-1$ //$NON-NLS-2$
248 		copyConfigValueToSession(session, cfg, "KexAlgorithms", "CheckKexes"); //$NON-NLS-1$ //$NON-NLS-2$
249 		copyConfigValueToSession(session, cfg, "HostKeyAlgorithms", //$NON-NLS-1$
250 				"CheckSignatures"); //$NON-NLS-1$
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"; //$NON-NLS-1$
261 			String current = session.getConfig(serverHostKey);
262 			if (current == null) {
263 				session.setConfig(serverHostKey, String.join(",", known)); //$NON-NLS-1$
264 				return;
265 			}
266 
267 			String knownFirst = Stream.concat(
268 							known.stream(),
269 							Stream.of(current.split(",")) //$NON-NLS-1$
270 									.filter(s -> !known.contains(s)))
271 					.collect(joining(",")); //$NON-NLS-1$
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(), //$NON-NLS-1$
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 		// Jsch 0.1.54 picks up the user name from the ssh config, even if an
294 		// explicit user name was given! We must correct that if ~/.ssh/config
295 		// has a different user name.
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", //$NON-NLS-1$
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 	 * Create a new remote session for the requested address.
316 	 *
317 	 * @param hc
318 	 *            host configuration
319 	 * @param user
320 	 *            login to authenticate as.
321 	 * @param host
322 	 *            server name to connect to.
323 	 * @param port
324 	 *            port number of the SSH daemon (typically 22).
325 	 * @param fs
326 	 *            the file system abstraction which will be necessary to
327 	 *            perform certain file system operations.
328 	 * @return new session instance, but otherwise unconfigured.
329 	 * @throws com.jcraft.jsch.JSchException
330 	 *             the session could not be created.
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 	 * Provide additional configuration for the JSch instance. This method could
340 	 * be overridden to supply a preferred
341 	 * {@link com.jcraft.jsch.IdentityRepository}.
342 	 *
343 	 * @param jsch
344 	 *            jsch instance
345 	 * @since 4.5
346 	 */
347 	protected void configureJSch(JSch jsch) {
348 		// No additional configuration required.
349 	}
350 
351 	/**
352 	 * Provide additional configuration for the session based on the host
353 	 * information. This method could be used to supply
354 	 * {@link com.jcraft.jsch.UserInfo}.
355 	 *
356 	 * @param hc
357 	 *            host configuration
358 	 * @param session
359 	 *            session to configure
360 	 */
361 	protected abstract void configure(OpenSshConfig.Host hc, Session session);
362 
363 	/**
364 	 * Obtain the JSch used to create new sessions.
365 	 *
366 	 * @param hc
367 	 *            host configuration
368 	 * @param fs
369 	 *            the file system abstraction which will be necessary to
370 	 *            perform certain file system operations.
371 	 * @return the JSch instance to use.
372 	 * @throws com.jcraft.jsch.JSchException
373 	 *             the user configuration could not be created.
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 	 * Create default instance of jsch
407 	 *
408 	 * @param fs
409 	 *            the file system abstraction which will be necessary to perform
410 	 *            certain file system operations.
411 	 * @return the new default JSch implementation.
412 	 * @throws com.jcraft.jsch.JSchException
413 	 *             known host keys cannot be loaded.
414 	 */
415 	protected JSch createDefaultJSch(FS fs) throws JSchException {
416 		final JSch jsch = new JSch();
417 		JSch.setConfig("ssh-rsa", JSch.getConfig("signature.rsa")); //$NON-NLS-1$ //$NON-NLS-2$
418 		JSch.setConfig("ssh-dss", JSch.getConfig("signature.dss")); //$NON-NLS-1$ //$NON-NLS-2$
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"); //$NON-NLS-1$ //$NON-NLS-2$
430 		try (FileInputStream in = new FileInputStream(known_hosts)) {
431 			sch.setKnownHosts(in);
432 		} catch (FileNotFoundException none) {
433 			// Oh well. They don't have a known hosts in home.
434 		} catch (IOException err) {
435 			// Oh well. They don't have a known hosts in home.
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"); //$NON-NLS-1$
444 		if (sshdir.isDirectory()) {
445 			loadIdentity(sch, new File(sshdir, "identity")); //$NON-NLS-1$
446 			loadIdentity(sch, new File(sshdir, "id_rsa")); //$NON-NLS-1$
447 			loadIdentity(sch, new File(sshdir, "id_dsa")); //$NON-NLS-1$
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 				// Instead, pretend the key doesn't exist.
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 		 * A {@link com.jcraft.jsch.ConfigRepository.Config} that transforms
477 		 * some values from the config file into the format Jsch 0.1.54 expects.
478 		 * This is a work-around for bugs in Jsch.
479 		 * <p>
480 		 * Additionally, this config hides the IdentityFile config entries from
481 		 * Jsch; we manage those ourselves. Otherwise Jsch would cache passwords
482 		 * (or rather, decrypted keys) only for a single session, resulting in
483 		 * multiple password prompts for user operations that use several Jsch
484 		 * sessions.
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)) { //$NON-NLS-1$
515 					return null;
516 				}
517 				String result = real.getValue(key);
518 				if (result != null) {
519 					if ("SERVERALIVEINTERVAL".equals(k) //$NON-NLS-1$
520 							|| "CONNECTTIMEOUT".equals(k)) { //$NON-NLS-1$
521 						// These values are in seconds. Jsch 0.1.54 passes them
522 						// on as is to java.net.Socket.setSoTimeout(), which
523 						// expects milliseconds. So convert here to
524 						// milliseconds.
525 						try {
526 							int timeout = Integer.parseInt(result);
527 							result = Long.toString(
528 									TimeUnit.SECONDS.toMillis(timeout));
529 						} catch (NumberFormatException e) {
530 							// Ignore
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)) { //$NON-NLS-1$
541 					return NO_IDENTITIES;
542 				}
543 				return real.getValues(key);
544 			}
545 		}
546 	}
547 
548 	/**
549 	 * Set the {@link OpenSshConfig} to use. Intended for use in tests.
550 	 *
551 	 * @param config
552 	 *            to use
553 	 */
554 	synchronized void setConfig(OpenSshConfig config) {
555 		this.config = config;
556 	}
557 }