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