/*
 *  SSHTools - Java SSH2 API
 *
 *  Copyright (C) 2002-2003 Lee David Painter and Contributors.
 *
 *  Contributions made by:
 *
 *  Brett Smith
 *  Richard Pernavas
 *  Erwin Bolwidt
 *
 *  This program is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Library General Public License
 *  as published by the Free Software Foundation; either version 2 of
 *  the License, or (at your option) any later version.
 *
 *  You may also distribute it and/or modify it under the terms of the
 *  Apache style J2SSH Software License. A copy of which should have
 *  been provided with the distribution.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  License document supplied with your distribution for more details.
 *
 */
package com.sshtools.j2ssh;

import com.sshtools.j2ssh.authentication.AuthenticationProtocolClient;
import com.sshtools.j2ssh.authentication.AuthenticationProtocolState;
import com.sshtools.j2ssh.authentication.PublicKeyAuthenticationClient;
import com.sshtools.j2ssh.authentication.SshAuthenticationClient;
import com.sshtools.j2ssh.configuration.SshConnectionProperties;
import com.sshtools.j2ssh.connection.Channel;
import com.sshtools.j2ssh.connection.ChannelEventAdapter;
import com.sshtools.j2ssh.connection.ChannelEventListener;
import com.sshtools.j2ssh.connection.ChannelFactory;
import com.sshtools.j2ssh.connection.ConnectionProtocol;
import com.sshtools.j2ssh.forwarding.ForwardingClient;
import com.sshtools.j2ssh.net.TransportProvider;
import com.sshtools.j2ssh.net.TransportProviderFactory;
import com.sshtools.j2ssh.session.SessionChannelClient;
import com.sshtools.j2ssh.sftp.SftpSubsystemClient;
import com.sshtools.j2ssh.transport.ConsoleKnownHostsKeyVerification;
import com.sshtools.j2ssh.transport.HostKeyVerification;
import com.sshtools.j2ssh.transport.TransportProtocolClient;
import com.sshtools.j2ssh.transport.TransportProtocolState;
import com.sshtools.j2ssh.transport.publickey.SshPublicKey;
import com.sshtools.j2ssh.util.State;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import java.io.File;
import java.io.IOException;

import java.net.UnknownHostException;

import java.util.Iterator;
import java.util.List;
import java.util.Vector;


/**
 * <p>
 * Implements an SSH client with methods to connect to a remote server and
 * perform all necersary SSH functions such as SCP, SFTP, executing commands,
 * starting the users shell and perform port forwarding.
 * </p>
 *
 * <p>
 * There are several steps to perform prior to performing the desired task.
 * This involves the making the initial connection, authenticating the user
 * and creating a session to execute a command, shell or subsystem and/or
 * configuring the port forwarding manager.
 * </p>
 *
 * <p>
 * To create a connection use the following code:<br>
 * <blockquote><pre>
 * // Create a instance and connect SshClient
 * ssh = new SshClient();
 * ssh.connect("hostname");
 * </pre></blockquote>
 * Once this code has executed and returned
 * the connection is ready for authentication:<br>
 * <blockquote><pre>
 * PasswordAuthenticationClient pwd = new PasswordAuthenticationClient();
 * pwd.setUsername("foo");
 * pwd.setPassword("xxxxx");
 * // Authenticate the user
 * int result = ssh.authenticate(pwd);
 * if(result==AuthenticationProtocolState.COMPLETED) {
 *    // Authentication complete
 * }
 * </pre></blockquote>
 * Once authenticated the user's shell can be started:<br>
 * <blockquote><pre>
 * // Open a session channel
 * SessionChannelClient session =
 *                      ssh.openSessionChannel();
 *
 * // Request a pseudo terminal, if you do not you may not see the prompt
 * if(session.requestPseudoTerminal("ansi", 80, 24, 0, 0, "") {
 *      // Start the users shell
 *      if(session.startShell()) {
 *         // Do something with the session output
 *         session.getOutputStream().write("echo message\n");
 *         ....
 *       }
 * }
 * </pre></blockquote>
 * </p>
 *
 * @author Lee David Painter
 * @version $Revision: 1.75 $
 *
 * @since 0.2.0
 */
public class SshClient {
    private static Log log = LogFactory.getLog(SshClient.class);

    /**
     * The SSH Authentication protocol implementation for this SSH client. The
     * SSH Authentication protocol runs over the SSH Transport protocol as a
     * transport protocol service.
     */
    protected AuthenticationProtocolClient authentication;

    /**
     * The SSH Connection protocol implementation for this SSH client. The
     * connection protocol runs over the SSH Transport protocol as a transport
     * protocol service and is started by the authentication protocol after a
     * successful authentication.
     */
    protected ConnectionProtocol connection;

    /** Provides a high level management interface for SSH port forwarding. */
    protected ForwardingClient forwarding;

