View Javadoc

1   /**
2    * Copyright 2010-2013 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
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 			// 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 	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 		// Make sure file is not in UNKNOWN status
506 		validate(file, Status.MISSING, Status.EXISTS);
507 
508 		// Convenience flags
509 		boolean missing = isStatus(file, Status.MISSING);
510 		boolean exists = isStatus(file, Status.EXISTS);
511 
512 		// Compare the actual file type to the file type it needs to be
513 		boolean correctFileType = file.isDirectory() == directoryIndicator;
514 
515 		// Is everything as it should be?
516 		boolean valid = missing || exists && correctFileType;
517 		if (valid) {
518 			return true;
519 		} else {
520 			// Something has gone awry
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 }