org.jboss.netty.handler.ssl.SslHandler Maven / Gradle / Ivy
Show all versions of hello-world-plugin Show documentation
/*
* Copyright 2012 The Netty Project
*
* The Netty Project licenses this file to you 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.jboss.netty.handler.ssl;
import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.buffer.ChannelBufferFactory;
import org.jboss.netty.buffer.ChannelBuffers;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelDownstreamHandler;
import org.jboss.netty.channel.ChannelEvent;
import org.jboss.netty.channel.ChannelFuture;
import org.jboss.netty.channel.ChannelFutureListener;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.ChannelPipeline;
import org.jboss.netty.channel.ChannelStateEvent;
import org.jboss.netty.channel.Channels;
import org.jboss.netty.channel.DefaultChannelFuture;
import org.jboss.netty.channel.DownstreamMessageEvent;
import org.jboss.netty.channel.ExceptionEvent;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.handler.codec.frame.FrameDecoder;
import org.jboss.netty.logging.InternalLogger;
import org.jboss.netty.logging.InternalLoggerFactory;
import org.jboss.netty.util.Timeout;
import org.jboss.netty.util.Timer;
import org.jboss.netty.util.TimerTask;
import org.jboss.netty.util.internal.DetectionUtil;
import org.jboss.netty.util.internal.NonReentrantLock;
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 java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.DatagramChannel;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
import java.util.regex.Pattern;
import static org.jboss.netty.channel.Channels.*;
/**
* Adds SSL
* · TLS and StartTLS support to a {@link Channel}. Please refer
* to the "SecureChat" example in the distribution or the web
* site for the detailed usage.
*
* Beginning the handshake
*
* You must make sure not to write a message while the
* {@linkplain #handshake() handshake} is in progress unless you are
* renegotiating. You will be notified by the {@link ChannelFuture} which is
* returned by the {@link #handshake()} method when the handshake
* process succeeds or fails.
*
*
Handshake
*
* If {@link #isIssueHandshake()} is {@code false}
* (default) you will need to take care of calling {@link #handshake()} by your own. In most
* situations were {@link SslHandler} is used in 'client mode' you want to issue a handshake once
* the connection was established. if {@link #setIssueHandshake(boolean)} is set to {@code true}
* you don't need to worry about this as the {@link SslHandler} will take care of it.
*
*
*
Renegotiation
*
* If {@link #isEnableRenegotiation() enableRenegotiation} is {@code true}
* (default) and the initial handshake has been done successfully, you can call
* {@link #handshake()} to trigger the renegotiation.
*
* If {@link #isEnableRenegotiation() enableRenegotiation} is {@code false},
* an attempt to trigger renegotiation will result in the connection closure.
*
* Please note that TLS renegotiation had a security issue before. If your
* runtime environment did not fix it, please make sure to disable TLS
* renegotiation by calling {@link #setEnableRenegotiation(boolean)} with
* {@code false}. For more information, please refer to the following documents:
*
*
* Closing the session
*
* To close the SSL session, the {@link #close()} method should be
* called to send the {@code close_notify} message to the remote peer. One
* exception is when you close the {@link Channel} - {@link SslHandler}
* intercepts the close request and send the {@code close_notify} message
* before the channel closure automatically. Once the SSL session is closed,
* it is not reusable, and consequently you should create a new
* {@link SslHandler} with a new {@link SSLEngine} as explained in the
* following section.
*
*
Restarting the session
*
* To restart the SSL session, you must remove the existing closed
* {@link SslHandler} from the {@link ChannelPipeline}, insert a new
* {@link SslHandler} with a new {@link SSLEngine} into the pipeline,
* and start the handshake process as described in the first section.
*
*
Implementing StartTLS
*
* StartTLS is the
* communication pattern that secures the wire in the middle of the plaintext
* connection. Please note that it is different from SSL · TLS, that
* secures the wire from the beginning of the connection. Typically, StartTLS
* is composed of three steps:
*
* - Client sends a StartTLS request to server.
* - Server sends a StartTLS response to client.
* - Client begins SSL handshake.
*
* If you implement a server, you need to:
*
* - create a new {@link SslHandler} instance with {@code startTls} flag set
* to {@code true},
* - insert the {@link SslHandler} to the {@link ChannelPipeline}, and
* - write a StartTLS response.
*
* Please note that you must insert {@link SslHandler} before sending
* the StartTLS response. Otherwise the client can send begin SSL handshake
* before {@link SslHandler} is inserted to the {@link ChannelPipeline}, causing
* data corruption.
*
* The client-side implementation is much simpler.
*
* - Write a StartTLS request,
* - wait for the StartTLS response,
* - create a new {@link SslHandler} instance with {@code startTls} flag set
* to {@code false},
* - insert the {@link SslHandler} to the {@link ChannelPipeline}, and
* - Initiate SSL handshake by calling {@link SslHandler#handshake()}.
*
*
* Known issues
*
* Because of a known issue with the current implementation of the SslEngine that comes
* with Java it may be possible that you see blocked IO-Threads while a full GC is done.
*
* So if you are affected you can workaround this problem by adjust the cache settings
* like shown below:
*
*
* SslContext context = ...;
* context.getServerSessionContext().setSessionCacheSize(someSaneSize);
* context.getServerSessionContext().setSessionTime(someSameTimeout);
*
*
* What values to use here depends on the nature of your application and should be set
* based on monitoring and debugging of it.
* For more details see
* #832 in our issue tracker.
* @apiviz.landmark
* @apiviz.uses org.jboss.netty.handler.ssl.SslBufferPool
*/
public class SslHandler extends FrameDecoder
implements ChannelDownstreamHandler {
private static final InternalLogger logger = InternalLoggerFactory.getInstance(SslHandler.class);
private static final ByteBuffer EMPTY_BUFFER = ByteBuffer.allocate(0);
private static final Pattern IGNORABLE_CLASS_IN_STACK = Pattern.compile(
"^.*(?:Socket|Datagram|Sctp|Udt)Channel.*$");
private static final Pattern IGNORABLE_ERROR_MESSAGE = Pattern.compile(
"^.*(?:connection.*(?:reset|closed|abort|broken)|broken.*pipe).*$", Pattern.CASE_INSENSITIVE);
private static SslBufferPool defaultBufferPool;
/**
* Returns the default {@link SslBufferPool} used when no pool is
* specified in the constructor.
*/
public static synchronized SslBufferPool getDefaultBufferPool() {
if (defaultBufferPool == null) {
defaultBufferPool = new SslBufferPool();
}
return defaultBufferPool;
}
private volatile ChannelHandlerContext ctx;
private final SSLEngine engine;
private final SslBufferPool bufferPool;
private final boolean startTls;
private volatile boolean enableRenegotiation = true;
final Object handshakeLock = new Object();
private boolean handshaking;
private volatile boolean handshaken;
private volatile ChannelFuture handshakeFuture;
@SuppressWarnings("UnusedDeclaration")
private volatile int sentFirstMessage;
@SuppressWarnings("UnusedDeclaration")
private volatile int sentCloseNotify;
@SuppressWarnings("UnusedDeclaration")
private volatile int closedOutboundAndChannel;
private static final AtomicIntegerFieldUpdater SENT_FIRST_MESSAGE_UPDATER =
AtomicIntegerFieldUpdater.newUpdater(SslHandler.class, "sentFirstMessage");
private static final AtomicIntegerFieldUpdater SENT_CLOSE_NOTIFY_UPDATER =
AtomicIntegerFieldUpdater.newUpdater(SslHandler.class, "sentCloseNotify");
private static final AtomicIntegerFieldUpdater CLOSED_OUTBOUND_AND_CHANNEL_UPDATER =
AtomicIntegerFieldUpdater.newUpdater(SslHandler.class, "closedOutboundAndChannel");
int ignoreClosedChannelException;
final Object ignoreClosedChannelExceptionLock = new Object();
private final Queue pendingUnencryptedWrites = new LinkedList();
private final NonReentrantLock pendingUnencryptedWritesLock = new NonReentrantLock();
private final Queue pendingEncryptedWrites = new ConcurrentLinkedQueue();
private final NonReentrantLock pendingEncryptedWritesLock = new NonReentrantLock();
private volatile boolean issueHandshake;
private volatile boolean writeBeforeHandshakeDone;
private final SSLEngineInboundCloseFuture sslEngineCloseFuture = new SSLEngineInboundCloseFuture();
private boolean closeOnSslException;
private int packetLength;
private final Timer timer;
private final long handshakeTimeoutInMillis;
private Timeout handshakeTimeout;
/**
* Creates a new instance.
*
* @param engine the {@link SSLEngine} this handler will use
*/
public SslHandler(SSLEngine engine) {
this(engine, getDefaultBufferPool(), false, null, 0);
}
/**
* Creates a new instance.
*
* @param engine the {@link SSLEngine} this handler will use
* @param bufferPool the {@link SslBufferPool} where this handler will
* acquire the buffers required by the {@link SSLEngine}
*/
public SslHandler(SSLEngine engine, SslBufferPool bufferPool) {
this(engine, bufferPool, false, null, 0);
}
/**
* Creates a new instance.
*
* @param engine the {@link SSLEngine} this handler will use
* @param startTls {@code true} if the first write request shouldn't be
* encrypted by the {@link SSLEngine}
*/
public SslHandler(SSLEngine engine, boolean startTls) {
this(engine, getDefaultBufferPool(), startTls);
}
/**
* Creates a new instance.
*
* @param engine the {@link SSLEngine} this handler will use
* @param bufferPool the {@link SslBufferPool} where this handler will
* acquire the buffers required by the {@link SSLEngine}
* @param startTls {@code true} if the first write request shouldn't be
* encrypted by the {@link SSLEngine}
*/
public SslHandler(SSLEngine engine, SslBufferPool bufferPool, boolean startTls) {
this(engine, bufferPool, startTls, null, 0);
}
/**
* Creates a new instance.
*
* @param engine
* the {@link SSLEngine} this handler will use
* @param bufferPool
* the {@link SslBufferPool} where this handler will acquire
* the buffers required by the {@link SSLEngine}
* @param startTls
* {@code true} if the first write request shouldn't be encrypted
* by the {@link SSLEngine}
* @param timer
* the {@link Timer} which will be used to process the timeout of the {@link #handshake()}.
* Be aware that the given {@link Timer} will not get stopped automaticly, so it is up to you to cleanup
* once you not need it anymore
* @param handshakeTimeoutInMillis
* the time in milliseconds after whic the {@link #handshake()} will be failed, and so the future notified
*/
public SslHandler(SSLEngine engine, SslBufferPool bufferPool, boolean startTls, Timer timer,
long handshakeTimeoutInMillis) {
if (engine == null) {
throw new NullPointerException("engine");
}
if (bufferPool == null) {
throw new NullPointerException("bufferPool");
}
if (timer == null && handshakeTimeoutInMillis > 0) {
throw new IllegalArgumentException("No Timer was given but a handshakeTimeoutInMillis, need both or none");
}
this.engine = engine;
this.bufferPool = bufferPool;
this.startTls = startTls;
this.timer = timer;
this.handshakeTimeoutInMillis = handshakeTimeoutInMillis;
}
/**
* Returns the {@link SSLEngine} which is used by this handler.
*/
public SSLEngine getEngine() {
return engine;
}
/**
* Starts an SSL / TLS handshake for the specified channel.
*
* @return a {@link ChannelFuture} which is notified when the handshake
* succeeds or fails.
*/
public ChannelFuture handshake() {
synchronized (handshakeLock) {
if (handshaken && !isEnableRenegotiation()) {
throw new IllegalStateException("renegotiation disabled");
}
final ChannelHandlerContext ctx = this.ctx;
final Channel channel = ctx.getChannel();
ChannelFuture handshakeFuture;
Exception exception = null;
if (handshaking) {
return this.handshakeFuture;
}
handshaking = true;
try {
engine.beginHandshake();
runDelegatedTasks();
handshakeFuture = this.handshakeFuture = future(channel);
if (handshakeTimeoutInMillis > 0) {
handshakeTimeout = timer.newTimeout(new TimerTask() {
public void run(Timeout timeout) throws Exception {
ChannelFuture future = SslHandler.this.handshakeFuture;
if (future != null && future.isDone()) {
return;
}
setHandshakeFailure(channel, new SSLException("Handshake did not complete within " +
handshakeTimeoutInMillis + "ms"));
}
}, handshakeTimeoutInMillis, TimeUnit.MILLISECONDS);
}
} catch (Exception e) {
handshakeFuture = this.handshakeFuture = failedFuture(channel, e);
exception = e;
}
if (exception == null) { // Began handshake successfully.
try {
final ChannelFuture hsFuture = handshakeFuture;
wrapNonAppData(ctx, channel).addListener(new ChannelFutureListener() {
public void operationComplete(ChannelFuture future) throws Exception {
if (!future.isSuccess()) {
Throwable cause = future.getCause();
hsFuture.setFailure(cause);
fireExceptionCaught(ctx, cause);
if (closeOnSslException) {
Channels.close(ctx, future(channel));
}
}
}
});
} catch (SSLException e) {
handshakeFuture.setFailure(e);
fireExceptionCaught(ctx, e);
if (closeOnSslException) {
Channels.close(ctx, future(channel));
}
}
} else { // Failed to initiate handshake.
fireExceptionCaught(ctx, exception);
if (closeOnSslException) {
Channels.close(ctx, future(channel));
}
}
return handshakeFuture;
}
}
/**
* Sends an SSL {@code close_notify} message to the specified channel and
* destroys the underlying {@link SSLEngine}.
*/
public ChannelFuture close() {
ChannelHandlerContext ctx = this.ctx;
Channel channel = ctx.getChannel();
try {
engine.closeOutbound();
return wrapNonAppData(ctx, channel);
} catch (SSLException e) {
fireExceptionCaught(ctx, e);
if (closeOnSslException) {
Channels.close(ctx, future(channel));
}
return failedFuture(channel, e);
}
}
/**
* Returns {@code true} if and only if TLS renegotiation is enabled.
*/
public boolean isEnableRenegotiation() {
return enableRenegotiation;
}
/**
* Enables or disables TLS renegotiation.
*/
public void setEnableRenegotiation(boolean enableRenegotiation) {
this.enableRenegotiation = enableRenegotiation;
}
/**
* Enables or disables the automatic handshake once the {@link Channel} is
* connected. The value will only have affect if its set before the
* {@link Channel} is connected.
*/
public void setIssueHandshake(boolean issueHandshake) {
this.issueHandshake = issueHandshake;
}
/**
* Returns {@code true} if the automatic handshake is enabled
*/
public boolean isIssueHandshake() {
return issueHandshake;
}
/**
* Return the {@link ChannelFuture} that will get notified if the inbound of the {@link SSLEngine} will get closed.
*
* This method will return the same {@link ChannelFuture} all the time.
*
* For more informations see the apidocs of {@link SSLEngine}
*
*/
public ChannelFuture getSSLEngineInboundCloseFuture() {
return sslEngineCloseFuture;
}
/**
* Return the timeout (in ms) after which the {@link ChannelFuture} of {@link #handshake()} will be failed, while
* a handshake is in progress
*/
public long getHandshakeTimeout() {
return handshakeTimeoutInMillis;
}
/**
* If set to {@code true}, the {@link Channel} will automatically get closed
* one a {@link SSLException} was caught. This is most times what you want, as after this
* its almost impossible to recover.
*
* Anyway the default is {@code false} to not break compatibility with older releases. This
* will be changed to {@code true} in the next major release.
*
*/
public void setCloseOnSSLException(boolean closeOnSslException) {
if (ctx != null) {
throw new IllegalStateException("Can only get changed before attached to ChannelPipeline");
}
this.closeOnSslException = closeOnSslException;
}
public boolean getCloseOnSSLException() {
return closeOnSslException;
}
public void handleDownstream(
final ChannelHandlerContext context, final ChannelEvent evt) throws Exception {
if (evt instanceof ChannelStateEvent) {
ChannelStateEvent e = (ChannelStateEvent) evt;
switch (e.getState()) {
case OPEN:
case CONNECTED:
case BOUND:
if (Boolean.FALSE.equals(e.getValue()) || e.getValue() == null) {
closeOutboundAndChannel(context, e);
return;
}
}
}
if (!(evt instanceof MessageEvent)) {
context.sendDownstream(evt);
return;
}
MessageEvent e = (MessageEvent) evt;
if (!(e.getMessage() instanceof ChannelBuffer)) {
context.sendDownstream(evt);
return;
}
// Do not encrypt the first write request if this handler is
// created with startTLS flag turned on.
if (startTls && SENT_FIRST_MESSAGE_UPDATER.compareAndSet(this, 0, 1)) {
context.sendDownstream(evt);
return;
}
// Otherwise, all messages are encrypted.
ChannelBuffer msg = (ChannelBuffer) e.getMessage();
PendingWrite pendingWrite;
if (msg.readable()) {
pendingWrite = new PendingWrite(evt.getFuture(), msg.toByteBuffer(msg.readerIndex(), msg.readableBytes()));
} else {
pendingWrite = new PendingWrite(evt.getFuture(), null);
}
pendingUnencryptedWritesLock.lock();
try {
pendingUnencryptedWrites.add(pendingWrite);
} finally {
pendingUnencryptedWritesLock.unlock();
}
if (handshakeFuture == null || !handshakeFuture.isDone()) {
writeBeforeHandshakeDone = true;
}
wrap(context, evt.getChannel());
}
private void cancelHandshakeTimeout() {
if (handshakeTimeout != null) {
// cancel the task as we will fail the handshake future now
handshakeTimeout.cancel();
}
}
@Override
public void channelDisconnected(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
// Make sure the handshake future is notified when a connection has
// been closed during handshake.
synchronized (handshakeLock) {
if (handshaking) {
cancelHandshakeTimeout();
handshakeFuture.setFailure(new ClosedChannelException());
}
}
try {
super.channelDisconnected(ctx, e);
} finally {
unwrapNonAppData(ctx, e.getChannel(), false);
closeEngine();
}
}
private void closeEngine() {
engine.closeOutbound();
if (sentCloseNotify == 0 && handshaken) {
try {
engine.closeInbound();
} catch (SSLException ex) {
if (logger.isDebugEnabled()) {
logger.debug("Failed to clean up SSLEngine.", ex);
}
}
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e)
throws Exception {
Throwable cause = e.getCause();
if (cause instanceof IOException) {
if (cause instanceof ClosedChannelException) {
synchronized (ignoreClosedChannelExceptionLock) {
if (ignoreClosedChannelException > 0) {
ignoreClosedChannelException --;
if (logger.isDebugEnabled()) {
logger.debug(
"Swallowing an exception raised while " +
"writing non-app data", cause);
}
return;
}
}
} else {
if (ignoreException(cause)) {
return;
}
}
}
ctx.sendUpstream(e);
}
/**
* Checks if the given {@link Throwable} can be ignore and just "swallowed"
*
* When an ssl connection is closed a close_notify message is sent.
* After that the peer also sends close_notify however, it's not mandatory to receive
* the close_notify. The party who sent the initial close_notify can close the connection immediately
* then the peer will get connection reset error.
*
*/
private boolean ignoreException(Throwable t) {
if (!(t instanceof SSLException) && t instanceof IOException && engine.isOutboundDone()) {
String message = String.valueOf(t.getMessage()).toLowerCase();
// first try to match connection reset / broke peer based on the regex. This is the fastest way
// but may fail on different jdk impls or OS's
if (IGNORABLE_ERROR_MESSAGE.matcher(message).matches()) {
return true;
}
// Inspect the StackTraceElements to see if it was a connection reset / broken pipe or not
StackTraceElement[] elements = t.getStackTrace();
for (StackTraceElement element: elements) {
String classname = element.getClassName();
String methodname = element.getMethodName();
// skip all classes that belong to the io.netty package
if (classname.startsWith("org.jboss.netty.")) {
continue;
}
// check if the method name is read if not skip it
if (!"read".equals(methodname)) {
continue;
}
// This will also match against SocketInputStream which is used by openjdk 7 and maybe
// also others
if (IGNORABLE_CLASS_IN_STACK.matcher(classname).matches()) {
return true;
}
try {
// No match by now.. Try to load the class via classloader and inspect it.
// This is mainly done as other JDK implementations may differ in name of
// the impl.
Class> clazz = getClass().getClassLoader().loadClass(classname);
if (SocketChannel.class.isAssignableFrom(clazz)
|| DatagramChannel.class.isAssignableFrom(clazz)) {
return true;
}
// also match against SctpChannel via String matching as it may not present.
if (DetectionUtil.javaVersion() >= 7
&& "com.sun.nio.sctp.SctpChannel".equals(clazz.getSuperclass().getName())) {
return true;
}
} catch (ClassNotFoundException e) {
// This should not happen just ignore
}
}
}
return false;
}
/**
* Returns {@code true} if the given {@link ChannelBuffer} is encrypted. Be aware that this method
* will not increase the readerIndex of the given {@link ChannelBuffer}.
*
* @param buffer
* The {@link ChannelBuffer} to read from. Be aware that it must have at least 5 bytes to read,
* otherwise it will throw an {@link IllegalArgumentException}.
* @return encrypted
* {@code true} if the {@link ChannelBuffer} is encrypted, {@code false} otherwise.
* @throws IllegalArgumentException
* Is thrown if the given {@link ChannelBuffer} has not at least 5 bytes to read.
*/
public static boolean isEncrypted(ChannelBuffer buffer) {
return getEncryptedPacketLength(buffer, buffer.readerIndex()) != -1;
}
/**
* Return how much bytes can be read out of the encrypted data. Be aware that this method will not increase
* the readerIndex of the given {@link ChannelBuffer}.
*
* @param buffer
* The {@link ChannelBuffer} to read from. Be aware that it must have at least 5 bytes to read,
* otherwise it will throw an {@link IllegalArgumentException}.
* @return length
* The length of the encrypted packet that is included in the buffer. This will
* return {@code -1} if the given {@link ChannelBuffer} is not encrypted at all.
* @throws IllegalArgumentException
* Is thrown if the given {@link ChannelBuffer} has not at least 5 bytes to read.
*/
private static int getEncryptedPacketLength(ChannelBuffer buffer, int offset) {
int packetLength = 0;
// SSLv3 or TLS - Check ContentType
boolean tls;
switch (buffer.getUnsignedByte(offset)) {
case 20: // change_cipher_spec
case 21: // alert
case 22: // handshake
case 23: // application_data
tls = true;
break;
default:
// SSLv2 or bad data
tls = false;
}
if (tls) {
// SSLv3 or TLS - Check ProtocolVersion
int majorVersion = buffer.getUnsignedByte(offset + 1);
if (majorVersion == 3) {
// SSLv3 or TLS
packetLength = (getShort(buffer, offset + 3) & 0xFFFF) + 5;
if (packetLength <= 5) {
// Neither SSLv3 or TLSv1 (i.e. SSLv2 or bad data)
tls = false;
}
} else {
// Neither SSLv3 or TLSv1 (i.e. SSLv2 or bad data)
tls = false;
}
}
if (!tls) {
// SSLv2 or bad data - Check the version
boolean sslv2 = true;
int headerLength = (buffer.getUnsignedByte(offset) & 0x80) != 0 ? 2 : 3;
int majorVersion = buffer.getUnsignedByte(offset + headerLength + 1);
if (majorVersion == 2 || majorVersion == 3) {
// SSLv2
if (headerLength == 2) {
packetLength = (getShort(buffer, offset) & 0x7FFF) + 2;
} else {
packetLength = (getShort(buffer, offset) & 0x3FFF) + 3;
}
if (packetLength <= headerLength) {
sslv2 = false;
}
} else {
sslv2 = false;
}
if (!sslv2) {
return -1;
}
}
return packetLength;
}
@Override
protected Object decode(
final ChannelHandlerContext ctx, Channel channel, ChannelBuffer in) throws Exception {
final int startOffset = in.readerIndex();
final int endOffset = in.writerIndex();
int offset = startOffset;
int totalLength = 0;
// If we calculated the length of the current SSL record before, use that information.
if (packetLength > 0) {
if (endOffset - startOffset < packetLength) {
return null;
} else {
offset += packetLength;
totalLength = packetLength;
packetLength = 0;
}
}
boolean nonSslRecord = false;
while (totalLength < OpenSslEngine.MAX_ENCRYPTED_PACKET_LENGTH) {
final int readableBytes = endOffset - offset;
if (readableBytes < 5) {
break;
}
final int packetLength = getEncryptedPacketLength(in, offset);
if (packetLength == -1) {
nonSslRecord = true;
break;
}
assert packetLength > 0;
if (packetLength > readableBytes) {
// wait until the whole packet can be read
this.packetLength = packetLength;
break;
}
int newTotalLength = totalLength + packetLength;
if (newTotalLength > OpenSslEngine.MAX_ENCRYPTED_PACKET_LENGTH) {
// Don't read too much.
break;
}
// We have a whole packet.
// Increment the offset to handle the next packet.
offset += packetLength;
totalLength = newTotalLength;
}
ChannelBuffer unwrapped = null;
if (totalLength > 0) {
// The buffer contains one or more full SSL records.
// Slice out the whole packet so unwrap will only be called with complete packets.
// Also directly reset the packetLength. This is needed as unwrap(..) may trigger
// decode(...) again via:
// 1) unwrap(..) is called
// 2) wrap(...) is called from within unwrap(...)
// 3) wrap(...) calls unwrapLater(...)
// 4) unwrapLater(...) calls decode(...)
//
// See https://github.com/netty/netty/issues/1534
in.skipBytes(totalLength);
final ByteBuffer inNetBuf = in.toByteBuffer(startOffset, totalLength);
unwrapped = unwrap(ctx, channel, inNetBuf, totalLength, true);
}
if (nonSslRecord) {
// Not an SSL/TLS packet
NotSslRecordException e = new NotSslRecordException(
"not an SSL/TLS record: " + ChannelBuffers.hexDump(in));
in.skipBytes(in.readableBytes());
if (closeOnSslException) {
// first trigger the exception and then close the channel
fireExceptionCaught(ctx, e);
Channels.close(ctx, future(channel));
// just return null as we closed the channel before, that
// will take care of cleanup etc
return null;
} else {
throw e;
}
}
return unwrapped;
}
/**
* Reads a big-endian short integer from the buffer. Please note that we do not use
* {@link ChannelBuffer#getShort(int)} because it might be a little-endian buffer.
*/
private static short getShort(ChannelBuffer buf, int offset) {
return (short) (buf.getByte(offset) << 8 | buf.getByte(offset + 1) & 0xFF);
}
private void wrap(ChannelHandlerContext context, Channel channel) throws SSLException {
ChannelBuffer msg;
ByteBuffer outNetBuf = bufferPool.acquireBuffer();
boolean success = true;
boolean offered = false;
boolean needsUnwrap = false;
PendingWrite pendingWrite = null;
try {
loop:
for (;;) {
// Acquire a lock to make sure unencrypted data is polled
// in order and their encrypted counterpart is offered in
// order.
pendingUnencryptedWritesLock.lock();
try {
pendingWrite = pendingUnencryptedWrites.peek();
if (pendingWrite == null) {
break;
}
ByteBuffer outAppBuf = pendingWrite.outAppBuf;
if (outAppBuf == null) {
// A write request with an empty buffer
pendingUnencryptedWrites.remove();
offerEncryptedWriteRequest(
new DownstreamMessageEvent(
channel, pendingWrite.future,
ChannelBuffers.EMPTY_BUFFER,
channel.getRemoteAddress()));
offered = true;
} else {
synchronized (handshakeLock) {
SSLEngineResult result = null;
try {
result = engine.wrap(outAppBuf, outNetBuf);
} finally {
if (!outAppBuf.hasRemaining()) {
pendingUnencryptedWrites.remove();
}
}
if (result.bytesProduced() > 0) {
outNetBuf.flip();
int remaining = outNetBuf.remaining();
msg = ctx.getChannel().getConfig().getBufferFactory().getBuffer(remaining);
// Transfer the bytes to the new ChannelBuffer using some safe method that will also
// work with "non" heap buffers
//
// See https://github.com/netty/netty/issues/329
msg.writeBytes(outNetBuf);
outNetBuf.clear();
ChannelFuture future;
if (pendingWrite.outAppBuf.hasRemaining()) {
// pendingWrite's future shouldn't be notified if
// only partial data is written.
future = succeededFuture(channel);
} else {
future = pendingWrite.future;
}
MessageEvent encryptedWrite = new DownstreamMessageEvent(
channel, future, msg, channel.getRemoteAddress());
offerEncryptedWriteRequest(encryptedWrite);
offered = true;
} else if (result.getStatus() == Status.CLOSED) {
// SSLEngine has been closed already.
// Any further write attempts should be denied.
success = false;
break;
} else {
final HandshakeStatus handshakeStatus = result.getHandshakeStatus();
handleRenegotiation(handshakeStatus);
switch (handshakeStatus) {
case NEED_WRAP:
if (outAppBuf.hasRemaining()) {
break;
} else {
break loop;
}
case NEED_UNWRAP:
needsUnwrap = true;
break loop;
case NEED_TASK:
runDelegatedTasks();
break;
case FINISHED:
setHandshakeSuccess(channel);
if (result.getStatus() == Status.CLOSED) {
success = false;
}
break loop;
case NOT_HANDSHAKING:
setHandshakeSuccessIfStillHandshaking(channel);
if (result.getStatus() == Status.CLOSED) {
success = false;
}
break loop;
default:
throw new IllegalStateException(
"Unknown handshake status: " +
handshakeStatus);
}
}
}
}
} finally {
pendingUnencryptedWritesLock.unlock();
}
}
} catch (SSLException e) {
success = false;
setHandshakeFailure(channel, e);
throw e;
} finally {
bufferPool.releaseBuffer(outNetBuf);
if (offered) {
flushPendingEncryptedWrites(context);
}
if (!success) {
Exception cause = channel.isOpen()
? new SSLException("SSLEngine already closed")
: new ClosedChannelException();
// Check if we had a pendingWrite in process, if so we need to also notify as otherwise
// the ChannelFuture will never get notified
if (pendingWrite != null) {
pendingWrite.future.setFailure(cause);
}
// Mark all remaining pending writes as failure if anything
// wrong happened before the write requests are wrapped.
// Please note that we do not call setFailure while a lock is
// acquired, to avoid a potential dead lock.
for (;;) {
pendingUnencryptedWritesLock.lock();
try {
pendingWrite = pendingUnencryptedWrites.poll();
if (pendingWrite == null) {
break;
}
} finally {
pendingUnencryptedWritesLock.unlock();
}
pendingWrite.future.setFailure(cause);
}
}
}
if (needsUnwrap) {
unwrapNonAppData(ctx, channel, true);
}
}
private void offerEncryptedWriteRequest(MessageEvent encryptedWrite) {
final boolean locked = pendingEncryptedWritesLock.tryLock();
try {
pendingEncryptedWrites.add(encryptedWrite);
} finally {
if (locked) {
pendingEncryptedWritesLock.unlock();
}
}
}
private void flushPendingEncryptedWrites(ChannelHandlerContext ctx) {
while (!pendingEncryptedWrites.isEmpty()) {
// Avoid possible dead lock and data integrity issue
// which is caused by cross communication between more than one channel
// in the same VM.
if (!pendingEncryptedWritesLock.tryLock()) {
return;
}
try {
MessageEvent e;
while ((e = pendingEncryptedWrites.poll()) != null) {
ctx.sendDownstream(e);
}
} finally {
pendingEncryptedWritesLock.unlock();
}
// Other thread might have added more elements at this point, so we loop again if the queue got unempty.
}
}
private ChannelFuture wrapNonAppData(ChannelHandlerContext ctx, Channel channel) throws SSLException {
ChannelFuture future = null;
ByteBuffer outNetBuf = bufferPool.acquireBuffer();
SSLEngineResult result;
try {
for (;;) {
synchronized (handshakeLock) {
result = engine.wrap(EMPTY_BUFFER, outNetBuf);
}
if (result.bytesProduced() > 0) {
outNetBuf.flip();
ChannelBuffer msg =
ctx.getChannel().getConfig().getBufferFactory().getBuffer(outNetBuf.remaining());
// Transfer the bytes to the new ChannelBuffer using some safe method that will also
// work with "non" heap buffers
//
// See https://github.com/netty/netty/issues/329
msg.writeBytes(outNetBuf);
outNetBuf.clear();
future = future(channel);
future.addListener(new ChannelFutureListener() {
public void operationComplete(ChannelFuture future)
throws Exception {
if (future.getCause() instanceof ClosedChannelException) {
synchronized (ignoreClosedChannelExceptionLock) {
ignoreClosedChannelException ++;
}
}
}
});
write(ctx, future, msg);
}
final HandshakeStatus handshakeStatus = result.getHandshakeStatus();
handleRenegotiation(handshakeStatus);
switch (handshakeStatus) {
case FINISHED:
setHandshakeSuccess(channel);
runDelegatedTasks();
break;
case NEED_TASK:
runDelegatedTasks();
break;
case NEED_UNWRAP:
if (!Thread.holdsLock(handshakeLock)) {
// unwrap shouldn't be called when this method was
// called by unwrap - unwrap will keep running after
// this method returns.
unwrapNonAppData(ctx, channel, true);
}
break;
case NOT_HANDSHAKING:
if (setHandshakeSuccessIfStillHandshaking(channel)) {
runDelegatedTasks();
}
break;
case NEED_WRAP:
break;
default:
throw new IllegalStateException(
"Unexpected handshake status: " + handshakeStatus);
}
if (result.bytesProduced() == 0) {
break;
}
}
} catch (SSLException e) {
setHandshakeFailure(channel, e);
throw e;
} finally {
bufferPool.releaseBuffer(outNetBuf);
}
if (future == null) {
future = succeededFuture(channel);
}
return future;
}
/**
* Calls {@link SSLEngine#unwrap(ByteBuffer, ByteBuffer)} with an empty buffer to handle handshakes, etc.
*/
private void unwrapNonAppData(
ChannelHandlerContext ctx, Channel channel, boolean mightNeedHandshake) throws SSLException {
unwrap(ctx, channel, EMPTY_BUFFER, -1, mightNeedHandshake);
}
/**
* Unwraps inbound SSL records.
*/
private ChannelBuffer unwrap(
ChannelHandlerContext ctx, Channel channel,
ByteBuffer nioInNetBuf,
int initialNettyOutAppBufCapacity, boolean mightNeedHandshake) throws SSLException {
final int nioInNetBufStartOffset = nioInNetBuf.position();
final ByteBuffer nioOutAppBuf = bufferPool.acquireBuffer();
ChannelBuffer nettyOutAppBuf = null;
try {
boolean needsWrap = false;
for (;;) {
SSLEngineResult result;
boolean needsHandshake = false;
if (mightNeedHandshake) {
synchronized (handshakeLock) {
if (!handshaken && !handshaking &&
!engine.getUseClientMode() &&
!engine.isInboundDone() && !engine.isOutboundDone()) {
needsHandshake = true;
}
}
}
if (needsHandshake) {
handshake();
}
synchronized (handshakeLock) {
// Decrypt at least one record in the inbound network buffer.
// It is impossible to consume no record here because we made sure the inbound network buffer
// always contain at least one record in decode(). Therefore, if SSLEngine.unwrap() returns
// BUFFER_OVERFLOW, it is always resolved by retrying after emptying the application buffer.
for (;;) {
final int outAppBufSize = engine.getSession().getApplicationBufferSize();
final ByteBuffer outAppBuf;
if (nioOutAppBuf.capacity() < outAppBufSize) {
// SSLEngine wants a buffer larger than what the pool can provide.
// Allocate a temporary heap buffer.
outAppBuf = ByteBuffer.allocate(outAppBufSize);
} else {
outAppBuf = nioOutAppBuf;
}
try {
result = engine.unwrap(nioInNetBuf, outAppBuf);
switch (result.getStatus()) {
case CLOSED:
// notify about the CLOSED state of the SSLEngine. See #137
sslEngineCloseFuture.setClosed();
break;
case BUFFER_OVERFLOW:
// Flush the unwrapped data in the outAppBuf into frame and try again.
// See the finally block.
continue;
}
break;
} finally {
outAppBuf.flip();
// Copy the unwrapped data into a smaller buffer.
if (outAppBuf.hasRemaining()) {
if (nettyOutAppBuf == null) {
ChannelBufferFactory factory = ctx.getChannel().getConfig().getBufferFactory();
nettyOutAppBuf = factory.getBuffer(initialNettyOutAppBufCapacity);
}
nettyOutAppBuf.writeBytes(outAppBuf);
}
outAppBuf.clear();
}
}
final HandshakeStatus handshakeStatus = result.getHandshakeStatus();
handleRenegotiation(handshakeStatus);
switch (handshakeStatus) {
case NEED_UNWRAP:
break;
case NEED_WRAP:
wrapNonAppData(ctx, channel);
break;
case NEED_TASK:
runDelegatedTasks();
break;
case FINISHED:
setHandshakeSuccess(channel);
needsWrap = true;
continue;
case NOT_HANDSHAKING:
if (setHandshakeSuccessIfStillHandshaking(channel)) {
needsWrap = true;
continue;
}
if (writeBeforeHandshakeDone) {
// We need to call wrap(...) in case there was a flush done before the handshake completed.
//
// See https://github.com/netty/netty/pull/2437
writeBeforeHandshakeDone = false;
needsWrap = true;
}
break;
default:
throw new IllegalStateException(
"Unknown handshake status: " + handshakeStatus);
}
if (result.getStatus() == Status.BUFFER_UNDERFLOW ||
result.bytesConsumed() == 0 && result.bytesProduced() == 0) {
if (nioInNetBuf.hasRemaining() && !engine.isInboundDone()) {
// We expect SSLEngine to consume all the bytes we feed it, but
// empirical evidence indicates that we sometimes end up with leftovers
// Log when this happens to get a better understanding of this corner
// case.
// See https://github.com/netty/netty/pull/3584
logger.warn("Unexpected leftover data after SSLEngine.unwrap():"
+ " status=" + result.getStatus()
+ " handshakeStatus=" + result.getHandshakeStatus()
+ " consumed=" + result.bytesConsumed()
+ " produced=" + result.bytesProduced()
+ " remaining=" + nioInNetBuf.remaining()
+ " data=" + ChannelBuffers.hexDump(ChannelBuffers.wrappedBuffer(nioInNetBuf)));
}
break;
}
}
}
if (needsWrap) {
// wrap() acquires pendingUnencryptedWrites first and then
// handshakeLock. If handshakeLock is already hold by the
// current thread, calling wrap() will lead to a dead lock
// i.e. pendingUnencryptedWrites -> handshakeLock vs.
// handshakeLock -> pendingUnencryptedLock -> handshakeLock
//
// There is also the same issue between pendingEncryptedWrites
// and pendingUnencryptedWrites.
if (!Thread.holdsLock(handshakeLock) && !pendingEncryptedWritesLock.isHeldByCurrentThread()) {
wrap(ctx, channel);
}
}
} catch (SSLException e) {
setHandshakeFailure(channel, e);
throw e;
} finally {
bufferPool.releaseBuffer(nioOutAppBuf);
}
if (nettyOutAppBuf != null && nettyOutAppBuf.readable()) {
return nettyOutAppBuf;
} else {
return null;
}
}
private void handleRenegotiation(HandshakeStatus handshakeStatus) {
synchronized (handshakeLock) {
if (handshakeStatus == HandshakeStatus.NOT_HANDSHAKING ||
handshakeStatus == HandshakeStatus.FINISHED) {
// Not handshaking
return;
}
if (!handshaken) {
// Not renegotiation
return;
}
final boolean renegotiate;
if (handshaking) {
// Renegotiation in progress or failed already.
// i.e. Renegotiation check has been done already below.
return;
}
if (engine.isInboundDone() || engine.isOutboundDone()) {
// Not handshaking but closing.
return;
}
if (isEnableRenegotiation()) {
// Continue renegotiation.
renegotiate = true;
} else {
// Do not renegotiate.
renegotiate = false;
// Prevent reentrance of this method.
handshaking = true;
}
if (renegotiate) {
// Renegotiate.
handshake();
} else {
// Raise an exception.
fireExceptionCaught(
ctx, new SSLException(
"renegotiation attempted by peer; " +
"closing the connection"));
// Close the connection to stop renegotiation.
Channels.close(ctx, succeededFuture(ctx.getChannel()));
}
}
}
/**
* Fetches all delegated tasks from the {@link SSLEngine} and runs them immediately.
*/
private void runDelegatedTasks() {
for (;;) {
final Runnable task;
synchronized (handshakeLock) {
task = engine.getDelegatedTask();
}
if (task == null) {
break;
}
task.run();
}
}
/**
* Works around some Android {@link SSLEngine} implementations that skip {@link HandshakeStatus#FINISHED} and
* go straight into {@link HandshakeStatus#NOT_HANDSHAKING} when handshake is finished.
*
* @return {@code true} if and only if the workaround has been applied and thus {@link #handshakeFuture} has been
* marked as success by this method
*/
private boolean setHandshakeSuccessIfStillHandshaking(Channel channel) {
if (handshaking && !handshakeFuture.isDone()) {
setHandshakeSuccess(channel);
return true;
}
return false;
}
private void setHandshakeSuccess(Channel channel) {
synchronized (handshakeLock) {
handshaking = false;
handshaken = true;
if (handshakeFuture == null) {
handshakeFuture = future(channel);
}
cancelHandshakeTimeout();
}
if (logger.isDebugEnabled()) {
logger.debug(channel + " HANDSHAKEN: " + engine.getSession().getCipherSuite());
}
handshakeFuture.setSuccess();
}
private void setHandshakeFailure(Channel channel, SSLException cause) {
synchronized (handshakeLock) {
if (!handshaking) {
return;
}
handshaking = false;
handshaken = false;
if (handshakeFuture == null) {
handshakeFuture = future(channel);
}
// cancel the timeout now
cancelHandshakeTimeout();
// Release all resources such as internal buffers that SSLEngine
// is managing.
engine.closeOutbound();
try {
engine.closeInbound();
} catch (SSLException e) {
if (logger.isDebugEnabled()) {
logger.debug(
"SSLEngine.closeInbound() raised an exception after " +
"a handshake failure.", e);
}
}
}
handshakeFuture.setFailure(cause);
if (closeOnSslException) {
Channels.close(ctx, future(channel));
}
}
private void closeOutboundAndChannel(
final ChannelHandlerContext context, final ChannelStateEvent e) {
if (!e.getChannel().isConnected()) {
context.sendDownstream(e);
return;
}
// Ensure that the tear-down logic beyond this point is never invoked concurrently nor multiple times.
if (!CLOSED_OUTBOUND_AND_CHANNEL_UPDATER.compareAndSet(this, 0, 1)) {
// The other thread called this method already, and thus the connection will be closed eventually.
// So, just wait until the connection is closed, and then forward the event so that the sink handles
// the duplicate close attempt.
e.getChannel().getCloseFuture().addListener(new ChannelFutureListener() {
public void operationComplete(ChannelFuture future) throws Exception {
context.sendDownstream(e);
}
});
return;
}
boolean passthrough = true;
try {
try {
unwrapNonAppData(ctx, e.getChannel(), false);
} catch (SSLException ex) {
if (logger.isDebugEnabled()) {
logger.debug("Failed to unwrap before sending a close_notify message", ex);
}
}
if (!engine.isOutboundDone()) {
if (SENT_CLOSE_NOTIFY_UPDATER.compareAndSet(this, 0, 1)) {
engine.closeOutbound();
try {
ChannelFuture closeNotifyFuture = wrapNonAppData(context, e.getChannel());
closeNotifyFuture.addListener(
new ClosingChannelFutureListener(context, e));
passthrough = false;
} catch (SSLException ex) {
if (logger.isDebugEnabled()) {
logger.debug("Failed to encode a close_notify message", ex);
}
}
}
}
} finally {
if (passthrough) {
context.sendDownstream(e);
}
}
}
private static final class PendingWrite {
final ChannelFuture future;
final ByteBuffer outAppBuf;
PendingWrite(ChannelFuture future, ByteBuffer outAppBuf) {
this.future = future;
this.outAppBuf = outAppBuf;
}
}
private static final class ClosingChannelFutureListener implements ChannelFutureListener {
private final ChannelHandlerContext context;
private final ChannelStateEvent e;
ClosingChannelFutureListener(
ChannelHandlerContext context, ChannelStateEvent e) {
this.context = context;
this.e = e;
}
public void operationComplete(ChannelFuture closeNotifyFuture) throws Exception {
if (!(closeNotifyFuture.getCause() instanceof ClosedChannelException)) {
Channels.close(context, e.getFuture());
} else {
e.getFuture().setSuccess();
}
}
}
@Override
public void beforeAdd(ChannelHandlerContext ctx) throws Exception {
super.beforeAdd(ctx);
this.ctx = ctx;
}
/**
* Fail all pending writes which we were not able to flush out
*/
@Override
public void afterRemove(ChannelHandlerContext ctx) throws Exception {
closeEngine();
// there is no need for synchronization here as we do not receive downstream events anymore
Throwable cause = null;
for (;;) {
PendingWrite pw = pendingUnencryptedWrites.poll();
if (pw == null) {
break;
}
if (cause == null) {
cause = new IOException("Unable to write data");
}
pw.future.setFailure(cause);
}
for (;;) {
MessageEvent ev = pendingEncryptedWrites.poll();
if (ev == null) {
break;
}
if (cause == null) {
cause = new IOException("Unable to write data");
}
ev.getFuture().setFailure(cause);
}
if (cause != null) {
fireExceptionCaughtLater(ctx, cause);
}
}
/**
* Calls {@link #handshake()} once the {@link Channel} is connected
*/
@Override
public void channelConnected(final ChannelHandlerContext ctx, final ChannelStateEvent e) throws Exception {
if (issueHandshake) {
// issue and handshake and add a listener to it which will fire an exception event if
// an exception was thrown while doing the handshake
handshake().addListener(new ChannelFutureListener() {
public void operationComplete(ChannelFuture future) throws Exception {
if (future.isSuccess()) {
// Send the event upstream after the handshake was completed without an error.
//
// See https://github.com/netty/netty/issues/358
ctx.sendUpstream(e);
}
}
});
} else {
super.channelConnected(ctx, e);
}
}
/**
* Loop over all the pending writes and fail them.
*
* See #305 for more details.
*/
@Override
public void channelClosed(final ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
// Move the fail of the writes to the IO-Thread to prevent possible deadlock
// See https://github.com/netty/netty/issues/989
ctx.getPipeline().execute(new Runnable() {
public void run() {
if (!pendingUnencryptedWritesLock.tryLock()) {
return;
}
List futures = null;
try {
for (;;) {
PendingWrite pw = pendingUnencryptedWrites.poll();
if (pw == null) {
break;
}
if (futures == null) {
futures = new ArrayList();
}
futures.add(pw.future);
}
for (;;) {
MessageEvent ev = pendingEncryptedWrites.poll();
if (ev == null) {
break;
}
if (futures == null) {
futures = new ArrayList();
}
futures.add(ev.getFuture());
}
} finally {
pendingUnencryptedWritesLock.unlock();
}
if (futures != null) {
final ClosedChannelException cause = new ClosedChannelException();
final int size = futures.size();
for (int i = 0; i < size; i ++) {
futures.get(i).setFailure(cause);
}
fireExceptionCaught(ctx, cause);
}
}
});
super.channelClosed(ctx, e);
}
private final class SSLEngineInboundCloseFuture extends DefaultChannelFuture {
SSLEngineInboundCloseFuture() {
super(null, true);
}
void setClosed() {
super.setSuccess();
}
@Override
public Channel getChannel() {
if (ctx == null) {
// Maybe we should better throw an IllegalStateException() ?
return null;
} else {
return ctx.getChannel();
}
}
@Override
public boolean setSuccess() {
return false;
}
@Override
public boolean setFailure(Throwable cause) {
return false;
}
}
}