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