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. */ protected 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 */ protected 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 */ protected 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 String 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.getHostAddress(); } 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() ) { 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(); // } }