/* -*-mode:java; c-basic-offset:2; indent-tabs-mode:nil -*- */
/*
Copyright (c) 2013-2018 ymnk, JCraft,Inc. All rights reserved.
Copyright (c) 2019, 2023, 2025 D. R. Commander. All rights reserved.
Copyright (c) 2020-2021 Jeremy Norris. All rights reserved.
Copyright (c) 2021 Matthias Wiedemann. 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.Reader;
import java.io.StringReader;
import java.io.FileReader;
import java.io.BufferedReader;
import java.io.IOException;
import java.util.Arrays;
import java.util.Hashtable;
import java.util.List;
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>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 List<String> keysWithListAdoption = Stream.of("KexAlgorithms", "Ciphers", "HostbasedAcceptedAlgorithms",
  "HostKeyAlgorithms", "MACs", "PubkeyAcceptedAlgorithms", "PubkeyAcceptedKeyTypes", "CASignatureAlgorithms").map(String::toUpperCase).collect(Collectors.toList());

  /**
   * 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 {
    Reader r = new StringReader(conf);
    try {
      return new OpenSSHConfig(r);
    }
    finally {
      r.close();
    }
  }

  /**
   * 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 {
    Reader r = new FileReader(Util.checkTilde(file));
    try {
      return new OpenSSHConfig(r);
    }
    finally {
      r.close();
    }
  }

  OpenSSHConfig(Reader r) throws IOException {
    _parse(r);
  }

  private final Hashtable config = new Hashtable();
  private final Vector hosts = new Vector();

  private void _parse(Reader r) throws IOException {
    BufferedReader br = new BufferedReader(r);

    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 if(key_value[0].equalsIgnoreCase("ServerAliveInterval")){
        int serverAliveInterval = -1;
        try {
          serverAliveInterval = Integer.parseInt(key_value[1]);
        }
        catch(NumberFormatException e){
          // wrong format
        }
        if(serverAliveInterval>=0){
          key_value[1]=Integer.toString(serverAliveInterval*1000);
        }
        kv.addElement(key_value);
      }
      else if(key_value[0].equalsIgnoreCase("ConnectTimeout")){
        int connectTimeout = -1;
        try {
          connectTimeout = Integer.parseInt(key_value[1]);
        }
        catch(NumberFormatException e){
          // wrong format
        }
        if(connectTimeout>=0){
          key_value[1]=Integer.toString(connectTimeout*1000);
        }
        kv.addElement(key_value);
      }
      else {
        kv.addElement(key_value);
      }
    }
    config.put(host, kv);
    hosts.addElement(host);
  }

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

  private static final Hashtable 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 _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[] = ((String)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((String)hosts.elementAt(i)));
              }
            }
            else if(negate){
              _configs.addElement(config.get((String)hosts.elementAt(i)));
            }
          }
        }
      }
    }

    private String find(String key) {
      String originalKey=key;
      if(keymap.get(key)!=null) {
        key = (String)keymap.get(key);
      }
      key = key.toUpperCase();
      String value = null;
      for(int i = 0; i < _configs.size(); i++) {
        Vector v = (Vector)_configs.elementAt(i);
        for(int j = 0; j < v.size(); j++) {
          String[] kv = (String[])v.elementAt(j);
          if(kv[0].toUpperCase().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);
        } else if (value.startsWith("-")) {
          List<String> algList = Arrays.stream(origConfig.split(",")).collect(Collectors.toList());
          for (String alg : value.substring(1).split(",")) {
            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();
      Vector value = new Vector();
      for(int i = 0; i < _configs.size(); i++) {
        Vector v = (Vector)_configs.elementAt(i);
        for(int j = 0; j < v.size(); j++) {
          String[] kv = (String[])v.elementAt(j);
          if(kv[0].toUpperCase().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;
    }

    public String getHostname(){ return find("Hostname"); }
    public String getUser(){ return find("User"); }
    public int getPort(){
      String foo = find("Port");
      int port = -1;
      try {
        port = Integer.parseInt(foo);
      }
      catch(NumberFormatException e){
        // wrong format
      }
      return port;
    }
    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);
    }
    public String[] getValues(String key){ return multiFind(key); }
  }
}
