/*
* Copyright 2015 Cel Skeggs.
*
* This file is part of the CCRE, the Common Chicken Runtime Engine.
*
* The CCRE is free software: you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* The CCRE 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 Lesser General Public License for more
* details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with the CCRE. If not, see <http://www.gnu.org/licenses/>.
*/
package ccre.deployment;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
import java.nio.file.Files;
import java.security.PublicKey;
import java.util.concurrent.TimeUnit;
import ccre.util.Utils;
import net.schmizz.sshj.SSHClient;
import net.schmizz.sshj.connection.channel.direct.Session;
import net.schmizz.sshj.connection.channel.direct.Session.Command;
import net.schmizz.sshj.transport.verification.HostKeyVerifier;
import net.schmizz.sshj.xfer.FileSystemFile;
import net.schmizz.sshj.xfer.InMemorySourceFile;
import net.schmizz.sshj.xfer.scp.SCPFileTransfer;
/**
* A connection to a remote SSH server.
*
* @author skeggsc
*/
public class Shell implements AutoCloseable {
private final SSHClient client;
/**
* Creates a new SSH connection to <code>ip</code> with
* <code>username</code> and <code>password</code>.
*
* Any host key will be accepted.
*
* @param ip the IP address of the remote target.
* @param username the username of the user to log in as.
* @param password the password of the user to log in as.
* @throws IOException if the connection cannot be established.
*/
public Shell(InetAddress ip, String username, String password) throws IOException {
client = new SSHClient();
client.setConnectTimeout(5000);
client.setTimeout(5000);
client.addHostKeyVerifier(new HostKeyVerifier() {
@Override
public boolean verify(String hostname, int port, PublicKey key) {
// TODO: is this a huge security hole? the official version does
// this too.
return true;
}
});
client.connect(ip);
client.authPassword(username, password);
}
/**
* Runs <code>command</code> on the remote SSH server, and throw an
* IOException if it fails.
*
* @param command the command to attempt.
* @throws IOException if the command cannot be executed or if it fails.
*/
public void execCheck(String command) throws IOException {
int code = exec(command);
if (code != 0) {
throw new IOException("Command return nonzero exit code " + code + ": '" + command + "'");
}
}
/**
* Runs <code>command</code> on the remote SSH server, and return its exit
* code, or 257 if it timed out (took more than a minute.)
*
* @param command the command to attempt.
* @return the command's exit code, or 257 if it timed out.
* @throws IOException if the command cannot be executed.
*/
public int exec(String command) throws IOException {
try (Session session = client.startSession()) {
try (Command running = session.exec(command)) {
running.join(1, TimeUnit.MINUTES);
Integer status = running.getExitStatus();
return status == null ? 257 : status;
}
}
}
/**
* Downloads a file from the remote server.
*
* This first downloads the file to a local temporary file and then provides
* an input stream to read from that file.
*
* @param sourcePath the path on the remote end to receive a file from.
* @return the InputStream reading from that file.
* @throws IOException if the file cannot be received.
*/
public InputStream receiveFile(String sourcePath) throws IOException {
SCPFileTransfer transfer = client.newSCPFileTransfer();
File tempFile = File.createTempFile("scp-", ".recv");
tempFile.deleteOnExit();
// TODO: does this actually tell us if it fails?
transfer.download(sourcePath, new FileSystemFile(tempFile));
return new FileInputStream(tempFile);
}
/**
* Uploads a file to the remote SSH server.
*
* @param localFile the local file to upload.
* @param remotePath the file or directory to upload the file to.
* @throws IOException if the file cannot be sent.
*/
public void sendFileTo(File localFile, String remotePath) throws IOException {
SCPFileTransfer transfer = client.newSCPFileTransfer();
transfer.upload(new FileSystemFile(localFile), remotePath);
}
/**
* Uploads an InputStream as a file to the remote SSH server.
*
* @param stream an InputStream to use as the file's data.
* @param name the name of the file on the remote end, if the remote path is
* a directory.
* @param remotePath the file or directory to upload the file to.
* @param permissions the permissions for the file to have on the remote
* end.
* @throws IOException if the file cannot be sent.
*/
public void sendFileTo(InputStream stream, String name, String remotePath, int permissions) throws IOException {
if (stream == null) {
throw new NullPointerException("Stream is NULL!");
}
SCPFileTransfer transfer = client.newSCPFileTransfer();
// TODO: clean this part up?
File temp = File.createTempFile("scp-", ".send");
temp.delete();
temp.deleteOnExit();
Files.copy(stream, temp.toPath());
transfer.upload(new InMemorySourceFile() {
@Override
public String getName() {
return name;
}
@Override
public long getLength() {
return temp.length();
}
@Override
public InputStream getInputStream() throws IOException {
return new FileInputStream(temp);
}
@Override
public int getPermissions() throws IOException {
return permissions;
}
}, remotePath);
}
/**
* Uploads a binary resource from a class to the remote SSH server. The
* difference from {@link #sendTextResourceTo(Class, String, String, int)}
* is that this will not modify the linefeeds on the file.
*
* @param clazz the class to find the resource on.
* @param resource the resource name.
* @param remotePath the remote path to upload the file to.
* @param permissions the permissions for the file to have.
* @throws IOException if the file cannot be sent.
*/
public void sendBinResourceTo(Class<?> clazz, String resource, String remotePath, int permissions) throws IOException {
try (InputStream resin = clazz.getResourceAsStream(resource)) {
if (resin == null) {
throw new RuntimeException("Cannot find resource: " + resource);
}
sendFileTo(resin, resource.substring(resource.lastIndexOf('/') + 1), remotePath, permissions);
}
}
/**
* Uploads a textual resource from a class to the remote SSH server. The
* difference from {@link #sendBinResourceTo(Class, String, String, int)} is
* that this will convert CRLFs to LFs.
*
* @param clazz the class to find the resource on.
* @param resource the resource name.
* @param remotePath the remote path to upload the file to.
* @param permissions the permissions for the file to have.
* @throws IOException if the file cannot be sent.
*/
public void sendTextResourceTo(Class<?> clazz, String resource, String remotePath, int permissions) throws IOException {
try (InputStream resin = clazz.getResourceAsStream(resource)) {
if (resin == null) {
throw new RuntimeException("Cannot find resource: " + resource);
}
// rewrites CRLF to LF.
InputStream stripped = Utils.stripCarriageReturns(resin);
sendFileTo(stripped, resource.substring(resource.lastIndexOf('/') + 1), remotePath, permissions);
}
}
@Override
public void close() throws IOException {
client.close();
}
}