package org.playorm.nio.impl.libs; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.NotYetConnectedException; import java.util.logging.Level; import java.util.logging.Logger; import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLEngineResult; import javax.net.ssl.SSLEngineResult.HandshakeStatus; import javax.net.ssl.SSLEngineResult.Status; import javax.net.ssl.SSLException; import javax.net.ssl.SSLSession; import org.playorm.nio.api.channels.NioException; import org.playorm.nio.api.deprecated.ChannelServiceFactory; import org.playorm.nio.api.libs.AsyncSSLEngineException; import org.playorm.nio.api.libs.AsyncSSLEngine; import org.playorm.nio.api.libs.BufferHelper; import org.playorm.nio.api.libs.PacketAction; import org.playorm.nio.api.libs.SSLListener; /** * There is synchronization on the close so if two threads call close, they both return * normally ending up in the SSLEngine closing. * * There is a synchronization on * @author dean.hiller * */ public class AsynchSSLEngineImpl implements AsyncSSLEngine { private static final Logger log = Logger.getLogger(AsyncSSLEngine.class.getName()); private static final SSLListener NULL_LIST = new NullListener(); private static final BufferHelper HELPER = ChannelServiceFactory.bufferHelper(null); //private TCPChannel realChannel; private boolean isConnected = false; private Object id; //only one of these two should exist. The other is null... private SSLListener sslListener = NULL_LIST; private SSLEngine sslEngine; private ByteBuffer socketToEngineData2; private ByteBuffer engineToAppData; private ByteBuffer engineToSocketData; private boolean isClosing; private boolean runningRunnable; private ByteBuffer empty = ByteBuffer.allocate(1); private boolean isClosed; private boolean clientInitiated; public AsynchSSLEngineImpl(Object id, SSLEngine sslEngine) { this.id = id; this.sslEngine = sslEngine; SSLSession session = sslEngine.getSession(); socketToEngineData2 = ByteBuffer.allocate(session.getPacketBufferSize()); engineToSocketData = ByteBuffer.allocate(session.getPacketBufferSize()); engineToAppData = ByteBuffer.allocate(session.getApplicationBufferSize()); log.fine("peerNetData: " + socketToEngineData2.capacity() + ", peerAppData: " + engineToAppData.capacity() + ", netData: " + engineToSocketData.capacity()); // The engineToAppData buffer is assumed to be ready to be written, // while the other buffers are assumed to be ready to be read from. // Change the position of the buffers so that a // call to hasRemaining() returns false. A buffer is considered // empty when the position is set to its limit, that is when // hasRemaining() returns false. HELPER.eraseBuffer(engineToAppData); HELPER.eraseBuffer(engineToSocketData); } public void setListener(SSLListener l) { //this prevents NullPointerExceptions when firing to listener //as well as preventing need for synchronization to avoid nullpointerExceptions.... if(l == null) sslListener = NULL_LIST; else sslListener = l; } private static final class NullListener implements SSLListener { public void encryptedLinkEstablished() throws IOException { } public void packetEncrypted(ByteBuffer engineToSocketData, Object passThrough) throws IOException { } public void packetUnencrypted(ByteBuffer out, Object passThrough) { } public void runTask(Runnable r) {} public void closed(boolean fromEncryptedPacket) {} } public void beginHandshake() { if(isClosing || isClosed) { throw new IllegalStateException(id+"SSLEngine is in the process of closing or is closed"); } else if(runningRunnable) throw new IllegalStateException(id+"SSLListener was passed a Runnable object " + "that has not completed yet and must complete before encryption can continue"); if(log.isLoggable(Level.FINE)) log.fine(id+"start handshake"); try { sslEngine.beginHandshake(); } catch (SSLException e) { throw new AsyncSSLEngineException(e); } HandshakeStatus status = sslEngine.getHandshakeStatus(); continueHandShake(status); } public void feedPlainPacket(ByteBuffer b, Object passThrough) { if(!isConnected) throw new NotYetConnectedException(); else if(isClosing || isClosed) { throw new IllegalStateException(id+"SSLEngine is in the process of closing or is closed"); } else if(runningRunnable) throw new IllegalStateException(id+"SSLListener was passed a Runnable object that" + " has not completed yet and must complete before encryption can continue"); if(log.isLoggable(Level.FINE)) log.fine(id+"feedPlainPacket [in-buffer] pos="+b.position()+" lim="+b.limit()); HELPER.eraseBuffer(engineToSocketData); SSLEngineResult result; try { result = sslEngine.wrap(b, engineToSocketData); } catch (SSLException e) { throw new AsyncSSLEngineException(e); } Status status = result.getStatus(); HandshakeStatus hsStatus = result.getHandshakeStatus(); if(status != Status.OK) throw new RuntimeException("Bug, status="+status+" instead of OK. hsStatus="+ hsStatus+" Something went wrong and we could not encrypt the data"); else if(b.hasRemaining()) throw new RuntimeException(id+"Bug, should read all my data every time"); HELPER.doneFillingBuffer(engineToSocketData); if(log.isLoggable(Level.FINE)) log.fine(id+"SSLListener.packetEncrypted pos="+engineToSocketData.position()+ " lim="+engineToSocketData.limit()+" hsStatus="+hsStatus+" status="+status); fireEncryptedPacketToListener(passThrough); } /** * This is synchronized as the socketToEngineData2 buffer is modified in this method * and modified in other methods that are called on other threads.(ie. the put is called) * */ public PacketAction feedEncryptedPacket(ByteBuffer b, Object passthrough) { if(isClosed) { throw new IllegalStateException(id+"SSLEngine is closed"); } else if(runningRunnable) throw new IllegalStateException(id+"SSLListener was passed a Runnable object" + " that has not completed yet and must complete before decryption can continue"); try { HandshakeStatus hsStatus = sslEngine.getHandshakeStatus(); if(log.isLoggable(Level.FINE)) { log.fine(id+"feedEncryptedPacket [in-buffer] pos="+b.position()+ " lim="+b.limit()+" hsStatus="+hsStatus+" cap="+b.capacity()); } socketToEngineData2.put(b); if(log.isLoggable(Level.FINEST)) log.finest(id+"[sockToEngine] pos="+socketToEngineData2.position()+" lim="+socketToEngineData2.limit()); PacketAction result = feedEncryptedPacketImpl(passthrough); if(b.hasRemaining()) throw new RuntimeException(this+"BUG, need to read all data from ByteBuffer. incoming="+b+" socketToEngineBuf="+socketToEngineData2); return result; } catch (AsyncSSLEngineException e) { //try to close SSLEngine throw closeTry(e); } catch(NioException e) { throw closeTry(e); } } private RuntimeException closeTry(RuntimeException e) { try { close(); } catch(Exception ee) { log.log(Level.WARNING, "Failure trying to shutdown Link properly. Encryption Engine is closed", ee); } throw e; } private PacketAction feedEncryptedPacketImpl(Object passthrough) { PacketAction action = PacketAction.NOT_ENOUGH_BYTES_YET; ByteBuffer b = socketToEngineData2; if(log.isLoggable(Level.FINEST)) log.finest(id+"[sockToEngine] finished filling pos="+b.position()+" lim="+b.limit()); ByteBuffer out = engineToAppData; HELPER.eraseBuffer(out); HELPER.doneFillingBuffer(b); HandshakeStatus hsStatus = sslEngine.getHandshakeStatus(); Status status = null; if(log.isLoggable(Level.FINEST)) log.finest(id+"[sockToEngine] going to unwrap pos="+b.position()+ " lim="+b.limit()+" hsStatus="+hsStatus); int i = 0; //stay in loop while we //1. need unwrap or not_handshaking or need_task AND //2. have data in buffer //3. have enough data in buffer(ie. not underflow) while(b.hasRemaining() && status != Status.BUFFER_UNDERFLOW && status != Status.CLOSED) { i++; SSLEngineResult result; try { result = sslEngine.unwrap(b, out); } catch(SSLException e) { AsyncSSLEngineException ee = new AsyncSSLEngineException("status="+status+" hsStatus="+hsStatus+" b="+b, e); throw ee; } status = result.getStatus(); hsStatus = result.getHandshakeStatus(); if(log.isLoggable(Level.FINEST)) log.finest(id+"[sockToEngine] unwrap done pos="+b.position()+" lim="+ b.limit()+" status="+status+" hs="+hsStatus); if(out.position() != 0 && status != Status.CLOSED) { //hsStatus == HandshakeStatus.NOT_HANDSHAKING && status != Status.BUFFER_UNDERFLOW) { HELPER.doneFillingBuffer(out); if(log.isLoggable(Level.FINE)) log.fine(id+"packetUnencrypted pos="+out.position()+" lim="+ out.limit()+" hs="+hsStatus+" status="+status); action = PacketAction.DECRYPTED_AND_FEDTOLISTENER; fireUnencryptedPackToListener(passthrough, out); if(out.hasRemaining()) log.warning(id+"Discarding unread data"); out.clear(); } if(i > 1000) throw new RuntimeException(this+"Bug, stuck in loop, bufIn="+b+" bufOut="+out+ " hsStatus="+hsStatus+" status="+status); else if(hsStatus == HandshakeStatus.NEED_TASK) { //if status is need task, we need to break to run the task before other handshake //messages? break; } } resetBuffer(status); if(log.isLoggable(Level.FINEST)) log.finest(id+"[sockToEngine] reset pos="+b.position()+" lim="+b.limit()+" status="+status+" hs="+hsStatus); if(log.isLoggable(Level.FINEST) && isConnected && status != Status.CLOSED) { log.finest(id+"[out-buffer] pos="+out.position()+" lim="+out.limit()); } //First if avoids case where the close handshake is still going on so we are not closed //yet I think(I am writing this from memory)... if(status == Status.CLOSED && hsStatus == HandshakeStatus.NOT_HANDSHAKING) { isClosed = true; closeInbound(); sslEngine.closeOutbound(); sslListener.closed(clientInitiated); } else //TODO: add else if(!hsStatus == HandshakeStatus.NOT_HANDSHAKING) continueHandShake(hsStatus); return action; } private void closeInbound() { try { sslEngine.closeInbound(); } catch (SSLException e) { throw new AsyncSSLEngineException(e); } } private void fireUnencryptedPackToListener(Object passthrough, ByteBuffer out) { try { sslListener.packetUnencrypted(out, passthrough); } catch (IOException e) { throw new NioException(e); } } private void resetBuffer(Status status) { ByteBuffer b = socketToEngineData2; if(!b.hasRemaining()) { //if the buffer doesn't have any data in it...clear it... b.clear(); } else { //TODO: don't need to move bytes unless the incoming data is bigger than (limit-position) //so we could do this one in the feedPacket method and move it there. //TODO: rethink using byte array?....... byte[] tmp = new byte[b.remaining()]; b.get(tmp); b.clear(); b.put(tmp); //put the remaining data at the beginning of this buffer if(log.isLoggable(Level.FINEST)) log.finest("[sockToEngine] underflow pos="+b.position()+" lim="+b.limit()); } } private void continueHandShake(HandshakeStatus status) { switch (status) { case FINISHED: fireConnected(); break; case NEED_TASK: createRunnable(); //break; case NEED_UNWRAP: if(log.isLoggable(Level.FINE)) log.fine(id+"need unwrap so wait"); break; case NEED_WRAP: if(log.isLoggable(Level.FINEST)) log.finest(id+"need wrap"); //if we go to need wrap, I think we can reset the incoming data buffer HELPER.eraseBuffer(engineToSocketData); sendHandshakeMessage(); break; case NOT_HANDSHAKING: if(log.isLoggable(Level.FINEST)) log.finest(id+"not handshaking"); break; default: log.warning("Bug, should never end up here"); break; } } private void createRunnable() { Runnable r = sslEngine.getDelegatedTask(); if(r == null) //task has already been retrieved return; Runnable sslRun = new SSLRunnable(r); boolean isInitialHandshake = !isConnected; if(!isInitialHandshake) runningRunnable = true; scheduleRunnable(sslRun, isInitialHandshake); } protected void scheduleRunnable(Runnable sslRun, boolean isInitialHandshake) { if(log.isLoggable(Level.FINE)) log.fine(id+"runTask"); sslListener.runTask(sslRun); } protected void runRunnable(Runnable r) { r.run(); try { if(log.isLoggable(Level.FINER)) log.finer(id+"Running actual task now"); continueFromTask(); } catch(IOException e) { log.log(Level.WARNING, id+"not continuing handshake, exception occurred", e); initiateClose(); } finally { runningRunnable = false; } } private class SSLRunnable implements Runnable { private Runnable r; public SSLRunnable(Runnable r) { if(r == null) throw new IllegalArgumentException("r cannot be null"); this.r = r; } public void run() { runRunnable(r); } } /** * Only called from the task but must be synchronized as other packets may be * in process of a call to sslEngine.unwrap which may change the status of the engine. * ie. This may check the status, while the call to unwrap changes the status and the * if statement needs to be atomic with the method feedEncryptedPacketImpl!!! * * @throws IOException */ private void continueFromTask() throws IOException { HandshakeStatus hsStatus = sslEngine.getHandshakeStatus(); ByteBuffer b = socketToEngineData2; //here be very careful...if this is run while someone is calling //feedEncryptedPacket(which DOES happen), we need to synchronize if(hsStatus == HandshakeStatus.NEED_UNWRAP && b.hasRemaining()) { if(log.isLoggable(Level.FINER)) { log.finer(id+"[Runnable][socketToEngine] refeeding myself pos="+b.position()+" lim="+b.limit()); } feedEncryptedPacketImpl(null); } else { if(log.isLoggable(Level.FINER)) log.finer(id+"[Runnable]continuing handshake"); continueHandShake(hsStatus); } } private void sendHandshakeMessage() { try { sendHandshakeMessageImpl(); } catch (SSLException e) { throw new AsyncSSLEngineException(e); } } private void sendHandshakeMessageImpl() throws SSLException { if(log.isLoggable(Level.FINEST)) log.finest(id+"sending handshake message"); HELPER.eraseBuffer(empty); if(log.isLoggable(Level.FINEST)) log.finest(id+"handshake pos="+engineToSocketData.position()+" lim="+engineToSocketData.limit()); HandshakeStatus hsStatus = HandshakeStatus.NEED_WRAP; while(hsStatus == HandshakeStatus.NEED_WRAP) { HELPER.eraseBuffer(engineToSocketData); if(log.isLoggable(Level.FINEST)) log.finest(id+"prepare packet pos="+engineToSocketData.position()+" lim="+engineToSocketData.limit()); engineToSocketData.clear(); SSLEngineResult result = sslEngine.wrap(empty, engineToSocketData); Status status = result.getStatus(); hsStatus = result.getHandshakeStatus(); HELPER.doneFillingBuffer(engineToSocketData); if(log.isLoggable(Level.FINEST)) log.finest(id+"write packet pos="+engineToSocketData.position()+" lim="+ engineToSocketData.limit()+" status="+status+" hs="+hsStatus); if(status == Status.BUFFER_OVERFLOW || status == Status.BUFFER_UNDERFLOW) throw new RuntimeException("status not right, status="+status); //realChannel.write(engineToSocketData); if(log.isLoggable(Level.FINE)) log.fine(id+"SSLListener.packetEncrypted pos="+engineToSocketData.position()+ " lim="+engineToSocketData.limit()+" status="+status+" hs="+hsStatus); fireEncryptedPacketToListener(null); if(status == Status.CLOSED) { isClosed = true; closeInbound(); sslEngine.closeOutbound(); sslListener.closed(clientInitiated); } } if(hsStatus == HandshakeStatus.NEED_WRAP || hsStatus == HandshakeStatus.NEED_TASK) throw new RuntimeException(id+"BUG, need to implement more here status="+hsStatus); if(log.isLoggable(Level.FINEST)) log.finest(id+"status="+hsStatus+" isConn="+isConnected); if(hsStatus == HandshakeStatus.FINISHED && !isConnected) { //this is a sslserver side connect //sslserver may be client side :) fireConnected(); } } private void fireEncryptedPacketToListener(Object passthrough) { try { sslListener.packetEncrypted(engineToSocketData, passthrough); } catch (IOException e) { throw new NioException(e); } } private void fireConnected() { if(!isConnected) { //else this is a rehandshake and we don't care!!!! isConnected = true; if(log.isLoggable(Level.FINE)) log.fine(id+"SSLListener.encryptedLinkEstablished"); try { sslListener.encryptedLinkEstablished(); } catch (IOException e) { throw new NioException(e); } } } public synchronized void close() { try { closeImpl(); } catch(Exception e) { //We expect exceptions when hard closing the SSL Engine as it prefers a handshake. log.log(Level.FINE, id+"Exception trying to close channel", e); } sslListener.closed(clientInitiated); } public synchronized void closeImpl() throws IOException { clientInitiated = true; if(isClosed) return; if(log.isLoggable(Level.FINE)) log.fine(id+"closing AsynchSSLEngine"); sslEngine.closeOutbound(); //close outbound is like intiating a close try { initiateCloseImpl(); } catch(IOException e) { log.log(Level.FINEST, id+"Typical exception as we may be called after farEndClosed", e); } isClosed = true; //set after as initiateClose check for this too! try { closeInbound(); } catch(AsyncSSLEngineException e) { log.log(Level.FINEST, id+"Normal Expected Exception. Close packet already sent, not waiting for response", e); } } public synchronized void initiateClose() { try { initiateCloseImpl(); } catch(Exception e) { log.log(Level.WARNING, id+"Exception trying to close channel", e); } } private synchronized void initiateCloseImpl() throws IOException { clientInitiated = true; if(isClosing || isClosed) return; if(log.isLoggable(Level.FINE)) log.fine(id+"closing AsynchSSLEngine"); isConnected = true; isClosing = true; sslEngine.closeOutbound(); engineToSocketData.clear(); if(log.isLoggable(Level.FINER)) log.finer(id+"pos1="+engineToAppData.position()+" lim="+engineToSocketData.limit()); sslEngine.wrap(empty, engineToSocketData); if(log.isLoggable(Level.FINER)) log.finer(id+"pos2="+engineToAppData.position()+" lim="+engineToSocketData.limit()); HELPER.doneFillingBuffer(engineToSocketData); if(log.isLoggable(Level.FINE)) log.fine(id+"packetEncrypted pos="+engineToSocketData.position()+" lim="+engineToSocketData.limit()); fireEncryptedPacketToListener(null); } public boolean isClosed() { return isClosed; } public boolean isClosing() { return isClosing; } public String toString() { return id+""; } public Object getId() { return id; } }