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.List;
47 import java.util.Optional;
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
350
351
352
353
354
355
356 public long fileSize(final String filePath) throws IOException {
357 SFTPv3Client sftpClient = null;
358 try {
359
360 checkIfAuthenticated();
361
362
363 sftpClient = new SFTPv3Client(sshConnection);
364
365
366 final SFTPv3FileAttributes attributes = sftpClient.stat(filePath);
367
368 return attributes.size;
369 } finally {
370
371 if (sftpClient != null) {
372 sftpClient.close();
373 }
374 }
375 }
376
377 private StringBuilder listSubDirectory(
378 SFTPv3Client sftpClient,
379 String remoteDirectoryPath,
380 Pattern fileMaskPattern,
381 boolean includeSubfolders,
382 Integer depth,
383 StringBuilder resultBuilder
384 ) throws IOException {
385 if (depth <= 15) {
386 List<SFTPv3DirectoryEntry> pathContents = sftpClient.ls(remoteDirectoryPath);
387
388
389 if (remoteDirectoryPath.endsWith("/")) {
390 remoteDirectoryPath = remoteDirectoryPath.substring(0, remoteDirectoryPath.lastIndexOf("/"));
391 }
392
393 depth++;
394 for (SFTPv3DirectoryEntry file : pathContents) {
395 String filename = file.filename.trim();
396
397 if (filename.equals(".") || filename.equals("..")) {
398 continue;
399 }
400
401 SFTPv3FileAttributes fileAttributes = file.attributes;
402 String filePath = remoteDirectoryPath + "/" + filename;
403
404 if ((fileAttributes.permissions & 0120000) == 0120000) {
405
406 continue;
407 }
408
409
410 if (
411 ((fileAttributes.permissions & 0100000) == 0100000) ||
412 ((fileAttributes.permissions & 0060000) == 0060000) ||
413 ((fileAttributes.permissions & 0020000) == 0020000) ||
414 ((fileAttributes.permissions & 0140000) == 0140000)
415 ) {
416
417 final Matcher m = fileMaskPattern.matcher(filename);
418 if (m.find()) {
419 resultBuilder
420 .append(filePath)
421 .append(";")
422 .append(fileAttributes.mtime.toString())
423 .append(";")
424 .append(fileAttributes.size.toString())
425 .append("\n");
426 }
427 continue;
428 }
429
430
431 if ((fileAttributes.permissions & 0040000) == 0040000) {
432
433 if (includeSubfolders) {
434 resultBuilder =
435 listSubDirectory(sftpClient, filePath, fileMaskPattern, includeSubfolders, depth, resultBuilder);
436 }
437 }
438 }
439 }
440
441 return resultBuilder;
442 }
443
444
445
446
447
448
449
450
451
452
453
454
455
456 public String listFiles(String remoteDirectoryPath, String regExpFileMask, boolean includeSubfolders)
457 throws IOException {
458 checkIfAuthenticated();
459
460
461 SFTPv3Client sftpClient = new SFTPv3Client(sshConnection);
462
463
464 Pattern fileMaskPattern;
465 if (regExpFileMask != null && !regExpFileMask.isEmpty()) {
466 fileMaskPattern = Pattern.compile(regExpFileMask, Pattern.CASE_INSENSITIVE);
467 } else {
468 fileMaskPattern = DEFAULT_MASK_PATTERN;
469 }
470
471
472 StringBuilder resultBuilder = new StringBuilder();
473 listSubDirectory(sftpClient, remoteDirectoryPath, fileMaskPattern, includeSubfolders, 1, resultBuilder);
474
475
476 sftpClient.close();
477
478
479 return resultBuilder.toString();
480 }
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499 public String readFile(String remoteFilePath, Long readOffset, Integer readSize) throws IOException {
500 checkIfAuthenticated();
501
502
503 SFTPv3Client sftpClient = new SFTPv3Client(sshConnection);
504
505
506 long offset = 0;
507 if (readOffset != null) {
508 offset = readOffset;
509 }
510
511
512 SFTPv3FileHandle handle = sftpClient.openFileRO(remoteFilePath);
513
514
515 int remainingBytes;
516 if (readSize == null) {
517
518 SFTPv3FileAttributes attributes = sftpClient.fstat(handle);
519 if (attributes == null) {
520 throw new IOException("Couldn't find file " + remoteFilePath + " and get its attributes");
521 }
522 remainingBytes = (int) (attributes.size - offset);
523 if (remainingBytes < 0) {
524 remainingBytes = 0;
525 }
526 } else {
527 remainingBytes = readSize;
528 }
529
530
531 OutputStream out = new ByteArrayOutputStream();
532 byte[] readBuffer = new byte[READ_BUFFER_SIZE];
533 int bytesRead;
534 int bufferSize;
535
536
537 while (remainingBytes > 0) {
538
539
540 if (remainingBytes < READ_BUFFER_SIZE) {
541 bufferSize = remainingBytes;
542 } else {
543 bufferSize = READ_BUFFER_SIZE;
544 }
545
546
547 bytesRead = sftpClient.read(handle, offset, readBuffer, 0, bufferSize);
548
549
550
551 if (bytesRead < 0) {
552 break;
553 }
554
555
556 out.write(readBuffer, 0, bytesRead);
557
558
559 remainingBytes -= bytesRead;
560 offset += bytesRead;
561 }
562
563
564
565 sftpClient.closeFile(handle);
566
567
568 sftpClient.close();
569
570
571 return out.toString();
572 }
573
574
575
576
577
578
579
580
581 public void removeFile(String[] remoteFilePathArray) throws IOException {
582 checkIfAuthenticated();
583
584
585 SFTPv3Client sftpClient = null;
586 try {
587 sftpClient = new SFTPv3Client(sshConnection);
588
589
590 for (String remoteFilePath : remoteFilePathArray) {
591 sftpClient.rm(remoteFilePath);
592 }
593 } catch (IOException e) {
594
595
596
597 Session rmSession = null;
598 try {
599 for (String remoteFilePath : remoteFilePathArray) {
600 rmSession = sshConnection.openSession();
601 rmSession.execCommand("/usr/bin/rm -f \"" + remoteFilePath + "\"");
602 rmSession.waitForCondition(ChannelCondition.CLOSED | ChannelCondition.EOF, 5000);
603 }
604 } catch (IOException e1) {
605 throw e1;
606 } finally {
607 if (rmSession != null) {
608 rmSession.close();
609 }
610 }
611 } finally {
612
613 if (sftpClient != null) {
614 sftpClient.close();
615 }
616 }
617 }
618
619
620
621
622
623
624
625
626 public void removeFile(String remoteFilePath) throws IOException {
627 removeFile(new String[] { remoteFilePath });
628 }
629
630
631
632
633
634
635
636 public static class CommandResult {
637
638
639
640
641 public boolean success = true;
642
643
644
645
646
647 public float executionTime = 0;
648
649
650
651
652
653 public Integer exitStatus = null;
654
655
656
657
658 public String result = "";
659 }
660
661
662
663
664
665
666
667
668
669 public CommandResult executeCommand(String command) throws IOException {
670 return executeCommand(command, 0);
671 }
672
673
674
675
676
677
678
679
680
681
682 public CommandResult executeCommand(String command, int timeout) throws IOException {
683 openSession();
684
685 InputStream stdout = sshSession.getStdout();
686 InputStream stderr = sshSession.getStderr();
687
688
689
690
691 CommandResult commandResult = new CommandResult();
692
693
694 try (ByteArrayOutputStream output = new ByteArrayOutputStream()) {
695
696 long startTime = System.currentTimeMillis();
697 long timeoutTime;
698 if (timeout > 0) {
699 timeoutTime = startTime + timeout;
700 } else {
701
702 timeoutTime = Long.MAX_VALUE;
703 }
704
705
706 sshSession.execCommand(command);
707
708 int waitForCondition = 0;
709 long currentTime;
710
711 while (
712 !hasSessionClosed(waitForCondition) &&
713 !hasEndOfFileSession(waitForCondition) &&
714 ((currentTime = System.currentTimeMillis()) < timeoutTime)
715 ) {
716
717 waitForCondition = waitForNewData(Math.min(timeoutTime - currentTime, 5000));
718
719
720 if (hasStdoutData(waitForCondition)) {
721 transferAllBytes(stdout, output);
722 }
723
724 if (hasStderrData(waitForCondition)) {
725 transferAllBytes(stderr, output);
726 }
727 }
728
729
730
731 currentTime = System.currentTimeMillis();
732 if (currentTime >= timeoutTime) {
733
734
735
736 commandResult.success = false;
737 commandResult.result = "Timeout (" + timeout / 1000 + " seconds)";
738 } else {
739
740
741
742 commandResult.executionTime = (currentTime - startTime) / 1000;
743
744
745 waitForCondition = sshSession.waitForCondition(ChannelCondition.EXIT_STATUS, 5000);
746 if ((waitForCondition & ChannelCondition.EXIT_STATUS) != 0) {
747 commandResult.exitStatus = sshSession.getExitStatus();
748 }
749
750
751 commandResult.result = new String(output.toByteArray(), charset);
752 }
753 }
754
755
756 return commandResult;
757 }
758
759
760
761
762
763
764
765
766
767
768 public void interactiveSession(InputStream in, OutputStream out) throws IOException, InterruptedException {
769 openSession();
770
771 openTerminal();
772
773
774 BufferedReader inputReader = new BufferedReader(new InputStreamReader(in));
775 OutputStream outputWriter = sshSession.getStdin();
776 Thread stdinPipeThread = new Thread() {
777 @Override
778 public void run() {
779 try {
780 String line;
781 while ((line = inputReader.readLine()) != null) {
782 outputWriter.write(line.getBytes());
783 outputWriter.write('\n');
784 }
785 } catch (Exception e) {
786
787 }
788
789
790 sshSession.close();
791 }
792 };
793 stdinPipeThread.setDaemon(true);
794 stdinPipeThread.start();
795
796
797 InputStream stdout = sshSession.getStdout();
798 InputStream stderr = sshSession.getStderr();
799
800 int waitForCondition = 0;
801 while (!hasSessionClosed(waitForCondition) && !hasEndOfFileSession(waitForCondition)) {
802
803 waitForCondition = waitForNewData(5000L);
804
805
806 if (hasStdoutData(waitForCondition)) {
807 transferAllBytes(stdout, out);
808 }
809
810 if (hasStderrData(waitForCondition)) {
811 transferAllBytes(stderr, out);
812 }
813 }
814
815
816
817 if (stdinPipeThread.isAlive()) {
818 stdinPipeThread.interrupt();
819 }
820 }
821
822
823
824
825
826
827
828
829
830
831 public void scp(String localFilePath, String remoteFilename, String remoteDirectory, String fileMode)
832 throws IOException {
833 checkIfAuthenticated();
834
835
836 SCPClient scpClient = new SCPClient(sshConnection);
837
838
839 scpClient.put(localFilePath, remoteFilename, remoteDirectory, fileMode);
840 }
841
842
843
844
845
846
847 public void openSession() throws IOException {
848 checkIfConnected();
849 checkIfAuthenticated();
850
851
852 sshSession = getSshConnection().openSession();
853 }
854
855
856
857
858
859
860
861 public void openTerminal() throws IOException {
862 checkIfConnected();
863 checkIfAuthenticated();
864 checkIfSessionOpened();
865
866 getSshSession().requestPTY("dumb", 10000, 24, 640, 480, new byte[] { 53, 0, 0, 0, 0, 0 });
867 getSshSession().startShell();
868 }
869
870
871
872
873
874
875
876 public void write(final String text) throws IOException {
877 if (text == null || text.isEmpty()) {
878 return;
879 }
880
881 checkIfConnected();
882 checkIfAuthenticated();
883 checkIfSessionOpened();
884
885 Utils.checkNonNullField(charset, "charset");
886
887 final OutputStream outputStream = getSshSession().getStdin();
888 Utils.checkNonNullField(outputStream, "Stdin");
889
890
891 final String[] split = text.split("\\R", -1);
892 if (split.length == 1) {
893 outputStream.write(text.getBytes(charset));
894 } else {
895 for (int i = 0; i < split.length; i++) {
896 if (split[i].length() != 0) {
897 outputStream.write(split[i].getBytes(charset));
898 }
899
900 if (i < split.length - 1) {
901 outputStream.write('\n');
902 }
903 }
904 }
905
906 outputStream.flush();
907 }
908
909
910
911
912
913
914
915
916
917
918 public Optional<String> read(final int size, final int timeout) throws IOException {
919 Utils.checkArgumentNotZeroOrNegative(timeout, "timeout");
920
921 checkIfConnected();
922 checkIfAuthenticated();
923 checkIfSessionOpened();
924
925 Utils.checkNonNullField(charset, "charset");
926
927 final InputStream stdout = getSshSession().getStdout();
928 final InputStream stderr = getSshSession().getStderr();
929 Utils.checkNonNullField(stdout, "stdout");
930 Utils.checkNonNullField(stderr, "stderr");
931
932 try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
933
934 final int waitForCondition = waitForNewData(timeout * 1000L);
935
936 final boolean stdoutData = hasStdoutData(waitForCondition);
937 final boolean stderrData = hasStderrData(waitForCondition);
938
939
940 int stdoutRead = 0;
941 if (stdoutData) {
942 stdoutRead = transferBytes(stdout, byteArrayOutputStream, size);
943 if (size > 0 && stdoutRead >= size) {
944 return Optional.of(new String(byteArrayOutputStream.toByteArray(), charset));
945 }
946 }
947
948
949 if (stderrData) {
950 transferBytes(stderr, byteArrayOutputStream, size - stdoutRead);
951 }
952
953 return stdoutData || stderrData
954 ? Optional.of(new String(byteArrayOutputStream.toByteArray(), charset))
955 : Optional.empty();
956 }
957 }
958
959
960
961
962
963
964
965 static boolean hasTimeoutSession(final int waitForCondition) {
966 return (waitForCondition & ChannelCondition.TIMEOUT) != 0;
967 }
968
969
970
971
972
973
974
975 static boolean hasEndOfFileSession(final int waitForCondition) {
976 return (waitForCondition & ChannelCondition.EOF) != 0;
977 }
978
979
980
981
982
983
984
985 static boolean hasSessionClosed(final int waitForCondition) {
986 return (waitForCondition & ChannelCondition.CLOSED) != 0;
987 }
988
989
990
991
992
993
994
995 static boolean hasStdoutData(final int waitForCondition) {
996 return (waitForCondition & ChannelCondition.STDOUT_DATA) != 0;
997 }
998
999
1000
1001
1002
1003
1004
1005 static boolean hasStderrData(final int waitForCondition) {
1006 return (waitForCondition & ChannelCondition.STDERR_DATA) != 0;
1007 }
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019 int waitForNewData(final long timeout) {
1020 return sshSession.waitForCondition(
1021 ChannelCondition.STDOUT_DATA | ChannelCondition.STDERR_DATA | ChannelCondition.EOF | ChannelCondition.CLOSED,
1022 timeout
1023 );
1024 }
1025
1026
1027
1028
1029 void checkIfConnected() {
1030 if (getSshConnection() == null) {
1031 throw new IllegalStateException("Connection is required first");
1032 }
1033 }
1034
1035
1036
1037
1038 void checkIfAuthenticated() {
1039 if (getSshConnection() == null || !getSshConnection().isAuthenticationComplete()) {
1040 throw new IllegalStateException("Authentication is required first");
1041 }
1042 }
1043
1044
1045
1046
1047 public void checkIfSessionOpened() {
1048 if (getSshSession() == null) {
1049 throw new IllegalStateException("SSH session should be opened first");
1050 }
1051 }
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061 static int transferAllBytes(final InputStream inputStream, final OutputStream outputStream) throws IOException {
1062 return transferBytes(inputStream, outputStream, -1);
1063 }
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074 static int transferBytes(final InputStream inputStream, final OutputStream outputStream, final int size)
1075 throws IOException {
1076 final int bufferSize = size > 0 && size < READ_BUFFER_SIZE ? size : READ_BUFFER_SIZE;
1077 final byte[] buffer = new byte[bufferSize];
1078
1079 int total = 0;
1080 int bytesRead = 0;
1081
1082 while (inputStream.available() > 0 && (bytesRead = inputStream.read(buffer)) > 0) {
1083 final int bytesCopy = Math.min(bytesRead, READ_BUFFER_SIZE);
1084
1085 outputStream.write(Arrays.copyOf(buffer, bytesCopy));
1086 outputStream.flush();
1087
1088 total += bytesRead;
1089
1090 if (size > 0 && total >= size) {
1091 return total;
1092 }
1093 }
1094 return total;
1095 }
1096
1097 Connection getSshConnection() {
1098 return sshConnection;
1099 }
1100
1101 Session getSshSession() {
1102 return sshSession;
1103 }
1104 }