    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 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 * <p>
    050 * By default, the standard output and standard error streams of the remote process are transmitted over the same connection, readable from the input stream
    051 * returned by {@link org.apache.commons.net.bsd.RExecClient#getInputStream getInputStream() } . However, it is possible to tell the rshd daemon to return the
    052 * standard error stream over a separate connection, readable from the input stream returned by {@link org.apache.commons.net.bsd.RExecClient#getErrorStream
    053 * getErrorStream() } . You can specify that a separate connection should be created for standard error by setting the boolean
    054 * <code> separateErrorStream </code> parameter of {@link #rcommand rcommand() } to <code> true </code>. The standard input of the remote process can be written
    055 * to through the output stream returned by {@link org.apache.commons.net.bsd.RExecClient#getOutputStream getOutputStream() } .
    056 * </p>
    057 *
    058 * @see org.apache.commons.net.SocketClient
    059 * @see RExecClient
    060 * @see RLoginClient
    061 */
    062public class RCommandClient extends RExecClient {
    063    /**
    064     * The default rshell port. Set to 514 in BSD Unix.
    065     */
    066    public static final int DEFAULT_PORT = 514;
    067
    068    /**
    069     * The smallest port number a rcmd client may use. By BSD convention this number is 512.
    070     */
    071    public static final int MIN_CLIENT_PORT = 512;
    072
    073    /**
    074     * The largest port number a rcmd client may use. By BSD convention this number is 1023.
    075     */
    076    public static final int MAX_CLIENT_PORT = 1023;
    077
    078    /**
    079     * The default RCommandClient constructor. Initializes the default port to <code> DEFAULT_PORT </code>.
    080     */
    081    public RCommandClient() {
    082        setDefaultPort(DEFAULT_PORT);
    083    }
    084
    085    /**
    086     * 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
    087     * daemon. Before returning, {@link org.apache.commons.net.SocketClient#_connectAction_ _connectAction_() } is called to perform connection initialization
    088     * actions.
    089     *
    090     * @param host The remote host.
    091     * @param port The port to connect to on the remote host.
    092     * @throws SocketException If the socket timeout could not be set.
    093     * @throws BindException   If all acceptable rshell ports are in use.
    094     * @throws IOException     If the socket could not be opened. In most cases you will only want to catch IOException since SocketException is derived from
    095     *                         it.
    096     */
    097    @Override
    098    public void connect(final InetAddress host, final int port) throws SocketException, IOException {
    099        connect(host, port, InetAddress.getLocalHost());
    100    }
    101
    102    /**
    103     * 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
    104     * the BSD rshell daemon. Before returning, {@link org.apache.commons.net.SocketClient#_connectAction_ _connectAction_() } is called to perform connection
    105     * initialization actions.
    106     *
    107     * @param host      The remote host.
    108     * @param port      The port to connect to on the remote host.
    109     * @param localAddr The local address to use.
    110     * @throws SocketException If the socket timeout could not be set.
    111     * @throws BindException   If all acceptable rshell ports are in use.
    112     * @throws IOException     If the socket could not be opened. In most cases you will only want to catch IOException since SocketException is derived from
    113     *                         it.
    114     */
    115    public void connect(final InetAddress host, final int port, final InetAddress localAddr) throws SocketException, BindException, IOException {
    116        int localPort;
    117
    118        for (localPort = MAX_CLIENT_PORT; localPort >= MIN_CLIENT_PORT; --localPort) {
    119            try {
    120                _socket_ = _socketFactory_.createSocket(host, port, localAddr, localPort);
    121            } catch (final SocketException e) {
    122                continue;
    123            }
    124            break;
    125        }
    126
    127        if (localPort < MIN_CLIENT_PORT) {
    128            throw new BindException("All ports in use or insufficient permssion.");
    129        }
    130
    131        _connectAction_();
    132    }
    133
    134    /**
    135     * 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
    136     * between <code> MIN_CLIENT_PORT </code> and <code> MAX_CLIENT_PORT </code> or an IllegalArgumentException will be thrown. Before returning,
    137     * {@link org.apache.commons.net.SocketClient#_connectAction_ _connectAction_() } is called to perform connection initialization actions.
    138     *
    139     * @param host      The remote host.
    140     * @param port      The port to connect to on the remote host.
    141     * @param localAddr The local address to use.
    142     * @param localPort The local port to use.
    143     * @throws SocketException          If the socket timeout could not be set.
    144     * @throws IOException              If the socket could not be opened. In most cases you will only want to catch IOException since SocketException is
    145     *                                  derived from it.
    146     * @throws IllegalArgumentException If an invalid local port number is specified.
    147     */
    148    @Override
    149    public void connect(final InetAddress host, final int port, final InetAddress localAddr, final int localPort)
    150            throws SocketException, IOException, IllegalArgumentException {
    151        if (localPort < MIN_CLIENT_PORT || localPort > MAX_CLIENT_PORT) {
    152            throw new IllegalArgumentException("Invalid port number " + localPort);
    153        }
    154        super.connect(host, port, localAddr, localPort);
    155    }
    156
    157    /**
    158     * 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
    159     * daemon. Before returning, {@link org.apache.commons.net.SocketClient#_connectAction_ _connectAction_() } is called to perform connection initialization
    160     * actions.
    161     *
    162     * @param hostname The name of the remote host.
    163     * @param port     The port to connect to on the remote host.
    164     * @throws SocketException      If the socket timeout could not be set.
    165     * @throws BindException        If all acceptable rshell ports are in use.
    166     * @throws IOException          If the socket could not be opened. In most cases you will only want to catch IOException since SocketException is derived
    167     *                              from it.
    168     * @throws UnknownHostException If the hostname cannot be resolved.
    169     */
    170    @Override
    171    public void connect(final String hostname, final int port) throws SocketException, IOException, UnknownHostException {
    172        connect(InetAddress.getByName(hostname), port, InetAddress.getLocalHost());
    173    }
    174
    175    /**
    176     * 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
    177     * the BSD rshell daemon. Before returning, {@link org.apache.commons.net.SocketClient#_connectAction_ _connectAction_() } is called to perform connection
    178     * initialization actions.
    179     *
    180     * @param hostname  The remote host.
    181     * @param port      The port to connect to on the remote host.
    182     * @param localAddr The local address to use.
    183     * @throws SocketException If the socket timeout could not be set.
    184     * @throws BindException   If all acceptable rshell ports are in use.
    185     * @throws IOException     If the socket could not be opened. In most cases you will only want to catch IOException since SocketException is derived from
    186     *                         it.
    187     */
    188    public void connect(final String hostname, final int port, final InetAddress localAddr) throws SocketException, IOException {
    189        connect(InetAddress.getByName(hostname), port, localAddr);
    190    }
    191
    192    /**
    193     * 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
    194     * between <code> MIN_CLIENT_PORT </code> and <code> MAX_CLIENT_PORT </code> or an IllegalArgumentException will be thrown. Before returning,
    195     * {@link org.apache.commons.net.SocketClient#_connectAction_ _connectAction_() } is called to perform connection initialization actions.
    196     *
    197     * @param hostname  The name of the remote host.
    198     * @param port      The port to connect to on the remote host.
    199     * @param localAddr The local address to use.
    200     * @param localPort The local port to use.
    201     * @throws SocketException          If the socket timeout could not be set.
    202     * @throws IOException              If the socket could not be opened. In most cases you will only want to catch IOException since SocketException is
    203     *                                  derived from it.
    204     * @throws UnknownHostException     If the hostname cannot be resolved.
    205     * @throws IllegalArgumentException If an invalid local port number is specified.
    206     */
    207    @Override
    208    public void connect(final String hostname, final int port, final InetAddress localAddr, final int localPort)
    209            throws SocketException, IOException, IllegalArgumentException, UnknownHostException {
    210        if (localPort < MIN_CLIENT_PORT || localPort > MAX_CLIENT_PORT) {
    211            throw new IllegalArgumentException("Invalid port number " + localPort);
    212        }
    213        super.connect(hostname, port, localAddr, localPort);
    214    }
    215
    216    // Overrides method in RExecClient in order to implement proper
    217    // port number limitations.
    218    @Override
    219    InputStream createErrorStream() throws IOException {
    220        final Socket socket;
    221
    222        try (ServerSocket server = createServer()) {
    223            _output_.write(Integer.toString(server.getLocalPort()).getBytes(StandardCharsets.UTF_8));
    224            _output_.write(NULL_CHAR);
    225            _output_.flush();
    226            socket = server.accept();
    227        }
    228
    229        if (isRemoteVerificationEnabled() && !verifyRemote(socket)) {
    230            socket.close();
    231            throw new IOException("Security violation: unexpected connection attempt by " + socket.getInetAddress().getHostAddress());
    232        }
    233
    234        return new SocketInputStream(socket, socket.getInputStream());
    235    }
    236
    237    private ServerSocket createServer() throws IOException {
    238        for (int localPort = MAX_CLIENT_PORT; localPort >= MIN_CLIENT_PORT; --localPort) {
    239            try {
    240                return _serverSocketFactory_.createServerSocket(localPort, 1, getLocalAddress());
    241            } catch (final SocketException e) {
    242                continue;
    243            }
    244        }
    245        throw new BindException("All ports in use.");
    246    }
    247
    248    /**
    249     * Same as <code> rcommand(localUsername, remoteUsername, command, false); </code>
    250     *
    251     * @param localUser  the local user
    252     * @param remoteUser the remote user
    253     * @param command        the command
    254     * @throws IOException on error
    255     */
    256    public void rcommand(final String localUser, final String remoteUser, final String command) throws IOException {
    257        rcommand(localUser, remoteUser, command, false);
    258    }
    259
    260    /**
    261     * Remotely executes a command through the rshd daemon on the server to which the RCommandClient is connected. After calling this method, you may interact
    262     * 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
    263     * after reaching end of file on its standard output (accessible through {@link #getInputStream getInputStream()}). Disconnecting from the server or closing
    264     * the process streams before reaching end of file will not necessarily terminate the remote process.
    265     * <p>
    266     * If a separate error stream is requested, the remote server will connect to a local socket opened by RCommandClient, providing an independent stream
    267     * through which standard error will be transmitted. The local socket must originate from a secure port (512 - 1023), and rcommand() ensures that this will
    268     * 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
    269     * the remote server, an IOException will be thrown. This serves as a simple protection against possible hijacking of the error stream by an attacker
    270     * monitoring the rexec() negotiation. You may disable this behavior with {@link org.apache.commons.net.bsd.RExecClient#setRemoteVerificationEnabled
    271     * setRemoteVerificationEnabled()} .
    272     * </p>
    273     *
    274     * @param localUser       The user account on the local machine that is requesting the command execution.
    275     * @param remoteUser      The account name on the server through which to execute the command.
    276     * @param command             The command, including any arguments, to execute.
    277     * @param separateErrorStream True if you would like the standard error to be transmitted through a different stream than standard output. False if not.
    278     * @throws IOException If the rcommand() attempt fails. The exception will contain a message indicating the nature of the failure.
    279     */
    280    public void rcommand(final String localUser, final String remoteUser, final String command, final boolean separateErrorStream) throws IOException {
    281        rexec(localUser, remoteUser, command, separateErrorStream);
    282    }
    283
    284}