package com.cloudhopper.commons.rfs.provider;
/*
* #%L
* ch-commons-rfs
* %%
* Copyright (C) 2012 - 2013 Cloudhopper by Twitter
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import com.cloudhopper.commons.rfs.*;
import com.jcraft.jsch.ChannelSftp;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
import com.jcraft.jsch.SftpATTRS;
import com.jcraft.jsch.SftpException;
import com.jcraft.jsch.UserInfo;
import java.io.File;
import java.io.InputStream;
import java.util.ArrayList;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* SFTP remote filesystem.
*
* The URL configuration for SFTP is fairly simple. However, the path component
* is flexible. To stay in the default directory after logging in, do not include
* a path component on the URL such as "sftp://user@host". If a path is included,
* it will be treated as an absolute path on the remote system, such that the
* URL "sftp://user@host/" will result in an attempt to change directories to "/"
* after connecting.
*
* @author joelauer
*/
public class SftpRemoteFileSystem extends BaseRemoteFileSystem {
private static final Logger logger = LoggerFactory.getLogger(SftpRemoteFileSystem.class);
private JSch jsch;
private Session session;
private ChannelSftp channel;
public SftpRemoteFileSystem() {
super();
}
/**
* Best attempt to find a default .ssh directory on this particular server.
* While more directories may be attempted, for now the user's home directory
* will be scanned for a .ssh directory.
* @return An array of .ssh directories to search or null if none were
* found.
*/
protected File[] findSshDirs() {
ArrayList<File> dirs = new ArrayList<File>();
// user's home directory and .ssh subdir
File sshHomeDir = new File(System.getProperty("user.home"), ".ssh");
if (sshHomeDir.exists() && sshHomeDir.isDirectory()) {
dirs.add(sshHomeDir);
}
// FIXME: any other directories we should try to scan?
return dirs.toArray(new File[0]);
}
/**
* Best attempt to find all .ssh private keys (identity) by searching inside
* every provided .ssh directory. Currently searches for any "id_rsa" or
* "id_dsa" files.
*/
protected File[] findSshPrivateKeys(File[] sshDirs) {
ArrayList<File> files = new ArrayList<File>();
// search every directory
for (File sshDir : sshDirs) {
File f0 = new File(sshDir, "id_dsa");
if (f0.exists() && f0.canRead() && f0.isFile()) {
files.add(f0);
}
File f1 = new File(sshDir, "id_rsa");
if (f1.exists() && f1.canRead() && f1.isFile()) {
files.add(f1);
}
}
return files.toArray(new File[0]);
}
@Override
public void validateURL() throws FileSystemException {
// a username and host must have been configured
if (getURL().getUsername() == null) {
throw new FileSystemException("The SFTP protocol requires a username");
}
if (getURL().getHost() == null) {
throw new FileSystemException("The SFTP protocol requires a host");
}
}
public void connect() throws FileSystemException {
// make sure we don't connect twice
if (session != null) {
throw new FileSystemException("Already connected to SFTP server");
}
// validate the url -- required for sftp (user and host)
// setup a new SFTP session
jsch = new JSch();
// attempt to load identities from the operating system
// find any .ssh directories we'll scan
File[] sshDirs = findSshDirs();
for (File sshDir : sshDirs) {
logger.info("Going to scan directory for .ssh private keys: " + sshDir.getAbsolutePath());
}
// find any identities that we'll then load
File[] sshPrivateKeys = findSshPrivateKeys(sshDirs);
for (File sshPrivateKeyFile : sshPrivateKeys) {
logger.info("Attempting to load .ssh private key (identity): " + sshPrivateKeyFile.getAbsolutePath());
try {
jsch.addIdentity(sshPrivateKeyFile.getAbsolutePath());
} catch (JSchException e) {
logger.warn("Failed to load private key file " + sshPrivateKeyFile + " - going to ignore");
}
}
try {
session = jsch.getSession(getURL().getUsername(), getURL().getHost(), (getURL().getPort() == null ? 22 : getURL().getPort().intValue()));
} catch (JSchException e) {
throw new FileSystemException("Unable to create SSH session: " + e.getMessage(), e);
}
// create fully trusted instance -- any hosts will be accepted
session.setUserInfo(new UserInfo() {
public String getPassphrase() {
return null;
}
public String getPassword() {
return null;
}
public boolean promptPassphrase(String string) {
return false;
}
public boolean promptPassword(String string) {
return false;
}
// called when a host's authenticity is questioned
public boolean promptYesNo(String string) {
//logger.debug("Jsch promptYesNo: " + string);
return true;
}
public void showMessage(String string) {
//logger.debug("Jsch showMessage: " + string);
}
});
// if the password is set
if (getURL().getPassword() != null) {
session.setPassword(getURL().getPassword());
}
// don't cause app to hang
session.setDaemonThread(true);
try {
session.connect();
} catch (JSchException e) {
session = null;
throw new FileSystemException("Unable to connect to SSH server: " + e.getMessage(), e);
}
logger.info("Connected to remote SSH server " + getURL().getUsername() + "@" + getURL().getHost());
// create an SFTP channel
try {
channel = (ChannelSftp) session.openChannel("sftp");
channel.connect();
} catch (JSchException e) {
// in case the channel failed, always close the parent session first
try { session.disconnect(); } catch (Exception ex) { }
session = null;
throw new FileSystemException("Unable to create SFTP channel on SSH session: " + e.getMessage(), e);
}
// based on the URL, make a decision if we should attempt to change dirs
if (getURL().getPath() != null) {
logger.info("Changing SFTP directory to: " + getURL().getPath());
try {
channel.cd(getURL().getPath());
} catch (SftpException e) {
// make sure we disconnect
try { disconnect(); } catch (Exception ex) { }
session = null;
throw new FileSystemException("Unable to change directory on SFTP channel to " + getURL().getPath(), e);
}
} else {
// staying in whatever directory we were assigned by default
// for information purposeds, let's try to print out that dir
try {
String currentDir = channel.pwd();
logger.info("Current SFTP directory: " + currentDir);
} catch (SftpException e) {
// ignore this error
logger.warn("Unable to get current directory -- safe to ignore");
}
}
}
public void disconnect() throws FileSystemException {
// we can't disconnect twice
if (session == null) {
throw new FileSystemException("Already disconnected from SFTP server");
}
// close channel
if (channel != null) {
try {
channel.disconnect();
} catch (Exception e) {
logger.warn("", e);
}
channel = null;
}
if (session != null) {
try {
session.disconnect();
} catch (Exception e) {
logger.warn("", e);
}
session = null;
}
logger.info("Disconnected to remote SSH server " + getURL().getUsername() + "@" + getURL().getHost());
}
public boolean exists(String filename) throws FileSystemException {
// we have to be connected
if (channel == null) {
throw new FileSystemException("Not yet connected to SFTP server");
}
// easiest way to check if a file already exists is to do a file stat
// this method will error out if the remote file does not exist!
try {
SftpATTRS attrs = channel.stat(filename);
// if we get here, then file exists
return true;
} catch (SftpException e) {
// map "no file" message to return correct result
if (e.getMessage().toLowerCase().indexOf("no such file") >= 0) {
return false;
}
// otherwise, this means an underlying error occurred
throw new FileSystemException("Underlying error with SFTP session while checking if file exists", e);
}
}
public void copy(InputStream in, String filename) throws FileSystemException {
// does this filename already exist?
if (exists(filename)) {
throw new FileSystemException("File " + filename + " already exists on SFTP server");
}
// copy the file
try {
channel.put(in, filename);
} catch (SftpException e) {
throw new FileSystemException("Failed to copy data during PUT with SFTP server", e);
}
}
}