io.netty.handler.ssl.SslHandler Maven / Gradle / Ivy
/*
* 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 io.netty.handler.ssl;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.buffer.ByteBufUtil;
import io.netty.buffer.CompositeByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.AbstractCoalescingBufferQueue;
import io.netty.channel.Channel;
import io.netty.channel.ChannelConfig;
import io.netty.channel.ChannelException;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandler;
import io.netty.channel.ChannelOutboundHandler;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.ChannelPromise;
import io.netty.channel.ChannelPromiseNotifier;
import io.netty.handler.codec.ByteToMessageDecoder;
import io.netty.handler.codec.UnsupportedMessageTypeException;
import io.netty.util.ReferenceCountUtil;
import io.netty.util.ReferenceCounted;
import io.netty.util.concurrent.DefaultPromise;
import io.netty.util.concurrent.EventExecutor;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.FutureListener;
import io.netty.util.concurrent.ImmediateExecutor;
import io.netty.util.concurrent.Promise;
import io.netty.util.internal.PlatformDependent;
import io.netty.util.internal.ThrowableUtil;
import io.netty.util.internal.UnstableApi;
import io.netty.util.internal.logging.InternalLogger;
import io.netty.util.internal.logging.InternalLoggerFactory;
import java.io.IOException;
import java.net.SocketAddress;
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.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
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;
import static io.netty.buffer.ByteBufUtil.ensureWritableSuccess;
import static io.netty.handler.ssl.SslUtils.getEncryptedPacketLength;
/**
* 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
*
* Beside using the handshake {@link ChannelFuture} to get notified about the completion of the handshake it's
* also possible to detect it by implement the
* {@link ChannelInboundHandler#userEventTriggered(ChannelHandlerContext, Object)}
* method and check for a {@link SslHandshakeCompletionEvent}.
*
*
Handshake
*
* The handshake will be automatically issued for you once the {@link Channel} is active and
* {@link SSLEngine#getUseClientMode()} returns {@code true}.
* So no need to bother with it by your self.
*
*
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.
*
*
* 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.
*/
public class SslHandler extends ByteToMessageDecoder implements ChannelOutboundHandler {
private static final InternalLogger logger =
InternalLoggerFactory.getInstance(SslHandler.class);
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);
/**
* Used in {@link #unwrapNonAppData(ChannelHandlerContext)} as input for
* {@link #unwrap(ChannelHandlerContext, ByteBuf, int, int)}. Using this static instance reduce object
* creation as {@link Unpooled#EMPTY_BUFFER#nioBuffer()} creates a new {@link ByteBuffer} everytime.
*/
private static final SSLException SSLENGINE_CLOSED = ThrowableUtil.unknownStackTrace(
new SSLException("SSLEngine closed already"), SslHandler.class, "wrap(...)");
private static final SSLException HANDSHAKE_TIMED_OUT = ThrowableUtil.unknownStackTrace(
new SSLException("handshake timed out"), SslHandler.class, "handshake(...)");
private static final ClosedChannelException CHANNEL_CLOSED = ThrowableUtil.unknownStackTrace(
new ClosedChannelException(), SslHandler.class, "channelInactive(...)");
/**
* 2^14 which is the maximum sized plaintext chunk
* allowed by the TLS RFC.
*/
private static final int MAX_PLAINTEXT_LENGTH = 16 * 1024;
private enum SslEngineType {
TCNATIVE(true, COMPOSITE_CUMULATOR) {
@Override
SSLEngineResult unwrap(SslHandler handler, ByteBuf in, int readerIndex, int len, ByteBuf out)
throws SSLException {
int nioBufferCount = in.nioBufferCount();
int writerIndex = out.writerIndex();
final SSLEngineResult result;
if (nioBufferCount > 1) {
/*
* If {@link OpenSslEngine} is in use,
* we can use a special {@link OpenSslEngine#unwrap(ByteBuffer[], ByteBuffer[])} method
* that accepts multiple {@link ByteBuffer}s without additional memory copies.
*/
ReferenceCountedOpenSslEngine opensslEngine = (ReferenceCountedOpenSslEngine) handler.engine;
try {
handler.singleBuffer[0] = toByteBuffer(out, writerIndex,
out.writableBytes());
result = opensslEngine.unwrap(in.nioBuffers(readerIndex, len), handler.singleBuffer);
} finally {
handler.singleBuffer[0] = null;
}
} else {
result = handler.engine.unwrap(toByteBuffer(in, readerIndex, len),
toByteBuffer(out, writerIndex, out.writableBytes()));
}
out.writerIndex(writerIndex + result.bytesProduced());
return result;
}
@Override
int getPacketBufferSize(SslHandler handler) {
return ((ReferenceCountedOpenSslEngine) handler.engine).maxEncryptedPacketLength0();
}
@Override
int calculateWrapBufferCapacity(SslHandler handler, int pendingBytes, int numComponents) {
return ((ReferenceCountedOpenSslEngine) handler.engine).calculateMaxLengthForWrap(pendingBytes,
numComponents);
}
@Override
int calculatePendingData(SslHandler handler, int guess) {
int sslPending = ((ReferenceCountedOpenSslEngine) handler.engine).sslPending();
return sslPending > 0 ? sslPending : guess;
}
@Override
boolean jdkCompatibilityMode(SSLEngine engine) {
return ((ReferenceCountedOpenSslEngine) engine).jdkCompatibilityMode;
}
},
CONSCRYPT(true, COMPOSITE_CUMULATOR) {
@Override
SSLEngineResult unwrap(SslHandler handler, ByteBuf in, int readerIndex, int len, ByteBuf out)
throws SSLException {
int nioBufferCount = in.nioBufferCount();
int writerIndex = out.writerIndex();
final SSLEngineResult result;
if (nioBufferCount > 1) {
/*
* Use a special unwrap method without additional memory copies.
*/
try {
handler.singleBuffer[0] = toByteBuffer(out, writerIndex, out.writableBytes());
result = ((ConscryptAlpnSslEngine) handler.engine).unwrap(
in.nioBuffers(readerIndex, len),
handler.singleBuffer);
} finally {
handler.singleBuffer[0] = null;
}
} else {
result = handler.engine.unwrap(toByteBuffer(in, readerIndex, len),
toByteBuffer(out, writerIndex, out.writableBytes()));
}
out.writerIndex(writerIndex + result.bytesProduced());
return result;
}
@Override
int calculateWrapBufferCapacity(SslHandler handler, int pendingBytes, int numComponents) {
return ((ConscryptAlpnSslEngine) handler.engine).calculateOutNetBufSize(pendingBytes, numComponents);
}
@Override
int calculatePendingData(SslHandler handler, int guess) {
return guess;
}
@Override
boolean jdkCompatibilityMode(SSLEngine engine) {
return true;
}
},
JDK(false, MERGE_CUMULATOR) {
@Override
SSLEngineResult unwrap(SslHandler handler, ByteBuf in, int readerIndex, int len, ByteBuf out)
throws SSLException {
int writerIndex = out.writerIndex();
final SSLEngineResult result = handler.engine.unwrap(toByteBuffer(in, readerIndex, len),
toByteBuffer(out, writerIndex, out.writableBytes()));
out.writerIndex(writerIndex + result.bytesProduced());
return result;
}
@Override
int calculateWrapBufferCapacity(SslHandler handler, int pendingBytes, int numComponents) {
return handler.engine.getSession().getPacketBufferSize();
}
@Override
int calculatePendingData(SslHandler handler, int guess) {
return guess;
}
@Override
boolean jdkCompatibilityMode(SSLEngine engine) {
return true;
}
};
static SslEngineType forEngine(SSLEngine engine) {
return engine instanceof ReferenceCountedOpenSslEngine ? TCNATIVE :
engine instanceof ConscryptAlpnSslEngine ? CONSCRYPT : JDK;
}
SslEngineType(boolean wantsDirectBuffer, Cumulator cumulator) {
this.wantsDirectBuffer = wantsDirectBuffer;
this.cumulator = cumulator;
}
int getPacketBufferSize(SslHandler handler) {
return handler.engine.getSession().getPacketBufferSize();
}
abstract SSLEngineResult unwrap(SslHandler handler, ByteBuf in, int readerIndex, int len, ByteBuf out)
throws SSLException;
abstract int calculateWrapBufferCapacity(SslHandler handler, int pendingBytes, int numComponents);
abstract int calculatePendingData(SslHandler handler, int guess);
abstract boolean jdkCompatibilityMode(SSLEngine engine);
// BEGIN Platform-dependent flags
/**
* {@code true} if and only if {@link SSLEngine} expects a direct buffer.
*/
final boolean wantsDirectBuffer;
// END Platform-dependent flags
/**
* When using JDK {@link SSLEngine}, we use {@link #MERGE_CUMULATOR} because it works only with
* one {@link ByteBuffer}.
*
* When using {@link OpenSslEngine}, we can use {@link #COMPOSITE_CUMULATOR} because it has
* {@link OpenSslEngine#unwrap(ByteBuffer[], ByteBuffer[])} which works with multiple {@link ByteBuffer}s
* and which does not need to do extra memory copies.
*/
final Cumulator cumulator;
}
private volatile ChannelHandlerContext ctx;
private final SSLEngine engine;
private final SslEngineType engineType;
private final Executor delegatedTaskExecutor;
private final boolean jdkCompatibilityMode;
/**
* Used if {@link SSLEngine#wrap(ByteBuffer[], ByteBuffer)} and {@link SSLEngine#unwrap(ByteBuffer, ByteBuffer[])}
* should be called with a {@link ByteBuf} that is only backed by one {@link ByteBuffer} to reduce the object
* creation.
*/
private final ByteBuffer[] singleBuffer = new ByteBuffer[1];
private final boolean startTls;
private boolean sentFirstMessage;
private boolean flushedBeforeHandshake;
private boolean readDuringHandshake;
private boolean handshakeStarted;
private SslHandlerCoalescingBufferQueue pendingUnencryptedWrites;
private Promise handshakePromise = new LazyChannelPromise();
private final LazyChannelPromise sslClosePromise = new LazyChannelPromise();
/**
* Set by wrap*() methods when something is produced.
* {@link #channelReadComplete(ChannelHandlerContext)} will check this flag, clear it, and call ctx.flush().
*/
private boolean needsFlush;
private boolean outboundClosed;
private int packetLength;
/**
* This flag is used to determine if we need to call {@link ChannelHandlerContext#read()} to consume more data
* when {@link ChannelConfig#isAutoRead()} is {@code false}.
*/
private boolean firedChannelRead;
private volatile long handshakeTimeoutMillis = 10000;
private volatile long closeNotifyFlushTimeoutMillis = 3000;
private volatile long closeNotifyReadTimeoutMillis;
volatile int wrapDataSize = MAX_PLAINTEXT_LENGTH;
/**
* Creates a new instance.
*
* @param engine the {@link SSLEngine} this handler will use
*/
public SslHandler(SSLEngine engine) {
this(engine, false);
}
/**
* 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}
*/
@SuppressWarnings("deprecation")
public SslHandler(SSLEngine engine, boolean startTls) {
this(engine, startTls, ImmediateExecutor.INSTANCE);
}
/**
* @deprecated Use {@link #SslHandler(SSLEngine)} instead.
*/
@Deprecated
public SslHandler(SSLEngine engine, Executor delegatedTaskExecutor) {
this(engine, false, delegatedTaskExecutor);
}
/**
* @deprecated Use {@link #SslHandler(SSLEngine, boolean)} instead.
*/
@Deprecated
public SslHandler(SSLEngine engine, boolean startTls, Executor delegatedTaskExecutor) {
if (engine == null) {
throw new NullPointerException("engine");
}
if (delegatedTaskExecutor == null) {
throw new NullPointerException("delegatedTaskExecutor");
}
this.engine = engine;
engineType = SslEngineType.forEngine(engine);
this.delegatedTaskExecutor = delegatedTaskExecutor;
this.startTls = startTls;
this.jdkCompatibilityMode = engineType.jdkCompatibilityMode(engine);
setCumulator(engineType.cumulator);
}
public long getHandshakeTimeoutMillis() {
return handshakeTimeoutMillis;
}
public void setHandshakeTimeout(long handshakeTimeout, TimeUnit unit) {
if (unit == null) {
throw new NullPointerException("unit");
}
setHandshakeTimeoutMillis(unit.toMillis(handshakeTimeout));
}
public void setHandshakeTimeoutMillis(long handshakeTimeoutMillis) {
if (handshakeTimeoutMillis < 0) {
throw new IllegalArgumentException(
"handshakeTimeoutMillis: " + handshakeTimeoutMillis + " (expected: >= 0)");
}
this.handshakeTimeoutMillis = handshakeTimeoutMillis;
}
/**
* Sets the number of bytes to pass to each {@link SSLEngine#wrap(ByteBuffer[], int, int, ByteBuffer)} call.
*
* This value will partition data which is passed to write
* {@link #write(ChannelHandlerContext, Object, ChannelPromise)}. The partitioning will work as follows:
*
* - If {@code wrapDataSize <= 0} then we will write each data chunk as is.
* - If {@code wrapDataSize > data size} then we will attempt to aggregate multiple data chunks together.
* - If {@code wrapDataSize > data size} Else if {@code wrapDataSize <= data size} then we will divide the data
* into chunks of {@code wrapDataSize} when writing.
*
*
* If the {@link SSLEngine} doesn't support a gather wrap operation (e.g. {@link SslProvider#OPENSSL}) then
* aggregating data before wrapping can help reduce the ratio between TLS overhead vs data payload which will lead
* to better goodput. Writing fixed chunks of data can also help target the underlying transport's (e.g. TCP)
* frame size. Under lossy/congested network conditions this may help the peer get full TLS packets earlier and
* be able to do work sooner, as opposed to waiting for the all the pieces of the TLS packet to arrive.
* @param wrapDataSize the number of bytes which will be passed to each
* {@link SSLEngine#wrap(ByteBuffer[], int, int, ByteBuffer)} call.
*/
@UnstableApi
public final void setWrapDataSize(int wrapDataSize) {
this.wrapDataSize = wrapDataSize;
}
/**
* @deprecated use {@link #getCloseNotifyFlushTimeoutMillis()}
*/
@Deprecated
public long getCloseNotifyTimeoutMillis() {
return getCloseNotifyFlushTimeoutMillis();
}
/**
* @deprecated use {@link #setCloseNotifyFlushTimeout(long, TimeUnit)}
*/
@Deprecated
public void setCloseNotifyTimeout(long closeNotifyTimeout, TimeUnit unit) {
setCloseNotifyFlushTimeout(closeNotifyTimeout, unit);
}
/**
* @deprecated use {@link #setCloseNotifyFlushTimeoutMillis(long)}
*/
@Deprecated
public void setCloseNotifyTimeoutMillis(long closeNotifyFlushTimeoutMillis) {
setCloseNotifyFlushTimeoutMillis(closeNotifyFlushTimeoutMillis);
}
/**
* Gets the timeout for flushing the close_notify that was triggered by closing the
* {@link Channel}. If the close_notify was not flushed in the given timeout the {@link Channel} will be closed
* forcibly.
*/
public final long getCloseNotifyFlushTimeoutMillis() {
return closeNotifyFlushTimeoutMillis;
}
/**
* Sets the timeout for flushing the close_notify that was triggered by closing the
* {@link Channel}. If the close_notify was not flushed in the given timeout the {@link Channel} will be closed
* forcibly.
*/
public final void setCloseNotifyFlushTimeout(long closeNotifyFlushTimeout, TimeUnit unit) {
setCloseNotifyFlushTimeoutMillis(unit.toMillis(closeNotifyFlushTimeout));
}
/**
* See {@link #setCloseNotifyFlushTimeout(long, TimeUnit)}.
*/
public final void setCloseNotifyFlushTimeoutMillis(long closeNotifyFlushTimeoutMillis) {
if (closeNotifyFlushTimeoutMillis < 0) {
throw new IllegalArgumentException(
"closeNotifyFlushTimeoutMillis: " + closeNotifyFlushTimeoutMillis + " (expected: >= 0)");
}
this.closeNotifyFlushTimeoutMillis = closeNotifyFlushTimeoutMillis;
}
/**
* Gets the timeout (in ms) for receiving the response for the close_notify that was triggered by closing the
* {@link Channel}. This timeout starts after the close_notify message was successfully written to the
* remote peer. Use {@code 0} to directly close the {@link Channel} and not wait for the response.
*/
public final long getCloseNotifyReadTimeoutMillis() {
return closeNotifyReadTimeoutMillis;
}
/**
* Sets the timeout for receiving the response for the close_notify that was triggered by closing the
* {@link Channel}. This timeout starts after the close_notify message was successfully written to the
* remote peer. Use {@code 0} to directly close the {@link Channel} and not wait for the response.
*/
public final void setCloseNotifyReadTimeout(long closeNotifyReadTimeout, TimeUnit unit) {
setCloseNotifyReadTimeoutMillis(unit.toMillis(closeNotifyReadTimeout));
}
/**
* See {@link #setCloseNotifyReadTimeout(long, TimeUnit)}.
*/
public final void setCloseNotifyReadTimeoutMillis(long closeNotifyReadTimeoutMillis) {
if (closeNotifyReadTimeoutMillis < 0) {
throw new IllegalArgumentException(
"closeNotifyReadTimeoutMillis: " + closeNotifyReadTimeoutMillis + " (expected: >= 0)");
}
this.closeNotifyReadTimeoutMillis = closeNotifyReadTimeoutMillis;
}
/**
* Returns the {@link SSLEngine} which is used by this handler.
*/
public SSLEngine engine() {
return engine;
}
/**
* Returns the name of the current application-level protocol.
*
* @return the protocol name or {@code null} if application-level protocol has not been negotiated
*/
public String applicationProtocol() {
SSLEngine engine = engine();
if (!(engine instanceof ApplicationProtocolAccessor)) {
return null;
}
return ((ApplicationProtocolAccessor) engine).getNegotiatedApplicationProtocol();
}
/**
* Returns a {@link Future} that will get notified once the current TLS handshake completes.
*
* @return the {@link Future} for the initial TLS handshake if {@link #renegotiate()} was not invoked.
* The {@link Future} for the most recent {@linkplain #renegotiate() TLS renegotiation} otherwise.
*/
public Future handshakeFuture() {
return handshakePromise;
}
/**
* Sends an SSL {@code close_notify} message to the specified channel and
* destroys the underlying {@link SSLEngine}.
*
* @deprecated use {@link Channel#close()} or {@link ChannelHandlerContext#close()}
*/
@Deprecated
public ChannelFuture close() {
return close(ctx.newPromise());
}
/**
* See {@link #close()}
*
* @deprecated use {@link Channel#close()} or {@link ChannelHandlerContext#close()}
*/
@Deprecated
public ChannelFuture close(final ChannelPromise promise) {
final ChannelHandlerContext ctx = this.ctx;
ctx.executor().execute(new Runnable() {
@Override
public void run() {
outboundClosed = true;
engine.closeOutbound();
try {
flush(ctx, promise);
} catch (Exception e) {
if (!promise.tryFailure(e)) {
logger.warn("{} flush() raised a masked exception.", ctx.channel(), e);
}
}
}
});
return promise;
}
/**
* Return the {@link Future} that will get notified if the inbound of the {@link SSLEngine} is closed.
*
* This method will return the same {@link Future} all the time.
*
* @see SSLEngine
*/
public Future sslCloseFuture() {
return sslClosePromise;
}
@Override
public void handlerRemoved0(ChannelHandlerContext ctx) throws Exception {
if (!pendingUnencryptedWrites.isEmpty()) {
// Check if queue is not empty first because create a new ChannelException is expensive
pendingUnencryptedWrites.releaseAndFailAll(ctx,
new ChannelException("Pending write on removal of SslHandler"));
}
pendingUnencryptedWrites = null;
if (engine instanceof ReferenceCounted) {
((ReferenceCounted) engine).release();
}
}
@Override
public void bind(ChannelHandlerContext ctx, SocketAddress localAddress, ChannelPromise promise) throws Exception {
ctx.bind(localAddress, promise);
}
@Override
public void connect(ChannelHandlerContext ctx, SocketAddress remoteAddress, SocketAddress localAddress,
ChannelPromise promise) throws Exception {
ctx.connect(remoteAddress, localAddress, promise);
}
@Override
public void deregister(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
ctx.deregister(promise);
}
@Override
public void disconnect(final ChannelHandlerContext ctx,
final ChannelPromise promise) throws Exception {
closeOutboundAndChannel(ctx, promise, true);
}
@Override
public void close(final ChannelHandlerContext ctx,
final ChannelPromise promise) throws Exception {
closeOutboundAndChannel(ctx, promise, false);
}
@Override
public void read(ChannelHandlerContext ctx) throws Exception {
if (!handshakePromise.isDone()) {
readDuringHandshake = true;
}
ctx.read();
}
private static IllegalStateException newPendingWritesNullException() {
return new IllegalStateException("pendingUnencryptedWrites is null, handlerRemoved0 called?");
}
@Override
public void write(final ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
if (!(msg instanceof ByteBuf)) {
UnsupportedMessageTypeException exception = new UnsupportedMessageTypeException(msg, ByteBuf.class);
ReferenceCountUtil.safeRelease(msg);
promise.setFailure(exception);
} else if (pendingUnencryptedWrites == null) {
ReferenceCountUtil.safeRelease(msg);
promise.setFailure(newPendingWritesNullException());
} else {
pendingUnencryptedWrites.add((ByteBuf) msg, promise);
}
}
@Override
public void flush(ChannelHandlerContext ctx) throws Exception {
// Do not encrypt the first write request if this handler is
// created with startTLS flag turned on.
if (startTls && !sentFirstMessage) {
sentFirstMessage = true;
pendingUnencryptedWrites.writeAndRemoveAll(ctx);
forceFlush(ctx);
return;
}
try {
wrapAndFlush(ctx);
} catch (Throwable cause) {
setHandshakeFailure(ctx, cause);
PlatformDependent.throwException(cause);
}
}
private void wrapAndFlush(ChannelHandlerContext ctx) throws SSLException {
if (pendingUnencryptedWrites.isEmpty()) {
// It's important to NOT use a voidPromise here as the user
// may want to add a ChannelFutureListener to the ChannelPromise later.
//
// See https://github.com/netty/netty/issues/3364
pendingUnencryptedWrites.add(Unpooled.EMPTY_BUFFER, ctx.newPromise());
}
if (!handshakePromise.isDone()) {
flushedBeforeHandshake = true;
}
try {
wrap(ctx, false);
} finally {
// We may have written some parts of data before an exception was thrown so ensure we always flush.
// See https://github.com/netty/netty/issues/3900#issuecomment-172481830
forceFlush(ctx);
}
}
// This method will not call setHandshakeFailure(...) !
private void wrap(ChannelHandlerContext ctx, boolean inUnwrap) throws SSLException {
ByteBuf out = null;
ChannelPromise promise = null;
ByteBufAllocator alloc = ctx.alloc();
boolean needUnwrap = false;
ByteBuf buf = null;
try {
final int wrapDataSize = this.wrapDataSize;
// Only continue to loop if the handler was not removed in the meantime.
// See https://github.com/netty/netty/issues/5860
while (!ctx.isRemoved()) {
promise = ctx.newPromise();
buf = wrapDataSize > 0 ?
pendingUnencryptedWrites.remove(alloc, wrapDataSize, promise) :
pendingUnencryptedWrites.removeFirst(promise);
if (buf == null) {
break;
}
if (out == null) {
out = allocateOutNetBuf(ctx, buf.readableBytes(), buf.nioBufferCount());
}
SSLEngineResult result = wrap(alloc, engine, buf, out);
if (result.getStatus() == Status.CLOSED) {
buf.release();
buf = null;
promise.tryFailure(SSLENGINE_CLOSED);
promise = null;
// SSLEngine has been closed already.
// Any further write attempts should be denied.
pendingUnencryptedWrites.releaseAndFailAll(ctx, SSLENGINE_CLOSED);
return;
} else {
if (buf.isReadable()) {
pendingUnencryptedWrites.addFirst(buf, promise);
// When we add the buffer/promise pair back we need to be sure we don't complete the promise
// later in finishWrap. We only complete the promise if the buffer is completely consumed.
promise = null;
} else {
buf.release();
}
buf = null;
switch (result.getHandshakeStatus()) {
case NEED_TASK:
runDelegatedTasks();
break;
case FINISHED:
setHandshakeSuccess();
// deliberate fall-through
case NOT_HANDSHAKING:
setHandshakeSuccessIfStillHandshaking();
// deliberate fall-through
case NEED_WRAP:
finishWrap(ctx, out, promise, inUnwrap, false);
promise = null;
out = null;
break;
case NEED_UNWRAP:
needUnwrap = true;
return;
default:
throw new IllegalStateException(
"Unknown handshake status: " + result.getHandshakeStatus());
}
}
}
} finally {
// Ownership of buffer was not transferred, release it.
if (buf != null) {
buf.release();
}
finishWrap(ctx, out, promise, inUnwrap, needUnwrap);
}
}
private void finishWrap(ChannelHandlerContext ctx, ByteBuf out, ChannelPromise promise, boolean inUnwrap,
boolean needUnwrap) {
if (out == null) {
out = Unpooled.EMPTY_BUFFER;
} else if (!out.isReadable()) {
out.release();
out = Unpooled.EMPTY_BUFFER;
}
if (promise != null) {
ctx.write(out, promise);
} else {
ctx.write(out);
}
if (inUnwrap) {
needsFlush = true;
}
if (needUnwrap) {
// The underlying engine is starving so we need to feed it with more data.
// See https://github.com/netty/netty/pull/5039
readIfNeeded(ctx);
}
}
/**
* This method will not call
* {@link #setHandshakeFailure(ChannelHandlerContext, Throwable, boolean, boolean)} or
* {@link #setHandshakeFailure(ChannelHandlerContext, Throwable)}.
* @return {@code true} if this method ends on {@link SSLEngineResult.HandshakeStatus#NOT_HANDSHAKING}.
*/
private boolean wrapNonAppData(ChannelHandlerContext ctx, boolean inUnwrap) throws SSLException {
ByteBuf out = null;
ByteBufAllocator alloc = ctx.alloc();
try {
// Only continue to loop if the handler was not removed in the meantime.
// See https://github.com/netty/netty/issues/5860
while (!ctx.isRemoved()) {
if (out == null) {
// As this is called for the handshake we have no real idea how big the buffer needs to be.
// That said 2048 should give us enough room to include everything like ALPN / NPN data.
// If this is not enough we will increase the buffer in wrap(...).
out = allocateOutNetBuf(ctx, 2048, 1);
}
SSLEngineResult result = wrap(alloc, engine, Unpooled.EMPTY_BUFFER, out);
if (result.bytesProduced() > 0) {
ctx.write(out);
if (inUnwrap) {
needsFlush = true;
}
out = null;
}
switch (result.getHandshakeStatus()) {
case FINISHED:
setHandshakeSuccess();
return false;
case NEED_TASK:
runDelegatedTasks();
break;
case NEED_UNWRAP:
if (inUnwrap) {
// If we asked for a wrap, the engine requested an unwrap, and we are in unwrap there is
// no use in trying to call wrap again because we have already attempted (or will after we
// return) to feed more data to the engine.
return false;
}
unwrapNonAppData(ctx);
break;
case NEED_WRAP:
break;
case NOT_HANDSHAKING:
setHandshakeSuccessIfStillHandshaking();
// Workaround for TLS False Start problem reported at:
// https://github.com/netty/netty/issues/1108#issuecomment-14266970
if (!inUnwrap) {
unwrapNonAppData(ctx);
}
return true;
default:
throw new IllegalStateException("Unknown handshake status: " + result.getHandshakeStatus());
}
if (result.bytesProduced() == 0) {
break;
}
// It should not consume empty buffers when it is not handshaking
// Fix for Android, where it was encrypting empty buffers even when not handshaking
if (result.bytesConsumed() == 0 && result.getHandshakeStatus() == HandshakeStatus.NOT_HANDSHAKING) {
break;
}
}
} finally {
if (out != null) {
out.release();
}
}
return false;
}
private SSLEngineResult wrap(ByteBufAllocator alloc, SSLEngine engine, ByteBuf in, ByteBuf out)
throws SSLException {
ByteBuf newDirectIn = null;
try {
int readerIndex = in.readerIndex();
int readableBytes = in.readableBytes();
// We will call SslEngine.wrap(ByteBuffer[], ByteBuffer) to allow efficient handling of
// CompositeByteBuf without force an extra memory copy when CompositeByteBuffer.nioBuffer() is called.
final ByteBuffer[] in0;
if (in.isDirect() || !engineType.wantsDirectBuffer) {
// As CompositeByteBuf.nioBufferCount() can be expensive (as it needs to check all composed ByteBuf
// to calculate the count) we will just assume a CompositeByteBuf contains more then 1 ByteBuf.
// The worst that can happen is that we allocate an extra ByteBuffer[] in CompositeByteBuf.nioBuffers()
// which is better then walking the composed ByteBuf in most cases.
if (!(in instanceof CompositeByteBuf) && in.nioBufferCount() == 1) {
in0 = singleBuffer;
// We know its only backed by 1 ByteBuffer so use internalNioBuffer to keep object allocation
// to a minimum.
in0[0] = in.internalNioBuffer(readerIndex, readableBytes);
} else {
in0 = in.nioBuffers();
}
} else {
// We could even go further here and check if its a CompositeByteBuf and if so try to decompose it and
// only replace the ByteBuffer that are not direct. At the moment we just will replace the whole
// CompositeByteBuf to keep the complexity to a minimum
newDirectIn = alloc.directBuffer(readableBytes);
newDirectIn.writeBytes(in, readerIndex, readableBytes);
in0 = singleBuffer;
in0[0] = newDirectIn.internalNioBuffer(newDirectIn.readerIndex(), readableBytes);
}
for (;;) {
ByteBuffer out0 = out.nioBuffer(out.writerIndex(), out.writableBytes());
SSLEngineResult result = engine.wrap(in0, out0);
in.skipBytes(result.bytesConsumed());
out.writerIndex(out.writerIndex() + result.bytesProduced());
switch (result.getStatus()) {
case BUFFER_OVERFLOW:
out.ensureWritable(engine.getSession().getPacketBufferSize());
break;
default:
return result;
}
}
} finally {
// Null out to allow GC of ByteBuffer
singleBuffer[0] = null;
if (newDirectIn != null) {
newDirectIn.release();
}
}
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
// Make sure to release SSLEngine,
// and notify the handshake future if the connection has been closed during handshake.
setHandshakeFailure(ctx, CHANNEL_CLOSED, !outboundClosed, handshakeStarted);
// Ensure we always notify the sslClosePromise as well
notifyClosePromise(CHANNEL_CLOSED);
super.channelInactive(ctx);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
if (ignoreException(cause)) {
// It is safe to ignore the 'connection reset by peer' or
// 'broken pipe' error after sending close_notify.
if (logger.isDebugEnabled()) {
logger.debug(
"{} Swallowing a harmless 'connection reset by peer / broken pipe' error that occurred " +
"while writing close_notify in response to the peer's close_notify", ctx.channel(), cause);
}
// Close the connection explicitly just in case the transport
// did not close the connection automatically.
if (ctx.channel().isActive()) {
ctx.close();
}
} else {
ctx.fireExceptionCaught(cause);
}
}
/**
* 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 && sslClosePromise.isDone()) {
String message = t.getMessage();
// 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 (message != null && 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("io.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 = PlatformDependent.getClassLoader(getClass()).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 (PlatformDependent.javaVersion() >= 7
&& "com.sun.nio.sctp.SctpChannel".equals(clazz.getSuperclass().getName())) {
return true;
}
} catch (Throwable cause) {
logger.debug("Unexpected exception while loading class {} classname {}",
getClass(), classname, cause);
}
}
}
return false;
}
/**
* Returns {@code true} if the given {@link ByteBuf} is encrypted. Be aware that this method
* will not increase the readerIndex of the given {@link ByteBuf}.
*
* @param buffer
* The {@link ByteBuf} 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 ByteBuf} is encrypted, {@code false} otherwise.
* @throws IllegalArgumentException
* Is thrown if the given {@link ByteBuf} has not at least 5 bytes to read.
*/
public static boolean isEncrypted(ByteBuf buffer) {
if (buffer.readableBytes() < SslUtils.SSL_RECORD_HEADER_LENGTH) {
throw new IllegalArgumentException(
"buffer must have at least " + SslUtils.SSL_RECORD_HEADER_LENGTH + " readable bytes");
}
return getEncryptedPacketLength(buffer, buffer.readerIndex()) != SslUtils.NOT_ENCRYPTED;
}
private void decodeJdkCompatible(ChannelHandlerContext ctx, ByteBuf in) throws NotSslRecordException {
int packetLength = this.packetLength;
// If we calculated the length of the current SSL record before, use that information.
if (packetLength > 0) {
if (in.readableBytes() < packetLength) {
return;
}
} else {
// Get the packet length and wait until we get a packets worth of data to unwrap.
final int readableBytes = in.readableBytes();
if (readableBytes < SslUtils.SSL_RECORD_HEADER_LENGTH) {
return;
}
packetLength = getEncryptedPacketLength(in, in.readerIndex());
if (packetLength == SslUtils.NOT_ENCRYPTED) {
// Not an SSL/TLS packet
NotSslRecordException e = new NotSslRecordException(
"not an SSL/TLS record: " + ByteBufUtil.hexDump(in));
in.skipBytes(in.readableBytes());
// First fail the handshake promise as we may need to have access to the SSLEngine which may
// be released because the user will remove the SslHandler in an exceptionCaught(...) implementation.
setHandshakeFailure(ctx, e);
throw e;
}
assert packetLength > 0;
if (packetLength > readableBytes) {
// wait until the whole packet can be read
this.packetLength = packetLength;
return;
}
}
// Reset the state of this class so we can get the length of the next packet. We assume the entire packet will
// be consumed by the SSLEngine.
this.packetLength = 0;
try {
int bytesConsumed = unwrap(ctx, in, in.readerIndex(), packetLength);
assert bytesConsumed == packetLength || engine.isInboundDone() :
"we feed the SSLEngine a packets worth of data: " + packetLength + " but it only consumed: " +
bytesConsumed;
in.skipBytes(bytesConsumed);
} catch (Throwable cause) {
handleUnwrapThrowable(ctx, cause);
}
}
private void decodeNonJdkCompatible(ChannelHandlerContext ctx, ByteBuf in) {
try {
in.skipBytes(unwrap(ctx, in, in.readerIndex(), in.readableBytes()));
} catch (Throwable cause) {
handleUnwrapThrowable(ctx, cause);
}
}
private void handleUnwrapThrowable(ChannelHandlerContext ctx, Throwable cause) {
try {
// We need to flush one time as there may be an alert that we should send to the remote peer because
// of the SSLException reported here.
wrapAndFlush(ctx);
} catch (SSLException ex) {
logger.debug("SSLException during trying to call SSLEngine.wrap(...)" +
" because of an previous SSLException, ignoring...", ex);
} finally {
setHandshakeFailure(ctx, cause);
}
PlatformDependent.throwException(cause);
}
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List