001 /**
002 * Copyright 2010-2013 The Kuali Foundation
003 *
004 * Licensed under the Educational Community License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.opensource.org/licenses/ecl2.php
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016 package org.kuali.common.util.secure;
017
018 import java.io.BufferedOutputStream;
019 import java.io.ByteArrayInputStream;
020 import java.io.ByteArrayOutputStream;
021 import java.io.File;
022 import java.io.IOException;
023 import java.io.InputStream;
024 import java.io.OutputStream;
025 import java.lang.reflect.InvocationTargetException;
026 import java.util.ArrayList;
027 import java.util.List;
028 import java.util.Properties;
029
030 import org.apache.commons.beanutils.BeanUtils;
031 import org.apache.commons.io.FileUtils;
032 import org.apache.commons.io.FilenameUtils;
033 import org.apache.commons.io.IOUtils;
034 import org.apache.commons.lang3.StringUtils;
035 import org.kuali.common.util.Assert;
036 import org.kuali.common.util.CollectionUtils;
037 import org.kuali.common.util.LocationUtils;
038 import org.kuali.common.util.PropertyUtils;
039 import org.kuali.common.util.Str;
040 import org.slf4j.Logger;
041 import org.slf4j.LoggerFactory;
042
043 import com.jcraft.jsch.Channel;
044 import com.jcraft.jsch.ChannelExec;
045 import com.jcraft.jsch.ChannelSftp;
046 import com.jcraft.jsch.JSch;
047 import com.jcraft.jsch.JSchException;
048 import com.jcraft.jsch.Session;
049 import com.jcraft.jsch.SftpATTRS;
050 import com.jcraft.jsch.SftpException;
051
052 public class DefaultSecureChannel implements SecureChannel {
053
054 private static final Logger logger = LoggerFactory.getLogger(DefaultSecureChannel.class);
055 private static final String SFTP = "sftp";
056 private static final String EXEC = "exec";
057 private static final String FORWARDSLASH = "/";
058 private static final int DEFAULT_SLEEP_MILLIS = 10;
059 private static final String DEFAULT_ENCODING = "UTF-8";
060
061 File knownHosts = SSHUtils.DEFAULT_KNOWN_HOSTS;
062 File config = SSHUtils.DEFAULT_CONFIG_FILE;
063 boolean useConfigFile = true;
064 boolean includeDefaultPrivateKeyLocations = true;
065 boolean strictHostKeyChecking = true;
066 int port = SSHUtils.DEFAULT_PORT;
067 int waitForClosedSleepMillis = DEFAULT_SLEEP_MILLIS;
068 String encoding = DEFAULT_ENCODING;
069 String username;
070 String hostname;
071 Integer connectTimeout;
072 List<File> privateKeys;
073 List<String> privateKeyStrings;
074 Properties options;
075
076 protected Session session;
077 protected ChannelSftp sftp;
078
079 @Override
080 public synchronized void open() throws IOException {
081 logOpen();
082 validate();
083 try {
084 JSch jsch = getJSch();
085 this.session = openSession(jsch);
086 this.sftp = openSftpChannel(session, connectTimeout);
087 } catch (JSchException e) {
088 throw new IOException("Unexpected error opening secure channel", e);
089 }
090 }
091
092 @Override
093 public synchronized void close() {
094 logger.info("Closing secure channel [{}]", ChannelUtils.getLocation(username, hostname));
095 closeQuietly(sftp);
096 closeQuietly(session);
097 }
098
099 @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 // Preserve start time
113 long start = System.currentTimeMillis();
114 // Open an exec channel
115 exec = (ChannelExec) session.openChannel(EXEC);
116 // Convert the command string to bytes
117 byte[] commandBytes = Str.getBytes(command, encoding);
118 // Store the command on the exec channel
119 exec.setCommand(commandBytes);
120 // Prepare the stdin stream
121 stdinStream = getInputStream(stdin, encoding);
122 // Prepare the stderr stream
123 stderrStream = new ByteArrayOutputStream();
124 // Get the stdout stream from the ChannelExec object
125 stdoutStream = exec.getInputStream();
126 // Update the ChannelExec object with the stdin stream
127 exec.setInputStream(stdinStream);
128 // Update the ChannelExec object with the stderr stream
129 exec.setErrStream(stderrStream);
130 // Execute the command.
131 // This consumes anything from stdin and stores output in stdout/stderr
132 connect(exec, null);
133 // Convert stdout and stderr to String's
134 String stdout = Str.getString(IOUtils.toByteArray(stdoutStream), encoding);
135 String stderr = Str.getString(stderrStream.toByteArray(), encoding);
136 // Make sure the channel is closed
137 waitForClosed(exec, waitForClosedSleepMillis);
138 // Return the result of executing the command
139 return ChannelUtils.getExecutionResult(exec.getExitStatus(), start, command, stdin, stdout, stderr, encoding);
140 } catch (Exception e) {
141 throw new IllegalStateException(e);
142 } finally {
143 // Cleanup
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 // Open an exec channel
157 exec = (ChannelExec) session.openChannel(EXEC);
158 // Convert the command string to bytes
159 byte[] commandBytes = Str.getBytes(command, encoding);
160 // Store the command on the exec channel
161 exec.setCommand(commandBytes);
162 // Execute the command.
163 // This consumes anything from stdin and stores output in stdout/stderr
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 // Make sure file is not in UNKNOWN status
557 validate(file, Status.MISSING, Status.EXISTS);
558
559 // Convenience flags
560 boolean missing = isStatus(file, Status.MISSING);
561 boolean exists = isStatus(file, Status.EXISTS);
562
563 // Compare the actual file type to the file type it needs to be
564 boolean correctFileType = file.isDirectory() == directoryIndicator;
565
566 // Is everything as it should be?
567 boolean valid = missing || exists && correctFileType;
568 if (valid) {
569 return true;
570 } else {
571 // Something has gone awry
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 }