View Javadoc

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  			// Preserve start time
98  			long start = System.currentTimeMillis();
99  			// Open an exec channel
100 			exec = (ChannelExec) session.openChannel(EXEC);
101 			// Convert the command string to bytes
102 			byte[] commandBytes = Str.getBytes(command, encoding);
103 			// Store the command on the exec channel
104 			exec.setCommand(commandBytes);
105 			// Prepare the stdin stream
106 			stdinStream = getInputStream(stdin, encoding);
107 			// Prepare the stderr stream
108 			stderrStream = new ByteArrayOutputStream();
109 			// Get the stdout stream from the ChannelExec object
110 			stdoutStream = exec.getInputStream();
111 			// Update the ChannelExec object with the stdin stream
112 			exec.setInputStream(stdinStream);
113 			// Update the ChannelExec object with the stderr stream
114 			exec.setErrStream(stderrStream);
115 			// Execute the command.
116 			// This consumes anything from stdin and stores output in stdout/stderr
117 			connect(exec, null);
118 			// Convert stdout and stderr to String's
119 			String stdout = Str.getString(IOUtils.toByteArray(stdoutStream), encoding);
120 			String stderr = Str.getString(stderrStream.toByteArray(), encoding);
121 			// Make sure the channel is closed
122 			waitForClosed(exec, waitForClosedSleepMillis);
123 			// Return the result of executing the command
124 			return ChannelUtils.getExecutionResult(exec.getExitStatus(), start, command, stdin, stdout, stderr, encoding);
125 		} catch (Exception e) {
126 			throw new IllegalStateException(e);
127 		} finally {
128 			// Cleanup
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 		// Make sure file is not in UNKNOWN status
491 		validate(file, Status.MISSING, Status.EXISTS);
492 
493 		// Convenience flags
494 		boolean missing = isStatus(file, Status.MISSING);
495 		boolean exists = isStatus(file, Status.EXISTS);
496 
497 		// Compare the actual file type to the file type it needs to be
498 		boolean correctFileType = file.isDirectory() == directoryIndicator;
499 
500 		// Is everything as it should be?
501 		boolean valid = missing || exists && correctFileType;
502 		if (valid) {
503 			return true;
504 		} else {
505 			// Something has gone awry
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 }