View Javadoc
1   /*
2    * Copyright (C) 2008, 2014, Google Inc.
3    * and other copyright owners as documented in the project's IP log.
4    *
5    * This program and the accompanying materials are made available
6    * under the terms of the Eclipse Distribution License v1.0 which
7    * accompanies this distribution, is reproduced below, and is
8    * available at http://www.eclipse.org/org/documents/edl-v10.php
9    *
10   * All rights reserved.
11   *
12   * Redistribution and use in source and binary forms, with or
13   * without modification, are permitted provided that the following
14   * conditions are met:
15   *
16   * - Redistributions of source code must retain the above copyright
17   *   notice, this list of conditions and the following disclaimer.
18   *
19   * - Redistributions in binary form must reproduce the above
20   *   copyright notice, this list of conditions and the following
21   *   disclaimer in the documentation and/or other materials provided
22   *   with the distribution.
23   *
24   * - Neither the name of the Eclipse Foundation, Inc. nor the
25   *   names of its contributors may be used to endorse or promote
26   *   products derived from this software without specific prior
27   *   written permission.
28   *
29   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
30   * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
31   * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
32   * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
33   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
34   * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
35   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
36   * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
37   * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
38   * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
39   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
40   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
41   * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
42   */
43  
44  package org.eclipse.jgit.transport;
45  
46  import java.io.BufferedReader;
47  import java.io.File;
48  import java.io.FileInputStream;
49  import java.io.FileNotFoundException;
50  import java.io.IOException;
51  import java.io.InputStream;
52  import java.io.InputStreamReader;
53  import java.security.AccessController;
54  import java.security.PrivilegedAction;
55  import java.util.ArrayList;
56  import java.util.Collections;
57  import java.util.LinkedHashMap;
58  import java.util.List;
59  import java.util.Map;
60  
61  import org.eclipse.jgit.errors.InvalidPatternException;
62  import org.eclipse.jgit.fnmatch.FileNameMatcher;
63  import org.eclipse.jgit.lib.Constants;
64  import org.eclipse.jgit.util.FS;
65  import org.eclipse.jgit.util.StringUtils;
66  
67  /**
68   * Simple configuration parser for the OpenSSH ~/.ssh/config file.
69   * <p>
70   * Since JSch does not (currently) have the ability to parse an OpenSSH
71   * configuration file this is a simple parser to read that file and make the
72   * critical options available to {@link SshSessionFactory}.
73   */
74  public class OpenSshConfig {
75  	/** IANA assigned port number for SSH. */
76  	static final int SSH_PORT = 22;
77  
78  	/**
79  	 * Obtain the user's configuration data.
80  	 * <p>
81  	 * The configuration file is always returned to the caller, even if no file
82  	 * exists in the user's home directory at the time the call was made. Lookup
83  	 * requests are cached and are automatically updated if the user modifies
84  	 * the configuration file since the last time it was cached.
85  	 *
86  	 * @param fs
87  	 *            the file system abstraction which will be necessary to
88  	 *            perform certain file system operations.
89  	 * @return a caching reader of the user's configuration file.
90  	 */
91  	public static OpenSshConfig get(FS fs) {
92  		File home = fs.userHome();
93  		if (home == null)
94  			home = new File(".").getAbsoluteFile(); //$NON-NLS-1$
95  
96  		final File config = new File(new File(home, ".ssh"), Constants.CONFIG); //$NON-NLS-1$
97  		final OpenSshConfig osc = new OpenSshConfig(home, config);
98  		osc.refresh();
99  		return osc;
100 	}
101 
102 	/** The user's home directory, as key files may be relative to here. */
103 	private final File home;
104 
105 	/** The .ssh/config file we read and monitor for updates. */
106 	private final File configFile;
107 
108 	/** Modification time of {@link #configFile} when {@link #hosts} loaded. */
109 	private long lastModified;
110 
111 	/** Cached entries read out of the configuration file. */
112 	private Map<String, Host> hosts;
113 
114 	OpenSshConfig(final File h, final File cfg) {
115 		home = h;
116 		configFile = cfg;
117 		hosts = Collections.emptyMap();
118 	}
119 
120 	/**
121 	 * Locate the configuration for a specific host request.
122 	 *
123 	 * @param hostName
124 	 *            the name the user has supplied to the SSH tool. This may be a
125 	 *            real host name, or it may just be a "Host" block in the
126 	 *            configuration file.
127 	 * @return r configuration for the requested name. Never null.
128 	 */
129 	public Host lookup(final String hostName) {
130 		final Map<String, Host> cache = refresh();
131 		Host h = cache.get(hostName);
132 		if (h == null)
133 			h = new Host();
134 		if (h.patternsApplied)
135 			return h;
136 
137 		for (final Map.Entry<String, Host> e : cache.entrySet()) {
138 			if (!isHostPattern(e.getKey()))
139 				continue;
140 			if (!isHostMatch(e.getKey(), hostName))
141 				continue;
142 			h.copyFrom(e.getValue());
143 		}
144 
145 		if (h.hostName == null)
146 			h.hostName = hostName;
147 		if (h.user == null)
148 			h.user = OpenSshConfig.userName();
149 		if (h.port == 0)
150 			h.port = OpenSshConfig.SSH_PORT;
151 		if (h.connectionAttempts == 0)
152 			h.connectionAttempts = 1;
153 		h.patternsApplied = true;
154 		return h;
155 	}
156 
157 	private synchronized Map<String, Host> refresh() {
158 		final long mtime = configFile.lastModified();
159 		if (mtime != lastModified) {
160 			try {
161 				final FileInputStream in = new FileInputStream(configFile);
162 				try {
163 					hosts = parse(in);
164 				} finally {
165 					in.close();
166 				}
167 			} catch (FileNotFoundException none) {
168 				hosts = Collections.emptyMap();
169 			} catch (IOException err) {
170 				hosts = Collections.emptyMap();
171 			}
172 			lastModified = mtime;
173 		}
174 		return hosts;
175 	}
176 
177 	private Map<String, Host> parse(final InputStream in) throws IOException {
178 		final Map<String, Host> m = new LinkedHashMap<String, Host>();
179 		final BufferedReader br = new BufferedReader(new InputStreamReader(in));
180 		final List<Host> current = new ArrayList<Host>(4);
181 		String line;
182 
183 		while ((line = br.readLine()) != null) {
184 			line = line.trim();
185 			if (line.length() == 0 || line.startsWith("#")) //$NON-NLS-1$
186 				continue;
187 
188 			final String[] parts = line.split("[ \t]*[= \t]", 2); //$NON-NLS-1$
189 			final String keyword = parts[0].trim();
190 			final String argValue = parts[1].trim();
191 
192 			if (StringUtils.equalsIgnoreCase("Host", keyword)) { //$NON-NLS-1$
193 				current.clear();
194 				for (final String pattern : argValue.split("[ \t]")) { //$NON-NLS-1$
195 					final String name = dequote(pattern);
196 					Host c = m.get(name);
197 					if (c == null) {
198 						c = new Host();
199 						m.put(name, c);
200 					}
201 					current.add(c);
202 				}
203 				continue;
204 			}
205 
206 			if (current.isEmpty()) {
207 				// We received an option outside of a Host block. We
208 				// don't know who this should match against, so skip.
209 				//
210 				continue;
211 			}
212 
213 			if (StringUtils.equalsIgnoreCase("HostName", keyword)) { //$NON-NLS-1$
214 				for (final Host c : current)
215 					if (c.hostName == null)
216 						c.hostName = dequote(argValue);
217 			} else if (StringUtils.equalsIgnoreCase("User", keyword)) { //$NON-NLS-1$
218 				for (final Host c : current)
219 					if (c.user == null)
220 						c.user = dequote(argValue);
221 			} else if (StringUtils.equalsIgnoreCase("Port", keyword)) { //$NON-NLS-1$
222 				try {
223 					final int port = Integer.parseInt(dequote(argValue));
224 					for (final Host c : current)
225 						if (c.port == 0)
226 							c.port = port;
227 				} catch (NumberFormatException nfe) {
228 					// Bad port number. Don't set it.
229 				}
230 			} else if (StringUtils.equalsIgnoreCase("IdentityFile", keyword)) { //$NON-NLS-1$
231 				for (final Host c : current)
232 					if (c.identityFile == null)
233 						c.identityFile = toFile(dequote(argValue));
234 			} else if (StringUtils.equalsIgnoreCase(
235 					"PreferredAuthentications", keyword)) { //$NON-NLS-1$
236 				for (final Host c : current)
237 					if (c.preferredAuthentications == null)
238 						c.preferredAuthentications = nows(dequote(argValue));
239 			} else if (StringUtils.equalsIgnoreCase("BatchMode", keyword)) { //$NON-NLS-1$
240 				for (final Host c : current)
241 					if (c.batchMode == null)
242 						c.batchMode = yesno(dequote(argValue));
243 			} else if (StringUtils.equalsIgnoreCase(
244 					"StrictHostKeyChecking", keyword)) { //$NON-NLS-1$
245 				String value = dequote(argValue);
246 				for (final Host c : current)
247 					if (c.strictHostKeyChecking == null)
248 						c.strictHostKeyChecking = value;
249 			} else if (StringUtils.equalsIgnoreCase(
250 					"ConnectionAttempts", keyword)) { //$NON-NLS-1$
251 				try {
252 					final int connectionAttempts = Integer.parseInt(dequote(argValue));
253 					if (connectionAttempts > 0) {
254 						for (final Host c : current)
255 							if (c.connectionAttempts == 0)
256 								c.connectionAttempts = connectionAttempts;
257 					}
258 				} catch (NumberFormatException nfe) {
259 					// ignore bad values
260 				}
261 			}
262 		}
263 
264 		return m;
265 	}
266 
267 	private static boolean isHostPattern(final String s) {
268 		return s.indexOf('*') >= 0 || s.indexOf('?') >= 0;
269 	}
270 
271 	private static boolean isHostMatch(final String pattern, final String name) {
272 		final FileNameMatcher fn;
273 		try {
274 			fn = new FileNameMatcher(pattern, null);
275 		} catch (InvalidPatternException e) {
276 			return false;
277 		}
278 		fn.append(name);
279 		return fn.isMatch();
280 	}
281 
282 	private static String dequote(final String value) {
283 		if (value.startsWith("\"") && value.endsWith("\"")) //$NON-NLS-1$ //$NON-NLS-2$
284 			return value.substring(1, value.length() - 1);
285 		return value;
286 	}
287 
288 	private static String nows(final String value) {
289 		final StringBuilder b = new StringBuilder();
290 		for (int i = 0; i < value.length(); i++) {
291 			if (!Character.isSpaceChar(value.charAt(i)))
292 				b.append(value.charAt(i));
293 		}
294 		return b.toString();
295 	}
296 
297 	private static Boolean yesno(final String value) {
298 		if (StringUtils.equalsIgnoreCase("yes", value)) //$NON-NLS-1$
299 			return Boolean.TRUE;
300 		return Boolean.FALSE;
301 	}
302 
303 	private File toFile(final String path) {
304 		if (path.startsWith("~/")) //$NON-NLS-1$
305 			return new File(home, path.substring(2));
306 		File ret = new File(path);
307 		if (ret.isAbsolute())
308 			return ret;
309 		return new File(home, path);
310 	}
311 
312 	static String userName() {
313 		return AccessController.doPrivileged(new PrivilegedAction<String>() {
314 			public String run() {
315 				return System.getProperty("user.name"); //$NON-NLS-1$
316 			}
317 		});
318 	}
319 
320 	/**
321 	 * Configuration of one "Host" block in the configuration file.
322 	 * <p>
323 	 * If returned from {@link OpenSshConfig#lookup(String)} some or all of the
324 	 * properties may not be populated. The properties which are not populated
325 	 * should be defaulted by the caller.
326 	 * <p>
327 	 * When returned from {@link OpenSshConfig#lookup(String)} any wildcard
328 	 * entries which appear later in the configuration file will have been
329 	 * already merged into this block.
330 	 */
331 	public static class Host {
332 		boolean patternsApplied;
333 
334 		String hostName;
335 
336 		int port;
337 
338 		File identityFile;
339 
340 		String user;
341 
342 		String preferredAuthentications;
343 
344 		Boolean batchMode;
345 
346 		String strictHostKeyChecking;
347 
348 		int connectionAttempts;
349 
350 		void copyFrom(final Host src) {
351 			if (hostName == null)
352 				hostName = src.hostName;
353 			if (port == 0)
354 				port = src.port;
355 			if (identityFile == null)
356 				identityFile = src.identityFile;
357 			if (user == null)
358 				user = src.user;
359 			if (preferredAuthentications == null)
360 				preferredAuthentications = src.preferredAuthentications;
361 			if (batchMode == null)
362 				batchMode = src.batchMode;
363 			if (strictHostKeyChecking == null)
364 				strictHostKeyChecking = src.strictHostKeyChecking;
365 			if (connectionAttempts == 0)
366 				connectionAttempts = src.connectionAttempts;
367 		}
368 
369 		/**
370 		 * @return the value StrictHostKeyChecking property, the valid values
371 		 *         are "yes" (unknown hosts are not accepted), "no" (unknown
372 		 *         hosts are always accepted), and "ask" (user should be asked
373 		 *         before accepting the host)
374 		 */
375 		public String getStrictHostKeyChecking() {
376 			return strictHostKeyChecking;
377 		}
378 
379 		/**
380 		 * @return the real IP address or host name to connect to; never null.
381 		 */
382 		public String getHostName() {
383 			return hostName;
384 		}
385 
386 		/**
387 		 * @return the real port number to connect to; never 0.
388 		 */
389 		public int getPort() {
390 			return port;
391 		}
392 
393 		/**
394 		 * @return path of the private key file to use for authentication; null
395 		 *         if the caller should use default authentication strategies.
396 		 */
397 		public File getIdentityFile() {
398 			return identityFile;
399 		}
400 
401 		/**
402 		 * @return the real user name to connect as; never null.
403 		 */
404 		public String getUser() {
405 			return user;
406 		}
407 
408 		/**
409 		 * @return the preferred authentication methods, separated by commas if
410 		 *         more than one authentication method is preferred.
411 		 */
412 		public String getPreferredAuthentications() {
413 			return preferredAuthentications;
414 		}
415 
416 		/**
417 		 * @return true if batch (non-interactive) mode is preferred for this
418 		 *         host connection.
419 		 */
420 		public boolean isBatchMode() {
421 			return batchMode != null && batchMode.booleanValue();
422 		}
423 
424 		/**
425 		 * @return the number of tries (one per second) to connect before
426 		 *         exiting. The argument must be an integer. This may be useful
427 		 *         in scripts if the connection sometimes fails. The default is
428 		 *         1.
429 		 * @since 3.4
430 		 */
431 		public int getConnectionAttempts() {
432 			return connectionAttempts;
433 		}
434 	}
435 }