/*
 * Copyright (c) 2002-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.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.io.OutputStream;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Properties;
import java.util.Vector;
import javax.crypto.AEADBadTagException;

public class Session {

  // http://ietf.org/internet-drafts/draft-ietf-secsh-assignednumbers-01.txt
  static final int SSH_MSG_DISCONNECT = 1;
  static final int SSH_MSG_IGNORE = 2;
  static final int SSH_MSG_UNIMPLEMENTED = 3;
  static final int SSH_MSG_DEBUG = 4;
  static final int SSH_MSG_SERVICE_REQUEST = 5;
  static final int SSH_MSG_SERVICE_ACCEPT = 6;
  static final int SSH_MSG_EXT_INFO = 7;
  static final int SSH_MSG_KEXINIT = 20;
  static final int SSH_MSG_NEWKEYS = 21;
  static final int SSH_MSG_KEXDH_INIT = 30;
  static final int SSH_MSG_KEXDH_REPLY = 31;
  static final int SSH_MSG_KEX_DH_GEX_GROUP = 31;
  static final int SSH_MSG_KEX_DH_GEX_INIT = 32;
  static final int SSH_MSG_KEX_DH_GEX_REPLY = 33;
  static final int SSH_MSG_KEX_DH_GEX_REQUEST = 34;
  static final int SSH_MSG_GLOBAL_REQUEST = 80;
  static final int SSH_MSG_REQUEST_SUCCESS = 81;
  static final int SSH_MSG_REQUEST_FAILURE = 82;
  static final int SSH_MSG_CHANNEL_OPEN = 90;
  static final int SSH_MSG_CHANNEL_OPEN_CONFIRMATION = 91;
  static final int SSH_MSG_CHANNEL_OPEN_FAILURE = 92;
  static final int SSH_MSG_CHANNEL_WINDOW_ADJUST = 93;
  static final int SSH_MSG_CHANNEL_DATA = 94;
  static final int SSH_MSG_CHANNEL_EXTENDED_DATA = 95;
  static final int SSH_MSG_CHANNEL_EOF = 96;
  static final int SSH_MSG_CHANNEL_CLOSE = 97;
  static final int SSH_MSG_CHANNEL_REQUEST = 98;
  static final int SSH_MSG_CHANNEL_SUCCESS = 99;
  static final int SSH_MSG_CHANNEL_FAILURE = 100;

  private static final int PACKET_MAX_SIZE = 256 * 1024;

  private byte[] V_S; // server version
  private byte[] V_C = Util.str2byte("SSH-2.0-JSCH_" + JSch.VERSION); // client version

  private byte[] I_C; // the payload of the client's SSH_MSG_KEXINIT
  private byte[] I_S; // the payload of the server's SSH_MSG_KEXINIT
  private byte[] K_S; // the host key

  private byte[] session_id;

  private byte[] IVc2s;
  private byte[] IVs2c;
  private byte[] Ec2s;
  private byte[] Es2c;
  private byte[] MACc2s;
  private byte[] MACs2c;

  // RFC 4253 6.4. each direction must run independently hence we have an incoming and outgoing
  // sequence number
  private int seqi = 0;
  private int seqo = 0;

  String[] guess = null;
  private Cipher s2ccipher;
  private Cipher c2scipher;
  private MAC s2cmac;
  private MAC c2smac;
  // private byte[] mac_buf;
  private byte[] s2cmac_result1;
  private byte[] s2cmac_result2;

  private Compression deflater;
  private Compression inflater;

  private IO io;
  private Socket socket;
  private int timeout = 0;

  private volatile boolean isConnected = false;

  private volatile boolean doExtInfo = false;
  private boolean enable_server_sig_algs = true;
  private boolean enable_ext_info_in_auth = true;

  private volatile boolean initialKex = true;
  private volatile boolean doStrictKex = false;
  private boolean enable_strict_kex = true;
  private boolean require_strict_kex = false;

  private volatile boolean isAuthed = false;

  private Thread connectThread = null;
  private Object lock = new Object();

  boolean x11_forwarding = false;
  boolean agent_forwarding = false;

  InputStream in = null;
  OutputStream out = null;

  static Random random;

  Buffer buf;
  Packet packet;

  SocketFactory socket_factory = null;

  private Hashtable<String, String> config = null;

  private Proxy proxy = null;
  private UserInfo userinfo;

  private String hostKeyAlias = null;
  private int serverAliveInterval = 0;
  private int serverAliveCountMax = 1;

  private IdentityRepository identityRepository = null;
  private HostKeyRepository hostkeyRepository = null;
  private volatile String[] serverSigAlgs = null;
  private volatile boolean sshBugSigType74 = false;

  protected boolean daemon_thread = false;

  private volatile long kex_start_time = 0L;

  int max_auth_tries = 6;
  int auth_failures = 0;

  String host = "127.0.0.1";
  String org_host = "127.0.0.1";
  int port = 22;

  String username = null;
  byte[] password = null;

  JSch jsch;
  Logger logger;

  Session(JSch jsch, String username, String host, int port) throws JSchException {
    super();
    this.jsch = jsch;
    buf = new Buffer();
    packet = new Packet(buf);
    this.username = username;
    this.org_host = this.host = host;
    this.port = port;

    applyConfig();

    if (this.username == null) {
      this.username = Util.getSystemProperty("user.name");
    }

    if (this.username == null) {
      throw new JSchException("username is not given.");
    }
  }

  public void connect() throws JSchException {
    connect(timeout);
  }

  public void connect(int connectTimeout) throws JSchException {
    if (isConnected) {
      throw new JSchException("session is already connected");
    }
    initialKex = true;

    io = new IO();
    if (random == null) {
      try {
        Class<? extends Random> c = Class.forName(getConfig("random")).asSubclass(Random.class);
        random = c.getDeclaredConstructor().newInstance();
      } catch (Exception e) {
        throw new JSchException(e.toString(), e);
      }
    }
    Packet.setRandom(random);

    if (getLogger().isEnabled(Logger.INFO)) {
      getLogger().log(Logger.INFO, "Connecting to " + host + " port " + port);
    }

    try {
      int i, j;

      if (proxy == null) {
        InputStream in;
        OutputStream out;
        if (socket_factory == null) {
          socket = Util.createSocket(host, port, connectTimeout);
          in = socket.getInputStream();
          out = socket.getOutputStream();
        } else {
          socket = socket_factory.createSocket(host, port);
          in = socket_factory.getInputStream(socket);
          out = socket_factory.getOutputStream(socket);
        }
        // if(timeout>0){ socket.setSoTimeout(timeout); }
        socket.setTcpNoDelay(true);
        io.setInputStream(in);
        io.setOutputStream(out);
      } else {
        synchronized (proxy) {
          proxy.connect(socket_factory, host, port, connectTimeout);
          io.setInputStream(proxy.getInputStream());
          io.setOutputStream(proxy.getOutputStream());
          socket = proxy.getSocket();
        }
      }

      if (connectTimeout > 0 && socket != null) {
        socket.setSoTimeout(connectTimeout);
      }

      isConnected = true;

      if (getLogger().isEnabled(Logger.INFO)) {
        getLogger().log(Logger.INFO, "Connection established");
      }

      jsch.addSession(this);

      {
        // Some Cisco devices will miss to read '\n' if it is sent separately.
        byte[] foo = new byte[V_C.length + 2];
        System.arraycopy(V_C, 0, foo, 0, V_C.length);
        foo[foo.length - 2] = (byte) '\r';
        foo[foo.length - 1] = (byte) '\n';
        io.put(foo, 0, foo.length);
      }

      while (true) {
        i = 0;
        j = 0;
        while (i < buf.buffer.length) {
          j = io.getByte();
          if (j < 0)
            break;
          buf.buffer[i] = (byte) j;
          i++;
          if (j == 10)
            break;
        }
        if (j < 0) {
          throw new JSchException("connection is closed by foreign host");
        }

        if (buf.buffer[i - 1] == 10) { // 0x0a
          i--;
          if (i > 0 && buf.buffer[i - 1] == 13) { // 0x0d
            i--;
          }
        }

        if (i <= 3 || ((i != buf.buffer.length) && (buf.buffer[0] != 'S' || buf.buffer[1] != 'S'
            || buf.buffer[2] != 'H' || buf.buffer[3] != '-'))) {
          // It must not start with 'SSH-'
          // System.err.println(new String(buf.buffer, 0, i);
          continue;
        }

        if (i == buf.buffer.length || i < 7 || // SSH-1.99 or SSH-2.0
            (buf.buffer[4] == '1' && buf.buffer[6] != '9') // SSH-1.5
        ) {
          throw new JSchException("invalid server's version string");
        }
        break;
      }

      V_S = new byte[i];
      System.arraycopy(buf.buffer, 0, V_S, 0, i);
      // System.err.println("V_S: ("+i+") ["+new String(V_S)+"]");
      String _v_s = Util.byte2str(V_S);
      sshBugSigType74 = _v_s.startsWith("SSH-2.0-OpenSSH_7.4");

      if (getLogger().isEnabled(Logger.INFO)) {
        getLogger().log(Logger.INFO, "Remote version string: " + _v_s);
        getLogger().log(Logger.INFO, "Local version string: " + Util.byte2str(V_C));
      }

      enable_server_sig_algs = getConfig("enable_server_sig_algs").equals("yes");
      enable_ext_info_in_auth = getConfig("enable_ext_info_in_auth").equals("yes");
      enable_strict_kex = getConfig("enable_strict_kex").equals("yes");
      require_strict_kex = getConfig("require_strict_kex").equals("yes");
      send_kexinit();

      buf = read(buf);
      if (buf.getCommand() != SSH_MSG_KEXINIT) {
        in_kex = false;
        throw new JSchException("invalid protocol: " + buf.getCommand());
      }

      if (getLogger().isEnabled(Logger.INFO)) {
        getLogger().log(Logger.INFO, "SSH_MSG_KEXINIT received");
      }

      KeyExchange kex = receive_kexinit(buf);

      while (true) {
        buf = read(buf);
        if (kex.getState() == buf.getCommand()) {
          kex_start_time = System.currentTimeMillis();
          boolean result = kex.next(buf);
          if (!result) {
            // System.err.println("verify: "+result);
            in_kex = false;
            throw new JSchException("verify: " + result);
          }
        } else {
          in_kex = false;
          throw new JSchException("invalid protocol(kex): " + buf.getCommand());
        }
        if (kex.getState() == KeyExchange.STATE_END) {
          break;
        }
      }

      try {
        long tmp = System.currentTimeMillis();
        in_prompt = true;
        checkHost(host, port, kex);
        in_prompt = false;
        kex_start_time += (System.currentTimeMillis() - tmp);
      } catch (JSchException ee) {
        in_kex = false;
        in_prompt = false;
        throw ee;
      }

      send_newkeys();

      // receive SSH_MSG_NEWKEYS(21)
      buf = read(buf);
      // System.err.println("read: 21 ? "+buf.getCommand());
      if (buf.getCommand() == SSH_MSG_NEWKEYS) {

        if (getLogger().isEnabled(Logger.INFO)) {
          getLogger().log(Logger.INFO, "SSH_MSG_NEWKEYS received");
        }

        receive_newkeys(buf, kex);
        initialKex = false;
      } else {
        in_kex = false;
        throw new JSchException("invalid protocol(newkeys): " + buf.getCommand());
      }

      if (enable_server_sig_algs && enable_ext_info_in_auth && doExtInfo) {
        send_extinfo();
      }

      try {
        String s = getConfig("MaxAuthTries");
        if (s != null) {
          max_auth_tries = Integer.parseInt(s);
        }
      } catch (NumberFormatException e) {
        throw new JSchException("MaxAuthTries: " + getConfig("MaxAuthTries"), e);
      }

      boolean auth = false;
      boolean auth_cancel = false;

      UserAuthNone uan = null;
      try {
        Class<? extends UserAuthNone> c =
            Class.forName(getConfig("userauth.none")).asSubclass(UserAuthNone.class);
        uan = c.getDeclaredConstructor().newInstance();
      } catch (Exception e) {
        throw new JSchException(e.toString(), e);
      }

      auth = uan.start(this);

      String cmethods = getConfig("PreferredAuthentications");

      String[] cmethoda = Util.split(cmethods, ",");

      String smethods = null;
      if (!auth) {
        smethods = uan.getMethods();
        if (smethods != null) {
          smethods = smethods.toLowerCase(Locale.ROOT);
        } else {
          // methods: publickey,password,keyboard-interactive
          // smethods = "publickey,password,keyboard-interactive";
          smethods = cmethods;
        }
      }

      String[] smethoda = Util.split(smethods, ",");

      int methodi = 0;

      loop: while (true) {

        while (!auth && cmethoda != null && methodi < cmethoda.length) {

          String method = cmethoda[methodi++];
          boolean acceptable = false;
          for (int k = 0; k < smethoda.length; k++) {
            if (smethoda[k].equals(method)) {
              acceptable = true;
              break;
            }
          }
          if (!acceptable) {
            continue;
          }

          // System.err.println(" method: " + method);

          if (getLogger().isEnabled(Logger.INFO)) {
            String str = "Authentications that can continue: ";
            for (int k = methodi - 1; k < cmethoda.length; k++) {
              str += cmethoda[k];
              if (k + 1 < cmethoda.length)
                str += ",";
            }
            getLogger().log(Logger.INFO, str);
            getLogger().log(Logger.INFO, "Next authentication method: " + method);
          }

          UserAuth ua = null;
          try {
            Class<? extends UserAuth> c = null;
            if (getConfig("userauth." + method) != null) {
              c = Class.forName(getConfig("userauth." + method)).asSubclass(UserAuth.class);
              ua = c.getDeclaredConstructor().newInstance();
            }
          } catch (Exception e) {
            if (getLogger().isEnabled(Logger.WARN)) {
              getLogger().log(Logger.WARN, "failed to load " + method + " method");
            }
          }

          if (ua != null) {
            auth_cancel = false;
            try {
              auth = ua.start(this);
              if (auth && getLogger().isEnabled(Logger.INFO)) {
                getLogger().log(Logger.INFO, "Authentication succeeded (" + method + ").");
              }
            } catch (JSchAuthCancelException ee) {
              auth_cancel = true;
            } catch (JSchPartialAuthException ee) {
              String tmp = smethods;
              smethods = ee.getMethods();
              smethoda = Util.split(smethods, ",");
              if (!tmp.equals(smethods)) {
                methodi = 0;
              }
              // System.err.println("PartialAuth: " + methods);
              auth_cancel = false;
              continue loop;
            } catch (RuntimeException ee) {
              throw ee;
            } catch (JSchException ee) {
              throw ee;
            } catch (Exception ee) {
              // SSH_MSG_DISCONNECT: Too many authentication failures
              // System.err.println("ee: " + ee);
              if (getLogger().isEnabled(Logger.WARN)) {
                getLogger().log(Logger.WARN,
                    "an exception during authentication\n" + ee.toString());
              }
              break loop;
            }
          }
        }
        break;
      }

      if (!auth) {
        if (auth_failures >= max_auth_tries) {
          if (getLogger().isEnabled(Logger.INFO)) {
            getLogger().log(Logger.INFO, "Login trials exceeds " + max_auth_tries);
          }
        }
        throw new JSchException(
            (auth_cancel ? "Auth cancel" : "Auth fail") + " for methods '" + smethods + "'");
      }

      if (socket != null && (connectTimeout > 0 || timeout > 0)) {
        socket.setSoTimeout(timeout);
      }

      isAuthed = true;

      synchronized (lock) {
        if (isConnected) {
          connectThread = new Thread(this::run);
          connectThread.setName("Connect thread " + host + " session");
          if (daemon_thread) {
            connectThread.setDaemon(daemon_thread);
          }
          connectThread.start();

          requestPortForwarding();
        } else {
          // The session has been already down and
          // we don't have to start new thread.
        }
      }
    } catch (Exception e) {
      in_kex = false;
      try {
        if (isConnected) {
          String message = e.toString();
          packet.reset();
          buf.checkFreeSize(1 + 4 * 3 + message.length() + 2 + getBufferMargin());
          buf.putByte((byte) SSH_MSG_DISCONNECT);
          buf.putInt(3);
          buf.putString(Util.str2byte(message));
          buf.putString(Util.str2byte("en"));
          write(packet);
        }
      } catch (Exception ee) {
      }
      try {
        disconnect();
      } catch (Exception ee) {
      }
      isConnected = false;
      // e.printStackTrace();
      if (e instanceof RuntimeException)
        throw (RuntimeException) e;
      if (e instanceof JSchException)
        throw (JSchException) e;
      throw new JSchException("Session.connect: " + e, e);
    } finally {
      Util.bzero(this.password);
      this.password = null;
    }
  }

  private KeyExchange receive_kexinit(Buffer buf) throws Exception {
    int j = buf.getInt();
    if (j != buf.getLength()) { // packet was compressed and
      buf.getByte(); // j is the size of deflated packet.
      I_S = new byte[buf.index - 5];
    } else {
      I_S = new byte[j - 1 - buf.getByte()];
    }
    System.arraycopy(buf.buffer, buf.s, I_S, 0, I_S.length);

    if (initialKex) {
      if (enable_strict_kex || require_strict_kex) {
        doStrictKex = checkServerStrictKex();
        if (doStrictKex) {
          if (getLogger().isEnabled(Logger.INFO)) {
            getLogger().log(Logger.INFO, "Doing strict KEX");
          }

          if (seqi != 1) {
            throw new JSchStrictKexException("KEXINIT not first packet from server");
          }
        } else if (require_strict_kex) {
          throw new JSchStrictKexException("Strict KEX not supported by server");
        }
      }

      if (enable_server_sig_algs) {
        doExtInfo = checkServerExtInfo();
        if (doExtInfo && getLogger().isEnabled(Logger.INFO)) {
          getLogger().log(Logger.INFO, "ext-info messaging supported by server");
        }
      }
    }

    if (!in_kex) { // We are in rekeying activated by the remote!
      send_kexinit();
    }

    guess = KeyExchange.guess(this, I_S, I_C);

    if (guess[KeyExchange.PROPOSAL_KEX_ALGS].equals("ext-info-c")
        || guess[KeyExchange.PROPOSAL_KEX_ALGS].equals("ext-info-s")
        || guess[KeyExchange.PROPOSAL_KEX_ALGS].equals("kex-strict-c-v00@openssh.com")
        || guess[KeyExchange.PROPOSAL_KEX_ALGS].equals("kex-strict-s-v00@openssh.com")) {
      throw new JSchException("Invalid Kex negotiated: " + guess[KeyExchange.PROPOSAL_KEX_ALGS]);
    }

    if (!isAuthed && (guess[KeyExchange.PROPOSAL_ENC_ALGS_CTOS].equals("none")
        || (guess[KeyExchange.PROPOSAL_ENC_ALGS_STOC].equals("none")))) {
      throw new JSchException(
          "NONE Cipher should not be chosen before authentification is successed.");
    }

    KeyExchange kex = null;
    try {
      Class<? extends KeyExchange> c = Class
          .forName(getConfig(guess[KeyExchange.PROPOSAL_KEX_ALGS])).asSubclass(KeyExchange.class);
      kex = c.getDeclaredConstructor().newInstance();
    } catch (Exception | NoClassDefFoundError e) {
      throw new JSchException(e.toString(), e);
    }

    kex.doInit(this, V_S, V_C, I_S, I_C);
    return kex;
  }

  private boolean checkServerStrictKex() {
    Buffer sb = new Buffer(I_S);
    sb.setOffSet(17);
    byte[] sp = sb.getString(); // server proposal

    int l = 0;
    int m = 0;
    while (l < sp.length) {
      while (l < sp.length && sp[l] != ',')
        l++;
      if (m == l)
        continue;
      if ("kex-strict-s-v00@openssh.com".equals(Util.byte2str(sp, m, l - m))) {
        return true;
      }
      l++;
      m = l;
    }

    return false;
  }

  private boolean checkServerExtInfo() {
    Buffer sb = new Buffer(I_S);
    sb.setOffSet(17);
    byte[] sp = sb.getString(); // server proposal

    int l = 0;
    int m = 0;
    while (l < sp.length) {
      while (l < sp.length && sp[l] != ',')
        l++;
      if (m == l)
        continue;
      if ("ext-info-s".equals(Util.byte2str(sp, m, l - m))) {
        return true;
      }
      l++;
      m = l;
    }

    return false;
  }

  private volatile boolean in_kex = false;
  private volatile boolean in_prompt = false;
  private volatile String[] not_available_shks = null;

  public String[] getUnavailableSignatures() {
    return not_available_shks;
  }

  public void rekey() throws Exception {
    send_kexinit();
  }

  private void send_kexinit() throws Exception {
    if (in_kex)
      return;

    String cipherc2s = getConfig("cipher.c2s");
    String ciphers2c = getConfig("cipher.s2c");
    String[] not_available_ciphers = checkCiphers(getConfig("CheckCiphers"));
    if (not_available_ciphers != null && not_available_ciphers.length > 0) {
      if (getLogger().isEnabled(Logger.DEBUG)) {
        getLogger().log(Logger.DEBUG,
            "cipher.c2s proposal before removing unavailable algos is: " + cipherc2s);
        getLogger().log(Logger.DEBUG,
            "cipher.s2c proposal before removing unavailable algos is: " + ciphers2c);
      }

      cipherc2s = Util.diffString(cipherc2s, not_available_ciphers);
      ciphers2c = Util.diffString(ciphers2c, not_available_ciphers);
      if (cipherc2s == null || ciphers2c == null) {
        throw new JSchException("There are not any available ciphers.");
      }

      if (getLogger().isEnabled(Logger.DEBUG)) {
        getLogger().log(Logger.DEBUG,
            "cipher.c2s proposal after removing unavailable algos is: " + cipherc2s);
        getLogger().log(Logger.DEBUG,
            "cipher.s2c proposal after removing unavailable algos is: " + ciphers2c);
      }
    }

    String macc2s = getConfig("mac.c2s");
    String macs2c = getConfig("mac.s2c");
    String[] not_available_macs = checkMacs(getConfig("CheckMacs"));
    if (not_available_macs != null && not_available_macs.length > 0) {
      if (getLogger().isEnabled(Logger.DEBUG)) {
        getLogger().log(Logger.DEBUG,
            "mac.c2s proposal before removing unavailable algos is: " + macc2s);
        getLogger().log(Logger.DEBUG,
            "mac.s2c proposal before removing unavailable algos is: " + macs2c);
      }

      macc2s = Util.diffString(macc2s, not_available_macs);
      macs2c = Util.diffString(macs2c, not_available_macs);
      if (macc2s == null || macs2c == null) {
        throw new JSchException("There are not any available macs.");
      }

      if (getLogger().isEnabled(Logger.DEBUG)) {
        getLogger().log(Logger.DEBUG,
            "mac.c2s proposal after removing unavailable algos is: " + macc2s);
        getLogger().log(Logger.DEBUG,
            "mac.s2c proposal after removing unavailable algos is: " + macs2c);
      }
    }

    String kex = getConfig("kex");
    String[] not_available_kexes = checkKexes(getConfig("CheckKexes"));
    if (not_available_kexes != null && not_available_kexes.length > 0) {
      if (getLogger().isEnabled(Logger.DEBUG)) {
        getLogger().log(Logger.DEBUG, "kex proposal before removing unavailable algos is: " + kex);
      }

      kex = Util.diffString(kex, not_available_kexes);
      if (kex == null) {
        throw new JSchException("There are not any available kexes.");
      }

      if (getLogger().isEnabled(Logger.DEBUG)) {
        getLogger().log(Logger.DEBUG, "kex proposal after removing unavailable algos is: " + kex);
      }
    }

    if (enable_server_sig_algs && !isAuthed) {
      kex += ",ext-info-c";
    }

    if ((enable_strict_kex || require_strict_kex) && initialKex) {
      kex += ",kex-strict-c-v00@openssh.com";
    }

    String server_host_key = getConfig("server_host_key");
    String[] not_available_shks = checkSignatures(getConfig("CheckSignatures"));
    // Cache for UserAuthPublicKey
    this.not_available_shks = not_available_shks;
    if (not_available_shks != null && not_available_shks.length > 0) {
      if (getLogger().isEnabled(Logger.DEBUG)) {
        getLogger().log(Logger.DEBUG,
            "server_host_key proposal before removing unavailable algos is: " + server_host_key);
      }

      server_host_key = Util.diffString(server_host_key, not_available_shks);
      if (server_host_key == null) {
        throw new JSchException("There are not any available sig algorithm.");
      }

      if (getLogger().isEnabled(Logger.DEBUG)) {
        getLogger().log(Logger.DEBUG,
            "server_host_key proposal after removing unavailable algos is: " + server_host_key);
      }
    }

    String prefer_hkr = getConfig("prefer_known_host_key_types");
    if (prefer_hkr.equals("yes")) {
      if (getLogger().isEnabled(Logger.DEBUG)) {
        getLogger().log(Logger.DEBUG,
            "server_host_key proposal before known_host reordering is: " + server_host_key);
      }

      HostKeyRepository hkr = getHostKeyRepository();
      String chost = host;
      if (hostKeyAlias != null) {
        chost = hostKeyAlias;
      }
      if (hostKeyAlias == null && port != 22) {
        chost = ("[" + chost + "]:" + port);
      }
      HostKey[] hks = hkr.getHostKey(chost, null);
      if (hks != null && hks.length > 0) {
        List<String> pref_shks = new ArrayList<>();
        List<String> shks = new ArrayList<>(Arrays.asList(Util.split(server_host_key, ",")));
        Iterator<String> it = shks.iterator();
        while (it.hasNext()) {
          String algo = it.next();
          String type = algo;
          if (type.equals("rsa-sha2-256") || type.equals("rsa-sha2-512")
              || type.equals("ssh-rsa-sha224@ssh.com") || type.equals("ssh-rsa-sha256@ssh.com")
              || type.equals("ssh-rsa-sha384@ssh.com") || type.equals("ssh-rsa-sha512@ssh.com")) {
            type = "ssh-rsa";
          }
          for (HostKey hk : hks) {
            if (hk.getType().equals(type)) {
              pref_shks.add(algo);
              it.remove();
              break;
            }
          }
        }
        if (pref_shks.size() > 0) {
          pref_shks.addAll(shks);
          server_host_key = String.join(",", pref_shks);
        }
      }

      if (getLogger().isEnabled(Logger.DEBUG)) {
        getLogger().log(Logger.DEBUG,
            "server_host_key proposal after known_host reordering is: " + server_host_key);
      }
    }

    kex_start_time = System.currentTimeMillis();
    in_kex = true;

    // byte SSH_MSG_KEXINIT(20)
    // byte[16] cookie (random bytes)
    // string kex_algorithms
    // string server_host_key_algorithms
    // string encryption_algorithms_client_to_server
    // string encryption_algorithms_server_to_client
    // string mac_algorithms_client_to_server
    // string mac_algorithms_server_to_client
    // string compression_algorithms_client_to_server
    // string compression_algorithms_server_to_client
    // string languages_client_to_server
    // string languages_server_to_client
    Buffer buf = new Buffer(); // send_kexinit may be invoked
    Packet packet = new Packet(buf); // by user thread.
    packet.reset();
    buf.putByte((byte) SSH_MSG_KEXINIT);
    synchronized (random) {
      random.fill(buf.buffer, buf.index, 16);
      buf.skip(16);
    }
    buf.putString(Util.str2byte(kex));
    buf.putString(Util.str2byte(server_host_key));
    buf.putString(Util.str2byte(cipherc2s));
    buf.putString(Util.str2byte(ciphers2c));
    buf.putString(Util.str2byte(getConfig("mac.c2s")));
    buf.putString(Util.str2byte(getConfig("mac.s2c")));
    buf.putString(Util.str2byte(getConfig("compression.c2s")));
    buf.putString(Util.str2byte(getConfig("compression.s2c")));
    buf.putString(Util.str2byte(getConfig("lang.c2s")));
    buf.putString(Util.str2byte(getConfig("lang.s2c")));
    buf.putByte((byte) 0);
    buf.putInt(0);

    buf.setOffSet(5);
    I_C = new byte[buf.getLength()];
    buf.getByte(I_C);

    write(packet);

    if (getLogger().isEnabled(Logger.INFO)) {
      getLogger().log(Logger.INFO, "SSH_MSG_KEXINIT sent");
    }
  }

  private void send_newkeys() throws Exception {
    // send SSH_MSG_NEWKEYS(21)
    packet.reset();
    buf.putByte((byte) SSH_MSG_NEWKEYS);
    write(packet);

    if (getLogger().isEnabled(Logger.INFO)) {
      getLogger().log(Logger.INFO, "SSH_MSG_NEWKEYS sent");
    }
  }

  private void send_extinfo() throws Exception {
    // send SSH_MSG_EXT_INFO(7)
    packet.reset();
    buf.putByte((byte) SSH_MSG_EXT_INFO);
    buf.putInt(1);
    buf.putString(Util.str2byte("ext-info-in-auth@openssh.com"));
    buf.putString(Util.str2byte("0"));
    write(packet);

    if (getLogger().isEnabled(Logger.INFO)) {
      getLogger().log(Logger.INFO, "SSH_MSG_EXT_INFO sent");
    }
  }

  private void checkHost(String chost, int port, KeyExchange kex) throws JSchException {
    String shkc = getConfig("StrictHostKeyChecking");

    if (hostKeyAlias != null) {
      chost = hostKeyAlias;
    }

    // System.err.println("shkc: "+shkc);

    byte[] K_S = kex.getHostKey();
    String key_type = kex.getKeyType();
    String key_fprint = kex.getFingerPrint();

    if (hostKeyAlias == null && port != 22) {
      chost = ("[" + chost + "]:" + port);
    }

    HostKeyRepository hkr = getHostKeyRepository();

    String hkh = getConfig("HashKnownHosts");
    if (hkh.equals("yes") && (hkr instanceof KnownHosts)) {
      hostkey = ((KnownHosts) hkr).createHashedHostKey(chost, K_S);
    } else {
      hostkey = new HostKey(chost, K_S);
    }

    int i = 0;
    synchronized (hkr) {
      i = hkr.check(chost, K_S);
    }

    boolean insert = false;
    if ((shkc.equals("ask") || shkc.equals("yes")) && i == HostKeyRepository.CHANGED) {
      String file = null;
      synchronized (hkr) {
        file = hkr.getKnownHostsRepositoryID();
      }
      if (file == null) {
        file = "known_hosts";
      }

      boolean b = false;

      if (userinfo != null) {
        String message = "WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!\n"
            + "IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!\n"
            + "Someone could be eavesdropping on you right now (man-in-the-middle attack)!\n"
            + "It is also possible that the " + key_type + " host key has just been changed.\n"
            + "The fingerprint for the " + key_type + " key sent by the remote host " + chost
            + " is\n" + key_fprint + ".\n" + "Please contact your system administrator.\n"
            + "Add correct host key in " + file + " to get rid of this message.";

        if (shkc.equals("ask")) {
          b = userinfo
              .promptYesNo(message + "\nDo you want to delete the old key and insert the new key?");
        } else { // shkc.equals("yes")
          userinfo.showMessage(message);
        }
      }

      if (!b) {
        throw new JSchException("HostKey has been changed: " + chost);
      }

      synchronized (hkr) {
        hkr.remove(chost, kex.getKeyAlgorithName(), null);
        insert = true;
      }
    }

    if ((shkc.equals("ask") || shkc.equals("yes")) && (i != HostKeyRepository.OK) && !insert) {
      if (shkc.equals("yes")) {
        throw new JSchException("reject HostKey: " + chost);
      }
      // System.err.println("finger-print: "+key_fprint);
      if (userinfo != null) {
        boolean foo = userinfo.promptYesNo("The authenticity of host '" + chost
            + "' can't be established.\n" + key_type + " key fingerprint is " + key_fprint + ".\n"
            + "Are you sure you want to continue connecting?");
        if (!foo) {
          throw new JSchException("reject HostKey: " + chost);
        }
        insert = true;
      } else {
        if (i == HostKeyRepository.NOT_INCLUDED)
          throw new JSchUnknownHostKeyException(
              "UnknownHostKey: " + chost + ". " + key_type + " key fingerprint is " + key_fprint);
        else
          throw new JSchChangedHostKeyException("HostKey has been changed: " + chost);
      }
    }

    if (shkc.equals("no") && HostKeyRepository.NOT_INCLUDED == i) {
      insert = true;
    }

    if (i == HostKeyRepository.OK) {
      HostKey[] keys = hkr.getHostKey(chost, kex.getKeyAlgorithName());
      String _key = Util.byte2str(Util.toBase64(K_S, 0, K_S.length, true));
      for (int j = 0; j < keys.length; j++) {
        if (keys[j].getKey().equals(_key) && keys[j].getMarker().equals("@revoked")) {
          if (userinfo != null) {
            userinfo.showMessage("The " + key_type + " host key for " + chost
                + " is marked as revoked.\n" + "This could mean that a stolen key is being used to "
                + "impersonate this host.");
          }
          if (getLogger().isEnabled(Logger.INFO)) {
            getLogger().log(Logger.INFO, "Host '" + chost + "' has provided revoked key.");
          }
          throw new JSchRevokedHostKeyException("revoked HostKey: " + chost);
        }
      }
    }

    if (i == HostKeyRepository.OK && getLogger().isEnabled(Logger.INFO)) {
      getLogger().log(Logger.INFO,
          "Host '" + chost + "' is known and matches the " + key_type + " host key");
    }

    if (insert && getLogger().isEnabled(Logger.WARN)) {
      getLogger().log(Logger.WARN,
          "Permanently added '" + chost + "' (" + key_type + ") to the list of known hosts.");
    }

    if (insert) {
      synchronized (hkr) {
        hkr.add(hostkey, userinfo);
      }
    }
  }

  // public void start(){ (new Thread(this)).start(); }

  public Channel openChannel(String type) throws JSchException {
    if (!isConnected) {
      throw new JSchException("session is down");
    }
    try {
      Channel channel = Channel.getChannel(type, this);
      addChannel(channel);
      channel.init();
      if (channel instanceof ChannelSession) {
        applyConfigChannel((ChannelSession) channel);
      }
      return channel;
    } catch (Exception e) {
      // e.printStackTrace();
    }
    return null;
  }

  // encode will bin invoked in write with synchronization.
  void encode(Packet packet) throws Exception {
    // System.err.println("encode: "+packet.buffer.getCommand());
    // System.err.println(" "+packet.buffer.index);
    // if(packet.buffer.getCommand()==96){
    // Thread.dumpStack();
    // }
    if (deflater != null) {
      compress_len[0] = packet.buffer.index;
      packet.buffer.buffer = deflater.compress(packet.buffer.buffer, 5, compress_len);
      packet.buffer.index = compress_len[0];
    }
    int bsize = 8;
    if (c2scipher != null) {
      // bsize=c2scipher.getIVSize();
      bsize = c2scipher_size;
    }
    boolean isChaCha20 = (c2scipher != null && c2scipher.isChaCha20());
    boolean isAEAD = (c2scipher != null && c2scipher.isAEAD());
    boolean isEtM =
        (!isChaCha20 && !isAEAD && c2scipher != null && c2smac != null && c2smac.isEtM());
    packet.padding(bsize, !(isChaCha20 || isAEAD || isEtM));

    byte[] buf = packet.buffer.buffer;
    if (isChaCha20) {
      // init cipher with seq number
      c2scipher.update(seqo);
      // encrypt packet length field
      c2scipher.update(buf, 0, 4, buf, 0);
      // encrypt rest of packet & add tag
      c2scipher.doFinal(buf, 0, packet.buffer.index, buf, 0);
      packet.buffer.skip(c2scipher.getTagSize());
    } else if (isAEAD) {
      c2scipher.updateAAD(buf, 0, 4);
      c2scipher.doFinal(buf, 4, packet.buffer.index - 4, buf, 4);
      packet.buffer.skip(c2scipher.getTagSize());
    } else if (isEtM) {
      c2scipher.update(buf, 4, packet.buffer.index - 4, buf, 4);
      c2smac.update(seqo);
      c2smac.update(packet.buffer.buffer, 0, packet.buffer.index);
      c2smac.doFinal(packet.buffer.buffer, packet.buffer.index);
      packet.buffer.skip(c2smac.getBlockSize());
    } else {
      if (c2smac != null) {
        c2smac.update(seqo);
        c2smac.update(packet.buffer.buffer, 0, packet.buffer.index);
        c2smac.doFinal(packet.buffer.buffer, packet.buffer.index);
      }
      if (c2scipher != null) {
        c2scipher.update(buf, 0, packet.buffer.index, buf, 0);
      }
      if (c2smac != null) {
        packet.buffer.skip(c2smac.getBlockSize());
      }
    }
  }

  int[] uncompress_len = new int[1];
  int[] compress_len = new int[1];

  private int s2ccipher_size = 8;
  private int c2scipher_size = 8;

  Buffer read(Buffer buf) throws Exception {
    int j = 0;
    boolean isChaCha20 = (s2ccipher != null && s2ccipher.isChaCha20());
    boolean isAEAD = (s2ccipher != null && s2ccipher.isAEAD());
    boolean isEtM =
        (!isChaCha20 && !isAEAD && s2ccipher != null && s2cmac != null && s2cmac.isEtM());
    while (true) {
      buf.reset();
      if (isChaCha20) {
        // read encrypted packet length field
        io.getByte(buf.buffer, buf.index, 4);
        buf.index += 4;
        // init cipher with seq number
        s2ccipher.update(seqi);
        // decrypt packet length field
        byte[] tmp = new byte[4];
        s2ccipher.update(buf.buffer, 0, 4, tmp, 0);
        j = ((tmp[0] << 24) & 0xff000000) | ((tmp[1] << 16) & 0x00ff0000)
            | ((tmp[2] << 8) & 0x0000ff00) | ((tmp[3]) & 0x000000ff);
        // RFC 4253 6.1. Maximum Packet Length
        if (j < 5 || j > PACKET_MAX_SIZE) {
          start_discard(buf, s2ccipher, s2cmac, 0, PACKET_MAX_SIZE);
        }
        j += s2ccipher.getTagSize();
        if ((buf.index + j) > buf.buffer.length) {
          byte[] foo = new byte[buf.index + j];
          System.arraycopy(buf.buffer, 0, foo, 0, buf.index);
          buf.buffer = foo;
        }

        if ((j % s2ccipher_size) != 0) {
          String message = "Bad packet length " + j;
          if (getLogger().isEnabled(Logger.FATAL)) {
            getLogger().log(Logger.FATAL, message);
          }
          start_discard(buf, s2ccipher, s2cmac, 0, PACKET_MAX_SIZE - s2ccipher_size);
        }

        io.getByte(buf.buffer, buf.index, j);
        // subtract tag size now that whole packet has been fetched
        j -= s2ccipher.getTagSize();
        buf.index += (j);
        try {
          s2ccipher.doFinal(buf.buffer, 0, j + 4, buf.buffer, 0);
        } catch (AEADBadTagException e) {
          throw new JSchException("Packet corrupt", e);
        }
        // overwrite encrypted packet length field with decrypted version
        System.arraycopy(tmp, 0, buf.buffer, 0, 4);
      } else if (isAEAD || isEtM) {
        io.getByte(buf.buffer, buf.index, 4);
        buf.index += 4;
        j = ((buf.buffer[0] << 24) & 0xff000000) | ((buf.buffer[1] << 16) & 0x00ff0000)
            | ((buf.buffer[2] << 8) & 0x0000ff00) | ((buf.buffer[3]) & 0x000000ff);
        // RFC 4253 6.1. Maximum Packet Length
        if (j < 5 || j > PACKET_MAX_SIZE) {
          start_discard(buf, s2ccipher, s2cmac, 0, PACKET_MAX_SIZE);
        }
        if (isAEAD) {
          j += s2ccipher.getTagSize();
        }
        if ((buf.index + j) > buf.buffer.length) {
          byte[] foo = new byte[buf.index + j];
          System.arraycopy(buf.buffer, 0, foo, 0, buf.index);
          buf.buffer = foo;
        }

        if ((j % s2ccipher_size) != 0) {
          String message = "Bad packet length " + j;
          if (getLogger().isEnabled(Logger.FATAL)) {
            getLogger().log(Logger.FATAL, message);
          }
          start_discard(buf, s2ccipher, s2cmac, 0, PACKET_MAX_SIZE - s2ccipher_size);
        }

        io.getByte(buf.buffer, buf.index, j);
        buf.index += (j);

        if (isAEAD) {
          try {
            s2ccipher.updateAAD(buf.buffer, 0, 4);
            s2ccipher.doFinal(buf.buffer, 4, j, buf.buffer, 4);
          } catch (AEADBadTagException e) {
            throw new JSchException("Packet corrupt", e);
          }
          // don't include AEAD tag size in buf so that decompression works below
          buf.index -= s2ccipher.getTagSize();
        } else {
          s2cmac.update(seqi);
          s2cmac.update(buf.buffer, 0, buf.index);
          s2cmac.doFinal(s2cmac_result1, 0);

          io.getByte(s2cmac_result2, 0, s2cmac_result2.length);
          if (!Util.arraysequals(s2cmac_result1, s2cmac_result2)) {
            throw new JSchException("Packet corrupt");
          }
          s2ccipher.update(buf.buffer, 4, j, buf.buffer, 4);
        }
      } else {
        io.getByte(buf.buffer, buf.index, s2ccipher_size);
        buf.index += s2ccipher_size;
        if (s2ccipher != null) {
          s2ccipher.update(buf.buffer, 0, s2ccipher_size, buf.buffer, 0);
        }
        j = ((buf.buffer[0] << 24) & 0xff000000) | ((buf.buffer[1] << 16) & 0x00ff0000)
            | ((buf.buffer[2] << 8) & 0x0000ff00) | ((buf.buffer[3]) & 0x000000ff);
        // RFC 4253 6.1. Maximum Packet Length
        if (j < 5 || j > PACKET_MAX_SIZE) {
          start_discard(buf, s2ccipher, s2cmac, 0, PACKET_MAX_SIZE);
        }
        int need = j + 4 - s2ccipher_size;
        // if(need<0){
        // throw new IOException("invalid data");
        // }
        if ((buf.index + need) > buf.buffer.length) {
          byte[] foo = new byte[buf.index + need];
          System.arraycopy(buf.buffer, 0, foo, 0, buf.index);
          buf.buffer = foo;
        }

        if ((need % s2ccipher_size) != 0) {
          String message = "Bad packet length " + need;
          if (getLogger().isEnabled(Logger.FATAL)) {
            getLogger().log(Logger.FATAL, message);
          }
          start_discard(buf, s2ccipher, s2cmac, 0, PACKET_MAX_SIZE - s2ccipher_size);
        }

        if (need > 0) {
          io.getByte(buf.buffer, buf.index, need);
          buf.index += (need);
          if (s2ccipher != null) {
            s2ccipher.update(buf.buffer, s2ccipher_size, need, buf.buffer, s2ccipher_size);
          }
        }

        if (s2cmac != null) {
          s2cmac.update(seqi);
          s2cmac.update(buf.buffer, 0, buf.index);
          s2cmac.doFinal(s2cmac_result1, 0);

          io.getByte(s2cmac_result2, 0, s2cmac_result2.length);
          if (!Util.arraysequals(s2cmac_result1, s2cmac_result2)) {
            if (need + s2ccipher_size > PACKET_MAX_SIZE) {
              throw new IOException("MAC Error");
            }
            start_discard(buf, s2ccipher, s2cmac, buf.index,
                PACKET_MAX_SIZE - need - s2ccipher_size);
            continue;
          }
        }
      }

      if (++seqi == 0 && (enable_strict_kex || require_strict_kex) && initialKex) {
        throw new JSchStrictKexException("incoming sequence number wrapped during initial KEX");
      }

      if (inflater != null) {
        // inflater.uncompress(buf);
        int pad = buf.buffer[4];
        uncompress_len[0] = buf.index - 5 - pad;
        byte[] foo = inflater.uncompress(buf.buffer, 5, uncompress_len);
        if (foo != null) {
          buf.buffer = foo;
          buf.index = 5 + uncompress_len[0];
        } else {
          if (getLogger().isEnabled(Logger.ERROR)) {
            getLogger().log(Logger.ERROR, "fail in inflater");
          }
          break;
        }
      }

      int type = buf.getCommand() & 0xff;
      // System.err.println("read: "+type);
      if (type == SSH_MSG_DISCONNECT) {
        buf.rewind();
        buf.getInt();
        buf.getShort();
        int reason_code = buf.getInt();
        byte[] description_array = buf.getString();
        byte[] language_tag_array = buf.getString();
        String description = Util.byte2str(description_array);
        String language_tag = Util.byte2str(language_tag_array);
        throw new JSchSessionDisconnectException(
            "SSH_MSG_DISCONNECT: " + reason_code + " " + description + " " + language_tag,
            reason_code, description, language_tag);
        // break;
      } else if (initialKex && doStrictKex) {
        break;
      } else if (type == SSH_MSG_IGNORE) {
      } else if (type == SSH_MSG_UNIMPLEMENTED) {
        buf.rewind();
        buf.getInt();
        buf.getShort();
        int reason_id = buf.getInt();
        if (getLogger().isEnabled(Logger.INFO)) {
          getLogger().log(Logger.INFO, "Received SSH_MSG_UNIMPLEMENTED for " + reason_id);
        }
      } else if (type == SSH_MSG_DEBUG) {
        buf.rewind();
        buf.getInt();
        buf.getShort();
        /*
         * byte always_display=(byte)buf.getByte(); byte[] message=buf.getString(); byte[]
         * language_tag=buf.getString(); System.err.println("SSH_MSG_DEBUG:"+
         * " "+Util.byte2str(message)+ " "+Util.byte2str(language_tag));
         */
      } else if (type == SSH_MSG_CHANNEL_WINDOW_ADJUST) {
        buf.rewind();
        buf.getInt();
        buf.getShort();
        Channel c = Channel.getChannel(buf.getInt(), this);
        if (c == null) {
        } else {
          c.addRemoteWindowSize(buf.getUInt());
        }
      } else if (type == SSH_MSG_EXT_INFO) {
        buf.rewind();
        buf.getInt();
        buf.getShort();
        boolean ignore = false;
        if (!enable_server_sig_algs) {
          ignore = true;
          if (getLogger().isEnabled(Logger.INFO)) {
            getLogger().log(Logger.INFO,
                "Ignoring SSH_MSG_EXT_INFO while enable_server_sig_algs != yes");
          }
        } else if (isAuthed) {
          ignore = true;
          if (getLogger().isEnabled(Logger.INFO)) {
            getLogger().log(Logger.INFO,
                "Ignoring SSH_MSG_EXT_INFO received after SSH_MSG_USERAUTH_SUCCESS");
          }
        } else if (in_kex) {
          ignore = true;
          if (getLogger().isEnabled(Logger.INFO)) {
            getLogger().log(Logger.INFO,
                "Ignoring SSH_MSG_EXT_INFO received before SSH_MSG_NEWKEYS");
          }
        } else {
          if (getLogger().isEnabled(Logger.INFO)) {
            getLogger().log(Logger.INFO, "SSH_MSG_EXT_INFO received");
          }
        }
        long num_extensions = buf.getUInt();
        for (long i = 0; i < num_extensions; i++) {
          byte[] ext_name = buf.getString();
          byte[] ext_value = buf.getString();
          if (!ignore && Util.byte2str(ext_name).equals("server-sig-algs")) {
            String foo = Util.byte2str(ext_value);
            if (getLogger().isEnabled(Logger.INFO)) {
              getLogger().log(Logger.INFO, "server-sig-algs=<" + foo + ">");
            }
            if (sshBugSigType74) {
              if (!foo.isEmpty()) {
                foo += ",rsa-sha2-256,rsa-sha2-512";
              } else {
                foo = "rsa-sha2-256,rsa-sha2-512";
              }
              if (getLogger().isEnabled(Logger.INFO)) {
                getLogger().log(Logger.INFO,
                    "OpenSSH 7.4 detected: adding rsa-sha2-256 & rsa-sha2-512 to server-sig-algs");
              }
            }
            serverSigAlgs = Util.split(foo, ",");
          }
        }
      } else if (type == UserAuth.SSH_MSG_USERAUTH_SUCCESS) {
        isAuthed = true;
        if (inflater == null && deflater == null) {
          String method;
          method = guess[KeyExchange.PROPOSAL_COMP_ALGS_CTOS];
          initDeflater(method);
          method = guess[KeyExchange.PROPOSAL_COMP_ALGS_STOC];
          initInflater(method);
        }
        break;
      } else {
        break;
      }
    }
    buf.rewind();
    return buf;
  }

  private void start_discard(Buffer buf, Cipher cipher, MAC mac, int mac_already, int discard)
      throws JSchException {
    if (!cipher.isCBC() || (mac != null && mac.isEtM())) {
      throw new JSchException("Packet corrupt");
    }

    if (mac != null) {
      mac.update(seqi);
      mac.update(buf.buffer, 0, mac_already);
    }

    IOException ioe = null;
    try {
      while (discard > 0) {
        buf.reset();
        int len = discard > buf.buffer.length ? buf.buffer.length : discard;
        io.getByte(buf.buffer, 0, len);
        if (mac != null) {
          mac.update(buf.buffer, 0, len);
        }
        discard -= len;
      }
    } catch (IOException e) {
      ioe = e;
      if (getLogger().isEnabled(Logger.ERROR)) {
        getLogger().log(Logger.ERROR, "start_discard finished early due to " + e.getMessage(), e);
      }
    }

    if (mac != null) {
      mac.doFinal(buf.buffer, 0);
    }

    JSchException e = new JSchException("Packet corrupt");
    if (ioe != null) {
      e.addSuppressed(ioe);
    }
    throw e;
  }

  byte[] getSessionId() {
    return session_id;
  }

  private void receive_newkeys(Buffer buf, KeyExchange kex) throws Exception {
    try {
      updateKeys(kex);
    } finally {
      kex.clearK();
    }
    in_kex = false;
    if (doStrictKex) {
      seqi = 0;
      if (getLogger().isEnabled(Logger.INFO)) {
        getLogger().log(Logger.INFO,
            "Reset incoming sequence number after receiving SSH_MSG_NEWKEYS for strict KEX");
      }
    }
  }

  private void updateKeys(KeyExchange kex) throws Exception {
    byte[] K = kex.getK();
    byte[] H = kex.getH();
    HASH hash = kex.getHash();

    if (session_id == null) {
      session_id = new byte[H.length];
      System.arraycopy(H, 0, session_id, 0, H.length);
    }

    /*
     * Initial IV client to server: HASH (K || H || "A" || session_id) Initial IV server to client:
     * HASH (K || H || "B" || session_id) Encryption key client to server: HASH (K || H || "C" ||
     * session_id) Encryption key server to client: HASH (K || H || "D" || session_id) Integrity key
     * client to server: HASH (K || H || "E" || session_id) Integrity key server to client: HASH (K
     * || H || "F" || session_id)
     */

    buf.reset();
    buf.putByte(K);
    buf.putByte(H);
    buf.putByte((byte) 0x41);
    buf.putByte(session_id);
    hash.update(buf.buffer, 0, buf.index);
    IVc2s = hash.digest();

    int j = buf.index - session_id.length - 1;

    buf.buffer[j]++;
    hash.update(buf.buffer, 0, buf.index);
    IVs2c = hash.digest();

    buf.buffer[j]++;
    hash.update(buf.buffer, 0, buf.index);
    Ec2s = hash.digest();

    buf.buffer[j]++;
    hash.update(buf.buffer, 0, buf.index);
    Es2c = hash.digest();

    buf.buffer[j]++;
    hash.update(buf.buffer, 0, buf.index);
    MACc2s = hash.digest();

    buf.buffer[j]++;
    hash.update(buf.buffer, 0, buf.index);
    MACs2c = hash.digest();

    try {
      Class<? extends Cipher> cc;
      Class<? extends MAC> cm;
      String method;

      method = guess[KeyExchange.PROPOSAL_ENC_ALGS_STOC];
      cc = Class.forName(getConfig(method)).asSubclass(Cipher.class);
      s2ccipher = cc.getDeclaredConstructor().newInstance();
      while (s2ccipher.getBlockSize() > Es2c.length) {
        buf.reset();
        buf.putByte(K);
        buf.putByte(H);
        buf.putByte(Es2c);
        hash.update(buf.buffer, 0, buf.index);
        byte[] foo = hash.digest();
        byte[] bar = new byte[Es2c.length + foo.length];
        System.arraycopy(Es2c, 0, bar, 0, Es2c.length);
        System.arraycopy(foo, 0, bar, Es2c.length, foo.length);
        Es2c = bar;
      }
      s2ccipher.init(Cipher.DECRYPT_MODE, Es2c, IVs2c);
      s2ccipher_size = s2ccipher.getIVSize();

      if (!s2ccipher.isAEAD()) {
        method = guess[KeyExchange.PROPOSAL_MAC_ALGS_STOC];
        cm = Class.forName(getConfig(method)).asSubclass(MAC.class);
        s2cmac = cm.getDeclaredConstructor().newInstance();
        MACs2c = expandKey(buf, K, H, MACs2c, hash, s2cmac.getBlockSize());
        s2cmac.init(MACs2c);
        // mac_buf=new byte[s2cmac.getBlockSize()];
        s2cmac_result1 = new byte[s2cmac.getBlockSize()];
        s2cmac_result2 = new byte[s2cmac.getBlockSize()];
      }

      method = guess[KeyExchange.PROPOSAL_ENC_ALGS_CTOS];
      cc = Class.forName(getConfig(method)).asSubclass(Cipher.class);
      c2scipher = cc.getDeclaredConstructor().newInstance();
      while (c2scipher.getBlockSize() > Ec2s.length) {
        buf.reset();
        buf.putByte(K);
        buf.putByte(H);
        buf.putByte(Ec2s);
        hash.update(buf.buffer, 0, buf.index);
        byte[] foo = hash.digest();
        byte[] bar = new byte[Ec2s.length + foo.length];
        System.arraycopy(Ec2s, 0, bar, 0, Ec2s.length);
        System.arraycopy(foo, 0, bar, Ec2s.length, foo.length);
        Ec2s = bar;
      }
      c2scipher.init(Cipher.ENCRYPT_MODE, Ec2s, IVc2s);
      c2scipher_size = c2scipher.getIVSize();

      if (!c2scipher.isAEAD()) {
        method = guess[KeyExchange.PROPOSAL_MAC_ALGS_CTOS];
        cm = Class.forName(getConfig(method)).asSubclass(MAC.class);
        c2smac = cm.getDeclaredConstructor().newInstance();
        MACc2s = expandKey(buf, K, H, MACc2s, hash, c2smac.getBlockSize());
        c2smac.init(MACc2s);
      }

      method = guess[KeyExchange.PROPOSAL_COMP_ALGS_CTOS];
      initDeflater(method);

      method = guess[KeyExchange.PROPOSAL_COMP_ALGS_STOC];
      initInflater(method);
    } catch (Exception | NoClassDefFoundError e) {
      if (e instanceof JSchException)
        throw e;
      throw new JSchException(e.toString(), e);
      // System.err.println("updatekeys: "+e);
    }
  }

  /*
   * RFC 4253 7.2. Output from Key Exchange If the key length needed is longer than the output of
   * the HASH, the key is extended by computing HASH of the concatenation of K and H and the entire
   * key so far, and appending the resulting bytes (as many as HASH generates) to the key. This
   * process is repeated until enough key material is available; the key is taken from the beginning
   * of this value. In other words: K1 = HASH(K || H || X || session_id) (X is e.g., "A") K2 =
   * HASH(K || H || K1) K3 = HASH(K || H || K1 || K2) ... key = K1 || K2 || K3 || ...
   */
  private byte[] expandKey(Buffer buf, byte[] K, byte[] H, byte[] key, HASH hash,
      int required_length) throws Exception {
    byte[] result = key;
    int size = hash.getBlockSize();
    while (result.length < required_length) {
      buf.reset();
      buf.putByte(K);
      buf.putByte(H);
      buf.putByte(result);
      hash.update(buf.buffer, 0, buf.index);
      byte[] tmp = new byte[result.length + size];
      System.arraycopy(result, 0, tmp, 0, result.length);
      System.arraycopy(hash.digest(), 0, tmp, result.length, size);
      Util.bzero(result);
      result = tmp;
    }
    return result;
  }

  /* synchronized */ void write(Packet packet, Channel c, int length) throws Exception {
    long t = getTimeout();
    while (true) {
      if (in_kex) {
        if (t > 0L && (System.currentTimeMillis() - kex_start_time) > t) {
          throw new JSchException("timeout in waiting for rekeying process.");
        }
        try {
          Thread.sleep(10);
        } catch (InterruptedException e) {
        } ;
        continue;
      }
      synchronized (c) {
        if (c.rwsize < length) {
          try {
            c.notifyme++;
            c.wait(100);
          } catch (InterruptedException e) {
          } finally {
            c.notifyme--;
          }
        }

        if (in_kex) {
          continue;
        }

        if (c.rwsize >= length) {
          c.rwsize -= length;
          break;
        }
      }
      if (c.close || !c.isConnected()) {
        throw new IOException("channel is broken");
      }

      boolean sendit = false;
      int s = 0;
      byte command = 0;
      int recipient = -1;
      synchronized (c) {
        if (c.rwsize > 0) {
          long len = c.rwsize;
          if (len > length) {
            len = length;
          }
          if (len != length) {
            s = packet.shift((int) len, (c2scipher != null ? c2scipher_size : 8),
                (c2smac != null ? c2smac.getBlockSize() : 0));
          }
          command = packet.buffer.getCommand();
          recipient = c.getRecipient();
          length -= (int) len;
          c.rwsize -= len;
          sendit = true;
        }
      }
      if (sendit) {
        _write(packet);
        if (length == 0) {
          return;
        }
        packet.unshift(command, recipient, s, length);
      }

      synchronized (c) {
        if (in_kex) {
          continue;
        }
        if (c.rwsize >= length) {
          c.rwsize -= length;
          break;
        }

        // try{
        // System.out.println("1wait: "+c.rwsize);
        // c.notifyme++;
        // c.wait(100);
        // }
        // catch(InterruptedException e){
        // }
        // finally{
        // c.notifyme--;
        // }
      }
    }
    _write(packet);
  }

  void write(Packet packet) throws Exception {
    // System.err.println("in_kex="+in_kex+" "+(packet.buffer.getCommand()));
    long t = getTimeout();
    while (in_kex) {
      if (t > 0L && (System.currentTimeMillis() - kex_start_time) > t && !in_prompt) {
        throw new JSchException("timeout in waiting for rekeying process.");
      }
      byte command = packet.buffer.getCommand();
      // System.err.println("command: "+command);
      if (command == SSH_MSG_KEXINIT || command == SSH_MSG_NEWKEYS || command == SSH_MSG_KEXDH_INIT
          || command == SSH_MSG_KEXDH_REPLY || command == SSH_MSG_KEX_DH_GEX_GROUP
          || command == SSH_MSG_KEX_DH_GEX_INIT || command == SSH_MSG_KEX_DH_GEX_REPLY
          || command == SSH_MSG_KEX_DH_GEX_REQUEST || command == SSH_MSG_DISCONNECT) {
        break;
      }
      try {
        Thread.sleep(10);
      } catch (InterruptedException e) {
      } ;
    }
    _write(packet);
  }

  private void _write(Packet packet) throws Exception {
    boolean initialKex = this.initialKex;
    boolean doStrictKex = this.doStrictKex;
    boolean enable_strict_kex = this.enable_strict_kex;
    boolean require_strict_kex = this.require_strict_kex;
    boolean resetSeqo = packet.buffer.getCommand() == SSH_MSG_NEWKEYS && doStrictKex;

    synchronized (lock) {
      encode(packet);
      if (io != null) {
        io.put(packet);
        if (++seqo == 0 && (enable_strict_kex || require_strict_kex) && initialKex) {
          throw new JSchStrictKexException("outgoing sequence number wrapped during initial KEX");
        }
        if (resetSeqo) {
          seqo = 0;
        }
      }
    }

    if (resetSeqo && io != null && getLogger().isEnabled(Logger.INFO)) {
      getLogger().log(Logger.INFO,
          "Reset outgoing sequence number after sending SSH_MSG_NEWKEYS for strict KEX");
    }
  }

  Runnable thread;

  void run() {
    thread = this::run;

    byte[] foo;
    Buffer buf = new Buffer();
    Packet packet = new Packet(buf);
    int i = 0;
    Channel channel;
    int[] start = new int[1];
    int[] length = new int[1];
    KeyExchange kex = null;

    int stimeout = 0;
    try {
      while (isConnected && thread != null) {
        try {
          buf = read(buf);
          stimeout = 0;
        } catch (InterruptedIOException /* SocketTimeoutException */ ee) {
          if (!in_kex && stimeout < serverAliveCountMax) {
            sendKeepAliveMsg();
            stimeout++;
            continue;
          } else if (in_kex && stimeout < serverAliveCountMax) {
            stimeout++;
            continue;
          }
          throw ee;
        }

        int msgType = buf.getCommand() & 0xff;

        if (kex != null && kex.getState() == msgType) {
          kex_start_time = System.currentTimeMillis();
          boolean result = kex.next(buf);
          if (!result) {
            throw new JSchException("verify: " + result);
          }
          continue;
        }

        switch (msgType) {
          case SSH_MSG_KEXINIT:
            // System.err.println("KEXINIT");
            kex = receive_kexinit(buf);
            break;

          case SSH_MSG_NEWKEYS:
            // System.err.println("NEWKEYS");
            send_newkeys();
            receive_newkeys(buf, kex);
            kex = null;
            break;

          case SSH_MSG_CHANNEL_DATA:
            buf.getInt();
            buf.getByte();
            buf.getByte();
            i = buf.getInt();
            channel = Channel.getChannel(i, this);
            foo = buf.getString(start, length);
            if (channel == null) {
              break;
            }

            if (length[0] == 0) {
              break;
            }

            try {
              channel.write(foo, start[0], length[0]);
            } catch (Exception e) {
              // System.err.println(e);
              try {
                channel.disconnect();
              } catch (Exception ee) {
              }
              break;
            }
            int len = length[0];
            channel.setLocalWindowSize(channel.lwsize - len);
            if (channel.lwsize < channel.lwsize_max / 2) {
              packet.reset();
              buf.putByte((byte) SSH_MSG_CHANNEL_WINDOW_ADJUST);
              buf.putInt(channel.getRecipient());
              buf.putInt(channel.lwsize_max - channel.lwsize);
              synchronized (channel) {
                if (!channel.close)
                  write(packet);
              }
              channel.setLocalWindowSize(channel.lwsize_max);
            }
            break;

          case SSH_MSG_CHANNEL_EXTENDED_DATA:
            buf.getInt();
            buf.getShort();
            i = buf.getInt();
            channel = Channel.getChannel(i, this);
            buf.getInt(); // data_type_code == 1
            foo = buf.getString(start, length);
            // System.err.println("stderr: "+new String(foo,start[0],length[0]));
            if (channel == null) {
              break;
            }

            if (length[0] == 0) {
              break;
            }

            channel.write_ext(foo, start[0], length[0]);

            len = length[0];
            channel.setLocalWindowSize(channel.lwsize - len);
            if (channel.lwsize < channel.lwsize_max / 2) {
              packet.reset();
              buf.putByte((byte) SSH_MSG_CHANNEL_WINDOW_ADJUST);
              buf.putInt(channel.getRecipient());
              buf.putInt(channel.lwsize_max - channel.lwsize);
              synchronized (channel) {
                if (!channel.close)
                  write(packet);
              }
              channel.setLocalWindowSize(channel.lwsize_max);
            }
            break;

          case SSH_MSG_CHANNEL_WINDOW_ADJUST:
            buf.getInt();
            buf.getShort();
            i = buf.getInt();
            channel = Channel.getChannel(i, this);
            if (channel == null) {
              break;
            }
            channel.addRemoteWindowSize(buf.getUInt());
            break;

          case SSH_MSG_CHANNEL_EOF:
            buf.getInt();
            buf.getShort();
            i = buf.getInt();
            channel = Channel.getChannel(i, this);
            if (channel != null) {
              // channel.eof_remote=true;
              // channel.eof();
              channel.eof_remote();
            }
            /*
             * packet.reset(); buf.putByte((byte)SSH_MSG_CHANNEL_EOF);
             * buf.putInt(channel.getRecipient()); write(packet);
             */
            break;
          case SSH_MSG_CHANNEL_CLOSE:
            buf.getInt();
            buf.getShort();
            i = buf.getInt();
            channel = Channel.getChannel(i, this);
            if (channel != null) {
              // channel.close();
              channel.disconnect();
            }
            /*
             * if(Channel.pool.size()==0){ thread=null; }
             */
            break;
          case SSH_MSG_CHANNEL_OPEN_CONFIRMATION:
            buf.getInt();
            buf.getShort();
            i = buf.getInt();
            channel = Channel.getChannel(i, this);
            int r = buf.getInt();
            long rws = buf.getUInt();
            int rps = buf.getInt();
            if (channel != null) {
              channel.setRemoteWindowSize(rws);
              channel.setRemotePacketSize(rps);
              channel.open_confirmation = true;
              channel.setRecipient(r);
            }
            break;
          case SSH_MSG_CHANNEL_OPEN_FAILURE:
            buf.getInt();
            buf.getShort();
            i = buf.getInt();
            channel = Channel.getChannel(i, this);
            if (channel != null) {
              int reason_code = buf.getInt();
              // foo=buf.getString(); // additional textual information
              // foo=buf.getString(); // language tag
              channel.setExitStatus(reason_code);
              channel.close = true;
              channel.eof_remote = true;
              channel.setRecipient(0);
            }
            break;
          case SSH_MSG_CHANNEL_REQUEST:
            buf.getInt();
            buf.getShort();
            i = buf.getInt();
            foo = buf.getString();
            boolean reply = (buf.getByte() != 0);
            channel = Channel.getChannel(i, this);
            if (channel != null) {
              byte reply_type = (byte) SSH_MSG_CHANNEL_FAILURE;
              if ((Util.byte2str(foo)).equals("exit-status")) {
                i = buf.getInt(); // exit-status
                channel.setExitStatus(i);
                reply_type = (byte) SSH_MSG_CHANNEL_SUCCESS;
              }
              if (reply) {
                packet.reset();
                buf.putByte(reply_type);
                buf.putInt(channel.getRecipient());
                write(packet);
              }
            } else {
            }
            break;
          case SSH_MSG_CHANNEL_OPEN:
            buf.getInt();
            buf.getShort();
            foo = buf.getString();
            String ctyp = Util.byte2str(foo);
            if (!"forwarded-tcpip".equals(ctyp) && !("x11".equals(ctyp) && x11_forwarding)
                && !("auth-agent@openssh.com".equals(ctyp) && agent_forwarding)) {
              // System.err.println("Session.run: CHANNEL OPEN "+ctyp);
              // throw new IOException("Session.run: CHANNEL OPEN "+ctyp);
              packet.reset();
              buf.putByte((byte) SSH_MSG_CHANNEL_OPEN_FAILURE);
              buf.putInt(buf.getInt());
              buf.putInt(Channel.SSH_OPEN_ADMINISTRATIVELY_PROHIBITED);
              buf.putString(Util.empty);
              buf.putString(Util.empty);
              write(packet);
            } else {
              channel = Channel.getChannel(ctyp, this);
              addChannel(channel);
              channel.getData(buf);
              channel.init();

              Thread tmp = new Thread(channel::run);
              tmp.setName("Channel " + ctyp + " " + host);
              if (daemon_thread) {
                tmp.setDaemon(daemon_thread);
              }
              tmp.start();
            }
            break;
          case SSH_MSG_CHANNEL_SUCCESS:
            buf.getInt();
            buf.getShort();
            i = buf.getInt();
            channel = Channel.getChannel(i, this);
            if (channel == null) {
              break;
            }
            channel.reply = 1;
            break;
          case SSH_MSG_CHANNEL_FAILURE:
            buf.getInt();
            buf.getShort();
            i = buf.getInt();
            channel = Channel.getChannel(i, this);
            if (channel == null) {
              break;
            }
            channel.reply = 0;
            break;
          case SSH_MSG_GLOBAL_REQUEST:
            buf.getInt();
            buf.getShort();
            foo = buf.getString(); // request name
            reply = (buf.getByte() != 0);
            if (reply) {
              packet.reset();
              buf.putByte((byte) SSH_MSG_REQUEST_FAILURE);
              write(packet);
            }
            break;
          case SSH_MSG_REQUEST_FAILURE:
          case SSH_MSG_REQUEST_SUCCESS:
            Thread t = grr.getThread();
            if (t != null) {
              grr.setReply(msgType == SSH_MSG_REQUEST_SUCCESS ? 1 : 0);
              if (msgType == SSH_MSG_REQUEST_SUCCESS && grr.getPort() == 0) {
                buf.getInt();
                buf.getShort();
                grr.setPort(buf.getInt());
              }
              t.interrupt();
            }
            break;
          default:
            // System.err.println("Session.run: unsupported type "+msgType);
            throw new IOException("Unknown SSH message type " + msgType);
        }
      }
    } catch (Exception e) {
      in_kex = false;
      if (getLogger().isEnabled(Logger.INFO)) {
        getLogger().log(Logger.INFO,
            "Caught an exception, leaving main loop due to " + e.getMessage());
      }
      // System.err.println("# Session.run");
      // e.printStackTrace();
    }
    try {
      disconnect();
    } catch (NullPointerException e) {
      // System.err.println("@1");
      // e.printStackTrace();
    } catch (Exception e) {
      // System.err.println("@2");
      // e.printStackTrace();
    }
    isConnected = false;
  }

  public void disconnect() {
    if (!isConnected)
      return;
    // System.err.println(this+": disconnect");
    // Thread.dumpStack();
    if (getLogger().isEnabled(Logger.INFO)) {
      getLogger().log(Logger.INFO, "Disconnecting from " + host + " port " + port);
    }
    /*
     * for(int i=0; i<Channel.pool.size(); i++){ try{ Channel
     * c=((Channel)(Channel.pool.elementAt(i))); if(c.session==this) c.eof(); } catch(Exception e){
     * } }
     */

    Channel.disconnect(this);

    isConnected = false;

    PortWatcher.delPort(this);
    ChannelForwardedTCPIP.delPort(this);
    ChannelX11.removeFakedCookie(this);

    synchronized (lock) {
      if (connectThread != null) {
        Thread.yield();
        connectThread.interrupt();
        connectThread = null;
      }
    }
    thread = null;
    try {
      if (io != null) {
        if (io.in != null)
          io.in.close();
        if (io.out != null)
          io.out.close();
        if (io.out_ext != null)
          io.out_ext.close();
      }
      if (proxy == null) {
        if (socket != null)
          socket.close();
      } else {
        synchronized (proxy) {
          proxy.close();
        }
        proxy = null;
      }
    } catch (Exception e) {
      // e.printStackTrace();
    }
    io = null;
    socket = null;

    // RFC 4253 6.4. the 'sequence_number' is never reset, even if keys/algorithms are renegotiated
    // later. Hence we only reset these on session disconnect as the sequence has to start at zero
    // for the first packet during (re)connect.
    seqi = 0;
    seqo = 0;
    initialKex = true;
    doStrictKex = false;
    doExtInfo = false;
    serverSigAlgs = null;

    // synchronized(jsch.pool){
    // jsch.pool.removeElement(this);
    // }

    jsch.removeSession(this);

    // System.gc();
  }

  /**
   * Registers the local port forwarding for loop-back interface. If <code>lport</code> is
   * <code>0</code>, the tcp port will be allocated.
   *
   * @param lport local port for local port forwarding
   * @param host host address for local port forwarding
   * @param rport remote port number for local port forwarding
   * @return an allocated local TCP port number
   * @see #setPortForwardingL(String bind_address, int lport, String host, int rport,
   *      ServerSocketFactory ssf, int connectTimeout)
   */
  public int setPortForwardingL(int lport, String host, int rport) throws JSchException {
    return setPortForwardingL("127.0.0.1", lport, host, rport);
  }

  /**
   * Registers the local port forwarding. If <code>bind_address</code> is an empty string or '*',
   * the port should be available from all interfaces. If <code>bind_address</code> is
   * <code>"localhost"</code> or <code>null</code>, the listening port will be bound for local use
   * only. If <code>lport</code> is <code>0</code>, the tcp port will be allocated.
   *
   * @param bind_address bind address for local port forwarding
   * @param lport local port for local port forwarding
   * @param host host address for local port forwarding
   * @param rport remote port number for local port forwarding
   * @return an allocated local TCP port number
   * @see #setPortForwardingL(String bind_address, int lport, String host, int rport,
   *      ServerSocketFactory ssf, int connectTimeout)
   */
  public int setPortForwardingL(String bind_address, int lport, String host, int rport)
      throws JSchException {
    return setPortForwardingL(bind_address, lport, host, rport, null);
  }

  /**
   * Registers the local port forwarding. If <code>bind_address</code> is an empty string or
   * <code>"*"</code>, the port should be available from all interfaces. If
   * <code>bind_address</code> is <code>"localhost"</code> or <code>null</code>, the listening port
   * will be bound for local use only. If <code>lport</code> is <code>0</code>, the tcp port will be
   * allocated.
   *
   * @param bind_address bind address for local port forwarding
   * @param lport local port for local port forwarding
   * @param host host address for local port forwarding
   * @param rport remote port number for local port forwarding
   * @param ssf socket factory
   * @return an allocated local TCP port number
   * @see #setPortForwardingL(String bind_address, int lport, String host, int rport,
   *      ServerSocketFactory ssf, int connectTimeout)
   */
  public int setPortForwardingL(String bind_address, int lport, String host, int rport,
      ServerSocketFactory ssf) throws JSchException {
    return setPortForwardingL(bind_address, lport, host, rport, ssf, 0);
  }

  /**
   * Registers the local port forwarding. If <code>bind_address</code> is an empty string or
   * <code>"*"</code>, the port should be available from all interfaces. If
   * <code>bind_address</code> is <code>"localhost"</code> or <code>null</code>, the listening port
   * will be bound for local use only. If <code>lport</code> is <code>0</code>, the tcp port will be
   * allocated.
   *
   * @param bind_address bind address for local port forwarding
   * @param lport local port for local port forwarding
   * @param host host address for local port forwarding
   * @param rport remote port number for local port forwarding
   * @param ssf socket factory
   * @param connectTimeout timeout for establishing port connection
   * @return an allocated local TCP port number
   */
  public int setPortForwardingL(String bind_address, int lport, String host, int rport,
      ServerSocketFactory ssf, int connectTimeout) throws JSchException {
    PortWatcher pw = PortWatcher.addPort(this, bind_address, lport, host, rport, ssf);
    pw.setConnectTimeout(connectTimeout);
    Thread tmp = new Thread(pw::run);
    tmp.setName("PortWatcher Thread for " + host);
    if (daemon_thread) {
      tmp.setDaemon(daemon_thread);
    }
    tmp.start();
    return pw.lport;
  }

  public int setSocketForwardingL(String bindAddress, int lport, String socketPath,
      ServerSocketFactory ssf, int connectTimeout) throws JSchException {
    PortWatcher pw = PortWatcher.addSocket(this, bindAddress, lport, socketPath, ssf);
    pw.setConnectTimeout(connectTimeout);
    Thread tmp = new Thread(pw::run);
    tmp.setName("PortWatcher Thread for " + host);
    if (daemon_thread) {
      tmp.setDaemon(daemon_thread);
    }
    tmp.start();
    return pw.lport;
  }

  /**
   * Cancels the local port forwarding assigned at local TCP port <code>lport</code> on loopback
   * interface.
   *
   * @param lport local TCP port
   */
  public void delPortForwardingL(int lport) throws JSchException {
    delPortForwardingL("127.0.0.1", lport);
  }

  /**
   * Cancels the local port forwarding assigned at local TCP port <code>lport</code> on
   * <code>bind_address</code> interface.
   *
   * @param bind_address bind_address of network interfaces
   * @param lport local TCP port
   */
  public void delPortForwardingL(String bind_address, int lport) throws JSchException {
    PortWatcher.delPort(this, bind_address, lport);
  }

  /**
   * Lists the registered local port forwarding.
   *
   * @return a list of "lport:host:hostport"
   */
  public String[] getPortForwardingL() throws JSchException {
    return PortWatcher.getPortForwarding(this);
  }

  /**
   * Registers the remote port forwarding for the loopback interface of the remote.
   *
   * @param rport remote port
   * @param host host address
   * @param lport local port
   * @see #setPortForwardingR(String bind_address, int rport, String host, int lport, SocketFactory
   *      sf)
   */
  public void setPortForwardingR(int rport, String host, int lport) throws JSchException {
    setPortForwardingR(null, rport, host, lport, (SocketFactory) null);
  }

  /**
   * Registers the remote port forwarding. If <code>bind_address</code> is an empty string or
   * <code>"*"</code>, the port should be available from all interfaces. If
   * <code>bind_address</code> is <code>"localhost"</code> or is not given, the listening port will
   * be bound for local use only. Note that if <code>GatewayPorts</code> is <code>"no"</code> on the
   * remote, <code>"localhost"</code> is always used as a bind_address.
   *
   * @param bind_address bind address
   * @param rport remote port
   * @param host host address
   * @param lport local port
   * @see #setPortForwardingR(String bind_address, int rport, String host, int lport, SocketFactory
   *      sf)
   */
  public void setPortForwardingR(String bind_address, int rport, String host, int lport)
      throws JSchException {
    setPortForwardingR(bind_address, rport, host, lport, (SocketFactory) null);
  }

  /**
   * Registers the remote port forwarding for the loopback interface of the remote.
   *
   * @param rport remote port
   * @param host host address
   * @param lport local port
   * @param sf socket factory
   * @see #setPortForwardingR(String bind_address, int rport, String host, int lport, SocketFactory
   *      sf)
   */
  public void setPortForwardingR(int rport, String host, int lport, SocketFactory sf)
      throws JSchException {
    setPortForwardingR(null, rport, host, lport, sf);
  }

  // TODO: This method should return the integer value as the assigned port.
  /**
   * Registers the remote port forwarding. If <code>bind_address</code> is an empty string or
   * <code>"*"</code>, the port should be available from all interfaces. If
   * <code>bind_address</code> is <code>"localhost"</code> or is not given, the listening port will
   * be bound for local use only. Note that if <code>GatewayPorts</code> is <code>"no"</code> on the
   * remote, <code>"localhost"</code> is always used as a bind_address. If <code>rport</code> is
   * <code>0</code>, the TCP port will be allocated on the remote.
   *
   * @param bind_address bind address
   * @param rport remote port
   * @param host host address
   * @param lport local port
   * @param sf socket factory
   */
  public void setPortForwardingR(String bind_address, int rport, String host, int lport,
      SocketFactory sf) throws JSchException {
    int allocated = _setPortForwardingR(bind_address, rport);
    ChannelForwardedTCPIP.addPort(this, bind_address, rport, allocated, host, lport, sf);
  }

  /**
   * Registers the remote port forwarding for the loopback interface of the remote. The TCP
   * connection to <code>rport</code> on the remote will be forwarded to an instance of the class
   * <code>daemon</code>. The class specified by <code>daemon</code> must implement
   * <code>ForwardedTCPIPDaemon</code>.
   *
   * @param rport remote port
   * @param daemon class name, which implements "ForwardedTCPIPDaemon"
   * @see #setPortForwardingR(String bind_address, int rport, String daemon, Object[] arg)
   */
  public void setPortForwardingR(int rport, String daemon) throws JSchException {
    setPortForwardingR(null, rport, daemon, null);
  }

  /**
   * Registers the remote port forwarding for the loopback interface of the remote. The TCP
   * connection to <code>rport</code> on the remote will be forwarded to an instance of the class
   * <code>daemon</code> with the argument <code>arg</code>. The class specified by
   * <code>daemon</code> must implement <code>ForwardedTCPIPDaemon</code>.
   *
   * @param rport remote port
   * @param daemon class name, which implements "ForwardedTCPIPDaemon"
   * @param arg arguments for "daemon"
   * @see #setPortForwardingR(String bind_address, int rport, String daemon, Object[] arg)
   */
  public void setPortForwardingR(int rport, String daemon, Object[] arg) throws JSchException {
    setPortForwardingR(null, rport, daemon, arg);
  }

  /**
   * Registers the remote port forwarding. If <code>bind_address</code> is an empty string or
   * <code>"*"</code>, the port should be available from all interfaces. If
   * <code>bind_address</code> is <code>"localhost"</code> or is not given, the listening port will
   * be bound for local use only. Note that if <code>GatewayPorts</code> is <code>"no"</code> on the
   * remote, <code>"localhost"</code> is always used as a bind_address. The TCP connection to
   * <code>rport</code> on the remote will be forwarded to an instance of the class
   * <code>daemon</code> with the argument <code>arg</code>. The class specified by
   * <code>daemon</code> must implement <code>ForwardedTCPIPDaemon</code>.
   *
   * @param bind_address bind address
   * @param rport remote port
   * @param daemon class name, which implements "ForwardedTCPIPDaemon"
   * @param arg arguments for "daemon"
   * @see #setPortForwardingR(String bind_address, int rport, String daemon, Object[] arg)
   */
  public void setPortForwardingR(String bind_address, int rport, String daemon, Object[] arg)
      throws JSchException {
    int allocated = _setPortForwardingR(bind_address, rport);
    ChannelForwardedTCPIP.addPort(this, bind_address, rport, allocated, daemon, arg);
  }

  /**
   * Lists the registered remote port forwarding.
   *
   * @return a list of "rport:host:hostport"
   */
  public String[] getPortForwardingR() throws JSchException {
    return ChannelForwardedTCPIP.getPortForwarding(this);
  }

  static class Forwarding {
    String bind_address = null;
    int port = -1;
    String host = null;
    int hostport = -1;
    String socketPath = null;
  }

  /**
   * The given argument may be "[bind_address:]port:host:hostport" or "[bind_address:]port
   * host:hostport", which is from LocalForward command of ~/.ssh/config . Also allows
   * "[bind_address:]port:socketPath" or "[bind_address:]port socketPath" for socket forwarding.
   */
  Forwarding parseForwarding(String conf) throws JSchException {
    String[] tmp = conf.split(" ");
    if (tmp.length > 1) { // "[bind_address:]port host:hostport"
      Vector<String> foo = new Vector<>();
      for (int i = 0; i < tmp.length; i++) {
        if (tmp[i].length() == 0)
          continue;
        foo.addElement(tmp[i].trim());
      }
      StringBuilder sb = new StringBuilder(); // join
      for (int i = 0; i < foo.size(); i++) {
        sb.append(foo.elementAt(i));
        if (i + 1 < foo.size())
          sb.append(":");
      }
      conf = sb.toString();
    }

    String org = conf;
    Forwarding f = new Forwarding();
    try {
      if (conf.lastIndexOf(":") == -1)
        throw new JSchException("parseForwarding: " + org);
      try {
        f.hostport = Integer.parseInt(conf.substring(conf.lastIndexOf(":") + 1));
        conf = conf.substring(0, conf.lastIndexOf(":"));
        if (conf.lastIndexOf(":") == -1)
          throw new JSchException("parseForwarding: " + org);
        f.host = conf.substring(conf.lastIndexOf(":") + 1);
      } catch (NumberFormatException e) {
        f.socketPath = conf.substring(conf.lastIndexOf(":") + 1);
      }
      conf = conf.substring(0, conf.lastIndexOf(":"));
      if (conf.lastIndexOf(":") != -1) {
        f.port = Integer.parseInt(conf.substring(conf.lastIndexOf(":") + 1));
        conf = conf.substring(0, conf.lastIndexOf(":"));
        if (conf.length() == 0 || conf.equals("*"))
          conf = "0.0.0.0";
        if (conf.equals("localhost"))
          conf = "127.0.0.1";
        f.bind_address = conf;
      } else {
        f.port = Integer.parseInt(conf);
        f.bind_address = "127.0.0.1";
      }
    } catch (NumberFormatException e) {
      throw new JSchException("parseForwarding: " + e.toString(), e);
    }
    return f;
  }

  /**
   * Registers the local port forwarding. The argument should be in the format like
   * "[bind_address:]port:host:hostport". If <code>bind_address</code> is an empty string or
   * <code>"*"</code>, the port should be available from all interfaces. If
   * <code>bind_address</code> is <code>"localhost"</code> or is not given, the listening port will
   * be bound for local use only.
   *
   * @param conf configuration of local port forwarding
   * @return an assigned port number
   * @see #setPortForwardingL(String bind_address, int lport, String host, int rport)
   */
  public int setPortForwardingL(String conf) throws JSchException {
    Forwarding f = parseForwarding(conf);
    return setPortForwardingL(f.bind_address, f.port, f.host, f.hostport);
  }

  /**
   * Registers the remote port forwarding. The argument should be in the format like
   * "[bind_address:]port:host:hostport". If the bind_address is not given, the default is to only
   * bind to loopback addresses. If the bind_address is <code>"*"</code> or an empty string, then
   * the forwarding is requested to listen on all interfaces. Note that if <code>GatewayPorts</code>
   * is <code>"no"</code> on the remote, <code>"localhost"</code> is always used for bind_address.
   * If the specified remote is <code>"0"</code>, the TCP port will be allocated on the remote.
   *
   * @param conf configuration of remote port forwarding
   * @return an allocated TCP port on the remote.
   * @see #setPortForwardingR(String bind_address, int rport, String host, int rport)
   */
  public int setPortForwardingR(String conf) throws JSchException {
    Forwarding f = parseForwarding(conf);
    int allocated = _setPortForwardingR(f.bind_address, f.port);
    ChannelForwardedTCPIP.addPort(this, f.bind_address, f.port, allocated, f.host, f.hostport,
        null);
    return allocated;
  }

  /**
   * Instantiates an instance of stream-forwarder to <code>host</code>:<code>port</code>. Set I/O
   * stream to the given channel, and then invoke Channel#connect() method.
   *
   * @param host remote host, which the given stream will be plugged to.
   * @param port remote port, which the given stream will be plugged to.
   */
  public Channel getStreamForwarder(String host, int port) throws JSchException {
    ChannelDirectTCPIP channel = new ChannelDirectTCPIP();
    channel.init();
    this.addChannel(channel);
    channel.setHost(host);
    channel.setPort(port);
    return channel;
  }

  private static class GlobalRequestReply {
    private Thread thread = null;
    private int reply = -1;
    private int port = 0;

    void setThread(Thread thread) {
      this.thread = thread;
      this.reply = -1;
    }

    Thread getThread() {
      return thread;
    }

    void setReply(int reply) {
      this.reply = reply;
    }

    int getReply() {
      return this.reply;
    }

    int getPort() {
      return this.port;
    }

    void setPort(int port) {
      this.port = port;
    }
  }

  private GlobalRequestReply grr = new GlobalRequestReply();

  private int _setPortForwardingR(String bind_address, int rport) throws JSchException {
    synchronized (grr) {
      Buffer buf = new Buffer(200); // ??
      Packet packet = new Packet(buf);

      String address_to_bind = ChannelForwardedTCPIP.normalize(bind_address);

      grr.setThread(Thread.currentThread());
      grr.setPort(rport);

      try {
        // byte SSH_MSG_GLOBAL_REQUEST 80
        // string "tcpip-forward"
        // boolean want_reply
        // string address_to_bind
        // uint32 port number to bind
        packet.reset();
        buf.putByte((byte) SSH_MSG_GLOBAL_REQUEST);
        buf.putString(Util.str2byte("tcpip-forward"));
        buf.putByte((byte) 1);
        buf.putString(Util.str2byte(address_to_bind));
        buf.putInt(rport);
        write(packet);
      } catch (Exception e) {
        grr.setThread(null);
        throw new JSchException(e.toString(), e);
      }

      int count = 0;
      int reply = grr.getReply();
      while (count < 10 && reply == -1) {
        try {
          Thread.sleep(1000);
        } catch (Exception e) {
        }
        count++;
        reply = grr.getReply();
      }
      grr.setThread(null);
      if (reply != 1) {
        throw new JSchException("remote port forwarding failed for listen port " + rport);
      }
      rport = grr.getPort();
    }
    return rport;
  }

  /**
   * Cancels the remote port forwarding assigned at remote TCP port <code>rport</code>.
   *
   * @param rport remote TCP port
   */
  public void delPortForwardingR(int rport) throws JSchException {
    this.delPortForwardingR(null, rport);
  }

  /**
   * Cancels the remote port forwarding assigned at remote TCP port <code>rport</code> bound on the
   * interface at <code>bind_address</code>.
   *
   * @param bind_address bind address of the interface on the remote
   * @param rport remote TCP port
   */
  public void delPortForwardingR(String bind_address, int rport) throws JSchException {
    ChannelForwardedTCPIP.delPort(this, bind_address, rport);
  }

  private void initDeflater(String method) throws JSchException {
    Compression odeflater = deflater;
    if (method.equals("none")) {
      deflater = null;
      if (odeflater != null) {
        odeflater.end();
      }
      return;
    }
    String foo = getConfig(method);
    if (foo != null) {
      if (method.equals("zlib") || (isAuthed && method.equals("zlib@openssh.com"))) {
        try {
          Class<? extends Compression> c = Class.forName(foo).asSubclass(Compression.class);
          deflater = c.getDeclaredConstructor().newInstance();
          int level = 6;
          try {
            level = Integer.parseInt(getConfig("compression_level"));
          } catch (Exception ee) {
          }
          deflater.init(Compression.DEFLATER, level, this);
        } catch (Exception ee) {
          throw new JSchException(ee.toString(), ee);
          // System.err.println(foo+" isn't accessible.");
        } finally {
          if (odeflater != null) {
            odeflater.end();
          }
        }
      }
    }
  }

  private void initInflater(String method) throws JSchException {
    Compression oinflater = inflater;
    if (method.equals("none")) {
      inflater = null;
      if (oinflater != null) {
        oinflater.end();
      }
      return;
    }
    String foo = getConfig(method);
    if (foo != null) {
      if (method.equals("zlib") || (isAuthed && method.equals("zlib@openssh.com"))) {
        try {
          Class<? extends Compression> c = Class.forName(foo).asSubclass(Compression.class);
          inflater = c.getDeclaredConstructor().newInstance();
          inflater.init(Compression.INFLATER, 0, this);
        } catch (Exception ee) {
          throw new JSchException(ee.toString(), ee);
          // System.err.println(foo+" isn't accessible.");
        } finally {
          if (oinflater != null) {
            oinflater.end();
          }
        }
      }
    }
  }

  void addChannel(Channel channel) {
    channel.setSession(this);
  }

  String[] getServerSigAlgs() {
    return serverSigAlgs;
  }

  public void setProxy(Proxy proxy) {
    this.proxy = proxy;
  }

  public void setHost(String host) {
    this.host = host;
  }

  public void setPort(int port) {
    this.port = port;
  }

  void setUserName(String username) {
    this.username = username;
  }

  public void setUserInfo(UserInfo userinfo) {
    this.userinfo = userinfo;
  }

  public UserInfo getUserInfo() {
    return userinfo;
  }

  public void setInputStream(InputStream in) {
    this.in = in;
  }

  public void setOutputStream(OutputStream out) {
    this.out = out;
  }

  public void setX11Host(String host) {
    ChannelX11.setHost(host);
  }

  public void setX11Port(int port) {
    ChannelX11.setPort(port);
  }

  public void setX11Cookie(String cookie) {
    ChannelX11.setCookie(cookie);
  }

  public void setPassword(String password) {
    if (password != null)
      this.password = Util.str2byte(password);
  }

  public void setPassword(byte[] password) {
    if (password != null) {
      this.password = new byte[password.length];
      System.arraycopy(password, 0, this.password, 0, password.length);
    }
  }

  public void setConfig(Properties newconf) {
    Hashtable<String, String> foo = new Hashtable<>();
    for (String key : newconf.stringPropertyNames()) {
      foo.put(key, newconf.getProperty(key));
    }
    setConfig(foo);
  }

  public void setConfig(Hashtable<String, String> newconf) {
    synchronized (lock) {
      if (config == null)
        config = new Hashtable<>();
      for (Enumeration<String> e = newconf.keys(); e.hasMoreElements();) {
        String newkey = e.nextElement();
        String key =
            (newkey.equals("PubkeyAcceptedKeyTypes") ? "PubkeyAcceptedAlgorithms" : newkey);
        String value = newconf.get(newkey);
        config.put(key, value);
      }
    }
  }

  public void setConfig(String key, String value) {
    synchronized (lock) {
      if (config == null) {
        config = new Hashtable<>();
      }
      if (key.equals("PubkeyAcceptedKeyTypes")) {
        config.put("PubkeyAcceptedAlgorithms", value);
      } else {
        config.put(key, value);
      }
    }
  }

  public String getConfig(String key) {
    if (key.equals("PubkeyAcceptedKeyTypes")) {
      key = "PubkeyAcceptedAlgorithms";
    }
    Object foo = null;
    if (config != null) {
      foo = config.get(key);
      if (foo instanceof String)
        return (String) foo;
    }
    foo = JSch.getConfig(key);
    if (foo instanceof String)
      return (String) foo;
    return null;
  }

  public void setSocketFactory(SocketFactory sfactory) {
    socket_factory = sfactory;
  }

  public boolean isConnected() {
    return isConnected;
  }

  public int getTimeout() {
    return timeout;
  }

  public void setTimeout(int timeout) throws JSchException {
    if (socket == null) {
      if (timeout < 0) {
        throw new JSchException("invalid timeout value");
      }
      this.timeout = timeout;
      return;
    }
    try {
      socket.setSoTimeout(timeout);
      this.timeout = timeout;
    } catch (Exception e) {
      throw new JSchException(e.toString(), e);
    }
  }

  public String getServerVersion() {
    return Util.byte2str(V_S);
  }

  public String getClientVersion() {
    return Util.byte2str(V_C);
  }

  public void setClientVersion(String cv) {
    V_C = Util.str2byte(cv);
  }

  public void sendIgnore() throws Exception {
    Buffer buf = new Buffer();
    Packet packet = new Packet(buf);
    packet.reset();
    buf.putByte((byte) SSH_MSG_IGNORE);
    write(packet);
  }

  private static final byte[] keepalivemsg = Util.str2byte("keepalive@jcraft.com");

  public void sendKeepAliveMsg() throws Exception {
    Buffer buf = new Buffer();
    Packet packet = new Packet(buf);
    packet.reset();
    buf.putByte((byte) SSH_MSG_GLOBAL_REQUEST);
    buf.putString(keepalivemsg);
    buf.putByte((byte) 1);
    write(packet);
  }

  private static final byte[] nomoresessions = Util.str2byte("no-more-sessions@openssh.com");

  public void noMoreSessionChannels() throws Exception {
    Buffer buf = new Buffer();
    Packet packet = new Packet(buf);
    packet.reset();
    buf.putByte((byte) SSH_MSG_GLOBAL_REQUEST);
    buf.putString(nomoresessions);
    buf.putByte((byte) 0);
    write(packet);
  }

  private HostKey hostkey = null;

  public HostKey getHostKey() {
    return hostkey;
  }

  public String getHost() {
    return host;
  }

  public String getUserName() {
    return username;
  }

  public int getPort() {
    return port;
  }

  public void setHostKeyAlias(String hostKeyAlias) {
    this.hostKeyAlias = hostKeyAlias;
  }

  public String getHostKeyAlias() {
    return hostKeyAlias;
  }

  /**
   * Sets the interval to send a keep-alive message. If zero is specified, any keep-alive message
   * must not be sent. The default interval is zero.
   *
   * @param interval the specified interval, in milliseconds.
   * @see #getServerAliveInterval()
   */
  public void setServerAliveInterval(int interval) throws JSchException {
    setTimeout(interval);
    this.serverAliveInterval = interval;
  }

  /**
   * Returns setting for the interval to send a keep-alive message.
   *
   * @see #setServerAliveInterval(int)
   */
  public int getServerAliveInterval() {
    return this.serverAliveInterval;
  }

  /**
   * Sets the number of keep-alive messages which may be sent without receiving any messages back
   * from the server. If this threshold is reached while keep-alive messages are being sent, the
   * connection will be disconnected. The default value is one.
   *
   * @param count the specified count
   * @see #getServerAliveCountMax()
   */
  public void setServerAliveCountMax(int count) {
    this.serverAliveCountMax = count;
  }

  /**
   * Returns setting for the threshold to send keep-alive messages.
   *
   * @see #setServerAliveCountMax(int)
   */
  public int getServerAliveCountMax() {
    return this.serverAliveCountMax;
  }

  public void setDaemonThread(boolean enable) {
    this.daemon_thread = enable;
  }

  private String[] checkCiphers(String ciphers) {
    if (ciphers == null || ciphers.length() == 0)
      return null;

    if (getLogger().isEnabled(Logger.INFO)) {
      getLogger().log(Logger.INFO, "CheckCiphers: " + ciphers);
    }

    String cipherc2s = getConfig("cipher.c2s");
    String ciphers2c = getConfig("cipher.s2c");

    Vector<String> result = new Vector<>();
    String[] _ciphers = Util.split(ciphers, ",");
    for (int i = 0; i < _ciphers.length; i++) {
      String cipher = _ciphers[i];
      if (ciphers2c.indexOf(cipher) == -1 && cipherc2s.indexOf(cipher) == -1)
        continue;
      if (!checkCipher(getConfig(cipher))) {
        result.addElement(cipher);
      }
    }
    if (result.size() == 0)
      return null;
    String[] foo = new String[result.size()];
    System.arraycopy(result.toArray(), 0, foo, 0, result.size());

    if (getLogger().isEnabled(Logger.INFO)) {
      for (int i = 0; i < foo.length; i++) {
        getLogger().log(Logger.INFO, foo[i] + " is not available.");
      }
    }

    return foo;
  }

  static boolean checkCipher(String cipher) {
    try {
      Class<? extends Cipher> c = Class.forName(cipher).asSubclass(Cipher.class);
      Cipher _c = c.getDeclaredConstructor().newInstance();
      _c.init(Cipher.ENCRYPT_MODE, new byte[_c.getBlockSize()], new byte[_c.getIVSize()]);
      return true;
    } catch (Exception | NoClassDefFoundError e) {
      return false;
    }
  }

  private String[] checkMacs(String macs) {
    if (macs == null || macs.length() == 0)
      return null;

    if (getLogger().isEnabled(Logger.INFO)) {
      getLogger().log(Logger.INFO, "CheckMacs: " + macs);
    }

    String macc2s = getConfig("mac.c2s");
    String macs2c = getConfig("mac.s2c");

    Vector<String> result = new Vector<>();
    String[] _macs = Util.split(macs, ",");
    for (int i = 0; i < _macs.length; i++) {
      String mac = _macs[i];
      if (macs2c.indexOf(mac) == -1 && macc2s.indexOf(mac) == -1)
        continue;
      if (!checkMac(getConfig(mac))) {
        result.addElement(mac);
      }
    }
    if (result.size() == 0)
      return null;
    String[] foo = new String[result.size()];
    System.arraycopy(result.toArray(), 0, foo, 0, result.size());

    if (getLogger().isEnabled(Logger.INFO)) {
      for (int i = 0; i < foo.length; i++) {
        getLogger().log(Logger.INFO, foo[i] + " is not available.");
      }
    }

    return foo;
  }

  static boolean checkMac(String mac) {
    try {
      Class<? extends MAC> c = Class.forName(mac).asSubclass(MAC.class);
      MAC _c = c.getDeclaredConstructor().newInstance();
      _c.init(new byte[_c.getBlockSize()]);
      return true;
    } catch (Exception | NoClassDefFoundError e) {
      return false;
    }
  }

  private String[] checkKexes(String kexes) {
    if (kexes == null || kexes.length() == 0)
      return null;

    if (getLogger().isEnabled(Logger.INFO)) {
      getLogger().log(Logger.INFO, "CheckKexes: " + kexes);
    }

    Vector<String> result = new Vector<>();
    String[] _kexes = Util.split(kexes, ",");
    for (int i = 0; i < _kexes.length; i++) {
      if (!checkKex(this, getConfig(_kexes[i]))) {
        result.addElement(_kexes[i]);
      }
    }
    if (result.size() == 0)
      return null;
    String[] foo = new String[result.size()];
    System.arraycopy(result.toArray(), 0, foo, 0, result.size());

    if (getLogger().isEnabled(Logger.INFO)) {
      for (int i = 0; i < foo.length; i++) {
        getLogger().log(Logger.INFO, foo[i] + " is not available.");
      }
    }

    return foo;
  }

  static boolean checkKex(Session s, String kex) {
    try {
      Class<? extends KeyExchange> c = Class.forName(kex).asSubclass(KeyExchange.class);
      KeyExchange _c = c.getDeclaredConstructor().newInstance();
      _c.doInit(s, null, null, null, null);
      return true;
    } catch (Exception | NoClassDefFoundError e) {
      return false;
    }
  }

  private String[] checkSignatures(String sigs) {
    if (sigs == null || sigs.length() == 0)
      return null;

    if (getLogger().isEnabled(Logger.INFO)) {
      getLogger().log(Logger.INFO, "CheckSignatures: " + sigs);
    }

    Vector<String> result = new Vector<>();
    String[] _sigs = Util.split(sigs, ",");
    for (int i = 0; i < _sigs.length; i++) {
      try {
        Class<? extends Signature> c =
            Class.forName(JSch.getConfig(_sigs[i])).asSubclass(Signature.class);
        final Signature sig = c.getDeclaredConstructor().newInstance();
        sig.init();
      } catch (Exception | NoClassDefFoundError e) {
        result.addElement(_sigs[i]);
      }
    }
    if (result.size() == 0)
      return null;
    String[] foo = new String[result.size()];
    System.arraycopy(result.toArray(), 0, foo, 0, result.size());
    if (getLogger().isEnabled(Logger.INFO)) {
      for (int i = 0; i < foo.length; i++) {
        getLogger().log(Logger.INFO, foo[i] + " is not available.");
      }
    }
    return foo;
  }

  /**
   * Sets the identityRepository, which will be referred in the public key authentication. The
   * default value is <code>null</code>.
   *
   * @param identityRepository
   * @see #getIdentityRepository()
   */
  public void setIdentityRepository(IdentityRepository identityRepository) {
    this.identityRepository = identityRepository;
  }

  /**
   * Gets the identityRepository. If this.identityRepository is <code>null</code>,
   * JSch#getIdentityRepository() will be invoked.
   *
   * @see JSch#getIdentityRepository()
   */
  IdentityRepository getIdentityRepository() {
    if (identityRepository == null)
      return jsch.getIdentityRepository();
    return identityRepository;
  }

  /**
   * Sets the hostkeyRepository, which will be referred in checking host keys.
   *
   * @param hostkeyRepository
   * @see #getHostKeyRepository()
   */
  public void setHostKeyRepository(HostKeyRepository hostkeyRepository) {
    this.hostkeyRepository = hostkeyRepository;
  }

  /**
   * Gets the hostkeyRepository. If this.hostkeyRepository is <code>null</code>,
   * JSch#getHostKeyRepository() will be invoked.
   *
   * @see JSch#getHostKeyRepository()
   */
  public HostKeyRepository getHostKeyRepository() {
    if (hostkeyRepository == null)
      return jsch.getHostKeyRepository();
    return hostkeyRepository;
  }

  /*
   * // setProxyCommand("ssh -l user2 host2 -o 'ProxyCommand ssh user1@host1 nc host2 22' nc %h %p")
   * public void setProxyCommand(String command){ setProxy(new ProxyCommand(command)); }
   *
   * class ProxyCommand implements Proxy { String command; Process p = null; InputStream in = null;
   * OutputStream out = null; ProxyCommand(String command){ this.command = command; } public void
   * connect(SocketFactory socket_factory, String host, int port, int timeout) throws Exception {
   * String _command = command.replace("%h", host); _command = _command.replace("%p", new
   * Integer(port).toString()); p = Runtime.getRuntime().exec(_command); in = p.getInputStream();
   * out = p.getOutputStream(); } public Socket getSocket() { return null; } public InputStream
   * getInputStream() { return in; } public OutputStream getOutputStream() { return out; } public
   * void close() { try{ if(p!=null){ p.getErrorStream().close(); p.getOutputStream().close();
   * p.getInputStream().close(); p.destroy(); p=null; } } catch(IOException e){ } } }
   */

  private void applyConfig() throws JSchException {
    ConfigRepository configRepository = jsch.getConfigRepository();
    if (configRepository == null) {
      return;
    }

    ConfigRepository.Config config = configRepository.getConfig(org_host);

    String value = null;

    if (username == null) {
      value = config.getUser();
      if (value != null)
        username = value;
    }

    value = config.getHostname();
    if (value != null)
      host = value;

    int port = config.getPort();
    if (port != -1)
      this.port = port;

    checkConfig(config, "kex");
    checkConfig(config, "server_host_key");
    checkConfig(config, "prefer_known_host_key_types");
    checkConfig(config, "enable_server_sig_algs");
    checkConfig(config, "enable_ext_info_in_auth");
    checkConfig(config, "enable_strict_kex");
    checkConfig(config, "require_strict_kex");
    checkConfig(config, "enable_pubkey_auth_query");
    checkConfig(config, "try_additional_pubkey_algorithms");
    checkConfig(config, "enable_auth_none");
    checkConfig(config, "use_sftp_write_flush_workaround");

    checkConfig(config, "cipher.c2s");
    checkConfig(config, "cipher.s2c");
    checkConfig(config, "mac.c2s");
    checkConfig(config, "mac.s2c");
    checkConfig(config, "compression.c2s");
    checkConfig(config, "compression.s2c");
    checkConfig(config, "compression_level");

    checkConfig(config, "StrictHostKeyChecking");
    checkConfig(config, "HashKnownHosts");
    checkConfig(config, "PreferredAuthentications");
    checkConfig(config, "PubkeyAcceptedAlgorithms");
    checkConfig(config, "FingerprintHash");
    checkConfig(config, "MaxAuthTries");
    checkConfig(config, "ClearAllForwardings");

    value = config.getValue("HostKeyAlias");
    if (value != null)
      this.setHostKeyAlias(value);

    value = config.getValue("UserKnownHostsFile");
    if (value != null) {
      KnownHosts kh = new KnownHosts(jsch);
      kh.setKnownHosts(value);
      this.setHostKeyRepository(kh);
    }

    String[] values = config.getValues("IdentityFile");
    if (values != null) {
      String[] global = configRepository.getConfig("").getValues("IdentityFile");
      if (global != null) {
        for (int i = 0; i < global.length; i++) {
          jsch.addIdentity(global[i]);
        }
      } else {
        global = new String[0];
      }
      if (values.length - global.length > 0) {
        IdentityRepositoryWrapper ir =
            new IdentityRepositoryWrapper(jsch.getIdentityRepository(), true);
        for (int i = 0; i < values.length; i++) {
          String ifile = values[i];
          for (int j = 0; j < global.length; j++) {
            if (!ifile.equals(global[j]))
              continue;
            ifile = null;
            break;
          }
          if (ifile == null)
            continue;
          Identity identity = IdentityFile.newInstance(ifile, null, jsch.instLogger);
          ir.add(identity);
        }
        this.setIdentityRepository(ir);
      }
    }

    value = config.getValue("ServerAliveInterval");
    if (value != null) {
      try {
        this.setServerAliveInterval(Integer.parseInt(value));
      } catch (NumberFormatException e) {
      }
    }

    value = config.getValue("ConnectTimeout");
    if (value != null) {
      try {
        setTimeout(Integer.parseInt(value));
      } catch (NumberFormatException e) {
      }
    }

    value = config.getValue("MaxAuthTries");
    if (value != null) {
      setConfig("MaxAuthTries", value);
    }

    value = config.getValue("ClearAllForwardings");
    if (value != null) {
      setConfig("ClearAllForwardings", value);
    }
  }

  private void applyConfigChannel(ChannelSession channel) throws JSchException {
    ConfigRepository configRepository = jsch.getConfigRepository();
    if (configRepository == null) {
      return;
    }

    ConfigRepository.Config config = configRepository.getConfig(org_host);

    String value = null;

    value = config.getValue("ForwardAgent");
    if (value != null) {
      channel.setAgentForwarding(value.equals("yes"));
    }

    value = config.getValue("RequestTTY");
    if (value != null) {
      channel.setPty(value.equals("yes"));
    }
  }

  private void requestPortForwarding() throws JSchException {

    if (getConfig("ClearAllForwardings").equals("yes"))
      return;

    ConfigRepository configRepository = jsch.getConfigRepository();
    if (configRepository == null) {
      return;
    }

    ConfigRepository.Config config = configRepository.getConfig(org_host);

    String[] values = config.getValues("LocalForward");
    if (values != null) {
      for (int i = 0; i < values.length; i++) {
        setPortForwardingL(values[i]);
      }
    }

    values = config.getValues("RemoteForward");
    if (values != null) {
      for (int i = 0; i < values.length; i++) {
        setPortForwardingR(values[i]);
      }
    }
  }

  private void checkConfig(ConfigRepository.Config config, String key) {
    String value = config.getValue(key);
    if (value == null && key.equals("PubkeyAcceptedAlgorithms"))
      value = config.getValue("PubkeyAcceptedKeyTypes");
    if (value != null)
      this.setConfig(key, value);
  }

  /**
   * Returns the logger being used by this instance of Session. If no particular logger has been
   * set, the instance logger of the jsch instance is returned this session belongs to.
   *
   * @return The logger
   */
  public Logger getLogger() {
    if (logger != null) {
      return logger;
    }
    return jsch.getInstanceLogger();
  }

  /**
   * Sets the logger being used by this instance of Session
   *
   * @param logger The logger or <code>null</code> if the instance logger of this instance's jsch
   *        instance should be used
   */
  public void setLogger(Logger logger) {
    this.logger = logger;
  }

  int getBufferMargin() {
    int buffer_margin = 32 + // maximum padding length
        32; // margin for deflater; deflater may inflate data

    Cipher _c2scipher = c2scipher;
    MAC _c2smac = c2smac;

    // maximum mac length
    int mac_length = 20;
    if (_c2scipher != null && (_c2scipher.isChaCha20() || _c2scipher.isAEAD())) {
      if (_c2scipher.getTagSize() > mac_length) {
        mac_length = _c2scipher.getTagSize();
      }
    } else if (_c2smac != null) {
      if (_c2smac.getBlockSize() > mac_length) {
        mac_length = _c2smac.getBlockSize();
      }
    }
    buffer_margin += mac_length;

    return buffer_margin;
  }
}
