1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 package org.kuali.common.util.secure;
17
18 import java.io.BufferedOutputStream;
19 import java.io.ByteArrayInputStream;
20 import java.io.ByteArrayOutputStream;
21 import java.io.File;
22 import java.io.IOException;
23 import java.io.InputStream;
24 import java.io.OutputStream;
25 import java.lang.reflect.InvocationTargetException;
26 import java.util.ArrayList;
27 import java.util.List;
28 import java.util.Properties;
29
30 import org.apache.commons.beanutils.BeanUtils;
31 import org.apache.commons.io.FileUtils;
32 import org.apache.commons.io.FilenameUtils;
33 import org.apache.commons.io.IOUtils;
34 import org.apache.commons.lang3.StringUtils;
35 import org.kuali.common.util.Assert;
36 import org.kuali.common.util.CollectionUtils;
37 import org.kuali.common.util.LocationUtils;
38 import org.kuali.common.util.PropertyUtils;
39 import org.kuali.common.util.Str;
40 import org.slf4j.Logger;
41 import org.slf4j.LoggerFactory;
42
43 import com.jcraft.jsch.Channel;
44 import com.jcraft.jsch.ChannelExec;
45 import com.jcraft.jsch.ChannelSftp;
46 import com.jcraft.jsch.JSch;
47 import com.jcraft.jsch.JSchException;
48 import com.jcraft.jsch.Session;
49 import com.jcraft.jsch.SftpATTRS;
50 import com.jcraft.jsch.SftpException;
51
52 public class DefaultSecureChannel implements SecureChannel {
53
54 private static final Logger logger = LoggerFactory.getLogger(DefaultSecureChannel.class);
55 private static final String SFTP = "sftp";
56 private static final String EXEC = "exec";
57 private static final String FORWARDSLASH = "/";
58 private static final int DEFAULT_SLEEP_MILLIS = 10;
59 private static final String DEFAULT_ENCODING = "UTF-8";
60
61 File knownHosts = SSHUtils.DEFAULT_KNOWN_HOSTS;
62 File config = SSHUtils.DEFAULT_CONFIG_FILE;
63 boolean useConfigFile = true;
64 boolean includeDefaultPrivateKeyLocations = true;
65 boolean strictHostKeyChecking = true;
66 int port = SSHUtils.DEFAULT_PORT;
67 int waitForClosedSleepMillis = DEFAULT_SLEEP_MILLIS;
68 String encoding = DEFAULT_ENCODING;
69 String username;
70 String hostname;
71 Integer connectTimeout;
72 List<File> privateKeys;
73 List<String> privateKeyStrings;
74 Properties options;
75
76 protected Session session;
77 protected ChannelSftp sftp;
78
79 @Override
80 public synchronized void open() throws IOException {
81 logOpen();
82 validate();
83 try {
84 JSch jsch = getJSch();
85 this.session = openSession(jsch);
86 this.sftp = openSftpChannel(session, connectTimeout);
87 } catch (JSchException e) {
88 throw new IOException("Unexpected error opening secure channel", e);
89 }
90 }
91
92 @Override
93 public synchronized void close() {
94 logger.info("Closing secure channel [{}]", ChannelUtils.getLocation(username, hostname));
95 closeQuietly(sftp);
96 closeQuietly(session);
97 }
98
99 @Override
100 public Result executeCommand(String command) {
101 return executeCommand(command, null);
102 }
103
104 @Override
105 public Result executeCommand(String command, String stdin) {
106 Assert.notBlank(command);
107 ChannelExec exec = null;
108 InputStream stdoutStream = null;
109 ByteArrayOutputStream stderrStream = null;
110 InputStream stdinStream = null;
111 try {
112
113 long start = System.currentTimeMillis();
114
115 exec = (ChannelExec) session.openChannel(EXEC);
116
117 byte[] commandBytes = Str.getBytes(command, encoding);
118
119 exec.setCommand(commandBytes);
120
121 stdinStream = getInputStream(stdin, encoding);
122
123 stderrStream = new ByteArrayOutputStream();
124
125 stdoutStream = exec.getInputStream();
126
127 exec.setInputStream(stdinStream);
128
129 exec.setErrStream(stderrStream);
130
131
132 connect(exec, null);
133
134 String stdout = Str.getString(IOUtils.toByteArray(stdoutStream), encoding);
135 String stderr = Str.getString(stderrStream.toByteArray(), encoding);
136
137 waitForClosed(exec, waitForClosedSleepMillis);
138
139 return ChannelUtils.getExecutionResult(exec.getExitStatus(), start, command, stdin, stdout, stderr, encoding);
140 } catch (Exception e) {
141 throw new IllegalStateException(e);
142 } finally {
143
144 IOUtils.closeQuietly(stdinStream);
145 IOUtils.closeQuietly(stdoutStream);
146 IOUtils.closeQuietly(stderrStream);
147 closeQuietly(exec);
148 }
149 }
150
151 @Override
152 public void executeNoWait(String command) {
153 Assert.notBlank(command);
154 ChannelExec exec = null;
155 try {
156
157 exec = (ChannelExec) session.openChannel(EXEC);
158
159 byte[] commandBytes = Str.getBytes(command, encoding);
160
161 exec.setCommand(commandBytes);
162
163
164 connect(exec, null);
165 } catch (Exception e) {
166 throw new IllegalStateException(e);
167 } finally {
168 closeQuietly(exec);
169 }
170 }
171
172 protected InputStream getInputStream(String s, String encoding) {
173 if (s == null) {
174 return null;
175 } else {
176 return new ByteArrayInputStream(Str.getBytes(s, encoding));
177 }
178 }
179
180 protected void waitForClosed(ChannelExec exec, long millis) {
181 while (!exec.isClosed()) {
182 sleep(millis);
183 }
184 }
185
186 protected void sleep(long millis) {
187 try {
188 Thread.sleep(millis);
189 } catch (InterruptedException e) {
190 throw new IllegalStateException(e);
191 }
192 }
193
194 @Override
195 public RemoteFile getWorkingDirectory() {
196 try {
197 String workingDirectory = sftp.pwd();
198 return getMetaData(workingDirectory);
199 } catch (SftpException e) {
200 throw new IllegalStateException(e);
201 }
202 }
203
204 protected void validate() {
205 Assert.isTrue(SSHUtils.isValidPort(port));
206 Assert.notBlank(hostname);
207 Assert.notBlank(encoding);
208 }
209
210 protected void logOpen() {
211 logger.info("Opening secure channel [{}] encoding={}", ChannelUtils.getLocation(username, hostname), encoding);
212 logger.debug("Private key files - {}", CollectionUtils.toEmptyList(privateKeys).size());
213 logger.debug("Private key strings - {}", CollectionUtils.toEmptyList(privateKeyStrings).size());
214 logger.debug("Private key config file - {}", config);
215 logger.debug("Private key config file use - {}", useConfigFile);
216 logger.debug("Include default private key locations - {}", includeDefaultPrivateKeyLocations);
217 logger.debug("Known hosts file - {}", knownHosts);
218 logger.debug("Port - {}", port);
219 logger.debug("Connect timeout - {}", connectTimeout);
220 logger.debug("Strict host key checking - {}", strictHostKeyChecking);
221 logger.debug("Configuring channel with {} custom options", PropertyUtils.toEmpty(options).size());
222 if (options != null) {
223 PropertyUtils.debug(options);
224 }
225 }
226
227 protected ChannelSftp openSftpChannel(Session session, Integer timeout) throws JSchException {
228 ChannelSftp sftp = (ChannelSftp) session.openChannel(SFTP);
229 connect(sftp, timeout);
230 return sftp;
231 }
232
233 protected void connect(Channel channel, Integer timeout) throws JSchException {
234 if (timeout == null) {
235 channel.connect();
236 } else {
237 channel.connect(timeout);
238 }
239 }
240
241 protected void closeQuietly(Session session) {
242 if (session != null) {
243 session.disconnect();
244 }
245 }
246
247 protected void closeQuietly(Channel channel) {
248 if (channel != null) {
249 channel.disconnect();
250 }
251 }
252
253 protected Properties getSessionProperties(Properties options, boolean strictHostKeyChecking) {
254 Properties properties = new Properties();
255 if (options != null) {
256 properties.putAll(options);
257 }
258 if (!strictHostKeyChecking) {
259 properties.setProperty(SSHUtils.STRICT_HOST_KEY_CHECKING, SSHUtils.NO);
260 }
261 return properties;
262 }
263
264 protected Session openSession(JSch jsch) throws JSchException {
265 Session session = jsch.getSession(username, hostname, port);
266 session.setConfig(getSessionProperties(options, strictHostKeyChecking));
267 if (connectTimeout == null) {
268 session.connect();
269 } else {
270 session.connect(connectTimeout);
271 }
272 return session;
273 }
274
275 protected JSch getJSch() throws JSchException {
276 List<File> uniquePrivateKeyFiles = getUniquePrivateKeyFiles();
277 logger.debug("Located {} private keys on the file system", uniquePrivateKeyFiles.size());
278 JSch jsch = getJSch(uniquePrivateKeyFiles, privateKeyStrings);
279 if (strictHostKeyChecking && knownHosts != null) {
280 String path = LocationUtils.getCanonicalPath(knownHosts);
281 jsch.setKnownHosts(path);
282 }
283 return jsch;
284 }
285
286 protected JSch getJSch(List<File> privateKeys, List<String> privateKeyStrings) throws JSchException {
287 JSch jsch = new JSch();
288 for (File privateKey : privateKeys) {
289 String path = LocationUtils.getCanonicalPath(privateKey);
290 jsch.addIdentity(path);
291 }
292 int count = 0;
293 for (String privateKeyString : CollectionUtils.toEmptyList(privateKeyStrings)) {
294 String name = "privateKeyString-" + Integer.toString(count++);
295 byte[] bytes = Str.getBytes(privateKeyString, encoding);
296 jsch.addIdentity(name, bytes, null, null);
297 }
298 return jsch;
299 }
300
301 protected List<File> getUniquePrivateKeyFiles() {
302 List<String> paths = new ArrayList<String>();
303 if (privateKeys != null) {
304 for (File privateKey : privateKeys) {
305 paths.add(LocationUtils.getCanonicalPath(privateKey));
306 }
307 }
308 if (useConfigFile) {
309 for (String path : SSHUtils.getFilenames(config)) {
310 paths.add(path);
311 }
312 }
313 if (includeDefaultPrivateKeyLocations) {
314 for (String path : SSHUtils.PRIVATE_KEY_DEFAULTS) {
315 paths.add(path);
316 }
317 }
318 List<String> uniquePaths = CollectionUtils.getUniqueStrings(paths);
319 return SSHUtils.getExistingAndReadable(uniquePaths);
320 }
321
322 @Override
323 public RemoteFile getMetaData(String absolutePath) {
324 Assert.hasLength(absolutePath);
325 RemoteFile file = new RemoteFile();
326 file.setAbsolutePath(absolutePath);
327 fillInAttributes(file, absolutePath);
328 return file;
329 }
330
331 @Override
332 public void deleteFile(String absolutePath) {
333 RemoteFile file = getMetaData(absolutePath);
334 if (isStatus(file, Status.MISSING)) {
335 return;
336 }
337 if (file.isDirectory()) {
338 throw new IllegalArgumentException("[" + ChannelUtils.getLocation(username, hostname, file) + "] is a directory.");
339 }
340 try {
341 sftp.rm(absolutePath);
342 } catch (SftpException e) {
343 throw new IllegalStateException(e);
344 }
345 }
346
347 @Override
348 public boolean exists(String absolutePath) {
349 RemoteFile file = getMetaData(absolutePath);
350 return isStatus(file, Status.EXISTS);
351 }
352
353 @Override
354 public boolean isDirectory(String absolutePath) {
355 RemoteFile file = getMetaData(absolutePath);
356 return isStatus(file, Status.EXISTS) && file.isDirectory();
357 }
358
359 protected void fillInAttributes(RemoteFile file) {
360 fillInAttributes(file, file.getAbsolutePath());
361 }
362
363 protected void fillInAttributes(RemoteFile file, String path) {
364 try {
365 SftpATTRS attributes = sftp.stat(path);
366 fillInAttributes(file, attributes);
367 } catch (SftpException e) {
368 handleNoSuchFileException(file, e);
369 }
370 }
371
372 protected void fillInAttributes(RemoteFile file, SftpATTRS attributes) {
373 file.setDirectory(attributes.isDir());
374 file.setPermissions(attributes.getPermissions());
375 file.setUserId(attributes.getUId());
376 file.setGroupId(attributes.getGId());
377 file.setSize(attributes.getSize());
378 file.setStatus(Status.EXISTS);
379 }
380
381 @Override
382 public void copyFile(File source, RemoteFile destination) {
383 Assert.notNull(source);
384 Assert.isTrue(source.exists());
385 Assert.isTrue(!source.isDirectory());
386 Assert.isTrue(source.canRead());
387 copyLocationToFile(LocationUtils.getCanonicalURLString(source), destination);
388 }
389
390 @Override
391 public void copyFileToDirectory(File source, RemoteFile destination) {
392 RemoteFile clone = clone(destination);
393 String filename = source.getName();
394 addFilenameToPath(clone, filename);
395 copyFile(source, clone);
396 }
397
398 protected RemoteFile clone(RemoteFile file) {
399 try {
400 RemoteFile clone = new RemoteFile();
401 BeanUtils.copyProperties(clone, file);
402 return clone;
403 } catch (IllegalAccessException e) {
404 throw new IllegalStateException(e);
405 } catch (InvocationTargetException e) {
406 throw new IllegalStateException(e);
407 }
408 }
409
410 @Override
411 public void copyLocationToFile(String location, RemoteFile destination) {
412 Assert.notNull(location);
413 Assert.isTrue(LocationUtils.exists(location), location + " does not exist");
414 InputStream in = null;
415 try {
416 in = LocationUtils.getInputStream(location);
417 copyInputStreamToFile(in, destination);
418 } catch (Exception e) {
419 throw new IllegalStateException(e);
420 } finally {
421 IOUtils.closeQuietly(in);
422 }
423 }
424
425 @Override
426 public void copyStringToFile(String string, RemoteFile destination) {
427 Assert.notNull(string);
428 Assert.notBlank(encoding);
429 InputStream in = new ByteArrayInputStream(Str.getBytes(string, encoding));
430 copyInputStreamToFile(in, destination);
431 IOUtils.closeQuietly(in);
432 }
433
434 @Override
435 public String toString(RemoteFile source) {
436 Assert.notNull(source);
437 Assert.hasText(source.getAbsolutePath());
438 Assert.notBlank(encoding);
439 ByteArrayOutputStream out = new ByteArrayOutputStream();
440 try {
441 copyFile(source, out);
442 return out.toString(encoding);
443 } catch (IOException e) {
444 throw new IllegalStateException("Unexpected IO error", e);
445 } finally {
446 IOUtils.closeQuietly(out);
447 }
448 }
449
450 @Override
451 public void copyInputStreamToFile(InputStream source, RemoteFile destination) {
452 Assert.notNull(source);
453 try {
454 createDirectories(destination);
455 sftp.put(source, destination.getAbsolutePath());
456 } catch (SftpException e) {
457 throw new IllegalStateException(e);
458 }
459 }
460
461 protected String getAbsolutePath(String absolutePath, String filename) {
462 if (StringUtils.endsWith(absolutePath, FORWARDSLASH)) {
463 return absolutePath + filename;
464 } else {
465 return absolutePath + FORWARDSLASH + filename;
466 }
467 }
468
469 protected void addFilenameToPath(RemoteFile destination, String filename) {
470 String newAbsolutePath = getAbsolutePath(destination.getAbsolutePath(), filename);
471 destination.setAbsolutePath(newAbsolutePath);
472 destination.setDirectory(false);
473 }
474
475 @Override
476 public void copyLocationToDirectory(String location, RemoteFile destination) {
477 RemoteFile clone = clone(destination);
478 String filename = LocationUtils.getFilename(location);
479 addFilenameToPath(clone, filename);
480 copyLocationToFile(location, clone);
481 }
482
483 @Override
484 public void copyFile(RemoteFile source, File destination) {
485 OutputStream out = null;
486 try {
487 out = new BufferedOutputStream(FileUtils.openOutputStream(destination));
488 copyFile(source, out);
489 } catch (Exception e) {
490 throw new IllegalStateException(e);
491 } finally {
492 IOUtils.closeQuietly(out);
493 }
494 }
495
496 @Override
497 public void copyRemoteFile(String absolutePath, OutputStream out) throws IOException {
498 try {
499 sftp.get(absolutePath, out);
500 } catch (SftpException e) {
501 throw new IOException("Unexpected IO error", e);
502 }
503 }
504
505 @Override
506 public void copyFile(RemoteFile source, OutputStream out) throws IOException {
507 copyRemoteFile(source.getAbsolutePath(), out);
508 }
509
510 @Override
511 public void copyFileToDirectory(RemoteFile source, File destination) {
512 String filename = FilenameUtils.getName(source.getAbsolutePath());
513 File newDestination = new File(destination, filename);
514 copyFile(source, newDestination);
515 }
516
517 @Override
518 public void createDirectory(RemoteFile dir) {
519 Assert.isTrue(dir.isDirectory());
520 try {
521 createDirectories(dir);
522 } catch (SftpException e) {
523 throw new IllegalStateException(e);
524 }
525 }
526
527 protected void createDirectories(RemoteFile file) throws SftpException {
528 boolean directoryIndicator = file.isDirectory();
529 fillInAttributes(file);
530 validate(file, directoryIndicator);
531 List<String> directories = LocationUtils.getNormalizedPathFragments(file.getAbsolutePath(), file.isDirectory());
532 for (String directory : directories) {
533 RemoteFile parentDir = new RemoteFile(directory);
534 fillInAttributes(parentDir);
535 validate(parentDir, true);
536 if (!isStatus(parentDir, Status.EXISTS)) {
537 mkdir(parentDir);
538 }
539 }
540 }
541
542 protected boolean isStatus(RemoteFile file, Status status) {
543 return file.getStatus().equals(status);
544 }
545
546 protected void validate(RemoteFile file, Status... allowed) {
547 for (Status status : allowed) {
548 if (isStatus(file, status)) {
549 return;
550 }
551 }
552 throw new IllegalArgumentException("Invalid status - " + file.getStatus());
553 }
554
555 protected boolean validate(RemoteFile file, boolean directoryIndicator) {
556
557 validate(file, Status.MISSING, Status.EXISTS);
558
559
560 boolean missing = isStatus(file, Status.MISSING);
561 boolean exists = isStatus(file, Status.EXISTS);
562
563
564 boolean correctFileType = file.isDirectory() == directoryIndicator;
565
566
567 boolean valid = missing || exists && correctFileType;
568 if (valid) {
569 return true;
570 } else {
571
572 throw new IllegalArgumentException(getInvalidExistingFileMessage(file));
573 }
574 }
575
576 protected String getInvalidExistingFileMessage(RemoteFile existing) {
577 if (existing.isDirectory()) {
578 return "[" + ChannelUtils.getLocation(username, hostname, existing) + "] is an existing directory. Unable to create file.";
579 } else {
580 return "[" + ChannelUtils.getLocation(username, hostname, existing) + "] is an existing file. Unable to create directory.";
581 }
582 }
583
584 protected void mkdir(RemoteFile dir) {
585 try {
586 String path = dir.getAbsolutePath();
587 logger.debug("Creating [{}]", path);
588 sftp.mkdir(path);
589 setAttributes(dir);
590 } catch (SftpException e) {
591 throw new IllegalStateException(e);
592 }
593 }
594
595 protected void setAttributes(RemoteFile file) throws SftpException {
596 String path = file.getAbsolutePath();
597 if (file.getPermissions() != null) {
598 sftp.chmod(file.getPermissions(), path);
599 }
600 if (file.getGroupId() != null) {
601 sftp.chgrp(file.getGroupId(), path);
602 }
603 if (file.getUserId() != null) {
604 sftp.chown(file.getUserId(), path);
605 }
606 }
607
608 protected void handleNoSuchFileException(RemoteFile file, SftpException e) {
609 if (isNoSuchFileException(e)) {
610 file.setStatus(Status.MISSING);
611 } else {
612 throw new IllegalStateException(e);
613 }
614 }
615
616 protected boolean isNoSuchFileException(SftpException exception) {
617 return exception.id == ChannelSftp.SSH_FX_NO_SUCH_FILE;
618 }
619
620 public File getKnownHosts() {
621 return knownHosts;
622 }
623
624 public void setKnownHosts(File knownHosts) {
625 this.knownHosts = knownHosts;
626 }
627
628 public File getConfig() {
629 return config;
630 }
631
632 public void setConfig(File config) {
633 this.config = config;
634 }
635
636 public boolean isIncludeDefaultPrivateKeyLocations() {
637 return includeDefaultPrivateKeyLocations;
638 }
639
640 public void setIncludeDefaultPrivateKeyLocations(boolean includeDefaultPrivateKeyLocations) {
641 this.includeDefaultPrivateKeyLocations = includeDefaultPrivateKeyLocations;
642 }
643
644 public boolean isStrictHostKeyChecking() {
645 return strictHostKeyChecking;
646 }
647
648 public void setStrictHostKeyChecking(boolean strictHostKeyChecking) {
649 this.strictHostKeyChecking = strictHostKeyChecking;
650 }
651
652 public String getUsername() {
653 return username;
654 }
655
656 public void setUsername(String username) {
657 this.username = username;
658 }
659
660 public String getHostname() {
661 return hostname;
662 }
663
664 public void setHostname(String hostname) {
665 this.hostname = hostname;
666 }
667
668 public int getPort() {
669 return port;
670 }
671
672 public void setPort(int port) {
673 this.port = port;
674 }
675
676 public int getConnectTimeout() {
677 return connectTimeout;
678 }
679
680 public void setConnectTimeout(int connectTimeout) {
681 this.connectTimeout = connectTimeout;
682 }
683
684 public List<File> getPrivateKeys() {
685 return privateKeys;
686 }
687
688 public void setPrivateKeys(List<File> privateKeys) {
689 this.privateKeys = privateKeys;
690 }
691
692 public Properties getOptions() {
693 return options;
694 }
695
696 public void setOptions(Properties options) {
697 this.options = options;
698 }
699
700 public void setConnectTimeout(Integer connectTimeout) {
701 this.connectTimeout = connectTimeout;
702 }
703
704 public int getWaitForClosedSleepMillis() {
705 return waitForClosedSleepMillis;
706 }
707
708 public void setWaitForClosedSleepMillis(int waitForClosedSleepMillis) {
709 this.waitForClosedSleepMillis = waitForClosedSleepMillis;
710 }
711
712 public String getEncoding() {
713 return encoding;
714 }
715
716 public void setEncoding(String encoding) {
717 this.encoding = encoding;
718 }
719
720 public List<String> getPrivateKeyStrings() {
721 return privateKeyStrings;
722 }
723
724 public void setPrivateKeyStrings(List<String> privateKeyStrings) {
725 this.privateKeyStrings = privateKeyStrings;
726 }
727
728 public boolean isUseConfigFile() {
729 return useConfigFile;
730 }
731
732 public void setUseConfigFile(boolean useConfigFile) {
733 this.useConfigFile = useConfigFile;
734 }
735
736 }