1 /* 2 * Copyright (c) 2006-2011 Christian Plattner. All rights reserved. 3 * Please refer to the LICENSE.txt for licensing details. 4 */ 5 package ch.ethz.ssh2; 6 7 import java.io.IOException; 8 import java.io.InputStream; 9 import java.io.OutputStream; 10 import java.security.SecureRandom; 11 12 import ch.ethz.ssh2.channel.Channel; 13 import ch.ethz.ssh2.channel.ChannelManager; 14 import ch.ethz.ssh2.channel.X11ServerData; 15 16 /** 17 * A <code>Session</code> is a remote execution of a program. "Program" means 18 * in this context either a shell, an application or a system command. The 19 * program may or may not have a tty. Only one single program can be started on 20 * a session. However, multiple sessions can be active simultaneously. 21 * 22 * @author Christian Plattner 23 * @version $Id: Session.java 32 2011-05-28 21:56:21Z dkocher (at) sudo.ch $ 24 */ 25 public class Session 26 { 27 private ChannelManager cm; 28 private Channel cn; 29 30 private boolean flag_pty_requested = false; 31 private boolean flag_x11_requested = false; 32 private boolean flag_execution_started = false; 33 private boolean flag_closed = false; 34 35 private String x11FakeCookie = null; 36 37 private final SecureRandom rnd; 38 39 protected Session(ChannelManager cm, SecureRandom rnd) throws IOException 40 { 41 this.cm = cm; 42 this.cn = cm.openSessionChannel(); 43 this.rnd = rnd; 44 } 45 46 /** 47 * Basically just a wrapper for lazy people - identical to calling 48 * <code>requestPTY("dumb", 0, 0, 0, 0, null)</code>. 49 * 50 * @throws IOException 51 */ 52 public void requestDumbPTY() throws IOException 53 { 54 requestPTY("dumb", 0, 0, 0, 0, null); 55 } 56 57 /** 58 * Basically just another wrapper for lazy people - identical to calling 59 * <code>requestPTY(term, 0, 0, 0, 0, null)</code>. 60 * 61 * @throws IOException 62 */ 63 public void requestPTY(String term) throws IOException 64 { 65 requestPTY(term, 0, 0, 0, 0, null); 66 } 67 68 /** 69 * Allocate a pseudo-terminal for this session. 70 * <p/> 71 * This method may only be called before a program or shell is started in 72 * this session. 73 * <p/> 74 * Different aspects can be specified: 75 * <p/> 76 * <ul> 77 * <li>The TERM environment variable value (e.g., vt100)</li> 78 * <li>The terminal's dimensions.</li> 79 * <li>The encoded terminal modes.</li> 80 * </ul> 81 * Zero dimension parameters are ignored. The character/row dimensions 82 * override the pixel dimensions (when nonzero). Pixel dimensions refer to 83 * the drawable area of the window. The dimension parameters are only 84 * informational. The encoding of terminal modes (parameter 85 * <code>terminal_modes</code>) is described in RFC4254. 86 * 87 * @param term The TERM environment variable value (e.g., vt100) 88 * @param term_width_characters terminal width, characters (e.g., 80) 89 * @param term_height_characters terminal height, rows (e.g., 24) 90 * @param term_width_pixels terminal width, pixels (e.g., 640) 91 * @param term_height_pixels terminal height, pixels (e.g., 480) 92 * @param terminal_modes encoded terminal modes (may be <code>null</code>) 93 * @throws IOException 94 */ 95 public void requestPTY(String term, int term_width_characters, int term_height_characters, int term_width_pixels, 96 int term_height_pixels, byte[] terminal_modes) throws IOException 97 { 98 if (term == null) 99 throw new IllegalArgumentException("TERM cannot be null."); 100 101 if ((terminal_modes != null) && (terminal_modes.length > 0)) 102 { 103 if (terminal_modes[terminal_modes.length - 1] != 0) 104 throw new IOException("Illegal terminal modes description, does not end in zero byte"); 105 } 106 else 107 terminal_modes = new byte[]{0}; 108 109 synchronized (this) 110 { 111 /* The following is just a nicer error, we would catch it anyway later in the channel code */ 112 if (flag_closed) 113 throw new IOException("This session is closed."); 114 115 if (flag_pty_requested) 116 throw new IOException("A PTY was already requested."); 117 118 if (flag_execution_started) 119 throw new IOException( 120 "Cannot request PTY at this stage anymore, a remote execution has already started."); 121 122 flag_pty_requested = true; 123 } 124 125 cm.requestPTY(cn, term, term_width_characters, term_height_characters, term_width_pixels, term_height_pixels, 126 terminal_modes); 127 } 128 129 /** 130 * Request X11 forwarding for the current session. 131 * <p/> 132 * You have to supply the name and port of your X-server. 133 * <p/> 134 * This method may only be called before a program or shell is started in 135 * this session. 136 * 137 * @param hostname the hostname of the real (target) X11 server (e.g., 127.0.0.1) 138 * @param port the port of the real (target) X11 server (e.g., 6010) 139 * @param cookie if non-null, then present this cookie to the real X11 server 140 * @param singleConnection if true, then the server is instructed to only forward one single 141 * connection, no more connections shall be forwarded after first, or after the session 142 * channel has been closed 143 * @throws IOException 144 */ 145 public void requestX11Forwarding(String hostname, int port, byte[] cookie, boolean singleConnection) 146 throws IOException 147 { 148 if (hostname == null) 149 throw new IllegalArgumentException("hostname argument may not be null"); 150 151 synchronized (this) 152 { 153 /* The following is just a nicer error, we would catch it anyway later in the channel code */ 154 if (flag_closed) 155 throw new IOException("This session is closed."); 156 157 if (flag_x11_requested) 158 throw new IOException("X11 forwarding was already requested."); 159 160 if (flag_execution_started) 161 throw new IOException( 162 "Cannot request X11 forwarding at this stage anymore, a remote execution has already started."); 163 164 flag_x11_requested = true; 165 } 166 167 /* X11ServerData - used to store data about the target X11 server */ 168 169 X11ServerData x11data = new X11ServerData(); 170 171 x11data.hostname = hostname; 172 x11data.port = port; 173 x11data.x11_magic_cookie = cookie; /* if non-null, then present this cookie to the real X11 server */ 174 175 /* Generate fake cookie - this one is used between remote clients and the ganymed proxy */ 176 177 byte[] fakeCookie = new byte[16]; 178 String hexEncodedFakeCookie; 179 180 /* Make sure that this fake cookie is unique for this connection */ 181 182 while (true) 183 { 184 rnd.nextBytes(fakeCookie); 185 186 /* Generate also hex representation of fake cookie */ 187 188 StringBuilder tmp = new StringBuilder(32); 189 for (int i = 0; i < fakeCookie.length; i++) 190 { 191 String digit2 = Integer.toHexString(fakeCookie[i] & 0xff); 192 tmp.append((digit2.length() == 2) ? digit2 : "0" + digit2); 193 } 194 hexEncodedFakeCookie = tmp.toString(); 195 196 /* Well, yes, chances are low, but we want to be on the safe side */ 197 198 if (cm.checkX11Cookie(hexEncodedFakeCookie) == null) 199 break; 200 } 201 202 /* Ask for X11 forwarding */ 203 204 cm.requestX11(cn, singleConnection, "MIT-MAGIC-COOKIE-1", hexEncodedFakeCookie, 0); 205 206 /* OK, that went fine, get ready to accept X11 connections... */ 207 /* ... but only if the user has not called close() in the meantime =) */ 208 209 synchronized (this) 210 { 211 if (flag_closed == false) 212 { 213 this.x11FakeCookie = hexEncodedFakeCookie; 214 cm.registerX11Cookie(hexEncodedFakeCookie, x11data); 215 } 216 } 217 218 /* Now it is safe to start remote X11 programs */ 219 } 220 221 /** 222 * Execute a command on the remote machine. 223 * 224 * @param cmd The command to execute on the remote host. 225 * @throws IOException 226 */ 227 public void execCommand(String cmd) throws IOException 228 { 229 this.execCommand(cmd, null); 230 } 231 232 /** 233 * Execute a command on the remote machine. 234 * 235 * @param cmd The command to execute on the remote host. 236 * @param charsetName The charset used to convert between Java Unicode Strings and byte encodings 237 * @throws IOException 238 */ 239 public void execCommand(String cmd, String charsetName) throws IOException 240 { 241 if (cmd == null) 242 throw new IllegalArgumentException("cmd argument may not be null"); 243 244 synchronized (this) 245 { 246 /* The following is just a nicer error, we would catch it anyway later in the channel code */ 247 if (flag_closed) 248 throw new IOException("This session is closed."); 249 250 if (flag_execution_started) 251 throw new IOException("A remote execution has already started."); 252 253 flag_execution_started = true; 254 } 255 256 cm.requestExecCommand(cn, cmd, charsetName); 257 } 258 259 /** 260 * Start a shell on the remote machine. 261 * 262 * @throws IOException 263 */ 264 public void startShell() throws IOException 265 { 266 synchronized (this) 267 { 268 /* The following is just a nicer error, we would catch it anyway later in the channel code */ 269 if (flag_closed) 270 throw new IOException("This session is closed."); 271 272 if (flag_execution_started) 273 throw new IOException("A remote execution has already started."); 274 275 flag_execution_started = true; 276 } 277 278 cm.requestShell(cn); 279 } 280 281 /** 282 * Start a subsystem on the remote machine. 283 * Unless you know what you are doing, you will never need this. 284 * 285 * @param name the name of the subsystem. 286 * @throws IOException 287 */ 288 public void startSubSystem(String name) throws IOException 289 { 290 if (name == null) 291 throw new IllegalArgumentException("name argument may not be null"); 292 293 synchronized (this) 294 { 295 /* The following is just a nicer error, we would catch it anyway later in the channel code */ 296 if (flag_closed) 297 throw new IOException("This session is closed."); 298 299 if (flag_execution_started) 300 throw new IOException("A remote execution has already started."); 301 302 flag_execution_started = true; 303 } 304 305 cm.requestSubSystem(cn, name); 306 } 307 308 public int getState() 309 { 310 return cn.getState(); 311 } 312 313 public InputStream getStdout() 314 { 315 return cn.getStdoutStream(); 316 } 317 318 public InputStream getStderr() 319 { 320 return cn.getStderrStream(); 321 } 322 323 public OutputStream getStdin() 324 { 325 return cn.getStdinStream(); 326 } 327 328 /** 329 * This method blocks until there is more data available on either the 330 * stdout or stderr InputStream of this <code>Session</code>. Very useful 331 * if you do not want to use two parallel threads for reading from the two 332 * InputStreams. One can also specify a timeout. NOTE: do NOT call this 333 * method if you use concurrent threads that operate on either of the two 334 * InputStreams of this <code>Session</code> (otherwise this method may 335 * block, even though more data is available). 336 * 337 * @param timeout The (non-negative) timeout in <code>ms</code>. <code>0</code> means no 338 * timeout, the call may block forever. 339 * @return <ul> 340 * <li><code>0</code> if no more data will arrive.</li> 341 * <li><code>1</code> if more data is available.</li> 342 * <li><code>-1</code> if a timeout occurred.</li> 343 * </ul> 344 * @throws IOException 345 * @deprecated This method has been replaced with a much more powerful wait-for-condition 346 * interface and therefore acts only as a wrapper. 347 */ 348 public int waitUntilDataAvailable(long timeout) throws IOException 349 { 350 if (timeout < 0) 351 throw new IllegalArgumentException("timeout must not be negative!"); 352 353 int conditions = cm.waitForCondition(cn, timeout, ChannelCondition.STDOUT_DATA | ChannelCondition.STDERR_DATA 354 | ChannelCondition.EOF); 355 356 if ((conditions & ChannelCondition.TIMEOUT) != 0) 357 return -1; 358 359 if ((conditions & (ChannelCondition.STDOUT_DATA | ChannelCondition.STDERR_DATA)) != 0) 360 return 1; 361 362 /* Here we do not need to check separately for CLOSED, since CLOSED implies EOF */ 363 364 if ((conditions & ChannelCondition.EOF) != 0) 365 return 0; 366 367 throw new IllegalStateException("Unexpected condition result (" + conditions + ")"); 368 } 369 370 /** 371 * This method blocks until certain conditions hold true on the underlying SSH-2 channel. 372 * <p/> 373 * This method returns as soon as one of the following happens: 374 * <ul> 375 * <li>at least of the specified conditions (see {@link ChannelCondition}) holds true</li> 376 * <li>timeout > 0 and a timeout occured (TIMEOUT will be set in result conditions)</a> 377 * <li>the underlying channel was closed (CLOSED will be set in result conditions)</a> 378 * </ul> 379 * <p/> 380 * In any case, the result value contains ALL current conditions, which may be more 381 * than the specified condition set (i.e., never use the "==" operator to test for conditions 382 * in the bitmask, see also comments in {@link ChannelCondition}). 383 * <p/> 384 * Note: do NOT call this method if you want to wait for STDOUT_DATA or STDERR_DATA and 385 * there are concurrent threads (e.g., StreamGobblers) that operate on either of the two 386 * InputStreams of this <code>Session</code> (otherwise this method may 387 * block, even though more data is available in the StreamGobblers). 388 * 389 * @param condition_set a bitmask based on {@link ChannelCondition} values 390 * @param timeout non-negative timeout in ms, <code>0</code> means no timeout 391 * @return all bitmask specifying all current conditions that are true 392 */ 393 394 public int waitForCondition(int condition_set, long timeout) 395 { 396 if (timeout < 0) 397 throw new IllegalArgumentException("timeout must be non-negative!"); 398 399 return cm.waitForCondition(cn, timeout, condition_set); 400 } 401 402 /** 403 * Get the exit code/status from the remote command - if available. Be 404 * careful - not all server implementations return this value. It is 405 * generally a good idea to call this method only when all data from the 406 * remote side has been consumed (see also the <code<WaitForCondition</code> method). 407 * 408 * @return An <code>Integer</code> holding the exit code, or 409 * <code>null</code> if no exit code is (yet) available. 410 */ 411 public Integer getExitStatus() 412 { 413 return cn.getExitStatus(); 414 } 415 416 /** 417 * Get the name of the signal by which the process on the remote side was 418 * stopped - if available and applicable. Be careful - not all server 419 * implementations return this value. 420 * 421 * @return An <code>String</code> holding the name of the signal, or 422 * <code>null</code> if the process exited normally or is still 423 * running (or if the server forgot to send this information). 424 */ 425 public String getExitSignal() 426 { 427 return cn.getExitSignal(); 428 } 429 430 /** 431 * Close this session. NEVER forget to call this method to free up resources - 432 * even if you got an exception from one of the other methods (or when 433 * getting an Exception on the Input- or OutputStreams). Sometimes these other 434 * methods may throw an exception, saying that the underlying channel is 435 * closed (this can happen, e.g., if the other server sent a close message.) 436 * However, as long as you have not called the <code>close()</code> 437 * method, you may be wasting (local) resources. 438 */ 439 public void close() 440 { 441 synchronized (this) 442 { 443 if (flag_closed) 444 return; 445 446 flag_closed = true; 447 448 if (x11FakeCookie != null) 449 cm.unRegisterX11Cookie(x11FakeCookie, true); 450 451 try 452 { 453 cm.closeChannel(cn, "Closed due to user request", true); 454 } 455 catch (IOException ignored) 456 { 457 } 458 } 459 } 460 } 461