Package com.asakusafw.yaess.jsch

Source Code of com.asakusafw.yaess.jsch.JschProcessExecutor

/**
* Copyright 2011-2014 Asakusa Framework Team.
*
* 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.
*/
package com.asakusafw.yaess.jsch;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.text.MessageFormat;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.asakusafw.yaess.basic.ProcessExecutor;
import com.asakusafw.yaess.core.ExecutionContext;
import com.asakusafw.yaess.core.VariableResolver;
import com.asakusafw.yaess.core.YaessLogger;
import com.jcraft.jsch.ChannelExec;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;

/**
* An implementation of {@link ProcessExecutor} using JSch.
* @since 0.2.3
*/
public class JschProcessExecutor implements ProcessExecutor {

    static final YaessLogger YSLOG = new YaessJschLogger(JschProcessExecutor.class);

    static final Logger LOG = LoggerFactory.getLogger(JschProcessExecutor.class);

    private static final String PREFIX = "ssh.";

    /**
     * The key of user name.
     * This value can includes environment variables in form of <code>${VARIABLE-NAME}</code>.
     */
    public static final String KEY_USER = PREFIX + "user";

    /**
     * The key of host name.
     * This value can includes environment variables in form of <code>${VARIABLE-NAME}</code>.
     */
    public static final String KEY_HOST = PREFIX + "host";

    /**
     * The key of port number.
     */
    public static final String KEY_PORT = PREFIX + "port";

    /**
     * The key of path to the private key.
     * This value can includes environment variables in form of <code>${VARIABLE-NAME}</code>.
     */
    public static final String KEY_PRIVATE_KEY = PREFIX + "privateKey";

    /**
     * The key of passphrase.
     */
    public static final String KEY_PASS_PHRASE = PREFIX + "passPhrase";

    // see
    // man bash > DEFINITIONS
    private static final Pattern SH_NAME = Pattern.compile("[A-Za-z_][0-9A-Za-z_]*");

    // see
    // man bash > QUOTING
    // $, `, ", \, or <newline>
    private static final Pattern SH_METACHARACTERS = Pattern.compile("[\\$`\"\\\\\n]");

    private final String user;

    private final String host;

    private final Integer port;

    private final String privateKey;

    private final String passPhrase;

    private final JSch jsch;

    /**
     * Creates a new instance.
     * @param user remote user name
     * @param host remote host name
     * @param portOrNull remote port number (nullable)
     * @param privateKeyPath path to private key file
     * @param passPhraseOrNull passphrase for the private key (nullable)
     * @throws JSchException if failed to initialize SSH client
     * @throws IllegalArgumentException if some parameters were {@code null}
     */
    public JschProcessExecutor(
            String user,
            String host,
            Integer portOrNull,
            String privateKeyPath,
            String passPhraseOrNull) throws JSchException {
        if (user == null) {
            throw new IllegalArgumentException("user must not be null"); //$NON-NLS-1$
        }
        if (host == null) {
            throw new IllegalArgumentException("host must not be null"); //$NON-NLS-1$
        }
        if (privateKeyPath == null) {
            throw new IllegalArgumentException("privateKeyPath must not be null"); //$NON-NLS-1$
        }
        this.user = user;
        this.host = host;
        this.port = portOrNull;
        this.jsch = new JSch();
        this.privateKey = privateKeyPath;
        this.passPhrase = passPhraseOrNull;
        jsch.addIdentity(privateKeyPath, passPhraseOrNull);
    }

    /**
     * Returns the remote user name.
     * @return the user name
     */
    public String getUser() {
        return user;
    }

    /**
     * Returns the remote host name.
     * @return the host name
     */
    public String getHost() {
        return host;
    }

    /**
     * Returns the remote port number.
     * @return the port number, or {@code null} if is not specified
     */
    public Integer getPort() {
        return port;
    }

    /**
     * Returns the path to the private key file.
     * @return the path to the private key
     */
    public String getPrivateKey() {
        return privateKey;
    }

    /**
     * Returns a pass phrase for private key.
     * @return the pass phrase, or {@code null} if is not specified
     */
    public String getPassPhrase() {
        return passPhrase;
    }

    /**
     * Extracts SSH profiles from configuration and returns a related executor.
     * This operation extracts following entries from {@code configuration}:
     * <ul>
     * <li> {@link #KEY_USER remote user name} </li>
     * <li> {@link #KEY_HOST remote host name} </li>
     * <li> {@link #KEY_PORT remote port number} (can omit) </li>
     * <li> {@link #KEY_PRIVATE_KEY private key path} </li>
     * <li> {@link #KEY_PASS_PHRASE} (can omit) </li>
     * </ul>
     * @param servicePrefix prefix of configuration keys
     * @param configuration target configuration
     * @param variables variable resolver
     * @return the created executor
     * @throws JSchException if failed to initialize SSH client
     * @throws IllegalArgumentException if configuration is invalid
     */
    public static JschProcessExecutor extract(
            String servicePrefix,
            Map<String, String> configuration,
            VariableResolver variables) throws JSchException {
        if (servicePrefix == null) {
            throw new IllegalArgumentException("servicePrefix must not be null"); //$NON-NLS-1$
        }
        if (configuration == null) {
            throw new IllegalArgumentException("configuration must not be null"); //$NON-NLS-1$
        }
        if (variables == null) {
            throw new IllegalArgumentException("variables must not be null"); //$NON-NLS-1$
        }
        String user = extract(KEY_USER, servicePrefix, configuration, variables, true);
        String host = extract(KEY_HOST, servicePrefix, configuration, variables, true);
        String portString = extract(KEY_PORT, servicePrefix, configuration, variables, false);
        String privateKey = extract(KEY_PRIVATE_KEY, servicePrefix, configuration, variables, false);
        String passPhrase = extract(KEY_PASS_PHRASE, servicePrefix, configuration, variables, false);
        Integer port = null;
        if (portString != null) {
            try {
                port = Integer.valueOf(portString);
            } catch (NumberFormatException e) {
                throw new IllegalArgumentException(MessageFormat.format(
                        "Invalid port number in \"{0}\": {1}",
                        servicePrefix + '.' + KEY_PORT,
                        portString));
            }
        }
        return new JschProcessExecutor(user, host, port, privateKey, passPhrase);
    }

