/*
 * @(#)Rsh.java
 *
 * Copyright (c) 2001-2002 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.bsd;

import com.jscape.inet.util.TimedSocket;

import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.SocketException;
import java.net.UnknownHostException;


/**
 * Implements the functionality of a BSD rsh client.
 * <P>
 * Example Usage:
 * <P>
 * <pre>
 * Rsh rsh = new Rsh("remote.host.com", "johndoe", "johndoe","ls -la", false);
 * try
 * {
 *    rsh.connect();
 *    rsh.execute();
 *    InputStream input = rsh.getInputStream();
 *    int in = input.read();
 *    StringBuffer buffer = new StringBuffer();
 *    if(in == 0)
 *    {
 *       buffer.append("Success! Remote host returned: \n");
 *    }
 *    else
 *    {
 *       buffer.append("Failure! Remote host returned: \n");
 *    }
 *       while((in = input.read()) != -1)
 *    {
 *       buffer.append((char)in);
 *    }
 *    System.out.println(buffer.toString());
 * }
 * catch(Exception e)
 * {
 *    System.out.println(e);
 * }
 * </pre>
 * </p>
 */
public class Rsh
        extends Rexec
        implements Serializable {
    private static final int END_PORT = 1023;
    private static final int START_PORT = 512;
    private String localUsername = "";

    private static final int DEFAULT_PROXY_PORT = 1080;
    private static final String DEFAULT_PROXY_TYPE = "SOCKS5";
    private String proxyHostname = null;
    private int proxyPort = DEFAULT_PROXY_PORT;
    private String proxyUsername = null;
    private String proxyPassword = null;
    private String proxyType = DEFAULT_PROXY_TYPE;

    /**
     * Initializes the Rsh object with port set to 514. All connection
     * parameters must be set using member functions.
     * <p/>
     * <p></p>
     *
     * @param hasErrorStream if true stderr will be processed through separate
     *                       stream
     */
    public Rsh(boolean hasErrorStream) {
        super(hasErrorStream);
        port = 514; //default rsh port
    }

    /**
     * Initializes the Rsh object with port set to 514 and all connection
     * parameters.
     * <p/>
     * <p></p>
     *
     * @param remoteHost     the Internet name of the remote host.
     * @param username       the username of the user, whose credentials are to be
     *                       used to execute the <code>command</code>.
     * @param localUsername  the username of the user who is initiating the
     *                       connection.
     * @param command        the command with arguments, to execute.
     * @param hasErrorStream if true, remote host's stderr will be outputted
     *                       through separate error stream. If false, stderr will be
     *                       outputted along with stdout stream.
     */
    public Rsh(String remoteHost, String username, String localUsername,
               String command, boolean hasErrorStream) {
        super(hasErrorStream);
        setRemoteHost(remoteHost);
        setUsername(username);
        setLocalUsername(localUsername);
        setCommand(command);
        port = 514;
    }

    /**
     * Initializes the <code>InputStream</code> that will be used to receive
     * stderr. BSD convention only allows use of local ports between 512 and
     * 1023.
     * <p/>
     * <p></p>
     *
     * @return InputStream the <code>InputStream</code> from which the stderr
     *         can be read.
     *
     * @throws BsdException if all ports between 512 and 1023 are in use.
     * @throws IOException  if server socket can not be created.
     */
    private InputStream initErrorStream()
            throws BsdException, IOException {
        int localPort;
        ServerSocket err = null;

        for (localPort = END_PORT; localPort >= START_PORT; --localPort) {
            try {
                err = new ServerSocket(localPort);
            } catch (SocketException e) {
                continue;
            }

            break;
        }

        if (localPort < START_PORT) {
            throw new BsdException(
                    "Can not initialize error stream - No free local ports between " + START_PORT + " and " + END_PORT + ", or insufficient permissions.");
        }

        output.write(Integer.toString(err.getLocalPort()).getBytes());
        output.write('\0');
        output.flush();

        errSocket = err.accept();
        err.close();

        return errSocket.getInputStream();
    }

    /**
     * Initializes input and output streams to handle stdin and stdout of the
     * remote host. Before calling this method, all connection parameters must
     * be set.
     *
     * @throws BsdException if connection can not be established.
     */
    public void connect()
            throws BsdException {
        synchronized (this) {
            int localPort;
            InetAddress localHost = null;
            InetAddress remote = null;

            try {
                localHost = InetAddress.getLocalHost();
                remote = InetAddress.getByName(remoteHost);
            } catch (UnknownHostException e) {
                throw new BsdException(e.toString());
            }

            for (localPort = END_PORT; localPort >= START_PORT; --localPort) {
                try {
                    if (this.proxyHostname == null) {
                        rexecSocket = TimedSocket.getSocket(remote, port,
                                                            localHost, localPort, timeout);
                    } else {
                        rexecSocket = TimedSocket.getSocket(remoteHost, port,
                                                            timeout, this.proxyHostname, this.proxyPort, this.proxyUsername,
                                                            this.proxyPassword, this.proxyType);
                    }
                    input = rexecSocket.getInputStream();
                    output = rexecSocket.getOutputStream();
                    fireBsdConnected(getRemoteHost());
                } catch (SocketException e) {
                    continue;
                } catch (java.io.IOException e) {
                    throw new BsdException(e.toString());
                }

                break;
            }

            if (localPort < START_PORT) {
                throw new BsdException(
                        "Can not initialize error stream - No free local ports between " + START_PORT + " and " + END_PORT + ", or insufficient permissions.");
            }
        }
    }

    /**
     * Executes a command through the rshd daemon on the remote host. After
     * calling this method, stdout of the remote host is received through an
     * <code>InputStream</code> returned by <code>getInputStream()</code>
     * method. stdin can be accessed through an <code>OutputStream</code>
     * returned by <code>getOutputStream()</code>. If separate stderr
     * processing is required, stderr can be read using an
     * <code>InputStream</code> returned by <code>getInputStream()</code>
     * method.
     * <p/>
     * <p>
     * To properly terminate connection, use <code>disconnect()</code> method.
     * Beware that remote process will finish after EOF is sent to stdout, so
     * disconnecting too early will not terminate remote process.
     * </p>
     * <p/>
     * <p></p>
     *
     * @throws BsdException if rsh negotiation fails.
     */
    public void execute()
            throws BsdException {
        synchronized (this) {
            try {
                if (hasErrorStream) {
                    error = initErrorStream();
                } else {
                    output.write('\0');
                }

                output.write(getLocalUsername().getBytes());
                output.write('\0');
                output.write(getUsername().getBytes());
                output.write('\0');
                output.write(getCommand().getBytes());
                output.write('\0');
                output.flush();
            } catch (IOException e) {
                throw new BsdException(e.toString(), e);
            }
        }
    }

    /**
     * Sets the username of the user originating the connection. Unix
     * restrictions apply - only 8 characters are allowed. Any excess is
     * removed.
     *
     * @param localUsername the local username to be sent to the remote host.
     */
    public void setLocalUsername(String localUsername) {
        synchronized (this) {
            if (localUsername.length() > 8) {
                localUsername = localUsername.substring(0, 7);
            }

            this.localUsername = localUsername;
        }
    }

    /**
     * Gets the local username to be sent to the remote host.
     *
     * @return the local username to be sent to the remote host.
     */
    public String getLocalUsername() {
        return localUsername;
    }

    /**
     * Sets the username and password to use when for authentication with
     * proxy server.
     * To clear these settings invoke the <code>#clearProxySettings</code> method.
     *
     * @param proxyUsername the proxy username
     * @param proxyPassword the proxy password
     *
     * @see #clearProxySettings
     */
    public void setProxyAuthentication(String proxyUsername,
                                       String proxyPassword) {
        this.proxyUsername = proxyUsername;
        this.proxyPassword = proxyPassword;
    }

    // PROXY CONTROL

    /**
     * Sets the proxy hostname and port for this connection.
     * To clear these settings invoke the <code>#clearProxySettings</code> method.
     *
     * @param proxyHostname the hostname or ip address of the proxy server
     * @param proxyPort     the port of the proxy server
     *
     * @see #clearProxySettings
     */
    public void setProxyHost(String proxyHostname, int proxyPort) {
        this.proxyHostname = proxyHostname;
        this.proxyPort = proxyPort;
    }

    /**
     * Sets the proxy type will be used for this connection.
     *
     * @param proxyType The proxy type. Valid values: HTTP, SOCKS5
     */
    public void setProxyType(String proxyType) {
        this.proxyType = proxyType;
    }

    /**
     * Clears proxy server values.
     */
    public void clearProxySettings() {
        setProxyAuthentication(null, null);
        setProxyHost(null, DEFAULT_PROXY_PORT);
        setProxyType(DEFAULT_PROXY_TYPE);
    }
}
