/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package jade.imtp.leap.nio;
//#J2ME_EXCLUDE_FILE
import jade.imtp.leap.ICPException;
import jade.imtp.leap.SSLHelper;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.net.ssl.SSLContext;
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;
/**
* helper class that holds the ByteBuffers, SSLEngine and other Objects that deal with the non static part of the
* ssl/nio handshaking/input/output. The contained SSLEngine is hidden to apps, because this helper takes on the responsibility
* of dealing with concurrency issues
*
* @author eduard
*/
public final class SSLEngineHelper implements BufferTransformer {
public static final ByteBuffer EMPTY_BUFFER = NIOHelper.EMPTY_BUFFER;
/**
* todo why 5k?? configurable?
*/
public static final int INCREASE_SIZE = 5120;
private SSLEngine ssle = null;
private ByteBuffer wrapData;
private ByteBuffer unwrapData;
private ByteBuffer sendData;
private NIOJICPConnection connection = null;
private static Logger log = Logger.getLogger(SSLEngineHelper.class.getName());
/**
* Creates and initializes ByteBuffers and SSLEngine necessary for ssl/nio.
* @see NIOHelper
* @see SSLHelper
* @param host provides a hint for optimization
* @param port provides a hint for optimization
* @param connection the connection that will use this helper
* @throws ICPException
*/
public SSLEngineHelper(String host, int port, NIOJICPConnection connection) throws ICPException {
SSLContext context = SSLHelper.createContext();
// get a SSLEngine, use host and port for optimization
ssle = context.createSSLEngine(host, port);
ssle.setUseClientMode(false);
ssle.setNeedClientAuth(SSLHelper.needAuth());
if (!SSLHelper.needAuth()) {
// if we don't do authentication we restrict our ssl connection to use specific cipher suites
ssle.setEnabledCipherSuites(SSLHelper.getSupportedKeys());
}
SSLSession session = ssle.getSession();
// TODO prevent buffer overflow, why *2?
unwrapData = ByteBuffer.allocateDirect(session.getApplicationBufferSize() + 1500);
wrapData = ByteBuffer.allocateDirect(session.getPacketBufferSize());
sendData = ByteBuffer.allocateDirect(wrapData.capacity());;
this.connection = connection;
}
/**
* executes a wrap on the SSLEngine, prevents threads from calling this method concurrently
* @param source
* @return
* @throws SSLException
*/
private SSLEngineResult encode(ByteBuffer source) throws SSLException {
return ssle.wrap(source, wrapData);
}
/**
* runs all background handshaking tasks, blocks until these are finished
* @return the new handshake status after finishing the background tasks
*/
private SSLEngineResult.HandshakeStatus runHandshakeTasks() {
Runnable task = null;
while ((task = ssle.getDelegatedTask()) != null) {
task.run();
}
// re evaluate handshake status
return ssle.getHandshakeStatus();
}
/**
* closes the SSLEngine, tries to send a ssl close message
* @throws IOException
*/
public void close() throws IOException {
/*
* try to nicely terminate ssl connection
* 1 closeoutbound
* 2 send ssl close message
* 3 wait for client to also send close message
* 4 close inbound
* 5 don't let all this frustrate the channel closing
*/
ssle.closeOutbound();
sendSSLClose();
ssle.closeInbound();
}
private void sendSSLClose() {
try {
// try to send close message
while (!ssle.isOutboundDone()) {
wrapAndSend();
}
} catch (IOException e) {
log.log(Level.FINE, "unable to send ssl close packet", e);
}
}
private final int writeToChannel(ByteBuffer b) throws IOException {
int m = b.remaining();
int n = connection.writeToChannel(b);
if (n != m) {
throw new IOException("should write " + m + ", written " + n);
}
return n;
}
private String getRemoteHost() {
return connection.getRemoteHost();
}
private SSLEngineResult unwrapData(ByteBuffer socketData) throws SSLException {
SSLEngineResult result = null;
do {
try {
// Unwrap (decode) the next SSL block. Unwrapped application data (if any) are put into the unwrapData buffer
result = ssle.unwrap(socketData, unwrapData);
if (log.isLoggable(Level.FINE))
log.fine("Decoded " + result.bytesConsumed() + " bytes; Produced "+result.bytesProduced()+" application-data bytes ["+getRemoteHost()+"]");
} catch (SSLException e) {
// send close message to the client and re-throw the exception
log.log(Level.WARNING, "Unwrap failure ["+getRemoteHost()+"]", e);
try {
close();
} catch(IOException ex) {}
throw e;
}
} while (result.getStatus().equals(Status.OK) && result.getHandshakeStatus().equals(HandshakeStatus.NEED_UNWRAP));
return result;
}
private void checkStatusAfterHandshakeTasks(SSLEngineResult.HandshakeStatus handshakeStatus) throws SSLException, IOException {
if (handshakeStatus.equals(HandshakeStatus.FINISHED)) {
log.warning("Unexpected FINISHED SSL handshake status after execution of handshake tasks ["+getRemoteHost()+"]");
} else if (handshakeStatus.equals(HandshakeStatus.NEED_TASK)) {
log.warning("Unexpected NEED_TASK SSL handshake status after execution of handshake tasks ["+getRemoteHost()+"]");
} else if (handshakeStatus.equals(HandshakeStatus.NEED_UNWRAP)) {
if (log.isLoggable(Level.FINE))
log.fine("Need more data to proceed with Handshake ["+getRemoteHost()+"]");
} else if (handshakeStatus.equals(HandshakeStatus.NEED_WRAP)) {
if (log.isLoggable(Level.FINE))
log.fine("Send back Handshake data after task execution ["+getRemoteHost()+"]");
wrapAndSend();
} else if (handshakeStatus.equals(HandshakeStatus.NOT_HANDSHAKING)) {
log.warning("Unexpected NOT_HANDSHAKING SSL handshake status after execution of handshake tasks ["+getRemoteHost()+"]");
}
}
/**
* First initialize your {@link SSLEngineHelper} by clearing its buffers and filling {@link SSLEngineHelper#getSocketData() }
* with data from a connection. After this this method possibly recursively deals with handshaking and unwrapping
* application data. The supplied appBuffer argument will be filled remaining unwrapped data will stay in the
* {@link SSLEngineHelper#getUnwrapData() }.
* @return the number of bytes available in the unwrapBuffer
* @throws IOException
*/
private synchronized int decrypt(ByteBuffer socketData) throws SSLException, IOException {
if (log.isLoggable(Level.FINE))
log.fine("Decrypt incoming data: remaining = "+socketData.remaining()+", position = " + socketData.position() + ", limit = " + socketData.limit() + " ["+getRemoteHost()+"]");
SSLEngineResult result = unwrapData(socketData);
if (log.isLoggable(Level.FINE))
log.fine("Checking handshake result ["+getRemoteHost()+"]");
int n = 0;
SSLEngineResult.Status status = result.getStatus();
SSLEngineResult.HandshakeStatus handshakeStatus = result.getHandshakeStatus();
if (status.equals(Status.OK)) {
if (handshakeStatus.equals(HandshakeStatus.FINISHED)) {
// Handshake completed
if (log.isLoggable(Level.FINE))
log.fine("Handshake finished ["+getRemoteHost()+"]");
} else if (handshakeStatus.equals(HandshakeStatus.NEED_TASK)) {
// The SSLEngine requires one or more "tasks" to be executed before the handshake can proceed -->
// Run them and then go on taking into account the new handshake-status
if (log.isLoggable(Level.FINE))
log.fine("Activate Handshake task ["+getRemoteHost()+"]");
handshakeStatus = runHandshakeTasks();
checkStatusAfterHandshakeTasks(handshakeStatus);
} else if (handshakeStatus.equals(HandshakeStatus.NEED_UNWRAP)) {
// This should never happen since we cannot exit the unwrapData() method with status == OK and handshakeStatus == NEED_UNWRAP
throw new SSLException("Unexpected NEED_UNWRAP SSL handshake status! ["+getRemoteHost()+"]");
} else if (handshakeStatus.equals(HandshakeStatus.NEED_WRAP)) {
// We must send data to the remote side for the handshake to continue
if (log.isLoggable(Level.FINE))
log.fine("Send back Handshake data ["+getRemoteHost()+"]");
wrapAndSend();
} else if (handshakeStatus.equals(HandshakeStatus.NOT_HANDSHAKING)) {
// Application data
n = result.bytesProduced();
}
} else if (status.equals(Status.CLOSED)) {
log.info(" sslengine closed ["+getRemoteHost()+"]");
// send ssl close
close();
} else if (status.equals(Status.BUFFER_UNDERFLOW)) {
// There is not enough data in the socketData buffer to unwrap a meaningful SSL block -->
// Wait for next data from the socket
if (log.isLoggable(Level.FINE))
log.fine("Not enough data to decode a meaningful SSL block. " + socketData.remaining() + " unprocessed bytes. ["+getRemoteHost()+"]");
// Avoid entering an infinite recursion trying to continuously decode bytes still present in socketData (that are
// not sufficient to unwrap a meaningful SSL block)
return n;
} else if (status.equals(Status.BUFFER_OVERFLOW)) {
// The unwrapData buffer is too small to hold all unwrapped data --> Repeat with a larger buffer
if (log.isLoggable(Level.FINE)) {
NIOHelper.logBuffer(socketData,"socketData");
NIOHelper.logBuffer(unwrapData,"overflow unwrapData");
log.fine("enlarging unwrap buffer with" + INCREASE_SIZE);
}
log.info("Buffer overflow. Enlarge buffer and retry ["+getRemoteHost()+"]");
unwrapData.flip();
unwrapData = NIOHelper.enlargeAndFillBuffer(unwrapData, INCREASE_SIZE);
return decrypt(socketData);
}
// If the socketData buffer contains unprocessed data, manage them
if (socketData.hasRemaining()) {
n += decrypt(socketData);
}
// Return the number of valid application data
return n;
}
/**
* generates handshake data and calls {@link NIOJICPConnection#writeToChannel(java.nio.ByteBuffer) }, possibly
* recursive when required by handshaking
* @return the amount of bytes written to the connection
* @throws SSLException
* @throws IOException
*/
private int wrapAndSend() throws SSLException, IOException {
wrapData.clear();
int n = 0;
SSLEngineResult result = encode(EMPTY_BUFFER);
if (log.isLoggable(Level.FINE)) {
log.fine("wrapped " + result);
}
if (result.bytesProduced() > 0) {
wrapData.flip();
n = writeToChannel(wrapData);
if (result.getHandshakeStatus().equals(HandshakeStatus.NEED_WRAP)) {
n += wrapAndSend();
}
return n;
} else {
log.warning("wrap produced no data " + getRemoteHost());
}
return n;
}
/**
* encrypt application data, does not write data to socket. After this method call the data to be
* send can be retrieved through {@link SSLEngineHelper#getWrapData() }, the data will be ready for usage.
* @param b the appData to wrap
* @return the status object for the wrap or null
* @throws SSLException
* @throws IOException
*/
private SSLEngineResult wrapAppData(ByteBuffer b) throws SSLException, IOException {
wrapData.clear();
SSLEngineResult result = encode(b);
if (log.isLoggable(Level.FINE)) {
log.fine("wrapped " + result);
}
if (result.bytesProduced() > 0) {
wrapData.flip();
return result;
} else {
throw new IOException("wrap produced no data " + getRemoteHost());
}
}
public synchronized ByteBuffer preprocessBufferToWrite(ByteBuffer dataToSend) throws IOException {
sendData.clear();
while (dataToSend.hasRemaining()) {
SSLEngineResult res = wrapAppData(dataToSend);
if (wrapData.remaining() > sendData.remaining()) {
int extra = wrapData.remaining() - sendData.remaining();
sendData.flip();
sendData = NIOHelper.enlargeAndFillBuffer(sendData, extra);
}
NIOHelper.copyAsMuchAsFits(sendData, wrapData);
}
sendData.flip();
return sendData;
}
public synchronized ByteBuffer postprocessBufferRead(ByteBuffer socketData) throws PacketIncompleteException, IOException {
//needMoreSocketData = false;
unwrapData.clear();
int n = decrypt(socketData);
if (n > 0) {
unwrapData.flip();
return unwrapData;
} else {
return EMPTY_BUFFER;
}
}
public boolean needSocketData() {
return false;
}
}