    private static String extract(
            String key,
            String prefix,
            Map<String, String> configuration,
            VariableResolver variables,
            boolean mandatory) {
        assert key != null;
        assert prefix != null;
        assert configuration != null;
        assert variables != null;
        String value = configuration.get(key);
        if (value == null) {
            if (mandatory) {
                throw new IllegalArgumentException(MessageFormat.format(
                        "Mandatory entry \"{0}\" is not set",
                        prefix + '.' + key));
            } else {
                return null;
            }
        }
        try {
            return variables.replace(value, true);
        } catch (IllegalArgumentException e) {
            throw new IllegalArgumentException(MessageFormat.format(
                    "Failed to resolve variables in \"{0}\": {1}",
                    prefix + '.' + key,
                    value));
        }
    }

    @Override
    public int execute(
            ExecutionContext context,
            List<String> commandLineTokens,
            Map<String, String> environmentVariables) throws InterruptedException, IOException {
        return execute(context, commandLineTokens, environmentVariables, System.out);
    }

    @Override
    public int execute(
            ExecutionContext context,
            List<String> commandLineTokens,
            Map<String, String> environmentVariables,
            OutputStream output) throws InterruptedException, IOException {
        try {
            return execute0(context, commandLineTokens, environmentVariables, output);
        } catch (JSchException e) {
            throw new IOException(MessageFormat.format(
                    "Failed to execute command via SSH ({0}@{1}:{2})",
                    user,
                    host,
                    String.valueOf(port)), e);
        }
    }

    private int execute0(
            ExecutionContext context,
            List<String> commandLineTokens,
            Map<String, String> environmentVariables,
            OutputStream output) throws JSchException, InterruptedException {
        assert context != null;
        assert commandLineTokens != null;
        assert environmentVariables != null;
        assert output != null;
        Session session = jsch.getSession(user, host);
        if (port != null) {
            session.setPort(port);
        }
        session.setConfig("StrictHostKeyChecking", "no");
        session.setServerAliveInterval((int) TimeUnit.SECONDS.toMillis(30));

        try {
            YSLOG.info("I00001", user, host, port, privateKey);
            long sessionStart = System.currentTimeMillis();
            session.connect((int) TimeUnit.SECONDS.toMillis(60));
            long sessionEnd = System.currentTimeMillis();
            YSLOG.info("I00002", user, host, port, privateKey, sessionEnd - sessionStart);

            int exitStatus;
            try {
                ChannelExec channel = (ChannelExec) session.openChannel("exec");
                channel.setCommand(buildCommand(commandLineTokens, environmentVariables));
                channel.setInputStream(new ByteArrayInputStream(new byte[0]), true);
                channel.setOutputStream(output, true);
                channel.setErrStream(output, true);

                YSLOG.info("I00003", user, host, port, privateKey, commandLineTokens.get(0));
                long channelStart = System.currentTimeMillis();
                channel.connect((int) TimeUnit.SECONDS.toMillis(60));
                long channelEnd = System.currentTimeMillis();
                YSLOG.info("I00004", user, host, port, privateKey, commandLineTokens.get(0), channelEnd - channelStart);

                try {
                    while (true) {
                        if (channel.isClosed()) {
                            break;
                        }
                        Thread.sleep(100);
                    }
                    exitStatus = channel.getExitStatus();
                } finally {
                    channel.disconnect();
                }
            } finally {
                session.disconnect();
            }
            YSLOG.info("I00005", user, host, port, privateKey, commandLineTokens.get(0), exitStatus);
            return exitStatus;
        } catch (JSchException e) {
            YSLOG.error(e, "E00001", user, host, port, privateKey);
            throw e;
        }
    }

    private String buildCommand(List<String> commandLineTokens, Map<String, String> environmentVariables) {
        assert commandLineTokens != null;
        assert environmentVariables != null;

        // FIXME for bsh only
        StringBuilder buf = new StringBuilder();
        for (Map.Entry<String, String> entry : environmentVariables.entrySet()) {
            if (SH_NAME.matcher(entry.getKey()).matches() == false) {
                YSLOG.warn("W00001",
                        entry.getKey(),
                        entry.getValue());
                continue;
            }
            if (buf.length() > 0) {
                buf.append(' ');
            }
            buf.append(entry.getKey());
            String replaced = SH_METACHARACTERS.matcher(entry.getValue()).replaceAll("\\\\$0");
            buf.append('=');
            buf.append('"');
            buf.append(replaced);
            buf.append('"');
        }
        for (String token : commandLineTokens) {
            if (buf.length() > 0) {
                buf.append(' ');
            }
            String replaced = SH_METACHARACTERS.matcher(token).replaceAll("\\\\$0");
            buf.append('"');
            buf.append(replaced);
            buf.append('"');
        }
        return buf.toString();
    }
}
TOP

Related Classes of com.asakusafw.yaess.jsch.JschProcessExecutor

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.