View Javadoc
1   /*
2    * Copyright (C) 2008, 2018, Google Inc. and others
3    *
4    * This program and the accompanying materials are made available under the
5    * terms of the Eclipse Distribution License v. 1.0 which is available at
6    * https://www.eclipse.org/org/documents/edl-v10.php.
7    *
8    * SPDX-License-Identifier: BSD-3-Clause
9    */
10  
11  package org.eclipse.jgit.transport.ssh.jsch;
12  
13  import static org.eclipse.jgit.internal.transport.ssh.OpenSshConfigFile.positive;
14  
15  import java.io.File;
16  import java.util.List;
17  import java.util.Map;
18  import java.util.TreeMap;
19  
20  import org.eclipse.jgit.internal.transport.ssh.OpenSshConfigFile;
21  import org.eclipse.jgit.internal.transport.ssh.OpenSshConfigFile.HostEntry;
22  import org.eclipse.jgit.transport.SshConstants;
23  import org.eclipse.jgit.transport.SshSessionFactory;
24  import org.eclipse.jgit.util.FS;
25  
26  import com.jcraft.jsch.ConfigRepository;
27  
28  /**
29   * Fairly complete configuration parser for the OpenSSH ~/.ssh/config file.
30   * <p>
31   * JSch does have its own config file parser
32   * {@link com.jcraft.jsch.OpenSSHConfig} since version 0.1.50, but it has a
33   * number of problems:
34   * <ul>
35   * <li>it splits lines of the format "keyword = value" wrongly: you'd end up
36   * with the value "= value".
37   * <li>its "Host" keyword is not case insensitive.
38   * <li>it doesn't handle quoted values.
39   * <li>JSch's OpenSSHConfig doesn't monitor for config file changes.
40   * </ul>
41   * <p>
42   * This parser makes the critical options available to
43   * {@link org.eclipse.jgit.transport.SshSessionFactory} via
44   * {@link org.eclipse.jgit.transport.ssh.jsch.OpenSshConfig.Host} objects returned
45   * by {@link #lookup(String)}, and implements a fully conforming
46   * {@link com.jcraft.jsch.ConfigRepository} providing
47   * {@link com.jcraft.jsch.ConfigRepository.Config}s via
48   * {@link #getConfig(String)}.
49   * </p>
50   *
51   * @see OpenSshConfigFile
52   * @since 6.0
53   */
54  public class OpenSshConfig implements ConfigRepository {
55  
56  	/**
57  	 * Obtain the user's configuration data.
58  	 * <p>
59  	 * The configuration file is always returned to the caller, even if no file
60  	 * exists in the user's home directory at the time the call was made. Lookup
61  	 * requests are cached and are automatically updated if the user modifies
62  	 * the configuration file since the last time it was cached.
63  	 *
64  	 * @param fs
65  	 *            the file system abstraction which will be necessary to
66  	 *            perform certain file system operations.
67  	 * @return a caching reader of the user's configuration file.
68  	 */
69  	public static OpenSshConfig get(FS fs) {
70  		File home = fs.userHome();
71  		if (home == null)
72  			home = new File(".").getAbsoluteFile(); //$NON-NLS-1$
73  
74  		final File config = new File(new File(home, SshConstants.SSH_DIR),
75  				SshConstants.CONFIG);
76  		return new OpenSshConfig(home, config);
77  	}
78  
79  	/** The base file. */
80  	private OpenSshConfigFile configFile;
81  
82  	/**
83  	 * Create an OpenSshConfig
84  	 *
85  	 * @param h
86  	 *            user's home directory
87  	 * @param cfg
88  	 *            ssh configuration file
89  	 */
90  	public OpenSshConfig(File h, File cfg) {
91  		configFile = new OpenSshConfigFile(h, cfg,
92  				SshSessionFactory.getLocalUserName());
93  	}
94  
95  	/**
96  	 * Locate the configuration for a specific host request.
97  	 *
98  	 * @param hostName
99  	 *            the name the user has supplied to the SSH tool. This may be a
100 	 *            real host name, or it may just be a "Host" block in the
101 	 *            configuration file.
102 	 * @return r configuration for the requested name. Never null.
103 	 */
104 	public Host lookup(String hostName) {
105 		HostEntry entry = configFile.lookup(hostName, -1, null);
106 		return new Host(entry, hostName, configFile.getLocalUserName());
107 	}
108 
109 	/**
110 	 * Configuration of one "Host" block in the configuration file.
111 	 * <p>
112 	 * If returned from {@link OpenSshConfig#lookup(String)} some or all of the
113 	 * properties may not be populated. The properties which are not populated
114 	 * should be defaulted by the caller.
115 	 * <p>
116 	 * When returned from {@link OpenSshConfig#lookup(String)} any wildcard
117 	 * entries which appear later in the configuration file will have been
118 	 * already merged into this block.
119 	 */
120 	public static class Host {
121 		String hostName;
122 
123 		int port;
124 
125 		File identityFile;
126 
127 		String user;
128 
129 		String preferredAuthentications;
130 
131 		Boolean batchMode;
132 
133 		String strictHostKeyChecking;
134 
135 		int connectionAttempts;
136 
137 		private HostEntry entry;
138 
139 		private Config config;
140 
141 		// See com.jcraft.jsch.OpenSSHConfig. Translates some command-line keys
142 		// to ssh-config keys.
143 		private static final Map<String, String> KEY_MAP = new TreeMap<>(
144 				String.CASE_INSENSITIVE_ORDER);
145 
146 		static {
147 			KEY_MAP.put("kex", SshConstants.KEX_ALGORITHMS); //$NON-NLS-1$
148 			KEY_MAP.put("server_host_key", SshConstants.HOST_KEY_ALGORITHMS); //$NON-NLS-1$
149 			KEY_MAP.put("cipher.c2s", SshConstants.CIPHERS); //$NON-NLS-1$
150 			KEY_MAP.put("cipher.s2c", SshConstants.CIPHERS); //$NON-NLS-1$
151 			KEY_MAP.put("mac.c2s", SshConstants.MACS); //$NON-NLS-1$
152 			KEY_MAP.put("mac.s2c", SshConstants.MACS); //$NON-NLS-1$
153 			KEY_MAP.put("compression.s2c", SshConstants.COMPRESSION); //$NON-NLS-1$
154 			KEY_MAP.put("compression.c2s", SshConstants.COMPRESSION); //$NON-NLS-1$
155 			KEY_MAP.put("compression_level", "CompressionLevel"); //$NON-NLS-1$ //$NON-NLS-2$
156 			KEY_MAP.put("MaxAuthTries", //$NON-NLS-1$
157 					SshConstants.NUMBER_OF_PASSWORD_PROMPTS);
158 		}
159 
160 		private static String mapKey(String key) {
161 			String k = KEY_MAP.get(key);
162 			return k != null ? k : key;
163 		}
164 
165 		/**
166 		 * Creates a new uninitialized {@link Host}.
167 		 */
168 		public Host() {
169 			// For API backwards compatibility with pre-4.9 JGit
170 		}
171 
172 		Host(HostEntry entry, String hostName, String localUserName) {
173 			this.entry = entry;
174 			complete(hostName, localUserName);
175 		}
176 
177 		/**
178 		 * @return the value StrictHostKeyChecking property, the valid values
179 		 *         are "yes" (unknown hosts are not accepted), "no" (unknown
180 		 *         hosts are always accepted), and "ask" (user should be asked
181 		 *         before accepting the host)
182 		 */
183 		public String getStrictHostKeyChecking() {
184 			return strictHostKeyChecking;
185 		}
186 
187 		/**
188 		 * @return the real IP address or host name to connect to; never null.
189 		 */
190 		public String getHostName() {
191 			return hostName;
192 		}
193 
194 		/**
195 		 * @return the real port number to connect to; never 0.
196 		 */
197 		public int getPort() {
198 			return port;
199 		}
200 
201 		/**
202 		 * @return path of the private key file to use for authentication; null
203 		 *         if the caller should use default authentication strategies.
204 		 */
205 		public File getIdentityFile() {
206 			return identityFile;
207 		}
208 
209 		/**
210 		 * @return the real user name to connect as; never null.
211 		 */
212 		public String getUser() {
213 			return user;
214 		}
215 
216 		/**
217 		 * @return the preferred authentication methods, separated by commas if
218 		 *         more than one authentication method is preferred.
219 		 */
220 		public String getPreferredAuthentications() {
221 			return preferredAuthentications;
222 		}
223 
224 		/**
225 		 * @return true if batch (non-interactive) mode is preferred for this
226 		 *         host connection.
227 		 */
228 		public boolean isBatchMode() {
229 			return batchMode != null && batchMode.booleanValue();
230 		}
231 
232 		/**
233 		 * @return the number of tries (one per second) to connect before
234 		 *         exiting. The argument must be an integer. This may be useful
235 		 *         in scripts if the connection sometimes fails. The default is
236 		 *         1.
237 		 * @since 3.4
238 		 */
239 		public int getConnectionAttempts() {
240 			return connectionAttempts;
241 		}
242 
243 
244 		private void complete(String initialHostName, String localUserName) {
245 			// Try to set values from the options.
246 			hostName = entry.getValue(SshConstants.HOST_NAME);
247 			user = entry.getValue(SshConstants.USER);
248 			port = positive(entry.getValue(SshConstants.PORT));
249 			connectionAttempts = positive(
250 					entry.getValue(SshConstants.CONNECTION_ATTEMPTS));
251 			strictHostKeyChecking = entry
252 					.getValue(SshConstants.STRICT_HOST_KEY_CHECKING);
253 			batchMode = Boolean.valueOf(OpenSshConfigFile
254 					.flag(entry.getValue(SshConstants.BATCH_MODE)));
255 			preferredAuthentications = entry
256 					.getValue(SshConstants.PREFERRED_AUTHENTICATIONS);
257 			// Fill in defaults if still not set
258 			if (hostName == null || hostName.isEmpty()) {
259 				hostName = initialHostName;
260 			}
261 			if (user == null || user.isEmpty()) {
262 				user = localUserName;
263 			}
264 			if (port <= 0) {
265 				port = SshConstants.SSH_DEFAULT_PORT;
266 			}
267 			if (connectionAttempts <= 0) {
268 				connectionAttempts = 1;
269 			}
270 			List<String> identityFiles = entry
271 					.getValues(SshConstants.IDENTITY_FILE);
272 			if (identityFiles != null && !identityFiles.isEmpty()) {
273 				identityFile = new File(identityFiles.get(0));
274 			}
275 		}
276 
277 		/**
278 		 * Get the ssh configuration
279 		 *
280 		 * @return the ssh configuration
281 		 */
282 		public Config getConfig() {
283 			if (config == null) {
284 				config = new Config() {
285 
286 					@Override
287 					public String getHostname() {
288 						return Host.this.getHostName();
289 					}
290 
291 					@Override
292 					public String getUser() {
293 						return Host.this.getUser();
294 					}
295 
296 					@Override
297 					public int getPort() {
298 						return Host.this.getPort();
299 					}
300 
301 					@Override
302 					public String getValue(String key) {
303 						// See com.jcraft.jsch.OpenSSHConfig.MyConfig.getValue()
304 						// for this special case.
305 						if (key.equals("compression.s2c") //$NON-NLS-1$
306 								|| key.equals("compression.c2s")) { //$NON-NLS-1$
307 							if (!OpenSshConfigFile.flag(
308 									Host.this.entry.getValue(mapKey(key)))) {
309 								return "none,zlib@openssh.com,zlib"; //$NON-NLS-1$
310 							}
311 							return "zlib@openssh.com,zlib,none"; //$NON-NLS-1$
312 						}
313 						return Host.this.entry.getValue(mapKey(key));
314 					}
315 
316 					@Override
317 					public String[] getValues(String key) {
318 						List<String> values = Host.this.entry
319 								.getValues(mapKey(key));
320 						if (values == null) {
321 							return new String[0];
322 						}
323 						return values.toArray(new String[0]);
324 					}
325 				};
326 			}
327 			return config;
328 		}
329 
330 		@Override
331 		@SuppressWarnings("nls")
332 		public String toString() {
333 			return "Host [hostName=" + hostName + ", port=" + port
334 					+ ", identityFile=" + identityFile + ", user=" + user
335 					+ ", preferredAuthentications=" + preferredAuthentications
336 					+ ", batchMode=" + batchMode + ", strictHostKeyChecking="
337 					+ strictHostKeyChecking + ", connectionAttempts="
338 					+ connectionAttempts + ", entry=" + entry + "]";
339 		}
340 	}
341 
342 	/**
343 	 * {@inheritDoc}
344 	 * <p>
345 	 * Retrieves the full {@link com.jcraft.jsch.ConfigRepository.Config Config}
346 	 * for the given host name. Should be called only by Jsch and tests.
347 	 *
348 	 * @since 4.9
349 	 */
350 	@Override
351 	public Config getConfig(String hostName) {
352 		Host host = lookup(hostName);
353 		return host.getConfig();
354 	}
355 
356 	/** {@inheritDoc} */
357 	@Override
358 	public String toString() {
359 		return "OpenSshConfig [configFile=" + configFile + ']'; //$NON-NLS-1$
360 	}
361 }