/*
* Copyright (C) 2006-2016 DLR, Germany
*
* All rights reserved
*
* http://www.rcenvironment.de/
*/
package de.rcenvironment.core.utils.ssh.jsch;
import java.io.File;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Logger;
import com.jcraft.jsch.Session;
import com.jcraft.jsch.UIKeyboardInteractive;
import com.jcraft.jsch.UserInfo;
import de.rcenvironment.core.utils.common.StringUtils;
/**
* A factory for configuration-based creation of JSch {@link Session}s. Supported authentication methods are password and SSH keyfile (with
* an optional passphrase).
*
* @author Robert Mischke
*/
public final class JschSessionFactory {
private static Log log = LogFactory.getLog(JschSessionFactory.class);
private JschSessionFactory() {
// prevent instantiation
}
/**
* Simple adapter from com.jcraft.jsch.Logger to Apache Commons Logging.
*
* @author Robert Mischke
* @author Brigitte Boden
*/
private static final class ACLDelegate implements Logger {
private final Log apacheCommonsLogger;
private final int minLevel;
private ACLDelegate(Log apacheCommonsLogger, int minLevel) {
this.apacheCommonsLogger = apacheCommonsLogger;
this.minLevel = minLevel;
}
@Override
public void log(int level, String arg1) {
if (level >= minLevel) {
final String logMessage = "JSch connection log: " + level + ": " + arg1;
if (level == 0 || level == 1) {
// Debug and info messages are both logged as debug, as the "info" output is quite verbose.
apacheCommonsLogger.debug(logMessage);
} else if (level == 2) {
apacheCommonsLogger.warn(logMessage);
} else {
apacheCommonsLogger.error(logMessage);
}
}
}
@Override
public boolean isEnabled(int level) {
return level >= minLevel;
}
}
/**
* A stub class that provides pseudo user responses for password authentication.
*
* @author Robert Mischke
*/
private static final class UserInfoAdapter implements UserInfo, UIKeyboardInteractive {
private String pw;
UserInfoAdapter(String pw) {
this.pw = pw;
}
@Override
public String getPassphrase() {
// not expected to be called
log.warn("SSH called getPassphrase() unexpectedly");
return null;
}
@Override
public String getPassword() {
// not expected to be called
log.warn("SSH called getPassword() unexpectedly");
return pw;
}
@Override
public boolean promptPassphrase(String arg0) {
// not expected to be called
log.warn("SSH login sent a passphrase prompt (answered with no passphrase): " + arg0);
return false;
}
@Override
public boolean promptPassword(String arg0) {
log.debug("SSH login sent a password prompt: " + arg0);
return true;
}
@Override
public boolean promptYesNo(String arg0) {
// not expected to be called
log.warn("SSH login sent a yes/no prompt (answered 'no'): " + arg0);
return false;
}
@Override
public void showMessage(String arg0) {
log.debug("SSH login sent a message: " + arg0);
}
@Override
public String[] promptKeyboardInteractive(String arg0, String arg1, String arg2, String[] arg3, boolean[] arg4) {
if (log.isDebugEnabled()) {
log.debug(StringUtils
.format("Simulating keyboard-interactive login; display parameters: %s, %s, %s, %d", arg0, arg1, arg2, arg3.length));
}
return new String[] { pw };
}
}
/**
* @param host the target host to connect to
* @param port the port number
* @param user the login username
* @param keyfileLocation the path of the local keyfile (optional); if null or empty, password auth is used. For convenience, "~" is
* automatically resolved to ${user.home}. (NB: This is one of the reasons why a String is used instead of a {@link File}.)
* @param authPhrase the authentication phrase; if a keyfile is set, this is taken as the keyfile passphase; otherwise, it is used as
* the login password
* @param connectionLogger the JSch-internal logger instance to use
* @return the initialized JSch session on success
* @throws SshParameterException on invalid parameters
* @throws JSchException on SSH errors
*/
public static Session setupSession(String host, int port, String user, String keyfileLocation, String authPhrase,
Logger connectionLogger) throws JSchException, SshParameterException {
// TODO provide a constructor that accepts a SSHSessionConfiguration directly?
JSch jsch = new JSch();
Session jschSession = jsch.getSession(user, host, port);
jschSession.setConfig("StrictHostKeyChecking", "no");
// sanitize & trim keyfile location
keyfileLocation = normalizeKeyfilePath(keyfileLocation);
// validate parameters
if (host.length() == 0) {
throw new SshParameterException("The host name or address cannot be empty");
}
if (port < 0) {
throw new SshParameterException("The port must be greater than zero");
}
if (user.length() == 0) {
throw new SshParameterException("The user name cannot be empty");
}
if (keyfileLocation.length() == 0) {
// use password authentication
if (authPhrase.length() == 0) {
throw new SshParameterException("The authentication phrase cannot be empty");
}
if (log.isDebugEnabled()) {
log.debug("Setting up JSCH/SSH connection, password authentication, host='"
+ host + "', user='" + user + "'");
}
UserInfo ui = new UserInfoAdapter(authPhrase);
jschSession.setUserInfo(ui);
} else {
// use keyfile authentication
keyfileLocation = resolveAndVerifyKeyfilePath(keyfileLocation);
if (log.isDebugEnabled()) {
log.debug("Setting up JSCH/SSH connection, keyfile authentication, host='"
+ host + "', user='" + user + "', keyfile='" + keyfileLocation + "'");
}
jsch.addIdentity(keyfileLocation, authPhrase);
}
JSch.setLogger(connectionLogger);
jschSession.connect();
return jschSession;
}
/**
* Creates an SSH {@link Logger} from a provided Apache Commons Logging instance.
*
* @param apacheCommonsLogger the ACL instance to delegate to
* @return a new SSH logger delegate
*/
public static Logger createDelegateLogger(final Log apacheCommonsLogger) {
return new ACLDelegate(apacheCommonsLogger, Logger.INFO);
}
private static String normalizeKeyfilePath(String location) throws SshParameterException {
// "null" is valid input; treat it like an empty string
if (location == null) {
location = "";
}
return location.trim();
}
private static String resolveAndVerifyKeyfilePath(String location) throws SshParameterException {
// resolve "~" with home dir
location = location.replace("~", System.getProperty("user.home"));
// normalize path
File sshKeyFile = new File(location);
location = sshKeyFile.getAbsolutePath();
// check file existence
if (!sshKeyFile.isFile()) {
throw new SshParameterException("SSH keyfile '" + location + "' does not exist");
}
return location;
}
}