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