View Javadoc
1   /*
2    * To change this template, choose Tools | Templates
3    * and open the template in the editor.
4    */
5   
6   package org.metricshub.ssh;
7   
8   /*-
9    * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲
10   * SSH Java Client
11   * ჻჻჻჻჻჻
12   * Copyright (C) 2023 Metricshub
13   * ჻჻჻჻჻჻
14   * Licensed under the Apache License, Version 2.0 (the "License");
15   * you may not use this file except in compliance with the License.
16   * You may obtain a copy of the License at
17   *
18   *      http://www.apache.org/licenses/LICENSE-2.0
19   *
20   * Unless required by applicable law or agreed to in writing, software
21   * distributed under the License is distributed on an "AS IS" BASIS,
22   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
23   * See the License for the specific language governing permissions and
24   * limitations under the License.
25   * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱
26   */
27  
28  import com.trilead.ssh2.ChannelCondition;
29  import com.trilead.ssh2.Connection;
30  import com.trilead.ssh2.InteractiveCallback;
31  import com.trilead.ssh2.SCPClient;
32  import com.trilead.ssh2.SFTPv3Client;
33  import com.trilead.ssh2.SFTPv3DirectoryEntry;
34  import com.trilead.ssh2.SFTPv3FileAttributes;
35  import com.trilead.ssh2.SFTPv3FileHandle;
36  import com.trilead.ssh2.Session;
37  import java.io.BufferedReader;
38  import java.io.ByteArrayOutputStream;
39  import java.io.File;
40  import java.io.IOException;
41  import java.io.InputStream;
42  import java.io.InputStreamReader;
43  import java.io.OutputStream;
44  import java.nio.charset.Charset;
45  import java.util.Arrays;
46  import java.util.Optional;
47  import java.util.Vector;
48  import java.util.regex.Matcher;
49  import java.util.regex.Pattern;
50  
51  /**
52   * SSH Client that lets you perform basic SSH operations
53   * <ol>
54   * <li>Instantiate the SSH Client
55   * <li>Connect
56   * <li>Authenticate
57   * <li>Enjoy!
58   * <li><b>DISCONNECT!</b>
59   * </ol>
60   *
61   * @author Bertrand
62   */
63  public class SshClient implements AutoCloseable {
64  
65  	private static final Pattern DEFAULT_MASK_PATTERN = Pattern.compile(".*");
66  
67  	private static final int READ_BUFFER_SIZE = 8192;
68  
69  	private String hostname;
70  	private Connection sshConnection = null;
71  
72  	private Charset charset = null;
73  
74  	/**
75  	 * Session object that needs to be closed when we disconnect
76  	 */
77  	private Session sshSession = null;
78  
79  	/**
80  	 * Creates an SSHClient to connect to the specified hostname
81  	 *
82  	 * @param pHostname Hostname of the SSH server to connect to
83  	 */
84  	public SshClient(String pHostname) {
85  		this(pHostname, "");
86  	}
87  
88  	/**
89  	 * Creates an SSHClient to connect to the specified hostname
90  	 *
91  	 * @param pHostname Hostname of the SSH server to connect to
92  	 * @param pLocale Locale used on the remote server (e.g. zh_CN.utf8)
93  	 */
94  	public SshClient(String pHostname, String pLocale) {
95  		this(pHostname, Utils.getCharsetFromLocale(pLocale));
96  	}
97  
98  	/**
99  	 * Creates an SSHClient to connect to the specified hostname
100 	 *
101 	 * @param pHostname Hostname of the SSH server to connect to
102 	 * @param pCharset Charset used on the remote server
103 	 */
104 	public SshClient(String pHostname, Charset pCharset) {
105 		hostname = pHostname;
106 		charset = pCharset;
107 	}
108 
109 	/**
110 	 * Connects the SSH Client to the SSH server
111 	 *
112 	 * @throws IOException
113 	 */
114 	public void connect() throws IOException {
115 		connect(0);
116 	}
117 
118 	/**
119 	 * Connects the SSH client to the SSH server using the default SSH port (22).
120 	 *
121 	 * @param timeout Timeout in milliseconds
122 	 * @throws IOException when connection fails or when the server does not respond (SocketTimeoutException)
123 	 */
124 	public void connect(final int timeout) throws IOException {
125 		this.connect(timeout, 22);
126 	}
127 
128 	/**
129 	 *  Connects the SSH client to the SSH server using a specified SSH port.
130 	 *
131 	 * @param timeout Timeout in milliseconds
132 	 * @param port SSH server port.
133 	 * @throws IOException when connection fails or when the server does not respond (SocketTimeoutException)
134 	 */
135 	public void connect(final int timeout, final int port) throws IOException {
136 		sshConnection = new Connection(hostname, port);
137 		sshConnection.connect(null, timeout, timeout);
138 	}
139 
140 	/**
141 	 * Disconnects the SSH Client from the SSH server
142 	 * <p>
143 	 * Note: <b>This is important!</b> Otherwise, the listener thread will
144 	 * remain running forever!
145 	 * Use a try with resource instead or the {@link close} instead.
146 	 * @deprecated (since = "3.14.00", forRemoval = true)
147 	 */
148 	@Deprecated
149 	public void disconnect() {
150 		if (sshSession != null) {
151 			sshSession.close();
152 		}
153 		if (sshConnection != null) {
154 			sshConnection.close();
155 		}
156 	}
157 
158 	/**
159 	 * Disconnects the SSH Client from the SSH server
160 	 * <p>
161 	 * Note: <b>This is important!</b> Otherwise, the listener thread will
162 	 * remain running forever!
163 	 */
164 	@Override
165 	public void close() {
166 		if (sshSession != null) {
167 			sshSession.close();
168 		}
169 		if (sshConnection != null) {
170 			sshConnection.close();
171 		}
172 	}
173 
174 	/**
175 	 * Authenticate the SSH Client against the SSH server using a private key
176 	 *
177 	 * @deprecated (since = "3.14.00", forRemoval = true)
178 	 * @param username
179 	 * @param privateKeyFile
180 	 * @param password
181 	 * @return a boolean stating whether the authentication worked or not
182 	 * @throws IOException
183 	 */
184 	@Deprecated
185 	public boolean authenticate(String username, String privateKeyFile, String password) throws IOException {
186 		return authenticate(
187 			username,
188 			privateKeyFile != null ? new File(privateKeyFile) : null,
189 			password != null ? password.toCharArray() : null
190 		);
191 	}
192 
193 	/**
194 	 * Authenticate the SSH Client against the SSH server using a private key
195 	 *
196 	 * @param username
197 	 * @param privateKeyFile
198 	 * @param password
199 	 * @return a boolean stating whether the authentication worked or not
200 	 * @throws IOException
201 	 */
202 	public boolean authenticate(String username, File privateKeyFile, char[] password) throws IOException {
203 		if (sshConnection.isAuthMethodAvailable(username, "publickey")) {
204 			return sshConnection.authenticateWithPublicKey(
205 				username,
206 				privateKeyFile,
207 				password != null ? String.valueOf(password) : null
208 			);
209 		}
210 
211 		return false;
212 	}
213 
214 	/**
215 	 * Authenticate the SSH Client against the SSH server using a password
216 	 *
217 	 * @deprecated (since = "3.14.00", forRemoval = true)
218 	 * @param username
219 	 * @param password
220 	 * @return a boolean stating whether the authentication worked or not
221 	 * @throws IOException
222 	 */
223 	@Deprecated
224 	public boolean authenticate(String username, String password) throws IOException {
225 		return authenticate(username, password != null ? password.toCharArray() : null);
226 	}
227 
228 	/**
229 	 * Authenticate the SSH Client against the SSH server using a password
230 	 *
231 	 * @param username
232 	 * @param password
233 	 * @return a boolean stating whether the authentication worked or not
234 	 * @throws IOException
235 	 */
236 	public boolean authenticate(String username, char[] password) throws IOException {
237 		// Is the "password" method available? If yes, try it first
238 		// Using normal login & password
239 		if (
240 			sshConnection.isAuthMethodAvailable(username, "password") &&
241 			sshConnection.authenticateWithPassword(username, password != null ? String.valueOf(password) : null)
242 		) {
243 			return true;
244 		}
245 
246 		// Now, is the "keyboard-interactive" method available?
247 		if (sshConnection.isAuthMethodAvailable(username, "keyboard-interactive")) {
248 			return sshConnection.authenticateWithKeyboardInteractive(
249 				username,
250 				new InteractiveCallback() {
251 					@Override
252 					public String[] replyToChallenge(
253 						String name,
254 						String instruction,
255 						int numPrompts,
256 						String[] prompt,
257 						boolean[] echo
258 					) throws Exception {
259 						// Prepare responses to the challenges
260 						String[] challengeResponse = new String[numPrompts];
261 						for (int i = 0; i < numPrompts; i++) {
262 							// If we're told the input can be displayed (echoed),
263 							// we'll assume this is not a password
264 							// that we're being asked for, hence the username.
265 							// Otherwise, we'll send the password
266 							if (echo[i]) {
267 								challengeResponse[i] = username;
268 							} else {
269 								challengeResponse[i] = password != null ? String.valueOf(password) : null;
270 							}
271 						}
272 						return challengeResponse;
273 					}
274 				}
275 			);
276 		}
277 
278 		// If none of the above methods are available, just quit
279 		return false;
280 	}
281 
282 	/**
283 	 * Authenticate the SSH Client against the SSH server using NO password
284 	 *
285 	 * @param username
286 	 * @return a boolean stating whether the authentication worked or not
287 	 * @throws IOException
288 	 */
289 	public boolean authenticate(String username) throws IOException {
290 		return sshConnection.authenticateWithNone(username);
291 	}
292 
293 	/**
294 	 * Return information about the specified file on the connected system, in
295 	 * the same format as the PSL function <code>file()</code>
296 	 *
297 	 * @param filePath
298 	 *            Path to the file on the remote system
299 	 * @throws IOException
300 	 */
301 	public String readFileAttributes(String filePath) throws IOException {
302 		// Sanity check
303 		checkIfAuthenticated();
304 
305 		// Create the SFTP client
306 		SFTPv3Client sftpClient = new SFTPv3Client(sshConnection);
307 
308 		// Read the file attributes
309 		SFTPv3FileAttributes fileAttributes = sftpClient.stat(filePath);
310 
311 		// Determine the file type
312 		String fileType;
313 		if (fileAttributes.isRegularFile()) {
314 			fileType = "FILE";
315 		} else if (fileAttributes.isDirectory()) {
316 			fileType = "DIR";
317 		} else if (fileAttributes.isSymlink()) {
318 			fileType = "LINK";
319 		} else {
320 			fileType = "UNKNOWN";
321 		}
322 
323 		// Build the result in the same format as the PSL function file()
324 		StringBuilder pslFileResult = new StringBuilder();
325 		pslFileResult
326 			.append(fileAttributes.mtime.toString())
327 			.append("\t")
328 			.append(fileAttributes.atime.toString())
329 			.append("\t-\t")
330 			.append(Integer.toString(fileAttributes.permissions & 0000777, 8))
331 			.append("\t")
332 			.append(fileAttributes.size.toString())
333 			.append("\t-\t")
334 			.append(fileType)
335 			.append("\t")
336 			.append(fileAttributes.uid.toString())
337 			.append("\t")
338 			.append(fileAttributes.gid.toString())
339 			.append("\t")
340 			.append(sftpClient.canonicalPath(filePath));
341 
342 		// Deallocate
343 		sftpClient.close();
344 
345 		// Return
346 		return pslFileResult.toString();
347 	}
348 
349 	private StringBuilder listSubDirectory(
350 		SFTPv3Client sftpClient,
351 		String remoteDirectoryPath,
352 		Pattern fileMaskPattern,
353 		boolean includeSubfolders,
354 		Integer depth,
355 		StringBuilder resultBuilder
356 	) throws IOException {
357 		if (depth <= 15) {
358 			@SuppressWarnings("unchecked")
359 			Vector<SFTPv3DirectoryEntry> pathContents = sftpClient.ls(remoteDirectoryPath);
360 
361 			// Fix the remoteDirectoryPath (without the last '/')
362 			if (remoteDirectoryPath.endsWith("/")) {
363 				remoteDirectoryPath = remoteDirectoryPath.substring(0, remoteDirectoryPath.lastIndexOf("/"));
364 			}
365 
366 			depth++;
367 			for (SFTPv3DirectoryEntry file : pathContents) {
368 				String filename = file.filename.trim();
369 
370 				if (filename.equals(".") || filename.equals("..")) {
371 					continue;
372 				}
373 
374 				SFTPv3FileAttributes fileAttributes = file.attributes;
375 				String filePath = remoteDirectoryPath + "/" + filename;
376 
377 				if ((fileAttributes.permissions & 0120000) == 0120000) {
378 					// Symbolic link
379 					continue;
380 				}
381 
382 				// CHECKSTYLE:OFF
383 				if (
384 					((fileAttributes.permissions & 0100000) == 0100000) ||
385 					((fileAttributes.permissions & 0060000) == 0060000) ||
386 					((fileAttributes.permissions & 0020000) == 0020000) ||
387 					((fileAttributes.permissions & 0140000) == 0140000)
388 				) {
389 					// Regular/Block/Character/Socket files
390 					final Matcher m = fileMaskPattern.matcher(filename);
391 					if (m.find()) {
392 						resultBuilder
393 							.append(filePath)
394 							.append(";")
395 							.append(fileAttributes.mtime.toString())
396 							.append(";")
397 							.append(fileAttributes.size.toString())
398 							.append("\n");
399 					}
400 					continue;
401 				}
402 				// CHECKSTYLE:ON
403 
404 				if ((fileAttributes.permissions & 0040000) == 0040000) {
405 					// Directory
406 					if (includeSubfolders) {
407 						resultBuilder =
408 							listSubDirectory(sftpClient, filePath, fileMaskPattern, includeSubfolders, depth, resultBuilder);
409 					}
410 				}
411 			}
412 		}
413 
414 		return resultBuilder;
415 	}
416 
417 	/**
418 	 * List the content of the specified directory through the SSH connection
419 	 * (using SCP)
420 	 *
421 	 * @param remoteDirectoryPath The path to the directory to list on the remote host
422 	 * @param regExpFileMask A regular expression that listed files must match with to be listed
423 	 * @param includeSubfolders Whether to parse subdirectories as well
424 	 * @return The list of files in the specified directory, separated by end-of-lines
425 	 *
426 	 * @throws IOException When something bad happens while communicating with the remote host
427 	 * @throws IllegalStateException If called while not yet connected
428 	 */
429 	public String listFiles(String remoteDirectoryPath, String regExpFileMask, boolean includeSubfolders)
430 		throws IOException {
431 		checkIfAuthenticated();
432 
433 		// Create an SFTP Client
434 		SFTPv3Client sftpClient = new SFTPv3Client(sshConnection);
435 
436 		// Prepare the Pattern for fileMask
437 		Pattern fileMaskPattern;
438 		if (regExpFileMask != null && !regExpFileMask.isEmpty()) {
439 			fileMaskPattern = Pattern.compile(regExpFileMask, Pattern.CASE_INSENSITIVE);
440 		} else {
441 			fileMaskPattern = DEFAULT_MASK_PATTERN;
442 		}
443 
444 		// Read the directory listing
445 		StringBuilder resultBuilder = new StringBuilder();
446 		listSubDirectory(sftpClient, remoteDirectoryPath, fileMaskPattern, includeSubfolders, 1, resultBuilder);
447 
448 		// Close the SFTP client
449 		sftpClient.close();
450 
451 		// Update the response
452 		return resultBuilder.toString();
453 	}
454 
455 	/**
456 	 * Read the specified file over the SSH session that was established.
457 	 *
458 	 * @param remoteFilePath
459 	 *            Path to the file to be read on the remote host
460 	 * @param readOffset
461 	 *            Offset to read from
462 	 * @param readSize
463 	 *            Amount of bytes to be read
464 	 * @return The content of the file read
465 	 *
466 	 * @throws IOException
467 	 *             when something gets wrong while reading the file (we get
468 	 *             disconnected, for example, or we couldn't read the file)
469 	 * @throws IllegalStateException
470 	 *             when the session hasn't been properly authenticated first
471 	 */
472 	public String readFile(String remoteFilePath, Long readOffset, Integer readSize) throws IOException {
473 		checkIfAuthenticated();
474 
475 		// Create an SFTP Client
476 		SFTPv3Client sftpClient = new SFTPv3Client(sshConnection);
477 
478 		// Where do we read from (offset)?
479 		long offset = 0; // from the beginning by default
480 		if (readOffset != null) {
481 			offset = readOffset; // use the set offset
482 		}
483 
484 		// Open the remote file
485 		SFTPv3FileHandle handle = sftpClient.openFileRO(remoteFilePath);
486 
487 		// How much data to read?
488 		int remainingBytes;
489 		if (readSize == null) {
490 			// If size was not specified, we read the file entirely
491 			SFTPv3FileAttributes attributes = sftpClient.fstat(handle);
492 			if (attributes == null) {
493 				throw new IOException("Couldn't find file " + remoteFilePath + " and get its attributes");
494 			}
495 			remainingBytes = (int) (attributes.size - offset);
496 			if (remainingBytes < 0) {
497 				remainingBytes = 0;
498 			}
499 		} else {
500 			remainingBytes = readSize;
501 		}
502 
503 		// Read the remote file
504 		OutputStream out = new ByteArrayOutputStream();
505 		byte[] readBuffer = new byte[READ_BUFFER_SIZE];
506 		int bytesRead;
507 		int bufferSize;
508 
509 		// Loop until there is nothing else to read
510 		while (remainingBytes > 0) {
511 			// Read by chunk of 8192 bytes. However, if there is less to read,
512 			// well, read less.
513 			if (remainingBytes < READ_BUFFER_SIZE) {
514 				bufferSize = remainingBytes;
515 			} else {
516 				bufferSize = READ_BUFFER_SIZE;
517 			}
518 
519 			// Read and store that in our buffer
520 			bytesRead = sftpClient.read(handle, offset, readBuffer, 0, bufferSize);
521 
522 			// If we already reached the end of the file, exit (probably, we
523 			// were asked to read more than what is available)
524 			if (bytesRead < 0) {
525 				break;
526 			}
527 
528 			// Write our buffer to the result stream
529 			out.write(readBuffer, 0, bytesRead);
530 
531 			// Keep counting!
532 			remainingBytes -= bytesRead;
533 			offset += bytesRead;
534 		}
535 
536 		// File read complete
537 		// Close the remote file
538 		sftpClient.closeFile(handle);
539 
540 		// Close the SFTP client
541 		sftpClient.close();
542 
543 		// Metricshub Collection format
544 		return out.toString();
545 	}
546 
547 	/**
548 	 * Removes a list of files on the remote system.
549 	 *
550 	 * @param remoteFilePathArray Array of paths to the files to be deleted on the remote host
551 	 * @throws IOException when something bad happens while deleting the file
552 	 * @throws IllegalStateException when not connected and authenticated yet
553 	 */
554 	public void removeFile(String[] remoteFilePathArray) throws IOException {
555 		checkIfAuthenticated();
556 
557 		// Create an SFTP Client
558 		SFTPv3Client sftpClient = null;
559 		try {
560 			sftpClient = new SFTPv3Client(sshConnection);
561 
562 			// Remove the files
563 			for (String remoteFilePath : remoteFilePathArray) {
564 				sftpClient.rm(remoteFilePath);
565 			}
566 		} catch (IOException e) {
567 			// Okay, we got an issue here with the SFTP client
568 			// We're going to try again but with a good old "rm" command...
569 
570 			Session rmSession = null;
571 			try {
572 				for (String remoteFilePath : remoteFilePathArray) {
573 					rmSession = sshConnection.openSession();
574 					rmSession.execCommand("/usr/bin/rm -f \"" + remoteFilePath + "\"");
575 					rmSession.waitForCondition(ChannelCondition.CLOSED | ChannelCondition.EOF, 5000);
576 				}
577 			} catch (IOException e1) {
578 				throw e1;
579 			} finally {
580 				if (rmSession != null) {
581 					rmSession.close();
582 				}
583 			}
584 		} finally {
585 			// Close the SFTP client
586 			if (sftpClient != null) {
587 				sftpClient.close();
588 			}
589 		}
590 	}
591 
592 	/**
593 	 * Removes the specified file on the remote system.
594 	 *
595 	 * @param remoteFilePath
596 	 * @throws IOException
597 	 * @throws IllegalStateException
598 	 */
599 	public void removeFile(String remoteFilePath) throws IOException {
600 		removeFile(new String[] { remoteFilePath });
601 	}
602 
603 	/**
604 	 * Represents the result of a command execution
605 	 *
606 	 * @author bertrand
607 	 *
608 	 */
609 	public static class CommandResult {
610 
611 		/**
612 		 * Whether the command was successful or not
613 		 */
614 		public boolean success = true;
615 
616 		/**
617 		 * How much time was taken by the execution itself (not counting the
618 		 * connection time), in seconds
619 		 */
620 		public float executionTime = 0;
621 
622 		/**
623 		 * The exit code (status) returned by the command (process return code).
624 		 * <code>null</code> if unsupported by the remote platform.
625 		 */
626 		public Integer exitStatus = null;
627 
628 		/**
629 		 * The result of the command (stdout and stderr is merged into result)
630 		 */
631 		public String result = "";
632 	}
633 
634 	/**
635 	 * Executes a command through the SSH connection
636 	 *
637 	 * @param command	The command to be executed
638 	 * @return a CommandResult object with the result of the execution
639 	 * @throws IllegalStateException when the connection is not established first
640 	 * @throws IOException when there is a problem while communicating with the remote system
641 	 */
642 	public CommandResult executeCommand(String command) throws IOException {
643 		return executeCommand(command, 0);
644 	}
645 
646 	/**
647 	 * Executes a command through the SSH connection
648 	 *
649 	 * @param command	The command to be executed
650 	 * @param timeout	Milliseconds after which the command is considered failed
651 	 * @return a CommandResult object with the result of the execution
652 	 * @throws IllegalStateException when the connection is not established first
653 	 * @throws IOException when there is a problem while communicating with the remote system
654 	 */
655 	public CommandResult executeCommand(String command, int timeout) throws IOException {
656 		openSession();
657 
658 		InputStream stdout = sshSession.getStdout();
659 		InputStream stderr = sshSession.getStderr();
660 		// DO NOT request for a PTY, as Trilead SSH's execCommand() would get stuck on AIX...
661 		//sshSession.requestPTY("dumb", 10000, 24, 640, 480, new byte[] {53, 0, 0, 0, 0, 0}); // request for a wiiiiide terminal
662 
663 		// Initialization
664 		CommandResult commandResult = new CommandResult();
665 
666 		// Output to a byte stream
667 		try (ByteArrayOutputStream output = new ByteArrayOutputStream()) {
668 			// Time to be remembered
669 			long startTime = System.currentTimeMillis();
670 			long timeoutTime;
671 			if (timeout > 0) {
672 				timeoutTime = startTime + timeout;
673 			} else {
674 				// If no timeout, we use the max long value for the time when we're supposed to stop
675 				timeoutTime = Long.MAX_VALUE;
676 			}
677 
678 			// Run the command
679 			sshSession.execCommand(command);
680 
681 			int waitForCondition = 0;
682 			long currentTime;
683 			// CHECKSTYLE:OFF
684 			while (
685 				!hasSessionClosed(waitForCondition) &&
686 				!hasEndOfFileSession(waitForCondition) &&
687 				((currentTime = System.currentTimeMillis()) < timeoutTime)
688 			) {
689 				// Wait for new data (timeout = 5 seconds)
690 				waitForCondition = waitForNewData(Math.min(timeoutTime - currentTime, 5000));
691 
692 				// Print available data (if any)
693 				if (hasStdoutData(waitForCondition)) {
694 					transferAllBytes(stdout, output);
695 				}
696 
697 				if (hasStderrData(waitForCondition)) {
698 					transferAllBytes(stderr, output);
699 				}
700 			}
701 			// CHECKSTYLE:ON
702 
703 			// What time is it?
704 			currentTime = System.currentTimeMillis();
705 			if (currentTime >= timeoutTime) {
706 				// If we exceeded the timeout, we're not successful
707 
708 				// Build the "timed out" result
709 				commandResult.success = false;
710 				commandResult.result = "Timeout (" + timeout / 1000 + " seconds)";
711 			} else {
712 				// We completed in time
713 
714 				// Execution time (in seconds)
715 				commandResult.executionTime = (currentTime - startTime) / 1000;
716 
717 				// Read exit status, when available
718 				waitForCondition = sshSession.waitForCondition(ChannelCondition.EXIT_STATUS, 5000);
719 				if ((waitForCondition & ChannelCondition.EXIT_STATUS) != 0) {
720 					commandResult.exitStatus = sshSession.getExitStatus();
721 				}
722 
723 				// Stringify the stdout stream
724 				commandResult.result = new String(output.toByteArray(), charset);
725 			}
726 		}
727 
728 		// Return
729 		return commandResult;
730 	}
731 
732 	/**
733 	 * Starts an interactive session.
734 	 *
735 	 * @param in Where the input is coming from (typically System.in)
736 	 * @param out Where the output has to go (e.g. System.out)
737 	 * @throws IllegalStateException when not connected and authenticated
738 	 * @throws IOException in case of communication problems with the host
739 	 * @throws InterruptedException when a thread is interrupted
740 	 */
741 	public void interactiveSession(InputStream in, OutputStream out) throws IOException, InterruptedException {
742 		openSession();
743 
744 		openTerminal();
745 
746 		// Pipe specified InputStream to SSH's stdin -- use a separate thread
747 		BufferedReader inputReader = new BufferedReader(new InputStreamReader(in));
748 		OutputStream outputWriter = sshSession.getStdin();
749 		Thread stdinPipeThread = new Thread() {
750 			@Override
751 			public void run() {
752 				try {
753 					String line;
754 					while ((line = inputReader.readLine()) != null) {
755 						outputWriter.write(line.getBytes());
756 						outputWriter.write('\n');
757 					}
758 				} catch (Exception e) {
759 					// Things ended up badly. Exit thread.
760 				}
761 				// End of the input stream. We need to exit.
762 				// Let's close the session so the main thread exits nicely.
763 				sshSession.close();
764 			}
765 		};
766 		stdinPipeThread.setDaemon(true);
767 		stdinPipeThread.start();
768 
769 		// Now, pipe stdout and stderr to specified OutputStream
770 		InputStream stdout = sshSession.getStdout();
771 		InputStream stderr = sshSession.getStderr();
772 
773 		int waitForCondition = 0;
774 		while (!hasSessionClosed(waitForCondition) && !hasEndOfFileSession(waitForCondition)) {
775 			// Wait for new data (timeout = 5 seconds)
776 			waitForCondition = waitForNewData(5000L);
777 
778 			// Print available data (if any)
779 			if (hasStdoutData(waitForCondition)) {
780 				transferAllBytes(stdout, out);
781 			}
782 
783 			if (hasStderrData(waitForCondition)) {
784 				transferAllBytes(stderr, out);
785 			}
786 		}
787 
788 		// Attempt to interrupt the stdinPipeThread thread
789 		// (may be useless if we're reading a blocking InputStream like System.in)
790 		if (stdinPipeThread.isAlive()) {
791 			stdinPipeThread.interrupt();
792 		}
793 	}
794 
795 	/**
796 	 * Copy a file to the remote host through SCP
797 	 *
798 	 * @param localFilePath
799 	 * @param remoteFilename
800 	 * @param remoteDirectory
801 	 * @param fileMode
802 	 * @throws IOException
803 	 */
804 	public void scp(String localFilePath, String remoteFilename, String remoteDirectory, String fileMode)
805 		throws IOException {
806 		checkIfAuthenticated();
807 
808 		// Create the SCP client
809 		SCPClient scpClient = new SCPClient(sshConnection);
810 
811 		// Copy the file
812 		scpClient.put(localFilePath, remoteFilename, remoteDirectory, fileMode);
813 	}
814 
815 	/**
816 	 * Open a SSH Session.
817 	 *
818 	 * @throws IOException When an I/O error occurred.
819 	 */
820 	public void openSession() throws IOException {
821 		checkIfConnected();
822 		checkIfAuthenticated();
823 
824 		// Open a shell session
825 		sshSession = getSshConnection().openSession();
826 	}
827 
828 	/**
829 	 * Open a Terminal
830 	 * request for a wiiiiide terminal, with no ECHO (see https://tools.ietf.org/html/rfc4254#section-8)
831 	 *
832 	 * @throws IOException When an I/O error occurred.
833 	 */
834 	public void openTerminal() throws IOException {
835 		checkIfConnected();
836 		checkIfAuthenticated();
837 		checkIfSessionOpened();
838 
839 		getSshSession().requestPTY("dumb", 10000, 24, 640, 480, new byte[] { 53, 0, 0, 0, 0, 0 });
840 		getSshSession().startShell();
841 	}
842 
843 	/**
844 	 * Write into the SSH session.
845 	 *
846 	 * @param text The text to be written.
847 	 * @throws IOException When an I/O error occurred.
848 	 */
849 	public void write(final String text) throws IOException {
850 		if (text == null || text.isEmpty()) {
851 			return;
852 		}
853 
854 		checkIfConnected();
855 		checkIfAuthenticated();
856 		checkIfSessionOpened();
857 
858 		Utils.checkNonNullField(charset, "charset");
859 
860 		final OutputStream outputStream = getSshSession().getStdin();
861 		Utils.checkNonNullField(outputStream, "Stdin");
862 
863 		// Replace "\n" string with write of '\n' character.
864 		final String[] split = text.split("\\R", -1);
865 		if (split.length == 1) {
866 			outputStream.write(text.getBytes(charset));
867 		} else {
868 			for (int i = 0; i < split.length; i++) {
869 				if (split[i].length() != 0) {
870 					outputStream.write(split[i].getBytes(charset));
871 				}
872 
873 				if (i < split.length - 1) {
874 					outputStream.write('\n');
875 				}
876 			}
877 		}
878 
879 		outputStream.flush();
880 	}
881 
882 	/**
883 	 * Read the stdout and stderr from the SSH session.
884 	 *
885 	 * @param size The buffer size of stdout and/or stderr to be read. (If less than 0 all data)
886 	 * @param timeout Timeout in seconds
887 	 *
888 	 * @return An optional with all the data read (stdout and stderr). Empty if nothing was read.
889 	 * @throws IOException When an I/O error occurred.
890 	 */
891 	public Optional<String> read(final int size, final int timeout) throws IOException {
892 		Utils.checkArgumentNotZeroOrNegative(timeout, "timeout");
893 
894 		checkIfConnected();
895 		checkIfAuthenticated();
896 		checkIfSessionOpened();
897 
898 		Utils.checkNonNullField(charset, "charset");
899 
900 		final InputStream stdout = getSshSession().getStdout();
901 		final InputStream stderr = getSshSession().getStderr();
902 		Utils.checkNonNullField(stdout, "stdout");
903 		Utils.checkNonNullField(stderr, "stderr");
904 
905 		try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
906 			// Wait for new data
907 			final int waitForCondition = waitForNewData(timeout * 1000L);
908 
909 			final boolean stdoutData = hasStdoutData(waitForCondition);
910 			final boolean stderrData = hasStderrData(waitForCondition);
911 
912 			// read stdout
913 			int stdoutRead = 0;
914 			if (stdoutData) {
915 				stdoutRead = transferBytes(stdout, byteArrayOutputStream, size);
916 				if (size > 0 && stdoutRead >= size) {
917 					return Optional.of(new String(byteArrayOutputStream.toByteArray(), charset));
918 				}
919 			}
920 
921 			// If still bytes to read or no stdout, read stderr
922 			if (stderrData) {
923 				transferBytes(stderr, byteArrayOutputStream, size - stdoutRead);
924 			}
925 
926 			return stdoutData || stderrData
927 				? Optional.of(new String(byteArrayOutputStream.toByteArray(), charset))
928 				: Optional.empty();
929 		}
930 	}
931 
932 	/**
933 	 * Check if the waitForCondition bit mask contains the timeout condition.
934 	 *
935 	 * @param waitForCondition The waitForCondition bit mask
936 	 * @return true if the bit mask contains the condition, false otherwise
937 	 */
938 	static boolean hasTimeoutSession(final int waitForCondition) {
939 		return (waitForCondition & ChannelCondition.TIMEOUT) != 0;
940 	}
941 
942 	/**
943 	 * Check if the waitForCondition bit mask contains the end of file condition.
944 	 *
945 	 * @param waitForCondition The waitForCondition bit mask
946 	 * @return true if the bit mask contains the condition, false otherwise
947 	 */
948 	static boolean hasEndOfFileSession(final int waitForCondition) {
949 		return (waitForCondition & ChannelCondition.EOF) != 0;
950 	}
951 
952 	/**
953 	 * Check if the waitForCondition bit mask contains the session closed condition.
954 	 *
955 	 * @param waitForCondition The waitForCondition bit mask
956 	 * @return true if the bit mask contains the condition, false otherwise
957 	 */
958 	static boolean hasSessionClosed(final int waitForCondition) {
959 		return (waitForCondition & ChannelCondition.CLOSED) != 0;
960 	}
961 
962 	/**
963 	 * Check if the waitForCondition bit mask contains the stdout data condition.
964 	 *
965 	 * @param waitForCondition The waitForCondition bit mask
966 	 * @return true if the bit mask contains the condition, false otherwise
967 	 */
968 	static boolean hasStdoutData(final int waitForCondition) {
969 		return (waitForCondition & ChannelCondition.STDOUT_DATA) != 0;
970 	}
971 
972 	/**
973 	 * Check if the waitForCondition bit mask contains the stderr data condition.
974 	 *
975 	 * @param waitForCondition The waitForCondition bit mask
976 	 * @return true if the bit mask contains the condition, false otherwise
977 	 */
978 	static boolean hasStderrData(final int waitForCondition) {
979 		return (waitForCondition & ChannelCondition.STDERR_DATA) != 0;
980 	}
981 
982 	/**
983 	 * <p>Wait until the session contains at least one of the condition:
984 	 * <li>stdout data</li>
985 	 * <li>stderr data</li>
986 	 * <li>end of file</li>
987 	 * <li>session closed</li>
988 	 * </p>
989 	 * @param timeout Timeout in milliseconds
990 	 * @return A bit mask specifying all current conditions that are true
991 	 */
992 	int waitForNewData(final long timeout) {
993 		return sshSession.waitForCondition(
994 			ChannelCondition.STDOUT_DATA | ChannelCondition.STDERR_DATA | ChannelCondition.EOF | ChannelCondition.CLOSED,
995 			timeout
996 		);
997 	}
998 
999 	/**
1000 	 * Check if the SSH connection exists.
1001 	 */
1002 	void checkIfConnected() {
1003 		if (getSshConnection() == null) {
1004 			throw new IllegalStateException("Connection is required first");
1005 		}
1006 	}
1007 
1008 	/**
1009 	 * Check if already authenticate.
1010 	 */
1011 	void checkIfAuthenticated() {
1012 		if (!getSshConnection().isAuthenticationComplete()) {
1013 			throw new IllegalStateException("Authentication is required first");
1014 		}
1015 	}
1016 
1017 	/**
1018 	 * Check if the SSH session exists.
1019 	 */
1020 	public void checkIfSessionOpened() {
1021 		if (getSshSession() == null) {
1022 			throw new IllegalStateException("SSH session should be opened first");
1023 		}
1024 	}
1025 
1026 	/**
1027 	 * Read all the bytes from the inputstream and write them into a string in the outputstream.
1028 	 *
1029 	 * @param inputStream The inputStream.
1030 	 * @param outputStream The outputstream.
1031 	 * @return The total number of copy bytes.
1032 	 * @throws IOException When an I/O error occurred.
1033 	 */
1034 	static int transferAllBytes(final InputStream inputStream, final OutputStream outputStream) throws IOException {
1035 		return transferBytes(inputStream, outputStream, -1);
1036 	}
1037 
1038 	/**
1039 	 * Read a size number of bytes from the inputstream and write them into a string in the outputstream.
1040 	 *
1041 	 * @param inputStream The inputStream.
1042 	 * @param outputStream The outputstream.
1043 	 * @param size the number of bytes to copy. If the size is negative or zero, it copy all the bytes from the inputstream.
1044 	 * @return The total number of copy bytes.
1045 	 * @throws IOException When an I/O error occurred.
1046 	 */
1047 	static int transferBytes(final InputStream inputStream, final OutputStream outputStream, final int size)
1048 		throws IOException {
1049 		final int bufferSize = size > 0 && size < READ_BUFFER_SIZE ? size : READ_BUFFER_SIZE;
1050 		final byte[] buffer = new byte[bufferSize];
1051 
1052 		int total = 0;
1053 		int bytesRead = 0;
1054 
1055 		while (inputStream.available() > 0 && (bytesRead = inputStream.read(buffer)) > 0) {
1056 			final int bytesCopy = Math.min(bytesRead, READ_BUFFER_SIZE);
1057 
1058 			outputStream.write(Arrays.copyOf(buffer, bytesCopy));
1059 			outputStream.flush();
1060 
1061 			total += bytesRead;
1062 
1063 			if (size > 0 && total >= size) {
1064 				return total;
1065 			}
1066 		}
1067 		return total;
1068 	}
1069 
1070 	Connection getSshConnection() {
1071 		return sshConnection;
1072 	}
1073 
1074 	Session getSshSession() {
1075 		return sshSession;
1076 	}
1077 }