All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.glassfish.cluster.ssh.launcher.SSHLauncher Maven / Gradle / Ivy

There is a newer version: 8.0.0-JDK17-M9
Show newest version
/*
 * Copyright (c) 2022, 2025 Contributors to the Eclipse Foundation
 * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0, which is available at
 * http://www.eclipse.org/legal/epl-2.0.
 *
 * This Source Code may also be made available under the following Secondary
 * Licenses when the conditions for such availability set forth in the
 * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
 * version 2 with the GNU Classpath Exception, which is available at
 * https://www.gnu.org/software/classpath/license.html.
 *
 * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
 */

package org.glassfish.cluster.ssh.launcher;

import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
import com.sun.enterprise.config.serverbeans.Node;
import com.sun.enterprise.config.serverbeans.SshAuth;
import com.sun.enterprise.config.serverbeans.SshConnector;
import com.sun.enterprise.util.io.FileUtils;

import java.io.File;
import java.lang.Runtime.Version;
import java.lang.System.Logger;
import java.nio.charset.Charset;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Map;
import java.util.function.Function;

import org.glassfish.cluster.ssh.sftp.SFTPClient;
import org.glassfish.cluster.ssh.sftp.SFTPPath;
import org.glassfish.cluster.ssh.util.SSHUtil;
import org.glassfish.internal.api.RelativePathResolver;

import static java.lang.System.Logger.Level.DEBUG;
import static java.lang.System.Logger.Level.ERROR;
import static java.lang.System.Logger.Level.INFO;
import static java.lang.System.Logger.Level.WARNING;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.glassfish.cluster.ssh.launcher.JavaSystemJschLogger.maskForLogging;
import static org.glassfish.cluster.ssh.launcher.OperatingSystem.GENERIC;

/**
 * @author Rajiv Mordani, Krishna Deepak
 */
public class SSHLauncher {

    static final String SSH_DIR_NAME = ".ssh";
    private static final int SSH_PORT_DEFAULT = 22;

    private static final Logger LOG = System.getLogger(SSHLauncher.class.getName());

    static {
        JSch.setLogger(new JavaSystemJschLogger());
    }

    /** The host name which to connect to via ssh */
    private final String host;

    /** The port on which the ssh daemon is running */
    private final int port;

    /** The user name to use for authenticating with the ssh daemon */
    private final String userName;

    /** The name of private key file. */
    private final File keyFile;

    private final String keyPassPhraseParameter;
    private final String keyPassPhrase;

    /** Password before it has been expanded. */
    private final String passwordParameter;
    private final String password;
    private final RemoteSystemCapabilities capabilities;


    /**
     * Initialize the SSHLauncher use a {@link Node} config object
     *
     * @param node
     */
    public SSHLauncher(Node node) {
        final SshConnector connector = node.getSshConnector();
        this.host = getHost(node);
        LOG.log(DEBUG, "Connecting to host {0}", host);
        this.port = getPort(connector);

        final SshAuth sshAuth = connector.getSshAuth();
        this.userName = getUserName(sshAuth == null ? null : sshAuth.getUserName());
        this.keyFile = sshAuth == null || sshAuth.getKeyfile() == null
            ? SSHUtil.getExistingKeyFile()
            : new File(sshAuth.getKeyfile());
        this.passwordParameter = sshAuth == null ? null : sshAuth.getPassword();
        this.password = passwordParameter == null || passwordParameter.isEmpty()
            ? null
            : expandPasswordAlias(passwordParameter);
        this.keyPassPhraseParameter = sshAuth == null ? null : sshAuth.getKeyPassphrase();
        this.keyPassPhrase = keyPassPhraseParameter == null || keyPassPhraseParameter.isEmpty()
            ? null
            : expandPasswordAlias(keyPassPhraseParameter);
        this.capabilities = analyzeRemote(this.host, this.port, this.userName, this.password, this.keyFile,
            this.keyPassPhrase);
        LOG.log(DEBUG, "SSH client configuration: {0}", this);
    }