    /** The SSH Transport protocol implementation for this SSH Client. */
    protected TransportProtocolClient transport;

    /** The current state of the authentication for the current connection. */
    protected int authenticationState = AuthenticationProtocolState.READY;

    /**
     * The timeout in milliseconds for the underlying transport provider
     * (typically a Socket).
     */
    protected int socketTimeout = 0;

    /**
     * A Transport protocol event handler instance that receives notifications
     * of transport layer events such as Socket timeouts and disconnection.
     */
    protected SshEventAdapter eventHandler = null;

    /** The currently active channels for this SSH Client connection. */
    protected Vector activeChannels = new Vector();

    /**
     * An channel event listener implemention to maintain the active channel
     * list.
     */
    protected ActiveChannelEventListener activeChannelListener = new ActiveChannelEventListener();

    /**
     * Flag indicating whether the forwarding instance is created when the
     * connection is made.
     */
    protected boolean useDefaultForwarding = true;

    /** The currently active Sftp clients */
    private Vector activeSftpClients = new Vector();

    /**
     * <p>
     * Contructs an unitilialized SshClient ready for connecting.
     * </p>
     */
    public SshClient() {
    }

    /**
     * <p>
     * Returns the server's authentication banner.
     * </p>
     *
     * <p>
     * In some jurisdictions, sending a warning message before authentication
     * may be relevant for getting legal protection.  Many UNIX machines, for
     * example, normally display text from `/etc/issue', or use "tcp wrappers"
     * or similar software to display a banner before issuing a login prompt.
     * </p>
     *
     * <p>
     * The server may or may not send this message. Call this method to
     * retrieve the message, specifying a timeout limit to wait for the
     * message.
     * </p>
     *
     * @param timeout The number of milliseconds to wait for the banner message
     *        before returning
     *
     * @return The server's banner message
     *
     * @exception IOException If an IO error occurs reading the message
     *
     * @since 0.2.0
     */
    public String getAuthenticationBanner(int timeout)
        throws IOException {
        if (authentication == null) {
            return "";
        } else {
            return authentication.getBannerMessage(timeout);
        }
    }

    /**
     * <p>
     * Returns the list of available authentication methods for a given user.
     * </p>
     *
     * <p>
     * A client may request a list of authentication methods that may continue
     * by using the "none" authentication method.This method calls the "none"
     * method and returns the available authentication methods.
     * </p>
     *
     * @param username The name of the account for which you require the
     *        available authentication methods
     *
     * @return A list of Strings, for example "password", "publickey" &
     *         "keyboard-interactive"
     *
     * @exception IOException If an IO error occurs during the operation
     *
     * @since 0.2.0
     */
    public List getAvailableAuthMethods(String username)
        throws IOException {
        if (authentication != null) {
            return authentication.getAvailableAuths(username,
                connection.getServiceName());
        } else {
            return null;
        }
    }

    /**
     * <p>
     * Returns the connection state of the client.
     * </p>
     *
     * @return true if the client is connected, false otherwise
     *
     * @since 0.2.0
     */
    public boolean isConnected() {
        State state = (transport == null) ? null : transport.getState();
        int value = (state == null) ? TransportProtocolState.DISCONNECTED
                                    : state.getValue();

        return ((value == TransportProtocolState.CONNECTED) ||
        (value == TransportProtocolState.PERFORMING_KEYEXCHANGE));
    }

    /**
     * <p>
     * Evaluate whether the client has successfully authenticated.
     * </p>
     *
     * @return true if the client is authenticated, otherwise false
     */
    public boolean isAuthenticated() {
        return authenticationState == AuthenticationProtocolState.COMPLETE;
    }

    /**
     * <p>
     * Returns the identification string sent by the server during protocol
     * negotiation. For example "SSH-2.0-OpenSSH_p3.4".
     * </p>
     *
     * @return The server's identification string.
     *
     * @since 0.2.0
     */
    public String getServerId() {
        return transport.getRemoteId();
    }

    /**
     * <p>
     * Returns the server's public key supplied during key exchange.
     * </p>
     *
     * @return the server's public key
     *
     * @since 0.2.0
     */
    public SshPublicKey getServerHostKey() {
        return transport.getServerHostKey();
    }

    /**
     * <p>
     * Returns the transport protocol's connection state.
     * </p>
     *
     * @return The transport protocol's state
     *
     * @since 0.2.0
     */
    public TransportProtocolState getConnectionState() {
        return transport.getState();
    }

    /**
     * <p>
     * Returns the default port forwarding manager.
     * </p>
     *
     * @return This connection's forwarding client
     *
     * @since 0.2.0
     */
    public ForwardingClient getForwardingClient() {
        return forwarding;
    }

