com.xebialabs.overthere.ssh.SshConnection Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of overthere Show documentation
Show all versions of overthere Show documentation
Remote file manipulation and process execution framework for Java
/**
* Copyright (c) 2008-2015, XebiaLabs B.V., All rights reserved.
*
*
* Overthere is licensed under the terms of the GPLv2
* , like most XebiaLabs Libraries.
* There are special exceptions to the terms and conditions of the GPLv2 as it is applied to
* this software, see the FLOSS License Exception
* .
*
* This program is free software; you can redistribute it and/or modify it under the terms
* of the GNU General Public License as published by the Free Software Foundation; version 2
* of the License.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this
* program; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth
* Floor, Boston, MA 02110-1301 USA
*/
package com.xebialabs.overthere.ssh;
import com.xebialabs.overthere.*;
import com.xebialabs.overthere.spi.AddressPortMapper;
import com.xebialabs.overthere.spi.BaseOverthereConnection;
import net.schmizz.keepalive.KeepAliveProvider;
import net.schmizz.sshj.Config;
import net.schmizz.sshj.DefaultConfig;
import net.schmizz.sshj.SSHClient;
import net.schmizz.sshj.common.Factory;
import net.schmizz.sshj.common.SSHException;
import net.schmizz.sshj.connection.ConnectionException;
import net.schmizz.sshj.connection.channel.direct.PTYMode;
import net.schmizz.sshj.connection.channel.direct.Session;
import net.schmizz.sshj.transport.TransportException;
import net.schmizz.sshj.transport.verification.PromiscuousVerifier;
import net.schmizz.sshj.userauth.keyprovider.KeyProvider;
import net.schmizz.sshj.userauth.method.AuthKeyboardInteractive;
import net.schmizz.sshj.userauth.method.AuthPassword;
import net.schmizz.sshj.userauth.password.PasswordFinder;
import net.schmizz.sshj.userauth.password.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.util.Collections;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static com.xebialabs.overthere.ConnectionOptions.*;
import static com.xebialabs.overthere.ssh.SshConnectionBuilder.*;
import static com.xebialabs.overthere.util.OverthereUtils.*;
import static java.lang.String.format;
import static java.net.InetSocketAddress.createUnresolved;
/**
* Base class for host connections using SSH.
*/
abstract class SshConnection extends BaseOverthereConnection {
public static final String PTY_PATTERN = "([\\w-]+):(\\d+):(\\d+):(\\d+):(\\d+)";
public static final String NOCD_PSEUDO_COMMAND = "nocd";
protected SshConnectionType sshConnectionType;
protected String host;
protected int port;
protected String localAddress;
protected int localPort;
protected String username;
protected String password;
protected String interactiveKeyboardAuthPromptRegex;
protected String privateKey;
protected String privateKeyFile;
protected String passphrase;
protected boolean allocateDefaultPty;
protected boolean openShellBeforeExecute;
protected String allocatePty;
protected int heartbeatInterval;
protected SSHClient sshClient;
private static final Pattern ptyPattern = Pattern.compile(PTY_PATTERN);
private static final Config config = new DefaultConfig();
protected Factory sshClientFactory = new Factory() {
@Override
public SSHClient create() {
return new SSHClient(config);
}
};
public SshConnection(final String protocol, final ConnectionOptions options, final AddressPortMapper mapper) {
super(protocol, options, mapper, true);
sshConnectionType = options.getEnum(CONNECTION_TYPE, SshConnectionType.class);
String unmappedAddress = options.get(ADDRESS);
int unmappedPort = options.getInteger(PORT, PORT_DEFAULT_SSH);
InetSocketAddress addressPort = mapper.map(createUnresolved(unmappedAddress, unmappedPort));
host = addressPort.getHostName();
port = addressPort.getPort();
localAddress = options.getOptional(LOCAL_ADDRESS);
localPort = options.getInteger(LOCAL_PORT, 0);
username = options.get(USERNAME);
password = options.getOptional(PASSWORD);
interactiveKeyboardAuthPromptRegex = options.get(INTERACTIVE_KEYBOARD_AUTH_PROMPT_REGEX, INTERACTIVE_KEYBOARD_AUTH_PROMPT_REGEX_DEFAULT);
privateKey = options.getOptional(PRIVATE_KEY);
privateKeyFile = options.getOptional(PRIVATE_KEY_FILE);
passphrase = options.getOptional(PASSPHRASE);
allocateDefaultPty = options.getBoolean(ALLOCATE_DEFAULT_PTY, ALLOCATE_DEFAULT_PTY_DEFAULT);
heartbeatInterval = options.getInteger(HEARTBEAT_INTERVAL, HEARTBEAT_INTERVAL_DEFAULT);
if (allocateDefaultPty) {
logger.warn("The " + ALLOCATE_DEFAULT_PTY + " connection option has been deprecated in favour of the " + ALLOCATE_PTY + " option. See https://github.com/xebialabs/overthere#ssh_allocatePty");
}
allocatePty = options.getOptional(ALLOCATE_PTY);
openShellBeforeExecute = options.getBoolean(OPEN_SHELL_BEFORE_EXECUTE, OPEN_SHELL_BEFORE_EXECUTE_DEFAULT);
}
protected void connect() {
try {
config.setKeepAliveProvider(KeepAliveProvider.KEEP_ALIVE);
SSHClient client = sshClientFactory.create();
client.setConnectTimeout(connectionTimeoutMillis);
client.addHostKeyVerifier(new PromiscuousVerifier());
client.setTimeout(socketTimeoutMillis);
client.getConnection().getKeepAlive().setKeepAliveInterval(heartbeatInterval);
try {
if (localAddress == null) {
client.connect(host, port);
} else {
client.connect(host, port, InetAddress.getByName(localAddress), localPort);
}
} catch (IOException e) {
throw new RuntimeIOException("Cannot connect to " + host + ":" + port, e);
}
if (!onlyOneNotNull(privateKey, privateKeyFile, password)) {
logger.warn("You should only set one connection options between: {}, {}, {}. They are evaluated in this order, and latter would have no effect on the connection.", PRIVATE_KEY, PRIVATE_KEY_FILE, PASSWORD);
}
KeyProvider keys;
if (privateKey != null) {
try {
if (passphrase == null) {
keys = client.loadKeys(privateKey, null, null);
} else {
keys = client.loadKeys(privateKey, null, getPassphraseFinder());
}
} catch (IOException e) {
throw new RuntimeIOException("The supplied key is not in a recognized format", e);
}
client.authPublickey(username, keys);
} else if (privateKeyFile != null) {
try {
if (passphrase == null) {
keys = client.loadKeys(privateKeyFile);
} else {
keys = client.loadKeys(privateKeyFile, getPassphraseFinder());
}
} catch (IOException e) {
throw new RuntimeIOException("Cannot read key from private key file " + privateKeyFile, e);
}
client.authPublickey(username, keys);
} else if (password != null) {
PasswordFinder passwordFinder = getPasswordFinder();
client.auth(username, new AuthPassword(passwordFinder),
new AuthKeyboardInteractive(new RegularExpressionPasswordResponseProvider(passwordFinder, interactiveKeyboardAuthPromptRegex)));
}
sshClient = client;
connected();
} catch (SSHException e) {
throw new RuntimeIOException("Cannot connect to " + this, e);
}
}
private PasswordFinder getPasswordFinder() {
return new PasswordFinder() {
@Override
public char[] reqPassword(Resource> resource) {
return password.toCharArray();
}
@Override
public boolean shouldRetry(Resource> resource) {
return false;
}
};
}
private PasswordFinder getPassphraseFinder() {
return new PasswordFinder() {
@Override
public char[] reqPassword(Resource> resource) {
return passphrase.toCharArray();
}
@Override
public boolean shouldRetry(Resource> resource) {
return false;
}
};
}
private boolean onlyOneNotNull(Object... objs) {
int guard = 0;
for (Object obj: objs) {
guard += obj != null ? 1 : 0;
}
return guard == 1;
}
@Override
public void doClose() {
if (sshClient == null) return;
try {
sshClient.disconnect();
} catch (Exception e) {
// Even though we get an exception, we expect the connection to have been closed, so we are ignoring
logger.error("Unexpected exception received while disconnecting from " + this, e);
} finally {
sshClient = null;
}
}
protected SSHClient getSshClient() {
checkState(sshClient != null, "Not (yet) connected");
return sshClient;
}
@Override
public OverthereFile getFile(OverthereFile parent, String child) throws RuntimeIOException {
checkParentFile(parent);
return getFile(constructPath(parent, child));
}
@Override
protected OverthereFile getFileForTempFile(OverthereFile parent, String name) {
checkParentFile(parent);
return getFile(parent, name);
}
protected void checkParentFile(final OverthereFile parent) {
if (!(parent instanceof SshFile)) {
throw new IllegalStateException("parent is not a file on an SSH host");
}
if (parent.getConnection() != this) {
throw new IllegalStateException("parent is not a file in this connection");
}
}
@Override
public OverthereProcess startProcess(final CmdLine origCmd) {
checkNotNull(origCmd, "Cannot execute null command line");
checkArgument(origCmd.getArguments().size() > 0, "Cannot execute empty command line");
final CmdLine cmd = processCommandLine(origCmd);
final String obfuscatedCmd = origCmd.toCommandLine(os, true);
logger.info("Starting command [{}] on [{}]", obfuscatedCmd, this);
try {
if (openShellBeforeExecute) {
Session session = null;
try {
logger.debug("Creating a temporary shell to allow for deferred home dir creation.");
session = getSshClient().startSession();
Session.Shell shell = session.startShell();
shell.close();
} finally {
closeQuietly(session);
}
}
Session session = getSshClient().startSession();
if (allocatePty != null && !allocatePty.isEmpty()) {
if (allocateDefaultPty) {
logger.warn("The " + ALLOCATE_PTY + " and " + ALLOCATE_DEFAULT_PTY
+ " connection options have both been set for the connection {}. Ignoring "
+ ALLOCATE_DEFAULT_PTY + " and using " + ALLOCATE_PTY + ".", this);
}
Matcher matcher = ptyPattern.matcher(allocatePty);
checkArgument(matcher.matches(), "Value for allocatePty [%s] does not match pattern \"" + PTY_PATTERN + "\"", allocateDefaultPty);
String term = matcher.group(1);
int cols = Integer.valueOf(matcher.group(2));
int rows = Integer.valueOf(matcher.group(3));
int width = Integer.valueOf(matcher.group(4));
int height = Integer.valueOf(matcher.group(5));
logger.debug("Allocating PTY {}:{}:{}:{}:{}", new Object[]{term, cols, rows, width, height});
session.allocatePTY(term, cols, rows, width, height, Collections.emptyMap());
} else if (allocateDefaultPty) {
logger.debug("Allocating default PTY");
session.allocateDefaultPTY();
}
return createProcess(session, cmd);
} catch (SSHException e) {
throw new RuntimeIOException(format("Cannot start command [%s] on [%s]", obfuscatedCmd, this), e);
}
}
protected CmdLine processCommandLine(final CmdLine cmd) {
CmdLine processedCmd;
logger.trace("Checking whether to prefix command line with cd: {}", cmd);
if (startsWithPseudoCommand(cmd, NOCD_PSEUDO_COMMAND)) {
logger.trace("Not prefixing command line with cd statement because the " + NOCD_PSEUDO_COMMAND
+ " pseudo command was present, but the pseudo command will be stripped");
processedCmd = stripPrefixedPseudoCommand(cmd);
} else if (getWorkingDirectory() != null) {
logger.trace("Prefixing command line with cd statement because the current working directory was set");
logger.trace("Replacing: {}", cmd);
processedCmd = new CmdLine();
processedCmd.addArgument("cd");
processedCmd.addArgument(workingDirectory.getPath());
processedCmd.addRaw(os.getCommandSeparator());
for (CmdLineArgument a : cmd.getArguments()) {
processedCmd.add(a);
}
} else {
logger.trace("Not prefixing command line with cd statement because the current working directory was not set");
processedCmd = cmd;
}
logger.trace("Processed command line for cd : {}", processedCmd);
return processedCmd;
}
protected boolean startsWithPseudoCommand(final CmdLine commandLine, final String pseudoCommand) {
return commandLine.getArguments().size() >= 2 && commandLine.getArguments().get(0).toString(os, false).equals(pseudoCommand);
}
protected SshProcess createProcess(Session session, CmdLine commandLine) throws TransportException, ConnectionException {
return new SshProcess(this, os, session, commandLine);
}
@Override
public String toString() {
return "ssh:" + sshConnectionType.toString().toLowerCase() + "://" + username + "@" + host + ":" + port;
}
protected static CmdLine stripPrefixedPseudoCommand(final CmdLine commandLine) {
return new CmdLine().add(commandLine.getArguments().subList(1, commandLine.getArguments().size()));
}
protected static CmdLine prefixWithPseudoCommand(final CmdLine commandLine, final String pseudoCommand) {
CmdLine nosudoCommandLine = new CmdLine();
nosudoCommandLine.addArgument(pseudoCommand);
nosudoCommandLine.add(commandLine.getArguments());
return nosudoCommandLine;
}
private static Logger logger = LoggerFactory.getLogger(SshConnection.class);
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy