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.List;
47  import java.util.Optional;
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 	/**
350 	 * Returns the file size.
351 	 *
352 	 * @param filePath Path to the file on the remote system
353 	 * @return the file size
354 	 * @throws IOException if the file does not exist
355 	 */
356 	public long fileSize(final String filePath) throws IOException {
357 		SFTPv3Client sftpClient = null;
358 		try {
359 			// Sanity check
360 			checkIfAuthenticated();
361 
362 			// Create the SFTP client
363 			sftpClient = new SFTPv3Client(sshConnection);
364 
365 			// Read the file attributes
366 			final SFTPv3FileAttributes attributes = sftpClient.stat(filePath);
367 
368 			return attributes.size;
369 		} finally {
370 			// Deallocate
371 			if (sftpClient != null) {
372 				sftpClient.close();
373 			}
374 		}
375 	}
376 
377 	private StringBuilder listSubDirectory(
378 		SFTPv3Client sftpClient,
379 		String remoteDirectoryPath,
380 		Pattern fileMaskPattern,
381 		boolean includeSubfolders,
382 		Integer depth,
383 		StringBuilder resultBuilder
384 	) throws IOException {
385 		if (depth <= 15) {
386 			List<SFTPv3DirectoryEntry> pathContents = sftpClient.ls(remoteDirectoryPath);
387 
388 			// Fix the remoteDirectoryPath (without the last '/')
389 			if (remoteDirectoryPath.endsWith("/")) {
390 				remoteDirectoryPath = remoteDirectoryPath.substring(0, remoteDirectoryPath.lastIndexOf("/"));
391 			}
392 
393 			depth++;
394 			for (SFTPv3DirectoryEntry file : pathContents) {
395 				String filename = file.filename.trim();
396 
397 				if (filename.equals(".") || filename.equals("..")) {
398 					continue;
399 				}
400 
401 				SFTPv3FileAttributes fileAttributes = file.attributes;
402 				String filePath = remoteDirectoryPath + "/" + filename;
403 
404 				if ((fileAttributes.permissions & 0120000) == 0120000) {
405 					// Symbolic link
406 					continue;
407 				}
408 
409 				// CHECKSTYLE:OFF
410 				if (
411 					((fileAttributes.permissions & 0100000) == 0100000) ||
412 					((fileAttributes.permissions & 0060000) == 0060000) ||
413 					((fileAttributes.permissions & 0020000) == 0020000) ||
414 					((fileAttributes.permissions & 0140000) == 0140000)
415 				) {
416 					// Regular/Block/Character/Socket files
417 					final Matcher m = fileMaskPattern.matcher(filename);
418 					if (m.find()) {
419 						resultBuilder
420 							.append(filePath)
421 							.append(";")
422 							.append(fileAttributes.mtime.toString())
423 							.append(";")
424 							.append(fileAttributes.size.toString())
425 							.append("\n");
426 					}
427 					continue;
428 				}
429 				// CHECKSTYLE:ON
430 
431 				if ((fileAttributes.permissions & 0040000) == 0040000) {
432 					// Directory
433 					if (includeSubfolders) {
434 						resultBuilder =
435 							listSubDirectory(sftpClient, filePath, fileMaskPattern, includeSubfolders, depth, resultBuilder);
436 					}
437 				}
438 			}
439 		}
440 
441 		return resultBuilder;
442 	}
443 
444 	/**
445 	 * List the content of the specified directory through the SSH connection
446 	 * (using SCP)
447 	 *
448 	 * @param remoteDirectoryPath The path to the directory to list on the remote host
449 	 * @param regExpFileMask A regular expression that listed files must match with to be listed
450 	 * @param includeSubfolders Whether to parse subdirectories as well
451 	 * @return The list of files in the specified directory, separated by end-of-lines
452 	 *
453 	 * @throws IOException When something bad happens while communicating with the remote host
454 	 * @throws IllegalStateException If called while not yet connected
455 	 */
456 	public String listFiles(String remoteDirectoryPath, String regExpFileMask, boolean includeSubfolders)
457 		throws IOException {
458 		checkIfAuthenticated();
459 
460 		// Create an SFTP Client
461 		SFTPv3Client sftpClient = new SFTPv3Client(sshConnection);
462 
463 		// Prepare the Pattern for fileMask
464 		Pattern fileMaskPattern;
465 		if (regExpFileMask != null && !regExpFileMask.isEmpty()) {
466 			fileMaskPattern = Pattern.compile(regExpFileMask, Pattern.CASE_INSENSITIVE);
467 		} else {
468 			fileMaskPattern = DEFAULT_MASK_PATTERN;
469 		}
470 
471 		// Read the directory listing
472 		StringBuilder resultBuilder = new StringBuilder();
473 		listSubDirectory(sftpClient, remoteDirectoryPath, fileMaskPattern, includeSubfolders, 1, resultBuilder);
474 
475 		// Close the SFTP client
476 		sftpClient.close();
477 
478 		// Update the response
479 		return resultBuilder.toString();
480 	}
481 
482 	/**
483 	 * Read the specified file over the SSH session that was established.
484 	 *
485 	 * @param remoteFilePath
486 	 *            Path to the file to be read on the remote host
487 	 * @param readOffset
488 	 *            Offset to read from
489 	 * @param readSize
490 	 *            Amount of bytes to be read
491 	 * @return The content of the file read
492 	 *
493 	 * @throws IOException
494 	 *             when something gets wrong while reading the file (we get
495 	 *             disconnected, for example, or we couldn't read the file)
496 	 * @throws IllegalStateException
497 	 *             when the session hasn't been properly authenticated first
498 	 */
499 	public String readFile(String remoteFilePath, Long readOffset, Integer readSize) throws IOException {
500 		checkIfAuthenticated();
501 
502 		// Create an SFTP Client
503 		SFTPv3Client sftpClient = new SFTPv3Client(sshConnection);
504 
505 		// Where do we read from (offset)?
506 		long offset = 0; // from the beginning by default
507 		if (readOffset != null) {
508 			offset = readOffset; // use the set offset
509 		}
510 
511 		// Open the remote file
512 		SFTPv3FileHandle handle = sftpClient.openFileRO(remoteFilePath);
513 
514 		// How much data to read?
515 		int remainingBytes;
516 		if (readSize == null) {
517 			// If size was not specified, we read the file entirely
518 			SFTPv3FileAttributes attributes = sftpClient.fstat(handle);
519 			if (attributes == null) {
520 				throw new IOException("Couldn't find file " + remoteFilePath + " and get its attributes");
521 			}
522 			remainingBytes = (int) (attributes.size - offset);
523 			if (remainingBytes < 0) {
524 				remainingBytes = 0;
525 			}
526 		} else {
527 			remainingBytes = readSize;
528 		}
529 
530 		// Read the remote file
531 		OutputStream out = new ByteArrayOutputStream();
532 		byte[] readBuffer = new byte[READ_BUFFER_SIZE];
533 		int bytesRead;
534 		int bufferSize;
535 
536 		// Loop until there is nothing else to read
537 		while (remainingBytes > 0) {
538 			// Read by chunk of 8192 bytes. However, if there is less to read,
539 			// well, read less.
540 			if (remainingBytes < READ_BUFFER_SIZE) {
541 				bufferSize = remainingBytes;
542 			} else {
543 				bufferSize = READ_BUFFER_SIZE;
544 			}
545 
546 			// Read and store that in our buffer
547 			bytesRead = sftpClient.read(handle, offset, readBuffer, 0, bufferSize);
548 
549 			// If we already reached the end of the file, exit (probably, we
550 			// were asked to read more than what is available)
551 			if (bytesRead < 0) {
552 				break;
553 			}
554 
555 			// Write our buffer to the result stream
556 			out.write(readBuffer, 0, bytesRead);
557 
558 			// Keep counting!
559 			remainingBytes -= bytesRead;
560 			offset += bytesRead;
561 		}
562 
563 		// File read complete
564 		// Close the remote file
565 		sftpClient.closeFile(handle);
566 
567 		// Close the SFTP client
568 		sftpClient.close();
569 
570 		// Metricshub Collection format
571 		return out.toString();
572 	}
573 
574 	/**
575 	 * Removes a list of files on the remote system.
576 	 *
577 	 * @param remoteFilePathArray Array of paths to the files to be deleted on the remote host
578 	 * @throws IOException when something bad happens while deleting the file
579 	 * @throws IllegalStateException when not connected and authenticated yet
580 	 */
581 	public void removeFile(String[] remoteFilePathArray) throws IOException {
582 		checkIfAuthenticated();
583 
584 		// Create an SFTP Client
585 		SFTPv3Client sftpClient = null;
586 		try {
587 			sftpClient = new SFTPv3Client(sshConnection);
588 
589 			// Remove the files
590 			for (String remoteFilePath : remoteFilePathArray) {
591 				sftpClient.rm(remoteFilePath);
592 			}
593 		} catch (IOException e) {
594 			// Okay, we got an issue here with the SFTP client
595 			// We're going to try again but with a good old "rm" command...
596 
597 			Session rmSession = null;
598 			try {
599 				for (String remoteFilePath : remoteFilePathArray) {
600 					rmSession = sshConnection.openSession();
601 					rmSession.execCommand("/usr/bin/rm -f \"" + remoteFilePath + "\"");
602 					rmSession.waitForCondition(ChannelCondition.CLOSED | ChannelCondition.EOF, 5000);
603 				}
604 			} catch (IOException e1) {
605 				throw e1;
606 			} finally {
607 				if (rmSession != null) {
608 					rmSession.close();
609 				}
610 			}
611 		} finally {
612 			// Close the SFTP client
613 			if (sftpClient != null) {
614 				sftpClient.close();
615 			}
616 		}
617 	}
618 
619 	/**
620 	 * Removes the specified file on the remote system.
621 	 *
622 	 * @param remoteFilePath
623 	 * @throws IOException
624 	 * @throws IllegalStateException
625 	 */
626 	public void removeFile(String remoteFilePath) throws IOException {
627 		removeFile(new String[] { remoteFilePath });
628 	}
629 
630 	/**
631 	 * Represents the result of a command execution
632 	 *
633 	 * @author bertrand
634 	 *
635 	 */
636 	public static class CommandResult {
637 
638 		/**
639 		 * Whether the command was successful or not
640 		 */
641 		public boolean success = true;
642 
643 		/**
644 		 * How much time was taken by the execution itself (not counting the
645 		 * connection time), in seconds
646 		 */
647 		public float executionTime = 0;
648 
649 		/**
650 		 * The exit code (status) returned by the command (process return code).
651 		 * <code>null</code> if unsupported by the remote platform.
652 		 */
653 		public Integer exitStatus = null;
654 
655 		/**
656 		 * The result of the command (stdout and stderr is merged into result)
657 		 */
658 		public String result = "";
659 	}
660 
661 	/**
662 	 * Executes a command through the SSH connection
663 	 *
664 	 * @param command	The command to be executed
665 	 * @return a CommandResult object with the result of the execution
666 	 * @throws IllegalStateException when the connection is not established first
667 	 * @throws IOException when there is a problem while communicating with the remote system
668 	 */
669 	public CommandResult executeCommand(String command) throws IOException {
670 		return executeCommand(command, 0);
671 	}
672 
673 	/**
674 	 * Executes a command through the SSH connection
675 	 *
676 	 * @param command	The command to be executed
677 	 * @param timeout	Milliseconds after which the command is considered failed
678 	 * @return a CommandResult object with the result of the execution
679 	 * @throws IllegalStateException when the connection is not established first
680 	 * @throws IOException when there is a problem while communicating with the remote system
681 	 */
682 	public CommandResult executeCommand(String command, int timeout) throws IOException {
683 		openSession();
684 
685 		InputStream stdout = sshSession.getStdout();
686 		InputStream stderr = sshSession.getStderr();
687 		// DO NOT request for a PTY, as Trilead SSH's execCommand() would get stuck on AIX...
688 		//sshSession.requestPTY("dumb", 10000, 24, 640, 480, new byte[] {53, 0, 0, 0, 0, 0}); // request for a wiiiiide terminal
689 
690 		// Initialization
691 		CommandResult commandResult = new CommandResult();
692 
693 		// Output to a byte stream
694 		try (ByteArrayOutputStream output = new ByteArrayOutputStream()) {
695 			// Time to be remembered
696 			long startTime = System.currentTimeMillis();
697 			long timeoutTime;
698 			if (timeout > 0) {
699 				timeoutTime = startTime + timeout;
700 			} else {
701 				// If no timeout, we use the max long value for the time when we're supposed to stop
702 				timeoutTime = Long.MAX_VALUE;
703 			}
704 
705 			// Run the command
706 			sshSession.execCommand(command);
707 
708 			int waitForCondition = 0;
709 			long currentTime;
710 			// CHECKSTYLE:OFF
711 			while (
712 				!hasSessionClosed(waitForCondition) &&
713 				!hasEndOfFileSession(waitForCondition) &&
714 				((currentTime = System.currentTimeMillis()) < timeoutTime)
715 			) {
716 				// Wait for new data (timeout = 5 seconds)
717 				waitForCondition = waitForNewData(Math.min(timeoutTime - currentTime, 5000));
718 
719 				// Print available data (if any)
720 				if (hasStdoutData(waitForCondition)) {
721 					transferAllBytes(stdout, output);
722 				}
723 
724 				if (hasStderrData(waitForCondition)) {
725 					transferAllBytes(stderr, output);
726 				}
727 			}
728 			// CHECKSTYLE:ON
729 
730 			// What time is it?
731 			currentTime = System.currentTimeMillis();
732 			if (currentTime >= timeoutTime) {
733 				// If we exceeded the timeout, we're not successful
734 
735 				// Build the "timed out" result
736 				commandResult.success = false;
737 				commandResult.result = "Timeout (" + timeout / 1000 + " seconds)";
738 			} else {
739 				// We completed in time
740 
741 				// Execution time (in seconds)
742 				commandResult.executionTime = (currentTime - startTime) / 1000;
743 
744 				// Read exit status, when available
745 				waitForCondition = sshSession.waitForCondition(ChannelCondition.EXIT_STATUS, 5000);
746 				if ((waitForCondition & ChannelCondition.EXIT_STATUS) != 0) {
747 					commandResult.exitStatus = sshSession.getExitStatus();
748 				}
749 
750 				// Stringify the stdout stream
751 				commandResult.result = new String(output.toByteArray(), charset);
752 			}
753 		}
754 
755 		// Return
756 		return commandResult;
757 	}
758 
759 	/**
760 	 * Starts an interactive session.
761 	 *
762 	 * @param in Where the input is coming from (typically System.in)
763 	 * @param out Where the output has to go (e.g. System.out)
764 	 * @throws IllegalStateException when not connected and authenticated
765 	 * @throws IOException in case of communication problems with the host
766 	 * @throws InterruptedException when a thread is interrupted
767 	 */
768 	public void interactiveSession(InputStream in, OutputStream out) throws IOException, InterruptedException {
769 		openSession();
770 
771 		openTerminal();
772 
773 		// Pipe specified InputStream to SSH's stdin -- use a separate thread
774 		BufferedReader inputReader = new BufferedReader(new InputStreamReader(in));
775 		OutputStream outputWriter = sshSession.getStdin();
776 		Thread stdinPipeThread = new Thread() {
777 			@Override
778 			public void run() {
779 				try {
780 					String line;
781 					while ((line = inputReader.readLine()) != null) {
782 						outputWriter.write(line.getBytes());
783 						outputWriter.write('\n');
784 					}
785 				} catch (Exception e) {
786 					// Things ended up badly. Exit thread.
787 				}
788 				// End of the input stream. We need to exit.
789 				// Let's close the session so the main thread exits nicely.
790 				sshSession.close();
791 			}
792 		};
793 		stdinPipeThread.setDaemon(true);
794 		stdinPipeThread.start();
795 
796 		// Now, pipe stdout and stderr to specified OutputStream
797 		InputStream stdout = sshSession.getStdout();
798 		InputStream stderr = sshSession.getStderr();
799 
800 		int waitForCondition = 0;
801 		while (!hasSessionClosed(waitForCondition) && !hasEndOfFileSession(waitForCondition)) {
802 			// Wait for new data (timeout = 5 seconds)
803 			waitForCondition = waitForNewData(5000L);
804 
805 			// Print available data (if any)
806 			if (hasStdoutData(waitForCondition)) {
807 				transferAllBytes(stdout, out);
808 			}
809 
810 			if (hasStderrData(waitForCondition)) {
811 				transferAllBytes(stderr, out);
812 			}
813 		}
814 
815 		// Attempt to interrupt the stdinPipeThread thread
816 		// (may be useless if we're reading a blocking InputStream like System.in)
817 		if (stdinPipeThread.isAlive()) {
818 			stdinPipeThread.interrupt();
819 		}
820 	}
821 
822 	/**
823 	 * Copy a file to the remote host through SCP
824 	 *
825 	 * @param localFilePath
826 	 * @param remoteFilename
827 	 * @param remoteDirectory
828 	 * @param fileMode
829 	 * @throws IOException
830 	 */
831 	public void scp(String localFilePath, String remoteFilename, String remoteDirectory, String fileMode)
832 		throws IOException {
833 		checkIfAuthenticated();
834 
835 		// Create the SCP client
836 		SCPClient scpClient = new SCPClient(sshConnection);
837 
838 		// Copy the file
839 		scpClient.put(localFilePath, remoteFilename, remoteDirectory, fileMode);
840 	}
841 
842 	/**
843 	 * Open a SSH Session.
844 	 *
845 	 * @throws IOException When an I/O error occurred.
846 	 */
847 	public void openSession() throws IOException {
848 		checkIfConnected();
849 		checkIfAuthenticated();
850 
851 		// Open a shell session
852 		sshSession = getSshConnection().openSession();
853 	}
854 
855 	/**
856 	 * Open a Terminal
857 	 * request for a wiiiiide terminal, with no ECHO (see https://tools.ietf.org/html/rfc4254#section-8)
858 	 *
859 	 * @throws IOException When an I/O error occurred.
860 	 */
861 	public void openTerminal() throws IOException {
862 		checkIfConnected();
863 		checkIfAuthenticated();
864 		checkIfSessionOpened();
865 
866 		getSshSession().requestPTY("dumb", 10000, 24, 640, 480, new byte[] { 53, 0, 0, 0, 0, 0 });
867 		getSshSession().startShell();
868 	}
869 
870 	/**
871 	 * Write into the SSH session.
872 	 *
873 	 * @param text The text to be written.
874 	 * @throws IOException When an I/O error occurred.
875 	 */
876 	public void write(final String text) throws IOException {
877 		if (text == null || text.isEmpty()) {
878 			return;
879 		}
880 
881 		checkIfConnected();
882 		checkIfAuthenticated();
883 		checkIfSessionOpened();
884 
885 		Utils.checkNonNullField(charset, "charset");
886 
887 		final OutputStream outputStream = getSshSession().getStdin();
888 		Utils.checkNonNullField(outputStream, "Stdin");
889 
890 		// Replace "\n" string with write of '\n' character.
891 		final String[] split = text.split("\\R", -1);
892 		if (split.length == 1) {
893 			outputStream.write(text.getBytes(charset));
894 		} else {
895 			for (int i = 0; i < split.length; i++) {
896 				if (split[i].length() != 0) {
897 					outputStream.write(split[i].getBytes(charset));
898 				}
899 
900 				if (i < split.length - 1) {
901 					outputStream.write('\n');
902 				}
903 			}
904 		}
905 
906 		outputStream.flush();
907 	}
908 
909 	/**
910 	 * Read the stdout and stderr from the SSH session.
911 	 *
912 	 * @param size The buffer size of stdout and/or stderr to be read. (If less than 0 all data)
913 	 * @param timeout Timeout in seconds
914 	 *
915 	 * @return An optional with all the data read (stdout and stderr). Empty if nothing was read.
916 	 * @throws IOException When an I/O error occurred.
917 	 */
918 	public Optional<String> read(final int size, final int timeout) throws IOException {
919 		Utils.checkArgumentNotZeroOrNegative(timeout, "timeout");
920 
921 		checkIfConnected();
922 		checkIfAuthenticated();
923 		checkIfSessionOpened();
924 
925 		Utils.checkNonNullField(charset, "charset");
926 
927 		final InputStream stdout = getSshSession().getStdout();
928 		final InputStream stderr = getSshSession().getStderr();
929 		Utils.checkNonNullField(stdout, "stdout");
930 		Utils.checkNonNullField(stderr, "stderr");
931 
932 		try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
933 			// Wait for new data
934 			final int waitForCondition = waitForNewData(timeout * 1000L);
935 
936 			final boolean stdoutData = hasStdoutData(waitForCondition);
937 			final boolean stderrData = hasStderrData(waitForCondition);
938 
939 			// read stdout
940 			int stdoutRead = 0;
941 			if (stdoutData) {
942 				stdoutRead = transferBytes(stdout, byteArrayOutputStream, size);
943 				if (size > 0 && stdoutRead >= size) {
944 					return Optional.of(new String(byteArrayOutputStream.toByteArray(), charset));
945 				}
946 			}
947 
948 			// If still bytes to read or no stdout, read stderr
949 			if (stderrData) {
950 				transferBytes(stderr, byteArrayOutputStream, size - stdoutRead);
951 			}
952 
953 			return stdoutData || stderrData
954 				? Optional.of(new String(byteArrayOutputStream.toByteArray(), charset))
955 				: Optional.empty();
956 		}
957 	}
958 
959 	/**
960 	 * Check if the waitForCondition bit mask contains the timeout condition.
961 	 *
962 	 * @param waitForCondition The waitForCondition bit mask
963 	 * @return true if the bit mask contains the condition, false otherwise
964 	 */
965 	static boolean hasTimeoutSession(final int waitForCondition) {
966 		return (waitForCondition & ChannelCondition.TIMEOUT) != 0;
967 	}
968 
969 	/**
970 	 * Check if the waitForCondition bit mask contains the end of file condition.
971 	 *
972 	 * @param waitForCondition The waitForCondition bit mask
973 	 * @return true if the bit mask contains the condition, false otherwise
974 	 */
975 	static boolean hasEndOfFileSession(final int waitForCondition) {
976 		return (waitForCondition & ChannelCondition.EOF) != 0;
977 	}
978 
979 	/**
980 	 * Check if the waitForCondition bit mask contains the session closed condition.
981 	 *
982 	 * @param waitForCondition The waitForCondition bit mask
983 	 * @return true if the bit mask contains the condition, false otherwise
984 	 */
985 	static boolean hasSessionClosed(final int waitForCondition) {
986 		return (waitForCondition & ChannelCondition.CLOSED) != 0;
987 	}
988 
989 	/**
990 	 * Check if the waitForCondition bit mask contains the stdout data condition.
991 	 *
992 	 * @param waitForCondition The waitForCondition bit mask
993 	 * @return true if the bit mask contains the condition, false otherwise
994 	 */
995 	static boolean hasStdoutData(final int waitForCondition) {
996 		return (waitForCondition & ChannelCondition.STDOUT_DATA) != 0;
997 	}
998 
999 	/**
1000 	 * Check if the waitForCondition bit mask contains the stderr data condition.
1001 	 *
1002 	 * @param waitForCondition The waitForCondition bit mask
1003 	 * @return true if the bit mask contains the condition, false otherwise
1004 	 */
1005 	static boolean hasStderrData(final int waitForCondition) {
1006 		return (waitForCondition & ChannelCondition.STDERR_DATA) != 0;
1007 	}
1008 
1009 	/**
1010 	 * <p>Wait until the session contains at least one of the condition:
1011 	 * <li>stdout data</li>
1012 	 * <li>stderr data</li>
1013 	 * <li>end of file</li>
1014 	 * <li>session closed</li>
1015 	 * </p>
1016 	 * @param timeout Timeout in milliseconds
1017 	 * @return A bit mask specifying all current conditions that are true
1018 	 */
1019 	int waitForNewData(final long timeout) {
1020 		return sshSession.waitForCondition(
1021 			ChannelCondition.STDOUT_DATA | ChannelCondition.STDERR_DATA | ChannelCondition.EOF | ChannelCondition.CLOSED,
1022 			timeout
1023 		);
1024 	}
1025 
1026 	/**
1027 	 * Check if the SSH connection exists.
1028 	 */
1029 	void checkIfConnected() {
1030 		if (getSshConnection() == null) {
1031 			throw new IllegalStateException("Connection is required first");
1032 		}
1033 	}
1034 
1035 	/**
1036 	 * Check if already authenticate.
1037 	 */
1038 	void checkIfAuthenticated() {
1039 		if (getSshConnection() == null || !getSshConnection().isAuthenticationComplete()) {
1040 			throw new IllegalStateException("Authentication is required first");
1041 		}
1042 	}
1043 
1044 	/**
1045 	 * Check if the SSH session exists.
1046 	 */
1047 	public void checkIfSessionOpened() {
1048 		if (getSshSession() == null) {
1049 			throw new IllegalStateException("SSH session should be opened first");
1050 		}
1051 	}
1052 
1053 	/**
1054 	 * Read all the bytes from the inputstream and write them into a string in the outputstream.
1055 	 *
1056 	 * @param inputStream The inputStream.
1057 	 * @param outputStream The outputstream.
1058 	 * @return The total number of copy bytes.
1059 	 * @throws IOException When an I/O error occurred.
1060 	 */
1061 	static int transferAllBytes(final InputStream inputStream, final OutputStream outputStream) throws IOException {
1062 		return transferBytes(inputStream, outputStream, -1);
1063 	}
1064 
1065 	/**
1066 	 * Read a size number of bytes from the inputstream and write them into a string in the outputstream.
1067 	 *
1068 	 * @param inputStream The inputStream.
1069 	 * @param outputStream The outputstream.
1070 	 * @param size the number of bytes to copy. If the size is negative or zero, it copy all the bytes from the inputstream.
1071 	 * @return The total number of copy bytes.
1072 	 * @throws IOException When an I/O error occurred.
1073 	 */
1074 	static int transferBytes(final InputStream inputStream, final OutputStream outputStream, final int size)
1075 		throws IOException {
1076 		final int bufferSize = size > 0 && size < READ_BUFFER_SIZE ? size : READ_BUFFER_SIZE;
1077 		final byte[] buffer = new byte[bufferSize];
1078 
1079 		int total = 0;
1080 		int bytesRead = 0;
1081 
1082 		while (inputStream.available() > 0 && (bytesRead = inputStream.read(buffer)) > 0) {
1083 			final int bytesCopy = Math.min(bytesRead, READ_BUFFER_SIZE);
1084 
1085 			outputStream.write(Arrays.copyOf(buffer, bytesCopy));
1086 			outputStream.flush();
1087 
1088 			total += bytesRead;
1089 
1090 			if (size > 0 && total >= size) {
1091 				return total;
1092 			}
1093 		}
1094 		return total;
1095 	}
1096 
1097 	Connection getSshConnection() {
1098 		return sshConnection;
1099 	}
1100 
1101 	Session getSshSession() {
1102 		return sshSession;
1103 	}
1104 }