OpenSSHConfig.java

/*
 * Copyright (c) 2013-2018 ymnk, JCraft,Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, are permitted
 * provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice, this list of conditions
 * and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright notice, this list of
 * conditions and the following disclaimer in the documentation and/or other materials provided with
 * the distribution.
 *
 * 3. The names of the authors may not be used to endorse or promote products derived from this
 * software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL JCRAFT, INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
 * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package com.jcraft.jsch;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Hashtable;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.Vector;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * This class implements ConfigRepository interface, and parses OpenSSH's configuration file. The
 * following keywords will be recognized,
 *
 * <ul>
 * <li>Host</li>
 * <li>User</li>
 * <li>Hostname</li>
 * <li>Port</li>
 * <li>PreferredAuthentications</li>
 * <li>PubkeyAcceptedAlgorithms</li>
 * <li>FingerprintHash</li>
 * <li>IdentityFile</li>
 * <li>NumberOfPasswordPrompts</li>
 * <li>ConnectTimeout</li>
 * <li>HostKeyAlias</li>
 * <li>UserKnownHostsFile</li>
 * <li>KexAlgorithms</li>
 * <li>HostKeyAlgorithms</li>
 * <li>Ciphers</li>
 * <li>Macs</li>
 * <li>Compression</li>
 * <li>CompressionLevel</li>
 * <li>ForwardAgent</li>
 * <li>RequestTTY</li>
 * <li>ServerAliveInterval</li>
 * <li>LocalForward</li>
 * <li>RemoteForward</li>
 * <li>ClearAllForwardings</li>
 * </ul>
 *
 * @see ConfigRepository
 */
public class OpenSSHConfig implements ConfigRepository {

  private static final Set<String> keysWithListAdoption = Stream
      .of("KexAlgorithms", "Ciphers", "HostKeyAlgorithms", "MACs", "PubkeyAcceptedAlgorithms",
          "PubkeyAcceptedKeyTypes")
      .map(string -> string.toUpperCase(Locale.ROOT)).collect(Collectors.toSet());

  /**
   * Parses the given string, and returns an instance of ConfigRepository.
   *
   * @param conf string, which includes OpenSSH's config
   * @return an instanceof OpenSSHConfig
   */
  public static OpenSSHConfig parse(String conf) throws IOException {
    try (Reader r = new StringReader(conf)) {
      try (BufferedReader br = new BufferedReader(r)) {
        return new OpenSSHConfig(br);
      }
    }
  }

  /**
   * Parses the given file, and returns an instance of ConfigRepository.
   *
   * @param file OpenSSH's config file
   * @return an instanceof OpenSSHConfig
   */
  public static OpenSSHConfig parseFile(String file) throws IOException {
    try (BufferedReader br =
        Files.newBufferedReader(Paths.get(Util.checkTilde(file)), StandardCharsets.UTF_8)) {
      return new OpenSSHConfig(br);
    }
  }

  OpenSSHConfig(BufferedReader br) throws IOException {
    _parse(br);
  }

  private final Hashtable<String, Vector<String[]>> config = new Hashtable<>();
  private final Vector<String> hosts = new Vector<>();

  private void _parse(BufferedReader br) throws IOException {
    String host = "";
    Vector<String[]> kv = new Vector<>();
    String l = null;

    while ((l = br.readLine()) != null) {
      l = l.trim();
      if (l.length() == 0 || l.startsWith("#"))
        continue;

      String[] key_value = l.split("[= \t]", 2);
      for (int i = 0; i < key_value.length; i++)
        key_value[i] = key_value[i].trim();

      if (key_value.length <= 1)
        continue;

      if (key_value[0].equalsIgnoreCase("Host")) {
        config.put(host, kv);
        hosts.addElement(host);
        host = key_value[1];
        kv = new Vector<>();
      } else {
        kv.addElement(key_value);
      }
    }
    config.put(host, kv);
    hosts.addElement(host);
  }

  @Override
  public Config getConfig(String host) {
    return new MyConfig(host);
  }

  /**
   * Returns mapping of jsch config property names to OpenSSH property names.
   *
   * @return map
   */
  static Hashtable<String, String> getKeymap() {
    return keymap;
  }

  private static final Hashtable<String, String> keymap = new Hashtable<>();