    /**
     * Initialize the SSHLauncher
     *
     * @param userName
     * @param host
     * @param port
     * @param password
     * @param keyFile
     * @param keyPassPhrase
     */
    public SSHLauncher(String userName, String host, int port, String password, File keyFile, String keyPassPhrase) {
        this.host = host;
        this.port = port == 0 ? SSH_PORT_DEFAULT : port;
        this.keyFile = keyFile == null ? SSHUtil.getExistingKeyFile(): keyFile;
        this.userName = getUserName(userName);
        this.passwordParameter = password;
        this.password = password == null || password.isEmpty() ? null : expandPasswordAlias(password);
        this.keyPassPhraseParameter = keyPassPhrase;
        this.keyPassPhrase = keyPassPhrase == null || keyPassPhrase.isEmpty()
            ? null
            : expandPasswordAlias(keyPassPhrase);
        this.capabilities = analyzeRemote(this.host, this.port, this.userName, this.password, this.keyFile,
            this.keyPassPhrase);
        LOG.log(DEBUG, "SSH client configuration: {0}", this);
    }


    /**
     * @return the remote host or IP address
     */
    public String getHost() {
        return this.host;
    }


    /**
     * @return the remote port supporting SSH
     */
    public int getPort() {
        return this.port;
    }


    /**
     * @return ssh login
     */
    public String getUserName() {
        return this.userName;
    }


    File getKeyFile() {
        return this.keyFile;
    }


    String getKeyFilePassphrase() {
        return this.keyPassPhrase;
    }


    /**
     * @return preloaded {@link RemoteSystemCapabilities}
     */
    public RemoteSystemCapabilities getCapabilities() {
        return this.capabilities;
    }


    /**
     * Check if we can authenticate using public key auth
     * @return true|false
     */
    public boolean checkConnection() {
        LOG.log(DEBUG, "Checking connection...");
        JSch jsch = new JSch();
        Session sess = null;
        try {
            jsch.addIdentity(keyFile.getAbsolutePath(), keyPassPhrase);
            sess = jsch.getSession(userName, host, port);
            sess.setConfig("StrictHostKeyChecking", "accept-new");
            sess.connect();
            if (sess.isConnected()) {
                LOG.log(INFO, () -> "Successfully connected to " + userName + "@" + host + " using keyfile " + keyFile);
                return true;
            }
            return false;
        } catch (JSchException ex) {
            Throwable t = ex.getCause();
            if (t != null) {
                String msg = t.getMessage();
                LOG.log(WARNING, "Failed to connect or authenticate: " + msg);
            }
            LOG.log(DEBUG, "Failed to connect or autheticate: ", ex);
            return false;
        } finally {
            if (sess != null) {
                sess.disconnect();
            }
        }
    }

    /**
     * Check if we can connect using password auth
     * @return true|false
     */
    public boolean checkPasswordAuth() {
        LOG.log(DEBUG, "Checking connection...");
        JSch jsch = new JSch();
        Session sess = null;
        try {
            sess = jsch.getSession(userName, host, port);
            sess.setConfig("StrictHostKeyChecking", "accept-new");
            sess.setPassword(password);
            sess.connect();
            if (sess.isConnected()) {
                LOG.log(DEBUG,
                    () -> "Successfully connected to " + userName + "@" + host + " using password authentication");
                return true;
            }
            return false;
        } catch (JSchException ex) {
            LOG.log(ERROR, "Failed to connect or autheticate: ", ex);
            return false;
        } finally {
            if (sess != null) {
                sess.disconnect();
            }
        }
    }


    /**
     * Open the {@link SSHSession}.
     *
     * @return open {@link SSHSession}
     * @throws SSHException
     */
    public SSHSession openSession() throws SSHException {
        return openSession(getCapabilities(), host, port, userName, password, keyFile, keyPassPhrase);
    }