    /**
     * <p>
     * Return's a rough guess at the server's EOL setting. This is simply
     * derived from the identification string and should not be used as a cast
     * iron proof on the EOL setting.
     * </p>
     *
     * @return The transport protocol's EOL constant
     *
     * @since 0.2.0
     */
    public int getRemoteEOL() {
        return transport.getRemoteEOL();
    }

    /**
     * <p>
     * Set the event handler for the underlying transport protocol.
     * </p>
     * <blockquote>
     * <pre>
     * ssh.setEventHandler(new TransportProtocolEventHandler() {
     *
     *   public void onSocketTimeout(TransportProtocol transport) {<br>
     *     // Do something to handle the socket timeout<br>
     *   }
     *
     *   public void onDisconnect(TransportProtocol transport) {
     *     // Perhaps some clean up?
     *   }
     * });
     * </pre>
     * </blockquote>
     *
     * @param eventHandler The event handler instance to receive transport
     *        protocol events
     *
     * @see com.sshtools.j2ssh.transport.TransportProtocolEventHandler
     * @since 0.2.0
     */
    public void addEventHandler(SshEventAdapter eventHandler) {
        // If were connected then add, otherwise store for later connection
        if (transport != null) {
            transport.addEventHandler(eventHandler);
            authentication.addEventListener(eventHandler);
        } else {
            this.eventHandler = eventHandler;
        }
    }

    /**
     * <p>
     * Set's the socket timeout (in milliseconds) for the underlying transport
     * provider. This MUST be called prior to connect.
     * </p>
     * <blockquote>
     * SshClient ssh = new SshClient();
     * ssh.setSocketTimeout(30000);
     * ssh.connect("hostname");
     * </blockquote>
     *
     * @param milliseconds The number of milliseconds without activity before
     *        the timeout event occurs
     *
     * @since 0.2.0
     */
    public void setSocketTimeout(int milliseconds) {
        this.socketTimeout = milliseconds;
    }

    /**
     * <p>
     * Return's a rough guess at the server's EOL setting. This is simply
     * derived from the identification string and should not be used as a cast
     * iron proof on the EOL setting.
     * </p>
     *
     * @return The EOL string
     *
     * @since 0.2.0
     */
    public String getRemoteEOLString() {
        return ((transport.getRemoteEOL() == TransportProtocolClient.EOL_CRLF)
        ? "\r\n" : "\n");
    }

    /**
     * Get the connection properties for this connection.
     *
     * @return
     */
    public SshConnectionProperties getConnectionProperties() {
        return transport.getProperties();
    }

    /**
     * <p>
     * Authenticate the user on the remote host.
     * </p>
     *
     * <p>
     * To authenticate the user, create an <code>SshAuthenticationClient</code>
     * instance and configure it with the authentication details.
     * </p>
     * <code> PasswordAuthenticationClient pwd = new
     * PasswordAuthenticationClient(); pwd.setUsername("root");
     * pwd.setPassword("xxxxxxxxx"); int result = ssh.authenticate(pwd);
     * </code>
     *
     * <p>
     * The method returns a result value will one of the public static values
     * defined in <code>AuthenticationProtocolState</code>. These are<br>
     * <br>
     * COMPLETED - The authentication succeeded.<br>
     * PARTIAL   - The authentication succeeded but a further authentication
     * method is required.<br>
     * FAILED    - The authentication failed.<br>
     * CANCELLED - The user cancelled authentication (can only be returned
     * when the user is prompted for information.<br>
     * </p>
     *
     * @param auth A configured SshAuthenticationClient instance ready for
     *        authentication
     *
     * @return The authentication result
     *
     * @exception IOException If an IO error occurs during authentication
     *
     * @since 0.2.0
     */
    public int authenticate(SshAuthenticationClient auth)
        throws IOException {
        // Do the authentication
        authenticationState = authentication.authenticate(auth, connection);

        if ((authenticationState == AuthenticationProtocolState.COMPLETE) &&
                useDefaultForwarding) {
            // Use some method to synchronize forwardings on the ForwardingClient
            forwarding.synchronizeConfiguration(transport.getProperties());
        }

        return authenticationState;
    }

    /**
     * <p>
     * Determine whether a private/public key pair will be accepted for public
     * key authentication.
     * </p>
     *
     * <p>
     * When using public key authentication, the signing of data could take
     * some time depending upon the available machine resources. By calling
     * this method, you can determine whether the server will accept a key for
     * authentication by providing the public key. The server will verify the
     * key against the user's authorized keys and return true should the
     * public key be authorized. The caller can then proceed with the private
     * key operation.
     * </p>
     *
     * @param username The username for authentication
     * @param key The public key for which authentication will be attempted
     *
     * @return true if the server will accept the key, otherwise false
     *
     * @exception IOException If an IO error occurs during the operation
     * @throws SshException
     *
     * @since 0.2.0
     */
    public boolean acceptsKey(String username, SshPublicKey key)
        throws IOException {
        if (authenticationState != AuthenticationProtocolState.COMPLETE) {
            PublicKeyAuthenticationClient pk = new PublicKeyAuthenticationClient();

            return pk.acceptsKey(authentication, username,
                connection.getServiceName(), key);
        } else {
            throw new SshException("Authentication has been completed!");
        }
    }

