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