1
2
3
4
5
6 package org.metricshub.ssh;
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28 import com.trilead.ssh2.ChannelCondition;
29 import com.trilead.ssh2.Connection;
30 import com.trilead.ssh2.InteractiveCallback;
31 import com.trilead.ssh2.SCPClient;
32 import com.trilead.ssh2.SFTPv3Client;
33 import com.trilead.ssh2.SFTPv3DirectoryEntry;
34 import com.trilead.ssh2.SFTPv3FileAttributes;
35 import com.trilead.ssh2.SFTPv3FileHandle;
36 import com.trilead.ssh2.Session;
37 import java.io.BufferedReader;
38 import java.io.ByteArrayOutputStream;
39 import java.io.File;
40 import java.io.IOException;
41 import java.io.InputStream;
42 import java.io.InputStreamReader;
43 import java.io.OutputStream;
44 import java.nio.charset.Charset;
45 import java.util.Arrays;
46 import java.util.Optional;
47 import java.util.Vector;
48 import java.util.regex.Matcher;
49 import java.util.regex.Pattern;
50
51
52
53
54
55
56
57
58
59
60
61
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
76
77 private Session sshSession = null;
78
79
80
81
82
83
84 public SshClient(String pHostname) {
85 this(pHostname, "");
86 }
87
88
89
90
91
92
93
94 public SshClient(String pHostname, String pLocale) {
95 this(pHostname, Utils.getCharsetFromLocale(pLocale));
96 }
97
98
99
100
101
102
103
104 public SshClient(String pHostname, Charset pCharset) {
105 hostname = pHostname;
106 charset = pCharset;
107 }
108
109
110
111
112
113
114 public void connect() throws IOException {
115 connect(0);
116 }
117
118
119
120
121
122
123
124 public void connect(final int timeout) throws IOException {
125 this.connect(timeout, 22);
126 }
127
128
129
130
131
132
133
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
142
143
144
145
146
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
160
161
162
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
176
177
178
179
180
181
182
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
195
196
197
198
199
200
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
216
217
218
219
220
221
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
230
231
232
233
234
235
236 public boolean authenticate(String username, char[] password) throws IOException {
237
238
239 if (
240 sshConnection.isAuthMethodAvailable(username, "password") &&
241 sshConnection.authenticateWithPassword(username, password != null ? String.valueOf(password) : null)
242 ) {
243 return true;
244 }
245
246
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
260 String[] challengeResponse = new String[numPrompts];
261 for (int i = 0; i < numPrompts; i++) {
262
263
264
265
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
279 return false;
280 }
281
282
283
284
285
286
287
288
289 public boolean authenticate(String username) throws IOException {
290 return sshConnection.authenticateWithNone(username);
291 }
292
293
294
295
296
297
298
299
300
301 public String readFileAttributes(String filePath) throws IOException {
302
303 checkIfAuthenticated();
304
305
306 SFTPv3Client sftpClient = new SFTPv3Client(sshConnection);
307
308
309 SFTPv3FileAttributes fileAttributes = sftpClient.stat(filePath);
310
311
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
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
343 sftpClient.close();
344
345
346 return pslFileResult.toString();
347 }
348
349 private StringBuilder listSubDirectory(
350 SFTPv3Client sftpClient,
351 String remoteDirectoryPath,
352 Pattern fileMaskPattern,
353 boolean includeSubfolders,
354 Integer depth,
355 StringBuilder resultBuilder
356 ) throws IOException {
357 if (depth <= 15) {
358 @SuppressWarnings("unchecked")
359 Vector<SFTPv3DirectoryEntry> pathContents = sftpClient.ls(remoteDirectoryPath);
360
361
362 if (remoteDirectoryPath.endsWith("/")) {
363 remoteDirectoryPath = remoteDirectoryPath.substring(0, remoteDirectoryPath.lastIndexOf("/"));
364 }
365
366 depth++;
367 for (SFTPv3DirectoryEntry file : pathContents) {
368 String filename = file.filename.trim();
369
370 if (filename.equals(".") || filename.equals("..")) {
371 continue;
372 }
373
374 SFTPv3FileAttributes fileAttributes = file.attributes;
375 String filePath = remoteDirectoryPath + "/" + filename;
376
377 if ((fileAttributes.permissions & 0120000) == 0120000) {
378
379 continue;
380 }
381
382
383 if (
384 ((fileAttributes.permissions & 0100000) == 0100000) ||
385 ((fileAttributes.permissions & 0060000) == 0060000) ||
386 ((fileAttributes.permissions & 0020000) == 0020000) ||
387 ((fileAttributes.permissions & 0140000) == 0140000)
388 ) {
389
390 final Matcher m = fileMaskPattern.matcher(filename);
391 if (m.find()) {
392 resultBuilder
393 .append(filePath)
394 .append(";")
395 .append(fileAttributes.mtime.toString())
396 .append(";")
397 .append(fileAttributes.size.toString())
398 .append("\n");
399 }
400 continue;
401 }
402
403
404 if ((fileAttributes.permissions & 0040000) == 0040000) {
405
406 if (includeSubfolders) {
407 resultBuilder =
408 listSubDirectory(sftpClient, filePath, fileMaskPattern, includeSubfolders, depth, resultBuilder);
409 }
410 }
411 }
412 }
413
414 return resultBuilder;
415 }
416
417
418
419
420
421
422
423
424
425
426
427
428
429 public String listFiles(String remoteDirectoryPath, String regExpFileMask, boolean includeSubfolders)
430 throws IOException {
431 checkIfAuthenticated();
432
433
434 SFTPv3Client sftpClient = new SFTPv3Client(sshConnection);
435
436
437 Pattern fileMaskPattern;
438 if (regExpFileMask != null && !regExpFileMask.isEmpty()) {
439 fileMaskPattern = Pattern.compile(regExpFileMask, Pattern.CASE_INSENSITIVE);
440 } else {
441 fileMaskPattern = DEFAULT_MASK_PATTERN;
442 }
443
444
445 StringBuilder resultBuilder = new StringBuilder();
446 listSubDirectory(sftpClient, remoteDirectoryPath, fileMaskPattern, includeSubfolders, 1, resultBuilder);
447
448
449 sftpClient.close();
450
451
452 return resultBuilder.toString();
453 }
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472 public String readFile(String remoteFilePath, Long readOffset, Integer readSize) throws IOException {
473 checkIfAuthenticated();
474
475
476 SFTPv3Client sftpClient = new SFTPv3Client(sshConnection);
477
478
479 long offset = 0;
480 if (readOffset != null) {
481 offset = readOffset;
482 }
483
484
485 SFTPv3FileHandle handle = sftpClient.openFileRO(remoteFilePath);
486
487
488 int remainingBytes;
489 if (readSize == null) {
490
491 SFTPv3FileAttributes attributes = sftpClient.fstat(handle);
492 if (attributes == null) {
493 throw new IOException("Couldn't find file " + remoteFilePath + " and get its attributes");
494 }
495 remainingBytes = (int) (attributes.size - offset);
496 if (remainingBytes < 0) {
497 remainingBytes = 0;
498 }
499 } else {
500 remainingBytes = readSize;
501 }
502
503
504 OutputStream out = new ByteArrayOutputStream();
505 byte[] readBuffer = new byte[READ_BUFFER_SIZE];
506 int bytesRead;
507 int bufferSize;
508
509
510 while (remainingBytes > 0) {
511
512
513 if (remainingBytes < READ_BUFFER_SIZE) {
514 bufferSize = remainingBytes;
515 } else {
516 bufferSize = READ_BUFFER_SIZE;
517 }
518
519
520 bytesRead = sftpClient.read(handle, offset, readBuffer, 0, bufferSize);
521
522
523
524 if (bytesRead < 0) {
525 break;
526 }
527
528
529 out.write(readBuffer, 0, bytesRead);
530
531
532 remainingBytes -= bytesRead;
533 offset += bytesRead;
534 }
535
536
537
538 sftpClient.closeFile(handle);
539
540
541 sftpClient.close();
542
543
544 return out.toString();
545 }
546
547
548
549
550
551
552
553
554 public void removeFile(String[] remoteFilePathArray) throws IOException {
555 checkIfAuthenticated();
556
557
558 SFTPv3Client sftpClient = null;
559 try {
560 sftpClient = new SFTPv3Client(sshConnection);
561
562
563 for (String remoteFilePath : remoteFilePathArray) {
564 sftpClient.rm(remoteFilePath);
565 }
566 } catch (IOException e) {
567
568
569
570 Session rmSession = null;
571 try {
572 for (String remoteFilePath : remoteFilePathArray) {
573 rmSession = sshConnection.openSession();
574 rmSession.execCommand("/usr/bin/rm -f \"" + remoteFilePath + "\"");
575 rmSession.waitForCondition(ChannelCondition.CLOSED | ChannelCondition.EOF, 5000);
576 }
577 } catch (IOException e1) {
578 throw e1;
579 } finally {
580 if (rmSession != null) {
581 rmSession.close();
582 }
583 }
584 } finally {
585
586 if (sftpClient != null) {
587 sftpClient.close();
588 }
589 }
590 }
591
592
593
594
595
596
597
598
599 public void removeFile(String remoteFilePath) throws IOException {
600 removeFile(new String[] { remoteFilePath });
601 }
602
603
604
605
606
607
608
609 public static class CommandResult {
610
611
612
613
614 public boolean success = true;
615
616
617
618
619
620 public float executionTime = 0;
621
622
623
624
625
626 public Integer exitStatus = null;
627
628
629
630
631 public String result = "";
632 }
633
634
635
636
637
638
639
640
641
642 public CommandResult executeCommand(String command) throws IOException {
643 return executeCommand(command, 0);
644 }
645
646
647
648
649
650
651
652
653
654
655 public CommandResult executeCommand(String command, int timeout) throws IOException {
656 openSession();
657
658 InputStream stdout = sshSession.getStdout();
659 InputStream stderr = sshSession.getStderr();
660
661
662
663
664 CommandResult commandResult = new CommandResult();
665
666
667 try (ByteArrayOutputStream output = new ByteArrayOutputStream()) {
668
669 long startTime = System.currentTimeMillis();
670 long timeoutTime;
671 if (timeout > 0) {
672 timeoutTime = startTime + timeout;
673 } else {
674
675 timeoutTime = Long.MAX_VALUE;
676 }
677
678
679 sshSession.execCommand(command);
680
681 int waitForCondition = 0;
682 long currentTime;
683
684 while (
685 !hasSessionClosed(waitForCondition) &&
686 !hasEndOfFileSession(waitForCondition) &&
687 ((currentTime = System.currentTimeMillis()) < timeoutTime)
688 ) {
689
690 waitForCondition = waitForNewData(Math.min(timeoutTime - currentTime, 5000));
691
692
693 if (hasStdoutData(waitForCondition)) {
694 transferAllBytes(stdout, output);
695 }
696
697 if (hasStderrData(waitForCondition)) {
698 transferAllBytes(stderr, output);
699 }
700 }
701
702
703
704 currentTime = System.currentTimeMillis();
705 if (currentTime >= timeoutTime) {
706
707
708
709 commandResult.success = false;
710 commandResult.result = "Timeout (" + timeout / 1000 + " seconds)";
711 } else {
712
713
714
715 commandResult.executionTime = (currentTime - startTime) / 1000;
716
717
718 waitForCondition = sshSession.waitForCondition(ChannelCondition.EXIT_STATUS, 5000);
719 if ((waitForCondition & ChannelCondition.EXIT_STATUS) != 0) {
720 commandResult.exitStatus = sshSession.getExitStatus();
721 }
722
723
724 commandResult.result = new String(output.toByteArray(), charset);
725 }
726 }
727
728
729 return commandResult;
730 }
731
732
733
734
735
736
737
738
739
740
741 public void interactiveSession(InputStream in, OutputStream out) throws IOException, InterruptedException {
742 openSession();
743
744 openTerminal();
745
746
747 BufferedReader inputReader = new BufferedReader(new InputStreamReader(in));
748 OutputStream outputWriter = sshSession.getStdin();
749 Thread stdinPipeThread = new Thread() {
750 @Override
751 public void run() {
752 try {
753 String line;
754 while ((line = inputReader.readLine()) != null) {
755 outputWriter.write(line.getBytes());
756 outputWriter.write('\n');
757 }
758 } catch (Exception e) {
759
760 }
761
762
763 sshSession.close();
764 }
765 };
766 stdinPipeThread.setDaemon(true);
767 stdinPipeThread.start();
768
769
770 InputStream stdout = sshSession.getStdout();
771 InputStream stderr = sshSession.getStderr();
772
773 int waitForCondition = 0;
774 while (!hasSessionClosed(waitForCondition) && !hasEndOfFileSession(waitForCondition)) {
775
776 waitForCondition = waitForNewData(5000L);
777
778
779 if (hasStdoutData(waitForCondition)) {
780 transferAllBytes(stdout, out);
781 }
782
783 if (hasStderrData(waitForCondition)) {
784 transferAllBytes(stderr, out);
785 }
786 }
787
788
789
790 if (stdinPipeThread.isAlive()) {
791 stdinPipeThread.interrupt();
792 }
793 }
794
795
796
797
798
799
800
801
802
803
804 public void scp(String localFilePath, String remoteFilename, String remoteDirectory, String fileMode)
805 throws IOException {
806 checkIfAuthenticated();
807
808
809 SCPClient scpClient = new SCPClient(sshConnection);
810
811
812 scpClient.put(localFilePath, remoteFilename, remoteDirectory, fileMode);
813 }
814
815
816
817
818
819
820 public void openSession() throws IOException {
821 checkIfConnected();
822 checkIfAuthenticated();
823
824
825 sshSession = getSshConnection().openSession();
826 }
827
828
829
830
831
832
833
834 public void openTerminal() throws IOException {
835 checkIfConnected();
836 checkIfAuthenticated();
837 checkIfSessionOpened();
838
839 getSshSession().requestPTY("dumb", 10000, 24, 640, 480, new byte[] { 53, 0, 0, 0, 0, 0 });
840 getSshSession().startShell();
841 }
842
843
844
845
846
847
848
849 public void write(final String text) throws IOException {
850 if (text == null || text.isEmpty()) {
851 return;
852 }
853
854 checkIfConnected();
855 checkIfAuthenticated();
856 checkIfSessionOpened();
857
858 Utils.checkNonNullField(charset, "charset");
859
860 final OutputStream outputStream = getSshSession().getStdin();
861 Utils.checkNonNullField(outputStream, "Stdin");
862
863
864 final String[] split = text.split("\\R", -1);
865 if (split.length == 1) {
866 outputStream.write(text.getBytes(charset));
867 } else {
868 for (int i = 0; i < split.length; i++) {
869 if (split[i].length() != 0) {
870 outputStream.write(split[i].getBytes(charset));
871 }
872
873 if (i < split.length - 1) {
874 outputStream.write('\n');
875 }
876 }
877 }
878
879 outputStream.flush();
880 }
881
882
883
884
885
886
887
888
889
890
891 public Optional<String> read(final int size, final int timeout) throws IOException {
892 Utils.checkArgumentNotZeroOrNegative(timeout, "timeout");
893
894 checkIfConnected();
895 checkIfAuthenticated();
896 checkIfSessionOpened();
897
898 Utils.checkNonNullField(charset, "charset");
899
900 final InputStream stdout = getSshSession().getStdout();
901 final InputStream stderr = getSshSession().getStderr();
902 Utils.checkNonNullField(stdout, "stdout");
903 Utils.checkNonNullField(stderr, "stderr");
904
905 try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
906
907 final int waitForCondition = waitForNewData(timeout * 1000L);
908
909 final boolean stdoutData = hasStdoutData(waitForCondition);
910 final boolean stderrData = hasStderrData(waitForCondition);
911
912
913 int stdoutRead = 0;
914 if (stdoutData) {
915 stdoutRead = transferBytes(stdout, byteArrayOutputStream, size);
916 if (size > 0 && stdoutRead >= size) {
917 return Optional.of(new String(byteArrayOutputStream.toByteArray(), charset));
918 }
919 }
920
921
922 if (stderrData) {
923 transferBytes(stderr, byteArrayOutputStream, size - stdoutRead);
924 }
925
926 return stdoutData || stderrData
927 ? Optional.of(new String(byteArrayOutputStream.toByteArray(), charset))
928 : Optional.empty();
929 }
930 }
931
932
933
934
935
936
937
938 static boolean hasTimeoutSession(final int waitForCondition) {
939 return (waitForCondition & ChannelCondition.TIMEOUT) != 0;
940 }
941
942
943
944
945
946
947
948 static boolean hasEndOfFileSession(final int waitForCondition) {
949 return (waitForCondition & ChannelCondition.EOF) != 0;
950 }
951
952
953
954
955
956
957
958 static boolean hasSessionClosed(final int waitForCondition) {
959 return (waitForCondition & ChannelCondition.CLOSED) != 0;
960 }
961
962
963
964
965
966
967
968 static boolean hasStdoutData(final int waitForCondition) {
969 return (waitForCondition & ChannelCondition.STDOUT_DATA) != 0;
970 }
971
972
973
974
975
976
977
978 static boolean hasStderrData(final int waitForCondition) {
979 return (waitForCondition & ChannelCondition.STDERR_DATA) != 0;
980 }
981
982
983
984
985
986
987
988
989
990
991
992 int waitForNewData(final long timeout) {
993 return sshSession.waitForCondition(
994 ChannelCondition.STDOUT_DATA | ChannelCondition.STDERR_DATA | ChannelCondition.EOF | ChannelCondition.CLOSED,
995 timeout
996 );
997 }
998
999
1000
1001
1002 void checkIfConnected() {
1003 if (getSshConnection() == null) {
1004 throw new IllegalStateException("Connection is required first");
1005 }
1006 }
1007
1008
1009
1010
1011 void checkIfAuthenticated() {
1012 if (!getSshConnection().isAuthenticationComplete()) {
1013 throw new IllegalStateException("Authentication is required first");
1014 }
1015 }
1016
1017
1018
1019
1020 public void checkIfSessionOpened() {
1021 if (getSshSession() == null) {
1022 throw new IllegalStateException("SSH session should be opened first");
1023 }
1024 }
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034 static int transferAllBytes(final InputStream inputStream, final OutputStream outputStream) throws IOException {
1035 return transferBytes(inputStream, outputStream, -1);
1036 }
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047 static int transferBytes(final InputStream inputStream, final OutputStream outputStream, final int size)
1048 throws IOException {
1049 final int bufferSize = size > 0 && size < READ_BUFFER_SIZE ? size : READ_BUFFER_SIZE;
1050 final byte[] buffer = new byte[bufferSize];
1051
1052 int total = 0;
1053 int bytesRead = 0;
1054
1055 while (inputStream.available() > 0 && (bytesRead = inputStream.read(buffer)) > 0) {
1056 final int bytesCopy = Math.min(bytesRead, READ_BUFFER_SIZE);
1057
1058 outputStream.write(Arrays.copyOf(buffer, bytesCopy));
1059 outputStream.flush();
1060
1061 total += bytesRead;
1062
1063 if (size > 0 && total >= size) {
1064 return total;
1065 }
1066 }
1067 return total;
1068 }
1069
1070 Connection getSshConnection() {
1071 return sshConnection;
1072 }
1073
1074 Session getSshSession() {
1075 return sshSession;
1076 }
1077 }