OpenSshConfig.java

  1. /*
  2.  * Copyright (C) 2008, 2018, 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. package org.eclipse.jgit.transport;

  44. import static org.eclipse.jgit.internal.transport.ssh.OpenSshConfigFile.positive;

  45. import java.io.File;
  46. import java.util.List;
  47. import java.util.Map;
  48. import java.util.TreeMap;

  49. import org.eclipse.jgit.internal.transport.ssh.OpenSshConfigFile;
  50. import org.eclipse.jgit.internal.transport.ssh.OpenSshConfigFile.HostEntry;
  51. import org.eclipse.jgit.util.FS;

  52. import com.jcraft.jsch.ConfigRepository;

  53. /**
  54.  * Fairly complete configuration parser for the OpenSSH ~/.ssh/config file.
  55.  * <p>
  56.  * JSch does have its own config file parser
  57.  * {@link com.jcraft.jsch.OpenSSHConfig} since version 0.1.50, but it has a
  58.  * number of problems:
  59.  * <ul>
  60.  * <li>it splits lines of the format "keyword = value" wrongly: you'd end up
  61.  * with the value "= value".
  62.  * <li>its "Host" keyword is not case insensitive.
  63.  * <li>it doesn't handle quoted values.
  64.  * <li>JSch's OpenSSHConfig doesn't monitor for config file changes.
  65.  * </ul>
  66.  * <p>
  67.  * This parser makes the critical options available to
  68.  * {@link org.eclipse.jgit.transport.SshSessionFactory} via
  69.  * {@link org.eclipse.jgit.transport.OpenSshConfig.Host} objects returned by
  70.  * {@link #lookup(String)}, and implements a fully conforming
  71.  * {@link com.jcraft.jsch.ConfigRepository} providing
  72.  * {@link com.jcraft.jsch.ConfigRepository.Config}s via
  73.  * {@link #getConfig(String)}.
  74.  * </p>
  75.  *
  76.  * @see OpenSshConfigFile
  77.  */
  78. public class OpenSshConfig implements ConfigRepository {

  79.     /**
  80.      * Obtain the user's configuration data.
  81.      * <p>
  82.      * The configuration file is always returned to the caller, even if no file
  83.      * exists in the user's home directory at the time the call was made. Lookup
  84.      * requests are cached and are automatically updated if the user modifies
  85.      * the configuration file since the last time it was cached.
  86.      *
  87.      * @param fs
  88.      *            the file system abstraction which will be necessary to
  89.      *            perform certain file system operations.
  90.      * @return a caching reader of the user's configuration file.
  91.      */
  92.     public static OpenSshConfig get(FS fs) {
  93.         File home = fs.userHome();
  94.         if (home == null)
  95.             home = new File(".").getAbsoluteFile(); //$NON-NLS-1$

  96.         final File config = new File(new File(home, SshConstants.SSH_DIR),
  97.                 SshConstants.CONFIG);
  98.         return new OpenSshConfig(home, config);
  99.     }

  100.     /** The base file. */
  101.     private OpenSshConfigFile configFile;

  102.     OpenSshConfig(File h, File cfg) {
  103.         configFile = new OpenSshConfigFile(h, cfg,
  104.                 SshSessionFactory.getLocalUserName());
  105.     }

  106.     /**
  107.      * Locate the configuration for a specific host request.
  108.      *
  109.      * @param hostName
  110.      *            the name the user has supplied to the SSH tool. This may be a
  111.      *            real host name, or it may just be a "Host" block in the
  112.      *            configuration file.
  113.      * @return r configuration for the requested name. Never null.
  114.      */
  115.     public Host lookup(String hostName) {
  116.         HostEntry entry = configFile.lookup(hostName, -1, null);
  117.         return new Host(entry, hostName, configFile.getLocalUserName());
  118.     }

  119.     /**
  120.      * Configuration of one "Host" block in the configuration file.
  121.      * <p>
  122.      * If returned from {@link OpenSshConfig#lookup(String)} some or all of the
  123.      * properties may not be populated. The properties which are not populated
  124.      * should be defaulted by the caller.
  125.      * <p>
  126.      * When returned from {@link OpenSshConfig#lookup(String)} any wildcard
  127.      * entries which appear later in the configuration file will have been
  128.      * already merged into this block.
  129.      */
  130.     public static class Host {
  131.         String hostName;

  132.         int port;

  133.         File identityFile;

  134.         String user;

  135.         String preferredAuthentications;

  136.         Boolean batchMode;

  137.         String strictHostKeyChecking;

  138.         int connectionAttempts;

  139.         private HostEntry entry;

  140.         private Config config;

  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.         static {
  146.             KEY_MAP.put("kex", SshConstants.KEX_ALGORITHMS); //$NON-NLS-1$
  147.             KEY_MAP.put("server_host_key", SshConstants.HOST_KEY_ALGORITHMS); //$NON-NLS-1$
  148.             KEY_MAP.put("cipher.c2s", SshConstants.CIPHERS); //$NON-NLS-1$
  149.             KEY_MAP.put("cipher.s2c", SshConstants.CIPHERS); //$NON-NLS-1$
  150.             KEY_MAP.put("mac.c2s", SshConstants.MACS); //$NON-NLS-1$
  151.             KEY_MAP.put("mac.s2c", SshConstants.MACS); //$NON-NLS-1$
  152.             KEY_MAP.put("compression.s2c", SshConstants.COMPRESSION); //$NON-NLS-1$
  153.             KEY_MAP.put("compression.c2s", SshConstants.COMPRESSION); //$NON-NLS-1$
  154.             KEY_MAP.put("compression_level", "CompressionLevel"); //$NON-NLS-1$ //$NON-NLS-2$
  155.             KEY_MAP.put("MaxAuthTries", //$NON-NLS-1$
  156.                     SshConstants.NUMBER_OF_PASSWORD_PROMPTS);
  157.         }

  158.         private static String mapKey(String key) {
  159.             String k = KEY_MAP.get(key);
  160.             return k != null ? k : key;
  161.         }

  162.         /**
  163.          * Creates a new uninitialized {@link Host}.
  164.          */
  165.         public Host() {
  166.             // For API backwards compatibility with pre-4.9 JGit
  167.         }

  168.         Host(HostEntry entry, String hostName, String localUserName) {
  169.             this.entry = entry;
  170.             complete(hostName, localUserName);
  171.         }

  172.         /**
  173.          * @return the value StrictHostKeyChecking property, the valid values
  174.          *         are "yes" (unknown hosts are not accepted), "no" (unknown
  175.          *         hosts are always accepted), and "ask" (user should be asked
  176.          *         before accepting the host)
  177.          */
  178.         public String getStrictHostKeyChecking() {
  179.             return strictHostKeyChecking;
  180.         }

  181.         /**
  182.          * @return the real IP address or host name to connect to; never null.
  183.          */
  184.         public String getHostName() {
  185.             return hostName;
  186.         }

  187.         /**
  188.          * @return the real port number to connect to; never 0.
  189.          */
  190.         public int getPort() {
  191.             return port;
  192.         }

  193.         /**
  194.          * @return path of the private key file to use for authentication; null
  195.          *         if the caller should use default authentication strategies.
  196.          */
  197.         public File getIdentityFile() {
  198.             return identityFile;
  199.         }

  200.         /**
  201.          * @return the real user name to connect as; never null.
  202.          */
  203.         public String getUser() {
  204.             return user;
  205.         }

  206.         /**
  207.          * @return the preferred authentication methods, separated by commas if
  208.          *         more than one authentication method is preferred.
  209.          */
  210.         public String getPreferredAuthentications() {
  211.             return preferredAuthentications;
  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.          * @return the number of tries (one per second) to connect before
  222.          *         exiting. The argument must be an integer. This may be useful
  223.          *         in scripts if the connection sometimes fails. The default is
  224.          *         1.
  225.          * @since 3.4
  226.          */
  227.         public int getConnectionAttempts() {
  228.             return connectionAttempts;
  229.         }


  230.         private void complete(String initialHostName, String localUserName) {
  231.             // Try to set values from the options.
  232.             hostName = entry.getValue(SshConstants.HOST_NAME);
  233.             user = entry.getValue(SshConstants.USER);
  234.             port = positive(entry.getValue(SshConstants.PORT));
  235.             connectionAttempts = positive(
  236.                     entry.getValue(SshConstants.CONNECTION_ATTEMPTS));
  237.             strictHostKeyChecking = entry
  238.                     .getValue(SshConstants.STRICT_HOST_KEY_CHECKING);
  239.             batchMode = Boolean.valueOf(OpenSshConfigFile
  240.                     .flag(entry.getValue(SshConstants.BATCH_MODE)));
  241.             preferredAuthentications = entry
  242.                     .getValue(SshConstants.PREFERRED_AUTHENTICATIONS);
  243.             // Fill in defaults if still not set
  244.             if (hostName == null || hostName.isEmpty()) {
  245.                 hostName = initialHostName;
  246.             }
  247.             if (user == null || user.isEmpty()) {
  248.                 user = localUserName;
  249.             }
  250.             if (port <= 0) {
  251.                 port = SshConstants.SSH_DEFAULT_PORT;
  252.             }
  253.             if (connectionAttempts <= 0) {
  254.                 connectionAttempts = 1;
  255.             }
  256.             List<String> identityFiles = entry
  257.                     .getValues(SshConstants.IDENTITY_FILE);
  258.             if (identityFiles != null && !identityFiles.isEmpty()) {
  259.                 identityFile = new File(identityFiles.get(0));
  260.             }
  261.         }

  262.         Config getConfig() {
  263.             if (config == null) {
  264.                 config = new Config() {

  265.                     @Override
  266.                     public String getHostname() {
  267.                         return Host.this.getHostName();
  268.                     }

  269.                     @Override
  270.                     public String getUser() {
  271.                         return Host.this.getUser();
  272.                     }

  273.                     @Override
  274.                     public int getPort() {
  275.                         return Host.this.getPort();
  276.                     }

  277.                     @Override
  278.                     public String getValue(String key) {
  279.                         // See com.jcraft.jsch.OpenSSHConfig.MyConfig.getValue()
  280.                         // for this special case.
  281.                         if (key.equals("compression.s2c") //$NON-NLS-1$
  282.                                 || key.equals("compression.c2s")) { //$NON-NLS-1$
  283.                             if (!OpenSshConfigFile.flag(
  284.                                     Host.this.entry.getValue(mapKey(key)))) {
  285.                                 return "none,zlib@openssh.com,zlib"; //$NON-NLS-1$
  286.                             }
  287.                             return "zlib@openssh.com,zlib,none"; //$NON-NLS-1$
  288.                         }
  289.                         return Host.this.entry.getValue(mapKey(key));
  290.                     }

  291.                     @Override
  292.                     public String[] getValues(String key) {
  293.                         List<String> values = Host.this.entry
  294.                                 .getValues(mapKey(key));
  295.                         if (values == null) {
  296.                             return new String[0];
  297.                         }
  298.                         return values.toArray(new String[0]);
  299.                     }
  300.                 };
  301.             }
  302.             return config;
  303.         }

  304.         @Override
  305.         @SuppressWarnings("nls")
  306.         public String toString() {
  307.             return "Host [hostName=" + hostName + ", port=" + port
  308.                     + ", identityFile=" + identityFile + ", user=" + user
  309.                     + ", preferredAuthentications=" + preferredAuthentications
  310.                     + ", batchMode=" + batchMode + ", strictHostKeyChecking="
  311.                     + strictHostKeyChecking + ", connectionAttempts="
  312.                     + connectionAttempts + ", entry=" + entry + "]";
  313.         }
  314.     }

  315.     /**
  316.      * {@inheritDoc}
  317.      * <p>
  318.      * Retrieves the full {@link com.jcraft.jsch.ConfigRepository.Config Config}
  319.      * for the given host name. Should be called only by Jsch and tests.
  320.      *
  321.      * @since 4.9
  322.      */
  323.     @Override
  324.     public Config getConfig(String hostName) {
  325.         Host host = lookup(hostName);
  326.         return host.getConfig();
  327.     }

  328.     /** {@inheritDoc} */
  329.     @Override
  330.     public String toString() {
  331.         return "OpenSshConfig [configFile=" + configFile + ']'; //$NON-NLS-1$
  332.     }
  333. }