    /**
     * Open the {@link SSHSession}.
     *
     * @return open {@link SSHSession}
     * @throws SSHException
     */
    private static SSHSession openSession(
        RemoteSystemCapabilities capabilities,
        String host,
        int port,
        String userName,
        String password,
        File keyFile,
        String keyPassPhrase) throws SSHException {
        JSch jsch = new JSch();
        String message = "";
        boolean triedAuthentication = false;
        // Private key file is provided - Public Key Authentication
        if (keyFile != null) {
            LOG.log(DEBUG, () -> "Specified key file is " + keyFile);
            if (keyFile.exists()) {
                triedAuthentication = true;
                LOG.log(DEBUG, () -> "Adding identity for private key at " + keyFile);
                addIdentity(jsch, keyFile, keyPassPhrase);
            } else {
                message = "Specified key file does not exist \n";
            }
        } else if (password == null || password.isEmpty()) {
            message += "No key or password specified - trying default keys \n";
            LOG.log(DEBUG, "keyfile and password are null. Will try to authenticate with default key file if available");
            // check the default key locations if no authentication
            // method is explicitly configured.
            Path home = FileUtils.USER_HOME.toPath();
            for (String keyName : SSHUtil.SSH_KEY_FILE_NAMES) {
                message += "Tried to authenticate using " + keyName + "\n";
                File key = home.resolve(Path.of(SSH_DIR_NAME, keyName)).toFile();
                if (key.exists()) {
                    triedAuthentication = true;
                    addIdentity(jsch, key, keyPassPhrase);
                }
            }
        }

        final Session session = openSession(jsch, host, port, userName);
        try {
            // TODO: Insecure, maybe we could create an input field and allow user to check the host key?
            session.setConfig("StrictHostKeyChecking", "accept-new");
            session.setUserInfo(new GlassFishSshUserInfo());
            if (password != null && !password.isEmpty()) {
                LOG.log(DEBUG, () -> "Authenticating with password " + maskForLogging(password));
                triedAuthentication = true;
                session.setPassword(password);
            }
            if (!triedAuthentication) {
                throw new SSHException("Could not authenticate: " + message + '.');
            }
            session.connect();
            return new SSHSession(session, capabilities);
        } catch (SSHException e) {
            session.disconnect();
            throw e;
        } catch (JSchException e) {
            session.disconnect();
            throw new SSHException("Could not authenticate: " + message + '.', e);
        }
    }


    /**
     * Opens the SSH session using user password.
     * The resulting {@link SSHSession} is very limited, doesn't distinguish between operating
     * system capabilities, etc.
     *
     * @param passwordParam
     * @return {@link SSHSession}
     * @throws SSHException if the connection attempt failed.
     */
    public SSHSession openSession(String passwordParam) throws SSHException {
        JSch jsch = new JSch();
        Session session = openSession(jsch, host, port, userName);
        try {
            session.setConfig("StrictHostKeyChecking", "accept-new");
            session.setPassword(passwordParam);
            session.connect();
            return new SSHSession(session, getCapabilities());
        } catch (JSchException e) {
            throw new SSHException("Failed to connect.", e);
        }
    }


    /**
     * Open and close the session.
     *
     * @throws SSHException
     */
    public void pingConnection() throws SSHException {
        LOG.log(DEBUG, () -> "Trying to establish connection to host: " + this.host);
        try (SSHSession session = openSession()) {
            LOG.log(INFO, () -> "Establishing SSH connection to host " + this.host + " succeeded!");
        }
    }


    /**
     * Check if the remote path exists.
     * This method is a shortcut to simplify your code as it uses {@link SSHSession}
     * and {@link SFTPClient}.
     *
     * @param path absolute path
     * @return true if the path exists in the SFTP server.
     * @throws SSHException
     */
    public boolean exists(SFTPPath path) throws SSHException {
        try (SSHSession session = openSession(); SFTPClient sftpClient = session.createSFTPClient()) {
            return sftpClient.exists(path);
        }
    }


    @Override
    public String toString() {
        String displayPassword = maskForLogging(passwordParameter);
        String displayKeyPassPhrase = maskForLogging(keyPassPhraseParameter);
        return String.format("host=%s port=%d user=%s password=%s keyFile=%s keyPassPhrase=%s, capabilities=%s", host, port, userName,
            displayPassword, keyFile, displayKeyPassPhrase, capabilities);
    }


