RCommandClient.java

  1. /*
  2.  * Licensed to the Apache Software Foundation (ASF) under one or more
  3.  * contributor license agreements.  See the NOTICE file distributed with
  4.  * this work for additional information regarding copyright ownership.
  5.  * The ASF licenses this file to You under the Apache License, Version 2.0
  6.  * (the "License"); you may not use this file except in compliance with
  7.  * the License.  You may obtain a copy of the License at
  8.  *
  9.  *      http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */

  17. package org.apache.commons.net.bsd;

  18. import java.io.IOException;
  19. import java.io.InputStream;
  20. import java.net.BindException;
  21. import java.net.InetAddress;
  22. import java.net.ServerSocket;
  23. import java.net.Socket;
  24. import java.net.SocketException;
  25. import java.net.UnknownHostException;
  26. import java.nio.charset.StandardCharsets;

  27. import org.apache.commons.net.io.SocketInputStream;

  28. /**
  29.  * RCommandClient is very similar to {@link org.apache.commons.net.bsd.RExecClient}, from which it is derived, and implements the rcmd() facility that first
  30.  * 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
  31.  * 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
  32.  * .rhosts file. These files specify from which hosts and accounts on those hosts rcmd() requests will be accepted. The only additional measure for establishing
  33.  * 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
  34.  * 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
  35.  * (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
  36.  * 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
  37.  * obvious. However, when carefully used, rcmd() can be very useful when used behind a firewall.
  38.  * <p>
  39.  * 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
  40.  * 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
  41.  * {@link #rcommand rcommand() } method, and then fetch the connection's input, output, and optionally error streams. Interaction with the remote command is
  42.  * controlled entirely through the I/O streams. Once you have finished processing the streams, you should invoke
  43.  * {@link org.apache.commons.net.bsd.RExecClient#disconnect disconnect() } to clean up properly.
  44.  * </p>
  45.  * <p>
  46.  * By default, the standard output and standard error streams of the remote process are transmitted over the same connection, readable from the input stream
  47.  * returned by {@link org.apache.commons.net.bsd.RExecClient#getInputStream getInputStream() } . However, it is possible to tell the rshd daemon to return the
  48.  * standard error stream over a separate connection, readable from the input stream returned by {@link org.apache.commons.net.bsd.RExecClient#getErrorStream
  49.  * getErrorStream() } . You can specify that a separate connection should be created for standard error by setting the boolean
  50.  * <code>separateErrorStream</code> parameter of {@link #rcommand rcommand() } to <code>true</code>. The standard input of the remote process can be written
  51.  * to through the output stream returned by {@link org.apache.commons.net.bsd.RExecClient#getOutputStream getOutputStream() } .
  52.  * </p>
  53.  *
  54.  * @see org.apache.commons.net.SocketClient
  55.  * @see RExecClient
  56.  * @see RLoginClient
  57.  */
  58. public class RCommandClient extends RExecClient {

  59.     /**
  60.      * The default rshell port. Set to 514 in BSD Unix.
  61.      */
  62.     public static final int DEFAULT_PORT = 514;

  63.     /**
  64.      * The smallest port number a rcmd client may use. By BSD convention this number is 512.
  65.      */
  66.     public static final int MIN_CLIENT_PORT = 512;

  67.     /**
  68.      * The largest port number a rcmd client may use. By BSD convention this number is 1023.
  69.      */
  70.     public static final int MAX_CLIENT_PORT = 1023;

  71.     /**
  72.      * The default RCommandClient constructor. Initializes the default port to <code>DEFAULT_PORT</code>.
  73.      */
  74.     public RCommandClient() {
  75.         setDefaultPort(DEFAULT_PORT);
  76.     }

  77.     /**
  78.      * 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
  79.      * daemon. Before returning, {@link org.apache.commons.net.SocketClient#_connectAction_ _connectAction_() } is called to perform connection initialization
  80.      * actions.
  81.      *
  82.      * @param host The remote host.
  83.      * @param port The port to connect to on the remote host.
  84.      * @throws SocketException If the socket timeout could not be set.
  85.      * @throws BindException   If all acceptable rshell ports are in use.
  86.      * @throws IOException     If the socket could not be opened. In most cases you will only want to catch IOException since SocketException is derived from
  87.      *                         it.
  88.      */
  89.     @Override
  90.     public void connect(final InetAddress host, final int port) throws SocketException, IOException {
  91.         connect(host, port, InetAddress.getLocalHost());
  92.     }

  93.     /**
  94.      * 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
  95.      * the BSD rshell daemon. Before returning, {@link org.apache.commons.net.SocketClient#_connectAction_ _connectAction_() } is called to perform connection
  96.      * initialization actions.
  97.      *
  98.      * @param host      The remote host.
  99.      * @param port      The port to connect to on the remote host.
  100.      * @param localAddr The local address to use.
  101.      * @throws SocketException If the socket timeout could not be set.
  102.      * @throws BindException   If all acceptable rshell ports are in use.
  103.      * @throws IOException     If the socket could not be opened. In most cases you will only want to catch IOException since SocketException is derived from
  104.      *                         it.
  105.      */
  106.     public void connect(final InetAddress host, final int port, final InetAddress localAddr) throws SocketException, BindException, IOException {
  107.         int localPort;

  108.         for (localPort = MAX_CLIENT_PORT; localPort >= MIN_CLIENT_PORT; --localPort) {
  109.             try {
  110.                 _socket_ = _socketFactory_.createSocket(host, port, localAddr, localPort);
  111.             } catch (final SocketException e) {
  112.                 continue;
  113.             }
  114.             break;
  115.         }

  116.         if (localPort < MIN_CLIENT_PORT) {
  117.             throw new BindException("All ports in use or insufficient permssion.");
  118.         }

  119.         _connectAction_();
  120.     }

  121.     /**
  122.      * 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
  123.      * between <code>MIN_CLIENT_PORT</code> and <code>MAX_CLIENT_PORT</code> or an IllegalArgumentException will be thrown. Before returning,
  124.      * {@link org.apache.commons.net.SocketClient#_connectAction_ _connectAction_() } is called to perform connection initialization actions.
  125.      *
  126.      * @param host      The remote host.
  127.      * @param port      The port to connect to on the remote host.
  128.      * @param localAddr The local address to use.
  129.      * @param localPort The local port to use.
  130.      * @throws SocketException          If the socket timeout could not be set.
  131.      * @throws IOException              If the socket could not be opened. In most cases you will only want to catch IOException since SocketException is
  132.      *                                  derived from it.
  133.      * @throws IllegalArgumentException If an invalid local port number is specified.
  134.      */
  135.     @Override
  136.     public void connect(final InetAddress host, final int port, final InetAddress localAddr, final int localPort)
  137.             throws SocketException, IOException, IllegalArgumentException {
  138.         if (localPort < MIN_CLIENT_PORT || localPort > MAX_CLIENT_PORT) {
  139.             throw new IllegalArgumentException("Invalid port number " + localPort);
  140.         }
  141.         super.connect(host, port, localAddr, localPort);
  142.     }

  143.     /**
  144.      * 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
  145.      * daemon. Before returning, {@link org.apache.commons.net.SocketClient#_connectAction_ _connectAction_() } is called to perform connection initialization
  146.      * actions.
  147.      *
  148.      * @param hostname The name of the remote host.
  149.      * @param port     The port to connect to on the remote host.
  150.      * @throws SocketException      If the socket timeout could not be set.
  151.      * @throws BindException        If all acceptable rshell ports are in use.
  152.      * @throws IOException          If the socket could not be opened. In most cases you will only want to catch IOException since SocketException is derived
  153.      *                              from it.
  154.      * @throws UnknownHostException If the hostname cannot be resolved.
  155.      */
  156.     @Override
  157.     public void connect(final String hostname, final int port) throws SocketException, IOException, UnknownHostException {
  158.         connect(InetAddress.getByName(hostname), port, InetAddress.getLocalHost());
  159.     }

  160.     /**
  161.      * 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
  162.      * the BSD rshell daemon. Before returning, {@link org.apache.commons.net.SocketClient#_connectAction_ _connectAction_() } is called to perform connection
  163.      * initialization actions.
  164.      *
  165.      * @param hostname  The remote host.
  166.      * @param port      The port to connect to on the remote host.
  167.      * @param localAddr The local address to use.
  168.      * @throws SocketException If the socket timeout could not be set.
  169.      * @throws BindException   If all acceptable rshell ports are in use.
  170.      * @throws IOException     If the socket could not be opened. In most cases you will only want to catch IOException since SocketException is derived from
  171.      *                         it.
  172.      */
  173.     public void connect(final String hostname, final int port, final InetAddress localAddr) throws SocketException, IOException {
  174.         connect(InetAddress.getByName(hostname), port, localAddr);
  175.     }

  176.     /**
  177.      * 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
  178.      * between <code>MIN_CLIENT_PORT</code> and <code>MAX_CLIENT_PORT</code> or an IllegalArgumentException will be thrown. Before returning,
  179.      * {@link org.apache.commons.net.SocketClient#_connectAction_ _connectAction_() } is called to perform connection initialization actions.
  180.      *
  181.      * @param hostname  The name of the remote host.
  182.      * @param port      The port to connect to on the remote host.
  183.      * @param localAddr The local address to use.
  184.      * @param localPort The local port to use.
  185.      * @throws SocketException          If the socket timeout could not be set.
  186.      * @throws IOException              If the socket could not be opened. In most cases you will only want to catch IOException since SocketException is
  187.      *                                  derived from it.
  188.      * @throws UnknownHostException     If the hostname cannot be resolved.
  189.      * @throws IllegalArgumentException If an invalid local port number is specified.
  190.      */
  191.     @Override
  192.     public void connect(final String hostname, final int port, final InetAddress localAddr, final int localPort)
  193.             throws SocketException, IOException, IllegalArgumentException, UnknownHostException {
  194.         if (localPort < MIN_CLIENT_PORT || localPort > MAX_CLIENT_PORT) {
  195.             throw new IllegalArgumentException("Invalid port number " + localPort);
  196.         }
  197.         super.connect(hostname, port, localAddr, localPort);
  198.     }

  199.     // Overrides method in RExecClient in order to implement proper
  200.     // port number limitations.
  201.     @Override
  202.     InputStream createErrorStream() throws IOException {
  203.         final Socket socket;

  204.         try (ServerSocket server = createServer()) {
  205.             _output_.write(Integer.toString(server.getLocalPort()).getBytes(StandardCharsets.UTF_8));
  206.             _output_.write(NULL_CHAR);
  207.             _output_.flush();
  208.             socket = server.accept();
  209.         }

  210.         if (isRemoteVerificationEnabled() && !verifyRemote(socket)) {
  211.             socket.close();
  212.             throw new IOException("Security violation: unexpected connection attempt by " + socket.getInetAddress().getHostAddress());
  213.         }

  214.         return new SocketInputStream(socket, socket.getInputStream());
  215.     }

  216.     private ServerSocket createServer() throws IOException {
  217.         for (int localPort = MAX_CLIENT_PORT; localPort >= MIN_CLIENT_PORT; --localPort) {
  218.             try {
  219.                 return _serverSocketFactory_.createServerSocket(localPort, 1, getLocalAddress());
  220.             } catch (final SocketException e) {
  221.                 continue;
  222.             }
  223.         }
  224.         throw new BindException("All ports in use.");
  225.     }

  226.     /**
  227.      * Same as <code>rcommand(localUserName, remoteUserName, command, false);</code>
  228.      *
  229.      * @param localUser  the local user
  230.      * @param remoteUser the remote user
  231.      * @param command        the command
  232.      * @throws IOException on error
  233.      */
  234.     public void rcommand(final String localUser, final String remoteUser, final String command) throws IOException {
  235.         rcommand(localUser, remoteUser, command, false);
  236.     }

  237.     /**
  238.      * Remotely executes a command through the rshd daemon on the server to which the RCommandClient is connected. After calling this method, you may interact
  239.      * 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
  240.      * after reaching end of file on its standard output (accessible through {@link #getInputStream getInputStream()}). Disconnecting from the server or closing
  241.      * the process streams before reaching end of file will not necessarily terminate the remote process.
  242.      * <p>
  243.      * If a separate error stream is requested, the remote server will connect to a local socket opened by RCommandClient, providing an independent stream
  244.      * through which standard error will be transmitted. The local socket must originate from a secure port (512 - 1023), and rcommand() ensures that this will
  245.      * 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
  246.      * the remote server, an IOException will be thrown. This serves as a simple protection against possible hijacking of the error stream by an attacker
  247.      * monitoring the rexec() negotiation. You may disable this behavior with {@link org.apache.commons.net.bsd.RExecClient#setRemoteVerificationEnabled
  248.      * setRemoteVerificationEnabled()} .
  249.      * </p>
  250.      *
  251.      * @param localUser       The user account on the local machine that is requesting the command execution.
  252.      * @param remoteUser      The account name on the server through which to execute the command.
  253.      * @param command             The command, including any arguments, to execute.
  254.      * @param separateErrorStream True if you would like the standard error to be transmitted through a different stream than standard output. False if not.
  255.      * @throws IOException If the rcommand() attempt fails. The exception will contain a message indicating the nature of the failure.
  256.      */
  257.     public void rcommand(final String localUser, final String remoteUser, final String command, final boolean separateErrorStream) throws IOException {
  258.         rexec(localUser, remoteUser, command, separateErrorStream);
  259.     }

  260. }