    /**
     * <p>
     * Connect the client to the server using default connection properties.
     * </p>
     *
     * <p>
     * This call attempts to connect to the hostname specified on the standard
     * SSH port of 22 and uses all the default connection properties. This
     * call is the equivilent of calling:
     * </p>
     * <blockquote><pre>
     * SshConnectionProperties properties = new
     *                           SshConnectionProperties();
     * properties.setHostname("hostname");
     * ssh.connect(properties);
     * </pre></blockquote>
     *
     * @param hostname The hostname of the server to connect
     *
     * @exception IOException If an IO error occurs during the connect
     *            operation
     *
     * @see #connect(com.sshtools.j2ssh.configuration.SshConnectionProperties)
     * @since 0.2.0
     */
    public void connect(String hostname) throws IOException {
        connect(hostname, 22, new ConsoleKnownHostsKeyVerification());
    }

    /**
     * <p>
     * Connect the client to the server using the default connection
     * properties.
     * </p>
     *
     * <p>
     * This call attempts to connect to the hostname specified on the standard
     * SSH port of 22 and uses all the default connection properties. When
     * this method returns the connection has been established, the server's
     * identity been verified and the connection is ready for user
     * authentication. Host key verification will be performed using the host
     * key verification instance provided:
     * </p>
     * <blockquote><pre>
     * // Connect and consult $HOME/.ssh/known_hosts
     * ssh.connect("hostname", new ConsoleKnownHostsKeyVerification());
     * // Connect and allow any host
     * ssh.connect("hostname", new
     *                 IgnoreHostKeyVerification());
     * </pre></blockquote>
     *
     * <p>
     * You can provide your own host key verification process by implementing
     * the <code>HostKeyVerification</code> interface.
     * </p>
     *
     * @param hostname The hostname of the server to connect
     * @param hosts The host key verification instance to consult for host key
     *        validation
     *
     * @exception IOException If an IO error occurs during the connect
     *            operation
     *
     * @see #connect(com.sshtools.j2ssh.configuration.SshConnectionProperties,
     *      com.sshtools.j2ssh.transport.HostKeyVerification)
     * @since 0.2.0
     */
    public void connect(String hostname, HostKeyVerification hosts)
        throws IOException {
        connect(hostname, 22, hosts);
    }

    /**
     * <p>
     * Connect the client to the server on a specified port with default
     * connection properties.
     * </p>
     *
     * <p>
     * This call attempts to connect to the hostname and port specified. This
     * call is the equivilent of calling:
     * </p>
     * <blockquote></pre>
     * SshConnectionProperties properties = new
     *                               SshConnectionProperties();
     * properties.setHostname("hostname");
     * properties.setPort(10022);
     * ssh.connect(properties);
     * </pre></blockquote>
     *
     * @param hostname The hostname of the server to connect
     * @param port The port to connect
     *
     * @exception IOException If an IO error occurs during the connect
     *            operation
     *
     * @see #connect(com.sshtools.j2ssh.configuration.SshConnectionProperties)
     * @since 0.2.0
     */
    public void connect(String hostname, int port) throws IOException {
        connect(hostname, port, new ConsoleKnownHostsKeyVerification());
    }

    /**
     * <p>
     * Connect the client to the server on a specified port with default
     * connection properties.
     * </p>
     *
     * <p>
     * This call attempts to connect to the hostname and port specified. When
     * this method returns the connection has been established, the server's
     * identity been verified and the connection is ready for user
     * authentication. Host key verification will be performed using the host
     * key verification instance provided:
     * </p>
     * <blockquote><pre>
     * // Connect and consult $HOME/.ssh/known_hosts
     * ssh.connect("hostname", new ConsoleKnownHostsKeyVerification());
     * // Connect and allow any host
     * ssh.connect("hostname", new
     *                 IgnoreHostKeyVerification());
     * </pre></blockquote>
     *
     * <p>
     * You can provide your own host key verification process by implementing
     * the <code>HostKeyVerification</code> interface.
     * </p>
     *
     * @param hostname The hostname of the server to connect
     * @param port The port to connect
     * @param hosts The host key verification instance to consult for host key
     *        validation
     *
     * @exception IOException If an IO error occurs during the connect
     *            operation
     *
     * @see #connect(com.sshtools.j2ssh.configuration.SshConnectionProperties,
     *      com.sshtools.j2ssh.transport.HostKeyVerification)
     * @since 0.2.0
     */
    public void connect(String hostname, int port, HostKeyVerification hosts)
        throws IOException {
        SshConnectionProperties properties = new SshConnectionProperties();
        properties.setHost(hostname);
        properties.setPort(port);
        connect(properties, hosts);
    }

