diff --git a/handler/src/main/java/io/netty/handler/ssl/SslHandler.java b/handler/src/main/java/io/netty/handler/ssl/SslHandler.java index dd74f7f0e9..8138d4cdf6 100644 --- a/handler/src/main/java/io/netty/handler/ssl/SslHandler.java +++ b/handler/src/main/java/io/netty/handler/ssl/SslHandler.java @@ -29,10 +29,15 @@ import io.netty.channel.ChannelPipeline; import io.netty.channel.DefaultChannelFuture; import io.netty.logging.InternalLogger; import io.netty.logging.InternalLoggerFactory; +import io.netty.util.internal.DetectionUtil; import java.io.IOException; +import java.net.DatagramSocket; +import java.net.Socket; import java.nio.ByteBuffer; import java.nio.channels.ClosedChannelException; +import java.nio.channels.DatagramChannel; +import java.nio.channels.SocketChannel; import java.util.ArrayDeque; import java.util.Queue; import java.util.concurrent.Executor; @@ -149,9 +154,8 @@ public class SslHandler private static final InternalLogger logger = InternalLoggerFactory.getInstance(SslHandler.class); - private static final Pattern IGNORABLE_ERROR_MESSAGE = Pattern.compile( - "^.*(?:connection.*reset|connection.*closed|broken.*pipe).*$", - Pattern.CASE_INSENSITIVE); + private static final Pattern IGNORABLE_CLASS_IN_STACK = Pattern.compile( + "^.*(Socket|DatagramChannel|SctpChannel).*$"); private volatile ChannelHandlerContext ctx; private final SSLEngine engine; @@ -438,29 +442,89 @@ public class SslHandler @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { - if (cause instanceof IOException && engine.isOutboundDone()) { - String message = String.valueOf(cause.getMessage()).toLowerCase(); - if (IGNORABLE_ERROR_MESSAGE.matcher(message).matches()) { - // It is safe to ignore the 'connection reset by peer' or - // 'broken pipe' error after sending closure_notify. - if (logger.isDebugEnabled()) { - logger.debug( - "Swallowing a 'connection reset by peer / " + - "broken pipe' error occurred while writing " + - "'closure_notify'", cause); - } - - // Close the connection explicitly just in case the transport - // did not close the connection automatically. - if (ctx.channel().isActive()) { - ctx.close(); - } - return; + if (ignoreException(cause)) { + // It is safe to ignore the 'connection reset by peer' or + // 'broken pipe' error after sending closure_notify. + if (logger.isDebugEnabled()) { + logger.debug( + "Swallowing a 'connection reset by peer / " + + "broken pipe' error occurred while writing " + + "'closure_notify'", cause); } + + // Close the connection explicitly just in case the transport + // did not close the connection automatically. + if (ctx.channel().isActive()) { + ctx.close(); + } + return; + } super.exceptionCaught(ctx, 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 && engine.isOutboundDone()) { + + // 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 (!methodname.equals("read")) { + 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) + || Socket.class.isAssignableFrom(clazz) + || DatagramSocket.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; + } + @Override public void inboundBufferUpdated(final ChannelHandlerContext ctx) throws Exception { final ByteBuf in = ctx.inboundByteBuffer();