/*
 * @(#)IpClientSsh.java
 *
 * Copyright (c) 2003 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.ipclientssh;

import com.jscape.inet.ipclient.IpClient;
import com.jscape.inet.ipclient.IpClientException;
import com.jscape.inet.proxy.Proxies;
import com.jscape.inet.proxy.Proxy;
import com.jscape.inet.proxy.ProxyDescriptor;
import com.jscape.inet.proxy.ProxySocketConnector;
import com.jscape.inet.ssh.SshConfiguration;
import com.jscape.inet.ssh.protocol.v2.authentication.*;
import com.jscape.inet.ssh.protocol.v2.connection.ChannelConnection;
import com.jscape.inet.ssh.protocol.v2.connection.SessionConnection;
import com.jscape.inet.ssh.protocol.v2.connection.SessionConnector;
import com.jscape.inet.ssh.util.SshHostKeys;
import com.jscape.inet.ssh.util.SshParameters;
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.SyncMessageConnectionSyncRawBase;
import com.jscape.util.net.connection.SyncRawConnection;
import com.jscape.util.net.socket.SocketConnector;
import com.jscape.util.net.socket.SocketParameters;
import com.jscape.util.net.socket.SocketRawConnection;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.net.InetAddress;
import java.net.Socket;
import java.util.logging.Level;
import java.util.logging.Logger;

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


/**
 * Implements basic functionality of a TCP/IP client connection tunnelling data
 * through a secure SSH connection. <P> Example usage: <P>
 * <pre>
 *
 * // connection information for SSH server
 * String sshHostname = "10.0.0.1";
 * String sshUsername = "jsmith";
 * String sshPassword = "secret";
 * try {
 *   // create new SshParameters instance
 *   SshParameters sshParams = new SshParameters(sshHostname,sshUsername,sshPassword);
 *
 *   // create new IpClient instance with SSH parameters, hostname, port and
 * timeout arguments
 *   IpClientSsh client = new IpClientSsh(sshParams,"10.0.0.1",53,5000);
 *
 *   // establish connection with server
 *   client.connect();
 *
 *   // obtain OutputStream for sending data to server
 *   OutputStream out = client.getOutputStream();
 *
 *   // obtain InputStream for reading data from server
 *   InputStream in = client.getInputStream();
 *
 *   // disconnect from server
 *   client.disconnect();
 *
 * } catch(IpClientException ipe) {
 *   ipe.printStackTrace();
 * }
 *
 * </pre>
 */
public class IpClientSsh
        extends IpClient {

    private final SshParameters parameters;
    private final SshConfiguration configuration;
    private final SshHostKeys hostKeys;
    private final Logger logger;
    private SessionConnection sessionConnection;
    private ChannelConnection channelConnection;

    /**
     * Creates new IpClientSsh instance.
     *
     * @param parameters the SSH parameters used in establishing secure connection
     * @param hostname      the hostname of server to connect to
     * @param port          the port of server to connect to
     * @param timeout       the timeout period to wait before connection to server is established
     * @param configuration        configuration parameters
     */
    public IpClientSsh(SshParameters parameters,
                       String hostname,
                       int port,
                       int timeout,
                       SshConfiguration configuration) {
        super(hostname, port, timeout);

        assertNotNull(parameters);
        this.parameters = new SshParameters(parameters);

        assertNotNull(configuration);
        this.configuration = configuration;

        this.hostKeys = new SshHostKeys();
        this.logger = DebugStreamLogger.systemOutLogger(Level.OFF);
    }

    /**
     * Creates a new IpClientSsh instance.
     *
     * @param hostname      the hostname of server to connect to
     * @param port          the port of server to connect to
     * @param timeout       the timeout period to wait before connection to server is established
     * @param parameters the SSH parameters used in establishing secure connection
     */
    public IpClientSsh(SshParameters parameters, String hostname, int port, int timeout) {
        this(parameters, hostname, port, timeout, SshConfiguration.defaultClientConfiguration());
    }

    /**
     * Creates a new IpClientSsh instance.
     *
     * @param hostname      the hostname of server to connect to
     * @param port          the port of server to connect to
     * @param parameters the SSH parameters used in establishing secure
     *                      connection
     */
    public IpClientSsh(SshParameters parameters, String hostname, int port) {
        this(parameters, hostname, port, 0);
    }

    /**
     * Establishes connection with SSH server, performs authentication and
     * creates tunnel to hostname and port provided in constructor.
     *
     * @throws IpClientException if an error occurs while establishing
     *                           connection
     */
    public synchronized IpClientSsh connect()
            throws IpClientException {
        try {
            openSessionConnection();
            openChannelConnection();
            fireIpConnected();

            return this;
        } catch (Exception e) {
            closeChannelConnection();
            closeSessionConnection();
            throw new IpClientException(e.getMessage(), e);
        }
    }

    /**
     * Closes tunnel between SSH server and host and closes connection to SSH
     * server.
     *
     * @throws IpClientException if an I/O related error occurs
     */
    public synchronized void disconnect()
            throws IpClientException {
        if (!isConnected()) {
            return;
        }

        closeChannelConnection();
        closeSessionConnection();
        this.input = null;
        this.output = null;

        fireIpDisconnected();
    }

    /**
     * Checks if the connection is still alive.
     *
     * @return <code>true</code> if the connection is alive;
     *         <code>false</code> otherwise
     */
    private boolean isConnected() {
        return this.channelConnection != null;
    }

    /**
     * Gets Socket established after invoking the IpClient#connect method.
     *
     * @return a Socket
     *
     * @see #connect
     */
    public Socket getSocket() {
        SyncMessageConnectionSyncRawBase messageConnection = (SyncMessageConnectionSyncRawBase) this.sessionConnection.base().base();
        return messageConnection.base() instanceof SocketRawConnection ? ((SocketRawConnection) messageConnection.base()).socket() : null;
    }

    /**
     * Returns SSH host keys for further use.
     *
     * @return SSH host keys or <code>null</code> if keys are not available
     */
    public SshHostKeys getHostKeys() {
        return this.hostKeys;
    }

    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.openDirectTcpIpChannel(this.hostname, this.port, InetAddress.getLocalHost().getHostAddress(), 0, false);
        this.input = new BufferedInputStream(new SyncRawConnection.ConnectionInputStream(this.channelConnection));
        this.output = 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 proxyService = Proxies.proxyFor(this.parameters.getProxyType());
            return new ProxySocketConnector(proxyDescriptor, proxyService, 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(PasswordClientAuthentication.defaultAuthentication(this.parameters.getUsername(), this.parameters.getPassword()),
                                                     PublicKeyClientAuthentication.defaultAuthentication(this.parameters.getUsername(), this.parameters.getPublicKeyAlgorithm(), this.parameters.getKeyPair()));
        } else if (this.parameters.getPassword() != null) {
            return PasswordClientAuthentication.defaultAuthentication(this.parameters.getUsername(), 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());
        }
    }
}