    /**
     * <p>
     * Connect the client to the server with the specified properties.
     * </p>
     *
     * <p>
     * This call attempts to connect to using the connection properties
     * specified. When this method returns the connection has been
     * established, the server's identity been verified and the connection is
     * ready for user authentication. To use this method first create a
     * properties instance and set the required fields.
     * </p>
     * <blockquote><pre>
     * SshConnectionProperties properties = new
     *                         SshConnectionProperties();
     * properties.setHostname("hostname");
     * properties.setPort(10022);
     * properties.setPrefCSEncryption("blowfish-cbc");
     * ssh.connect(properties);
     * </pre></blockquote>
     *
     * <p>
     * Host key verification will be performed using
     * <code>ConsoleKnownHostsKeyVerification</code> and so this call is the
     * equivilent of calling:
     * </p>
     * <blockquote><pre>
     * ssh.connect("hostname", new ConsoleKnownHostsKeyVerification());
     * </pre></blockquote>
     *
     * <p>
     * If the key is not matched to any keys already in the
     * $HOME/.ssh/known_hosts file, the user will be prompted via the console
     * to confirm the identity of the remote server. The user will receive the
     * following prompt.
     * </p>
     * <code> The host shell.sourceforge.net is currently unknown to the system
     * The host key fingerprint is: 1024: 4c 68 3 d4 5c 58 a6 1d 9d 17 13 24
     * 14 48 ba 99 Do you want to allow this host key? [Yes|No|Always]:
     * </code>
     *
     * <p>
     * Selecting the "always" option will write the key to the known_hosts
     * file.
     * </p>
     *
     * @param properties The connection properties
     *
     * @exception IOException If an IO error occurs during the connect
     *            operation
     *
     * @since 0.2.0
     */
    public void connect(SshConnectionProperties properties)
        throws IOException {
        connect(properties, new ConsoleKnownHostsKeyVerification());
    }

    /**
     * <p>
     * Connect the client to the server with the specified properties.
     * </p>
     *
     * <p>
     * This call attempts to connect to using the connection properties
     * specified. When this method returns the connection has been
     * established, the server's identity been verified and the connection is
     * ready for user authentication. To use this method first create a
     * properties instance and set the required fields.
     * </p>
     * <blockquote><pre>
     * SshConnectionProperties properties = new
     *                             SshConnectionProperties();
     * properties.setHostname("hostname");
     * properties.setPort(22);             // Defaults to 22
     * // Set the prefered client->server encryption
     * ssh.setPrefCSEncryption("blowfish-cbc");
     * // Set the prefered server->client encrpytion
     * ssh.setPrefSCEncrpyion("3des-cbc");
     * ssh.connect(properties);
     * </pre></blockquote>
     *
     * <p>
     * Host key verification will be performed using the host key verification
     * instance provided:<br>
     * <blockquote><pre>
     * // Connect and consult $HOME/.ssh/known_hosts
     * ssh.connect("hostname", new ConsoleKnownHostsKeyVerification());
     * // Connect and allow any host
     * ssh.connect("hostname", new
     *                 IgnoreHostKeyVerification());
     * </pre></blockquote>
     * You can provide your own host key verification process by implementing the
     * <code>HostKeyVerification</code> interface.
     * </p>
     *
     * @param properties The connection properties
     * @param hostVerification The host key verification instance to consult
     *        for host  key validation
     *
     * @exception UnknownHostException If the host is unknown
     * @exception IOException If an IO error occurs during the connect
     *            operation
     *
     * @since 0.2.0
     */
    public void connect(SshConnectionProperties properties,
        HostKeyVerification hostVerification)
        throws UnknownHostException, IOException {
        TransportProvider provider = TransportProviderFactory.connectTransportProvider(properties /*, connectTimeout*/,
                socketTimeout);

        // Start the transport protocol
        transport = new TransportProtocolClient(hostVerification);
        transport.addEventHandler(eventHandler);
        transport.startTransportProtocol(provider, properties);

        // Start the authentication protocol
        authentication = new AuthenticationProtocolClient();
        authentication.addEventListener(eventHandler);
        transport.requestService(authentication);
        connection = new ConnectionProtocol();

        if (useDefaultForwarding) {
            forwarding = new ForwardingClient(connection);
        }
    }

    /**
     * <p>
     * Sets the timeout value for the key exchange.
     * </p>
     *
     * <p>
     * When this time limit is reached the transport protocol will initiate a
     * key re-exchange. The default value is one hour with the minumin timeout
     * being 60 seconds.
     * </p>
     *
     * @param seconds The number of seconds beofre key re-exchange
     *
     * @exception IOException If the timeout value is invalid
     *
     * @since 0.2.0
     */
    public void setKexTimeout(long seconds) throws IOException {
        transport.setKexTimeout(seconds);
    }

