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