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