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