    /**
     * <p>
     * Sets the key exchance transfer limit in kilobytes.
     * </p>
     *
     * <p>
     * Once this amount of data has been transfered the transport protocol will
     * initiate a key re-exchange. The default value is one gigabyte of data
     * with the mimimun value of 10 kilobytes.
     * </p>
     *
     * @param kilobytes The data transfer limit in kilobytes
     *
     * @exception IOException If the data transfer limit is invalid
     */
    public void setKexTransferLimit(long kilobytes) throws IOException {
        transport.setKexTransferLimit(kilobytes);
    }

    /**
     * <p>
     * Set's the send ignore flag to send random data packets.
     * </p>
     *
     * <p>
     * If this flag is set to true, then the transport protocol will send
     * additional SSH_MSG_IGNORE packets with random data.
     * </p>
     *
     * @param sendIgnore true if you want to turn on random packet data,
     *        otherwise false
     *
     * @since 0.2.0
     */
    public void setSendIgnore(boolean sendIgnore) {
        transport.setSendIgnore(sendIgnore);
    }

    /**
     * <p>
     * Turn the default forwarding manager on/off.
     * </p>
     *
     * <p>
     * If this flag is set to false before connection, the client will not
     * create a port forwarding manager. Use this to provide you own
     * forwarding implementation.
     * </p>
     *
     * @param useDefaultForwarding Set to false if you not wish to use the
     *        default forwarding manager.
     *
     * @since 0.2.0
     */
    public void setUseDefaultForwarding(boolean useDefaultForwarding) {
        this.useDefaultForwarding = useDefaultForwarding;
    }

    /**
     * <p>
     * Disconnect the client.
     * </p>
     *
     * @since 0.2.0
     */
    public void disconnect() {
        if (connection != null) {
            connection.stop();
        }

        if (transport != null) {
            transport.disconnect("Terminating connection");
        }
    }

    /**
     * <p>
     * Returns the number of bytes transmitted to the remote server.
     * </p>
     *
     * @return The number of bytes transmitted
     *
     * @since 0.2.0
     */
    public long getOutgoingByteCount() {
        return transport.getOutgoingByteCount();
    }

    /**
     * <p>
     * Returns the number of bytes received from the remote server.
     * </p>
     *
     * @return The number of bytes received
     *
     * @since 0.2.0
     */
    public long getIncomingByteCount() {
        return transport.getIncomingByteCount();
    }

    /**
     * <p>
     * Returns the number of active channels for this client.
     * </p>
     *
     * <p>
     * This is the total count of sessions, port forwarding, sftp, scp and
     * custom channels currently open.
     * </p>
     *
     * @return The number of active channels
     *
     * @since 0.2.0
     */
    public int getActiveChannelCount() {
        synchronized (activeChannels) {
            return activeChannels.size();
        }
    }

    /**
     * <p>
     * Returns the list of active channels.
     * </p>
     *
     * @return The list of active channels
     *
     * @since 0.2.0
     */
    public List getActiveChannels() {
        synchronized (activeChannels) {
            return (List) activeChannels.clone();
        }
    }

    /**
     * <p>
     * Returns true if there is an active session channel of the specified
     * type.
     * </p>
     *
     * <p>
     * When a session is created, it is assigned a default type. For instance,
     * when a session is created it as a type of "uninitialized"; however when
     * a shell is started on the session, the type is set to "shell". This
     * also occurs for commands where the type is set to the command which is
     * executed and subsystems where the type is set to the subsystem name.
     * This allows each session to be saved in the active session channel's
     * list and recalled later. It is also possible to set the session
     * channel's type using the setSessionType method of the
     * <code>SessionChannelClient</code> class.
     * </p>
     * <blockquote><pre>
     * if(ssh.hasActiveSession("shell")) {
     *      SessionChannelClient session =
     *           ssh.getActiveSession("shell");
     * }
     * </pre></blockquote>
     *
     * @param type The string specifying the channel type
     *
     * @return true if an active session channel exists, otherwise false
     *
     * @since 0.2.0
     */
    public boolean hasActiveSession(String type) {
        Iterator it = activeChannels.iterator();
        Object obj;

        while (it.hasNext()) {
            obj = it.next();

            if (obj instanceof SessionChannelClient) {
                if (((SessionChannelClient) obj).getSessionType().equals(type)) {
                    return true;
                }
            }
        }

        return false;
    }

    /**
     * <p>
     * Returns the active session channel of the given type.
     * </p>
     *
     * @param type The type fo session channel
     *
     * @return The session channel instance
     *
     * @exception IOException If the session type does not exist
     *
     * @since 0.2.0
     */
    public SessionChannelClient getActiveSession(String type)
        throws IOException {
        Iterator it = activeChannels.iterator();
        Object obj;

        while (it.hasNext()) {
            obj = it.next();

            if (obj instanceof SessionChannelClient) {
                if (((SessionChannelClient) obj).getSessionType().equals(type)) {
                    return (SessionChannelClient) obj;
                }
            }
        }

        throw new IOException("There are no active " + type + " sessions");
    }

