package com.laytonsmith.PureUtilities; import com.jcraft.jsch.Channel; import com.jcraft.jsch.ChannelExec; import com.jcraft.jsch.JSch; import com.jcraft.jsch.JSchException; import com.jcraft.jsch.Session; import com.jcraft.jsch.UserInfo; import com.laytonsmith.PureUtilities.Common.StreamUtils; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * This class wraps the JSch library, to make atomic operations easier to do. * * */ public class SSHWrapper { private SSHWrapper() { } /** * Copies a file from/to a remote host, via ssh. Currently, both paths * being remote is not supported. A path can look like the following: * user@remote[:port[:password]]:path/to/remote/file If the password is * not specified, then public key authentication will be assumed. The * port must be specified if the password is specified, but setting it * to 0 will use the default (22), allowing it to be bypassed. * @param from * @param to */ public static void SCP(String from, String to) throws IOException { if ((from.contains("@") && to.contains("@")) || (!from.contains("@") && !to.contains("@"))) { throw new IOException("Paths cannot be both remote, or both local."); } //Now that we've handled the case where both paths are remote, we //can determine which one is the remote path, and proceed from there. String remote = to; if (from.contains("@")) { remote = from; } //Now, parse the remote connection for information Matcher m = Pattern.compile("(.+?)@(.+?)(?:\\:(.+?)(?:\\:(.+?))?)?\\:(.+)").matcher(remote); String syntaxErrorMsg = "Remote host connection must match the following syntax: user@host[:port[:password]]:path/to/file"; if (m.find()) { String user = m.group(1); String host = m.group(2); String sport = m.group(3); int port = 22; final String password = m.group(4); String file = m.group(5); try { if (sport != null) { port = Integer.parseInt(sport); } if (port == 0) { port = 22; } } catch (NumberFormatException e) { //They may have been trying this: //user@host:password:/file/path //If that's the case, password will //be null, so let's give them a better error message. if (password == null) { throw new IOException(syntaxErrorMsg + " (It appears as though you may have been trying a password" + " in place of the port. You may specify the port to be 0 if you want it to use the default," + " to bypass the port parameter.)"); } } if (port < 1 || port > 65535) { throw new IOException("Port numbers must be between 1 and 65535"); } try { JSch jsch = new JSch(); Session sshSession = null; File known_hosts = new File(System.getProperty("user.home") + "/.ssh/known_hosts"); if (!known_hosts.exists()) { throw new IOException("No known hosts file exists at " + known_hosts.getAbsolutePath()); } jsch.setKnownHosts(known_hosts.getAbsolutePath()); if (password == null) { //We need to try public key authentication File privKey = new File(System.getProperty("user.home") + "/.ssh/id_rsa"); if (privKey.exists()) { jsch.addIdentity(privKey.getAbsolutePath()); } else { throw new IOException("No password provided, and no private key exists at " + privKey.getAbsolutePath()); } } sshSession = jsch.getSession(user, host, port); sshSession.setUserInfo(new UserInfo() { @Override public String getPassphrase() { //This may need to be made more granular later return password; } @Override public String getPassword() { return password; } @Override public boolean promptPassword(String message) { return true; } @Override public boolean promptPassphrase(String message) { return true; } @Override public boolean promptYesNo(String message) { StreamUtils.GetSystemOut().println(message + " (Automatically responding with 'Yes')"); return true; } @Override public void showMessage(String message) { StreamUtils.GetSystemOut().println(message); } }); //10 second timeout sshSession.connect(10 * 1000); // http://www.jcraft.com/jsch/examples/ if (from.contains("@")) { //We are pulling a remote file here, so we need to use SCPFrom File localFile = new File(to); SCPFrom(file, localFile, sshSession); } else { //We are pushing a local file to a remote, so we need to use SCPTo File localFile = new File(from); SCPTo(localFile, file, sshSession); } sshSession.disconnect(); } catch (JSchException ex) { throw new IOException(ex); } } else { throw new IOException(syntaxErrorMsg); } } private static void SCPTo(File lfile, String rfile, Session session) throws JSchException, IOException { boolean ptimestamp = true; // exec 'scp -t rfile' remotely String command = "scp " + (ptimestamp ? "-p" : "") + " -t " + rfile; Channel channel = session.openChannel("exec"); ((ChannelExec) channel).setCommand(command); // get I/O streams for remote scp OutputStream out = channel.getOutputStream(); InputStream in = channel.getInputStream(); channel.connect(); checkAck(in); if (ptimestamp) { command = "T " + (lfile.lastModified() / 1000) + " 0"; // The access time should be sent here, // but it is not accessible with JavaAPI ;-< command += (" " + (lfile.lastModified() / 1000) + " 0\n"); out.write(command.getBytes()); out.flush(); checkAck(in); } // send "C0644 filesize filename", where filename should not include '/' long filesize = lfile.length(); command = "C0644 " + filesize + " "; if (lfile.getPath().lastIndexOf('/') > 0) { command += lfile.getPath().substring(lfile.getPath().lastIndexOf('/') + 1); } else { command += lfile; } command += "\n"; out.write(command.getBytes()); out.flush(); checkAck(in); // send a content of lfile FileInputStream fis = new FileInputStream(lfile); byte[] buf = new byte[1024]; while (true) { int len = fis.read(buf, 0, buf.length); if (len <= 0) { break; } out.write(buf, 0, len); //out.flush(); } fis.close(); fis = null; // send '\0' buf[0] = 0; out.write(buf, 0, 1); out.flush(); checkAck(in); out.close(); channel.disconnect(); } private static void SCPFrom(String remote, File local, Session session) throws IOException, JSchException { // exec 'scp -f rfile' remotely String command = "scp -f " + remote; Channel channel = session.openChannel("exec"); ((ChannelExec) channel).setCommand(command); // get I/O streams for remote scp OutputStream out = channel.getOutputStream(); InputStream in = channel.getInputStream(); channel.connect(); byte[] buf = new byte[1024]; // send '\0' buf[0] = 0; out.write(buf, 0, 1); out.flush(); while (true) { int c = checkAckFrom(in); if (c != 'C') { break; } // read '0644 ' in.read(buf, 0, 5); long filesize = 0L; while (true) { if (in.read(buf, 0, 1) < 0) { // error break; } if (buf[0] == ' ') { break; } filesize = filesize * 10L + (long) (buf[0] - '0'); } String file = null; for (int i = 0;; i++) { in.read(buf, i, 1); if (buf[i] == (byte) 0x0a) { file = new String(buf, 0, i); break; } } // send '\0' buf[0] = 0; out.write(buf, 0, 1); out.flush(); String prefix = null; if (local.isDirectory()) { prefix = local.getPath() + File.separator; } // read a content of lfile FileOutputStream fos = new FileOutputStream(prefix == null ? local.getPath() : prefix + file); int foo; while (true) { if (buf.length < filesize) { foo = buf.length; } else { foo = (int) filesize; } foo = in.read(buf, 0, foo); if (foo < 0) { // error break; } fos.write(buf, 0, foo); filesize -= foo; if (filesize == 0L) { break; } } fos.close(); fos = null; checkAckFrom(in); // send '\0' buf[0] = 0; out.write(buf, 0, 1); out.flush(); } channel.disconnect(); } private static void checkAck(InputStream in) throws IOException { int b = in.read(); // b may be 0 for success, // 1 for error, // 2 for fatal error, // -1 if (b == 1 || b == 2) { StringBuffer sb = new StringBuffer(); int c; do { c = in.read(); sb.append((char) c); } while (c != '\n'); if (b == 1) { // error throw new IOException(sb.toString()); } if (b == 2) { // fatal error throw new IOException(sb.toString()); } } } static int checkAckFrom(InputStream in) throws IOException { int b = in.read(); // b may be 0 for success, // 1 for error, // 2 for fatal error, // -1 if (b == 0) { return b; } if (b == -1) { return b; } if (b == 1 || b == 2) { StringBuffer sb = new StringBuffer(); int c; do { c = in.read(); sb.append((char) c); } while (c != '\n'); if (b == 1) { // error throw new IOException(sb.toString()); } if (b == 2) { // fatal error throw new IOException(sb.toString()); } } return b; } /** * Given an input stream, writes it out to a remote file system. The * path given (to) must be a remote path. * * @param is */ public static void SCPWrite(InputStream is, String to) throws IOException { File temp = File.createTempFile("methodscript-temp-file", ".tmp"); FileOutputStream fos = new FileOutputStream(temp); StreamUtils.Copy(is, fos); fos.close(); try { SCP(temp.getAbsolutePath(), to); } finally { temp.delete(); temp.deleteOnExit(); } } /** * Returns an InputStream to a file on a remote file system. * * @param from * @return */ public static InputStream SCPRead(String from) throws IOException { File temp = File.createTempFile("methodscript-temp-file", ".tmp"); SCP(from, temp.getAbsolutePath()); FileInputStream fis = new FileInputStream(temp); temp.deleteOnExit(); return fis; } /** * Writes some textual contents to a remote file. * * @param contents * @param to */ public static void SCPWrite(String contents, String to) throws IOException { SCPWrite(StreamUtils.GetInputStream(contents), to); } public static String SCPReadString(String from) throws IOException { return StreamUtils.GetString(SCPRead(from)); } // /** // * Executes a command over ssh. // */ // public static void SSHExec(){ // // } }