ChannelIOSecurepublic class ChannelIOSecure extends ChannelIO A helper class which performs I/O using the SSLEngine API.
Each connection has a SocketChannel and a SSLEngine that is
used through the lifetime of the Channel. We allocate byte buffers
for use as the outbound and inbound network buffers.
Application Data
src requestBB
| ^
| | |
v | |
+----+-----|-----+----+
| | |
| SSL|Engine |
wrap() | | | unwrap()
| OUTBOUND | INBOUND |
| | |
+----+-----|-----+----+
| | ^
| | |
v |
outNetBB inNetBB
Net data
These buffers handle all of the intermediary data for the SSL
connection. To make things easy, we'll require outNetBB be
completely flushed before trying to wrap any more data, but we
could certainly remove that restriction by using larger buffers.
There are many, many ways to handle compute and I/O strategies.
What follows is a relatively simple one. The reader is encouraged
to develop the strategy that best fits the application.
In most of the non-blocking operations in this class, we let the
Selector tell us when we're ready to attempt an I/O operation (by the
application repeatedly calling our methods). Another option would be
to attempt the operation and return from the method when no forward
progress can be made.
There's lots of room for enhancements and improvement in this example.
We're checking for SSL/TLS end-of-stream truncation attacks via
sslEngine.closeInbound(). When you reach the end of a input stream
via a read() returning -1 or an IOException, we call
sslEngine.closeInbound() to signal to the sslEngine that no more
input will be available. If the peer's close_notify message has not
yet been received, this could indicate a trucation attack, in which
an attacker is trying to prematurely close the connection. The
closeInbound() will throw an exception if this condition were
present. |
Fields Summary |
---|
private SSLEngine | sslEngine | private int | appBBSize | private int | netBBSize | private ByteBuffer | inNetBB | private ByteBuffer | outNetBB | private static ByteBuffer | hsBB | private ByteBuffer | fileChannelBB | private HandshakeStatus | initialHSStatus | private boolean | initialHSComplete | private boolean | shutdown |
Constructors Summary |
---|
protected ChannelIOSecure(SocketChannel sc, boolean blocking, SSLContext sslc)
/*
* Constructor for a secure ChannelIO variant.
*/
super(sc, blocking);
/*
* We're a server, so no need to use host/port variant.
*
* The first call for a server is a NEED_UNWRAP.
*/
sslEngine = sslc.createSSLEngine();
sslEngine.setUseClientMode(false);
initialHSStatus = HandshakeStatus.NEED_UNWRAP;
initialHSComplete = false;
netBBSize = sslEngine.getSession().getPacketBufferSize();
inNetBB = ByteBuffer.allocate(netBBSize);
outNetBB = ByteBuffer.allocate(netBBSize);
outNetBB.position(0);
outNetBB.limit(0);
|
Methods Summary |
---|
boolean | dataFlush()
boolean fileFlushed = true;
if ((fileChannelBB != null) && fileChannelBB.hasRemaining()) {
doWrite(fileChannelBB);
fileFlushed = !fileChannelBB.hasRemaining();
} else if (outNetBB.hasRemaining()) {
tryFlush(outNetBB);
}
return (fileFlushed && !outNetBB.hasRemaining());
| boolean | doHandshake()
return doHandshake(null);
| boolean | doHandshake(java.nio.channels.SelectionKey sk)
SSLEngineResult result;
if (initialHSComplete) {
return initialHSComplete;
}
/*
* Flush out the outgoing buffer, if there's anything left in
* it.
*/
if (outNetBB.hasRemaining()) {
if (!tryFlush(outNetBB)) {
return false;
}
// See if we need to switch from write to read mode.
switch (initialHSStatus) {
/*
* Is this the last buffer?
*/
case FINISHED:
initialHSComplete = true;
// Fall-through to reregister need for a Read.
case NEED_UNWRAP:
if (sk != null) {
sk.interestOps(SelectionKey.OP_READ);
}
break;
}
return initialHSComplete;
}
switch (initialHSStatus) {
case NEED_UNWRAP:
if (sc.read(inNetBB) == -1) {
sslEngine.closeInbound();
return initialHSComplete;
}
needIO:
while (initialHSStatus == HandshakeStatus.NEED_UNWRAP) {
/*
* Don't need to resize requestBB, since no app data should
* be generated here.
*/
inNetBB.flip();
result = sslEngine.unwrap(inNetBB, requestBB);
inNetBB.compact();
initialHSStatus = result.getHandshakeStatus();
switch (result.getStatus()) {
case OK:
switch (initialHSStatus) {
case NOT_HANDSHAKING:
throw new IOException(
"Not handshaking during initial handshake");
case NEED_TASK:
initialHSStatus = doTasks();
break;
case FINISHED:
initialHSComplete = true;
break needIO;
}
break;
case BUFFER_UNDERFLOW:
/*
* Need to go reread the Channel for more data.
*/
if (sk != null) {
sk.interestOps(SelectionKey.OP_READ);
}
break needIO;
default: // BUFFER_OVERFLOW/CLOSED:
throw new IOException("Received" + result.getStatus() +
"during initial handshaking");
}
} // "needIO" block.
/*
* Just transitioned from read to write.
*/
if (initialHSStatus != HandshakeStatus.NEED_WRAP) {
break;
}
// Fall through and fill the write buffers.
case NEED_WRAP:
/*
* The flush above guarantees the out buffer to be empty
*/
outNetBB.clear();
result = sslEngine.wrap(hsBB, outNetBB);
outNetBB.flip();
initialHSStatus = result.getHandshakeStatus();
switch (result.getStatus()) {
case OK:
if (initialHSStatus == HandshakeStatus.NEED_TASK) {
initialHSStatus = doTasks();
}
if (sk != null) {
sk.interestOps(SelectionKey.OP_WRITE);
}
break;
default: // BUFFER_OVERFLOW/BUFFER_UNDERFLOW/CLOSED:
throw new IOException("Received" + result.getStatus() +
"during initial handshaking");
}
break;
default: // NOT_HANDSHAKING/NEED_TASK/FINISHED
throw new RuntimeException("Invalid Handshaking State" +
initialHSStatus);
} // switch
return initialHSComplete;
| private javax.net.ssl.SSLEngineResult$HandshakeStatus | doTasks()
Runnable runnable;
/*
* We could run this in a separate thread, but
* do in the current for now.
*/
while ((runnable = sslEngine.getDelegatedTask()) != null) {
runnable.run();
}
return sslEngine.getHandshakeStatus();
| private int | doWrite(java.nio.ByteBuffer src)
int retValue = 0;
if (outNetBB.hasRemaining() && !tryFlush(outNetBB)) {
return retValue;
}
/*
* The data buffer is empty, we can reuse the entire buffer.
*/
outNetBB.clear();
SSLEngineResult result = sslEngine.wrap(src, outNetBB);
retValue = result.bytesConsumed();
outNetBB.flip();
switch (result.getStatus()) {
case OK:
if (result.getHandshakeStatus() == HandshakeStatus.NEED_TASK) {
doTasks();
}
break;
default:
throw new IOException("sslEngine error during data write: " +
result.getStatus());
}
/*
* Try to flush the data, regardless of whether or not
* it's been selected. Odds of a write buffer being full
* is less than a read buffer being empty.
*/
if (outNetBB.hasRemaining()) {
tryFlush(outNetBB);
}
return retValue;
| static ChannelIOSecure | getInstance(java.nio.channels.SocketChannel sc, boolean blocking, javax.net.ssl.SSLContext sslc)
ChannelIOSecure cio = new ChannelIOSecure(sc, blocking, sslc);
cio.appBBSize = cio.sslEngine.getSession().getApplicationBufferSize();
cio.requestBB = ByteBuffer.allocate(cio.appBBSize);
return cio;
| int | read()
SSLEngineResult result;
if (!initialHSComplete) {
throw new IllegalStateException();
}
int pos = requestBB.position();
if (sc.read(inNetBB) == -1) {
sslEngine.closeInbound(); // probably throws exception
return -1;
}
do {
resizeRequestBB(); // guarantees enough room for unwrap
inNetBB.flip();
result = sslEngine.unwrap(inNetBB, requestBB);
inNetBB.compact();
/*
* Could check here for a renegotation, but we're only
* doing a simple read/write, and won't have enough state
* transitions to do a complete handshake, so ignore that
* possibility.
*/
switch (result.getStatus()) {
case BUFFER_UNDERFLOW:
case OK:
if (result.getHandshakeStatus() == HandshakeStatus.NEED_TASK) {
doTasks();
}
break;
default:
throw new IOException("sslEngine error during data read: " +
result.getStatus());
}
} while ((inNetBB.position() != 0) &&
result.getStatus() != Status.BUFFER_UNDERFLOW);
return (requestBB.position() - pos);
| protected void | resizeRequestBB()
resizeRequestBB(appBBSize);
| boolean | shutdown()
if (!shutdown) {
sslEngine.closeOutbound();
shutdown = true;
}
if (outNetBB.hasRemaining() && tryFlush(outNetBB)) {
return false;
}
/*
* By RFC 2616, we can "fire and forget" our close_notify
* message, so that's what we'll do here.
*/
outNetBB.clear();
SSLEngineResult result = sslEngine.wrap(hsBB, outNetBB);
if (result.getStatus() != Status.CLOSED) {
throw new SSLException("Improper close state");
}
outNetBB.flip();
/*
* We won't wait for a select here, but if this doesn't work,
* we'll cycle back through on the next select.
*/
if (outNetBB.hasRemaining()) {
tryFlush(outNetBB);
}
return (!outNetBB.hasRemaining() &&
(result.getHandshakeStatus() != HandshakeStatus.NEED_WRAP));
| long | transferTo(java.nio.channels.FileChannel fc, long pos, long len)
if (!initialHSComplete) {
throw new IllegalStateException();
}
if (fileChannelBB == null) {
fileChannelBB = ByteBuffer.allocate(appBBSize);
fileChannelBB.limit(0);
}
fileChannelBB.compact();
int fileRead = fc.read(fileChannelBB);
fileChannelBB.flip();
/*
* We ignore the return value here, we return the
* number of bytes actually consumed from the the file.
* We'll flush the output buffer before we start shutting down.
*/
doWrite(fileChannelBB);
return fileRead;
| private boolean | tryFlush(java.nio.ByteBuffer bb)
super.write(bb);
return !bb.hasRemaining();
| int | write(java.nio.ByteBuffer src)
if (!initialHSComplete) {
throw new IllegalStateException();
}
return doWrite(src);
|
|