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