001    package org.kuali.common.util.secure;
002    
003    import java.io.BufferedOutputStream;
004    import java.io.ByteArrayInputStream;
005    import java.io.ByteArrayOutputStream;
006    import java.io.File;
007    import java.io.IOException;
008    import java.io.InputStream;
009    import java.io.OutputStream;
010    import java.lang.reflect.InvocationTargetException;
011    import java.util.ArrayList;
012    import java.util.List;
013    import java.util.Properties;
014    
015    import org.apache.commons.beanutils.BeanUtils;
016    import org.apache.commons.io.FileUtils;
017    import org.apache.commons.io.FilenameUtils;
018    import org.apache.commons.io.IOUtils;
019    import org.apache.commons.lang3.StringUtils;
020    import org.kuali.common.util.Assert;
021    import org.kuali.common.util.CollectionUtils;
022    import org.kuali.common.util.LocationUtils;
023    import org.kuali.common.util.PropertyUtils;
024    import org.kuali.common.util.Str;
025    import org.slf4j.Logger;
026    import org.slf4j.LoggerFactory;
027    
028    import com.jcraft.jsch.Channel;
029    import com.jcraft.jsch.ChannelExec;
030    import com.jcraft.jsch.ChannelSftp;
031    import com.jcraft.jsch.JSch;
032    import com.jcraft.jsch.JSchException;
033    import com.jcraft.jsch.Session;
034    import com.jcraft.jsch.SftpATTRS;
035    import com.jcraft.jsch.SftpException;
036    
037    public class DefaultSecureChannel implements SecureChannel {
038    
039            private static final Logger logger = LoggerFactory.getLogger(DefaultSecureChannel.class);
040            private static final String SFTP = "sftp";
041            private static final String EXEC = "exec";
042            private static final String FORWARDSLASH = "/";
043            private static final int DEFAULT_SLEEP_MILLIS = 10;
044            private static final String DEFAULT_ENCODING = "UTF-8";
045    
046            File knownHosts = SSHUtils.DEFAULT_KNOWN_HOSTS;
047            File config = SSHUtils.DEFAULT_CONFIG_FILE;
048            boolean useConfigFile = true;
049            boolean includeDefaultPrivateKeyLocations = true;
050            boolean strictHostKeyChecking = true;
051            int port = SSHUtils.DEFAULT_PORT;
052            int waitForClosedSleepMillis = DEFAULT_SLEEP_MILLIS;
053            String encoding = DEFAULT_ENCODING;
054            String username;
055            String hostname;
056            Integer connectTimeout;
057            List<File> privateKeys;
058            List<String> privateKeyStrings;
059            Properties options;
060    
061            protected Session session;
062            protected ChannelSftp sftp;
063    
064            @Override
065            public synchronized void open() throws IOException {
066                    logOpen();
067                    validate();
068                    try {
069                            JSch jsch = getJSch();
070                            this.session = openSession(jsch);
071                            this.sftp = openSftpChannel(session, connectTimeout);
072                    } catch (JSchException e) {
073                            throw new IOException("Unexpected error opening secure channel", e);
074                    }
075            }
076    
077            @Override
078            public synchronized void close() {
079                    logger.info("Closing secure channel [{}]", ChannelUtils.getLocation(username, hostname));
080                    closeQuietly(sftp);
081                    closeQuietly(session);
082            }
083    
084            @Override
085            public Result executeCommand(String command) {
086                    return executeCommand(command, null);
087            }
088    
089            @Override
090            public Result executeCommand(String command, String stdin) {
091                    Assert.notBlank(command);
092                    ChannelExec exec = null;
093                    InputStream stdoutStream = null;
094                    ByteArrayOutputStream stderrStream = null;
095                    InputStream stdinStream = null;
096                    try {
097                            // Preserve start time
098                            long start = System.currentTimeMillis();
099                            // 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    }