Package com.xebialabs.overthere.ssh

Source Code of com.xebialabs.overthere.ssh.SshConnection

/*
* Copyright (c) 2008-2014, XebiaLabs B.V., All rights reserved.
*
*
* Overthere is licensed under the terms of the GPLv2
* <http://www.gnu.org/licenses/old-licenses/gpl-2.0.html>, 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
* <http://github.com/xebialabs/overthere/blob/master/LICENSE>.
*
* 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 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 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 com.xebialabs.overthere.CmdLine;
import com.xebialabs.overthere.CmdLineArgument;
import com.xebialabs.overthere.ConnectionOptions;
import com.xebialabs.overthere.OverthereFile;
import com.xebialabs.overthere.OverthereProcess;
import com.xebialabs.overthere.RuntimeIOException;
import com.xebialabs.overthere.spi.AddressPortMapper;
import com.xebialabs.overthere.spi.BaseOverthereConnection;

import static com.xebialabs.overthere.util.OverthereUtils.checkArgument;
import static com.xebialabs.overthere.util.OverthereUtils.checkNotNull;
import static com.xebialabs.overthere.util.OverthereUtils.checkState;
import static com.xebialabs.overthere.ConnectionOptions.ADDRESS;
import static com.xebialabs.overthere.ConnectionOptions.PASSWORD;
import static com.xebialabs.overthere.ConnectionOptions.PORT;
import static com.xebialabs.overthere.ConnectionOptions.USERNAME;
import static com.xebialabs.overthere.ssh.SshConnectionBuilder.ALLOCATE_DEFAULT_PTY;
import static com.xebialabs.overthere.ssh.SshConnectionBuilder.ALLOCATE_DEFAULT_PTY_DEFAULT;
import static com.xebialabs.overthere.ssh.SshConnectionBuilder.ALLOCATE_PTY;
import static com.xebialabs.overthere.ssh.SshConnectionBuilder.CONNECTION_TYPE;
import static com.xebialabs.overthere.ssh.SshConnectionBuilder.INTERACTIVE_KEYBOARD_AUTH_PROMPT_REGEX;
import static com.xebialabs.overthere.ssh.SshConnectionBuilder.INTERACTIVE_KEYBOARD_AUTH_PROMPT_REGEX_DEFAULT;
import static com.xebialabs.overthere.ssh.SshConnectionBuilder.LOCAL_ADDRESS;
import static com.xebialabs.overthere.ssh.SshConnectionBuilder.LOCAL_PORT;
import static com.xebialabs.overthere.ssh.SshConnectionBuilder.OPEN_SHELL_BEFORE_EXECUTE;
import static com.xebialabs.overthere.ssh.SshConnectionBuilder.OPEN_SHELL_BEFORE_EXECUTE_DEFAULT;
import static com.xebialabs.overthere.ssh.SshConnectionBuilder.PASSPHRASE;
import static com.xebialabs.overthere.ssh.SshConnectionBuilder.PORT_DEFAULT_SSH;
import static com.xebialabs.overthere.ssh.SshConnectionBuilder.PRIVATE_KEY_FILE;
import static com.xebialabs.overthere.util.OverthereUtils.closeQuietly;
import static com.xebialabs.overthere.util.OverthereUtils.constructPath;
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 privateKeyFile;

    protected String passphrase;

    protected boolean allocateDefaultPty;

    protected boolean openShellBeforeExecute;

    protected String allocatePty;

    protected SSHClient sshClient;

    private static final Pattern ptyPattern = Pattern.compile(PTY_PATTERN);

    private static final Config config = new DefaultConfig();

    protected Factory<SSHClient> sshClientFactory = new Factory<SSHClient>() {
        @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);
        privateKeyFile = options.getOptional(PRIVATE_KEY_FILE);
        passphrase = options.getOptional(PASSPHRASE);
        allocateDefaultPty = options.getBoolean(ALLOCATE_DEFAULT_PTY, ALLOCATE_DEFAULT_PTY_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 {
            SSHClient client = sshClientFactory.create();
            client.setConnectTimeout(connectionTimeoutMillis);
            client.addHostKeyVerifier(new PromiscuousVerifier());

            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 (privateKeyFile != null) {
                if (password != null) {
                    logger.warn("The " + PRIVATE_KEY_FILE + " and " + PASSWORD + " connection options have both been set for the connection {}. Ignoring "
                            + PASSWORD
                            + " and using " + PRIVATE_KEY_FILE + ".", this);
                }
                KeyProvider keys;
                try {
                    if (passphrase == null) {
                        keys = client.loadKeys(privateKeyFile);
                    } else {
                        keys = client.loadKeys(privateKeyFile, passphrase);
                    }
                } 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;
            }
        };
    }

    @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.<PTYMode, Integer>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);

}
TOP

Related Classes of com.xebialabs.overthere.ssh.SshConnection

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.