/******************************************************************************* * Copyright (c) 2012-2017 Codenvy, S.A. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Codenvy, S.A. - initial API and implementation *******************************************************************************/ package org.eclipse.che.plugin.machine.ssh.jsch; import com.jcraft.jsch.ChannelExec; import com.jcraft.jsch.ChannelSftp; import com.jcraft.jsch.JSch; import com.jcraft.jsch.JSchException; import com.jcraft.jsch.Session; import com.jcraft.jsch.SftpException; import org.eclipse.che.api.core.util.ListLineConsumer; import org.eclipse.che.api.machine.server.exception.MachineException; import org.eclipse.che.commons.lang.IoUtil; import org.eclipse.che.plugin.machine.ssh.SshClient; import org.eclipse.che.plugin.machine.ssh.SshMachineRecipe; import javax.inject.Inject; import javax.inject.Named; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; import java.util.Map; import static java.lang.String.format; /** * Client for communication with ssh machine using ssh protocol. * * @author Alexander Garagatyi */ // todo think about replacement JSch with Apace SSHD // todo tests for ssh library that ensures that it works as expected public class JschSshClient implements SshClient { private final JSch jsch; private final JschUserInfoImpl user; private final String host; private final int port; private final String username; private final Map<String, String> envVars; private final int connectionTimeout; private Session session; @Inject public JschSshClient(SshMachineRecipe sshMachineRecipe, Map<String, String> envVars, JSch jsch, @Named("che.workspace.ssh_connection_timeout_ms") int connectionTimeoutMs) { this.envVars = envVars; this.connectionTimeout = connectionTimeoutMs; this.user = JschUserInfoImpl.builder() .password(sshMachineRecipe.getPassword()) .promptPassword(true) .passphrase(null) .promptPassphrase(false) .promptYesNo(true) .build(); this.jsch = jsch; this.host = sshMachineRecipe.getHost(); this.port = sshMachineRecipe.getPort(); this.username = sshMachineRecipe.getUsername(); } @Override public String getHost() { return host; } @Override public void start() throws MachineException { try { session = jsch.getSession(username, host, port); session.setUserInfo(user); // todo remember parent pid of shell to be able to kill all processes on client stop if (!session.isConnected()) { session.connect(connectionTimeout); } } catch (JSchException e) { throw new MachineException("Ssh machine creation failed because ssh of machine is inaccessible. Error: " + e.getLocalizedMessage()); } } //todo add method to read env vars by client // ChannelExec execAndGetCode = (ChannelExec)session.openChannel("execAndGetCode"); // execAndGetCode.setCommand("env"); //envVars.entrySet() // .stream() // .forEach(envVariableEntry -> execAndGetCode.setEnv(envVariableEntry.getKey(), // envVariableEntry.getValue())); // todo process output @Override public void stop() throws MachineException { session.disconnect(); } @Override public JschSshProcess createProcess(String commandLine) throws MachineException { try { ChannelExec exec = (ChannelExec)session.openChannel("exec"); exec.setCommand(commandLine); exec.setPty(true); envVars.entrySet() .stream() .forEach(envVariableEntry -> exec.setEnv(envVariableEntry.getKey(), envVariableEntry.getValue())); return new JschSshProcess(exec); } catch (JSchException e) { throw new MachineException("Can't establish connection to perform command execution in ssh machine. Error: " + e.getLocalizedMessage(), e); } } @Override public void copy(String sourcePath, String targetPath) throws MachineException { File source = new File(sourcePath); if (!source.exists()) { throw new MachineException("Source of copying '" + sourcePath + "' doesn't exist."); } if (source.isDirectory()) { copyRecursively(sourcePath, targetPath); } else { copyFile(sourcePath, targetPath); } } private void copyRecursively(String sourceFolder, String targetFolder) throws MachineException { // create target dir try { int execCode = execAndGetCode("mkdir -p " + targetFolder); if (execCode != 0) { throw new MachineException(format("Creation of folder %s failed. Exit code is %s", targetFolder, execCode)); } } catch (JSchException | IOException e) { throw new MachineException(format("Creation of folder %s failed. Error: %s", targetFolder, e.getLocalizedMessage())); } // not normalized paths don't work final String targetAbsolutePath = getAbsolutePath(targetFolder); // copy files ChannelSftp sftp = null; try { sftp = (ChannelSftp)session.openChannel("sftp"); sftp.connect(connectionTimeout); final ChannelSftp finalSftp = sftp; Files.walkFileTree(Paths.get(sourceFolder), new SimpleFileVisitor<Path>() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { try { if (!attrs.isDirectory()) { copyFile(file.toString(), Paths.get(targetAbsolutePath, file.getFileName().toString()).toString(), finalSftp); } else { finalSftp.mkdir(file.normalize().toString()); } } catch (MachineException | SftpException e) { throw new IOException(format("Sftp copying of file %s failed. Error: %s", file, e.getLocalizedMessage())); } return FileVisitResult.CONTINUE; } }); } catch (JSchException | IOException e) { throw new MachineException("Copying failed. Error: " + e.getLocalizedMessage()); } finally { if (sftp != null) { sftp.disconnect(); } } } private void copyFile(String sourcePath, String targetPath) throws MachineException { ChannelSftp sftp = null; try { sftp = (ChannelSftp)session.openChannel("sftp"); sftp.connect(connectionTimeout); String absoluteTargetPath = getAbsolutePath(targetPath); copyFile(sourcePath, absoluteTargetPath, sftp); } catch (JSchException e) { throw new MachineException("Sftp copying failed. Error: " + e.getLocalizedMessage()); } finally { if (sftp != null) { sftp.disconnect(); } } } private void copyFile(String sourcePath, String absoluteTargetPath, ChannelSftp channelSftp) throws MachineException { try { channelSftp.put(sourcePath, absoluteTargetPath); // apply permissions File file = new File(sourcePath); // read int permissions = 256; // execute if (file.canExecute()) { permissions += 64; } // write if (file.canWrite()) { permissions += 128; } channelSftp.chmod(permissions, absoluteTargetPath); } catch (SftpException e) { throw new MachineException(format("Sftp copying of file %s failed. Error: %s", absoluteTargetPath, e.getLocalizedMessage())); } } private String getAbsolutePath(String path) throws MachineException { try { return execAndGetOutput("cd " + path + "; pwd"); } catch (JSchException | IOException | MachineException e) { throw new MachineException("Target directory lookup failed. " + e.getLocalizedMessage()); } } private int execAndGetCode(String command) throws JSchException, IOException { ChannelExec exec = (ChannelExec)session.openChannel("exec"); exec.setCommand(command); try (InputStream inStream = exec.getInputStream(); InputStream erStream = exec.getErrStream()) { exec.connect(connectionTimeout); // read streams to wait until command finishes its work IoUtil.readStream(inStream); IoUtil.readStream(erStream); } finally { exec.disconnect(); } return exec.getExitStatus(); } private String execAndGetOutput(String command) throws JSchException, MachineException, IOException { ChannelExec exec = (ChannelExec)session.openChannel("exec"); exec.setCommand(command); try (BufferedReader reader = new BufferedReader(new InputStreamReader(exec.getInputStream())); InputStream erStream = exec.getErrStream()) { exec.connect(connectionTimeout); ListLineConsumer listLineConsumer = new ListLineConsumer(); String line; while ((line = reader.readLine()) != null) { listLineConsumer.writeLine(line); } // read stream to wait until command finishes its work IoUtil.readStream(erStream); if (exec.getExitStatus() != 0) { throw new MachineException(format("Error code: %s. Error: %s", exec.getExitStatus(), IoUtil.readAndCloseQuietly(exec.getErrStream()))); } return listLineConsumer.getText(); } finally { exec.disconnect(); } } }