org.xnio.ssl.JsseSslConduitEngine Maven / Gradle / Ivy
/*
* JBoss, Home of Professional Open Source.
*
* Copyright 2013 Red Hat, Inc. and/or its affiliates, and individual
* contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.xnio.ssl;
import static java.lang.Thread.currentThread;
import static java.util.concurrent.locks.LockSupport.park;
import static java.util.concurrent.locks.LockSupport.parkNanos;
import static java.util.concurrent.locks.LockSupport.unpark;
import static org.xnio.Bits.allAreClear;
import static org.xnio.Bits.allAreSet;
import static org.xnio.Bits.anyAreSet;
import static org.xnio.Bits.intBitMask;
import static org.xnio._private.Messages.msg;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLEngineResult;
import javax.net.ssl.SSLEngineResult.HandshakeStatus;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.SSLSession;
import org.jboss.logging.Logger;
import org.xnio.BufferAllocator;
import org.xnio.Buffers;
import org.xnio.ByteBufferSlicePool;
import org.xnio.Pool;
import org.xnio.Pooled;
import org.xnio.conduits.StreamSinkConduit;
import org.xnio.conduits.StreamSourceConduit;
/**
* {@link SSLEngine} wrapper, used by Jsse SSL conduits.
*
* @author Flavia Rainone
*/
final class JsseSslConduitEngine {
private static final Logger log = Logger.getLogger("org.xnio.conduits");
private static final String FQCN = JsseSslConduitEngine.class.getName();
// read-side
private static final int NEED_WRAP = 1 << 0x00; // conduit cannot be read due to pending wrap
private static final int READ_SHUT_DOWN = 1 << 0x01; // user shut down reads
private static final int BUFFER_UNDERFLOW = 1 << 0x02; // even though there is data in the buffer there is not enough to form a complete packet
@SuppressWarnings("unused")
private static final int READ_FLAGS = intBitMask(0x00, 0x0F);
// write-side
private static final int NEED_UNWRAP = 1 << 0x10; // conduit cannot be written to due to pending unwrap
private static final int WRITE_SHUT_DOWN = 1 << 0x11; // user requested shut down of writes
private static final int WRITE_COMPLETE = 1 << 0x12; // flush acknowledged full write shutdown
private static final int FIRST_HANDSHAKE = 1 << 0x16; // first handshake has not been performed
private static final int ENGINE_CLOSED = 1 << 0x17; // engine is fully closed
// engine is fully closed
@SuppressWarnings("unused")
private static final int WRITE_FLAGS = intBitMask(0x10, 0x1F);
// empty buffer
private static final ByteBuffer EMPTY_BUFFER = ByteBuffer.allocate(0);
// final fields
/** The SSL engine. */
private final SSLEngine engine;
/** The buffer into which incoming SSL data is written. */
private final Pooled receiveBuffer;
/** The buffer from which outbound SSL data is sent. */
private final Pooled sendBuffer;
/** An expanded non-final buffer from which outbound SSL data is sent when
* large fragments handling is enabled in the underlying SSL Engine. When
* this happens, we need a specific buffer with expanded capacity. */
private ByteBuffer expandedSendBuffer;
/** The buffer into which inbound clear data is written. */
private final Pooled readBuffer;
// the next conduits
private final StreamSinkConduit sinkConduit;
private final StreamSourceConduit sourceConduit;
// the connection
private final JsseSslStreamConnection connection;
// state
private volatile int state;
private static final AtomicIntegerFieldUpdater stateUpdater = AtomicIntegerFieldUpdater.newUpdater(JsseSslConduitEngine.class, "state");
// waiters
@SuppressWarnings("unused")
private volatile Thread readWaiter;
@SuppressWarnings("unused")
private volatile Thread writeWaiter;
private static final AtomicReferenceFieldUpdater readWaiterUpdater = AtomicReferenceFieldUpdater.newUpdater(JsseSslConduitEngine.class, Thread.class, "readWaiter");
private static final AtomicReferenceFieldUpdater writeWaiterUpdater = AtomicReferenceFieldUpdater.newUpdater(JsseSslConduitEngine.class, Thread.class, "writeWaiter");
/**
* Construct a new instance.
*
* @param connection the ssl connection associated with this engine
* @param sinkConduit the sink channel to use for write operations
* @param sourceConduit the source channel to use for read operations
* @param engine the SSL engine to use
* @param socketBufferPool the socket buffer pool
* @param applicationBufferPool the application buffer pool
*/
JsseSslConduitEngine(final JsseSslStreamConnection connection, final StreamSinkConduit sinkConduit, final StreamSourceConduit sourceConduit, final SSLEngine engine, final Pool socketBufferPool, final Pool applicationBufferPool) {
if (connection == null) {
throw msg.nullParameter("connection");
}
if (sinkConduit == null) {
throw msg.nullParameter("sinkConduit");
}
if (sourceConduit == null) {
throw msg.nullParameter("sourceConduit");
}
if (engine == null) {
throw msg.nullParameter("engine");
}
if (socketBufferPool == null) {
throw msg.nullParameter("socketBufferPool");
}
if (applicationBufferPool == null) {
throw msg.nullParameter("applicationBufferPool");
}
this.connection = connection;
this.sinkConduit = sinkConduit;
this.sourceConduit = sourceConduit;
this.engine = engine;
this.state = FIRST_HANDSHAKE;
final SSLSession session = engine.getSession();
final int packetBufferSize = session.getPacketBufferSize();
boolean ok = false;
receiveBuffer = socketBufferPool.allocate();
try {
receiveBuffer.getResource().flip();
sendBuffer = socketBufferPool.allocate();
try {
if (receiveBuffer.getResource().capacity() < packetBufferSize || sendBuffer.getResource().capacity() < packetBufferSize) {
// create expanded send buffer
expandedSendBuffer = ByteBuffer.allocate(packetBufferSize);
}
readBuffer = applicationBufferPool.allocate();
ok = true;
} finally {
if (! ok) sendBuffer.free();
}
} finally {
if (! ok) receiveBuffer.free();
}
}
/**
* Begins handshake.
*
* @throws IOException if an I/O error occurs
*/
public void beginHandshake() throws IOException {
engine.beginHandshake();
}
/**
* Returns the engine's session.
*/
public SSLSession getSession() {
return engine.getSession();
}
/**
* Attempt to wrap the bytes in {@code src}. The wrapped bytes can be later retrieved by calling
* {@link #getWrappedBuffer()}.
*
* If the engine is performing handshake during this request, not all bytes could be wrapped. In this case, a later
* call can be performed to attempt to wrap more bytes.
*
* This method must not be invoked inside the {@link #getWrapLock() wrap lock}, or else unexpected behavior
* could occur.
* @param src the bytes to be wrapped
* @return the amount of wrapped bytes.
*
* @throws IOException if an IO exception occurs during wrapping
*/
public int wrap(final ByteBuffer src) throws IOException {
return wrap(src, false);
}
/**
* Attempt to wrap the bytes in {@code srcs}. The wrapped bytes can be later retrieved by calling
* {@link #getWrappedBuffer()}.
*
* If the engine is performing handshake during this request, not all bytes could be wrapped. In this case, a later
* call can be performed to attempt to wrap more bytes.
*
* This method must not be invoked inside the {@link #getWrapLock() wrap lock}, or else unexpected behavior
* could occur.
*
* @param srcs contains the bytes to be wrapped
* @param offset offset
* @param length length
* @return the amount of wrapped bytes.
*
* @throws IOException if an IO exception occurs during wrapping
*/
public long wrap(final ByteBuffer[] srcs, final int offset, final int length) throws IOException {
assert ! Thread.holdsLock(getWrapLock());
assert ! Thread.holdsLock(getUnwrapLock());
if (length < 1) {
return 0L;
}
if (allAreSet(state, WRITE_COMPLETE)) { // atempted write after shutdown, this is
// a workaround for a bug found in SSLEngine
throw new ClosedChannelException();
}
long bytesConsumed = 0;
boolean run;
try {
do {
final SSLEngineResult result;
synchronized (getWrapLock()) {
run = handleWrapResult(result = engineWrap(srcs, offset, length, getSendBuffer()), false);
bytesConsumed += (long) result.bytesConsumed();
}
// handshake will tell us whether to keep the loop
run = run && (handleHandshake(result, true) || (!isUnwrapNeeded() && Buffers.hasRemaining(srcs, offset, length)));
} while (run);
} catch (SSLHandshakeException e) {
try {
synchronized (getWrapLock()) {
engine.wrap(EMPTY_BUFFER, getSendBuffer());
doFlush();
}
} catch (IOException ignore) {}
throw e;
}
return bytesConsumed;
}
/**
* Returns the buffer that contains the wrapped data.
*
* Retrieval and manipulation of this buffer should always be protected by the {@link #getWrapLock() wrap lock}.
*
* @return the buffer containing wrapped bytes
*/
public ByteBuffer getWrappedBuffer() {
assert Thread.holdsLock(getWrapLock());
assert ! Thread.holdsLock(getUnwrapLock());
return allAreSet(stateUpdater.get(this), ENGINE_CLOSED)? Buffers.EMPTY_BYTE_BUFFER: getSendBuffer();
}
/**
* Returns the wrap lock, that must be used whenever the {@link #getWrappedBuffer() wrapped buffer} is being
* accessed.
*
* This lock is also used internally by wrapping operations, thus guaranteeing safe concurrent execution of
* both wrap and unwrap operations, specially during handshake handling.
*
* @return lock for protecting access to the unwrapped buffer
*/
public Object getWrapLock() {
return sendBuffer;
}
/**
* Wrap operation used internally.
*
* @param src contains the bytes to be wrapped
* @param isCloseExpected indicates if close is expected, information that is used to perform special handling
* when closing the engine
* @return the amount of resulting wrapped bytes
*
* @throws IOException if an IO exception occurs during wrapping
*/
private int wrap(final ByteBuffer src, boolean isCloseExpected) throws IOException {
assert ! Thread.holdsLock(getWrapLock());
assert ! Thread.holdsLock(getUnwrapLock());
if (allAreSet(state, WRITE_COMPLETE)) { // attempted write after shutdown, this is
// a workaround for a bug found in SSLEngine
throw new ClosedChannelException();
}
clearFlags(FIRST_HANDSHAKE);
int bytesConsumed = 0;
boolean run;
try {
do {
final SSLEngineResult result;
synchronized (getWrapLock()) {
run = handleWrapResult(result = engineWrap(src, getSendBuffer()), isCloseExpected);
bytesConsumed += result.bytesConsumed();
}
// handshake will tell us whether to keep the loop
run = run && bytesConsumed == 0 && (handleHandshake(result, true) || (!isUnwrapNeeded() && src.hasRemaining()));
} while (run);
} catch (SSLHandshakeException e) {
try {
synchronized (getWrapLock()) {
engine.wrap(EMPTY_BUFFER, getSendBuffer());
doFlush();
}
} catch (IOException ignore) {}
throw e;
}
return bytesConsumed;
}
/**
* Invoke inner SSL engine to wrap.
*/
private SSLEngineResult engineWrap(final ByteBuffer[] srcs, final int offset, final int length, final ByteBuffer dest) throws SSLException {
assert Thread.holdsLock(getWrapLock());
assert ! Thread.holdsLock(getUnwrapLock());
log.logf(FQCN, Logger.Level.TRACE, null, "Wrapping %s into %s", srcs, dest);
try {
return engine.wrap(srcs, offset, length, dest);
} catch (SSLHandshakeException e) {
try {
engine.wrap(srcs, offset, length, dest);
doFlush();
} catch (IOException ignore) {}
throw e;
}
}
/**
* Invoke inner SSL engine to wrap.
*/
private SSLEngineResult engineWrap(final ByteBuffer src, final ByteBuffer dest) throws SSLException {
assert Thread.holdsLock(getWrapLock());
assert ! Thread.holdsLock(getUnwrapLock());
log.logf(FQCN, Logger.Level.TRACE, null, "Wrapping %s into %s", src, dest);
return engine.wrap(src, dest);
}
/**
* Handles the wrap result, indicating if caller should perform a new attempt to wrap.
*
* @param result the wrap result
* @param closeExpected is a closed engine result expected
* @return {@code true} if a new attempt to wrap should be made
*
* @throws IOException if an IO exception occurs
*/
private boolean handleWrapResult(SSLEngineResult result, boolean closeExpected) throws IOException {
assert Thread.holdsLock(getWrapLock());
assert ! Thread.holdsLock(getUnwrapLock());
log.logf(FQCN, Logger.Level.TRACE, null, "Wrap result is %s", result);
switch (result.getStatus()) {
case BUFFER_UNDERFLOW: {
assert result.bytesConsumed() == 0;
assert result.bytesProduced() == 0;
// should not be possible but just to be safe...
break;
}
case BUFFER_OVERFLOW: {
assert result.bytesConsumed() == 0;
assert result.bytesProduced() == 0;
final ByteBuffer buffer = getSendBuffer();
if (buffer.position() == 0) {
final int bufferSize = engine.getSession().getPacketBufferSize();
if (buffer.capacity() < bufferSize) {
msg.expandedSslBufferEnabled(bufferSize);
expandedSendBuffer = ByteBuffer.allocate(bufferSize);
} else throw msg.wrongBufferExpansion();
} else {
// there's some data in there, so send it first
buffer.flip();
try {
while (buffer.hasRemaining()) {
final int res = sinkConduit.write(buffer);
if (res == 0) {
return false;
}
}
} finally {
buffer.compact();
}
}
break;
}
case CLOSED: {
if (! closeExpected) {
// attempted write after shutdown
throw new ClosedChannelException();
}
// else treat as OK
// fall thru!!!
}
case OK: {
if (result.bytesConsumed() == 0) {
if (result.bytesProduced() > 0) {
if (! doFlush()) {
return false;
}
}
}
break;
}
default: {
throw msg.unexpectedWrapResult(result.getStatus());
}
}
return true;
}
/**
* Handle handshake process, after a wrap or an unwrap operation.
*
* @param result the wrap/unwrap result
* @param write if {@code true}, indicates caller executed a {@code wrap} operation; if {@code false}, indicates
* caller executed an {@code unwrap} operation
* @return {@code true} to indicate that caller should rerun the previous wrap or unwrap operation, hence
* producing a new result; {@code false} to indicate otherwise
*
* @throws IOException if an IO error occurs during handshake handling
*/
private boolean handleHandshake(SSLEngineResult result, boolean write) throws IOException {
assert ! Thread.holdsLock(getUnwrapLock());
// if read needs wrap, the only possible reason is that something went wrong with flushing, try to flush now
if (isWrapNeeded()) {
synchronized(getWrapLock()) {
if (doFlush()) {
clearNeedWrap();
}
}
}
boolean newResult = false;
for (;;) {
switch (result.getHandshakeStatus()) {
case FINISHED: {
clearNeedUnwrap();
connection.handleHandshakeFinished();
// Operation can continue immediately
return true;
}
case NOT_HANDSHAKING: {
// Operation can continue immediately
clearNeedUnwrap();
return false;
}
case NEED_WRAP: {
// clear writeRequiresRead
clearNeedUnwrap();
// if write, let caller do the wrap
if (write) {
return true;
}
final ByteBuffer buffer = getSendBuffer();
// else, trigger a write call
// Needs wrap, so we wrap (if possible)...
synchronized (getWrapLock()) {
// given caller is reading, tell it to continue only if we can move away from NEED_WRAP
// and flush any wrapped data we may have left
if (doFlush()) {
if (result.getStatus() == SSLEngineResult.Status.CLOSED) {
closeOutbound();
return false;
}
if (!handleWrapResult(result = engineWrap(Buffers.EMPTY_BYTE_BUFFER, buffer), true) || !doFlush()) {
needWrap();
return false;
}
// success, clear need wrap and handle the new handshake status
newResult = true;
clearNeedWrap();
continue;
}
assert !isUnwrapNeeded();
// else... oops, there is unflushed data, and handshake status is NEED_WRAP
needWrap();
// tell read caller to break read loop
return false;
}
}
case NEED_UNWRAP: {
// if read, let caller do the unwrap
if (! write) {
return newResult;
}
synchronized(getWrapLock()) {
// there could be unflushed data from a previous wrap, make sure everything is flushed at this point
doFlush();
}
final ByteBuffer buffer = receiveBuffer.getResource();
final ByteBuffer unwrappedBuffer = readBuffer.getResource();
// FIXME this if block is a workaround for a bug in SSLEngine
if (result.getHandshakeStatus() == HandshakeStatus.NEED_UNWRAP && engine.isOutboundDone()) {
synchronized (getUnwrapLock()) {
buffer.compact();
sourceConduit.read(buffer);
buffer.flip();
if (buffer.hasRemaining() && sourceConduit.isReadResumed()) {
sourceConduit.wakeupReads();
}
return false;
}
}
synchronized (getUnwrapLock()) {
// attempt to unwrap
int unwrapResult = handleUnwrapResult(result = engineUnwrap(buffer, unwrappedBuffer));
if (buffer.hasRemaining() && sourceConduit.isReadResumed()) {
sourceConduit.wakeupReads();
}
if (unwrapResult >= 0) {
// have we made some progress?
if(result.getHandshakeStatus() != HandshakeStatus.NEED_UNWRAP || result.bytesConsumed() > 0) {
clearNeedUnwrap();
continue;
}
assert !isWrapNeeded();
// no point in proceeding, we're stuck until the user reads anyway
needUnwrap();
return false;
} else if (unwrapResult == -1 && result.getHandshakeStatus() == HandshakeStatus.NEED_UNWRAP) {
if (!allAreSet(state, READ_SHUT_DOWN)) {
// connection has been closed by peer prior to handshake finished
throw new ClosedChannelException();
}
return false;
}
}
continue;
}
case NEED_TASK: {
Runnable task;
synchronized (engine) {
// run the tasks needed for handshaking
while ((task = engine.getDelegatedTask()) != null) {
try {
task.run();
} catch (Exception e) {
throw new IOException(e);
}
}
}
// caller should try to wrap/unwrap again
return true;
}
default:
throw msg.unexpectedHandshakeStatus(result.getHandshakeStatus());
}
}
}
/**
* Unwraps the bytes contained in {@link #getUnwrapBuffer()}, copying the resulting unwrapped bytes into
* {@code dst}.
*
* If the engine is performing handshake during this request, not all bytes could be unwrapped. In this case, a
* later call can be performed to attempt to unwrap more bytes.
*
* This method must not be invoked inside the {@link #getUnwrapLock() unwrap lock}, or else unexpected behavior
* could occur.
*
* @param dst where the resulting unwrapped bytes will be copied to
* @return the amount of resulting unwrapped bytes
*
* @throws IOException if an IO exception occurs during unwrapping
*/
public int unwrap(final ByteBuffer dst) throws IOException {
return (int) unwrap(new ByteBuffer[]{dst}, 0, 1);
}
private int failureCount = 0;
/**
* Unwraps the bytes contained in {@link #getUnwrapBuffer()}, copying the resulting unwrapped bytes into
* {@code dsts}.
*
* If the engine is performing handshake during this request, not all bytes could be unwrapped. In this case, a
* later call can be performed to attempt to unwrap more bytes.
*
* This method must not be invoked inside the {@link #getUnwrapLock() unwrap lock}, or else unexpected behavior
* could occur.
*
* @param dsts where the resulting unwrapped bytes will be copied to
* @param offset offset
* @param length length
* @return the amount of resulting unwrapped bytes
*
* @throws IOException if an IO exception occurs during unwrapping
*/
public long unwrap(final ByteBuffer[] dsts, final int offset, final int length) throws IOException {
assert ! Thread.holdsLock(getUnwrapLock());
assert ! Thread.holdsLock(getWrapLock());
if (dsts.length == 0 || length == 0 || isClosed()) {
return 0L;
}
clearFlags(FIRST_HANDSHAKE | BUFFER_UNDERFLOW);
final ByteBuffer buffer = receiveBuffer.getResource();
final ByteBuffer unwrappedBuffer = readBuffer.getResource();
long total = 0;
SSLEngineResult result;
synchronized(getUnwrapLock()) {
if (unwrappedBuffer.position() > 0) {
total += (long) copyUnwrappedData(dsts, offset, length, unwrappedBuffer);
}
}
int res = 0;
try {
do {
synchronized (getUnwrapLock()) {
if (! Buffers.hasRemaining(dsts, offset, length)) {
if (unwrappedBuffer.hasRemaining() && sourceConduit.isReadResumed()) {
sourceConduit.wakeupReads();
}
return total;
}
res = handleUnwrapResult(result = engineUnwrap(buffer, unwrappedBuffer));
if (unwrappedBuffer.position() > 0) { // test the position of the buffer instead of the
// the amount of produced bytes, because in a concurrent scenario, during this loop,
// another thread could read more bytes as a side effect of a need unwrap
total += (long) copyUnwrappedData(dsts, offset, length, unwrappedBuffer);
}
}
} while ((handleHandshake(result, false) || res > 0));
} catch (SSLHandshakeException e) {
try {
synchronized (getWrapLock()) {
engine.wrap(EMPTY_BUFFER, getSendBuffer());
doFlush();
}
} catch (IOException ignore) {}
throw e;
}
if (total == 0L) {
if (res == -1) {
return -1L;
}
}
if(res == 0 && result.getStatus() == SSLEngineResult.Status.BUFFER_UNDERFLOW) {
int old;
do {
old = state;
} while(!stateUpdater.compareAndSet(this, old, old | BUFFER_UNDERFLOW));
}
return total;
}
/**
* Returns the buffer that contains the data to be unwrapped.
*
* Retrieval and manipulation of this buffer should always be protected by the {@link #getUnwrapLock() unwrap lock}.
*
* @return the buffer containing bytes to be unwrapped
*/
public ByteBuffer getUnwrapBuffer() {
assert Thread.holdsLock(getUnwrapLock());
assert ! Thread.holdsLock(getWrapLock());
return receiveBuffer.getResource();
}
/**
* Returns the unwrap lock, that must be used whenever the {@link #getUnwrapBuffer() unwrap buffer} is being
* accessed.
*
* This lock is also used internally by unwrapping operations, thus guaranteeing safe concurrent execution of
* both wrap and unwrap operations, specially during handshake handling.
*
* @return lock for protecting access to the unwrap buffer
*/
public Object getUnwrapLock() {
return receiveBuffer;
}
/**
* Invoke inner SSL engine to unwrap.
*/
private SSLEngineResult engineUnwrap(final ByteBuffer buffer, final ByteBuffer unwrappedBuffer) throws IOException {
assert Thread.holdsLock(getUnwrapLock());
if (!buffer.hasRemaining()) {
buffer.compact();
sourceConduit.read(buffer);
buffer.flip();
}
log.logf(FQCN, Logger.Level.TRACE, null, "Unwrapping %s into %s", buffer, unwrappedBuffer);
return engine.unwrap(buffer, unwrappedBuffer);
}
/**
* Copy unwrapped data from {@code unwrappedBuffer} into {@code dsts}.
*
* @param dsts destine of copy
* @param offset offset
* @param length length
* @param unwrappedBuffer source from where byte will be copied
* @return the amount of copied bytes
*/
private int copyUnwrappedData(final ByteBuffer[] dsts, final int offset, final int length, ByteBuffer unwrappedBuffer) {
assert Thread.holdsLock(getUnwrapLock());
unwrappedBuffer.flip();
try {
return Buffers.copy(dsts, offset, length, unwrappedBuffer);
} finally {
unwrappedBuffer.compact();
}
}
/**
* Handles the unwrap result, indicating how many bytes have been consumed.
*
* @param result the unwrap result
* @return the amount of bytes consumed by unwrap. If the engine is closed and no bytes were consumed,
* returns {@code -1}.
*
* @throws IOException if an IO exception occurs
*/
private int handleUnwrapResult(final SSLEngineResult result) throws IOException {
assert Thread.holdsLock(getUnwrapLock());
log.logf(FQCN, Logger.Level.TRACE, null, "Unwrap result is %s", result);
switch (result.getStatus()) {
case BUFFER_OVERFLOW: {
assert result.bytesConsumed() == 0;
assert result.bytesProduced() == 0;
// not enough space in destination buffer; caller should flush & retry
return 0;
}
case BUFFER_UNDERFLOW: {
assert result.bytesConsumed() == 0;
assert result.bytesProduced() == 0;
// fill the rest of the buffer, then retry!
final ByteBuffer buffer = receiveBuffer.getResource();
synchronized (getUnwrapLock()) {
buffer.compact();
try {
return sourceConduit.read(buffer);
} finally {
buffer.flip();
}
}
}
case CLOSED: {
// if unwrap processed any data, it should return bytes produced instead of -1
if (result.bytesConsumed() > 0) {
return result.bytesConsumed();
}
return -1;
}
case OK: {
// continue
return result.bytesConsumed();
}
default: {
throw msg.unexpectedUnwrapResult(result.getStatus());
}
}
}
/**
* Flush any data needed for handshaking into the {@link #getWrappedBuffer() wrapped buffer}.
*
* The flushed data can later be retrieved by reading the buffer. If the engine is closed, this method can be used
* to flush all data associated to engine close messages.
*
* @return {@code true} if there is no left data to be flushed; {@code false} if for some reason the engine was
* unable to flush all data
*
* @throws IOException if an IO exception occurs during attempt to flush
*/
public boolean flush() throws IOException {
int oldState, newState;
oldState = stateUpdater.get(this);
if (allAreSet(oldState, WRITE_COMPLETE)) {
if (engine.isOutboundDone()) {
connection.writeClosed();
}
return true;
}
synchronized (getWrapLock()) {
if (allAreSet(oldState, WRITE_SHUT_DOWN)) {
if (!wrapCloseMessage()) {
return false;
}
} else {
if (engine.isOutboundDone()) {
connection.writeClosed();
}
return true;
}
}
// conclude write
newState = oldState | WRITE_COMPLETE;
while (! stateUpdater.compareAndSet(this, oldState, newState)) {
oldState = stateUpdater.get(this);
if (allAreSet(oldState, WRITE_COMPLETE)) {
if (engine.isOutboundDone()) {
connection.writeClosed();
}
return true;//sinkConduit.flush();
}
newState = oldState | WRITE_COMPLETE;
}
// close the engine if read is shut down
if (allAreSet(oldState, READ_SHUT_DOWN)) {
closeEngine();
}
if (engine.isOutboundDone()) {
connection.writeClosed();
}
return true;
}
/**
* Attempt to finish wrapping close handshake bytes.
*
* @return {@code true} only if all bytes concerning close handshake messages have been wrapped.
* @throws IOException if an unexpected IO exception occurs
*/
private boolean wrapCloseMessage() throws IOException {
assert ! Thread.holdsLock(getUnwrapLock());
assert Thread.holdsLock(getWrapLock());
if (sinkConduit.isWriteShutdown()) {
return true;
}
if (!engine.isOutboundDone() || !engine.isInboundDone()) {
SSLEngineResult result;
do {
if (!handleWrapResult(result = engineWrap(Buffers.EMPTY_BYTE_BUFFER, getSendBuffer()), true)) {
return false;
}
} while (handleHandshake(result, true) && (result.getHandshakeStatus() != HandshakeStatus.NEED_UNWRAP || !engine.isOutboundDone()));
handleWrapResult(result = engineWrap(Buffers.EMPTY_BYTE_BUFFER, getSendBuffer()), true);
if (!engine.isOutboundDone() || (result.getHandshakeStatus() != HandshakeStatus.NOT_HANDSHAKING &&
result.getHandshakeStatus() != HandshakeStatus.NEED_UNWRAP)) {
return false;
}
}
return true;
}
/**
* Flushes all data in the wrapped bytes buffer.
*
* @return {@code true} if all data available has been flushed
*
* @throws IOException if an unexpected IO exception occurs
*/
private boolean doFlush() throws IOException {
assert Thread.holdsLock(getWrapLock());
assert ! Thread.holdsLock(getUnwrapLock());
final ByteBuffer buffer;
buffer = getSendBuffer();
buffer.flip();
try {
while (buffer.hasRemaining()) {
final int res = sinkConduit.write(buffer);
if (res == 0) {
return false;
}
}
} finally {
buffer.compact();
}
return sinkConduit.flush();
}
/**
* Closes this engine for both inbound and outbound, clearing the buffers.
*
* @throws IOException
*/
private void closeEngine() throws IOException {
int old = setFlags(ENGINE_CLOSED);
// idempotent
if (allAreSet(old, ENGINE_CLOSED)) {
return;
}
try {
synchronized(getWrapLock()) {
if (! doFlush()) {
throw msg.unflushedData();
}
}
} finally {
readBuffer.free();
receiveBuffer.free();
sendBuffer.free();
}
}
/**
* Signals that no outbound data will be sent.
*
* @throws IOException if an IO exception occurs
*/
public void closeOutbound() throws IOException {
int old = setFlags(WRITE_SHUT_DOWN);
try {
if (allAreClear(old, WRITE_SHUT_DOWN)) {
engine.closeOutbound();
synchronized (getWrapLock()) {
wrapCloseMessage();
flush();
}
}
if (!allAreClear(old, READ_SHUT_DOWN)) {
closeEngine();
}
} catch (Exception e) {
//if there is an exception on close we immediately close the engine to make sure buffers are freed
try {
closeEngine();
} catch (Exception closeEngineException) {
msg.failedToCloseSSLEngine(e, closeEngineException);
}
if(e instanceof IOException) {
throw (IOException) e;
} else {
throw (RuntimeException)e;
}
}
}
/**
* Closes the engine for both inbound and outbound connection, and discards this engine's internal
* resources immediately without handshaking if this engine has not been used.
*
* @throws IOException
*/
void close() throws IOException {
if (isFirstHandshake()) {
setFlags(WRITE_SHUT_DOWN|WRITE_COMPLETE|READ_SHUT_DOWN);
closeEngine();
} else {
try {
closeInbound();
} catch (Throwable t) {
try {
closeOutbound();
} catch (Throwable t2) {
t2.addSuppressed(t);
throw t2;
}
throw t;
}
closeOutbound();
}
}
/**
* Indicates if outbound is closed.
*
* @throws IOException if an IO exception occurs
*/
public boolean isOutboundClosed() {
return allAreSet(stateUpdater.get(this), WRITE_SHUT_DOWN);
}
/**
* Block until this engine can wrap messages.
*
* The engine may be unable to wrap if a handshake message needs to be unwrapped first. In this case, this thread
* will be awakened as soon as the message is made available for reading by the internal source conduit.
*
* @throws IOException if an IO exception occurs during await
*/
public void awaitCanWrap() throws IOException {
int oldState = state;
if (anyAreSet(oldState, WRITE_SHUT_DOWN) || !allAreSet(oldState, NEED_UNWRAP)) {
return;
}
final Thread thread = currentThread();
final Thread next = writeWaiterUpdater.getAndSet(this, thread);
try {
if (anyAreSet(oldState = state, WRITE_SHUT_DOWN)) {
return;
}
if (allAreSet(oldState, NEED_UNWRAP)) {
unwrap(Buffers.EMPTY_BYTE_BUFFER);
}
park(this);
if (thread.isInterrupted()) {
throw msg.interruptedIO();
}
} finally {
// always unpark because we cannot know if our awaken was spurious
if (next != null) unpark(next);
}
}
/**
* Block until this engine can wrap messages.
*
* The engine may be unable to wrap if a handshake message needs to be unwrapped first. In this case, this thread
* will be awakened as soon as the message is made available for reading by the internal source conduit.
*
* @param time timeout for blocking
* @param timeUnit timeout unit
* @throws IOException if an IO exception occurs during await
*/
public void awaitCanWrap(long time, TimeUnit timeUnit) throws IOException {
int oldState = state;
if (anyAreSet(oldState, WRITE_SHUT_DOWN) || !allAreSet(oldState, NEED_UNWRAP)) {
return;
}
final Thread thread = currentThread();
final Thread next = writeWaiterUpdater.getAndSet(this, thread);
long duration = timeUnit.toNanos(time);
try {
if (anyAreSet(oldState = state, WRITE_SHUT_DOWN)) {
return;
}
if (allAreSet(oldState, NEED_UNWRAP)) {
unwrap(Buffers.EMPTY_BYTE_BUFFER);
}
parkNanos(this, duration);
if (thread.isInterrupted()) {
throw msg.interruptedIO();
}
} finally {
// always unpark because we cannot know if our awaken was spurious
if (next != null) unpark(next);
}
}
/**
* Signals that no inbound data will be read
*
* @throws IOException if an IO exception occurs
*/
public void closeInbound() throws IOException {
connection.readClosed();
int old = setFlags(READ_SHUT_DOWN);
try {
if (allAreClear(old, READ_SHUT_DOWN)) {
sourceConduit.terminateReads();
}
if (allAreSet(old, WRITE_SHUT_DOWN) && !allAreSet(old, WRITE_COMPLETE)) {
synchronized (getWrapLock()) {
wrapCloseMessage();
flush();
}
}
if (allAreSet(old, WRITE_COMPLETE)) {
closeEngine();
}
} catch (Exception e) {
//if there is an exception on close we immediately close the engine to make sure buffers are freed
closeEngine();
if(e instanceof IOException) {
throw (IOException)e;
} else {
throw (RuntimeException)e;
}
}
}
/**
* Indicates if inbound is closed.
*
* @throws IOException if an IO exception occurs
*/
public boolean isInboundClosed() {
return allAreSet(state, READ_SHUT_DOWN);
}
/**
* Indicates if engine is closed.
*
*/
public boolean isClosed() {
return allAreSet(state, ENGINE_CLOSED);
}
/**
* Block until this engine can unwrap messages.
*
* @throws IOException if an IO exception occurs during await
*/
public void awaitCanUnwrap() throws IOException {
int oldState = state;
if (anyAreSet(oldState, READ_SHUT_DOWN) || ! anyAreSet(oldState, NEED_WRAP)) {
return;
}
final Thread thread = currentThread();
final Thread next = readWaiterUpdater.getAndSet(this, thread);
try {
if (anyAreSet(oldState = state, READ_SHUT_DOWN)) {
return;
}
if (allAreSet(oldState, NEED_WRAP)) {
wrap(Buffers.EMPTY_BYTE_BUFFER);
}
park(this);
if (thread.isInterrupted()) {
throw msg.interruptedIO();
}
} finally {
// always unpark because we cannot know if our awaken was spurious
if (next != null) unpark(next);
}
}
/**
* Block until this engine can unwrap messages.
*
* @param time timeout for blocking
* @param timeUnit timeout unit
* @throws IOException if an IO exception occurs during await
*/
public void awaitCanUnwrap(long time, TimeUnit timeUnit) throws IOException {
int oldState = state;
if (anyAreSet(oldState, READ_SHUT_DOWN) || ! anyAreSet(oldState, NEED_WRAP)) {
return;
}
final Thread thread = currentThread();
final Thread next = readWaiterUpdater.getAndSet(this, thread);
long duration = timeUnit.toNanos(time);
try {
if (anyAreSet(oldState = state, READ_SHUT_DOWN)) {
return;
}
if (allAreSet(oldState, NEED_WRAP)) {
wrap(Buffers.EMPTY_BYTE_BUFFER);
}
parkNanos(this, duration);
if (thread.isInterrupted()) {
throw msg.interruptedIO();
}
} finally {
// always unpark because we cannot know if our awaken was spurious
if (next != null) unpark(next);
}
}
public boolean isFirstHandshake() {
return allAreSet(state, FIRST_HANDSHAKE);
}
SSLEngine getEngine() {
return engine;
}
/**
* Indicate that the engine will not be able unwrap before a successful wrap is performed.
*/
private void needWrap() {
setFlags(NEED_WRAP);
}
/**
* Indicate if the engine can unwrap.
*/
private boolean isWrapNeeded() {
return allAreSet(state, NEED_WRAP);
}
/**
* Indicate that the engine no longer requires a successful wrap to proceed with unwrap operations.
*/
private void clearNeedWrap() {
clearFlags(NEED_WRAP);
}
/**
* Indicate that the engine will not be able wrap before a successful unwrap is performed.
*/
private void needUnwrap() {
setFlags(NEED_UNWRAP);
}
/**
* Indicate if the engine can wrap.
*/
private boolean isUnwrapNeeded() {
return allAreSet(state, NEED_UNWRAP);
}
/**
* Indicates that even though there is data available there is not enough to form a complete packet
*/
private boolean isUnderflow() {
return allAreSet(state, BUFFER_UNDERFLOW);
}
/**
* Indicate that the engine no longer requires a successful unwrap to proceed with wrap operations.
*/
private void clearNeedUnwrap() {
clearFlags(NEED_UNWRAP);
}
private int setFlags(int flags) {
int oldState;
do {
oldState = state;
if ((oldState & flags) == flags) {
return oldState;
}
} while (! stateUpdater.compareAndSet(this, oldState, oldState | flags));
return oldState;
}
private int clearFlags(int flags) {
int oldState;
do {
oldState = state;
if ((oldState & flags) == 0) {
return oldState;
}
} while (! stateUpdater.compareAndSet(this, oldState, oldState & ~flags));
return oldState;
}
public boolean isDataAvailable() {
synchronized (getUnwrapLock()) {
try {
return readBuffer.getResource().hasRemaining() || (receiveBuffer.getResource().hasRemaining() && !isUnderflow());
} catch (IllegalStateException ignored) {
return false;
}
}
}
private final ByteBuffer getSendBuffer() {
return expandedSendBuffer != null? expandedSendBuffer : sendBuffer.getResource();
}
}