package org.openslx.taskmanager.tasks;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.security.KeyStore;
import java.security.cert.X509Certificate;
import java.util.Date;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLServerSocket;
import javax.net.ssl.SSLServerSocketFactory;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import org.openslx.taskmanager.api.AbstractTask;
import com.google.gson.annotations.Expose;
/**
* @author Christoph Schulthess
*
*/
public class RemoteDebug extends AbstractTask
{
protected Output status = new Output();
@Expose
public String ip;
@Expose
public int port;
/**
* Run debug task
*/
@Override
protected boolean execute()
{
status.addMessage( "INFO: Executing." );
SSLContext ctx = getSSLContext();
if ( ctx == null ) {
status.addMessage( "ERROR: SSLContext is null." );
return false;
}
status.addMessage( "INFO: SSLContext successfully created." );
try ( SSLSocket dbgSock = getDbgSock( ctx );
SSLServerSocket srvSock = getSrvSock( ctx ) ) {
connectToDbg( dbgSock );
bindToPort( srvSock );
try ( SSLSocket poolSock = ( SSLSocket ) srvSock.accept() ) {
status.addMessage( "INFO: Connection from pool client established." );
status.setCltAddr( poolSock.getInetAddress() );
relay( dbgSock, poolSock );
} catch ( Exception ex ) {
throw( ex );
}
} catch ( Exception e ) {
status.addMessage( "ERROR: " + e.getMessage() );
return false;
}
status.addMessage( "INFO: Task finished properly." );
return true;
}
/**
* @param SSLSocket dbgSock
* @throws IOException
* Connect SSLSocket dbgSock to debug server at ip:port specified by the JSON object
*/
protected void connectToDbg ( SSLSocket dbgSock ) throws IOException {
InetSocketAddress addr = new InetSocketAddress( ip, port );
dbgSock.connect( addr );
status.addMessage( "INFO: Connected to debug server at " + ip + ":" + port + "." );
}
/**
* @param SSLServerSocket srvSock
* @throws IOException
* Bind SSLServerSocket srvSock to local port and update status object accordingly.
*/
protected void bindToPort ( SSLServerSocket srvSock ) throws IOException {
srvSock.bind( null );
status.setListenPort( srvSock.getLocalPort() );
status.addMessage( "INFO: Listening on localhost:" + status.getListenPort() + "." );
}
/**
* @return SSLContext
* Get all-trusting SSLContext via the trustAll() method
*/
protected SSLContext getSSLContext() {
SSLContext ctx = null;
try {
ctx = trustAll();
} catch ( Exception e ) {
status.addMessage( "ERROR: Failed to create SSLContext." );
//status.addMessage( "DEBUG: " + getStrStackTrace(e) );
}
return ctx;
}
/**
* @param dbgSock - SSLSocket connected to debug server
* @param poolSock - SSLSocket retrieved via srvSock.accept()
* Create and start the two necessary relays. Join them, so that proper postcondition is enforced.
*/
private void relay( SSLSocket dbgSock, SSLSocket poolSock ) {
Relay toDbg = new Relay( poolSock, dbgSock, status );
Relay toPool = new Relay( dbgSock, poolSock, status );
toDbg.setName( "PoolToDebug" );
toPool.setName( "DebugToPool" );
toDbg.start();
toPool.start();
status.addMessage( "INFO: Threads started." );
try {
for ( Relay r : new Relay[]{ toDbg, toPool })
r.join();
} catch ( InterruptedException ix ) {
status.addMessage( "INFO: Relay closed: " + Thread.currentThread().getName() );
}
}
/**
* @return SSLContext
* @throws Exception
* Create all-trusting X509 certificate manager to disable verification in the bwlp-environment.
*/
protected SSLContext trustAll () throws Exception
{
TrustManager[] trustAllMgr = new TrustManager[] { new X509TrustManager() {
public java.security.cert.X509Certificate[] getAcceptedIssuers() {
return null;
}
public void checkClientTrusted( X509Certificate[] certs, String authType ) {}
public void checkServerTrusted( X509Certificate[] certs, String authType ) {}
}
};
KeyStore ks = KeyStore.getInstance( "JKS" );
try ( InputStream ksIs = new FileInputStream( "/opt/taskmanager/data/keystore.jks" ) ){
ks.load( ksIs, "password".toCharArray() );
}
KeyManagerFactory kmf = KeyManagerFactory.getInstance(
KeyManagerFactory.getDefaultAlgorithm() );
kmf.init( ks, "password".toCharArray() );
SSLContext ctx = SSLContext.getInstance( "SSL" );
ctx.init( kmf.getKeyManagers(), trustAllMgr, new java.security.SecureRandom() );
return ctx;
}
/**
* @param ctx - SSLContext
* @return SSLServerSocket
* @throws IOException
*/
private SSLServerSocket getSrvSock( SSLContext ctx ) throws IOException
{
status.addMessage( "INFO: Creating server socket." );
SSLServerSocketFactory sssf = ctx.getServerSocketFactory();
status.addMessage( "INFO: Server socket factory created." );
SSLServerSocket s = ( SSLServerSocket ) sssf.createServerSocket();
status.addMessage( "INFO: Server socket created." );
return s;
}
/**
* @param ctx - SSLContext
* @return SSLSocket
* @throws IOException
*/
private SSLSocket getDbgSock( SSLContext ctx ) throws IOException
{
status.addMessage( "INFO: Creating debug socket." );
SSLSocketFactory ssf = ctx.getSocketFactory();
SSLSocket s = ( SSLSocket ) ssf.createSocket();
status.addMessage( "INFO: Debug socket created." );
return s;
}
/**
* Set status class for this object and push a first message to it.
*/
@Override
protected boolean initTask()
{
this.setStatusObject( this.status );
status.addMessage( "INFO: Initiating directed relay to debug server at: " + ip + ":" + port + "." );
return true;
}
/**
* Status class that holds information about the current debug task.
* Most important is probably the listen port which is sent to the client to connect its VNC server in reverse mode.
*/
public static class Output
{
protected String messages = null;
protected Date d = null;
protected InetAddress cltAddr = null;
protected int listenPort = -1;
public void setListenPort ( int port ) { listenPort = port; }
public int getListenPort () { return listenPort; }
public void setCltAddr ( InetAddress addr ) { cltAddr = addr; }
public void addMessage( String str )
{
d = new Date();
if ( messages == null ) {
messages = d.toString() + "-" + str;
} else {
messages += "\n" + d.toString() + "-" + str;
}
}
}
/**
* Generic relay class to write data from one socket to another.
* For a working debug task, you need two of these - one for each direction.
*/
private class Relay extends Thread
{
boolean active = true;
private byte[] buffer = new byte[16384];
private SSLSocket srcSock;
private SSLSocket destSock;
private Output status;
public Relay ( SSLSocket srcSock, SSLSocket destSock, Output status ) {
this.srcSock = srcSock;
this.destSock = destSock;
this.status = status;
}
@Override
public void run() {
boolean first = true;
int readBytes;
try ( InputStream in = srcSock.getInputStream();
OutputStream out = destSock.getOutputStream() ) {
status.addMessage( "INFO: " + this.getName() );
while( active ) {
readBytes = in.read( buffer );
out.write( buffer, 0, readBytes );
if ( first ) {
status.addMessage( "INFO: Relay operating: " + this.getName() );
first = false;
}
}
} catch ( Exception e ) {
status.addMessage( "DEBUG: " + this.getName() + " - " + getStrStackTrace( e ) );
active = false;
return;
}
}
}
public static String getStrStackTrace(Throwable aThrowable) {
Writer result = new StringWriter();
PrintWriter printWriter = new PrintWriter(result);
aThrowable.printStackTrace(printWriter);
return result.toString();
}
}