1
2
3
4
5
6 package org.sentrysoftware.ssh;
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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
55
56
57
58
59
60
61
62
63
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
78
79 private Session sshSession = null;
80
81
82
83
84
85
86 public SshClient(String pHostname) {
87 this(pHostname, "");
88 }
89
90
91
92
93
94
95
96 public SshClient(String pHostname, String pLocale) {
97 this(pHostname, Utils.getCharsetFromLocale(pLocale));
98 }
99
100
101
102
103
104
105
106 public SshClient(String pHostname, Charset pCharset) {
107 hostname = pHostname;
108 charset = pCharset;
109 }
110
111
112
113
114
115
116 public void connect() throws IOException {
117 connect(0);
118 }
119
120
121
122
123
124
125
126 public void connect(final int timeout) throws IOException {
127 this.connect(timeout, 22);
128 }
129
130
131
132
133
134
135
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
144
145
146
147
148
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
162
163
164
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
179
180
181
182
183
184
185
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
197
198
199
200
201
202
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
218
219
220
221
222
223
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
232
233
234
235
236
237
238 public boolean authenticate(String username, char[] password) throws IOException {
239
240
241
242 if (sshConnection.isAuthMethodAvailable(username, "password") &&
243 sshConnection.authenticateWithPassword(username, password != null ? String.valueOf(password) : null)) {
244 return true;
245 }
246
247
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
255 String[] challengeResponse = new String[numPrompts];
256 for (int i = 0; i < numPrompts; i++) {
257
258
259
260
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
273 return false;
274 }
275
276
277
278
279
280
281
282
283 public boolean authenticate(String username) throws IOException {
284 return sshConnection.authenticateWithNone(username);
285 }
286
287
288
289
290
291
292
293
294
295 public String readFileAttributes(String filePath) throws IOException {
296
297
298 checkIfAuthenticated();
299
300
301 SFTPv3Client sftpClient = new SFTPv3Client(sshConnection);
302
303
304 SFTPv3FileAttributes fileAttributes = sftpClient.stat(filePath);
305
306
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
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
330 sftpClient.close();
331
332
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
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
360 continue;
361 }
362
363 if (((fileAttributes.permissions & 0100000) == 0100000) || ((fileAttributes.permissions & 0060000) == 0060000) || ((fileAttributes.permissions & 0020000) == 0020000)
364 || ((fileAttributes.permissions & 0140000) == 0140000)) {
365
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
382 if (includeSubfolders)
383 resultBuilder = listSubDirectory(sftpClient, filePath, fileMaskPattern, includeSubfolders, depth, resultBuilder);
384 }
385 }
386 }
387
388 return resultBuilder;
389 }
390
391
392
393
394
395
396
397
398
399
400
401
402
403 public String listFiles(String remoteDirectoryPath, String regExpFileMask, boolean includeSubfolders) throws IOException {
404
405 checkIfAuthenticated();
406
407
408 SFTPv3Client sftpClient = new SFTPv3Client(sshConnection);
409
410
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
419 StringBuilder resultBuilder = new StringBuilder();
420 listSubDirectory(sftpClient, remoteDirectoryPath, fileMaskPattern, includeSubfolders, 1, resultBuilder);
421
422
423 sftpClient.close();
424
425
426 return resultBuilder.toString();
427 }
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446 public String readFile(String remoteFilePath, Long readOffset, Integer readSize) throws IOException {
447
448 checkIfAuthenticated();
449
450
451 SFTPv3Client sftpClient = new SFTPv3Client(sshConnection);
452
453
454 long offset = 0;
455 if (readOffset != null)
456 offset = readOffset;
457
458
459 SFTPv3FileHandle handle = sftpClient.openFileRO(remoteFilePath);
460
461
462 int remainingBytes;
463 if (readSize == null) {
464
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
478 OutputStream out = new ByteArrayOutputStream();
479 byte[] readBuffer = new byte[READ_BUFFER_SIZE];
480 int bytesRead;
481 int bufferSize;
482
483
484 while (remainingBytes > 0) {
485
486
487
488 if (remainingBytes < READ_BUFFER_SIZE) {
489 bufferSize = remainingBytes;
490 } else {
491 bufferSize = READ_BUFFER_SIZE;
492 }
493
494
495 bytesRead = sftpClient.read(handle, offset, readBuffer, 0, bufferSize);
496
497
498
499 if (bytesRead < 0) {
500 break;
501 }
502
503
504 out.write(readBuffer, 0, bytesRead);
505
506
507 remainingBytes -= bytesRead;
508 offset += bytesRead;
509 }
510
511
512
513 sftpClient.closeFile(handle);
514
515
516 sftpClient.close();
517
518
519 return out.toString();
520 }
521
522
523
524
525
526
527
528
529 public void removeFile(String[] remoteFilePathArray) throws IOException {
530
531 checkIfAuthenticated();
532
533
534 SFTPv3Client sftpClient = null;
535 try {
536 sftpClient = new SFTPv3Client(sshConnection);
537
538
539 for (String remoteFilePath : remoteFilePathArray) {
540 sftpClient.rm(remoteFilePath);
541 }
542
543 } catch (IOException e) {
544
545
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
564 if (sftpClient != null) {
565 sftpClient.close();
566 }
567 }
568
569 }
570
571
572
573
574
575
576
577
578 public void removeFile(String remoteFilePath) throws IOException {
579 removeFile(new String[] { remoteFilePath });
580 }
581
582
583
584
585
586
587
588 public static class CommandResult {
589
590
591
592 public boolean success = true;
593
594
595
596
597
598 public float executionTime = 0;
599
600
601
602
603
604 public Integer exitStatus = null;
605
606
607
608
609 public String result = "";
610
611 }
612
613
614
615
616
617
618
619
620
621 public CommandResult executeCommand(String command) throws IOException {
622 return executeCommand(command, 0);
623 }
624
625
626
627
628
629
630
631
632
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
641
642
643
644 CommandResult commandResult = new CommandResult();
645
646
647 try (final ByteArrayOutputStream output = new ByteArrayOutputStream()) {
648
649
650 long startTime = System.currentTimeMillis();
651 long timeoutTime;
652 if (timeout > 0) {
653 timeoutTime = startTime + timeout;
654 } else {
655
656 timeoutTime = Long.MAX_VALUE;
657 }
658
659
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
669 waitForCondition = waitForNewData(Math.min(timeoutTime - currentTime, 5000));
670
671
672 if (hasStdoutData(waitForCondition)) {
673 transferAllBytes(stdout, output);
674 }
675
676 if (hasStderrData(waitForCondition)) {
677 transferAllBytes(stderr, output);
678 }
679
680 }
681
682
683 currentTime = System.currentTimeMillis();
684 if (currentTime >= timeoutTime) {
685
686
687
688
689 commandResult.success = false;
690 commandResult.result = "Timeout (" + timeout / 1000 + " seconds)";
691
692 } else {
693
694
695
696
697 commandResult.executionTime = (currentTime - startTime) / 1000;
698
699
700 waitForCondition = sshSession.waitForCondition(ChannelCondition.EXIT_STATUS, 5000);
701 if ((waitForCondition & ChannelCondition.EXIT_STATUS) != 0) {
702 commandResult.exitStatus = sshSession.getExitStatus();
703 }
704
705
706 commandResult.result = new String(output.toByteArray(), charset);
707
708 }
709 }
710
711
712 return commandResult;
713 }
714
715
716
717
718
719
720
721
722
723
724 public void interactiveSession(InputStream in, OutputStream out) throws IOException, InterruptedException {
725
726 openSession();
727
728 openTerminal();
729
730
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
744 }
745
746
747 sshSession.close();
748 }
749 };
750 stdinPipeThread.setDaemon(true);
751 stdinPipeThread.start();
752
753
754 InputStream stdout = sshSession.getStdout();
755 InputStream stderr = sshSession.getStderr();
756
757 int waitForCondition = 0;
758 while (!hasSessionClosed(waitForCondition) && !hasEndOfFileSession(waitForCondition)) {
759
760
761 waitForCondition = waitForNewData(5000L);
762
763
764 if (hasStdoutData(waitForCondition)) {
765 transferAllBytes(stdout, out);
766 }
767
768 if (hasStderrData(waitForCondition)) {
769 transferAllBytes(stderr, out);
770 }
771
772 }
773
774
775
776 if (stdinPipeThread.isAlive()) {
777 stdinPipeThread.interrupt();
778 }
779 }
780
781
782
783
784
785
786
787
788
789
790 public void scp(String localFilePath, String remoteFilename, String remoteDirectory, String fileMode) throws IOException {
791
792 checkIfAuthenticated();
793
794
795 SCPClient scpClient = new SCPClient(sshConnection);
796
797
798 scpClient.put(localFilePath, remoteFilename, remoteDirectory, fileMode);
799
800 }
801
802
803
804
805
806
807 public void openSession() throws IOException {
808
809 checkIfConnected();
810 checkIfAuthenticated();
811
812
813 sshSession = getSshConnection().openSession();
814 }
815
816
817
818
819
820
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
834
835
836
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
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
874
875
876
877
878
879
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
899 final int waitForCondition = waitForNewData(timeout * 1000L);
900
901 final boolean stdoutData = hasStdoutData(waitForCondition);
902 final boolean stderrData = hasStderrData(waitForCondition);
903
904
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
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
926
927
928
929
930 static boolean hasTimeoutSession(final int waitForCondition) {
931 return (waitForCondition & ChannelCondition.TIMEOUT) != 0;
932 }
933
934
935
936
937
938
939
940 static boolean hasEndOfFileSession(final int waitForCondition) {
941 return (waitForCondition & ChannelCondition.EOF) != 0;
942 }
943
944
945
946
947
948
949
950 static boolean hasSessionClosed(final int waitForCondition) {
951 return (waitForCondition & ChannelCondition.CLOSED) != 0;
952 }
953
954
955
956
957
958
959
960 static boolean hasStdoutData(final int waitForCondition) {
961 return (waitForCondition & ChannelCondition.STDOUT_DATA) != 0;
962 }
963
964
965
966
967
968
969
970 static boolean hasStderrData(final int waitForCondition) {
971 return (waitForCondition & ChannelCondition.STDERR_DATA) != 0;
972 }
973
974
975
976
977
978
979
980
981
982
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
995
996 void checkIfConnected() {
997 if (getSshConnection() == null) {
998 throw new IllegalStateException("Connection is required first");
999 }
1000 }
1001
1002
1003
1004
1005 void checkIfAuthenticated() {
1006 if (!getSshConnection().isAuthenticationComplete()) {
1007 throw new IllegalStateException("Authentication is required first");
1008 }
1009 }
1010
1011
1012
1013
1014 public void checkIfSessionOpened() {
1015 if (getSshSession() == null) {
1016 throw new IllegalStateException("SSH session should be opened first");
1017 }
1018 }
1019
1020
1021
1022
1023
1024
1025
1026
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
1037
1038
1039
1040
1041
1042
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 }