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 * https://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
18 package org.apache.commons.net.bsd;
19
20 import java.io.IOException;
21 import java.io.InputStream;
22 import java.net.BindException;
23 import java.net.InetAddress;
24 import java.net.ServerSocket;
25 import java.net.Socket;
26 import java.net.SocketException;
27 import java.net.UnknownHostException;
28 import java.nio.charset.StandardCharsets;
29
30 import org.apache.commons.io.IOUtils;
31 import org.apache.commons.net.io.SocketInputStream;
32
33 /**
34 * RCommandClient is very similar to {@link org.apache.commons.net.bsd.RExecClient}, from which it is derived, and implements the rcmd() facility that first
35 * 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
36 * 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
37 * .rhosts file. These files specify from which hosts and accounts on those hosts rcmd() requests will be accepted. The only additional measure for establishing
38 * 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
39 * 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
40 * (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
41 * 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
42 * obvious. However, when carefully used, rcmd() can be very useful when used behind a firewall.
43 * <p>
44 * 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
45 * 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
46 * {@link #rcommand rcommand()} method, and then fetch the connection's input, output, and optionally error streams. Interaction with the remote command is
47 * controlled entirely through the I/O streams. Once you have finished processing the streams, you should invoke
48 * {@link org.apache.commons.net.bsd.RExecClient#disconnect disconnect()} to clean up properly.
49 * </p>
50 * <p>
51 * By default, the standard output and standard error streams of the remote process are transmitted over the same connection, readable from the input stream
52 * returned by {@link org.apache.commons.net.bsd.RExecClient#getInputStream getInputStream()} . However, it is possible to tell the rshd daemon to return the
53 * standard error stream over a separate connection, readable from the input stream returned by {@link org.apache.commons.net.bsd.RExecClient#getErrorStream
54 * getErrorStream()} . You can specify that a separate connection should be created for standard error by setting the boolean
55 * {@code separateErrorStream} parameter of {@link #rcommand rcommand()} to {@code true}. The standard input of the remote process can be written
56 * to through the output stream returned by {@link org.apache.commons.net.bsd.RExecClient#getOutputStream getOutputStream()}.
57 * </p>
58 *
59 * @see org.apache.commons.net.SocketClient
60 * @see RExecClient
61 * @see RLoginClient
62 */
63 public class RCommandClient extends RExecClient {
64
65 /**
66 * The default rshell port. Set to 514 in BSD Unix.
67 */
68 public static final int DEFAULT_PORT = 514;
69
70 /**
71 * The smallest port number a rcmd client may use. By BSD convention this number is 512.
72 */
73 public static final int MIN_CLIENT_PORT = 512;
74
75 /**
76 * The largest port number a rcmd client may use. By BSD convention this number is 1023.
77 */
78 public static final int MAX_CLIENT_PORT = 1023;
79
80 /**
81 * The default RCommandClient constructor. Initializes the default port to {@code DEFAULT_PORT}.
82 */
83 public RCommandClient() {
84 setDefaultPort(DEFAULT_PORT);
85 }
86
87 /**
88 * 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
89 * daemon. Before returning, {@link org.apache.commons.net.SocketClient#_connectAction_ _connectAction_()} is called to perform connection initialization
90 * actions.
91 *
92 * @param host The remote host.
93 * @param port The port to connect to on the remote host.
94 * @throws SocketException If the socket timeout could not be set.
95 * @throws BindException If all acceptable rshell ports are in use.
96 * @throws IOException If the socket could not be opened. In most cases you will only want to catch IOException since SocketException is derived from
97 * it.
98 */
99 @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 }