    001/*
    002 * Licensed to the Apache Software Foundation (ASF) under one or more
    003 * contributor license agreements.  See the NOTICE file distributed with
    004 * this work for additional information regarding copyright ownership.
    005 * The ASF licenses this file to You under the Apache License, Version 2.0
    006 * (the "License"); you may not use this file except in compliance with
    007 * the License.  You may obtain a copy of the License at
    008 *
    009 *      http://www.apache.org/licenses/LICENSE-2.0
    010 *
    011 * Unless required by applicable law or agreed to in writing, software
    012 * distributed under the License is distributed on an "AS IS" BASIS,
    013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    014 * See the License for the specific language governing permissions and
    015 * limitations under the License.
    016 */
    017
    018package org.apache.commons.net.bsd;
    019
    020import java.io.IOException;
    021import java.io.InputStream;
    022import java.net.BindException;
    023import java.net.InetAddress;
    024import java.net.ServerSocket;
    025import java.net.Socket;
    026import java.net.SocketException;
    027import java.net.UnknownHostException;
    028import java.nio.charset.StandardCharsets;
    029
    030import org.apache.commons.net.io.SocketInputStream;
    031
    032/**
    033 * RCommandClient is very similar to {@link org.apache.commons.net.bsd.RExecClient}, from which it is derived, and implements the rcmd() facility that first
    034 * appeared in 4.2BSD Unix. rcmd() is the facility used by the rsh (rshell) and other commands to execute a command on another machine from a trusted host
    035 * without issuing a password. The trust relationship between two machines is established by the contents of a machine's /etc/hosts.equiv file and a user's
    036 * .rhosts file. These files specify from which hosts and accounts on those hosts rcmd() requests will be accepted. The only additional measure for establishing
    037 * trust is that all client connections must originate from a port between 512 and 1023. Consequently, there is an upper limit to the number of rcmd connections
    038 * that can be running simultaneously. The required ports are reserved ports on Unix systems, and can only be bound by a process running with root permissions
    039 * (to accomplish this rsh, rlogin, and related commands usualy have the suid bit set). Therefore, on a Unix system, you will only be able to successfully use
    040 * the RCommandClient class if the process runs as root. However, there is no such restriction on Windows95 and some other systems. The security risks are
    041 * obvious. However, when carefully used, rcmd() can be very useful when used behind a firewall.
    042 * <p>
    043 * As with virtually all of the client classes in org.apache.commons.net, this class derives from SocketClient. But it overrides most of its connection methods
    044 * so that the local Socket will originate from an acceptable rshell port. The way to use RCommandClient is to first connect to the server, call the
    045 * {@link #rcommand rcommand() } method, and then fetch the connection's input, output, and optionally error streams. Interaction with the remote command is
    046 * controlled entirely through the I/O streams. Once you have finished processing the streams, you should invoke
    047 * {@link org.apache.commons.net.bsd.RExecClient#disconnect disconnect() } to clean up properly.
    048 * <p>
    049 * By default the standard output and standard error streams of the remote process are transmitted over the same connection, readable from the input stream
    050 * returned by {@link org.apache.commons.net.bsd.RExecClient#getInputStream getInputStream() } . However, it is possible to tell the rshd daemon to return the
    051 * standard error stream over a separate connection, readable from the input stream returned by {@link org.apache.commons.net.bsd.RExecClient#getErrorStream
    052 * getErrorStream() } . You can specify that a separate connection should be created for standard error by setting the boolean
    053 * <code> separateErrorStream </code> parameter of {@link #rcommand rcommand() } to <code> true </code>. The standard input of the remote process can be written
    054 * to through the output stream returned by {@link org.apache.commons.net.bsd.RExecClient#getOutputStream getOutputStream() } .
    055 *
    056 * @see org.apache.commons.net.SocketClient
    057 * @see RExecClient
    058 * @see RLoginClient
    059 */
    060
    061public class RCommandClient extends RExecClient {
    062    /**
    063     * The default rshell port. Set to 514 in BSD Unix.
    064     */
    065    public static final int DEFAULT_PORT = 514;
    066
    067    /**
    068     * The smallest port number an rcmd client may use. By BSD convention this number is 512.
    069     */
    070    public static final int MIN_CLIENT_PORT = 512;
    071
    072    /**
    073     * The largest port number an rcmd client may use. By BSD convention this number is 1023.
    074     */
    075    public static final int MAX_CLIENT_PORT = 1023;
    076
    077    /**
    078     * The default RCommandClient constructor. Initializes the default port to <code> DEFAULT_PORT </code>.
    079     */
    080    public RCommandClient() {
    081        setDefaultPort(DEFAULT_PORT);
    082    }
    083
    084    /**
    085     * Opens a Socket connected to a remote host at the specified port and originating from the current host at a port in a range acceptable to the BSD rshell
    086     * daemon. Before returning, {@link org.apache.commons.net.SocketClient#_connectAction_ _connectAction_() } is called to perform connection initialization
    087     * actions.
    088     *
    089     * @param host The remote host.
    090     * @param port The port to connect to on the remote host.
    091     * @throws SocketException If the socket timeout could not be set.
    092     * @throws BindException   If all acceptable rshell ports are in use.
    093     * @throws IOException     If the socket could not be opened. In most cases you will only want to catch IOException since SocketException is derived from
    094     *                         it.
    095     */
    096    @Override
    097    public void connect(final InetAddress host, final int port) throws SocketException, IOException {
    098        connect(host, port, InetAddress.getLocalHost());
    099    }
    100
    101    /**
    102     * Opens a Socket connected to a remote host at the specified port and originating from the specified local address using a port in a range acceptable to
    103     * the BSD rshell daemon. Before returning, {@link org.apache.commons.net.SocketClient#_connectAction_ _connectAction_() } is called to perform connection
    104     * initialization actions.
    105     *
    106     * @param host      The remote host.
    107     * @param port      The port to connect to on the remote host.
    108     * @param localAddr The local address to use.
    109     * @throws SocketException If the socket timeout could not be set.
    110     * @throws BindException   If all acceptable rshell ports are in use.
    111     * @throws IOException     If the socket could not be opened. In most cases you will only want to catch IOException since SocketException is derived from
    112     *                         it.
    113     */
    114    public void connect(final InetAddress host, final int port, final InetAddress localAddr) throws SocketException, BindException, IOException {
    115        int localPort;
    116
    117        localPort = MAX_CLIENT_PORT;
    118
    119        for (localPort = MAX_CLIENT_PORT; localPort >= MIN_CLIENT_PORT; --localPort) {
    120            try {
    121                _socket_ = _socketFactory_.createSocket(host, port, localAddr, localPort);
    122            } catch (final SocketException e) {
    123                continue;
    124            }
    125            break;
    126        }
    127
    128        if (localPort < MIN_CLIENT_PORT) {
    129            throw new BindException("All ports in use or insufficient permssion.");
    130        }
    131
    132        _connectAction_();
    133    }
    134
    135    /**
    136     * Opens a Socket connected to a remote host at the specified port and originating from the specified local address and port. The local port must lie
    137     * between <code> MIN_CLIENT_PORT </code> and <code> MAX_CLIENT_PORT </code> or an IllegalArgumentException will be thrown. Before returning,
    138     * {@link org.apache.commons.net.SocketClient#_connectAction_ _connectAction_() } is called to perform connection initialization actions.
    139     *
    140     * @param host      The remote host.
    141     * @param port      The port to connect to on the remote host.
    142     * @param localAddr The local address to use.
    143     * @param localPort The local port to use.
    144     * @throws SocketException          If the socket timeout could not be set.
    145     * @throws IOException              If the socket could not be opened. In most cases you will only want to catch IOException since SocketException is
    146     *                                  derived from it.
    147     * @throws IllegalArgumentException If an invalid local port number is specified.
    148     */
    149    @Override
    150    public void connect(final InetAddress host, final int port, final InetAddress localAddr, final int localPort)
    151            throws SocketException, IOException, IllegalArgumentException {
    152        if (localPort < MIN_CLIENT_PORT || localPort > MAX_CLIENT_PORT) {
    153            throw new IllegalArgumentException("Invalid port number " + localPort);
    154        }
    155        super.connect(host, port, localAddr, localPort);
    156    }
    157
    158    /**
    159     * Opens a Socket connected to a remote host at the specified port and originating from the current host at a port in a range acceptable to the BSD rshell
    160     * daemon. Before returning, {@link org.apache.commons.net.SocketClient#_connectAction_ _connectAction_() } is called to perform connection initialization
    161     * actions.
    162     *
    163     * @param hostname The name of the remote host.
    164     * @param port     The port to connect to on the remote host.
    165     * @throws SocketException      If the socket timeout could not be set.
    166     * @throws BindException        If all acceptable rshell ports are in use.
    167     * @throws IOException          If the socket could not be opened. In most cases you will only want to catch IOException since SocketException is derived
    168     *                              from it.
    169     * @throws UnknownHostException If the hostname cannot be resolved.
    170     */
    171    @Override
    172    public void connect(final String hostname, final int port) throws SocketException, IOException, UnknownHostException {
    173        connect(InetAddress.getByName(hostname), port, InetAddress.getLocalHost());
    174    }
    175
    176    /**
    177     * Opens a Socket connected to a remote host at the specified port and originating from the specified local address using a port in a range acceptable to
    178     * the BSD rshell daemon. Before returning, {@link org.apache.commons.net.SocketClient#_connectAction_ _connectAction_() } is called to perform connection
    179     * initialization actions.
    180     *
    181     * @param hostname  The remote host.
    182     * @param port      The port to connect to on the remote host.
    183     * @param localAddr The local address to use.
    184     * @throws SocketException If the socket timeout could not be set.
    185     * @throws BindException   If all acceptable rshell ports are in use.
    186     * @throws IOException     If the socket could not be opened. In most cases you will only want to catch IOException since SocketException is derived from
    187     *                         it.
    188     */
    189    public void connect(final String hostname, final int port, final InetAddress localAddr) throws SocketException, IOException {
    190        connect(InetAddress.getByName(hostname), port, localAddr);
    191    }
    192
    193    /**
    194     * Opens a Socket connected to a remote host at the specified port and originating from the specified local address and port. The local port must lie
    195     * between <code> MIN_CLIENT_PORT </code> and <code> MAX_CLIENT_PORT </code> or an IllegalArgumentException will be thrown. Before returning,
    196     * {@link org.apache.commons.net.SocketClient#_connectAction_ _connectAction_() } is called to perform connection initialization actions.
    197     *
    198     * @param hostname  The name of the remote host.
    199     * @param port      The port to connect to on the remote host.
    200     * @param localAddr The local address to use.
    201     * @param localPort The local port to use.
    202     * @throws SocketException          If the socket timeout could not be set.
    203     * @throws IOException              If the socket could not be opened. In most cases you will only want to catch IOException since SocketException is
    204     *                                  derived from it.
    205     * @throws UnknownHostException     If the hostname cannot be resolved.
    206     * @throws IllegalArgumentException If an invalid local port number is specified.
    207     */
    208    @Override
    209    public void connect(final String hostname, final int port, final InetAddress localAddr, final int localPort)
    210            throws SocketException, IOException, IllegalArgumentException, UnknownHostException {
    211        if (localPort < MIN_CLIENT_PORT || localPort > MAX_CLIENT_PORT) {
    212            throw new IllegalArgumentException("Invalid port number " + localPort);
    213        }
    214        super.connect(hostname, port, localAddr, localPort);
    215    }
    216
    217    // Overrides method in RExecClient in order to implement proper
    218    // port number limitations.
    219    @Override
    220    InputStream createErrorStream() throws IOException {
    221        int localPort;
    222        final Socket socket;
    223
    224        localPort = MAX_CLIENT_PORT;
    225        ServerSocket server = null;
    226
    227        for (localPort = MAX_CLIENT_PORT; localPort >= MIN_CLIENT_PORT; --localPort) {
    228            try {
    229                server = _serverSocketFactory_.createServerSocket(localPort, 1, getLocalAddress());
    230                break; // got a socket
    231            } catch (final SocketException e) {
    232                continue;
    233            }
    234        }
    235
    236        if (server == null) {
    237            throw new BindException("All ports in use.");
    238        }
    239
    240        _output_.write(Integer.toString(server.getLocalPort()).getBytes(StandardCharsets.UTF_8)); // $NON-NLS
    241        _output_.write(NULL_CHAR);
    242        _output_.flush();
    243
    244        socket = server.accept();
    245        server.close();
    246
    247        if (isRemoteVerificationEnabled() && !verifyRemote(socket)) {
    248            socket.close();
    249            throw new IOException("Security violation: unexpected connection attempt by " + socket.getInetAddress().getHostAddress());
    250        }
    251
    252        return new SocketInputStream(socket, socket.getInputStream());
    253    }
    254
    255    /**
    256     * Same as <code> rcommand(localUsername, remoteUsername, command, false); </code>
    257     *
    258     * @param localUsername  the local user
    259     * @param remoteUsername the remote user
    260     * @param command        the command
    261     * @throws IOException on error
    262     */
    263    public void rcommand(final String localUsername, final String remoteUsername, final String command) throws IOException {
    264        rcommand(localUsername, remoteUsername, command, false);
    265    }
    266
    267    /**
    268     * Remotely executes a command through the rshd daemon on the server to which the RCommandClient is connected. After calling this method, you may interact
    269     * with the remote process through its standard input, output, and error streams. You will typically be able to detect the termination of the remote process
    270     * after reaching end of file on its standard output (accessible through {@link #getInputStream getInputStream() }. Disconnecting from the server or closing
    271     * the process streams before reaching end of file will not necessarily terminate the remote process.
    272     * <p>
    273     * If a separate error stream is requested, the remote server will connect to a local socket opened by RCommandClient, providing an independent stream
    274     * through which standard error will be transmitted. The local socket must originate from a secure port (512 - 1023), and rcommand() ensures that this will
    275     * be so. RCommandClient will also do a simple security check when it accepts a connection for this error stream. If the connection does not originate from
    276     * the remote server, an IOException will be thrown. This serves as a simple protection against possible hijacking of the error stream by an attacker
    277     * monitoring the rexec() negotiation. You may disable this behavior with {@link org.apache.commons.net.bsd.RExecClient#setRemoteVerificationEnabled
    278     * setRemoteVerificationEnabled()} .
    279     * <p>
    280     *
    281     * @param localUsername       The user account on the local machine that is requesting the command execution.
    282     * @param remoteUsername      The account name on the server through which to execute the command.
    283     * @param command             The command, including any arguments, to execute.
    284     * @param separateErrorStream True if you would like the standard error to be transmitted through a different stream than standard output. False if not.
    285     * @throws IOException If the rcommand() attempt fails. The exception will contain a message indicating the nature of the failure.
    286     */
    287    public void rcommand(final String localUsername, final String remoteUsername, final String command, final boolean separateErrorStream) throws IOException {
    288        rexec(localUsername, remoteUsername, command, separateErrorStream);
    289    }
    290
    291}