/*
 * @(#)Ssh.java
 *
 * Copyright (c) 2001-2004 JScape
 * 1147 S. 53rd Pl., Mesa, Arizona, 85206, U.S.A.
 * All rights reserved.
 *
 * This software is the confidential and proprietary information of
 * JScape. ("Confidential Information").  You shall not disclose such
 * Confidential Information and shall use it only in accordance with
 * the terms of the license agreement you entered into with JScape.
 */
package com.jscape.inet.ssh;

import com.jscape.inet.proxy.Proxies;
import com.jscape.inet.proxy.ProxyDescriptor;
import com.jscape.inet.proxy.Proxy;
import com.jscape.inet.proxy.ProxySocketConnector;
import com.jscape.inet.ssh.protocol.v2.authentication.*;
import com.jscape.inet.ssh.protocol.v2.connection.SessionChannelConnection;
import com.jscape.inet.ssh.protocol.v2.connection.SessionConnection;
import com.jscape.inet.ssh.protocol.v2.connection.SessionConnector;
import com.jscape.inet.ssh.protocol.v2.messages.Opcode;
import com.jscape.inet.ssh.protocol.v2.messages.TerminalMode;
import com.jscape.inet.ssh.util.SshHostKeys;
import com.jscape.inet.ssh.util.SshParameters;
import com.jscape.util.CollectionTools;
import com.jscape.util.Time;
import com.jscape.util.log.DebugStreamLogger;
import com.jscape.util.net.ConnectionDescriptor;
import com.jscape.util.net.TransportAddress;
import com.jscape.util.net.connection.Connector;
import com.jscape.util.net.connection.SyncRawConnection;
import com.jscape.util.net.socket.SocketConnector;
import com.jscape.util.net.socket.SocketParameters;

import java.io.*;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.logging.Level;
import java.util.logging.Logger;

import static com.jscape.util.Parameters.assertNotNull;

/**
 * Implements the basic functionality of a SSH2 (Secure Shell) client.
 * <p/>
 * Example Usage:
 * <p/>
 * <pre>
 * public class SshExample implements SshListener {
 *
 *      // state of SSH connection
 *      private boolean connected = false;
 *
 *      public SshExample() {
 *              String hostname = null;
 *              String username = null;
 *              String password = null;
 *              Ssh ssh = null;
 *
 *              try {
 *                      BufferedReader bin =
 *                              new BufferedReader(new InputStreamReader(System.in));
 *                      System.out.print("Enter SSH hostname: ");
 *                      hostname = bin.readLine();
 *
 *                      System.out.print("Enter SSH username: ");
 *                      username = bin.readLine();
 *
 *                      System.out.print("Enter SSH password: ");
 *                      password = bin.readLine();
 *
 *                      // create new SshParameters instance
 *                      SshParameters sshParams = new SshParameters(hostname,username,password);
 *
 *                      // set key exchanges, ciphers, macs and compressions if needed
 *                      SshConfiguration sshConfiguration = SshConfiguration.configurationFor(new ComponentClientKeyExchangeFactory(ClientKeyExchanges.DH_GROUP_SHA256, ClientKeyExchanges.DH_GROUP_SHA1),
 *                                                                      new JceEncryptionFactory(JceEncryptions.AES256_CBC, JceEncryptions.BLOWFISH_CBC),
 *                                                                      new JceMacFactory(JceMacs.HMAC_SHA2_512, JceMacs.HMAC_SHA1),
 *                                                                      new ComponentCompressionFactory(Compressions.NONE));
 *
 *                      // create new Ssh instance
 *                      ssh = new Ssh(sshParams, sshConfiguration);
 *
 *                      // register to capture events
 *                      ssh.addSshListener(this);
 *
 *                      System.out.println("Connecting please wait...");
 *
 *
 *                      // connect
 *                      ssh.connect();
 *
 *                      // get output stream for writing data to SSH server
 *                      OutputStream out = ssh.getOutputStream();
 *
 *                      // holds line entered at console
 *                      String line = null;
 *
 *                      // read data from console
 *                      while (connected && (line = bin.readLine()) != null) {
 *                              // send line with LF to SSH server
 *                              line += "\n";
 *                              try {
 *                                out.write(line.getBytes());
 *                                out.flush();
 *                 } catch(Exception ioe){
 *                                connected = false;
 *                 }
 *             }
 *         } catch (Exception e) {
 *                      e.printStackTrace();
 *         } finally {
 *                      try {
 *                              if(connected) {
 *                                ssh.disconnect();
 *                 }
 *             } catch(Exception e) {
 *
 *             }
 *         }
 *     }
 *
 *      // captures SshConnectedEvent
 *      public void connected(SshConnectedEvent ev) {
 *              System.out.println("Connected: " + ev.getHost());
 *              connected = true;
 *     }
 *
 *
 *      // captures SshDataReceivedEvent
 *      public void dataReceived(SshDataReceivedEvent ev) {
 *              // send data received to console
 *              System.out.print(ev.getData());
 *     }
 *
 *      // captures SshDisconnectedEvent event
 *      public void disconnected(SshDisconnectedEvent ev) {
 *              System.out.println("Disconnected: " + ev.getHost() + ". Press Enter to exit");
 *              connected = false;
 *     }
 *     }
 * </pre>
 *
 * @author vglass
 */
