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 protected InputStream getInputStream(String s, String encoding) {
152 if (s == null) {
153 return null;
154 } else {
155 return new ByteArrayInputStream(Str.getBytes(s, encoding));
156 }
157 }
158
159 protected void waitForClosed(ChannelExec exec, long millis) {
160 while (!exec.isClosed()) {
161 sleep(millis);
162 }
163 }
164
165 protected void sleep(long millis) {
166 try {
167 Thread.sleep(millis);
168 } catch (InterruptedException e) {
169 throw new IllegalStateException(e);
170 }
171 }
172
173 @Override
174 public RemoteFile getWorkingDirectory() {
175 try {
176 String workingDirectory = sftp.pwd();
177 return getMetaData(workingDirectory);
178 } catch (SftpException e) {
179 throw new IllegalStateException(e);
180 }
181 }
182
183 protected void validate() {
184 Assert.isTrue(SSHUtils.isValidPort(port));
185 Assert.notBlank(hostname);
186 Assert.notBlank(encoding);
187 }
188
189 protected void logOpen() {
190 logger.info("Opening secure channel [{}] encoding={}", ChannelUtils.getLocation(username, hostname), encoding);
191 logger.debug("Private key files - {}", CollectionUtils.toEmptyList(privateKeys).size());
192 logger.debug("Private key strings - {}", CollectionUtils.toEmptyList(privateKeyStrings).size());
193 logger.debug("Private key config file - {}", config);
194 logger.debug("Private key config file use - {}", useConfigFile);
195 logger.debug("Include default private key locations - {}", includeDefaultPrivateKeyLocations);
196 logger.debug("Known hosts file - {}", knownHosts);
197 logger.debug("Port - {}", port);
198 logger.debug("Connect timeout - {}", connectTimeout);
199 logger.debug("Strict host key checking - {}", strictHostKeyChecking);
200 logger.debug("Configuring channel with {} custom options", PropertyUtils.toEmpty(options).size());
201 if (options != null) {
202 PropertyUtils.debug(options);
203 }
204 }
205
206 protected ChannelSftp openSftpChannel(Session session, Integer timeout) throws JSchException {
207 ChannelSftp sftp = (ChannelSftp) session.openChannel(SFTP);
208 connect(sftp, timeout);
209 return sftp;
210 }
211
212 protected void connect(Channel channel, Integer timeout) throws JSchException {
213 if (timeout == null) {
214 channel.connect();
215 } else {
216 channel.connect(timeout);
217 }
218 }
219
220 protected void closeQuietly(Session session) {
221 if (session != null) {
222 session.disconnect();
223 }
224 }
225
226 protected void closeQuietly(Channel channel) {
227 if (channel != null) {
228 channel.disconnect();
229 }
230 }
231
232 protected Properties getSessionProperties(Properties options, boolean strictHostKeyChecking) {
233 Properties properties = new Properties();
234 if (options != null) {
235 properties.putAll(options);
236 }
237 if (!strictHostKeyChecking) {
238 properties.setProperty(SSHUtils.STRICT_HOST_KEY_CHECKING, SSHUtils.NO);
239 }
240 return properties;
241 }
242
243 protected Session openSession(JSch jsch) throws JSchException {
244 Session session = jsch.getSession(username, hostname, port);
245 session.setConfig(getSessionProperties(options, strictHostKeyChecking));
246 if (connectTimeout == null) {
247 session.connect();
248 } else {
249 session.connect(connectTimeout);
250 }
251 return session;
252 }
253
254 protected JSch getJSch() throws JSchException {
255 List<File> uniquePrivateKeyFiles = getUniquePrivateKeyFiles();
256 logger.debug("Located {} private keys on the file system", uniquePrivateKeyFiles.size());
257 JSch jsch = getJSch(uniquePrivateKeyFiles, privateKeyStrings);
258 if (strictHostKeyChecking && knownHosts != null) {
259 String path = LocationUtils.getCanonicalPath(knownHosts);
260 jsch.setKnownHosts(path);
261 }
262 return jsch;
263 }
264
265 protected JSch getJSch(List<File> privateKeys, List<String> privateKeyStrings) throws JSchException {
266 JSch jsch = new JSch();
267 for (File privateKey : privateKeys) {
268 String path = LocationUtils.getCanonicalPath(privateKey);
269 jsch.addIdentity(path);
270 }
271 int count = 0;
272 for (String privateKeyString : CollectionUtils.toEmptyList(privateKeyStrings)) {
273 String name = "privateKeyString-" + Integer.toString(count++);
274 byte[] bytes = Str.getBytes(privateKeyString, encoding);
275 jsch.addIdentity(name, bytes, null, null);
276 }
277 return jsch;
278 }
279
280 protected List<File> getUniquePrivateKeyFiles() {
281 List<String> paths = new ArrayList<String>();
282 if (privateKeys != null) {
283 for (File privateKey : privateKeys) {
284 paths.add(LocationUtils.getCanonicalPath(privateKey));
285 }
286 }
287 if (useConfigFile) {
288 for (String path : SSHUtils.getFilenames(config)) {
289 paths.add(path);
290 }
291 }
292 if (includeDefaultPrivateKeyLocations) {
293 for (String path : SSHUtils.PRIVATE_KEY_DEFAULTS) {
294 paths.add(path);
295 }
296 }
297 List<String> uniquePaths = CollectionUtils.getUniqueStrings(paths);
298 return SSHUtils.getExistingAndReadable(uniquePaths);
299 }
300
301 @Override
302 public RemoteFile getMetaData(String absolutePath) {
303 Assert.hasLength(absolutePath);
304 RemoteFile file = new RemoteFile();
305 file.setAbsolutePath(absolutePath);
306 fillInAttributes(file, absolutePath);
307 return file;
308 }
309
310 @Override
311 public void deleteFile(String absolutePath) {
312 RemoteFile file = getMetaData(absolutePath);
313 if (isStatus(file, Status.MISSING)) {
314 return;
315 }
316 if (file.isDirectory()) {
317 throw new IllegalArgumentException("[" + ChannelUtils.getLocation(username, hostname, file) + "] is a directory.");
318 }
319 try {
320 sftp.rm(absolutePath);
321 } catch (SftpException e) {
322 throw new IllegalStateException(e);
323 }
324 }
325
326 @Override
327 public boolean exists(String absolutePath) {
328 RemoteFile file = getMetaData(absolutePath);
329 return isStatus(file, Status.EXISTS);
330 }
331
332 @Override
333 public boolean isDirectory(String absolutePath) {
334 RemoteFile file = getMetaData(absolutePath);
335 return isStatus(file, Status.EXISTS) && file.isDirectory();
336 }
337
338 protected void fillInAttributes(RemoteFile file) {
339 fillInAttributes(file, file.getAbsolutePath());
340 }
341
342 protected void fillInAttributes(RemoteFile file, String path) {
343 try {
344 SftpATTRS attributes = sftp.stat(path);
345 fillInAttributes(file, attributes);
346 } catch (SftpException e) {
347 handleNoSuchFileException(file, e);
348 }
349 }
350
351 protected void fillInAttributes(RemoteFile file, SftpATTRS attributes) {
352 file.setDirectory(attributes.isDir());
353 file.setPermissions(attributes.getPermissions());
354 file.setUserId(attributes.getUId());
355 file.setGroupId(attributes.getGId());
356 file.setSize(attributes.getSize());
357 file.setStatus(Status.EXISTS);
358 }
359
360 @Override
361 public void copyFile(File source, RemoteFile destination) {
362 Assert.notNull(source);
363 Assert.isTrue(source.exists());
364 Assert.isTrue(!source.isDirectory());
365 Assert.isTrue(source.canRead());
366 copyLocationToFile(LocationUtils.getCanonicalURLString(source), destination);
367 }
368
369 @Override
370 public void copyFileToDirectory(File source, RemoteFile destination) {
371 RemoteFile clone = clone(destination);
372 String filename = source.getName();
373 addFilenameToPath(clone, filename);
374 copyFile(source, clone);
375 }
376
377 protected RemoteFile clone(RemoteFile file) {
378 try {
379 RemoteFile clone = new RemoteFile();
380 BeanUtils.copyProperties(clone, file);
381 return clone;
382 } catch (IllegalAccessException e) {
383 throw new IllegalStateException(e);
384 } catch (InvocationTargetException e) {
385 throw new IllegalStateException(e);
386 }
387 }
388
389 @Override
390 public void copyLocationToFile(String location, RemoteFile destination) {
391 Assert.notNull(location);
392 Assert.isTrue(LocationUtils.exists(location), location + " does not exist");
393 InputStream in = null;
394 try {
395 in = LocationUtils.getInputStream(location);
396 copyInputStreamToFile(in, destination);
397 } catch (Exception e) {
398 throw new IllegalStateException(e);
399 } finally {
400 IOUtils.closeQuietly(in);
401 }
402 }
403
404 @Override
405 public void copyStringToFile(String string, RemoteFile destination) {
406 Assert.notNull(string);
407 Assert.notBlank(encoding);
408 InputStream in = new ByteArrayInputStream(Str.getBytes(string, encoding));
409 copyInputStreamToFile(in, destination);
410 IOUtils.closeQuietly(in);
411 }
412
413 @Override
414 public void copyInputStreamToFile(InputStream source, RemoteFile destination) {
415 Assert.notNull(source);
416 try {
417 createDirectories(destination);
418 sftp.put(source, destination.getAbsolutePath());
419 } catch (SftpException e) {
420 throw new IllegalStateException(e);
421 }
422 }
423
424 protected String getAbsolutePath(String absolutePath, String filename) {
425 if (StringUtils.endsWith(absolutePath, FORWARDSLASH)) {
426 return absolutePath + filename;
427 } else {
428 return absolutePath + FORWARDSLASH + filename;
429 }
430 }
431
432 protected void addFilenameToPath(RemoteFile destination, String filename) {
433 String newAbsolutePath = getAbsolutePath(destination.getAbsolutePath(), filename);
434 destination.setAbsolutePath(newAbsolutePath);
435 destination.setDirectory(false);
436 }
437
438 @Override
439 public void copyLocationToDirectory(String location, RemoteFile destination) {
440 RemoteFile clone = clone(destination);
441 String filename = LocationUtils.getFilename(location);
442 addFilenameToPath(clone, filename);
443 copyLocationToFile(location, clone);
444 }
445
446 @Override
447 public void copyFile(RemoteFile source, File destination) {
448 OutputStream out = null;
449 try {
450 out = new BufferedOutputStream(FileUtils.openOutputStream(destination));
451 sftp.get(source.getAbsolutePath(), out);
452 } catch (Exception e) {
453 throw new IllegalStateException(e);
454 } finally {
455 IOUtils.closeQuietly(out);
456 }
457 }
458
459 @Override
460 public void copyFileToDirectory(RemoteFile source, File destination) {
461 String filename = FilenameUtils.getName(source.getAbsolutePath());
462 File newDestination = new File(destination, filename);
463 copyFile(source, newDestination);
464 }
465
466 @Override
467 public void createDirectory(RemoteFile dir) {
468 Assert.isTrue(dir.isDirectory());
469 try {
470 createDirectories(dir);
471 } catch (SftpException e) {
472 throw new IllegalStateException(e);
473 }
474 }
475
476 protected void createDirectories(RemoteFile file) throws SftpException {
477 boolean directoryIndicator = file.isDirectory();
478 fillInAttributes(file);
479 validate(file, directoryIndicator);
480 List<String> directories = LocationUtils.getNormalizedPathFragments(file.getAbsolutePath(), file.isDirectory());
481 for (String directory : directories) {
482 RemoteFile parentDir = new RemoteFile(directory);
483 fillInAttributes(parentDir);
484 validate(parentDir, true);
485 if (!isStatus(parentDir, Status.EXISTS)) {
486 mkdir(parentDir);
487 }
488 }
489 }
490
491 protected boolean isStatus(RemoteFile file, Status status) {
492 return file.getStatus().equals(status);
493 }
494
495 protected void validate(RemoteFile file, Status... allowed) {
496 for (Status status : allowed) {
497 if (isStatus(file, status)) {
498 return;
499 }
500 }
501 throw new IllegalArgumentException("Invalid status - " + file.getStatus());
502 }
503
504 protected boolean validate(RemoteFile file, boolean directoryIndicator) {
505
506 validate(file, Status.MISSING, Status.EXISTS);
507
508
509 boolean missing = isStatus(file, Status.MISSING);
510 boolean exists = isStatus(file, Status.EXISTS);
511
512
513 boolean correctFileType = file.isDirectory() == directoryIndicator;
514
515
516 boolean valid = missing || exists && correctFileType;
517 if (valid) {
518 return true;
519 } else {
520
521 throw new IllegalArgumentException(getInvalidExistingFileMessage(file));
522 }
523 }
524
525 protected String getInvalidExistingFileMessage(RemoteFile existing) {
526 if (existing.isDirectory()) {
527 return "[" + ChannelUtils.getLocation(username, hostname, existing) + "] is an existing directory. Unable to create file.";
528 } else {
529 return "[" + ChannelUtils.getLocation(username, hostname, existing) + "] is an existing file. Unable to create directory.";
530 }
531 }
532
533 protected void mkdir(RemoteFile dir) {
534 try {
535 String path = dir.getAbsolutePath();
536 logger.debug("Creating [{}]", path);
537 sftp.mkdir(path);
538 setAttributes(dir);
539 } catch (SftpException e) {
540 throw new IllegalStateException(e);
541 }
542 }
543
544 protected void setAttributes(RemoteFile file) throws SftpException {
545 String path = file.getAbsolutePath();
546 if (file.getPermissions() != null) {
547 sftp.chmod(file.getPermissions(), path);
548 }
549 if (file.getGroupId() != null) {
550 sftp.chgrp(file.getGroupId(), path);
551 }
552 if (file.getUserId() != null) {
553 sftp.chown(file.getUserId(), path);
554 }
555 }
556
557 protected void handleNoSuchFileException(RemoteFile file, SftpException e) {
558 if (isNoSuchFileException(e)) {
559 file.setStatus(Status.MISSING);
560 } else {
561 throw new IllegalStateException(e);
562 }
563 }
564
565 protected boolean isNoSuchFileException(SftpException exception) {
566 return exception.id == ChannelSftp.SSH_FX_NO_SUCH_FILE;
567 }
568
569 public File getKnownHosts() {
570 return knownHosts;
571 }
572
573 public void setKnownHosts(File knownHosts) {
574 this.knownHosts = knownHosts;
575 }
576
577 public File getConfig() {
578 return config;
579 }
580
581 public void setConfig(File config) {
582 this.config = config;
583 }
584
585 public boolean isIncludeDefaultPrivateKeyLocations() {
586 return includeDefaultPrivateKeyLocations;
587 }
588
589 public void setIncludeDefaultPrivateKeyLocations(boolean includeDefaultPrivateKeyLocations) {
590 this.includeDefaultPrivateKeyLocations = includeDefaultPrivateKeyLocations;
591 }
592
593 public boolean isStrictHostKeyChecking() {
594 return strictHostKeyChecking;
595 }
596
597 public void setStrictHostKeyChecking(boolean strictHostKeyChecking) {
598 this.strictHostKeyChecking = strictHostKeyChecking;
599 }
600
601 public String getUsername() {
602 return username;
603 }
604
605 public void setUsername(String username) {
606 this.username = username;
607 }
608
609 public String getHostname() {
610 return hostname;
611 }
612
613 public void setHostname(String hostname) {
614 this.hostname = hostname;
615 }
616
617 public int getPort() {
618 return port;
619 }
620
621 public void setPort(int port) {
622 this.port = port;
623 }
624
625 public int getConnectTimeout() {
626 return connectTimeout;
627 }
628
629 public void setConnectTimeout(int connectTimeout) {
630 this.connectTimeout = connectTimeout;
631 }
632
633 public List<File> getPrivateKeys() {
634 return privateKeys;
635 }
636
637 public void setPrivateKeys(List<File> privateKeys) {
638 this.privateKeys = privateKeys;
639 }
640
641 public Properties getOptions() {
642 return options;
643 }
644
645 public void setOptions(Properties options) {
646 this.options = options;
647 }
648
649 public void setConnectTimeout(Integer connectTimeout) {
650 this.connectTimeout = connectTimeout;
651 }
652
653 public int getWaitForClosedSleepMillis() {
654 return waitForClosedSleepMillis;
655 }
656
657 public void setWaitForClosedSleepMillis(int waitForClosedSleepMillis) {
658 this.waitForClosedSleepMillis = waitForClosedSleepMillis;
659 }
660
661 public String getEncoding() {
662 return encoding;
663 }
664
665 public void setEncoding(String encoding) {
666 this.encoding = encoding;
667 }
668
669 public List<String> getPrivateKeyStrings() {
670 return privateKeyStrings;
671 }
672
673 public void setPrivateKeyStrings(List<String> privateKeyStrings) {
674 this.privateKeyStrings = privateKeyStrings;
675 }
676
677 public boolean isUseConfigFile() {
678 return useConfigFile;
679 }
680
681 public void setUseConfigFile(boolean useConfigFile) {
682 this.useConfigFile = useConfigFile;
683 }
684
685 }