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