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  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   * The base session factory that loads known hosts and private keys from
89   * <code>$HOME/.ssh</code>.
90   * <p>
91   * This is the default implementation used by JGit and provides most of the
92   * compatibility necessary to match OpenSSH, a popular implementation of SSH
93   * used by C Git.
94   * <p>
95   * The factory does not provide UI behavior. Override the method
96   * {@link #configure(org.eclipse.jgit.transport.OpenSshConfig.Host, Session)} to
97   * supply appropriate {@link com.jcraft.jsch.UserInfo} to the session.
98   */
99  public abstract class JschConfigSessionFactory extends SshSessionFactory {
100 
101 	private static final Logger LOG = LoggerFactory
102 			.getLogger(JschConfigSessionFactory.class);
103 
104 	/**
105 	 * We use different Jsch instances for hosts that have an IdentityFile
106 	 * configured in ~/.ssh/config. Jsch by default would cache decrypted keys
107 	 * only per session, which results in repeated password prompts. Using
108 	 * different Jsch instances, we can cache the keys on these instances so
109 	 * that they will be re-used for successive sessions, and thus the user is
110 	 * prompted for a key password only once while Eclipse runs.
111 	 */
112 	private final Map<String, JSch> byIdentityFile = new HashMap<>();
113 
114 	private JSch defaultJSch;
115 
116 	private OpenSshConfig config;
117 
118 	/** {@inheritDoc} */
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 					// Make sure our known_hosts is not outdated
151 					knownHosts(getJSch(hc, fs), fs);
152 
153 					if (isAuthenticationCanceled(e)) {
154 						throw e;
155 					} else if (isAuthenticationFailed(e)
156 							&& credentialsProvider != null) {
157 						// if authentication failed maybe credentials changed at
158 						// the remote end therefore reset credentials and retry
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"); //$NON-NLS-1$
199 	}
200 
201 	private static boolean isAuthenticationCanceled(JSchException e) {
202 		return e.getCause() == null && e.getMessage().equals("Auth cancel"); //$NON-NLS-1$
203 	}
204 
205 	// Package visibility for tests
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 		// Jsch will have overridden the explicit user by the one from the SSH
211 		// config file...
212 		setUserName(session, user);
213 		// Jsch will also have overridden the port.
214 		if (port > 0 && port != session.getPort()) {
215 			session.setPort(port);
216 		}
217 		// We retry already in getSession() method. JSch must not retry
218 		// on its own.
219 		session.setConfig("MaxAuthTries", "1"); //$NON-NLS-1$ //$NON-NLS-2$
220 		if (pass != null)
221 			session.setPassword(pass);
222 		final String strictHostKeyCheckingPolicy = hc
223 				.getStrictHostKeyChecking();
224 		if (strictHostKeyCheckingPolicy != null)
225 			session.setConfig("StrictHostKeyChecking", //$NON-NLS-1$
226 					strictHostKeyCheckingPolicy);
227 		final String pauth = hc.getPreferredAuthentications();
228 		if (pauth != null)
229 			session.setConfig("PreferredAuthentications", pauth); //$NON-NLS-1$
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) { //$NON-NLS-1$
237 			setPreferredKeyTypesOrder(session);
238 		}
239 		configure(hc, session);
240 		return session;
241 	}
242 
243 	private void safeConfig(Session session, Config cfg) {
244 		// Ensure that Jsch checks all configured algorithms, not just its
245 		// built-in ones. Otherwise it may propose an algorithm for which it
246 		// doesn't have an implementation, and then run into an NPE if that
247 		// algorithm ends up being chosen.
248 		copyConfigValueToSession(session, cfg, "Ciphers", "CheckCiphers"); //$NON-NLS-1$ //$NON-NLS-2$
249 		copyConfigValueToSession(session, cfg, "KexAlgorithms", "CheckKexes"); //$NON-NLS-1$ //$NON-NLS-2$
250 		copyConfigValueToSession(session, cfg, "HostKeyAlgorithms", //$NON-NLS-1$
251 				"CheckSignatures"); //$NON-NLS-1$
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"; //$NON-NLS-1$
262 			String current = session.getConfig(serverHostKey);
263 			if (current == null) {
264 				session.setConfig(serverHostKey, String.join(",", known)); //$NON-NLS-1$
265 				return;
266 			}
267 
268 			String knownFirst = Stream.concat(
269 							known.stream(),
270 							Stream.of(current.split(",")) //$NON-NLS-1$
271 									.filter(s -> !known.contains(s)))
272 					.collect(joining(",")); //$NON-NLS-1$
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(), //$NON-NLS-1$
282 				Integer.valueOf(s.getPort()));
283 	}
284 
285 	private void copyConfigValueToSession(Session session, Config cfg,
286 			String from, String to) {
287 		String value = cfg.getValue(from);
288 		if (value != null) {
289 			session.setConfig(to, value);
290 		}
291 	}
292 
293 	private void setUserName(Session session, String userName) {
294 		// Jsch 0.1.54 picks up the user name from the ssh config, even if an
295 		// explicit user name was given! We must correct that if ~/.ssh/config
296 		// has a different user name.
297 		if (userName == null || userName.isEmpty()
298 				|| userName.equals(session.getUserName())) {
299 			return;
300 		}
301 		try {
302 			Class<?>[] parameterTypes = { String.class };
303 			Method method = Session.class.getDeclaredMethod("setUserName", //$NON-NLS-1$
304 					parameterTypes);
305 			method.setAccessible(true);
306 			method.invoke(session, userName);
307 		} catch (NullPointerException | IllegalAccessException
308 				| IllegalArgumentException | InvocationTargetException
309 				| NoSuchMethodException | SecurityException e) {
310 			LOG.error(MessageFormat.format(JGitText.get().sshUserNameError,
311 					userName, session.getUserName()), e);
312 		}
313 	}
314 
315 	/**
316 	 * Create a new remote session for the requested address.
317 	 *
318 	 * @param hc
319 	 *            host configuration
320 	 * @param user
321 	 *            login to authenticate as.
322 	 * @param host
323 	 *            server name to connect to.
324 	 * @param port
325 	 *            port number of the SSH daemon (typically 22).
326 	 * @param fs
327 	 *            the file system abstraction which will be necessary to
328 	 *            perform certain file system operations.
329 	 * @return new session instance, but otherwise unconfigured.
330 	 * @throws com.jcraft.jsch.JSchException
331 	 *             the session could not be created.
332 	 */
333 	protected Session createSession(final OpenSshConfig.Host hc,
334 			final String user, final String host, final int port, FS fs)
335 			throws JSchException {
336 		return getJSch(hc, fs).getSession(user, host, port);
337 	}
338 
339 	/**
340 	 * Provide additional configuration for the JSch instance. This method could
341 	 * be overridden to supply a preferred
342 	 * {@link com.jcraft.jsch.IdentityRepository}.
343 	 *
344 	 * @param jsch
345 	 *            jsch instance
346 	 * @since 4.5
347 	 */
348 	protected void configureJSch(JSch jsch) {
349 		// No additional configuration required.
350 	}
351 
352 	/**
353 	 * Provide additional configuration for the session based on the host
354 	 * information. This method could be used to supply
355 	 * {@link com.jcraft.jsch.UserInfo}.
356 	 *
357 	 * @param hc
358 	 *            host configuration
359 	 * @param session
360 	 *            session to configure
361 	 */
362 	protected abstract void configure(OpenSshConfig.Host hc, Session session);
363 
364 	/**
365 	 * Obtain the JSch used to create new sessions.
366 	 *
367 	 * @param hc
368 	 *            host configuration
369 	 * @param fs
370 	 *            the file system abstraction which will be necessary to
371 	 *            perform certain file system operations.
372 	 * @return the JSch instance to use.
373 	 * @throws com.jcraft.jsch.JSchException
374 	 *             the user configuration could not be created.
375 	 */
376 	protected JSch getJSch(OpenSshConfig.Host hc, FS fs) throws JSchException {
377 		if (defaultJSch == null) {
378 			defaultJSch = createDefaultJSch(fs);
379 			if (defaultJSch.getConfigRepository() == null) {
380 				defaultJSch.setConfigRepository(
381 						new JschBugFixingConfigRepository(config));
382 			}
383 			for (Object name : defaultJSch.getIdentityNames())
384 				byIdentityFile.put((String) name, defaultJSch);
385 		}
386 
387 		final File identityFile = hc.getIdentityFile();
388 		if (identityFile == null)
389 			return defaultJSch;
390 
391 		final String identityKey = identityFile.getAbsolutePath();
392 		JSch jsch = byIdentityFile.get(identityKey);
393 		if (jsch == null) {
394 			jsch = new JSch();
395 			configureJSch(jsch);
396 			if (jsch.getConfigRepository() == null) {
397 				jsch.setConfigRepository(defaultJSch.getConfigRepository());
398 			}
399 			jsch.setHostKeyRepository(defaultJSch.getHostKeyRepository());
400 			jsch.addIdentity(identityKey);
401 			byIdentityFile.put(identityKey, jsch);
402 		}
403 		return jsch;
404 	}
405 
406 	/**
407 	 * Create default instance of jsch
408 	 *
409 	 * @param fs
410 	 *            the file system abstraction which will be necessary to perform
411 	 *            certain file system operations.
412 	 * @return the new default JSch implementation.
413 	 * @throws com.jcraft.jsch.JSchException
414 	 *             known host keys cannot be loaded.
415 	 */
416 	protected JSch createDefaultJSch(FS fs) throws JSchException {
417 		final JSch jsch = new JSch();
418 		JSch.setConfig("ssh-rsa", JSch.getConfig("signature.rsa")); //$NON-NLS-1$ //$NON-NLS-2$
419 		JSch.setConfig("ssh-dss", JSch.getConfig("signature.dss")); //$NON-NLS-1$ //$NON-NLS-2$
420 		configureJSch(jsch);
421 		knownHosts(jsch, fs);
422 		identities(jsch, fs);
423 		return jsch;
424 	}
425 
426 	private static void knownHosts(JSch sch, FS fs) throws JSchException {
427 		final File home = fs.userHome();
428 		if (home == null)
429 			return;
430 		final File known_hosts = new File(new File(home, ".ssh"), "known_hosts"); //$NON-NLS-1$ //$NON-NLS-2$
431 		try (FileInputStream in = new FileInputStream(known_hosts)) {
432 			sch.setKnownHosts(in);
433 		} catch (FileNotFoundException none) {
434 			// Oh well. They don't have a known hosts in home.
435 		} catch (IOException err) {
436 			// Oh well. They don't have a known hosts in home.
437 		}
438 	}
439 
440 	private static void identities(JSch sch, FS fs) {
441 		final File home = fs.userHome();
442 		if (home == null)
443 			return;
444 		final File sshdir = new File(home, ".ssh"); //$NON-NLS-1$
445 		if (sshdir.isDirectory()) {
446 			loadIdentity(sch, new File(sshdir, "identity")); //$NON-NLS-1$
447 			loadIdentity(sch, new File(sshdir, "id_rsa")); //$NON-NLS-1$
448 			loadIdentity(sch, new File(sshdir, "id_dsa")); //$NON-NLS-1$
449 		}
450 	}
451 
452 	private static void loadIdentity(JSch sch, File priv) {
453 		if (priv.isFile()) {
454 			try {
455 				sch.addIdentity(priv.getAbsolutePath());
456 			} catch (JSchException e) {
457 				// Instead, pretend the key doesn't exist.
458 			}
459 		}
460 	}
461 
462 	private static class JschBugFixingConfigRepository
463 			implements ConfigRepository {
464 
465 		private final ConfigRepository base;
466 
467 		public JschBugFixingConfigRepository(ConfigRepository base) {
468 			this.base = base;
469 		}
470 
471 		@Override
472 		public Config getConfig(String host) {
473 			return new JschBugFixingConfig(base.getConfig(host));
474 		}
475 
476 		/**
477 		 * A {@link com.jcraft.jsch.ConfigRepository.Config} that transforms
478 		 * some values from the config file into the format Jsch 0.1.54 expects.
479 		 * This is a work-around for bugs in Jsch.
480 		 * <p>
481 		 * Additionally, this config hides the IdentityFile config entries from
482 		 * Jsch; we manage those ourselves. Otherwise Jsch would cache passwords
483 		 * (or rather, decrypted keys) only for a single session, resulting in
484 		 * multiple password prompts for user operations that use several Jsch
485 		 * sessions.
486 		 */
487 		private static class JschBugFixingConfig implements Config {
488 
489 			private static final String[] NO_IDENTITIES = {};
490 
491 			private final Config real;
492 
493 			public JschBugFixingConfig(Config delegate) {
494 				real = delegate;
495 			}
496 
497 			@Override
498 			public String getHostname() {
499 				return real.getHostname();
500 			}
501 
502 			@Override
503 			public String getUser() {
504 				return real.getUser();
505 			}
506 
507 			@Override
508 			public int getPort() {
509 				return real.getPort();
510 			}
511 
512 			@Override
513 			public String getValue(String key) {
514 				String k = key.toUpperCase(Locale.ROOT);
515 				if ("IDENTITYFILE".equals(k)) { //$NON-NLS-1$
516 					return null;
517 				}
518 				String result = real.getValue(key);
519 				if (result != null) {
520 					if ("SERVERALIVEINTERVAL".equals(k) //$NON-NLS-1$
521 							|| "CONNECTTIMEOUT".equals(k)) { //$NON-NLS-1$
522 						// These values are in seconds. Jsch 0.1.54 passes them
523 						// on as is to java.net.Socket.setSoTimeout(), which
524 						// expects milliseconds. So convert here to
525 						// milliseconds.
526 						try {
527 							int timeout = Integer.parseInt(result);
528 							result = Long.toString(
529 									TimeUnit.SECONDS.toMillis(timeout));
530 						} catch (NumberFormatException e) {
531 							// Ignore
532 						}
533 					}
534 				}
535 				return result;
536 			}
537 
538 			@Override
539 			public String[] getValues(String key) {
540 				String k = key.toUpperCase(Locale.ROOT);
541 				if ("IDENTITYFILE".equals(k)) { //$NON-NLS-1$
542 					return NO_IDENTITIES;
543 				}
544 				return real.getValues(key);
545 			}
546 		}
547 	}
548 
549 	/**
550 	 * Set the {@link OpenSshConfig} to use. Intended for use in tests.
551 	 *
552 	 * @param config
553 	 *            to use
554 	 */
555 	void setConfig(OpenSshConfig config) {
556 		this.config = config;
557 	}
558 }