  static {
    keymap.put("kex", "KexAlgorithms");
    keymap.put("server_host_key", "HostKeyAlgorithms");
    keymap.put("cipher.c2s", "Ciphers");
    keymap.put("cipher.s2c", "Ciphers");
    keymap.put("mac.c2s", "Macs");
    keymap.put("mac.s2c", "Macs");
    keymap.put("compression.s2c", "Compression");
    keymap.put("compression.c2s", "Compression");
    keymap.put("compression_level", "CompressionLevel");
    keymap.put("MaxAuthTries", "NumberOfPasswordPrompts");
  }

  class MyConfig implements Config {

    private String host;
    private Vector<Vector<String[]>> _configs = new Vector<>();

    MyConfig(String host) {
      this.host = host;

      _configs.addElement(config.get(""));

      byte[] _host = Util.str2byte(host);
      if (hosts.size() > 1) {
        for (int i = 1; i < hosts.size(); i++) {
          String patterns[] = hosts.elementAt(i).split("[ \t]");
          for (int j = 0; j < patterns.length; j++) {
            boolean negate = false;
            String foo = patterns[j].trim();
            if (foo.startsWith("!")) {
              negate = true;
              foo = foo.substring(1).trim();
            }
            if (Util.glob(Util.str2byte(foo), _host)) {
              if (!negate) {
                _configs.addElement(config.get(hosts.elementAt(i)));
              }
            } else if (negate) {
              _configs.addElement(config.get(hosts.elementAt(i)));
            }
          }
        }
      }
    }

    private String find(String key) {
      String originalKey = key;
      if (keymap.get(key) != null) {
        key = keymap.get(key);
      }
      key = key.toUpperCase(Locale.ROOT);
      String value = null;
      for (int i = 0; i < _configs.size(); i++) {
        Vector<String[]> v = _configs.elementAt(i);
        for (int j = 0; j < v.size(); j++) {
          String[] kv = v.elementAt(j);
          if (kv[0].toUpperCase(Locale.ROOT).equals(key)) {
            value = kv[1];
            break;
          }
        }
        if (value != null)
          break;
      }
      // TODO: The following change should be applied,
      // but it is breaking changes.
      // The consensus is required to enable it.
      /*
       * if(value!=null && (key.equals("SERVERALIVEINTERVAL") || key.equals("CONNECTTIMEOUT"))){ try
       * { int timeout = Integer.parseInt(value); value = Integer.toString(timeout*1000); } catch
       * (NumberFormatException e) { } }
       */

      if (keysWithListAdoption.contains(key) && value != null
          && (value.startsWith("+") || value.startsWith("-") || value.startsWith("^"))) {

        String origConfig = JSch.getConfig(originalKey).trim();

        if (value.startsWith("+")) {
          value = origConfig + "," + value.substring(1).trim();
        } else if (value.startsWith("-")) {
          List<String> algList =
              Arrays.stream(Util.split(origConfig, ",")).collect(Collectors.toList());
          for (String alg : Util.split(value.substring(1).trim(), ",")) {
            algList.remove(alg.trim());
          }
          value = String.join(",", algList);
        } else if (value.startsWith("^")) {
          value = value.substring(1).trim() + "," + origConfig;
        }
      }

      return value;
    }

    private String[] multiFind(String key) {
      key = key.toUpperCase(Locale.ROOT);
      Vector<String> value = new Vector<>();
      for (int i = 0; i < _configs.size(); i++) {
        Vector<String[]> v = _configs.elementAt(i);
        for (int j = 0; j < v.size(); j++) {
          String[] kv = v.elementAt(j);
          if (kv[0].toUpperCase(Locale.ROOT).equals(key)) {
            String foo = kv[1];
            if (foo != null) {
              value.remove(foo);
              value.addElement(foo);
            }
          }
        }
      }
      String[] result = new String[value.size()];
      value.toArray(result);
      return result;
    }

    @Override
    public String getHostname() {
      return find("Hostname");
    }

    @Override
    public String getUser() {
      return find("User");
    }

    @Override
    public int getPort() {
      String foo = find("Port");
      int port = -1;
      try {
        port = Integer.parseInt(foo);
      } catch (NumberFormatException e) {
        // wrong format
      }
      return port;
    }

    @Override
    public String getValue(String key) {
      if (key.equals("compression.s2c") || key.equals("compression.c2s")) {
        String foo = find(key);
        if (foo == null || foo.equals("no"))
          return "none,zlib@openssh.com,zlib";
        return "zlib@openssh.com,zlib,none";
      }
      return find(key);
    }

    @Override
    public String[] getValues(String key) {
      return multiFind(key);
    }
  }
}