public class Ssh
        implements AutoCloseable {

    public static final int MSG_DISCONNECT = 1;     // [SSH-TRANS]
    public static final int MSG_IGNORE = 2;     // [SSH-TRANS]
    public static final int MSG_UNIMPLEMENTED = 3;     // [SSH-TRANS]
    public static final int MSG_DEBUG = 4;     // [SSH-TRANS]
    public static final int MSG_SERVICE_REQUEST = 5;     // [SSH-TRANS]
    public static final int MSG_SERVICE_ACCEPT = 6;     // [SSH-TRANS]
    public static final int MSG_KEXINIT = 20;    // [SSH-TRANS]
    public static final int MSG_NEWKEYS = 21;    // [SSH-TRANS]
    public static final int MSG_KEXDH_INIT = 30;    // [SSH-TRANS]
    public static final int MSG_KEXDH_REPLY = 31;    // [SSH-TRANS]
    public static final int MSG_USERAUTH_REQUEST = 50;    // [SSH-USERAUTH]
    public static final int MSG_USERAUTH_FAILURE = 51;    // [SSH-USERAUTH]
    public static final int MSG_USERAUTH_SUCCESS = 52;    // [SSH-USERAUTH]
    public static final int MSG_USERAUTH_BANNER = 53;    // [SSH-USERAUTH]
    public static final int MSG_USERAUTH_PK_OK = 60;    // [SSH-USERAUTH]
    public static final int MSG_GLOBAL_REQUEST = 80;    // [SSH-CONNECT]
    public static final int MSG_REQUEST_SUCCESS = 81;    // [SSH-CONNECT]
    public static final int MSG_REQUEST_FAILURE = 82;    // [SSH-CONNECT]
    public static final int MSG_CHANNEL_OPEN = 90;    // [SSH-CONNECT]
    public static final int MSG_CHANNEL_OPEN_CONFIRMATION = 91;    // [SSH-CONNECT]
    public static final int MSG_CHANNEL_OPEN_FAILURE = 92;    // [SSH-CONNECT]
    public static final int MSG_CHANNEL_WINDOW_ADJUST = 93;    // [SSH-CONNECT]
    public static final int MSG_CHANNEL_DATA = 94;    // [SSH-CONNECT]
    public static final int MSG_CHANNEL_EXTENDED_DATA = 95;    // [SSH-CONNECT]
    public static final int MSG_CHANNEL_EOF = 96;    // [SSH-CONNECT]
    public static final int MSG_CHANNEL_CLOSE = 97;    // [SSH-CONNECT]
    public static final int MSG_CHANNEL_REQUEST = 98;    // [SSH-CONNECT]
    public static final int MSG_CHANNEL_SUCCESS = 99;    // [SSH-CONNECT]
    public static final int MSG_CHANNEL_FAILURE = 100;   // [SSH-CONNECT]


    /* DisconnectReasons Codes
     *
     * The DisconnectReasons code is an 8-bit value, which describes the disconnect
     * reason. Requests for assignments of new disconnect codes must be
     * accompanied by an RFC which describes the new disconnect reason code.
     *
     *
     *                 DisconnectReasons code                               Value    Reference
     *                 ----------------                              -----    ---------   */
    public static final int DISCONNECT_HOST_NOT_ALLOWED_TO_CONNECT = 1;  // [SSH-TRANS]
    public static final int DISCONNECT_PROTOCOL_ERROR = 2;  // [SSH-TRANS]
    public static final int DISCONNECT_KEY_EXCHANGE_FAILED = 3;  // [SSH-TRANS]
    public static final int DISCONNECT_RESERVED = 4;  // [SSH-TRANS]
    public static final int DISCONNECT_MAC_ERROR = 5;  // [SSH-TRANS]
    public static final int DISCONNECT_COMPRESSION_ERROR = 6;  // [SSH-TRANS]
    public static final int DISCONNECT_SERVICE_NOT_AVAILABLE = 7;  // [SSH-TRANS]
    public static final int DISCONNECT_PROTOCOL_VERSION_NOT_SUPPORTED = 8;  // [SSH-TRANS]
    public static final int DISCONNECT_HOST_KEY_NOT_VERIFIABLE = 9;  // [SSH-TRANS]
    public static final int DISCONNECT_CONNECTION_LOST = 10; // [SSH-TRANS]
    public static final int DISCONNECT_BY_APPLICATION = 11; // [SSH-TRANS]
    public static final int DISCONNECT_TOO_MANY_CONNECTIONS = 12; // [SSH-TRANS]
    public static final int DISCONNECT_AUTH_CANCELLED_BY_USER = 13; // [SSH-TRANS]
    public static final int DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE = 14; // [SSH-TRANS]
    public static final int DISCONNECT_ILLEGAL_USER_NAME = 15; // [SSH-TRANS]


    /* Service Names
     *
     * The Service Name is used to describe a protocol layer.  These names
     * MUST be printable US-ASCII strings, and MUST NOT contain the
     * characters at-sign ('@'), comma (','), or whitespace or control
     * characters (ASCII codes 32 or less).  Names are case-sensitive, and
     * MUST NOT be longer than 64 characters.
     *
     *                   Service name                                   Reference
     *                   -------------                                  ---------         */
    public static final String SERVICE_USERAUTH = "ssh-userauth"; // [SSH-USERAUTH]
    public static final String SERVICE_CONNECTION = "ssh-connection"; // [SSH-CONNECT]


    /* Authentication Method Names
     *
     * The Authentication Method Name is used to describe an authentication
     * method for the "ssh-userauth" service [SSH-USERAUTH]. These names
     * MUST be printable US-ASCII strings, and MUST NOT contain the
     * characters at-sign ('@'), comma (','), or whitespace or control
     * characters (ASCII codes 32 or less).  Names are case-sensitive, and
     * MUST NOT be longer than 64 characters.
     *
     *                  Method name                                  Reference
     *                  ------------                                 ---------           */
    public static final String AUTH_METHOD_KBI = "keyboard-interactive";
    public static final String AUTH_METHOD_PUBLICKEY = "publickey"; // [SSH-USERAUTH, Section 4]
    public static final String AUTH_METHOD_PASSWORD = "password";  // [SSH-USERAUTH, Section 5]
    public static final String AUTH_METHOD_HOSTBASE = "hostbased"; // [SSH-USERAUTH, Section 6]
    public static final String AUTH_METHOD_NONE = "none";      // [SSH-USERAUTH, Section 2.3]


    /* Connection Protocol Assigned Names
     *
     * The following request and type names MUST be printable US-ASCII
     * strings, and MUST NOT contain the characters at-sign ('@'), comma
     * (','), or whitespace or control characters (ASCII codes 32 or less).
     * Names are case-sensitive, and MUST NOT be longer than 64 characters.
     *
     *
     * Connection Protocol Channel Types
     */
    public static final String CHANNEL_TYPE_SESSION = "session";         // [SSH-CONNECT, Section 4.1]
    public static final String CHANNEL_TYPE_X11 = "x11";             // [SSH-CONNECT, Section 4.3.2]
    public static final String CHANNEL_TYPE_FORWARDED_TCPIP = "forwarded-tcpip"; // [SSH-CONNECT, Section 5.2]
    public static final String CHANNEL_TYPE_DIRECT_TCPIP = "direct-tcpip";    // [SSH-CONNECT, Section 5.2]


    /* Connection Protocol Global Request Names
     */
    public static final String PROTOCOL_REQUEST_TCPIP_FORWARD = "tcpip-forward";        // [SSH-CONNECT, Section 5.1]
    public static final String PROTOCOL_REQUEST_TCPIP_FORWARD_CANCEL = "cancel-tcpip-forward"; // [SSH-CONNECT, Section 5.1]


    /* Connection Protocol Channel Request Names
     */
    public static final String CHANNEL_REQUEST_PTY_REQ = "pty-req";       // [SSH-CONNECT, Section 4.2]
    public static final String CHANNEL_REQUEST_X11_REQ = "x11-req";       // [SSH-CONNECT, Section 4.3.1]
    public static final String CHANNEL_REQUEST_ENV = "env";           // [SSH-CONNECT, Section 4.4]
    public static final String CHANNEL_REQUEST_SHELL = "shell";         // [SSH-CONNECT, Section 4.5]
    public static final String CHANNEL_REQUEST_EXEC = "exec";          // [SSH-CONNECT, Section 4.5]
    public static final String CHANNEL_REQUEST_SUBSYSTEM = "subsystem";     // [SSH-CONNECT, Section 4.5]
    public static final String CHANNEL_REQUEST_WINDOW_CHANGE = "window-change"; // [SSH-CONNECT, Section 4.7]
    public static final String CHANNEL_REQUEST_XON_XOFF = "xon-xoff";      // [SSH-CONNECT, Section 4.8]
    public static final String CHANNEL_REQUEST_SIGNAL = "signal";        // [SSH-CONNECT, Section 4.9]
    public static final String CHANNEL_REQUEST_EXIT_STATUS = "exit-status";   // [SSH-CONNECT, Section 4.10]
    public static final String CHANNEL_REQUEST_EXIT_SIGNAL = "exit-signal";   // [SSH-CONNECT, Section 4.10]


    /* Key Exchange Method Names
     *
     * The Key Exchange Method Name describes a key-exchange method for the
     * protocol [SSH-TRANS].  The names MUST be printable US-ASCII strings,
     * and MUST NOT contain the characters at-sign ('@'), comma (','), or
     * whitespace or control characters (ASCII codes 32 or less).  Names are
     * case-sensitive, and MUST NOT be longer than 64 characters.
     */
    public static final String KEY_EXCHANGE_DIFFIE_HELLMAN = "diffie-hellman-group1-sha1"; // [SSH-TRANS, Section 4.5]


    /* Assigned Algorithm Names
     *
     * The following identifiers (names) MUST be printable US-ASCII strings,
     * and MUST NOT contain the characters at-sign ('@'), comma (','), or
     * whitespace or control characters (ASCII codes 32 or less).  Names are
     * case-sensitive, and MUST NOT be longer than 64 characters.
     *
     * Encryption Algorithm Names
     */
    public static final String ENCRYPTION_ALG_3DES_CBC = "3des-cbc";       // [SSH-TRANS, Section 4.3]
    public static final String ENCRYPTION_ALG_BLOWFISH_CBC = "blowfish-cbc";   // [SSH-TRANS, Section 4.3]
    public static final String ENCRYPTION_ALG_TWOFISH256_CBC = "twofish256-cbc"; // [SSH-TRANS, Section 4.3]
    public static final String ENCRYPTION_ALG_TWOFISH_CBC = "twofish-cbc";    // [SSH-TRANS, Section 4.3]
    public static final String ENCRYPTION_ALG_TWOFISH192_CBC = "twofish192-cbc"; // [SSH-TRANS, Section 4.3]
    public static final String ENCRYPTION_ALG_TWOFISH128_CBC = "twofish128-cbc"; // [SSH-TRANS, Section 4.3]
    public static final String ENCRYPTION_ALG_AES256_CBC = "aes256-cbc";     // [SSH-TRANS, Section 4.3]
    public static final String ENCRYPTION_ALG_AES192_CBC = "aes192-cbc";     // [SSH-TRANS, Section 4.3]
    public static final String ENCRYPTION_ALG_AES128_CBC = "aes128-cbc";     // [SSH-TRANS, Section 4.3]
    public static final String ENCRYPTION_ALG_SERPENT256_CBC = "serpent256-cbc"; // [SSH-TRANS, Section 4.3]
    public static final String ENCRYPTION_ALG_SERPENT192_CBC = "serpent192-cbc"; // [SSH-TRANS, Section 4.3]
    public static final String ENCRYPTION_ALG_SERPENT128_CBC = "serpent128-cbc"; // [SSH-TRANS, Section 4.3]
    public static final String ENCRYPTION_ALG_ARCFOUR = "arcfour";        // [SSH-TRANS, Section 4.3]
    public static final String ENCRYPTION_ALG_IDEA_CBC = "idea-cbc";       // [SSH-TRANS, Section 4.3]
    public static final String ENCRYPTION_ALG_CAST128_CBC = "cast128-cbc";    // [SSH-TRANS, Section 4.3]
    public static final String ENCRYPTION_ALG_NONE = "none";           // [SSH-TRANS, Section 4.3]
    public static final String ENCRYPTION_ALG_DES_CBC = "des-cbc";        // [FIPS-46-3] HISTORIC; See page 4 of [FIPS 46-3]


    /* MAC Algorithm Names
     */
    public static final String MAC_ALG_HMAC_SHA1 = "hmac-sha1";    // [SSH-TRANS, Section 4.4]
    public static final String MAC_ALG_HMAC_SHA1_96 = "hmac-sha1-96"; // [SSH-TRANS, Section 4.4]
    public static final String MAC_ALG_HMAC_MD5 = "hmac-md5";     // [SSH-TRANS, Section 4.4]
    public static final String MAC_ALG_HMAC_MD5_96 = "hmac-md5-96";  // [SSH-TRANS, Section 4.4]
    public static final String MAC_ALG_NONE = "none";         // [SSH-TRANS, Section 4.4]


    /* Public Key Algorithm Names
     */
    public static final String PUB_KEY_ALG_SSH_DSS = "ssh-dss";         // [SSH-TRANS, Section 4.6]
    public static final String PUB_KEY_ALG_SSH_RSA = "ssh-rsa";         // [SSH-TRANS, Section 4.6]
    public static final String PUB_KEY_ALG_X509V3_SIGN_RSA = "x509v3-sign-rsa"; // [SSH-TRANS, Section 4.6]
    public static final String PUB_KEY_ALG_X509V3_SIGN_DSS = "x509v3-sign-dss"; // [SSH-TRANS, Section 4.6]
    public static final String PUB_KEY_ALG_SPKI_SIGN_RSA = "spki-sign-rsa";   // [SSH-TRANS, Section 4.6]
    public static final String PUB_KEY_ALG_SPKI_SIGN_DSS = "spki-sign-dss";   // [SSH-TRANS, Section 4.6]
    public static final String PUB_KEY_ALG_PGP_SIGN_RSA = "pgp-sign-rsa";    // [SSH-TRANS, Section 4.6]
    public static final String PUB_KEY_ALG_PGP_SIGN_DSS = "pgp-sign-dss";    // [SSH-TRANS, Section 4.6]


    /* Compression Algorithm Names
     */
    public static final String COMPRESSION_ALG_NONE = "none"; // [SSH-TRANS, Section 4.2]
    public static final String COMPRESSION_ALG_ZLIB = "zlib"; // [SSH-TRANS, Section 4.2]

    public static final String DEFAULT_TERMINAL_TYPE = "dumb";

    protected static final int DEFAULT_SSH_PORT = 22;
    protected static final Time DEFAULT_TIMEOUT = Time.seconds(30);

    protected static final int DEFAULT_CHAR_WIDTH = 80;
    protected static final int DEFAULT_CHAR_HIGHT = 24;
    protected static final int DEFAULT_PIXEL_WIDTH = 640;
    protected static final int DEFAULT_PIXEL_HEIGTH = 480;

    protected final SshParameters parameters;
    protected final SshConfiguration configuration;
    protected String terminalType;
    protected int terminalWidthCharacters;
    protected int terminalHeightCharacters;
    protected int terminalWidthPixels;
    protected int terminalHeightPixels;
    protected boolean echoModeRequired;
    protected boolean readerThreadRequired;
    protected String characterSet;
    protected final Logger logger;
    protected final Set<SshListener> listeners;
    protected final SshHostKeys hostKeys;
    protected SessionConnection sessionConnection;
    protected SessionChannelConnection channelConnection;
    protected InputStream in;
    protected OutputStream out;
    protected volatile boolean disconnectDetected;
    protected volatile boolean disconnectEventRaised;

    /**
     * Creates a new Ssh client instance.
     *
     * @param parameters SSH connection parameters
     * @param configuration SSH configuration
     * @param terminalType target terminal type
     * @param terminalWidthCharacters terminal width in characters
     * @param terminalHeightCharacters terminal height in characters
     * @param terminalWidthPixels terminal width in pixels
     * @param terminalHeightPixels terminal height in pixels
     * @param echoModeRequired echo mode flag
     * @param readerThreadRequired separated reader thread flag
     * @param characterSet character set
     * @param logger logger
     */
    public Ssh(SshParameters parameters,
               SshConfiguration configuration,
               String terminalType,
               int terminalWidthCharacters,
               int terminalHeightCharacters,
               int terminalWidthPixels,
               int terminalHeightPixels,
               boolean echoModeRequired,
               boolean readerThreadRequired,
               String characterSet,
               Logger logger) {
        assertLicenseValid();

        assertNotNull(parameters);
        this.parameters = parameters;

        assertNotNull(configuration);
        this.configuration = configuration;

        assertNotNull(terminalType);
        this.terminalType = terminalType;

        this.terminalWidthCharacters = terminalWidthCharacters;
        this.terminalHeightCharacters = terminalHeightCharacters;
        this.terminalWidthPixels = terminalWidthPixels;
        this.terminalHeightPixels = terminalHeightPixels;
        this.echoModeRequired = echoModeRequired;
        this.readerThreadRequired = readerThreadRequired;
        this.characterSet = characterSet;

        assertNotNull(logger);
        this.logger = logger;

        this.listeners = new CopyOnWriteArraySet<SshListener>();
        this.hostKeys = new SshHostKeys();
    }

    /**
     * Constructs a new Ssh instance.
     *
     * @param parameters the SSH connection parameters
     * @param configuration the SSH protocol configuration
     * @param logger the logger
     */
    public Ssh(SshParameters parameters, SshConfiguration configuration, Logger logger) {
        this(parameters, configuration, DEFAULT_TERMINAL_TYPE, DEFAULT_CHAR_WIDTH, DEFAULT_CHAR_HIGHT, DEFAULT_PIXEL_WIDTH, DEFAULT_PIXEL_HEIGTH, false, true, null, logger);
    }

    /**
     * Constructs a new Ssh instance.
     *
     * @param parameters the SSH connection parameters
     * @param configuration the SSH protocol configuration
     */
    public Ssh(SshParameters parameters, SshConfiguration configuration) {
        this(parameters, configuration, DebugStreamLogger.systemOutLogger(Level.OFF));
    }

    /**
     * Constructs a new Ssh instance
     *
     * @param parameters SSH connection parameters
     */
    public Ssh(SshParameters parameters) {
        this(parameters, SshConfiguration.defaultClientConfiguration());
    }

    /**
     * Constructs a new Ssh instance.
     *
     * @param host     the SSH server hostname
     * @param port     the SSH server port
     * @param user     the SSH server username
     * @param password the SSH server password
     * @param configuration   the SSH protocol configuration
     */
    public Ssh(String host, int port, String user, String password, SshConfiguration configuration) {
        this(new SshParameters(host, port, user, password), configuration);
    }

    /**
     * Constructs a new Ssh instance.
     *
     * @param host     the SSH server hostname
     * @param port     the SSH server port
     * @param user     the SSH server username
     * @param password the SSH server password
     */
    public Ssh(String host, int port, String user, String password) {
        this(host, port, user, password, SshConfiguration.defaultClientConfiguration());
    }

    /**
     * Constructs a new Ssh instance.
     *
     * @param host SSH host
     * @param port SSH port
     * @param user SSH username
     */
    public Ssh(String host, int port, String user) {
        this(host, port, user, "");
    }

    /**
     * Consructs a new Ssh instance.
     *
     * @param host     the SSH host
     * @param user     the SSH user
     * @param password the SSH password
     */
    public Ssh(String host, String user, String password) {
        this(host, DEFAULT_SSH_PORT, user, password);
    }

    /**
     * Constructs a new Ssh instance.
     *
     * @param host SSH host
     * @param user SSH username
     */
    public Ssh(String host, String user) {
        this(host, user, "");
    }

    /**
     * Return server hostname.
     *
     * @return the hostname or IP address of the SSH server
     */
    public String getHostname() {
        return this.parameters.getHostname();
    }

    /**
     * Return server port.
     *
     * @return the port of the SSH server
     */
    public int getPort() {
        return this.parameters.getPort();
    }

    /**
     * Gets the timeout for opening connection to SSH server.
     *
     * @return timeout in milliseconds
     */
    public long getTimeout() {
        return this.parameters.getConnectionTimeout();
    }

    /**
     * Sets the timeout for opening connection to SSH server.
     *
     * @param value the timeout in milliseconds
     */
    public void setTimeout(long value) {
        this.parameters.setConnectionTimeout(value);
    }

    /**
     * Returns current read timeout value.
     *
     * @return current read timeout value
     */
    public long getReadTimeout() {
        return this.parameters.getReadingTimeout();
    }

    /**
     * Sets read timeout value.
     * Must be greater or equal to 0. A value of 0 indicates an infinite timeout.
     *
     * @param value read timeout value in milliseconds.
     */
    public void setReadTimeout(long value) {
        this.parameters.setReadingTimeout(value);
    }

    /**
     * Sets character set used when reading data using ssh
     *
     * @param value the encoding name
     */
    public void setCharacterSet(String value) {
        this.characterSet = value;
    }

    /**
     * Gets the character set
     *
     * @return The character set used by the service
     */
    public String getCharacterSet() {
        return this.characterSet;
    }

    /**
     * Sets the terminal type.
     *
     * @return the terminal type
     */
    public String getTerminalType() {
        return this.terminalType;
    }

    /**
     * Sets terminal type emulation to use for this SSH session.
     * An example terminal type is vt100, default is "dumb".
     *
     * @param value the terminal type
     */
    public void setTerminalType(String value) {
        assertNotNull(value);
        this.terminalType = value;
    }

    /**
     * Returns echo flag.
     *
     * @return <code>true</code> if server echo is enabled;
     *         <code>false</code> otherwise
     */
    public boolean getEcho() {
        return this.echoModeRequired;
    }

    /**
     * Sets server echo flag for the next connection.
     *
     * @param value if <code>true</code> server echo will
     *             be enabled for the next connection.
     */
    public void setEcho(boolean value) {
        this.echoModeRequired = value;
    }

    /**
     * Gets if reader should be initialized automatically or if will be created by user.
     * By default reader is initialized automatically during <code>connect()</code>.
     *
     * @return true if reader initialized automatically, false otherwise
     */
    public boolean isInitReader() {
        return this.readerThreadRequired;
    }

    /**
     * Sets if reader should be initialized automatically or if will be created by user.
     * By default reader is initialized automatically during <code>connect()</code>.
     *
     * @param value true to initialize reader, false otherwise
     */
    public void setInitReader(boolean value) {
        this.readerThreadRequired = value;
    }

    /**
     * Returns current log level.
     *
     * @return current log level
     */
    public Level getLogLevel() {
        return this.logger.getLevel();
    }

    /**
     * Sets current log level.
     *
     * @param level new log level
     */
    public void setLogLevel(Level level) {
        this.logger.setLevel(level);
    }

    /**
     * Gets whether debugging is enabled.
     * Default debugging stream is <code>System.out</code>.
     *
     * @return <code>true</code> if debugging is enabled, <code>false</code> otherwise
     *
     * @see #setDebugStream(PrintStream)
     */
    public boolean getDebug() {
        return this.logger.getLevel() == Level.FINEST;
    }

    /**
     * Enable debugging output to debugging stream.
     * Default debugging stream is <code>System.out</code>.
     *
     * @param value <code>true</code> if debugging is enabled
     *
     * @see #setDebugStream
     */
    public void setDebug(boolean value) {
        this.logger.setLevel(value ? Level.FINEST : Level.OFF);
    }

    /**
     * Gets <code>PrintStream</code> used in reporting debug statements. Default <code>PrintStream</code>
     * is <code>System.out</code>.
     *
     * @return the <code>PrintStream</code> used in reporting debug statements
     *
     * @see java.io.PrintStream
     */
    public PrintStream getDebugStream() {
        return this.logger instanceof DebugStreamLogger ? (PrintStream) ((DebugStreamLogger) this.logger).stream() : null;
    }

    /**
     * Sets <code>PrintStream</code> used in reporting debug statements. Default <code>PrintStream</code>
     * is <code>System.out</code>.
     *
     * @param value the <code>PrintStream</code> to send debug statements to
     *
     * @see java.io.PrintStream
     */
    public void setDebugStream(PrintStream value) {
        if (this.logger instanceof DebugStreamLogger) {
            DebugStreamLogger debugStreamLogger = (DebugStreamLogger) this.logger;
            debugStreamLogger.stream(value);
        }
    }

    /**
     * Add SSH event listener.
     *
     * @param listener a SshListener
     *
     * @see SshListener
     */
    public void addSshListener(SshListener listener) {
        assertNotNull(listener);
        this.listeners.add(listener);
    }

    /**
     * Remove SSH event listener.
     *
     * @param listener a SshListener
     *
     * @see SshListener
     */
    public void removeSshListener(SshListener listener) {
        this.listeners.remove(listener);
    }

    /**
     * Returns SSH host keys for further use.
     *
     * @return SSH host keys
     */
    public SshHostKeys getHostKeys() {
        return this.hostKeys;
    }

    /**
     * Connects to SSH server.
     *
     * @throws SshException if an I/O error occurs.
     */
    public synchronized Ssh connect()
            throws SshException {
        this.disconnectDetected = false;
        this.disconnectEventRaised = false;

        try {
            openSessionConnection();
            openChannelConnection();
            requestTerminal();
            requestShell();

            logConnected();
            raiseEvent(new SshConnectedEvent(this, this.parameters.getHostname(), this.parameters.getPort()));
            startReaderThread();

            return this;
        } catch (Throwable e) {
            closeChannelConnection();
            closeSessionConnection();
            throw wrap(e);
        }
    }

    /**
     * Closes open connection.
     */
    public synchronized void disconnect() {
        if (!isConnected()) {
            return;
        }

        awaitDataComing();
        closeChannelConnection();
        closeSessionConnection();

        logDisconnected();
        raiseDisconnectedEvent();
    }

    @Override
    public void close() {
        disconnect();
    }

    /**
     * Checks if the current client is connected.
     *
     * @return <code>true</code> if the client is connected;
     *         <code>false</code> otherwise
     */
    public synchronized boolean isConnected() {
        return this.channelConnection != null && !this.channelConnection.closed() && !this.disconnectDetected;
    }

    /**
     * Returns local socket address.
     *
     * @return local socket address
     */
    public synchronized InetSocketAddress getLocalSocketAddress() {
        return isConnected()
               ? this.channelConnection.localAddress().asSocketAddress()
               : new InetSocketAddress("localhost", 0);
    }

    /**
     * Returns remote socket address.
     *
     * @return remote socket address
     */
    public synchronized InetSocketAddress getRemoteSocketAddress() {
        return isConnected()
               ? this.channelConnection.remoteAddress().asSocketAddress()
               : new InetSocketAddress(this.parameters.getHostname(), this.parameters.getPort());
    }

    /**
     * Returns connection input stream.
     *
     * @return connection input stream.
     */
    public synchronized InputStream getInputStream() {
        assertConnected();
        return this.in;
    }

    /**
     * Returns connection output stream.
     *
     * @return connection output stream.
     */
    public synchronized OutputStream getOutputStream() {
        assertConnected();
        return this.out;
    }

    /**
     * Method requestExec request the SSH server to start the execution of the given command.
     * The command string may contain a path.
     *
     * @param command String command for execution.
     *
     * @throws SshException if error occurs
     */
    public synchronized void requestExec(String command)
            throws SshException {
        assertConnected();

        try {
            this.channelConnection.exec(command, true);
        } catch (Exception e) {
            throw wrap(e);
        }
    }

    /**
     * Requests terminal window change on the server side.
     *
     * @param widthChars   window width in chars
     * @param heightChars  window heigth in chars
     * @param widthPixels  window width in pixels
     * @param heightPixels window height in pixels
     *
     * @throws SshException if an error occurs
     */
    public synchronized void requestTerminalWindowChange(int widthChars, int heightChars, int widthPixels, int heightPixels)
            throws SshException {
        assertConnected();

        try {
            this.channelConnection.windowChange(widthChars, heightChars, widthPixels, heightPixels);
        } catch (Exception e) {
            throw wrap(e);
        }
    }

    private void assertLicenseValid() {
        
    }

    protected void openSessionConnection()
            throws Exception {
        Connector<SyncRawConnection> rawConnector = rawConnector();
        ClientAuthentication authentication = authentication();
        Connector<SessionConnection> connector = new SessionConnector(rawConnector,
                                                                      this.configuration.identificationString,
                                                                      this.configuration.publicKeyAlgorithms,
                                                                      this.configuration.keyExchangeFactory,
                                                                      this.configuration.encryptionFactory,
                                                                      this.configuration.macFactory,
                                                                      this.configuration.compressionFactory,
                                                                      this.parameters.getHostKeyVerifier(),
                                                                      authentication,
                                                                      this.configuration.initialWindowSize,
                                                                      this.configuration.maxPacketSize,
                                                                      this.logger);
        this.sessionConnection = connector.connect();
        this.hostKeys.put(this.sessionConnection.remoteAddress().inetAddress(), this.sessionConnection.hostKey());
    }

    protected void closeSessionConnection() {
        if (this.sessionConnection != null) {
            this.sessionConnection.close();
        }
    }

    protected void openChannelConnection()
            throws Exception {
        this.channelConnection = this.sessionConnection.openSessionChannel(false);
        this.in = new BufferedInputStream(new SyncRawConnection.ConnectionInputStream(this.channelConnection));
        this.out = new BufferedOutputStream(new SyncRawConnection.ConnectionOutputStream(this.channelConnection));
    }

    protected void closeChannelConnection() {
        if (this.channelConnection != null) {
            this.channelConnection.close();
        }
    }

    protected Connector<SyncRawConnection> rawConnector() {
        ConnectionDescriptor connectionDescriptor = new ConnectionDescriptor(new TransportAddress(this.parameters.getHostname(), this.parameters.getPort()), this.parameters.getConnectionTimeout());
        SocketParameters socketParameters = new SocketParameters(true, null, null, this.parameters.getSocketTrafficClass(), this.parameters.getTcpNoDelay(), null, null, Time.millis(this.parameters.getReadingTimeout()));

        if (this.parameters.getProxyType() != null && this.parameters.getProxyHost() != null) {
            ProxyDescriptor proxyDescriptor = new ProxyDescriptor(this.parameters.getProxyHost(),
                                                                  this.parameters.getProxyPort(),
                                                                  this.parameters.getProxyUsername(),
                                                                  this.parameters.getProxyPassword());
            Proxy proxy = Proxies.proxyFor(this.parameters.getProxyType());
            return new ProxySocketConnector(proxyDescriptor, proxy, connectionDescriptor, socketParameters);
        } else {
            return new SocketConnector(connectionDescriptor, socketParameters);
        }
    }

    protected ClientAuthentication authentication() {
        if (this.parameters.getClientAuthentication() != null) {
            return this.parameters.getClientAuthentication();
        } else if (this.parameters.getPassword() != null && this.parameters.getKeyPair() != null) {
            return new ComponentClientAuthentication(PublicKeyClientAuthentication.defaultAuthentication(this.parameters.getUsername(), this.parameters.getPublicKeyAlgorithm(), this.parameters.getKeyPair()),
                                                     PasswordClientAuthentication.defaultAuthentication(this.parameters.getUsername(), this.parameters.getPassword()),
                                                     KeyboardInteractiveRegexClientAuthentication.defaultAuthentication(this.parameters.getUsername(), new KeyboardInteractiveRegexClientAuthentication.Entry(".*[pP][aA][sS][sS][wW][oO][rR][dD].*", this.parameters.getPassword())));
        } else if (this.parameters.getPassword() != null) {
            return new ComponentClientAuthentication(PasswordClientAuthentication.defaultAuthentication(this.parameters.getUsername(), this.parameters.getPassword()),
                                                     KeyboardInteractiveRegexClientAuthentication.defaultAuthentication(this.parameters.getUsername(), new KeyboardInteractiveRegexClientAuthentication.Entry(".*[pP][aA][sS][sS][wW][oO][rR][dD].*", this.parameters.getPassword())));
        } else if (this.parameters.getKeyPair() != null) {
            return PublicKeyClientAuthentication.defaultAuthentication(this.parameters.getUsername(), this.parameters.getPublicKeyAlgorithm(), this.parameters.getKeyPair());
        } else {
            return NoneClientAuthentication.defaultAuthentication(this.parameters.getUsername());
        }
    }

    protected void awaitDataComing() {
        Time.seconds(1).sleepSafe();
    }

    protected void assertConnected() {
        if (!isConnected()) {
            throw new IllegalStateException("SSH connection closed.");
        }
    }

    protected void requestTerminal() {
        try {
//            List<TerminalMode> modes = this.echoModeRequired ? CollectionTools.<TerminalMode, TerminalMode>list(new TerminalMode(Opcode.ECHO, 1)) : new ArrayList<TerminalMode>();
            List<TerminalMode> modes = CollectionTools.<TerminalMode, TerminalMode>list(new TerminalMode(Opcode.ECHO, this.echoModeRequired ? 1 : 0));
            this.channelConnection.pseudoTerminal(this.terminalType,
                                                  this.terminalWidthCharacters,
                                                  this.terminalHeightCharacters,
                                                  this.terminalWidthPixels,
                                                  this.terminalHeightPixels,
                                                  modes,
                                                  false);
        } catch (Exception e) {
            logError(e);
        }
    }

    protected void requestShell() {
        try {
            this.channelConnection.shell(false);
        } catch (Exception e) {
            logError(e);
        }
    }

    protected void startReaderThread()
            throws Exception {
        if (this.readerThreadRequired) {
            ReaderThread readerThread = new ReaderThread(this.characterSet != null ? new InputStreamReader(this.in, this.characterSet) : new InputStreamReader(this.in, StandardCharsets.UTF_8));
            readerThread.start();
        }
    }

    protected SshException wrap(Throwable e) {
        Throwable cause = e.getCause();

        return cause != null && cause.getCause() instanceof ClientAuthentication.InvalidCredentialsException
               ? new SshAuthenticationException("Invalid credentials.")
               : new SshException(e);
    }

    protected synchronized void raiseDisconnectedEvent() {
        if (!this.disconnectEventRaised) {
            raiseEvent(new SshDisconnectedEvent(this, this.parameters.getHostname(), this.parameters.getPort()));
            this.disconnectEventRaised = true;
        }
    }

    protected void raiseEvent(SshEvent event) {
        for (SshListener listener : this.listeners) {
            event.accept(listener);
        }
    }

    protected void logConnected() {
        if (this.logger.isLoggable(Level.FINE)) {
            this.logger.log(Level.FINE, String.format("SSH connection created: [%s <-> %s].",
                                                      this.channelConnection.localAddress(),
                                                      this.channelConnection.remoteAddress()));
        }
    }

    protected void logDisconnected() {
        if (this.logger.isLoggable(Level.FINE)) {
            this.logger.log(Level.FINE, String.format("SSH connection closed: [%s <-> %s].",
                                                      this.channelConnection.localAddress(),
                                                      this.channelConnection.remoteAddress()));
        }
    }

    protected void logError(Throwable e) {
        if (this.logger.isLoggable(Level.WARNING)) {
            this.logger.log(Level.WARNING, "SSH operation error.", e);
        }
    }

    protected class ReaderThread
            extends Thread {

        protected final Reader reader;

        protected ReaderThread(Reader reader) {
            super("SSH Reader Thread");

            setDaemon(true);

            assertNotNull(reader);
            this.reader = reader;
        }

        public void run() {
            try {
                int read;
                while ((read = this.reader.read()) != -1) {
                    char ch = (char) read;
                    raiseEvent(new SshDataReceivedEvent(Ssh.this, ch));
                }
            } catch (Exception e) {
                processError(e);
            } finally {
                if (isConnected()) {
                    disconnectDetected = true;
                    raiseDisconnectedEvent();
                }
            }
        }

        protected void processError(Exception e) {
            if (isConnected()) {
                logError(e);
            }
        }
    }

}