    /**
     * Connects to the remote SSH server and does some simple analysis to be able to work both
     * with Linux or Windows based operating systems.
     *
     * @return {@link RemoteSystemCapabilities}
     * @throws SSHException
     */
    private static RemoteSystemCapabilities analyzeRemote(String host, int port, String userName, String password,
        File keyFile, String keyPassPhrase) {
        final String[] sysPropOutputLines;
        final RemoteSystemCapabilities capabilities = new RemoteSystemCapabilities(null, null, GENERIC, UTF_8);
        try (SSHSession session = openSession(capabilities, host, port, userName, password, keyFile, keyPassPhrase)) {
            if (LOG.isLoggable(DEBUG)) {
                Map env = session.detectShellEnv();
                LOG.log(DEBUG, "Environment of the operating system obtained for the SSH client: {0}", env);
            }
            sysPropOutputLines = loadRemoteJavaSystemProperties(session);
        } catch (SSHException e) {
            String msg = "Failed to analyze the remote system. Some commands probably are not supported.";
            LOG.log(WARNING, msg, e);
            return new RemoteSystemCapabilities(null, null, GENERIC, UTF_8);
        }

        String javaHome = getValue("java.home", sysPropOutputLines);
        Version javaVersion = getProperty("java.version", sysPropOutputLines, Version::parse);
        OperatingSystem os = getProperty("os.name", sysPropOutputLines, OperatingSystem::parse);
        Charset charset = getProperty("file.encoding", sysPropOutputLines, Charset::forName);
        return new RemoteSystemCapabilities(javaHome, javaVersion, os, charset);
    }


    private static String[] loadRemoteJavaSystemProperties(SSHSession session) throws SSHException {
        StringBuilder outputBuilder = new StringBuilder();
        // java must be available in the environment.
        // If you use docker java images, check UsePam=yes and /etc/environment
        // By default images configure ENV properties just for the container app, not for ssh clients.
        final int code = session.exec(Arrays.asList("java", "-XshowSettings:properties", "-version"), null,
            outputBuilder);
        if (code != 0) {
            throw new SSHException("Java command on the remote host failed. Output: " + outputBuilder + '.');
        }

        return outputBuilder.toString().split("\\R");
    }


    /**
     * @param alias The alias in the format of ${ALIAS=aliasname}
     * @return expanded password
     */
    public static String expandPasswordAlias(String alias) {
        String expandedPassword = null;
        if (alias == null) {
            return null;
        }
        try {
            expandedPassword = RelativePathResolver.getRealPasswordFromAlias(alias);
        } catch (Exception e) {
            LOG.log(WARNING, "Expansion failed for {0}: {1}", new Object[] {alias, e.getMessage()});
            return null;
        }
        return expandedPassword;
    }


    private static void addIdentity(JSch jsch, File identityFile, String keyPassPhrase) throws SSHException {
        try {
            jsch.addIdentity(identityFile.getAbsolutePath(), keyPassPhrase);
        } catch (JSchException e) {
            throw new SSHException("Invalid key passphrase for key: " + identityFile.getAbsolutePath() + ".", e);
        }
    }

    private static String getHost(Node node) {
        SshConnector sshConnector = node.getSshConnector();
        String sshHost = sshConnector.getSshHost();
        return sshHost == null || sshHost.isEmpty() ? node.getNodeHost() : sshConnector.getSshHost();
    }


    private static int getPort(final SshConnector connector) {
        try {
            int sshPort = Integer.parseInt(connector.getSshPort());
            return sshPort > 0 ? sshPort : SSH_PORT_DEFAULT;
        } catch(NumberFormatException nfe) {
            return SSH_PORT_DEFAULT;
        }
    }


    private static String getUserName(final String userName) {
        return userName == null || userName.isEmpty() ? System.getProperty("user.name") : userName;
    }


    private static  T getProperty(String key, String[] lines, Function converter) {
        String value = getValue(key, lines);
        if (value == null) {
            return null;
        }
        return converter.apply(value);
    }


    private static String getValue(String keyName, String[] propertiesOutputLines) {
        for (String line : propertiesOutputLines) {
            int equalSignPosition = line.indexOf('=');
            if (equalSignPosition <= 0) {
                continue;
            }
            String key = line.substring(0, equalSignPosition).strip();
            if (keyName.equals(key)) {
                return equalSignPosition == line.length() - 1 ? "" : line.substring(equalSignPosition + 1).strip();
            }
        }
        return null;
    }


    private static Session openSession(JSch jsch, String host, int port, String userName) throws SSHException {
        try {
            return jsch.getSession(userName, host, port);
        } catch (JSchException e) {
            throw new SSHException("Could not authenticate user " + userName + " to " + host + ':' + port + '.', e);
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy