package org.kisst.gft.ssh;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import org.kisst.gft.RetryableException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.UIKeyboardInteractive;
import com.jcraft.jsch.UserInfo;
public class Ssh {
public static class ExecResult {
public final int exitcode;
public final String stdout;
public final String stderr;
public ExecResult(int exitcode, String stdout, String stderr) {
this.exitcode=exitcode;
this.stdout=stdout;
this.stderr=stderr;
}
}
public static class ExitCodeException extends RuntimeException {
private static final long serialVersionUID = 1L;
public ExitCodeException (SshHost host, String command, int exitvalue, String result) {
super("On host "+host.host+" when running command \""+command+"\" resulted in exitcode "+exitvalue+"\noutput was:"+result);
}
}
private static final Logger logger=LoggerFactory.getLogger(Ssh.class);
static {
JSch.setLogger(new MyLogger());
}
//public static String ssh(Credentials cred, String host, String command) {
public static String ssh(SshHost host, Ssh.Credentials cred, String command) {
ExecResult result=exec(host, cred, command);
if (result.exitcode!=0)
throw new ExitCodeException(host, command, result.exitcode, result.stdout+result.stderr);
return result.stdout+result.stderr;
}
public static ExecResult exec(SshHost host, Ssh.Credentials cred, String command) {
logger.info("Calling {} with command [{}]", host, command);
FileOutputStream fos=null;
try{
JSch jsch=new JSch();
if (host.known_hosts!=null)
jsch.setKnownHosts(host.known_hosts);
if (cred.keyfile!=null) {
logger.debug("Using keyfile {}",cred.keyfile);
jsch.addIdentity(cred.keyfile);
}
Session session=jsch.getSession(cred.user, host.host, 22);
// username and password will be given via UserInfo interface.
session.setUserInfo(cred);
session.connect();
// exec 'scp -f rfile' remotely
Channel channel=session.openChannel("exec");
((ChannelExec)channel).setCommand(command);
((ChannelExec)channel).setAgentForwarding(true);
ByteArrayOutputStream err = new ByteArrayOutputStream();
((ChannelExec)channel).setErrStream(err);
channel.setInputStream(null);
InputStream in=channel.getInputStream();
channel.connect();
StringBuilder result= new StringBuilder();
byte[] tmp=new byte[1024];
int i;
do {
i=in.read(tmp, 0, 1024);
if(i>0)
result.append(new String(tmp, 0, i));
} while (i>=0);
int count=0;
while (! channel.isClosed()) {
if (count>=50)
throw new RuntimeException("SSH Channel was not closed after "+count+" waiting attempts");
logger.info("Sleeping some time because channel is not yet closed, attempt "+count++);
try{Thread.sleep(200);}catch(Exception ee){}
}
int exitvalue = channel.getExitStatus();
//channel.disconnect();
session.disconnect();
if (logger.isWarnEnabled()){
if (exitvalue!=0 )
logger.warn("Call to {} returned exitvalue "+exitvalue, host);
if (err.size()>0)
logger.warn("Call to {} returned stderr {}", host, err.toString());
if (logger.isInfoEnabled())
logger.info("Call to {} returned stdout [{}]", host, result);
}
return new ExecResult(exitvalue, result.toString(), err.toString());
}
catch(IOException e) { throw new RetryableException(e); }
catch(JSchException e) {
if (e.getCause() instanceof IOException)
throw new RetryableException(e);
else
throw new RuntimeException(e);
}
finally {
try {
if(fos!=null) fos.close();
}
catch(IOException e) { throw new RuntimeException(e); }
}
}
public static Session openSession(SshHost host) {
logger.info("creating sftp connection to {} ", host);
try{
JSch jsch=new JSch();
if (host.known_hosts!=null)
jsch.setKnownHosts(host.known_hosts);
Credentials cred = host.cred;
if (cred.keyfile!=null) {
logger.debug("Using keyfile {}",cred.keyfile);
jsch.addIdentity(cred.keyfile);
}
Session session=jsch.getSession(cred.user, host.host, 22);
session.setConfig("PreferredAuthentications", host.preferredAuthentications);
if (host.useCompression) {
session.setConfig("compression.s2c", "zlib,none");
session.setConfig("compression.c2s", "zlib,none");
}
// username and password will be given via UserInfo interface.
session.setUserInfo(cred);
session.connect();
return session;
}
catch(JSchException e) { throw new RuntimeException(e); }
}
public static void closeChannel(Channel channel) {
int count=0;
while (! channel.isClosed()) {
if (count>=50)
throw new RuntimeException("SSH Channel was not closed after "+count+" waiting attempts");
logger.info("Sleeping some time because channel is not yet closed, attempt "+count++);
try{Thread.sleep(200);}catch(Exception ee){}
}
//channel.disconnect();
}
public static class MyLogger implements com.jcraft.jsch.Logger {
public boolean isEnabled(int level){
if (level==DEBUG) return logger.isTraceEnabled();
if (level==INFO) return logger.isDebugEnabled();
if (level==WARN) return logger.isWarnEnabled();
if (level==ERROR) return logger.isErrorEnabled();
if (level==FATAL) return true;
return false;
}
public void log(int level, String message){
// Dirty hack to prevent all the Warnings in the log
if (level==WARN && message.trim().startsWith("Permanently added") && message.trim().endsWith("to the list of known hosts."))
level=DEBUG;
if (level==DEBUG) logger.debug(message);
if (level==INFO) logger.info(message);
if (level==WARN) logger.warn(message);
if (level==ERROR) logger.error(message);
if (level==FATAL) logger.error(message);
}
}
public static class Credentials implements UserInfo , UIKeyboardInteractive{
private final String user;
private final String password;
private final String keyfile;
public Credentials(String user, String password, String keyfile) {
this.user=user;
this.password=password;
this.keyfile=keyfile;
if (keyfile!=null) {
File f=new File(keyfile);
if (! f.exists())
throw new RuntimeException("keyfile "+f+" does not exist");
if (! f.isFile())
throw new RuntimeException("keyfile "+f+" is not a file");
}
}
public void showMessage(String message){ logger.debug("Message: {}",message); }
public boolean promptYesNo(String str){ logger.debug("YesOrNo: {}",str); return true; }
public boolean promptPassphrase(String message){ logger.debug("prompt Passphrase: {}",message); return true; }
public String getPassphrase(){ return ""; }
public boolean promptPassword(String message) { logger.debug("prompt Password: {}",message); return true; }
public String getPassword(){ logger.debug("using Password: {}",password); return password; }
public String[] promptKeyboardInteractive(String destination,
String name,
String instruction,
String[] prompt,
boolean[] echo) {
logger.debug("destination: {}",destination);
logger.debug("name: {}",name);
logger.debug("instruction: {}",instruction);
for (String s: prompt)
logger.debug("prompt[i]: {}",s);
for (boolean b: echo)
logger.debug("echo[i]: {}",b);
return null;
}
}
}