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.Map;
63  
64  import org.eclipse.jgit.errors.TransportException;
65  import org.eclipse.jgit.internal.JGitText;
66  import org.eclipse.jgit.util.FS;
67  import org.slf4j.Logger;
68  import org.slf4j.LoggerFactory;
69  
70  import com.jcraft.jsch.JSch;
71  import com.jcraft.jsch.JSchException;
72  import com.jcraft.jsch.Session;
73  
74  /**
75   * The base session factory that loads known hosts and private keys from
76   * <code>$HOME/.ssh</code>.
77   * <p>
78   * This is the default implementation used by JGit and provides most of the
79   * compatibility necessary to match OpenSSH, a popular implementation of SSH
80   * used by C Git.
81   * <p>
82   * The factory does not provide UI behavior. Override the method
83   * {@link #configure(org.eclipse.jgit.transport.OpenSshConfig.Host, Session)} to
84   * supply appropriate {@link com.jcraft.jsch.UserInfo} to the session.
85   */
86  public abstract class JschConfigSessionFactory extends SshSessionFactory {
87  
88  	private static final Logger LOG = LoggerFactory
89  			.getLogger(JschConfigSessionFactory.class);
90  
91  	private final Map<String, JSch> byIdentityFile = new HashMap<>();
92  
93  	private JSch defaultJSch;
94  
95  	private OpenSshConfig config;
96  
97  	/** {@inheritDoc} */
98  	@Override
99  	public synchronized RemoteSession getSession(URIish uri,
100 			CredentialsProvider credentialsProvider, FS fs, int tms)
101 			throws TransportException {
102 
103 		String user = uri.getUser();
104 		final String pass = uri.getPass();
105 		String host = uri.getHost();
106 		int port = uri.getPort();
107 
108 		try {
109 			if (config == null)
110 				config = OpenSshConfig.get(fs);
111 
112 			final OpenSshConfig.Host hc = config.lookup(host);
113 			host = hc.getHostName();
114 			if (port <= 0)
115 				port = hc.getPort();
116 			if (user == null)
117 				user = hc.getUser();
118 
119 			Session session = createSession(credentialsProvider, fs, user,
120 					pass, host, port, hc);
121 
122 			int retries = 0;
123 			while (!session.isConnected()) {
124 				try {
125 					retries++;
126 					session.connect(tms);
127 				} catch (JSchException e) {
128 					session.disconnect();
129 					session = null;
130 					// Make sure our known_hosts is not outdated
131 					knownHosts(getJSch(hc, fs), fs);
132 
133 					if (isAuthenticationCanceled(e)) {
134 						throw e;
135 					} else if (isAuthenticationFailed(e)
136 							&& credentialsProvider != null) {
137 						// if authentication failed maybe credentials changed at
138 						// the remote end therefore reset credentials and retry
139 						if (retries < 3) {
140 							credentialsProvider.reset(uri);
141 							session = createSession(credentialsProvider, fs,
142 									user, pass, host, port, hc);
143 						} else
144 							throw e;
145 					} else if (retries >= hc.getConnectionAttempts()) {
146 						throw e;
147 					} else {
148 						try {
149 							Thread.sleep(1000);
150 							session = createSession(credentialsProvider, fs,
151 									user, pass, host, port, hc);
152 						} catch (InterruptedException e1) {
153 							throw new TransportException(
154 									JGitText.get().transportSSHRetryInterrupt,
155 									e1);
156 						}
157 					}
158 				}
159 			}
160 
161 			return new JschSession(session, uri);
162 
163 		} catch (JSchException je) {
164 			final Throwable c = je.getCause();
165 			if (c instanceof UnknownHostException) {
166 				throw new TransportException(uri, JGitText.get().unknownHost,
167 						je);
168 			}
169 			if (c instanceof ConnectException) {
170 				throw new TransportException(uri, c.getMessage(), je);
171 			}
172 			throw new TransportException(uri, je.getMessage(), je);
173 		}
174 
175 	}
176 
177 	private static boolean isAuthenticationFailed(JSchException e) {
178 		return e.getCause() == null && e.getMessage().equals("Auth fail"); //$NON-NLS-1$
179 	}
180 
181 	private static boolean isAuthenticationCanceled(JSchException e) {
182 		return e.getCause() == null && e.getMessage().equals("Auth cancel"); //$NON-NLS-1$
183 	}
184 
185 	private Session createSession(CredentialsProvider credentialsProvider,
186 			FS fs, String user, final String pass, String host, int port,
187 			final OpenSshConfig.Host hc) throws JSchException {
188 		final Session session = createSession(hc, user, host, port, fs);
189 		// Jsch will have overridden the explicit user by the one from the SSH
190 		// config file...
191 		setUserName(session, user);
192 		// We retry already in getSession() method. JSch must not retry
193 		// on its own.
194 		session.setConfig("MaxAuthTries", "1"); //$NON-NLS-1$ //$NON-NLS-2$
195 		if (pass != null)
196 			session.setPassword(pass);
197 		final String strictHostKeyCheckingPolicy = hc
198 				.getStrictHostKeyChecking();
199 		if (strictHostKeyCheckingPolicy != null)
200 			session.setConfig("StrictHostKeyChecking", //$NON-NLS-1$
201 					strictHostKeyCheckingPolicy);
202 		final String pauth = hc.getPreferredAuthentications();
203 		if (pauth != null)
204 			session.setConfig("PreferredAuthentications", pauth); //$NON-NLS-1$
205 		if (credentialsProvider != null
206 				&& (!hc.isBatchMode() || !credentialsProvider.isInteractive())) {
207 			session.setUserInfo(new CredentialsProviderUserInfo(session,
208 					credentialsProvider));
209 		}
210 		configure(hc, session);
211 		return session;
212 	}
213 
214 	private void setUserName(Session session, String userName) {
215 		// Jsch 0.1.54 picks up the user name from the ssh config, even if an
216 		// explicit user name was given! We must correct that if ~/.ssh/config
217 		// has a different user name.
218 		if (userName == null || userName.isEmpty()
219 				|| userName.equals(session.getUserName())) {
220 			return;
221 		}
222 		try {
223 			Class<?>[] parameterTypes = { String.class };
224 			Method method = Session.class.getDeclaredMethod("setUserName", //$NON-NLS-1$
225 					parameterTypes);
226 			method.setAccessible(true);
227 			method.invoke(session, userName);
228 		} catch (NullPointerException | IllegalAccessException
229 				| IllegalArgumentException | InvocationTargetException
230 				| NoSuchMethodException | SecurityException e) {
231 			LOG.error(MessageFormat.format(JGitText.get().sshUserNameError,
232 					userName, session.getUserName()), e);
233 		}
234 	}
235 
236 	/**
237 	 * Create a new remote session for the requested address.
238 	 *
239 	 * @param hc
240 	 *            host configuration
241 	 * @param user
242 	 *            login to authenticate as.
243 	 * @param host
244 	 *            server name to connect to.
245 	 * @param port
246 	 *            port number of the SSH daemon (typically 22).
247 	 * @param fs
248 	 *            the file system abstraction which will be necessary to
249 	 *            perform certain file system operations.
250 	 * @return new session instance, but otherwise unconfigured.
251 	 * @throws com.jcraft.jsch.JSchException
252 	 *             the session could not be created.
253 	 */
254 	protected Session createSession(final OpenSshConfig.Host hc,
255 			final String user, final String host, final int port, FS fs)
256 			throws JSchException {
257 		return getJSch(hc, fs).getSession(user, host, port);
258 	}
259 
260 	/**
261 	 * Provide additional configuration for the JSch instance. This method could
262 	 * be overridden to supply a preferred
263 	 * {@link com.jcraft.jsch.IdentityRepository}.
264 	 *
265 	 * @param jsch
266 	 *            jsch instance
267 	 * @since 4.5
268 	 */
269 	protected void configureJSch(JSch jsch) {
270 		// No additional configuration required.
271 	}
272 
273 	/**
274 	 * Provide additional configuration for the session based on the host
275 	 * information. This method could be used to supply
276 	 * {@link com.jcraft.jsch.UserInfo}.
277 	 *
278 	 * @param hc
279 	 *            host configuration
280 	 * @param session
281 	 *            session to configure
282 	 */
283 	protected abstract void configure(OpenSshConfig.Host hc, Session session);
284 
285 	/**
286 	 * Obtain the JSch used to create new sessions.
287 	 *
288 	 * @param hc
289 	 *            host configuration
290 	 * @param fs
291 	 *            the file system abstraction which will be necessary to
292 	 *            perform certain file system operations.
293 	 * @return the JSch instance to use.
294 	 * @throws com.jcraft.jsch.JSchException
295 	 *             the user configuration could not be created.
296 	 */
297 	protected JSch getJSch(final OpenSshConfig.Host hc, FS fs) throws JSchException {
298 		if (defaultJSch == null) {
299 			defaultJSch = createDefaultJSch(fs);
300 			if (defaultJSch.getConfigRepository() == null) {
301 				defaultJSch.setConfigRepository(config);
302 			}
303 			for (Object name : defaultJSch.getIdentityNames())
304 				byIdentityFile.put((String) name, defaultJSch);
305 		}
306 
307 		final File identityFile = hc.getIdentityFile();
308 		if (identityFile == null)
309 			return defaultJSch;
310 
311 		final String identityKey = identityFile.getAbsolutePath();
312 		JSch jsch = byIdentityFile.get(identityKey);
313 		if (jsch == null) {
314 			jsch = new JSch();
315 			configureJSch(jsch);
316 			if (jsch.getConfigRepository() == null) {
317 				jsch.setConfigRepository(defaultJSch.getConfigRepository());
318 			}
319 			jsch.setHostKeyRepository(defaultJSch.getHostKeyRepository());
320 			jsch.addIdentity(identityKey);
321 			byIdentityFile.put(identityKey, jsch);
322 		}
323 		return jsch;
324 	}
325 
326 	/**
327 	 * Create default instance of jsch
328 	 *
329 	 * @param fs
330 	 *            the file system abstraction which will be necessary to perform
331 	 *            certain file system operations.
332 	 * @return the new default JSch implementation.
333 	 * @throws com.jcraft.jsch.JSchException
334 	 *             known host keys cannot be loaded.
335 	 */
336 	protected JSch createDefaultJSch(FS fs) throws JSchException {
337 		final JSch jsch = new JSch();
338 		configureJSch(jsch);
339 		knownHosts(jsch, fs);
340 		identities(jsch, fs);
341 		return jsch;
342 	}
343 
344 	private static void knownHosts(final JSch sch, FS fs) throws JSchException {
345 		final File home = fs.userHome();
346 		if (home == null)
347 			return;
348 		final File known_hosts = new File(new File(home, ".ssh"), "known_hosts"); //$NON-NLS-1$ //$NON-NLS-2$
349 		try {
350 			final FileInputStream in = new FileInputStream(known_hosts);
351 			try {
352 				sch.setKnownHosts(in);
353 			} finally {
354 				in.close();
355 			}
356 		} catch (FileNotFoundException none) {
357 			// Oh well. They don't have a known hosts in home.
358 		} catch (IOException err) {
359 			// Oh well. They don't have a known hosts in home.
360 		}
361 	}
362 
363 	private static void identities(final JSch sch, FS fs) {
364 		final File home = fs.userHome();
365 		if (home == null)
366 			return;
367 		final File sshdir = new File(home, ".ssh"); //$NON-NLS-1$
368 		if (sshdir.isDirectory()) {
369 			loadIdentity(sch, new File(sshdir, "identity")); //$NON-NLS-1$
370 			loadIdentity(sch, new File(sshdir, "id_rsa")); //$NON-NLS-1$
371 			loadIdentity(sch, new File(sshdir, "id_dsa")); //$NON-NLS-1$
372 		}
373 	}
374 
375 	private static void loadIdentity(final JSch sch, final File priv) {
376 		if (priv.isFile()) {
377 			try {
378 				sch.addIdentity(priv.getAbsolutePath());
379 			} catch (JSchException e) {
380 				// Instead, pretend the key doesn't exist.
381 			}
382 		}
383 	}
384 }