View Javadoc
1   /*
2    * Copyright (C) 2009, Constantine Plotnikov <constantine.plotnikov@gmail.com>
3    * Copyright (C) 2008-2009, Google Inc.
4    * Copyright (C) 2009, Google, Inc.
5    * Copyright (C) 2009, JetBrains s.r.o.
6    * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com>
7    * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org>
8    * and other copyright owners as documented in the project's IP log.
9    *
10   * This program and the accompanying materials are made available
11   * under the terms of the Eclipse Distribution License v1.0 which
12   * accompanies this distribution, is reproduced below, and is
13   * available at http://www.eclipse.org/org/documents/edl-v10.php
14   *
15   * All rights reserved.
16   *
17   * Redistribution and use in source and binary forms, with or
18   * without modification, are permitted provided that the following
19   * conditions are met:
20   *
21   * - Redistributions of source code must retain the above copyright
22   *   notice, this list of conditions and the following disclaimer.
23   *
24   * - Redistributions in binary form must reproduce the above
25   *   copyright notice, this list of conditions and the following
26   *   disclaimer in the documentation and/or other materials provided
27   *   with the distribution.
28   *
29   * - Neither the name of the Eclipse Foundation, Inc. nor the
30   *   names of its contributors may be used to endorse or promote
31   *   products derived from this software without specific prior
32   *   written permission.
33   *
34   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
35   * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
36   * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
37   * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
38   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
39   * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
40   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
41   * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
42   * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
43   * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
44   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
45   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
46   * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
47   */
48  
49  package org.eclipse.jgit.transport;
50  
51  import java.io.File;
52  import java.io.FileInputStream;
53  import java.io.FileNotFoundException;
54  import java.io.IOException;
55  import java.net.ConnectException;
56  import java.net.UnknownHostException;
57  import java.util.HashMap;
58  import java.util.Map;
59  
60  import org.eclipse.jgit.errors.TransportException;
61  import org.eclipse.jgit.internal.JGitText;
62  import org.eclipse.jgit.util.FS;
63  
64  import com.jcraft.jsch.JSch;
65  import com.jcraft.jsch.JSchException;
66  import com.jcraft.jsch.Session;
67  import com.jcraft.jsch.UserInfo;
68  
69  /**
70   * The base session factory that loads known hosts and private keys from
71   * <code>$HOME/.ssh</code>.
72   * <p>
73   * This is the default implementation used by JGit and provides most of the
74   * compatibility necessary to match OpenSSH, a popular implementation of SSH
75   * used by C Git.
76   * <p>
77   * The factory does not provide UI behavior. Override the method
78   * {@link #configure(org.eclipse.jgit.transport.OpenSshConfig.Host, Session)}
79   * to supply appropriate {@link UserInfo} to the session.
80   */
81  public abstract class JschConfigSessionFactory extends SshSessionFactory {
82  	private final Map<String, JSch> byIdentityFile = new HashMap<String, JSch>();
83  
84  	private JSch defaultJSch;
85  
86  	private OpenSshConfig config;
87  
88  	@Override
89  	public synchronized RemoteSession getSession(URIish uri,
90  			CredentialsProvider credentialsProvider, FS fs, int tms)
91  			throws TransportException {
92  
93  		String user = uri.getUser();
94  		final String pass = uri.getPass();
95  		String host = uri.getHost();
96  		int port = uri.getPort();
97  
98  		try {
99  			if (config == null)
100 				config = OpenSshConfig.get(fs);
101 
102 			final OpenSshConfig.Host hc = config.lookup(host);
103 			host = hc.getHostName();
104 			if (port <= 0)
105 				port = hc.getPort();
106 			if (user == null)
107 				user = hc.getUser();
108 
109 			Session session = createSession(credentialsProvider, fs, user,
110 					pass, host, port, hc);
111 
112 			int retries = 0;
113 			while (!session.isConnected()) {
114 				try {
115 					retries++;
116 					session.connect(tms);
117 				} catch (JSchException e) {
118 					session.disconnect();
119 					session = null;
120 					// Make sure our known_hosts is not outdated
121 					knownHosts(getJSch(hc, fs), fs);
122 
123 					if (isAuthenticationCanceled(e)) {
124 						throw e;
125 					} else if (isAuthenticationFailed(e)
126 							&& credentialsProvider != null) {
127 						// if authentication failed maybe credentials changed at
128 						// the remote end therefore reset credentials and retry
129 						if (retries < 3) {
130 							credentialsProvider.reset(uri);
131 							session = createSession(credentialsProvider, fs,
132 									user, pass, host, port, hc);
133 						} else
134 							throw e;
135 					} else if (retries >= hc.getConnectionAttempts()) {
136 						throw e;
137 					} else {
138 						try {
139 							Thread.sleep(1000);
140 							session = createSession(credentialsProvider, fs,
141 									user, pass, host, port, hc);
142 						} catch (InterruptedException e1) {
143 							throw new TransportException(
144 									JGitText.get().transportSSHRetryInterrupt,
145 									e1);
146 						}
147 					}
148 				}
149 			}
150 
151 			return new JschSession(session, uri);
152 
153 		} catch (JSchException je) {
154 			final Throwable c = je.getCause();
155 			if (c instanceof UnknownHostException)
156 				throw new TransportException(uri, JGitText.get().unknownHost);
157 			if (c instanceof ConnectException)
158 				throw new TransportException(uri, c.getMessage());
159 			throw new TransportException(uri, je.getMessage(), je);
160 		}
161 
162 	}
163 
164 	private static boolean isAuthenticationFailed(JSchException e) {
165 		return e.getCause() == null && e.getMessage().equals("Auth fail"); //$NON-NLS-1$
166 	}
167 
168 	private static boolean isAuthenticationCanceled(JSchException e) {
169 		return e.getCause() == null && e.getMessage().equals("Auth cancel"); //$NON-NLS-1$
170 	}
171 
172 	private 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 		// We retry already in getSession() method. JSch must not retry
177 		// on its own.
178 		session.setConfig("MaxAuthTries", "1"); //$NON-NLS-1$ //$NON-NLS-2$
179 		if (pass != null)
180 			session.setPassword(pass);
181 		final String strictHostKeyCheckingPolicy = hc
182 				.getStrictHostKeyChecking();
183 		if (strictHostKeyCheckingPolicy != null)
184 			session.setConfig("StrictHostKeyChecking", //$NON-NLS-1$
185 					strictHostKeyCheckingPolicy);
186 		final String pauth = hc.getPreferredAuthentications();
187 		if (pauth != null)
188 			session.setConfig("PreferredAuthentications", pauth); //$NON-NLS-1$
189 		if (credentialsProvider != null
190 				&& (!hc.isBatchMode() || !credentialsProvider.isInteractive())) {
191 			session.setUserInfo(new CredentialsProviderUserInfo(session,
192 					credentialsProvider));
193 		}
194 		configure(hc, session);
195 		return session;
196 	}
197 
198 	/**
199 	 * Create a new remote session for the requested address.
200 	 *
201 	 * @param hc
202 	 *            host configuration
203 	 * @param user
204 	 *            login to authenticate as.
205 	 * @param host
206 	 *            server name to connect to.
207 	 * @param port
208 	 *            port number of the SSH daemon (typically 22).
209 	 * @param fs
210 	 *            the file system abstraction which will be necessary to
211 	 *            perform certain file system operations.
212 	 * @return new session instance, but otherwise unconfigured.
213 	 * @throws JSchException
214 	 *             the session could not be created.
215 	 */
216 	protected Session createSession(final OpenSshConfig.Host hc,
217 			final String user, final String host, final int port, FS fs)
218 			throws JSchException {
219 		return getJSch(hc, fs).getSession(user, host, port);
220 	}
221 
222 	/**
223 	 * Provide additional configuration for the session based on the host
224 	 * information. This method could be used to supply {@link UserInfo}.
225 	 *
226 	 * @param hc
227 	 *            host configuration
228 	 * @param session
229 	 *            session to configure
230 	 */
231 	protected abstract void configure(OpenSshConfig.Host hc, Session session);
232 
233 	/**
234 	 * Obtain the JSch used to create new sessions.
235 	 *
236 	 * @param hc
237 	 *            host configuration
238 	 * @param fs
239 	 *            the file system abstraction which will be necessary to
240 	 *            perform certain file system operations.
241 	 * @return the JSch instance to use.
242 	 * @throws JSchException
243 	 *             the user configuration could not be created.
244 	 */
245 	protected JSch getJSch(final OpenSshConfig.Host hc, FS fs) throws JSchException {
246 		if (defaultJSch == null) {
247 			defaultJSch = createDefaultJSch(fs);
248 			for (Object name : defaultJSch.getIdentityNames())
249 				byIdentityFile.put((String) name, defaultJSch);
250 		}
251 
252 		final File identityFile = hc.getIdentityFile();
253 		if (identityFile == null)
254 			return defaultJSch;
255 
256 		final String identityKey = identityFile.getAbsolutePath();
257 		JSch jsch = byIdentityFile.get(identityKey);
258 		if (jsch == null) {
259 			jsch = new JSch();
260 			jsch.setHostKeyRepository(defaultJSch.getHostKeyRepository());
261 			jsch.addIdentity(identityKey);
262 			byIdentityFile.put(identityKey, jsch);
263 		}
264 		return jsch;
265 	}
266 
267 	/**
268 	 * @param fs
269 	 *            the file system abstraction which will be necessary to
270 	 *            perform certain file system operations.
271 	 * @return the new default JSch implementation.
272 	 * @throws JSchException
273 	 *             known host keys cannot be loaded.
274 	 */
275 	protected JSch createDefaultJSch(FS fs) throws JSchException {
276 		final JSch jsch = new JSch();
277 		knownHosts(jsch, fs);
278 		identities(jsch, fs);
279 		return jsch;
280 	}
281 
282 	private static void knownHosts(final JSch sch, FS fs) throws JSchException {
283 		final File home = fs.userHome();
284 		if (home == null)
285 			return;
286 		final File known_hosts = new File(new File(home, ".ssh"), "known_hosts"); //$NON-NLS-1$ //$NON-NLS-2$
287 		try {
288 			final FileInputStream in = new FileInputStream(known_hosts);
289 			try {
290 				sch.setKnownHosts(in);
291 			} finally {
292 				in.close();
293 			}
294 		} catch (FileNotFoundException none) {
295 			// Oh well. They don't have a known hosts in home.
296 		} catch (IOException err) {
297 			// Oh well. They don't have a known hosts in home.
298 		}
299 	}
300 
301 	private static void identities(final JSch sch, FS fs) {
302 		final File home = fs.userHome();
303 		if (home == null)
304 			return;
305 		final File sshdir = new File(home, ".ssh"); //$NON-NLS-1$
306 		if (sshdir.isDirectory()) {
307 			loadIdentity(sch, new File(sshdir, "identity")); //$NON-NLS-1$
308 			loadIdentity(sch, new File(sshdir, "id_rsa")); //$NON-NLS-1$
309 			loadIdentity(sch, new File(sshdir, "id_dsa")); //$NON-NLS-1$
310 		}
311 	}
312 
313 	private static void loadIdentity(final JSch sch, final File priv) {
314 		if (priv.isFile()) {
315 			try {
316 				sch.addIdentity(priv.getAbsolutePath());
317 			} catch (JSchException e) {
318 				// Instead, pretend the key doesn't exist.
319 			}
320 		}
321 	}
322 }