package org.limewire.nio.ssl;
import java.nio.ByteBuffer;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLEngineResult;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLEngineResult.HandshakeStatus;
import javax.net.ssl.SSLEngineResult.Status;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.limewire.nio.ByteBufferCache;
import org.limewire.util.BufferUtils;
/** A simple test that connects two SSLEngines and ensures SSL works. */
public class SSLEngineTest {
private static final Log LOG = LogFactory.getLog(SSLEngineTest.class);
private final SSLContext context;
private final String[] cipherSuites;
private final ByteBufferCache cache;
private Throwable lastFailureCause;
public SSLEngineTest(SSLContext context, String[] cipherSuites, ByteBufferCache cache) {
this.context = context;
this.cipherSuites = cipherSuites;
this.cache = cache;
}
/**
* Performs a test to ensure that SSLEngines can be created, handshaked, and
* transfer data.
* @return true if the test is successful, false if it isn't.
*/
public boolean go() {
try {
goImpl();
return true;
} catch(Throwable t) {
LOG.error("Error in TLS!", t);
lastFailureCause = t;
return false;
}
}
/** Returns the last Throwable that caused this to fail. */
public Throwable getLastFailureCause() {
return lastFailureCause;
}
/** The actual implementation of the test. */
private void goImpl() throws SSLException {
SSLEngine server = context.createSSLEngine();
SSLEngine client = context.createSSLEngine();
server.setEnabledCipherSuites(cipherSuites);
client.setEnabledCipherSuites(cipherSuites);
server.setUseClientMode(false);
client.setUseClientMode(true);
server.setNeedClientAuth(false);
server.setWantClientAuth(false);
SSLSession session = server.getSession();
ByteBuffer clientOut = cache.getHeap(session.getPacketBufferSize());
ByteBuffer serverOut = cache.getHeap(session.getPacketBufferSize());
if(LOG.isDebugEnabled())
LOG.debug("Starting handshake loop.\nServer: " + server + "\nClient: " + client);
try {
doHandshake(client, server, clientOut, serverOut);
doData(client, server, clientOut, serverOut);
} finally {
cache.release(clientOut);
cache.release(serverOut);
}
}
/** Handshakes between the client & server. */
private void doHandshake(SSLEngine client, SSLEngine server, ByteBuffer clientOut, ByteBuffer serverOut) throws SSLException {
SSLEngineResult clientResult = new SSLEngineResult(SSLEngineResult.Status.OK, SSLEngineResult.HandshakeStatus.NEED_WRAP, 0, 0);
SSLEngineResult serverResult = new SSLEngineResult(SSLEngineResult.Status.OK, SSLEngineResult.HandshakeStatus.NEED_UNWRAP, 0, 0);
while(true) {
LOG.debug("Processing client handshake loop");
serverOut.flip();
clientResult = handshakeLoop(clientResult, client, serverOut, clientOut);
serverOut.compact();
LOG.debug("Processing server handshake loop");
clientOut.flip();
serverResult = handshakeLoop(serverResult, server, clientOut, serverOut);
clientOut.compact();
// If both are finished / not_handshaking, then exit.
HandshakeStatus ch = clientResult.getHandshakeStatus();
HandshakeStatus sh = serverResult.getHandshakeStatus();
if( (ch == HandshakeStatus.FINISHED || ch == HandshakeStatus.NOT_HANDSHAKING) &&
(sh == HandshakeStatus.FINISHED || sh == HandshakeStatus.NOT_HANDSHAKING)) {
break;
}
}
}
/**
* Loops in handshaking until an unwrap is required and no data is available
* to unwrap, or a wrap is required by the destination buffer already has
* some prewrapped data.
*/
private SSLEngineResult handshakeLoop(SSLEngineResult result, SSLEngine engine, ByteBuffer src, ByteBuffer dst) throws SSLException {
while(true) {
if(LOG.isDebugEnabled())
LOG.debug("Processing result: " + result + ", from engine: " + engine + ", src: " + src + ", dst: " + dst);
if(result.getStatus() != SSLEngineResult.Status.OK)
throw new IllegalStateException("Invalid result status: " + result);
switch(result.getHandshakeStatus()) {
case FINISHED:
case NOT_HANDSHAKING:
return result;
case NEED_UNWRAP:
// Nothing is left to unwrap, let the other side wrap.
if(!src.hasRemaining())
return result;
result = engine.unwrap(src, dst);
break;
case NEED_TASK:
Runnable runner = engine.getDelegatedTask();
runner.run();
result = new SSLEngineResult(SSLEngineResult.Status.OK, engine.getHandshakeStatus(), 0, 0);
break;
case NEED_WRAP:
// Need to let the other side unwrap if we already wrote data.
if(dst.position() != 0)
return result;
result = engine.wrap(BufferUtils.getEmptyBuffer(), dst);
break;
}
}
}
/** Transfers data from the client -> server, and server -> client. */
private void doData(SSLEngine client, SSLEngine server, ByteBuffer clientOut, ByteBuffer serverOut) throws SSLException {
LOG.debug("Doing client -> server data test");
doDataTest(new byte[] { 'C', 'L', 'I', 'E', 'N', 'T', ' ', 'T', 'E', 'S', 'T', ' ', 'O', 'U', 'T' } ,
client, server, clientOut, serverOut);
LOG.debug("Doing server -> client data test");
doDataTest(new byte[] { 'S', 'E', 'R', 'V', 'E', 'R', ' ', 'T', 'E', 'S', 'T', ' ', 'O', 'U', 'T' }, server, client, serverOut, clientOut);
LOG.debug("Finished data test");
}
/** Transfers a testString from srcEngine to dstEngine, using the buffers as scratch space. */
private void doDataTest(byte[] testData, SSLEngine srcEngine, SSLEngine dstEngine, ByteBuffer writeBuf, ByteBuffer readBuf) throws SSLException {
ByteBuffer data = ByteBuffer.wrap(testData);
SSLEngineResult result = srcEngine.wrap(data, writeBuf);
if(result.getStatus() != Status.OK)
throw new IllegalStateException("Can't wrap data: " + result);
if(data.hasRemaining())
throw new IllegalStateException("Didn't wrap all data (" + testData + "): " + data);
writeBuf.flip();
result = dstEngine.unwrap(writeBuf, readBuf);
if(result.getStatus() != Status.OK)
throw new IllegalStateException("Can't unwrap data: " + result);
if(writeBuf.hasRemaining())
throw new IllegalStateException("Didn't unwrap all data! readIn: " + writeBuf + ", made: " + readBuf);
byte[] read = Arrays.copyOf(readBuf.array(), readBuf.position());
if (!Arrays.equals(testData, read))
throw new IllegalStateException("Wrong data read! Wanted: " + Arrays.asList(testData) + ", was: " + Arrays.asList(read));
readBuf.clear();
writeBuf.clear();
}
/**
* Determines if the error is ignorable.
*/
public boolean isIgnorable(Throwable t) {
// See CORE-286
if(causeIs(t, NoSuchAlgorithmException.class))
return true;
// See CORE-286
if(t instanceof NoSuchMethodError)
return true;
// See CORE-253
if(t instanceof NoClassDefFoundError)
return true;
String msg = t.getMessage();
// See CORE-253
if(msg != null && msg.contains("Cipher buffering error"))
return true;
// We might make everything ignorable, but for now we want to see them.
return false;
}
private boolean causeIs(Throwable t, Class<? extends Throwable> causeClass) {
while(t != null) {
if(causeClass.isAssignableFrom(t.getClass()))
return true;
t = t.getCause();
}
return false;
}
}