    /**
     * Determine whether the channel supplied is an active channel
     *
     * @param channel
     *
     * @return
     */
    public boolean isActiveChannel(Channel channel) {
        return activeChannels.contains(channel);
    }

    /**
     * <p>
     * Open's a session channel on the remote server.
     * </p>
     *
     * <p>
     * A session channel may be used to start the user's shell, execute a
     * command or start a subsystem such as SFTP.
     * </p>
     *
     * @return An new session channel
     *
     * @exception IOException If authentication has not been completed, the
     *            server refuses to open the channel or a general IO error
     *            occurs
     *
     * @see com.sshtools.j2ssh.session.SessionChannelClient
     * @since 0.2.0
     */
    public SessionChannelClient openSessionChannel() throws IOException {
        return openSessionChannel(null);
    }

    /**
     *
     * <p>
     * Open's a session channel on the remote server.
     * </p>
     *
     * <p>
     * A session channel may be used to start the user's shell, execute a
     * command or start a subsystem such as SFTP.
     * </p>
     *
     * @param eventListener an event listner interface to add to the channel
     *
     * @return
     *
     * @throws IOException
     * @throws SshException
     */
    public SessionChannelClient openSessionChannel(
        ChannelEventListener eventListener) throws IOException {
        if (authenticationState != AuthenticationProtocolState.COMPLETE) {
            throw new SshException("Authentication has not been completed!");
        }

        SessionChannelClient session = new SessionChannelClient();
        session.addEventListener(activeChannelListener);

        if (!connection.openChannel(session, eventListener)) {
            throw new SshException("The server refused to open a session");
        }

        return session;
    }

    /**
     * <p>
     * Open an SFTP client for file transfer operations.
     * </p>
     * <blockquote><pre>
     * SftpClient sftp = ssh.openSftpClient();
     * sftp.cd("foo");
     * sftp.put("somefile.txt");
     * sftp.quit();
     * </pre></blockquote>
     *
     * @return Returns an initialized SFTP client
     *
     * @exception IOException If an IO error occurs during the operation
     *
     * @see SftpClient
     * @since 0.2.0
     */
    public SftpClient openSftpClient() throws IOException {
        return openSftpClient(null);
    }

    /**
     * <p>
     * Open an SFTP client for file transfer operations. Adds the supplied
     * event listener to the underlying channel.
     * </p>
     *
     * @param eventListener
     *
     * @return
     *
     * @throws IOException
     */
    public SftpClient openSftpClient(ChannelEventListener eventListener)
        throws IOException {
        SftpClient sftp = new SftpClient(this, eventListener);
        activeSftpClients.add(sftp);

        return sftp;
    }

    /**
     * Determine if there are existing sftp clients open
     *
     * @return
     */
    public boolean hasActiveSftpClient() {
        synchronized (activeSftpClients) {
            return activeSftpClients.size() > 0;
        }
    }

    /**
     * Get an active sftp client
     *
     * @return
     *
     * @throws IOException
     * @throws SshException
     */
    public SftpClient getActiveSftpClient() throws IOException {
        synchronized (activeSftpClients) {
            if (activeSftpClients.size() > 0) {
                return (SftpClient) activeSftpClients.get(0);
            } else {
                throw new SshException("There are no active SFTP clients");
            }
        }
    }

    /**
     * <p>
     * Open an SCP client for file transfer operations where SFTP is not
     * supported.
     * </p>
     *
     * <p>
     * Sets the local working directory to the user's home directory
     * </p>
     * <blockquote><pre>
     * ScpClient scp = ssh.openScpClient();
     * scp.put("somefile.txt");
     * </pre></blockquote>
     *
     * @return An initialized SCP client
     *
     * @exception IOException If an IO error occurs during the operation
     *
     * @see ScpClient
     * @since 0.2.0
     */
    public ScpClient openScpClient() throws IOException {
        return new ScpClient(new File(System.getProperty("user.home")), this,
            false, activeChannelListener);
    }

    /**
     * <p>
     * Open an SCP client for file transfer operations where SFTP is not
     * supported.
     * </p>
     *
     * <p>
     * This method sets a local current working directory.
     * </p>
     * <blockquote><pre>
     * ScpClient scp = ssh.openScpClient("foo");
     * scp.put("somefile.txt");
     * </pre></blockquote>
     *
     * @param cwd The local directory as the base for all local files
     *
     * @return An intialized SCP client
     *
     * @exception IOException If an IO error occurs during the operation
     *
     * @see SftpClient
     * @since 0.2.0
     */
    public ScpClient openScpClient(File cwd) throws IOException {
        return new ScpClient(cwd, this, false, activeChannelListener);
    }

    /**
     * <p>
     * Open's an Sftp channel.
     * </p>
     *
     * <p>
     * Use this sftp channel if you require a lower level api into the SFTP
     * protocol.
     * </p>
     *
     * @return an initialized sftp subsystem instance
     *
     * @throws IOException if an IO error occurs or the channel cannot be
     *         opened
     *
     * @since 0.2.0
     */
    public SftpSubsystemClient openSftpChannel() throws IOException {
        return openSftpChannel(null);
    }

    /**
     * Open an SftpSubsystemChannel. For advanced use only
     *
     * @param eventListener
     *
     * @return
     *
     * @throws IOException
     * @throws SshException
     */
    public SftpSubsystemClient openSftpChannel(
        ChannelEventListener eventListener) throws IOException {
        SessionChannelClient session = openSessionChannel(eventListener);
        SftpSubsystemClient sftp = new SftpSubsystemClient();

        if (!openChannel(sftp)) {
            throw new SshException("The SFTP subsystem failed to start");
        }

        // Initialize SFTP
        if (!sftp.initialize()) {
            throw new SshException(
                "The SFTP Subsystem could not be initialized");
        }

        return sftp;
    }

    /**
     * <p>
     * Open's a channel.
     * </p>
     *
     * <p>
     * Call this method to open a custom channel. This method is used by all
     * other channel opening methods. For example the openSessionChannel
     * method could be implemented as:<br>
     * <blockquote><pre>
     * SessionChannelClient session =
     *                 new SessionChannelClient();
     * if(ssh.openChannel(session)) {
     *    // Channel is now open
     * }
     * </pre></blockquote>
     * </p>
     *
     * @param channel
     *
     * @return true if the channel was opened, otherwise false
     *
     * @exception IOException if an IO error occurs
     * @throws SshException
     *
     * @since 0.2.0
     */
    public boolean openChannel(Channel channel) throws IOException {
        if (authenticationState != AuthenticationProtocolState.COMPLETE) {
            throw new SshException("Authentication has not been completed!");
        }

        // Open the channel providing our channel listener so we can track
        return connection.openChannel(channel, activeChannelListener);
    }

    /**
     * <p>
     * Instructs the underlying connection protocol to allow channels of the
     * given type to be opened by the server.
     * </p>
     *
     * <p>
     * The client does not allow channels to be opened by default. Call this
     * method to allow the server to open channels by providing a
     * <code>ChannelFactory</code> implementation to create instances upon
     * request.
     * </p>
     *
     * @param channelName The channel type name
     * @param cf The factory implementation that will create instances of the
     *        channel when a channel open request is recieved.
     *
     * @exception IOException if an IO error occurs
     *
     * @since 0.2.0
     */
    public void allowChannelOpen(String channelName, ChannelFactory cf)
        throws IOException {
        connection.addChannelFactory(channelName, cf);
    }

    /**
     * <p>
     * Stops the specified channel type from being opended.
     * </p>
     *
     * @param channelName The channel type name
     *
     * @throws IOException if an IO error occurs
     *
     * @since 0.2.1
     */
    public void denyChannelOpen(String channelName) throws IOException {
        connection.removeChannelFactory(channelName);
    }

    /**
     * <p>
     * Send a global request to the server.
     * </p>
     *
     * <p>
     * The SSH specification provides a global request mechanism which is used
     * for starting/stopping remote forwarding. This is a general mechanism
     * which can be used for other purposes if the server supports the global
     * requests.
     * </p>
     *
     * @param requestName The name of the global request
     * @param wantReply true if the server should send an explict reply
     * @param requestData the global request data
     *
     * @return true if the global request succeeded or wantReply==false,
     *         otherwise false
     *
     * @throws IOException if an IO error occurs
     *
     * @since 0.2.0
     */
    public byte[] sendGlobalRequest(String requestName, boolean wantReply,
        byte[] requestData) throws IOException {
        return connection.sendGlobalRequest(requestName, wantReply, requestData);
    }

    /**
     * <p>
     * Implements the <code>ChannelEventListener</code> interface to provide
     * real time tracking of active channels.
     * </p>
     */
    class ActiveChannelEventListener extends ChannelEventAdapter {
        /**
         * <p>
         * Adds the channel to the active channel list.
         * </p>
         *
         * @param channel The channel being opened
         */
        public void onChannelOpen(Channel channel) {
            synchronized (activeChannels) {
                activeChannels.add(channel);
            }
        }

        /**
         * <p>
         * Removes the closed channel from the clients active channels list.
         * </p>
         *
         * @param channel The channle being closed
         */
        public void onChannelClose(Channel channel) {
            synchronized (activeChannels) {
                activeChannels.remove(channel);
            }
        }
    }
}
