From 3879298f0f7938135135fb6769f26a21a05feeeb Mon Sep 17 00:00:00 2001 From: Apertis CI <devel@lists.apertis.org> Date: Sat, 6 Mar 2021 05:22:14 +0000 Subject: [PATCH] Import Upstream version 4.1.48 --- .github/CONTRIBUTING.md | 2 +- .gitignore | 8 + .lgtm.yml | 13 + CONTRIBUTING.md | 2 +- NOTICE.txt | 31 +- README.md | 10 +- all/pom.xml | 83 +- bom/pom.xml | 76 +- buffer/pom.xml | 2 +- .../java/io/netty/buffer/AbstractByteBuf.java | 183 +- .../buffer/AbstractByteBufAllocator.java | 16 +- .../netty/buffer/AbstractDerivedByteBuf.java | 10 + .../buffer/AbstractPooledDerivedByteBuf.java | 9 +- .../AbstractReferenceCountedByteBuf.java | 143 +- .../AdvancedLeakAwareCompositeByteBuf.java | 6 + .../main/java/io/netty/buffer/ByteBuf.java | 40 +- .../io/netty/buffer/ByteBufInputStream.java | 21 +- .../io/netty/buffer/ByteBufOutputStream.java | 6 +- .../java/io/netty/buffer/ByteBufUtil.java | 131 +- .../io/netty/buffer/CompositeByteBuf.java | 419 +- .../io/netty/buffer/DefaultByteBufHolder.java | 21 +- .../java/io/netty/buffer/EmptyByteBuf.java | 37 +- .../netty/buffer/FixedCompositeByteBuf.java | 2 +- .../main/java/io/netty/buffer/PoolArena.java | 81 +- .../java/io/netty/buffer/PoolSubpage.java | 43 +- .../java/io/netty/buffer/PoolThreadCache.java | 63 +- .../java/io/netty/buffer/PooledByteBuf.java | 136 +- .../netty/buffer/PooledByteBufAllocator.java | 74 +- .../io/netty/buffer/PooledDirectByteBuf.java | 174 +- .../netty/buffer/PooledDuplicatedByteBuf.java | 12 +- .../io/netty/buffer/PooledHeapByteBuf.java | 115 +- .../io/netty/buffer/PooledSlicedByteBuf.java | 12 +- .../buffer/PooledUnsafeDirectByteBuf.java | 133 +- .../netty/buffer/PooledUnsafeHeapByteBuf.java | 12 +- .../netty/buffer/ReadOnlyByteBufferBuf.java | 19 +- .../buffer/ReadOnlyUnsafeDirectByteBuf.java | 23 +- .../java/io/netty/buffer/SwappedByteBuf.java | 26 +- .../main/java/io/netty/buffer/Unpooled.java | 17 +- .../netty/buffer/UnpooledDirectByteBuf.java | 107 +- .../io/netty/buffer/UnpooledHeapByteBuf.java | 50 +- .../buffer/UnpooledUnsafeDirectByteBuf.java | 360 +- .../buffer/UnpooledUnsafeHeapByteBuf.java | 9 +- .../UnpooledUnsafeNoCleanerDirectByteBuf.java | 14 +- .../io/netty/buffer/UnreleasableByteBuf.java | 7 +- .../java/io/netty/buffer/WrappedByteBuf.java | 21 +- .../netty/buffer/WrappedCompositeByteBuf.java | 15 +- .../io.netty/buffer/native-image.properties | 15 + .../io/netty/buffer/AbstractByteBufTest.java | 48 +- .../buffer/AbstractCompositeByteBufTest.java | 249 +- .../buffer/AbstractPooledByteBufTest.java | 66 + .../buffer/AdvancedLeakAwareByteBufTest.java | 23 + .../buffer/BigEndianDirectByteBufTest.java | 9 + .../netty/buffer/ByteBufDerivationTest.java | 1 - .../io/netty/buffer/ByteBufStreamTest.java | 103 +- .../java/io/netty/buffer/ByteBufUtilTest.java | 105 + .../buffer/DefaultByteBufHolderTest.java | 60 + .../netty/buffer/DuplicatedByteBufTest.java | 7 + .../io/netty/buffer/EmptyByteBufTest.java | 14 + .../buffer/FixedCompositeByteBufTest.java | 4 +- .../java/io/netty/buffer/PoolArenaTest.java | 27 +- .../buffer/PooledByteBufAllocatorTest.java | 55 +- .../ReadOnlyDirectByteBufferBufTest.java | 28 +- .../SimpleLeakAwareCompositeByteBufTest.java | 24 + .../io/netty/buffer/SlicedByteBufTest.java | 7 + codec-dns/pom.xml | 2 +- .../handler/codec/dns/AbstractDnsRecord.java | 19 +- .../codec/dns/DatagramDnsQueryEncoder.java | 44 +- .../codec/dns/DatagramDnsResponseDecoder.java | 79 +- .../codec/dns/DefaultDnsRecordDecoder.java | 74 +- .../codec/dns/DefaultDnsRecordEncoder.java | 23 +- .../netty/handler/codec/dns/DnsCodecUtil.java | 132 + .../handler/codec/dns/DnsQueryEncoder.java | 75 + .../handler/codec/dns/DnsResponseDecoder.java | 105 + .../handler/codec/dns/TcpDnsQueryEncoder.java | 64 + .../codec/dns/TcpDnsResponseDecoder.java | 72 + .../dns/DefaultDnsRecordDecoderTest.java | 98 +- codec-haproxy/pom.xml | 2 +- .../handler/codec/haproxy/HAProxyMessage.java | 159 +- .../codec/haproxy/HAProxyMessageDecoder.java | 231 +- .../handler/codec/haproxy/HAProxyTLV.java | 4 +- .../haproxy/HAProxyMessageDecoderTest.java | 100 +- codec-http/pom.xml | 3 +- .../codec/http/CombinedHttpHeaders.java | 2 +- .../handler/codec/http/DefaultCookie.java | 6 +- .../codec/http/DefaultHttpContent.java | 6 +- .../codec/http/DefaultHttpHeaders.java | 15 +- .../codec/http/DefaultHttpMessage.java | 7 +- .../handler/codec/http/DefaultHttpObject.java | 6 +- .../codec/http/DefaultHttpRequest.java | 12 +- .../codec/http/DefaultHttpResponse.java | 7 +- .../handler/codec/http/HttpClientCodec.java | 94 +- .../codec/http/HttpClientUpgradeHandler.java | 11 +- .../codec/http/HttpContentCompressor.java | 8 +- .../codec/http/HttpContentDecoder.java | 173 +- .../codec/http/HttpContentEncoder.java | 52 +- .../netty/handler/codec/http/HttpHeaders.java | 5 +- .../netty/handler/codec/http/HttpMessage.java | 2 +- .../netty/handler/codec/http/HttpMethod.java | 6 + .../codec/http/HttpObjectAggregator.java | 7 - .../handler/codec/http/HttpObjectDecoder.java | 297 +- .../codec/http/HttpResponseStatus.java | 14 +- .../handler/codec/http/HttpServerCodec.java | 11 +- .../codec/http/HttpServerUpgradeHandler.java | 21 +- .../io/netty/handler/codec/http/HttpUtil.java | 44 +- .../netty/handler/codec/http/HttpVersion.java | 23 +- .../codec/http/QueryStringDecoder.java | 76 +- .../codec/http/QueryStringEncoder.java | 178 +- .../http/cookie/ClientCookieDecoder.java | 9 +- .../http/cookie/ClientCookieEncoder.java | 12 +- .../codec/http/cookie/CookieHeaderNames.java | 29 + .../handler/codec/http/cookie/CookieUtil.java | 26 +- .../codec/http/cookie/DefaultCookie.java | 29 +- .../http/cookie/ServerCookieDecoder.java | 32 +- .../http/cookie/ServerCookieEncoder.java | 24 +- .../handler/codec/http/cors/CorsConfig.java | 2 +- .../codec/http/cors/CorsConfigBuilder.java | 2 +- .../handler/codec/http/cors/CorsHandler.java | 5 +- .../http/multipart/AbstractDiskHttpData.java | 72 +- .../http/multipart/AbstractHttpData.java | 14 +- .../multipart/AbstractMemoryHttpData.java | 41 +- .../codec/http/multipart/DiskAttribute.java | 5 +- .../codec/http/multipart/DiskFileUpload.java | 11 +- .../HttpPostMultipartRequestDecoder.java | 12 +- .../multipart/HttpPostRequestDecoder.java | 22 +- .../multipart/HttpPostRequestEncoder.java | 15 +- .../HttpPostStandardRequestDecoder.java | 24 +- .../http/multipart/InternalAttribute.java | 13 +- .../codec/http/multipart/MemoryAttribute.java | 5 +- .../http/multipart/MemoryFileUpload.java | 11 +- .../http/websocketx/BinaryWebSocketFrame.java | 2 +- .../http/websocketx/CloseWebSocketFrame.java | 41 +- .../ContinuationWebSocketFrame.java | 8 +- .../CorruptedWebSocketFrameException.java | 64 + .../http/websocketx/PingWebSocketFrame.java | 4 +- .../http/websocketx/PongWebSocketFrame.java | 2 +- .../http/websocketx/TextWebSocketFrame.java | 10 +- .../http/websocketx/Utf8FrameValidator.java | 65 +- .../codec/http/websocketx/Utf8Validator.java | 9 +- .../websocketx/WebSocket00FrameDecoder.java | 16 +- .../websocketx/WebSocket07FrameDecoder.java | 25 +- .../websocketx/WebSocket08FrameDecoder.java | 484 +- .../websocketx/WebSocket08FrameEncoder.java | 4 +- .../websocketx/WebSocket13FrameDecoder.java | 19 +- .../websocketx/WebSocketClientHandshaker.java | 187 +- .../WebSocketClientHandshaker00.java | 73 +- .../WebSocketClientHandshaker07.java | 106 +- .../WebSocketClientHandshaker08.java | 111 +- .../WebSocketClientHandshaker13.java | 112 +- .../WebSocketClientHandshakerFactory.java | 101 +- .../WebSocketClientProtocolConfig.java | 392 ++ .../WebSocketClientProtocolHandler.java | 202 +- ...bSocketClientProtocolHandshakeHandler.java | 66 + .../http/websocketx/WebSocketCloseStatus.java | 314 ++ .../websocketx/WebSocketDecoderConfig.java | 165 + .../codec/http/websocketx/WebSocketFrame.java | 2 +- .../websocketx/WebSocketProtocolHandler.java | 119 +- .../websocketx/WebSocketServerHandshaker.java | 59 +- .../WebSocketServerHandshaker00.java | 38 +- .../WebSocketServerHandshaker07.java | 36 +- .../WebSocketServerHandshaker08.java | 43 +- .../WebSocketServerHandshaker13.java | 49 +- .../WebSocketServerHandshakerFactory.java | 43 +- .../WebSocketServerProtocolConfig.java | 296 + .../WebSocketServerProtocolHandler.java | 148 +- ...bSocketServerProtocolHandshakeHandler.java | 93 +- .../codec/http/websocketx/WebSocketUtil.java | 5 + .../http/websocketx/WebSocketVersion.java | 38 +- .../WebSocketClientExtensionHandler.java | 5 +- .../extensions/WebSocketExtensionData.java | 13 +- .../extensions/WebSocketExtensionFilter.java | 54 + .../WebSocketExtensionFilterProvider.java | 45 + .../WebSocketServerExtensionHandler.java | 70 +- .../compression/DeflateDecoder.java | 107 +- .../compression/DeflateEncoder.java | 85 +- ...DeflateFrameClientExtensionHandshaker.java | 32 +- ...DeflateFrameServerExtensionHandshaker.java | 30 +- .../compression/PerFrameDeflateDecoder.java | 31 +- .../compression/PerFrameDeflateEncoder.java | 39 +- ...ssageDeflateClientExtensionHandshaker.java | 51 +- .../compression/PerMessageDeflateDecoder.java | 40 +- .../compression/PerMessageDeflateEncoder.java | 42 +- ...ssageDeflateServerExtensionHandshaker.java | 50 +- .../codec/http/websocketx/package-info.java | 10 +- .../netty/handler/codec/rtsp/RtspMethods.java | 27 +- .../handler/codec/rtsp/RtspVersions.java | 5 +- .../codec/spdy/DefaultSpdyDataFrame.java | 7 +- .../codec/spdy/DefaultSpdyGoAwayFrame.java | 7 +- .../codec/spdy/DefaultSpdyStreamFrame.java | 7 +- .../codec/spdy/DefaultSpdySynReplyFrame.java | 3 +- .../codec/spdy/DefaultSpdySynStreamFrame.java | 8 +- .../spdy/DefaultSpdyWindowUpdateFrame.java | 14 +- .../handler/codec/spdy/SpdyCodecUtil.java | 9 +- .../handler/codec/spdy/SpdyFrameDecoder.java | 18 +- .../handler/codec/spdy/SpdyFrameEncoder.java | 6 +- .../codec/spdy/SpdyHeaderBlockRawDecoder.java | 15 +- .../codec/spdy/SpdyHeaderBlockRawEncoder.java | 6 +- .../spdy/SpdyHeaderBlockZlibEncoder.java | 10 +- .../handler/codec/spdy/SpdyHttpDecoder.java | 17 +- .../handler/codec/spdy/SpdyHttpEncoder.java | 5 +- .../spdy/SpdyHttpResponseStreamIdHandler.java | 4 +- .../codec/spdy/SpdyProtocolException.java | 17 + .../codec/spdy/SpdySessionHandler.java | 29 +- .../handler/codec/spdy/SpdySessionStatus.java | 8 +- .../handler/codec/spdy/SpdyStreamStatus.java | 8 +- .../codec-http/native-image.properties | 16 + .../codec/http/CombinedHttpHeadersTest.java | 16 +- .../codec/http/HttpClientCodecTest.java | 87 + .../codec/http/HttpContentCompressorTest.java | 160 +- .../codec/http/HttpContentDecoderTest.java | 10 +- .../http/HttpContentDecompressorTest.java | 70 + .../codec/http/HttpContentEncoderTest.java | 4 +- .../codec/http/HttpObjectAggregatorTest.java | 214 +- .../codec/http/HttpRequestDecoderTest.java | 174 +- .../codec/http/HttpResponseDecoderTest.java | 76 +- .../handler/codec/http/HttpUtilTest.java | 31 + .../codec/http/QueryStringDecoderTest.java | 17 +- .../codec/http/QueryStringEncoderTest.java | 11 +- .../http/cookie/ClientCookieDecoderTest.java | 10 +- .../http/cookie/ClientCookieEncoderTest.java | 12 + .../http/cookie/ServerCookieDecoderTest.java | 21 + .../http/cookie/ServerCookieEncoderTest.java | 8 +- .../codec/http/cors/CorsHandlerTest.java | 31 +- .../multipart/HttpPostRequestDecoderTest.java | 147 + .../multipart/HttpPostRequestEncoderTest.java | 38 +- .../WebSocket08EncoderDecoderTest.java | 102 +- .../WebSocketClientHandshaker00Test.java | 9 +- .../WebSocketClientHandshaker07Test.java | 31 +- .../WebSocketClientHandshaker08Test.java | 7 +- .../WebSocketClientHandshaker13Test.java | 15 +- .../WebSocketClientHandshakerTest.java | 78 +- .../websocketx/WebSocketCloseStatusTest.java | 127 + .../WebSocketHandshakeHandOverTest.java | 185 +- .../WebSocketProtocolHandlerTest.java | 64 +- .../websocketx/WebSocketRequestBuilder.java | 6 +- .../WebSocketServerHandshaker00Test.java | 32 +- .../WebSocketServerHandshaker13Test.java | 65 +- .../WebSocketServerProtocolHandlerTest.java | 236 +- .../WebSocketUtf8FrameValidatorTest.java | 4 +- .../WebSocketExtensionFilterProviderTest.java | 32 + .../WebSocketExtensionFilterTest.java | 87 + .../WebSocketExtensionTestUtil.java | 2 +- .../WebSocketServerExtensionHandlerTest.java | 47 +- .../PerFrameDeflateDecoderTest.java | 58 +- .../PerFrameDeflateEncoderTest.java | 69 +- .../PerMessageDeflateDecoderTest.java | 241 +- .../PerMessageDeflateEncoderTest.java | 202 +- .../handler/codec/rtsp/RtspDecoderTest.java | 1 - codec-http/src/test/resources/file-03.txt | 1 + codec-http2/pom.xml | 2 +- ...AbstractHttp2ConnectionHandlerBuilder.java | 161 +- .../http2/AbstractHttp2StreamChannel.java | 1106 ++++ .../CleartextHttp2ServerUpgradeHandler.java | 42 +- .../CompressorHttp2ConnectionEncoder.java | 13 +- .../DecoratingHttp2ConnectionEncoder.java | 13 +- .../codec/http2/DefaultHttp2Connection.java | 2 +- .../http2/DefaultHttp2ConnectionDecoder.java | 89 +- .../http2/DefaultHttp2ConnectionEncoder.java | 95 +- .../codec/http2/DefaultHttp2FrameWriter.java | 46 +- .../codec/http2/DefaultHttp2GoAwayFrame.java | 6 +- .../http2/DefaultHttp2HeadersDecoder.java | 9 +- .../http2/DefaultHttp2HeadersEncoder.java | 8 +- .../DefaultHttp2LocalFlowController.java | 31 +- .../codec/http2/DefaultHttp2PingFrame.java | 5 +- .../DefaultHttp2RemoteFlowController.java | 5 +- .../http2/DefaultHttp2SettingsAckFrame.java | 33 + .../http2/DefaultHttp2SettingsFrame.java | 14 + .../codec/http2/DefaultHttp2UnknownFrame.java | 22 +- .../DelegatingDecompressorFrameListener.java | 7 +- .../handler/codec/http2/HpackDecoder.java | 39 +- .../handler/codec/http2/HpackEncoder.java | 35 +- .../handler/codec/http2/HpackHeaderField.java | 20 +- .../codec/http2/HpackHuffmanDecoder.java | 4848 ++++++++++++++++- .../handler/codec/http2/HpackStaticTable.java | 8 +- .../netty/handler/codec/http2/HpackUtil.java | 10 + .../codec/http2/Http2ClientUpgradeCodec.java | 56 +- .../handler/codec/http2/Http2CodecUtil.java | 7 +- .../codec/http2/Http2ConnectionHandler.java | 117 +- .../http2/Http2ConnectionHandlerBuilder.java | 8 +- .../http2/Http2ControlFrameLimitEncoder.java | 113 + .../Http2EmptyDataFrameConnectionDecoder.java | 56 + .../http2/Http2EmptyDataFrameListener.java | 65 + .../handler/codec/http2/Http2Exception.java | 28 +- .../handler/codec/http2/Http2FrameCodec.java | 175 +- .../codec/http2/Http2FrameCodecBuilder.java | 58 +- .../codec/http2/Http2FrameListener.java | 2 +- .../handler/codec/http2/Http2FrameStream.java | 2 +- .../http2/Http2FrameStreamException.java | 2 +- .../handler/codec/http2/Http2Headers.java | 2 +- .../codec/http2/Http2HeadersEncoder.java | 7 +- .../codec/http2/Http2MultiplexCodec.java | 1169 +--- .../http2/Http2MultiplexCodecBuilder.java | 78 +- .../codec/http2/Http2MultiplexHandler.java | 365 ++ .../http2/Http2PromisedRequestVerifier.java | 4 +- .../codec/http2/Http2SecurityUtil.java | 2 +- .../codec/http2/Http2ServerUpgradeCodec.java | 20 +- .../codec/http2/Http2SettingsAckFrame.java | 29 + .../http2/Http2SettingsReceivedConsumer.java | 25 + .../http2/Http2StreamChannelBootstrap.java | 138 +- .../codec/http2/HttpConversionUtil.java | 83 +- .../http2/HttpToHttp2ConnectionHandler.java | 9 +- .../HttpToHttp2ConnectionHandlerBuilder.java | 9 +- .../http2/InboundHttp2ToHttpAdapter.java | 15 +- .../handler/codec/http2/MaxCapacityQueue.java | 129 + .../http2/UniformStreamByteDistributor.java | 5 +- .../WeightedFairQueueByteDistributor.java | 11 +- .../codec-http2/native-image.properties | 16 + ...leartextHttp2ServerUpgradeHandlerTest.java | 114 +- .../DecoratingHttp2ConnectionEncoderTest.java | 45 + .../DefaultHttp2ConnectionDecoderTest.java | 34 +- .../DefaultHttp2ConnectionEncoderTest.java | 115 +- .../http2/DefaultHttp2ConnectionTest.java | 4 +- .../http2/DefaultHttp2FrameWriterTest.java | 67 +- .../DefaultHttp2LocalFlowControllerTest.java | 66 +- .../handler/codec/http2/HpackDecoderTest.java | 2 +- .../handler/codec/http2/HpackEncoderTest.java | 2 +- .../handler/codec/http2/HpackHuffmanTest.java | 38 +- .../netty/handler/codec/http2/HpackTest.java | 5 +- .../handler/codec/http2/HpackTestCase.java | 20 +- .../http2/Http2ClientUpgradeCodecTest.java | 32 +- .../http2/Http2ConnectionHandlerTest.java | 80 +- .../http2/Http2ConnectionRoundtripTest.java | 92 +- .../Http2ControlFrameLimitEncoderTest.java | 277 + .../codec/http2/Http2DefaultFramesTest.java | 44 + ...p2EmptyDataFrameConnectionDecoderTest.java | 60 + .../Http2EmptyDataFrameListenerTest.java | 142 + .../codec/http2/Http2FrameCodecTest.java | 91 +- .../Http2MultiplexClientUpgradeTest.java | 89 + .../Http2MultiplexCodecClientUpgradeTest.java | 67 +- .../codec/http2/Http2MultiplexCodecTest.java | 995 +--- ...ttp2MultiplexHandlerClientUpgradeTest.java | 30 + .../http2/Http2MultiplexHandlerTest.java | 43 + .../codec/http2/Http2MultiplexTest.java | 1230 +++++ .../http2/Http2MultiplexTransportTest.java | 257 + .../http2/Http2ServerUpgradeCodecTest.java | 39 +- .../codec/http2/Http2StreamChannelIdTest.java | 60 + .../handler/codec/http2/Http2TestUtil.java | 8 +- .../codec/http2/HttpConversionUtilTest.java | 46 + .../codec/http2/LastInboundHandler.java | 2 +- codec-memcache/pom.xml | 2 +- .../memcache/AbstractMemcacheObject.java | 7 +- .../memcache/DefaultMemcacheContent.java | 6 +- .../binary/AbstractBinaryMemcacheDecoder.java | 6 +- .../binary/AbstractBinaryMemcacheMessage.java | 18 +- .../binary/DefaultBinaryMemcacheRequest.java | 10 + .../binary/DefaultBinaryMemcacheResponse.java | 12 +- .../DefaultFullBinaryMemcacheRequest.java | 19 +- .../DefaultFullBinaryMemcacheResponse.java | 19 +- .../DefaultFullBinaryMemcacheRequestTest.java | 99 + ...DefaultFullBinaryMemcacheResponseTest.java | 98 + codec-mqtt/pom.xml | 2 +- .../codec/mqtt/MqttConnectPayload.java | 6 +- .../netty/handler/codec/mqtt/MqttMessage.java | 11 + .../handler/codec/mqtt/MqttSubAckPayload.java | 9 +- .../codec/mqtt/MqttSubscribePayload.java | 9 +- .../codec/mqtt/MqttUnsubscribePayload.java | 9 +- .../handler/codec/mqtt/MqttCodecTest.java | 9 +- .../codec/mqtt/MqttConnectPayloadTest.java | 9 + codec-redis/pom.xml | 2 +- .../handler/codec/redis/RedisDecoderTest.java | 8 + codec-smtp/pom.xml | 2 +- .../netty/handler/codec/smtp/SmtpCommand.java | 4 + .../codec/smtp/SmtpRequestEncoder.java | 9 +- .../handler/codec/smtp/SmtpRequests.java | 23 +- .../codec/smtp/SmtpRequestEncoderTest.java | 27 +- codec-socks/pom.xml | 2 +- .../handler/codec/socks/SocksAuthRequest.java | 11 +- .../codec/socks/SocksAuthResponse.java | 6 +- .../handler/codec/socks/SocksCmdRequest.java | 14 +- .../handler/codec/socks/SocksCmdResponse.java | 9 +- .../handler/codec/socks/SocksInitRequest.java | 6 +- .../codec/socks/SocksInitResponse.java | 6 +- .../handler/codec/socks/SocksMessage.java | 6 +- .../handler/codec/socks/SocksRequest.java | 7 +- .../handler/codec/socks/SocksResponse.java | 7 +- .../codec/socksx/AbstractSocksMessage.java | 6 +- .../SocksPortUnificationServerHandler.java | 7 +- .../v4/DefaultSocks4CommandRequest.java | 18 +- .../v4/DefaultSocks4CommandResponse.java | 6 +- .../codec/socksx/v4/Socks4CommandStatus.java | 8 +- .../codec/socksx/v4/Socks4CommandType.java | 7 +- .../v5/DefaultSocks5CommandRequest.java | 14 +- .../v5/DefaultSocks5CommandResponse.java | 10 +- .../v5/DefaultSocks5InitialRequest.java | 9 +- .../v5/DefaultSocks5InitialResponse.java | 6 +- .../v5/DefaultSocks5PasswordAuthRequest.java | 9 +- .../v5/DefaultSocks5PasswordAuthResponse.java | 7 +- .../codec/socksx/v5/Socks5AddressType.java | 8 +- .../codec/socksx/v5/Socks5AuthMethod.java | 8 +- .../codec/socksx/v5/Socks5ClientEncoder.java | 7 +- .../v5/Socks5CommandRequestDecoder.java | 7 +- .../v5/Socks5CommandResponseDecoder.java | 7 +- .../codec/socksx/v5/Socks5CommandStatus.java | 8 +- .../codec/socksx/v5/Socks5CommandType.java | 8 +- .../v5/Socks5InitialRequestDecoder.java | 3 - .../socksx/v5/Socks5PasswordAuthStatus.java | 8 +- .../codec/socksx/v5/Socks5ServerEncoder.java | 7 +- .../v5/Socks5InitialRequestDecoderTest.java | 38 + codec-stomp/pom.xml | 2 +- .../codec/stomp/DefaultStompFrame.java | 8 +- .../stomp/DefaultStompHeadersSubframe.java | 7 +- .../codec/stomp/StompSubframeDecoder.java | 279 +- .../codec/stomp/StompSubframeEncoder.java | 18 +- .../codec/stomp/StompSubframeDecoderTest.java | 49 +- .../codec/stomp/StompSubframeEncoderTest.java | 20 + .../codec/stomp/StompTestConstants.java | 13 + codec-xml/pom.xml | 2 +- .../netty/handler/codec/xml/XmlAttribute.java | 28 +- .../netty/handler/codec/xml/XmlContent.java | 12 +- .../io/netty/handler/codec/xml/XmlDTD.java | 12 +- .../netty/handler/codec/xml/XmlDecoder.java | 2 +- .../handler/codec/xml/XmlDocumentStart.java | 20 +- .../netty/handler/codec/xml/XmlElement.java | 24 +- .../handler/codec/xml/XmlElementStart.java | 16 +- .../handler/codec/xml/XmlEntityReference.java | 16 +- .../netty/handler/codec/xml/XmlNamespace.java | 16 +- .../codec/xml/XmlProcessingInstruction.java | 16 +- .../handler/codec/xml/XmlDecoderTest.java | 22 +- codec/pom.xml | 2 +- .../handler/codec/AsciiHeadersEncoder.java | 17 +- .../handler/codec/ByteToMessageDecoder.java | 153 +- .../handler/codec/DatagramPacketEncoder.java | 2 +- .../io/netty/handler/codec/DateFormatter.java | 32 +- .../io/netty/handler/codec/DecoderResult.java | 11 +- .../netty/handler/codec/DefaultHeaders.java | 29 +- .../codec/DelimiterBasedFrameDecoder.java | 20 +- .../codec/FixedLengthFrameDecoder.java | 7 +- .../codec/LengthFieldBasedFrameDecoder.java | 39 +- .../handler/codec/LengthFieldPrepender.java | 11 +- .../handler/codec/MessageAggregator.java | 34 +- .../codec/MessageToMessageEncoder.java | 2 +- .../codec/ProtocolDetectionResult.java | 4 +- .../codec/ReplayingDecoderByteBuf.java | 26 +- .../io/netty/handler/codec/base64/Base64.java | 71 +- .../handler/codec/base64/Base64Decoder.java | 6 +- .../handler/codec/base64/Base64Dialect.java | 66 +- .../handler/codec/base64/Base64Encoder.java | 7 +- .../codec/compression/ByteBufChecksum.java | 7 +- .../codec/compression/Bzip2DivSufSort.java | 4 +- .../codec/compression/CompressionUtil.java | 5 + .../codec/compression/JZlibDecoder.java | 66 +- .../codec/compression/JZlibEncoder.java | 10 +- .../codec/compression/JdkZlibDecoder.java | 76 +- .../codec/compression/JdkZlibEncoder.java | 25 +- .../codec/compression/Lz4FrameDecoder.java | 11 +- .../codec/compression/Lz4FrameEncoder.java | 18 +- .../codec/compression/Lz4XXHash32.java | 107 + .../handler/codec/compression/LzfEncoder.java | 129 +- .../handler/codec/compression/Snappy.java | 6 +- .../codec/compression/ZlibDecoder.java | 65 + .../codec/protobuf/ProtobufDecoder.java | 6 +- .../CompatibleObjectEncoder.java | 2 - .../ObjectDecoderInputStream.java | 11 +- .../ObjectEncoderOutputStream.java | 9 +- .../handler/codec/string/StringDecoder.java | 6 +- .../handler/codec/string/StringEncoder.java | 7 +- .../handler/codec/ByteToMessageCodecTest.java | 2 +- .../codec/ByteToMessageDecoderTest.java | 191 +- .../codec/DatagramPacketEncoderTest.java | 11 +- .../handler/codec/DateFormatterTest.java | 23 + .../handler/codec/DefaultHeadersTest.java | 78 +- .../handler/codec/base64/Base64Test.java | 20 +- .../compression/ByteBufChecksumTest.java | 90 + .../handler/codec/compression/JZlibTest.java | 4 +- .../codec/compression/JdkZlibTest.java | 4 +- .../LengthAwareLzfIntegrationTest.java | 28 + .../compression/SnappyFrameDecoderTest.java | 56 +- .../compression/SnappyFrameEncoderTest.java | 7 +- .../handler/codec/compression/SnappyTest.java | 14 +- .../codec/compression/ZlibCrossTest1.java | 4 +- .../codec/compression/ZlibCrossTest2.java | 4 +- .../handler/codec/compression/ZlibTest.java | 61 +- .../CompatibleObjectEncoderTest.java | 1 - .../io/netty/handler/codec/xml/sample-04.xml | 8 +- common/pom.xml | 16 +- .../netty/util/AbstractReferenceCounted.java | 131 +- .../main/java/io/netty/util/AsciiString.java | 68 +- .../main/java/io/netty/util/AttributeMap.java | 2 +- .../main/java/io/netty/util/CharsetUtil.java | 4 +- .../main/java/io/netty/util/ConstantPool.java | 12 +- .../io/netty/util/DefaultAttributeMap.java | 10 +- .../java/io/netty/util/HashedWheelTimer.java | 34 +- .../src/main/java/io/netty/util/NetUtil.java | 9 +- .../src/main/java/io/netty/util/Recycler.java | 192 +- .../io/netty/util/ResourceLeakDetector.java | 40 +- .../util/ResourceLeakDetectorFactory.java | 9 +- .../io/netty/util/ThreadDeathWatcher.java | 25 +- .../concurrent/AbstractEventExecutor.java | 22 + .../AbstractScheduledEventExecutor.java | 143 +- .../netty/util/concurrent/CompleteFuture.java | 14 +- .../netty/util/concurrent/DefaultPromise.java | 117 +- .../util/concurrent/DefaultThreadFactory.java | 10 +- .../netty/util/concurrent/FailedFuture.java | 6 +- .../util/concurrent/FastThreadLocal.java | 45 +- .../java/io/netty/util/concurrent/Future.java | 4 +- .../util/concurrent/GlobalEventExecutor.java | 33 +- .../concurrent/ImmediateEventExecutor.java | 5 +- .../util/concurrent/ImmediateExecutor.java | 9 +- .../NonStickyEventExecutorGroup.java | 2 +- .../util/concurrent/PromiseAggregator.java | 13 +- .../util/concurrent/PromiseCombiner.java | 58 +- .../io/netty/util/concurrent/PromiseTask.java | 69 +- .../util/concurrent/ScheduledFutureTask.java | 110 +- .../concurrent/SingleThreadEventExecutor.java | 231 +- .../concurrent/ThreadPerTaskExecutor.java | 7 +- .../UnorderedThreadPoolEventExecutor.java | 4 +- .../util/internal/AppendableCharSequence.java | 13 + .../java/io/netty/util/internal/Hidden.java | 105 + .../util/internal/InternalThreadLocalMap.java | 6 +- .../netty/util/internal/LongAdderCounter.java | 1 + .../util/internal/NativeLibraryLoader.java | 84 +- .../io/netty/util/internal/ObjectPool.java | 87 + .../io/netty/util/internal/PendingWrite.java | 13 +- .../util/internal/PlatformDependent.java | 249 +- .../util/internal/PlatformDependent0.java | 121 +- .../internal/PromiseNotificationUtil.java | 2 +- .../netty/util/internal/ReadOnlyIterator.java | 5 +- .../util/internal/RecyclableArrayList.java | 26 +- .../util/internal/ReferenceCountUpdater.java | 179 + .../io/netty/util/internal/SocketUtils.java | 21 +- .../io/netty/util/internal/StringUtil.java | 92 +- .../internal/SuppressJava6Requirement.java | 2 +- .../util/internal/SystemPropertyUtil.java | 4 +- .../util/internal/ThreadExecutorMap.java | 96 + .../util/internal/ThreadLocalRandom.java | 2 + .../logging/AbstractInternalLogger.java | 6 +- .../util/internal/logging/CommonsLogger.java | 6 +- .../logging/InternalLoggerFactory.java | 15 +- .../logging/LocationAwareSlf4JLogger.java | 10 +- .../svm/CleanerJava6Substitution.java | 33 + .../svm/PlatformDependent0Substitution.java | 33 + .../svm/PlatformDependentSubstitution.java | 39 + .../svm/UnsafeRefArrayAccessSubstitution.java | 32 + .../netty/util/internal/svm/package-info.java | 21 + .../io.netty/common/native-image.properties | 15 + ...ockhound.integration.BlockHoundIntegration | 14 + .../util/collection/KObjectHashMap.template | 2 +- .../netty/util/AsciiStringCharacterTest.java | 27 +- .../test/java/io/netty/util/RecyclerTest.java | 1 + .../util/concurrent/DefaultPromiseTest.java | 49 +- .../util/concurrent/FastThreadLocalTest.java | 19 + .../concurrent/GlobalEventExecutorTest.java | 74 +- .../concurrent/ImmediateExecutorTest.java | 47 + .../util/concurrent/PromiseCombinerTest.java | 48 +- .../SingleThreadEventExecutorTest.java | 249 +- .../internal/AppendableCharSequenceTest.java | 10 + .../internal/DefaultPriorityQueueTest.java | 2 - .../io/netty/util/internal/MathUtilTest.java | 88 + .../internal/NativeLibraryLoaderTest.java | 74 + .../netty/util/internal/StringUtilTest.java | 66 +- .../util/internal/ThreadExecutorMapTest.java | 63 + .../internal/logging/Log4J2LoggerTest.java | 2 +- .../logging/Slf4JLoggerFactoryTest.java | 21 +- .../collection/KObjectHashMapTest.template | 30 + dev-tools/pom.xml | 4 +- docker/Dockerfile.centos | 3 + docker/README.md | 4 +- docker/docker-compose.centos-6.110.yaml | 2 +- docker/docker-compose.centos-6.111.yaml | 2 +- docker/docker-compose.centos-6.112.yaml | 2 +- docker/docker-compose.centos-6.113.yaml | 2 +- docker/docker-compose.centos-6.18.yaml | 2 +- docker/docker-compose.centos-6.19.yaml | 2 +- docker/docker-compose.centos-6.graalvm1.yaml | 22 + docker/docker-compose.centos-6.openj9111.yaml | 22 + docker/docker-compose.centos-7.110.yaml | 2 +- docker/docker-compose.centos-7.111.yaml | 2 +- docker/docker-compose.centos-7.112.yaml | 2 +- docker/docker-compose.centos-7.113.yaml | 2 +- docker/docker-compose.centos-7.18.yaml | 2 +- docker/docker-compose.centos-7.19.yaml | 2 +- docker/docker-compose.yaml | 14 +- example/pom.xml | 10 +- .../example/http/cors/OkResponseHandler.java | 4 +- .../file/HttpStaticFileServerHandler.java | 114 +- .../HttpHelloWorldServerHandler.java | 42 +- .../example/http/snoop/HttpSnoopClient.java | 3 +- .../http/snoop/HttpSnoopServerHandler.java | 2 +- .../example/http/upload/HttpUploadServer.java | 2 +- .../http/upload/HttpUploadServerHandler.java | 51 +- .../WebSocketServerHandler.java | 38 +- .../server/WebSocketFrameHandler.java | 4 +- .../server/WebSocketIndexPageHandler.java | 44 +- .../websocketx/server/WebSocketServer.java | 2 +- .../http2/helloworld/client/Http2Client.java | 11 +- .../client/Http2ClientInitializer.java | 19 +- .../client/Http2ClientFrameInitializer.java | 57 + ...Http2ClientStreamFrameResponseHandler.java | 61 + .../frame/client/Http2FrameClient.java | 124 + .../frame/server/Http2OrHttpHandler.java | 1 - .../helloworld/frame/server/Http2Server.java | 2 +- .../frame/server/Http2ServerInitializer.java | 3 +- .../multiplex/server/Http2OrHttpHandler.java | 6 +- .../multiplex/server/Http2Server.java | 2 +- .../server/Http2ServerInitializer.java | 9 +- .../server/HelloWorldHttp1Handler.java | 19 +- .../http2/helloworld/server/Http2Server.java | 2 +- .../server/Http2ServerInitializer.java | 3 +- .../http2/tiles/FallbackRequestHandler.java | 3 +- .../http2/tiles/Http1RequestHandler.java | 14 +- .../example/http2/tiles/Http2Server.java | 2 +- .../netty/example/http2/tiles/HttpServer.java | 2 +- .../mqtt/heartBeat/MqttHeartBeatBroker.java | 64 + .../heartBeat/MqttHeartBeatBrokerHandler.java | 82 + .../mqtt/heartBeat/MqttHeartBeatClient.java | 64 + .../heartBeat/MqttHeartBeatClientHandler.java | 83 + .../netty/example/ocsp/OcspClientExample.java | 12 +- .../netty/example/ocsp/OcspServerExample.java | 4 +- .../netty/example/spdy/client/SpdyClient.java | 4 +- .../example/spdy/client/SpdyFrameLogger.java | 9 +- .../spdy/server/SpdyServerHandler.java | 34 +- .../worldclock/WorldClockProtocol.java | 147 +- handler-proxy/pom.xml | 2 +- .../netty/handler/proxy/HttpProxyHandler.java | 175 +- .../handler/proxy/ProxyConnectionEvent.java | 22 +- .../io/netty/handler/proxy/ProxyHandler.java | 6 +- .../handler/proxy/HttpProxyHandlerTest.java | 94 +- .../netty/handler/proxy/ProxyHandlerTest.java | 4 +- handler/pom.xml | 15 +- .../address/DynamicAddressConnectHandler.java | 82 + .../address/ResolveAddressHandler.java | 66 + .../netty/handler/address/package-info.java | 20 + .../handler/flow/FlowControlHandler.java | 65 +- .../flush/FlushConsolidationHandler.java | 12 +- .../handler/ipfilter/IpSubnetFilterRule.java | 10 +- .../handler/ipfilter/RuleBasedIpFilter.java | 7 +- .../netty/handler/logging/ByteBufFormat.java | 36 + .../netty/handler/logging/LoggingHandler.java | 107 +- .../netty/handler/ssl/AbstractSniHandler.java | 352 +- ...ApplicationProtocolNegotiationHandler.java | 32 +- .../java/io/netty/handler/ssl/Conscrypt.java | 14 +- .../handler/ssl/DelegatingSslContext.java | 16 + .../handler/ssl/ExtendedOpenSslSession.java | 4 + .../handler/ssl/Java7SslParametersUtils.java | 3 + .../io/netty/handler/ssl/Java8SslUtils.java | 3 + .../io/netty/handler/ssl/Java9SslEngine.java | 2 + .../io/netty/handler/ssl/Java9SslUtils.java | 2 + .../JdkAlpnApplicationProtocolNegotiator.java | 4 + .../handler/ssl/JdkSslClientContext.java | 24 +- .../io/netty/handler/ssl/JdkSslContext.java | 84 +- .../io/netty/handler/ssl/JdkSslEngine.java | 3 + .../handler/ssl/JdkSslServerContext.java | 64 +- .../handler/ssl/KeyManagerFactoryWrapper.java | 44 + .../java/io/netty/handler/ssl/OpenSsl.java | 187 +- .../OpenSslCachingKeyMaterialProvider.java | 13 +- .../OpenSslCachingX509KeyManagerFactory.java | 21 + .../handler/ssl/OpenSslClientContext.java | 10 +- .../ssl/OpenSslJavaxX509Certificate.java | 2 +- .../ssl/OpenSslKeyMaterialManager.java | 18 +- .../ssl/OpenSslKeyMaterialProvider.java | 56 +- .../netty/handler/ssl/OpenSslPrivateKey.java | 65 +- .../handler/ssl/OpenSslPrivateKeyMethod.java | 62 + .../handler/ssl/OpenSslServerContext.java | 13 +- .../handler/ssl/OpenSslSessionContext.java | 6 +- ...OpenSslTlsv13X509ExtendedTrustManager.java | 268 +- .../handler/ssl/OpenSslX509Certificate.java | 5 +- .../ssl/OpenSslX509KeyManagerFactory.java | 62 +- .../ssl/OpenSslX509TrustManagerWrapper.java | 5 + .../io/netty/handler/ssl/PemPrivateKey.java | 6 + .../java/io/netty/handler/ssl/PemReader.java | 2 +- .../java/io/netty/handler/ssl/PemValue.java | 2 +- .../handler/ssl/PseudoRandomFunction.java | 94 + .../ReferenceCountedOpenSslClientContext.java | 53 +- .../ssl/ReferenceCountedOpenSslContext.java | 197 +- .../ssl/ReferenceCountedOpenSslEngine.java | 318 +- .../ReferenceCountedOpenSslServerContext.java | 49 +- .../java/io/netty/handler/ssl/SniHandler.java | 11 +- .../handler/ssl/SslClientHelloHandler.java | 312 ++ .../java/io/netty/handler/ssl/SslContext.java | 192 +- .../netty/handler/ssl/SslContextBuilder.java | 153 +- .../java/io/netty/handler/ssl/SslHandler.java | 497 +- .../ssl/SslHandshakeTimeoutException.java | 28 + .../handler/ssl/SslMasterKeyHandler.java | 188 + .../io/netty/handler/ssl/SslProvider.java | 18 +- .../java/io/netty/handler/ssl/SslUtils.java | 5 +- .../ssl/SupportedCipherSuiteFilter.java | 10 +- .../ssl/TrustManagerFactoryWrapper.java | 44 + .../handler/ssl/ocsp/OcspClientHandler.java | 6 +- .../util/FingerprintTrustManagerFactory.java | 11 +- .../util/OpenJdkSelfSignedCertGenerator.java | 2 + .../ssl/util/SelfSignedCertificate.java | 18 +- .../ssl/util/SimpleKeyManagerFactory.java | 154 + .../ssl/util/SimpleTrustManagerFactory.java | 23 +- .../ssl/util/X509KeyManagerWrapper.java | 78 + .../ssl/util/X509TrustManagerWrapper.java | 3 + .../io/netty/handler/stream/ChunkedFile.java | 23 +- .../netty/handler/stream/ChunkedNioFile.java | 35 +- .../handler/stream/ChunkedNioStream.java | 5 +- .../netty/handler/stream/ChunkedStream.java | 11 +- .../handler/stream/ChunkedWriteHandler.java | 154 +- .../netty/handler/timeout/IdleStateEvent.java | 38 +- .../handler/timeout/IdleStateHandler.java | 16 +- .../handler/timeout/ReadTimeoutException.java | 11 +- .../handler/timeout/TimeoutException.java | 7 +- .../timeout/WriteTimeoutException.java | 13 +- .../handler/timeout/WriteTimeoutHandler.java | 5 +- .../AbstractTrafficShapingHandler.java | 2 +- .../traffic/GlobalChannelTrafficCounter.java | 4 +- .../traffic/GlobalTrafficShapingHandler.java | 10 +- .../netty/handler/traffic/TrafficCounter.java | 17 +- .../io.netty/handler/native-image.properties | 15 + .../DynamicAddressConnectHandlerTest.java | 107 + .../address/ResolveAddressHandlerTest.java | 139 + .../handler/flow/FlowControlHandlerTest.java | 119 +- .../flush/FlushConsolidationHandlerTest.java | 22 + .../handler/logging/LoggingHandlerTest.java | 32 +- .../ssl/AmazonCorrettoSslEngineTest.java | 118 + .../handler/ssl/CipherSuiteCanaryTest.java | 34 +- .../ssl/ConscryptJdkSslEngineInteropTest.java | 16 +- .../ConscryptOpenSslEngineInteropTest.java | 158 + .../handler/ssl/ConscryptSslEngineTest.java | 16 +- .../ssl/JdkConscryptSslEngineInteropTest.java | 16 +- .../ssl/JdkOpenSslEngineInteroptTest.java | 43 +- .../netty/handler/ssl/JdkSslEngineTest.java | 16 +- ...OpenSslCachingKeyMaterialProviderTest.java | 21 +- .../OpenSslConscryptSslEngineInteropTest.java | 150 + .../netty/handler/ssl/OpenSslEngineTest.java | 355 +- .../ssl/OpenSslJdkSslEngineInteroptTest.java | 57 +- .../ssl/OpenSslKeyMaterialProviderTest.java | 104 +- .../ssl/OpenSslPrivateKeyMethodTest.java | 402 ++ .../io/netty/handler/ssl/PemEncodedTest.java | 1 - .../handler/ssl/PseudoRandomFunctionTest.java | 53 + .../ReferenceCountedOpenSslEngineTest.java | 39 +- .../io/netty/handler/ssl/SSLEngineTest.java | 695 ++- .../handler/ssl/SniClientJava8TestUtil.java | 2 +- .../io/netty/handler/ssl/SniClientTest.java | 3 +- .../io/netty/handler/ssl/SniHandlerTest.java | 184 +- .../handler/ssl/SslContextBuilderTest.java | 192 +- .../ssl/SslContextTrustManagerTest.java | 2 +- .../io/netty/handler/ssl/SslErrorTest.java | 191 +- .../io/netty/handler/ssl/SslHandlerTest.java | 364 +- .../io/netty/handler/ssl/ocsp/OcspTest.java | 6 +- .../stream/ChunkedWriteHandlerTest.java | 271 +- .../handler/timeout/IdleStateEventTest.java | 34 + .../handler/timeout/IdleStateHandlerTest.java | 52 +- license/LICENSE.dnsinfo.txt | 22 + license/LICENSE.hyper-hpack.txt | 21 + license/LICENSE.jboss-marshalling.txt | 678 +-- license/LICENSE.nghttp2-hpack.txt | 23 + microbench/README.md | 2 +- microbench/pom.xml | 6 +- ...tractReferenceCountedByteBufBenchmark.java | 6 +- .../netty/buffer/ByteBufAccessBenchmark.java | 165 + .../codec/DateFormatter2Benchmark.java | 95 + .../codec/http/DecodeHexBenchmark.java | 136 + .../codec/http/HttpMethodMapBenchmark.java | 39 +- .../http/QueryStringDecoderBenchmark.java | 66 + .../http/QueryStringEncoderBenchmark.java | 95 + .../codec/http2/HpackBenchmarkUtil.java | 2 +- .../codec/http2/HpackDecoderBenchmark.java | 2 +- .../handler/codec/http2/HpackHeader.java | 7 +- .../DefaultChannelPipelineBenchmark.java | 84 + .../EmbeddedChannelHandlerContext.java | 2 +- ...ddedChannelWriteReleaseHandlerContext.java | 1 + .../epoll/EpollSocketChannelBenchmark.java | 16 + .../BurstCostExecutorsBenchmark.java | 2 +- .../FastThreadLocalFastPathBenchmark.java | 13 +- .../FastThreadLocalSlowPathBenchmark.java | 13 +- .../ReadOnlyHttp2HeadersBenchmark.java | 33 +- .../http/HttpRequestDecoderBenchmark.java | 6 +- .../RecyclableArrayListBenchmark.java | 4 +- .../ScheduleFutureTaskBenchmark.java | 109 + .../netty/util/concurrent/package-info.java | 19 + pom.xml | 236 +- resolver-dns-native-macos/pom.xml | 185 + .../src/main/c/dnsinfo.h | 106 + .../src/main/c/netty_resolver_dns_macos.c | 263 + .../netty/resolver/dns/macos/DnsResolver.java | 81 + .../MacOSDnsServerAddressStreamProvider.java | 179 + .../resolver/dns/macos/package-info.java | 20 + ...cOSDnsServerAddressStreamProviderTest.java | 52 + resolver-dns/pom.xml | 2 +- .../dns/AuthoritativeDnsServerCache.java | 2 - .../AuthoritativeDnsServerCacheAdapter.java | 2 - .../dns/BiDnsQueryLifecycleObserver.java | 2 - .../BiDnsQueryLifecycleObserverFactory.java | 2 - .../resolver/dns/DatagramDnsQueryContext.java | 51 + .../DefaultAuthoritativeDnsServerCache.java | 6 +- .../netty/resolver/dns/DefaultDnsCache.java | 2 - .../resolver/dns/DefaultDnsCnameCache.java | 8 +- ...DefaultDnsServerAddressStreamProvider.java | 6 +- .../dns/DnsAddressResolveContext.java | 43 +- .../resolver/dns/DnsAddressResolverGroup.java | 3 - .../java/io/netty/resolver/dns/DnsCache.java | 2 - .../io/netty/resolver/dns/DnsCacheEntry.java | 3 - .../io/netty/resolver/dns/DnsCnameCache.java | 2 - .../netty/resolver/dns/DnsNameResolver.java | 354 +- .../resolver/dns/DnsNameResolverBuilder.java | 56 +- .../dns/DnsNameResolverException.java | 2 - .../dns/DnsNameResolverTimeoutException.java | 2 - .../netty/resolver/dns/DnsQueryContext.java | 59 +- .../dns/DnsQueryLifecycleObserver.java | 2 - .../dns/DnsQueryLifecycleObserverFactory.java | 2 - .../resolver/dns/DnsRecordResolveContext.java | 10 + .../netty/resolver/dns/DnsResolveContext.java | 96 +- .../resolver/dns/DnsServerAddressStream.java | 3 - .../dns/DnsServerAddressStreamProvider.java | 3 - .../dns/DnsServerAddressStreamProviders.java | 126 +- .../resolver/dns/DnsServerAddresses.java | 15 +- .../MultiDnsServerAddressStreamProvider.java | 3 - .../dns/NoopAuthoritativeDnsServerCache.java | 4 - .../io/netty/resolver/dns/NoopDnsCache.java | 2 - .../netty/resolver/dns/NoopDnsCnameCache.java | 2 - .../NoopDnsQueryLifecycleObserverFactory.java | 2 - .../dns/PreferredAddressTypeComparator.java | 54 + .../RoundRobinDnsAddressResolverGroup.java | 2 - ...uentialDnsServerAddressStreamProvider.java | 3 - .../dns/ShuffledDnsServerAddressStream.java | 1 - ...ngletonDnsServerAddressStreamProvider.java | 3 - .../resolver/dns/TcpDnsQueryContext.java | 53 + ...esolverDnsServerAddressStreamProvider.java | 106 +- .../io/netty/resolver/dns/package-info.java | 3 - ...efaultAuthoritativeDnsServerCacheTest.java | 1 - .../resolver/dns/DnsNameResolverTest.java | 441 +- .../DnsServerAddressStreamProvidersTest.java | 27 + .../PreferredAddressTypeComparatorTest.java | 70 + .../io/netty/resolver/dns/TestDnsServer.java | 36 +- ...verDnsServerAddressStreamProviderTest.java | 24 + resolver/pom.xml | 2 +- .../resolver/AbstractAddressResolver.java | 6 +- .../io/netty/resolver/AddressResolver.java | 2 - .../netty/resolver/AddressResolverGroup.java | 36 +- .../netty/resolver/CompositeNameResolver.java | 9 +- .../resolver/DefaultAddressResolverGroup.java | 2 - .../DefaultHostsFileEntriesResolver.java | 2 - .../netty/resolver/DefaultNameResolver.java | 2 - .../io/netty/resolver/HostsFileEntries.java | 3 - .../resolver/HostsFileEntriesResolver.java | 3 - .../io/netty/resolver/HostsFileParser.java | 2 - .../io/netty/resolver/InetNameResolver.java | 2 - .../resolver/InetSocketAddressResolver.java | 2 - .../java/io/netty/resolver/NameResolver.java | 2 - .../netty/resolver/NoopAddressResolver.java | 2 - .../resolver/NoopAddressResolverGroup.java | 2 - .../netty/resolver/ResolvedAddressTypes.java | 3 - .../RoundRobinInetAddressResolver.java | 7 +- .../io/netty/resolver/SimpleNameResolver.java | 2 - .../java/io/netty/resolver/package-info.java | 3 - tarball/pom.xml | 18 +- testsuite-autobahn/pom.xml | 6 +- .../autobahn/AutobahnServerHandler.java | 6 +- testsuite-http2/pom.xml | 6 +- .../http2/HelloWorldHttp1Handler.java | 2 +- .../io/netty/testsuite/http2/Http2Server.java | 2 +- .../http2/Http2ServerInitializer.java | 7 +- testsuite-native-image/pom.xml | 121 + .../netty/testsuite/svm/HttpNativeServer.java | 64 + .../svm/HttpNativeServerHandler.java | 67 + .../svm/HttpNativeServerInitializer.java | 33 + .../io/netty/testsuite/svm/package-info.java | 20 + testsuite-osgi/pom.xml | 99 +- .../io/netty/osgitests/OsgiBundleTest.java | 70 +- testsuite-shading/pom.xml | 18 +- testsuite/pom.xml | 3 +- .../AbstractSingleThreadEventLoopTest.java | 168 + .../transport/DefaultEventLoopTest.java | 41 + .../testsuite/transport/NioEventLoopTest.java | 41 + .../socket/AbstractDatagramTest.java | 21 +- .../socket/AbstractSocketReuseFdTest.java | 180 + ...bstractSocketShutdownOutputByPeerTest.java | 163 + .../CompositeBufferGatheringWriteTest.java | 1 - .../socket/DatagramMulticastIPv6Test.java | 26 + .../socket/DatagramMulticastTest.java | 118 +- .../socket/DatagramUnicastIPv6MappedTest.java | 39 + .../socket/DatagramUnicastIPv6Test.java | 50 + .../transport/socket/DatagramUnicastTest.java | 209 +- .../transport/socket/SocketConnectTest.java | 2 - .../socket/SocketFileRegionTest.java | 41 +- .../SocketShutdownOutputByPeerTest.java | 139 +- .../SocketSslClientRenegotiateTest.java | 121 +- .../socket/SocketSslGreetingTest.java | 100 +- .../socket/SocketTestPermutation.java | 5 +- .../udt/UDTClientServerConnectionTest.java | 19 + .../io/netty/testsuite/util/TestUtils.java | 9 +- transport-blockhound-tests/pom.xml | 77 + .../NettyBlockHoundIntegrationTest.java | 215 + transport-native-epoll/README.md | 2 +- transport-native-epoll/pom.xml | 126 +- .../src/main/c/netty_epoll_linuxsocket.c | 453 +- .../src/main/c/netty_epoll_native.c | 424 +- .../channel/epoll/AbstractEpollChannel.java | 29 +- .../epoll/AbstractEpollStreamChannel.java | 88 +- .../java/io/netty/channel/epoll/Epoll.java | 18 +- .../channel/epoll/EpollChannelConfig.java | 6 +- .../channel/epoll/EpollChannelOption.java | 2 + .../channel/epoll/EpollDatagramChannel.java | 323 +- .../epoll/EpollDatagramChannelConfig.java | 82 +- .../epoll/EpollDomainSocketChannelConfig.java | 84 +- .../netty/channel/epoll/EpollEventLoop.java | 254 +- .../channel/epoll/EpollEventLoopGroup.java | 29 +- .../epoll/EpollRecvByteAllocatorHandle.java | 58 +- ...EpollRecvByteAllocatorStreamingHandle.java | 2 +- .../epoll/EpollServerChannelConfig.java | 15 +- .../io/netty/channel/epoll/LinuxSocket.java | 197 +- .../java/io/netty/channel/epoll/Native.java | 89 +- .../epoll/NativeDatagramPacketArray.java | 127 +- .../NativeStaticallyReferencedJniMethods.java | 1 + .../io/netty/channel/epoll/TcpMd5Util.java | 4 +- .../epoll/EpollDatagramChannelTest.java | 108 + .../epoll/EpollDatagramMulticastIPv6Test.java | 30 + .../epoll/EpollDatagramMulticastTest.java | 29 + .../EpollDatagramScatteringReadTest.java | 276 + .../EpollDatagramUnicastIPv6MappedTest.java | 29 + .../epoll/EpollDatagramUnicastIPv6Test.java | 29 + .../epoll/EpollDatagramUnicastTest.java | 3 +- .../epoll/EpollDomainSocketReuseFdTest.java | 36 + ...lDomainSocketShutdownOutputByPeerTest.java | 69 + ...lDomainSocketSslClientRenegotiateTest.java | 42 + .../EpollDomainSocketSslGreetingTest.java | 4 +- .../channel/epoll/EpollEventLoopTest.java | 76 +- .../channel/epoll/EpollReuseAddrTest.java | 4 +- .../EpollSocketSslClientRenegotiateTest.java | 36 + .../epoll/EpollSocketSslGreetingTest.java | 4 +- .../epoll/EpollSocketSslSessionReuseTest.java | 36 + .../epoll/EpollSocketTestPermutation.java | 43 +- .../netty/channel/epoll/EpollSpliceTest.java | 17 +- .../io/netty/channel/epoll/EpollTest.java | 4 +- .../epoll/EpollWriteBeforeRegisteredTest.java | 30 + transport-native-kqueue/pom.xml | 35 +- .../src/main/c/netty_kqueue_bsdsocket.c | 173 +- .../src/main/c/netty_kqueue_native.c | 16 +- .../channel/kqueue/AbstractKQueueChannel.java | 65 +- .../kqueue/AbstractKQueueStreamChannel.java | 7 +- .../io/netty/channel/kqueue/BsdSocket.java | 15 +- .../java/io/netty/channel/kqueue/KQueue.java | 16 +- .../channel/kqueue/KQueueDatagramChannel.java | 125 +- .../KQueueDomainSocketChannelConfig.java | 78 +- .../netty/channel/kqueue/KQueueEventLoop.java | 65 +- .../channel/kqueue/KQueueEventLoopGroup.java | 22 +- .../kqueue/KQueueRecvByteAllocatorHandle.java | 48 +- .../kqueue/KQueueServerChannelConfig.java | 11 +- .../netty/channel/kqueue/NativeLongArray.java | 4 + .../KQueueDatagramUnicastIPv6MappedTest.java | 29 + .../kqueue/KQueueDatagramUnicastIPv6Test.java | 31 + .../kqueue/KQueueDatagramUnicastTest.java | 3 +- .../kqueue/KQueueDomainSocketReuseFdTest.java | 36 + ...eDomainSocketShutdownOutputByPeerTest.java | 68 + ...eDomainSocketSslClientRenegotiateTest.java | 42 + .../KQueueDomainSocketSslGreetingTest.java | 4 +- .../kqueue/KQueueETSocketAutoReadTest.java | 1 - .../KQueueETSocketExceptionHandlingTest.java | 1 - .../kqueue/KQueueETSocketReadPendingTest.java | 1 - .../channel/kqueue/KQueueEventLoopTest.java | 20 +- .../KQueueSocketConnectionAttemptTest.java | 4 - .../KQueueSocketSslClientRenegotiateTest.java | 36 + .../kqueue/KQueueSocketSslGreetingTest.java | 4 +- .../KQueueSocketSslSessionReuseTest.java | 36 + .../channel/kqueue/KQueueSocketTest.java | 30 +- .../kqueue/KQueueSocketTestPermutation.java | 5 +- .../KqueueWriteBeforeRegisteredTest.java | 30 + transport-native-unix-common-tests/pom.xml | 2 +- transport-native-unix-common/pom.xml | 25 +- .../src/main/c/netty_unix_errors.c | 120 +- .../src/main/c/netty_unix_filedescriptor.c | 60 +- .../src/main/c/netty_unix_socket.c | 339 +- .../src/main/c/netty_unix_socket.h | 5 +- .../src/main/c/netty_unix_util.c | 71 +- .../src/main/c/netty_unix_util.h | 84 + .../channel/unix/DatagramSocketAddress.java | 15 +- .../channel/unix/DomainSocketAddress.java | 7 +- .../java/io/netty/channel/unix/Errors.java | 41 +- .../io/netty/channel/unix/FileDescriptor.java | 55 +- .../java/io/netty/channel/unix/IovArray.java | 51 +- .../netty/channel/unix/NativeInetAddress.java | 8 +- .../java/io/netty/channel/unix/Socket.java | 98 +- transport-rxtx/pom.xml | 2 +- transport-sctp/pom.xml | 2 +- .../sctp/DefaultSctpChannelConfig.java | 6 +- .../sctp/DefaultSctpServerChannelConfig.java | 12 +- .../io/netty/channel/sctp/SctpMessage.java | 12 +- .../channel/sctp/SctpNotificationHandler.java | 7 +- .../sctp/oio/OioSctpServerChannel.java | 7 +- .../sctp/SctpMessageCompletionHandler.java | 6 +- transport-udt/pom.xml | 2 +- .../nio/NioUdtMessageConnectorChannel.java | 4 +- .../io/netty/test/udt/util/CaliperBench.java | 1 - transport/pom.xml | 2 +- .../io/netty/bootstrap/AbstractBootstrap.java | 98 +- .../java/io/netty/bootstrap/Bootstrap.java | 37 +- .../io/netty/bootstrap/ServerBootstrap.java | 81 +- .../io/netty/channel/AbstractChannel.java | 55 +- .../AbstractChannelHandlerContext.java | 363 +- .../channel/AdaptiveRecvByteBufAllocator.java | 9 +- .../netty/channel/ChannelDuplexHandler.java | 10 + .../io/netty/channel/ChannelException.java | 19 + .../channel/ChannelFlushPromiseNotifier.java | 16 +- .../java/io/netty/channel/ChannelHandler.java | 3 +- .../netty/channel/ChannelHandlerAdapter.java | 5 + .../io/netty/channel/ChannelHandlerMask.java | 205 + .../channel/ChannelInboundHandlerAdapter.java | 12 + .../io/netty/channel/ChannelMetadata.java | 7 +- .../java/io/netty/channel/ChannelOption.java | 8 +- .../netty/channel/ChannelOutboundBuffer.java | 34 +- .../ChannelOutboundHandlerAdapter.java | 10 + .../netty/channel/CoalescingBufferQueue.java | 1 - .../channel/CombinedChannelDuplexHandler.java | 10 +- .../netty/channel/CompleteChannelFuture.java | 6 +- .../channel/DefaultAddressedEnvelope.java | 6 +- .../netty/channel/DefaultChannelConfig.java | 46 +- .../channel/DefaultChannelHandlerContext.java | 13 +- .../netty/channel/DefaultChannelPipeline.java | 56 +- .../netty/channel/DefaultEventLoopGroup.java | 9 + .../io/netty/channel/DefaultFileRegion.java | 57 +- .../DefaultMaxBytesRecvByteBufAllocator.java | 20 +- ...efaultMaxMessagesRecvByteBufAllocator.java | 6 +- .../channel/DefaultMessageSizeEstimator.java | 6 +- .../channel/EventLoopTaskQueueFactory.java | 35 + .../ExtendedClosedChannelException.java | 32 + .../io/netty/channel/FailedChannelFuture.java | 6 +- .../channel/FixedRecvByteBufAllocator.java | 9 +- .../netty/channel/MessageSizeEstimator.java | 4 +- .../channel/MultithreadEventLoopGroup.java | 1 + .../io/netty/channel/PendingWriteQueue.java | 33 +- .../channel/SimpleChannelInboundHandler.java | 9 - .../netty/channel/SingleThreadEventLoop.java | 32 +- .../channel/ThreadPerChannelEventLoop.java | 5 + .../ThreadPerChannelEventLoopGroup.java | 24 +- .../io/netty/channel/VoidChannelPromise.java | 6 +- .../netty/channel/WriteBufferWaterMark.java | 6 +- .../channel/embedded/EmbeddedChannel.java | 24 +- .../channel/embedded/EmbeddedEventLoop.java | 5 +- .../channel/group/ChannelGroupException.java | 9 +- .../netty/channel/group/CombinedIterator.java | 14 +- .../channel/group/DefaultChannelGroup.java | 33 +- .../group/DefaultChannelGroupFuture.java | 11 +- .../io/netty/channel/local/LocalAddress.java | 5 +- .../io/netty/channel/local/LocalChannel.java | 15 +- .../channel/local/LocalEventLoopGroup.java | 9 + .../netty/channel/nio/AbstractNioChannel.java | 10 +- .../io/netty/channel/nio/NioEventLoop.java | 321 +- .../netty/channel/nio/NioEventLoopGroup.java | 21 +- .../channel/oio/OioByteStreamChannel.java | 11 +- .../netty/channel/oio/OioEventLoopGroup.java | 3 +- .../channel/pool/AbstractChannelPoolMap.java | 59 +- .../netty/channel/pool/FixedChannelPool.java | 84 +- .../netty/channel/pool/SimpleChannelPool.java | 42 +- .../socket/DefaultDatagramChannelConfig.java | 6 +- .../DefaultServerSocketChannelConfig.java | 11 +- .../socket/DefaultSocketChannelConfig.java | 6 +- .../channel/socket/nio/NioChannelOption.java | 5 + .../socket/nio/NioDatagramChannel.java | 36 +- .../socket/nio/NioServerSocketChannel.java | 7 +- .../channel/socket/nio/NioSocketChannel.java | 6 +- .../socket/nio/ProtocolFamilyConverter.java | 2 + .../socket/oio/OioServerSocketChannel.java | 7 +- .../transport/native-image.properties | 15 + .../io.netty/transport/reflection-config.json | 15 + .../io/netty/bootstrap/BootstrapTest.java | 87 + .../io/netty/channel/AbstractChannelTest.java | 91 +- .../netty/channel/AbstractEventLoopTest.java | 2 +- .../AdaptiveRecvByteBufAllocatorTest.java | 20 + .../DefaultChannelPipelineTailTest.java | 4 +- .../channel/DefaultChannelPipelineTest.java | 540 +- .../netty/channel/DefaultFileRegionTest.java | 120 + .../DelegatingChannelPromiseNotifierTest.java | 2 - .../channel/embedded/EmbeddedChannelTest.java | 22 + .../netty/channel/nio/NioEventLoopTest.java | 79 + .../pool/AbstractChannelPoolMapTest.java | 97 +- .../channel/pool/ChannelPoolTestUtils.java | 29 + .../pool/FixedChannelPoolMapDeadlockTest.java | 264 + .../channel/pool/FixedChannelPoolTest.java | 83 +- .../channel/pool/SimpleChannelPoolTest.java | 85 +- .../nio/NioServerSocketChannelTest.java | 18 + transport/test.log | 0 1063 files changed, 46237 insertions(+), 14229 deletions(-) create mode 100644 .lgtm.yml create mode 100644 buffer/src/main/resources/META-INF/native-image/io.netty/buffer/native-image.properties create mode 100644 codec-dns/src/main/java/io/netty/handler/codec/dns/DnsCodecUtil.java create mode 100644 codec-dns/src/main/java/io/netty/handler/codec/dns/DnsQueryEncoder.java create mode 100644 codec-dns/src/main/java/io/netty/handler/codec/dns/DnsResponseDecoder.java create mode 100644 codec-dns/src/main/java/io/netty/handler/codec/dns/TcpDnsQueryEncoder.java create mode 100644 codec-dns/src/main/java/io/netty/handler/codec/dns/TcpDnsResponseDecoder.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/websocketx/CorruptedWebSocketFrameException.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientProtocolConfig.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketCloseStatus.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketDecoderConfig.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerProtocolConfig.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketExtensionFilter.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketExtensionFilterProvider.java create mode 100644 codec-http/src/main/resources/META-INF/native-image/io.netty/codec-http/native-image.properties create mode 100644 codec-http/src/test/java/io/netty/handler/codec/http/HttpContentDecompressorTest.java create mode 100644 codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketCloseStatusTest.java create mode 100644 codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketExtensionFilterProviderTest.java create mode 100644 codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketExtensionFilterTest.java create mode 100644 codec-http/src/test/resources/file-03.txt create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/AbstractHttp2StreamChannel.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2SettingsAckFrame.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/Http2ControlFrameLimitEncoder.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/Http2EmptyDataFrameConnectionDecoder.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/Http2EmptyDataFrameListener.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/Http2MultiplexHandler.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/Http2SettingsAckFrame.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/Http2SettingsReceivedConsumer.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/MaxCapacityQueue.java create mode 100644 codec-http2/src/main/resources/META-INF/native-image/io.netty/codec-http2/native-image.properties create mode 100644 codec-http2/src/test/java/io/netty/handler/codec/http2/DecoratingHttp2ConnectionEncoderTest.java create mode 100644 codec-http2/src/test/java/io/netty/handler/codec/http2/Http2ControlFrameLimitEncoderTest.java create mode 100644 codec-http2/src/test/java/io/netty/handler/codec/http2/Http2DefaultFramesTest.java create mode 100644 codec-http2/src/test/java/io/netty/handler/codec/http2/Http2EmptyDataFrameConnectionDecoderTest.java create mode 100644 codec-http2/src/test/java/io/netty/handler/codec/http2/Http2EmptyDataFrameListenerTest.java create mode 100644 codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MultiplexClientUpgradeTest.java create mode 100644 codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MultiplexHandlerClientUpgradeTest.java create mode 100644 codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MultiplexHandlerTest.java create mode 100644 codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MultiplexTest.java create mode 100644 codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MultiplexTransportTest.java create mode 100644 codec-http2/src/test/java/io/netty/handler/codec/http2/Http2StreamChannelIdTest.java create mode 100644 codec-memcache/src/test/java/io/netty/handler/codec/memcache/binary/DefaultFullBinaryMemcacheRequestTest.java create mode 100644 codec-memcache/src/test/java/io/netty/handler/codec/memcache/binary/DefaultFullBinaryMemcacheResponseTest.java create mode 100644 codec-socks/src/test/java/io/netty/handler/codec/socksx/v5/Socks5InitialRequestDecoderTest.java create mode 100644 codec/src/main/java/io/netty/handler/codec/compression/Lz4XXHash32.java create mode 100644 codec/src/test/java/io/netty/handler/codec/compression/ByteBufChecksumTest.java create mode 100644 codec/src/test/java/io/netty/handler/codec/compression/LengthAwareLzfIntegrationTest.java create mode 100644 common/src/main/java/io/netty/util/internal/Hidden.java create mode 100644 common/src/main/java/io/netty/util/internal/ObjectPool.java create mode 100644 common/src/main/java/io/netty/util/internal/ReferenceCountUpdater.java create mode 100644 common/src/main/java/io/netty/util/internal/ThreadExecutorMap.java create mode 100644 common/src/main/java/io/netty/util/internal/svm/CleanerJava6Substitution.java create mode 100644 common/src/main/java/io/netty/util/internal/svm/PlatformDependent0Substitution.java create mode 100644 common/src/main/java/io/netty/util/internal/svm/PlatformDependentSubstitution.java create mode 100644 common/src/main/java/io/netty/util/internal/svm/UnsafeRefArrayAccessSubstitution.java create mode 100644 common/src/main/java/io/netty/util/internal/svm/package-info.java create mode 100644 common/src/main/resources/META-INF/native-image/io.netty/common/native-image.properties create mode 100644 common/src/main/resources/META-INF/services/reactor.blockhound.integration.BlockHoundIntegration create mode 100644 common/src/test/java/io/netty/util/concurrent/ImmediateExecutorTest.java create mode 100644 common/src/test/java/io/netty/util/internal/MathUtilTest.java create mode 100644 common/src/test/java/io/netty/util/internal/ThreadExecutorMapTest.java create mode 100644 docker/docker-compose.centos-6.graalvm1.yaml create mode 100644 docker/docker-compose.centos-6.openj9111.yaml create mode 100644 example/src/main/java/io/netty/example/http2/helloworld/frame/client/Http2ClientFrameInitializer.java create mode 100644 example/src/main/java/io/netty/example/http2/helloworld/frame/client/Http2ClientStreamFrameResponseHandler.java create mode 100644 example/src/main/java/io/netty/example/http2/helloworld/frame/client/Http2FrameClient.java create mode 100644 example/src/main/java/io/netty/example/mqtt/heartBeat/MqttHeartBeatBroker.java create mode 100644 example/src/main/java/io/netty/example/mqtt/heartBeat/MqttHeartBeatBrokerHandler.java create mode 100644 example/src/main/java/io/netty/example/mqtt/heartBeat/MqttHeartBeatClient.java create mode 100644 example/src/main/java/io/netty/example/mqtt/heartBeat/MqttHeartBeatClientHandler.java create mode 100644 handler/src/main/java/io/netty/handler/address/DynamicAddressConnectHandler.java create mode 100644 handler/src/main/java/io/netty/handler/address/ResolveAddressHandler.java create mode 100644 handler/src/main/java/io/netty/handler/address/package-info.java create mode 100644 handler/src/main/java/io/netty/handler/logging/ByteBufFormat.java create mode 100644 handler/src/main/java/io/netty/handler/ssl/KeyManagerFactoryWrapper.java create mode 100644 handler/src/main/java/io/netty/handler/ssl/OpenSslPrivateKeyMethod.java create mode 100644 handler/src/main/java/io/netty/handler/ssl/PseudoRandomFunction.java create mode 100644 handler/src/main/java/io/netty/handler/ssl/SslClientHelloHandler.java create mode 100644 handler/src/main/java/io/netty/handler/ssl/SslHandshakeTimeoutException.java create mode 100644 handler/src/main/java/io/netty/handler/ssl/SslMasterKeyHandler.java create mode 100644 handler/src/main/java/io/netty/handler/ssl/TrustManagerFactoryWrapper.java create mode 100644 handler/src/main/java/io/netty/handler/ssl/util/SimpleKeyManagerFactory.java create mode 100644 handler/src/main/java/io/netty/handler/ssl/util/X509KeyManagerWrapper.java create mode 100644 handler/src/main/resources/META-INF/native-image/io.netty/handler/native-image.properties create mode 100644 handler/src/test/java/io/netty/handler/address/DynamicAddressConnectHandlerTest.java create mode 100644 handler/src/test/java/io/netty/handler/address/ResolveAddressHandlerTest.java create mode 100644 handler/src/test/java/io/netty/handler/ssl/AmazonCorrettoSslEngineTest.java create mode 100644 handler/src/test/java/io/netty/handler/ssl/ConscryptOpenSslEngineInteropTest.java create mode 100644 handler/src/test/java/io/netty/handler/ssl/OpenSslConscryptSslEngineInteropTest.java create mode 100644 handler/src/test/java/io/netty/handler/ssl/OpenSslPrivateKeyMethodTest.java create mode 100644 handler/src/test/java/io/netty/handler/ssl/PseudoRandomFunctionTest.java create mode 100644 handler/src/test/java/io/netty/handler/timeout/IdleStateEventTest.java create mode 100644 license/LICENSE.dnsinfo.txt create mode 100644 license/LICENSE.hyper-hpack.txt create mode 100644 license/LICENSE.nghttp2-hpack.txt create mode 100644 microbench/src/main/java/io/netty/buffer/ByteBufAccessBenchmark.java create mode 100644 microbench/src/main/java/io/netty/handler/codec/DateFormatter2Benchmark.java create mode 100644 microbench/src/main/java/io/netty/handler/codec/http/DecodeHexBenchmark.java create mode 100644 microbench/src/main/java/io/netty/handler/codec/http/QueryStringDecoderBenchmark.java create mode 100644 microbench/src/main/java/io/netty/handler/codec/http/QueryStringEncoderBenchmark.java create mode 100644 microbench/src/main/java/io/netty/microbench/channel/DefaultChannelPipelineBenchmark.java create mode 100644 microbench/src/main/java/io/netty/util/concurrent/ScheduleFutureTaskBenchmark.java create mode 100644 microbench/src/main/java/io/netty/util/concurrent/package-info.java create mode 100644 resolver-dns-native-macos/pom.xml create mode 100644 resolver-dns-native-macos/src/main/c/dnsinfo.h create mode 100644 resolver-dns-native-macos/src/main/c/netty_resolver_dns_macos.c create mode 100644 resolver-dns-native-macos/src/main/java/io/netty/resolver/dns/macos/DnsResolver.java create mode 100644 resolver-dns-native-macos/src/main/java/io/netty/resolver/dns/macos/MacOSDnsServerAddressStreamProvider.java create mode 100644 resolver-dns-native-macos/src/main/java/io/netty/resolver/dns/macos/package-info.java create mode 100644 resolver-dns-native-macos/src/test/java/io/netty/resolver/dns/macos/MacOSDnsServerAddressStreamProviderTest.java create mode 100644 resolver-dns/src/main/java/io/netty/resolver/dns/DatagramDnsQueryContext.java create mode 100644 resolver-dns/src/main/java/io/netty/resolver/dns/PreferredAddressTypeComparator.java create mode 100644 resolver-dns/src/main/java/io/netty/resolver/dns/TcpDnsQueryContext.java create mode 100644 resolver-dns/src/test/java/io/netty/resolver/dns/DnsServerAddressStreamProvidersTest.java create mode 100644 resolver-dns/src/test/java/io/netty/resolver/dns/PreferredAddressTypeComparatorTest.java create mode 100644 testsuite-native-image/pom.xml create mode 100644 testsuite-native-image/src/main/java/io/netty/testsuite/svm/HttpNativeServer.java create mode 100644 testsuite-native-image/src/main/java/io/netty/testsuite/svm/HttpNativeServerHandler.java create mode 100644 testsuite-native-image/src/main/java/io/netty/testsuite/svm/HttpNativeServerInitializer.java create mode 100644 testsuite-native-image/src/main/java/io/netty/testsuite/svm/package-info.java create mode 100644 testsuite/src/main/java/io/netty/testsuite/transport/AbstractSingleThreadEventLoopTest.java create mode 100644 testsuite/src/main/java/io/netty/testsuite/transport/DefaultEventLoopTest.java create mode 100644 testsuite/src/main/java/io/netty/testsuite/transport/NioEventLoopTest.java create mode 100644 testsuite/src/main/java/io/netty/testsuite/transport/socket/AbstractSocketReuseFdTest.java create mode 100644 testsuite/src/main/java/io/netty/testsuite/transport/socket/AbstractSocketShutdownOutputByPeerTest.java create mode 100644 testsuite/src/main/java/io/netty/testsuite/transport/socket/DatagramMulticastIPv6Test.java create mode 100644 testsuite/src/main/java/io/netty/testsuite/transport/socket/DatagramUnicastIPv6MappedTest.java create mode 100644 testsuite/src/main/java/io/netty/testsuite/transport/socket/DatagramUnicastIPv6Test.java create mode 100644 transport-blockhound-tests/pom.xml create mode 100644 transport-blockhound-tests/src/test/java/io/netty/util/internal/NettyBlockHoundIntegrationTest.java create mode 100644 transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDatagramChannelTest.java create mode 100644 transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDatagramMulticastIPv6Test.java create mode 100644 transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDatagramMulticastTest.java create mode 100644 transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDatagramScatteringReadTest.java create mode 100644 transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDatagramUnicastIPv6MappedTest.java create mode 100644 transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDatagramUnicastIPv6Test.java create mode 100644 transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDomainSocketReuseFdTest.java create mode 100644 transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDomainSocketShutdownOutputByPeerTest.java create mode 100644 transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDomainSocketSslClientRenegotiateTest.java create mode 100644 transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollSocketSslClientRenegotiateTest.java create mode 100644 transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollSocketSslSessionReuseTest.java create mode 100644 transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollWriteBeforeRegisteredTest.java create mode 100644 transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueDatagramUnicastIPv6MappedTest.java create mode 100644 transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueDatagramUnicastIPv6Test.java create mode 100644 transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueDomainSocketReuseFdTest.java create mode 100644 transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueDomainSocketShutdownOutputByPeerTest.java create mode 100644 transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueDomainSocketSslClientRenegotiateTest.java create mode 100644 transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueSocketSslClientRenegotiateTest.java create mode 100644 transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueSocketSslSessionReuseTest.java create mode 100644 transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KqueueWriteBeforeRegisteredTest.java create mode 100644 transport/src/main/java/io/netty/channel/ChannelHandlerMask.java create mode 100644 transport/src/main/java/io/netty/channel/EventLoopTaskQueueFactory.java create mode 100644 transport/src/main/java/io/netty/channel/ExtendedClosedChannelException.java create mode 100644 transport/src/main/resources/META-INF/native-image/io.netty/transport/native-image.properties create mode 100644 transport/src/main/resources/META-INF/native-image/io.netty/transport/reflection-config.json create mode 100644 transport/src/test/java/io/netty/channel/DefaultFileRegionTest.java create mode 100644 transport/src/test/java/io/netty/channel/pool/ChannelPoolTestUtils.java create mode 100644 transport/src/test/java/io/netty/channel/pool/FixedChannelPoolMapDeadlockTest.java delete mode 100644 transport/test.log diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 81c2cea..8ca9413 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1 +1 @@ -Please review the [guidelines for contributing](http://netty.io/wiki/developer-guide.html) for this repository. +Please review the [guidelines for contributing](https://netty.io/wiki/developer-guide.html) for this repository. diff --git a/.gitignore b/.gitignore index ed43883..8af92c7 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,11 @@ dependency-reduced-pom.xml # exclude mainframer files mainframer .mainframer + +# exclude docker-sync stuff +.docker-sync +*/.docker-sync + +# exclude vscode files +.vscode/ +*.factorypath diff --git a/.lgtm.yml b/.lgtm.yml new file mode 100644 index 0000000..9aa74b1 --- /dev/null +++ b/.lgtm.yml @@ -0,0 +1,13 @@ +extraction: + java: + prepare: + packages: + - "autoconf" + - "automake" + - "libtool" + - "make" + - "tar" + - "libaio-dev" + - "libssl-dev" + - "libapr1-dev" + - "lksctp-tools" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1a8b4d8..a18232f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -42,5 +42,5 @@ My system has IPv6 disabled. ## How to contribute your work -Before submitting a pull request or push a commit, please read [our developer guide](http://netty.io/wiki/developer-guide.html). +Before submitting a pull request or push a commit, please read [our developer guide](https://netty.io/wiki/developer-guide.html). diff --git a/NOTICE.txt b/NOTICE.txt index f973663..9cf3e81 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -4,7 +4,7 @@ Please visit the Netty web site for more information: - * http://netty.io/ + * https://netty.io/ Copyright 2014 The Netty Project @@ -162,9 +162,9 @@ This product optionally depends on 'JBoss Marshalling', an alternative Java serialization API, which can be obtained at: * LICENSE: - * license/LICENSE.jboss-marshalling.txt (GNU LGPL 2.1) + * license/LICENSE.jboss-marshalling.txt (Apache License 2.0) * HOMEPAGE: - * http://www.jboss.org/jbossmarshalling + * https://github.com/jboss-remoting/jboss-marshalling This product optionally depends on 'Caliper', Google's micro- benchmarking framework, which can be obtained at: @@ -205,6 +205,22 @@ the HTTP/2 HPACK algorithm written by Twitter. It can be obtained at: * license/LICENSE.hpack.txt (Apache License 2.0) * HOMEPAGE: * https://github.com/twitter/hpack + +This product contains a modified version of 'HPACK', a Java implementation of +the HTTP/2 HPACK algorithm written by Cory Benfield. It can be obtained at: + + * LICENSE: + * license/LICENSE.hyper-hpack.txt (MIT License) + * HOMEPAGE: + * https://github.com/python-hyper/hpack/ + +This product contains a modified version of 'HPACK', a Java implementation of +the HTTP/2 HPACK algorithm written by Tatsuhiro Tsujikawa. It can be obtained at: + + * LICENSE: + * license/LICENSE.nghttp2-hpack.txt (MIT License) + * HOMEPAGE: + * https://github.com/nghttp2/nghttp2/ This product contains a modified portion of 'Apache Commons Lang', a Java library provides utilities for the java.lang API, which can be obtained at: @@ -221,3 +237,12 @@ This product contains the Maven wrapper scripts from 'Maven Wrapper', that provi * license/LICENSE.mvn-wrapper.txt (Apache License 2.0) * HOMEPAGE: * https://github.com/takari/maven-wrapper + +This product contains the dnsinfo.h header file, that provides a way to retrieve the system DNS configuration on MacOS. +This private header is also used by Apple's open source + mDNSResponder (https://opensource.apple.com/tarballs/mDNSResponder/). + + * LICENSE: + * license/LICENSE.dnsinfo.txt (Apache License 2.0) + * HOMEPAGE: + * http://www.opensource.apple.com/source/configd/configd-453.19/dnsinfo/dnsinfo.h \ No newline at end of file diff --git a/README.md b/README.md index 5c4c4b5..6b557b1 100644 --- a/README.md +++ b/README.md @@ -4,20 +4,20 @@ Netty is an asynchronous event-driven network application framework for rapid de ## Links -* [Web Site](http://netty.io/) -* [Downloads](http://netty.io/downloads.html) -* [Documentation](http://netty.io/wiki/) +* [Web Site](https://netty.io/) +* [Downloads](https://netty.io/downloads.html) +* [Documentation](https://netty.io/wiki/) * [@netty_project](https://twitter.com/netty_project) ## How to build -For the detailed information about building and developing Netty, please visit [the developer guide](http://netty.io/wiki/developer-guide.html). This page only gives very basic information. +For the detailed information about building and developing Netty, please visit [the developer guide](https://netty.io/wiki/developer-guide.html). This page only gives very basic information. You require the following to build Netty: * Latest stable [Oracle JDK 7](http://www.oracle.com/technetwork/java/) * Latest stable [Apache Maven](http://maven.apache.org/) -* If you are on Linux, you need [additional development packages](http://netty.io/wiki/native-transports.html) installed on your system, because you'll build the native transport. +* If you are on Linux, you need [additional development packages](https://netty.io/wiki/native-transports.html) installed on your system, because you'll build the native transport. Note that this is build-time requirement. JDK 5 (for 3.x) or 6 (for 4.0+) is enough to run your Netty-based application. diff --git a/all/pom.xml b/all/pom.xml index b97afa8..3a11e7d 100644 --- a/all/pom.xml +++ b/all/pom.xml @@ -20,7 +20,7 @@ <parent> <groupId>io.netty</groupId> <artifactId>netty-parent</artifactId> - <version>4.1.33.Final</version> + <version>4.1.48.Final</version> </parent> <artifactId>netty-all</artifactId> @@ -31,6 +31,7 @@ <properties> <generatedSourceDir>${project.build.directory}/src</generatedSourceDir> <dependencyVersionsDir>${project.build.directory}/versions</dependencyVersionsDir> + <skipJapicmp>true</skipJapicmp> </properties> <profiles> @@ -64,6 +65,14 @@ <scope>compile</scope> <optional>true</optional> </dependency> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>netty-resolver-dns-native-macos</artifactId> + <version>${project.version}</version> + <classifier>osx-x86_64</classifier> + <scope>compile</scope> + <optional>true</optional> + </dependency> </dependencies> </profile> <profile> @@ -88,6 +97,14 @@ <scope>compile</scope> <optional>true</optional> </dependency> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>netty-resolver-dns-native-macos</artifactId> + <version>${project.version}</version> + <classifier>osx-x86_64</classifier> + <scope>compile</scope> + <optional>true</optional> + </dependency> </dependencies> </profile> @@ -111,6 +128,21 @@ <scope>compile</scope> <optional>true</optional> </dependency> + <!-- Just include the classes for the other platform so these are at least present in the netty-all artifact --> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>netty-transport-native-kqueue</artifactId> + <version>${project.version}</version> + <scope>compile</scope> + <optional>true</optional> + </dependency> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>netty-resolver-dns-native-macos</artifactId> + <version>${project.version}</version> + <scope>compile</scope> + <optional>true</optional> + </dependency> </dependencies> </profile> <!-- The mac, openbsd and freebsd profile will only include the native jar for epol to the all jar. @@ -133,6 +165,22 @@ <scope>compile</scope> <optional>true</optional> </dependency> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>netty-resolver-dns-native-macos</artifactId> + <version>${project.version}</version> + <classifier>osx-x86_64</classifier> + <scope>compile</scope> + <optional>true</optional> + </dependency> + <!-- Just include the classes for the other platform so these are at least present in the netty-all artifact --> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>netty-transport-native-epoll</artifactId> + <version>${project.version}</version> + <scope>compile</scope> + <optional>true</optional> + </dependency> </dependencies> </profile> <profile> @@ -153,6 +201,14 @@ <scope>compile</scope> <optional>true</optional> </dependency> + <!-- Just include the classes for the other platform so these are at least present in the netty-all artifact --> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>netty-transport-native-epoll</artifactId> + <version>${project.version}</version> + <scope>compile</scope> + <optional>true</optional> + </dependency> </dependencies> </profile> <profile> @@ -173,6 +229,14 @@ <scope>compile</scope> <optional>true</optional> </dependency> + <!-- Just include the classes for the other platform so these are at least present in the netty-all artifact --> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>netty-transport-native-epoll</artifactId> + <version>${project.version}</version> + <scope>compile</scope> + <optional>true</optional> + </dependency> </dependencies> </profile> @@ -205,20 +269,7 @@ <dependency> <groupId>${project.groupId}</groupId> <artifactId>netty-build</artifactId> - <version>19</version> - <exclusions> - <!-- Use version 7.3 until a new netty-build release is out --> - <!-- See https://issues.apache.org/jira/browse/JXR-133 --> - <exclusion> - <groupId>com.puppycrawl.tools</groupId> - <artifactId>checkstyle</artifactId> - </exclusion> - </exclusions> - </dependency> - <dependency> - <groupId>com.puppycrawl.tools</groupId> - <artifactId>checkstyle</artifactId> - <version>7.3</version> + <version>${netty.build.version}</version> </dependency> </dependencies> </plugin> @@ -552,7 +603,7 @@ </goals> <configuration> <excludes>io/netty/internal/tcnative/**,io/netty/example/**,META-INF/native/libnetty_tcnative*,META-INF/native/include/**,META-INF/native/**/*.a</excludes> - <includes>io/netty/**,META-INF/native/**</includes> + <includes>io/netty/**,META-INF/native/**,META-INF/native-image/**</includes> <includeScope>runtime</includeScope> <includeGroupIds>${project.groupId}</includeGroupIds> <outputDirectory>${project.build.outputDirectory}</outputDirectory> diff --git a/bom/pom.xml b/bom/pom.xml index 34d6b7a..685acaf 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -25,16 +25,16 @@ <groupId>io.netty</groupId> <artifactId>netty-bom</artifactId> - <version>4.1.33.Final</version> + <version>4.1.48.Final</version> <packaging>pom</packaging> <name>Netty/BOM</name> <description>Netty (Bill of Materials)</description> - <url>http://netty.io/</url> + <url>https://netty.io/</url> <organization> <name>The Netty Project</name> - <url>http://netty.io/</url> + <url>https://netty.io/</url> </organization> <licenses> @@ -49,7 +49,7 @@ <url>https://github.com/netty/netty</url> <connection>scm:git:git://github.com/netty/netty.git</connection> <developerConnection>scm:git:ssh://git@github.com/netty/netty.git</developerConnection> - <tag>netty-4.1.33.Final</tag> + <tag>netty-4.1.48.Final</tag> </scm> <developers> @@ -57,9 +57,9 @@ <id>netty.io</id> <name>The Netty Project Contributors</name> <email>netty@googlegroups.com</email> - <url>http://netty.io/</url> + <url>https://netty.io/</url> <organization>The Netty Project</organization> - <organizationUrl>http://netty.io/</organizationUrl> + <organizationUrl>https://netty.io/</organizationUrl> </developer> </developers> @@ -69,165 +69,165 @@ <dependency> <groupId>io.netty</groupId> <artifactId>netty-buffer</artifactId> - <version>4.1.33.Final</version> + <version>4.1.48.Final</version> </dependency> <dependency> <groupId>io.netty</groupId> <artifactId>netty-codec</artifactId> - <version>4.1.33.Final</version> + <version>4.1.48.Final</version> </dependency> <dependency> <groupId>io.netty</groupId> <artifactId>netty-codec-dns</artifactId> - <version>4.1.33.Final</version> + <version>4.1.48.Final</version> </dependency> <dependency> <groupId>io.netty</groupId> <artifactId>netty-codec-haproxy</artifactId> - <version>4.1.33.Final</version> + <version>4.1.48.Final</version> </dependency> <dependency> <groupId>io.netty</groupId> <artifactId>netty-codec-http</artifactId> - <version>4.1.33.Final</version> + <version>4.1.48.Final</version> </dependency> <dependency> <groupId>io.netty</groupId> <artifactId>netty-codec-http2</artifactId> - <version>4.1.33.Final</version> + <version>4.1.48.Final</version> </dependency> <dependency> <groupId>io.netty</groupId> <artifactId>netty-codec-memcache</artifactId> - <version>4.1.33.Final</version> + <version>4.1.48.Final</version> </dependency> <dependency> <groupId>io.netty</groupId> <artifactId>netty-codec-mqtt</artifactId> - <version>4.1.33.Final</version> + <version>4.1.48.Final</version> </dependency> <dependency> <groupId>io.netty</groupId> <artifactId>netty-codec-redis</artifactId> - <version>4.1.33.Final</version> + <version>4.1.48.Final</version> </dependency> <dependency> <groupId>io.netty</groupId> <artifactId>netty-codec-smtp</artifactId> - <version>4.1.33.Final</version> + <version>4.1.48.Final</version> </dependency> <dependency> <groupId>io.netty</groupId> <artifactId>netty-codec-socks</artifactId> - <version>4.1.33.Final</version> + <version>4.1.48.Final</version> </dependency> <dependency> <groupId>io.netty</groupId> <artifactId>netty-codec-stomp</artifactId> - <version>4.1.33.Final</version> + <version>4.1.48.Final</version> </dependency> <dependency> <groupId>io.netty</groupId> <artifactId>netty-codec-xml</artifactId> - <version>4.1.33.Final</version> + <version>4.1.48.Final</version> </dependency> <dependency> <groupId>io.netty</groupId> <artifactId>netty-common</artifactId> - <version>4.1.33.Final</version> + <version>4.1.48.Final</version> </dependency> <dependency> <groupId>io.netty</groupId> <artifactId>netty-dev-tools</artifactId> - <version>4.1.33.Final</version> + <version>4.1.48.Final</version> </dependency> <dependency> <groupId>io.netty</groupId> <artifactId>netty-handler</artifactId> - <version>4.1.33.Final</version> + <version>4.1.48.Final</version> </dependency> <dependency> <groupId>io.netty</groupId> <artifactId>netty-handler-proxy</artifactId> - <version>4.1.33.Final</version> + <version>4.1.48.Final</version> </dependency> <dependency> <groupId>io.netty</groupId> <artifactId>netty-resolver</artifactId> - <version>4.1.33.Final</version> + <version>4.1.48.Final</version> </dependency> <dependency> <groupId>io.netty</groupId> <artifactId>netty-resolver-dns</artifactId> - <version>4.1.33.Final</version> + <version>4.1.48.Final</version> </dependency> <dependency> <groupId>io.netty</groupId> <artifactId>netty-transport</artifactId> - <version>4.1.33.Final</version> + <version>4.1.48.Final</version> </dependency> <dependency> <groupId>io.netty</groupId> <artifactId>netty-transport-rxtx</artifactId> - <version>4.1.33.Final</version> + <version>4.1.48.Final</version> </dependency> <dependency> <groupId>io.netty</groupId> <artifactId>netty-transport-sctp</artifactId> - <version>4.1.33.Final</version> + <version>4.1.48.Final</version> </dependency> <dependency> <groupId>io.netty</groupId> <artifactId>netty-transport-udt</artifactId> - <version>4.1.33.Final</version> + <version>4.1.48.Final</version> </dependency> <dependency> <groupId>io.netty</groupId> <artifactId>netty-example</artifactId> - <version>4.1.33.Final</version> + <version>4.1.48.Final</version> </dependency> <dependency> <groupId>io.netty</groupId> <artifactId>netty-all</artifactId> - <version>4.1.33.Final</version> + <version>4.1.48.Final</version> </dependency> <dependency> <groupId>io.netty</groupId> <artifactId>netty-transport-native-unix-common</artifactId> - <version>4.1.33.Final</version> + <version>4.1.48.Final</version> </dependency> <dependency> <groupId>io.netty</groupId> <artifactId>netty-transport-native-unix-common</artifactId> - <version>4.1.33.Final</version> + <version>4.1.48.Final</version> <classifier>linux-x86_64</classifier> </dependency> <dependency> <groupId>io.netty</groupId> <artifactId>netty-transport-native-unix-common</artifactId> - <version>4.1.33.Final</version> + <version>4.1.48.Final</version> <classifier>osx-x86_64</classifier> </dependency> <dependency> <groupId>io.netty</groupId> <artifactId>netty-transport-native-epoll</artifactId> - <version>4.1.33.Final</version> + <version>4.1.48.Final</version> </dependency> <dependency> <groupId>io.netty</groupId> <artifactId>netty-transport-native-epoll</artifactId> - <version>4.1.33.Final</version> + <version>4.1.48.Final</version> <classifier>linux-x86_64</classifier> </dependency> <dependency> <groupId>io.netty</groupId> <artifactId>netty-transport-native-kqueue</artifactId> - <version>4.1.33.Final</version> + <version>4.1.48.Final</version> </dependency> <dependency> <groupId>io.netty</groupId> <artifactId>netty-transport-native-kqueue</artifactId> - <version>4.1.33.Final</version> + <version>4.1.48.Final</version> <classifier>osx-x86_64</classifier> </dependency> </dependencies> diff --git a/buffer/pom.xml b/buffer/pom.xml index 3ea7bd8..cd859ed 100644 --- a/buffer/pom.xml +++ b/buffer/pom.xml @@ -20,7 +20,7 @@ <parent> <groupId>io.netty</groupId> <artifactId>netty-parent</artifactId> - <version>4.1.33.Final</version> + <version>4.1.48.Final</version> </parent> <artifactId>netty-buffer</artifactId> diff --git a/buffer/src/main/java/io/netty/buffer/AbstractByteBuf.java b/buffer/src/main/java/io/netty/buffer/AbstractByteBuf.java index 6864cee..9ce8ccc 100644 --- a/buffer/src/main/java/io/netty/buffer/AbstractByteBuf.java +++ b/buffer/src/main/java/io/netty/buffer/AbstractByteBuf.java @@ -21,6 +21,7 @@ import io.netty.util.CharsetUtil; import io.netty.util.IllegalReferenceCountException; import io.netty.util.ResourceLeakDetector; import io.netty.util.ResourceLeakDetectorFactory; +import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.PlatformDependent; import io.netty.util.internal.StringUtil; import io.netty.util.internal.SystemPropertyUtil; @@ -38,6 +39,7 @@ import java.nio.channels.ScatteringByteChannel; import java.nio.charset.Charset; import static io.netty.util.internal.MathUtil.isOutOfBounds; +import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero; /** * A skeletal implementation of a buffer. @@ -73,9 +75,7 @@ public abstract class AbstractByteBuf extends ByteBuf { private int maxCapacity; protected AbstractByteBuf(int maxCapacity) { - if (maxCapacity < 0) { - throw new IllegalArgumentException("maxCapacity: " + maxCapacity + " (expected: >= 0)"); - } + checkPositiveOrZero(maxCapacity, "maxCapacity"); this.maxCapacity = maxCapacity; } @@ -214,8 +214,8 @@ public abstract class AbstractByteBuf extends ByteBuf { @Override public ByteBuf discardReadBytes() { - ensureAccessible(); if (readerIndex == 0) { + ensureAccessible(); return this; } @@ -225,6 +225,7 @@ public abstract class AbstractByteBuf extends ByteBuf { adjustMarkers(readerIndex); readerIndex = 0; } else { + ensureAccessible(); adjustMarkers(readerIndex); writerIndex = readerIndex = 0; } @@ -233,23 +234,23 @@ public abstract class AbstractByteBuf extends ByteBuf { @Override public ByteBuf discardSomeReadBytes() { - ensureAccessible(); - if (readerIndex == 0) { - return this; - } - - if (readerIndex == writerIndex) { - adjustMarkers(readerIndex); - writerIndex = readerIndex = 0; - return this; - } + if (readerIndex > 0) { + if (readerIndex == writerIndex) { + ensureAccessible(); + adjustMarkers(readerIndex); + writerIndex = readerIndex = 0; + return this; + } - if (readerIndex >= capacity() >>> 1) { - setBytes(0, this, readerIndex, writerIndex - readerIndex); - writerIndex -= readerIndex; - adjustMarkers(readerIndex); - readerIndex = 0; + if (readerIndex >= capacity() >>> 1) { + setBytes(0, this, readerIndex, writerIndex - readerIndex); + writerIndex -= readerIndex; + adjustMarkers(readerIndex); + readerIndex = 0; + return this; + } } + ensureAccessible(); return this; } @@ -269,31 +270,37 @@ public abstract class AbstractByteBuf extends ByteBuf { } } + // Called after a capacity reduction + protected final void trimIndicesToCapacity(int newCapacity) { + if (writerIndex() > newCapacity) { + setIndex0(Math.min(readerIndex(), newCapacity), newCapacity); + } + } + @Override public ByteBuf ensureWritable(int minWritableBytes) { - if (minWritableBytes < 0) { - throw new IllegalArgumentException(String.format( - "minWritableBytes: %d (expected: >= 0)", minWritableBytes)); - } - ensureWritable0(minWritableBytes); + ensureWritable0(checkPositiveOrZero(minWritableBytes, "minWritableBytes")); return this; } final void ensureWritable0(int minWritableBytes) { - ensureAccessible(); - if (minWritableBytes <= writableBytes()) { + final int writerIndex = writerIndex(); + final int targetCapacity = writerIndex + minWritableBytes; + if (targetCapacity <= capacity()) { + ensureAccessible(); return; } - if (checkBounds) { - if (minWritableBytes > maxCapacity - writerIndex) { - throw new IndexOutOfBoundsException(String.format( - "writerIndex(%d) + minWritableBytes(%d) exceeds maxCapacity(%d): %s", - writerIndex, minWritableBytes, maxCapacity, this)); - } + if (checkBounds && targetCapacity > maxCapacity) { + ensureAccessible(); + throw new IndexOutOfBoundsException(String.format( + "writerIndex(%d) + minWritableBytes(%d) exceeds maxCapacity(%d): %s", + writerIndex, minWritableBytes, maxCapacity, this)); } - // Normalize the current capacity to the power of 2. - int newCapacity = alloc().calculateNewCapacity(writerIndex + minWritableBytes, maxCapacity); + // Normalize the target capacity to the power of 2. + final int fastWritable = maxFastWritableBytes(); + int newCapacity = fastWritable >= minWritableBytes ? writerIndex + fastWritable + : alloc().calculateNewCapacity(targetCapacity, maxCapacity); // Adjust to the new capacity. capacity(newCapacity); @@ -302,10 +309,7 @@ public abstract class AbstractByteBuf extends ByteBuf { @Override public int ensureWritable(int minWritableBytes, boolean force) { ensureAccessible(); - if (minWritableBytes < 0) { - throw new IllegalArgumentException(String.format( - "minWritableBytes: %d (expected: >= 0)", minWritableBytes)); - } + checkPositiveOrZero(minWritableBytes, "minWritableBytes"); if (minWritableBytes <= writableBytes()) { return 0; @@ -322,8 +326,9 @@ public abstract class AbstractByteBuf extends ByteBuf { return 3; } - // Normalize the current capacity to the power of 2. - int newCapacity = alloc().calculateNewCapacity(writerIndex + minWritableBytes, maxCapacity); + int fastWritable = maxFastWritableBytes(); + int newCapacity = fastWritable >= minWritableBytes ? writerIndex + fastWritable + : alloc().calculateNewCapacity(writerIndex + minWritableBytes, maxCapacity); // Adjust to the new capacity. capacity(newCapacity); @@ -335,9 +340,7 @@ public abstract class AbstractByteBuf extends ByteBuf { if (endianness == order()) { return this; } - if (endianness == null) { - throw new NullPointerException("endianness"); - } + ObjectUtil.checkNotNull(endianness, "endianness"); return newSwappedByteBuf(); } @@ -645,9 +648,7 @@ public abstract class AbstractByteBuf extends ByteBuf { @Override public ByteBuf setBytes(int index, ByteBuf src, int length) { checkIndex(index, length); - if (src == null) { - throw new NullPointerException("src"); - } + ObjectUtil.checkNotNull(src, "src"); if (checkBounds) { checkReadableBounds(src, length); } @@ -1248,7 +1249,44 @@ public abstract class AbstractByteBuf extends ByteBuf { @Override public int indexOf(int fromIndex, int toIndex, byte value) { - return ByteBufUtil.indexOf(this, fromIndex, toIndex, value); + if (fromIndex <= toIndex) { + return firstIndexOf(fromIndex, toIndex, value); + } else { + return lastIndexOf(fromIndex, toIndex, value); + } + } + + private int firstIndexOf(int fromIndex, int toIndex, byte value) { + fromIndex = Math.max(fromIndex, 0); + if (fromIndex >= toIndex || capacity() == 0) { + return -1; + } + checkIndex(fromIndex, toIndex - fromIndex); + + for (int i = fromIndex; i < toIndex; i ++) { + if (_getByte(i) == value) { + return i; + } + } + + return -1; + } + + private int lastIndexOf(int fromIndex, int toIndex, byte value) { + fromIndex = Math.min(fromIndex, capacity()); + if (fromIndex < 0 || capacity() == 0) { + return -1; + } + + checkIndex(toIndex, fromIndex - toIndex); + + for (int i = fromIndex - 1; i >= toIndex; i --) { + if (_getByte(i) == value) { + return i; + } + } + + return -1; } @Override @@ -1381,30 +1419,38 @@ public abstract class AbstractByteBuf extends ByteBuf { checkIndex0(index, fieldLength); } - private static void checkRangeBounds(final int index, final int fieldLength, final int capacity) { + private static void checkRangeBounds(final String indexName, final int index, + final int fieldLength, final int capacity) { if (isOutOfBounds(index, fieldLength, capacity)) { throw new IndexOutOfBoundsException(String.format( - "index: %d, length: %d (expected: range(0, %d))", index, fieldLength, capacity)); + "%s: %d, length: %d (expected: range(0, %d))", indexName, index, fieldLength, capacity)); } } final void checkIndex0(int index, int fieldLength) { if (checkBounds) { - checkRangeBounds(index, fieldLength, capacity()); + checkRangeBounds("index", index, fieldLength, capacity()); } } protected final void checkSrcIndex(int index, int length, int srcIndex, int srcCapacity) { checkIndex(index, length); if (checkBounds) { - checkRangeBounds(srcIndex, length, srcCapacity); + checkRangeBounds("srcIndex", srcIndex, length, srcCapacity); } } protected final void checkDstIndex(int index, int length, int dstIndex, int dstCapacity) { checkIndex(index, length); if (checkBounds) { - checkRangeBounds(dstIndex, length, dstCapacity); + checkRangeBounds("dstIndex", dstIndex, length, dstCapacity); + } + } + + protected final void checkDstIndex(int length, int dstIndex, int dstCapacity) { + checkReadableBytes(length); + if (checkBounds) { + checkRangeBounds("dstIndex", dstIndex, length, dstCapacity); } } @@ -1414,30 +1460,23 @@ public abstract class AbstractByteBuf extends ByteBuf { * than the specified value. */ protected final void checkReadableBytes(int minimumReadableBytes) { - if (minimumReadableBytes < 0) { - throw new IllegalArgumentException("minimumReadableBytes: " + minimumReadableBytes + " (expected: >= 0)"); - } - checkReadableBytes0(minimumReadableBytes); + checkReadableBytes0(checkPositiveOrZero(minimumReadableBytes, "minimumReadableBytes")); } protected final void checkNewCapacity(int newCapacity) { ensureAccessible(); - if (checkBounds) { - if (newCapacity < 0 || newCapacity > maxCapacity()) { - throw new IllegalArgumentException("newCapacity: " + newCapacity + - " (expected: 0-" + maxCapacity() + ')'); - } + if (checkBounds && (newCapacity < 0 || newCapacity > maxCapacity())) { + throw new IllegalArgumentException("newCapacity: " + newCapacity + + " (expected: 0-" + maxCapacity() + ')'); } } private void checkReadableBytes0(int minimumReadableBytes) { ensureAccessible(); - if (checkBounds) { - if (readerIndex > writerIndex - minimumReadableBytes) { - throw new IndexOutOfBoundsException(String.format( - "readerIndex(%d) + length(%d) exceeds writerIndex(%d): %s", - readerIndex, minimumReadableBytes, writerIndex, this)); - } + if (checkBounds && readerIndex > writerIndex - minimumReadableBytes) { + throw new IndexOutOfBoundsException(String.format( + "readerIndex(%d) + length(%d) exceeds writerIndex(%d): %s", + readerIndex, minimumReadableBytes, writerIndex, this)); } } @@ -1446,19 +1485,11 @@ public abstract class AbstractByteBuf extends ByteBuf { * if the buffer was released before. */ protected final void ensureAccessible() { - if (checkAccessible && internalRefCnt() == 0) { + if (checkAccessible && !isAccessible()) { throw new IllegalReferenceCountException(0); } } - /** - * Returns the reference count that is used internally by {@link #ensureAccessible()} to try to guard - * against using the buffer after it was released (best-effort). - */ - int internalRefCnt() { - return refCnt(); - } - final void setIndex0(int readerIndex, int writerIndex) { this.readerIndex = readerIndex; this.writerIndex = writerIndex; diff --git a/buffer/src/main/java/io/netty/buffer/AbstractByteBufAllocator.java b/buffer/src/main/java/io/netty/buffer/AbstractByteBufAllocator.java index 4052514..9e15822 100644 --- a/buffer/src/main/java/io/netty/buffer/AbstractByteBufAllocator.java +++ b/buffer/src/main/java/io/netty/buffer/AbstractByteBufAllocator.java @@ -16,6 +16,8 @@ package io.netty.buffer; +import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero; + import io.netty.util.ResourceLeakDetector; import io.netty.util.ResourceLeakTracker; import io.netty.util.internal.PlatformDependent; @@ -125,7 +127,7 @@ public abstract class AbstractByteBufAllocator implements ByteBufAllocator { @Override public ByteBuf ioBuffer() { - if (PlatformDependent.hasUnsafe()) { + if (PlatformDependent.hasUnsafe() || isDirectBufferPooled()) { return directBuffer(DEFAULT_INITIAL_CAPACITY); } return heapBuffer(DEFAULT_INITIAL_CAPACITY); @@ -133,7 +135,7 @@ public abstract class AbstractByteBufAllocator implements ByteBufAllocator { @Override public ByteBuf ioBuffer(int initialCapacity) { - if (PlatformDependent.hasUnsafe()) { + if (PlatformDependent.hasUnsafe() || isDirectBufferPooled()) { return directBuffer(initialCapacity); } return heapBuffer(initialCapacity); @@ -141,7 +143,7 @@ public abstract class AbstractByteBufAllocator implements ByteBufAllocator { @Override public ByteBuf ioBuffer(int initialCapacity, int maxCapacity) { - if (PlatformDependent.hasUnsafe()) { + if (PlatformDependent.hasUnsafe() || isDirectBufferPooled()) { return directBuffer(initialCapacity, maxCapacity); } return heapBuffer(initialCapacity, maxCapacity); @@ -222,9 +224,7 @@ public abstract class AbstractByteBufAllocator implements ByteBufAllocator { } private static void validate(int initialCapacity, int maxCapacity) { - if (initialCapacity < 0) { - throw new IllegalArgumentException("initialCapacity: " + initialCapacity + " (expected: 0+)"); - } + checkPositiveOrZero(initialCapacity, "initialCapacity"); if (initialCapacity > maxCapacity) { throw new IllegalArgumentException(String.format( "initialCapacity: %d (expected: not greater than maxCapacity(%d)", @@ -249,9 +249,7 @@ public abstract class AbstractByteBufAllocator implements ByteBufAllocator { @Override public int calculateNewCapacity(int minNewCapacity, int maxCapacity) { - if (minNewCapacity < 0) { - throw new IllegalArgumentException("minNewCapacity: " + minNewCapacity + " (expected: 0+)"); - } + checkPositiveOrZero(minNewCapacity, "minNewCapacity"); if (minNewCapacity > maxCapacity) { throw new IllegalArgumentException(String.format( "minNewCapacity: %d (expected: not greater than maxCapacity(%d)", diff --git a/buffer/src/main/java/io/netty/buffer/AbstractDerivedByteBuf.java b/buffer/src/main/java/io/netty/buffer/AbstractDerivedByteBuf.java index 58f1d90..3fd10aa 100644 --- a/buffer/src/main/java/io/netty/buffer/AbstractDerivedByteBuf.java +++ b/buffer/src/main/java/io/netty/buffer/AbstractDerivedByteBuf.java @@ -31,6 +31,11 @@ public abstract class AbstractDerivedByteBuf extends AbstractByteBuf { super(maxCapacity); } + @Override + final boolean isAccessible() { + return unwrap().isAccessible(); + } + @Override public final int refCnt() { return refCnt0(); @@ -112,4 +117,9 @@ public abstract class AbstractDerivedByteBuf extends AbstractByteBuf { public ByteBuffer nioBuffer(int index, int length) { return unwrap().nioBuffer(index, length); } + + @Override + public boolean isContiguous() { + return unwrap().isContiguous(); + } } diff --git a/buffer/src/main/java/io/netty/buffer/AbstractPooledDerivedByteBuf.java b/buffer/src/main/java/io/netty/buffer/AbstractPooledDerivedByteBuf.java index 3388b14..8cf8295 100644 --- a/buffer/src/main/java/io/netty/buffer/AbstractPooledDerivedByteBuf.java +++ b/buffer/src/main/java/io/netty/buffer/AbstractPooledDerivedByteBuf.java @@ -16,8 +16,8 @@ package io.netty.buffer; -import io.netty.util.Recycler.Handle; import io.netty.util.ReferenceCounted; +import io.netty.util.internal.ObjectPool.Handle; import java.nio.ByteBuffer; import java.nio.ByteOrder; @@ -63,7 +63,7 @@ abstract class AbstractPooledDerivedByteBuf extends AbstractReferenceCountedByte try { maxCapacity(maxCapacity); setIndex0(readerIndex, writerIndex); // It is assumed the bounds checking is done by the caller. - setRefCnt(1); + resetRefCnt(); @SuppressWarnings("unchecked") final U castThis = (U) this; @@ -123,6 +123,11 @@ abstract class AbstractPooledDerivedByteBuf extends AbstractReferenceCountedByte return unwrap().hasMemoryAddress(); } + @Override + public boolean isContiguous() { + return unwrap().isContiguous(); + } + @Override public final int nioBufferCount() { return unwrap().nioBufferCount(); diff --git a/buffer/src/main/java/io/netty/buffer/AbstractReferenceCountedByteBuf.java b/buffer/src/main/java/io/netty/buffer/AbstractReferenceCountedByteBuf.java index ba26b19..22eea0e 100644 --- a/buffer/src/main/java/io/netty/buffer/AbstractReferenceCountedByteBuf.java +++ b/buffer/src/main/java/io/netty/buffer/AbstractReferenceCountedByteBuf.java @@ -16,97 +16,73 @@ package io.netty.buffer; -import io.netty.util.IllegalReferenceCountException; -import io.netty.util.internal.PlatformDependent; - import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; -import static io.netty.util.internal.ObjectUtil.checkPositive; +import io.netty.util.internal.ReferenceCountUpdater; /** * Abstract base class for {@link ByteBuf} implementations that count references. */ public abstract class AbstractReferenceCountedByteBuf extends AbstractByteBuf { - private static final long REFCNT_FIELD_OFFSET; - private static final AtomicIntegerFieldUpdater<AbstractReferenceCountedByteBuf> refCntUpdater = + private static final long REFCNT_FIELD_OFFSET = + ReferenceCountUpdater.getUnsafeOffset(AbstractReferenceCountedByteBuf.class, "refCnt"); + private static final AtomicIntegerFieldUpdater<AbstractReferenceCountedByteBuf> AIF_UPDATER = AtomicIntegerFieldUpdater.newUpdater(AbstractReferenceCountedByteBuf.class, "refCnt"); - // even => "real" refcount is (refCnt >>> 1); odd => "real" refcount is 0 - @SuppressWarnings("unused") - private volatile int refCnt = 2; - - static { - long refCntFieldOffset = -1; - try { - if (PlatformDependent.hasUnsafe()) { - refCntFieldOffset = PlatformDependent.objectFieldOffset( - AbstractReferenceCountedByteBuf.class.getDeclaredField("refCnt")); - } - } catch (Throwable ignore) { - refCntFieldOffset = -1; + private static final ReferenceCountUpdater<AbstractReferenceCountedByteBuf> updater = + new ReferenceCountUpdater<AbstractReferenceCountedByteBuf>() { + @Override + protected AtomicIntegerFieldUpdater<AbstractReferenceCountedByteBuf> updater() { + return AIF_UPDATER; } + @Override + protected long unsafeOffset() { + return REFCNT_FIELD_OFFSET; + } + }; - REFCNT_FIELD_OFFSET = refCntFieldOffset; - } - - private static int realRefCnt(int rawCnt) { - return (rawCnt & 1) != 0 ? 0 : rawCnt >>> 1; - } + // Value might not equal "real" reference count, all access should be via the updater + @SuppressWarnings("unused") + private volatile int refCnt = updater.initialValue(); protected AbstractReferenceCountedByteBuf(int maxCapacity) { super(maxCapacity); } - private int nonVolatileRawCnt() { - // TODO: Once we compile against later versions of Java we can replace the Unsafe usage here by varhandles. - return REFCNT_FIELD_OFFSET != -1 ? PlatformDependent.getInt(this, REFCNT_FIELD_OFFSET) - : refCntUpdater.get(this); - } - @Override - int internalRefCnt() { + boolean isAccessible() { // Try to do non-volatile read for performance as the ensureAccessible() is racy anyway and only provide // a best-effort guard. - return realRefCnt(nonVolatileRawCnt()); + return updater.isLiveNonVolatile(this); } @Override public int refCnt() { - return realRefCnt(refCntUpdater.get(this)); + return updater.refCnt(this); } /** * An unsafe operation intended for use by a subclass that sets the reference count of the buffer directly */ - protected final void setRefCnt(int newRefCnt) { - refCntUpdater.set(this, newRefCnt << 1); // overflow OK here + protected final void setRefCnt(int refCnt) { + updater.setRefCnt(this, refCnt); + } + + /** + * An unsafe operation intended for use by a subclass that resets the reference count of the buffer to 1 + */ + protected final void resetRefCnt() { + updater.resetRefCnt(this); } @Override public ByteBuf retain() { - return retain0(1); + return updater.retain(this); } @Override public ByteBuf retain(int increment) { - return retain0(checkPositive(increment, "increment")); - } - - private ByteBuf retain0(final int increment) { - // all changes to the raw count are 2x the "real" change - int adjustedIncrement = increment << 1; // overflow OK here - int oldRef = refCntUpdater.getAndAdd(this, adjustedIncrement); - if ((oldRef & 1) != 0) { - throw new IllegalReferenceCountException(0, increment); - } - // don't pass 0! - if ((oldRef <= 0 && oldRef + adjustedIncrement >= 0) - || (oldRef >= 0 && oldRef + adjustedIncrement < oldRef)) { - // overflow case - refCntUpdater.getAndAdd(this, -adjustedIncrement); - throw new IllegalReferenceCountException(realRefCnt(oldRef), increment); - } - return this; + return updater.retain(this, increment); } @Override @@ -121,64 +97,19 @@ public abstract class AbstractReferenceCountedByteBuf extends AbstractByteBuf { @Override public boolean release() { - return release0(1); + return handleRelease(updater.release(this)); } @Override public boolean release(int decrement) { - return release0(checkPositive(decrement, "decrement")); - } - - private boolean release0(int decrement) { - int rawCnt = nonVolatileRawCnt(), realCnt = toLiveRealCnt(rawCnt, decrement); - if (decrement == realCnt) { - if (refCntUpdater.compareAndSet(this, rawCnt, 1)) { - deallocate(); - return true; - } - return retryRelease0(decrement); - } - return releaseNonFinal0(decrement, rawCnt, realCnt); - } - - private boolean releaseNonFinal0(int decrement, int rawCnt, int realCnt) { - if (decrement < realCnt - // all changes to the raw count are 2x the "real" change - && refCntUpdater.compareAndSet(this, rawCnt, rawCnt - (decrement << 1))) { - return false; - } - return retryRelease0(decrement); + return handleRelease(updater.release(this, decrement)); } - private boolean retryRelease0(int decrement) { - for (;;) { - int rawCnt = refCntUpdater.get(this), realCnt = toLiveRealCnt(rawCnt, decrement); - if (decrement == realCnt) { - if (refCntUpdater.compareAndSet(this, rawCnt, 1)) { - deallocate(); - return true; - } - } else if (decrement < realCnt) { - // all changes to the raw count are 2x the "real" change - if (refCntUpdater.compareAndSet(this, rawCnt, rawCnt - (decrement << 1))) { - return false; - } - } else { - throw new IllegalReferenceCountException(realCnt, -decrement); - } - Thread.yield(); // this benefits throughput under high contention - } - } - - /** - * Like {@link #realRefCnt(int)} but throws if refCnt == 0 - */ - private static int toLiveRealCnt(int rawCnt, int decrement) { - if ((rawCnt & 1) == 0) { - return rawCnt >>> 1; + private boolean handleRelease(boolean result) { + if (result) { + deallocate(); } - // odd rawCnt => already deallocated - throw new IllegalReferenceCountException(0, -decrement); + return result; } /** diff --git a/buffer/src/main/java/io/netty/buffer/AdvancedLeakAwareCompositeByteBuf.java b/buffer/src/main/java/io/netty/buffer/AdvancedLeakAwareCompositeByteBuf.java index ff93de6..5b36354 100644 --- a/buffer/src/main/java/io/netty/buffer/AdvancedLeakAwareCompositeByteBuf.java +++ b/buffer/src/main/java/io/netty/buffer/AdvancedLeakAwareCompositeByteBuf.java @@ -939,6 +939,12 @@ final class AdvancedLeakAwareCompositeByteBuf extends SimpleLeakAwareCompositeBy return super.addComponent(increaseWriterIndex, cIndex, buffer); } + @Override + public CompositeByteBuf addFlattenedComponents(boolean increaseWriterIndex, ByteBuf buffer) { + recordLeakNonRefCountingOperation(leak); + return super.addFlattenedComponents(increaseWriterIndex, buffer); + } + @Override public CompositeByteBuf removeComponent(int cIndex) { recordLeakNonRefCountingOperation(leak); diff --git a/buffer/src/main/java/io/netty/buffer/ByteBuf.java b/buffer/src/main/java/io/netty/buffer/ByteBuf.java index 2ac7e98..850c85a 100644 --- a/buffer/src/main/java/io/netty/buffer/ByteBuf.java +++ b/buffer/src/main/java/io/netty/buffer/ByteBuf.java @@ -245,7 +245,6 @@ import java.nio.charset.UnsupportedCharsetException; * Please refer to {@link ByteBufInputStream} and * {@link ByteBufOutputStream}. */ -@SuppressWarnings("ClassMayBeInterface") public abstract class ByteBuf implements ReferenceCounted, Comparable<ByteBuf> { /** @@ -422,6 +421,15 @@ public abstract class ByteBuf implements ReferenceCounted, Comparable<ByteBuf> { */ public abstract int maxWritableBytes(); + /** + * Returns the maximum number of bytes which can be written for certain without involving + * an internal reallocation or data-copy. The returned value will be ≥ {@link #writableBytes()} + * and ≤ {@link #maxWritableBytes()}. + */ + public int maxFastWritableBytes() { + return writableBytes(); + } + /** * Returns {@code true} * if and only if {@code (this.writerIndex - this.readerIndex)} is greater @@ -2052,11 +2060,14 @@ public abstract class ByteBuf implements ReferenceCounted, Comparable<ByteBuf> { /** * Locates the first occurrence of the specified {@code value} in this - * buffer. The search takes place from the specified {@code fromIndex} - * (inclusive) to the specified {@code toIndex} (exclusive). + * buffer. The search takes place from the specified {@code fromIndex} + * (inclusive) to the specified {@code toIndex} (exclusive). * <p> * If {@code fromIndex} is greater than {@code toIndex}, the search is - * performed in a reversed order. + * performed in a reversed order from {@code fromIndex} (exclusive) + * down to {@code toIndex} (inclusive). + * <p> + * Note that the lower index is always included and higher always excluded. * <p> * This method does not modify {@code readerIndex} or {@code writerIndex} of * this buffer. @@ -2372,6 +2383,19 @@ public abstract class ByteBuf implements ReferenceCounted, Comparable<ByteBuf> { */ public abstract long memoryAddress(); + /** + * Returns {@code true} if this {@link ByteBuf} implementation is backed by a single memory region. + * Composite buffer implementations must return false even if they currently hold ≤ 1 components. + * For buffers that return {@code true}, it's guaranteed that a successful call to {@link #discardReadBytes()} + * will increase the value of {@link #maxFastWritableBytes()} by the current {@code readerIndex}. + * <p> + * This method will return {@code false} by default, and a {@code false} return value does not necessarily + * mean that the implementation is composite or that it is <i>not</i> backed by a single memory region. + */ + public boolean isContiguous() { + return false; + } + /** * Decodes this buffer's readable bytes into a string with the specified * character set name. This method is identical to @@ -2445,4 +2469,12 @@ public abstract class ByteBuf implements ReferenceCounted, Comparable<ByteBuf> { @Override public abstract ByteBuf touch(Object hint); + + /** + * Used internally by {@link AbstractByteBuf#ensureAccessible()} to try to guard + * against using the buffer after it was released (best-effort). + */ + boolean isAccessible() { + return refCnt() != 0; + } } diff --git a/buffer/src/main/java/io/netty/buffer/ByteBufInputStream.java b/buffer/src/main/java/io/netty/buffer/ByteBufInputStream.java index 038cd8d..5b371fd 100644 --- a/buffer/src/main/java/io/netty/buffer/ByteBufInputStream.java +++ b/buffer/src/main/java/io/netty/buffer/ByteBufInputStream.java @@ -16,6 +16,7 @@ package io.netty.buffer; import io.netty.util.ReferenceCounted; +import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.StringUtil; import java.io.DataInput; @@ -102,9 +103,7 @@ public class ByteBufInputStream extends InputStream implements DataInput { * {@code writerIndex} */ public ByteBufInputStream(ByteBuf buffer, int length, boolean releaseOnClose) { - if (buffer == null) { - throw new NullPointerException("buffer"); - } + ObjectUtil.checkNotNull(buffer, "buffer"); if (length < 0) { if (releaseOnClose) { buffer.release(); @@ -163,7 +162,8 @@ public class ByteBufInputStream extends InputStream implements DataInput { @Override public int read() throws IOException { - if (!buffer.isReadable()) { + int available = available(); + if (available == 0) { return -1; } return buffer.readByte() & 0xff; @@ -203,7 +203,8 @@ public class ByteBufInputStream extends InputStream implements DataInput { @Override public byte readByte() throws IOException { - if (!buffer.isReadable()) { + int available = available(); + if (available == 0) { throw new EOFException(); } return buffer.readByte(); @@ -245,22 +246,26 @@ public class ByteBufInputStream extends InputStream implements DataInput { @Override public String readLine() throws IOException { - if (!buffer.isReadable()) { + int available = available(); + if (available == 0) { return null; } + if (lineBuf != null) { lineBuf.setLength(0); } loop: do { int c = buffer.readUnsignedByte(); + --available; switch (c) { case '\n': break loop; case '\r': - if (buffer.isReadable() && (char) buffer.getUnsignedByte(buffer.readerIndex()) == '\n') { + if (available > 0 && (char) buffer.getUnsignedByte(buffer.readerIndex()) == '\n') { buffer.skipBytes(1); + --available; } break loop; @@ -270,7 +275,7 @@ public class ByteBufInputStream extends InputStream implements DataInput { } lineBuf.append((char) c); } - } while (buffer.isReadable()); + } while (available > 0); return lineBuf != null && lineBuf.length() > 0 ? lineBuf.toString() : StringUtil.EMPTY_STRING; } diff --git a/buffer/src/main/java/io/netty/buffer/ByteBufOutputStream.java b/buffer/src/main/java/io/netty/buffer/ByteBufOutputStream.java index c4f7805..d874dc6 100644 --- a/buffer/src/main/java/io/netty/buffer/ByteBufOutputStream.java +++ b/buffer/src/main/java/io/netty/buffer/ByteBufOutputStream.java @@ -16,6 +16,7 @@ package io.netty.buffer; import io.netty.util.CharsetUtil; +import io.netty.util.internal.ObjectUtil; import java.io.DataOutput; import java.io.DataOutputStream; @@ -45,10 +46,7 @@ public class ByteBufOutputStream extends OutputStream implements DataOutput { * Creates a new stream which writes data to the specified {@code buffer}. */ public ByteBufOutputStream(ByteBuf buffer) { - if (buffer == null) { - throw new NullPointerException("buffer"); - } - this.buffer = buffer; + this.buffer = ObjectUtil.checkNotNull(buffer, "buffer"); startIndex = buffer.writerIndex(); } diff --git a/buffer/src/main/java/io/netty/buffer/ByteBufUtil.java b/buffer/src/main/java/io/netty/buffer/ByteBufUtil.java index d2b2ec3..9aed166 100644 --- a/buffer/src/main/java/io/netty/buffer/ByteBufUtil.java +++ b/buffer/src/main/java/io/netty/buffer/ByteBufUtil.java @@ -18,9 +18,11 @@ package io.netty.buffer; import io.netty.util.AsciiString; import io.netty.util.ByteProcessor; import io.netty.util.CharsetUtil; -import io.netty.util.Recycler; -import io.netty.util.Recycler.Handle; import io.netty.util.concurrent.FastThreadLocal; +import io.netty.util.internal.MathUtil; +import io.netty.util.internal.ObjectPool; +import io.netty.util.internal.ObjectPool.Handle; +import io.netty.util.internal.ObjectPool.ObjectCreator; import io.netty.util.internal.PlatformDependent; import io.netty.util.internal.StringUtil; import io.netty.util.internal.SystemPropertyUtil; @@ -43,6 +45,7 @@ import java.util.Locale; import static io.netty.util.internal.MathUtil.isOutOfBounds; import static io.netty.util.internal.ObjectUtil.checkNotNull; +import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero; import static io.netty.util.internal.StringUtil.NEWLINE; import static io.netty.util.internal.StringUtil.isSurrogate; @@ -471,6 +474,14 @@ public final class ByteBufUtil { return buffer.forEachByteDesc(toIndex, fromIndex - toIndex, new ByteProcessor.IndexOfProcessor(value)); } + private static CharSequence checkCharSequenceBounds(CharSequence seq, int start, int end) { + if (MathUtil.isOutOfBounds(start, end - start, seq.length())) { + throw new IndexOutOfBoundsException("expected: 0 <= start(" + start + ") <= end (" + end + + ") <= seq.length(" + seq.length() + ')'); + } + return seq; + } + /** * Encode a {@link CharSequence} in <a href="http://en.wikipedia.org/wiki/UTF-8">UTF-8</a> and write * it to a {@link ByteBuf} allocated with {@code alloc}. @@ -495,7 +506,17 @@ public final class ByteBufUtil { * This method returns the actual number of bytes written. */ public static int writeUtf8(ByteBuf buf, CharSequence seq) { - return reserveAndWriteUtf8(buf, seq, utf8MaxBytes(seq)); + int seqLength = seq.length(); + return reserveAndWriteUtf8Seq(buf, seq, 0, seqLength, utf8MaxBytes(seqLength)); + } + + /** + * Equivalent to <code>{@link #writeUtf8(ByteBuf, CharSequence) writeUtf8(buf, seq.subSequence(start, end))}</code> + * but avoids subsequence object allocation. + */ + public static int writeUtf8(ByteBuf buf, CharSequence seq, int start, int end) { + checkCharSequenceBounds(seq, start, end); + return reserveAndWriteUtf8Seq(buf, seq, start, end, utf8MaxBytes(end - start)); } /** @@ -508,6 +529,21 @@ public final class ByteBufUtil { * This method returns the actual number of bytes written. */ public static int reserveAndWriteUtf8(ByteBuf buf, CharSequence seq, int reserveBytes) { + return reserveAndWriteUtf8Seq(buf, seq, 0, seq.length(), reserveBytes); + } + + /** + * Equivalent to <code>{@link #reserveAndWriteUtf8(ByteBuf, CharSequence, int) + * reserveAndWriteUtf8(buf, seq.subSequence(start, end), reserveBytes)}</code> but avoids + * subsequence object allocation if possible. + * + * @return actual number of bytes written + */ + public static int reserveAndWriteUtf8(ByteBuf buf, CharSequence seq, int start, int end, int reserveBytes) { + return reserveAndWriteUtf8Seq(buf, checkCharSequenceBounds(seq, start, end), start, end, reserveBytes); + } + + private static int reserveAndWriteUtf8Seq(ByteBuf buf, CharSequence seq, int start, int end, int reserveBytes) { for (;;) { if (buf instanceof WrappedCompositeByteBuf) { // WrappedCompositeByteBuf is a sub-class of AbstractByteBuf so it needs special handling. @@ -515,27 +551,31 @@ public final class ByteBufUtil { } else if (buf instanceof AbstractByteBuf) { AbstractByteBuf byteBuf = (AbstractByteBuf) buf; byteBuf.ensureWritable0(reserveBytes); - int written = writeUtf8(byteBuf, byteBuf.writerIndex, seq, seq.length()); + int written = writeUtf8(byteBuf, byteBuf.writerIndex, seq, start, end); byteBuf.writerIndex += written; return written; } else if (buf instanceof WrappedByteBuf) { // Unwrap as the wrapped buffer may be an AbstractByteBuf and so we can use fast-path. buf = buf.unwrap(); } else { - byte[] bytes = seq.toString().getBytes(CharsetUtil.UTF_8); + byte[] bytes = seq.subSequence(start, end).toString().getBytes(CharsetUtil.UTF_8); buf.writeBytes(bytes); return bytes.length; } } } - // Fast-Path implementation static int writeUtf8(AbstractByteBuf buffer, int writerIndex, CharSequence seq, int len) { + return writeUtf8(buffer, writerIndex, seq, 0, len); + } + + // Fast-Path implementation + static int writeUtf8(AbstractByteBuf buffer, int writerIndex, CharSequence seq, int start, int end) { int oldWriterIndex = writerIndex; // We can use the _set methods as these not need to do any index checks and reference checks. // This is possible as we called ensureWritable(...) before. - for (int i = 0; i < len; i++) { + for (int i = start; i < end; i++) { char c = seq.charAt(i); if (c < 0x80) { buffer._setByte(writerIndex++, (byte) c); @@ -547,18 +587,13 @@ public final class ByteBufUtil { buffer._setByte(writerIndex++, WRITE_UTF_UNKNOWN); continue; } - final char c2; - try { - // Surrogate Pair consumes 2 characters. Optimistically try to get the next character to avoid - // duplicate bounds checking with charAt. If an IndexOutOfBoundsException is thrown we will - // re-throw a more informative exception describing the problem. - c2 = seq.charAt(++i); - } catch (IndexOutOfBoundsException ignored) { + // Surrogate Pair consumes 2 characters. + if (++i == end) { buffer._setByte(writerIndex++, WRITE_UTF_UNKNOWN); break; } // Extra method to allow inlining the rest of writeUtf8 which is the most likely code path. - writerIndex = writeUtf8Surrogate(buffer, writerIndex, c, c2); + writerIndex = writeUtf8Surrogate(buffer, writerIndex, c, seq.charAt(i)); } else { buffer._setByte(writerIndex++, (byte) (0xe0 | (c >> 12))); buffer._setByte(writerIndex++, (byte) (0x80 | ((c >> 6) & 0x3f))); @@ -605,22 +640,35 @@ public final class ByteBufUtil { * This method is producing the exact length according to {@link #writeUtf8(ByteBuf, CharSequence)}. */ public static int utf8Bytes(final CharSequence seq) { + return utf8ByteCount(seq, 0, seq.length()); + } + + /** + * Equivalent to <code>{@link #utf8Bytes(CharSequence) utf8Bytes(seq.subSequence(start, end))}</code> + * but avoids subsequence object allocation. + * <p> + * This method is producing the exact length according to {@link #writeUtf8(ByteBuf, CharSequence, int, int)}. + */ + public static int utf8Bytes(final CharSequence seq, int start, int end) { + return utf8ByteCount(checkCharSequenceBounds(seq, start, end), start, end); + } + + private static int utf8ByteCount(final CharSequence seq, int start, int end) { if (seq instanceof AsciiString) { - return seq.length(); + return end - start; } - int seqLength = seq.length(); - int i = 0; + int i = start; // ASCII fast path - while (i < seqLength && seq.charAt(i) < 0x80) { + while (i < end && seq.charAt(i) < 0x80) { ++i; } // !ASCII is packed in a separate method to let the ASCII case be smaller - return i < seqLength ? i + utf8Bytes(seq, i, seqLength) : i; + return i < end ? (i - start) + utf8BytesNonAscii(seq, i, end) : i - start; } - private static int utf8Bytes(final CharSequence seq, final int start, final int length) { + private static int utf8BytesNonAscii(final CharSequence seq, final int start, final int end) { int encodedLength = 0; - for (int i = start; i < length; i++) { + for (int i = start; i < end; i++) { final char c = seq.charAt(i); // making it 100% branchless isn't rewarding due to the many bit operations necessary! if (c < 0x800) { @@ -632,17 +680,13 @@ public final class ByteBufUtil { // WRITE_UTF_UNKNOWN continue; } - final char c2; - try { - // Surrogate Pair consumes 2 characters. Optimistically try to get the next character to avoid - // duplicate bounds checking with charAt. - c2 = seq.charAt(++i); - } catch (IndexOutOfBoundsException ignored) { + // Surrogate Pair consumes 2 characters. + if (++i == end) { encodedLength++; // WRITE_UTF_UNKNOWN break; } - if (!Character.isLowSurrogate(c2)) { + if (!Character.isLowSurrogate(seq.charAt(i))) { // WRITE_UTF_UNKNOWN + (Character.isHighSurrogate(c2) ? WRITE_UTF_UNKNOWN : c2) encodedLength += 2; continue; @@ -1000,9 +1044,7 @@ public final class ByteBufUtil { } private static String hexDump(ByteBuf buffer, int fromIndex, int length) { - if (length < 0) { - throw new IllegalArgumentException("length: " + length); - } + checkPositiveOrZero(length, "length"); if (length == 0) { return ""; } @@ -1022,9 +1064,7 @@ public final class ByteBufUtil { } private static String hexDump(byte[] array, int fromIndex, int length) { - if (length < 0) { - throw new IllegalArgumentException("length: " + length); - } + checkPositiveOrZero(length, "length"); if (length == 0) { return ""; } @@ -1047,7 +1087,7 @@ public final class ByteBufUtil { if (length == 0) { return StringUtil.EMPTY_STRING; } else { - int rows = length / 16 + (length % 15 == 0? 0 : 1) + 4; + int rows = length / 16 + ((length & 15) == 0? 0 : 1) + 4; StringBuilder buf = new StringBuilder(rows * 80); appendPrettyHexDump(buf, buffer, offset, length); return buf.toString(); @@ -1132,17 +1172,17 @@ public final class ByteBufUtil { static final class ThreadLocalUnsafeDirectByteBuf extends UnpooledUnsafeDirectByteBuf { - private static final Recycler<ThreadLocalUnsafeDirectByteBuf> RECYCLER = - new Recycler<ThreadLocalUnsafeDirectByteBuf>() { + private static final ObjectPool<ThreadLocalUnsafeDirectByteBuf> RECYCLER = + ObjectPool.newPool(new ObjectCreator<ThreadLocalUnsafeDirectByteBuf>() { @Override - protected ThreadLocalUnsafeDirectByteBuf newObject(Handle<ThreadLocalUnsafeDirectByteBuf> handle) { + public ThreadLocalUnsafeDirectByteBuf newObject(Handle<ThreadLocalUnsafeDirectByteBuf> handle) { return new ThreadLocalUnsafeDirectByteBuf(handle); } - }; + }); static ThreadLocalUnsafeDirectByteBuf newInstance() { ThreadLocalUnsafeDirectByteBuf buf = RECYCLER.get(); - buf.setRefCnt(1); + buf.resetRefCnt(); return buf; } @@ -1166,16 +1206,17 @@ public final class ByteBufUtil { static final class ThreadLocalDirectByteBuf extends UnpooledDirectByteBuf { - private static final Recycler<ThreadLocalDirectByteBuf> RECYCLER = new Recycler<ThreadLocalDirectByteBuf>() { + private static final ObjectPool<ThreadLocalDirectByteBuf> RECYCLER = ObjectPool.newPool( + new ObjectCreator<ThreadLocalDirectByteBuf>() { @Override - protected ThreadLocalDirectByteBuf newObject(Handle<ThreadLocalDirectByteBuf> handle) { + public ThreadLocalDirectByteBuf newObject(Handle<ThreadLocalDirectByteBuf> handle) { return new ThreadLocalDirectByteBuf(handle); } - }; + }); static ThreadLocalDirectByteBuf newInstance() { ThreadLocalDirectByteBuf buf = RECYCLER.get(); - buf.setRefCnt(1); + buf.resetRefCnt(); return buf; } diff --git a/buffer/src/main/java/io/netty/buffer/CompositeByteBuf.java b/buffer/src/main/java/io/netty/buffer/CompositeByteBuf.java index 96c14cd..f8cae7b 100644 --- a/buffer/src/main/java/io/netty/buffer/CompositeByteBuf.java +++ b/buffer/src/main/java/io/netty/buffer/CompositeByteBuf.java @@ -19,6 +19,7 @@ import io.netty.util.ByteProcessor; import io.netty.util.IllegalReferenceCountException; import io.netty.util.ReferenceCountUtil; import io.netty.util.internal.EmptyArrays; +import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.RecyclableArrayList; import java.io.IOException; @@ -61,14 +62,13 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements private CompositeByteBuf(ByteBufAllocator alloc, boolean direct, int maxNumComponents, int initSize) { super(AbstractByteBufAllocator.DEFAULT_MAX_CAPACITY); - if (alloc == null) { - throw new NullPointerException("alloc"); - } + + this.alloc = ObjectUtil.checkNotNull(alloc, "alloc"); if (maxNumComponents < 1) { throw new IllegalArgumentException( "maxNumComponents: " + maxNumComponents + " (expected: >= 1)"); } - this.alloc = alloc; + this.direct = direct; this.maxNumComponents = maxNumComponents; components = newCompArray(initSize, maxNumComponents); @@ -96,8 +96,7 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements this(alloc, direct, maxNumComponents, buffers instanceof Collection ? ((Collection<ByteBuf>) buffers).size() : 0); - addComponents0(false, 0, buffers); - consolidateIfNeeded(); + addComponents(false, 0, buffers); setIndex(0, capacity()); } @@ -220,10 +219,7 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements * {@link CompositeByteBuf}. */ public CompositeByteBuf addComponent(boolean increaseWriterIndex, ByteBuf buffer) { - checkNotNull(buffer, "buffer"); - addComponent0(increaseWriterIndex, componentCount, buffer); - consolidateIfNeeded(); - return this; + return addComponent(increaseWriterIndex, componentCount, buffer); } /** @@ -252,9 +248,7 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements * ownership of all {@link ByteBuf} objects is transferred to this {@link CompositeByteBuf}. */ public CompositeByteBuf addComponents(boolean increaseWriterIndex, Iterable<ByteBuf> buffers) { - addComponents0(increaseWriterIndex, componentCount, buffers); - consolidateIfNeeded(); - return this; + return addComponents(increaseWriterIndex, componentCount, buffers); } /** @@ -283,7 +277,7 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements checkComponentIndex(cIndex); // No need to consolidate - just add a component to the list. - Component c = newComponent(buffer, 0); + Component c = newComponent(ensureAccessible(buffer), 0); int readableBytes = c.length(); addComp(cIndex, c); @@ -294,7 +288,7 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements c.reposition(components[cIndex - 1].endOffset); } if (increaseWriterIndex) { - writerIndex(writerIndex() + readableBytes); + writerIndex += readableBytes; } return cIndex; } finally { @@ -304,24 +298,42 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements } } - // unwrap if already sliced - @SuppressWarnings("deprecation") - private Component newComponent(ByteBuf buf, int offset) { - if (checkAccessible && buf.refCnt() == 0) { + private static ByteBuf ensureAccessible(final ByteBuf buf) { + if (checkAccessible && !buf.isAccessible()) { throw new IllegalReferenceCountException(0); } - int srcIndex = buf.readerIndex(), len = buf.readableBytes(); - ByteBuf slice = null; - if (buf instanceof AbstractUnpooledSlicedByteBuf) { - srcIndex += ((AbstractUnpooledSlicedByteBuf) buf).idx(0); - slice = buf; - buf = buf.unwrap(); - } else if (buf instanceof PooledSlicedByteBuf) { - srcIndex += ((PooledSlicedByteBuf) buf).adjustment; - slice = buf; - buf = buf.unwrap(); + return buf; + } + + @SuppressWarnings("deprecation") + private Component newComponent(final ByteBuf buf, final int offset) { + final int srcIndex = buf.readerIndex(); + final int len = buf.readableBytes(); + + // unpeel any intermediate outer layers (UnreleasableByteBuf, LeakAwareByteBufs, SwappedByteBuf) + ByteBuf unwrapped = buf; + int unwrappedIndex = srcIndex; + while (unwrapped instanceof WrappedByteBuf || unwrapped instanceof SwappedByteBuf) { + unwrapped = unwrapped.unwrap(); + } + + // unwrap if already sliced + if (unwrapped instanceof AbstractUnpooledSlicedByteBuf) { + unwrappedIndex += ((AbstractUnpooledSlicedByteBuf) unwrapped).idx(0); + unwrapped = unwrapped.unwrap(); + } else if (unwrapped instanceof PooledSlicedByteBuf) { + unwrappedIndex += ((PooledSlicedByteBuf) unwrapped).adjustment; + unwrapped = unwrapped.unwrap(); + } else if (unwrapped instanceof DuplicatedByteBuf || unwrapped instanceof PooledDuplicatedByteBuf) { + unwrapped = unwrapped.unwrap(); } - return new Component(buf.order(ByteOrder.BIG_ENDIAN), srcIndex, offset, len, slice); + + // We don't need to slice later to expose the internal component if the readable range + // is already the entire buffer + final ByteBuf slice = buf.capacity() == len ? buf : null; + + return new Component(buf.order(ByteOrder.BIG_ENDIAN), srcIndex, + unwrapped.order(ByteOrder.BIG_ENDIAN), unwrappedIndex, offset, len, slice); } /** @@ -345,20 +357,25 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements return this; } - private int addComponents0(boolean increaseWriterIndex, final int cIndex, ByteBuf[] buffers, int arrOffset) { + private CompositeByteBuf addComponents0(boolean increaseWriterIndex, + final int cIndex, ByteBuf[] buffers, int arrOffset) { final int len = buffers.length, count = len - arrOffset; + // only set ci after we've shifted so that finally block logic is always correct int ci = Integer.MAX_VALUE; try { checkComponentIndex(cIndex); shiftComps(cIndex, count); // will increase componentCount - ci = cIndex; // only set this after we've shifted so that finally block logic is always correct int nextOffset = cIndex > 0 ? components[cIndex - 1].endOffset : 0; - for (ByteBuf b; arrOffset < len && (b = buffers[arrOffset]) != null; arrOffset++, ci++) { - Component c = newComponent(b, nextOffset); + for (ci = cIndex; arrOffset < len; arrOffset++, ci++) { + ByteBuf b = buffers[arrOffset]; + if (b == null) { + break; + } + Component c = newComponent(ensureAccessible(b), nextOffset); components[ci] = c; nextOffset = c.endOffset; } - return ci; + return this; } finally { // ci is now the index following the last successfully added component if (ci < componentCount) { @@ -372,14 +389,13 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements updateComponentOffsets(ci); // only need to do this here for components after the added ones } if (increaseWriterIndex && ci > cIndex && ci <= componentCount) { - writerIndex(writerIndex() + components[ci - 1].endOffset - components[cIndex].offset); + writerIndex += components[ci - 1].endOffset - components[cIndex].offset; } } } private <T> int addComponents0(boolean increaseWriterIndex, int cIndex, ByteWrapper<T> wrapper, T[] buffers, int offset) { - checkNotNull(buffers, "buffers"); checkComponentIndex(cIndex); // No need for consolidation @@ -413,18 +429,82 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements * {@link CompositeByteBuf}. */ public CompositeByteBuf addComponents(int cIndex, Iterable<ByteBuf> buffers) { - addComponents0(false, cIndex, buffers); - consolidateIfNeeded(); - return this; + return addComponents(false, cIndex, buffers); + } + + /** + * Add the given {@link ByteBuf} and increase the {@code writerIndex} if {@code increaseWriterIndex} is + * {@code true}. If the provided buffer is a {@link CompositeByteBuf} itself, a "shallow copy" of its + * readable components will be performed. Thus the actual number of new components added may vary + * and in particular will be zero if the provided buffer is not readable. + * <p> + * {@link ByteBuf#release()} ownership of {@code buffer} is transferred to this {@link CompositeByteBuf}. + * @param buffer the {@link ByteBuf} to add. {@link ByteBuf#release()} ownership is transferred to this + * {@link CompositeByteBuf}. + */ + public CompositeByteBuf addFlattenedComponents(boolean increaseWriterIndex, ByteBuf buffer) { + checkNotNull(buffer, "buffer"); + final int ridx = buffer.readerIndex(); + final int widx = buffer.writerIndex(); + if (ridx == widx) { + buffer.release(); + return this; + } + if (!(buffer instanceof CompositeByteBuf)) { + addComponent0(increaseWriterIndex, componentCount, buffer); + consolidateIfNeeded(); + return this; + } + final CompositeByteBuf from = (CompositeByteBuf) buffer; + from.checkIndex(ridx, widx - ridx); + final Component[] fromComponents = from.components; + final int compCountBefore = componentCount; + final int writerIndexBefore = writerIndex; + try { + for (int cidx = from.toComponentIndex0(ridx), newOffset = capacity();; cidx++) { + final Component component = fromComponents[cidx]; + final int compOffset = component.offset; + final int fromIdx = Math.max(ridx, compOffset); + final int toIdx = Math.min(widx, component.endOffset); + final int len = toIdx - fromIdx; + if (len > 0) { // skip empty components + addComp(componentCount, new Component( + component.srcBuf.retain(), component.srcIdx(fromIdx), + component.buf, component.idx(fromIdx), newOffset, len, null)); + } + if (widx == toIdx) { + break; + } + newOffset += len; + } + if (increaseWriterIndex) { + writerIndex = writerIndexBefore + (widx - ridx); + } + consolidateIfNeeded(); + buffer.release(); + buffer = null; + return this; + } finally { + if (buffer != null) { + // if we did not succeed, attempt to rollback any components that were added + if (increaseWriterIndex) { + writerIndex = writerIndexBefore; + } + for (int cidx = componentCount - 1; cidx >= compCountBefore; cidx--) { + components[cidx].free(); + removeComp(cidx); + } + } + } } // TODO optimize further, similar to ByteBuf[] version // (difference here is that we don't know *always* know precise size increase in advance, // but we do in the most common case that the Iterable is a Collection) - private int addComponents0(boolean increaseIndex, int cIndex, Iterable<ByteBuf> buffers) { + private CompositeByteBuf addComponents(boolean increaseIndex, int cIndex, Iterable<ByteBuf> buffers) { if (buffers instanceof ByteBuf) { // If buffers also implements ByteBuf (e.g. CompositeByteBuf), it has to go to addComponent(ByteBuf). - return addComponent0(increaseIndex, cIndex, (ByteBuf) buffers); + return addComponent(increaseIndex, cIndex, (ByteBuf) buffers); } checkNotNull(buffers, "buffers"); Iterator<ByteBuf> it = buffers.iterator(); @@ -440,12 +520,13 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements cIndex = addComponent0(increaseIndex, cIndex, b) + 1; cIndex = Math.min(cIndex, componentCount); } - return cIndex; } finally { while (it.hasNext()) { ReferenceCountUtil.safeRelease(it.next()); } } + consolidateIfNeeded(); + return this; } /** @@ -457,18 +538,7 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements // operation. int size = componentCount; if (size > maxNumComponents) { - final int capacity = components[size - 1].endOffset; - - ByteBuf consolidated = allocBuffer(capacity); - lastAccessed = null; - - // We're not using foreach to avoid creating an iterator. - for (int i = 0; i < size; i ++) { - components[i].transferTo(consolidated); - } - - components[0] = new Component(consolidated, 0, 0, capacity, consolidated); - removeCompRange(1, size); + consolidate0(0, size); } } @@ -497,7 +567,7 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements return; } - int nextIndex = cIndex > 0 ? components[cIndex].endOffset : 0; + int nextIndex = cIndex > 0 ? components[cIndex - 1].endOffset : 0; for (; cIndex < size; cIndex++) { Component c = components[cIndex]; c.reposition(nextIndex); @@ -516,7 +586,7 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements if (lastAccessed == comp) { lastAccessed = null; } - comp.freeIfNecessary(); + comp.free(); removeComp(cIndex); if (comp.length() > 0) { // Only need to call updateComponentOffsets if the length was > 0 @@ -547,7 +617,7 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements if (lastAccessed == c) { lastAccessed = null; } - c.freeIfNecessary(); + c.free(); } removeCompRange(cIndex, endIndex); @@ -748,6 +818,7 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements consolidateIfNeeded(); } } else if (newCapacity < oldCapacity) { + lastAccessed = null; int i = size - 1; for (int bytesToTrim = oldCapacity - newCapacity; i >= 0; i--) { Component c = components[i]; @@ -755,18 +826,23 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements if (bytesToTrim < cLength) { // Trim the last component c.endOffset -= bytesToTrim; - c.slice = null; + ByteBuf slice = c.slice; + if (slice != null) { + // We must replace the cached slice with a derived one to ensure that + // it can later be released properly in the case of PooledSlicedByteBuf. + c.slice = slice.slice(0, c.length()); + } break; } - c.freeIfNecessary(); + c.free(); bytesToTrim -= cLength; } removeCompRange(i + 1, size); if (readerIndex() > newCapacity) { - setIndex(newCapacity, newCapacity); - } else if (writerIndex() > newCapacity) { - writerIndex(newCapacity); + setIndex0(newCapacity, newCapacity); + } else if (writerIndex > newCapacity) { + writerIndex = newCapacity; } } return this; @@ -801,7 +877,7 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements */ public int toComponentIndex(int offset) { checkIndex(offset); - return toComponentIndex(offset); + return toComponentIndex0(offset); } private int toComponentIndex0(int offset) { @@ -813,6 +889,9 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements } } } + if (size <= 2) { // fast-path for 1 and 2 component count + return size == 1 || offset < components[0].endOffset ? 0 : 1; + } for (int low = 0, high = size; low <= high;) { int mid = low + high >>> 1; Component c = components[mid]; @@ -1076,7 +1155,8 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements @Override public CompositeByteBuf setShort(int index, int value) { - super.setShort(index, value); + checkIndex(index, 2); + _setShort(index, value); return this; } @@ -1110,7 +1190,8 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements @Override public CompositeByteBuf setMedium(int index, int value) { - super.setMedium(index, value); + checkIndex(index, 3); + _setMedium(index, value); return this; } @@ -1144,7 +1225,8 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements @Override public CompositeByteBuf setInt(int index, int value) { - super.setInt(index, value); + checkIndex(index, 4); + _setInt(index, value); return this; } @@ -1178,7 +1260,8 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements @Override public CompositeByteBuf setLong(int index, long value) { - super.setLong(index, value); + checkIndex(index, 8); + _setLong(index, value); return this; } @@ -1286,7 +1369,6 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements int i = toComponentIndex0(index); int readBytes = 0; - do { Component c = components[i]; int localLength = Math.min(length, c.endOffset - index); @@ -1304,15 +1386,11 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements } } + index += localReadBytes; + length -= localReadBytes; + readBytes += localReadBytes; if (localReadBytes == localLength) { - index += localLength; - length -= localLength; - readBytes += localLength; i ++; - } else { - index += localReadBytes; - length -= localReadBytes; - readBytes += localReadBytes; } } while (length > 0); @@ -1350,15 +1428,11 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements } } + index += localReadBytes; + length -= localReadBytes; + readBytes += localReadBytes; if (localReadBytes == localLength) { - index += localLength; - length -= localLength; - readBytes += localLength; i ++; - } else { - index += localReadBytes; - length -= localReadBytes; - readBytes += localReadBytes; } } while (length > 0); @@ -1396,15 +1470,11 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements } } + index += localReadBytes; + length -= localReadBytes; + readBytes += localReadBytes; if (localReadBytes == localLength) { - index += localLength; - length -= localLength; - readBytes += localLength; i ++; - } else { - index += localReadBytes; - length -= localReadBytes; - readBytes += localReadBytes; } } while (length > 0); @@ -1541,8 +1611,7 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements case 0: return EMPTY_NIO_BUFFER; case 1: - Component c = components[0]; - return c.buf.internalNioBuffer(c.idx(index), length); + return components[0].internalNioBuffer(index, length); default: throw new UnsupportedOperationException(); } @@ -1566,7 +1635,7 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements ByteBuffer[] buffers = nioBuffers(index, length); if (buffers.length == 1) { - return buffers[0].duplicate(); + return buffers[0]; } ByteBuffer merged = ByteBuffer.allocate(length).order(order()); @@ -1618,20 +1687,7 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements */ public CompositeByteBuf consolidate() { ensureAccessible(); - final int numComponents = componentCount; - if (numComponents <= 1) { - return this; - } - - final int capacity = components[numComponents - 1].endOffset; - final ByteBuf consolidated = allocBuffer(capacity); - - for (int i = 0; i < numComponents; i ++) { - components[i].transferTo(consolidated); - } - lastAccessed = null; - components[0] = new Component(consolidated, 0, 0, capacity, consolidated); - removeCompRange(1, numComponents); + consolidate0(0, componentCount); return this; } @@ -1643,13 +1699,18 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements */ public CompositeByteBuf consolidate(int cIndex, int numComponents) { checkComponentIndex(cIndex, numComponents); + consolidate0(cIndex, numComponents); + return this; + } + + private void consolidate0(int cIndex, int numComponents) { if (numComponents <= 1) { - return this; + return; } final int endCIndex = cIndex + numComponents; - final Component last = components[endCIndex - 1]; - final int capacity = last.endOffset - components[cIndex].offset; + final int startOffset = cIndex != 0 ? components[cIndex].offset : 0; + final int capacity = components[endCIndex - 1].endOffset - startOffset; final ByteBuf consolidated = allocBuffer(capacity); for (int i = cIndex; i < endCIndex; i ++) { @@ -1657,9 +1718,10 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements } lastAccessed = null; removeCompRange(cIndex + 1, endCIndex); - components[cIndex] = new Component(consolidated, 0, 0, capacity, consolidated); - updateComponentOffsets(cIndex); - return this; + components[cIndex] = newComponent(consolidated, 0); + if (cIndex != 0 || numComponents != componentCount) { + updateComponentOffsets(cIndex); + } } /** @@ -1676,7 +1738,7 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements int writerIndex = writerIndex(); if (readerIndex == writerIndex && writerIndex == capacity()) { for (int i = 0, size = componentCount; i < size; i++) { - components[i].freeIfNecessary(); + components[i].free(); } lastAccessed = null; clearComps(); @@ -1686,16 +1748,26 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements } // Remove read components. - int firstComponentId = toComponentIndex0(readerIndex); - for (int i = 0; i < firstComponentId; i ++) { - components[i].freeIfNecessary(); + int firstComponentId = 0; + Component c = null; + for (int size = componentCount; firstComponentId < size; firstComponentId++) { + c = components[firstComponentId]; + if (c.endOffset > readerIndex) { + break; + } + c.free(); + } + if (firstComponentId == 0) { + return this; // Nothing to discard + } + Component la = lastAccessed; + if (la != null && la.endOffset <= readerIndex) { + lastAccessed = null; } - lastAccessed = null; removeCompRange(0, firstComponentId); // Update indexes and markers. - Component first = components[0]; - int offset = first.offset; + int offset = c.offset; updateComponentOffsets(0); setIndex(readerIndex - offset, writerIndex - offset); adjustMarkers(offset); @@ -1714,7 +1786,7 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements int writerIndex = writerIndex(); if (readerIndex == writerIndex && writerIndex == capacity()) { for (int i = 0, size = componentCount; i < size; i++) { - components[i].freeIfNecessary(); + components[i].free(); } lastAccessed = null; clearComps(); @@ -1723,30 +1795,31 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements return this; } - // Remove read components. - int firstComponentId = toComponentIndex0(readerIndex); - for (int i = 0; i < firstComponentId; i ++) { - Component c = components[i]; - c.freeIfNecessary(); - if (lastAccessed == c) { - lastAccessed = null; + int firstComponentId = 0; + Component c = null; + for (int size = componentCount; firstComponentId < size; firstComponentId++) { + c = components[firstComponentId]; + if (c.endOffset > readerIndex) { + break; } + c.free(); } - // Remove or replace the first readable component with a new slice. - Component c = components[firstComponentId]; - if (readerIndex == c.endOffset) { - // new slice would be empty, so remove instead - c.freeIfNecessary(); - if (lastAccessed == c) { - lastAccessed = null; - } - firstComponentId++; - } else { - c.offset = 0; - c.endOffset -= readerIndex; - c.adjustment += readerIndex; - c.slice = null; + // Replace the first readable component with a new slice. + int trimmedBytes = readerIndex - c.offset; + c.offset = 0; + c.endOffset -= readerIndex; + c.srcAdjustment += readerIndex; + c.adjustment += readerIndex; + ByteBuf slice = c.slice; + if (slice != null) { + // We must replace the cached slice with a derived one to ensure that + // it can later be released properly in the case of PooledSlicedByteBuf. + c.slice = slice.slice(trimmedBytes, c.length()); + } + Component la = lastAccessed; + if (la != null && la.endOffset <= readerIndex) { + lastAccessed = null; } removeCompRange(0, firstComponentId); @@ -1770,21 +1843,32 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements } private static final class Component { - final ByteBuf buf; - int adjustment; - int offset; - int endOffset; + final ByteBuf srcBuf; // the originally added buffer + final ByteBuf buf; // srcBuf unwrapped zero or more times + + int srcAdjustment; // index of the start of this CompositeByteBuf relative to srcBuf + int adjustment; // index of the start of this CompositeByteBuf relative to buf + + int offset; // offset of this component within this CompositeByteBuf + int endOffset; // end offset of this component within this CompositeByteBuf private ByteBuf slice; // cached slice, may be null - Component(ByteBuf buf, int srcOffset, int offset, int len, ByteBuf slice) { + Component(ByteBuf srcBuf, int srcOffset, ByteBuf buf, int bufOffset, + int offset, int len, ByteBuf slice) { + this.srcBuf = srcBuf; + this.srcAdjustment = srcOffset - offset; this.buf = buf; + this.adjustment = bufOffset - offset; this.offset = offset; this.endOffset = offset + len; - this.adjustment = srcOffset - offset; this.slice = slice; } + int srcIdx(int index) { + return index + srcAdjustment; + } + int idx(int index) { return index + adjustment; } @@ -1796,6 +1880,7 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements void reposition(int newOffset) { int move = newOffset - offset; endOffset += move; + srcAdjustment -= move; adjustment -= move; offset = newOffset; } @@ -1803,28 +1888,31 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements // copy then release void transferTo(ByteBuf dst) { dst.writeBytes(buf, idx(offset), length()); - freeIfNecessary(); + free(); } ByteBuf slice() { - return slice != null ? slice : (slice = buf.slice(idx(offset), length())); + ByteBuf s = slice; + if (s == null) { + slice = s = srcBuf.slice(srcIdx(offset), length()); + } + return s; } ByteBuf duplicate() { - return buf.duplicate().setIndex(idx(offset), idx(endOffset)); + return srcBuf.duplicate(); } - void freeIfNecessary() { - // Release the slice if present since it may have a different - // refcount to the unwrapped buf if it is a PooledSlicedByteBuf - ByteBuf buffer = slice; - if (buffer != null) { - buffer.release(); - } else { - buf.release(); - } - // null out in either case since it could be racy + ByteBuffer internalNioBuffer(int index, int length) { + // Some buffers override this so we must use srcBuf + return srcBuf.internalNioBuffer(srcIdx(index), length); + } + + void free() { slice = null; + // Release the original buffer since it may have a different + // refcount to the unwrapped buf (e.g. if PooledSlicedByteBuf) + srcBuf.release(); } } @@ -2065,7 +2153,7 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements @Override public CompositeByteBuf writeBytes(byte[] src) { - writeBytes(src, 0, src.length); + super.writeBytes(src, 0, src.length); return this; } @@ -2129,10 +2217,15 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements // We're not using foreach to avoid creating an iterator. // see https://github.com/netty/netty/issues/2642 for (int i = 0, size = componentCount; i < size; i++) { - components[i].freeIfNecessary(); + components[i].free(); } } + @Override + boolean isAccessible() { + return !freed; + } + @Override public ByteBuf unwrap() { return null; diff --git a/buffer/src/main/java/io/netty/buffer/DefaultByteBufHolder.java b/buffer/src/main/java/io/netty/buffer/DefaultByteBufHolder.java index 8198eae..ad247ad 100644 --- a/buffer/src/main/java/io/netty/buffer/DefaultByteBufHolder.java +++ b/buffer/src/main/java/io/netty/buffer/DefaultByteBufHolder.java @@ -16,6 +16,7 @@ package io.netty.buffer; import io.netty.util.IllegalReferenceCountException; +import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.StringUtil; /** @@ -27,10 +28,7 @@ public class DefaultByteBufHolder implements ByteBufHolder { private final ByteBuf data; public DefaultByteBufHolder(ByteBuf data) { - if (data == null) { - throw new NullPointerException("data"); - } - this.data = data; + this.data = ObjectUtil.checkNotNull(data, "data"); } @Override @@ -135,13 +133,24 @@ public class DefaultByteBufHolder implements ByteBufHolder { return StringUtil.simpleClassName(this) + '(' + contentToString() + ')'; } + /** + * This implementation of the {@code equals} operation is restricted to + * work only with instances of the same class. The reason for that is that + * Netty library already has a number of classes that extend {@link DefaultByteBufHolder} and + * override {@code equals} method with an additional comparison logic and we + * need the symmetric property of the {@code equals} operation to be preserved. + * + * @param o the reference object with which to compare. + * @return {@code true} if this object is the same as the obj + * argument; {@code false} otherwise. + */ @Override public boolean equals(Object o) { if (this == o) { return true; } - if (o instanceof ByteBufHolder) { - return data.equals(((ByteBufHolder) o).content()); + if (o != null && getClass() == o.getClass()) { + return data.equals(((DefaultByteBufHolder) o).data); } return false; } diff --git a/buffer/src/main/java/io/netty/buffer/EmptyByteBuf.java b/buffer/src/main/java/io/netty/buffer/EmptyByteBuf.java index b954318..c9717ba 100644 --- a/buffer/src/main/java/io/netty/buffer/EmptyByteBuf.java +++ b/buffer/src/main/java/io/netty/buffer/EmptyByteBuf.java @@ -16,8 +16,11 @@ package io.netty.buffer; +import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero; + import io.netty.util.ByteProcessor; import io.netty.util.internal.EmptyArrays; +import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.PlatformDependent; import io.netty.util.internal.StringUtil; @@ -62,11 +65,7 @@ public final class EmptyByteBuf extends ByteBuf { } private EmptyByteBuf(ByteBufAllocator alloc, ByteOrder order) { - if (alloc == null) { - throw new NullPointerException("alloc"); - } - - this.alloc = alloc; + this.alloc = ObjectUtil.checkNotNull(alloc, "alloc"); this.order = order; str = StringUtil.simpleClassName(this) + (order == ByteOrder.BIG_ENDIAN? "BE" : "LE"); } @@ -118,10 +117,7 @@ public final class EmptyByteBuf extends ByteBuf { @Override public ByteBuf order(ByteOrder endianness) { - if (endianness == null) { - throw new NullPointerException("endianness"); - } - if (endianness == order()) { + if (ObjectUtil.checkNotNull(endianness, "endianness") == order()) { return this; } @@ -223,9 +219,7 @@ public final class EmptyByteBuf extends ByteBuf { @Override public ByteBuf ensureWritable(int minWritableBytes) { - if (minWritableBytes < 0) { - throw new IllegalArgumentException("minWritableBytes: " + minWritableBytes + " (expected: >= 0)"); - } + checkPositiveOrZero(minWritableBytes, "minWritableBytes"); if (minWritableBytes != 0) { throw new IndexOutOfBoundsException(); } @@ -234,9 +228,7 @@ public final class EmptyByteBuf extends ByteBuf { @Override public int ensureWritable(int minWritableBytes, boolean force) { - if (minWritableBytes < 0) { - throw new IllegalArgumentException("minWritableBytes: " + minWritableBytes + " (expected: >= 0)"); - } + checkPositiveOrZero(minWritableBytes, "minWritableBytes"); if (minWritableBytes == 0) { return 0; @@ -686,7 +678,7 @@ public final class EmptyByteBuf extends ByteBuf { @Override public CharSequence readCharSequence(int length, Charset charset) { checkLength(length); - return null; + return StringUtil.EMPTY_STRING; } @Override @@ -964,6 +956,11 @@ public final class EmptyByteBuf extends ByteBuf { } } + @Override + public boolean isContiguous() { + return true; + } + @Override public String toString(Charset charset) { return ""; @@ -1048,9 +1045,7 @@ public final class EmptyByteBuf extends ByteBuf { } private ByteBuf checkIndex(int index, int length) { - if (length < 0) { - throw new IllegalArgumentException("length: " + length); - } + checkPositiveOrZero(length, "length"); if (index != 0 || length != 0) { throw new IndexOutOfBoundsException(); } @@ -1058,9 +1053,7 @@ public final class EmptyByteBuf extends ByteBuf { } private ByteBuf checkLength(int length) { - if (length < 0) { - throw new IllegalArgumentException("length: " + length + " (expected: >= 0)"); - } + checkPositiveOrZero(length, "length"); if (length != 0) { throw new IndexOutOfBoundsException(); } diff --git a/buffer/src/main/java/io/netty/buffer/FixedCompositeByteBuf.java b/buffer/src/main/java/io/netty/buffer/FixedCompositeByteBuf.java index 08c8d7f..c28af6f 100644 --- a/buffer/src/main/java/io/netty/buffer/FixedCompositeByteBuf.java +++ b/buffer/src/main/java/io/netty/buffer/FixedCompositeByteBuf.java @@ -49,7 +49,7 @@ final class FixedCompositeByteBuf extends AbstractReferenceCountedByteBuf { order = ByteOrder.BIG_ENDIAN; nioBufferCount = 1; capacity = 0; - direct = false; + direct = Unpooled.EMPTY_BUFFER.isDirect(); } else { ByteBuf b = buffers[0]; this.buffers = buffers; diff --git a/buffer/src/main/java/io/netty/buffer/PoolArena.java b/buffer/src/main/java/io/netty/buffer/PoolArena.java index 3bc948b..308eb96 100644 --- a/buffer/src/main/java/io/netty/buffer/PoolArena.java +++ b/buffer/src/main/java/io/netty/buffer/PoolArena.java @@ -26,6 +26,7 @@ import java.util.Collections; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; +import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero; import static java.lang.Math.max; abstract class PoolArena<T> implements PoolArenaMetric { @@ -275,7 +276,7 @@ abstract class PoolArena<T> implements PoolArenaMetric { return; } - freeChunk(chunk, handle, sizeClass, nioBuffer); + freeChunk(chunk, handle, sizeClass, nioBuffer, false); } } @@ -286,21 +287,25 @@ abstract class PoolArena<T> implements PoolArenaMetric { return isTiny(normCapacity) ? SizeClass.Tiny : SizeClass.Small; } - void freeChunk(PoolChunk<T> chunk, long handle, SizeClass sizeClass, ByteBuffer nioBuffer) { + void freeChunk(PoolChunk<T> chunk, long handle, SizeClass sizeClass, ByteBuffer nioBuffer, boolean finalizer) { final boolean destroyChunk; synchronized (this) { - switch (sizeClass) { - case Normal: - ++deallocationsNormal; - break; - case Small: - ++deallocationsSmall; - break; - case Tiny: - ++deallocationsTiny; - break; - default: - throw new Error(); + // We only call this if freeChunk is not called because of the PoolThreadCache finalizer as otherwise this + // may fail due lazy class-loading in for example tomcat. + if (!finalizer) { + switch (sizeClass) { + case Normal: + ++deallocationsNormal; + break; + case Small: + ++deallocationsSmall; + break; + case Tiny: + ++deallocationsTiny; + break; + default: + throw new Error(); + } } destroyChunk = !chunk.parent.free(chunk, handle, nioBuffer); } @@ -330,9 +335,7 @@ abstract class PoolArena<T> implements PoolArenaMetric { } int normalizeCapacity(int reqCapacity) { - if (reqCapacity < 0) { - throw new IllegalArgumentException("capacity: " + reqCapacity + " (expected: 0+)"); - } + checkPositiveOrZero(reqCapacity, "reqCapacity"); if (reqCapacity >= chunkSize) { return directMemoryCacheAlignment == 0 ? reqCapacity : alignCapacity(reqCapacity); @@ -376,9 +379,7 @@ abstract class PoolArena<T> implements PoolArenaMetric { } void reallocate(PooledByteBuf<T> buf, int newCapacity, boolean freeOldMemory) { - if (newCapacity < 0 || newCapacity > buf.maxCapacity()) { - throw new IllegalArgumentException("newCapacity: " + newCapacity); - } + assert newCapacity >= 0 && newCapacity <= buf.maxCapacity(); int oldCapacity = buf.length; if (oldCapacity == newCapacity) { @@ -391,29 +392,17 @@ abstract class PoolArena<T> implements PoolArenaMetric { T oldMemory = buf.memory; int oldOffset = buf.offset; int oldMaxLength = buf.maxLength; - int readerIndex = buf.readerIndex(); - int writerIndex = buf.writerIndex(); + // This does not touch buf's reader/writer indices allocate(parent.threadCache(), buf, newCapacity); + int bytesToCopy; if (newCapacity > oldCapacity) { - memoryCopy( - oldMemory, oldOffset, - buf.memory, buf.offset, oldCapacity); - } else if (newCapacity < oldCapacity) { - if (readerIndex < newCapacity) { - if (writerIndex > newCapacity) { - writerIndex = newCapacity; - } - memoryCopy( - oldMemory, oldOffset + readerIndex, - buf.memory, buf.offset + readerIndex, writerIndex - readerIndex); - } else { - readerIndex = writerIndex = newCapacity; - } + bytesToCopy = oldCapacity; + } else { + buf.trimIndicesToCapacity(newCapacity); + bytesToCopy = newCapacity; } - - buf.setIndex(readerIndex, writerIndex); - + memoryCopy(oldMemory, oldOffset, buf, bytesToCopy); if (freeOldMemory) { free(oldChunk, oldNioBuffer, oldHandle, oldMaxLength, buf.cache); } @@ -580,7 +569,7 @@ abstract class PoolArena<T> implements PoolArenaMetric { protected abstract PoolChunk<T> newChunk(int pageSize, int maxOrder, int pageShifts, int chunkSize); protected abstract PoolChunk<T> newUnpooledChunk(int capacity); protected abstract PooledByteBuf<T> newByteBuf(int maxCapacity); - protected abstract void memoryCopy(T src, int srcOffset, T dst, int dstOffset, int length); + protected abstract void memoryCopy(T src, int srcOffset, PooledByteBuf<T> dst, int length); protected abstract void destroyChunk(PoolChunk<T> chunk); @Override @@ -703,12 +692,12 @@ abstract class PoolArena<T> implements PoolArenaMetric { } @Override - protected void memoryCopy(byte[] src, int srcOffset, byte[] dst, int dstOffset, int length) { + protected void memoryCopy(byte[] src, int srcOffset, PooledByteBuf<byte[]> dst, int length) { if (length == 0) { return; } - System.arraycopy(src, srcOffset, dst, dstOffset, length); + System.arraycopy(src, srcOffset, dst.memory, dst.offset, length); } } @@ -788,7 +777,7 @@ abstract class PoolArena<T> implements PoolArenaMetric { } @Override - protected void memoryCopy(ByteBuffer src, int srcOffset, ByteBuffer dst, int dstOffset, int length) { + protected void memoryCopy(ByteBuffer src, int srcOffset, PooledByteBuf<ByteBuffer> dstBuf, int length) { if (length == 0) { return; } @@ -796,13 +785,13 @@ abstract class PoolArena<T> implements PoolArenaMetric { if (HAS_UNSAFE) { PlatformDependent.copyMemory( PlatformDependent.directBufferAddress(src) + srcOffset, - PlatformDependent.directBufferAddress(dst) + dstOffset, length); + PlatformDependent.directBufferAddress(dstBuf.memory) + dstBuf.offset, length); } else { // We must duplicate the NIO buffers because they may be accessed by other Netty buffers. src = src.duplicate(); - dst = dst.duplicate(); + ByteBuffer dst = dstBuf.internalNioBuffer(); src.position(srcOffset).limit(srcOffset + length); - dst.position(dstOffset); + dst.position(dstBuf.offset); dst.put(src); } } diff --git a/buffer/src/main/java/io/netty/buffer/PoolSubpage.java b/buffer/src/main/java/io/netty/buffer/PoolSubpage.java index f897eee..328aae6 100644 --- a/buffer/src/main/java/io/netty/buffer/PoolSubpage.java +++ b/buffer/src/main/java/io/netty/buffer/PoolSubpage.java @@ -204,16 +204,24 @@ final class PoolSubpage<T> implements PoolSubpageMetric { final int maxNumElems; final int numAvail; final int elemSize; - synchronized (chunk.arena) { - if (!this.doNotDestroy) { - doNotDestroy = false; - // Not used for creating the String. - maxNumElems = numAvail = elemSize = -1; - } else { - doNotDestroy = true; - maxNumElems = this.maxNumElems; - numAvail = this.numAvail; - elemSize = this.elemSize; + if (chunk == null) { + // This is the head so there is no need to synchronize at all as these never change. + doNotDestroy = true; + maxNumElems = 0; + numAvail = 0; + elemSize = -1; + } else { + synchronized (chunk.arena) { + if (!this.doNotDestroy) { + doNotDestroy = false; + // Not used for creating the String. + maxNumElems = numAvail = elemSize = -1; + } else { + doNotDestroy = true; + maxNumElems = this.maxNumElems; + numAvail = this.numAvail; + elemSize = this.elemSize; + } } } @@ -227,6 +235,11 @@ final class PoolSubpage<T> implements PoolSubpageMetric { @Override public int maxNumElements() { + if (chunk == null) { + // It's the head. + return 0; + } + synchronized (chunk.arena) { return maxNumElems; } @@ -234,6 +247,11 @@ final class PoolSubpage<T> implements PoolSubpageMetric { @Override public int numAvailable() { + if (chunk == null) { + // It's the head. + return 0; + } + synchronized (chunk.arena) { return numAvail; } @@ -241,6 +259,11 @@ final class PoolSubpage<T> implements PoolSubpageMetric { @Override public int elementSize() { + if (chunk == null) { + // It's the head. + return -1; + } + synchronized (chunk.arena) { return elemSize; } diff --git a/buffer/src/main/java/io/netty/buffer/PoolThreadCache.java b/buffer/src/main/java/io/netty/buffer/PoolThreadCache.java index de2a9be..c1bf7e6 100644 --- a/buffer/src/main/java/io/netty/buffer/PoolThreadCache.java +++ b/buffer/src/main/java/io/netty/buffer/PoolThreadCache.java @@ -17,10 +17,13 @@ package io.netty.buffer; +import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero; + import io.netty.buffer.PoolArena.SizeClass; -import io.netty.util.Recycler; -import io.netty.util.Recycler.Handle; import io.netty.util.internal.MathUtil; +import io.netty.util.internal.ObjectPool; +import io.netty.util.internal.ObjectPool.Handle; +import io.netty.util.internal.ObjectPool.ObjectCreator; import io.netty.util.internal.PlatformDependent; import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; @@ -65,10 +68,7 @@ final class PoolThreadCache { PoolThreadCache(PoolArena<byte[]> heapArena, PoolArena<ByteBuffer> directArena, int tinyCacheSize, int smallCacheSize, int normalCacheSize, int maxCachedBufferCapacity, int freeSweepAllocationThreshold) { - if (maxCachedBufferCapacity < 0) { - throw new IllegalArgumentException("maxCachedBufferCapacity: " - + maxCachedBufferCapacity + " (expected: >= 0)"); - } + checkPositiveOrZero(maxCachedBufferCapacity, "maxCachedBufferCapacity"); this.freeSweepAllocationThreshold = freeSweepAllocationThreshold; this.heapArena = heapArena; this.directArena = directArena; @@ -228,23 +228,23 @@ final class PoolThreadCache { try { super.finalize(); } finally { - free(); + free(true); } } /** * Should be called if the Thread that uses this cache is about to exist to release resources out of the cache */ - void free() { + void free(boolean finalizer) { // As free() may be called either by the finalizer or by FastThreadLocal.onRemoval(...) we need to ensure // we only call this one time. if (freed.compareAndSet(false, true)) { - int numFreed = free(tinySubPageDirectCaches) + - free(smallSubPageDirectCaches) + - free(normalDirectCaches) + - free(tinySubPageHeapCaches) + - free(smallSubPageHeapCaches) + - free(normalHeapCaches); + int numFreed = free(tinySubPageDirectCaches, finalizer) + + free(smallSubPageDirectCaches, finalizer) + + free(normalDirectCaches, finalizer) + + free(tinySubPageHeapCaches, finalizer) + + free(smallSubPageHeapCaches, finalizer) + + free(normalHeapCaches, finalizer); if (numFreed > 0 && logger.isDebugEnabled()) { logger.debug("Freed {} thread-local buffer(s) from thread: {}", numFreed, @@ -261,23 +261,23 @@ final class PoolThreadCache { } } - private static int free(MemoryRegionCache<?>[] caches) { + private static int free(MemoryRegionCache<?>[] caches, boolean finalizer) { if (caches == null) { return 0; } int numFreed = 0; for (MemoryRegionCache<?> c: caches) { - numFreed += free(c); + numFreed += free(c, finalizer); } return numFreed; } - private static int free(MemoryRegionCache<?> cache) { + private static int free(MemoryRegionCache<?> cache, boolean finalizer) { if (cache == null) { return 0; } - return cache.free(); + return cache.free(finalizer); } void trim() { @@ -419,16 +419,16 @@ final class PoolThreadCache { /** * Clear out this cache and free up all previous cached {@link PoolChunk}s and {@code handle}s. */ - public final int free() { - return free(Integer.MAX_VALUE); + public final int free(boolean finalizer) { + return free(Integer.MAX_VALUE, finalizer); } - private int free(int max) { + private int free(int max, boolean finalizer) { int numFreed = 0; for (; numFreed < max; numFreed++) { Entry<T> entry = queue.poll(); if (entry != null) { - freeEntry(entry); + freeEntry(entry, finalizer); } else { // all cleared return numFreed; @@ -446,20 +446,23 @@ final class PoolThreadCache { // We not even allocated all the number that are if (free > 0) { - free(free); + free(free, false); } } @SuppressWarnings({ "unchecked", "rawtypes" }) - private void freeEntry(Entry entry) { + private void freeEntry(Entry entry, boolean finalizer) { PoolChunk chunk = entry.chunk; long handle = entry.handle; ByteBuffer nioBuffer = entry.nioBuffer; - // recycle now so PoolChunk can be GC'ed. - entry.recycle(); + if (!finalizer) { + // recycle now so PoolChunk can be GC'ed. This will only be done if this is not freed because of + // a finalizer. + entry.recycle(); + } - chunk.arena.freeChunk(chunk, handle, sizeClass, nioBuffer); + chunk.arena.freeChunk(chunk, handle, sizeClass, nioBuffer, finalizer); } static final class Entry<T> { @@ -490,12 +493,12 @@ final class PoolThreadCache { } @SuppressWarnings("rawtypes") - private static final Recycler<Entry> RECYCLER = new Recycler<Entry>() { + private static final ObjectPool<Entry> RECYCLER = ObjectPool.newPool(new ObjectCreator<Entry>() { @SuppressWarnings("unchecked") @Override - protected Entry newObject(Handle<Entry> handle) { + public Entry newObject(Handle<Entry> handle) { return new Entry(handle); } - }; + }); } } diff --git a/buffer/src/main/java/io/netty/buffer/PooledByteBuf.java b/buffer/src/main/java/io/netty/buffer/PooledByteBuf.java index beffbb0..3ef1e26 100644 --- a/buffer/src/main/java/io/netty/buffer/PooledByteBuf.java +++ b/buffer/src/main/java/io/netty/buffer/PooledByteBuf.java @@ -16,15 +16,19 @@ package io.netty.buffer; -import io.netty.util.Recycler; -import io.netty.util.Recycler.Handle; +import io.netty.util.internal.ObjectPool.Handle; +import java.io.IOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.FileChannel; +import java.nio.channels.GatheringByteChannel; +import java.nio.channels.ScatteringByteChannel; abstract class PooledByteBuf<T> extends AbstractReferenceCountedByteBuf { - private final Recycler.Handle<PooledByteBuf<T>> recyclerHandle; + private final Handle<PooledByteBuf<T>> recyclerHandle; protected PoolChunk<T> chunk; protected long handle; @@ -37,7 +41,7 @@ abstract class PooledByteBuf<T> extends AbstractReferenceCountedByteBuf { private ByteBufAllocator allocator; @SuppressWarnings("unchecked") - protected PooledByteBuf(Recycler.Handle<? extends PooledByteBuf<T>> recyclerHandle, int maxCapacity) { + protected PooledByteBuf(Handle<? extends PooledByteBuf<T>> recyclerHandle, int maxCapacity) { super(maxCapacity); this.recyclerHandle = (Handle<PooledByteBuf<T>>) recyclerHandle; } @@ -72,7 +76,7 @@ abstract class PooledByteBuf<T> extends AbstractReferenceCountedByteBuf { */ final void reuse(int maxCapacity) { maxCapacity(maxCapacity); - setRefCnt(1); + resetRefCnt(); setIndex0(0, 0); discardMarks(); } @@ -82,36 +86,30 @@ abstract class PooledByteBuf<T> extends AbstractReferenceCountedByteBuf { return length; } + @Override + public int maxFastWritableBytes() { + return Math.min(maxLength, maxCapacity()) - writerIndex; + } + @Override public final ByteBuf capacity(int newCapacity) { + if (newCapacity == length) { + ensureAccessible(); + return this; + } checkNewCapacity(newCapacity); - - // If the request capacity does not require reallocation, just update the length of the memory. - if (chunk.unpooled) { - if (newCapacity == length) { - return this; - } - } else { + if (!chunk.unpooled) { + // If the request capacity does not require reallocation, just update the length of the memory. if (newCapacity > length) { if (newCapacity <= maxLength) { length = newCapacity; return this; } - } else if (newCapacity < length) { - if (newCapacity > maxLength >>> 1) { - if (maxLength <= 512) { - if (newCapacity > maxLength - 16) { - length = newCapacity; - setIndex(Math.min(readerIndex(), newCapacity), Math.min(writerIndex(), newCapacity)); - return this; - } - } else { // > 512 (i.e. >= 1024) - length = newCapacity; - setIndex(Math.min(readerIndex(), newCapacity), Math.min(writerIndex(), newCapacity)); - return this; - } - } - } else { + } else if (newCapacity > maxLength >>> 1 && + (maxLength > 512 || newCapacity > maxLength - 16)) { + // here newCapacity < length + length = newCapacity; + trimIndicesToCapacity(newCapacity); return this; } } @@ -156,6 +154,8 @@ abstract class PooledByteBuf<T> extends AbstractReferenceCountedByteBuf { ByteBuffer tmpNioBuf = this.tmpNioBuf; if (tmpNioBuf == null) { this.tmpNioBuf = tmpNioBuf = newInternalNioBuffer(memory); + } else { + tmpNioBuf.clear(); } return tmpNioBuf; } @@ -182,4 +182,86 @@ abstract class PooledByteBuf<T> extends AbstractReferenceCountedByteBuf { protected final int idx(int index) { return offset + index; } + + final ByteBuffer _internalNioBuffer(int index, int length, boolean duplicate) { + index = idx(index); + ByteBuffer buffer = duplicate ? newInternalNioBuffer(memory) : internalNioBuffer(); + buffer.limit(index + length).position(index); + return buffer; + } + + ByteBuffer duplicateInternalNioBuffer(int index, int length) { + checkIndex(index, length); + return _internalNioBuffer(index, length, true); + } + + @Override + public final ByteBuffer internalNioBuffer(int index, int length) { + checkIndex(index, length); + return _internalNioBuffer(index, length, false); + } + + @Override + public final int nioBufferCount() { + return 1; + } + + @Override + public final ByteBuffer nioBuffer(int index, int length) { + return duplicateInternalNioBuffer(index, length).slice(); + } + + @Override + public final ByteBuffer[] nioBuffers(int index, int length) { + return new ByteBuffer[] { nioBuffer(index, length) }; + } + + @Override + public final boolean isContiguous() { + return true; + } + + @Override + public final int getBytes(int index, GatheringByteChannel out, int length) throws IOException { + return out.write(duplicateInternalNioBuffer(index, length)); + } + + @Override + public final int readBytes(GatheringByteChannel out, int length) throws IOException { + checkReadableBytes(length); + int readBytes = out.write(_internalNioBuffer(readerIndex, length, false)); + readerIndex += readBytes; + return readBytes; + } + + @Override + public final int getBytes(int index, FileChannel out, long position, int length) throws IOException { + return out.write(duplicateInternalNioBuffer(index, length), position); + } + + @Override + public final int readBytes(FileChannel out, long position, int length) throws IOException { + checkReadableBytes(length); + int readBytes = out.write(_internalNioBuffer(readerIndex, length, false), position); + readerIndex += readBytes; + return readBytes; + } + + @Override + public final int setBytes(int index, ScatteringByteChannel in, int length) throws IOException { + try { + return in.read(internalNioBuffer(index, length)); + } catch (ClosedChannelException ignored) { + return -1; + } + } + + @Override + public final int setBytes(int index, FileChannel in, long position, int length) throws IOException { + try { + return in.read(internalNioBuffer(index, length), position); + } catch (ClosedChannelException ignored) { + return -1; + } + } } diff --git a/buffer/src/main/java/io/netty/buffer/PooledByteBufAllocator.java b/buffer/src/main/java/io/netty/buffer/PooledByteBufAllocator.java index de6eee1..3d4bc94 100644 --- a/buffer/src/main/java/io/netty/buffer/PooledByteBufAllocator.java +++ b/buffer/src/main/java/io/netty/buffer/PooledByteBufAllocator.java @@ -16,12 +16,16 @@ package io.netty.buffer; +import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero; + import io.netty.util.NettyRuntime; +import io.netty.util.concurrent.EventExecutor; import io.netty.util.concurrent.FastThreadLocal; import io.netty.util.concurrent.FastThreadLocalThread; import io.netty.util.internal.PlatformDependent; import io.netty.util.internal.StringUtil; import io.netty.util.internal.SystemPropertyUtil; +import io.netty.util.internal.ThreadExecutorMap; import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; @@ -29,6 +33,7 @@ import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.concurrent.TimeUnit; public class PooledByteBufAllocator extends AbstractByteBufAllocator implements ByteBufAllocatorMetricProvider { @@ -43,6 +48,7 @@ public class PooledByteBufAllocator extends AbstractByteBufAllocator implements private static final int DEFAULT_NORMAL_CACHE_SIZE; private static final int DEFAULT_MAX_CACHED_BUFFER_CAPACITY; private static final int DEFAULT_CACHE_TRIM_INTERVAL; + private static final long DEFAULT_CACHE_TRIM_INTERVAL_MILLIS; private static final boolean DEFAULT_USE_CACHE_FOR_ALL_THREADS; private static final int DEFAULT_DIRECT_MEMORY_CACHE_ALIGNMENT; static final int DEFAULT_MAX_CACHED_BYTEBUFFERS_PER_CHUNK; @@ -50,6 +56,13 @@ public class PooledByteBufAllocator extends AbstractByteBufAllocator implements private static final int MIN_PAGE_SIZE = 4096; private static final int MAX_CHUNK_SIZE = (int) (((long) Integer.MAX_VALUE + 1) / 2); + private final Runnable trimTask = new Runnable() { + @Override + public void run() { + PooledByteBufAllocator.this.trimCurrentThreadCache(); + } + }; + static { int defaultPageSize = SystemPropertyUtil.getInt("io.netty.allocator.pageSize", 8192); Throwable pageSizeFallbackCause = null; @@ -111,6 +124,23 @@ public class PooledByteBufAllocator extends AbstractByteBufAllocator implements DEFAULT_CACHE_TRIM_INTERVAL = SystemPropertyUtil.getInt( "io.netty.allocator.cacheTrimInterval", 8192); + if (SystemPropertyUtil.contains("io.netty.allocation.cacheTrimIntervalMillis")) { + logger.warn("-Dio.netty.allocation.cacheTrimIntervalMillis is deprecated," + + " use -Dio.netty.allocator.cacheTrimIntervalMillis"); + + if (SystemPropertyUtil.contains("io.netty.allocator.cacheTrimIntervalMillis")) { + // Both system properties are specified. Use the non-deprecated one. + DEFAULT_CACHE_TRIM_INTERVAL_MILLIS = SystemPropertyUtil.getLong( + "io.netty.allocator.cacheTrimIntervalMillis", 0); + } else { + DEFAULT_CACHE_TRIM_INTERVAL_MILLIS = SystemPropertyUtil.getLong( + "io.netty.allocation.cacheTrimIntervalMillis", 0); + } + } else { + DEFAULT_CACHE_TRIM_INTERVAL_MILLIS = SystemPropertyUtil.getLong( + "io.netty.allocator.cacheTrimIntervalMillis", 0); + } + DEFAULT_USE_CACHE_FOR_ALL_THREADS = SystemPropertyUtil.getBoolean( "io.netty.allocator.useCacheForAllThreads", true); @@ -141,6 +171,7 @@ public class PooledByteBufAllocator extends AbstractByteBufAllocator implements logger.debug("-Dio.netty.allocator.normalCacheSize: {}", DEFAULT_NORMAL_CACHE_SIZE); logger.debug("-Dio.netty.allocator.maxCachedBufferCapacity: {}", DEFAULT_MAX_CACHED_BUFFER_CAPACITY); logger.debug("-Dio.netty.allocator.cacheTrimInterval: {}", DEFAULT_CACHE_TRIM_INTERVAL); + logger.debug("-Dio.netty.allocator.cacheTrimIntervalMillis: {}", DEFAULT_CACHE_TRIM_INTERVAL_MILLIS); logger.debug("-Dio.netty.allocator.useCacheForAllThreads: {}", DEFAULT_USE_CACHE_FOR_ALL_THREADS); logger.debug("-Dio.netty.allocator.maxCachedByteBuffersPerChunk: {}", DEFAULT_MAX_CACHED_BYTEBUFFERS_PER_CHUNK); @@ -215,17 +246,10 @@ public class PooledByteBufAllocator extends AbstractByteBufAllocator implements this.normalCacheSize = normalCacheSize; chunkSize = validateAndCalculateChunkSize(pageSize, maxOrder); - if (nHeapArena < 0) { - throw new IllegalArgumentException("nHeapArena: " + nHeapArena + " (expected: >= 0)"); - } - if (nDirectArena < 0) { - throw new IllegalArgumentException("nDirectArea: " + nDirectArena + " (expected: >= 0)"); - } + checkPositiveOrZero(nHeapArena, "nHeapArena"); + checkPositiveOrZero(nDirectArena, "nDirectArena"); - if (directMemoryCacheAlignment < 0) { - throw new IllegalArgumentException("directMemoryCacheAlignment: " - + directMemoryCacheAlignment + " (expected: >= 0)"); - } + checkPositiveOrZero(directMemoryCacheAlignment, "directMemoryCacheAlignment"); if (directMemoryCacheAlignment > 0 && !isDirectMemoryCacheAlignmentSupported()) { throw new IllegalArgumentException("directMemoryCacheAlignment is not supported"); } @@ -443,11 +467,20 @@ public class PooledByteBufAllocator extends AbstractByteBufAllocator implements final PoolArena<byte[]> heapArena = leastUsedArena(heapArenas); final PoolArena<ByteBuffer> directArena = leastUsedArena(directArenas); - Thread current = Thread.currentThread(); + final Thread current = Thread.currentThread(); if (useCacheForAllThreads || current instanceof FastThreadLocalThread) { - return new PoolThreadCache( + final PoolThreadCache cache = new PoolThreadCache( heapArena, directArena, tinyCacheSize, smallCacheSize, normalCacheSize, DEFAULT_MAX_CACHED_BUFFER_CAPACITY, DEFAULT_CACHE_TRIM_INTERVAL); + + if (DEFAULT_CACHE_TRIM_INTERVAL_MILLIS > 0) { + final EventExecutor executor = ThreadExecutorMap.currentExecutor(); + if (executor != null) { + executor.scheduleAtFixedRate(trimTask, DEFAULT_CACHE_TRIM_INTERVAL_MILLIS, + DEFAULT_CACHE_TRIM_INTERVAL_MILLIS, TimeUnit.MILLISECONDS); + } + } + return cache; } // No caching so just use 0 as sizes. return new PoolThreadCache(heapArena, directArena, 0, 0, 0, 0, 0); @@ -455,7 +488,7 @@ public class PooledByteBufAllocator extends AbstractByteBufAllocator implements @Override protected void onRemoval(PoolThreadCache threadCache) { - threadCache.free(); + threadCache.free(false); } private <T> PoolArena<T> leastUsedArena(PoolArena<T>[] arenas) { @@ -608,6 +641,21 @@ public class PooledByteBufAllocator extends AbstractByteBufAllocator implements return cache; } + /** + * Trim thread local cache for the current {@link Thread}, which will give back any cached memory that was not + * allocated frequently since the last trim operation. + * + * Returns {@code true} if a cache for the current {@link Thread} exists and so was trimmed, false otherwise. + */ + public boolean trimCurrentThreadCache() { + PoolThreadCache cache = threadCache.getIfExists(); + if (cache != null) { + cache.trim(); + return true; + } + return false; + } + /** * Returns the status of the allocator (which contains all metrics) as string. Be aware this may be expensive * and so should not called too frequently. diff --git a/buffer/src/main/java/io/netty/buffer/PooledDirectByteBuf.java b/buffer/src/main/java/io/netty/buffer/PooledDirectByteBuf.java index 9601150..3d77ecf 100644 --- a/buffer/src/main/java/io/netty/buffer/PooledDirectByteBuf.java +++ b/buffer/src/main/java/io/netty/buffer/PooledDirectByteBuf.java @@ -16,25 +16,24 @@ package io.netty.buffer; -import io.netty.util.Recycler; +import io.netty.util.internal.ObjectPool; +import io.netty.util.internal.ObjectPool.Handle; +import io.netty.util.internal.ObjectPool.ObjectCreator; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.ByteBuffer; -import java.nio.channels.ClosedChannelException; -import java.nio.channels.FileChannel; -import java.nio.channels.GatheringByteChannel; -import java.nio.channels.ScatteringByteChannel; final class PooledDirectByteBuf extends PooledByteBuf<ByteBuffer> { - private static final Recycler<PooledDirectByteBuf> RECYCLER = new Recycler<PooledDirectByteBuf>() { + private static final ObjectPool<PooledDirectByteBuf> RECYCLER = ObjectPool.newPool( + new ObjectCreator<PooledDirectByteBuf>() { @Override - protected PooledDirectByteBuf newObject(Handle<PooledDirectByteBuf> handle) { + public PooledDirectByteBuf newObject(Handle<PooledDirectByteBuf> handle) { return new PooledDirectByteBuf(handle, 0); } - }; + }); static PooledDirectByteBuf newInstance(int maxCapacity) { PooledDirectByteBuf buf = RECYCLER.get(); @@ -42,7 +41,7 @@ final class PooledDirectByteBuf extends PooledByteBuf<ByteBuffer> { return buf; } - private PooledDirectByteBuf(Recycler.Handle<PooledDirectByteBuf> recyclerHandle, int maxCapacity) { + private PooledDirectByteBuf(Handle<PooledDirectByteBuf> recyclerHandle, int maxCapacity) { super(recyclerHandle, maxCapacity); } @@ -126,55 +125,30 @@ final class PooledDirectByteBuf extends PooledByteBuf<ByteBuffer> { @Override public ByteBuf getBytes(int index, byte[] dst, int dstIndex, int length) { - getBytes(index, dst, dstIndex, length, false); - return this; - } - - private void getBytes(int index, byte[] dst, int dstIndex, int length, boolean internal) { checkDstIndex(index, length, dstIndex, dst.length); - ByteBuffer tmpBuf; - if (internal) { - tmpBuf = internalNioBuffer(); - } else { - tmpBuf = memory.duplicate(); - } - index = idx(index); - tmpBuf.clear().position(index).limit(index + length); - tmpBuf.get(dst, dstIndex, length); + _internalNioBuffer(index, length, true).get(dst, dstIndex, length); + return this; } @Override public ByteBuf readBytes(byte[] dst, int dstIndex, int length) { - checkReadableBytes(length); - getBytes(readerIndex, dst, dstIndex, length, true); + checkDstIndex(length, dstIndex, dst.length); + _internalNioBuffer(readerIndex, length, false).get(dst, dstIndex, length); readerIndex += length; return this; } @Override public ByteBuf getBytes(int index, ByteBuffer dst) { - getBytes(index, dst, false); + dst.put(duplicateInternalNioBuffer(index, dst.remaining())); return this; } - private void getBytes(int index, ByteBuffer dst, boolean internal) { - checkIndex(index, dst.remaining()); - ByteBuffer tmpBuf; - if (internal) { - tmpBuf = internalNioBuffer(); - } else { - tmpBuf = memory.duplicate(); - } - index = idx(index); - tmpBuf.clear().position(index).limit(index + dst.remaining()); - dst.put(tmpBuf); - } - @Override public ByteBuf readBytes(ByteBuffer dst) { int length = dst.remaining(); checkReadableBytes(length); - getBytes(readerIndex, dst, true); + dst.put(_internalNioBuffer(readerIndex, length, false)); readerIndex += length; return this; } @@ -201,61 +175,6 @@ final class PooledDirectByteBuf extends PooledByteBuf<ByteBuffer> { return this; } - @Override - public int getBytes(int index, GatheringByteChannel out, int length) throws IOException { - return getBytes(index, out, length, false); - } - - private int getBytes(int index, GatheringByteChannel out, int length, boolean internal) throws IOException { - checkIndex(index, length); - if (length == 0) { - return 0; - } - - ByteBuffer tmpBuf; - if (internal) { - tmpBuf = internalNioBuffer(); - } else { - tmpBuf = memory.duplicate(); - } - index = idx(index); - tmpBuf.clear().position(index).limit(index + length); - return out.write(tmpBuf); - } - - @Override - public int getBytes(int index, FileChannel out, long position, int length) throws IOException { - return getBytes(index, out, position, length, false); - } - - private int getBytes(int index, FileChannel out, long position, int length, boolean internal) throws IOException { - checkIndex(index, length); - if (length == 0) { - return 0; - } - - ByteBuffer tmpBuf = internal ? internalNioBuffer() : memory.duplicate(); - index = idx(index); - tmpBuf.clear().position(index).limit(index + length); - return out.write(tmpBuf, position); - } - - @Override - public int readBytes(GatheringByteChannel out, int length) throws IOException { - checkReadableBytes(length); - int readBytes = getBytes(readerIndex, out, length, true); - readerIndex += readBytes; - return readBytes; - } - - @Override - public int readBytes(FileChannel out, long position, int length) throws IOException { - checkReadableBytes(length); - int readBytes = getBytes(readerIndex, out, position, length, true); - readerIndex += readBytes; - return readBytes; - } - @Override protected void _setByte(int index, int value) { memory.put(idx(index), (byte) value); @@ -327,23 +246,21 @@ final class PooledDirectByteBuf extends PooledByteBuf<ByteBuffer> { @Override public ByteBuf setBytes(int index, byte[] src, int srcIndex, int length) { checkSrcIndex(index, length, srcIndex, src.length); - ByteBuffer tmpBuf = internalNioBuffer(); - index = idx(index); - tmpBuf.clear().position(index).limit(index + length); - tmpBuf.put(src, srcIndex, length); + _internalNioBuffer(index, length, false).put(src, srcIndex, length); return this; } @Override public ByteBuf setBytes(int index, ByteBuffer src) { - checkIndex(index, src.remaining()); + int length = src.remaining(); + checkIndex(index, length); ByteBuffer tmpBuf = internalNioBuffer(); if (src == tmpBuf) { src = src.duplicate(); } index = idx(index); - tmpBuf.clear().position(index).limit(index + src.remaining()); + tmpBuf.limit(index + length).position(index); tmpBuf.put(src); return this; } @@ -357,67 +274,16 @@ final class PooledDirectByteBuf extends PooledByteBuf<ByteBuffer> { return readBytes; } ByteBuffer tmpBuf = internalNioBuffer(); - tmpBuf.clear().position(idx(index)); + tmpBuf.position(idx(index)); tmpBuf.put(tmp, 0, readBytes); return readBytes; } - @Override - public int setBytes(int index, ScatteringByteChannel in, int length) throws IOException { - checkIndex(index, length); - ByteBuffer tmpBuf = internalNioBuffer(); - index = idx(index); - tmpBuf.clear().position(index).limit(index + length); - try { - return in.read(tmpBuf); - } catch (ClosedChannelException ignored) { - return -1; - } - } - - @Override - public int setBytes(int index, FileChannel in, long position, int length) throws IOException { - checkIndex(index, length); - ByteBuffer tmpBuf = internalNioBuffer(); - index = idx(index); - tmpBuf.clear().position(index).limit(index + length); - try { - return in.read(tmpBuf, position); - } catch (ClosedChannelException ignored) { - return -1; - } - } - @Override public ByteBuf copy(int index, int length) { checkIndex(index, length); ByteBuf copy = alloc().directBuffer(length, maxCapacity()); - copy.writeBytes(this, index, length); - return copy; - } - - @Override - public int nioBufferCount() { - return 1; - } - - @Override - public ByteBuffer nioBuffer(int index, int length) { - checkIndex(index, length); - index = idx(index); - return ((ByteBuffer) memory.duplicate().position(index).limit(index + length)).slice(); - } - - @Override - public ByteBuffer[] nioBuffers(int index, int length) { - return new ByteBuffer[] { nioBuffer(index, length) }; - } - - @Override - public ByteBuffer internalNioBuffer(int index, int length) { - checkIndex(index, length); - index = idx(index); - return (ByteBuffer) internalNioBuffer().clear().position(index).limit(index + length); + return copy.writeBytes(this, index, length); } @Override diff --git a/buffer/src/main/java/io/netty/buffer/PooledDuplicatedByteBuf.java b/buffer/src/main/java/io/netty/buffer/PooledDuplicatedByteBuf.java index 1260f4e..89646e2 100644 --- a/buffer/src/main/java/io/netty/buffer/PooledDuplicatedByteBuf.java +++ b/buffer/src/main/java/io/netty/buffer/PooledDuplicatedByteBuf.java @@ -17,8 +17,9 @@ package io.netty.buffer; import io.netty.util.ByteProcessor; -import io.netty.util.Recycler; -import io.netty.util.Recycler.Handle; +import io.netty.util.internal.ObjectPool; +import io.netty.util.internal.ObjectPool.Handle; +import io.netty.util.internal.ObjectPool.ObjectCreator; import java.io.IOException; import java.io.InputStream; @@ -30,12 +31,13 @@ import java.nio.channels.ScatteringByteChannel; final class PooledDuplicatedByteBuf extends AbstractPooledDerivedByteBuf { - private static final Recycler<PooledDuplicatedByteBuf> RECYCLER = new Recycler<PooledDuplicatedByteBuf>() { + private static final ObjectPool<PooledDuplicatedByteBuf> RECYCLER = ObjectPool.newPool( + new ObjectCreator<PooledDuplicatedByteBuf>() { @Override - protected PooledDuplicatedByteBuf newObject(Handle<PooledDuplicatedByteBuf> handle) { + public PooledDuplicatedByteBuf newObject(Handle<PooledDuplicatedByteBuf> handle) { return new PooledDuplicatedByteBuf(handle); } - }; + }); static PooledDuplicatedByteBuf newInstance(AbstractByteBuf unwrapped, ByteBuf wrapped, int readerIndex, int writerIndex) { diff --git a/buffer/src/main/java/io/netty/buffer/PooledHeapByteBuf.java b/buffer/src/main/java/io/netty/buffer/PooledHeapByteBuf.java index 467bde0..f271e6c 100644 --- a/buffer/src/main/java/io/netty/buffer/PooledHeapByteBuf.java +++ b/buffer/src/main/java/io/netty/buffer/PooledHeapByteBuf.java @@ -14,26 +14,25 @@ package io.netty.buffer; -import io.netty.util.Recycler; +import io.netty.util.internal.ObjectPool; +import io.netty.util.internal.ObjectPool.Handle; +import io.netty.util.internal.ObjectPool.ObjectCreator; import io.netty.util.internal.PlatformDependent; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.ByteBuffer; -import java.nio.channels.ClosedChannelException; -import java.nio.channels.FileChannel; -import java.nio.channels.GatheringByteChannel; -import java.nio.channels.ScatteringByteChannel; class PooledHeapByteBuf extends PooledByteBuf<byte[]> { - private static final Recycler<PooledHeapByteBuf> RECYCLER = new Recycler<PooledHeapByteBuf>() { + private static final ObjectPool<PooledHeapByteBuf> RECYCLER = ObjectPool.newPool( + new ObjectCreator<PooledHeapByteBuf>() { @Override - protected PooledHeapByteBuf newObject(Handle<PooledHeapByteBuf> handle) { + public PooledHeapByteBuf newObject(Handle<PooledHeapByteBuf> handle) { return new PooledHeapByteBuf(handle, 0); } - }; + }); static PooledHeapByteBuf newInstance(int maxCapacity) { PooledHeapByteBuf buf = RECYCLER.get(); @@ -41,7 +40,7 @@ class PooledHeapByteBuf extends PooledByteBuf<byte[]> { return buf; } - PooledHeapByteBuf(Recycler.Handle<? extends PooledHeapByteBuf> recyclerHandle, int maxCapacity) { + PooledHeapByteBuf(Handle<? extends PooledHeapByteBuf> recyclerHandle, int maxCapacity) { super(recyclerHandle, maxCapacity); } @@ -117,8 +116,9 @@ class PooledHeapByteBuf extends PooledByteBuf<byte[]> { @Override public final ByteBuf getBytes(int index, ByteBuffer dst) { - checkIndex(index, dst.remaining()); - dst.put(memory, idx(index), dst.remaining()); + int length = dst.remaining(); + checkIndex(index, length); + dst.put(memory, idx(index), length); return this; } @@ -129,51 +129,6 @@ class PooledHeapByteBuf extends PooledByteBuf<byte[]> { return this; } - @Override - public final int getBytes(int index, GatheringByteChannel out, int length) throws IOException { - return getBytes(index, out, length, false); - } - - private int getBytes(int index, GatheringByteChannel out, int length, boolean internal) throws IOException { - checkIndex(index, length); - index = idx(index); - ByteBuffer tmpBuf; - if (internal) { - tmpBuf = internalNioBuffer(); - } else { - tmpBuf = ByteBuffer.wrap(memory); - } - return out.write((ByteBuffer) tmpBuf.clear().position(index).limit(index + length)); - } - - @Override - public final int getBytes(int index, FileChannel out, long position, int length) throws IOException { - return getBytes(index, out, position, length, false); - } - - private int getBytes(int index, FileChannel out, long position, int length, boolean internal) throws IOException { - checkIndex(index, length); - index = idx(index); - ByteBuffer tmpBuf = internal ? internalNioBuffer() : ByteBuffer.wrap(memory); - return out.write((ByteBuffer) tmpBuf.clear().position(index).limit(index + length), position); - } - - @Override - public final int readBytes(GatheringByteChannel out, int length) throws IOException { - checkReadableBytes(length); - int readBytes = getBytes(readerIndex, out, length, true); - readerIndex += readBytes; - return readBytes; - } - - @Override - public final int readBytes(FileChannel out, long position, int length) throws IOException { - checkReadableBytes(length); - int readBytes = getBytes(readerIndex, out, position, length, true); - readerIndex += readBytes; - return readBytes; - } - @Override protected void _setByte(int index, int value) { HeapByteBufUtil.setByte(memory, idx(index), value); @@ -253,59 +208,17 @@ class PooledHeapByteBuf extends PooledByteBuf<byte[]> { return in.read(memory, idx(index), length); } - @Override - public final int setBytes(int index, ScatteringByteChannel in, int length) throws IOException { - checkIndex(index, length); - index = idx(index); - try { - return in.read((ByteBuffer) internalNioBuffer().clear().position(index).limit(index + length)); - } catch (ClosedChannelException ignored) { - return -1; - } - } - - @Override - public final int setBytes(int index, FileChannel in, long position, int length) throws IOException { - checkIndex(index, length); - index = idx(index); - try { - return in.read((ByteBuffer) internalNioBuffer().clear().position(index).limit(index + length), position); - } catch (ClosedChannelException ignored) { - return -1; - } - } - @Override public final ByteBuf copy(int index, int length) { checkIndex(index, length); ByteBuf copy = alloc().heapBuffer(length, maxCapacity()); - copy.writeBytes(memory, idx(index), length); - return copy; - } - - @Override - public final int nioBufferCount() { - return 1; - } - - @Override - public final ByteBuffer[] nioBuffers(int index, int length) { - return new ByteBuffer[] { nioBuffer(index, length) }; - } - - @Override - public final ByteBuffer nioBuffer(int index, int length) { - checkIndex(index, length); - index = idx(index); - ByteBuffer buf = ByteBuffer.wrap(memory, index, length); - return buf.slice(); + return copy.writeBytes(memory, idx(index), length); } @Override - public final ByteBuffer internalNioBuffer(int index, int length) { + final ByteBuffer duplicateInternalNioBuffer(int index, int length) { checkIndex(index, length); - index = idx(index); - return (ByteBuffer) internalNioBuffer().clear().position(index).limit(index + length); + return ByteBuffer.wrap(memory, idx(index), length).slice(); } @Override diff --git a/buffer/src/main/java/io/netty/buffer/PooledSlicedByteBuf.java b/buffer/src/main/java/io/netty/buffer/PooledSlicedByteBuf.java index 4405188..256a5f3 100644 --- a/buffer/src/main/java/io/netty/buffer/PooledSlicedByteBuf.java +++ b/buffer/src/main/java/io/netty/buffer/PooledSlicedByteBuf.java @@ -17,8 +17,9 @@ package io.netty.buffer; import io.netty.util.ByteProcessor; -import io.netty.util.Recycler; -import io.netty.util.Recycler.Handle; +import io.netty.util.internal.ObjectPool; +import io.netty.util.internal.ObjectPool.Handle; +import io.netty.util.internal.ObjectPool.ObjectCreator; import java.io.IOException; import java.io.InputStream; @@ -32,12 +33,13 @@ import static io.netty.buffer.AbstractUnpooledSlicedByteBuf.checkSliceOutOfBound final class PooledSlicedByteBuf extends AbstractPooledDerivedByteBuf { - private static final Recycler<PooledSlicedByteBuf> RECYCLER = new Recycler<PooledSlicedByteBuf>() { + private static final ObjectPool<PooledSlicedByteBuf> RECYCLER = ObjectPool.newPool( + new ObjectCreator<PooledSlicedByteBuf>() { @Override - protected PooledSlicedByteBuf newObject(Handle<PooledSlicedByteBuf> handle) { + public PooledSlicedByteBuf newObject(Handle<PooledSlicedByteBuf> handle) { return new PooledSlicedByteBuf(handle); } - }; + }); static PooledSlicedByteBuf newInstance(AbstractByteBuf unwrapped, ByteBuf wrapped, int index, int length) { diff --git a/buffer/src/main/java/io/netty/buffer/PooledUnsafeDirectByteBuf.java b/buffer/src/main/java/io/netty/buffer/PooledUnsafeDirectByteBuf.java index e2dc22c..fd7e27d 100644 --- a/buffer/src/main/java/io/netty/buffer/PooledUnsafeDirectByteBuf.java +++ b/buffer/src/main/java/io/netty/buffer/PooledUnsafeDirectByteBuf.java @@ -16,25 +16,24 @@ package io.netty.buffer; -import io.netty.util.Recycler; +import io.netty.util.internal.ObjectPool; +import io.netty.util.internal.ObjectPool.Handle; +import io.netty.util.internal.ObjectPool.ObjectCreator; import io.netty.util.internal.PlatformDependent; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.ByteBuffer; -import java.nio.channels.ClosedChannelException; -import java.nio.channels.FileChannel; -import java.nio.channels.GatheringByteChannel; -import java.nio.channels.ScatteringByteChannel; final class PooledUnsafeDirectByteBuf extends PooledByteBuf<ByteBuffer> { - private static final Recycler<PooledUnsafeDirectByteBuf> RECYCLER = new Recycler<PooledUnsafeDirectByteBuf>() { + private static final ObjectPool<PooledUnsafeDirectByteBuf> RECYCLER = ObjectPool.newPool( + new ObjectCreator<PooledUnsafeDirectByteBuf>() { @Override - protected PooledUnsafeDirectByteBuf newObject(Handle<PooledUnsafeDirectByteBuf> handle) { + public PooledUnsafeDirectByteBuf newObject(Handle<PooledUnsafeDirectByteBuf> handle) { return new PooledUnsafeDirectByteBuf(handle, 0); } - }; + }); static PooledUnsafeDirectByteBuf newInstance(int maxCapacity) { PooledUnsafeDirectByteBuf buf = RECYCLER.get(); @@ -44,7 +43,7 @@ final class PooledUnsafeDirectByteBuf extends PooledByteBuf<ByteBuffer> { private long memoryAddress; - private PooledUnsafeDirectByteBuf(Recycler.Handle<PooledUnsafeDirectByteBuf> recyclerHandle, int maxCapacity) { + private PooledUnsafeDirectByteBuf(Handle<PooledUnsafeDirectByteBuf> recyclerHandle, int maxCapacity) { super(recyclerHandle, maxCapacity); } @@ -138,78 +137,12 @@ final class PooledUnsafeDirectByteBuf extends PooledByteBuf<ByteBuffer> { return this; } - @Override - public ByteBuf readBytes(ByteBuffer dst) { - int length = dst.remaining(); - checkReadableBytes(length); - getBytes(readerIndex, dst); - readerIndex += length; - return this; - } - @Override public ByteBuf getBytes(int index, OutputStream out, int length) throws IOException { UnsafeByteBufUtil.getBytes(this, addr(index), index, out, length); return this; } - @Override - public int getBytes(int index, GatheringByteChannel out, int length) throws IOException { - return getBytes(index, out, length, false); - } - - private int getBytes(int index, GatheringByteChannel out, int length, boolean internal) throws IOException { - checkIndex(index, length); - if (length == 0) { - return 0; - } - - ByteBuffer tmpBuf; - if (internal) { - tmpBuf = internalNioBuffer(); - } else { - tmpBuf = memory.duplicate(); - } - index = idx(index); - tmpBuf.clear().position(index).limit(index + length); - return out.write(tmpBuf); - } - - @Override - public int getBytes(int index, FileChannel out, long position, int length) throws IOException { - return getBytes(index, out, position, length, false); - } - - private int getBytes(int index, FileChannel out, long position, int length, boolean internal) throws IOException { - checkIndex(index, length); - if (length == 0) { - return 0; - } - - ByteBuffer tmpBuf = internal ? internalNioBuffer() : memory.duplicate(); - index = idx(index); - tmpBuf.clear().position(index).limit(index + length); - return out.write(tmpBuf, position); - } - - @Override - public int readBytes(GatheringByteChannel out, int length) - throws IOException { - checkReadableBytes(length); - int readBytes = getBytes(readerIndex, out, length, true); - readerIndex += readBytes; - return readBytes; - } - - @Override - public int readBytes(FileChannel out, long position, int length) - throws IOException { - checkReadableBytes(length); - int readBytes = getBytes(readerIndex, out, position, length, true); - readerIndex += readBytes; - return readBytes; - } - @Override protected void _setByte(int index, int value) { UnsafeByteBufUtil.setByte(addr(index), (byte) value); @@ -278,61 +211,11 @@ final class PooledUnsafeDirectByteBuf extends PooledByteBuf<ByteBuffer> { return UnsafeByteBufUtil.setBytes(this, addr(index), index, in, length); } - @Override - public int setBytes(int index, ScatteringByteChannel in, int length) throws IOException { - checkIndex(index, length); - ByteBuffer tmpBuf = internalNioBuffer(); - index = idx(index); - tmpBuf.clear().position(index).limit(index + length); - try { - return in.read(tmpBuf); - } catch (ClosedChannelException ignored) { - return -1; - } - } - - @Override - public int setBytes(int index, FileChannel in, long position, int length) throws IOException { - checkIndex(index, length); - ByteBuffer tmpBuf = internalNioBuffer(); - index = idx(index); - tmpBuf.clear().position(index).limit(index + length); - try { - return in.read(tmpBuf, position); - } catch (ClosedChannelException ignored) { - return -1; - } - } - @Override public ByteBuf copy(int index, int length) { return UnsafeByteBufUtil.copy(this, addr(index), index, length); } - @Override - public int nioBufferCount() { - return 1; - } - - @Override - public ByteBuffer[] nioBuffers(int index, int length) { - return new ByteBuffer[] { nioBuffer(index, length) }; - } - - @Override - public ByteBuffer nioBuffer(int index, int length) { - checkIndex(index, length); - index = idx(index); - return ((ByteBuffer) memory.duplicate().position(index).limit(index + length)).slice(); - } - - @Override - public ByteBuffer internalNioBuffer(int index, int length) { - checkIndex(index, length); - index = idx(index); - return (ByteBuffer) internalNioBuffer().clear().position(index).limit(index + length); - } - @Override public boolean hasArray() { return false; diff --git a/buffer/src/main/java/io/netty/buffer/PooledUnsafeHeapByteBuf.java b/buffer/src/main/java/io/netty/buffer/PooledUnsafeHeapByteBuf.java index a644450..88273af 100644 --- a/buffer/src/main/java/io/netty/buffer/PooledUnsafeHeapByteBuf.java +++ b/buffer/src/main/java/io/netty/buffer/PooledUnsafeHeapByteBuf.java @@ -15,18 +15,20 @@ */ package io.netty.buffer; -import io.netty.util.Recycler; -import io.netty.util.Recycler.Handle; +import io.netty.util.internal.ObjectPool; +import io.netty.util.internal.ObjectPool.Handle; +import io.netty.util.internal.ObjectPool.ObjectCreator; import io.netty.util.internal.PlatformDependent; final class PooledUnsafeHeapByteBuf extends PooledHeapByteBuf { - private static final Recycler<PooledUnsafeHeapByteBuf> RECYCLER = new Recycler<PooledUnsafeHeapByteBuf>() { + private static final ObjectPool<PooledUnsafeHeapByteBuf> RECYCLER = ObjectPool.newPool( + new ObjectCreator<PooledUnsafeHeapByteBuf>() { @Override - protected PooledUnsafeHeapByteBuf newObject(Handle<PooledUnsafeHeapByteBuf> handle) { + public PooledUnsafeHeapByteBuf newObject(Handle<PooledUnsafeHeapByteBuf> handle) { return new PooledUnsafeHeapByteBuf(handle, 0); } - }; + }); static PooledUnsafeHeapByteBuf newUnsafeInstance(int maxCapacity) { PooledUnsafeHeapByteBuf buf = RECYCLER.get(); diff --git a/buffer/src/main/java/io/netty/buffer/ReadOnlyByteBufferBuf.java b/buffer/src/main/java/io/netty/buffer/ReadOnlyByteBufferBuf.java index c7cda05..bc79fb0 100644 --- a/buffer/src/main/java/io/netty/buffer/ReadOnlyByteBufferBuf.java +++ b/buffer/src/main/java/io/netty/buffer/ReadOnlyByteBufferBuf.java @@ -195,11 +195,6 @@ class ReadOnlyByteBufferBuf extends AbstractReferenceCountedByteBuf { public ByteBuf getBytes(int index, byte[] dst, int dstIndex, int length) { checkDstIndex(index, length, dstIndex, dst.length); - if (dstIndex < 0 || dstIndex > dst.length - length) { - throw new IndexOutOfBoundsException(String.format( - "dstIndex: %d, length: %d (expected: range(0, %d))", dstIndex, length, dst.length)); - } - ByteBuffer tmpBuf = internalNioBuffer(); tmpBuf.clear().position(index).limit(index + length); tmpBuf.get(dst, dstIndex, length); @@ -208,14 +203,10 @@ class ReadOnlyByteBufferBuf extends AbstractReferenceCountedByteBuf { @Override public ByteBuf getBytes(int index, ByteBuffer dst) { - checkIndex(index); - if (dst == null) { - throw new NullPointerException("dst"); - } + checkIndex(index, dst.remaining()); - int bytesToCopy = Math.min(capacity() - index, dst.remaining()); ByteBuffer tmpBuf = internalNioBuffer(); - tmpBuf.clear().position(index).limit(index + bytesToCopy); + tmpBuf.clear().position(index).limit(index + dst.remaining()); dst.put(tmpBuf); return this; } @@ -453,6 +444,7 @@ class ReadOnlyByteBufferBuf extends AbstractReferenceCountedByteBuf { @Override public ByteBuffer nioBuffer(int index, int length) { + checkIndex(index, length); return (ByteBuffer) buffer.duplicate().position(index).limit(index + length); } @@ -462,6 +454,11 @@ class ReadOnlyByteBufferBuf extends AbstractReferenceCountedByteBuf { return (ByteBuffer) internalNioBuffer().clear().position(index).limit(index + length); } + @Override + public final boolean isContiguous() { + return true; + } + @Override public boolean hasArray() { return buffer.hasArray(); diff --git a/buffer/src/main/java/io/netty/buffer/ReadOnlyUnsafeDirectByteBuf.java b/buffer/src/main/java/io/netty/buffer/ReadOnlyUnsafeDirectByteBuf.java index 316760e..a1e9714 100644 --- a/buffer/src/main/java/io/netty/buffer/ReadOnlyUnsafeDirectByteBuf.java +++ b/buffer/src/main/java/io/netty/buffer/ReadOnlyUnsafeDirectByteBuf.java @@ -16,6 +16,7 @@ package io.netty.buffer; +import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.PlatformDependent; import java.nio.ByteBuffer; @@ -62,9 +63,7 @@ final class ReadOnlyUnsafeDirectByteBuf extends ReadOnlyByteBufferBuf { @Override public ByteBuf getBytes(int index, ByteBuf dst, int dstIndex, int length) { checkIndex(index, length); - if (dst == null) { - throw new NullPointerException("dst"); - } + ObjectUtil.checkNotNull(dst, "dst"); if (dstIndex < 0 || dstIndex > dst.capacity() - length) { throw new IndexOutOfBoundsException("dstIndex: " + dstIndex); } @@ -82,9 +81,7 @@ final class ReadOnlyUnsafeDirectByteBuf extends ReadOnlyByteBufferBuf { @Override public ByteBuf getBytes(int index, byte[] dst, int dstIndex, int length) { checkIndex(index, length); - if (dst == null) { - throw new NullPointerException("dst"); - } + ObjectUtil.checkNotNull(dst, "dst"); if (dstIndex < 0 || dstIndex > dst.length - length) { throw new IndexOutOfBoundsException(String.format( "dstIndex: %d, length: %d (expected: range(0, %d))", dstIndex, length, dst.length)); @@ -96,20 +93,6 @@ final class ReadOnlyUnsafeDirectByteBuf extends ReadOnlyByteBufferBuf { return this; } - @Override - public ByteBuf getBytes(int index, ByteBuffer dst) { - checkIndex(index); - if (dst == null) { - throw new NullPointerException("dst"); - } - - int bytesToCopy = Math.min(capacity() - index, dst.remaining()); - ByteBuffer tmpBuf = internalNioBuffer(); - tmpBuf.clear().position(index).limit(index + bytesToCopy); - dst.put(tmpBuf); - return this; - } - @Override public ByteBuf copy(int index, int length) { checkIndex(index, length); diff --git a/buffer/src/main/java/io/netty/buffer/SwappedByteBuf.java b/buffer/src/main/java/io/netty/buffer/SwappedByteBuf.java index 5d54a1f..05725a9 100644 --- a/buffer/src/main/java/io/netty/buffer/SwappedByteBuf.java +++ b/buffer/src/main/java/io/netty/buffer/SwappedByteBuf.java @@ -16,6 +16,7 @@ package io.netty.buffer; import io.netty.util.ByteProcessor; +import io.netty.util.internal.ObjectUtil; import java.io.IOException; import java.io.InputStream; @@ -40,10 +41,7 @@ public class SwappedByteBuf extends ByteBuf { private final ByteOrder order; public SwappedByteBuf(ByteBuf buf) { - if (buf == null) { - throw new NullPointerException("buf"); - } - this.buf = buf; + this.buf = ObjectUtil.checkNotNull(buf, "buf"); if (buf.order() == ByteOrder.BIG_ENDIAN) { order = ByteOrder.LITTLE_ENDIAN; } else { @@ -58,10 +56,7 @@ public class SwappedByteBuf extends ByteBuf { @Override public ByteBuf order(ByteOrder endianness) { - if (endianness == null) { - throw new NullPointerException("endianness"); - } - if (endianness == order) { + if (ObjectUtil.checkNotNull(endianness, "endianness") == order) { return this; } return buf; @@ -151,6 +146,11 @@ public class SwappedByteBuf extends ByteBuf { return buf.maxWritableBytes(); } + @Override + public int maxFastWritableBytes() { + return buf.maxFastWritableBytes(); + } + @Override public boolean isReadable() { return buf.isReadable(); @@ -977,6 +977,11 @@ public class SwappedByteBuf extends ByteBuf { return buf.hasMemoryAddress(); } + @Override + public boolean isContiguous() { + return buf.isContiguous(); + } + @Override public long memoryAddress() { return buf.memoryAddress(); @@ -997,6 +1002,11 @@ public class SwappedByteBuf extends ByteBuf { return buf.refCnt(); } + @Override + final boolean isAccessible() { + return buf.isAccessible(); + } + @Override public ByteBuf retain() { buf.retain(); diff --git a/buffer/src/main/java/io/netty/buffer/Unpooled.java b/buffer/src/main/java/io/netty/buffer/Unpooled.java index d7df192..3976f4f 100644 --- a/buffer/src/main/java/io/netty/buffer/Unpooled.java +++ b/buffer/src/main/java/io/netty/buffer/Unpooled.java @@ -16,6 +16,7 @@ package io.netty.buffer; import io.netty.buffer.CompositeByteBuf.ByteWrapper; +import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.PlatformDependent; import java.nio.ByteBuffer; @@ -575,9 +576,7 @@ public final class Unpooled { * {@code 0} and the length of the encoded string respectively. */ public static ByteBuf copiedBuffer(CharSequence string, Charset charset) { - if (string == null) { - throw new NullPointerException("string"); - } + ObjectUtil.checkNotNull(string, "string"); if (string instanceof CharBuffer) { return copiedBuffer((CharBuffer) string, charset); @@ -594,9 +593,7 @@ public final class Unpooled { */ public static ByteBuf copiedBuffer( CharSequence string, int offset, int length, Charset charset) { - if (string == null) { - throw new NullPointerException("string"); - } + ObjectUtil.checkNotNull(string, "string"); if (length == 0) { return EMPTY_BUFFER; } @@ -626,9 +623,7 @@ public final class Unpooled { * {@code 0} and the length of the encoded string respectively. */ public static ByteBuf copiedBuffer(char[] array, Charset charset) { - if (array == null) { - throw new NullPointerException("array"); - } + ObjectUtil.checkNotNull(array, "array"); return copiedBuffer(array, 0, array.length, charset); } @@ -639,9 +634,7 @@ public final class Unpooled { * {@code 0} and the length of the encoded string respectively. */ public static ByteBuf copiedBuffer(char[] array, int offset, int length, Charset charset) { - if (array == null) { - throw new NullPointerException("array"); - } + ObjectUtil.checkNotNull(array, "array"); if (length == 0) { return EMPTY_BUFFER; } diff --git a/buffer/src/main/java/io/netty/buffer/UnpooledDirectByteBuf.java b/buffer/src/main/java/io/netty/buffer/UnpooledDirectByteBuf.java index 60167cf..66b48e8 100644 --- a/buffer/src/main/java/io/netty/buffer/UnpooledDirectByteBuf.java +++ b/buffer/src/main/java/io/netty/buffer/UnpooledDirectByteBuf.java @@ -15,6 +15,7 @@ */ package io.netty.buffer; +import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.PlatformDependent; import java.io.IOException; @@ -27,6 +28,8 @@ import java.nio.channels.FileChannel; import java.nio.channels.GatheringByteChannel; import java.nio.channels.ScatteringByteChannel; +import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero; + /** * A NIO {@link ByteBuffer} based buffer. It is recommended to use * {@link UnpooledByteBufAllocator#directBuffer(int, int)}, {@link Unpooled#directBuffer(int)} and @@ -36,7 +39,7 @@ public class UnpooledDirectByteBuf extends AbstractReferenceCountedByteBuf { private final ByteBufAllocator alloc; - private ByteBuffer buffer; + ByteBuffer buffer; // accessed by UnpooledUnsafeNoCleanerDirectByteBuf.reallocateDirect() private ByteBuffer tmpNioBuf; private int capacity; private boolean doNotFree; @@ -49,22 +52,16 @@ public class UnpooledDirectByteBuf extends AbstractReferenceCountedByteBuf { */ public UnpooledDirectByteBuf(ByteBufAllocator alloc, int initialCapacity, int maxCapacity) { super(maxCapacity); - if (alloc == null) { - throw new NullPointerException("alloc"); - } - if (initialCapacity < 0) { - throw new IllegalArgumentException("initialCapacity: " + initialCapacity); - } - if (maxCapacity < 0) { - throw new IllegalArgumentException("maxCapacity: " + maxCapacity); - } + ObjectUtil.checkNotNull(alloc, "alloc"); + checkPositiveOrZero(initialCapacity, "initialCapacity"); + checkPositiveOrZero(maxCapacity, "maxCapacity"); if (initialCapacity > maxCapacity) { throw new IllegalArgumentException(String.format( "initialCapacity(%d) > maxCapacity(%d)", initialCapacity, maxCapacity)); } this.alloc = alloc; - setByteBuffer(allocateDirect(initialCapacity)); + setByteBuffer(allocateDirect(initialCapacity), false); } /** @@ -73,13 +70,14 @@ public class UnpooledDirectByteBuf extends AbstractReferenceCountedByteBuf { * @param maxCapacity the maximum capacity of the underlying direct buffer */ protected UnpooledDirectByteBuf(ByteBufAllocator alloc, ByteBuffer initialBuffer, int maxCapacity) { + this(alloc, initialBuffer, maxCapacity, false, true); + } + + UnpooledDirectByteBuf(ByteBufAllocator alloc, ByteBuffer initialBuffer, + int maxCapacity, boolean doFree, boolean slice) { super(maxCapacity); - if (alloc == null) { - throw new NullPointerException("alloc"); - } - if (initialBuffer == null) { - throw new NullPointerException("initialBuffer"); - } + ObjectUtil.checkNotNull(alloc, "alloc"); + ObjectUtil.checkNotNull(initialBuffer, "initialBuffer"); if (!initialBuffer.isDirect()) { throw new IllegalArgumentException("initialBuffer is not a direct buffer."); } @@ -94,8 +92,8 @@ public class UnpooledDirectByteBuf extends AbstractReferenceCountedByteBuf { } this.alloc = alloc; - doNotFree = true; - setByteBuffer(initialBuffer.slice().order(ByteOrder.BIG_ENDIAN)); + doNotFree = !doFree; + setByteBuffer((slice ? initialBuffer.slice() : initialBuffer).order(ByteOrder.BIG_ENDIAN), false); writerIndex(initialCapacity); } @@ -113,13 +111,15 @@ public class UnpooledDirectByteBuf extends AbstractReferenceCountedByteBuf { PlatformDependent.freeDirectBuffer(buffer); } - private void setByteBuffer(ByteBuffer buffer) { - ByteBuffer oldBuffer = this.buffer; - if (oldBuffer != null) { - if (doNotFree) { - doNotFree = false; - } else { - freeDirect(oldBuffer); + void setByteBuffer(ByteBuffer buffer, boolean tryFree) { + if (tryFree) { + ByteBuffer oldBuffer = this.buffer; + if (oldBuffer != null) { + if (doNotFree) { + doNotFree = false; + } else { + freeDirect(oldBuffer); + } } } @@ -141,35 +141,23 @@ public class UnpooledDirectByteBuf extends AbstractReferenceCountedByteBuf { @Override public ByteBuf capacity(int newCapacity) { checkNewCapacity(newCapacity); - - int readerIndex = readerIndex(); - int writerIndex = writerIndex(); - int oldCapacity = capacity; + if (newCapacity == oldCapacity) { + return this; + } + int bytesToCopy; if (newCapacity > oldCapacity) { - ByteBuffer oldBuffer = buffer; - ByteBuffer newBuffer = allocateDirect(newCapacity); - oldBuffer.position(0).limit(oldBuffer.capacity()); - newBuffer.position(0).limit(oldBuffer.capacity()); - newBuffer.put(oldBuffer); - newBuffer.clear(); - setByteBuffer(newBuffer); - } else if (newCapacity < oldCapacity) { - ByteBuffer oldBuffer = buffer; - ByteBuffer newBuffer = allocateDirect(newCapacity); - if (readerIndex < newCapacity) { - if (writerIndex > newCapacity) { - writerIndex(writerIndex = newCapacity); - } - oldBuffer.position(readerIndex).limit(writerIndex); - newBuffer.position(readerIndex).limit(writerIndex); - newBuffer.put(oldBuffer); - newBuffer.clear(); - } else { - setIndex(newCapacity, newCapacity); - } - setByteBuffer(newBuffer); + bytesToCopy = oldCapacity; + } else { + trimIndicesToCapacity(newCapacity); + bytesToCopy = newCapacity; } + ByteBuffer oldBuffer = buffer; + ByteBuffer newBuffer = allocateDirect(newCapacity); + oldBuffer.position(0).limit(bytesToCopy); + newBuffer.position(0).limit(bytesToCopy); + newBuffer.put(oldBuffer).clear(); + setByteBuffer(newBuffer, true); return this; } @@ -310,7 +298,7 @@ public class UnpooledDirectByteBuf extends AbstractReferenceCountedByteBuf { return this; } - private void getBytes(int index, byte[] dst, int dstIndex, int length, boolean internal) { + void getBytes(int index, byte[] dst, int dstIndex, int length, boolean internal) { checkDstIndex(index, length, dstIndex, dst.length); ByteBuffer tmpBuf; @@ -337,7 +325,7 @@ public class UnpooledDirectByteBuf extends AbstractReferenceCountedByteBuf { return this; } - private void getBytes(int index, ByteBuffer dst, boolean internal) { + void getBytes(int index, ByteBuffer dst, boolean internal) { checkIndex(index, dst.remaining()); ByteBuffer tmpBuf; @@ -486,7 +474,7 @@ public class UnpooledDirectByteBuf extends AbstractReferenceCountedByteBuf { return this; } - private void getBytes(int index, OutputStream out, int length, boolean internal) throws IOException { + void getBytes(int index, OutputStream out, int length, boolean internal) throws IOException { ensureAccessible(); if (length == 0) { return; @@ -579,7 +567,7 @@ public class UnpooledDirectByteBuf extends AbstractReferenceCountedByteBuf { ByteBuffer tmpBuf = internalNioBuffer(); tmpBuf.clear().position(index).limit(index + length); try { - return in.read(tmpNioBuf); + return in.read(tmpBuf); } catch (ClosedChannelException ignored) { return -1; } @@ -591,7 +579,7 @@ public class UnpooledDirectByteBuf extends AbstractReferenceCountedByteBuf { ByteBuffer tmpBuf = internalNioBuffer(); tmpBuf.clear().position(index).limit(index + length); try { - return in.read(tmpNioBuf, position); + return in.read(tmpBuf, position); } catch (ClosedChannelException ignored) { return -1; } @@ -607,6 +595,11 @@ public class UnpooledDirectByteBuf extends AbstractReferenceCountedByteBuf { return new ByteBuffer[] { nioBuffer(index, length) }; } + @Override + public final boolean isContiguous() { + return true; + } + @Override public ByteBuf copy(int index, int length) { ensureAccessible(); diff --git a/buffer/src/main/java/io/netty/buffer/UnpooledHeapByteBuf.java b/buffer/src/main/java/io/netty/buffer/UnpooledHeapByteBuf.java index f37ceb0..f5d1fe5 100644 --- a/buffer/src/main/java/io/netty/buffer/UnpooledHeapByteBuf.java +++ b/buffer/src/main/java/io/netty/buffer/UnpooledHeapByteBuf.java @@ -50,14 +50,12 @@ public class UnpooledHeapByteBuf extends AbstractReferenceCountedByteBuf { public UnpooledHeapByteBuf(ByteBufAllocator alloc, int initialCapacity, int maxCapacity) { super(maxCapacity); - checkNotNull(alloc, "alloc"); - if (initialCapacity > maxCapacity) { throw new IllegalArgumentException(String.format( "initialCapacity(%d) > maxCapacity(%d)", initialCapacity, maxCapacity)); } - this.alloc = alloc; + this.alloc = checkNotNull(alloc, "alloc"); setArray(allocateArray(initialCapacity)); setIndex(0, 0); } @@ -73,7 +71,6 @@ public class UnpooledHeapByteBuf extends AbstractReferenceCountedByteBuf { checkNotNull(alloc, "alloc"); checkNotNull(initialArray, "initialArray"); - if (initialArray.length > maxCapacity) { throw new IllegalArgumentException(String.format( "initialCapacity(%d) > maxCapacity(%d)", initialArray.length, maxCapacity)); @@ -120,29 +117,23 @@ public class UnpooledHeapByteBuf extends AbstractReferenceCountedByteBuf { @Override public ByteBuf capacity(int newCapacity) { checkNewCapacity(newCapacity); - - int oldCapacity = array.length; byte[] oldArray = array; + int oldCapacity = oldArray.length; + if (newCapacity == oldCapacity) { + return this; + } + + int bytesToCopy; if (newCapacity > oldCapacity) { - byte[] newArray = allocateArray(newCapacity); - System.arraycopy(oldArray, 0, newArray, 0, oldArray.length); - setArray(newArray); - freeArray(oldArray); - } else if (newCapacity < oldCapacity) { - byte[] newArray = allocateArray(newCapacity); - int readerIndex = readerIndex(); - if (readerIndex < newCapacity) { - int writerIndex = writerIndex(); - if (writerIndex > newCapacity) { - writerIndex(writerIndex = newCapacity); - } - System.arraycopy(oldArray, readerIndex, newArray, readerIndex, writerIndex - readerIndex); - } else { - setIndex(newCapacity, newCapacity); - } - setArray(newArray); - freeArray(oldArray); + bytesToCopy = oldCapacity; + } else { + trimIndicesToCapacity(newCapacity); + bytesToCopy = newCapacity; } + byte[] newArray = allocateArray(newCapacity); + System.arraycopy(oldArray, 0, newArray, 0, bytesToCopy); + setArray(newArray); + freeArray(oldArray); return this; } @@ -194,7 +185,7 @@ public class UnpooledHeapByteBuf extends AbstractReferenceCountedByteBuf { @Override public ByteBuf getBytes(int index, ByteBuffer dst) { - checkIndex(index, dst.remaining()); + ensureAccessible(); dst.put(array, index, dst.remaining()); return this; } @@ -326,6 +317,11 @@ public class UnpooledHeapByteBuf extends AbstractReferenceCountedByteBuf { return (ByteBuffer) internalNioBuffer().clear().position(index).limit(index + length); } + @Override + public final boolean isContiguous() { + return true; + } + @Override public byte getByte(int index) { ensureAccessible(); @@ -536,9 +532,7 @@ public class UnpooledHeapByteBuf extends AbstractReferenceCountedByteBuf { @Override public ByteBuf copy(int index, int length) { checkIndex(index, length); - byte[] copiedArray = PlatformDependent.allocateUninitializedArray(length); - System.arraycopy(array, index, copiedArray, 0, length); - return new UnpooledHeapByteBuf(alloc(), copiedArray, maxCapacity()); + return alloc().heapBuffer(length, maxCapacity()).writeBytes(array, index, length); } private ByteBuffer internalNioBuffer() { diff --git a/buffer/src/main/java/io/netty/buffer/UnpooledUnsafeDirectByteBuf.java b/buffer/src/main/java/io/netty/buffer/UnpooledUnsafeDirectByteBuf.java index 9d425e3..5526f3f 100644 --- a/buffer/src/main/java/io/netty/buffer/UnpooledUnsafeDirectByteBuf.java +++ b/buffer/src/main/java/io/netty/buffer/UnpooledUnsafeDirectByteBuf.java @@ -21,25 +21,14 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.ByteBuffer; -import java.nio.ByteOrder; -import java.nio.channels.ClosedChannelException; -import java.nio.channels.FileChannel; -import java.nio.channels.GatheringByteChannel; -import java.nio.channels.ScatteringByteChannel; /** * A NIO {@link ByteBuffer} based buffer. It is recommended to use * {@link UnpooledByteBufAllocator#directBuffer(int, int)}, {@link Unpooled#directBuffer(int)} and * {@link Unpooled#wrappedBuffer(ByteBuffer)} instead of calling the constructor explicitly.} */ -public class UnpooledUnsafeDirectByteBuf extends AbstractReferenceCountedByteBuf { +public class UnpooledUnsafeDirectByteBuf extends UnpooledDirectByteBuf { - private final ByteBufAllocator alloc; - - private ByteBuffer tmpNioBuf; - private int capacity; - private boolean doNotFree; - ByteBuffer buffer; long memoryAddress; /** @@ -49,23 +38,7 @@ public class UnpooledUnsafeDirectByteBuf extends AbstractReferenceCountedByteBuf * @param maxCapacity the maximum capacity of the underlying direct buffer */ public UnpooledUnsafeDirectByteBuf(ByteBufAllocator alloc, int initialCapacity, int maxCapacity) { - super(maxCapacity); - if (alloc == null) { - throw new NullPointerException("alloc"); - } - if (initialCapacity < 0) { - throw new IllegalArgumentException("initialCapacity: " + initialCapacity); - } - if (maxCapacity < 0) { - throw new IllegalArgumentException("maxCapacity: " + maxCapacity); - } - if (initialCapacity > maxCapacity) { - throw new IllegalArgumentException(String.format( - "initialCapacity(%d) > maxCapacity(%d)", initialCapacity, maxCapacity)); - } - - this.alloc = alloc; - setByteBuffer(allocateDirect(initialCapacity), false); + super(alloc, initialCapacity, maxCapacity); } /** @@ -83,135 +56,17 @@ public class UnpooledUnsafeDirectByteBuf extends AbstractReferenceCountedByteBuf // sun/misc/Unsafe.java#l1250 // // We also call slice() explicitly here to preserve behaviour with previous netty releases. - this(alloc, initialBuffer.slice(), maxCapacity, false); + super(alloc, initialBuffer, maxCapacity, /* doFree = */ false, /* slice = */ true); } UnpooledUnsafeDirectByteBuf(ByteBufAllocator alloc, ByteBuffer initialBuffer, int maxCapacity, boolean doFree) { - super(maxCapacity); - if (alloc == null) { - throw new NullPointerException("alloc"); - } - if (initialBuffer == null) { - throw new NullPointerException("initialBuffer"); - } - if (!initialBuffer.isDirect()) { - throw new IllegalArgumentException("initialBuffer is not a direct buffer."); - } - if (initialBuffer.isReadOnly()) { - throw new IllegalArgumentException("initialBuffer is a read-only buffer."); - } - - int initialCapacity = initialBuffer.remaining(); - if (initialCapacity > maxCapacity) { - throw new IllegalArgumentException(String.format( - "initialCapacity(%d) > maxCapacity(%d)", initialCapacity, maxCapacity)); - } - - this.alloc = alloc; - doNotFree = !doFree; - setByteBuffer(initialBuffer.order(ByteOrder.BIG_ENDIAN), false); - writerIndex(initialCapacity); - } - - /** - * Allocate a new direct {@link ByteBuffer} with the given initialCapacity. - */ - protected ByteBuffer allocateDirect(int initialCapacity) { - return ByteBuffer.allocateDirect(initialCapacity); - } - - /** - * Free a direct {@link ByteBuffer} - */ - protected void freeDirect(ByteBuffer buffer) { - PlatformDependent.freeDirectBuffer(buffer); + super(alloc, initialBuffer, maxCapacity, doFree, false); } + @Override final void setByteBuffer(ByteBuffer buffer, boolean tryFree) { - if (tryFree) { - ByteBuffer oldBuffer = this.buffer; - if (oldBuffer != null) { - if (doNotFree) { - doNotFree = false; - } else { - freeDirect(oldBuffer); - } - } - } - this.buffer = buffer; + super.setByteBuffer(buffer, tryFree); memoryAddress = PlatformDependent.directBufferAddress(buffer); - tmpNioBuf = null; - capacity = buffer.remaining(); - } - - @Override - public boolean isDirect() { - return true; - } - - @Override - public int capacity() { - return capacity; - } - - @Override - public ByteBuf capacity(int newCapacity) { - checkNewCapacity(newCapacity); - - int readerIndex = readerIndex(); - int writerIndex = writerIndex(); - - int oldCapacity = capacity; - if (newCapacity > oldCapacity) { - ByteBuffer oldBuffer = buffer; - ByteBuffer newBuffer = allocateDirect(newCapacity); - oldBuffer.position(0).limit(oldBuffer.capacity()); - newBuffer.position(0).limit(oldBuffer.capacity()); - newBuffer.put(oldBuffer); - newBuffer.clear(); - setByteBuffer(newBuffer, true); - } else if (newCapacity < oldCapacity) { - ByteBuffer oldBuffer = buffer; - ByteBuffer newBuffer = allocateDirect(newCapacity); - if (readerIndex < newCapacity) { - if (writerIndex > newCapacity) { - writerIndex(writerIndex = newCapacity); - } - oldBuffer.position(readerIndex).limit(writerIndex); - newBuffer.position(readerIndex).limit(writerIndex); - newBuffer.put(oldBuffer); - newBuffer.clear(); - } else { - setIndex(newCapacity, newCapacity); - } - setByteBuffer(newBuffer, true); - } - return this; - } - - @Override - public ByteBufAllocator alloc() { - return alloc; - } - - @Override - public ByteOrder order() { - return ByteOrder.BIG_ENDIAN; - } - - @Override - public boolean hasArray() { - return false; - } - - @Override - public byte[] array() { - throw new UnsupportedOperationException("direct buffer"); - } - - @Override - public int arrayOffset() { - throw new UnsupportedOperationException("direct buffer"); } @Override @@ -225,11 +80,23 @@ public class UnpooledUnsafeDirectByteBuf extends AbstractReferenceCountedByteBuf return memoryAddress; } + @Override + public byte getByte(int index) { + checkIndex(index); + return _getByte(index); + } + @Override protected byte _getByte(int index) { return UnsafeByteBufUtil.getByte(addr(index)); } + @Override + public short getShort(int index) { + checkIndex(index, 2); + return _getShort(index); + } + @Override protected short _getShort(int index) { return UnsafeByteBufUtil.getShort(addr(index)); @@ -240,6 +107,12 @@ public class UnpooledUnsafeDirectByteBuf extends AbstractReferenceCountedByteBuf return UnsafeByteBufUtil.getShortLE(addr(index)); } + @Override + public int getUnsignedMedium(int index) { + checkIndex(index, 3); + return _getUnsignedMedium(index); + } + @Override protected int _getUnsignedMedium(int index) { return UnsafeByteBufUtil.getUnsignedMedium(addr(index)); @@ -250,6 +123,12 @@ public class UnpooledUnsafeDirectByteBuf extends AbstractReferenceCountedByteBuf return UnsafeByteBufUtil.getUnsignedMediumLE(addr(index)); } + @Override + public int getInt(int index) { + checkIndex(index, 4); + return _getInt(index); + } + @Override protected int _getInt(int index) { return UnsafeByteBufUtil.getInt(addr(index)); @@ -260,6 +139,12 @@ public class UnpooledUnsafeDirectByteBuf extends AbstractReferenceCountedByteBuf return UnsafeByteBufUtil.getIntLE(addr(index)); } + @Override + public long getLong(int index) { + checkIndex(index, 8); + return _getLong(index); + } + @Override protected long _getLong(int index) { return UnsafeByteBufUtil.getLong(addr(index)); @@ -277,23 +162,19 @@ public class UnpooledUnsafeDirectByteBuf extends AbstractReferenceCountedByteBuf } @Override - public ByteBuf getBytes(int index, byte[] dst, int dstIndex, int length) { + void getBytes(int index, byte[] dst, int dstIndex, int length, boolean internal) { UnsafeByteBufUtil.getBytes(this, addr(index), index, dst, dstIndex, length); - return this; } @Override - public ByteBuf getBytes(int index, ByteBuffer dst) { + void getBytes(int index, ByteBuffer dst, boolean internal) { UnsafeByteBufUtil.getBytes(this, addr(index), index, dst); - return this; } @Override - public ByteBuf readBytes(ByteBuffer dst) { - int length = dst.remaining(); - checkReadableBytes(length); - getBytes(readerIndex, dst); - readerIndex += length; + public ByteBuf setByte(int index, int value) { + checkIndex(index); + _setByte(index, value); return this; } @@ -302,6 +183,13 @@ public class UnpooledUnsafeDirectByteBuf extends AbstractReferenceCountedByteBuf UnsafeByteBufUtil.setByte(addr(index), value); } + @Override + public ByteBuf setShort(int index, int value) { + checkIndex(index, 2); + _setShort(index, value); + return this; + } + @Override protected void _setShort(int index, int value) { UnsafeByteBufUtil.setShort(addr(index), value); @@ -312,6 +200,13 @@ public class UnpooledUnsafeDirectByteBuf extends AbstractReferenceCountedByteBuf UnsafeByteBufUtil.setShortLE(addr(index), value); } + @Override + public ByteBuf setMedium(int index, int value) { + checkIndex(index, 3); + _setMedium(index, value); + return this; + } + @Override protected void _setMedium(int index, int value) { UnsafeByteBufUtil.setMedium(addr(index), value); @@ -322,6 +217,13 @@ public class UnpooledUnsafeDirectByteBuf extends AbstractReferenceCountedByteBuf UnsafeByteBufUtil.setMediumLE(addr(index), value); } + @Override + public ByteBuf setInt(int index, int value) { + checkIndex(index, 4); + _setInt(index, value); + return this; + } + @Override protected void _setInt(int index, int value) { UnsafeByteBufUtil.setInt(addr(index), value); @@ -332,6 +234,13 @@ public class UnpooledUnsafeDirectByteBuf extends AbstractReferenceCountedByteBuf UnsafeByteBufUtil.setIntLE(addr(index), value); } + @Override + public ByteBuf setLong(int index, long value) { + checkIndex(index, 8); + _setLong(index, value); + return this; + } + @Override protected void _setLong(int index, long value) { UnsafeByteBufUtil.setLong(addr(index), value); @@ -361,62 +270,8 @@ public class UnpooledUnsafeDirectByteBuf extends AbstractReferenceCountedByteBuf } @Override - public ByteBuf getBytes(int index, OutputStream out, int length) throws IOException { + void getBytes(int index, OutputStream out, int length, boolean internal) throws IOException { UnsafeByteBufUtil.getBytes(this, addr(index), index, out, length); - return this; - } - - @Override - public int getBytes(int index, GatheringByteChannel out, int length) throws IOException { - return getBytes(index, out, length, false); - } - - private int getBytes(int index, GatheringByteChannel out, int length, boolean internal) throws IOException { - ensureAccessible(); - if (length == 0) { - return 0; - } - - ByteBuffer tmpBuf; - if (internal) { - tmpBuf = internalNioBuffer(); - } else { - tmpBuf = buffer.duplicate(); - } - tmpBuf.clear().position(index).limit(index + length); - return out.write(tmpBuf); - } - - @Override - public int getBytes(int index, FileChannel out, long position, int length) throws IOException { - return getBytes(index, out, position, length, false); - } - - private int getBytes(int index, FileChannel out, long position, int length, boolean internal) throws IOException { - ensureAccessible(); - if (length == 0) { - return 0; - } - - ByteBuffer tmpBuf = internal ? internalNioBuffer() : buffer.duplicate(); - tmpBuf.clear().position(index).limit(index + length); - return out.write(tmpBuf, position); - } - - @Override - public int readBytes(GatheringByteChannel out, int length) throws IOException { - checkReadableBytes(length); - int readBytes = getBytes(readerIndex, out, length, true); - readerIndex += readBytes; - return readBytes; - } - - @Override - public int readBytes(FileChannel out, long position, int length) throws IOException { - checkReadableBytes(length); - int readBytes = getBytes(readerIndex, out, position, length, true); - readerIndex += readBytes; - return readBytes; } @Override @@ -424,85 +279,12 @@ public class UnpooledUnsafeDirectByteBuf extends AbstractReferenceCountedByteBuf return UnsafeByteBufUtil.setBytes(this, addr(index), index, in, length); } - @Override - public int setBytes(int index, ScatteringByteChannel in, int length) throws IOException { - ensureAccessible(); - ByteBuffer tmpBuf = internalNioBuffer(); - tmpBuf.clear().position(index).limit(index + length); - try { - return in.read(tmpBuf); - } catch (ClosedChannelException ignored) { - return -1; - } - } - - @Override - public int setBytes(int index, FileChannel in, long position, int length) throws IOException { - ensureAccessible(); - ByteBuffer tmpBuf = internalNioBuffer(); - tmpBuf.clear().position(index).limit(index + length); - try { - return in.read(tmpBuf, position); - } catch (ClosedChannelException ignored) { - return -1; - } - } - - @Override - public int nioBufferCount() { - return 1; - } - - @Override - public ByteBuffer[] nioBuffers(int index, int length) { - return new ByteBuffer[] { nioBuffer(index, length) }; - } - @Override public ByteBuf copy(int index, int length) { return UnsafeByteBufUtil.copy(this, addr(index), index, length); } - @Override - public ByteBuffer internalNioBuffer(int index, int length) { - checkIndex(index, length); - return (ByteBuffer) internalNioBuffer().clear().position(index).limit(index + length); - } - - private ByteBuffer internalNioBuffer() { - ByteBuffer tmpNioBuf = this.tmpNioBuf; - if (tmpNioBuf == null) { - this.tmpNioBuf = tmpNioBuf = buffer.duplicate(); - } - return tmpNioBuf; - } - - @Override - public ByteBuffer nioBuffer(int index, int length) { - checkIndex(index, length); - return ((ByteBuffer) buffer.duplicate().position(index).limit(index + length)).slice(); - } - - @Override - protected void deallocate() { - ByteBuffer buffer = this.buffer; - if (buffer == null) { - return; - } - - this.buffer = null; - - if (!doNotFree) { - freeDirect(buffer); - } - } - - @Override - public ByteBuf unwrap() { - return null; - } - - long addr(int index) { + final long addr(int index) { return memoryAddress + index; } diff --git a/buffer/src/main/java/io/netty/buffer/UnpooledUnsafeHeapByteBuf.java b/buffer/src/main/java/io/netty/buffer/UnpooledUnsafeHeapByteBuf.java index 0fbe856..ee3a86e 100644 --- a/buffer/src/main/java/io/netty/buffer/UnpooledUnsafeHeapByteBuf.java +++ b/buffer/src/main/java/io/netty/buffer/UnpooledUnsafeHeapByteBuf.java @@ -17,7 +17,12 @@ package io.netty.buffer; import io.netty.util.internal.PlatformDependent; -class UnpooledUnsafeHeapByteBuf extends UnpooledHeapByteBuf { +/** + * Big endian Java heap buffer implementation. It is recommended to use + * {@link UnpooledByteBufAllocator#heapBuffer(int, int)}, {@link Unpooled#buffer(int)} and + * {@link Unpooled#wrappedBuffer(byte[])} instead of calling the constructor explicitly. + */ +public class UnpooledUnsafeHeapByteBuf extends UnpooledHeapByteBuf { /** * Creates a new heap buffer with a newly allocated byte array. @@ -25,7 +30,7 @@ class UnpooledUnsafeHeapByteBuf extends UnpooledHeapByteBuf { * @param initialCapacity the initial capacity of the underlying byte array * @param maxCapacity the max capacity of the underlying byte array */ - UnpooledUnsafeHeapByteBuf(ByteBufAllocator alloc, int initialCapacity, int maxCapacity) { + public UnpooledUnsafeHeapByteBuf(ByteBufAllocator alloc, int initialCapacity, int maxCapacity) { super(alloc, initialCapacity, maxCapacity); } diff --git a/buffer/src/main/java/io/netty/buffer/UnpooledUnsafeNoCleanerDirectByteBuf.java b/buffer/src/main/java/io/netty/buffer/UnpooledUnsafeNoCleanerDirectByteBuf.java index 3b9c05b..cc00e86 100644 --- a/buffer/src/main/java/io/netty/buffer/UnpooledUnsafeNoCleanerDirectByteBuf.java +++ b/buffer/src/main/java/io/netty/buffer/UnpooledUnsafeNoCleanerDirectByteBuf.java @@ -48,18 +48,8 @@ class UnpooledUnsafeNoCleanerDirectByteBuf extends UnpooledUnsafeDirectByteBuf { return this; } - ByteBuffer newBuffer = reallocateDirect(buffer, newCapacity); - - if (newCapacity < oldCapacity) { - if (readerIndex() < newCapacity) { - if (writerIndex() > newCapacity) { - writerIndex(newCapacity); - } - } else { - setIndex(newCapacity, newCapacity); - } - } - setByteBuffer(newBuffer, false); + trimIndicesToCapacity(newCapacity); + setByteBuffer(reallocateDirect(buffer, newCapacity), false); return this; } } diff --git a/buffer/src/main/java/io/netty/buffer/UnreleasableByteBuf.java b/buffer/src/main/java/io/netty/buffer/UnreleasableByteBuf.java index ba06103..ad6dcaf 100644 --- a/buffer/src/main/java/io/netty/buffer/UnreleasableByteBuf.java +++ b/buffer/src/main/java/io/netty/buffer/UnreleasableByteBuf.java @@ -15,6 +15,8 @@ */ package io.netty.buffer; +import io.netty.util.internal.ObjectUtil; + import java.nio.ByteOrder; /** @@ -31,10 +33,7 @@ final class UnreleasableByteBuf extends WrappedByteBuf { @Override public ByteBuf order(ByteOrder endianness) { - if (endianness == null) { - throw new NullPointerException("endianness"); - } - if (endianness == order()) { + if (ObjectUtil.checkNotNull(endianness, "endianness") == order()) { return this; } diff --git a/buffer/src/main/java/io/netty/buffer/WrappedByteBuf.java b/buffer/src/main/java/io/netty/buffer/WrappedByteBuf.java index 45aa60c..f682f1b 100644 --- a/buffer/src/main/java/io/netty/buffer/WrappedByteBuf.java +++ b/buffer/src/main/java/io/netty/buffer/WrappedByteBuf.java @@ -17,6 +17,7 @@ package io.netty.buffer; import io.netty.util.ByteProcessor; +import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.StringUtil; import java.io.IOException; @@ -41,10 +42,7 @@ class WrappedByteBuf extends ByteBuf { protected final ByteBuf buf; protected WrappedByteBuf(ByteBuf buf) { - if (buf == null) { - throw new NullPointerException("buf"); - } - this.buf = buf; + this.buf = ObjectUtil.checkNotNull(buf, "buf"); } @Override @@ -52,6 +50,11 @@ class WrappedByteBuf extends ByteBuf { return buf.hasMemoryAddress(); } + @Override + public boolean isContiguous() { + return buf.isContiguous(); + } + @Override public final long memoryAddress() { return buf.memoryAddress(); @@ -151,6 +154,11 @@ class WrappedByteBuf extends ByteBuf { return buf.maxWritableBytes(); } + @Override + public int maxFastWritableBytes() { + return buf.maxFastWritableBytes(); + } + @Override public final boolean isReadable() { return buf.isReadable(); @@ -1033,4 +1041,9 @@ class WrappedByteBuf extends ByteBuf { public boolean release(int decrement) { return buf.release(decrement); } + + @Override + final boolean isAccessible() { + return buf.isAccessible(); + } } diff --git a/buffer/src/main/java/io/netty/buffer/WrappedCompositeByteBuf.java b/buffer/src/main/java/io/netty/buffer/WrappedCompositeByteBuf.java index b124eb2..e58623a 100644 --- a/buffer/src/main/java/io/netty/buffer/WrappedCompositeByteBuf.java +++ b/buffer/src/main/java/io/netty/buffer/WrappedCompositeByteBuf.java @@ -98,6 +98,11 @@ class WrappedCompositeByteBuf extends CompositeByteBuf { return wrapped.maxWritableBytes(); } + @Override + public int maxFastWritableBytes() { + return wrapped.maxFastWritableBytes(); + } + @Override public int ensureWritable(int minWritableBytes, boolean force) { return wrapped.ensureWritable(minWritableBytes, force); @@ -424,8 +429,8 @@ class WrappedCompositeByteBuf extends CompositeByteBuf { } @Override - int internalRefCnt() { - return wrapped.internalRefCnt(); + final boolean isAccessible() { + return wrapped.isAccessible(); } @Override @@ -548,6 +553,12 @@ class WrappedCompositeByteBuf extends CompositeByteBuf { return this; } + @Override + public CompositeByteBuf addFlattenedComponents(boolean increaseWriterIndex, ByteBuf buffer) { + wrapped.addFlattenedComponents(increaseWriterIndex, buffer); + return this; + } + @Override public CompositeByteBuf removeComponent(int cIndex) { wrapped.removeComponent(cIndex); diff --git a/buffer/src/main/resources/META-INF/native-image/io.netty/buffer/native-image.properties b/buffer/src/main/resources/META-INF/native-image/io.netty/buffer/native-image.properties new file mode 100644 index 0000000..33f761b --- /dev/null +++ b/buffer/src/main/resources/META-INF/native-image/io.netty/buffer/native-image.properties @@ -0,0 +1,15 @@ +# Copyright 2019 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. + +Args = --initialize-at-run-time=io.netty.buffer.PooledByteBufAllocator,io.netty.buffer.ByteBufAllocator,io.netty.buffer.ByteBufUtil,io.netty.buffer.AbstractReferenceCountedByteBuf diff --git a/buffer/src/test/java/io/netty/buffer/AbstractByteBufTest.java b/buffer/src/test/java/io/netty/buffer/AbstractByteBufTest.java index 59194ab..a3a1347 100644 --- a/buffer/src/test/java/io/netty/buffer/AbstractByteBufTest.java +++ b/buffer/src/test/java/io/netty/buffer/AbstractByteBufTest.java @@ -2125,6 +2125,9 @@ public abstract class AbstractByteBufTest { @Test public void testIndexOf() { buffer.clear(); + // Ensure the buffer is completely zero'ed. + buffer.setZero(0, buffer.capacity()); + buffer.writeByte((byte) 1); buffer.writeByte((byte) 2); buffer.writeByte((byte) 3); @@ -2135,6 +2138,38 @@ public abstract class AbstractByteBufTest { assertEquals(-1, buffer.indexOf(4, 1, (byte) 1)); assertEquals(1, buffer.indexOf(1, 4, (byte) 2)); assertEquals(3, buffer.indexOf(4, 1, (byte) 2)); + + try { + buffer.indexOf(0, buffer.capacity() + 1, (byte) 0); + fail(); + } catch (IndexOutOfBoundsException expected) { + // expected + } + + try { + buffer.indexOf(buffer.capacity(), -1, (byte) 0); + fail(); + } catch (IndexOutOfBoundsException expected) { + // expected + } + + assertEquals(4, buffer.indexOf(buffer.capacity() + 1, 0, (byte) 1)); + assertEquals(0, buffer.indexOf(-1, buffer.capacity(), (byte) 1)); + } + + @Test + public void testIndexOfReleaseBuffer() { + ByteBuf buffer = releasedBuffer(); + if (buffer.capacity() != 0) { + try { + buffer.indexOf(0, 1, (byte) 1); + fail(); + } catch (IllegalReferenceCountException expected) { + // expected + } + } else { + assertEquals(-1, buffer.indexOf(0, 1, (byte) 1)); + } } @Test @@ -2570,7 +2605,6 @@ public abstract class AbstractByteBufTest { private ByteBuf releasedBuffer() { ByteBuf buffer = newBuffer(8); - // Clear the buffer so we are sure the reader and writer indices are 0. // This is important as we may return a slice from newBuffer(...). buffer.clear(); @@ -4878,4 +4912,16 @@ public abstract class AbstractByteBufTest { buffer.release(); } } + + @Test + public void testMaxFastWritableBytes() { + ByteBuf buffer = newBuffer(150, 500).writerIndex(100); + assertEquals(50, buffer.writableBytes()); + assertEquals(150, buffer.capacity()); + assertEquals(500, buffer.maxCapacity()); + assertEquals(400, buffer.maxWritableBytes()); + // Default implementation has fast writable == writable + assertEquals(50, buffer.maxFastWritableBytes()); + buffer.release(); + } } diff --git a/buffer/src/test/java/io/netty/buffer/AbstractCompositeByteBufTest.java b/buffer/src/test/java/io/netty/buffer/AbstractCompositeByteBufTest.java index c51a991..2c198ac 100644 --- a/buffer/src/test/java/io/netty/buffer/AbstractCompositeByteBufTest.java +++ b/buffer/src/test/java/io/netty/buffer/AbstractCompositeByteBufTest.java @@ -16,6 +16,7 @@ package io.netty.buffer; import io.netty.util.ReferenceCountUtil; +import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.PlatformDependent; import org.junit.Assume; import org.junit.Test; @@ -57,10 +58,7 @@ public abstract class AbstractCompositeByteBufTest extends AbstractByteBufTest { private final ByteOrder order; protected AbstractCompositeByteBufTest(ByteOrder order) { - if (order == null) { - throw new NullPointerException("order"); - } - this.order = order; + this.order = ObjectUtil.checkNotNull(order, "order"); } @Override @@ -109,6 +107,13 @@ public abstract class AbstractCompositeByteBufTest extends AbstractByteBufTest { return false; } + @Test + public void testIsContiguous() { + ByteBuf buf = newBuffer(4); + assertFalse(buf.isContiguous()); + buf.release(); + } + /** * Tests the "getBufferFor" method */ @@ -135,6 +140,41 @@ public abstract class AbstractCompositeByteBufTest extends AbstractByteBufTest { buf.release(); } + @Test + public void testToComponentIndex() { + CompositeByteBuf buf = (CompositeByteBuf) wrappedBuffer(new byte[]{1, 2, 3, 4, 5}, + new byte[]{4, 5, 6, 7, 8, 9, 26}, new byte[]{10, 9, 8, 7, 6, 5, 33}); + + // spot checks + assertEquals(0, buf.toComponentIndex(4)); + assertEquals(1, buf.toComponentIndex(5)); + assertEquals(2, buf.toComponentIndex(15)); + + //Loop through each byte + + byte index = 0; + + while (index < buf.capacity()) { + int cindex = buf.toComponentIndex(index++); + assertTrue(cindex >= 0 && cindex < buf.numComponents()); + } + + buf.release(); + } + + @Test + public void testToByteIndex() { + CompositeByteBuf buf = (CompositeByteBuf) wrappedBuffer(new byte[]{1, 2, 3, 4, 5}, + new byte[]{4, 5, 6, 7, 8, 9, 26}, new byte[]{10, 9, 8, 7, 6, 5, 33}); + + // spot checks + assertEquals(0, buf.toByteIndex(0)); + assertEquals(5, buf.toByteIndex(1)); + assertEquals(12, buf.toByteIndex(2)); + + buf.release(); + } + @Test public void testDiscardReadBytes3() { ByteBuf a, b; @@ -747,6 +787,20 @@ public abstract class AbstractCompositeByteBufTest extends AbstractByteBufTest { buf.release(); } + @Test + public void testRemoveComponents() { + CompositeByteBuf buf = compositeBuffer(); + for (int i = 0; i < 10; i++) { + buf.addComponent(wrappedBuffer(new byte[]{1, 2})); + } + assertEquals(10, buf.numComponents()); + assertEquals(20, buf.capacity()); + buf.removeComponents(4, 3); + assertEquals(7, buf.numComponents()); + assertEquals(14, buf.capacity()); + buf.release(); + } + @Test public void testGatheringWritesHeap() throws Exception { testGatheringWrites(buffer().order(order), buffer().order(order)); @@ -928,7 +982,27 @@ public abstract class AbstractCompositeByteBufTest extends AbstractByteBufTest { @Override @Test public void testInternalNioBuffer() { - // ignore + CompositeByteBuf buf = compositeBuffer(); + assertEquals(0, buf.internalNioBuffer(0, 0).remaining()); + + // If non-derived buffer is added, its internal buffer should be returned + ByteBuf concreteBuffer = directBuffer().writeByte(1); + buf.addComponent(concreteBuffer); + assertSame(concreteBuffer.internalNioBuffer(0, 1), buf.internalNioBuffer(0, 1)); + buf.release(); + + // In derived cases, the original internal buffer must not be used + buf = compositeBuffer(); + concreteBuffer = directBuffer().writeByte(1); + buf.addComponent(concreteBuffer.slice()); + assertNotSame(concreteBuffer.internalNioBuffer(0, 1), buf.internalNioBuffer(0, 1)); + buf.release(); + + buf = compositeBuffer(); + concreteBuffer = directBuffer().writeByte(1); + buf.addComponent(concreteBuffer.duplicate()); + assertNotSame(concreteBuffer.internalNioBuffer(0, 1), buf.internalNioBuffer(0, 1)); + buf.release(); } @Test @@ -1047,6 +1121,71 @@ public abstract class AbstractCompositeByteBufTest extends AbstractByteBufTest { cbuf.release(); } + @Test + public void testAddFlattenedComponents() { + ByteBuf b1 = Unpooled.wrappedBuffer(new byte[] { 1, 2, 3 }); + CompositeByteBuf newComposite = Unpooled.compositeBuffer() + .addComponent(true, b1) + .addFlattenedComponents(true, b1.retain()) + .addFlattenedComponents(true, Unpooled.EMPTY_BUFFER); + + assertEquals(2, newComposite.numComponents()); + assertEquals(6, newComposite.capacity()); + assertEquals(6, newComposite.writerIndex()); + + // It is important to use a pooled allocator here to ensure + // the slices returned by readRetainedSlice are of type + // PooledSlicedByteBuf, which maintains an independent refcount + // (so that we can be sure to cover this case) + ByteBuf buffer = PooledByteBufAllocator.DEFAULT.buffer() + .writeBytes(new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}); + + // use mixture of slice and retained slice + ByteBuf s1 = buffer.readRetainedSlice(2); + ByteBuf s2 = s1.retainedSlice(0, 2); + ByteBuf s3 = buffer.slice(0, 2).retain(); + ByteBuf s4 = s2.retainedSlice(0, 2); + buffer.release(); + + ByteBuf compositeToAdd = Unpooled.compositeBuffer() + .addComponent(s1) + .addComponent(Unpooled.EMPTY_BUFFER) + .addComponents(s2, s3, s4); + // set readable range to be from middle of first component + // to middle of penultimate component + compositeToAdd.setIndex(1, 5); + + assertEquals(1, compositeToAdd.refCnt()); + assertEquals(1, s4.refCnt()); + + ByteBuf compositeCopy = compositeToAdd.copy(); + + newComposite.addFlattenedComponents(true, compositeToAdd); + + // verify that added range matches + ByteBufUtil.equals(compositeCopy, 0, + newComposite, 6, compositeCopy.readableBytes()); + + // should not include empty component or last component + // (latter outside of the readable range) + assertEquals(5, newComposite.numComponents()); + assertEquals(10, newComposite.capacity()); + assertEquals(10, newComposite.writerIndex()); + + assertEquals(0, compositeToAdd.refCnt()); + // s4 wasn't in added range so should have been jettisoned + assertEquals(0, s4.refCnt()); + assertEquals(1, newComposite.refCnt()); + + // releasing composite should release the remaining components + newComposite.release(); + assertEquals(0, newComposite.refCnt()); + assertEquals(0, s1.refCnt()); + assertEquals(0, s2.refCnt()); + assertEquals(0, s3.refCnt()); + assertEquals(0, b1.refCnt()); + } + @Test public void testIterator() { CompositeByteBuf cbuf = compositeBuffer(); @@ -1203,6 +1342,40 @@ public abstract class AbstractCompositeByteBufTest extends AbstractByteBufTest { assertEquals(0, b2.refCnt()); } + @Test + public void testReleasesOnShrink2() { + // It is important to use a pooled allocator here to ensure + // the slices returned by readRetainedSlice are of type + // PooledSlicedByteBuf, which maintains an independent refcount + // (so that we can be sure to cover this case) + ByteBuf buffer = PooledByteBufAllocator.DEFAULT.buffer(); + + buffer.writeShort(1).writeShort(2); + + ByteBuf b1 = buffer.readRetainedSlice(2); + ByteBuf b2 = b1.retainedSlice(b1.readerIndex(), 2); + + // composite takes ownership of b1 and b2 + ByteBuf composite = Unpooled.compositeBuffer() + .addComponents(b1, b2); + + assertEquals(4, composite.capacity()); + + // reduce capacity down to two, will drop the second component + composite.capacity(2); + assertEquals(2, composite.capacity()); + + // releasing composite should release the components + composite.release(); + assertEquals(0, composite.refCnt()); + assertEquals(0, b1.refCnt()); + assertEquals(0, b2.refCnt()); + + // release last remaining ref to buffer + buffer.release(); + assertEquals(0, buffer.refCnt()); + } + @Test public void testAllocatorIsSameWhenCopy() { testAllocatorIsSameWhenCopy(false); @@ -1295,4 +1468,70 @@ public abstract class AbstractCompositeByteBufTest extends AbstractByteBufTest { assertTrue(buf.release()); } + @Test + public void testDiscardSomeReadBytesCorrectlyUpdatesLastAccessed() { + testDiscardCorrectlyUpdatesLastAccessed(true); + } + + @Test + public void testDiscardReadBytesCorrectlyUpdatesLastAccessed() { + testDiscardCorrectlyUpdatesLastAccessed(false); + } + + private static void testDiscardCorrectlyUpdatesLastAccessed(boolean discardSome) { + CompositeByteBuf cbuf = compositeBuffer(); + List<ByteBuf> buffers = new ArrayList<ByteBuf>(4); + for (int i = 0; i < 4; i++) { + ByteBuf buf = buffer().writeInt(i); + cbuf.addComponent(true, buf); + buffers.add(buf); + } + + // Skip the first 2 bytes which means even if we call discard*ReadBytes() later we can no drop the first + // component as it is still used. + cbuf.skipBytes(2); + if (discardSome) { + cbuf.discardSomeReadBytes(); + } else { + cbuf.discardReadBytes(); + } + assertEquals(4, cbuf.numComponents()); + + // Now skip 3 bytes which means we should be able to drop the first component on the next discard*ReadBytes() + // call. + cbuf.skipBytes(3); + + if (discardSome) { + cbuf.discardSomeReadBytes(); + } else { + cbuf.discardReadBytes(); + } + assertEquals(3, cbuf.numComponents()); + // Now skip again 3 bytes which should bring our readerIndex == start of the 3 component. + cbuf.skipBytes(3); + + // Read one int (4 bytes) which should bring our readerIndex == start of the 4 component. + assertEquals(2, cbuf.readInt()); + if (discardSome) { + cbuf.discardSomeReadBytes(); + } else { + cbuf.discardReadBytes(); + } + + // Now all except the last component should have been dropped / released. + assertEquals(1, cbuf.numComponents()); + assertEquals(3, cbuf.readInt()); + if (discardSome) { + cbuf.discardSomeReadBytes(); + } else { + cbuf.discardReadBytes(); + } + assertEquals(0, cbuf.numComponents()); + + // These should have been released already. + for (ByteBuf buffer: buffers) { + assertEquals(0, buffer.refCnt()); + } + assertTrue(cbuf.release()); + } } diff --git a/buffer/src/test/java/io/netty/buffer/AbstractPooledByteBufTest.java b/buffer/src/test/java/io/netty/buffer/AbstractPooledByteBufTest.java index eb76157..c7f5815 100644 --- a/buffer/src/test/java/io/netty/buffer/AbstractPooledByteBufTest.java +++ b/buffer/src/test/java/io/netty/buffer/AbstractPooledByteBufTest.java @@ -21,6 +21,8 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.greaterThanOrEqualTo; import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; public abstract class AbstractPooledByteBufTest extends AbstractByteBufTest { @@ -59,4 +61,68 @@ public abstract class AbstractPooledByteBufTest extends AbstractByteBufTest { buf.release(); } } + + @Override + @Test + public void testMaxFastWritableBytes() { + ByteBuf buffer = newBuffer(150, 500).writerIndex(100); + assertEquals(50, buffer.writableBytes()); + assertEquals(150, buffer.capacity()); + assertEquals(500, buffer.maxCapacity()); + assertEquals(400, buffer.maxWritableBytes()); + + int chunkSize = pooledByteBuf(buffer).maxLength; + assertTrue(chunkSize >= 150); + int remainingInAlloc = Math.min(chunkSize - 100, 400); + assertEquals(remainingInAlloc, buffer.maxFastWritableBytes()); + + // write up to max, chunk alloc should not change (same handle) + long handleBefore = pooledByteBuf(buffer).handle; + buffer.writeBytes(new byte[remainingInAlloc]); + assertEquals(handleBefore, pooledByteBuf(buffer).handle); + + assertEquals(0, buffer.maxFastWritableBytes()); + // writing one more should trigger a reallocation (new handle) + buffer.writeByte(7); + assertNotEquals(handleBefore, pooledByteBuf(buffer).handle); + + // should not exceed maxCapacity even if chunk alloc does + buffer.capacity(500); + assertEquals(500 - buffer.writerIndex(), buffer.maxFastWritableBytes()); + buffer.release(); + } + + private static PooledByteBuf<?> pooledByteBuf(ByteBuf buffer) { + // might need to unwrap if swapped (LE) and/or leak-aware-wrapped + while (!(buffer instanceof PooledByteBuf)) { + buffer = buffer.unwrap(); + } + return (PooledByteBuf<?>) buffer; + } + + @Test + public void testEnsureWritableDoesntGrowTooMuch() { + ByteBuf buffer = newBuffer(150, 500).writerIndex(100); + + assertEquals(50, buffer.writableBytes()); + int fastWritable = buffer.maxFastWritableBytes(); + assertTrue(fastWritable > 50); + + long handleBefore = pooledByteBuf(buffer).handle; + + // capacity expansion should not cause reallocation + // (should grow precisely the specified amount) + buffer.ensureWritable(fastWritable); + assertEquals(handleBefore, pooledByteBuf(buffer).handle); + assertEquals(100 + fastWritable, buffer.capacity()); + assertEquals(buffer.writableBytes(), buffer.maxFastWritableBytes()); + buffer.release(); + } + + @Test + public void testIsContiguous() { + ByteBuf buf = newBuffer(4); + assertTrue(buf.isContiguous()); + buf.release(); + } } diff --git a/buffer/src/test/java/io/netty/buffer/AdvancedLeakAwareByteBufTest.java b/buffer/src/test/java/io/netty/buffer/AdvancedLeakAwareByteBufTest.java index 4e7747b..60b84d0 100644 --- a/buffer/src/test/java/io/netty/buffer/AdvancedLeakAwareByteBufTest.java +++ b/buffer/src/test/java/io/netty/buffer/AdvancedLeakAwareByteBufTest.java @@ -15,6 +15,12 @@ */ package io.netty.buffer; +import static io.netty.buffer.Unpooled.*; +import static org.junit.Assert.*; + +import org.junit.Test; + +import io.netty.util.CharsetUtil; import io.netty.util.ResourceLeakTracker; public class AdvancedLeakAwareByteBufTest extends SimpleLeakAwareByteBufTest { @@ -28,4 +34,21 @@ public class AdvancedLeakAwareByteBufTest extends SimpleLeakAwareByteBufTest { protected SimpleLeakAwareByteBuf wrap(ByteBuf buffer, ResourceLeakTracker<ByteBuf> tracker) { return new AdvancedLeakAwareByteBuf(buffer, tracker); } + + @Test + public void testAddComponentWithLeakAwareByteBuf() { + NoopResourceLeakTracker<ByteBuf> tracker = new NoopResourceLeakTracker<ByteBuf>(); + + ByteBuf buffer = wrappedBuffer("hello world".getBytes(CharsetUtil.US_ASCII)).slice(6, 5); + ByteBuf leakAwareBuf = wrap(buffer, tracker); + + CompositeByteBuf composite = compositeBuffer(); + composite.addComponent(true, leakAwareBuf); + byte[] result = new byte[5]; + ByteBuf bb = composite.component(0); + System.out.println(bb); + bb.readBytes(result); + assertArrayEquals("world".getBytes(CharsetUtil.US_ASCII), result); + composite.release(); + } } diff --git a/buffer/src/test/java/io/netty/buffer/BigEndianDirectByteBufTest.java b/buffer/src/test/java/io/netty/buffer/BigEndianDirectByteBufTest.java index 6943c2f..8731295 100644 --- a/buffer/src/test/java/io/netty/buffer/BigEndianDirectByteBufTest.java +++ b/buffer/src/test/java/io/netty/buffer/BigEndianDirectByteBufTest.java @@ -19,6 +19,8 @@ import static org.junit.Assert.*; import java.nio.ByteOrder; +import org.junit.Test; + /** * Tests big-endian direct channel buffers */ @@ -35,4 +37,11 @@ public class BigEndianDirectByteBufTest extends AbstractByteBufTest { protected ByteBuf newDirectBuffer(int length, int maxCapacity) { return new UnpooledDirectByteBuf(UnpooledByteBufAllocator.DEFAULT, length, maxCapacity); } + + @Test + public void testIsContiguous() { + ByteBuf buf = newBuffer(4); + assertTrue(buf.isContiguous()); + buf.release(); + } } diff --git a/buffer/src/test/java/io/netty/buffer/ByteBufDerivationTest.java b/buffer/src/test/java/io/netty/buffer/ByteBufDerivationTest.java index 2983ba9..b641a16 100644 --- a/buffer/src/test/java/io/netty/buffer/ByteBufDerivationTest.java +++ b/buffer/src/test/java/io/netty/buffer/ByteBufDerivationTest.java @@ -22,7 +22,6 @@ import java.nio.ByteOrder; import java.util.Random; import static org.hamcrest.Matchers.*; -import static org.hamcrest.Matchers.sameInstance; import static org.junit.Assert.*; /** diff --git a/buffer/src/test/java/io/netty/buffer/ByteBufStreamTest.java b/buffer/src/test/java/io/netty/buffer/ByteBufStreamTest.java index 222ddcf..e0f4f50 100644 --- a/buffer/src/test/java/io/netty/buffer/ByteBufStreamTest.java +++ b/buffer/src/test/java/io/netty/buffer/ByteBufStreamTest.java @@ -187,29 +187,110 @@ public class ByteBufStreamTest { String s = in.readLine(); assertNull(s); + in.close(); + ByteBuf buf2 = Unpooled.buffer(); int charCount = 7; //total chars in the string below without new line characters byte[] abc = "\na\n\nb\r\nc\nd\ne".getBytes(utf8); - buf.writeBytes(abc); - in.mark(charCount); - assertEquals("", in.readLine()); - assertEquals("a", in.readLine()); - assertEquals("", in.readLine()); - assertEquals("b", in.readLine()); - assertEquals("c", in.readLine()); - assertEquals("d", in.readLine()); - assertEquals("e", in.readLine()); + buf2.writeBytes(abc); + + ByteBufInputStream in2 = new ByteBufInputStream(buf2, true); + in2.mark(charCount); + assertEquals("", in2.readLine()); + assertEquals("a", in2.readLine()); + assertEquals("", in2.readLine()); + assertEquals("b", in2.readLine()); + assertEquals("c", in2.readLine()); + assertEquals("d", in2.readLine()); + assertEquals("e", in2.readLine()); assertNull(in.readLine()); - in.reset(); + in2.reset(); int count = 0; - while (in.readLine() != null) { + while (in2.readLine() != null) { ++count; if (count > charCount) { fail("readLine() should have returned null"); } } assertEquals(charCount, count); + in2.close(); + } + + @Test + public void testRead() throws Exception { + // case1 + ByteBuf buf = Unpooled.buffer(16); + buf.writeBytes(new byte[]{1, 2, 3, 4, 5, 6}); + + ByteBufInputStream in = new ByteBufInputStream(buf, 3); + + assertEquals(1, in.read()); + assertEquals(2, in.read()); + assertEquals(3, in.read()); + assertEquals(-1, in.read()); + assertEquals(-1, in.read()); + assertEquals(-1, in.read()); + + buf.release(); in.close(); + + // case2 + ByteBuf buf2 = Unpooled.buffer(16); + buf2.writeBytes(new byte[]{1, 2, 3, 4, 5, 6}); + + ByteBufInputStream in2 = new ByteBufInputStream(buf2, 4); + + assertEquals(1, in2.read()); + assertEquals(2, in2.read()); + assertEquals(3, in2.read()); + assertEquals(4, in2.read()); + assertNotEquals(5, in2.read()); + assertEquals(-1, in2.read()); + + buf2.release(); + in2.close(); + } + + @Test + public void testReadLineLengthRespected1() throws Exception { + // case1 + ByteBuf buf = Unpooled.buffer(16); + buf.writeBytes(new byte[] { 1, 2, 3, 4, 5, 6 }); + + ByteBufInputStream in = new ByteBufInputStream(buf, 0); + + assertNull(in.readLine()); + buf.release(); + in.close(); + } + + @Test + public void testReadLineLengthRespected2() throws Exception { + ByteBuf buf2 = Unpooled.buffer(16); + buf2.writeBytes(new byte[] { 'A', 'B', '\n', 'C', 'E', 'F'}); + + ByteBufInputStream in2 = new ByteBufInputStream(buf2, 4); + + assertEquals("AB", in2.readLine()); + assertEquals("C", in2.readLine()); + assertNull(in2.readLine()); + buf2.release(); + in2.close(); + } + + @Test(expected = EOFException.class) + public void testReadByteLengthRespected() throws Exception { + // case1 + ByteBuf buf = Unpooled.buffer(16); + buf.writeBytes(new byte[] { 1, 2, 3, 4, 5, 6 }); + + ByteBufInputStream in = new ByteBufInputStream(buf, 0); + try { + in.readByte(); + } finally { + buf.release(); + in.close(); + } } } diff --git a/buffer/src/test/java/io/netty/buffer/ByteBufUtilTest.java b/buffer/src/test/java/io/netty/buffer/ByteBufUtilTest.java index da9b344..3500c54 100644 --- a/buffer/src/test/java/io/netty/buffer/ByteBufUtilTest.java +++ b/buffer/src/test/java/io/netty/buffer/ByteBufUtilTest.java @@ -510,6 +510,111 @@ public class ByteBufUtilTest { assertTrue(buf instanceof WrappedByteBuf); } + @Test + public void testWriteUtf8Subsequence() { + String usAscii = "Some UTF-8 like äÄâˆÅ’Å’"; + ByteBuf buf = Unpooled.buffer(16); + buf.writeBytes(usAscii.substring(5, 18).getBytes(CharsetUtil.UTF_8)); + ByteBuf buf2 = Unpooled.buffer(16); + ByteBufUtil.writeUtf8(buf2, usAscii, 5, 18); + + assertEquals(buf, buf2); + + buf.release(); + buf2.release(); + } + + @Test + public void testWriteUtf8SubsequenceSplitSurrogate() { + String usAscii = "\uD800\uDC00"; // surrogate pair: one code point, two chars + ByteBuf buf = Unpooled.buffer(16); + buf.writeBytes(usAscii.substring(0, 1).getBytes(CharsetUtil.UTF_8)); + ByteBuf buf2 = Unpooled.buffer(16); + ByteBufUtil.writeUtf8(buf2, usAscii, 0, 1); + + assertEquals(buf, buf2); + + buf.release(); + buf2.release(); + } + + @Test + public void testReserveAndWriteUtf8Subsequence() { + String usAscii = "Some UTF-8 like äÄâˆÅ’Å’"; + ByteBuf buf = Unpooled.buffer(16); + buf.writeBytes(usAscii.substring(5, 18).getBytes(CharsetUtil.UTF_8)); + ByteBuf buf2 = Unpooled.buffer(16); + int count = ByteBufUtil.reserveAndWriteUtf8(buf2, usAscii, 5, 18, 16); + + assertEquals(buf, buf2); + assertEquals(buf.readableBytes(), count); + + buf.release(); + buf2.release(); + } + + @Test + public void testUtf8BytesSubsequence() { + String usAscii = "Some UTF-8 like äÄâˆÅ’Å’"; + assertEquals(usAscii.substring(5, 18).getBytes(CharsetUtil.UTF_8).length, + ByteBufUtil.utf8Bytes(usAscii, 5, 18)); + } + + private static int[][] INVALID_RANGES = new int[][] { + { -1, 5 }, { 5, 30 }, { 10, 5 } + }; + + interface TestMethod { + int invoke(Object... args); + } + + private void testInvalidSubsequences(TestMethod method) { + for (int [] range : INVALID_RANGES) { + ByteBuf buf = Unpooled.buffer(16); + try { + method.invoke(buf, "Some UTF-8 like äÄâˆÅ’Å’", range[0], range[1]); + fail("Did not throw IndexOutOfBoundsException for range (" + range[0] + ", " + range[1] + ")"); + } catch (IndexOutOfBoundsException iiobe) { + // expected + } finally { + assertFalse(buf.isReadable()); + buf.release(); + } + } + } + + @Test + public void testWriteUtf8InvalidSubsequences() { + testInvalidSubsequences(new TestMethod() { + @Override + public int invoke(Object... args) { + return ByteBufUtil.writeUtf8((ByteBuf) args[0], (String) args[1], + (Integer) args[2], (Integer) args[3]); + } + }); + } + + @Test + public void testReserveAndWriteUtf8InvalidSubsequences() { + testInvalidSubsequences(new TestMethod() { + @Override + public int invoke(Object... args) { + return ByteBufUtil.reserveAndWriteUtf8((ByteBuf) args[0], (String) args[1], + (Integer) args[2], (Integer) args[3], 32); + } + }); + } + + @Test + public void testUtf8BytesInvalidSubsequences() { + testInvalidSubsequences(new TestMethod() { + @Override + public int invoke(Object... args) { + return ByteBufUtil.utf8Bytes((String) args[1], (Integer) args[2], (Integer) args[3]); + } + }); + } + @Test public void testDecodeUsAscii() { testDecodeString("This is a test", CharsetUtil.US_ASCII); diff --git a/buffer/src/test/java/io/netty/buffer/DefaultByteBufHolderTest.java b/buffer/src/test/java/io/netty/buffer/DefaultByteBufHolderTest.java index 4c60d0e..6fd8aca 100644 --- a/buffer/src/test/java/io/netty/buffer/DefaultByteBufHolderTest.java +++ b/buffer/src/test/java/io/netty/buffer/DefaultByteBufHolderTest.java @@ -42,4 +42,64 @@ public class DefaultByteBufHolderTest { copy.release(); } } + + @SuppressWarnings("SimplifiableJUnitAssertion") + @Test + public void testDifferentClassesAreNotEqual() { + // all objects here have EMPTY_BUFFER data but are instances of different classes + // so we want to check that none of them are equal to another. + ByteBufHolder dflt = new DefaultByteBufHolder(Unpooled.EMPTY_BUFFER); + ByteBufHolder other = new OtherByteBufHolder(Unpooled.EMPTY_BUFFER, 123); + ByteBufHolder constant1 = new DefaultByteBufHolder(Unpooled.EMPTY_BUFFER) { + // intentionally empty + }; + ByteBufHolder constant2 = new DefaultByteBufHolder(Unpooled.EMPTY_BUFFER) { + // intentionally empty + }; + try { + // not using 'assertNotEquals' to be explicit about which object we are calling .equals() on + assertFalse(dflt.equals(other)); + assertFalse(dflt.equals(constant1)); + assertFalse(constant1.equals(dflt)); + assertFalse(constant1.equals(other)); + assertFalse(constant1.equals(constant2)); + } finally { + dflt.release(); + other.release(); + constant1.release(); + constant2.release(); + } + } + + private static class OtherByteBufHolder extends DefaultByteBufHolder { + + private final int extraField; + + OtherByteBufHolder(final ByteBuf data, final int extraField) { + super(data); + this.extraField = extraField; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + if (!super.equals(o)) { + return false; + } + final OtherByteBufHolder that = (OtherByteBufHolder) o; + return extraField == that.extraField; + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + extraField; + return result; + } + } } diff --git a/buffer/src/test/java/io/netty/buffer/DuplicatedByteBufTest.java b/buffer/src/test/java/io/netty/buffer/DuplicatedByteBufTest.java index 2e88657..f96b922 100644 --- a/buffer/src/test/java/io/netty/buffer/DuplicatedByteBufTest.java +++ b/buffer/src/test/java/io/netty/buffer/DuplicatedByteBufTest.java @@ -33,6 +33,13 @@ public class DuplicatedByteBufTest extends AbstractByteBufTest { return buffer; } + @Test + public void testIsContiguous() { + ByteBuf buf = newBuffer(4); + assertEquals(buf.unwrap().isContiguous(), buf.isContiguous()); + buf.release(); + } + @Test(expected = NullPointerException.class) public void shouldNotAllowNullInConstructor() { new DuplicatedByteBuf(null); diff --git a/buffer/src/test/java/io/netty/buffer/EmptyByteBufTest.java b/buffer/src/test/java/io/netty/buffer/EmptyByteBufTest.java index acafca1..0d9fc25 100644 --- a/buffer/src/test/java/io/netty/buffer/EmptyByteBufTest.java +++ b/buffer/src/test/java/io/netty/buffer/EmptyByteBufTest.java @@ -14,6 +14,7 @@ * under the License. */package io.netty.buffer; +import io.netty.util.CharsetUtil; import org.junit.Test; import static org.hamcrest.Matchers.*; @@ -21,6 +22,12 @@ import static org.junit.Assert.*; public class EmptyByteBufTest { + @Test + public void testIsContiguous() { + EmptyByteBuf empty = new EmptyByteBuf(UnpooledByteBufAllocator.DEFAULT); + assertTrue(empty.isContiguous()); + } + @Test public void testIsWritable() { EmptyByteBuf empty = new EmptyByteBuf(UnpooledByteBufAllocator.DEFAULT); @@ -93,4 +100,11 @@ public class EmptyByteBufTest { assertTrue(emptyAbstract.release()); assertFalse(empty.release()); } + + @Test + public void testGetCharSequence() { + EmptyByteBuf empty = new EmptyByteBuf(UnpooledByteBufAllocator.DEFAULT); + assertEquals("", empty.readCharSequence(0, CharsetUtil.US_ASCII)); + } + } diff --git a/buffer/src/test/java/io/netty/buffer/FixedCompositeByteBufTest.java b/buffer/src/test/java/io/netty/buffer/FixedCompositeByteBufTest.java index b6260f7..a564bf1 100644 --- a/buffer/src/test/java/io/netty/buffer/FixedCompositeByteBufTest.java +++ b/buffer/src/test/java/io/netty/buffer/FixedCompositeByteBufTest.java @@ -432,10 +432,12 @@ public class FixedCompositeByteBufTest { } @Test - public void testHasArrayWhenEmpty() { + public void testHasArrayWhenEmptyAndIsDirect() { ByteBuf buf = newBuffer(new ByteBuf[0]); assertTrue(buf.hasArray()); assertArrayEquals(EMPTY_BUFFER.array(), buf.array()); + assertEquals(EMPTY_BUFFER.isDirect(), buf.isDirect()); + assertEquals(EMPTY_BUFFER.memoryAddress(), buf.memoryAddress()); buf.release(); } diff --git a/buffer/src/test/java/io/netty/buffer/PoolArenaTest.java b/buffer/src/test/java/io/netty/buffer/PoolArenaTest.java index 2c1bd12..3ad331d 100644 --- a/buffer/src/test/java/io/netty/buffer/PoolArenaTest.java +++ b/buffer/src/test/java/io/netty/buffer/PoolArenaTest.java @@ -20,6 +20,8 @@ import io.netty.util.internal.PlatformDependent; import org.junit.Assert; import org.junit.Test; +import static org.junit.Assume.assumeTrue; + import java.nio.ByteBuffer; public class PoolArenaTest { @@ -46,6 +48,7 @@ public class PoolArenaTest { @Test public void testDirectArenaOffsetCacheLine() throws Exception { + assumeTrue(PlatformDependent.hasUnsafe()); int capacity = 5; int alignment = 128; @@ -64,7 +67,7 @@ public class PoolArenaTest { } @Test - public final void testAllocationCounter() { + public void testAllocationCounter() { final PooledByteBufAllocator allocator = new PooledByteBufAllocator( true, // preferDirect 0, // nHeapArena @@ -107,4 +110,26 @@ public class PoolArenaTest { Assert.assertEquals(1, metric.numNormalDeallocations()); Assert.assertEquals(1, metric.numNormalAllocations()); } + + @Test + public void testDirectArenaMemoryCopy() { + ByteBuf src = PooledByteBufAllocator.DEFAULT.directBuffer(512); + ByteBuf dst = PooledByteBufAllocator.DEFAULT.directBuffer(512); + + PooledByteBuf<ByteBuffer> pooledSrc = unwrapIfNeeded(src); + PooledByteBuf<ByteBuffer> pooledDst = unwrapIfNeeded(dst); + + // This causes the internal reused ByteBuffer duplicate limit to be set to 128 + pooledDst.writeBytes(ByteBuffer.allocate(128)); + // Ensure internal ByteBuffer duplicate limit is properly reset (used in memoryCopy non-Unsafe case) + pooledDst.chunk.arena.memoryCopy(pooledSrc.memory, 0, pooledDst, 512); + + src.release(); + dst.release(); + } + + @SuppressWarnings("unchecked") + private PooledByteBuf<ByteBuffer> unwrapIfNeeded(ByteBuf buf) { + return (PooledByteBuf<ByteBuffer>) (buf instanceof PooledByteBuf ? buf : buf.unwrap()); + } } diff --git a/buffer/src/test/java/io/netty/buffer/PooledByteBufAllocatorTest.java b/buffer/src/test/java/io/netty/buffer/PooledByteBufAllocatorTest.java index 495bb76..4f9ce33 100644 --- a/buffer/src/test/java/io/netty/buffer/PooledByteBufAllocatorTest.java +++ b/buffer/src/test/java/io/netty/buffer/PooledByteBufAllocatorTest.java @@ -63,6 +63,21 @@ public class PooledByteBufAllocatorTest extends AbstractByteBufAllocatorTest<Poo return allocator.metric().chunkSize(); } + @Test + public void testTrim() { + PooledByteBufAllocator allocator = newAllocator(true); + + // Should return false as we never allocated from this thread yet. + assertFalse(allocator.trimCurrentThreadCache()); + + ByteBuf directBuffer = allocator.directBuffer(); + + assertTrue(directBuffer.release()); + + // Should return true now a cache exists for the calling thread. + assertTrue(allocator.trimCurrentThreadCache()); + } + @Test public void testPooledUnsafeHeapBufferAndUnsafeDirectBuffer() { PooledByteBufAllocator allocator = newAllocator(true); @@ -77,6 +92,25 @@ public class PooledByteBufAllocatorTest extends AbstractByteBufAllocatorTest<Poo heapBuffer.release(); } + @Test + public void testIOBuffersAreDirectWhenUnsafeAvailableOrDirectBuffersPooled() { + PooledByteBufAllocator allocator = newAllocator(true); + ByteBuf ioBuffer = allocator.ioBuffer(); + + assertTrue(ioBuffer.isDirect()); + ioBuffer.release(); + + PooledByteBufAllocator unpooledAllocator = newUnpooledAllocator(); + ioBuffer = unpooledAllocator.ioBuffer(); + + if (PlatformDependent.hasUnsafe()) { + assertTrue(ioBuffer.isDirect()); + } else { + assertFalse(ioBuffer.isDirect()); + } + ioBuffer.release(); + } + @Test public void testWithoutUseCacheForAllThreads() { assertFalse(Thread.currentThread() instanceof FastThreadLocalThread); @@ -430,8 +464,14 @@ public class PooledByteBufAllocatorTest extends AbstractByteBufAllocatorTest<Poo Thread.sleep(100); } } finally { + // First mark all AllocationThreads to complete their work and then wait until these are complete + // and rethrow if there was any error. for (AllocationThread t : threads) { - t.finish(); + t.markAsFinished(); + } + + for (AllocationThread t: threads) { + t.joinAndCheckForError(); } } } @@ -461,7 +501,7 @@ public class PooledByteBufAllocatorTest extends AbstractByteBufAllocatorTest<Poo private final ByteBufAllocator allocator; private final AtomicReference<Object> finish = new AtomicReference<Object>(); - public AllocationThread(ByteBufAllocator allocator) { + AllocationThread(ByteBufAllocator allocator) { this.allocator = allocator; } @@ -494,14 +534,17 @@ public class PooledByteBufAllocatorTest extends AbstractByteBufAllocatorTest<Poo } } - public boolean isFinished() { + boolean isFinished() { return finish.get() != null; } - public void finish() throws Throwable { + void markAsFinished() { + finish.compareAndSet(null, Boolean.TRUE); + } + + void joinAndCheckForError() throws Throwable { try { // Mark as finish if not already done but ensure we not override the previous set error. - finish.compareAndSet(null, Boolean.TRUE); join(); } finally { releaseBuffers(); @@ -509,7 +552,7 @@ public class PooledByteBufAllocatorTest extends AbstractByteBufAllocatorTest<Poo checkForError(); } - public void checkForError() throws Throwable { + void checkForError() throws Throwable { Object obj = finish.get(); if (obj instanceof Throwable) { throw (Throwable) obj; diff --git a/buffer/src/test/java/io/netty/buffer/ReadOnlyDirectByteBufferBufTest.java b/buffer/src/test/java/io/netty/buffer/ReadOnlyDirectByteBufferBufTest.java index d51ce11..1e88bda 100644 --- a/buffer/src/test/java/io/netty/buffer/ReadOnlyDirectByteBufferBufTest.java +++ b/buffer/src/test/java/io/netty/buffer/ReadOnlyDirectByteBufferBufTest.java @@ -21,9 +21,8 @@ import org.junit.Test; import java.io.ByteArrayInputStream; import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; import java.io.IOException; +import java.io.RandomAccessFile; import java.nio.ByteBuffer; import java.nio.ReadOnlyBufferException; import java.nio.channels.FileChannel; @@ -38,6 +37,13 @@ public class ReadOnlyDirectByteBufferBufTest { return ByteBuffer.allocateDirect(size); } + @Test + public void testIsContiguous() { + ByteBuf buf = buffer(allocate(4).asReadOnlyBuffer()); + Assert.assertTrue(buf.isContiguous()); + buf.release(); + } + @Test(expected = IllegalArgumentException.class) public void testConstructWithWritable() { buffer(allocate(1)); @@ -236,6 +242,20 @@ public class ReadOnlyDirectByteBufferBufTest { buf.release(); } + @Test(expected = IndexOutOfBoundsException.class) + public void testGetBytesByteBuffer() { + byte[] bytes = {'a', 'b', 'c', 'd', 'e', 'f', 'g'}; + // Ensure destination buffer is bigger then what is in the ByteBuf. + ByteBuffer nioBuffer = ByteBuffer.allocate(bytes.length + 1); + ByteBuf buffer = buffer(((ByteBuffer) allocate(bytes.length) + .put(bytes).flip()).asReadOnlyBuffer()); + try { + buffer.getBytes(buffer.readerIndex(), nioBuffer); + } finally { + buffer.release(); + } + } + @Test public void testCopy() { ByteBuf buf = buffer(((ByteBuffer) allocate(16).putLong(1).putLong(2).flip()).asReadOnlyBuffer()); @@ -293,12 +313,12 @@ public class ReadOnlyDirectByteBufferBufTest { ByteBuf b2 = null; try { - output = new FileOutputStream(file).getChannel(); + output = new RandomAccessFile(file, "rw").getChannel(); byte[] bytes = new byte[1024]; PlatformDependent.threadLocalRandom().nextBytes(bytes); output.write(ByteBuffer.wrap(bytes)); - input = new FileInputStream(file).getChannel(); + input = new RandomAccessFile(file, "r").getChannel(); ByteBuffer m = input.map(FileChannel.MapMode.READ_ONLY, 0, input.size()); b1 = buffer(m); diff --git a/buffer/src/test/java/io/netty/buffer/SimpleLeakAwareCompositeByteBufTest.java b/buffer/src/test/java/io/netty/buffer/SimpleLeakAwareCompositeByteBufTest.java index 713fe72..34596f3 100644 --- a/buffer/src/test/java/io/netty/buffer/SimpleLeakAwareCompositeByteBufTest.java +++ b/buffer/src/test/java/io/netty/buffer/SimpleLeakAwareCompositeByteBufTest.java @@ -15,7 +15,9 @@ */ package io.netty.buffer; +import io.netty.util.ByteProcessor; import io.netty.util.ResourceLeakTracker; +import org.hamcrest.CoreMatchers; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -23,7 +25,9 @@ import org.junit.Test; import java.util.ArrayDeque; import java.util.Queue; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; public class SimpleLeakAwareCompositeByteBufTest extends WrappedCompositeByteBufTest { @@ -131,6 +135,26 @@ public class SimpleLeakAwareCompositeByteBufTest extends WrappedCompositeByteBuf assertWrapped(newBuffer(8).asReadOnly()); } + @Test + public void forEachByteUnderLeakDetectionShouldNotThrowException() { + CompositeByteBuf buf = (CompositeByteBuf) newBuffer(8); + assertThat(buf, CoreMatchers.instanceOf(SimpleLeakAwareCompositeByteBuf.class)); + CompositeByteBuf comp = (CompositeByteBuf) newBuffer(8); + assertThat(comp, CoreMatchers.instanceOf(SimpleLeakAwareCompositeByteBuf.class)); + + ByteBuf inner = comp.alloc().directBuffer(1).writeByte(0); + comp.addComponent(true, inner); + buf.addComponent(true, comp); + + assertEquals(-1, buf.forEachByte(new ByteProcessor() { + @Override + public boolean process(byte value) { + return true; + } + })); + assertTrue(buf.release()); + } + protected final void assertWrapped(ByteBuf buf) { try { assertSame(clazz, buf.getClass()); diff --git a/buffer/src/test/java/io/netty/buffer/SlicedByteBufTest.java b/buffer/src/test/java/io/netty/buffer/SlicedByteBufTest.java index 4079571..11e9f68 100644 --- a/buffer/src/test/java/io/netty/buffer/SlicedByteBufTest.java +++ b/buffer/src/test/java/io/netty/buffer/SlicedByteBufTest.java @@ -47,6 +47,13 @@ public class SlicedByteBufTest extends AbstractByteBufTest { return buffer.slice(offset, length); } + @Test + public void testIsContiguous() { + ByteBuf buf = newBuffer(4); + assertEquals(buf.unwrap().isContiguous(), buf.isContiguous()); + buf.release(); + } + @Test(expected = NullPointerException.class) public void shouldNotAllowNullInConstructor() { new SlicedByteBuf(null, 0, 0); diff --git a/codec-dns/pom.xml b/codec-dns/pom.xml index 41933ec..b884309 100644 --- a/codec-dns/pom.xml +++ b/codec-dns/pom.xml @@ -20,7 +20,7 @@ <parent> <groupId>io.netty</groupId> <artifactId>netty-parent</artifactId> - <version>4.1.33.Final</version> + <version>4.1.48.Final</version> </parent> <artifactId>netty-codec-dns</artifactId> diff --git a/codec-dns/src/main/java/io/netty/handler/codec/dns/AbstractDnsRecord.java b/codec-dns/src/main/java/io/netty/handler/codec/dns/AbstractDnsRecord.java index 28b92c2..6802d39 100644 --- a/codec-dns/src/main/java/io/netty/handler/codec/dns/AbstractDnsRecord.java +++ b/codec-dns/src/main/java/io/netty/handler/codec/dns/AbstractDnsRecord.java @@ -15,12 +15,14 @@ */ package io.netty.handler.codec.dns; +import io.netty.util.internal.PlatformDependent; import io.netty.util.internal.StringUtil; import io.netty.util.internal.UnstableApi; import java.net.IDN; import static io.netty.util.internal.ObjectUtil.checkNotNull; +import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero; /** * A skeletal implementation of {@link DnsRecord}. @@ -62,19 +64,28 @@ public abstract class AbstractDnsRecord implements DnsRecord { * @param timeToLive the TTL value of the record */ protected AbstractDnsRecord(String name, DnsRecordType type, int dnsClass, long timeToLive) { - if (timeToLive < 0) { - throw new IllegalArgumentException("timeToLive: " + timeToLive + " (expected: >= 0)"); - } + checkPositiveOrZero(timeToLive, "timeToLive"); // Convert to ASCII which will also check that the length is not too big. // See: // - https://github.com/netty/netty/issues/4937 // - https://github.com/netty/netty/issues/4935 - this.name = appendTrailingDot(IDN.toASCII(checkNotNull(name, "name"))); + this.name = appendTrailingDot(IDNtoASCII(name)); this.type = checkNotNull(type, "type"); this.dnsClass = (short) dnsClass; this.timeToLive = timeToLive; } + private static String IDNtoASCII(String name) { + checkNotNull(name, "name"); + if (PlatformDependent.isAndroid() && DefaultDnsRecordDecoder.ROOT.equals(name)) { + // Prior Android 10 there was a bug that did not correctly parse ".". + // + // See https://github.com/netty/netty/issues/10034 + return name; + } + return IDN.toASCII(name); + } + private static String appendTrailingDot(String name) { if (name.length() > 0 && name.charAt(name.length() - 1) != '.') { return name + '.'; diff --git a/codec-dns/src/main/java/io/netty/handler/codec/dns/DatagramDnsQueryEncoder.java b/codec-dns/src/main/java/io/netty/handler/codec/dns/DatagramDnsQueryEncoder.java index dbb562f..695a9c9 100644 --- a/codec-dns/src/main/java/io/netty/handler/codec/dns/DatagramDnsQueryEncoder.java +++ b/codec-dns/src/main/java/io/netty/handler/codec/dns/DatagramDnsQueryEncoder.java @@ -26,8 +26,6 @@ import io.netty.util.internal.UnstableApi; import java.net.InetSocketAddress; import java.util.List; -import static io.netty.util.internal.ObjectUtil.checkNotNull; - /** * Encodes a {@link DatagramDnsQuery} (or an {@link AddressedEnvelope} of {@link DnsQuery}} into a * {@link DatagramPacket}. @@ -36,7 +34,7 @@ import static io.netty.util.internal.ObjectUtil.checkNotNull; @ChannelHandler.Sharable public class DatagramDnsQueryEncoder extends MessageToMessageEncoder<AddressedEnvelope<DnsQuery, InetSocketAddress>> { - private final DnsRecordEncoder recordEncoder; + private final DnsQueryEncoder encoder; /** * Creates a new encoder with {@linkplain DnsRecordEncoder#DEFAULT the default record encoder}. @@ -49,7 +47,7 @@ public class DatagramDnsQueryEncoder extends MessageToMessageEncoder<AddressedEn * Creates a new encoder with the specified {@code recordEncoder}. */ public DatagramDnsQueryEncoder(DnsRecordEncoder recordEncoder) { - this.recordEncoder = checkNotNull(recordEncoder, "recordEncoder"); + this.encoder = new DnsQueryEncoder(recordEncoder); } @Override @@ -63,9 +61,7 @@ public class DatagramDnsQueryEncoder extends MessageToMessageEncoder<AddressedEn boolean success = false; try { - encodeHeader(query, buf); - encodeQuestions(query, buf); - encodeRecords(query, DnsSection.ADDITIONAL, buf); + encoder.encode(query, buf); success = true; } finally { if (!success) { @@ -85,38 +81,4 @@ public class DatagramDnsQueryEncoder extends MessageToMessageEncoder<AddressedEn @SuppressWarnings("unused") AddressedEnvelope<DnsQuery, InetSocketAddress> msg) throws Exception { return ctx.alloc().ioBuffer(1024); } - - /** - * Encodes the header that is always 12 bytes long. - * - * @param query the query header being encoded - * @param buf the buffer the encoded data should be written to - */ - private static void encodeHeader(DnsQuery query, ByteBuf buf) { - buf.writeShort(query.id()); - int flags = 0; - flags |= (query.opCode().byteValue() & 0xFF) << 14; - if (query.isRecursionDesired()) { - flags |= 1 << 8; - } - buf.writeShort(flags); - buf.writeShort(query.count(DnsSection.QUESTION)); - buf.writeShort(0); // answerCount - buf.writeShort(0); // authorityResourceCount - buf.writeShort(query.count(DnsSection.ADDITIONAL)); - } - - private void encodeQuestions(DnsQuery query, ByteBuf buf) throws Exception { - final int count = query.count(DnsSection.QUESTION); - for (int i = 0; i < count; i++) { - recordEncoder.encodeQuestion((DnsQuestion) query.recordAt(DnsSection.QUESTION, i), buf); - } - } - - private void encodeRecords(DnsQuery query, DnsSection section, ByteBuf buf) throws Exception { - final int count = query.count(section); - for (int i = 0; i < count; i++) { - recordEncoder.encodeRecord(query.recordAt(section, i), buf); - } - } } diff --git a/codec-dns/src/main/java/io/netty/handler/codec/dns/DatagramDnsResponseDecoder.java b/codec-dns/src/main/java/io/netty/handler/codec/dns/DatagramDnsResponseDecoder.java index 547d5ae..b9c4a1c 100644 --- a/codec-dns/src/main/java/io/netty/handler/codec/dns/DatagramDnsResponseDecoder.java +++ b/codec-dns/src/main/java/io/netty/handler/codec/dns/DatagramDnsResponseDecoder.java @@ -15,18 +15,15 @@ */ package io.netty.handler.codec.dns; -import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.socket.DatagramPacket; -import io.netty.handler.codec.CorruptedFrameException; import io.netty.handler.codec.MessageToMessageDecoder; import io.netty.util.internal.UnstableApi; +import java.net.InetSocketAddress; import java.util.List; -import static io.netty.util.internal.ObjectUtil.checkNotNull; - /** * Decodes a {@link DatagramPacket} into a {@link DatagramDnsResponse}. */ @@ -34,7 +31,7 @@ import static io.netty.util.internal.ObjectUtil.checkNotNull; @ChannelHandler.Sharable public class DatagramDnsResponseDecoder extends MessageToMessageDecoder<DatagramPacket> { - private final DnsRecordDecoder recordDecoder; + private final DnsResponseDecoder<InetSocketAddress> responseDecoder; /** * Creates a new decoder with {@linkplain DnsRecordDecoder#DEFAULT the default record decoder}. @@ -47,73 +44,21 @@ public class DatagramDnsResponseDecoder extends MessageToMessageDecoder<Datagram * Creates a new decoder with the specified {@code recordDecoder}. */ public DatagramDnsResponseDecoder(DnsRecordDecoder recordDecoder) { - this.recordDecoder = checkNotNull(recordDecoder, "recordDecoder"); + this.responseDecoder = new DnsResponseDecoder<InetSocketAddress>(recordDecoder) { + @Override + protected DnsResponse newResponse(InetSocketAddress sender, InetSocketAddress recipient, + int id, DnsOpCode opCode, DnsResponseCode responseCode) { + return new DatagramDnsResponse(sender, recipient, id, opCode, responseCode); + } + }; } @Override protected void decode(ChannelHandlerContext ctx, DatagramPacket packet, List<Object> out) throws Exception { - final ByteBuf buf = packet.content(); - - final DnsResponse response = newResponse(packet, buf); - boolean success = false; - try { - final int questionCount = buf.readUnsignedShort(); - final int answerCount = buf.readUnsignedShort(); - final int authorityRecordCount = buf.readUnsignedShort(); - final int additionalRecordCount = buf.readUnsignedShort(); - - decodeQuestions(response, buf, questionCount); - decodeRecords(response, DnsSection.ANSWER, buf, answerCount); - decodeRecords(response, DnsSection.AUTHORITY, buf, authorityRecordCount); - decodeRecords(response, DnsSection.ADDITIONAL, buf, additionalRecordCount); - - out.add(response); - success = true; - } finally { - if (!success) { - response.release(); - } - } + out.add(decodeResponse(ctx, packet)); } - private static DnsResponse newResponse(DatagramPacket packet, ByteBuf buf) { - final int id = buf.readUnsignedShort(); - - final int flags = buf.readUnsignedShort(); - if (flags >> 15 == 0) { - throw new CorruptedFrameException("not a response"); - } - - final DnsResponse response = new DatagramDnsResponse( - packet.sender(), - packet.recipient(), - id, - DnsOpCode.valueOf((byte) (flags >> 11 & 0xf)), DnsResponseCode.valueOf((byte) (flags & 0xf))); - - response.setRecursionDesired((flags >> 8 & 1) == 1); - response.setAuthoritativeAnswer((flags >> 10 & 1) == 1); - response.setTruncated((flags >> 9 & 1) == 1); - response.setRecursionAvailable((flags >> 7 & 1) == 1); - response.setZ(flags >> 4 & 0x7); - return response; - } - - private void decodeQuestions(DnsResponse response, ByteBuf buf, int questionCount) throws Exception { - for (int i = questionCount; i > 0; i --) { - response.addRecord(DnsSection.QUESTION, recordDecoder.decodeQuestion(buf)); - } - } - - private void decodeRecords( - DnsResponse response, DnsSection section, ByteBuf buf, int count) throws Exception { - for (int i = count; i > 0; i --) { - final DnsRecord r = recordDecoder.decodeRecord(buf); - if (r == null) { - // Truncated response - break; - } - - response.addRecord(section, r); - } + protected DnsResponse decodeResponse(ChannelHandlerContext ctx, DatagramPacket packet) throws Exception { + return responseDecoder.decode(packet.sender(), packet.recipient(), packet.content()); } } diff --git a/codec-dns/src/main/java/io/netty/handler/codec/dns/DefaultDnsRecordDecoder.java b/codec-dns/src/main/java/io/netty/handler/codec/dns/DefaultDnsRecordDecoder.java index b4e50ff..70df1c6 100644 --- a/codec-dns/src/main/java/io/netty/handler/codec/dns/DefaultDnsRecordDecoder.java +++ b/codec-dns/src/main/java/io/netty/handler/codec/dns/DefaultDnsRecordDecoder.java @@ -16,8 +16,6 @@ package io.netty.handler.codec.dns; import io.netty.buffer.ByteBuf; -import io.netty.handler.codec.CorruptedFrameException; -import io.netty.util.CharsetUtil; import io.netty.util.internal.UnstableApi; /** @@ -49,7 +47,7 @@ public class DefaultDnsRecordDecoder implements DnsRecordDecoder { final String name = decodeName(in); final int endOffset = in.writerIndex(); - if (endOffset - startOffset < 10) { + if (endOffset - in.readerIndex() < 10) { // Not enough data in.readerIndex(startOffset); return null; @@ -98,6 +96,11 @@ public class DefaultDnsRecordDecoder implements DnsRecordDecoder { return new DefaultDnsPtrRecord( name, dnsClass, timeToLive, decodeName0(in.duplicate().setIndex(offset, offset + length))); } + if (type == DnsRecordType.CNAME || type == DnsRecordType.NS) { + return new DefaultDnsRawRecord(name, type, dnsClass, timeToLive, + DnsCodecUtil.decompressDomainName( + in.duplicate().setIndex(offset, offset + length))); + } return new DefaultDnsRawRecord( name, type, dnsClass, timeToLive, in.retainedDuplicate().setIndex(offset, offset + length)); } @@ -123,69 +126,6 @@ public class DefaultDnsRecordDecoder implements DnsRecordDecoder { * @return the domain name for an entry */ public static String decodeName(ByteBuf in) { - int position = -1; - int checked = 0; - final int end = in.writerIndex(); - final int readable = in.readableBytes(); - - // Looking at the spec we should always have at least enough readable bytes to read a byte here but it seems - // some servers do not respect this for empty names. So just workaround this and return an empty name in this - // case. - // - // See: - // - https://github.com/netty/netty/issues/5014 - // - https://www.ietf.org/rfc/rfc1035.txt , Section 3.1 - if (readable == 0) { - return ROOT; - } - - final StringBuilder name = new StringBuilder(readable << 1); - while (in.isReadable()) { - final int len = in.readUnsignedByte(); - final boolean pointer = (len & 0xc0) == 0xc0; - if (pointer) { - if (position == -1) { - position = in.readerIndex() + 1; - } - - if (!in.isReadable()) { - throw new CorruptedFrameException("truncated pointer in a name"); - } - - final int next = (len & 0x3f) << 8 | in.readUnsignedByte(); - if (next >= end) { - throw new CorruptedFrameException("name has an out-of-range pointer"); - } - in.readerIndex(next); - - // check for loops - checked += 2; - if (checked >= end) { - throw new CorruptedFrameException("name contains a loop."); - } - } else if (len != 0) { - if (!in.isReadable(len)) { - throw new CorruptedFrameException("truncated label in a name"); - } - name.append(in.toString(in.readerIndex(), len, CharsetUtil.UTF_8)).append('.'); - in.skipBytes(len); - } else { // len == 0 - break; - } - } - - if (position != -1) { - in.readerIndex(position); - } - - if (name.length() == 0) { - return ROOT; - } - - if (name.charAt(name.length() - 1) != '.') { - name.append('.'); - } - - return name.toString(); + return DnsCodecUtil.decodeDomainName(in); } } diff --git a/codec-dns/src/main/java/io/netty/handler/codec/dns/DefaultDnsRecordEncoder.java b/codec-dns/src/main/java/io/netty/handler/codec/dns/DefaultDnsRecordEncoder.java index 48f60bc..45914ea 100644 --- a/codec-dns/src/main/java/io/netty/handler/codec/dns/DefaultDnsRecordEncoder.java +++ b/codec-dns/src/main/java/io/netty/handler/codec/dns/DefaultDnsRecordEncoder.java @@ -16,14 +16,11 @@ package io.netty.handler.codec.dns; import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufUtil; import io.netty.channel.socket.InternetProtocolFamily; import io.netty.handler.codec.UnsupportedMessageTypeException; import io.netty.util.internal.StringUtil; import io.netty.util.internal.UnstableApi; -import static io.netty.handler.codec.dns.DefaultDnsRecordDecoder.ROOT; - /** * The default {@link DnsRecordEncoder} implementation. * @@ -141,25 +138,7 @@ public class DefaultDnsRecordEncoder implements DnsRecordEncoder { } protected void encodeName(String name, ByteBuf buf) throws Exception { - if (ROOT.equals(name)) { - // Root domain - buf.writeByte(0); - return; - } - - final String[] labels = name.split("\\."); - for (String label : labels) { - final int labelLen = label.length(); - if (labelLen == 0) { - // zero-length label means the end of the name. - break; - } - - buf.writeByte(labelLen); - ByteBufUtil.writeAscii(buf, label); - } - - buf.writeByte(0); // marks end of name field + DnsCodecUtil.encodeDomainName(name, buf); } private static byte padWithZeros(byte b, int lowOrderBitsToPreserve) { diff --git a/codec-dns/src/main/java/io/netty/handler/codec/dns/DnsCodecUtil.java b/codec-dns/src/main/java/io/netty/handler/codec/dns/DnsCodecUtil.java new file mode 100644 index 0000000..8804cf7 --- /dev/null +++ b/codec-dns/src/main/java/io/netty/handler/codec/dns/DnsCodecUtil.java @@ -0,0 +1,132 @@ +/* + * Copyright 2019 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.codec.dns; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.Unpooled; +import io.netty.handler.codec.CorruptedFrameException; +import io.netty.util.CharsetUtil; + +import static io.netty.handler.codec.dns.DefaultDnsRecordDecoder.*; + +final class DnsCodecUtil { + private DnsCodecUtil() { + // Util class + } + + static void encodeDomainName(String name, ByteBuf buf) { + if (ROOT.equals(name)) { + // Root domain + buf.writeByte(0); + return; + } + + final String[] labels = name.split("\\."); + for (String label : labels) { + final int labelLen = label.length(); + if (labelLen == 0) { + // zero-length label means the end of the name. + break; + } + + buf.writeByte(labelLen); + ByteBufUtil.writeAscii(buf, label); + } + + buf.writeByte(0); // marks end of name field + } + + static String decodeDomainName(ByteBuf in) { + int position = -1; + int checked = 0; + final int end = in.writerIndex(); + final int readable = in.readableBytes(); + + // Looking at the spec we should always have at least enough readable bytes to read a byte here but it seems + // some servers do not respect this for empty names. So just workaround this and return an empty name in this + // case. + // + // See: + // - https://github.com/netty/netty/issues/5014 + // - https://www.ietf.org/rfc/rfc1035.txt , Section 3.1 + if (readable == 0) { + return ROOT; + } + + final StringBuilder name = new StringBuilder(readable << 1); + while (in.isReadable()) { + final int len = in.readUnsignedByte(); + final boolean pointer = (len & 0xc0) == 0xc0; + if (pointer) { + if (position == -1) { + position = in.readerIndex() + 1; + } + + if (!in.isReadable()) { + throw new CorruptedFrameException("truncated pointer in a name"); + } + + final int next = (len & 0x3f) << 8 | in.readUnsignedByte(); + if (next >= end) { + throw new CorruptedFrameException("name has an out-of-range pointer"); + } + in.readerIndex(next); + + // check for loops + checked += 2; + if (checked >= end) { + throw new CorruptedFrameException("name contains a loop."); + } + } else if (len != 0) { + if (!in.isReadable(len)) { + throw new CorruptedFrameException("truncated label in a name"); + } + name.append(in.toString(in.readerIndex(), len, CharsetUtil.UTF_8)).append('.'); + in.skipBytes(len); + } else { // len == 0 + break; + } + } + + if (position != -1) { + in.readerIndex(position); + } + + if (name.length() == 0) { + return ROOT; + } + + if (name.charAt(name.length() - 1) != '.') { + name.append('.'); + } + + return name.toString(); + } + + /** + * Decompress pointer data. + * @param compression comporession data + * @return decompressed data + */ + static ByteBuf decompressDomainName(ByteBuf compression) { + String domainName = decodeDomainName(compression); + ByteBuf result = compression.alloc().buffer(domainName.length() << 1); + encodeDomainName(domainName, result); + return result; + } +} diff --git a/codec-dns/src/main/java/io/netty/handler/codec/dns/DnsQueryEncoder.java b/codec-dns/src/main/java/io/netty/handler/codec/dns/DnsQueryEncoder.java new file mode 100644 index 0000000..db2a730 --- /dev/null +++ b/codec-dns/src/main/java/io/netty/handler/codec/dns/DnsQueryEncoder.java @@ -0,0 +1,75 @@ +/* + * Copyright 2019 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.codec.dns; + +import io.netty.buffer.ByteBuf; + +import static io.netty.util.internal.ObjectUtil.checkNotNull; + +final class DnsQueryEncoder { + + private final DnsRecordEncoder recordEncoder; + + /** + * Creates a new encoder with the specified {@code recordEncoder}. + */ + DnsQueryEncoder(DnsRecordEncoder recordEncoder) { + this.recordEncoder = checkNotNull(recordEncoder, "recordEncoder"); + } + + /** + * Encodes the given {@link DnsQuery} into a {@link ByteBuf}. + */ + void encode(DnsQuery query, ByteBuf out) throws Exception { + encodeHeader(query, out); + encodeQuestions(query, out); + encodeRecords(query, DnsSection.ADDITIONAL, out); + } + + /** + * Encodes the header that is always 12 bytes long. + * + * @param query the query header being encoded + * @param buf the buffer the encoded data should be written to + */ + private static void encodeHeader(DnsQuery query, ByteBuf buf) { + buf.writeShort(query.id()); + int flags = 0; + flags |= (query.opCode().byteValue() & 0xFF) << 14; + if (query.isRecursionDesired()) { + flags |= 1 << 8; + } + buf.writeShort(flags); + buf.writeShort(query.count(DnsSection.QUESTION)); + buf.writeShort(0); // answerCount + buf.writeShort(0); // authorityResourceCount + buf.writeShort(query.count(DnsSection.ADDITIONAL)); + } + + private void encodeQuestions(DnsQuery query, ByteBuf buf) throws Exception { + final int count = query.count(DnsSection.QUESTION); + for (int i = 0; i < count; i++) { + recordEncoder.encodeQuestion((DnsQuestion) query.recordAt(DnsSection.QUESTION, i), buf); + } + } + + private void encodeRecords(DnsQuery query, DnsSection section, ByteBuf buf) throws Exception { + final int count = query.count(section); + for (int i = 0; i < count; i++) { + recordEncoder.encodeRecord(query.recordAt(section, i), buf); + } + } +} diff --git a/codec-dns/src/main/java/io/netty/handler/codec/dns/DnsResponseDecoder.java b/codec-dns/src/main/java/io/netty/handler/codec/dns/DnsResponseDecoder.java new file mode 100644 index 0000000..aadf3c2 --- /dev/null +++ b/codec-dns/src/main/java/io/netty/handler/codec/dns/DnsResponseDecoder.java @@ -0,0 +1,105 @@ +/* + * Copyright 2019 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.codec.dns; + +import io.netty.buffer.ByteBuf; +import io.netty.handler.codec.CorruptedFrameException; + +import java.net.SocketAddress; + +import static io.netty.util.internal.ObjectUtil.checkNotNull; + +abstract class DnsResponseDecoder<A extends SocketAddress> { + + private final DnsRecordDecoder recordDecoder; + + /** + * Creates a new decoder with the specified {@code recordDecoder}. + */ + DnsResponseDecoder(DnsRecordDecoder recordDecoder) { + this.recordDecoder = checkNotNull(recordDecoder, "recordDecoder"); + } + + final DnsResponse decode(A sender, A recipient, ByteBuf buffer) throws Exception { + final int id = buffer.readUnsignedShort(); + + final int flags = buffer.readUnsignedShort(); + if (flags >> 15 == 0) { + throw new CorruptedFrameException("not a response"); + } + + final DnsResponse response = newResponse( + sender, + recipient, + id, + DnsOpCode.valueOf((byte) (flags >> 11 & 0xf)), DnsResponseCode.valueOf((byte) (flags & 0xf))); + + response.setRecursionDesired((flags >> 8 & 1) == 1); + response.setAuthoritativeAnswer((flags >> 10 & 1) == 1); + response.setTruncated((flags >> 9 & 1) == 1); + response.setRecursionAvailable((flags >> 7 & 1) == 1); + response.setZ(flags >> 4 & 0x7); + + boolean success = false; + try { + final int questionCount = buffer.readUnsignedShort(); + final int answerCount = buffer.readUnsignedShort(); + final int authorityRecordCount = buffer.readUnsignedShort(); + final int additionalRecordCount = buffer.readUnsignedShort(); + + decodeQuestions(response, buffer, questionCount); + if (!decodeRecords(response, DnsSection.ANSWER, buffer, answerCount)) { + success = true; + return response; + } + if (!decodeRecords(response, DnsSection.AUTHORITY, buffer, authorityRecordCount)) { + success = true; + return response; + } + + decodeRecords(response, DnsSection.ADDITIONAL, buffer, additionalRecordCount); + success = true; + return response; + } finally { + if (!success) { + response.release(); + } + } + } + + protected abstract DnsResponse newResponse(A sender, A recipient, int id, + DnsOpCode opCode, DnsResponseCode responseCode) throws Exception; + + private void decodeQuestions(DnsResponse response, ByteBuf buf, int questionCount) throws Exception { + for (int i = questionCount; i > 0; i --) { + response.addRecord(DnsSection.QUESTION, recordDecoder.decodeQuestion(buf)); + } + } + + private boolean decodeRecords( + DnsResponse response, DnsSection section, ByteBuf buf, int count) throws Exception { + for (int i = count; i > 0; i --) { + final DnsRecord r = recordDecoder.decodeRecord(buf); + if (r == null) { + // Truncated response + return false; + } + + response.addRecord(section, r); + } + return true; + } +} diff --git a/codec-dns/src/main/java/io/netty/handler/codec/dns/TcpDnsQueryEncoder.java b/codec-dns/src/main/java/io/netty/handler/codec/dns/TcpDnsQueryEncoder.java new file mode 100644 index 0000000..2978fff --- /dev/null +++ b/codec-dns/src/main/java/io/netty/handler/codec/dns/TcpDnsQueryEncoder.java @@ -0,0 +1,64 @@ +/* + * Copyright 2019 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.codec.dns; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToByteEncoder; +import io.netty.util.internal.UnstableApi; + +@ChannelHandler.Sharable +@UnstableApi +public final class TcpDnsQueryEncoder extends MessageToByteEncoder<DnsQuery> { + + private final DnsQueryEncoder encoder; + + /** + * Creates a new encoder with {@linkplain DnsRecordEncoder#DEFAULT the default record encoder}. + */ + public TcpDnsQueryEncoder() { + this(DnsRecordEncoder.DEFAULT); + } + + /** + * Creates a new encoder with the specified {@code recordEncoder}. + */ + public TcpDnsQueryEncoder(DnsRecordEncoder recordEncoder) { + this.encoder = new DnsQueryEncoder(recordEncoder); + } + + @Override + protected void encode(ChannelHandlerContext ctx, DnsQuery msg, ByteBuf out) throws Exception { + // Length is two octets as defined by RFC-7766 + // See https://tools.ietf.org/html/rfc7766#section-8 + out.writerIndex(out.writerIndex() + 2); + encoder.encode(msg, out); + + // Now fill in the correct length based on the amount of data that we wrote the ByteBuf. + out.setShort(0, out.readableBytes() - 2); + } + + @Override + protected ByteBuf allocateBuffer(ChannelHandlerContext ctx, @SuppressWarnings("unused") DnsQuery msg, + boolean preferDirect) { + if (preferDirect) { + return ctx.alloc().ioBuffer(1024); + } else { + return ctx.alloc().heapBuffer(1024); + } + } +} diff --git a/codec-dns/src/main/java/io/netty/handler/codec/dns/TcpDnsResponseDecoder.java b/codec-dns/src/main/java/io/netty/handler/codec/dns/TcpDnsResponseDecoder.java new file mode 100644 index 0000000..f8a92d9 --- /dev/null +++ b/codec-dns/src/main/java/io/netty/handler/codec/dns/TcpDnsResponseDecoder.java @@ -0,0 +1,72 @@ +/* + * Copyright 2019 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.codec.dns; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.LengthFieldBasedFrameDecoder; +import io.netty.util.internal.UnstableApi; + +import java.net.SocketAddress; + +@UnstableApi +public final class TcpDnsResponseDecoder extends LengthFieldBasedFrameDecoder { + + private final DnsResponseDecoder<SocketAddress> responseDecoder; + + /** + * Creates a new decoder with {@linkplain DnsRecordDecoder#DEFAULT the default record decoder}. + */ + public TcpDnsResponseDecoder() { + this(DnsRecordDecoder.DEFAULT, 64 * 1024); + } + + /** + * Creates a new decoder with the specified {@code recordDecoder} and {@code maxFrameLength} + */ + public TcpDnsResponseDecoder(DnsRecordDecoder recordDecoder, int maxFrameLength) { + // Length is two octets as defined by RFC-7766 + // See https://tools.ietf.org/html/rfc7766#section-8 + super(maxFrameLength, 0, 2, 0, 2); + + this.responseDecoder = new DnsResponseDecoder<SocketAddress>(recordDecoder) { + @Override + protected DnsResponse newResponse(SocketAddress sender, SocketAddress recipient, + int id, DnsOpCode opCode, DnsResponseCode responseCode) { + return new DefaultDnsResponse(id, opCode, responseCode); + } + }; + } + + @Override + protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception { + ByteBuf frame = (ByteBuf) super.decode(ctx, in); + if (frame == null) { + return null; + } + + try { + return responseDecoder.decode(ctx.channel().remoteAddress(), ctx.channel().localAddress(), frame.slice()); + } finally { + frame.release(); + } + } + + @Override + protected ByteBuf extractFrame(ChannelHandlerContext ctx, ByteBuf buffer, int index, int length) { + return buffer.copy(index, length); + } +} diff --git a/codec-dns/src/test/java/io/netty/handler/codec/dns/DefaultDnsRecordDecoderTest.java b/codec-dns/src/test/java/io/netty/handler/codec/dns/DefaultDnsRecordDecoderTest.java index 6de6ce5..b18c19e 100644 --- a/codec-dns/src/test/java/io/netty/handler/codec/dns/DefaultDnsRecordDecoderTest.java +++ b/codec-dns/src/test/java/io/netty/handler/codec/dns/DefaultDnsRecordDecoderTest.java @@ -16,10 +16,11 @@ package io.netty.handler.codec.dns; import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; import io.netty.buffer.Unpooled; import org.junit.Test; -import static org.junit.Assert.assertEquals; +import static org.junit.Assert.*; public class DefaultDnsRecordDecoderTest { @@ -89,6 +90,81 @@ public class DefaultDnsRecordDecoderTest { } } + @Test + public void testdecompressCompressPointer() { + byte[] compressionPointer = { + 5, 'n', 'e', 't', 't', 'y', 2, 'i', 'o', 0, + (byte) 0xC0, 0 + }; + ByteBuf buffer = Unpooled.wrappedBuffer(compressionPointer); + ByteBuf uncompressed = null; + try { + uncompressed = DnsCodecUtil.decompressDomainName(buffer.duplicate().setIndex(10, 12)); + assertEquals(0, ByteBufUtil.compare(buffer.duplicate().setIndex(0, 10), uncompressed)); + } finally { + buffer.release(); + if (uncompressed != null) { + uncompressed.release(); + } + } + } + + @Test + public void testdecompressNestedCompressionPointer() { + byte[] nestedCompressionPointer = { + 6, 'g', 'i', 't', 'h', 'u', 'b', 2, 'i', 'o', 0, // github.io + 5, 'n', 'e', 't', 't', 'y', (byte) 0xC0, 0, // netty.github.io + (byte) 0xC0, 11, // netty.github.io + }; + ByteBuf buffer = Unpooled.wrappedBuffer(nestedCompressionPointer); + ByteBuf uncompressed = null; + try { + uncompressed = DnsCodecUtil.decompressDomainName(buffer.duplicate().setIndex(19, 21)); + assertEquals(0, ByteBufUtil.compare( + Unpooled.wrappedBuffer(new byte[] { + 5, 'n', 'e', 't', 't', 'y', 6, 'g', 'i', 't', 'h', 'u', 'b', 2, 'i', 'o', 0 + }), uncompressed)); + } finally { + buffer.release(); + if (uncompressed != null) { + uncompressed.release(); + } + } + } + + @Test + public void testDecodeCompressionRDataPointer() throws Exception { + DefaultDnsRecordDecoder decoder = new DefaultDnsRecordDecoder(); + byte[] compressionPointer = { + 5, 'n', 'e', 't', 't', 'y', 2, 'i', 'o', 0, + (byte) 0xC0, 0 + }; + ByteBuf buffer = Unpooled.wrappedBuffer(compressionPointer); + DefaultDnsRawRecord cnameRecord = null; + DefaultDnsRawRecord nsRecord = null; + try { + cnameRecord = (DefaultDnsRawRecord) decoder.decodeRecord( + "netty.github.io", DnsRecordType.CNAME, DnsRecord.CLASS_IN, 60, buffer, 10, 2); + assertEquals("The rdata of CNAME-type record should be decompressed in advance", + 0, ByteBufUtil.compare(buffer.duplicate().setIndex(0, 10), cnameRecord.content())); + assertEquals("netty.io.", DnsCodecUtil.decodeDomainName(cnameRecord.content())); + nsRecord = (DefaultDnsRawRecord) decoder.decodeRecord( + "netty.github.io", DnsRecordType.NS, DnsRecord.CLASS_IN, 60, buffer, 10, 2); + assertEquals("The rdata of NS-type record should be decompressed in advance", + 0, ByteBufUtil.compare(buffer.duplicate().setIndex(0, 10), nsRecord.content())); + assertEquals("netty.io.", DnsCodecUtil.decodeDomainName(nsRecord.content())); + } finally { + buffer.release(); + if (cnameRecord != null) { + cnameRecord.release(); + } + + if (nsRecord != null) { + nsRecord.release(); + } + } + } + @Test public void testDecodeMessageCompression() throws Exception { // See https://www.ietf.org/rfc/rfc1035 [4.1.4. Message compression] @@ -156,4 +232,24 @@ public class DefaultDnsRecordDecoderTest { buffer.release(); } } + + @Test + public void testTruncatedPacket() throws Exception { + ByteBuf buffer = Unpooled.buffer(); + buffer.writeByte(0); + buffer.writeShort(DnsRecordType.A.intValue()); + buffer.writeShort(1); + buffer.writeInt(32); + + // Write a truncated last value. + buffer.writeByte(0); + DefaultDnsRecordDecoder decoder = new DefaultDnsRecordDecoder(); + try { + int readerIndex = buffer.readerIndex(); + assertNull(decoder.decodeRecord(buffer)); + assertEquals(readerIndex, buffer.readerIndex()); + } finally { + buffer.release(); + } + } } diff --git a/codec-haproxy/pom.xml b/codec-haproxy/pom.xml index 9e5f5f7..6d03c04 100644 --- a/codec-haproxy/pom.xml +++ b/codec-haproxy/pom.xml @@ -20,7 +20,7 @@ <parent> <groupId>io.netty</groupId> <artifactId>netty-parent</artifactId> - <version>4.1.33.Final</version> + <version>4.1.48.Final</version> </parent> <artifactId>netty-codec-haproxy</artifactId> diff --git a/codec-haproxy/src/main/java/io/netty/handler/codec/haproxy/HAProxyMessage.java b/codec-haproxy/src/main/java/io/netty/handler/codec/haproxy/HAProxyMessage.java index b40bf42..1777e67 100644 --- a/codec-haproxy/src/main/java/io/netty/handler/codec/haproxy/HAProxyMessage.java +++ b/codec-haproxy/src/main/java/io/netty/handler/codec/haproxy/HAProxyMessage.java @@ -17,9 +17,14 @@ package io.netty.handler.codec.haproxy; import io.netty.buffer.ByteBuf; import io.netty.handler.codec.haproxy.HAProxyProxiedProtocol.AddressFamily; +import io.netty.util.AbstractReferenceCounted; import io.netty.util.ByteProcessor; import io.netty.util.CharsetUtil; import io.netty.util.NetUtil; +import io.netty.util.ResourceLeakDetector; +import io.netty.util.ResourceLeakDetectorFactory; +import io.netty.util.ResourceLeakTracker; +import io.netty.util.internal.ObjectUtil; import java.util.ArrayList; import java.util.Collections; @@ -28,29 +33,11 @@ import java.util.List; /** * Message container for decoded HAProxy proxy protocol parameters */ -public final class HAProxyMessage { - - /** - * Version 1 proxy protocol message for 'UNKNOWN' proxied protocols. Per spec, when the proxied protocol is - * 'UNKNOWN' we must discard all other header values. - */ - private static final HAProxyMessage V1_UNKNOWN_MSG = new HAProxyMessage( - HAProxyProtocolVersion.V1, HAProxyCommand.PROXY, HAProxyProxiedProtocol.UNKNOWN, null, null, 0, 0); - - /** - * Version 2 proxy protocol message for 'UNKNOWN' proxied protocols. Per spec, when the proxied protocol is - * 'UNKNOWN' we must discard all other header values. - */ - private static final HAProxyMessage V2_UNKNOWN_MSG = new HAProxyMessage( - HAProxyProtocolVersion.V2, HAProxyCommand.PROXY, HAProxyProxiedProtocol.UNKNOWN, null, null, 0, 0); - - /** - * Version 2 proxy protocol message for local requests. Per spec, we should use an unspecified protocol and family - * for 'LOCAL' commands. Per spec, when the proxied protocol is 'UNKNOWN' we must discard all other header values. - */ - private static final HAProxyMessage V2_LOCAL_MSG = new HAProxyMessage( - HAProxyProtocolVersion.V2, HAProxyCommand.LOCAL, HAProxyProxiedProtocol.UNKNOWN, null, null, 0, 0); +public final class HAProxyMessage extends AbstractReferenceCounted { + private static final ResourceLeakDetector<HAProxyMessage> leakDetector = + ResourceLeakDetectorFactory.instance().newResourceLeakDetector(HAProxyMessage.class); + private final ResourceLeakTracker<HAProxyMessage> leak; private final HAProxyProtocolVersion protocolVersion; private final HAProxyCommand command; private final HAProxyProxiedProtocol proxiedProtocol; @@ -90,9 +77,7 @@ public final class HAProxyMessage { String sourceAddress, String destinationAddress, int sourcePort, int destinationPort, List<HAProxyTLV> tlvs) { - if (proxiedProtocol == null) { - throw new NullPointerException("proxiedProtocol"); - } + ObjectUtil.checkNotNull(proxiedProtocol, "proxiedProtocol"); AddressFamily addrFamily = proxiedProtocol.addressFamily(); checkAddress(sourceAddress, addrFamily); @@ -108,6 +93,8 @@ public final class HAProxyMessage { this.sourcePort = sourcePort; this.destinationPort = destinationPort; this.tlvs = Collections.unmodifiableList(tlvs); + + leak = leakDetector.track(this); } /** @@ -118,9 +105,7 @@ public final class HAProxyMessage { * @throws HAProxyProtocolException if any portion of the header is invalid */ static HAProxyMessage decodeHeader(ByteBuf header) { - if (header == null) { - throw new NullPointerException("header"); - } + ObjectUtil.checkNotNull(header, "header"); if (header.readableBytes() < 16) { throw new HAProxyProtocolException( @@ -150,7 +135,7 @@ public final class HAProxyMessage { } if (cmd == HAProxyCommand.LOCAL) { - return V2_LOCAL_MSG; + return unknownMsg(HAProxyProtocolVersion.V2, HAProxyCommand.LOCAL); } // Per spec, the 14th byte is the protocol and address family byte @@ -162,7 +147,7 @@ public final class HAProxyMessage { } if (protAndFam == HAProxyProxiedProtocol.UNKNOWN) { - return V2_UNKNOWN_MSG; + return unknownMsg(HAProxyProtocolVersion.V2, HAProxyCommand.PROXY); } int addressInfoLen = header.readUnsignedShort(); @@ -286,7 +271,7 @@ public final class HAProxyMessage { return new HAProxySSLTLV(verify, client, encapsulatedTlvs, rawContent); } return new HAProxySSLTLV(verify, client, Collections.<HAProxyTLV>emptyList(), rawContent); - // If we're not dealing with a SSL Type, we can use the same mechanism + // If we're not dealing with an SSL Type, we can use the same mechanism case PP2_TYPE_ALPN: case PP2_TYPE_AUTHORITY: case PP2_TYPE_SSL_VERSION: @@ -337,7 +322,7 @@ public final class HAProxyMessage { } if (protAndFam == HAProxyProxiedProtocol.UNKNOWN) { - return V1_UNKNOWN_MSG; + return unknownMsg(HAProxyProtocolVersion.V1, HAProxyCommand.PROXY); } if (numParts != 6) { @@ -349,6 +334,14 @@ public final class HAProxyMessage { protAndFam, parts[2], parts[3], parts[4], parts[5]); } + /** + * Proxy protocol message for 'UNKNOWN' proxied protocols. Per spec, when the proxied protocol is + * 'UNKNOWN' we must discard all other header values. + */ + private static HAProxyMessage unknownMsg(HAProxyProtocolVersion version, HAProxyCommand command) { + return new HAProxyMessage(version, command, HAProxyProxiedProtocol.UNKNOWN, null, null, 0, 0); + } + /** * Convert ip address bytes to string representation * @@ -358,31 +351,20 @@ public final class HAProxyMessage { */ private static String ipBytesToString(ByteBuf header, int addressLen) { StringBuilder sb = new StringBuilder(); - if (addressLen == 4) { - sb.append(header.readByte() & 0xff); - sb.append('.'); - sb.append(header.readByte() & 0xff); - sb.append('.'); - sb.append(header.readByte() & 0xff); - sb.append('.'); - sb.append(header.readByte() & 0xff); + final int ipv4Len = 4; + final int ipv6Len = 8; + if (addressLen == ipv4Len) { + for (int i = 0; i < ipv4Len; i++) { + sb.append(header.readByte() & 0xff); + sb.append('.'); + } } else { - sb.append(Integer.toHexString(header.readUnsignedShort())); - sb.append(':'); - sb.append(Integer.toHexString(header.readUnsignedShort())); - sb.append(':'); - sb.append(Integer.toHexString(header.readUnsignedShort())); - sb.append(':'); - sb.append(Integer.toHexString(header.readUnsignedShort())); - sb.append(':'); - sb.append(Integer.toHexString(header.readUnsignedShort())); - sb.append(':'); - sb.append(Integer.toHexString(header.readUnsignedShort())); - sb.append(':'); - sb.append(Integer.toHexString(header.readUnsignedShort())); - sb.append(':'); - sb.append(Integer.toHexString(header.readUnsignedShort())); + for (int i = 0; i < ipv6Len; i++) { + sb.append(Integer.toHexString(header.readUnsignedShort())); + sb.append(':'); + } } + sb.setLength(sb.length() - 1); return sb.toString(); } @@ -416,9 +398,7 @@ public final class HAProxyMessage { * @throws HAProxyProtocolException if the address is invalid */ private static void checkAddress(String address, AddressFamily addrFamily) { - if (addrFamily == null) { - throw new NullPointerException("addrFamily"); - } + ObjectUtil.checkNotNull(addrFamily, "addrFamily"); switch (addrFamily) { case AF_UNSPEC: @@ -430,9 +410,7 @@ public final class HAProxyMessage { return; } - if (address == null) { - throw new NullPointerException("address"); - } + ObjectUtil.checkNotNull(address, "address"); switch (addrFamily) { case AF_IPv4: @@ -519,4 +497,63 @@ public final class HAProxyMessage { public List<HAProxyTLV> tlvs() { return tlvs; } + + @Override + public HAProxyMessage touch() { + tryRecord(); + return (HAProxyMessage) super.touch(); + } + + @Override + public HAProxyMessage touch(Object hint) { + if (leak != null) { + leak.record(hint); + } + return this; + } + + @Override + public HAProxyMessage retain() { + tryRecord(); + return (HAProxyMessage) super.retain(); + } + + @Override + public HAProxyMessage retain(int increment) { + tryRecord(); + return (HAProxyMessage) super.retain(increment); + } + + @Override + public boolean release() { + tryRecord(); + return super.release(); + } + + @Override + public boolean release(int decrement) { + tryRecord(); + return super.release(decrement); + } + + private void tryRecord() { + if (leak != null) { + leak.record(); + } + } + + @Override + protected void deallocate() { + try { + for (HAProxyTLV tlv : tlvs) { + tlv.release(); + } + } finally { + final ResourceLeakTracker<HAProxyMessage> leak = this.leak; + if (leak != null) { + boolean closed = leak.close(this); + assert closed; + } + } + } } diff --git a/codec-haproxy/src/main/java/io/netty/handler/codec/haproxy/HAProxyMessageDecoder.java b/codec-haproxy/src/main/java/io/netty/handler/codec/haproxy/HAProxyMessageDecoder.java index df2a663..87bea65 100644 --- a/codec-haproxy/src/main/java/io/netty/handler/codec/haproxy/HAProxyMessageDecoder.java +++ b/codec-haproxy/src/main/java/io/netty/handler/codec/haproxy/HAProxyMessageDecoder.java @@ -18,7 +18,6 @@ package io.netty.handler.codec.haproxy; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.ByteToMessageDecoder; -import io.netty.handler.codec.LineBasedFrameDecoder; import io.netty.handler.codec.ProtocolDetectionResult; import io.netty.util.CharsetUtil; @@ -50,11 +49,6 @@ public class HAProxyMessageDecoder extends ByteToMessageDecoder { */ private static final int V2_MAX_TLV = 65535 - 216; - /** - * Version 1 header delimiter is always '\r\n' per spec - */ - private static final int DELIMITER_LENGTH = 2; - /** * Binary header prefix */ @@ -98,6 +92,11 @@ public class HAProxyMessageDecoder extends ByteToMessageDecoder { private static final ProtocolDetectionResult<HAProxyProtocolVersion> DETECTION_RESULT_V2 = ProtocolDetectionResult.detected(HAProxyProtocolVersion.V2); + /** + * Used to extract a header frame out of the {@link ByteBuf} and return it. + */ + private HeaderExtractor headerExtractor; + /** * {@code true} if we're discarding input because we're already over maxLength */ @@ -108,6 +107,11 @@ public class HAProxyMessageDecoder extends ByteToMessageDecoder { */ private int discardedBytes; + /** + * Whether or not to throw an exception as soon as we exceed maxLength. + */ + private final boolean failFast; + /** * {@code true} if we're finished decoding the proxy protocol header */ @@ -125,14 +129,27 @@ public class HAProxyMessageDecoder extends ByteToMessageDecoder { private final int v2MaxHeaderSize; /** - * Creates a new decoder with no additional data (TLV) restrictions + * Creates a new decoder with no additional data (TLV) restrictions, and should throw an exception as soon as + * we exceed maxLength. */ public HAProxyMessageDecoder() { + this(true); + } + + /** + * Creates a new decoder with no additional data (TLV) restrictions, whether or not to throw an exception as soon + * as we exceed maxLength. + * + * @param failFast Whether or not to throw an exception as soon as we exceed maxLength + */ + public HAProxyMessageDecoder(boolean failFast) { v2MaxHeaderSize = V2_MAX_LENGTH; + this.failFast = failFast; } /** - * Creates a new decoder with restricted additional data (TLV) size + * Creates a new decoder with restricted additional data (TLV) size, and should throw an exception as soon as + * we exceed maxLength. * <p> * <b>Note:</b> limiting TLV size only affects processing of v2, binary headers. Also, as allowed by the 1.5 spec * TLV data is currently ignored. For maximum performance it would be best to configure your upstream proxy host to @@ -142,6 +159,17 @@ public class HAProxyMessageDecoder extends ByteToMessageDecoder { * @param maxTlvSize maximum number of bytes allowed for additional data (Type-Length-Value vectors) in a v2 header */ public HAProxyMessageDecoder(int maxTlvSize) { + this(maxTlvSize, true); + } + + /** + * Creates a new decoder with restricted additional data (TLV) size, whether or not to throw an exception as soon + * as we exceed maxLength. + * + * @param maxTlvSize maximum number of bytes allowed for additional data (Type-Length-Value vectors) in a v2 header + * @param failFast Whether or not to throw an exception as soon as we exceed maxLength + */ + public HAProxyMessageDecoder(int maxTlvSize, boolean failFast) { if (maxTlvSize < 1) { v2MaxHeaderSize = V2_MIN_LENGTH; } else if (maxTlvSize > V2_MAX_TLV) { @@ -154,6 +182,7 @@ public class HAProxyMessageDecoder extends ByteToMessageDecoder { v2MaxHeaderSize = calcMax; } } + this.failFast = failFast; } /** @@ -259,7 +288,6 @@ public class HAProxyMessageDecoder extends ByteToMessageDecoder { /** * Create a frame out of the {@link ByteBuf} and return it. - * Based on code from {@link LineBasedFrameDecoder#decode(ChannelHandlerContext, ByteBuf)}. * * @param ctx the {@link ChannelHandlerContext} which this {@link HAProxyMessageDecoder} belongs to * @param buffer the {@link ByteBuf} from which to read data @@ -267,42 +295,14 @@ public class HAProxyMessageDecoder extends ByteToMessageDecoder { * be created */ private ByteBuf decodeStruct(ChannelHandlerContext ctx, ByteBuf buffer) throws Exception { - final int eoh = findEndOfHeader(buffer); - if (!discarding) { - if (eoh >= 0) { - final int length = eoh - buffer.readerIndex(); - if (length > v2MaxHeaderSize) { - buffer.readerIndex(eoh); - failOverLimit(ctx, length); - return null; - } - return buffer.readSlice(length); - } else { - final int length = buffer.readableBytes(); - if (length > v2MaxHeaderSize) { - discardedBytes = length; - buffer.skipBytes(length); - discarding = true; - failOverLimit(ctx, "over " + discardedBytes); - } - return null; - } - } else { - if (eoh >= 0) { - buffer.readerIndex(eoh); - discardedBytes = 0; - discarding = false; - } else { - discardedBytes = buffer.readableBytes(); - buffer.skipBytes(discardedBytes); - } - return null; + if (headerExtractor == null) { + headerExtractor = new StructHeaderExtractor(v2MaxHeaderSize); } + return headerExtractor.extract(ctx, buffer); } /** * Create a frame out of the {@link ByteBuf} and return it. - * Based on code from {@link LineBasedFrameDecoder#decode(ChannelHandlerContext, ByteBuf)}. * * @param ctx the {@link ChannelHandlerContext} which this {@link HAProxyMessageDecoder} belongs to * @param buffer the {@link ByteBuf} from which to read data @@ -310,40 +310,10 @@ public class HAProxyMessageDecoder extends ByteToMessageDecoder { * be created */ private ByteBuf decodeLine(ChannelHandlerContext ctx, ByteBuf buffer) throws Exception { - final int eol = findEndOfLine(buffer); - if (!discarding) { - if (eol >= 0) { - final int length = eol - buffer.readerIndex(); - if (length > V1_MAX_LENGTH) { - buffer.readerIndex(eol + DELIMITER_LENGTH); - failOverLimit(ctx, length); - return null; - } - ByteBuf frame = buffer.readSlice(length); - buffer.skipBytes(DELIMITER_LENGTH); - return frame; - } else { - final int length = buffer.readableBytes(); - if (length > V1_MAX_LENGTH) { - discardedBytes = length; - buffer.skipBytes(length); - discarding = true; - failOverLimit(ctx, "over " + discardedBytes); - } - return null; - } - } else { - if (eol >= 0) { - final int delimLength = buffer.getByte(eol) == '\r' ? 2 : 1; - buffer.readerIndex(eol + delimLength); - discardedBytes = 0; - discarding = false; - } else { - discardedBytes = buffer.readableBytes(); - buffer.skipBytes(discardedBytes); - } - return null; + if (headerExtractor == null) { + headerExtractor = new LineHeaderExtractor(V1_MAX_LENGTH); } + return headerExtractor.extract(ctx, buffer); } private void failOverLimit(final ChannelHandlerContext ctx, int length) { @@ -399,4 +369,119 @@ public class HAProxyMessageDecoder extends ByteToMessageDecoder { } return true; } + + /** + * HeaderExtractor create a header frame out of the {@link ByteBuf}. + */ + private abstract class HeaderExtractor { + /** Header max size */ + private final int maxHeaderSize; + + protected HeaderExtractor(int maxHeaderSize) { + this.maxHeaderSize = maxHeaderSize; + } + + /** + * Create a frame out of the {@link ByteBuf} and return it. + * + * @param ctx the {@link ChannelHandlerContext} which this {@link HAProxyMessageDecoder} belongs to + * @param buffer the {@link ByteBuf} from which to read data + * @return frame the {@link ByteBuf} which represent the frame or {@code null} if no frame could + * be created + * @throws Exception if exceed maxLength + */ + public ByteBuf extract(ChannelHandlerContext ctx, ByteBuf buffer) throws Exception { + final int eoh = findEndOfHeader(buffer); + if (!discarding) { + if (eoh >= 0) { + final int length = eoh - buffer.readerIndex(); + if (length > maxHeaderSize) { + buffer.readerIndex(eoh + delimiterLength(buffer, eoh)); + failOverLimit(ctx, length); + return null; + } + ByteBuf frame = buffer.readSlice(length); + buffer.skipBytes(delimiterLength(buffer, eoh)); + return frame; + } else { + final int length = buffer.readableBytes(); + if (length > maxHeaderSize) { + discardedBytes = length; + buffer.skipBytes(length); + discarding = true; + if (failFast) { + failOverLimit(ctx, "over " + discardedBytes); + } + } + return null; + } + } else { + if (eoh >= 0) { + final int length = discardedBytes + eoh - buffer.readerIndex(); + buffer.readerIndex(eoh + delimiterLength(buffer, eoh)); + discardedBytes = 0; + discarding = false; + if (!failFast) { + failOverLimit(ctx, "over " + length); + } + } else { + discardedBytes += buffer.readableBytes(); + buffer.skipBytes(buffer.readableBytes()); + } + return null; + } + } + + /** + * Find the end of the header from the given {@link ByteBuf},the end may be a CRLF, or the length given by the + * header. + * + * @param buffer the buffer to be searched + * @return {@code -1} if can not find the end, otherwise return the buffer index of end + */ + protected abstract int findEndOfHeader(ByteBuf buffer); + + /** + * Get the length of the header delimiter. + * + * @param buffer the buffer where delimiter is located + * @param eoh index of delimiter + * @return length of the delimiter + */ + protected abstract int delimiterLength(ByteBuf buffer, int eoh); + } + + private final class LineHeaderExtractor extends HeaderExtractor { + + LineHeaderExtractor(int maxHeaderSize) { + super(maxHeaderSize); + } + + @Override + protected int findEndOfHeader(ByteBuf buffer) { + return findEndOfLine(buffer); + } + + @Override + protected int delimiterLength(ByteBuf buffer, int eoh) { + return buffer.getByte(eoh) == '\r' ? 2 : 1; + } + } + + private final class StructHeaderExtractor extends HeaderExtractor { + + StructHeaderExtractor(int maxHeaderSize) { + super(maxHeaderSize); + } + + @Override + protected int findEndOfHeader(ByteBuf buffer) { + return HAProxyMessageDecoder.findEndOfHeader(buffer); + } + + @Override + protected int delimiterLength(ByteBuf buffer, int eoh) { + return 0; + } + } } diff --git a/codec-haproxy/src/main/java/io/netty/handler/codec/haproxy/HAProxyTLV.java b/codec-haproxy/src/main/java/io/netty/handler/codec/haproxy/HAProxyTLV.java index 380c6aa..38d79a0 100644 --- a/codec-haproxy/src/main/java/io/netty/handler/codec/haproxy/HAProxyTLV.java +++ b/codec-haproxy/src/main/java/io/netty/handler/codec/haproxy/HAProxyTLV.java @@ -85,9 +85,7 @@ public class HAProxyTLV extends DefaultByteBufHolder { */ HAProxyTLV(final Type type, final byte typeByteValue, final ByteBuf content) { super(content); - checkNotNull(type, "type"); - - this.type = type; + this.type = checkNotNull(type, "type"); this.typeByteValue = typeByteValue; } diff --git a/codec-haproxy/src/test/java/io/netty/handler/codec/haproxy/HAProxyMessageDecoderTest.java b/codec-haproxy/src/test/java/io/netty/handler/codec/haproxy/HAProxyMessageDecoderTest.java index 2d4039d..2c323ea 100644 --- a/codec-haproxy/src/test/java/io/netty/handler/codec/haproxy/HAProxyMessageDecoderTest.java +++ b/codec-haproxy/src/test/java/io/netty/handler/codec/haproxy/HAProxyMessageDecoderTest.java @@ -24,7 +24,9 @@ import io.netty.handler.codec.haproxy.HAProxyProxiedProtocol.AddressFamily; import io.netty.handler.codec.haproxy.HAProxyProxiedProtocol.TransportProtocol; import io.netty.util.CharsetUtil; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; import java.util.List; @@ -32,6 +34,8 @@ import static io.netty.buffer.Unpooled.*; import static org.junit.Assert.*; public class HAProxyMessageDecoderTest { + @Rule + public ExpectedException exceptionRule = ExpectedException.none(); private EmbeddedChannel ch; @@ -58,6 +62,7 @@ public class HAProxyMessageDecoderTest { assertEquals(443, msg.destinationPort()); assertNull(ch.readInbound()); assertFalse(ch.finish()); + assertTrue(msg.release()); } @Test @@ -78,6 +83,7 @@ public class HAProxyMessageDecoderTest { assertEquals(443, msg.destinationPort()); assertNull(ch.readInbound()); assertFalse(ch.finish()); + assertTrue(msg.release()); } @Test @@ -98,6 +104,7 @@ public class HAProxyMessageDecoderTest { assertEquals(0, msg.destinationPort()); assertNull(ch.readInbound()); assertFalse(ch.finish()); + assertTrue(msg.release()); } @Test(expected = HAProxyProtocolException.class) @@ -161,6 +168,43 @@ public class HAProxyMessageDecoderTest { ch.writeInbound(copiedBuffer(header, CharsetUtil.US_ASCII)); } + @Test + public void testFailSlowHeaderTooLong() { + EmbeddedChannel slowFailCh = new EmbeddedChannel(new HAProxyMessageDecoder(false)); + try { + String headerPart1 = "PROXY TCP4 192.168.0.1 192.168.0.11 56324 " + + "000000000000000000000000000000000000000000000000000000000000000000000443"; + // Should not throw exception + assertFalse(slowFailCh.writeInbound(copiedBuffer(headerPart1, CharsetUtil.US_ASCII))); + String headerPart2 = "more header data"; + // Should not throw exception + assertFalse(slowFailCh.writeInbound(copiedBuffer(headerPart2, CharsetUtil.US_ASCII))); + String headerPart3 = "end of header\r\n"; + + int discarded = headerPart1.length() + headerPart2.length() + headerPart3.length() - 2; + // Should throw exception + exceptionRule.expect(HAProxyProtocolException.class); + exceptionRule.expectMessage("over " + discarded); + assertFalse(slowFailCh.writeInbound(copiedBuffer(headerPart3, CharsetUtil.US_ASCII))); + } finally { + assertFalse(slowFailCh.finishAndReleaseAll()); + } + } + + @Test + public void testFailFastHeaderTooLong() { + EmbeddedChannel fastFailCh = new EmbeddedChannel(new HAProxyMessageDecoder(true)); + try { + String headerPart1 = "PROXY TCP4 192.168.0.1 192.168.0.11 56324 " + + "000000000000000000000000000000000000000000000000000000000000000000000443"; + exceptionRule.expect(HAProxyProtocolException.class); // Should throw exception, fail fast + exceptionRule.expectMessage("over " + headerPart1.length()); + assertFalse(fastFailCh.writeInbound(copiedBuffer(headerPart1, CharsetUtil.US_ASCII))); + } finally { + assertFalse(fastFailCh.finishAndReleaseAll()); + } + } + @Test public void testIncompleteHeader() { String header = "PROXY TCP4 192.168.0.1 192.168.0.11 56324"; @@ -264,6 +308,7 @@ public class HAProxyMessageDecoderTest { assertEquals(443, msg.destinationPort()); assertNull(ch.readInbound()); assertFalse(ch.finish()); + assertTrue(msg.release()); } @Test @@ -319,6 +364,7 @@ public class HAProxyMessageDecoderTest { assertEquals(443, msg.destinationPort()); assertNull(ch.readInbound()); assertFalse(ch.finish()); + assertTrue(msg.release()); } @Test @@ -398,6 +444,7 @@ public class HAProxyMessageDecoderTest { assertEquals(443, msg.destinationPort()); assertNull(ch.readInbound()); assertFalse(ch.finish()); + assertTrue(msg.release()); } @Test @@ -476,6 +523,7 @@ public class HAProxyMessageDecoderTest { assertEquals(0, msg.destinationPort()); assertNull(ch.readInbound()); assertFalse(ch.finish()); + assertTrue(msg.release()); } @Test @@ -531,6 +579,7 @@ public class HAProxyMessageDecoderTest { assertEquals(0, msg.destinationPort()); assertNull(ch.readInbound()); assertFalse(ch.finish()); + assertTrue(msg.release()); } @Test @@ -586,6 +635,7 @@ public class HAProxyMessageDecoderTest { assertEquals(0, msg.destinationPort()); assertNull(ch.readInbound()); assertFalse(ch.finish()); + assertTrue(msg.release()); } @Test @@ -642,9 +692,7 @@ public class HAProxyMessageDecoderTest { assertTrue(0 < firstTlv.refCnt()); assertTrue(0 < secondTlv.refCnt()); assertTrue(0 < thirdTLV.refCnt()); - assertFalse(thirdTLV.release()); - assertFalse(secondTlv.release()); - assertTrue(firstTlv.release()); + assertTrue(msg.release()); assertEquals(0, firstTlv.refCnt()); assertEquals(0, secondTlv.refCnt()); assertEquals(0, thirdTLV.refCnt()); @@ -653,6 +701,51 @@ public class HAProxyMessageDecoderTest { assertFalse(ch.finish()); } + @Test + public void testReleaseHAProxyMessage() { + ch = new EmbeddedChannel(new HAProxyMessageDecoder()); + + final byte[] bytes = { + 13, 10, 13, 10, 0, 13, 10, 81, 85, 73, 84, 10, 33, 17, 0, 35, 127, 0, 0, 1, 127, 0, 0, 1, + -55, -90, 7, 89, 32, 0, 20, 5, 0, 0, 0, 0, 33, 0, 5, 84, 76, 83, 118, 49, 34, 0, 4, 76, 69, 65, 70 + }; + + int startChannels = ch.pipeline().names().size(); + assertTrue(ch.writeInbound(copiedBuffer(bytes))); + Object msgObj = ch.readInbound(); + assertEquals(startChannels - 1, ch.pipeline().names().size()); + HAProxyMessage msg = (HAProxyMessage) msgObj; + + final List<HAProxyTLV> tlvs = msg.tlvs(); + assertEquals(3, tlvs.size()); + + assertEquals(1, msg.refCnt()); + for (HAProxyTLV tlv : tlvs) { + assertEquals(3, tlv.refCnt()); + } + + // Retain the haproxy message + msg.retain(); + assertEquals(2, msg.refCnt()); + for (HAProxyTLV tlv : tlvs) { + assertEquals(3, tlv.refCnt()); + } + + // Decrease the haproxy message refCnt + msg.release(); + assertEquals(1, msg.refCnt()); + for (HAProxyTLV tlv : tlvs) { + assertEquals(3, tlv.refCnt()); + } + + // Release haproxy message, TLVs will be released with it + msg.release(); + assertEquals(0, msg.refCnt()); + for (HAProxyTLV tlv : tlvs) { + assertEquals(0, tlv.refCnt()); + } + } + @Test public void testV2WithTLV() { ch = new EmbeddedChannel(new HAProxyMessageDecoder(4)); @@ -738,6 +831,7 @@ public class HAProxyMessageDecoderTest { assertEquals(0, msg.destinationPort()); assertNull(ch.readInbound()); assertFalse(ch.finish()); + assertTrue(msg.release()); } @Test(expected = HAProxyProtocolException.class) diff --git a/codec-http/pom.xml b/codec-http/pom.xml index 65c517e..2f09d40 100644 --- a/codec-http/pom.xml +++ b/codec-http/pom.xml @@ -20,7 +20,7 @@ <parent> <groupId>io.netty</groupId> <artifactId>netty-parent</artifactId> - <version>4.1.33.Final</version> + <version>4.1.48.Final</version> </parent> <artifactId>netty-codec-http</artifactId> @@ -57,7 +57,6 @@ <groupId>${project.groupId}</groupId> <artifactId>netty-handler</artifactId> <version>${project.version}</version> - <optional>true</optional> </dependency> <dependency> <groupId>com.jcraft</groupId> diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/CombinedHttpHeaders.java b/codec-http/src/main/java/io/netty/handler/codec/http/CombinedHttpHeaders.java index ae49493..b8a2d0b 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/CombinedHttpHeaders.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/CombinedHttpHeaders.java @@ -79,7 +79,7 @@ public class CombinedHttpHeaders extends DefaultHttpHeaders { return charSequenceEscaper; } - public CombinedHttpHeadersImpl(HashingStrategy<CharSequence> nameHashingStrategy, + CombinedHttpHeadersImpl(HashingStrategy<CharSequence> nameHashingStrategy, ValueConverter<CharSequence> valueConverter, io.netty.handler.codec.DefaultHeaders.NameValidator<CharSequence> nameValidator) { super(nameHashingStrategy, valueConverter, nameValidator); diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/DefaultCookie.java b/codec-http/src/main/java/io/netty/handler/codec/http/DefaultCookie.java index 902ba16..850f8e9 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/DefaultCookie.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/DefaultCookie.java @@ -15,6 +15,8 @@ */ package io.netty.handler.codec.http; +import io.netty.util.internal.ObjectUtil; + import java.util.Collections; import java.util.Set; import java.util.TreeSet; @@ -131,9 +133,7 @@ public class DefaultCookie extends io.netty.handler.codec.http.cookie.DefaultCoo @Override @Deprecated public void setPorts(int... ports) { - if (ports == null) { - throw new NullPointerException("ports"); - } + ObjectUtil.checkNotNull(ports, "ports"); int[] portsCopy = ports.clone(); if (portsCopy.length == 0) { diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/DefaultHttpContent.java b/codec-http/src/main/java/io/netty/handler/codec/http/DefaultHttpContent.java index 48fad8e..a77cb21 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/DefaultHttpContent.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/DefaultHttpContent.java @@ -16,6 +16,7 @@ package io.netty.handler.codec.http; import io.netty.buffer.ByteBuf; +import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.StringUtil; /** @@ -29,10 +30,7 @@ public class DefaultHttpContent extends DefaultHttpObject implements HttpContent * Creates a new instance with the specified chunk content. */ public DefaultHttpContent(ByteBuf content) { - if (content == null) { - throw new NullPointerException("content"); - } - this.content = content; + this.content = ObjectUtil.checkNotNull(content, "content"); } @Override diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/DefaultHttpHeaders.java b/codec-http/src/main/java/io/netty/handler/codec/http/DefaultHttpHeaders.java index 88af27f..675d513 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/DefaultHttpHeaders.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/DefaultHttpHeaders.java @@ -78,6 +78,18 @@ public class DefaultHttpHeaders extends HttpHeaders { this(true); } + /** + * <b>Warning!</b> Setting <code>validate</code> to <code>false</code> will mean that Netty won't + * validate & protect against user-supplied header values that are malicious. + * This can leave your server implementation vulnerable to + * <a href="https://cwe.mitre.org/data/definitions/113.html"> + * CWE-113: Improper Neutralization of CRLF Sequences in HTTP Headers ('HTTP Response Splitting') + * </a>. + * When disabling this validation, it is the responsibility of the caller to ensure that the values supplied + * do not contain a non-url-escaped carriage return (CR) and/or line feed (LF) characters. + * + * @param validate Should Netty validate Header values to ensure they aren't malicious. + */ public DefaultHttpHeaders(boolean validate) { this(validate, nameValidator(validate)); } @@ -372,8 +384,7 @@ public class DefaultHttpHeaders extends HttpHeaders { default: // Check to see if the character is not an ASCII character, or invalid if (value < 0) { - throw new IllegalArgumentException("a header name cannot contain non-ASCII character: " + - value); + throw new IllegalArgumentException("a header name cannot contain non-ASCII character: " + value); } } } diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/DefaultHttpMessage.java b/codec-http/src/main/java/io/netty/handler/codec/http/DefaultHttpMessage.java index 5a6a45a..3f6682f 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/DefaultHttpMessage.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/DefaultHttpMessage.java @@ -15,6 +15,8 @@ */ package io.netty.handler.codec.http; +import io.netty.util.internal.ObjectUtil; + import static io.netty.util.internal.ObjectUtil.checkNotNull; /** @@ -89,10 +91,7 @@ public abstract class DefaultHttpMessage extends DefaultHttpObject implements Ht @Override public HttpMessage setProtocolVersion(HttpVersion version) { - if (version == null) { - throw new NullPointerException("version"); - } - this.version = version; + this.version = ObjectUtil.checkNotNull(version, "version"); return this; } } diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/DefaultHttpObject.java b/codec-http/src/main/java/io/netty/handler/codec/http/DefaultHttpObject.java index c26ad39..71d8cdc 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/DefaultHttpObject.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/DefaultHttpObject.java @@ -16,6 +16,7 @@ package io.netty.handler.codec.http; import io.netty.handler.codec.DecoderResult; +import io.netty.util.internal.ObjectUtil; public class DefaultHttpObject implements HttpObject { @@ -39,10 +40,7 @@ public class DefaultHttpObject implements HttpObject { @Override public void setDecoderResult(DecoderResult decoderResult) { - if (decoderResult == null) { - throw new NullPointerException("decoderResult"); - } - this.decoderResult = decoderResult; + this.decoderResult = ObjectUtil.checkNotNull(decoderResult, "decoderResult"); } @Override diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/DefaultHttpRequest.java b/codec-http/src/main/java/io/netty/handler/codec/http/DefaultHttpRequest.java index 84be3bb..dbc7dd3 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/DefaultHttpRequest.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/DefaultHttpRequest.java @@ -15,6 +15,8 @@ */ package io.netty.handler.codec.http; +import io.netty.util.internal.ObjectUtil; + import static io.netty.util.internal.ObjectUtil.checkNotNull; /** @@ -88,19 +90,13 @@ public class DefaultHttpRequest extends DefaultHttpMessage implements HttpReques @Override public HttpRequest setMethod(HttpMethod method) { - if (method == null) { - throw new NullPointerException("method"); - } - this.method = method; + this.method = ObjectUtil.checkNotNull(method, "method"); return this; } @Override public HttpRequest setUri(String uri) { - if (uri == null) { - throw new NullPointerException("uri"); - } - this.uri = uri; + this.uri = ObjectUtil.checkNotNull(uri, "uri"); return this; } diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/DefaultHttpResponse.java b/codec-http/src/main/java/io/netty/handler/codec/http/DefaultHttpResponse.java index d5b7cf0..8ee4900 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/DefaultHttpResponse.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/DefaultHttpResponse.java @@ -15,6 +15,8 @@ */ package io.netty.handler.codec.http; +import io.netty.util.internal.ObjectUtil; + import static io.netty.util.internal.ObjectUtil.checkNotNull; /** @@ -88,10 +90,7 @@ public class DefaultHttpResponse extends DefaultHttpMessage implements HttpRespo @Override public HttpResponse setStatus(HttpResponseStatus status) { - if (status == null) { - throw new NullPointerException("status"); - } - this.status = status; + this.status = ObjectUtil.checkNotNull(status, "status"); return this; } diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/HttpClientCodec.java b/codec-http/src/main/java/io/netty/handler/codec/http/HttpClientCodec.java index dd1da34..1ca4c46 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/HttpClientCodec.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/HttpClientCodec.java @@ -160,7 +160,7 @@ public final class HttpClientCodec extends CombinedChannelDuplexHandler<HttpResp return; } - if (msg instanceof HttpRequest && !done) { + if (msg instanceof HttpRequest) { queue.offer(((HttpRequest) msg).method()); } @@ -222,57 +222,63 @@ public final class HttpClientCodec extends CombinedChannelDuplexHandler<HttpResp @Override protected boolean isContentAlwaysEmpty(HttpMessage msg) { + // Get the method of the HTTP request that corresponds to the + // current response. + // + // Even if we do not use the method to compare we still need to poll it to ensure we keep + // request / response pairs in sync. + HttpMethod method = queue.poll(); + final int statusCode = ((HttpResponse) msg).status().code(); - if (statusCode == 100 || statusCode == 101) { - // 100-continue and 101 switching protocols response should be excluded from paired comparison. + if (statusCode >= 100 && statusCode < 200) { + // An informational response should be excluded from paired comparison. // Just delegate to super method which has all the needed handling. return super.isContentAlwaysEmpty(msg); } - // Get the getMethod of the HTTP request that corresponds to the - // current response. - HttpMethod method = queue.poll(); - - char firstChar = method.name().charAt(0); - switch (firstChar) { - case 'H': - // According to 4.3, RFC2616: - // All responses to the HEAD request method MUST NOT include a - // message-body, even though the presence of entity-header fields - // might lead one to believe they do. - if (HttpMethod.HEAD.equals(method)) { - return true; - - // The following code was inserted to work around the servers - // that behave incorrectly. It has been commented out - // because it does not work with well behaving servers. - // Please note, even if the 'Transfer-Encoding: chunked' - // header exists in the HEAD response, the response should - // have absolutely no content. - // - //// Interesting edge case: - //// Some poorly implemented servers will send a zero-byte - //// chunk if Transfer-Encoding of the response is 'chunked'. - //// - //// return !msg.isChunked(); - } - break; - case 'C': - // Successful CONNECT request results in a response with empty body. - if (statusCode == 200) { - if (HttpMethod.CONNECT.equals(method)) { - // Proxy connection established - Parse HTTP only if configured by parseHttpAfterConnectRequest, - // else pass through. - if (!parseHttpAfterConnectRequest) { - done = true; - queue.clear(); + // If the remote peer did for example send multiple responses for one request (which is not allowed per + // spec but may still be possible) method will be null so guard against it. + if (method != null) { + char firstChar = method.name().charAt(0); + switch (firstChar) { + case 'H': + // According to 4.3, RFC2616: + // All responses to the HEAD request method MUST NOT include a + // message-body, even though the presence of entity-header fields + // might lead one to believe they do. + if (HttpMethod.HEAD.equals(method)) { + return true; + + // The following code was inserted to work around the servers + // that behave incorrectly. It has been commented out + // because it does not work with well behaving servers. + // Please note, even if the 'Transfer-Encoding: chunked' + // header exists in the HEAD response, the response should + // have absolutely no content. + // + //// Interesting edge case: + //// Some poorly implemented servers will send a zero-byte + //// chunk if Transfer-Encoding of the response is 'chunked'. + //// + //// return !msg.isChunked(); } - return true; - } + break; + case 'C': + // Successful CONNECT request results in a response with empty body. + if (statusCode == 200) { + if (HttpMethod.CONNECT.equals(method)) { + // Proxy connection established - Parse HTTP only if configured by + // parseHttpAfterConnectRequest, else pass through. + if (!parseHttpAfterConnectRequest) { + done = true; + queue.clear(); + } + return true; + } + } + break; } - break; } - return super.isContentAlwaysEmpty(msg); } diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/HttpClientUpgradeHandler.java b/codec-http/src/main/java/io/netty/handler/codec/http/HttpClientUpgradeHandler.java index bf2cd55..3ee5705 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/HttpClientUpgradeHandler.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/HttpClientUpgradeHandler.java @@ -18,6 +18,7 @@ import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelOutboundHandler; import io.netty.channel.ChannelPromise; import io.netty.util.AsciiString; +import io.netty.util.internal.ObjectUtil; import java.net.SocketAddress; import java.util.Collection; @@ -115,14 +116,8 @@ public class HttpClientUpgradeHandler extends HttpObjectAggregator implements Ch public HttpClientUpgradeHandler(SourceCodec sourceCodec, UpgradeCodec upgradeCodec, int maxContentLength) { super(maxContentLength); - if (sourceCodec == null) { - throw new NullPointerException("sourceCodec"); - } - if (upgradeCodec == null) { - throw new NullPointerException("upgradeCodec"); - } - this.sourceCodec = sourceCodec; - this.upgradeCodec = upgradeCodec; + this.sourceCodec = ObjectUtil.checkNotNull(sourceCodec, "sourceCodec"); + this.upgradeCodec = ObjectUtil.checkNotNull(upgradeCodec, "upgradeCodec"); } @Override diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/HttpContentCompressor.java b/codec-http/src/main/java/io/netty/handler/codec/http/HttpContentCompressor.java index e80469f..b40fae6 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/HttpContentCompressor.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/HttpContentCompressor.java @@ -132,15 +132,15 @@ public class HttpContentCompressor extends HttpContentEncoder { } @Override - protected Result beginEncode(HttpResponse headers, String acceptEncoding) throws Exception { + protected Result beginEncode(HttpResponse httpResponse, String acceptEncoding) throws Exception { if (this.contentSizeThreshold > 0) { - if (headers instanceof HttpContent && - ((HttpContent) headers).content().readableBytes() < contentSizeThreshold) { + if (httpResponse instanceof HttpContent && + ((HttpContent) httpResponse).content().readableBytes() < contentSizeThreshold) { return null; } } - String contentEncoding = headers.headers().get(HttpHeaderNames.CONTENT_ENCODING); + String contentEncoding = httpResponse.headers().get(HttpHeaderNames.CONTENT_ENCODING); if (contentEncoding != null) { // Content-Encoding was set, either as something specific or as the IDENTITY encoding // Therefore, we should NOT encode here diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/HttpContentDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/HttpContentDecoder.java index 3eb7ad3..d2513e4 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/HttpContentDecoder.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/HttpContentDecoder.java @@ -51,102 +51,107 @@ public abstract class HttpContentDecoder extends MessageToMessageDecoder<HttpObj protected ChannelHandlerContext ctx; private EmbeddedChannel decoder; private boolean continueResponse; + private boolean needRead = true; @Override protected void decode(ChannelHandlerContext ctx, HttpObject msg, List<Object> out) throws Exception { - if (msg instanceof HttpResponse && ((HttpResponse) msg).status().code() == 100) { + try { + if (msg instanceof HttpResponse && ((HttpResponse) msg).status().code() == 100) { - if (!(msg instanceof LastHttpContent)) { - continueResponse = true; + if (!(msg instanceof LastHttpContent)) { + continueResponse = true; + } + // 100-continue response must be passed through. + out.add(ReferenceCountUtil.retain(msg)); + return; } - // 100-continue response must be passed through. - out.add(ReferenceCountUtil.retain(msg)); - return; - } - if (continueResponse) { - if (msg instanceof LastHttpContent) { - continueResponse = false; + if (continueResponse) { + if (msg instanceof LastHttpContent) { + continueResponse = false; + } + // 100-continue response must be passed through. + out.add(ReferenceCountUtil.retain(msg)); + return; } - // 100-continue response must be passed through. - out.add(ReferenceCountUtil.retain(msg)); - return; - } - if (msg instanceof HttpMessage) { - cleanup(); - final HttpMessage message = (HttpMessage) msg; - final HttpHeaders headers = message.headers(); + if (msg instanceof HttpMessage) { + cleanup(); + final HttpMessage message = (HttpMessage) msg; + final HttpHeaders headers = message.headers(); - // Determine the content encoding. - String contentEncoding = headers.get(HttpHeaderNames.CONTENT_ENCODING); - if (contentEncoding != null) { - contentEncoding = contentEncoding.trim(); - } else { - contentEncoding = IDENTITY; - } - decoder = newContentDecoder(contentEncoding); + // Determine the content encoding. + String contentEncoding = headers.get(HttpHeaderNames.CONTENT_ENCODING); + if (contentEncoding != null) { + contentEncoding = contentEncoding.trim(); + } else { + contentEncoding = IDENTITY; + } + decoder = newContentDecoder(contentEncoding); - if (decoder == null) { - if (message instanceof HttpContent) { - ((HttpContent) message).retain(); + if (decoder == null) { + if (message instanceof HttpContent) { + ((HttpContent) message).retain(); + } + out.add(message); + return; } - out.add(message); - return; - } - // Remove content-length header: - // the correct value can be set only after all chunks are processed/decoded. - // If buffering is not an issue, add HttpObjectAggregator down the chain, it will set the header. - // Otherwise, rely on LastHttpContent message. - if (headers.contains(HttpHeaderNames.CONTENT_LENGTH)) { - headers.remove(HttpHeaderNames.CONTENT_LENGTH); - headers.set(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.CHUNKED); - } - // Either it is already chunked or EOF terminated. - // See https://github.com/netty/netty/issues/5892 + // Remove content-length header: + // the correct value can be set only after all chunks are processed/decoded. + // If buffering is not an issue, add HttpObjectAggregator down the chain, it will set the header. + // Otherwise, rely on LastHttpContent message. + if (headers.contains(HttpHeaderNames.CONTENT_LENGTH)) { + headers.remove(HttpHeaderNames.CONTENT_LENGTH); + headers.set(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.CHUNKED); + } + // Either it is already chunked or EOF terminated. + // See https://github.com/netty/netty/issues/5892 - // set new content encoding, - CharSequence targetContentEncoding = getTargetContentEncoding(contentEncoding); - if (HttpHeaderValues.IDENTITY.contentEquals(targetContentEncoding)) { - // Do NOT set the 'Content-Encoding' header if the target encoding is 'identity' - // as per: http://tools.ietf.org/html/rfc2616#section-14.11 - headers.remove(HttpHeaderNames.CONTENT_ENCODING); - } else { - headers.set(HttpHeaderNames.CONTENT_ENCODING, targetContentEncoding); - } + // set new content encoding, + CharSequence targetContentEncoding = getTargetContentEncoding(contentEncoding); + if (HttpHeaderValues.IDENTITY.contentEquals(targetContentEncoding)) { + // Do NOT set the 'Content-Encoding' header if the target encoding is 'identity' + // as per: http://tools.ietf.org/html/rfc2616#section-14.11 + headers.remove(HttpHeaderNames.CONTENT_ENCODING); + } else { + headers.set(HttpHeaderNames.CONTENT_ENCODING, targetContentEncoding); + } - if (message instanceof HttpContent) { - // If message is a full request or response object (headers + data), don't copy data part into out. - // Output headers only; data part will be decoded below. - // Note: "copy" object must not be an instance of LastHttpContent class, - // as this would (erroneously) indicate the end of the HttpMessage to other handlers. - HttpMessage copy; - if (message instanceof HttpRequest) { - HttpRequest r = (HttpRequest) message; // HttpRequest or FullHttpRequest - copy = new DefaultHttpRequest(r.protocolVersion(), r.method(), r.uri()); - } else if (message instanceof HttpResponse) { - HttpResponse r = (HttpResponse) message; // HttpResponse or FullHttpResponse - copy = new DefaultHttpResponse(r.protocolVersion(), r.status()); + if (message instanceof HttpContent) { + // If message is a full request or response object (headers + data), don't copy data part into out. + // Output headers only; data part will be decoded below. + // Note: "copy" object must not be an instance of LastHttpContent class, + // as this would (erroneously) indicate the end of the HttpMessage to other handlers. + HttpMessage copy; + if (message instanceof HttpRequest) { + HttpRequest r = (HttpRequest) message; // HttpRequest or FullHttpRequest + copy = new DefaultHttpRequest(r.protocolVersion(), r.method(), r.uri()); + } else if (message instanceof HttpResponse) { + HttpResponse r = (HttpResponse) message; // HttpResponse or FullHttpResponse + copy = new DefaultHttpResponse(r.protocolVersion(), r.status()); + } else { + throw new CodecException("Object of class " + message.getClass().getName() + + " is not an HttpRequest or HttpResponse"); + } + copy.headers().set(message.headers()); + copy.setDecoderResult(message.decoderResult()); + out.add(copy); } else { - throw new CodecException("Object of class " + message.getClass().getName() + - " is not a HttpRequest or HttpResponse"); + out.add(message); } - copy.headers().set(message.headers()); - copy.setDecoderResult(message.decoderResult()); - out.add(copy); - } else { - out.add(message); } - } - if (msg instanceof HttpContent) { - final HttpContent c = (HttpContent) msg; - if (decoder == null) { - out.add(c.retain()); - } else { - decodeContent(c, out); + if (msg instanceof HttpContent) { + final HttpContent c = (HttpContent) msg; + if (decoder == null) { + out.add(c.retain()); + } else { + decodeContent(c, out); + } } + } finally { + needRead = out.isEmpty(); } } @@ -170,6 +175,20 @@ public abstract class HttpContentDecoder extends MessageToMessageDecoder<HttpObj } } + @Override + public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { + boolean needRead = this.needRead; + this.needRead = true; + + try { + ctx.fireChannelReadComplete(); + } finally { + if (needRead && !ctx.channel().config().isAutoRead()) { + ctx.read(); + } + } + } + /** * Returns a new {@link EmbeddedChannel} that decodes the HTTP message * content encoded in the specified <tt>contentEncoding</tt>. diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/HttpContentEncoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/HttpContentEncoder.java index c486a67..6f1070c 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/HttpContentEncoder.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/HttpContentEncoder.java @@ -22,11 +22,15 @@ import io.netty.channel.embedded.EmbeddedChannel; import io.netty.handler.codec.DecoderResult; import io.netty.handler.codec.MessageToMessageCodec; import io.netty.util.ReferenceCountUtil; +import io.netty.util.internal.ObjectUtil; +import io.netty.util.internal.StringUtil; import java.util.ArrayDeque; import java.util.List; import java.util.Queue; +import static io.netty.handler.codec.http.HttpHeaderNames.*; + /** * Encodes the content of the outbound {@link HttpResponse} and {@link HttpContent}. * The original content is replaced with the new content encoded by the @@ -71,21 +75,30 @@ public abstract class HttpContentEncoder extends MessageToMessageCodec<HttpReque } @Override - protected void decode(ChannelHandlerContext ctx, HttpRequest msg, List<Object> out) - throws Exception { - CharSequence acceptedEncoding = msg.headers().get(HttpHeaderNames.ACCEPT_ENCODING); - if (acceptedEncoding == null) { - acceptedEncoding = HttpContentDecoder.IDENTITY; + protected void decode(ChannelHandlerContext ctx, HttpRequest msg, List<Object> out) throws Exception { + CharSequence acceptEncoding; + List<String> acceptEncodingHeaders = msg.headers().getAll(ACCEPT_ENCODING); + switch (acceptEncodingHeaders.size()) { + case 0: + acceptEncoding = HttpContentDecoder.IDENTITY; + break; + case 1: + acceptEncoding = acceptEncodingHeaders.get(0); + break; + default: + // Multiple message-header fields https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2 + acceptEncoding = StringUtil.join(",", acceptEncodingHeaders); + break; } - HttpMethod meth = msg.method(); - if (meth == HttpMethod.HEAD) { - acceptedEncoding = ZERO_LENGTH_HEAD; - } else if (meth == HttpMethod.CONNECT) { - acceptedEncoding = ZERO_LENGTH_CONNECT; + HttpMethod method = msg.method(); + if (HttpMethod.HEAD.equals(method)) { + acceptEncoding = ZERO_LENGTH_HEAD; + } else if (HttpMethod.CONNECT.equals(method)) { + acceptEncoding = ZERO_LENGTH_CONNECT; } - acceptEncodingQueue.add(acceptedEncoding); + acceptEncodingQueue.add(acceptEncoding); out.add(ReferenceCountUtil.retain(msg)); } @@ -275,8 +288,8 @@ public abstract class HttpContentEncoder extends MessageToMessageCodec<HttpReque /** * Prepare to encode the HTTP message content. * - * @param headers - * the headers + * @param httpResponse + * the http response * @param acceptEncoding * the value of the {@code "Accept-Encoding"} header * @@ -286,7 +299,7 @@ public abstract class HttpContentEncoder extends MessageToMessageCodec<HttpReque * {@code null} if {@code acceptEncoding} is unsupported or rejected * and thus the content should be handled as-is (i.e. no encoding). */ - protected abstract Result beginEncode(HttpResponse headers, String acceptEncoding) throws Exception; + protected abstract Result beginEncode(HttpResponse httpResponse, String acceptEncoding) throws Exception; @Override public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { @@ -350,15 +363,8 @@ public abstract class HttpContentEncoder extends MessageToMessageCodec<HttpReque private final EmbeddedChannel contentEncoder; public Result(String targetContentEncoding, EmbeddedChannel contentEncoder) { - if (targetContentEncoding == null) { - throw new NullPointerException("targetContentEncoding"); - } - if (contentEncoder == null) { - throw new NullPointerException("contentEncoder"); - } - - this.targetContentEncoding = targetContentEncoding; - this.contentEncoder = contentEncoder; + this.targetContentEncoding = ObjectUtil.checkNotNull(targetContentEncoding, "targetContentEncoding"); + this.contentEncoder = ObjectUtil.checkNotNull(contentEncoder, "contentEncoder"); } public String targetContentEncoding() { diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/HttpHeaders.java b/codec-http/src/main/java/io/netty/handler/codec/http/HttpHeaders.java index 694bc4d..580e5ce 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/HttpHeaders.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/HttpHeaders.java @@ -22,6 +22,7 @@ import io.netty.handler.codec.Headers; import io.netty.handler.codec.HeadersUtils; import io.netty.util.AsciiString; import io.netty.util.CharsetUtil; +import io.netty.util.internal.ObjectUtil; import java.text.ParseException; import java.util.Calendar; @@ -1412,9 +1413,7 @@ public abstract class HttpHeaders implements Iterable<Map.Entry<String, String>> * @return {@code this} */ public HttpHeaders add(HttpHeaders headers) { - if (headers == null) { - throw new NullPointerException("headers"); - } + ObjectUtil.checkNotNull(headers, "headers"); for (Map.Entry<String, String> e: headers) { add(e.getKey(), e.getValue()); } diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/HttpMessage.java b/codec-http/src/main/java/io/netty/handler/codec/http/HttpMessage.java index 357ad99..f2f1a40 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/HttpMessage.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/HttpMessage.java @@ -17,7 +17,7 @@ package io.netty.handler.codec.http; /** - * An interface that defines a HTTP message, providing common properties for + * An interface that defines an HTTP message, providing common properties for * {@link HttpRequest} and {@link HttpResponse}. * * @see HttpResponse diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/HttpMethod.java b/codec-http/src/main/java/io/netty/handler/codec/http/HttpMethod.java index 3d97551..a634bd0 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/HttpMethod.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/HttpMethod.java @@ -156,6 +156,9 @@ public class HttpMethod implements Comparable<HttpMethod> { @Override public boolean equals(Object o) { + if (this == o) { + return true; + } if (!(o instanceof HttpMethod)) { return false; } @@ -171,6 +174,9 @@ public class HttpMethod implements Comparable<HttpMethod> { @Override public int compareTo(HttpMethod o) { + if (o == this) { + return 0; + } return name().compareTo(o.name()); } diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/HttpObjectAggregator.java b/codec-http/src/main/java/io/netty/handler/codec/http/HttpObjectAggregator.java index 257581f..192a0ed 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/HttpObjectAggregator.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/HttpObjectAggregator.java @@ -271,13 +271,6 @@ public class HttpObjectAggregator } }); } - - // If an oversized request was handled properly and the connection is still alive - // (i.e. rejected 100-continue). the decoder should prepare to handle a new message. - HttpObjectDecoder decoder = ctx.pipeline().get(HttpObjectDecoder.class); - if (decoder != null) { - decoder.reset(); - } } else if (oversized instanceof HttpResponse) { ctx.close(); throw new TooLongFrameException("Response entity too large: " + oversized); diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/HttpObjectDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/HttpObjectDecoder.java index af1d642..4134735 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/HttpObjectDecoder.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/HttpObjectDecoder.java @@ -15,6 +15,8 @@ */ package io.netty.handler.codec.http; +import static io.netty.util.internal.ObjectUtil.checkPositive; + import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelHandlerContext; @@ -168,21 +170,10 @@ public abstract class HttpObjectDecoder extends ByteToMessageDecoder { protected HttpObjectDecoder( int maxInitialLineLength, int maxHeaderSize, int maxChunkSize, boolean chunkedSupported, boolean validateHeaders, int initialBufferSize) { - if (maxInitialLineLength <= 0) { - throw new IllegalArgumentException( - "maxInitialLineLength must be a positive integer: " + - maxInitialLineLength); - } - if (maxHeaderSize <= 0) { - throw new IllegalArgumentException( - "maxHeaderSize must be a positive integer: " + - maxHeaderSize); - } - if (maxChunkSize <= 0) { - throw new IllegalArgumentException( - "maxChunkSize must be a positive integer: " + - maxChunkSize); - } + checkPositive(maxInitialLineLength, "maxInitialLineLength"); + checkPositive(maxHeaderSize, "maxHeaderSize"); + checkPositive(maxChunkSize, "maxChunkSize"); + AppendableCharSequence seq = new AppendableCharSequence(initialBufferSize); lineParser = new LineParser(seq, maxInitialLineLength); headerParser = new HeaderParser(seq, maxHeaderSize); @@ -198,12 +189,8 @@ public abstract class HttpObjectDecoder extends ByteToMessageDecoder { } switch (currentState) { - case SKIP_CONTROL_CHARS: { - if (!skipControlCharacters(buffer)) { - return; - } - currentState = State.READ_INITIAL; - } + case SKIP_CONTROL_CHARS: + // Fall-through case READ_INITIAL: try { AppendableCharSequence line = lineParser.parse(buffer); if (line == null) { @@ -290,7 +277,7 @@ public abstract class HttpObjectDecoder extends ByteToMessageDecoder { // Check if the buffer is readable first as we use the readable byte count // to create the HttpChunk. This is needed as otherwise we may end up with - // create a HttpChunk instance that contains an empty buffer and so is + // create an HttpChunk instance that contains an empty buffer and so is // handled like it is the last HttpChunk. // // See https://github.com/netty/netty/issues/433 @@ -558,22 +545,6 @@ public abstract class HttpObjectDecoder extends ByteToMessageDecoder { return chunk; } - private static boolean skipControlCharacters(ByteBuf buffer) { - boolean skiped = false; - final int wIdx = buffer.writerIndex(); - int rIdx = buffer.readerIndex(); - while (wIdx > rIdx) { - int c = buffer.getUnsignedByte(rIdx++); - if (!Character.isISOControl(c) && !Character.isWhitespace(c)) { - rIdx--; - skiped = true; - break; - } - } - buffer.readerIndex(rIdx); - return skiped; - } - private State readHeaders(ByteBuf buffer) { final HttpMessage message = this.message; final HttpHeaders headers = message.headers(); @@ -584,7 +555,7 @@ public abstract class HttpObjectDecoder extends ByteToMessageDecoder { } if (line.length() > 0) { do { - char firstChar = line.charAt(0); + char firstChar = line.charAtUnsafe(0); if (name != null && (firstChar == ' ' || firstChar == '\t')) { //please do not make one line from below code //as it breaks +XX:OptimizeStringConcat optimization @@ -609,23 +580,73 @@ public abstract class HttpObjectDecoder extends ByteToMessageDecoder { if (name != null) { headers.add(name, value); } + // reset name and value fields name = null; value = null; - State nextState; + List<String> values = headers.getAll(HttpHeaderNames.CONTENT_LENGTH); + int contentLengthValuesCount = values.size(); + + if (contentLengthValuesCount > 0) { + // Guard against multiple Content-Length headers as stated in + // https://tools.ietf.org/html/rfc7230#section-3.3.2: + // + // If a message is received that has multiple Content-Length header + // fields with field-values consisting of the same decimal value, or a + // single Content-Length header field with a field value containing a + // list of identical decimal values (e.g., "Content-Length: 42, 42"), + // indicating that duplicate Content-Length header fields have been + // generated or combined by an upstream message processor, then the + // recipient MUST either reject the message as invalid or replace the + // duplicated field-values with a single valid Content-Length field + // containing that decimal value prior to determining the message body + // length or forwarding the message. + if (contentLengthValuesCount > 1 && message.protocolVersion() == HttpVersion.HTTP_1_1) { + throw new IllegalArgumentException("Multiple Content-Length headers found"); + } + contentLength = Long.parseLong(values.get(0)); + } if (isContentAlwaysEmpty(message)) { HttpUtil.setTransferEncodingChunked(message, false); - nextState = State.SKIP_CONTROL_CHARS; + return State.SKIP_CONTROL_CHARS; } else if (HttpUtil.isTransferEncodingChunked(message)) { - nextState = State.READ_CHUNK_SIZE; + if (contentLengthValuesCount > 0 && message.protocolVersion() == HttpVersion.HTTP_1_1) { + handleTransferEncodingChunkedWithContentLength(message); + } + return State.READ_CHUNK_SIZE; } else if (contentLength() >= 0) { - nextState = State.READ_FIXED_LENGTH_CONTENT; + return State.READ_FIXED_LENGTH_CONTENT; } else { - nextState = State.READ_VARIABLE_LENGTH_CONTENT; + return State.READ_VARIABLE_LENGTH_CONTENT; } - return nextState; + } + + /** + * Invoked when a message with both a "Transfer-Encoding: chunked" and a "Content-Length" header field is detected. + * The default behavior is to <i>remove</i> the Content-Length field, but this method could be overridden + * to change the behavior (to, e.g., throw an exception and produce an invalid message). + * <p> + * See: https://tools.ietf.org/html/rfc7230#section-3.3.3 + * <pre> + * If a message is received with both a Transfer-Encoding and a + * Content-Length header field, the Transfer-Encoding overrides the + * Content-Length. Such a message might indicate an attempt to + * perform request smuggling (Section 9.5) or response splitting + * (Section 9.4) and ought to be handled as an error. A sender MUST + * remove the received Content-Length field prior to forwarding such + * a message downstream. + * </pre> + * Also see: + * https://github.com/apache/tomcat/blob/b693d7c1981fa7f51e58bc8c8e72e3fe80b7b773/ + * java/org/apache/coyote/http11/Http11Processor.java#L747-L755 + * https://github.com/nginx/nginx/blob/0ad4393e30c119d250415cb769e3d8bc8dce5186/ + * src/http/ngx_http_request.c#L1946-L1953 + */ + protected void handleTransferEncodingChunkedWithContentLength(HttpMessage message) { + message.headers().remove(HttpHeaderNames.CONTENT_LENGTH); + contentLength = Long.MIN_VALUE; } private long contentLength() { @@ -640,49 +661,50 @@ public abstract class HttpObjectDecoder extends ByteToMessageDecoder { if (line == null) { return null; } + LastHttpContent trailer = this.trailer; + if (line.length() == 0 && trailer == null) { + // We have received the empty line which signals the trailer is complete and did not parse any trailers + // before. Just return an empty last content to reduce allocations. + return LastHttpContent.EMPTY_LAST_CONTENT; + } + CharSequence lastHeader = null; - if (line.length() > 0) { - LastHttpContent trailer = this.trailer; - if (trailer == null) { - trailer = this.trailer = new DefaultLastHttpContent(Unpooled.EMPTY_BUFFER, validateHeaders); - } - do { - char firstChar = line.charAt(0); - if (lastHeader != null && (firstChar == ' ' || firstChar == '\t')) { - List<String> current = trailer.trailingHeaders().getAll(lastHeader); - if (!current.isEmpty()) { - int lastPos = current.size() - 1; - //please do not make one line from below code - //as it breaks +XX:OptimizeStringConcat optimization - String lineTrimmed = line.toString().trim(); - String currentLastPos = current.get(lastPos); - current.set(lastPos, currentLastPos + lineTrimmed); - } - } else { - splitHeader(line); - CharSequence headerName = name; - if (!HttpHeaderNames.CONTENT_LENGTH.contentEqualsIgnoreCase(headerName) && + if (trailer == null) { + trailer = this.trailer = new DefaultLastHttpContent(Unpooled.EMPTY_BUFFER, validateHeaders); + } + while (line.length() > 0) { + char firstChar = line.charAtUnsafe(0); + if (lastHeader != null && (firstChar == ' ' || firstChar == '\t')) { + List<String> current = trailer.trailingHeaders().getAll(lastHeader); + if (!current.isEmpty()) { + int lastPos = current.size() - 1; + //please do not make one line from below code + //as it breaks +XX:OptimizeStringConcat optimization + String lineTrimmed = line.toString().trim(); + String currentLastPos = current.get(lastPos); + current.set(lastPos, currentLastPos + lineTrimmed); + } + } else { + splitHeader(line); + CharSequence headerName = name; + if (!HttpHeaderNames.CONTENT_LENGTH.contentEqualsIgnoreCase(headerName) && !HttpHeaderNames.TRANSFER_ENCODING.contentEqualsIgnoreCase(headerName) && !HttpHeaderNames.TRAILER.contentEqualsIgnoreCase(headerName)) { - trailer.trailingHeaders().add(headerName, value); - } - lastHeader = name; - // reset name and value fields - name = null; - value = null; - } - - line = headerParser.parse(buffer); - if (line == null) { - return null; + trailer.trailingHeaders().add(headerName, value); } - } while (line.length() > 0); - - this.trailer = null; - return trailer; + lastHeader = name; + // reset name and value fields + name = null; + value = null; + } + line = headerParser.parse(buffer); + if (line == null) { + return null; + } } - return LastHttpContent.EMPTY_LAST_CONTENT; + this.trailer = null; + return trailer; } protected abstract boolean isDecodingRequest(); @@ -710,13 +732,13 @@ public abstract class HttpObjectDecoder extends ByteToMessageDecoder { int cStart; int cEnd; - aStart = findNonWhitespace(sb, 0); - aEnd = findWhitespace(sb, aStart); + aStart = findNonSPLenient(sb, 0); + aEnd = findSPLenient(sb, aStart); - bStart = findNonWhitespace(sb, aEnd); - bEnd = findWhitespace(sb, bStart); + bStart = findNonSPLenient(sb, aEnd); + bEnd = findSPLenient(sb, bStart); - cStart = findNonWhitespace(sb, bEnd); + cStart = findNonSPLenient(sb, bEnd); cEnd = findEndOfString(sb); return new String[] { @@ -733,23 +755,42 @@ public abstract class HttpObjectDecoder extends ByteToMessageDecoder { int valueStart; int valueEnd; - nameStart = findNonWhitespace(sb, 0); + nameStart = findNonWhitespace(sb, 0, false); for (nameEnd = nameStart; nameEnd < length; nameEnd ++) { - char ch = sb.charAt(nameEnd); - if (ch == ':' || Character.isWhitespace(ch)) { + char ch = sb.charAtUnsafe(nameEnd); + // https://tools.ietf.org/html/rfc7230#section-3.2.4 + // + // No whitespace is allowed between the header field-name and colon. In + // the past, differences in the handling of such whitespace have led to + // security vulnerabilities in request routing and response handling. A + // server MUST reject any received request message that contains + // whitespace between a header field-name and colon with a response code + // of 400 (Bad Request). A proxy MUST remove any such whitespace from a + // response message before forwarding the message downstream. + if (ch == ':' || + // In case of decoding a request we will just continue processing and header validation + // is done in the DefaultHttpHeaders implementation. + // + // In the case of decoding a response we will "skip" the whitespace. + (!isDecodingRequest() && isOWS(ch))) { break; } } + if (nameEnd == length) { + // There was no colon present at all. + throw new IllegalArgumentException("No colon found"); + } + for (colonEnd = nameEnd; colonEnd < length; colonEnd ++) { - if (sb.charAt(colonEnd) == ':') { + if (sb.charAtUnsafe(colonEnd) == ':') { colonEnd ++; break; } } name = sb.subStringUnsafe(nameStart, nameEnd); - valueStart = findNonWhitespace(sb, colonEnd); + valueStart = findNonWhitespace(sb, colonEnd, true); if (valueStart == length) { value = EMPTY_VALUE; } else { @@ -758,19 +799,45 @@ public abstract class HttpObjectDecoder extends ByteToMessageDecoder { } } - private static int findNonWhitespace(AppendableCharSequence sb, int offset) { + private static int findNonSPLenient(AppendableCharSequence sb, int offset) { for (int result = offset; result < sb.length(); ++result) { - if (!Character.isWhitespace(sb.charAtUnsafe(result))) { + char c = sb.charAtUnsafe(result); + // See https://tools.ietf.org/html/rfc7230#section-3.5 + if (isSPLenient(c)) { + continue; + } + if (Character.isWhitespace(c)) { + // Any other whitespace delimiter is invalid + throw new IllegalArgumentException("Invalid separator"); + } + return result; + } + return sb.length(); + } + + private static int findSPLenient(AppendableCharSequence sb, int offset) { + for (int result = offset; result < sb.length(); ++result) { + if (isSPLenient(sb.charAtUnsafe(result))) { return result; } } return sb.length(); } - private static int findWhitespace(AppendableCharSequence sb, int offset) { + private static boolean isSPLenient(char c) { + // See https://tools.ietf.org/html/rfc7230#section-3.5 + return c == ' ' || c == (char) 0x09 || c == (char) 0x0B || c == (char) 0x0C || c == (char) 0x0D; + } + + private static int findNonWhitespace(AppendableCharSequence sb, int offset, boolean validateOWS) { for (int result = offset; result < sb.length(); ++result) { - if (Character.isWhitespace(sb.charAtUnsafe(result))) { + char c = sb.charAtUnsafe(result); + if (!Character.isWhitespace(c)) { return result; + } else if (validateOWS && !isOWS(c)) { + // Only OWS is supported for whitespace + throw new IllegalArgumentException("Invalid separator, only a single space or horizontal tab allowed," + + " but received a '" + c + "'"); } } return sb.length(); @@ -785,6 +852,10 @@ public abstract class HttpObjectDecoder extends ByteToMessageDecoder { return 0; } + private static boolean isOWS(char ch) { + return ch == ' ' || ch == (char) 0x09; + } + private static class HeaderParser implements ByteProcessor { private final AppendableCharSequence seq; private final int maxLength; @@ -814,13 +885,23 @@ public abstract class HttpObjectDecoder extends ByteToMessageDecoder { @Override public boolean process(byte value) throws Exception { char nextByte = (char) (value & 0xFF); - if (nextByte == HttpConstants.CR) { - return true; - } if (nextByte == HttpConstants.LF) { + int len = seq.length(); + // Drop CR if we had a CRLF pair + if (len >= 1 && seq.charAtUnsafe(len - 1) == HttpConstants.CR) { + -- size; + seq.setLength(len - 1); + } return false; } + increaseCount(); + + seq.append(nextByte); + return true; + } + + protected final void increaseCount() { if (++ size > maxLength) { // TODO: Respond with Bad Request and discard the traffic // or close the connection. @@ -828,9 +909,6 @@ public abstract class HttpObjectDecoder extends ByteToMessageDecoder { // If decoding a response, just throw an exception. throw newException(maxLength); } - - seq.append(nextByte); - return true; } protected TooLongFrameException newException(int maxLength) { @@ -838,7 +916,7 @@ public abstract class HttpObjectDecoder extends ByteToMessageDecoder { } } - private static final class LineParser extends HeaderParser { + private final class LineParser extends HeaderParser { LineParser(AppendableCharSequence seq, int maxLength) { super(seq, maxLength); @@ -850,6 +928,19 @@ public abstract class HttpObjectDecoder extends ByteToMessageDecoder { return super.parse(buffer); } + @Override + public boolean process(byte value) throws Exception { + if (currentState == State.SKIP_CONTROL_CHARS) { + char c = (char) (value & 0xFF); + if (Character.isISOControl(c) || Character.isWhitespace(c)) { + increaseCount(); + return true; + } + currentState = State.READ_INITIAL; + } + return super.process(value); + } + @Override protected TooLongFrameException newException(int maxLength) { return new TooLongFrameException("An HTTP line is larger than " + maxLength + " bytes."); diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/HttpResponseStatus.java b/codec-http/src/main/java/io/netty/handler/codec/http/HttpResponseStatus.java index b7e2c10..941ef49 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/HttpResponseStatus.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/HttpResponseStatus.java @@ -19,9 +19,11 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufUtil; import io.netty.util.AsciiString; import io.netty.util.CharsetUtil; +import io.netty.util.internal.ObjectUtil; import static io.netty.handler.codec.http.HttpConstants.SP; import static io.netty.util.ByteProcessor.FIND_ASCII_SPACE; +import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero; import static java.lang.Integer.parseInt; /** @@ -223,7 +225,7 @@ public class HttpResponseStatus implements Comparable<HttpResponseStatus> { /** * 421 Misdirected Request * - * <a href="https://tools.ietf.org/html/draft-ietf-httpbis-http2-15#section-9.1.2">421 Status Code</a> + * @see <a href="https://tools.ietf.org/html/rfc7540#section-9.1.2">421 (Misdirected Request) Status Code</a> */ public static final HttpResponseStatus MISDIRECTED_REQUEST = newStatus(421, "Misdirected Request"); @@ -538,14 +540,8 @@ public class HttpResponseStatus implements Comparable<HttpResponseStatus> { } private HttpResponseStatus(int code, String reasonPhrase, boolean bytes) { - if (code < 0) { - throw new IllegalArgumentException( - "code: " + code + " (expected: 0+)"); - } - - if (reasonPhrase == null) { - throw new NullPointerException("reasonPhrase"); - } + checkPositiveOrZero(code, "code"); + ObjectUtil.checkNotNull(reasonPhrase, "reasonPhrase"); for (int i = 0; i < reasonPhrase.length(); i ++) { char c = reasonPhrase.charAt(i); diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/HttpServerCodec.java b/codec-http/src/main/java/io/netty/handler/codec/http/HttpServerCodec.java index 4e8d613..6e36128 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/HttpServerCodec.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/HttpServerCodec.java @@ -81,16 +81,18 @@ public final class HttpServerCodec extends CombinedChannelDuplexHandler<HttpRequ } private final class HttpServerRequestDecoder extends HttpRequestDecoder { - public HttpServerRequestDecoder(int maxInitialLineLength, int maxHeaderSize, int maxChunkSize) { + + HttpServerRequestDecoder(int maxInitialLineLength, int maxHeaderSize, int maxChunkSize) { super(maxInitialLineLength, maxHeaderSize, maxChunkSize); } - public HttpServerRequestDecoder(int maxInitialLineLength, int maxHeaderSize, int maxChunkSize, + HttpServerRequestDecoder(int maxInitialLineLength, int maxHeaderSize, int maxChunkSize, boolean validateHeaders) { super(maxInitialLineLength, maxHeaderSize, maxChunkSize, validateHeaders); } - public HttpServerRequestDecoder(int maxInitialLineLength, int maxHeaderSize, int maxChunkSize, + HttpServerRequestDecoder(int maxInitialLineLength, int maxHeaderSize, int maxChunkSize, + boolean validateHeaders, int initialBufferSize) { super(maxInitialLineLength, maxHeaderSize, maxChunkSize, validateHeaders, initialBufferSize); } @@ -115,7 +117,8 @@ public final class HttpServerCodec extends CombinedChannelDuplexHandler<HttpRequ @Override protected void sanitizeHeadersBeforeEncode(HttpResponse msg, boolean isAlwaysEmpty) { - if (!isAlwaysEmpty && method == HttpMethod.CONNECT && msg.status().codeClass() == HttpStatusClass.SUCCESS) { + if (!isAlwaysEmpty && HttpMethod.CONNECT.equals(method) + && msg.status().codeClass() == HttpStatusClass.SUCCESS) { // Stripping Transfer-Encoding: // See https://tools.ietf.org/html/rfc7230#section-3.3.1 msg.headers().remove(HttpHeaderNames.TRANSFER_ENCODING); diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/HttpServerUpgradeHandler.java b/codec-http/src/main/java/io/netty/handler/codec/http/HttpServerUpgradeHandler.java index f1f3efc..2b54b0e 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/HttpServerUpgradeHandler.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/HttpServerUpgradeHandler.java @@ -14,9 +14,6 @@ */ package io.netty.handler.codec.http; -import static io.netty.util.AsciiString.containsContentEqualsIgnoreCase; -import static io.netty.util.AsciiString.containsAllContentEqualsIgnoreCase; - import io.netty.buffer.Unpooled; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; @@ -30,7 +27,10 @@ import java.util.List; import static io.netty.handler.codec.http.HttpResponseStatus.SWITCHING_PROTOCOLS; import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1; +import static io.netty.util.AsciiString.containsAllContentEqualsIgnoreCase; +import static io.netty.util.AsciiString.containsContentEqualsIgnoreCase; import static io.netty.util.internal.ObjectUtil.checkNotNull; +import static io.netty.util.internal.StringUtil.COMMA; /** * A server-side handler that receives HTTP requests and optionally performs a protocol switch if @@ -284,16 +284,23 @@ public class HttpServerUpgradeHandler extends HttpObjectAggregator { } // Make sure the CONNECTION header is present. - CharSequence connectionHeader = request.headers().get(HttpHeaderNames.CONNECTION); - if (connectionHeader == null) { + List<String> connectionHeaderValues = request.headers().getAll(HttpHeaderNames.CONNECTION); + + if (connectionHeaderValues == null) { return false; } + final StringBuilder concatenatedConnectionValue = new StringBuilder(connectionHeaderValues.size() * 10); + for (CharSequence connectionHeaderValue : connectionHeaderValues) { + concatenatedConnectionValue.append(connectionHeaderValue).append(COMMA); + } + concatenatedConnectionValue.setLength(concatenatedConnectionValue.length() - 1); + // Make sure the CONNECTION header contains UPGRADE as well as all protocol-specific headers. Collection<CharSequence> requiredHeaders = upgradeCodec.requiredUpgradeHeaders(); - List<CharSequence> values = splitHeader(connectionHeader); + List<CharSequence> values = splitHeader(concatenatedConnectionValue); if (!containsContentEqualsIgnoreCase(values, HttpHeaderNames.UPGRADE) || - !containsAllContentEqualsIgnoreCase(values, requiredHeaders)) { + !containsAllContentEqualsIgnoreCase(values, requiredHeaders)) { return false; } diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/HttpUtil.java b/codec-http/src/main/java/io/netty/handler/codec/http/HttpUtil.java index 94af790..31fd14d 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/HttpUtil.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/HttpUtil.java @@ -18,6 +18,7 @@ package io.netty.handler.codec.http; import java.net.InetSocketAddress; import java.net.URI; import java.nio.charset.Charset; +import java.nio.charset.IllegalCharsetNameException; import java.nio.charset.UnsupportedCharsetException; import java.util.ArrayList; import java.util.Iterator; @@ -26,6 +27,7 @@ import java.util.List; import io.netty.util.AsciiString; import io.netty.util.CharsetUtil; import io.netty.util.NetUtil; +import io.netty.util.internal.ObjectUtil; /** * Utility methods useful in the HTTP context. @@ -65,16 +67,9 @@ public final class HttpUtil { * {@link HttpVersion#isKeepAliveDefault()}. */ public static boolean isKeepAlive(HttpMessage message) { - CharSequence connection = message.headers().get(HttpHeaderNames.CONNECTION); - if (HttpHeaderValues.CLOSE.contentEqualsIgnoreCase(connection)) { - return false; - } - - if (message.protocolVersion().isKeepAliveDefault()) { - return !HttpHeaderValues.CLOSE.contentEqualsIgnoreCase(connection); - } else { - return HttpHeaderValues.KEEP_ALIVE.contentEqualsIgnoreCase(connection); - } + return !message.headers().containsValue(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE, true) && + (message.protocolVersion().isKeepAliveDefault() || + message.headers().containsValue(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE, true)); } /** @@ -251,13 +246,9 @@ public final class HttpUtil { * present */ public static boolean is100ContinueExpected(HttpMessage message) { - if (!isExpectHeaderValid(message)) { - return false; - } - - final String expectValue = message.headers().get(HttpHeaderNames.EXPECT); - // unquoted tokens in the expect header are case-insensitive, thus 100-continue is case insensitive - return HttpHeaderValues.CONTINUE.toString().equalsIgnoreCase(expectValue); + return isExpectHeaderValid(message) + // unquoted tokens in the expect header are case-insensitive, thus 100-continue is case insensitive + && message.headers().contains(HttpHeaderNames.EXPECT, HttpHeaderValues.CONTINUE, true); } /** @@ -402,15 +393,14 @@ public final class HttpUtil { if (charsetCharSequence != null) { try { return Charset.forName(charsetCharSequence.toString()); + } catch (IllegalCharsetNameException ignored) { + // just return the default charset } catch (UnsupportedCharsetException ignored) { - return defaultCharset; + // just return the default charset } - } else { - return defaultCharset; } - } else { - return defaultCharset; } + return defaultCharset; } /** @@ -459,9 +449,7 @@ public final class HttpUtil { * @throws NullPointerException in case if {@code contentTypeValue == null} */ public static CharSequence getCharsetAsSequence(CharSequence contentTypeValue) { - if (contentTypeValue == null) { - throw new NullPointerException("contentTypeValue"); - } + ObjectUtil.checkNotNull(contentTypeValue, "contentTypeValue"); int indexOfCharset = AsciiString.indexOfIgnoreCaseAscii(contentTypeValue, CHARSET_EQUALS, 0); if (indexOfCharset == AsciiString.INDEX_NOT_FOUND) { @@ -515,9 +503,7 @@ public final class HttpUtil { * @throws NullPointerException in case if {@code contentTypeValue == null} */ public static CharSequence getMimeType(CharSequence contentTypeValue) { - if (contentTypeValue == null) { - throw new NullPointerException("contentTypeValue"); - } + ObjectUtil.checkNotNull(contentTypeValue, "contentTypeValue"); int indexOfSemicolon = AsciiString.indexOfIgnoreCaseAscii(contentTypeValue, SEMICOLON, 0); if (indexOfSemicolon != AsciiString.INDEX_NOT_FOUND) { @@ -529,7 +515,7 @@ public final class HttpUtil { /** * Formats the host string of an address so it can be used for computing an HTTP component - * such as an URL or a Host header + * such as a URL or a Host header * * @param addr the address * @return the formatted String diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/HttpVersion.java b/codec-http/src/main/java/io/netty/handler/codec/http/HttpVersion.java index a643f42..dbf1bf6 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/HttpVersion.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/HttpVersion.java @@ -15,8 +15,11 @@ */ package io.netty.handler.codec.http; +import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero; + import io.netty.buffer.ByteBuf; import io.netty.util.CharsetUtil; +import io.netty.util.internal.ObjectUtil; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -53,9 +56,7 @@ public class HttpVersion implements Comparable<HttpVersion> { * returned. */ public static HttpVersion valueOf(String text) { - if (text == null) { - throw new NullPointerException("text"); - } + ObjectUtil.checkNotNull(text, "text"); text = text.trim(); @@ -107,9 +108,7 @@ public class HttpVersion implements Comparable<HttpVersion> { * the {@code "Connection"} header is set to {@code "close"} explicitly. */ public HttpVersion(String text, boolean keepAliveDefault) { - if (text == null) { - throw new NullPointerException("text"); - } + ObjectUtil.checkNotNull(text, "text"); text = text.trim().toUpperCase(); if (text.isEmpty()) { @@ -149,9 +148,7 @@ public class HttpVersion implements Comparable<HttpVersion> { private HttpVersion( String protocolName, int majorVersion, int minorVersion, boolean keepAliveDefault, boolean bytes) { - if (protocolName == null) { - throw new NullPointerException("protocolName"); - } + ObjectUtil.checkNotNull(protocolName, "protocolName"); protocolName = protocolName.trim().toUpperCase(); if (protocolName.isEmpty()) { @@ -165,12 +162,8 @@ public class HttpVersion implements Comparable<HttpVersion> { } } - if (majorVersion < 0) { - throw new IllegalArgumentException("negative majorVersion"); - } - if (minorVersion < 0) { - throw new IllegalArgumentException("negative minorVersion"); - } + checkPositiveOrZero(majorVersion, "majorVersion"); + checkPositiveOrZero(minorVersion, "minorVersion"); this.protocolName = protocolName; this.majorVersion = majorVersion; diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/QueryStringDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/QueryStringDecoder.java index 7c631f3..417074c 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/QueryStringDecoder.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/QueryStringDecoder.java @@ -16,23 +16,22 @@ package io.netty.handler.codec.http; import io.netty.util.CharsetUtil; +import io.netty.util.internal.PlatformDependent; import java.net.URI; import java.net.URLDecoder; -import java.nio.ByteBuffer; -import java.nio.CharBuffer; -import java.nio.charset.CharacterCodingException; import java.nio.charset.Charset; -import java.nio.charset.CharsetDecoder; -import java.nio.charset.CoderResult; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import static io.netty.util.internal.ObjectUtil.*; -import static io.netty.util.internal.StringUtil.*; +import static io.netty.util.internal.ObjectUtil.checkNotNull; +import static io.netty.util.internal.ObjectUtil.checkPositive; +import static io.netty.util.internal.StringUtil.EMPTY_STRING; +import static io.netty.util.internal.StringUtil.SPACE; +import static io.netty.util.internal.StringUtil.decodeHexByte; /** * Splits an HTTP query string into a path string and key-value parameter pairs. @@ -54,7 +53,7 @@ import static io.netty.util.internal.StringUtil.*; * * <h3>HashDOS vulnerability fix</h3> * - * As a workaround to the <a href="http://netty.io/s/hashdos">HashDOS</a> vulnerability, the decoder + * As a workaround to the <a href="https://netty.io/s/hashdos">HashDOS</a> vulnerability, the decoder * limits the maximum number of decoded key-value parameter pairs, up to {@literal 1024} by * default, and you can configure it when you construct the decoder by passing an additional * integer parameter. @@ -68,6 +67,7 @@ public class QueryStringDecoder { private final Charset charset; private final String uri; private final int maxParams; + private final boolean semicolonIsNormalChar; private int pathEndIdx; private String path; private Map<String, List<String>> params; @@ -109,9 +109,19 @@ public class QueryStringDecoder { * specified charset. */ public QueryStringDecoder(String uri, Charset charset, boolean hasPath, int maxParams) { + this(uri, charset, hasPath, maxParams, false); + } + + /** + * Creates a new decoder that decodes the specified URI encoded in the + * specified charset. + */ + public QueryStringDecoder(String uri, Charset charset, boolean hasPath, + int maxParams, boolean semicolonIsNormalChar) { this.uri = checkNotNull(uri, "uri"); this.charset = checkNotNull(charset, "charset"); this.maxParams = checkPositive(maxParams, "maxParams"); + this.semicolonIsNormalChar = semicolonIsNormalChar; // `-1` means that path end index will be initialized lazily pathEndIdx = hasPath ? -1 : 0; @@ -138,6 +148,14 @@ public class QueryStringDecoder { * specified charset. */ public QueryStringDecoder(URI uri, Charset charset, int maxParams) { + this(uri, charset, maxParams, false); + } + + /** + * Creates a new decoder that decodes the specified URI encoded in the + * specified charset. + */ + public QueryStringDecoder(URI uri, Charset charset, int maxParams, boolean semicolonIsNormalChar) { String rawPath = uri.getRawPath(); if (rawPath == null) { rawPath = EMPTY_STRING; @@ -147,6 +165,7 @@ public class QueryStringDecoder { this.uri = rawQuery == null? rawPath : rawPath + '?' + rawQuery; this.charset = checkNotNull(charset, "charset"); this.maxParams = checkPositive(maxParams, "maxParams"); + this.semicolonIsNormalChar = semicolonIsNormalChar; pathEndIdx = rawPath.length(); } @@ -177,7 +196,7 @@ public class QueryStringDecoder { */ public Map<String, List<String>> parameters() { if (params == null) { - params = decodeParams(uri, pathEndIdx(), charset, maxParams); + params = decodeParams(uri, pathEndIdx(), charset, maxParams, semicolonIsNormalChar); } return params; } @@ -204,7 +223,8 @@ public class QueryStringDecoder { return pathEndIdx; } - private static Map<String, List<String>> decodeParams(String s, int from, Charset charset, int paramsLimit) { + private static Map<String, List<String>> decodeParams(String s, int from, Charset charset, int paramsLimit, + boolean semicolonIsNormalChar) { int len = s.length(); if (from >= len) { return Collections.emptyMap(); @@ -226,8 +246,12 @@ public class QueryStringDecoder { valueStart = i + 1; } break; - case '&': case ';': + if (semicolonIsNormalChar) { + continue; + } + // fall-through + case '&': if (addParam(s, nameStart, valueStart, i, params, charset)) { paramsLimit--; if (paramsLimit == 0) { @@ -266,7 +290,7 @@ public class QueryStringDecoder { } /** - * Decodes a bit of an URL encoded by a browser. + * Decodes a bit of a URL encoded by a browser. * <p> * This is equivalent to calling {@link #decodeComponent(String, Charset)} * with the UTF-8 charset (recommended to comply with RFC 3986, Section 2). @@ -281,7 +305,7 @@ public class QueryStringDecoder { } /** - * Decodes a bit of an URL encoded by a browser. + * Decodes a bit of a URL encoded by a browser. * <p> * The string is expected to be encoded as per RFC 3986, Section 2. * This is the encoding used by JavaScript functions {@code encodeURI} @@ -326,12 +350,10 @@ public class QueryStringDecoder { return s.substring(from, toExcluded); } - CharsetDecoder decoder = CharsetUtil.decoder(charset); - // Each encoded byte takes 3 characters (e.g. "%20") int decodedCapacity = (toExcluded - firstEscaped) / 3; - ByteBuffer byteBuf = ByteBuffer.allocate(decodedCapacity); - CharBuffer charBuf = CharBuffer.allocate(decodedCapacity); + byte[] buf = PlatformDependent.allocateUninitializedArray(decodedCapacity); + int bufIdx; StringBuilder strBuf = new StringBuilder(len); strBuf.append(s, from, firstEscaped); @@ -343,31 +365,17 @@ public class QueryStringDecoder { continue; } - byteBuf.clear(); + bufIdx = 0; do { if (i + 3 > toExcluded) { throw new IllegalArgumentException("unterminated escape sequence at index " + i + " of: " + s); } - byteBuf.put(decodeHexByte(s, i + 1)); + buf[bufIdx++] = decodeHexByte(s, i + 1); i += 3; } while (i < toExcluded && s.charAt(i) == '%'); i--; - byteBuf.flip(); - charBuf.clear(); - CoderResult result = decoder.reset().decode(byteBuf, charBuf, true); - try { - if (!result.isUnderflow()) { - result.throwException(); - } - result = decoder.flush(charBuf); - if (!result.isUnderflow()) { - result.throwException(); - } - } catch (CharacterCodingException ex) { - throw new IllegalStateException(ex); - } - strBuf.append(charBuf.flip()); + strBuf.append(new String(buf, 0, bufIdx, charset)); } return strBuf.toString(); } diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/QueryStringEncoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/QueryStringEncoder.java index cb1de9f..b624016 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/QueryStringEncoder.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/QueryStringEncoder.java @@ -15,17 +15,18 @@ */ package io.netty.handler.codec.http; +import io.netty.buffer.ByteBufUtil; +import io.netty.util.CharsetUtil; import io.netty.util.internal.ObjectUtil; +import io.netty.util.internal.StringUtil; -import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; import java.net.URLEncoder; import java.nio.charset.Charset; -import java.nio.charset.UnsupportedCharsetException; /** - * Creates an URL-encoded URI from a path string and key-value parameter pairs. + * Creates a URL-encoded URI from a path string and key-value parameter pairs. * This encoder is for one time use only. Create a new instance for each URI. * * <pre> @@ -33,13 +34,16 @@ import java.nio.charset.UnsupportedCharsetException; * encoder.addParam("recipient", "world"); * assert encoder.toString().equals("/hello?recipient=world"); * </pre> + * * @see QueryStringDecoder */ public class QueryStringEncoder { - private final String charsetName; + private final Charset charset; private final StringBuilder uriBuilder; private boolean hasParams; + private static final byte WRITE_UTF_UNKNOWN = (byte) '?'; + private static final char[] CHAR_MAP = "0123456789ABCDEF".toCharArray(); /** * Creates a new encoder that encodes a URI that starts with the specified @@ -54,8 +58,9 @@ public class QueryStringEncoder { * path string in the specified charset. */ public QueryStringEncoder(String uri, Charset charset) { + ObjectUtil.checkNotNull(charset, "charset"); uriBuilder = new StringBuilder(uri); - charsetName = charset.name(); + this.charset = CharsetUtil.UTF_8.equals(charset) ? null : charset; } /** @@ -69,10 +74,19 @@ public class QueryStringEncoder { uriBuilder.append('?'); hasParams = true; } - appendComponent(name, charsetName, uriBuilder); + + encodeComponent(name); if (value != null) { uriBuilder.append('='); - appendComponent(value, charsetName, uriBuilder); + encodeComponent(value); + } + } + + private void encodeComponent(CharSequence s) { + if (charset == null) { + encodeUtf8Component(s); + } else { + encodeNonUtf8Component(s); } } @@ -95,28 +109,142 @@ public class QueryStringEncoder { return uriBuilder.toString(); } - private static void appendComponent(String s, String charset, StringBuilder sb) { - try { - s = URLEncoder.encode(s, charset); - } catch (UnsupportedEncodingException ignored) { - throw new UnsupportedCharsetException(charset); + /** + * Encode the String as per RFC 3986, Section 2. + * <p> + * There is a little different between the JDK's encode method : {@link URLEncoder#encode(String, String)}. + * The JDK's encoder encode the space to {@code +} and this method directly encode the blank to {@code %20} + * beyond that , this method reuse the {@link #uriBuilder} in this class rather then create a new one, + * thus generates less garbage for the GC. + * + * @param s The String to encode + */ + private void encodeNonUtf8Component(CharSequence s) { + //Don't allocate memory until needed + char[] buf = null; + + for (int i = 0, len = s.length(); i < len;) { + char c = s.charAt(i); + if (dontNeedEncoding(c)) { + uriBuilder.append(c); + i++; + } else { + int index = 0; + if (buf == null) { + buf = new char[s.length() - i]; + } + + do { + buf[index] = c; + index++; + i++; + } while (i < s.length() && !dontNeedEncoding(c = s.charAt(i))); + + byte[] bytes = new String(buf, 0, index).getBytes(charset); + + for (byte b : bytes) { + appendEncoded(b); + } + } } - // replace all '+' with "%20" - int idx = s.indexOf('+'); - if (idx == -1) { - sb.append(s); - return; + } + + /** + * @see ByteBufUtil#writeUtf8(io.netty.buffer.ByteBuf, CharSequence, int, int) + */ + private void encodeUtf8Component(CharSequence s) { + for (int i = 0, len = s.length(); i < len; i++) { + char c = s.charAt(i); + if (!dontNeedEncoding(c)) { + encodeUtf8Component(s, i, len); + return; + } } - sb.append(s, 0, idx).append("%20"); - int size = s.length(); - idx++; - for (; idx < size; idx++) { - char c = s.charAt(idx); - if (c != '+') { - sb.append(c); + uriBuilder.append(s); + } + + private void encodeUtf8Component(CharSequence s, int encodingStart, int len) { + if (encodingStart > 0) { + // Append non-encoded characters directly first. + uriBuilder.append(s, 0, encodingStart); + } + encodeUtf8ComponentSlow(s, encodingStart, len); + } + + private void encodeUtf8ComponentSlow(CharSequence s, int start, int len) { + for (int i = start; i < len; i++) { + char c = s.charAt(i); + if (c < 0x80) { + if (dontNeedEncoding(c)) { + uriBuilder.append(c); + } else { + appendEncoded(c); + } + } else if (c < 0x800) { + appendEncoded(0xc0 | (c >> 6)); + appendEncoded(0x80 | (c & 0x3f)); + } else if (StringUtil.isSurrogate(c)) { + if (!Character.isHighSurrogate(c)) { + appendEncoded(WRITE_UTF_UNKNOWN); + continue; + } + // Surrogate Pair consumes 2 characters. + if (++i == s.length()) { + appendEncoded(WRITE_UTF_UNKNOWN); + break; + } + // Extra method to allow inlining the rest of writeUtf8 which is the most likely code path. + writeUtf8Surrogate(c, s.charAt(i)); } else { - sb.append("%20"); + appendEncoded(0xe0 | (c >> 12)); + appendEncoded(0x80 | ((c >> 6) & 0x3f)); + appendEncoded(0x80 | (c & 0x3f)); } } } + + private void writeUtf8Surrogate(char c, char c2) { + if (!Character.isLowSurrogate(c2)) { + appendEncoded(WRITE_UTF_UNKNOWN); + appendEncoded(Character.isHighSurrogate(c2) ? WRITE_UTF_UNKNOWN : c2); + return; + } + int codePoint = Character.toCodePoint(c, c2); + // See http://www.unicode.org/versions/Unicode7.0.0/ch03.pdf#G2630. + appendEncoded(0xf0 | (codePoint >> 18)); + appendEncoded(0x80 | ((codePoint >> 12) & 0x3f)); + appendEncoded(0x80 | ((codePoint >> 6) & 0x3f)); + appendEncoded(0x80 | (codePoint & 0x3f)); + } + + private void appendEncoded(int b) { + uriBuilder.append('%').append(forDigit(b >> 4)).append(forDigit(b)); + } + + /** + * Convert the given digit to a upper hexadecimal char. + * + * @param digit the number to convert to a character. + * @return the {@code char} representation of the specified digit + * in hexadecimal. + */ + private static char forDigit(int digit) { + return CHAR_MAP[digit & 0xF]; + } + + /** + * Determines whether the given character is a unreserved character. + * <p> + * unreserved characters do not need to be encoded, and include uppercase and lowercase + * letters, decimal digits, hyphen, period, underscore, and tilde. + * <p> + * unreserved = ALPHA / DIGIT / "-" / "_" / "." / "*" + * + * @param ch the char to be judged whether it need to be encode + * @return true or false + */ + private static boolean dontNeedEncoding(char ch) { + return ch >= 'a' && ch <= 'z' || ch >= 'A' && ch <= 'Z' || ch >= '0' && ch <= '9' + || ch == '-' || ch == '_' || ch == '.' || ch == '*'; + } } diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/cookie/ClientCookieDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/cookie/ClientCookieDecoder.java index 74cace7..01a35f3 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/cookie/ClientCookieDecoder.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/cookie/ClientCookieDecoder.java @@ -16,6 +16,7 @@ package io.netty.handler.codec.http.cookie; import io.netty.handler.codec.DateFormatter; +import io.netty.handler.codec.http.cookie.CookieHeaderNames.SameSite; import java.util.Date; @@ -154,6 +155,7 @@ public final class ClientCookieDecoder extends CookieDecoder { private int expiresEnd; private boolean secure; private boolean httpOnly; + private SameSite sameSite; CookieBuilder(DefaultCookie cookie, String header) { this.cookie = cookie; @@ -180,6 +182,7 @@ public final class ClientCookieDecoder extends CookieDecoder { cookie.setMaxAge(mergeMaxAgeAndExpires()); cookie.setSecure(secure); cookie.setHttpOnly(httpOnly); + cookie.setSameSite(sameSite); return cookie; } @@ -206,7 +209,7 @@ public final class ClientCookieDecoder extends CookieDecoder { } else if (length == 7) { parse7(keyStart, valueStart, valueEnd); } else if (length == 8) { - parse8(keyStart); + parse8(keyStart, valueStart, valueEnd); } } @@ -241,9 +244,11 @@ public final class ClientCookieDecoder extends CookieDecoder { } } - private void parse8(int nameStart) { + private void parse8(int nameStart, int valueStart, int valueEnd) { if (header.regionMatches(true, nameStart, CookieHeaderNames.HTTPONLY, 0, 8)) { httpOnly = true; + } else if (header.regionMatches(true, nameStart, CookieHeaderNames.SAMESITE, 0, 8)) { + sameSite = SameSite.of(computeValue(valueStart, valueEnd)); } } diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/cookie/ClientCookieEncoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/cookie/ClientCookieEncoder.java index 0a66aaa..ac9bd03 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/cookie/ClientCookieEncoder.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/cookie/ClientCookieEncoder.java @@ -91,7 +91,8 @@ public final class ClientCookieEncoder extends CookieEncoder { * Sort cookies into decreasing order of path length, breaking ties by sorting into increasing chronological * order of creation time, as recommended by RFC 6265. */ - private static final Comparator<Cookie> COOKIE_COMPARATOR = new Comparator<Cookie>() { + // package-private for testing only. + static final Comparator<Cookie> COOKIE_COMPARATOR = new Comparator<Cookie>() { @Override public int compare(Cookie c1, Cookie c2) { String path1 = c1.path(); @@ -103,13 +104,10 @@ public final class ClientCookieEncoder extends CookieEncoder { // limited use. int len1 = path1 == null ? Integer.MAX_VALUE : path1.length(); int len2 = path2 == null ? Integer.MAX_VALUE : path2.length(); - int diff = len2 - len1; - if (diff != 0) { - return diff; - } - // Rely on Java's sort stability to retain creation order in cases where + + // Rely on Arrays.sort's stability to retain creation order in cases where // cookies have same path length. - return -1; + return len2 - len1; } }; diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/cookie/CookieHeaderNames.java b/codec-http/src/main/java/io/netty/handler/codec/http/cookie/CookieHeaderNames.java index 6d2e7f5..fef0567 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/cookie/CookieHeaderNames.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/cookie/CookieHeaderNames.java @@ -28,6 +28,35 @@ public final class CookieHeaderNames { public static final String HTTPONLY = "HTTPOnly"; + public static final String SAMESITE = "SameSite"; + + /** + * Possible values for the SameSite attribute. + * See <a href="https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-05">changes to RFC6265bis</a> + */ + public enum SameSite { + Lax, + Strict, + None; + + /** + * Return the enum value corresponding to the passed in same-site-flag, using a case insensitive comparison. + * + * @param name value for the SameSite Attribute + * @return enum value for the provided name or null + */ + static SameSite of(String name) { + if (name != null) { + for (SameSite each : SameSite.class.getEnumConstants()) { + if (each.name().equalsIgnoreCase(name)) { + return each; + } + } + } + return null; + } + } + private CookieHeaderNames() { // Unused. } diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/cookie/CookieUtil.java b/codec-http/src/main/java/io/netty/handler/codec/http/cookie/CookieUtil.java index 1e9d9c8..2e818b9 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/cookie/CookieUtil.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/cookie/CookieUtil.java @@ -97,24 +97,24 @@ final class CookieUtil { static void add(StringBuilder sb, String name, long val) { sb.append(name); - sb.append((char) HttpConstants.EQUALS); + sb.append('='); sb.append(val); - sb.append((char) HttpConstants.SEMICOLON); - sb.append((char) HttpConstants.SP); + sb.append(';'); + sb.append(HttpConstants.SP_CHAR); } static void add(StringBuilder sb, String name, String val) { sb.append(name); - sb.append((char) HttpConstants.EQUALS); + sb.append('='); sb.append(val); - sb.append((char) HttpConstants.SEMICOLON); - sb.append((char) HttpConstants.SP); + sb.append(';'); + sb.append(HttpConstants.SP_CHAR); } static void add(StringBuilder sb, String name) { sb.append(name); - sb.append((char) HttpConstants.SEMICOLON); - sb.append((char) HttpConstants.SP); + sb.append(';'); + sb.append(HttpConstants.SP_CHAR); } static void addQuoted(StringBuilder sb, String name, String val) { @@ -123,12 +123,12 @@ final class CookieUtil { } sb.append(name); - sb.append((char) HttpConstants.EQUALS); - sb.append((char) HttpConstants.DOUBLE_QUOTE); + sb.append('='); + sb.append('"'); sb.append(val); - sb.append((char) HttpConstants.DOUBLE_QUOTE); - sb.append((char) HttpConstants.SEMICOLON); - sb.append((char) HttpConstants.SP); + sb.append('"'); + sb.append(';'); + sb.append(HttpConstants.SP_CHAR); } static int firstInvalidCookieNameOctet(CharSequence cs) { diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/cookie/DefaultCookie.java b/codec-http/src/main/java/io/netty/handler/codec/http/cookie/DefaultCookie.java index cbd54cd..137a65d 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/cookie/DefaultCookie.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/cookie/DefaultCookie.java @@ -15,7 +15,10 @@ */ package io.netty.handler.codec.http.cookie; -import static io.netty.handler.codec.http.cookie.CookieUtil.*; +import io.netty.handler.codec.http.cookie.CookieHeaderNames.SameSite; + +import static io.netty.handler.codec.http.cookie.CookieUtil.stringBuilder; +import static io.netty.handler.codec.http.cookie.CookieUtil.validateAttributeValue; import static io.netty.util.internal.ObjectUtil.checkNotNull; /** @@ -31,6 +34,7 @@ public class DefaultCookie implements Cookie { private long maxAge = UNDEFINED_MAX_AGE; private boolean secure; private boolean httpOnly; + private SameSite sameSite; /** * Creates a new cookie with the specified name and value. @@ -119,6 +123,26 @@ public class DefaultCookie implements Cookie { this.httpOnly = httpOnly; } + /** + * Checks to see if this {@link Cookie} can be sent along cross-site requests. + * For more information, please look + * <a href="https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-05">here</a> + * @return <b>same-site-flag</b> value + */ + public SameSite sameSite() { + return sameSite; + } + + /** + * Determines if this this {@link Cookie} can be sent along cross-site requests. + * For more information, please look + * <a href="https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-05">here</a> + * @param sameSite <b>same-site-flag</b> value + */ + public void setSameSite(SameSite sameSite) { + this.sameSite = sameSite; + } + @Override public int hashCode() { return name().hashCode(); @@ -232,6 +256,9 @@ public class DefaultCookie implements Cookie { if (isHttpOnly()) { buf.append(", HTTPOnly"); } + if (sameSite() != null) { + buf.append(", SameSite=").append(sameSite()); + } return buf.toString(); } } diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/cookie/ServerCookieDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/cookie/ServerCookieDecoder.java index cf5349b..f56fb91 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/cookie/ServerCookieDecoder.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/cookie/ServerCookieDecoder.java @@ -17,7 +17,10 @@ package io.netty.handler.codec.http.cookie; import static io.netty.util.internal.ObjectUtil.checkNotNull; +import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; +import java.util.List; import java.util.Set; import java.util.TreeSet; @@ -56,20 +59,41 @@ public final class ServerCookieDecoder extends CookieDecoder { super(strict); } + /** + * Decodes the specified Set-Cookie HTTP header value into a {@link Cookie}. Unlike {@link #decode(String)}, this + * includes all cookie values present, even if they have the same name. + * + * @return the decoded {@link Cookie} + */ + public List<Cookie> decodeAll(String header) { + List<Cookie> cookies = new ArrayList<Cookie>(); + decode(cookies, header); + return Collections.unmodifiableList(cookies); + } + /** * Decodes the specified Set-Cookie HTTP header value into a {@link Cookie}. * * @return the decoded {@link Cookie} */ public Set<Cookie> decode(String header) { + Set<Cookie> cookies = new TreeSet<Cookie>(); + decode(cookies, header); + return cookies; + } + + /** + * Decodes the specified Set-Cookie HTTP header value into a {@link Cookie}. + * + * @return the decoded {@link Cookie} + */ + private void decode(Collection<? super Cookie> cookies, String header) { final int headerLen = checkNotNull(header, "header").length(); if (headerLen == 0) { - return Collections.emptySet(); + return; } - Set<Cookie> cookies = new TreeSet<Cookie>(); - int i = 0; boolean rfc2965Style = false; @@ -149,7 +173,5 @@ public final class ServerCookieDecoder extends CookieDecoder { cookies.add(cookie); } } - - return cookies; } } diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/cookie/ServerCookieEncoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/cookie/ServerCookieEncoder.java index b707dc3..b0ee21a 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/cookie/ServerCookieEncoder.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/cookie/ServerCookieEncoder.java @@ -15,12 +15,6 @@ */ package io.netty.handler.codec.http.cookie; -import static io.netty.handler.codec.http.cookie.CookieUtil.add; -import static io.netty.handler.codec.http.cookie.CookieUtil.addQuoted; -import static io.netty.handler.codec.http.cookie.CookieUtil.stringBuilder; -import static io.netty.handler.codec.http.cookie.CookieUtil.stripTrailingSeparator; -import static io.netty.util.internal.ObjectUtil.checkNotNull; - import io.netty.handler.codec.DateFormatter; import io.netty.handler.codec.http.HttpConstants; import io.netty.handler.codec.http.HttpResponse; @@ -34,6 +28,12 @@ import java.util.Iterator; import java.util.List; import java.util.Map; +import static io.netty.handler.codec.http.cookie.CookieUtil.add; +import static io.netty.handler.codec.http.cookie.CookieUtil.addQuoted; +import static io.netty.handler.codec.http.cookie.CookieUtil.stringBuilder; +import static io.netty.handler.codec.http.cookie.CookieUtil.stripTrailingSeparator; +import static io.netty.util.internal.ObjectUtil.checkNotNull; + /** * A <a href="http://tools.ietf.org/html/rfc6265">RFC6265</a> compliant cookie encoder to be used server side, * so some fields are sent (Version is typically ignored). @@ -105,10 +105,10 @@ public final class ServerCookieEncoder extends CookieEncoder { add(buf, CookieHeaderNames.MAX_AGE, cookie.maxAge()); Date expires = new Date(cookie.maxAge() * 1000 + System.currentTimeMillis()); buf.append(CookieHeaderNames.EXPIRES); - buf.append((char) HttpConstants.EQUALS); + buf.append('='); DateFormatter.append(expires, buf); - buf.append((char) HttpConstants.SEMICOLON); - buf.append((char) HttpConstants.SP); + buf.append(';'); + buf.append(HttpConstants.SP_CHAR); } if (cookie.path() != null) { @@ -124,6 +124,12 @@ public final class ServerCookieEncoder extends CookieEncoder { if (cookie.isHttpOnly()) { add(buf, CookieHeaderNames.HTTPONLY); } + if (cookie instanceof DefaultCookie) { + DefaultCookie c = (DefaultCookie) cookie; + if (c.sameSite() != null) { + add(buf, CookieHeaderNames.SAMESITE, c.sameSite().name()); + } + } return stripTrailingSeparator(buf); } diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/cors/CorsConfig.java b/codec-http/src/main/java/io/netty/handler/codec/http/cors/CorsConfig.java index 5ccae5d..e6036f7 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/cors/CorsConfig.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/cors/CorsConfig.java @@ -219,7 +219,7 @@ public final class CorsConfig { * * CORS headers are set after a request is processed. This may not always be desired * and this setting will check that the Origin is valid and if it is not valid no - * further processing will take place, and a error will be returned to the calling client. + * further processing will take place, and an error will be returned to the calling client. * * @return {@code true} if a CORS request should short-circuit upon receiving an invalid Origin header. */ diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/cors/CorsConfigBuilder.java b/codec-http/src/main/java/io/netty/handler/codec/http/cors/CorsConfigBuilder.java index c55eed1..c07369d 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/cors/CorsConfigBuilder.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/cors/CorsConfigBuilder.java @@ -341,7 +341,7 @@ public final class CorsConfigBuilder { * * CORS headers are set after a request is processed. This may not always be desired * and this setting will check that the Origin is valid and if it is not valid no - * further processing will take place, and a error will be returned to the calling client. + * further processing will take place, and an error will be returned to the calling client. * * @return {@link CorsConfigBuilder} to support method chaining. */ diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/cors/CorsHandler.java b/codec-http/src/main/java/io/netty/handler/codec/http/cors/CorsHandler.java index 39f79a4..3176957 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/cors/CorsHandler.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/cors/CorsHandler.java @@ -191,7 +191,7 @@ public class CorsHandler extends ChannelDuplexHandler { private static boolean isPreflightRequest(final HttpRequest request) { final HttpHeaders headers = request.headers(); - return request.method().equals(OPTIONS) && + return OPTIONS.equals(request.method()) && headers.contains(HttpHeaderNames.ORIGIN) && headers.contains(HttpHeaderNames.ACCESS_CONTROL_REQUEST_METHOD); } @@ -228,7 +228,8 @@ public class CorsHandler extends ChannelDuplexHandler { } private static void forbidden(final ChannelHandlerContext ctx, final HttpRequest request) { - HttpResponse response = new DefaultFullHttpResponse(request.protocolVersion(), FORBIDDEN); + HttpResponse response = new DefaultFullHttpResponse( + request.protocolVersion(), FORBIDDEN, ctx.alloc().buffer(0)); response.headers().set(HttpHeaderNames.CONTENT_LENGTH, HttpHeaderValues.ZERO); release(request); respond(ctx, request, response); diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/AbstractDiskHttpData.java b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/AbstractDiskHttpData.java index 544bc7c..71357a1 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/AbstractDiskHttpData.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/AbstractDiskHttpData.java @@ -18,14 +18,14 @@ package io.netty.handler.codec.http.multipart; import io.netty.buffer.ByteBuf; import io.netty.handler.codec.http.HttpConstants; import io.netty.util.internal.EmptyArrays; +import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.RandomAccessFile; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.nio.charset.Charset; @@ -100,9 +100,7 @@ public abstract class AbstractDiskHttpData extends AbstractHttpData { @Override public void setContent(ByteBuf buffer) throws IOException { - if (buffer == null) { - throw new NullPointerException("buffer"); - } + ObjectUtil.checkNotNull(buffer, "buffer"); try { size = buffer.readableBytes(); checkSize(size); @@ -125,9 +123,10 @@ public abstract class AbstractDiskHttpData extends AbstractHttpData { } return; } - FileOutputStream outputStream = new FileOutputStream(file); + RandomAccessFile accessFile = new RandomAccessFile(file, "rw"); + accessFile.setLength(0); try { - FileChannel localfileChannel = outputStream.getChannel(); + FileChannel localfileChannel = accessFile.getChannel(); ByteBuffer byteBuffer = buffer.nioBuffer(); int written = 0; while (written < size) { @@ -136,7 +135,7 @@ public abstract class AbstractDiskHttpData extends AbstractHttpData { buffer.readerIndex(buffer.readerIndex() + written); localfileChannel.force(false); } finally { - outputStream.close(); + accessFile.close(); } setCompleted(); } finally { @@ -163,8 +162,8 @@ public abstract class AbstractDiskHttpData extends AbstractHttpData { file = tempFile(); } if (fileChannel == null) { - FileOutputStream outputStream = new FileOutputStream(file); - fileChannel = outputStream.getChannel(); + RandomAccessFile accessFile = new RandomAccessFile(file, "rw"); + fileChannel = accessFile.getChannel(); } while (written < localsize) { written += fileChannel.write(byteBuffer); @@ -182,17 +181,15 @@ public abstract class AbstractDiskHttpData extends AbstractHttpData { file = tempFile(); } if (fileChannel == null) { - FileOutputStream outputStream = new FileOutputStream(file); - fileChannel = outputStream.getChannel(); + RandomAccessFile accessFile = new RandomAccessFile(file, "rw"); + fileChannel = accessFile.getChannel(); } fileChannel.force(false); fileChannel.close(); fileChannel = null; setCompleted(); } else { - if (buffer == null) { - throw new NullPointerException("buffer"); - } + ObjectUtil.checkNotNull(buffer, "buffer"); } } @@ -210,17 +207,16 @@ public abstract class AbstractDiskHttpData extends AbstractHttpData { @Override public void setContent(InputStream inputStream) throws IOException { - if (inputStream == null) { - throw new NullPointerException("inputStream"); - } + ObjectUtil.checkNotNull(inputStream, "inputStream"); if (file != null) { delete(); } file = tempFile(); - FileOutputStream outputStream = new FileOutputStream(file); + RandomAccessFile accessFile = new RandomAccessFile(file, "rw"); + accessFile.setLength(0); int written = 0; try { - FileChannel localfileChannel = outputStream.getChannel(); + FileChannel localfileChannel = accessFile.getChannel(); byte[] bytes = new byte[4096 * 4]; ByteBuffer byteBuffer = ByteBuffer.wrap(bytes); int read = inputStream.read(bytes); @@ -232,7 +228,7 @@ public abstract class AbstractDiskHttpData extends AbstractHttpData { } localfileChannel.force(false); } finally { - outputStream.close(); + accessFile.close(); } size = written; if (definedSize > 0 && definedSize < size) { @@ -290,8 +286,8 @@ public abstract class AbstractDiskHttpData extends AbstractHttpData { return EMPTY_BUFFER; } if (fileChannel == null) { - FileInputStream inputStream = new FileInputStream(file); - fileChannel = inputStream.getChannel(); + RandomAccessFile accessFile = new RandomAccessFile(file, "r"); + fileChannel = accessFile.getChannel(); } int read = 0; ByteBuffer byteBuffer = ByteBuffer.allocate(length); @@ -340,24 +336,22 @@ public abstract class AbstractDiskHttpData extends AbstractHttpData { @Override public boolean renameTo(File dest) throws IOException { - if (dest == null) { - throw new NullPointerException("dest"); - } + ObjectUtil.checkNotNull(dest, "dest"); if (file == null) { throw new IOException("No file defined so cannot be renamed"); } if (!file.renameTo(dest)) { // must copy IOException exception = null; - FileInputStream inputStream = null; - FileOutputStream outputStream = null; + RandomAccessFile inputAccessFile = null; + RandomAccessFile outputAccessFile = null; long chunkSize = 8196; long position = 0; try { - inputStream = new FileInputStream(file); - outputStream = new FileOutputStream(dest); - FileChannel in = inputStream.getChannel(); - FileChannel out = outputStream.getChannel(); + inputAccessFile = new RandomAccessFile(file, "r"); + outputAccessFile = new RandomAccessFile(dest, "rw"); + FileChannel in = inputAccessFile.getChannel(); + FileChannel out = outputAccessFile.getChannel(); while (position < size) { if (chunkSize < size - position) { chunkSize = size - position; @@ -367,9 +361,9 @@ public abstract class AbstractDiskHttpData extends AbstractHttpData { } catch (IOException e) { exception = e; } finally { - if (inputStream != null) { + if (inputAccessFile != null) { try { - inputStream.close(); + inputAccessFile.close(); } catch (IOException e) { if (exception == null) { // Choose to report the first exception exception = e; @@ -378,9 +372,9 @@ public abstract class AbstractDiskHttpData extends AbstractHttpData { } } } - if (outputStream != null) { + if (outputAccessFile != null) { try { - outputStream.close(); + outputAccessFile.close(); } catch (IOException e) { if (exception == null) { // Choose to report the first exception exception = e; @@ -422,17 +416,17 @@ public abstract class AbstractDiskHttpData extends AbstractHttpData { throw new IllegalArgumentException( "File too big to be loaded in memory"); } - FileInputStream inputStream = new FileInputStream(src); + RandomAccessFile accessFile = new RandomAccessFile(src, "r"); byte[] array = new byte[(int) srcsize]; try { - FileChannel fileChannel = inputStream.getChannel(); + FileChannel fileChannel = accessFile.getChannel(); ByteBuffer byteBuffer = ByteBuffer.wrap(array); int read = 0; while (read < srcsize) { read += fileChannel.read(byteBuffer); } } finally { - inputStream.close(); + accessFile.close(); } return array; } diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/AbstractHttpData.java b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/AbstractHttpData.java index ff05753..03f0582 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/AbstractHttpData.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/AbstractHttpData.java @@ -19,6 +19,7 @@ import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelException; import io.netty.handler.codec.http.HttpConstants; import io.netty.util.AbstractReferenceCounted; +import io.netty.util.internal.ObjectUtil; import java.io.IOException; import java.nio.charset.Charset; @@ -40,9 +41,7 @@ public abstract class AbstractHttpData extends AbstractReferenceCounted implemen private long maxSize = DefaultHttpDataFactory.MAXSIZE; protected AbstractHttpData(String name, Charset charset, long size) { - if (name == null) { - throw new NullPointerException("name"); - } + ObjectUtil.checkNotNull(name, "name"); name = REPLACE_PATTERN.matcher(name).replaceAll(" "); name = STRIP_PATTERN.matcher(name).replaceAll(""); @@ -59,7 +58,9 @@ public abstract class AbstractHttpData extends AbstractReferenceCounted implemen } @Override - public long getMaxSize() { return maxSize; } + public long getMaxSize() { + return maxSize; + } @Override public void setMaxSize(long maxSize) { @@ -94,10 +95,7 @@ public abstract class AbstractHttpData extends AbstractReferenceCounted implemen @Override public void setCharset(Charset charset) { - if (charset == null) { - throw new NullPointerException("charset"); - } - this.charset = charset; + this.charset = ObjectUtil.checkNotNull(charset, "charset"); } @Override diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/AbstractMemoryHttpData.java b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/AbstractMemoryHttpData.java index 31aa9ce..a08cdad 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/AbstractMemoryHttpData.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/AbstractMemoryHttpData.java @@ -18,12 +18,12 @@ package io.netty.handler.codec.http.multipart; import io.netty.buffer.ByteBuf; import io.netty.buffer.CompositeByteBuf; import io.netty.handler.codec.http.HttpConstants; +import io.netty.util.internal.ObjectUtil; import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.RandomAccessFile; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.nio.charset.Charset; @@ -47,9 +47,7 @@ public abstract class AbstractMemoryHttpData extends AbstractHttpData { @Override public void setContent(ByteBuf buffer) throws IOException { - if (buffer == null) { - throw new NullPointerException("buffer"); - } + ObjectUtil.checkNotNull(buffer, "buffer"); long localsize = buffer.readableBytes(); checkSize(localsize); if (definedSize > 0 && definedSize < localsize) { @@ -66,9 +64,8 @@ public abstract class AbstractMemoryHttpData extends AbstractHttpData { @Override public void setContent(InputStream inputStream) throws IOException { - if (inputStream == null) { - throw new NullPointerException("inputStream"); - } + ObjectUtil.checkNotNull(inputStream, "inputStream"); + ByteBuf buffer = buffer(); byte[] bytes = new byte[4096 * 4]; int read = inputStream.read(bytes); @@ -115,25 +112,21 @@ public abstract class AbstractMemoryHttpData extends AbstractHttpData { if (last) { setCompleted(); } else { - if (buffer == null) { - throw new NullPointerException("buffer"); - } + ObjectUtil.checkNotNull(buffer, "buffer"); } } @Override public void setContent(File file) throws IOException { - if (file == null) { - throw new NullPointerException("file"); - } + ObjectUtil.checkNotNull(file, "file"); + long newsize = file.length(); if (newsize > Integer.MAX_VALUE) { - throw new IllegalArgumentException( - "File too big to be loaded in memory"); + throw new IllegalArgumentException("File too big to be loaded in memory"); } checkSize(newsize); - FileInputStream inputStream = new FileInputStream(file); - FileChannel fileChannel = inputStream.getChannel(); + RandomAccessFile accessFile = new RandomAccessFile(file, "r"); + FileChannel fileChannel = accessFile.getChannel(); byte[] array = new byte[(int) newsize]; ByteBuffer byteBuffer = ByteBuffer.wrap(array); int read = 0; @@ -141,7 +134,7 @@ public abstract class AbstractMemoryHttpData extends AbstractHttpData { read += fileChannel.read(byteBuffer); } fileChannel.close(); - inputStream.close(); + accessFile.close(); byteBuffer.flip(); if (byteBuf != null) { byteBuf.release(); @@ -222,9 +215,7 @@ public abstract class AbstractMemoryHttpData extends AbstractHttpData { @Override public boolean renameTo(File dest) throws IOException { - if (dest == null) { - throw new NullPointerException("dest"); - } + ObjectUtil.checkNotNull(dest, "dest"); if (byteBuf == null) { // empty file if (!dest.createNewFile()) { @@ -233,8 +224,8 @@ public abstract class AbstractMemoryHttpData extends AbstractHttpData { return true; } int length = byteBuf.readableBytes(); - FileOutputStream outputStream = new FileOutputStream(dest); - FileChannel fileChannel = outputStream.getChannel(); + RandomAccessFile accessFile = new RandomAccessFile(dest, "rw"); + FileChannel fileChannel = accessFile.getChannel(); int written = 0; if (byteBuf.nioBufferCount() == 1) { ByteBuffer byteBuffer = byteBuf.nioBuffer(); @@ -250,7 +241,7 @@ public abstract class AbstractMemoryHttpData extends AbstractHttpData { fileChannel.force(false); fileChannel.close(); - outputStream.close(); + accessFile.close(); return written == length; } diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/DiskAttribute.java b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/DiskAttribute.java index 7439188..7905273 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/DiskAttribute.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/DiskAttribute.java @@ -18,6 +18,7 @@ package io.netty.handler.codec.http.multipart; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelException; import io.netty.handler.codec.http.HttpConstants; +import io.netty.util.internal.ObjectUtil; import java.io.IOException; import java.nio.charset.Charset; @@ -77,9 +78,7 @@ public class DiskAttribute extends AbstractDiskHttpData implements Attribute { @Override public void setValue(String value) throws IOException { - if (value == null) { - throw new NullPointerException("value"); - } + ObjectUtil.checkNotNull(value, "value"); byte [] bytes = value.getBytes(getCharset()); checkSize(bytes.length); ByteBuf buffer = wrappedBuffer(bytes); diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/DiskFileUpload.java b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/DiskFileUpload.java index 1a5076f..adb16a1 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/DiskFileUpload.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/DiskFileUpload.java @@ -19,6 +19,7 @@ import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelException; import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpHeaderValues; +import io.netty.util.internal.ObjectUtil; import java.io.File; import java.io.IOException; @@ -62,10 +63,7 @@ public class DiskFileUpload extends AbstractDiskHttpData implements FileUpload { @Override public void setFilename(String filename) { - if (filename == null) { - throw new NullPointerException("filename"); - } - this.filename = filename; + this.filename = ObjectUtil.checkNotNull(filename, "filename"); } @Override @@ -93,10 +91,7 @@ public class DiskFileUpload extends AbstractDiskHttpData implements FileUpload { @Override public void setContentType(String contentType) { - if (contentType == null) { - throw new NullPointerException("contentType"); - } - this.contentType = contentType; + this.contentType = ObjectUtil.checkNotNull(contentType, "contentType"); } @Override diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostMultipartRequestDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostMultipartRequestDecoder.java index 4fefc11..4d59e4d 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostMultipartRequestDecoder.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostMultipartRequestDecoder.java @@ -764,8 +764,6 @@ public class HttpPostMultipartRequestDecoder implements InterfaceHttpPostRequest } } } - } else { - throw new ErrorDataDecoderException("Unknown Params: " + newline); } } // Is it a FileUpload @@ -813,7 +811,7 @@ public class HttpPostMultipartRequestDecoder implements InterfaceHttpPostRequest } else if (FILENAME_ENCODED.equals(name)) { try { name = HttpHeaderValues.FILENAME.toString(); - String[] split = value.split("'", 3); + String[] split = cleanString(value).split("'", 3); value = QueryStringDecoder.decodeComponent(split[2], Charset.forName(split[0])); } catch (ArrayIndexOutOfBoundsException e) { throw new ErrorDataDecoderException(e); @@ -933,19 +931,15 @@ public class HttpPostMultipartRequestDecoder implements InterfaceHttpPostRequest */ @Override public void destroy() { - checkDestroyed(); + // Release all data items, including those not yet pulled cleanFiles(); + destroyed = true; if (undecodedChunk != null && undecodedChunk.refCnt() > 0) { undecodedChunk.release(); undecodedChunk = null; } - - // release all data which was not yet pulled - for (int i = bodyListHttpDataRank; i < bodyListHttpData.size(); i++) { - bodyListHttpData.get(i).release(); - } } /** diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostRequestDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostRequestDecoder.java index 0c10626..0183b23 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostRequestDecoder.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostRequestDecoder.java @@ -21,6 +21,7 @@ import io.netty.handler.codec.http.HttpContent; import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpHeaderValues; import io.netty.handler.codec.http.HttpRequest; +import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.StringUtil; import java.nio.charset.Charset; @@ -83,15 +84,10 @@ public class HttpPostRequestDecoder implements InterfaceHttpPostRequestDecoder { * errors */ public HttpPostRequestDecoder(HttpDataFactory factory, HttpRequest request, Charset charset) { - if (factory == null) { - throw new NullPointerException("factory"); - } - if (request == null) { - throw new NullPointerException("request"); - } - if (charset == null) { - throw new NullPointerException("charset"); - } + ObjectUtil.checkNotNull(factory, "factory"); + ObjectUtil.checkNotNull(request, "request"); + ObjectUtil.checkNotNull(charset, "charset"); + // Fill default values if (isMultipart(request)) { decoder = new HttpPostMultipartRequestDecoder(factory, request, charset); @@ -140,11 +136,11 @@ public class HttpPostRequestDecoder implements InterfaceHttpPostRequestDecoder { * @return True if the request is a Multipart request */ public static boolean isMultipart(HttpRequest request) { - if (request.headers().contains(HttpHeaderNames.CONTENT_TYPE)) { - return getMultipartDataBoundary(request.headers().get(HttpHeaderNames.CONTENT_TYPE)) != null; - } else { - return false; + String mimeType = request.headers().get(HttpHeaderNames.CONTENT_TYPE); + if (mimeType != null && mimeType.startsWith(HttpHeaderValues.MULTIPART_FORM_DATA.toString())) { + return getMultipartDataBoundary(mimeType) != null; } + return false; } /** diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostRequestEncoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostRequestEncoder.java index 21faffe..f42e7b0 100755 --- a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostRequestEncoder.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostRequestEncoder.java @@ -34,6 +34,7 @@ import io.netty.handler.codec.http.HttpUtil; import io.netty.handler.codec.http.HttpVersion; import io.netty.handler.codec.http.LastHttpContent; import io.netty.handler.stream.ChunkedInput; +import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.PlatformDependent; import io.netty.util.internal.StringUtil; @@ -310,9 +311,7 @@ public class HttpPostRequestEncoder implements ChunkedInput<HttpContent> { * if the encoding is in error or if the finalize were already done */ public void setBodyHttpDatas(List<InterfaceHttpData> datas) throws ErrorDataEncoderException { - if (datas == null) { - throw new NullPointerException("datas"); - } + ObjectUtil.checkNotNull(datas, "datas"); globalBodySize = 0; bodyListDatas.clear(); currentFileUpload = null; @@ -638,7 +637,7 @@ public class HttpPostRequestEncoder implements ChunkedInput<HttpContent> { replacement.append("; ") .append(HttpHeaderValues.FILENAME) .append("=\"") - .append(fileUpload.getFilename()) + .append(currentFileUpload.getFilename()) .append('"'); } @@ -867,7 +866,7 @@ public class HttpPostRequestEncoder implements ChunkedInput<HttpContent> { /** * - * @return the next ByteBuf to send as a HttpChunk and modifying currentBuffer accordingly + * @return the next ByteBuf to send as an HttpChunk and modifying currentBuffer accordingly */ private ByteBuf fillByteBuf() { int length = currentBuffer.readableBytes(); @@ -977,7 +976,11 @@ public class HttpPostRequestEncoder implements ChunkedInput<HttpContent> { if (buffer.capacity() == 0) { currentData = null; if (currentBuffer == null) { - currentBuffer = delimiter; + if (delimiter == null) { + return null; + } else { + currentBuffer = delimiter; + } } else { if (delimiter != null) { currentBuffer = wrappedBuffer(currentBuffer, delimiter); diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostStandardRequestDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostStandardRequestDecoder.java index 88cf05e..7b94a7c 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostStandardRequestDecoder.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostStandardRequestDecoder.java @@ -26,6 +26,7 @@ import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder.EndOfDataDec import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder.ErrorDataDecoderException; import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder.MultiPartStatus; import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder.NotEnoughDataDecoderException; +import io.netty.util.internal.PlatformDependent; import java.io.IOException; import java.nio.charset.Charset; @@ -148,13 +149,18 @@ public class HttpPostStandardRequestDecoder implements InterfaceHttpPostRequestD this.request = checkNotNull(request, "request"); this.charset = checkNotNull(charset, "charset"); this.factory = checkNotNull(factory, "factory"); - if (request instanceof HttpContent) { - // Offer automatically if the given request is als type of HttpContent - // See #1089 - offer((HttpContent) request); - } else { - undecodedChunk = buffer(); - parseBody(); + try { + if (request instanceof HttpContent) { + // Offer automatically if the given request is als type of HttpContent + // See #1089 + offer((HttpContent) request); + } else { + undecodedChunk = buffer(); + parseBody(); + } + } catch (Throwable e) { + destroy(); + PlatformDependent.throwException(e); } } @@ -481,6 +487,10 @@ public class HttpPostStandardRequestDecoder implements InterfaceHttpPostRequestD // error while decoding undecodedChunk.readerIndex(firstpos); throw new ErrorDataDecoderException(e); + } catch (IllegalArgumentException e) { + // error while decoding + undecodedChunk.readerIndex(firstpos); + throw new ErrorDataDecoderException(e); } } diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/InternalAttribute.java b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/InternalAttribute.java index 991100e..ccb17cd 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/InternalAttribute.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/InternalAttribute.java @@ -18,6 +18,7 @@ package io.netty.handler.codec.http.multipart; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.util.AbstractReferenceCounted; +import io.netty.util.internal.ObjectUtil; import java.nio.charset.Charset; import java.util.ArrayList; @@ -42,27 +43,21 @@ final class InternalAttribute extends AbstractReferenceCounted implements Interf } public void addValue(String value) { - if (value == null) { - throw new NullPointerException("value"); - } + ObjectUtil.checkNotNull(value, "value"); ByteBuf buf = Unpooled.copiedBuffer(value, charset); this.value.add(buf); size += buf.readableBytes(); } public void addValue(String value, int rank) { - if (value == null) { - throw new NullPointerException("value"); - } + ObjectUtil.checkNotNull(value, "value"); ByteBuf buf = Unpooled.copiedBuffer(value, charset); this.value.add(rank, buf); size += buf.readableBytes(); } public void setValue(String value, int rank) { - if (value == null) { - throw new NullPointerException("value"); - } + ObjectUtil.checkNotNull(value, "value"); ByteBuf buf = Unpooled.copiedBuffer(value, charset); ByteBuf old = this.value.set(rank, buf); if (old != null) { diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/MemoryAttribute.java b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/MemoryAttribute.java index 63c8c34..780929c 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/MemoryAttribute.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/MemoryAttribute.java @@ -18,6 +18,7 @@ package io.netty.handler.codec.http.multipart; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelException; import io.netty.handler.codec.http.HttpConstants; +import io.netty.util.internal.ObjectUtil; import java.io.IOException; import java.nio.charset.Charset; @@ -66,9 +67,7 @@ public class MemoryAttribute extends AbstractMemoryHttpData implements Attribute @Override public void setValue(String value) throws IOException { - if (value == null) { - throw new NullPointerException("value"); - } + ObjectUtil.checkNotNull(value, "value"); byte [] bytes = value.getBytes(getCharset()); checkSize(bytes.length); ByteBuf buffer = wrappedBuffer(bytes); diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/MemoryFileUpload.java b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/MemoryFileUpload.java index 28e3859..88e08f8 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/MemoryFileUpload.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/MemoryFileUpload.java @@ -19,6 +19,7 @@ import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelException; import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpHeaderValues; +import io.netty.util.internal.ObjectUtil; import java.io.IOException; import java.nio.charset.Charset; @@ -56,10 +57,7 @@ public class MemoryFileUpload extends AbstractMemoryHttpData implements FileUplo @Override public void setFilename(String filename) { - if (filename == null) { - throw new NullPointerException("filename"); - } - this.filename = filename; + this.filename = ObjectUtil.checkNotNull(filename, "filename"); } @Override @@ -87,10 +85,7 @@ public class MemoryFileUpload extends AbstractMemoryHttpData implements FileUplo @Override public void setContentType(String contentType) { - if (contentType == null) { - throw new NullPointerException("contentType"); - } - this.contentType = contentType; + this.contentType = ObjectUtil.checkNotNull(contentType, "contentType"); } @Override diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/BinaryWebSocketFrame.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/BinaryWebSocketFrame.java index 5fbcd90..91de5c3 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/BinaryWebSocketFrame.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/BinaryWebSocketFrame.java @@ -19,7 +19,7 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; /** - * Web Socket frame containing binary data + * Web Socket frame containing binary data. */ public class BinaryWebSocketFrame extends WebSocketFrame { diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/CloseWebSocketFrame.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/CloseWebSocketFrame.java index 371be6e..0286c6f 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/CloseWebSocketFrame.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/CloseWebSocketFrame.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 The Netty Project + * Copyright 2019 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 @@ -21,7 +21,7 @@ import io.netty.util.CharsetUtil; import io.netty.util.internal.StringUtil; /** - * Web Socket Frame for closing the connection + * Web Socket Frame for closing the connection. */ public class CloseWebSocketFrame extends WebSocketFrame { @@ -33,7 +33,31 @@ public class CloseWebSocketFrame extends WebSocketFrame { } /** - * Creates a new empty close frame with closing getStatus code and reason text + * Creates a new empty close frame with closing status code and reason text + * + * @param status + * Status code as per <a href="http://tools.ietf.org/html/rfc6455#section-7.4">RFC 6455</a>. For + * example, <tt>1000</tt> indicates normal closure. + */ + public CloseWebSocketFrame(WebSocketCloseStatus status) { + this(status.code(), status.reasonText()); + } + + /** + * Creates a new empty close frame with closing status code and reason text + * + * @param status + * Status code as per <a href="http://tools.ietf.org/html/rfc6455#section-7.4">RFC 6455</a>. For + * example, <tt>1000</tt> indicates normal closure. + * @param reasonText + * Reason text. Set to null if no text. + */ + public CloseWebSocketFrame(WebSocketCloseStatus status, String reasonText) { + this(status.code(), reasonText); + } + + /** + * Creates a new empty close frame with closing status code and reason text * * @param statusCode * Integer status code as per <a href="http://tools.ietf.org/html/rfc6455#section-7.4">RFC 6455</a>. For @@ -46,12 +70,12 @@ public class CloseWebSocketFrame extends WebSocketFrame { } /** - * Creates a new close frame with no losing getStatus code and no reason text + * Creates a new close frame with no losing status code and no reason text * * @param finalFragment * flag indicating if this frame is the final fragment * @param rsv - * reserved bits used for protocol extensions + * reserved bits used for protocol extensions. */ public CloseWebSocketFrame(boolean finalFragment, int rsv) { this(finalFragment, rsv, Unpooled.buffer(0)); @@ -105,7 +129,7 @@ public class CloseWebSocketFrame extends WebSocketFrame { /** * Returns the closing status code as per <a href="http://tools.ietf.org/html/rfc6455#section-7.4">RFC 6455</a>. If - * a getStatus code is set, -1 is returned. + * a status code is set, -1 is returned. */ public int statusCode() { ByteBuf binaryData = content(); @@ -114,10 +138,7 @@ public class CloseWebSocketFrame extends WebSocketFrame { } binaryData.readerIndex(0); - int statusCode = binaryData.readShort(); - binaryData.readerIndex(0); - - return statusCode; + return binaryData.getShort(0); } /** diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/ContinuationWebSocketFrame.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/ContinuationWebSocketFrame.java index bd25ea0..515fe4e 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/ContinuationWebSocketFrame.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/ContinuationWebSocketFrame.java @@ -43,7 +43,7 @@ public class ContinuationWebSocketFrame extends WebSocketFrame { } /** - * Creates a new continuation frame with the specified binary data + * Creates a new continuation frame with the specified binary data. * * @param finalFragment * flag indicating if this frame is the final fragment @@ -71,17 +71,17 @@ public class ContinuationWebSocketFrame extends WebSocketFrame { } /** - * Returns the text data in this frame + * Returns the text data in this frame. */ public String text() { return content().toString(CharsetUtil.UTF_8); } /** - * Sets the string for this frame + * Sets the string for this frame. * * @param text - * text to store + * text to store. */ private static ByteBuf fromText(String text) { if (text == null || text.isEmpty()) { diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/CorruptedWebSocketFrameException.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/CorruptedWebSocketFrameException.java new file mode 100644 index 0000000..ceeec83 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/CorruptedWebSocketFrameException.java @@ -0,0 +1,64 @@ +/* + * Copyright 2019 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.codec.http.websocketx; + +import io.netty.handler.codec.CorruptedFrameException; +import io.netty.handler.codec.DecoderException; + +/** + * An {@link DecoderException} which is thrown when the received {@link WebSocketFrame} data could not be decoded by + * an inbound handler. + */ +public final class CorruptedWebSocketFrameException extends CorruptedFrameException { + + private static final long serialVersionUID = 3918055132492988338L; + + private final WebSocketCloseStatus closeStatus; + + /** + * Creates a new instance. + */ + public CorruptedWebSocketFrameException() { + this(WebSocketCloseStatus.PROTOCOL_ERROR, null, null); + } + + /** + * Creates a new instance. + */ + public CorruptedWebSocketFrameException(WebSocketCloseStatus status, String message, Throwable cause) { + super(message == null ? status.reasonText() : message, cause); + closeStatus = status; + } + + /** + * Creates a new instance. + */ + public CorruptedWebSocketFrameException(WebSocketCloseStatus status, String message) { + this(status, message, null); + } + + /** + * Creates a new instance. + */ + public CorruptedWebSocketFrameException(WebSocketCloseStatus status, Throwable cause) { + this(status, null, cause); + } + + public WebSocketCloseStatus closeStatus() { + return closeStatus; + } + +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/PingWebSocketFrame.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/PingWebSocketFrame.java index 08e7025..6c3c79c 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/PingWebSocketFrame.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/PingWebSocketFrame.java @@ -19,7 +19,7 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; /** - * Web Socket frame containing binary data + * Web Socket frame containing binary data. */ public class PingWebSocketFrame extends WebSocketFrame { @@ -41,7 +41,7 @@ public class PingWebSocketFrame extends WebSocketFrame { } /** - * Creates a new ping frame with the specified binary data + * Creates a new ping frame with the specified binary data. * * @param finalFragment * flag indicating if this frame is the final fragment diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/PongWebSocketFrame.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/PongWebSocketFrame.java index 29c0b0f..d1b08eb 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/PongWebSocketFrame.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/PongWebSocketFrame.java @@ -19,7 +19,7 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; /** - * Web Socket frame containing binary data + * Web Socket frame containing binary data. */ public class PongWebSocketFrame extends WebSocketFrame { diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/TextWebSocketFrame.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/TextWebSocketFrame.java index 9b12471..7d6e2aa 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/TextWebSocketFrame.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/TextWebSocketFrame.java @@ -20,7 +20,7 @@ import io.netty.buffer.Unpooled; import io.netty.util.CharsetUtil; /** - * Web Socket text frame + * Web Socket text frame. */ public class TextWebSocketFrame extends WebSocketFrame { @@ -35,7 +35,7 @@ public class TextWebSocketFrame extends WebSocketFrame { * Creates a new text frame with the specified text string. The final fragment flag is set to true. * * @param text - * String to put in the frame + * String to put in the frame. */ public TextWebSocketFrame(String text) { super(fromText(text)); @@ -59,7 +59,7 @@ public class TextWebSocketFrame extends WebSocketFrame { * @param rsv * reserved bits used for protocol extensions * @param text - * String to put in the frame + * String to put in the frame. */ public TextWebSocketFrame(boolean finalFragment, int rsv, String text) { super(finalFragment, rsv, fromText(text)); @@ -74,7 +74,7 @@ public class TextWebSocketFrame extends WebSocketFrame { } /** - * Creates a new text frame with the specified binary data. The final fragment flag is set to true. + * Creates a new text frame with the specified binary data and the final fragment flag. * * @param finalFragment * flag indicating if this frame is the final fragment @@ -88,7 +88,7 @@ public class TextWebSocketFrame extends WebSocketFrame { } /** - * Returns the text data in this frame + * Returns the text data in this frame. */ public String text() { return content().toString(CharsetUtil.UTF_8); diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/Utf8FrameValidator.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/Utf8FrameValidator.java index 5ce5ec3..7edf5cf 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/Utf8FrameValidator.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/Utf8FrameValidator.java @@ -35,42 +35,47 @@ public class Utf8FrameValidator extends ChannelInboundHandlerAdapter { if (msg instanceof WebSocketFrame) { WebSocketFrame frame = (WebSocketFrame) msg; - // Processing for possible fragmented messages for text and binary - // frames - if (((WebSocketFrame) msg).isFinalFragment()) { - // Final frame of the sequence. Apparently ping frames are - // allowed in the middle of a fragmented message - if (!(frame instanceof PingWebSocketFrame)) { - fragmentedFramesCount = 0; + try { + // Processing for possible fragmented messages for text and binary + // frames + if (((WebSocketFrame) msg).isFinalFragment()) { + // Final frame of the sequence. Apparently ping frames are + // allowed in the middle of a fragmented message + if (!(frame instanceof PingWebSocketFrame)) { + fragmentedFramesCount = 0; - // Check text for UTF8 correctness - if ((frame instanceof TextWebSocketFrame) || - (utf8Validator != null && utf8Validator.isChecking())) { - // Check UTF-8 correctness for this payload - checkUTF8String(frame.content()); + // Check text for UTF8 correctness + if ((frame instanceof TextWebSocketFrame) || + (utf8Validator != null && utf8Validator.isChecking())) { + // Check UTF-8 correctness for this payload + checkUTF8String(frame.content()); - // This does a second check to make sure UTF-8 - // correctness for entire text message - utf8Validator.finish(); - } - } - } else { - // Not final frame so we can expect more frames in the - // fragmented sequence - if (fragmentedFramesCount == 0) { - // First text or binary frame for a fragmented set - if (frame instanceof TextWebSocketFrame) { - checkUTF8String(frame.content()); + // This does a second check to make sure UTF-8 + // correctness for entire text message + utf8Validator.finish(); + } } } else { - // Subsequent frames - only check if init frame is text - if (utf8Validator != null && utf8Validator.isChecking()) { - checkUTF8String(frame.content()); + // Not final frame so we can expect more frames in the + // fragmented sequence + if (fragmentedFramesCount == 0) { + // First text or binary frame for a fragmented set + if (frame instanceof TextWebSocketFrame) { + checkUTF8String(frame.content()); + } + } else { + // Subsequent frames - only check if init frame is text + if (utf8Validator != null && utf8Validator.isChecking()) { + checkUTF8String(frame.content()); + } } - } - // Increment counter - fragmentedFramesCount++; + // Increment counter + fragmentedFramesCount++; + } + } catch (CorruptedWebSocketFrameException e) { + frame.release(); + throw e; } } diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/Utf8Validator.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/Utf8Validator.java index 3a377e7..be85dc2 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/Utf8Validator.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/Utf8Validator.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 The Netty Project + * Copyright 2019 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 @@ -36,7 +36,6 @@ package io.netty.handler.codec.http.websocketx; import io.netty.buffer.ByteBuf; -import io.netty.handler.codec.CorruptedFrameException; import io.netty.util.ByteProcessor; /** @@ -79,7 +78,8 @@ final class Utf8Validator implements ByteProcessor { codep = 0; if (state != UTF8_ACCEPT) { state = UTF8_ACCEPT; - throw new CorruptedFrameException("bytes are not UTF-8"); + throw new CorruptedWebSocketFrameException( + WebSocketCloseStatus.INVALID_PAYLOAD_DATA, "bytes are not UTF-8"); } } @@ -93,7 +93,8 @@ final class Utf8Validator implements ByteProcessor { if (state == UTF8_REJECT) { checking = false; - throw new CorruptedFrameException("bytes are not UTF-8"); + throw new CorruptedWebSocketFrameException( + WebSocketCloseStatus.INVALID_PAYLOAD_DATA, "bytes are not UTF-8"); } return true; } diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocket00FrameDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocket00FrameDecoder.java index 1f6bad5..6d827db 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocket00FrameDecoder.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocket00FrameDecoder.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 The Netty Project + * Copyright 2019 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 @@ -19,6 +19,7 @@ import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.ReplayingDecoder; import io.netty.handler.codec.TooLongFrameException; +import io.netty.util.internal.ObjectUtil; import java.util.List; @@ -52,6 +53,17 @@ public class WebSocket00FrameDecoder extends ReplayingDecoder<Void> implements W this.maxFrameSize = maxFrameSize; } + /** + * Creates a new instance of {@code WebSocketFrameDecoder} with the specified {@code maxFrameSize}. If the client + * sends a frame size larger than {@code maxFrameSize}, the channel will be closed. + * + * @param decoderConfig + * Frames decoder configuration. + */ + public WebSocket00FrameDecoder(WebSocketDecoderConfig decoderConfig) { + this.maxFrameSize = ObjectUtil.checkNotNull(decoderConfig, "decoderConfig").maxFramePayloadLength(); + } + @Override protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception { // Discard all data received if closing handshake was received before. @@ -96,7 +108,7 @@ public class WebSocket00FrameDecoder extends ReplayingDecoder<Void> implements W if (type == (byte) 0xFF && frameSize == 0) { receivedClosingHandshake = true; - return new CloseWebSocketFrame(); + return new CloseWebSocketFrame(true, 0, ctx.alloc().buffer(0)); } ByteBuf payload = readBytes(ctx.alloc(), buffer, (int) frameSize); return new BinaryWebSocketFrame(payload); diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocket07FrameDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocket07FrameDecoder.java index 0ecb571..6aa2776 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocket07FrameDecoder.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocket07FrameDecoder.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 The Netty Project + * Copyright 2019 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 @@ -71,7 +71,11 @@ public class WebSocket07FrameDecoder extends WebSocket08FrameDecoder { * helps check for denial of services attacks. */ public WebSocket07FrameDecoder(boolean expectMaskedFrames, boolean allowExtensions, int maxFramePayloadLength) { - this(expectMaskedFrames, allowExtensions, maxFramePayloadLength, false); + this(WebSocketDecoderConfig.newBuilder() + .expectMaskedFrames(expectMaskedFrames) + .allowExtensions(allowExtensions) + .maxFramePayloadLength(maxFramePayloadLength) + .build()); } /** @@ -91,6 +95,21 @@ public class WebSocket07FrameDecoder extends WebSocket08FrameDecoder { */ public WebSocket07FrameDecoder(boolean expectMaskedFrames, boolean allowExtensions, int maxFramePayloadLength, boolean allowMaskMismatch) { - super(expectMaskedFrames, allowExtensions, maxFramePayloadLength, allowMaskMismatch); + this(WebSocketDecoderConfig.newBuilder() + .expectMaskedFrames(expectMaskedFrames) + .allowExtensions(allowExtensions) + .maxFramePayloadLength(maxFramePayloadLength) + .allowMaskMismatch(allowMaskMismatch) + .build()); + } + + /** + * Constructor + * + * @param decoderConfig + * Frames decoder configuration. + */ + public WebSocket07FrameDecoder(WebSocketDecoderConfig decoderConfig) { + super(decoderConfig); } } diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocket08FrameDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocket08FrameDecoder.java index f5a6dc1..a1320f9 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocket08FrameDecoder.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocket08FrameDecoder.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 The Netty Project + * Copyright 2019 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 @@ -58,8 +58,8 @@ import io.netty.buffer.Unpooled; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.ByteToMessageDecoder; -import io.netty.handler.codec.CorruptedFrameException; import io.netty.handler.codec.TooLongFrameException; +import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; @@ -93,10 +93,7 @@ public class WebSocket08FrameDecoder extends ByteToMessageDecoder private static final byte OPCODE_PING = 0x9; private static final byte OPCODE_PONG = 0xA; - private final long maxFramePayloadLength; - private final boolean allowExtensions; - private final boolean expectMaskedFrames; - private final boolean allowMaskMismatch; + private final WebSocketDecoderConfig config; private int fragmentedFramesCount; private boolean frameFinalFlag; @@ -142,242 +139,255 @@ public class WebSocket08FrameDecoder extends ByteToMessageDecoder */ public WebSocket08FrameDecoder(boolean expectMaskedFrames, boolean allowExtensions, int maxFramePayloadLength, boolean allowMaskMismatch) { - this.expectMaskedFrames = expectMaskedFrames; - this.allowMaskMismatch = allowMaskMismatch; - this.allowExtensions = allowExtensions; - this.maxFramePayloadLength = maxFramePayloadLength; + this(WebSocketDecoderConfig.newBuilder() + .expectMaskedFrames(expectMaskedFrames) + .allowExtensions(allowExtensions) + .maxFramePayloadLength(maxFramePayloadLength) + .allowMaskMismatch(allowMaskMismatch) + .build()); + } + + /** + * Constructor + * + * @param decoderConfig + * Frames decoder configuration. + */ + public WebSocket08FrameDecoder(WebSocketDecoderConfig decoderConfig) { + this.config = ObjectUtil.checkNotNull(decoderConfig, "decoderConfig"); } @Override protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception { - // Discard all data received if closing handshake was received before. if (receivedClosingHandshake) { in.skipBytes(actualReadableBytes()); return; } - switch (state) { - case READING_FIRST: - if (!in.isReadable()) { - return; - } - framePayloadLength = 0; + switch (state) { + case READING_FIRST: + if (!in.isReadable()) { + return; + } - // FIN, RSV, OPCODE - byte b = in.readByte(); - frameFinalFlag = (b & 0x80) != 0; - frameRsv = (b & 0x70) >> 4; - frameOpcode = b & 0x0F; + framePayloadLength = 0; - if (logger.isDebugEnabled()) { - logger.debug("Decoding WebSocket Frame opCode={}", frameOpcode); - } + // FIN, RSV, OPCODE + byte b = in.readByte(); + frameFinalFlag = (b & 0x80) != 0; + frameRsv = (b & 0x70) >> 4; + frameOpcode = b & 0x0F; - state = State.READING_SECOND; - case READING_SECOND: - if (!in.isReadable()) { - return; - } - // MASK, PAYLOAD LEN 1 - b = in.readByte(); - frameMasked = (b & 0x80) != 0; - framePayloadLen1 = b & 0x7F; - - if (frameRsv != 0 && !allowExtensions) { - protocolViolation(ctx, "RSV != 0 and no extension negotiated, RSV:" + frameRsv); - return; - } + if (logger.isTraceEnabled()) { + logger.trace("Decoding WebSocket Frame opCode={}", frameOpcode); + } - if (!allowMaskMismatch && expectMaskedFrames != frameMasked) { - protocolViolation(ctx, "received a frame that is not masked as expected"); - return; - } + state = State.READING_SECOND; + case READING_SECOND: + if (!in.isReadable()) { + return; + } + // MASK, PAYLOAD LEN 1 + b = in.readByte(); + frameMasked = (b & 0x80) != 0; + framePayloadLen1 = b & 0x7F; + + if (frameRsv != 0 && !config.allowExtensions()) { + protocolViolation(ctx, in, "RSV != 0 and no extension negotiated, RSV:" + frameRsv); + return; + } - if (frameOpcode > 7) { // control frame (have MSB in opcode set) - - // control frames MUST NOT be fragmented - if (!frameFinalFlag) { - protocolViolation(ctx, "fragmented control frame"); - return; - } - - // control frames MUST have payload 125 octets or less - if (framePayloadLen1 > 125) { - protocolViolation(ctx, "control frame with payload length > 125 octets"); - return; - } - - // check for reserved control frame opcodes - if (!(frameOpcode == OPCODE_CLOSE || frameOpcode == OPCODE_PING - || frameOpcode == OPCODE_PONG)) { - protocolViolation(ctx, "control frame using reserved opcode " + frameOpcode); - return; - } - - // close frame : if there is a body, the first two bytes of the - // body MUST be a 2-byte unsigned integer (in network byte - // order) representing a getStatus code - if (frameOpcode == 8 && framePayloadLen1 == 1) { - protocolViolation(ctx, "received close control frame with payload len 1"); - return; - } - } else { // data frame - // check for reserved data frame opcodes - if (!(frameOpcode == OPCODE_CONT || frameOpcode == OPCODE_TEXT - || frameOpcode == OPCODE_BINARY)) { - protocolViolation(ctx, "data frame using reserved opcode " + frameOpcode); - return; - } - - // check opcode vs message fragmentation state 1/2 - if (fragmentedFramesCount == 0 && frameOpcode == OPCODE_CONT) { - protocolViolation(ctx, "received continuation data frame outside fragmented message"); - return; - } - - // check opcode vs message fragmentation state 2/2 - if (fragmentedFramesCount != 0 && frameOpcode != OPCODE_CONT && frameOpcode != OPCODE_PING) { - protocolViolation(ctx, - "received non-continuation data frame while inside fragmented message"); - return; - } - } + if (!config.allowMaskMismatch() && config.expectMaskedFrames() != frameMasked) { + protocolViolation(ctx, in, "received a frame that is not masked as expected"); + return; + } - state = State.READING_SIZE; - case READING_SIZE: - - // Read frame payload length - if (framePayloadLen1 == 126) { - if (in.readableBytes() < 2) { - return; - } - framePayloadLength = in.readUnsignedShort(); - if (framePayloadLength < 126) { - protocolViolation(ctx, "invalid data frame length (not using minimal length encoding)"); - return; - } - } else if (framePayloadLen1 == 127) { - if (in.readableBytes() < 8) { - return; - } - framePayloadLength = in.readLong(); - // TODO: check if it's bigger than 0x7FFFFFFFFFFFFFFF, Maybe - // just check if it's negative? - - if (framePayloadLength < 65536) { - protocolViolation(ctx, "invalid data frame length (not using minimal length encoding)"); - return; - } - } else { - framePayloadLength = framePayloadLen1; - } + if (frameOpcode > 7) { // control frame (have MSB in opcode set) - if (framePayloadLength > maxFramePayloadLength) { - protocolViolation(ctx, "Max frame length of " + maxFramePayloadLength + " has been exceeded."); - return; - } + // control frames MUST NOT be fragmented + if (!frameFinalFlag) { + protocolViolation(ctx, in, "fragmented control frame"); + return; + } - if (logger.isDebugEnabled()) { - logger.debug("Decoding WebSocket Frame length={}", framePayloadLength); - } + // control frames MUST have payload 125 octets or less + if (framePayloadLen1 > 125) { + protocolViolation(ctx, in, "control frame with payload length > 125 octets"); + return; + } - state = State.MASKING_KEY; - case MASKING_KEY: - if (frameMasked) { - if (in.readableBytes() < 4) { - return; - } - if (maskingKey == null) { - maskingKey = new byte[4]; - } - in.readBytes(maskingKey); - } - state = State.PAYLOAD; - case PAYLOAD: - if (in.readableBytes() < framePayloadLength) { - return; - } + // check for reserved control frame opcodes + if (!(frameOpcode == OPCODE_CLOSE || frameOpcode == OPCODE_PING + || frameOpcode == OPCODE_PONG)) { + protocolViolation(ctx, in, "control frame using reserved opcode " + frameOpcode); + return; + } - ByteBuf payloadBuffer = null; - try { - payloadBuffer = readBytes(ctx.alloc(), in, toFrameLength(framePayloadLength)); - - // Now we have all the data, the next checkpoint must be the next - // frame - state = State.READING_FIRST; - - // Unmask data if needed - if (frameMasked) { - unmask(payloadBuffer); - } - - // Processing ping/pong/close frames because they cannot be - // fragmented - if (frameOpcode == OPCODE_PING) { - out.add(new PingWebSocketFrame(frameFinalFlag, frameRsv, payloadBuffer)); - payloadBuffer = null; - return; - } - if (frameOpcode == OPCODE_PONG) { - out.add(new PongWebSocketFrame(frameFinalFlag, frameRsv, payloadBuffer)); - payloadBuffer = null; - return; - } - if (frameOpcode == OPCODE_CLOSE) { - receivedClosingHandshake = true; - checkCloseFrameBody(ctx, payloadBuffer); - out.add(new CloseWebSocketFrame(frameFinalFlag, frameRsv, payloadBuffer)); - payloadBuffer = null; - return; - } - - // Processing for possible fragmented messages for text and binary - // frames - if (frameFinalFlag) { - // Final frame of the sequence. Apparently ping frames are - // allowed in the middle of a fragmented message - if (frameOpcode != OPCODE_PING) { - fragmentedFramesCount = 0; - } - } else { - // Increment counter - fragmentedFramesCount++; - } - - // Return the frame - if (frameOpcode == OPCODE_TEXT) { - out.add(new TextWebSocketFrame(frameFinalFlag, frameRsv, payloadBuffer)); - payloadBuffer = null; - return; - } else if (frameOpcode == OPCODE_BINARY) { - out.add(new BinaryWebSocketFrame(frameFinalFlag, frameRsv, payloadBuffer)); - payloadBuffer = null; - return; - } else if (frameOpcode == OPCODE_CONT) { - out.add(new ContinuationWebSocketFrame(frameFinalFlag, frameRsv, - payloadBuffer)); - payloadBuffer = null; - return; - } else { - throw new UnsupportedOperationException("Cannot decode web socket frame with opcode: " - + frameOpcode); - } - } finally { - if (payloadBuffer != null) { - payloadBuffer.release(); - } - } - case CORRUPT: - if (in.isReadable()) { - // If we don't keep reading Netty will throw an exception saying - // we can't return null if no bytes read and state not changed. - in.readByte(); + // close frame : if there is a body, the first two bytes of the + // body MUST be a 2-byte unsigned integer (in network byte + // order) representing a getStatus code + if (frameOpcode == 8 && framePayloadLen1 == 1) { + protocolViolation(ctx, in, "received close control frame with payload len 1"); + return; + } + } else { // data frame + // check for reserved data frame opcodes + if (!(frameOpcode == OPCODE_CONT || frameOpcode == OPCODE_TEXT + || frameOpcode == OPCODE_BINARY)) { + protocolViolation(ctx, in, "data frame using reserved opcode " + frameOpcode); + return; + } + + // check opcode vs message fragmentation state 1/2 + if (fragmentedFramesCount == 0 && frameOpcode == OPCODE_CONT) { + protocolViolation(ctx, in, "received continuation data frame outside fragmented message"); + return; + } + + // check opcode vs message fragmentation state 2/2 + if (fragmentedFramesCount != 0 && frameOpcode != OPCODE_CONT && frameOpcode != OPCODE_PING) { + protocolViolation(ctx, in, + "received non-continuation data frame while inside fragmented message"); + return; + } + } + + state = State.READING_SIZE; + case READING_SIZE: + + // Read frame payload length + if (framePayloadLen1 == 126) { + if (in.readableBytes() < 2) { + return; + } + framePayloadLength = in.readUnsignedShort(); + if (framePayloadLength < 126) { + protocolViolation(ctx, in, "invalid data frame length (not using minimal length encoding)"); + return; + } + } else if (framePayloadLen1 == 127) { + if (in.readableBytes() < 8) { + return; + } + framePayloadLength = in.readLong(); + // TODO: check if it's bigger than 0x7FFFFFFFFFFFFFFF, Maybe + // just check if it's negative? + + if (framePayloadLength < 65536) { + protocolViolation(ctx, in, "invalid data frame length (not using minimal length encoding)"); + return; + } + } else { + framePayloadLength = framePayloadLen1; + } + + if (framePayloadLength > config.maxFramePayloadLength()) { + protocolViolation(ctx, in, WebSocketCloseStatus.MESSAGE_TOO_BIG, + "Max frame length of " + config.maxFramePayloadLength() + " has been exceeded."); + return; + } + + if (logger.isTraceEnabled()) { + logger.trace("Decoding WebSocket Frame length={}", framePayloadLength); + } + + state = State.MASKING_KEY; + case MASKING_KEY: + if (frameMasked) { + if (in.readableBytes() < 4) { + return; + } + if (maskingKey == null) { + maskingKey = new byte[4]; + } + in.readBytes(maskingKey); + } + state = State.PAYLOAD; + case PAYLOAD: + if (in.readableBytes() < framePayloadLength) { + return; + } + + ByteBuf payloadBuffer = null; + try { + payloadBuffer = readBytes(ctx.alloc(), in, toFrameLength(framePayloadLength)); + + // Now we have all the data, the next checkpoint must be the next + // frame + state = State.READING_FIRST; + + // Unmask data if needed + if (frameMasked) { + unmask(payloadBuffer); + } + + // Processing ping/pong/close frames because they cannot be + // fragmented + if (frameOpcode == OPCODE_PING) { + out.add(new PingWebSocketFrame(frameFinalFlag, frameRsv, payloadBuffer)); + payloadBuffer = null; + return; + } + if (frameOpcode == OPCODE_PONG) { + out.add(new PongWebSocketFrame(frameFinalFlag, frameRsv, payloadBuffer)); + payloadBuffer = null; + return; + } + if (frameOpcode == OPCODE_CLOSE) { + receivedClosingHandshake = true; + checkCloseFrameBody(ctx, payloadBuffer); + out.add(new CloseWebSocketFrame(frameFinalFlag, frameRsv, payloadBuffer)); + payloadBuffer = null; + return; + } + + // Processing for possible fragmented messages for text and binary + // frames + if (frameFinalFlag) { + // Final frame of the sequence. Apparently ping frames are + // allowed in the middle of a fragmented message + if (frameOpcode != OPCODE_PING) { + fragmentedFramesCount = 0; } + } else { + // Increment counter + fragmentedFramesCount++; + } + + // Return the frame + if (frameOpcode == OPCODE_TEXT) { + out.add(new TextWebSocketFrame(frameFinalFlag, frameRsv, payloadBuffer)); + payloadBuffer = null; return; - default: - throw new Error("Shouldn't reach here."); + } else if (frameOpcode == OPCODE_BINARY) { + out.add(new BinaryWebSocketFrame(frameFinalFlag, frameRsv, payloadBuffer)); + payloadBuffer = null; + return; + } else if (frameOpcode == OPCODE_CONT) { + out.add(new ContinuationWebSocketFrame(frameFinalFlag, frameRsv, + payloadBuffer)); + payloadBuffer = null; + return; + } else { + throw new UnsupportedOperationException("Cannot decode web socket frame with opcode: " + + frameOpcode); + } + } finally { + if (payloadBuffer != null) { + payloadBuffer.release(); + } } + case CORRUPT: + if (in.isReadable()) { + // If we don't keep reading Netty will throw an exception saying + // we can't return null if no bytes read and state not changed. + in.readByte(); + } + return; + default: + throw new Error("Shouldn't reach here."); + } } private void unmask(ByteBuf frame) { @@ -408,18 +418,33 @@ public class WebSocket08FrameDecoder extends ByteToMessageDecoder } } - private void protocolViolation(ChannelHandlerContext ctx, String reason) { - protocolViolation(ctx, new CorruptedFrameException(reason)); + private void protocolViolation(ChannelHandlerContext ctx, ByteBuf in, String reason) { + protocolViolation(ctx, in, WebSocketCloseStatus.PROTOCOL_ERROR, reason); + } + + private void protocolViolation(ChannelHandlerContext ctx, ByteBuf in, WebSocketCloseStatus status, String reason) { + protocolViolation(ctx, in, new CorruptedWebSocketFrameException(status, reason)); } - private void protocolViolation(ChannelHandlerContext ctx, CorruptedFrameException ex) { + private void protocolViolation(ChannelHandlerContext ctx, ByteBuf in, CorruptedWebSocketFrameException ex) { state = State.CORRUPT; - if (ctx.channel().isActive()) { + int readableBytes = in.readableBytes(); + if (readableBytes > 0) { + // Fix for memory leak, caused by ByteToMessageDecoder#channelRead: + // buffer 'cumulation' is released ONLY when no more readable bytes available. + in.skipBytes(readableBytes); + } + if (ctx.channel().isActive() && config.closeOnProtocolViolation()) { Object closeMessage; if (receivedClosingHandshake) { closeMessage = Unpooled.EMPTY_BUFFER; } else { - closeMessage = new CloseWebSocketFrame(1002, null); + WebSocketCloseStatus closeStatus = ex.closeStatus(); + String reasonText = ex.getMessage(); + if (reasonText == null) { + reasonText = closeStatus.reasonText(); + } + closeMessage = new CloseWebSocketFrame(closeStatus, reasonText); } ctx.writeAndFlush(closeMessage).addListener(ChannelFutureListener.CLOSE); } @@ -441,7 +466,7 @@ public class WebSocket08FrameDecoder extends ByteToMessageDecoder return; } if (buffer.readableBytes() == 1) { - protocolViolation(ctx, "Invalid close frame body"); + protocolViolation(ctx, buffer, WebSocketCloseStatus.INVALID_PAYLOAD_DATA, "Invalid close frame body"); } // Save reader index @@ -450,17 +475,16 @@ public class WebSocket08FrameDecoder extends ByteToMessageDecoder // Must have 2 byte integer within the valid range int statusCode = buffer.readShort(); - if (statusCode >= 0 && statusCode <= 999 || statusCode >= 1004 && statusCode <= 1006 - || statusCode >= 1015 && statusCode <= 2999) { - protocolViolation(ctx, "Invalid close frame getStatus code: " + statusCode); + if (!WebSocketCloseStatus.isValidStatusCode(statusCode)) { + protocolViolation(ctx, buffer, "Invalid close frame getStatus code: " + statusCode); } // May have UTF-8 message if (buffer.isReadable()) { try { new Utf8Validator().check(buffer); - } catch (CorruptedFrameException ex) { - protocolViolation(ctx, ex); + } catch (CorruptedWebSocketFrameException ex) { + protocolViolation(ctx, buffer, ex); } } diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocket08FrameEncoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocket08FrameEncoder.java index cb16953..b8ef822 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocket08FrameEncoder.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocket08FrameEncoder.java @@ -126,8 +126,8 @@ public class WebSocket08FrameEncoder extends MessageToMessageEncoder<WebSocketFr int length = data.readableBytes(); - if (logger.isDebugEnabled()) { - logger.debug("Encoding WebSocket Frame opCode=" + opcode + " length=" + length); + if (logger.isTraceEnabled()) { + logger.trace("Encoding WebSocket Frame opCode={} length={}", opcode, length); } int b0 = 0; diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocket13FrameDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocket13FrameDecoder.java index 04fd682..0c4a9b1 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocket13FrameDecoder.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocket13FrameDecoder.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 The Netty Project + * Copyright 2019 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 @@ -91,6 +91,21 @@ public class WebSocket13FrameDecoder extends WebSocket08FrameDecoder { */ public WebSocket13FrameDecoder(boolean expectMaskedFrames, boolean allowExtensions, int maxFramePayloadLength, boolean allowMaskMismatch) { - super(expectMaskedFrames, allowExtensions, maxFramePayloadLength, allowMaskMismatch); + this(WebSocketDecoderConfig.newBuilder() + .expectMaskedFrames(expectMaskedFrames) + .allowExtensions(allowExtensions) + .maxFramePayloadLength(maxFramePayloadLength) + .allowMaskMismatch(allowMaskMismatch) + .build()); + } + + /** + * Constructor + * + * @param decoderConfig + * Frames decoder configuration. + */ + public WebSocket13FrameDecoder(WebSocketDecoderConfig decoderConfig) { + super(decoderConfig); } } diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker.java index 44c9144..08423e3 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker.java @@ -35,21 +35,23 @@ import io.netty.handler.codec.http.HttpResponseDecoder; import io.netty.handler.codec.http.HttpScheme; import io.netty.util.NetUtil; import io.netty.util.ReferenceCountUtil; -import io.netty.util.internal.ThrowableUtil; +import io.netty.util.internal.ObjectUtil; import java.net.URI; import java.nio.channels.ClosedChannelException; import java.util.Locale; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; /** * Base class for web socket client handshake implementations */ public abstract class WebSocketClientHandshaker { - private static final ClosedChannelException CLOSED_CHANNEL_EXCEPTION = ThrowableUtil.unknownStackTrace( - new ClosedChannelException(), WebSocketClientHandshaker.class, "processHandshake(...)"); private static final String HTTP_SCHEME_PREFIX = HttpScheme.HTTP + "://"; private static final String HTTPS_SCHEME_PREFIX = HttpScheme.HTTPS + "://"; + protected static final int DEFAULT_FORCE_CLOSE_TIMEOUT_MILLIS = 10000; private final URI uri; @@ -57,6 +59,15 @@ public abstract class WebSocketClientHandshaker { private volatile boolean handshakeComplete; + private volatile long forceCloseTimeoutMillis = DEFAULT_FORCE_CLOSE_TIMEOUT_MILLIS; + + private volatile int forceCloseInit; + + private static final AtomicIntegerFieldUpdater<WebSocketClientHandshaker> FORCE_CLOSE_INIT_UPDATER = + AtomicIntegerFieldUpdater.newUpdater(WebSocketClientHandshaker.class, "forceCloseInit"); + + private volatile boolean forceCloseComplete; + private final String expectedSubprotocol; private volatile String actualSubprotocol; @@ -65,6 +76,8 @@ public abstract class WebSocketClientHandshaker { private final int maxFramePayloadLength; + private final boolean absoluteUpgradeUrl; + /** * Base constructor * @@ -82,11 +95,62 @@ public abstract class WebSocketClientHandshaker { */ protected WebSocketClientHandshaker(URI uri, WebSocketVersion version, String subprotocol, HttpHeaders customHeaders, int maxFramePayloadLength) { + this(uri, version, subprotocol, customHeaders, maxFramePayloadLength, DEFAULT_FORCE_CLOSE_TIMEOUT_MILLIS); + } + + /** + * Base constructor + * + * @param uri + * URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be + * sent to this URL. + * @param version + * Version of web socket specification to use to connect to the server + * @param subprotocol + * Sub protocol request sent to the server. + * @param customHeaders + * Map of custom headers to add to the client request + * @param maxFramePayloadLength + * Maximum length of a frame's payload + * @param forceCloseTimeoutMillis + * Close the connection if it was not closed by the server after timeout specified + */ + protected WebSocketClientHandshaker(URI uri, WebSocketVersion version, String subprotocol, + HttpHeaders customHeaders, int maxFramePayloadLength, + long forceCloseTimeoutMillis) { + this(uri, version, subprotocol, customHeaders, maxFramePayloadLength, forceCloseTimeoutMillis, false); + } + + /** + * Base constructor + * + * @param uri + * URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be + * sent to this URL. + * @param version + * Version of web socket specification to use to connect to the server + * @param subprotocol + * Sub protocol request sent to the server. + * @param customHeaders + * Map of custom headers to add to the client request + * @param maxFramePayloadLength + * Maximum length of a frame's payload + * @param forceCloseTimeoutMillis + * Close the connection if it was not closed by the server after timeout specified + * @param absoluteUpgradeUrl + * Use an absolute url for the Upgrade request, typically when connecting through an HTTP proxy over + * clear HTTP + */ + protected WebSocketClientHandshaker(URI uri, WebSocketVersion version, String subprotocol, + HttpHeaders customHeaders, int maxFramePayloadLength, + long forceCloseTimeoutMillis, boolean absoluteUpgradeUrl) { this.uri = uri; this.version = version; expectedSubprotocol = subprotocol; this.customHeaders = customHeaders; this.maxFramePayloadLength = maxFramePayloadLength; + this.forceCloseTimeoutMillis = forceCloseTimeoutMillis; + this.absoluteUpgradeUrl = absoluteUpgradeUrl; } /** @@ -140,6 +204,29 @@ public abstract class WebSocketClientHandshaker { this.actualSubprotocol = actualSubprotocol; } + public long forceCloseTimeoutMillis() { + return forceCloseTimeoutMillis; + } + + /** + * Flag to indicate if the closing handshake was initiated because of timeout. + * For testing only. + */ + protected boolean isForceCloseComplete() { + return forceCloseComplete; + } + + /** + * Sets timeout to close the connection if it was not closed by the server. + * + * @param forceCloseTimeoutMillis + * Close the connection if it was not closed by the server after timeout specified + */ + public WebSocketClientHandshaker setForceCloseTimeoutMillis(long forceCloseTimeoutMillis) { + this.forceCloseTimeoutMillis = forceCloseTimeoutMillis; + return this; + } + /** * Begins the opening handshake * @@ -147,9 +234,7 @@ public abstract class WebSocketClientHandshaker { * Channel */ public ChannelFuture handshake(Channel channel) { - if (channel == null) { - throw new NullPointerException("channel"); - } + ObjectUtil.checkNotNull(channel, "channel"); return handshake(channel, channel.newPromise()); } @@ -162,18 +247,19 @@ public abstract class WebSocketClientHandshaker { * the {@link ChannelPromise} to be notified when the opening handshake is sent */ public final ChannelFuture handshake(Channel channel, final ChannelPromise promise) { - FullHttpRequest request = newHandshakeRequest(); - - HttpResponseDecoder decoder = channel.pipeline().get(HttpResponseDecoder.class); + ChannelPipeline pipeline = channel.pipeline(); + HttpResponseDecoder decoder = pipeline.get(HttpResponseDecoder.class); if (decoder == null) { - HttpClientCodec codec = channel.pipeline().get(HttpClientCodec.class); + HttpClientCodec codec = pipeline.get(HttpClientCodec.class); if (codec == null) { promise.setFailure(new IllegalStateException("ChannelPipeline does not contain " + - "a HttpResponseDecoder or HttpClientCodec")); + "an HttpResponseDecoder or HttpClientCodec")); return promise; } } + FullHttpRequest request = newHandshakeRequest(); + channel.writeAndFlush(request).addListener(new ChannelFutureListener() { @Override public void operationComplete(ChannelFuture future) { @@ -185,7 +271,7 @@ public abstract class WebSocketClientHandshaker { } if (ctx == null) { promise.setFailure(new IllegalStateException("ChannelPipeline does not contain " + - "a HttpRequestEncoder or HttpClientCodec")); + "an HttpRequestEncoder or HttpClientCodec")); return; } p.addAfter(ctx.name(), "ws-encoder", newWebSocketEncoder()); @@ -263,7 +349,7 @@ public abstract class WebSocketClientHandshaker { ctx = p.context(HttpClientCodec.class); if (ctx == null) { throw new IllegalStateException("ChannelPipeline does not contain " + - "a HttpRequestEncoder or HttpClientCodec"); + "an HttpRequestEncoder or HttpClientCodec"); } final HttpClientCodec codec = (HttpClientCodec) ctx.handler(); // Remove the encoder part of the codec as the user may start writing frames after this method returns. @@ -342,7 +428,7 @@ public abstract class WebSocketClientHandshaker { ctx = p.context(HttpClientCodec.class); if (ctx == null) { return promise.setFailure(new IllegalStateException("ChannelPipeline does not contain " + - "a HttpResponseDecoder or HttpClientCodec")); + "an HttpResponseDecoder or HttpClientCodec")); } } // Add aggregator and ensure we feed the HttpResponse so it is aggregated. A limit of 8192 should be more @@ -374,7 +460,9 @@ public abstract class WebSocketClientHandshaker { @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { // Fail promise if Channel was closed - promise.tryFailure(CLOSED_CHANNEL_EXCEPTION); + if (!promise.isDone()) { + promise.tryFailure(new ClosedChannelException()); + } ctx.fireChannelInactive(); } }); @@ -411,9 +499,7 @@ public abstract class WebSocketClientHandshaker { * Closing Frame that was received */ public ChannelFuture close(Channel channel, CloseWebSocketFrame frame) { - if (channel == null) { - throw new NullPointerException("channel"); - } + ObjectUtil.checkNotNull(channel, "channel"); return close(channel, frame, channel.newPromise()); } @@ -428,23 +514,61 @@ public abstract class WebSocketClientHandshaker { * the {@link ChannelPromise} to be notified when the closing handshake is done */ public ChannelFuture close(Channel channel, CloseWebSocketFrame frame, ChannelPromise promise) { - if (channel == null) { - throw new NullPointerException("channel"); + ObjectUtil.checkNotNull(channel, "channel"); + channel.writeAndFlush(frame, promise); + applyForceCloseTimeout(channel, promise); + return promise; + } + + private void applyForceCloseTimeout(final Channel channel, ChannelFuture flushFuture) { + final long forceCloseTimeoutMillis = this.forceCloseTimeoutMillis; + final WebSocketClientHandshaker handshaker = this; + if (forceCloseTimeoutMillis <= 0 || !channel.isActive() || forceCloseInit != 0) { + return; } - return channel.writeAndFlush(frame, promise); + + flushFuture.addListener(new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture future) throws Exception { + // If flush operation failed, there is no reason to expect + // a server to receive CloseFrame. Thus this should be handled + // by the application separately. + // Also, close might be called twice from different threads. + if (future.isSuccess() && channel.isActive() && + FORCE_CLOSE_INIT_UPDATER.compareAndSet(handshaker, 0, 1)) { + final Future<?> forceCloseFuture = channel.eventLoop().schedule(new Runnable() { + @Override + public void run() { + if (channel.isActive()) { + channel.close(); + forceCloseComplete = true; + } + } + }, forceCloseTimeoutMillis, TimeUnit.MILLISECONDS); + + channel.closeFuture().addListener(new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture future) throws Exception { + forceCloseFuture.cancel(false); + } + }); + } + } + }); } /** * Return the constructed raw path for the give {@link URI}. */ - static String rawPath(URI wsURL) { - String path = wsURL.getRawPath(); - String query = wsURL.getRawQuery(); - if (query != null && !query.isEmpty()) { - path = path + '?' + query; + protected String upgradeUrl(URI wsURL) { + if (absoluteUpgradeUrl) { + return wsURL.toString(); } - return path == null || path.isEmpty() ? "/" : path; + String path = wsURL.getRawPath(); + path = path == null || path.isEmpty() ? "/" : path; + String query = wsURL.getRawQuery(); + return query != null && !query.isEmpty() ? path + '?' + query : path; } static CharSequence websocketHostValue(URI wsURL) { @@ -453,14 +577,15 @@ public abstract class WebSocketClientHandshaker { return wsURL.getHost(); } String host = wsURL.getHost(); + String scheme = wsURL.getScheme(); if (port == HttpScheme.HTTP.port()) { - return HttpScheme.HTTP.name().contentEquals(wsURL.getScheme()) - || WebSocketScheme.WS.name().contentEquals(wsURL.getScheme()) ? + return HttpScheme.HTTP.name().contentEquals(scheme) + || WebSocketScheme.WS.name().contentEquals(scheme) ? host : NetUtil.toSocketAddressString(host, port); } if (port == HttpScheme.HTTPS.port()) { - return HttpScheme.HTTPS.name().contentEquals(wsURL.getScheme()) - || WebSocketScheme.WSS.name().contentEquals(wsURL.getScheme()) ? + return HttpScheme.HTTPS.name().contentEquals(scheme) + || WebSocketScheme.WSS.name().contentEquals(scheme) ? host : NetUtil.toSocketAddressString(host, port); } diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker00.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker00.java index f026263..ef4b6a3 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker00.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker00.java @@ -48,7 +48,7 @@ public class WebSocketClientHandshaker00 extends WebSocketClientHandshaker { private ByteBuf expectedChallengeResponseBytes; /** - * Constructor specifying the destination web socket location and version to initiate + * Creates a new instance with the specified destination WebSocket location and version to initiate. * * @param webSocketURL * URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be @@ -64,7 +64,58 @@ public class WebSocketClientHandshaker00 extends WebSocketClientHandshaker { */ public WebSocketClientHandshaker00(URI webSocketURL, WebSocketVersion version, String subprotocol, HttpHeaders customHeaders, int maxFramePayloadLength) { - super(webSocketURL, version, subprotocol, customHeaders, maxFramePayloadLength); + this(webSocketURL, version, subprotocol, customHeaders, maxFramePayloadLength, + DEFAULT_FORCE_CLOSE_TIMEOUT_MILLIS); + } + + /** + * Creates a new instance with the specified destination WebSocket location and version to initiate. + * + * @param webSocketURL + * URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be + * sent to this URL. + * @param version + * Version of web socket specification to use to connect to the server + * @param subprotocol + * Sub protocol request sent to the server. + * @param customHeaders + * Map of custom headers to add to the client request + * @param maxFramePayloadLength + * Maximum length of a frame's payload + * @param forceCloseTimeoutMillis + * Close the connection if it was not closed by the server after timeout specified + */ + public WebSocketClientHandshaker00(URI webSocketURL, WebSocketVersion version, String subprotocol, + HttpHeaders customHeaders, int maxFramePayloadLength, + long forceCloseTimeoutMillis) { + this(webSocketURL, version, subprotocol, customHeaders, maxFramePayloadLength, forceCloseTimeoutMillis, false); + } + + /** + * Creates a new instance with the specified destination WebSocket location and version to initiate. + * + * @param webSocketURL + * URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be + * sent to this URL. + * @param version + * Version of web socket specification to use to connect to the server + * @param subprotocol + * Sub protocol request sent to the server. + * @param customHeaders + * Map of custom headers to add to the client request + * @param maxFramePayloadLength + * Maximum length of a frame's payload + * @param forceCloseTimeoutMillis + * Close the connection if it was not closed by the server after timeout specified + * @param absoluteUpgradeUrl + * Use an absolute url for the Upgrade request, typically when connecting through an HTTP proxy over + * clear HTTP + */ + WebSocketClientHandshaker00(URI webSocketURL, WebSocketVersion version, String subprotocol, + HttpHeaders customHeaders, int maxFramePayloadLength, + long forceCloseTimeoutMillis, boolean absoluteUpgradeUrl) { + super(webSocketURL, version, subprotocol, customHeaders, maxFramePayloadLength, forceCloseTimeoutMillis, + absoluteUpgradeUrl); } /** @@ -124,12 +175,11 @@ public class WebSocketClientHandshaker00 extends WebSocketClientHandshaker { System.arraycopy(key3, 0, challenge, 8, 8); expectedChallengeResponseBytes = Unpooled.wrappedBuffer(WebSocketUtil.md5(challenge)); - // Get path URI wsURL = uri(); - String path = rawPath(wsURL); // Format request - FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, path); + FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, upgradeUrl(wsURL), + Unpooled.wrappedBuffer(key3)); HttpHeaders headers = request.headers(); if (customHeaders != null) { @@ -139,10 +189,13 @@ public class WebSocketClientHandshaker00 extends WebSocketClientHandshaker { headers.set(HttpHeaderNames.UPGRADE, WEBSOCKET) .set(HttpHeaderNames.CONNECTION, HttpHeaderValues.UPGRADE) .set(HttpHeaderNames.HOST, websocketHostValue(wsURL)) - .set(HttpHeaderNames.ORIGIN, websocketOriginValue(wsURL)) .set(HttpHeaderNames.SEC_WEBSOCKET_KEY1, key1) .set(HttpHeaderNames.SEC_WEBSOCKET_KEY2, key2); + if (!headers.contains(HttpHeaderNames.ORIGIN)) { + headers.set(HttpHeaderNames.ORIGIN, websocketOriginValue(wsURL)); + } + String expectedSubprotocol = expectedSubprotocol(); if (expectedSubprotocol != null && !expectedSubprotocol.isEmpty()) { headers.set(HttpHeaderNames.SEC_WEBSOCKET_PROTOCOL, expectedSubprotocol); @@ -151,7 +204,6 @@ public class WebSocketClientHandshaker00 extends WebSocketClientHandshaker { // Set Content-Length to workaround some known defect. // See also: http://www.ietf.org/mail-archive/web/hybi/current/msg02149.html headers.set(HttpHeaderNames.CONTENT_LENGTH, key3.length); - request.content().writeBytes(key3); return request; } @@ -243,4 +295,11 @@ public class WebSocketClientHandshaker00 extends WebSocketClientHandshaker { protected WebSocketFrameEncoder newWebSocketEncoder() { return new WebSocket00FrameEncoder(); } + + @Override + public WebSocketClientHandshaker00 setForceCloseTimeoutMillis(long forceCloseTimeoutMillis) { + super.setForceCloseTimeoutMillis(forceCloseTimeoutMillis); + return this; + } + } diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker07.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker07.java index 4632a4a..779b714 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker07.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker07.java @@ -15,6 +15,7 @@ */ package io.netty.handler.codec.http.websocketx; +import io.netty.buffer.Unpooled; import io.netty.handler.codec.http.DefaultFullHttpRequest; import io.netty.handler.codec.http.FullHttpRequest; import io.netty.handler.codec.http.FullHttpResponse; @@ -94,10 +95,81 @@ public class WebSocketClientHandshaker07 extends WebSocketClientHandshaker { * When set to true, frames which are not masked properly according to the standard will still be * accepted. */ + public WebSocketClientHandshaker07(URI webSocketURL, WebSocketVersion version, String subprotocol, + boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength, + boolean performMasking, boolean allowMaskMismatch) { + this(webSocketURL, version, subprotocol, allowExtensions, customHeaders, maxFramePayloadLength, performMasking, + allowMaskMismatch, DEFAULT_FORCE_CLOSE_TIMEOUT_MILLIS); + } + + /** + * Creates a new instance. + * + * @param webSocketURL + * URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be + * sent to this URL. + * @param version + * Version of web socket specification to use to connect to the server + * @param subprotocol + * Sub protocol request sent to the server. + * @param allowExtensions + * Allow extensions to be used in the reserved bits of the web socket frame + * @param customHeaders + * Map of custom headers to add to the client request + * @param maxFramePayloadLength + * Maximum length of a frame's payload + * @param performMasking + * Whether to mask all written websocket frames. This must be set to true in order to be fully compatible + * with the websocket specifications. Client applications that communicate with a non-standard server + * which doesn't require masking might set this to false to achieve a higher performance. + * @param allowMaskMismatch + * When set to true, frames which are not masked properly according to the standard will still be + * accepted + * @param forceCloseTimeoutMillis + * Close the connection if it was not closed by the server after timeout specified. + */ public WebSocketClientHandshaker07(URI webSocketURL, WebSocketVersion version, String subprotocol, boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength, - boolean performMasking, boolean allowMaskMismatch) { - super(webSocketURL, version, subprotocol, customHeaders, maxFramePayloadLength); + boolean performMasking, boolean allowMaskMismatch, long forceCloseTimeoutMillis) { + this(webSocketURL, version, subprotocol, allowExtensions, customHeaders, maxFramePayloadLength, performMasking, + allowMaskMismatch, forceCloseTimeoutMillis, false); + } + + /** + * Creates a new instance. + * + * @param webSocketURL + * URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be + * sent to this URL. + * @param version + * Version of web socket specification to use to connect to the server + * @param subprotocol + * Sub protocol request sent to the server. + * @param allowExtensions + * Allow extensions to be used in the reserved bits of the web socket frame + * @param customHeaders + * Map of custom headers to add to the client request + * @param maxFramePayloadLength + * Maximum length of a frame's payload + * @param performMasking + * Whether to mask all written websocket frames. This must be set to true in order to be fully compatible + * with the websocket specifications. Client applications that communicate with a non-standard server + * which doesn't require masking might set this to false to achieve a higher performance. + * @param allowMaskMismatch + * When set to true, frames which are not masked properly according to the standard will still be + * accepted + * @param forceCloseTimeoutMillis + * Close the connection if it was not closed by the server after timeout specified. + * @param absoluteUpgradeUrl + * Use an absolute url for the Upgrade request, typically when connecting through an HTTP proxy over + * clear HTTP + */ + WebSocketClientHandshaker07(URI webSocketURL, WebSocketVersion version, String subprotocol, + boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength, + boolean performMasking, boolean allowMaskMismatch, long forceCloseTimeoutMillis, + boolean absoluteUpgradeUrl) { + super(webSocketURL, version, subprotocol, customHeaders, maxFramePayloadLength, forceCloseTimeoutMillis, + absoluteUpgradeUrl); this.allowExtensions = allowExtensions; this.performMasking = performMasking; this.allowMaskMismatch = allowMaskMismatch; @@ -123,9 +195,7 @@ public class WebSocketClientHandshaker07 extends WebSocketClientHandshaker { */ @Override protected FullHttpRequest newHandshakeRequest() { - // Get path URI wsURL = uri(); - String path = rawPath(wsURL); // Get 16 bit nonce and base 64 encode it byte[] nonce = WebSocketUtil.randomBytes(16); @@ -142,25 +212,36 @@ public class WebSocketClientHandshaker07 extends WebSocketClientHandshaker { } // Format request - FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, path); + FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, upgradeUrl(wsURL), + Unpooled.EMPTY_BUFFER); HttpHeaders headers = request.headers(); if (customHeaders != null) { headers.add(customHeaders); + if (!headers.contains(HttpHeaderNames.HOST)) { + // Only add HOST header if customHeaders did not contain it. + // + // See https://github.com/netty/netty/issues/10101 + headers.set(HttpHeaderNames.HOST, websocketHostValue(wsURL)); + } + } else { + headers.set(HttpHeaderNames.HOST, websocketHostValue(wsURL)); } headers.set(HttpHeaderNames.UPGRADE, HttpHeaderValues.WEBSOCKET) .set(HttpHeaderNames.CONNECTION, HttpHeaderValues.UPGRADE) - .set(HttpHeaderNames.SEC_WEBSOCKET_KEY, key) - .set(HttpHeaderNames.HOST, websocketHostValue(wsURL)) - .set(HttpHeaderNames.SEC_WEBSOCKET_ORIGIN, websocketOriginValue(wsURL)); + .set(HttpHeaderNames.SEC_WEBSOCKET_KEY, key); + + if (!headers.contains(HttpHeaderNames.SEC_WEBSOCKET_ORIGIN)) { + headers.set(HttpHeaderNames.SEC_WEBSOCKET_ORIGIN, websocketOriginValue(wsURL)); + } String expectedSubprotocol = expectedSubprotocol(); if (expectedSubprotocol != null && !expectedSubprotocol.isEmpty()) { headers.set(HttpHeaderNames.SEC_WEBSOCKET_PROTOCOL, expectedSubprotocol); } - headers.set(HttpHeaderNames.SEC_WEBSOCKET_VERSION, "7"); + headers.set(HttpHeaderNames.SEC_WEBSOCKET_VERSION, version().toAsciiString()); return request; } @@ -216,4 +297,11 @@ public class WebSocketClientHandshaker07 extends WebSocketClientHandshaker { protected WebSocketFrameEncoder newWebSocketEncoder() { return new WebSocket07FrameEncoder(performMasking); } + + @Override + public WebSocketClientHandshaker07 setForceCloseTimeoutMillis(long forceCloseTimeoutMillis) { + super.setForceCloseTimeoutMillis(forceCloseTimeoutMillis); + return this; + } + } diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker08.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker08.java index 1a11aa6..c28efd0 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker08.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker08.java @@ -15,6 +15,7 @@ */ package io.netty.handler.codec.http.websocketx; +import io.netty.buffer.Unpooled; import io.netty.handler.codec.http.DefaultFullHttpRequest; import io.netty.handler.codec.http.FullHttpRequest; import io.netty.handler.codec.http.FullHttpResponse; @@ -68,7 +69,8 @@ public class WebSocketClientHandshaker08 extends WebSocketClientHandshaker { */ public WebSocketClientHandshaker08(URI webSocketURL, WebSocketVersion version, String subprotocol, boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength) { - this(webSocketURL, version, subprotocol, allowExtensions, customHeaders, maxFramePayloadLength, true, false); + this(webSocketURL, version, subprotocol, allowExtensions, customHeaders, maxFramePayloadLength, true, + false, DEFAULT_FORCE_CLOSE_TIMEOUT_MILLIS); } /** @@ -93,12 +95,83 @@ public class WebSocketClientHandshaker08 extends WebSocketClientHandshaker { * which doesn't require masking might set this to false to achieve a higher performance. * @param allowMaskMismatch * When set to true, frames which are not masked properly according to the standard will still be - * accepted. + * accepted + */ + public WebSocketClientHandshaker08(URI webSocketURL, WebSocketVersion version, String subprotocol, + boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength, + boolean performMasking, boolean allowMaskMismatch) { + this(webSocketURL, version, subprotocol, allowExtensions, customHeaders, maxFramePayloadLength, performMasking, + allowMaskMismatch, DEFAULT_FORCE_CLOSE_TIMEOUT_MILLIS); + } + + /** + * Creates a new instance. + * + * @param webSocketURL + * URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be + * sent to this URL. + * @param version + * Version of web socket specification to use to connect to the server + * @param subprotocol + * Sub protocol request sent to the server. + * @param allowExtensions + * Allow extensions to be used in the reserved bits of the web socket frame + * @param customHeaders + * Map of custom headers to add to the client request + * @param maxFramePayloadLength + * Maximum length of a frame's payload + * @param performMasking + * Whether to mask all written websocket frames. This must be set to true in order to be fully compatible + * with the websocket specifications. Client applications that communicate with a non-standard server + * which doesn't require masking might set this to false to achieve a higher performance. + * @param allowMaskMismatch + * When set to true, frames which are not masked properly according to the standard will still be + * accepted + * @param forceCloseTimeoutMillis + * Close the connection if it was not closed by the server after timeout specified. */ public WebSocketClientHandshaker08(URI webSocketURL, WebSocketVersion version, String subprotocol, boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength, - boolean performMasking, boolean allowMaskMismatch) { - super(webSocketURL, version, subprotocol, customHeaders, maxFramePayloadLength); + boolean performMasking, boolean allowMaskMismatch, long forceCloseTimeoutMillis) { + this(webSocketURL, version, subprotocol, allowExtensions, customHeaders, maxFramePayloadLength, performMasking, + allowMaskMismatch, forceCloseTimeoutMillis, false); + } + + /** + * Creates a new instance. + * + * @param webSocketURL + * URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be + * sent to this URL. + * @param version + * Version of web socket specification to use to connect to the server + * @param subprotocol + * Sub protocol request sent to the server. + * @param allowExtensions + * Allow extensions to be used in the reserved bits of the web socket frame + * @param customHeaders + * Map of custom headers to add to the client request + * @param maxFramePayloadLength + * Maximum length of a frame's payload + * @param performMasking + * Whether to mask all written websocket frames. This must be set to true in order to be fully compatible + * with the websocket specifications. Client applications that communicate with a non-standard server + * which doesn't require masking might set this to false to achieve a higher performance. + * @param allowMaskMismatch + * When set to true, frames which are not masked properly according to the standard will still be + * accepted + * @param forceCloseTimeoutMillis + * Close the connection if it was not closed by the server after timeout specified. + * @param absoluteUpgradeUrl + * Use an absolute url for the Upgrade request, typically when connecting through an HTTP proxy over + * clear HTTP + */ + WebSocketClientHandshaker08(URI webSocketURL, WebSocketVersion version, String subprotocol, + boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength, + boolean performMasking, boolean allowMaskMismatch, long forceCloseTimeoutMillis, + boolean absoluteUpgradeUrl) { + super(webSocketURL, version, subprotocol, customHeaders, maxFramePayloadLength, forceCloseTimeoutMillis, + absoluteUpgradeUrl); this.allowExtensions = allowExtensions; this.performMasking = performMasking; this.allowMaskMismatch = allowMaskMismatch; @@ -124,9 +197,7 @@ public class WebSocketClientHandshaker08 extends WebSocketClientHandshaker { */ @Override protected FullHttpRequest newHandshakeRequest() { - // Get path URI wsURL = uri(); - String path = rawPath(wsURL); // Get 16 bit nonce and base 64 encode it byte[] nonce = WebSocketUtil.randomBytes(16); @@ -143,25 +214,36 @@ public class WebSocketClientHandshaker08 extends WebSocketClientHandshaker { } // Format request - FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, path); + FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, upgradeUrl(wsURL), + Unpooled.EMPTY_BUFFER); HttpHeaders headers = request.headers(); if (customHeaders != null) { headers.add(customHeaders); + if (!headers.contains(HttpHeaderNames.HOST)) { + // Only add HOST header if customHeaders did not contain it. + // + // See https://github.com/netty/netty/issues/10101 + headers.set(HttpHeaderNames.HOST, websocketHostValue(wsURL)); + } + } else { + headers.set(HttpHeaderNames.HOST, websocketHostValue(wsURL)); } headers.set(HttpHeaderNames.UPGRADE, HttpHeaderValues.WEBSOCKET) .set(HttpHeaderNames.CONNECTION, HttpHeaderValues.UPGRADE) - .set(HttpHeaderNames.SEC_WEBSOCKET_KEY, key) - .set(HttpHeaderNames.HOST, websocketHostValue(wsURL)) - .set(HttpHeaderNames.SEC_WEBSOCKET_ORIGIN, websocketOriginValue(wsURL)); + .set(HttpHeaderNames.SEC_WEBSOCKET_KEY, key); + + if (!headers.contains(HttpHeaderNames.SEC_WEBSOCKET_ORIGIN)) { + headers.set(HttpHeaderNames.SEC_WEBSOCKET_ORIGIN, websocketOriginValue(wsURL)); + } String expectedSubprotocol = expectedSubprotocol(); if (expectedSubprotocol != null && !expectedSubprotocol.isEmpty()) { headers.set(HttpHeaderNames.SEC_WEBSOCKET_PROTOCOL, expectedSubprotocol); } - headers.set(HttpHeaderNames.SEC_WEBSOCKET_VERSION, "8"); + headers.set(HttpHeaderNames.SEC_WEBSOCKET_VERSION, version().toAsciiString()); return request; } @@ -217,4 +299,11 @@ public class WebSocketClientHandshaker08 extends WebSocketClientHandshaker { protected WebSocketFrameEncoder newWebSocketEncoder() { return new WebSocket08FrameEncoder(performMasking); } + + @Override + public WebSocketClientHandshaker08 setForceCloseTimeoutMillis(long forceCloseTimeoutMillis) { + super.setForceCloseTimeoutMillis(forceCloseTimeoutMillis); + return this; + } + } diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker13.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker13.java index 808f7fc..2d061d6 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker13.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker13.java @@ -15,6 +15,7 @@ */ package io.netty.handler.codec.http.websocketx; +import io.netty.buffer.Unpooled; import io.netty.handler.codec.http.DefaultFullHttpRequest; import io.netty.handler.codec.http.FullHttpRequest; import io.netty.handler.codec.http.FullHttpResponse; @@ -68,7 +69,8 @@ public class WebSocketClientHandshaker13 extends WebSocketClientHandshaker { */ public WebSocketClientHandshaker13(URI webSocketURL, WebSocketVersion version, String subprotocol, boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength) { - this(webSocketURL, version, subprotocol, allowExtensions, customHeaders, maxFramePayloadLength, true, false); + this(webSocketURL, version, subprotocol, allowExtensions, customHeaders, maxFramePayloadLength, + true, false); } /** @@ -98,7 +100,79 @@ public class WebSocketClientHandshaker13 extends WebSocketClientHandshaker { public WebSocketClientHandshaker13(URI webSocketURL, WebSocketVersion version, String subprotocol, boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength, boolean performMasking, boolean allowMaskMismatch) { - super(webSocketURL, version, subprotocol, customHeaders, maxFramePayloadLength); + this(webSocketURL, version, subprotocol, allowExtensions, customHeaders, maxFramePayloadLength, + performMasking, allowMaskMismatch, DEFAULT_FORCE_CLOSE_TIMEOUT_MILLIS); + } + + /** + * Creates a new instance. + * + * @param webSocketURL + * URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be + * sent to this URL. + * @param version + * Version of web socket specification to use to connect to the server + * @param subprotocol + * Sub protocol request sent to the server. + * @param allowExtensions + * Allow extensions to be used in the reserved bits of the web socket frame + * @param customHeaders + * Map of custom headers to add to the client request + * @param maxFramePayloadLength + * Maximum length of a frame's payload + * @param performMasking + * Whether to mask all written websocket frames. This must be set to true in order to be fully compatible + * with the websocket specifications. Client applications that communicate with a non-standard server + * which doesn't require masking might set this to false to achieve a higher performance. + * @param allowMaskMismatch + * When set to true, frames which are not masked properly according to the standard will still be + * accepted + * @param forceCloseTimeoutMillis + * Close the connection if it was not closed by the server after timeout specified. + */ + public WebSocketClientHandshaker13(URI webSocketURL, WebSocketVersion version, String subprotocol, + boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength, + boolean performMasking, boolean allowMaskMismatch, + long forceCloseTimeoutMillis) { + this(webSocketURL, version, subprotocol, allowExtensions, customHeaders, maxFramePayloadLength, performMasking, + allowMaskMismatch, forceCloseTimeoutMillis, false); + } + + /** + * Creates a new instance. + * + * @param webSocketURL + * URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be + * sent to this URL. + * @param version + * Version of web socket specification to use to connect to the server + * @param subprotocol + * Sub protocol request sent to the server. + * @param allowExtensions + * Allow extensions to be used in the reserved bits of the web socket frame + * @param customHeaders + * Map of custom headers to add to the client request + * @param maxFramePayloadLength + * Maximum length of a frame's payload + * @param performMasking + * Whether to mask all written websocket frames. This must be set to true in order to be fully compatible + * with the websocket specifications. Client applications that communicate with a non-standard server + * which doesn't require masking might set this to false to achieve a higher performance. + * @param allowMaskMismatch + * When set to true, frames which are not masked properly according to the standard will still be + * accepted + * @param forceCloseTimeoutMillis + * Close the connection if it was not closed by the server after timeout specified. + * @param absoluteUpgradeUrl + * Use an absolute url for the Upgrade request, typically when connecting through an HTTP proxy over + * clear HTTP + */ + WebSocketClientHandshaker13(URI webSocketURL, WebSocketVersion version, String subprotocol, + boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength, + boolean performMasking, boolean allowMaskMismatch, + long forceCloseTimeoutMillis, boolean absoluteUpgradeUrl) { + super(webSocketURL, version, subprotocol, customHeaders, maxFramePayloadLength, forceCloseTimeoutMillis, + absoluteUpgradeUrl); this.allowExtensions = allowExtensions; this.performMasking = performMasking; this.allowMaskMismatch = allowMaskMismatch; @@ -116,7 +190,7 @@ public class WebSocketClientHandshaker13 extends WebSocketClientHandshaker { * Upgrade: websocket * Connection: Upgrade * Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== - * Sec-WebSocket-Origin: http://example.com + * Origin: http://example.com * Sec-WebSocket-Protocol: chat, superchat * Sec-WebSocket-Version: 13 * </pre> @@ -124,9 +198,7 @@ public class WebSocketClientHandshaker13 extends WebSocketClientHandshaker { */ @Override protected FullHttpRequest newHandshakeRequest() { - // Get path URI wsURL = uri(); - String path = rawPath(wsURL); // Get 16 bit nonce and base 64 encode it byte[] nonce = WebSocketUtil.randomBytes(16); @@ -143,25 +215,36 @@ public class WebSocketClientHandshaker13 extends WebSocketClientHandshaker { } // Format request - FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, path); + FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, upgradeUrl(wsURL), + Unpooled.EMPTY_BUFFER); HttpHeaders headers = request.headers(); if (customHeaders != null) { headers.add(customHeaders); + if (!headers.contains(HttpHeaderNames.HOST)) { + // Only add HOST header if customHeaders did not contain it. + // + // See https://github.com/netty/netty/issues/10101 + headers.set(HttpHeaderNames.HOST, websocketHostValue(wsURL)); + } + } else { + headers.set(HttpHeaderNames.HOST, websocketHostValue(wsURL)); } headers.set(HttpHeaderNames.UPGRADE, HttpHeaderValues.WEBSOCKET) .set(HttpHeaderNames.CONNECTION, HttpHeaderValues.UPGRADE) - .set(HttpHeaderNames.SEC_WEBSOCKET_KEY, key) - .set(HttpHeaderNames.HOST, websocketHostValue(wsURL)) - .set(HttpHeaderNames.SEC_WEBSOCKET_ORIGIN, websocketOriginValue(wsURL)); + .set(HttpHeaderNames.SEC_WEBSOCKET_KEY, key); + + if (!headers.contains(HttpHeaderNames.ORIGIN)) { + headers.set(HttpHeaderNames.ORIGIN, websocketOriginValue(wsURL)); + } String expectedSubprotocol = expectedSubprotocol(); if (expectedSubprotocol != null && !expectedSubprotocol.isEmpty()) { headers.set(HttpHeaderNames.SEC_WEBSOCKET_PROTOCOL, expectedSubprotocol); } - headers.set(HttpHeaderNames.SEC_WEBSOCKET_VERSION, "13"); + headers.set(HttpHeaderNames.SEC_WEBSOCKET_VERSION, version().toAsciiString()); return request; } @@ -180,7 +263,7 @@ public class WebSocketClientHandshaker13 extends WebSocketClientHandshaker { * * @param response * HTTP response returned from the server for the request sent by beginOpeningHandshake00(). - * @throws WebSocketHandshakeException + * @throws WebSocketHandshakeException if handshake response is invalid. */ @Override protected void verify(FullHttpResponse response) { @@ -217,4 +300,11 @@ public class WebSocketClientHandshaker13 extends WebSocketClientHandshaker { protected WebSocketFrameEncoder newWebSocketEncoder() { return new WebSocket13FrameEncoder(performMasking); } + + @Override + public WebSocketClientHandshaker13 setForceCloseTimeoutMillis(long forceCloseTimeoutMillis) { + super.setForceCloseTimeoutMillis(forceCloseTimeoutMillis); + return this; + } + } diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshakerFactory.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshakerFactory.java index b07825f..01826ea 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshakerFactory.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshakerFactory.java @@ -107,24 +107,117 @@ public final class WebSocketClientHandshakerFactory { URI webSocketURL, WebSocketVersion version, String subprotocol, boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength, boolean performMasking, boolean allowMaskMismatch) { + return newHandshaker(webSocketURL, version, subprotocol, allowExtensions, customHeaders, + maxFramePayloadLength, performMasking, allowMaskMismatch, -1); + } + + /** + * Creates a new handshaker. + * + * @param webSocketURL + * URL for web socket communications. e.g "ws://myhost.com/mypath". + * Subsequent web socket frames will be sent to this URL. + * @param version + * Version of web socket specification to use to connect to the server + * @param subprotocol + * Sub protocol request sent to the server. Null if no sub-protocol support is required. + * @param allowExtensions + * Allow extensions to be used in the reserved bits of the web socket frame + * @param customHeaders + * Custom HTTP headers to send during the handshake + * @param maxFramePayloadLength + * Maximum allowable frame payload length. Setting this value to your application's + * requirement may reduce denial of service attacks using long data frames. + * @param performMasking + * Whether to mask all written websocket frames. This must be set to true in order to be fully compatible + * with the websocket specifications. Client applications that communicate with a non-standard server + * which doesn't require masking might set this to false to achieve a higher performance. + * @param allowMaskMismatch + * When set to true, frames which are not masked properly according to the standard will still be + * accepted. + * @param forceCloseTimeoutMillis + * Close the connection if it was not closed by the server after timeout specified + */ + public static WebSocketClientHandshaker newHandshaker( + URI webSocketURL, WebSocketVersion version, String subprotocol, + boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength, + boolean performMasking, boolean allowMaskMismatch, long forceCloseTimeoutMillis) { if (version == V13) { return new WebSocketClientHandshaker13( webSocketURL, V13, subprotocol, allowExtensions, customHeaders, - maxFramePayloadLength, performMasking, allowMaskMismatch); + maxFramePayloadLength, performMasking, allowMaskMismatch, forceCloseTimeoutMillis); } if (version == V08) { return new WebSocketClientHandshaker08( webSocketURL, V08, subprotocol, allowExtensions, customHeaders, - maxFramePayloadLength, performMasking, allowMaskMismatch); + maxFramePayloadLength, performMasking, allowMaskMismatch, forceCloseTimeoutMillis); } if (version == V07) { return new WebSocketClientHandshaker07( webSocketURL, V07, subprotocol, allowExtensions, customHeaders, - maxFramePayloadLength, performMasking, allowMaskMismatch); + maxFramePayloadLength, performMasking, allowMaskMismatch, forceCloseTimeoutMillis); + } + if (version == V00) { + return new WebSocketClientHandshaker00( + webSocketURL, V00, subprotocol, customHeaders, maxFramePayloadLength, forceCloseTimeoutMillis); + } + + throw new WebSocketHandshakeException("Protocol version " + version + " not supported."); + } + + /** + * Creates a new handshaker. + * + * @param webSocketURL + * URL for web socket communications. e.g "ws://myhost.com/mypath". + * Subsequent web socket frames will be sent to this URL. + * @param version + * Version of web socket specification to use to connect to the server + * @param subprotocol + * Sub protocol request sent to the server. Null if no sub-protocol support is required. + * @param allowExtensions + * Allow extensions to be used in the reserved bits of the web socket frame + * @param customHeaders + * Custom HTTP headers to send during the handshake + * @param maxFramePayloadLength + * Maximum allowable frame payload length. Setting this value to your application's + * requirement may reduce denial of service attacks using long data frames. + * @param performMasking + * Whether to mask all written websocket frames. This must be set to true in order to be fully compatible + * with the websocket specifications. Client applications that communicate with a non-standard server + * which doesn't require masking might set this to false to achieve a higher performance. + * @param allowMaskMismatch + * When set to true, frames which are not masked properly according to the standard will still be + * accepted. + * @param forceCloseTimeoutMillis + * Close the connection if it was not closed by the server after timeout specified + * @param absoluteUpgradeUrl + * Use an absolute url for the Upgrade request, typically when connecting through an HTTP proxy over + * clear HTTP + */ + public static WebSocketClientHandshaker newHandshaker( + URI webSocketURL, WebSocketVersion version, String subprotocol, + boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength, + boolean performMasking, boolean allowMaskMismatch, long forceCloseTimeoutMillis, boolean absoluteUpgradeUrl) { + if (version == V13) { + return new WebSocketClientHandshaker13( + webSocketURL, V13, subprotocol, allowExtensions, customHeaders, + maxFramePayloadLength, performMasking, allowMaskMismatch, forceCloseTimeoutMillis, absoluteUpgradeUrl); + } + if (version == V08) { + return new WebSocketClientHandshaker08( + webSocketURL, V08, subprotocol, allowExtensions, customHeaders, + maxFramePayloadLength, performMasking, allowMaskMismatch, forceCloseTimeoutMillis, absoluteUpgradeUrl); + } + if (version == V07) { + return new WebSocketClientHandshaker07( + webSocketURL, V07, subprotocol, allowExtensions, customHeaders, + maxFramePayloadLength, performMasking, allowMaskMismatch, forceCloseTimeoutMillis, absoluteUpgradeUrl); } if (version == V00) { return new WebSocketClientHandshaker00( - webSocketURL, V00, subprotocol, customHeaders, maxFramePayloadLength); + webSocketURL, V00, subprotocol, customHeaders, + maxFramePayloadLength, forceCloseTimeoutMillis, absoluteUpgradeUrl); } throw new WebSocketHandshakeException("Protocol version " + version + " not supported."); diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientProtocolConfig.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientProtocolConfig.java new file mode 100644 index 0000000..5605a8d --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientProtocolConfig.java @@ -0,0 +1,392 @@ +/* + * Copyright 2019 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.codec.http.websocketx; + +import io.netty.handler.codec.http.EmptyHttpHeaders; +import io.netty.handler.codec.http.HttpHeaders; +import io.netty.handler.codec.http.websocketx.WebSocketClientProtocolHandler.ClientHandshakeStateEvent; +import io.netty.util.internal.ObjectUtil; + +import java.net.URI; + +import static io.netty.handler.codec.http.websocketx.WebSocketServerProtocolConfig.DEFAULT_HANDSHAKE_TIMEOUT_MILLIS; +import static io.netty.util.internal.ObjectUtil.checkPositive; + +/** + * WebSocket server configuration. + */ +public final class WebSocketClientProtocolConfig { + + static final boolean DEFAULT_PERFORM_MASKING = true; + static final boolean DEFAULT_ALLOW_MASK_MISMATCH = false; + static final boolean DEFAULT_HANDLE_CLOSE_FRAMES = true; + static final boolean DEFAULT_DROP_PONG_FRAMES = true; + + private final URI webSocketUri; + private final String subprotocol; + private final WebSocketVersion version; + private final boolean allowExtensions; + private final HttpHeaders customHeaders; + private final int maxFramePayloadLength; + private final boolean performMasking; + private final boolean allowMaskMismatch; + private final boolean handleCloseFrames; + private final WebSocketCloseStatus sendCloseFrame; + private final boolean dropPongFrames; + private final long handshakeTimeoutMillis; + private final long forceCloseTimeoutMillis; + private final boolean absoluteUpgradeUrl; + + private WebSocketClientProtocolConfig( + URI webSocketUri, + String subprotocol, + WebSocketVersion version, + boolean allowExtensions, + HttpHeaders customHeaders, + int maxFramePayloadLength, + boolean performMasking, + boolean allowMaskMismatch, + boolean handleCloseFrames, + WebSocketCloseStatus sendCloseFrame, + boolean dropPongFrames, + long handshakeTimeoutMillis, + long forceCloseTimeoutMillis, + boolean absoluteUpgradeUrl + ) { + this.webSocketUri = webSocketUri; + this.subprotocol = subprotocol; + this.version = version; + this.allowExtensions = allowExtensions; + this.customHeaders = customHeaders; + this.maxFramePayloadLength = maxFramePayloadLength; + this.performMasking = performMasking; + this.allowMaskMismatch = allowMaskMismatch; + this.forceCloseTimeoutMillis = forceCloseTimeoutMillis; + this.handleCloseFrames = handleCloseFrames; + this.sendCloseFrame = sendCloseFrame; + this.dropPongFrames = dropPongFrames; + this.handshakeTimeoutMillis = checkPositive(handshakeTimeoutMillis, "handshakeTimeoutMillis"); + this.absoluteUpgradeUrl = absoluteUpgradeUrl; + } + + public URI webSocketUri() { + return webSocketUri; + } + + public String subprotocol() { + return subprotocol; + } + + public WebSocketVersion version() { + return version; + } + + public boolean allowExtensions() { + return allowExtensions; + } + + public HttpHeaders customHeaders() { + return customHeaders; + } + + public int maxFramePayloadLength() { + return maxFramePayloadLength; + } + + public boolean performMasking() { + return performMasking; + } + + public boolean allowMaskMismatch() { + return allowMaskMismatch; + } + + public boolean handleCloseFrames() { + return handleCloseFrames; + } + + public WebSocketCloseStatus sendCloseFrame() { + return sendCloseFrame; + } + + public boolean dropPongFrames() { + return dropPongFrames; + } + + public long handshakeTimeoutMillis() { + return handshakeTimeoutMillis; + } + + public long forceCloseTimeoutMillis() { + return forceCloseTimeoutMillis; + } + + public boolean absoluteUpgradeUrl() { + return absoluteUpgradeUrl; + } + + @Override + public String toString() { + return "WebSocketClientProtocolConfig" + + " {webSocketUri=" + webSocketUri + + ", subprotocol=" + subprotocol + + ", version=" + version + + ", allowExtensions=" + allowExtensions + + ", customHeaders=" + customHeaders + + ", maxFramePayloadLength=" + maxFramePayloadLength + + ", performMasking=" + performMasking + + ", allowMaskMismatch=" + allowMaskMismatch + + ", handleCloseFrames=" + handleCloseFrames + + ", sendCloseFrame=" + sendCloseFrame + + ", dropPongFrames=" + dropPongFrames + + ", handshakeTimeoutMillis=" + handshakeTimeoutMillis + + ", forceCloseTimeoutMillis=" + forceCloseTimeoutMillis + + ", absoluteUpgradeUrl=" + absoluteUpgradeUrl + + "}"; + } + + public Builder toBuilder() { + return new Builder(this); + } + + public static Builder newBuilder() { + return new Builder( + URI.create("https://localhost/"), + null, + WebSocketVersion.V13, + false, + EmptyHttpHeaders.INSTANCE, + 65536, + DEFAULT_PERFORM_MASKING, + DEFAULT_ALLOW_MASK_MISMATCH, + DEFAULT_HANDLE_CLOSE_FRAMES, + WebSocketCloseStatus.NORMAL_CLOSURE, + DEFAULT_DROP_PONG_FRAMES, + DEFAULT_HANDSHAKE_TIMEOUT_MILLIS, + -1, + false); + } + + public static final class Builder { + private URI webSocketUri; + private String subprotocol; + private WebSocketVersion version; + private boolean allowExtensions; + private HttpHeaders customHeaders; + private int maxFramePayloadLength; + private boolean performMasking; + private boolean allowMaskMismatch; + private boolean handleCloseFrames; + private WebSocketCloseStatus sendCloseFrame; + private boolean dropPongFrames; + private long handshakeTimeoutMillis; + private long forceCloseTimeoutMillis; + private boolean absoluteUpgradeUrl; + + private Builder(WebSocketClientProtocolConfig clientConfig) { + this(ObjectUtil.checkNotNull(clientConfig, "clientConfig").webSocketUri(), + clientConfig.subprotocol(), + clientConfig.version(), + clientConfig.allowExtensions(), + clientConfig.customHeaders(), + clientConfig.maxFramePayloadLength(), + clientConfig.performMasking(), + clientConfig.allowMaskMismatch(), + clientConfig.handleCloseFrames(), + clientConfig.sendCloseFrame(), + clientConfig.dropPongFrames(), + clientConfig.handshakeTimeoutMillis(), + clientConfig.forceCloseTimeoutMillis(), + clientConfig.absoluteUpgradeUrl()); + } + + private Builder(URI webSocketUri, + String subprotocol, + WebSocketVersion version, + boolean allowExtensions, + HttpHeaders customHeaders, + int maxFramePayloadLength, + boolean performMasking, + boolean allowMaskMismatch, + boolean handleCloseFrames, + WebSocketCloseStatus sendCloseFrame, + boolean dropPongFrames, + long handshakeTimeoutMillis, + long forceCloseTimeoutMillis, + boolean absoluteUpgradeUrl) { + this.webSocketUri = webSocketUri; + this.subprotocol = subprotocol; + this.version = version; + this.allowExtensions = allowExtensions; + this.customHeaders = customHeaders; + this.maxFramePayloadLength = maxFramePayloadLength; + this.performMasking = performMasking; + this.allowMaskMismatch = allowMaskMismatch; + this.handleCloseFrames = handleCloseFrames; + this.sendCloseFrame = sendCloseFrame; + this.dropPongFrames = dropPongFrames; + this.handshakeTimeoutMillis = handshakeTimeoutMillis; + this.forceCloseTimeoutMillis = forceCloseTimeoutMillis; + this.absoluteUpgradeUrl = absoluteUpgradeUrl; + } + + /** + * URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be + * sent to this URL. + */ + public Builder webSocketUri(String webSocketUri) { + return webSocketUri(URI.create(webSocketUri)); + } + + /** + * URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be + * sent to this URL. + */ + public Builder webSocketUri(URI webSocketUri) { + this.webSocketUri = webSocketUri; + return this; + } + + /** + * Sub protocol request sent to the server. + */ + public Builder subprotocol(String subprotocol) { + this.subprotocol = subprotocol; + return this; + } + + /** + * Version of web socket specification to use to connect to the server + */ + public Builder version(WebSocketVersion version) { + this.version = version; + return this; + } + + /** + * Allow extensions to be used in the reserved bits of the web socket frame + */ + public Builder allowExtensions(boolean allowExtensions) { + this.allowExtensions = allowExtensions; + return this; + } + + /** + * Map of custom headers to add to the client request + */ + public Builder customHeaders(HttpHeaders customHeaders) { + this.customHeaders = customHeaders; + return this; + } + + /** + * Maximum length of a frame's payload + */ + public Builder maxFramePayloadLength(int maxFramePayloadLength) { + this.maxFramePayloadLength = maxFramePayloadLength; + return this; + } + + /** + * Whether to mask all written websocket frames. This must be set to true in order to be fully compatible + * with the websocket specifications. Client applications that communicate with a non-standard server + * which doesn't require masking might set this to false to achieve a higher performance. + */ + public Builder performMasking(boolean performMasking) { + this.performMasking = performMasking; + return this; + } + + /** + * When set to true, frames which are not masked properly according to the standard will still be accepted. + */ + public Builder allowMaskMismatch(boolean allowMaskMismatch) { + this.allowMaskMismatch = allowMaskMismatch; + return this; + } + + /** + * {@code true} if close frames should not be forwarded and just close the channel + */ + public Builder handleCloseFrames(boolean handleCloseFrames) { + this.handleCloseFrames = handleCloseFrames; + return this; + } + + /** + * Close frame to send, when close frame was not send manually. Or {@code null} to disable proper close. + */ + public Builder sendCloseFrame(WebSocketCloseStatus sendCloseFrame) { + this.sendCloseFrame = sendCloseFrame; + return this; + } + + /** + * {@code true} if pong frames should not be forwarded + */ + public Builder dropPongFrames(boolean dropPongFrames) { + this.dropPongFrames = dropPongFrames; + return this; + } + + /** + * Handshake timeout in mills, when handshake timeout, will trigger user + * event {@link ClientHandshakeStateEvent#HANDSHAKE_TIMEOUT} + */ + public Builder handshakeTimeoutMillis(long handshakeTimeoutMillis) { + this.handshakeTimeoutMillis = handshakeTimeoutMillis; + return this; + } + + /** + * Close the connection if it was not closed by the server after timeout specified + */ + public Builder forceCloseTimeoutMillis(long forceCloseTimeoutMillis) { + this.forceCloseTimeoutMillis = forceCloseTimeoutMillis; + return this; + } + + /** + * Use an absolute url for the Upgrade request, typically when connecting through an HTTP proxy over clear HTTP + */ + public Builder absoluteUpgradeUrl(boolean absoluteUpgradeUrl) { + this.absoluteUpgradeUrl = absoluteUpgradeUrl; + return this; + } + + /** + * Build unmodifiable client protocol configuration. + */ + public WebSocketClientProtocolConfig build() { + return new WebSocketClientProtocolConfig( + webSocketUri, + subprotocol, + version, + allowExtensions, + customHeaders, + maxFramePayloadLength, + performMasking, + allowMaskMismatch, + handleCloseFrames, + sendCloseFrame, + dropPongFrames, + handshakeTimeoutMillis, + forceCloseTimeoutMillis, + absoluteUpgradeUrl + ); + } + } +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientProtocolHandler.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientProtocolHandler.java index 7c85c57..cdbc853 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientProtocolHandler.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientProtocolHandler.java @@ -23,6 +23,13 @@ import io.netty.handler.codec.http.HttpHeaders; import java.net.URI; import java.util.List; +import static io.netty.handler.codec.http.websocketx.WebSocketClientProtocolConfig.DEFAULT_ALLOW_MASK_MISMATCH; +import static io.netty.handler.codec.http.websocketx.WebSocketClientProtocolConfig.DEFAULT_DROP_PONG_FRAMES; +import static io.netty.handler.codec.http.websocketx.WebSocketClientProtocolConfig.DEFAULT_HANDLE_CLOSE_FRAMES; +import static io.netty.handler.codec.http.websocketx.WebSocketClientProtocolConfig.DEFAULT_PERFORM_MASKING; +import static io.netty.handler.codec.http.websocketx.WebSocketServerProtocolConfig.DEFAULT_HANDSHAKE_TIMEOUT_MILLIS; +import static io.netty.util.internal.ObjectUtil.*; + /** * This handler does all the heavy lifting for you to run a websocket client. * @@ -38,19 +45,25 @@ import java.util.List; * {@link ClientHandshakeStateEvent#HANDSHAKE_ISSUED} or {@link ClientHandshakeStateEvent#HANDSHAKE_COMPLETE}. */ public class WebSocketClientProtocolHandler extends WebSocketProtocolHandler { - private final WebSocketClientHandshaker handshaker; - private final boolean handleCloseFrames; + private final WebSocketClientProtocolConfig clientConfig; /** * Returns the used handshaker */ - public WebSocketClientHandshaker handshaker() { return handshaker; } + public WebSocketClientHandshaker handshaker() { + return handshaker; + } /** * Events that are fired to notify about handshake status */ public enum ClientHandshakeStateEvent { + /** + * The Handshake was timed out + */ + HANDSHAKE_TIMEOUT, + /** * The Handshake was started but the server did not response yet to the request */ @@ -62,6 +75,30 @@ public class WebSocketClientProtocolHandler extends WebSocketProtocolHandler { HANDSHAKE_COMPLETE } + /** + * Base constructor + * + * @param clientConfig + * Client protocol configuration. + */ + public WebSocketClientProtocolHandler(WebSocketClientProtocolConfig clientConfig) { + super(checkNotNull(clientConfig, "clientConfig").dropPongFrames(), + clientConfig.sendCloseFrame(), clientConfig.forceCloseTimeoutMillis()); + this.handshaker = WebSocketClientHandshakerFactory.newHandshaker( + clientConfig.webSocketUri(), + clientConfig.version(), + clientConfig.subprotocol(), + clientConfig.allowExtensions(), + clientConfig.customHeaders(), + clientConfig.maxFramePayloadLength(), + clientConfig.performMasking(), + clientConfig.allowMaskMismatch(), + clientConfig.forceCloseTimeoutMillis(), + clientConfig.absoluteUpgradeUrl() + ); + this.clientConfig = clientConfig; + } + /** * Base constructor * @@ -90,9 +127,45 @@ public class WebSocketClientProtocolHandler extends WebSocketProtocolHandler { boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength, boolean handleCloseFrames, boolean performMasking, boolean allowMaskMismatch) { + this(webSocketURL, version, subprotocol, allowExtensions, customHeaders, maxFramePayloadLength, + handleCloseFrames, performMasking, allowMaskMismatch, DEFAULT_HANDSHAKE_TIMEOUT_MILLIS); + } + + /** + * Base constructor + * + * @param webSocketURL + * URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be + * sent to this URL. + * @param version + * Version of web socket specification to use to connect to the server + * @param subprotocol + * Sub protocol request sent to the server. + * @param customHeaders + * Map of custom headers to add to the client request + * @param maxFramePayloadLength + * Maximum length of a frame's payload + * @param handleCloseFrames + * {@code true} if close frames should not be forwarded and just close the channel + * @param performMasking + * Whether to mask all written websocket frames. This must be set to true in order to be fully compatible + * with the websocket specifications. Client applications that communicate with a non-standard server + * which doesn't require masking might set this to false to achieve a higher performance. + * @param allowMaskMismatch + * When set to true, frames which are not masked properly according to the standard will still be + * accepted. + * @param handshakeTimeoutMillis + * Handshake timeout in mills, when handshake timeout, will trigger user + * event {@link ClientHandshakeStateEvent#HANDSHAKE_TIMEOUT} + */ + public WebSocketClientProtocolHandler(URI webSocketURL, WebSocketVersion version, String subprotocol, + boolean allowExtensions, HttpHeaders customHeaders, + int maxFramePayloadLength, boolean handleCloseFrames, boolean performMasking, + boolean allowMaskMismatch, long handshakeTimeoutMillis) { this(WebSocketClientHandshakerFactory.newHandshaker(webSocketURL, version, subprotocol, allowExtensions, customHeaders, maxFramePayloadLength, - performMasking, allowMaskMismatch), handleCloseFrames); + performMasking, allowMaskMismatch), + handleCloseFrames, handshakeTimeoutMillis); } /** @@ -116,7 +189,34 @@ public class WebSocketClientProtocolHandler extends WebSocketProtocolHandler { boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength, boolean handleCloseFrames) { this(webSocketURL, version, subprotocol, allowExtensions, customHeaders, maxFramePayloadLength, - handleCloseFrames, true, false); + handleCloseFrames, DEFAULT_HANDSHAKE_TIMEOUT_MILLIS); + } + + /** + * Base constructor + * + * @param webSocketURL + * URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be + * sent to this URL. + * @param version + * Version of web socket specification to use to connect to the server + * @param subprotocol + * Sub protocol request sent to the server. + * @param customHeaders + * Map of custom headers to add to the client request + * @param maxFramePayloadLength + * Maximum length of a frame's payload + * @param handleCloseFrames + * {@code true} if close frames should not be forwarded and just close the channel + * @param handshakeTimeoutMillis + * Handshake timeout in mills, when handshake timeout, will trigger user + * event {@link ClientHandshakeStateEvent#HANDSHAKE_TIMEOUT} + */ + public WebSocketClientProtocolHandler(URI webSocketURL, WebSocketVersion version, String subprotocol, + boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength, + boolean handleCloseFrames, long handshakeTimeoutMillis) { + this(webSocketURL, version, subprotocol, allowExtensions, customHeaders, maxFramePayloadLength, + handleCloseFrames, DEFAULT_PERFORM_MASKING, DEFAULT_ALLOW_MASK_MISMATCH, handshakeTimeoutMillis); } /** @@ -137,8 +237,33 @@ public class WebSocketClientProtocolHandler extends WebSocketProtocolHandler { public WebSocketClientProtocolHandler(URI webSocketURL, WebSocketVersion version, String subprotocol, boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength) { - this(webSocketURL, version, subprotocol, - allowExtensions, customHeaders, maxFramePayloadLength, true); + this(webSocketURL, version, subprotocol, allowExtensions, + customHeaders, maxFramePayloadLength, DEFAULT_HANDSHAKE_TIMEOUT_MILLIS); + } + + /** + * Base constructor + * + * @param webSocketURL + * URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be + * sent to this URL. + * @param version + * Version of web socket specification to use to connect to the server + * @param subprotocol + * Sub protocol request sent to the server. + * @param customHeaders + * Map of custom headers to add to the client request + * @param maxFramePayloadLength + * Maximum length of a frame's payload + * @param handshakeTimeoutMillis + * Handshake timeout in mills, when handshake timeout, will trigger user + * event {@link ClientHandshakeStateEvent#HANDSHAKE_TIMEOUT} + */ + public WebSocketClientProtocolHandler(URI webSocketURL, WebSocketVersion version, String subprotocol, + boolean allowExtensions, HttpHeaders customHeaders, + int maxFramePayloadLength, long handshakeTimeoutMillis) { + this(webSocketURL, version, subprotocol, allowExtensions, customHeaders, + maxFramePayloadLength, DEFAULT_HANDLE_CLOSE_FRAMES, handshakeTimeoutMillis); } /** @@ -151,7 +276,24 @@ public class WebSocketClientProtocolHandler extends WebSocketProtocolHandler { * {@code true} if close frames should not be forwarded and just close the channel */ public WebSocketClientProtocolHandler(WebSocketClientHandshaker handshaker, boolean handleCloseFrames) { - this(handshaker, handleCloseFrames, true); + this(handshaker, handleCloseFrames, DEFAULT_HANDSHAKE_TIMEOUT_MILLIS); + } + + /** + * Base constructor + * + * @param handshaker + * The {@link WebSocketClientHandshaker} which will be used to issue the handshake once the connection + * was established to the remote peer. + * @param handleCloseFrames + * {@code true} if close frames should not be forwarded and just close the channel + * @param handshakeTimeoutMillis + * Handshake timeout in mills, when handshake timeout, will trigger user + * event {@link ClientHandshakeStateEvent#HANDSHAKE_TIMEOUT} + */ + public WebSocketClientProtocolHandler(WebSocketClientHandshaker handshaker, boolean handleCloseFrames, + long handshakeTimeoutMillis) { + this(handshaker, handleCloseFrames, DEFAULT_DROP_PONG_FRAMES, handshakeTimeoutMillis); } /** @@ -167,9 +309,31 @@ public class WebSocketClientProtocolHandler extends WebSocketProtocolHandler { */ public WebSocketClientProtocolHandler(WebSocketClientHandshaker handshaker, boolean handleCloseFrames, boolean dropPongFrames) { + this(handshaker, handleCloseFrames, dropPongFrames, DEFAULT_HANDSHAKE_TIMEOUT_MILLIS); + } + + /** + * Base constructor + * + * @param handshaker + * The {@link WebSocketClientHandshaker} which will be used to issue the handshake once the connection + * was established to the remote peer. + * @param handleCloseFrames + * {@code true} if close frames should not be forwarded and just close the channel + * @param dropPongFrames + * {@code true} if pong frames should not be forwarded + * @param handshakeTimeoutMillis + * Handshake timeout in mills, when handshake timeout, will trigger user + * event {@link ClientHandshakeStateEvent#HANDSHAKE_TIMEOUT} + */ + public WebSocketClientProtocolHandler(WebSocketClientHandshaker handshaker, boolean handleCloseFrames, + boolean dropPongFrames, long handshakeTimeoutMillis) { super(dropPongFrames); this.handshaker = handshaker; - this.handleCloseFrames = handleCloseFrames; + this.clientConfig = WebSocketClientProtocolConfig.newBuilder() + .handleCloseFrames(handleCloseFrames) + .handshakeTimeoutMillis(handshakeTimeoutMillis) + .build(); } /** @@ -180,12 +344,26 @@ public class WebSocketClientProtocolHandler extends WebSocketProtocolHandler { * was established to the remote peer. */ public WebSocketClientProtocolHandler(WebSocketClientHandshaker handshaker) { - this(handshaker, true); + this(handshaker, DEFAULT_HANDSHAKE_TIMEOUT_MILLIS); + } + + /** + * Base constructor + * + * @param handshaker + * The {@link WebSocketClientHandshaker} which will be used to issue the handshake once the connection + * was established to the remote peer. + * @param handshakeTimeoutMillis + * Handshake timeout in mills, when handshake timeout, will trigger user + * event {@link ClientHandshakeStateEvent#HANDSHAKE_TIMEOUT} + */ + public WebSocketClientProtocolHandler(WebSocketClientHandshaker handshaker, long handshakeTimeoutMillis) { + this(handshaker, DEFAULT_HANDLE_CLOSE_FRAMES, handshakeTimeoutMillis); } @Override protected void decode(ChannelHandlerContext ctx, WebSocketFrame frame, List<Object> out) throws Exception { - if (handleCloseFrames && frame instanceof CloseWebSocketFrame) { + if (clientConfig.handleCloseFrames() && frame instanceof CloseWebSocketFrame) { ctx.close(); return; } @@ -198,7 +376,7 @@ public class WebSocketClientProtocolHandler extends WebSocketProtocolHandler { if (cp.get(WebSocketClientProtocolHandshakeHandler.class) == null) { // Add the WebSocketClientProtocolHandshakeHandler before this one. ctx.pipeline().addBefore(ctx.name(), WebSocketClientProtocolHandshakeHandler.class.getName(), - new WebSocketClientProtocolHandshakeHandler(handshaker)); + new WebSocketClientProtocolHandshakeHandler(handshaker, clientConfig.handshakeTimeoutMillis())); } if (cp.get(Utf8FrameValidator.class) == null) { // Add the UFT8 checking before this one. diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientProtocolHandshakeHandler.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientProtocolHandshakeHandler.java index 3d130c6..701af1b 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientProtocolHandshakeHandler.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientProtocolHandshakeHandler.java @@ -19,13 +19,37 @@ import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.ChannelPromise; import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.websocketx.WebSocketClientProtocolHandler.ClientHandshakeStateEvent; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.FutureListener; + +import java.util.concurrent.TimeUnit; + +import static io.netty.util.internal.ObjectUtil.*; class WebSocketClientProtocolHandshakeHandler extends ChannelInboundHandlerAdapter { + private static final long DEFAULT_HANDSHAKE_TIMEOUT_MS = 10000L; + private final WebSocketClientHandshaker handshaker; + private final long handshakeTimeoutMillis; + private ChannelHandlerContext ctx; + private ChannelPromise handshakePromise; WebSocketClientProtocolHandshakeHandler(WebSocketClientHandshaker handshaker) { + this(handshaker, DEFAULT_HANDSHAKE_TIMEOUT_MS); + } + + WebSocketClientProtocolHandshakeHandler(WebSocketClientHandshaker handshaker, long handshakeTimeoutMillis) { this.handshaker = handshaker; + this.handshakeTimeoutMillis = checkPositive(handshakeTimeoutMillis, "handshakeTimeoutMillis"); + } + + @Override + public void handlerAdded(ChannelHandlerContext ctx) throws Exception { + this.ctx = ctx; + handshakePromise = ctx.newPromise(); } @Override @@ -35,6 +59,7 @@ class WebSocketClientProtocolHandshakeHandler extends ChannelInboundHandlerAdapt @Override public void operationComplete(ChannelFuture future) throws Exception { if (!future.isSuccess()) { + handshakePromise.tryFailure(future.cause()); ctx.fireExceptionCaught(future.cause()); } else { ctx.fireUserEventTriggered( @@ -42,6 +67,7 @@ class WebSocketClientProtocolHandshakeHandler extends ChannelInboundHandlerAdapt } } }); + applyHandshakeTimeout(); } @Override @@ -55,6 +81,7 @@ class WebSocketClientProtocolHandshakeHandler extends ChannelInboundHandlerAdapt try { if (!handshaker.isHandshakeComplete()) { handshaker.finishHandshake(ctx.channel(), response); + handshakePromise.trySuccess(); ctx.fireUserEventTriggered( WebSocketClientProtocolHandler.ClientHandshakeStateEvent.HANDSHAKE_COMPLETE); ctx.pipeline().remove(this); @@ -65,4 +92,43 @@ class WebSocketClientProtocolHandshakeHandler extends ChannelInboundHandlerAdapt response.release(); } } + + private void applyHandshakeTimeout() { + final ChannelPromise localHandshakePromise = handshakePromise; + if (handshakeTimeoutMillis <= 0 || localHandshakePromise.isDone()) { + return; + } + + final Future<?> timeoutFuture = ctx.executor().schedule(new Runnable() { + @Override + public void run() { + if (localHandshakePromise.isDone()) { + return; + } + + if (localHandshakePromise.tryFailure(new WebSocketHandshakeException("handshake timed out"))) { + ctx.flush() + .fireUserEventTriggered(ClientHandshakeStateEvent.HANDSHAKE_TIMEOUT) + .close(); + } + } + }, handshakeTimeoutMillis, TimeUnit.MILLISECONDS); + + // Cancel the handshake timeout when handshake is finished. + localHandshakePromise.addListener(new FutureListener<Void>() { + @Override + public void operationComplete(Future<Void> f) throws Exception { + timeoutFuture.cancel(false); + } + }); + } + + /** + * This method is visible for testing. + * + * @return current handshake future + */ + ChannelFuture getHandshakeFuture() { + return handshakePromise; + } } diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketCloseStatus.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketCloseStatus.java new file mode 100644 index 0000000..3069613 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketCloseStatus.java @@ -0,0 +1,314 @@ +/* + * Copyright 2019 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.codec.http.websocketx; + +import static io.netty.util.internal.ObjectUtil.checkNotNull; + +/** + * WebSocket status codes specified in RFC-6455. + * <pre> + * + * RFC-6455 The WebSocket Protocol, December 2011: + * <a href="https://tools.ietf.org/html/rfc6455#section-7.4.1" + * >https://tools.ietf.org/html/rfc6455#section-7.4.1</a> + * + * WebSocket Protocol Registries, April 2019: + * <a href="https://www.iana.org/assignments/websocket/websocket.xhtml#close-code-number" + * >https://www.iana.org/assignments/websocket/websocket.xhtml</a> + * + * 7.4.1. Defined Status Codes + * + * Endpoints MAY use the following pre-defined status codes when sending + * a Close frame. + * + * 1000 + * + * 1000 indicates a normal closure, meaning that the purpose for + * which the connection was established has been fulfilled. + * + * 1001 + * + * 1001 indicates that an endpoint is "going away", such as a server + * going down or a browser having navigated away from a page. + * + * 1002 + * + * 1002 indicates that an endpoint is terminating the connection due + * to a protocol error. + * + * 1003 + * + * 1003 indicates that an endpoint is terminating the connection + * because it has received a type of data it cannot accept (e.g., an + * endpoint that understands only text data MAY send this if it + * receives a binary message). + * + * 1004 + * + * Reserved. The specific meaning might be defined in the future. + * + * 1005 + * + * 1005 is a reserved value and MUST NOT be set as a status code in a + * Close control frame by an endpoint. It is designated for use in + * applications expecting a status code to indicate that no status + * code was actually present. + * + * 1006 + * + * 1006 is a reserved value and MUST NOT be set as a status code in a + * Close control frame by an endpoint. It is designated for use in + * applications expecting a status code to indicate that the + * connection was closed abnormally, e.g., without sending or + * receiving a Close control frame. + * + * 1007 + * + * 1007 indicates that an endpoint is terminating the connection + * because it has received data within a message that was not + * consistent with the type of the message (e.g., non-UTF-8 [RFC3629] + * data within a text message). + * + * 1008 + * + * 1008 indicates that an endpoint is terminating the connection + * because it has received a message that violates its policy. This + * is a generic status code that can be returned when there is no + * other more suitable status code (e.g., 1003 or 1009) or if there + * is a need to hide specific details about the policy. + * + * 1009 + * + * 1009 indicates that an endpoint is terminating the connection + * because it has received a message that is too big for it to + * process. + * + * 1010 + * + * 1010 indicates that an endpoint (client) is terminating the + * connection because it has expected the server to negotiate one or + * more extension, but the server didn't return them in the response + * message of the WebSocket handshake. The list of extensions that + * are needed SHOULD appear in the /reason/ part of the Close frame. + * Note that this status code is not used by the server, because it + * can fail the WebSocket handshake instead. + * + * 1011 + * + * 1011 indicates that a server is terminating the connection because + * it encountered an unexpected condition that prevented it from + * fulfilling the request. + * + * 1012 (IANA Registry, Non RFC-6455) + * + * 1012 indicates that the service is restarted. a client may reconnect, + * and if it choses to do, should reconnect using a randomized delay + * of 5 - 30 seconds. + * + * 1013 (IANA Registry, Non RFC-6455) + * + * 1013 indicates that the service is experiencing overload. a client + * should only connect to a different IP (when there are multiple for the + * target) or reconnect to the same IP upon user action. + * + * 1014 (IANA Registry, Non RFC-6455) + * + * The server was acting as a gateway or proxy and received an invalid + * response from the upstream server. This is similar to 502 HTTP Status Code. + * + * 1015 + * + * 1015 is a reserved value and MUST NOT be set as a status code in a + * Close control frame by an endpoint. It is designated for use in + * applications expecting a status code to indicate that the + * connection was closed due to a failure to perform a TLS handshake + * (e.g., the server certificate can't be verified). + * + * + * 7.4.2. Reserved Status Code Ranges + * + * 0-999 + * + * Status codes in the range 0-999 are not used. + * + * 1000-2999 + * + * Status codes in the range 1000-2999 are reserved for definition by + * this protocol, its future revisions, and extensions specified in a + * permanent and readily available public specification. + * + * 3000-3999 + * + * Status codes in the range 3000-3999 are reserved for use by + * libraries, frameworks, and applications. These status codes are + * registered directly with IANA. The interpretation of these codes + * is undefined by this protocol. + * + * 4000-4999 + * + * Status codes in the range 4000-4999 are reserved for private use + * and thus can't be registered. Such codes can be used by prior + * agreements between WebSocket applications. The interpretation of + * these codes is undefined by this protocol. + * </pre> + * <p> + * While {@link WebSocketCloseStatus} is enum-like structure, its instances should NOT be compared by reference. + * Instead, either {@link #equals(Object)} should be used or direct comparison of {@link #code()} value. + */ +public final class WebSocketCloseStatus implements Comparable<WebSocketCloseStatus> { + + public static final WebSocketCloseStatus NORMAL_CLOSURE = + new WebSocketCloseStatus(1000, "Bye"); + + public static final WebSocketCloseStatus ENDPOINT_UNAVAILABLE = + new WebSocketCloseStatus(1001, "Endpoint unavailable"); + + public static final WebSocketCloseStatus PROTOCOL_ERROR = + new WebSocketCloseStatus(1002, "Protocol error"); + + public static final WebSocketCloseStatus INVALID_MESSAGE_TYPE = + new WebSocketCloseStatus(1003, "Invalid message type"); + + public static final WebSocketCloseStatus INVALID_PAYLOAD_DATA = + new WebSocketCloseStatus(1007, "Invalid payload data"); + + public static final WebSocketCloseStatus POLICY_VIOLATION = + new WebSocketCloseStatus(1008, "Policy violation"); + + public static final WebSocketCloseStatus MESSAGE_TOO_BIG = + new WebSocketCloseStatus(1009, "Message too big"); + + public static final WebSocketCloseStatus MANDATORY_EXTENSION = + new WebSocketCloseStatus(1010, "Mandatory extension"); + + public static final WebSocketCloseStatus INTERNAL_SERVER_ERROR = + new WebSocketCloseStatus(1011, "Internal server error"); + + public static final WebSocketCloseStatus SERVICE_RESTART = + new WebSocketCloseStatus(1012, "Service Restart"); + + public static final WebSocketCloseStatus TRY_AGAIN_LATER = + new WebSocketCloseStatus(1013, "Try Again Later"); + + public static final WebSocketCloseStatus BAD_GATEWAY = + new WebSocketCloseStatus(1014, "Bad Gateway"); + + // 1004, 1005, 1006, 1015 are reserved and should never be used by user + //public static final WebSocketCloseStatus SPECIFIC_MEANING = register(1004, "..."); + //public static final WebSocketCloseStatus EMPTY = register(1005, "Empty"); + //public static final WebSocketCloseStatus ABNORMAL_CLOSURE = register(1006, "Abnormal closure"); + //public static final WebSocketCloseStatus TLS_HANDSHAKE_FAILED(1015, "TLS handshake failed"); + + private final int statusCode; + private final String reasonText; + private String text; + + public WebSocketCloseStatus(int statusCode, String reasonText) { + if (!isValidStatusCode(statusCode)) { + throw new IllegalArgumentException( + "WebSocket close status code does NOT comply with RFC-6455: " + statusCode); + } + this.statusCode = statusCode; + this.reasonText = checkNotNull(reasonText, "reasonText"); + } + + public int code() { + return statusCode; + } + + public String reasonText() { + return reasonText; + } + + /** + * Order of {@link WebSocketCloseStatus} only depends on {@link #code()}. + */ + @Override + public int compareTo(WebSocketCloseStatus o) { + return code() - o.code(); + } + + /** + * Equality of {@link WebSocketCloseStatus} only depends on {@link #code()}. + */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (null == o || getClass() != o.getClass()) { + return false; + } + + WebSocketCloseStatus that = (WebSocketCloseStatus) o; + + return statusCode == that.statusCode; + } + + @Override + public int hashCode() { + return statusCode; + } + + @Override + public String toString() { + String text = this.text; + if (text == null) { + // E.g.: "1000 Bye", "1009 Message too big" + this.text = text = code() + " " + reasonText(); + } + return text; + } + + public static boolean isValidStatusCode(int code) { + return code < 0 || + 1000 <= code && code <= 1003 || + 1007 <= code && code <= 1014 || + 3000 <= code; + } + + public static WebSocketCloseStatus valueOf(int code) { + switch (code) { + case 1000: + return NORMAL_CLOSURE; + case 1001: + return ENDPOINT_UNAVAILABLE; + case 1002: + return PROTOCOL_ERROR; + case 1003: + return INVALID_MESSAGE_TYPE; + case 1007: + return INVALID_PAYLOAD_DATA; + case 1008: + return POLICY_VIOLATION; + case 1009: + return MESSAGE_TOO_BIG; + case 1010: + return MANDATORY_EXTENSION; + case 1011: + return INTERNAL_SERVER_ERROR; + case 1012: + return SERVICE_RESTART; + case 1013: + return TRY_AGAIN_LATER; + case 1014: + return BAD_GATEWAY; + default: + return new WebSocketCloseStatus(code, "Close status #" + code); + } + } + +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketDecoderConfig.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketDecoderConfig.java new file mode 100644 index 0000000..8880ced --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketDecoderConfig.java @@ -0,0 +1,165 @@ +/* + * Copyright 2019 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.codec.http.websocketx; + +import io.netty.util.internal.ObjectUtil; + +/** + * Frames decoder configuration. + */ +public final class WebSocketDecoderConfig { + + static final WebSocketDecoderConfig DEFAULT = + new WebSocketDecoderConfig(65536, true, false, false, true, true); + + private final int maxFramePayloadLength; + private final boolean expectMaskedFrames; + private final boolean allowMaskMismatch; + private final boolean allowExtensions; + private final boolean closeOnProtocolViolation; + private final boolean withUTF8Validator; + + /** + * Constructor + * + * @param maxFramePayloadLength + * Maximum length of a frame's payload. Setting this to an appropriate value for you application + * helps check for denial of services attacks. + * @param expectMaskedFrames + * Web socket servers must set this to true processed incoming masked payload. Client implementations + * must set this to false. + * @param allowMaskMismatch + * Allows to loosen the masking requirement on received frames. When this is set to false then also + * frames which are not masked properly according to the standard will still be accepted. + * @param allowExtensions + * Flag to allow reserved extension bits to be used or not + * @param closeOnProtocolViolation + * Flag to send close frame immediately on any protocol violation.ion. + * @param withUTF8Validator + * Allows you to avoid adding of Utf8FrameValidator to the pipeline on the + * WebSocketServerProtocolHandler creation. This is useful (less overhead) + * when you use only BinaryWebSocketFrame within your web socket connection. + */ + private WebSocketDecoderConfig(int maxFramePayloadLength, boolean expectMaskedFrames, boolean allowMaskMismatch, + boolean allowExtensions, boolean closeOnProtocolViolation, + boolean withUTF8Validator) { + this.maxFramePayloadLength = maxFramePayloadLength; + this.expectMaskedFrames = expectMaskedFrames; + this.allowMaskMismatch = allowMaskMismatch; + this.allowExtensions = allowExtensions; + this.closeOnProtocolViolation = closeOnProtocolViolation; + this.withUTF8Validator = withUTF8Validator; + } + + public int maxFramePayloadLength() { + return maxFramePayloadLength; + } + + public boolean expectMaskedFrames() { + return expectMaskedFrames; + } + + public boolean allowMaskMismatch() { + return allowMaskMismatch; + } + + public boolean allowExtensions() { + return allowExtensions; + } + + public boolean closeOnProtocolViolation() { + return closeOnProtocolViolation; + } + + public boolean withUTF8Validator() { + return withUTF8Validator; + } + + @Override + public String toString() { + return "WebSocketDecoderConfig" + + " [maxFramePayloadLength=" + maxFramePayloadLength + + ", expectMaskedFrames=" + expectMaskedFrames + + ", allowMaskMismatch=" + allowMaskMismatch + + ", allowExtensions=" + allowExtensions + + ", closeOnProtocolViolation=" + closeOnProtocolViolation + + ", withUTF8Validator=" + withUTF8Validator + + "]"; + } + + public Builder toBuilder() { + return new Builder(this); + } + + public static Builder newBuilder() { + return new Builder(DEFAULT); + } + + public static final class Builder { + private int maxFramePayloadLength; + private boolean expectMaskedFrames; + private boolean allowMaskMismatch; + private boolean allowExtensions; + private boolean closeOnProtocolViolation; + private boolean withUTF8Validator; + + private Builder(WebSocketDecoderConfig decoderConfig) { + ObjectUtil.checkNotNull(decoderConfig, "decoderConfig"); + maxFramePayloadLength = decoderConfig.maxFramePayloadLength(); + expectMaskedFrames = decoderConfig.expectMaskedFrames(); + allowMaskMismatch = decoderConfig.allowMaskMismatch(); + allowExtensions = decoderConfig.allowExtensions(); + closeOnProtocolViolation = decoderConfig.closeOnProtocolViolation(); + withUTF8Validator = decoderConfig.withUTF8Validator(); + } + + public Builder maxFramePayloadLength(int maxFramePayloadLength) { + this.maxFramePayloadLength = maxFramePayloadLength; + return this; + } + + public Builder expectMaskedFrames(boolean expectMaskedFrames) { + this.expectMaskedFrames = expectMaskedFrames; + return this; + } + + public Builder allowMaskMismatch(boolean allowMaskMismatch) { + this.allowMaskMismatch = allowMaskMismatch; + return this; + } + + public Builder allowExtensions(boolean allowExtensions) { + this.allowExtensions = allowExtensions; + return this; + } + + public Builder closeOnProtocolViolation(boolean closeOnProtocolViolation) { + this.closeOnProtocolViolation = closeOnProtocolViolation; + return this; + } + + public Builder withUTF8Validator(boolean withUTF8Validator) { + this.withUTF8Validator = withUTF8Validator; + return this; + } + + public WebSocketDecoderConfig build() { + return new WebSocketDecoderConfig( + maxFramePayloadLength, expectMaskedFrames, allowMaskMismatch, + allowExtensions, closeOnProtocolViolation, withUTF8Validator); + } + } +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketFrame.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketFrame.java index 677f047..b6f9a55 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketFrame.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketFrame.java @@ -20,7 +20,7 @@ import io.netty.buffer.DefaultByteBufHolder; import io.netty.util.internal.StringUtil; /** - * Base class for web socket frames + * Base class for web socket frames. */ public abstract class WebSocketFrame extends DefaultByteBufHolder { diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketProtocolHandler.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketProtocolHandler.java index 53532ca..ccbccac 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketProtocolHandler.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketProtocolHandler.java @@ -16,14 +16,27 @@ package io.netty.handler.codec.http.websocketx; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelOutboundHandler; +import io.netty.channel.ChannelPromise; import io.netty.handler.codec.MessageToMessageDecoder; +import io.netty.util.ReferenceCountUtil; +import io.netty.util.concurrent.ScheduledFuture; +import java.net.SocketAddress; +import java.nio.channels.ClosedChannelException; import java.util.List; +import java.util.concurrent.TimeUnit; -abstract class WebSocketProtocolHandler extends MessageToMessageDecoder<WebSocketFrame> { +abstract class WebSocketProtocolHandler extends MessageToMessageDecoder<WebSocketFrame> + implements ChannelOutboundHandler { private final boolean dropPongFrames; + private final WebSocketCloseStatus closeStatus; + private final long forceCloseTimeoutMillis; + private ChannelPromise closeSent; /** * Creates a new {@link WebSocketProtocolHandler} that will <i>drop</i> {@link PongWebSocketFrame}s. @@ -40,7 +53,15 @@ abstract class WebSocketProtocolHandler extends MessageToMessageDecoder<WebSocke * {@code true} if {@link PongWebSocketFrame}s should be dropped */ WebSocketProtocolHandler(boolean dropPongFrames) { + this(dropPongFrames, null, 0L); + } + + WebSocketProtocolHandler(boolean dropPongFrames, + WebSocketCloseStatus closeStatus, + long forceCloseTimeoutMillis) { this.dropPongFrames = dropPongFrames; + this.closeStatus = closeStatus; + this.forceCloseTimeoutMillis = forceCloseTimeoutMillis; } @Override @@ -48,15 +69,111 @@ abstract class WebSocketProtocolHandler extends MessageToMessageDecoder<WebSocke if (frame instanceof PingWebSocketFrame) { frame.content().retain(); ctx.channel().writeAndFlush(new PongWebSocketFrame(frame.content())); + readIfNeeded(ctx); return; } if (frame instanceof PongWebSocketFrame && dropPongFrames) { + readIfNeeded(ctx); return; } out.add(frame.retain()); } + private static void readIfNeeded(ChannelHandlerContext ctx) { + if (!ctx.channel().config().isAutoRead()) { + ctx.read(); + } + } + + @Override + public void close(final ChannelHandlerContext ctx, final ChannelPromise promise) throws Exception { + if (closeStatus == null || !ctx.channel().isActive()) { + ctx.close(promise); + } else { + if (closeSent == null) { + write(ctx, new CloseWebSocketFrame(closeStatus), ctx.newPromise()); + } + flush(ctx); + applyCloseSentTimeout(ctx); + closeSent.addListener(new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture future) { + ctx.close(promise); + } + }); + } + } + + @Override + public void write(final ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { + if (closeSent != null) { + ReferenceCountUtil.release(msg); + promise.setFailure(new ClosedChannelException()); + return; + } + if (msg instanceof CloseWebSocketFrame) { + promise = promise.unvoid(); + closeSent = promise; + } + ctx.write(msg, promise); + } + + private void applyCloseSentTimeout(ChannelHandlerContext ctx) { + if (closeSent.isDone() || forceCloseTimeoutMillis < 0) { + return; + } + + final ScheduledFuture<?> timeoutTask = ctx.executor().schedule(new Runnable() { + @Override + public void run() { + if (!closeSent.isDone()) { + closeSent.tryFailure(new WebSocketHandshakeException("send close frame timed out")); + } + } + }, forceCloseTimeoutMillis, TimeUnit.MILLISECONDS); + + closeSent.addListener(new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture future) { + timeoutTask.cancel(false); + } + }); + } + + @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 disconnect(ChannelHandlerContext ctx, ChannelPromise promise) + throws Exception { + ctx.disconnect(promise); + } + + @Override + public void deregister(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception { + ctx.deregister(promise); + } + + @Override + public void read(ChannelHandlerContext ctx) throws Exception { + ctx.read(); + } + + @Override + public void flush(ChannelHandlerContext ctx) throws Exception { + ctx.flush(); + } + @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { ctx.fireExceptionCaught(cause); diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshaker.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshaker.java index 4eb4348..c6b1b0e 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshaker.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshaker.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 The Netty Project + * Copyright 2019 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 @@ -33,7 +33,7 @@ import io.netty.handler.codec.http.HttpResponseEncoder; import io.netty.handler.codec.http.HttpServerCodec; import io.netty.util.ReferenceCountUtil; import io.netty.util.internal.EmptyArrays; -import io.netty.util.internal.ThrowableUtil; +import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; @@ -47,8 +47,6 @@ import java.util.Set; */ public abstract class WebSocketServerHandshaker { protected static final InternalLogger logger = InternalLoggerFactory.getInstance(WebSocketServerHandshaker.class); - private static final ClosedChannelException CLOSED_CHANNEL_EXCEPTION = ThrowableUtil.unknownStackTrace( - new ClosedChannelException(), WebSocketServerHandshaker.class, "handshake(...)"); private final String uri; @@ -56,7 +54,7 @@ public abstract class WebSocketServerHandshaker { private final WebSocketVersion version; - private final int maxFramePayloadLength; + private final WebSocketDecoderConfig decoderConfig; private String selectedSubprotocol; @@ -81,6 +79,26 @@ public abstract class WebSocketServerHandshaker { protected WebSocketServerHandshaker( WebSocketVersion version, String uri, String subprotocols, int maxFramePayloadLength) { + this(version, uri, subprotocols, WebSocketDecoderConfig.newBuilder() + .maxFramePayloadLength(maxFramePayloadLength) + .build()); + } + + /** + * Constructor specifying the destination web socket location + * + * @param version + * the protocol version + * @param uri + * URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be + * sent to this URL. + * @param subprotocols + * CSV of supported protocols. Null if sub protocols not supported. + * @param decoderConfig + * Frames decoder configuration. + */ + protected WebSocketServerHandshaker( + WebSocketVersion version, String uri, String subprotocols, WebSocketDecoderConfig decoderConfig) { this.version = version; this.uri = uri; if (subprotocols != null) { @@ -92,7 +110,7 @@ public abstract class WebSocketServerHandshaker { } else { this.subprotocols = EmptyArrays.EMPTY_STRINGS; } - this.maxFramePayloadLength = maxFramePayloadLength; + this.decoderConfig = ObjectUtil.checkNotNull(decoderConfig, "decoderConfig"); } /** @@ -124,7 +142,16 @@ public abstract class WebSocketServerHandshaker { * @return The maximum length for a frame's payload */ public int maxFramePayloadLength() { - return maxFramePayloadLength; + return decoderConfig.maxFramePayloadLength(); + } + + /** + * Gets this decoder configuration. + * + * @return This decoder configuration. + */ + public WebSocketDecoderConfig decoderConfig() { + return decoderConfig; } /** @@ -175,15 +202,15 @@ public abstract class WebSocketServerHandshaker { ChannelHandlerContext ctx = p.context(HttpRequestDecoder.class); final String encoderName; if (ctx == null) { - // this means the user use a HttpServerCodec + // this means the user use an HttpServerCodec ctx = p.context(HttpServerCodec.class); if (ctx == null) { promise.setFailure( new IllegalStateException("No HttpDecoder and no HttpServerCodec in the pipeline")); return promise; } - p.addBefore(ctx.name(), "wsdecoder", newWebsocketDecoder()); p.addBefore(ctx.name(), "wsencoder", newWebSocketEncoder()); + p.addBefore(ctx.name(), "wsdecoder", newWebsocketDecoder()); encoderName = ctx.name(); } else { p.replace(ctx.name(), "wsdecoder", newWebsocketDecoder()); @@ -249,7 +276,7 @@ public abstract class WebSocketServerHandshaker { ChannelPipeline p = channel.pipeline(); ChannelHandlerContext ctx = p.context(HttpRequestDecoder.class); if (ctx == null) { - // this means the user use a HttpServerCodec + // this means the user use an HttpServerCodec ctx = p.context(HttpServerCodec.class); if (ctx == null) { promise.setFailure( @@ -282,7 +309,9 @@ public abstract class WebSocketServerHandshaker { @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { // Fail promise if Channel was closed - promise.tryFailure(CLOSED_CHANNEL_EXCEPTION); + if (!promise.isDone()) { + promise.tryFailure(new ClosedChannelException()); + } ctx.fireChannelInactive(); } }); @@ -308,9 +337,7 @@ public abstract class WebSocketServerHandshaker { * Closing Frame that was received */ public ChannelFuture close(Channel channel, CloseWebSocketFrame frame) { - if (channel == null) { - throw new NullPointerException("channel"); - } + ObjectUtil.checkNotNull(channel, "channel"); return close(channel, frame, channel.newPromise()); } @@ -325,9 +352,7 @@ public abstract class WebSocketServerHandshaker { * the {@link ChannelPromise} to be notified when the closing handshake is done */ public ChannelFuture close(Channel channel, CloseWebSocketFrame frame, ChannelPromise promise) { - if (channel == null) { - throw new NullPointerException("channel"); - } + ObjectUtil.checkNotNull(channel, "channel"); return channel.writeAndFlush(frame, promise).addListener(ChannelFutureListener.CLOSE); } diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshaker00.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshaker00.java index ff1797d..7f2cbe2 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshaker00.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshaker00.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 The Netty Project + * Copyright 2019 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 @@ -60,7 +60,24 @@ public class WebSocketServerHandshaker00 extends WebSocketServerHandshaker { * reduce denial of service attacks using long data frames. */ public WebSocketServerHandshaker00(String webSocketURL, String subprotocols, int maxFramePayloadLength) { - super(WebSocketVersion.V00, webSocketURL, subprotocols, maxFramePayloadLength); + this(webSocketURL, subprotocols, WebSocketDecoderConfig.newBuilder() + .maxFramePayloadLength(maxFramePayloadLength) + .build()); + } + + /** + * Constructor specifying the destination web socket location + * + * @param webSocketURL + * URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be + * sent to this URL. + * @param subprotocols + * CSV of supported protocols + * @param decoderConfig + * Frames decoder configuration. + */ + public WebSocketServerHandshaker00(String webSocketURL, String subprotocols, WebSocketDecoderConfig decoderConfig) { + super(WebSocketVersion.V00, webSocketURL, subprotocols, decoderConfig); } /** @@ -116,9 +133,16 @@ public class WebSocketServerHandshaker00 extends WebSocketServerHandshaker { boolean isHixie76 = req.headers().contains(HttpHeaderNames.SEC_WEBSOCKET_KEY1) && req.headers().contains(HttpHeaderNames.SEC_WEBSOCKET_KEY2); + String origin = req.headers().get(HttpHeaderNames.ORIGIN); + //throw before allocating FullHttpResponse + if (origin == null && !isHixie76) { + throw new WebSocketHandshakeException("Missing origin header, got only " + req.headers().names()); + } + // Create the WebSocket handshake response. FullHttpResponse res = new DefaultFullHttpResponse(HTTP_1_1, new HttpResponseStatus(101, - isHixie76 ? "WebSocket Protocol Handshake" : "Web Socket Protocol Handshake")); + isHixie76 ? "WebSocket Protocol Handshake" : "Web Socket Protocol Handshake"), + req.content().alloc().buffer(0)); if (headers != null) { res.headers().add(headers); } @@ -129,7 +153,7 @@ public class WebSocketServerHandshaker00 extends WebSocketServerHandshaker { // Fill in the headers and contents depending on handshake getMethod. if (isHixie76) { // New handshake getMethod with a challenge: - res.headers().add(HttpHeaderNames.SEC_WEBSOCKET_ORIGIN, req.headers().get(HttpHeaderNames.ORIGIN)); + res.headers().add(HttpHeaderNames.SEC_WEBSOCKET_ORIGIN, origin); res.headers().add(HttpHeaderNames.SEC_WEBSOCKET_LOCATION, uri()); String subprotocols = req.headers().get(HttpHeaderNames.SEC_WEBSOCKET_PROTOCOL); @@ -152,14 +176,14 @@ public class WebSocketServerHandshaker00 extends WebSocketServerHandshaker { int b = (int) (Long.parseLong(BEGINNING_DIGIT.matcher(key2).replaceAll("")) / BEGINNING_SPACE.matcher(key2).replaceAll("").length()); long c = req.content().readLong(); - ByteBuf input = Unpooled.buffer(16); + ByteBuf input = Unpooled.wrappedBuffer(new byte[16]).setIndex(0, 0); input.writeInt(a); input.writeInt(b); input.writeLong(c); res.content().writeBytes(WebSocketUtil.md5(input.array())); } else { // Old Hixie 75 handshake getMethod with no challenge: - res.headers().add(HttpHeaderNames.WEBSOCKET_ORIGIN, req.headers().get(HttpHeaderNames.ORIGIN)); + res.headers().add(HttpHeaderNames.WEBSOCKET_ORIGIN, origin); res.headers().add(HttpHeaderNames.WEBSOCKET_LOCATION, uri()); String protocol = req.headers().get(HttpHeaderNames.WEBSOCKET_PROTOCOL); @@ -185,7 +209,7 @@ public class WebSocketServerHandshaker00 extends WebSocketServerHandshaker { @Override protected WebSocketFrameDecoder newWebsocketDecoder() { - return new WebSocket00FrameDecoder(maxFramePayloadLength()); + return new WebSocket00FrameDecoder(decoderConfig()); } @Override diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshaker07.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshaker07.java index 6529109..54f8ff4 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshaker07.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshaker07.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 The Netty Project + * Copyright 2019 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 @@ -37,9 +37,6 @@ public class WebSocketServerHandshaker07 extends WebSocketServerHandshaker { public static final String WEBSOCKET_07_ACCEPT_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; - private final boolean allowExtensions; - private final boolean allowMaskMismatch; - /** * Constructor specifying the destination web socket location * @@ -79,9 +76,21 @@ public class WebSocketServerHandshaker07 extends WebSocketServerHandshaker { public WebSocketServerHandshaker07( String webSocketURL, String subprotocols, boolean allowExtensions, int maxFramePayloadLength, boolean allowMaskMismatch) { - super(WebSocketVersion.V07, webSocketURL, subprotocols, maxFramePayloadLength); - this.allowExtensions = allowExtensions; - this.allowMaskMismatch = allowMaskMismatch; + this(webSocketURL, subprotocols, WebSocketDecoderConfig.newBuilder() + .allowExtensions(allowExtensions) + .maxFramePayloadLength(maxFramePayloadLength) + .allowMaskMismatch(allowMaskMismatch) + .build()); + } + + /** + * Constructor specifying the destination web socket location + * + * @param decoderConfig + * Frames decoder configuration. + */ + public WebSocketServerHandshaker07(String webSocketURL, String subprotocols, WebSocketDecoderConfig decoderConfig) { + super(WebSocketVersion.V07, webSocketURL, subprotocols, decoderConfig); } /** @@ -119,18 +128,19 @@ public class WebSocketServerHandshaker07 extends WebSocketServerHandshaker { */ @Override protected FullHttpResponse newHandshakeResponse(FullHttpRequest req, HttpHeaders headers) { + CharSequence key = req.headers().get(HttpHeaderNames.SEC_WEBSOCKET_KEY); + if (key == null) { + throw new WebSocketHandshakeException("not a WebSocket request: missing key"); + } FullHttpResponse res = - new DefaultFullHttpResponse(HTTP_1_1, HttpResponseStatus.SWITCHING_PROTOCOLS); + new DefaultFullHttpResponse(HTTP_1_1, HttpResponseStatus.SWITCHING_PROTOCOLS, + req.content().alloc().buffer(0)); if (headers != null) { res.headers().add(headers); } - CharSequence key = req.headers().get(HttpHeaderNames.SEC_WEBSOCKET_KEY); - if (key == null) { - throw new WebSocketHandshakeException("not a WebSocket request: missing key"); - } String acceptSeed = key + WEBSOCKET_07_ACCEPT_GUID; byte[] sha1 = WebSocketUtil.sha1(acceptSeed.getBytes(CharsetUtil.US_ASCII)); String accept = WebSocketUtil.base64(sha1); @@ -159,7 +169,7 @@ public class WebSocketServerHandshaker07 extends WebSocketServerHandshaker { @Override protected WebSocketFrameDecoder newWebsocketDecoder() { - return new WebSocket07FrameDecoder(true, allowExtensions, maxFramePayloadLength(), allowMaskMismatch); + return new WebSocket07FrameDecoder(decoderConfig()); } @Override diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshaker08.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshaker08.java index f0b58f8..1e454f4 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshaker08.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshaker08.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 The Netty Project + * Copyright 2019 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 @@ -37,9 +37,6 @@ public class WebSocketServerHandshaker08 extends WebSocketServerHandshaker { public static final String WEBSOCKET_08_ACCEPT_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; - private final boolean allowExtensions; - private final boolean allowMaskMismatch; - /** * Constructor specifying the destination web socket location * @@ -79,9 +76,27 @@ public class WebSocketServerHandshaker08 extends WebSocketServerHandshaker { public WebSocketServerHandshaker08( String webSocketURL, String subprotocols, boolean allowExtensions, int maxFramePayloadLength, boolean allowMaskMismatch) { - super(WebSocketVersion.V08, webSocketURL, subprotocols, maxFramePayloadLength); - this.allowExtensions = allowExtensions; - this.allowMaskMismatch = allowMaskMismatch; + this(webSocketURL, subprotocols, WebSocketDecoderConfig.newBuilder() + .allowExtensions(allowExtensions) + .maxFramePayloadLength(maxFramePayloadLength) + .allowMaskMismatch(allowMaskMismatch) + .build()); + } + + /** + * Constructor specifying the destination web socket location + * + * @param webSocketURL + * URL for web socket communications. e.g "ws://myhost.com/mypath". + * Subsequent web socket frames will be sent to this URL. + * @param subprotocols + * CSV of supported protocols + * @param decoderConfig + * Frames decoder configuration. + */ + public WebSocketServerHandshaker08( + String webSocketURL, String subprotocols, WebSocketDecoderConfig decoderConfig) { + super(WebSocketVersion.V08, webSocketURL, subprotocols, decoderConfig); } /** @@ -120,16 +135,18 @@ public class WebSocketServerHandshaker08 extends WebSocketServerHandshaker { */ @Override protected FullHttpResponse newHandshakeResponse(FullHttpRequest req, HttpHeaders headers) { - FullHttpResponse res = new DefaultFullHttpResponse(HTTP_1_1, HttpResponseStatus.SWITCHING_PROTOCOLS); + CharSequence key = req.headers().get(HttpHeaderNames.SEC_WEBSOCKET_KEY); + if (key == null) { + throw new WebSocketHandshakeException("not a WebSocket request: missing key"); + } + + FullHttpResponse res = new DefaultFullHttpResponse(HTTP_1_1, HttpResponseStatus.SWITCHING_PROTOCOLS, + req.content().alloc().buffer(0)); if (headers != null) { res.headers().add(headers); } - CharSequence key = req.headers().get(HttpHeaderNames.SEC_WEBSOCKET_KEY); - if (key == null) { - throw new WebSocketHandshakeException("not a WebSocket request: missing key"); - } String acceptSeed = key + WEBSOCKET_08_ACCEPT_GUID; byte[] sha1 = WebSocketUtil.sha1(acceptSeed.getBytes(CharsetUtil.US_ASCII)); String accept = WebSocketUtil.base64(sha1); @@ -158,7 +175,7 @@ public class WebSocketServerHandshaker08 extends WebSocketServerHandshaker { @Override protected WebSocketFrameDecoder newWebsocketDecoder() { - return new WebSocket08FrameDecoder(true, allowExtensions, maxFramePayloadLength(), allowMaskMismatch); + return new WebSocket08FrameDecoder(decoderConfig()); } @Override diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshaker13.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshaker13.java index f36d06c..8b445be 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshaker13.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshaker13.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 The Netty Project + * Copyright 2019 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 @@ -28,17 +28,14 @@ import static io.netty.handler.codec.http.HttpVersion.*; /** * <p> - * Performs server side opening and closing handshakes for <a href="http://netty.io/s/rfc6455">RFC 6455</a> - * (originally web socket specification <a href="http://netty.io/s/ws-17">draft-ietf-hybi-thewebsocketprotocol-17</a>). + * Performs server side opening and closing handshakes for <a href="https://netty.io/s/rfc6455">RFC 6455</a> + * (originally web socket specification <a href="https://netty.io/s/ws-17">draft-ietf-hybi-thewebsocketprotocol-17</a>). * </p> */ public class WebSocketServerHandshaker13 extends WebSocketServerHandshaker { public static final String WEBSOCKET_13_ACCEPT_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; - private final boolean allowExtensions; - private final boolean allowMaskMismatch; - /** * Constructor specifying the destination web socket location * @@ -78,9 +75,27 @@ public class WebSocketServerHandshaker13 extends WebSocketServerHandshaker { public WebSocketServerHandshaker13( String webSocketURL, String subprotocols, boolean allowExtensions, int maxFramePayloadLength, boolean allowMaskMismatch) { - super(WebSocketVersion.V13, webSocketURL, subprotocols, maxFramePayloadLength); - this.allowExtensions = allowExtensions; - this.allowMaskMismatch = allowMaskMismatch; + this(webSocketURL, subprotocols, WebSocketDecoderConfig.newBuilder() + .allowExtensions(allowExtensions) + .maxFramePayloadLength(maxFramePayloadLength) + .allowMaskMismatch(allowMaskMismatch) + .build()); + } + + /** + * Constructor specifying the destination web socket location + * + * @param webSocketURL + * URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web + * socket frames will be sent to this URL. + * @param subprotocols + * CSV of supported protocols + * @param decoderConfig + * Frames decoder configuration. + */ + public WebSocketServerHandshaker13( + String webSocketURL, String subprotocols, WebSocketDecoderConfig decoderConfig) { + super(WebSocketVersion.V13, webSocketURL, subprotocols, decoderConfig); } /** @@ -100,7 +115,7 @@ public class WebSocketServerHandshaker13 extends WebSocketServerHandshaker { * Upgrade: websocket * Connection: Upgrade * Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== - * Sec-WebSocket-Origin: http://example.com + * Origin: http://example.com * Sec-WebSocket-Protocol: chat, superchat * Sec-WebSocket-Version: 13 * </pre> @@ -119,15 +134,17 @@ public class WebSocketServerHandshaker13 extends WebSocketServerHandshaker { */ @Override protected FullHttpResponse newHandshakeResponse(FullHttpRequest req, HttpHeaders headers) { - FullHttpResponse res = new DefaultFullHttpResponse(HTTP_1_1, HttpResponseStatus.SWITCHING_PROTOCOLS); - if (headers != null) { - res.headers().add(headers); - } - CharSequence key = req.headers().get(HttpHeaderNames.SEC_WEBSOCKET_KEY); if (key == null) { throw new WebSocketHandshakeException("not a WebSocket request: missing key"); } + + FullHttpResponse res = new DefaultFullHttpResponse(HTTP_1_1, HttpResponseStatus.SWITCHING_PROTOCOLS, + req.content().alloc().buffer(0)); + if (headers != null) { + res.headers().add(headers); + } + String acceptSeed = key + WEBSOCKET_13_ACCEPT_GUID; byte[] sha1 = WebSocketUtil.sha1(acceptSeed.getBytes(CharsetUtil.US_ASCII)); String accept = WebSocketUtil.base64(sha1); @@ -156,7 +173,7 @@ public class WebSocketServerHandshaker13 extends WebSocketServerHandshaker { @Override protected WebSocketFrameDecoder newWebsocketDecoder() { - return new WebSocket13FrameDecoder(true, allowExtensions, maxFramePayloadLength(), allowMaskMismatch); + return new WebSocket13FrameDecoder(decoderConfig()); } @Override diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshakerFactory.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshakerFactory.java index 27fdfa0..8f1740f 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshakerFactory.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshakerFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 The Netty Project + * Copyright 2019 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 @@ -25,6 +25,7 @@ import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.HttpVersion; +import io.netty.util.internal.ObjectUtil; /** * Auto-detects the version of the Web Socket protocol in use and creates a new proper @@ -36,11 +37,7 @@ public class WebSocketServerHandshakerFactory { private final String subprotocols; - private final boolean allowExtensions; - - private final int maxFramePayloadLength; - - private final boolean allowMaskMismatch; + private final WebSocketDecoderConfig decoderConfig; /** * Constructor specifying the destination web socket location @@ -98,11 +95,29 @@ public class WebSocketServerHandshakerFactory { public WebSocketServerHandshakerFactory( String webSocketURL, String subprotocols, boolean allowExtensions, int maxFramePayloadLength, boolean allowMaskMismatch) { + this(webSocketURL, subprotocols, WebSocketDecoderConfig.newBuilder() + .allowExtensions(allowExtensions) + .maxFramePayloadLength(maxFramePayloadLength) + .allowMaskMismatch(allowMaskMismatch) + .build()); + } + + /** + * Constructor specifying the destination web socket location + * + * @param webSocketURL + * URL for web socket communications. e.g "ws://myhost.com/mypath". + * Subsequent web socket frames will be sent to this URL. + * @param subprotocols + * CSV of supported protocols. Null if sub protocols not supported. + * @param decoderConfig + * Frames decoder options. + */ + public WebSocketServerHandshakerFactory( + String webSocketURL, String subprotocols, WebSocketDecoderConfig decoderConfig) { this.webSocketURL = webSocketURL; this.subprotocols = subprotocols; - this.allowExtensions = allowExtensions; - this.maxFramePayloadLength = maxFramePayloadLength; - this.allowMaskMismatch = allowMaskMismatch; + this.decoderConfig = ObjectUtil.checkNotNull(decoderConfig, "decoderConfig"); } /** @@ -118,21 +133,21 @@ public class WebSocketServerHandshakerFactory { if (version.equals(WebSocketVersion.V13.toHttpHeaderValue())) { // Version 13 of the wire protocol - RFC 6455 (version 17 of the draft hybi specification). return new WebSocketServerHandshaker13( - webSocketURL, subprotocols, allowExtensions, maxFramePayloadLength, allowMaskMismatch); + webSocketURL, subprotocols, decoderConfig); } else if (version.equals(WebSocketVersion.V08.toHttpHeaderValue())) { // Version 8 of the wire protocol - version 10 of the draft hybi specification. return new WebSocketServerHandshaker08( - webSocketURL, subprotocols, allowExtensions, maxFramePayloadLength, allowMaskMismatch); + webSocketURL, subprotocols, decoderConfig); } else if (version.equals(WebSocketVersion.V07.toHttpHeaderValue())) { // Version 8 of the wire protocol - version 07 of the draft hybi specification. return new WebSocketServerHandshaker07( - webSocketURL, subprotocols, allowExtensions, maxFramePayloadLength, allowMaskMismatch); + webSocketURL, subprotocols, decoderConfig); } else { return null; } } else { // Assume version 00 where version header was not specified - return new WebSocketServerHandshaker00(webSocketURL, subprotocols, maxFramePayloadLength); + return new WebSocketServerHandshaker00(webSocketURL, subprotocols, decoderConfig); } } @@ -157,7 +172,7 @@ public class WebSocketServerHandshakerFactory { public static ChannelFuture sendUnsupportedVersionResponse(Channel channel, ChannelPromise promise) { HttpResponse res = new DefaultFullHttpResponse( HttpVersion.HTTP_1_1, - HttpResponseStatus.UPGRADE_REQUIRED); + HttpResponseStatus.UPGRADE_REQUIRED, channel.alloc().buffer(0)); res.headers().set(HttpHeaderNames.SEC_WEBSOCKET_VERSION, WebSocketVersion.V13.toHttpHeaderValue()); HttpUtil.setContentLength(res, 0); return channel.writeAndFlush(res, promise); diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerProtocolConfig.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerProtocolConfig.java new file mode 100644 index 0000000..a96d3ac --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerProtocolConfig.java @@ -0,0 +1,296 @@ +/* + * Copyright 2019 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.codec.http.websocketx; + +import io.netty.handler.codec.http.websocketx.WebSocketClientProtocolHandler.ClientHandshakeStateEvent; +import io.netty.util.internal.ObjectUtil; + +import static io.netty.util.internal.ObjectUtil.checkPositive; + +/** + * WebSocket server configuration. + */ +public final class WebSocketServerProtocolConfig { + + static final long DEFAULT_HANDSHAKE_TIMEOUT_MILLIS = 10000L; + + private final String websocketPath; + private final String subprotocols; + private final boolean checkStartsWith; + private final long handshakeTimeoutMillis; + private final long forceCloseTimeoutMillis; + private final boolean handleCloseFrames; + private final WebSocketCloseStatus sendCloseFrame; + private final boolean dropPongFrames; + private final WebSocketDecoderConfig decoderConfig; + + private WebSocketServerProtocolConfig( + String websocketPath, + String subprotocols, + boolean checkStartsWith, + long handshakeTimeoutMillis, + long forceCloseTimeoutMillis, + boolean handleCloseFrames, + WebSocketCloseStatus sendCloseFrame, + boolean dropPongFrames, + WebSocketDecoderConfig decoderConfig + ) { + this.websocketPath = websocketPath; + this.subprotocols = subprotocols; + this.checkStartsWith = checkStartsWith; + this.handshakeTimeoutMillis = checkPositive(handshakeTimeoutMillis, "handshakeTimeoutMillis"); + this.forceCloseTimeoutMillis = forceCloseTimeoutMillis; + this.handleCloseFrames = handleCloseFrames; + this.sendCloseFrame = sendCloseFrame; + this.dropPongFrames = dropPongFrames; + this.decoderConfig = decoderConfig == null ? WebSocketDecoderConfig.DEFAULT : decoderConfig; + } + + public String websocketPath() { + return websocketPath; + } + + public String subprotocols() { + return subprotocols; + } + + public boolean checkStartsWith() { + return checkStartsWith; + } + + public long handshakeTimeoutMillis() { + return handshakeTimeoutMillis; + } + + public long forceCloseTimeoutMillis() { + return forceCloseTimeoutMillis; + } + + public boolean handleCloseFrames() { + return handleCloseFrames; + } + + public WebSocketCloseStatus sendCloseFrame() { + return sendCloseFrame; + } + + public boolean dropPongFrames() { + return dropPongFrames; + } + + public WebSocketDecoderConfig decoderConfig() { + return decoderConfig; + } + + @Override + public String toString() { + return "WebSocketServerProtocolConfig" + + " {websocketPath=" + websocketPath + + ", subprotocols=" + subprotocols + + ", checkStartsWith=" + checkStartsWith + + ", handshakeTimeoutMillis=" + handshakeTimeoutMillis + + ", forceCloseTimeoutMillis=" + forceCloseTimeoutMillis + + ", handleCloseFrames=" + handleCloseFrames + + ", sendCloseFrame=" + sendCloseFrame + + ", dropPongFrames=" + dropPongFrames + + ", decoderConfig=" + decoderConfig + + "}"; + } + + public Builder toBuilder() { + return new Builder(this); + } + + public static Builder newBuilder() { + return new Builder("/", null, false, DEFAULT_HANDSHAKE_TIMEOUT_MILLIS, 0L, + true, WebSocketCloseStatus.NORMAL_CLOSURE, true, WebSocketDecoderConfig.DEFAULT); + } + + public static final class Builder { + private String websocketPath; + private String subprotocols; + private boolean checkStartsWith; + private long handshakeTimeoutMillis; + private long forceCloseTimeoutMillis; + private boolean handleCloseFrames; + private WebSocketCloseStatus sendCloseFrame; + private boolean dropPongFrames; + private WebSocketDecoderConfig decoderConfig; + private WebSocketDecoderConfig.Builder decoderConfigBuilder; + + private Builder(WebSocketServerProtocolConfig serverConfig) { + this(ObjectUtil.checkNotNull(serverConfig, "serverConfig").websocketPath(), + serverConfig.subprotocols(), + serverConfig.checkStartsWith(), + serverConfig.handshakeTimeoutMillis(), + serverConfig.forceCloseTimeoutMillis(), + serverConfig.handleCloseFrames(), + serverConfig.sendCloseFrame(), + serverConfig.dropPongFrames(), + serverConfig.decoderConfig() + ); + } + + private Builder(String websocketPath, + String subprotocols, + boolean checkStartsWith, + long handshakeTimeoutMillis, + long forceCloseTimeoutMillis, + boolean handleCloseFrames, + WebSocketCloseStatus sendCloseFrame, + boolean dropPongFrames, + WebSocketDecoderConfig decoderConfig) { + this.websocketPath = websocketPath; + this.subprotocols = subprotocols; + this.checkStartsWith = checkStartsWith; + this.handshakeTimeoutMillis = handshakeTimeoutMillis; + this.forceCloseTimeoutMillis = forceCloseTimeoutMillis; + this.handleCloseFrames = handleCloseFrames; + this.sendCloseFrame = sendCloseFrame; + this.dropPongFrames = dropPongFrames; + this.decoderConfig = decoderConfig; + } + + /** + * URI path component to handle websocket upgrade requests on. + */ + public Builder websocketPath(String websocketPath) { + this.websocketPath = websocketPath; + return this; + } + + /** + * CSV of supported protocols + */ + public Builder subprotocols(String subprotocols) { + this.subprotocols = subprotocols; + return this; + } + + /** + * {@code true} to handle all requests, where URI path component starts from + * {@link WebSocketServerProtocolConfig#websocketPath()}, {@code false} for exact match (default). + */ + public Builder checkStartsWith(boolean checkStartsWith) { + this.checkStartsWith = checkStartsWith; + return this; + } + + /** + * Handshake timeout in mills, when handshake timeout, will trigger user + * event {@link ClientHandshakeStateEvent#HANDSHAKE_TIMEOUT} + */ + public Builder handshakeTimeoutMillis(long handshakeTimeoutMillis) { + this.handshakeTimeoutMillis = handshakeTimeoutMillis; + return this; + } + + /** + * Close the connection if it was not closed by the client after timeout specified + */ + public Builder forceCloseTimeoutMillis(long forceCloseTimeoutMillis) { + this.forceCloseTimeoutMillis = forceCloseTimeoutMillis; + return this; + } + + /** + * {@code true} if close frames should not be forwarded and just close the channel + */ + public Builder handleCloseFrames(boolean handleCloseFrames) { + this.handleCloseFrames = handleCloseFrames; + return this; + } + + /** + * Close frame to send, when close frame was not send manually. Or {@code null} to disable proper close. + */ + public Builder sendCloseFrame(WebSocketCloseStatus sendCloseFrame) { + this.sendCloseFrame = sendCloseFrame; + return this; + } + + /** + * {@code true} if pong frames should not be forwarded + */ + public Builder dropPongFrames(boolean dropPongFrames) { + this.dropPongFrames = dropPongFrames; + return this; + } + + /** + * Frames decoder configuration. + */ + public Builder decoderConfig(WebSocketDecoderConfig decoderConfig) { + this.decoderConfig = decoderConfig == null ? WebSocketDecoderConfig.DEFAULT : decoderConfig; + this.decoderConfigBuilder = null; + return this; + } + + private WebSocketDecoderConfig.Builder decoderConfigBuilder() { + if (decoderConfigBuilder == null) { + decoderConfigBuilder = decoderConfig.toBuilder(); + } + return decoderConfigBuilder; + } + + public Builder maxFramePayloadLength(int maxFramePayloadLength) { + decoderConfigBuilder().maxFramePayloadLength(maxFramePayloadLength); + return this; + } + + public Builder expectMaskedFrames(boolean expectMaskedFrames) { + decoderConfigBuilder().expectMaskedFrames(expectMaskedFrames); + return this; + } + + public Builder allowMaskMismatch(boolean allowMaskMismatch) { + decoderConfigBuilder().allowMaskMismatch(allowMaskMismatch); + return this; + } + + public Builder allowExtensions(boolean allowExtensions) { + decoderConfigBuilder().allowExtensions(allowExtensions); + return this; + } + + public Builder closeOnProtocolViolation(boolean closeOnProtocolViolation) { + decoderConfigBuilder().closeOnProtocolViolation(closeOnProtocolViolation); + return this; + } + + public Builder withUTF8Validator(boolean withUTF8Validator) { + decoderConfigBuilder().withUTF8Validator(withUTF8Validator); + return this; + } + + /** + * Build unmodifiable server protocol configuration. + */ + public WebSocketServerProtocolConfig build() { + return new WebSocketServerProtocolConfig( + websocketPath, + subprotocols, + checkStartsWith, + handshakeTimeoutMillis, + forceCloseTimeoutMillis, + handleCloseFrames, + sendCloseFrame, + dropPongFrames, + decoderConfigBuilder == null ? decoderConfig : decoderConfigBuilder.build() + ); + } + } +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerProtocolHandler.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerProtocolHandler.java index 3a19b14..9d7f6ab 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerProtocolHandler.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerProtocolHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 The Netty Project + * Copyright 2019 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 @@ -18,13 +18,10 @@ package io.netty.handler.codec.http.websocketx; import io.netty.buffer.Unpooled; import io.netty.channel.Channel; import io.netty.channel.ChannelFutureListener; -import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandler; -import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.channel.ChannelPipeline; import io.netty.handler.codec.http.DefaultFullHttpResponse; -import io.netty.handler.codec.http.FullHttpRequest; import io.netty.handler.codec.http.FullHttpResponse; import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.HttpResponseStatus; @@ -32,7 +29,9 @@ import io.netty.util.AttributeKey; import java.util.List; -import static io.netty.handler.codec.http.HttpVersion.*; +import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1; +import static io.netty.handler.codec.http.websocketx.WebSocketServerProtocolConfig.DEFAULT_HANDSHAKE_TIMEOUT_MILLIS; +import static io.netty.util.internal.ObjectUtil.checkNotNull; /** * This handler does all the heavy lifting for you to run a websocket server. @@ -64,7 +63,12 @@ public class WebSocketServerProtocolHandler extends WebSocketProtocolHandler { * it provides extra information about the handshake */ @Deprecated - HANDSHAKE_COMPLETE + HANDSHAKE_COMPLETE, + + /** + * The Handshake was timed out + */ + HANDSHAKE_TIMEOUT } /** @@ -97,54 +101,119 @@ public class WebSocketServerProtocolHandler extends WebSocketProtocolHandler { private static final AttributeKey<WebSocketServerHandshaker> HANDSHAKER_ATTR_KEY = AttributeKey.valueOf(WebSocketServerHandshaker.class, "HANDSHAKER"); - private final String websocketPath; - private final String subprotocols; - private final boolean allowExtensions; - private final int maxFramePayloadLength; - private final boolean allowMaskMismatch; - private final boolean checkStartsWith; + private final WebSocketServerProtocolConfig serverConfig; + + /** + * Base constructor + * + * @param serverConfig + * Server protocol configuration. + */ + public WebSocketServerProtocolHandler(WebSocketServerProtocolConfig serverConfig) { + super(checkNotNull(serverConfig, "serverConfig").dropPongFrames(), + serverConfig.sendCloseFrame(), + serverConfig.forceCloseTimeoutMillis() + ); + this.serverConfig = serverConfig; + } public WebSocketServerProtocolHandler(String websocketPath) { - this(websocketPath, null, false); + this(websocketPath, DEFAULT_HANDSHAKE_TIMEOUT_MILLIS); + } + + public WebSocketServerProtocolHandler(String websocketPath, long handshakeTimeoutMillis) { + this(websocketPath, false, handshakeTimeoutMillis); } public WebSocketServerProtocolHandler(String websocketPath, boolean checkStartsWith) { - this(websocketPath, null, false, 65536, false, checkStartsWith); + this(websocketPath, checkStartsWith, DEFAULT_HANDSHAKE_TIMEOUT_MILLIS); + } + + public WebSocketServerProtocolHandler(String websocketPath, boolean checkStartsWith, long handshakeTimeoutMillis) { + this(websocketPath, null, false, 65536, false, checkStartsWith, handshakeTimeoutMillis); } public WebSocketServerProtocolHandler(String websocketPath, String subprotocols) { - this(websocketPath, subprotocols, false); + this(websocketPath, subprotocols, DEFAULT_HANDSHAKE_TIMEOUT_MILLIS); + } + + public WebSocketServerProtocolHandler(String websocketPath, String subprotocols, long handshakeTimeoutMillis) { + this(websocketPath, subprotocols, false, handshakeTimeoutMillis); } public WebSocketServerProtocolHandler(String websocketPath, String subprotocols, boolean allowExtensions) { - this(websocketPath, subprotocols, allowExtensions, 65536); + this(websocketPath, subprotocols, allowExtensions, DEFAULT_HANDSHAKE_TIMEOUT_MILLIS); + } + + public WebSocketServerProtocolHandler(String websocketPath, String subprotocols, boolean allowExtensions, + long handshakeTimeoutMillis) { + this(websocketPath, subprotocols, allowExtensions, 65536, handshakeTimeoutMillis); } public WebSocketServerProtocolHandler(String websocketPath, String subprotocols, boolean allowExtensions, int maxFrameSize) { - this(websocketPath, subprotocols, allowExtensions, maxFrameSize, false); + this(websocketPath, subprotocols, allowExtensions, maxFrameSize, DEFAULT_HANDSHAKE_TIMEOUT_MILLIS); + } + + public WebSocketServerProtocolHandler(String websocketPath, String subprotocols, + boolean allowExtensions, int maxFrameSize, long handshakeTimeoutMillis) { + this(websocketPath, subprotocols, allowExtensions, maxFrameSize, false, handshakeTimeoutMillis); } public WebSocketServerProtocolHandler(String websocketPath, String subprotocols, boolean allowExtensions, int maxFrameSize, boolean allowMaskMismatch) { - this(websocketPath, subprotocols, allowExtensions, maxFrameSize, allowMaskMismatch, false); + this(websocketPath, subprotocols, allowExtensions, maxFrameSize, allowMaskMismatch, + DEFAULT_HANDSHAKE_TIMEOUT_MILLIS); + } + + public WebSocketServerProtocolHandler(String websocketPath, String subprotocols, boolean allowExtensions, + int maxFrameSize, boolean allowMaskMismatch, long handshakeTimeoutMillis) { + this(websocketPath, subprotocols, allowExtensions, maxFrameSize, allowMaskMismatch, false, + handshakeTimeoutMillis); } public WebSocketServerProtocolHandler(String websocketPath, String subprotocols, boolean allowExtensions, int maxFrameSize, boolean allowMaskMismatch, boolean checkStartsWith) { - this(websocketPath, subprotocols, allowExtensions, maxFrameSize, allowMaskMismatch, checkStartsWith, true); + this(websocketPath, subprotocols, allowExtensions, maxFrameSize, allowMaskMismatch, checkStartsWith, + DEFAULT_HANDSHAKE_TIMEOUT_MILLIS); + } + + public WebSocketServerProtocolHandler(String websocketPath, String subprotocols, + boolean allowExtensions, int maxFrameSize, boolean allowMaskMismatch, + boolean checkStartsWith, long handshakeTimeoutMillis) { + this(websocketPath, subprotocols, allowExtensions, maxFrameSize, allowMaskMismatch, checkStartsWith, true, + handshakeTimeoutMillis); } public WebSocketServerProtocolHandler(String websocketPath, String subprotocols, boolean allowExtensions, int maxFrameSize, boolean allowMaskMismatch, boolean checkStartsWith, boolean dropPongFrames) { - super(dropPongFrames); - this.websocketPath = websocketPath; - this.subprotocols = subprotocols; - this.allowExtensions = allowExtensions; - maxFramePayloadLength = maxFrameSize; - this.allowMaskMismatch = allowMaskMismatch; - this.checkStartsWith = checkStartsWith; + this(websocketPath, subprotocols, allowExtensions, maxFrameSize, allowMaskMismatch, checkStartsWith, + dropPongFrames, DEFAULT_HANDSHAKE_TIMEOUT_MILLIS); + } + + public WebSocketServerProtocolHandler(String websocketPath, String subprotocols, boolean allowExtensions, + int maxFrameSize, boolean allowMaskMismatch, boolean checkStartsWith, + boolean dropPongFrames, long handshakeTimeoutMillis) { + this(websocketPath, subprotocols, checkStartsWith, dropPongFrames, handshakeTimeoutMillis, + WebSocketDecoderConfig.newBuilder() + .maxFramePayloadLength(maxFrameSize) + .allowMaskMismatch(allowMaskMismatch) + .allowExtensions(allowExtensions) + .build()); + } + + public WebSocketServerProtocolHandler(String websocketPath, String subprotocols, boolean checkStartsWith, + boolean dropPongFrames, long handshakeTimeoutMillis, + WebSocketDecoderConfig decoderConfig) { + this(WebSocketServerProtocolConfig.newBuilder() + .websocketPath(websocketPath) + .subprotocols(subprotocols) + .checkStartsWith(checkStartsWith) + .handshakeTimeoutMillis(handshakeTimeoutMillis) + .dropPongFrames(dropPongFrames) + .decoderConfig(decoderConfig) + .build()); } @Override @@ -152,20 +221,19 @@ public class WebSocketServerProtocolHandler extends WebSocketProtocolHandler { ChannelPipeline cp = ctx.pipeline(); if (cp.get(WebSocketServerProtocolHandshakeHandler.class) == null) { // Add the WebSocketHandshakeHandler before this one. - ctx.pipeline().addBefore(ctx.name(), WebSocketServerProtocolHandshakeHandler.class.getName(), - new WebSocketServerProtocolHandshakeHandler(websocketPath, subprotocols, - allowExtensions, maxFramePayloadLength, allowMaskMismatch, checkStartsWith)); + cp.addBefore(ctx.name(), WebSocketServerProtocolHandshakeHandler.class.getName(), + new WebSocketServerProtocolHandshakeHandler(serverConfig)); } - if (cp.get(Utf8FrameValidator.class) == null) { + if (serverConfig.decoderConfig().withUTF8Validator() && cp.get(Utf8FrameValidator.class) == null) { // Add the UFT8 checking before this one. - ctx.pipeline().addBefore(ctx.name(), Utf8FrameValidator.class.getName(), + cp.addBefore(ctx.name(), Utf8FrameValidator.class.getName(), new Utf8FrameValidator()); } } @Override protected void decode(ChannelHandlerContext ctx, WebSocketFrame frame, List<Object> out) throws Exception { - if (frame instanceof CloseWebSocketFrame) { + if (serverConfig.handleCloseFrames() && frame instanceof CloseWebSocketFrame) { WebSocketServerHandshaker handshaker = getHandshaker(ctx.channel()); if (handshaker != null) { frame.retain(); @@ -197,20 +265,4 @@ public class WebSocketServerProtocolHandler extends WebSocketProtocolHandler { static void setHandshaker(Channel channel, WebSocketServerHandshaker handshaker) { channel.attr(HANDSHAKER_ATTR_KEY).set(handshaker); } - - static ChannelHandler forbiddenHttpRequestResponder() { - return new ChannelInboundHandlerAdapter() { - @Override - public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { - if (msg instanceof FullHttpRequest) { - ((FullHttpRequest) msg).release(); - FullHttpResponse response = - new DefaultFullHttpResponse(HTTP_1_1, HttpResponseStatus.FORBIDDEN); - ctx.channel().writeAndFlush(response); - } else { - ctx.fireChannelRead(msg); - } - } - }; - } } diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerProtocolHandshakeHandler.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerProtocolHandshakeHandler.java index 26e01f1..ec9b4ff 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerProtocolHandshakeHandler.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerProtocolHandshakeHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 The Netty Project + * Copyright 2019 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 @@ -20,43 +20,42 @@ import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.channel.ChannelPipeline; +import io.netty.channel.ChannelPromise; import io.netty.handler.codec.http.DefaultFullHttpResponse; import io.netty.handler.codec.http.FullHttpRequest; import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponse; +import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler.ServerHandshakeStateEvent; import io.netty.handler.ssl.SslHandler; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.FutureListener; + +import java.util.concurrent.TimeUnit; -import static io.netty.handler.codec.http.HttpUtil.*; import static io.netty.handler.codec.http.HttpMethod.*; import static io.netty.handler.codec.http.HttpResponseStatus.*; +import static io.netty.handler.codec.http.HttpUtil.*; import static io.netty.handler.codec.http.HttpVersion.*; +import static io.netty.util.internal.ObjectUtil.*; /** * Handles the HTTP handshake (the HTTP Upgrade request) for {@link WebSocketServerProtocolHandler}. */ class WebSocketServerProtocolHandshakeHandler extends ChannelInboundHandlerAdapter { - private final String websocketPath; - private final String subprotocols; - private final boolean allowExtensions; - private final int maxFramePayloadSize; - private final boolean allowMaskMismatch; - private final boolean checkStartsWith; + private final WebSocketServerProtocolConfig serverConfig; + private ChannelHandlerContext ctx; + private ChannelPromise handshakePromise; - WebSocketServerProtocolHandshakeHandler(String websocketPath, String subprotocols, - boolean allowExtensions, int maxFrameSize, boolean allowMaskMismatch) { - this(websocketPath, subprotocols, allowExtensions, maxFrameSize, allowMaskMismatch, false); + WebSocketServerProtocolHandshakeHandler(WebSocketServerProtocolConfig serverConfig) { + this.serverConfig = checkNotNull(serverConfig, "serverConfig"); } - WebSocketServerProtocolHandshakeHandler(String websocketPath, String subprotocols, - boolean allowExtensions, int maxFrameSize, boolean allowMaskMismatch, boolean checkStartsWith) { - this.websocketPath = websocketPath; - this.subprotocols = subprotocols; - this.allowExtensions = allowExtensions; - maxFramePayloadSize = maxFrameSize; - this.allowMaskMismatch = allowMaskMismatch; - this.checkStartsWith = checkStartsWith; + @Override + public void handlerAdded(ChannelHandlerContext ctx) { + this.ctx = ctx; + handshakePromise = ctx.newPromise(); } @Override @@ -68,25 +67,36 @@ class WebSocketServerProtocolHandshakeHandler extends ChannelInboundHandlerAdapt } try { - if (req.method() != GET) { - sendHttpResponse(ctx, req, new DefaultFullHttpResponse(HTTP_1_1, FORBIDDEN)); + if (!GET.equals(req.method())) { + sendHttpResponse(ctx, req, new DefaultFullHttpResponse(HTTP_1_1, FORBIDDEN, ctx.alloc().buffer(0))); return; } final WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory( - getWebSocketLocation(ctx.pipeline(), req, websocketPath), subprotocols, - allowExtensions, maxFramePayloadSize, allowMaskMismatch); + getWebSocketLocation(ctx.pipeline(), req, serverConfig.websocketPath()), + serverConfig.subprotocols(), serverConfig.decoderConfig()); final WebSocketServerHandshaker handshaker = wsFactory.newHandshaker(req); + final ChannelPromise localHandshakePromise = handshakePromise; if (handshaker == null) { WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel()); } else { + // Ensure we set the handshaker and replace this handler before we + // trigger the actual handshake. Otherwise we may receive websocket bytes in this handler + // before we had a chance to replace it. + // + // See https://github.com/netty/netty/issues/9471. + WebSocketServerProtocolHandler.setHandshaker(ctx.channel(), handshaker); + ctx.pipeline().remove(this); + final ChannelFuture handshakeFuture = handshaker.handshake(ctx.channel(), req); handshakeFuture.addListener(new ChannelFutureListener() { @Override - public void operationComplete(ChannelFuture future) throws Exception { + public void operationComplete(ChannelFuture future) { if (!future.isSuccess()) { + localHandshakePromise.tryFailure(future.cause()); ctx.fireExceptionCaught(future.cause()); } else { + localHandshakePromise.trySuccess(); // Kept for compatibility ctx.fireUserEventTriggered( WebSocketServerProtocolHandler.ServerHandshakeStateEvent.HANDSHAKE_COMPLETE); @@ -96,9 +106,7 @@ class WebSocketServerProtocolHandshakeHandler extends ChannelInboundHandlerAdapt } } }); - WebSocketServerProtocolHandler.setHandshaker(ctx.channel(), handshaker); - ctx.pipeline().replace(this, "WS403Responder", - WebSocketServerProtocolHandler.forbiddenHttpRequestResponder()); + applyHandshakeTimeout(); } } finally { req.release(); @@ -106,7 +114,8 @@ class WebSocketServerProtocolHandshakeHandler extends ChannelInboundHandlerAdapt } private boolean isNotWebSocketPath(FullHttpRequest req) { - return checkStartsWith ? !req.uri().startsWith(websocketPath) : !req.uri().equals(websocketPath); + String websocketPath = serverConfig.websocketPath(); + return serverConfig.checkStartsWith() ? !req.uri().startsWith(websocketPath) : !req.uri().equals(websocketPath); } private static void sendHttpResponse(ChannelHandlerContext ctx, HttpRequest req, HttpResponse res) { @@ -125,4 +134,32 @@ class WebSocketServerProtocolHandshakeHandler extends ChannelInboundHandlerAdapt String host = req.headers().get(HttpHeaderNames.HOST); return protocol + "://" + host + path; } + + private void applyHandshakeTimeout() { + final ChannelPromise localHandshakePromise = handshakePromise; + final long handshakeTimeoutMillis = serverConfig.handshakeTimeoutMillis(); + if (handshakeTimeoutMillis <= 0 || localHandshakePromise.isDone()) { + return; + } + + final Future<?> timeoutFuture = ctx.executor().schedule(new Runnable() { + @Override + public void run() { + if (!localHandshakePromise.isDone() && + localHandshakePromise.tryFailure(new WebSocketHandshakeException("handshake timed out"))) { + ctx.flush() + .fireUserEventTriggered(ServerHandshakeStateEvent.HANDSHAKE_TIMEOUT) + .close(); + } + } + }, handshakeTimeoutMillis, TimeUnit.MILLISECONDS); + + // Cancel the handshake timeout when handshake is finished. + localHandshakePromise.addListener(new FutureListener<Void>() { + @Override + public void operationComplete(Future<Void> f) { + timeoutFuture.cancel(false); + } + }); + } } diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketUtil.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketUtil.java index b6a138b..fbd43eb 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketUtil.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketUtil.java @@ -21,6 +21,7 @@ import io.netty.handler.codec.base64.Base64; import io.netty.util.CharsetUtil; import io.netty.util.concurrent.FastThreadLocal; import io.netty.util.internal.PlatformDependent; +import io.netty.util.internal.SuppressJava6Requirement; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; @@ -90,7 +91,11 @@ final class WebSocketUtil { * @param data The data to encode * @return An encoded string containing the data */ + @SuppressJava6Requirement(reason = "Guarded with java version check") static String base64(byte[] data) { + if (PlatformDependent.javaVersion() >= 8) { + return java.util.Base64.getEncoder().encodeToString(data); + } ByteBuf encodedData = Unpooled.wrappedBuffer(data); ByteBuf encoded = Base64.encode(encodedData); String encodedString = encoded.toString(CharsetUtil.UTF_8); diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketVersion.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketVersion.java index 2cb1c19..ed39608 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketVersion.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketVersion.java @@ -15,6 +15,9 @@ */ package io.netty.handler.codec.http.websocketx; +import io.netty.util.AsciiString; +import io.netty.util.internal.StringUtil; + /** * <p> * Versions of the web socket specification. @@ -25,49 +28,50 @@ package io.netty.handler.codec.http.websocketx; * </p> */ public enum WebSocketVersion { - UNKNOWN, + UNKNOWN(AsciiString.cached(StringUtil.EMPTY_STRING)), /** * <a href= "http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-00" * >draft-ietf-hybi-thewebsocketprotocol- 00</a>. */ - V00, + V00(AsciiString.cached("0")), /** * <a href= "http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-07" * >draft-ietf-hybi-thewebsocketprotocol- 07</a> */ - V07, + V07(AsciiString.cached("7")), /** * <a href= "http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-10" * >draft-ietf-hybi-thewebsocketprotocol- 10</a> */ - V08, + V08(AsciiString.cached("8")), /** * <a href="http://tools.ietf.org/html/rfc6455 ">RFC 6455</a>. This was originally <a href= * "http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-17" >draft-ietf-hybi-thewebsocketprotocol- * 17</a> */ - V13; + V13(AsciiString.cached("13")); + + private final AsciiString headerValue; + WebSocketVersion(AsciiString headerValue) { + this.headerValue = headerValue; + } /** * @return Value for HTTP Header 'Sec-WebSocket-Version' */ public String toHttpHeaderValue() { - if (this == V00) { - return "0"; - } - if (this == V07) { - return "7"; - } - if (this == V08) { - return "8"; - } - if (this == V13) { - return "13"; + return toAsciiString().toString(); + } + + AsciiString toAsciiString() { + if (this == UNKNOWN) { + // Let's special case this to preserve behaviour + throw new IllegalStateException("Unknown web socket version: " + this); } - throw new IllegalStateException("Unknown web socket version: " + this); + return headerValue; } } diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketClientExtensionHandler.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketClientExtensionHandler.java index 92ee9cc..b8b3912 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketClientExtensionHandler.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketClientExtensionHandler.java @@ -22,6 +22,7 @@ import io.netty.handler.codec.CodecException; import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponse; +import io.netty.util.internal.ObjectUtil; import java.util.ArrayList; import java.util.Arrays; @@ -50,9 +51,7 @@ public class WebSocketClientExtensionHandler extends ChannelDuplexHandler { * with fallback configuration. */ public WebSocketClientExtensionHandler(WebSocketClientExtensionHandshaker... extensionHandshakers) { - if (extensionHandshakers == null) { - throw new NullPointerException("extensionHandshakers"); - } + ObjectUtil.checkNotNull(extensionHandshakers, "extensionHandshakers"); if (extensionHandshakers.length == 0) { throw new IllegalArgumentException("extensionHandshakers must contains at least one handshaker"); } diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketExtensionData.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketExtensionData.java index eb3c586..1f61c68 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketExtensionData.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketExtensionData.java @@ -15,6 +15,8 @@ */ package io.netty.handler.codec.http.websocketx.extensions; +import io.netty.util.internal.ObjectUtil; + import java.util.Collections; import java.util.Map; @@ -29,14 +31,9 @@ public final class WebSocketExtensionData { private final Map<String, String> parameters; public WebSocketExtensionData(String name, Map<String, String> parameters) { - if (name == null) { - throw new NullPointerException("name"); - } - if (parameters == null) { - throw new NullPointerException("parameters"); - } - this.name = name; - this.parameters = Collections.unmodifiableMap(parameters); + this.name = ObjectUtil.checkNotNull(name, "name"); + this.parameters = Collections.unmodifiableMap( + ObjectUtil.checkNotNull(parameters, "parameters")); } /** diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketExtensionFilter.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketExtensionFilter.java new file mode 100644 index 0000000..6485e27 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketExtensionFilter.java @@ -0,0 +1,54 @@ +/* + * Copyright 2019 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.codec.http.websocketx.extensions; + +import io.netty.handler.codec.http.websocketx.WebSocketFrame; + +/** + * Filter that is responsible to skip the evaluation of a certain extension + * according to standard. + */ +public interface WebSocketExtensionFilter { + + /** + * A {@link WebSocketExtensionFilter} that never skip the evaluation of an + * any given extensions {@link WebSocketExtension}. + */ + WebSocketExtensionFilter NEVER_SKIP = new WebSocketExtensionFilter() { + @Override + public boolean mustSkip(WebSocketFrame frame) { + return false; + } + }; + + /** + * A {@link WebSocketExtensionFilter} that always skip the evaluation of an + * any given extensions {@link WebSocketExtension}. + */ + WebSocketExtensionFilter ALWAYS_SKIP = new WebSocketExtensionFilter() { + @Override + public boolean mustSkip(WebSocketFrame frame) { + return true; + } + }; + + /** + * Returns {@code true} if the evaluation of the extension must skipped + * for the given frame otherwise {@code false}. + */ + boolean mustSkip(WebSocketFrame frame); + +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketExtensionFilterProvider.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketExtensionFilterProvider.java new file mode 100644 index 0000000..a5eff2b --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketExtensionFilterProvider.java @@ -0,0 +1,45 @@ +/* + * Copyright 2019 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.codec.http.websocketx.extensions; + +/** + * Extension filter provider that is responsible to provide filters for a certain {@link WebSocketExtension} extension. + */ +public interface WebSocketExtensionFilterProvider { + + WebSocketExtensionFilterProvider DEFAULT = new WebSocketExtensionFilterProvider() { + @Override + public WebSocketExtensionFilter encoderFilter() { + return WebSocketExtensionFilter.NEVER_SKIP; + } + + @Override + public WebSocketExtensionFilter decoderFilter() { + return WebSocketExtensionFilter.NEVER_SKIP; + } + }; + + /** + * Returns the extension filter for {@link WebSocketExtensionEncoder} encoder. + */ + WebSocketExtensionFilter encoderFilter(); + + /** + * Returns the extension filter for {@link WebSocketExtensionDecoder} decoder. + */ + WebSocketExtensionFilter decoderFilter(); + +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketServerExtensionHandler.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketServerExtensionHandler.java index 1b2e391..ff0ee3e 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketServerExtensionHandler.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketServerExtensionHandler.java @@ -21,8 +21,10 @@ import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelPromise; import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponse; +import io.netty.util.internal.ObjectUtil; import java.util.ArrayList; import java.util.Arrays; @@ -53,9 +55,7 @@ public class WebSocketServerExtensionHandler extends ChannelDuplexHandler { * with fallback configuration. */ public WebSocketServerExtensionHandler(WebSocketServerExtensionHandshaker... extensionHandshakers) { - if (extensionHandshakers == null) { - throw new NullPointerException("extensionHandshakers"); - } + ObjectUtil.checkNotNull(extensionHandshakers, "extensionHandshakers"); if (extensionHandshakers.length == 0) { throw new IllegalArgumentException("extensionHandshakers must contains at least one handshaker"); } @@ -63,8 +63,7 @@ public class WebSocketServerExtensionHandler extends ChannelDuplexHandler { } @Override - public void channelRead(ChannelHandlerContext ctx, Object msg) - throws Exception { + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { if (msg instanceof HttpRequest) { HttpRequest request = (HttpRequest) msg; @@ -104,35 +103,48 @@ public class WebSocketServerExtensionHandler extends ChannelDuplexHandler { @Override public void write(final ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { - if (msg instanceof HttpResponse && - WebSocketExtensionUtil.isWebsocketUpgrade(((HttpResponse) msg).headers()) && validExtensions != null) { - HttpResponse response = (HttpResponse) msg; - String headerValue = response.headers().getAsString(HttpHeaderNames.SEC_WEBSOCKET_EXTENSIONS); - - for (WebSocketServerExtension extension : validExtensions) { - WebSocketExtensionData extensionData = extension.newReponseData(); - headerValue = WebSocketExtensionUtil.appendExtension(headerValue, - extensionData.name(), extensionData.parameters()); - } + if (msg instanceof HttpResponse) { + HttpHeaders headers = ((HttpResponse) msg).headers(); - promise.addListener(new ChannelFutureListener() { - @Override - public void operationComplete(ChannelFuture future) throws Exception { - if (future.isSuccess()) { - for (WebSocketServerExtension extension : validExtensions) { - WebSocketExtensionDecoder decoder = extension.newExtensionDecoder(); - WebSocketExtensionEncoder encoder = extension.newExtensionEncoder(); - ctx.pipeline().addAfter(ctx.name(), decoder.getClass().getName(), decoder); - ctx.pipeline().addAfter(ctx.name(), encoder.getClass().getName(), encoder); - } + if (WebSocketExtensionUtil.isWebsocketUpgrade(headers)) { + + if (validExtensions != null) { + String headerValue = headers.getAsString(HttpHeaderNames.SEC_WEBSOCKET_EXTENSIONS); + + for (WebSocketServerExtension extension : validExtensions) { + WebSocketExtensionData extensionData = extension.newReponseData(); + headerValue = WebSocketExtensionUtil.appendExtension(headerValue, + extensionData.name(), + extensionData.parameters()); } + promise.addListener(new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture future) { + if (future.isSuccess()) { + for (WebSocketServerExtension extension : validExtensions) { + WebSocketExtensionDecoder decoder = extension.newExtensionDecoder(); + WebSocketExtensionEncoder encoder = extension.newExtensionEncoder(); + ctx.pipeline() + .addAfter(ctx.name(), decoder.getClass().getName(), decoder) + .addAfter(ctx.name(), encoder.getClass().getName(), encoder); + } + } + } + }); - ctx.pipeline().remove(ctx.name()); + if (headerValue != null) { + headers.set(HttpHeaderNames.SEC_WEBSOCKET_EXTENSIONS, headerValue); + } } - }); - if (headerValue != null) { - response.headers().set(HttpHeaderNames.SEC_WEBSOCKET_EXTENSIONS, headerValue); + promise.addListener(new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture future) { + if (future.isSuccess()) { + ctx.pipeline().remove(WebSocketServerExtensionHandler.this); + } + } + }); } } diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/DeflateDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/DeflateDecoder.java index 89053fd..4ed3dc8 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/DeflateDecoder.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/DeflateDecoder.java @@ -28,27 +28,47 @@ import io.netty.handler.codec.http.websocketx.ContinuationWebSocketFrame; import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; import io.netty.handler.codec.http.websocketx.WebSocketFrame; import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionDecoder; +import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionFilter; import java.util.List; +import static io.netty.util.internal.ObjectUtil.*; + /** * Deflate implementation of a payload decompressor for * <tt>io.netty.handler.codec.http.websocketx.WebSocketFrame</tt>. */ abstract class DeflateDecoder extends WebSocketExtensionDecoder { - static final byte[] FRAME_TAIL = new byte[] {0x00, 0x00, (byte) 0xff, (byte) 0xff}; + static final ByteBuf FRAME_TAIL = Unpooled.unreleasableBuffer( + Unpooled.wrappedBuffer(new byte[] {0x00, 0x00, (byte) 0xff, (byte) 0xff})) + .asReadOnly(); + + static final ByteBuf EMPTY_DEFLATE_BLOCK = Unpooled.unreleasableBuffer( + Unpooled.wrappedBuffer(new byte[] { 0x00 })) + .asReadOnly(); private final boolean noContext; + private final WebSocketExtensionFilter extensionDecoderFilter; private EmbeddedChannel decoder; /** * Constructor + * * @param noContext true to disable context takeover. + * @param extensionDecoderFilter extension decoder filter. */ - public DeflateDecoder(boolean noContext) { + DeflateDecoder(boolean noContext, WebSocketExtensionFilter extensionDecoderFilter) { this.noContext = noContext; + this.extensionDecoderFilter = checkNotNull(extensionDecoderFilter, "extensionDecoderFilter"); + } + + /** + * Returns the extension decoder filter. + */ + protected WebSocketExtensionFilter extensionDecoderFilter() { + return extensionDecoderFilter; } protected abstract boolean appendFrameTail(WebSocketFrame msg); @@ -57,6 +77,35 @@ abstract class DeflateDecoder extends WebSocketExtensionDecoder { @Override protected void decode(ChannelHandlerContext ctx, WebSocketFrame msg, List<Object> out) throws Exception { + final ByteBuf decompressedContent = decompressContent(ctx, msg); + + final WebSocketFrame outMsg; + if (msg instanceof TextWebSocketFrame) { + outMsg = new TextWebSocketFrame(msg.isFinalFragment(), newRsv(msg), decompressedContent); + } else if (msg instanceof BinaryWebSocketFrame) { + outMsg = new BinaryWebSocketFrame(msg.isFinalFragment(), newRsv(msg), decompressedContent); + } else if (msg instanceof ContinuationWebSocketFrame) { + outMsg = new ContinuationWebSocketFrame(msg.isFinalFragment(), newRsv(msg), decompressedContent); + } else { + throw new CodecException("unexpected frame type: " + msg.getClass().getName()); + } + + out.add(outMsg); + } + + @Override + public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { + cleanup(); + super.handlerRemoved(ctx); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) throws Exception { + cleanup(); + super.channelInactive(ctx); + } + + private ByteBuf decompressContent(ChannelHandlerContext ctx, WebSocketFrame msg) { if (decoder == null) { if (!(msg instanceof TextWebSocketFrame) && !(msg instanceof BinaryWebSocketFrame)) { throw new CodecException("unexpected initial frame type: " + msg.getClass().getName()); @@ -65,12 +114,14 @@ abstract class DeflateDecoder extends WebSocketExtensionDecoder { } boolean readable = msg.content().isReadable(); + boolean emptyDeflateBlock = EMPTY_DEFLATE_BLOCK.equals(msg.content()); + decoder.writeInbound(msg.content().retain()); if (appendFrameTail(msg)) { - decoder.writeInbound(Unpooled.wrappedBuffer(FRAME_TAIL)); + decoder.writeInbound(FRAME_TAIL.duplicate()); } - CompositeByteBuf compositeUncompressedContent = ctx.alloc().compositeBuffer(); + CompositeByteBuf compositeDecompressedContent = ctx.alloc().compositeBuffer(); for (;;) { ByteBuf partUncompressedContent = decoder.readInbound(); if (partUncompressedContent == null) { @@ -80,58 +131,30 @@ abstract class DeflateDecoder extends WebSocketExtensionDecoder { partUncompressedContent.release(); continue; } - compositeUncompressedContent.addComponent(true, partUncompressedContent); + compositeDecompressedContent.addComponent(true, partUncompressedContent); } // Correctly handle empty frames // See https://github.com/netty/netty/issues/4348 - if (readable && compositeUncompressedContent.numComponents() <= 0) { - compositeUncompressedContent.release(); - throw new CodecException("cannot read uncompressed buffer"); + if (!emptyDeflateBlock && readable && compositeDecompressedContent.numComponents() <= 0) { + // Sometimes after fragmentation the last frame + // May contain left-over data that doesn't affect decompression + if (!(msg instanceof ContinuationWebSocketFrame)) { + compositeDecompressedContent.release(); + throw new CodecException("cannot read uncompressed buffer"); + } } if (msg.isFinalFragment() && noContext) { cleanup(); } - WebSocketFrame outMsg; - if (msg instanceof TextWebSocketFrame) { - outMsg = new TextWebSocketFrame(msg.isFinalFragment(), newRsv(msg), compositeUncompressedContent); - } else if (msg instanceof BinaryWebSocketFrame) { - outMsg = new BinaryWebSocketFrame(msg.isFinalFragment(), newRsv(msg), compositeUncompressedContent); - } else if (msg instanceof ContinuationWebSocketFrame) { - outMsg = new ContinuationWebSocketFrame(msg.isFinalFragment(), newRsv(msg), - compositeUncompressedContent); - } else { - throw new CodecException("unexpected frame type: " + msg.getClass().getName()); - } - out.add(outMsg); - } - - @Override - public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { - cleanup(); - super.handlerRemoved(ctx); - } - - @Override - public void channelInactive(ChannelHandlerContext ctx) throws Exception { - cleanup(); - super.channelInactive(ctx); + return compositeDecompressedContent; } private void cleanup() { if (decoder != null) { // Clean-up the previous encoder if not cleaned up correctly. - if (decoder.finish()) { - for (;;) { - ByteBuf buf = decoder.readOutbound(); - if (buf == null) { - break; - } - // Release the buffer - buf.release(); - } - } + decoder.finishAndReleaseAll(); decoder = null; } } diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/DeflateEncoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/DeflateEncoder.java index 1aff7b7..4f3daa8 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/DeflateEncoder.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/DeflateEncoder.java @@ -15,7 +15,6 @@ */ package io.netty.handler.codec.http.websocketx.extensions.compression; -import static io.netty.handler.codec.http.websocketx.extensions.compression.PerMessageDeflateDecoder.*; import io.netty.buffer.ByteBuf; import io.netty.buffer.CompositeByteBuf; import io.netty.channel.ChannelHandlerContext; @@ -28,9 +27,13 @@ import io.netty.handler.codec.http.websocketx.ContinuationWebSocketFrame; import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; import io.netty.handler.codec.http.websocketx.WebSocketFrame; import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionEncoder; +import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionFilter; import java.util.List; +import static io.netty.handler.codec.http.websocketx.extensions.compression.PerMessageDeflateDecoder.*; +import static io.netty.util.internal.ObjectUtil.*; + /** * Deflate implementation of a payload compressor for * <tt>io.netty.handler.codec.http.websocketx.WebSocketFrame</tt>. @@ -40,6 +43,7 @@ abstract class DeflateEncoder extends WebSocketExtensionEncoder { private final int compressionLevel; private final int windowSize; private final boolean noContext; + private final WebSocketExtensionFilter extensionEncoderFilter; private EmbeddedChannel encoder; @@ -48,11 +52,21 @@ abstract class DeflateEncoder extends WebSocketExtensionEncoder { * @param compressionLevel compression level of the compressor. * @param windowSize maximum size of the window compressor buffer. * @param noContext true to disable context takeover. + * @param extensionEncoderFilter extension encoder filter. */ - public DeflateEncoder(int compressionLevel, int windowSize, boolean noContext) { + DeflateEncoder(int compressionLevel, int windowSize, boolean noContext, + WebSocketExtensionFilter extensionEncoderFilter) { this.compressionLevel = compressionLevel; this.windowSize = windowSize; this.noContext = noContext; + this.extensionEncoderFilter = checkNotNull(extensionEncoderFilter, "extensionEncoderFilter"); + } + + /** + * Returns the extension encoder filter. + */ + protected WebSocketExtensionFilter extensionEncoderFilter() { + return extensionEncoderFilter; } /** @@ -68,8 +82,39 @@ abstract class DeflateEncoder extends WebSocketExtensionEncoder { protected abstract boolean removeFrameTail(WebSocketFrame msg); @Override - protected void encode(ChannelHandlerContext ctx, WebSocketFrame msg, - List<Object> out) throws Exception { + protected void encode(ChannelHandlerContext ctx, WebSocketFrame msg, List<Object> out) throws Exception { + final ByteBuf compressedContent; + if (msg.content().isReadable()) { + compressedContent = compressContent(ctx, msg); + } else if (msg.isFinalFragment()) { + // Set empty DEFLATE block manually for unknown buffer size + // https://tools.ietf.org/html/rfc7692#section-7.2.3.6 + compressedContent = EMPTY_DEFLATE_BLOCK.duplicate(); + } else { + throw new CodecException("cannot compress content buffer"); + } + + final WebSocketFrame outMsg; + if (msg instanceof TextWebSocketFrame) { + outMsg = new TextWebSocketFrame(msg.isFinalFragment(), rsv(msg), compressedContent); + } else if (msg instanceof BinaryWebSocketFrame) { + outMsg = new BinaryWebSocketFrame(msg.isFinalFragment(), rsv(msg), compressedContent); + } else if (msg instanceof ContinuationWebSocketFrame) { + outMsg = new ContinuationWebSocketFrame(msg.isFinalFragment(), rsv(msg), compressedContent); + } else { + throw new CodecException("unexpected frame type: " + msg.getClass().getName()); + } + + out.add(outMsg); + } + + @Override + public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { + cleanup(); + super.handlerRemoved(ctx); + } + + private ByteBuf compressContent(ChannelHandlerContext ctx, WebSocketFrame msg) { if (encoder == null) { encoder = new EmbeddedChannel(ZlibCodecFactory.newZlibEncoder( ZlibWrapper.NONE, compressionLevel, windowSize, 8)); @@ -89,6 +134,7 @@ abstract class DeflateEncoder extends WebSocketExtensionEncoder { } fullCompressedContent.addComponent(true, partCompressedContent); } + if (fullCompressedContent.numComponents() <= 0) { fullCompressedContent.release(); throw new CodecException("cannot read compressed buffer"); @@ -100,44 +146,19 @@ abstract class DeflateEncoder extends WebSocketExtensionEncoder { ByteBuf compressedContent; if (removeFrameTail(msg)) { - int realLength = fullCompressedContent.readableBytes() - FRAME_TAIL.length; + int realLength = fullCompressedContent.readableBytes() - FRAME_TAIL.readableBytes(); compressedContent = fullCompressedContent.slice(0, realLength); } else { compressedContent = fullCompressedContent; } - WebSocketFrame outMsg; - if (msg instanceof TextWebSocketFrame) { - outMsg = new TextWebSocketFrame(msg.isFinalFragment(), rsv(msg), compressedContent); - } else if (msg instanceof BinaryWebSocketFrame) { - outMsg = new BinaryWebSocketFrame(msg.isFinalFragment(), rsv(msg), compressedContent); - } else if (msg instanceof ContinuationWebSocketFrame) { - outMsg = new ContinuationWebSocketFrame(msg.isFinalFragment(), rsv(msg), compressedContent); - } else { - throw new CodecException("unexpected frame type: " + msg.getClass().getName()); - } - out.add(outMsg); - } - - @Override - public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { - cleanup(); - super.handlerRemoved(ctx); + return compressedContent; } private void cleanup() { if (encoder != null) { // Clean-up the previous encoder if not cleaned up correctly. - if (encoder.finish()) { - for (;;) { - ByteBuf buf = encoder.readOutbound(); - if (buf == null) { - break; - } - // Release the buffer - buf.release(); - } - } + encoder.finishAndReleaseAll(); encoder = null; } } diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/DeflateFrameClientExtensionHandshaker.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/DeflateFrameClientExtensionHandshaker.java index 6671d1f..c5f8b60 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/DeflateFrameClientExtensionHandshaker.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/DeflateFrameClientExtensionHandshaker.java @@ -15,16 +15,18 @@ */ package io.netty.handler.codec.http.websocketx.extensions.compression; -import static io.netty.handler.codec.http.websocketx.extensions.compression. - DeflateFrameServerExtensionHandshaker.*; import io.netty.handler.codec.http.websocketx.extensions.WebSocketClientExtension; import io.netty.handler.codec.http.websocketx.extensions.WebSocketClientExtensionHandshaker; import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionData; import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionDecoder; import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionEncoder; +import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionFilterProvider; import java.util.Collections; +import static io.netty.handler.codec.http.websocketx.extensions.compression.DeflateFrameServerExtensionHandshaker.*; +import static io.netty.util.internal.ObjectUtil.*; + /** * <a href="https://tools.ietf.org/id/draft-tyoshino-hybi-websocket-perframe-deflate-06.txt">perframe-deflate</a> * handshake implementation. @@ -33,6 +35,7 @@ public final class DeflateFrameClientExtensionHandshaker implements WebSocketCli private final int compressionLevel; private final boolean useWebkitExtensionName; + private final WebSocketExtensionFilterProvider extensionFilterProvider; /** * Constructor with default configuration. @@ -48,12 +51,26 @@ public final class DeflateFrameClientExtensionHandshaker implements WebSocketCli * Compression level between 0 and 9 (default is 6). */ public DeflateFrameClientExtensionHandshaker(int compressionLevel, boolean useWebkitExtensionName) { + this(compressionLevel, useWebkitExtensionName, WebSocketExtensionFilterProvider.DEFAULT); + } + + /** + * Constructor with custom configuration. + * + * @param compressionLevel + * Compression level between 0 and 9 (default is 6). + * @param extensionFilterProvider + * provides client extension filters for per frame deflate encoder and decoder. + */ + public DeflateFrameClientExtensionHandshaker(int compressionLevel, boolean useWebkitExtensionName, + WebSocketExtensionFilterProvider extensionFilterProvider) { if (compressionLevel < 0 || compressionLevel > 9) { throw new IllegalArgumentException( "compressionLevel: " + compressionLevel + " (expected: 0-9)"); } this.compressionLevel = compressionLevel; this.useWebkitExtensionName = useWebkitExtensionName; + this.extensionFilterProvider = checkNotNull(extensionFilterProvider, "extensionFilterProvider"); } @Override @@ -71,7 +88,7 @@ public final class DeflateFrameClientExtensionHandshaker implements WebSocketCli } if (extensionData.parameters().isEmpty()) { - return new DeflateFrameClientExtension(compressionLevel); + return new DeflateFrameClientExtension(compressionLevel, extensionFilterProvider); } else { return null; } @@ -80,9 +97,11 @@ public final class DeflateFrameClientExtensionHandshaker implements WebSocketCli private static class DeflateFrameClientExtension implements WebSocketClientExtension { private final int compressionLevel; + private final WebSocketExtensionFilterProvider extensionFilterProvider; - public DeflateFrameClientExtension(int compressionLevel) { + DeflateFrameClientExtension(int compressionLevel, WebSocketExtensionFilterProvider extensionFilterProvider) { this.compressionLevel = compressionLevel; + this.extensionFilterProvider = extensionFilterProvider; } @Override @@ -92,12 +111,13 @@ public final class DeflateFrameClientExtensionHandshaker implements WebSocketCli @Override public WebSocketExtensionEncoder newExtensionEncoder() { - return new PerFrameDeflateEncoder(compressionLevel, 15, false); + return new PerFrameDeflateEncoder(compressionLevel, 15, false, + extensionFilterProvider.encoderFilter()); } @Override public WebSocketExtensionDecoder newExtensionDecoder() { - return new PerFrameDeflateDecoder(false); + return new PerFrameDeflateDecoder(false, extensionFilterProvider.decoderFilter()); } } diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/DeflateFrameServerExtensionHandshaker.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/DeflateFrameServerExtensionHandshaker.java index e7ea9f3..7a06a22 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/DeflateFrameServerExtensionHandshaker.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/DeflateFrameServerExtensionHandshaker.java @@ -18,11 +18,14 @@ package io.netty.handler.codec.http.websocketx.extensions.compression; import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionData; import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionDecoder; import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionEncoder; +import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionFilterProvider; import io.netty.handler.codec.http.websocketx.extensions.WebSocketServerExtension; import io.netty.handler.codec.http.websocketx.extensions.WebSocketServerExtensionHandshaker; import java.util.Collections; +import static io.netty.util.internal.ObjectUtil.*; + /** * <a href="https://tools.ietf.org/id/draft-tyoshino-hybi-websocket-perframe-deflate-06.txt">perframe-deflate</a> * handshake implementation. @@ -33,6 +36,7 @@ public final class DeflateFrameServerExtensionHandshaker implements WebSocketSer static final String DEFLATE_FRAME_EXTENSION = "deflate-frame"; private final int compressionLevel; + private final WebSocketExtensionFilterProvider extensionFilterProvider; /** * Constructor with default configuration. @@ -48,11 +52,25 @@ public final class DeflateFrameServerExtensionHandshaker implements WebSocketSer * Compression level between 0 and 9 (default is 6). */ public DeflateFrameServerExtensionHandshaker(int compressionLevel) { + this(compressionLevel, WebSocketExtensionFilterProvider.DEFAULT); + } + + /** + * Constructor with custom configuration. + * + * @param compressionLevel + * Compression level between 0 and 9 (default is 6). + * @param extensionFilterProvider + * provides server extension filters for per frame deflate encoder and decoder. + */ + public DeflateFrameServerExtensionHandshaker(int compressionLevel, + WebSocketExtensionFilterProvider extensionFilterProvider) { if (compressionLevel < 0 || compressionLevel > 9) { throw new IllegalArgumentException( "compressionLevel: " + compressionLevel + " (expected: 0-9)"); } this.compressionLevel = compressionLevel; + this.extensionFilterProvider = checkNotNull(extensionFilterProvider, "extensionFilterProvider"); } @Override @@ -63,7 +81,7 @@ public final class DeflateFrameServerExtensionHandshaker implements WebSocketSer } if (extensionData.parameters().isEmpty()) { - return new DeflateFrameServerExtension(compressionLevel, extensionData.name()); + return new DeflateFrameServerExtension(compressionLevel, extensionData.name(), extensionFilterProvider); } else { return null; } @@ -73,10 +91,13 @@ public final class DeflateFrameServerExtensionHandshaker implements WebSocketSer private final String extensionName; private final int compressionLevel; + private final WebSocketExtensionFilterProvider extensionFilterProvider; - public DeflateFrameServerExtension(int compressionLevel, String extensionName) { + DeflateFrameServerExtension(int compressionLevel, String extensionName, + WebSocketExtensionFilterProvider extensionFilterProvider) { this.extensionName = extensionName; this.compressionLevel = compressionLevel; + this.extensionFilterProvider = extensionFilterProvider; } @Override @@ -86,12 +107,13 @@ public final class DeflateFrameServerExtensionHandshaker implements WebSocketSer @Override public WebSocketExtensionEncoder newExtensionEncoder() { - return new PerFrameDeflateEncoder(compressionLevel, 15, false); + return new PerFrameDeflateEncoder(compressionLevel, 15, false, + extensionFilterProvider.encoderFilter()); } @Override public WebSocketExtensionDecoder newExtensionDecoder() { - return new PerFrameDeflateDecoder(false); + return new PerFrameDeflateDecoder(false, extensionFilterProvider.decoderFilter()); } @Override diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerFrameDeflateDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerFrameDeflateDecoder.java index ad95544..dc15352 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerFrameDeflateDecoder.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerFrameDeflateDecoder.java @@ -20,6 +20,7 @@ import io.netty.handler.codec.http.websocketx.ContinuationWebSocketFrame; import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; import io.netty.handler.codec.http.websocketx.WebSocketFrame; import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtension; +import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionFilter; /** * Per-frame implementation of deflate decompressor. @@ -28,18 +29,37 @@ class PerFrameDeflateDecoder extends DeflateDecoder { /** * Constructor + * * @param noContext true to disable context takeover. */ - public PerFrameDeflateDecoder(boolean noContext) { - super(noContext); + PerFrameDeflateDecoder(boolean noContext) { + super(noContext, WebSocketExtensionFilter.NEVER_SKIP); + } + + /** + * Constructor + * + * @param noContext true to disable context takeover. + * @param extensionDecoderFilter extension decoder filter for per frame deflate decoder. + */ + PerFrameDeflateDecoder(boolean noContext, WebSocketExtensionFilter extensionDecoderFilter) { + super(noContext, extensionDecoderFilter); } @Override public boolean acceptInboundMessage(Object msg) throws Exception { - return (msg instanceof TextWebSocketFrame || - msg instanceof BinaryWebSocketFrame || + if (!super.acceptInboundMessage(msg)) { + return false; + } + + WebSocketFrame wsFrame = (WebSocketFrame) msg; + if (extensionDecoderFilter().mustSkip(wsFrame)) { + return false; + } + + return (msg instanceof TextWebSocketFrame || msg instanceof BinaryWebSocketFrame || msg instanceof ContinuationWebSocketFrame) && - (((WebSocketFrame) msg).rsv() & WebSocketExtension.RSV1) > 0; + (wsFrame.rsv() & WebSocketExtension.RSV1) > 0; } @Override @@ -51,4 +71,5 @@ class PerFrameDeflateDecoder extends DeflateDecoder { protected boolean appendFrameTail(WebSocketFrame msg) { return true; } + } diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerFrameDeflateEncoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerFrameDeflateEncoder.java index aaffd8d..5da86b5 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerFrameDeflateEncoder.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerFrameDeflateEncoder.java @@ -20,6 +20,7 @@ import io.netty.handler.codec.http.websocketx.ContinuationWebSocketFrame; import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; import io.netty.handler.codec.http.websocketx.WebSocketFrame; import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtension; +import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionFilter; /** * Per-frame implementation of deflate compressor. @@ -28,21 +29,43 @@ class PerFrameDeflateEncoder extends DeflateEncoder { /** * Constructor + * * @param compressionLevel compression level of the compressor. - * @param windowSize maximum size of the window compressor buffer. - * @param noContext true to disable context takeover. + * @param windowSize maximum size of the window compressor buffer. + * @param noContext true to disable context takeover. */ - public PerFrameDeflateEncoder(int compressionLevel, int windowSize, boolean noContext) { - super(compressionLevel, windowSize, noContext); + PerFrameDeflateEncoder(int compressionLevel, int windowSize, boolean noContext) { + super(compressionLevel, windowSize, noContext, WebSocketExtensionFilter.NEVER_SKIP); + } + + /** + * Constructor + * + * @param compressionLevel compression level of the compressor. + * @param windowSize maximum size of the window compressor buffer. + * @param noContext true to disable context takeover. + * @param extensionEncoderFilter extension encoder filter for per frame deflate encoder. + */ + PerFrameDeflateEncoder(int compressionLevel, int windowSize, boolean noContext, + WebSocketExtensionFilter extensionEncoderFilter) { + super(compressionLevel, windowSize, noContext, extensionEncoderFilter); } @Override public boolean acceptOutboundMessage(Object msg) throws Exception { - return (msg instanceof TextWebSocketFrame || - msg instanceof BinaryWebSocketFrame || + if (!super.acceptOutboundMessage(msg)) { + return false; + } + + WebSocketFrame wsFrame = (WebSocketFrame) msg; + if (extensionEncoderFilter().mustSkip(wsFrame)) { + return false; + } + + return (msg instanceof TextWebSocketFrame || msg instanceof BinaryWebSocketFrame || msg instanceof ContinuationWebSocketFrame) && - ((WebSocketFrame) msg).content().readableBytes() > 0 && - (((WebSocketFrame) msg).rsv() & WebSocketExtension.RSV1) == 0; + wsFrame.content().readableBytes() > 0 && + (wsFrame.rsv() & WebSocketExtension.RSV1) == 0; } @Override diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateClientExtensionHandshaker.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateClientExtensionHandshaker.java index ddebaaa..df6f5f6 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateClientExtensionHandshaker.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateClientExtensionHandshaker.java @@ -15,20 +15,21 @@ */ package io.netty.handler.codec.http.websocketx.extensions.compression; -import static io.netty.handler.codec.http.websocketx.extensions.compression. - PerMessageDeflateServerExtensionHandshaker.*; - import io.netty.handler.codec.compression.ZlibCodecFactory; import io.netty.handler.codec.http.websocketx.extensions.WebSocketClientExtension; import io.netty.handler.codec.http.websocketx.extensions.WebSocketClientExtensionHandshaker; import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionData; import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionDecoder; import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionEncoder; +import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionFilterProvider; import java.util.HashMap; import java.util.Iterator; import java.util.Map.Entry; +import static io.netty.handler.codec.http.websocketx.extensions.compression.PerMessageDeflateServerExtensionHandshaker.*; +import static io.netty.util.internal.ObjectUtil.*; + /** * <a href="http://tools.ietf.org/html/draft-ietf-hybi-permessage-compression-18">permessage-deflate</a> * handshake implementation. @@ -40,6 +41,7 @@ public final class PerMessageDeflateClientExtensionHandshaker implements WebSock private final int requestedServerWindowSize; private final boolean allowClientNoContext; private final boolean requestedServerNoContext; + private final WebSocketExtensionFilterProvider extensionFilterProvider; /** * Constructor with default configuration. @@ -68,6 +70,34 @@ public final class PerMessageDeflateClientExtensionHandshaker implements WebSock public PerMessageDeflateClientExtensionHandshaker(int compressionLevel, boolean allowClientWindowSize, int requestedServerWindowSize, boolean allowClientNoContext, boolean requestedServerNoContext) { + this(compressionLevel, allowClientWindowSize, requestedServerWindowSize, + allowClientNoContext, requestedServerNoContext, WebSocketExtensionFilterProvider.DEFAULT); + } + + /** + * Constructor with custom configuration. + * + * @param compressionLevel + * Compression level between 0 and 9 (default is 6). + * @param allowClientWindowSize + * allows WebSocket server to customize the client inflater window size + * (default is false). + * @param requestedServerWindowSize + * indicates the requested sever window size to use if server inflater is customizable. + * @param allowClientNoContext + * allows WebSocket server to activate client_no_context_takeover + * (default is false). + * @param requestedServerNoContext + * indicates if client needs to activate server_no_context_takeover + * if server is compatible with (default is false). + * @param extensionFilterProvider + * provides client extension filters for per message deflate encoder and decoder. + */ + public PerMessageDeflateClientExtensionHandshaker(int compressionLevel, + boolean allowClientWindowSize, int requestedServerWindowSize, + boolean allowClientNoContext, boolean requestedServerNoContext, + WebSocketExtensionFilterProvider extensionFilterProvider) { + if (requestedServerWindowSize > MAX_WINDOW_SIZE || requestedServerWindowSize < MIN_WINDOW_SIZE) { throw new IllegalArgumentException( "requestedServerWindowSize: " + requestedServerWindowSize + " (expected: 8-15)"); @@ -81,6 +111,7 @@ public final class PerMessageDeflateClientExtensionHandshaker implements WebSock this.requestedServerWindowSize = requestedServerWindowSize; this.allowClientNoContext = allowClientNoContext; this.requestedServerNoContext = requestedServerNoContext; + this.extensionFilterProvider = checkNotNull(extensionFilterProvider, "extensionFilterProvider"); } @Override @@ -158,7 +189,7 @@ public final class PerMessageDeflateClientExtensionHandshaker implements WebSock if (succeed) { return new PermessageDeflateExtension(serverNoContext, serverWindowSize, - clientNoContext, clientWindowSize); + clientNoContext, clientWindowSize, extensionFilterProvider); } else { return null; } @@ -170,28 +201,32 @@ public final class PerMessageDeflateClientExtensionHandshaker implements WebSock private final int serverWindowSize; private final boolean clientNoContext; private final int clientWindowSize; + private final WebSocketExtensionFilterProvider extensionFilterProvider; @Override public int rsv() { return RSV1; } - public PermessageDeflateExtension(boolean serverNoContext, int serverWindowSize, - boolean clientNoContext, int clientWindowSize) { + PermessageDeflateExtension(boolean serverNoContext, int serverWindowSize, + boolean clientNoContext, int clientWindowSize, + WebSocketExtensionFilterProvider extensionFilterProvider) { this.serverNoContext = serverNoContext; this.serverWindowSize = serverWindowSize; this.clientNoContext = clientNoContext; this.clientWindowSize = clientWindowSize; + this.extensionFilterProvider = extensionFilterProvider; } @Override public WebSocketExtensionEncoder newExtensionEncoder() { - return new PerMessageDeflateEncoder(compressionLevel, clientWindowSize, clientNoContext); + return new PerMessageDeflateEncoder(compressionLevel, clientWindowSize, clientNoContext, + extensionFilterProvider.encoderFilter()); } @Override public WebSocketExtensionDecoder newExtensionDecoder() { - return new PerMessageDeflateDecoder(serverNoContext); + return new PerMessageDeflateDecoder(serverNoContext, extensionFilterProvider.decoderFilter()); } } diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateDecoder.java index a69294e..d2512bb 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateDecoder.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateDecoder.java @@ -21,6 +21,7 @@ import io.netty.handler.codec.http.websocketx.ContinuationWebSocketFrame; import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; import io.netty.handler.codec.http.websocketx.WebSocketFrame; import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtension; +import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionFilter; import java.util.List; @@ -33,23 +34,45 @@ class PerMessageDeflateDecoder extends DeflateDecoder { /** * Constructor + * * @param noContext true to disable context takeover. */ - public PerMessageDeflateDecoder(boolean noContext) { - super(noContext); + PerMessageDeflateDecoder(boolean noContext) { + super(noContext, WebSocketExtensionFilter.NEVER_SKIP); + } + + /** + * Constructor + * + * @param noContext true to disable context takeover. + * @param extensionDecoderFilter extension decoder for per message deflate decoder. + */ + PerMessageDeflateDecoder(boolean noContext, WebSocketExtensionFilter extensionDecoderFilter) { + super(noContext, extensionDecoderFilter); } @Override public boolean acceptInboundMessage(Object msg) throws Exception { - return ((msg instanceof TextWebSocketFrame || - msg instanceof BinaryWebSocketFrame) && - (((WebSocketFrame) msg).rsv() & WebSocketExtension.RSV1) > 0) || - (msg instanceof ContinuationWebSocketFrame && compressing); + if (!super.acceptInboundMessage(msg)) { + return false; + } + + WebSocketFrame wsFrame = (WebSocketFrame) msg; + if (extensionDecoderFilter().mustSkip(wsFrame)) { + if (compressing) { + throw new IllegalStateException("Cannot skip per message deflate decoder, compression in progress"); + } + return false; + } + + return ((wsFrame instanceof TextWebSocketFrame || wsFrame instanceof BinaryWebSocketFrame) && + (wsFrame.rsv() & WebSocketExtension.RSV1) > 0) || + (wsFrame instanceof ContinuationWebSocketFrame && compressing); } @Override protected int newRsv(WebSocketFrame msg) { - return (msg.rsv() & WebSocketExtension.RSV1) > 0 ? + return (msg.rsv() & WebSocketExtension.RSV1) > 0? msg.rsv() ^ WebSocketExtension.RSV1 : msg.rsv(); } @@ -60,7 +83,7 @@ class PerMessageDeflateDecoder extends DeflateDecoder { @Override protected void decode(ChannelHandlerContext ctx, WebSocketFrame msg, - List<Object> out) throws Exception { + List<Object> out) throws Exception { super.decode(ctx, msg, out); if (msg.isFinalFragment()) { @@ -69,4 +92,5 @@ class PerMessageDeflateDecoder extends DeflateDecoder { compressing = true; } } + } diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateEncoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateEncoder.java index b1cdc66..12a7c46 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateEncoder.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateEncoder.java @@ -21,6 +21,7 @@ import io.netty.handler.codec.http.websocketx.ContinuationWebSocketFrame; import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; import io.netty.handler.codec.http.websocketx.WebSocketFrame; import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtension; +import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionFilter; import java.util.List; @@ -33,25 +34,50 @@ class PerMessageDeflateEncoder extends DeflateEncoder { /** * Constructor + * * @param compressionLevel compression level of the compressor. * @param windowSize maximum size of the window compressor buffer. * @param noContext true to disable context takeover. */ - public PerMessageDeflateEncoder(int compressionLevel, int windowSize, boolean noContext) { - super(compressionLevel, windowSize, noContext); + PerMessageDeflateEncoder(int compressionLevel, int windowSize, boolean noContext) { + super(compressionLevel, windowSize, noContext, WebSocketExtensionFilter.NEVER_SKIP); + } + + /** + * Constructor + * + * @param compressionLevel compression level of the compressor. + * @param windowSize maximum size of the window compressor buffer. + * @param noContext true to disable context takeover. + * @param extensionEncoderFilter extension filter for per message deflate encoder. + */ + PerMessageDeflateEncoder(int compressionLevel, int windowSize, boolean noContext, + WebSocketExtensionFilter extensionEncoderFilter) { + super(compressionLevel, windowSize, noContext, extensionEncoderFilter); } @Override public boolean acceptOutboundMessage(Object msg) throws Exception { - return ((msg instanceof TextWebSocketFrame || - msg instanceof BinaryWebSocketFrame) && - (((WebSocketFrame) msg).rsv() & WebSocketExtension.RSV1) == 0) || - (msg instanceof ContinuationWebSocketFrame && compressing); + if (!super.acceptOutboundMessage(msg)) { + return false; + } + + WebSocketFrame wsFrame = (WebSocketFrame) msg; + if (extensionEncoderFilter().mustSkip(wsFrame)) { + if (compressing) { + throw new IllegalStateException("Cannot skip per message deflate encoder, compression in progress"); + } + return false; + } + + return ((wsFrame instanceof TextWebSocketFrame || wsFrame instanceof BinaryWebSocketFrame) && + (wsFrame.rsv() & WebSocketExtension.RSV1) == 0) || + (wsFrame instanceof ContinuationWebSocketFrame && compressing); } @Override protected int rsv(WebSocketFrame msg) { - return msg instanceof TextWebSocketFrame || msg instanceof BinaryWebSocketFrame ? + return msg instanceof TextWebSocketFrame || msg instanceof BinaryWebSocketFrame? msg.rsv() | WebSocketExtension.RSV1 : msg.rsv(); } @@ -62,7 +88,7 @@ class PerMessageDeflateEncoder extends DeflateEncoder { @Override protected void encode(ChannelHandlerContext ctx, WebSocketFrame msg, - List<Object> out) throws Exception { + List<Object> out) throws Exception { super.encode(ctx, msg, out); if (msg.isFinalFragment()) { diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateServerExtensionHandshaker.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateServerExtensionHandshaker.java index 0bf0162..3eafeb9 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateServerExtensionHandshaker.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateServerExtensionHandshaker.java @@ -19,6 +19,7 @@ import io.netty.handler.codec.compression.ZlibCodecFactory; import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionData; import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionDecoder; import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionEncoder; +import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionFilterProvider; import io.netty.handler.codec.http.websocketx.extensions.WebSocketServerExtension; import io.netty.handler.codec.http.websocketx.extensions.WebSocketServerExtensionHandshaker; @@ -26,6 +27,8 @@ import java.util.HashMap; import java.util.Iterator; import java.util.Map.Entry; +import static io.netty.util.internal.ObjectUtil.*; + /** * <a href="http://tools.ietf.org/html/draft-ietf-hybi-permessage-compression-18">permessage-deflate</a> * handshake implementation. @@ -46,6 +49,7 @@ public final class PerMessageDeflateServerExtensionHandshaker implements WebSock private final int preferredClientWindowSize; private final boolean allowServerNoContext; private final boolean preferredClientNoContext; + private final WebSocketExtensionFilterProvider extensionFilterProvider; /** * Constructor with default configuration. @@ -71,9 +75,36 @@ public final class PerMessageDeflateServerExtensionHandshaker implements WebSock * indicates if server prefers to activate client_no_context_takeover * if client is compatible with (default is false). */ - public PerMessageDeflateServerExtensionHandshaker(int compressionLevel, - boolean allowServerWindowSize, int preferredClientWindowSize, + public PerMessageDeflateServerExtensionHandshaker(int compressionLevel, boolean allowServerWindowSize, + int preferredClientWindowSize, boolean allowServerNoContext, boolean preferredClientNoContext) { + this(compressionLevel, allowServerWindowSize, preferredClientWindowSize, allowServerNoContext, + preferredClientNoContext, WebSocketExtensionFilterProvider.DEFAULT); + } + + /** + * Constructor with custom configuration. + * + * @param compressionLevel + * Compression level between 0 and 9 (default is 6). + * @param allowServerWindowSize + * allows WebSocket client to customize the server inflater window size + * (default is false). + * @param preferredClientWindowSize + * indicates the preferred client window size to use if client inflater is customizable. + * @param allowServerNoContext + * allows WebSocket client to activate server_no_context_takeover + * (default is false). + * @param preferredClientNoContext + * indicates if server prefers to activate client_no_context_takeover + * if client is compatible with (default is false). + * @param extensionFilterProvider + * provides server extension filters for per message deflate encoder and decoder. + */ + public PerMessageDeflateServerExtensionHandshaker(int compressionLevel, boolean allowServerWindowSize, + int preferredClientWindowSize, + boolean allowServerNoContext, boolean preferredClientNoContext, + WebSocketExtensionFilterProvider extensionFilterProvider) { if (preferredClientWindowSize > MAX_WINDOW_SIZE || preferredClientWindowSize < MIN_WINDOW_SIZE) { throw new IllegalArgumentException( "preferredServerWindowSize: " + preferredClientWindowSize + " (expected: 8-15)"); @@ -87,6 +118,7 @@ public final class PerMessageDeflateServerExtensionHandshaker implements WebSock this.preferredClientWindowSize = preferredClientWindowSize; this.allowServerNoContext = allowServerNoContext; this.preferredClientNoContext = preferredClientNoContext; + this.extensionFilterProvider = checkNotNull(extensionFilterProvider, "extensionFilterProvider"); } @Override @@ -137,7 +169,7 @@ public final class PerMessageDeflateServerExtensionHandshaker implements WebSock if (deflateEnabled) { return new PermessageDeflateExtension(compressionLevel, serverNoContext, - serverWindowSize, clientNoContext, clientWindowSize); + serverWindowSize, clientNoContext, clientWindowSize, extensionFilterProvider); } else { return null; } @@ -150,14 +182,17 @@ public final class PerMessageDeflateServerExtensionHandshaker implements WebSock private final int serverWindowSize; private final boolean clientNoContext; private final int clientWindowSize; + private final WebSocketExtensionFilterProvider extensionFilterProvider; - public PermessageDeflateExtension(int compressionLevel, boolean serverNoContext, - int serverWindowSize, boolean clientNoContext, int clientWindowSize) { + PermessageDeflateExtension(int compressionLevel, boolean serverNoContext, + int serverWindowSize, boolean clientNoContext, int clientWindowSize, + WebSocketExtensionFilterProvider extensionFilterProvider) { this.compressionLevel = compressionLevel; this.serverNoContext = serverNoContext; this.serverWindowSize = serverWindowSize; this.clientNoContext = clientNoContext; this.clientWindowSize = clientWindowSize; + this.extensionFilterProvider = extensionFilterProvider; } @Override @@ -167,12 +202,13 @@ public final class PerMessageDeflateServerExtensionHandshaker implements WebSock @Override public WebSocketExtensionEncoder newExtensionEncoder() { - return new PerMessageDeflateEncoder(compressionLevel, serverWindowSize, serverNoContext); + return new PerMessageDeflateEncoder(compressionLevel, serverWindowSize, serverNoContext, + extensionFilterProvider.encoderFilter()); } @Override public WebSocketExtensionDecoder newExtensionDecoder() { - return new PerMessageDeflateDecoder(clientNoContext); + return new PerMessageDeflateDecoder(clientNoContext, extensionFilterProvider.decoderFilter()); } @Override diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/package-info.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/package-info.java index 1425a82..824acb3 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/package-info.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/package-info.java @@ -21,11 +21,11 @@ * This package supports different web socket specification versions (hence the X suffix). * The specification current supported are: * <ul> - * <li><a href="http://netty.io/s/ws-00">draft-ietf-hybi-thewebsocketprotocol-00</a></li> - * <li><a href="http://netty.io/s/ws-07">draft-ietf-hybi-thewebsocketprotocol-07</a></li> - * <li><a href="http://netty.io/s/ws-10">draft-ietf-hybi-thewebsocketprotocol-10</a></li> - * <li><a href="http://netty.io/s/rfc6455">RFC 6455</a> - * (originally <a href="http://netty.io/s/ws-17">draft-ietf-hybi-thewebsocketprotocol-17</a>)</li> + * <li><a href="https://netty.io/s/ws-00">draft-ietf-hybi-thewebsocketprotocol-00</a></li> + * <li><a href="https://netty.io/s/ws-07">draft-ietf-hybi-thewebsocketprotocol-07</a></li> + * <li><a href="https://netty.io/s/ws-10">draft-ietf-hybi-thewebsocketprotocol-10</a></li> + * <li><a href="https://netty.io/s/rfc6455">RFC 6455</a> + * (originally <a href="https://netty.io/s/ws-17">draft-ietf-hybi-thewebsocketprotocol-17</a>)</li> * </ul> * </p> diff --git a/codec-http/src/main/java/io/netty/handler/codec/rtsp/RtspMethods.java b/codec-http/src/main/java/io/netty/handler/codec/rtsp/RtspMethods.java index 3e62928..8d98061 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/rtsp/RtspMethods.java +++ b/codec-http/src/main/java/io/netty/handler/codec/rtsp/RtspMethods.java @@ -16,6 +16,7 @@ package io.netty.handler.codec.rtsp; import io.netty.handler.codec.http.HttpMethod; +import io.netty.util.internal.ObjectUtil; import java.util.HashMap; import java.util.Map; @@ -38,62 +39,62 @@ public final class RtspMethods { * The DESCRIBE getMethod retrieves the description of a presentation or * media object identified by the request URL from a server. */ - public static final HttpMethod DESCRIBE = new HttpMethod("DESCRIBE"); + public static final HttpMethod DESCRIBE = HttpMethod.valueOf("DESCRIBE"); /** * The ANNOUNCE posts the description of a presentation or media object * identified by the request URL to a server, or updates the client-side * session description in real-time. */ - public static final HttpMethod ANNOUNCE = new HttpMethod("ANNOUNCE"); + public static final HttpMethod ANNOUNCE = HttpMethod.valueOf("ANNOUNCE"); /** * The SETUP request for a URI specifies the transport mechanism to be * used for the streamed media. */ - public static final HttpMethod SETUP = new HttpMethod("SETUP"); + public static final HttpMethod SETUP = HttpMethod.valueOf("SETUP"); /** * The PLAY getMethod tells the server to start sending data via the * mechanism specified in SETUP. */ - public static final HttpMethod PLAY = new HttpMethod("PLAY"); + public static final HttpMethod PLAY = HttpMethod.valueOf("PLAY"); /** * The PAUSE request causes the stream delivery to be interrupted * (halted) temporarily. */ - public static final HttpMethod PAUSE = new HttpMethod("PAUSE"); + public static final HttpMethod PAUSE = HttpMethod.valueOf("PAUSE"); /** * The TEARDOWN request stops the stream delivery for the given URI, * freeing the resources associated with it. */ - public static final HttpMethod TEARDOWN = new HttpMethod("TEARDOWN"); + public static final HttpMethod TEARDOWN = HttpMethod.valueOf("TEARDOWN"); /** * The GET_PARAMETER request retrieves the value of a parameter of a * presentation or stream specified in the URI. */ - public static final HttpMethod GET_PARAMETER = new HttpMethod("GET_PARAMETER"); + public static final HttpMethod GET_PARAMETER = HttpMethod.valueOf("GET_PARAMETER"); /** * The SET_PARAMETER requests to set the value of a parameter for a * presentation or stream specified by the URI. */ - public static final HttpMethod SET_PARAMETER = new HttpMethod("SET_PARAMETER"); + public static final HttpMethod SET_PARAMETER = HttpMethod.valueOf("SET_PARAMETER"); /** * The REDIRECT request informs the client that it must connect to another * server location. */ - public static final HttpMethod REDIRECT = new HttpMethod("REDIRECT"); + public static final HttpMethod REDIRECT = HttpMethod.valueOf("REDIRECT"); /** * The RECORD getMethod initiates recording a range of media data according to * the presentation description. */ - public static final HttpMethod RECORD = new HttpMethod("RECORD"); + public static final HttpMethod RECORD = HttpMethod.valueOf("RECORD"); private static final Map<String, HttpMethod> methodMap = new HashMap<String, HttpMethod>(); @@ -117,9 +118,7 @@ public final class RtspMethods { * will be returned. Otherwise, a new instance will be returned. */ public static HttpMethod valueOf(String name) { - if (name == null) { - throw new NullPointerException("name"); - } + ObjectUtil.checkNotNull(name, "name"); name = name.trim().toUpperCase(); if (name.isEmpty()) { @@ -130,7 +129,7 @@ public final class RtspMethods { if (result != null) { return result; } else { - return new HttpMethod(name); + return HttpMethod.valueOf(name); } } diff --git a/codec-http/src/main/java/io/netty/handler/codec/rtsp/RtspVersions.java b/codec-http/src/main/java/io/netty/handler/codec/rtsp/RtspVersions.java index 94d0368..19a060c 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/rtsp/RtspVersions.java +++ b/codec-http/src/main/java/io/netty/handler/codec/rtsp/RtspVersions.java @@ -16,6 +16,7 @@ package io.netty.handler.codec.rtsp; import io.netty.handler.codec.http.HttpVersion; +import io.netty.util.internal.ObjectUtil; /** * The version of RTSP. @@ -34,9 +35,7 @@ public final class RtspVersions { * Otherwise, a new {@link HttpVersion} instance will be returned. */ public static HttpVersion valueOf(String text) { - if (text == null) { - throw new NullPointerException("text"); - } + ObjectUtil.checkNotNull(text, "text"); text = text.trim().toUpperCase(); if ("RTSP/1.0".equals(text)) { diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyDataFrame.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyDataFrame.java index a1e9d73..322dd8c 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyDataFrame.java +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyDataFrame.java @@ -18,6 +18,7 @@ package io.netty.handler.codec.spdy; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.util.IllegalReferenceCountException; +import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.StringUtil; /** @@ -44,10 +45,8 @@ public class DefaultSpdyDataFrame extends DefaultSpdyStreamFrame implements Spdy */ public DefaultSpdyDataFrame(int streamId, ByteBuf data) { super(streamId); - if (data == null) { - throw new NullPointerException("data"); - } - this.data = validate(data); + this.data = validate( + ObjectUtil.checkNotNull(data, "data")); } private static ByteBuf validate(ByteBuf data) { diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyGoAwayFrame.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyGoAwayFrame.java index 4d88875..79c21f2 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyGoAwayFrame.java +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyGoAwayFrame.java @@ -15,6 +15,8 @@ */ package io.netty.handler.codec.spdy; +import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero; + import io.netty.util.internal.StringUtil; /** @@ -62,10 +64,7 @@ public class DefaultSpdyGoAwayFrame implements SpdyGoAwayFrame { @Override public SpdyGoAwayFrame setLastGoodStreamId(int lastGoodStreamId) { - if (lastGoodStreamId < 0) { - throw new IllegalArgumentException("Last-good-stream-ID" - + " cannot be negative: " + lastGoodStreamId); - } + checkPositiveOrZero(lastGoodStreamId, "lastGoodStreamId"); this.lastGoodStreamId = lastGoodStreamId; return this; } diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyStreamFrame.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyStreamFrame.java index 4618d4d..487844e 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyStreamFrame.java +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyStreamFrame.java @@ -15,6 +15,8 @@ */ package io.netty.handler.codec.spdy; +import static io.netty.util.internal.ObjectUtil.checkPositive; + /** * The default {@link SpdyStreamFrame} implementation. */ @@ -39,10 +41,7 @@ public abstract class DefaultSpdyStreamFrame implements SpdyStreamFrame { @Override public SpdyStreamFrame setStreamId(int streamId) { - if (streamId <= 0) { - throw new IllegalArgumentException( - "Stream-ID must be positive: " + streamId); - } + checkPositive(streamId, "streamId"); this.streamId = streamId; return this; } diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdySynReplyFrame.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdySynReplyFrame.java index 7efc905..f757d1d 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdySynReplyFrame.java +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdySynReplyFrame.java @@ -20,8 +20,7 @@ import io.netty.util.internal.StringUtil; /** * The default {@link SpdySynReplyFrame} implementation. */ -public class DefaultSpdySynReplyFrame extends DefaultSpdyHeadersFrame - implements SpdySynReplyFrame { +public class DefaultSpdySynReplyFrame extends DefaultSpdyHeadersFrame implements SpdySynReplyFrame { /** * Creates a new instance. diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdySynStreamFrame.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdySynStreamFrame.java index f8adc1c..46fe301 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdySynStreamFrame.java +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdySynStreamFrame.java @@ -15,6 +15,8 @@ */ package io.netty.handler.codec.spdy; +import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero; + import io.netty.util.internal.StringUtil; /** @@ -77,11 +79,7 @@ public class DefaultSpdySynStreamFrame extends DefaultSpdyHeadersFrame @Override public SpdySynStreamFrame setAssociatedStreamId(int associatedStreamId) { - if (associatedStreamId < 0) { - throw new IllegalArgumentException( - "Associated-To-Stream-ID cannot be negative: " + - associatedStreamId); - } + checkPositiveOrZero(associatedStreamId, "associatedStreamId"); this.associatedStreamId = associatedStreamId; return this; } diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyWindowUpdateFrame.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyWindowUpdateFrame.java index f14611b..22b0406 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyWindowUpdateFrame.java +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyWindowUpdateFrame.java @@ -15,6 +15,9 @@ */ package io.netty.handler.codec.spdy; +import static io.netty.util.internal.ObjectUtil.checkPositive; +import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero; + import io.netty.util.internal.StringUtil; /** @@ -43,10 +46,7 @@ public class DefaultSpdyWindowUpdateFrame implements SpdyWindowUpdateFrame { @Override public SpdyWindowUpdateFrame setStreamId(int streamId) { - if (streamId < 0) { - throw new IllegalArgumentException( - "Stream-ID cannot be negative: " + streamId); - } + checkPositiveOrZero(streamId, "streamId"); this.streamId = streamId; return this; } @@ -58,11 +58,7 @@ public class DefaultSpdyWindowUpdateFrame implements SpdyWindowUpdateFrame { @Override public SpdyWindowUpdateFrame setDeltaWindowSize(int deltaWindowSize) { - if (deltaWindowSize <= 0) { - throw new IllegalArgumentException( - "Delta-Window-Size must be positive: " + - deltaWindowSize); - } + checkPositive(deltaWindowSize, "deltaWindowSize"); this.deltaWindowSize = deltaWindowSize; return this; } diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyCodecUtil.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyCodecUtil.java index b25167c..0c61967 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyCodecUtil.java +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyCodecUtil.java @@ -16,6 +16,7 @@ package io.netty.handler.codec.spdy; import io.netty.buffer.ByteBuf; +import io.netty.util.internal.ObjectUtil; final class SpdyCodecUtil { @@ -286,9 +287,7 @@ final class SpdyCodecUtil { * Validate a SPDY header name. */ static void validateHeaderName(CharSequence name) { - if (name == null) { - throw new NullPointerException("name"); - } + ObjectUtil.checkNotNull(name, "name"); if (name.length() == 0) { throw new IllegalArgumentException( "name cannot be length zero"); @@ -319,9 +318,7 @@ final class SpdyCodecUtil { * Validate a SPDY header value. Does not validate max length. */ static void validateHeaderValue(CharSequence value) { - if (value == null) { - throw new NullPointerException("value"); - } + ObjectUtil.checkNotNull(value, "value"); for (int i = 0; i < value.length(); i ++) { char c = value.charAt(i); if (c == 0) { diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyFrameDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyFrameDecoder.java index e0d1112..c02081f 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyFrameDecoder.java +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyFrameDecoder.java @@ -38,8 +38,10 @@ import static io.netty.handler.codec.spdy.SpdyCodecUtil.getSignedInt; import static io.netty.handler.codec.spdy.SpdyCodecUtil.getUnsignedInt; import static io.netty.handler.codec.spdy.SpdyCodecUtil.getUnsignedMedium; import static io.netty.handler.codec.spdy.SpdyCodecUtil.getUnsignedShort; + import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; +import io.netty.util.internal.ObjectUtil; /** * Decodes {@link ByteBuf}s into SPDY Frames. @@ -89,19 +91,9 @@ public class SpdyFrameDecoder { * Creates a new instance with the specified parameters. */ public SpdyFrameDecoder(SpdyVersion spdyVersion, SpdyFrameDecoderDelegate delegate, int maxChunkSize) { - if (spdyVersion == null) { - throw new NullPointerException("spdyVersion"); - } - if (delegate == null) { - throw new NullPointerException("delegate"); - } - if (maxChunkSize <= 0) { - throw new IllegalArgumentException( - "maxChunkSize must be a positive integer: " + maxChunkSize); - } - this.spdyVersion = spdyVersion.getVersion(); - this.delegate = delegate; - this.maxChunkSize = maxChunkSize; + this.spdyVersion = ObjectUtil.checkNotNull(spdyVersion, "spdyVersion").getVersion(); + this.delegate = ObjectUtil.checkNotNull(delegate, "delegate"); + this.maxChunkSize = ObjectUtil.checkPositive(maxChunkSize, "maxChunkSize"); state = State.READ_COMMON_HEADER; } diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyFrameEncoder.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyFrameEncoder.java index 1524f95..448147a 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyFrameEncoder.java +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyFrameEncoder.java @@ -17,6 +17,7 @@ package io.netty.handler.codec.spdy; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; +import io.netty.util.internal.ObjectUtil; import java.nio.ByteOrder; import java.util.Set; @@ -34,10 +35,7 @@ public class SpdyFrameEncoder { * Creates a new instance with the specified {@code spdyVersion}. */ public SpdyFrameEncoder(SpdyVersion spdyVersion) { - if (spdyVersion == null) { - throw new NullPointerException("spdyVersion"); - } - version = spdyVersion.getVersion(); + version = ObjectUtil.checkNotNull(spdyVersion, "spdyVersion").getVersion(); } private void writeControlFrameHeader(ByteBuf buffer, int type, byte flags, int length) { diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaderBlockRawDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaderBlockRawDecoder.java index 70d4607..548b845 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaderBlockRawDecoder.java +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaderBlockRawDecoder.java @@ -17,8 +17,9 @@ package io.netty.handler.codec.spdy; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; +import io.netty.util.internal.ObjectUtil; -import static io.netty.handler.codec.spdy.SpdyCodecUtil.*; +import static io.netty.handler.codec.spdy.SpdyCodecUtil.getSignedInt; public class SpdyHeaderBlockRawDecoder extends SpdyHeaderBlockDecoder { @@ -48,9 +49,7 @@ public class SpdyHeaderBlockRawDecoder extends SpdyHeaderBlockDecoder { } public SpdyHeaderBlockRawDecoder(SpdyVersion spdyVersion, int maxHeaderSize) { - if (spdyVersion == null) { - throw new NullPointerException("spdyVersion"); - } + ObjectUtil.checkNotNull(spdyVersion, "spdyVersion"); this.maxHeaderSize = maxHeaderSize; state = State.READ_NUM_HEADERS; } @@ -63,12 +62,8 @@ public class SpdyHeaderBlockRawDecoder extends SpdyHeaderBlockDecoder { @Override void decode(ByteBufAllocator alloc, ByteBuf headerBlock, SpdyHeadersFrame frame) throws Exception { - if (headerBlock == null) { - throw new NullPointerException("headerBlock"); - } - if (frame == null) { - throw new NullPointerException("frame"); - } + ObjectUtil.checkNotNull(headerBlock, "headerBlock"); + ObjectUtil.checkNotNull(frame, "frame"); if (cumulation == null) { decodeHeaderBlock(headerBlock, frame); diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaderBlockRawEncoder.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaderBlockRawEncoder.java index afd5479..310d757 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaderBlockRawEncoder.java +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaderBlockRawEncoder.java @@ -19,6 +19,7 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.ByteBufUtil; import io.netty.buffer.Unpooled; +import io.netty.util.internal.ObjectUtil; import java.util.Set; @@ -29,10 +30,7 @@ public class SpdyHeaderBlockRawEncoder extends SpdyHeaderBlockEncoder { private final int version; public SpdyHeaderBlockRawEncoder(SpdyVersion version) { - if (version == null) { - throw new NullPointerException("version"); - } - this.version = version.getVersion(); + this.version = ObjectUtil.checkNotNull(version, "version").getVersion(); } private static void setLengthField(ByteBuf buffer, int writerIndex, int length) { diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaderBlockZlibEncoder.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaderBlockZlibEncoder.java index 9e4cf31..e3fef9d 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaderBlockZlibEncoder.java +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaderBlockZlibEncoder.java @@ -18,6 +18,8 @@ package io.netty.handler.codec.spdy; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.Unpooled; +import io.netty.util.internal.PlatformDependent; +import io.netty.util.internal.SuppressJava6Requirement; import java.util.zip.Deflater; @@ -70,11 +72,17 @@ class SpdyHeaderBlockZlibEncoder extends SpdyHeaderBlockRawEncoder { } } + @SuppressJava6Requirement(reason = "Guarded by java version check") private boolean compressInto(ByteBuf compressed) { byte[] out = compressed.array(); int off = compressed.arrayOffset() + compressed.writerIndex(); int toWrite = compressed.writableBytes(); - int numBytes = compressor.deflate(out, off, toWrite, Deflater.SYNC_FLUSH); + final int numBytes; + if (PlatformDependent.javaVersion() >= 7) { + numBytes = compressor.deflate(out, off, toWrite, Deflater.SYNC_FLUSH); + } else { + numBytes = compressor.deflate(out, off, toWrite); + } compressed.writerIndex(compressed.writerIndex() + numBytes); return numBytes == toWrite; } diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHttpDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHttpDecoder.java index 366ad15..73f15dd 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHttpDecoder.java +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHttpDecoder.java @@ -32,12 +32,14 @@ import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.HttpVersion; import io.netty.handler.codec.spdy.SpdyHttpHeaders.Names; import io.netty.util.ReferenceCountUtil; +import io.netty.util.internal.ObjectUtil; import java.util.HashMap; import java.util.List; import java.util.Map; import static io.netty.handler.codec.spdy.SpdyHeaders.HttpNames.*; +import static io.netty.util.internal.ObjectUtil.checkPositive; /** * Decodes {@link SpdySynStreamFrame}s, {@link SpdySynReplyFrame}s, @@ -100,15 +102,8 @@ public class SpdyHttpDecoder extends MessageToMessageDecoder<SpdyFrame> { */ protected SpdyHttpDecoder(SpdyVersion version, int maxContentLength, Map<Integer, FullHttpMessage> messageMap, boolean validateHeaders) { - if (version == null) { - throw new NullPointerException("version"); - } - if (maxContentLength <= 0) { - throw new IllegalArgumentException( - "maxContentLength must be a positive integer: " + maxContentLength); - } - spdyVersion = version.getVersion(); - this.maxContentLength = maxContentLength; + spdyVersion = ObjectUtil.checkNotNull(version, "version").getVersion(); + this.maxContentLength = checkPositive(maxContentLength, "maxContentLength"); this.messageMap = messageMap; this.validateHeaders = validateHeaders; } @@ -195,7 +190,7 @@ public class SpdyHttpDecoder extends MessageToMessageDecoder<SpdyFrame> { // SYN_STREAM frames initiated by the client are HTTP requests // If a client sends a request with a truncated header block, the server must - // reply with a HTTP 431 REQUEST HEADER FIELDS TOO LARGE reply. + // reply with an HTTP 431 REQUEST HEADER FIELDS TOO LARGE reply. if (spdySynStreamFrame.isTruncated()) { SpdySynReplyFrame spdySynReplyFrame = new DefaultSpdySynReplyFrame(streamId); spdySynReplyFrame.setLast(true); @@ -220,7 +215,7 @@ public class SpdyHttpDecoder extends MessageToMessageDecoder<SpdyFrame> { } } catch (Throwable t) { // If a client sends a SYN_STREAM without all of the getMethod, url (host and path), - // scheme, and version headers the server must reply with a HTTP 400 BAD REQUEST reply. + // scheme, and version headers the server must reply with an HTTP 400 BAD REQUEST reply. // Also sends HTTP 400 BAD REQUEST reply if header name/value pairs are invalid SpdySynReplyFrame spdySynReplyFrame = new DefaultSpdySynReplyFrame(streamId); spdySynReplyFrame.setLast(true); diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHttpEncoder.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHttpEncoder.java index d3c5d58..f1912c6 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHttpEncoder.java +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHttpEncoder.java @@ -28,6 +28,7 @@ import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.LastHttpContent; import io.netty.util.AsciiString; +import io.netty.util.internal.ObjectUtil; import java.util.Iterator; import java.util.List; @@ -144,9 +145,7 @@ public class SpdyHttpEncoder extends MessageToMessageEncoder<HttpObject> { * @param validateHeaders validate the header names and values when adding them to the {@link SpdyHeaders} */ public SpdyHttpEncoder(SpdyVersion version, boolean headersToLowerCase, boolean validateHeaders) { - if (version == null) { - throw new NullPointerException("version"); - } + ObjectUtil.checkNotNull(version, "version"); this.headersToLowerCase = headersToLowerCase; this.validateHeaders = validateHeaders; } diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHttpResponseStreamIdHandler.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHttpResponseStreamIdHandler.java index 4ad32e4..e664adf 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHttpResponseStreamIdHandler.java +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHttpResponseStreamIdHandler.java @@ -21,7 +21,7 @@ import io.netty.handler.codec.http.HttpMessage; import io.netty.handler.codec.spdy.SpdyHttpHeaders.Names; import io.netty.util.ReferenceCountUtil; -import java.util.LinkedList; +import java.util.ArrayDeque; import java.util.List; import java.util.Queue; @@ -33,7 +33,7 @@ import java.util.Queue; public class SpdyHttpResponseStreamIdHandler extends MessageToMessageCodec<Object, HttpMessage> { private static final Integer NO_ID = -1; - private final Queue<Integer> ids = new LinkedList<Integer>(); + private final Queue<Integer> ids = new ArrayDeque<Integer>(); @Override public boolean acceptInboundMessage(Object msg) throws Exception { diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyProtocolException.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyProtocolException.java index 2b5bcbc..477599a 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyProtocolException.java +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyProtocolException.java @@ -15,6 +15,9 @@ */ package io.netty.handler.codec.spdy; +import io.netty.util.internal.PlatformDependent; +import io.netty.util.internal.SuppressJava6Requirement; + public class SpdyProtocolException extends Exception { private static final long serialVersionUID = 7870000537743847264L; @@ -44,4 +47,18 @@ public class SpdyProtocolException extends Exception { public SpdyProtocolException(Throwable cause) { super(cause); } + + static SpdyProtocolException newStatic(String message) { + if (PlatformDependent.javaVersion() >= 7) { + return new SpdyProtocolException(message, true); + } + return new SpdyProtocolException(message); + } + + @SuppressJava6Requirement(reason = "uses Java 7+ Exception.<init>(String, Throwable, boolean, boolean)" + + " but is guarded by version checks") + private SpdyProtocolException(String message, boolean shared) { + super(message, null, false, true); + assert shared; + } } diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdySessionHandler.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdySessionHandler.java index 394f6c2..e3e00cf 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdySessionHandler.java +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdySessionHandler.java @@ -20,12 +20,14 @@ import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelPromise; +import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.ThrowableUtil; import java.util.concurrent.atomic.AtomicInteger; import static io.netty.handler.codec.spdy.SpdyCodecUtil.SPDY_SESSION_STREAM_ID; import static io.netty.handler.codec.spdy.SpdyCodecUtil.isServerId; +import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero; /** * Manages streams within a SPDY session. @@ -33,9 +35,9 @@ import static io.netty.handler.codec.spdy.SpdyCodecUtil.isServerId; public class SpdySessionHandler extends ChannelDuplexHandler { private static final SpdyProtocolException PROTOCOL_EXCEPTION = ThrowableUtil.unknownStackTrace( - new SpdyProtocolException(), SpdySessionHandler.class, "handleOutboundMessage(...)"); + SpdyProtocolException.newStatic(null), SpdySessionHandler.class, "handleOutboundMessage(...)"); private static final SpdyProtocolException STREAM_CLOSED = ThrowableUtil.unknownStackTrace( - new SpdyProtocolException("Stream closed"), SpdySessionHandler.class, "removeStream(...)"); + SpdyProtocolException.newStatic("Stream closed"), SpdySessionHandler.class, "removeStream(...)"); private static final int DEFAULT_WINDOW_SIZE = 64 * 1024; // 64 KB default initial window size private int initialSendWindowSize = DEFAULT_WINDOW_SIZE; @@ -69,24 +71,19 @@ public class SpdySessionHandler extends ChannelDuplexHandler { * handle the client endpoint of the connection. */ public SpdySessionHandler(SpdyVersion version, boolean server) { - if (version == null) { - throw new NullPointerException("version"); - } + this.minorVersion = ObjectUtil.checkNotNull(version, "version").getMinorVersion(); this.server = server; - minorVersion = version.getMinorVersion(); } public void setSessionReceiveWindowSize(int sessionReceiveWindowSize) { - if (sessionReceiveWindowSize < 0) { - throw new IllegalArgumentException("sessionReceiveWindowSize"); - } - // This will not send a window update frame immediately. - // If this value increases the allowed receive window size, - // a WINDOW_UPDATE frame will be sent when only half of the - // session window size remains during data frame processing. - // If this value decreases the allowed receive window size, - // the window will be reduced as data frames are processed. - initialSessionReceiveWindowSize = sessionReceiveWindowSize; + checkPositiveOrZero(sessionReceiveWindowSize, "sessionReceiveWindowSize"); + // This will not send a window update frame immediately. + // If this value increases the allowed receive window size, + // a WINDOW_UPDATE frame will be sent when only half of the + // session window size remains during data frame processing. + // If this value decreases the allowed receive window size, + // the window will be reduced as data frames are processed. + initialSessionReceiveWindowSize = sessionReceiveWindowSize; } @Override diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdySessionStatus.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdySessionStatus.java index fd79d1e..7ca86c8 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdySessionStatus.java +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdySessionStatus.java @@ -15,6 +15,8 @@ */ package io.netty.handler.codec.spdy; +import io.netty.util.internal.ObjectUtil; + /** * The SPDY session status code and its description. */ @@ -65,12 +67,8 @@ public class SpdySessionStatus implements Comparable<SpdySessionStatus> { * {@code statusPhrase}. */ public SpdySessionStatus(int code, String statusPhrase) { - if (statusPhrase == null) { - throw new NullPointerException("statusPhrase"); - } - + this.statusPhrase = ObjectUtil.checkNotNull(statusPhrase, "statusPhrase"); this.code = code; - this.statusPhrase = statusPhrase; } /** diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyStreamStatus.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyStreamStatus.java index 75ed740..c2b8674 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyStreamStatus.java +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyStreamStatus.java @@ -15,6 +15,8 @@ */ package io.netty.handler.codec.spdy; +import io.netty.util.internal.ObjectUtil; + /** * The SPDY stream status code and its description. */ @@ -139,12 +141,8 @@ public class SpdyStreamStatus implements Comparable<SpdyStreamStatus> { "0 is not a valid status code for a RST_STREAM"); } - if (statusPhrase == null) { - throw new NullPointerException("statusPhrase"); - } - + this.statusPhrase = ObjectUtil.checkNotNull(statusPhrase, "statusPhrase"); this.code = code; - this.statusPhrase = statusPhrase; } /** diff --git a/codec-http/src/main/resources/META-INF/native-image/io.netty/codec-http/native-image.properties b/codec-http/src/main/resources/META-INF/native-image/io.netty/codec-http/native-image.properties new file mode 100644 index 0000000..df76109 --- /dev/null +++ b/codec-http/src/main/resources/META-INF/native-image/io.netty/codec-http/native-image.properties @@ -0,0 +1,16 @@ +# Copyright 2019 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. + +Args = --initialize-at-build-time=io.netty \ + --initialize-at-run-time=io.netty.handler.codec.http.HttpObjectEncoder,io.netty.handler.codec.http.websocketx.WebSocket00FrameEncoder,io.netty.handler.codec.http.websocketx.extensions.compression.DeflateDecoder diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/CombinedHttpHeadersTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/CombinedHttpHeadersTest.java index c63c885..2023030 100644 --- a/codec-http/src/test/java/io/netty/handler/codec/http/CombinedHttpHeadersTest.java +++ b/codec-http/src/test/java/io/netty/handler/codec/http/CombinedHttpHeadersTest.java @@ -55,7 +55,7 @@ public class CombinedHttpHeadersTest { otherHeaders.add(HEADER_NAME, "a"); otherHeaders.add(HEADER_NAME, "b"); headers.add(otherHeaders); - assertEquals("a,b", headers.get(HEADER_NAME).toString()); + assertEquals("a,b", headers.get(HEADER_NAME)); } @Test @@ -66,7 +66,7 @@ public class CombinedHttpHeadersTest { otherHeaders.add(HEADER_NAME, "b"); otherHeaders.add(HEADER_NAME, "c"); headers.add(otherHeaders); - assertEquals("a,b,c", headers.get(HEADER_NAME).toString()); + assertEquals("a,b,c", headers.get(HEADER_NAME)); } @Test @@ -99,7 +99,7 @@ public class CombinedHttpHeadersTest { otherHeaders.add(HEADER_NAME, "b"); otherHeaders.add(HEADER_NAME, "c"); headers.set(otherHeaders); - assertEquals("b,c", headers.get(HEADER_NAME).toString()); + assertEquals("b,c", headers.get(HEADER_NAME)); } @Test @@ -110,7 +110,7 @@ public class CombinedHttpHeadersTest { otherHeaders.add(HEADER_NAME, "b"); otherHeaders.add(HEADER_NAME, "c"); headers.add(otherHeaders); - assertEquals("a,b,c", headers.get(HEADER_NAME).toString()); + assertEquals("a,b,c", headers.get(HEADER_NAME)); } @Test @@ -121,7 +121,7 @@ public class CombinedHttpHeadersTest { otherHeaders.add(HEADER_NAME, "b"); otherHeaders.add(HEADER_NAME, "c"); headers.set(otherHeaders); - assertEquals("b,c", headers.get(HEADER_NAME).toString()); + assertEquals("b,c", headers.get(HEADER_NAME)); } @Test @@ -196,7 +196,7 @@ public class CombinedHttpHeadersTest { public void addIterableCsvEmpty() { final CombinedHttpHeaders headers = newCombinedHttpHeaders(); headers.add(HEADER_NAME, Collections.<CharSequence>emptyList()); - assertEquals(Arrays.asList(""), headers.getAll(HEADER_NAME)); + assertEquals(Collections.singletonList(""), headers.getAll(HEADER_NAME)); } @Test @@ -294,9 +294,9 @@ public class CombinedHttpHeadersTest { headers.set(HEADER_NAME, Arrays.asList("\"a\"", "\"b\"", "\"c\"")); assertEquals(Arrays.asList("a", "b", "c"), headers.getAll(HEADER_NAME)); headers.set(HEADER_NAME, "a,b,c"); - assertEquals(Arrays.asList("a,b,c"), headers.getAll(HEADER_NAME)); + assertEquals(Collections.singletonList("a,b,c"), headers.getAll(HEADER_NAME)); headers.set(HEADER_NAME, "\"a,b,c\""); - assertEquals(Arrays.asList("a,b,c"), headers.getAll(HEADER_NAME)); + assertEquals(Collections.singletonList("a,b,c"), headers.getAll(HEADER_NAME)); } @Test diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/HttpClientCodecTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/HttpClientCodecTest.java index 16a6eff..a9195d7 100644 --- a/codec-http/src/test/java/io/netty/handler/codec/http/HttpClientCodecTest.java +++ b/codec-http/src/test/java/io/netty/handler/codec/http/HttpClientCodecTest.java @@ -35,6 +35,7 @@ import io.netty.handler.codec.CodecException; import io.netty.handler.codec.PrematureChannelClosureException; import io.netty.util.CharsetUtil; import io.netty.util.NetUtil; +import org.hamcrest.CoreMatchers; import org.junit.Test; import java.net.InetSocketAddress; @@ -331,4 +332,90 @@ public class HttpClientCodecTest { assertThat(ch.readInbound(), is(nullValue())); } + + @Test + public void testWebDavResponse() { + byte[] data = ("HTTP/1.1 102 Processing\r\n" + + "Status-URI: Status-URI:http://status.com; 404\r\n" + + "\r\n" + + "1234567812345678").getBytes(); + EmbeddedChannel ch = new EmbeddedChannel(new HttpClientCodec()); + assertTrue(ch.writeInbound(Unpooled.wrappedBuffer(data))); + + HttpResponse res = ch.readInbound(); + assertThat(res.protocolVersion(), sameInstance(HttpVersion.HTTP_1_1)); + assertThat(res.status(), is(HttpResponseStatus.PROCESSING)); + HttpContent content = ch.readInbound(); + // HTTP 102 is not allowed to have content. + assertThat(content.content().readableBytes(), is(0)); + content.release(); + + assertThat(ch.finish(), is(false)); + } + + @Test + public void testInformationalResponseKeepsPairsInSync() { + byte[] data = ("HTTP/1.1 102 Processing\r\n" + + "Status-URI: Status-URI:http://status.com; 404\r\n" + + "\r\n").getBytes(); + byte[] data2 = ("HTTP/1.1 200 OK\r\n" + + "Content-Length: 8\r\n" + + "\r\n" + + "12345678").getBytes(); + EmbeddedChannel ch = new EmbeddedChannel(new HttpClientCodec()); + assertTrue(ch.writeOutbound(new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.HEAD, "/"))); + ByteBuf buffer = ch.readOutbound(); + buffer.release(); + assertNull(ch.readOutbound()); + assertTrue(ch.writeInbound(Unpooled.wrappedBuffer(data))); + HttpResponse res = ch.readInbound(); + assertThat(res.protocolVersion(), sameInstance(HttpVersion.HTTP_1_1)); + assertThat(res.status(), is(HttpResponseStatus.PROCESSING)); + HttpContent content = ch.readInbound(); + // HTTP 102 is not allowed to have content. + assertThat(content.content().readableBytes(), is(0)); + assertThat(content, CoreMatchers.<HttpContent>instanceOf(LastHttpContent.class)); + content.release(); + + assertTrue(ch.writeOutbound(new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/"))); + buffer = ch.readOutbound(); + buffer.release(); + assertNull(ch.readOutbound()); + assertTrue(ch.writeInbound(Unpooled.wrappedBuffer(data2))); + + res = ch.readInbound(); + assertThat(res.protocolVersion(), sameInstance(HttpVersion.HTTP_1_1)); + assertThat(res.status(), is(HttpResponseStatus.OK)); + content = ch.readInbound(); + // HTTP 200 has content. + assertThat(content.content().readableBytes(), is(8)); + assertThat(content, CoreMatchers.<HttpContent>instanceOf(LastHttpContent.class)); + content.release(); + + assertThat(ch.finish(), is(false)); + } + + @Test + public void testMultipleResponses() { + String response = "HTTP/1.1 200 OK\r\n" + + "Content-Length: 0\r\n\r\n"; + + HttpClientCodec codec = new HttpClientCodec(4096, 8192, 8192, true); + EmbeddedChannel ch = new EmbeddedChannel(codec, new HttpObjectAggregator(1024)); + + HttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "http://localhost/"); + assertTrue(ch.writeOutbound(request)); + + assertTrue(ch.writeInbound(Unpooled.copiedBuffer(response, CharsetUtil.UTF_8))); + assertTrue(ch.writeInbound(Unpooled.copiedBuffer(response, CharsetUtil.UTF_8))); + FullHttpResponse resp = ch.readInbound(); + assertTrue(resp.decoderResult().isSuccess()); + resp.release(); + + resp = ch.readInbound(); + assertTrue(resp.decoderResult().isSuccess()); + resp.release(); + assertTrue(ch.finishAndReleaseAll()); + } + } diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/HttpContentCompressorTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/HttpContentCompressorTest.java index 508eb5a..bb43b3c 100644 --- a/codec-http/src/test/java/io/netty/handler/codec/http/HttpContentCompressorTest.java +++ b/codec-http/src/test/java/io/netty/handler/codec/http/HttpContentCompressorTest.java @@ -15,14 +15,32 @@ */ package io.netty.handler.codec.http; +import io.netty.bootstrap.Bootstrap; +import io.netty.bootstrap.ServerBootstrap; import io.netty.buffer.ByteBufUtil; import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelOutboundHandlerAdapter; +import io.netty.channel.ChannelPromise; +import io.netty.channel.DefaultEventLoopGroup; +import io.netty.channel.EventLoopGroup; import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.channel.local.LocalAddress; +import io.netty.channel.local.LocalChannel; +import io.netty.channel.local.LocalServerChannel; import io.netty.handler.codec.DecoderResult; import io.netty.handler.codec.EncoderException; import io.netty.handler.codec.compression.ZlibWrapper; import io.netty.util.CharsetUtil; import io.netty.util.ReferenceCountUtil; +import java.util.UUID; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; import org.junit.Test; import static io.netty.handler.codec.http.HttpHeadersTestUtils.of; @@ -259,6 +277,104 @@ public class HttpContentCompressorTest { assertThat(ch.readOutbound(), is(nullValue())); } + @Test + public void testExecutorPreserveOrdering() throws Exception { + final EventLoopGroup compressorGroup = new DefaultEventLoopGroup(1); + EventLoopGroup localGroup = new DefaultEventLoopGroup(1); + Channel server = null; + Channel client = null; + try { + ServerBootstrap bootstrap = new ServerBootstrap() + .channel(LocalServerChannel.class) + .group(localGroup) + .childHandler(new ChannelInitializer<LocalChannel>() { + @Override + protected void initChannel(LocalChannel ch) throws Exception { + ch.pipeline() + .addLast(new HttpServerCodec()) + .addLast(new HttpObjectAggregator(1024)) + .addLast(compressorGroup, new HttpContentCompressor()) + .addLast(new ChannelOutboundHandlerAdapter() { + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) + throws Exception { + super.write(ctx, msg, promise); + } + }) + .addLast(new ChannelInboundHandlerAdapter() { + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + if (msg instanceof FullHttpRequest) { + FullHttpResponse res = + new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, + Unpooled.copiedBuffer("Hello, World", CharsetUtil.US_ASCII)); + ctx.writeAndFlush(res); + ReferenceCountUtil.release(msg); + return; + } + super.channelRead(ctx, msg); + } + }); + } + }); + + LocalAddress address = new LocalAddress(UUID.randomUUID().toString()); + server = bootstrap.bind(address).sync().channel(); + + final BlockingQueue<HttpObject> responses = new LinkedBlockingQueue<HttpObject>(); + + client = new Bootstrap() + .channel(LocalChannel.class) + .remoteAddress(address) + .group(localGroup) + .handler(new ChannelInitializer<LocalChannel>() { + @Override + protected void initChannel(LocalChannel ch) throws Exception { + ch.pipeline().addLast(new HttpClientCodec()).addLast(new ChannelInboundHandlerAdapter() { + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + if (msg instanceof HttpObject) { + responses.put((HttpObject) msg); + return; + } + super.channelRead(ctx, msg); + } + }); + } + }).connect().sync().channel(); + + client.writeAndFlush(newRequest()).sync(); + + assertEncodedResponse((HttpResponse) responses.poll(1, TimeUnit.SECONDS)); + HttpContent c = (HttpContent) responses.poll(1, TimeUnit.SECONDS); + assertNotNull(c); + assertThat(ByteBufUtil.hexDump(c.content()), + is("1f8b0800000000000000f248cdc9c9d75108cf2fca4901000000ffff")); + c.release(); + + c = (HttpContent) responses.poll(1, TimeUnit.SECONDS); + assertNotNull(c); + assertThat(ByteBufUtil.hexDump(c.content()), is("0300c6865b260c000000")); + c.release(); + + LastHttpContent last = (LastHttpContent) responses.poll(1, TimeUnit.SECONDS); + assertNotNull(last); + assertThat(last.content().readableBytes(), is(0)); + last.release(); + + assertNull(responses.poll(1, TimeUnit.SECONDS)); + } finally { + if (client != null) { + client.close().sync(); + } + if (server != null) { + server.close().sync(); + } + compressorGroup.shutdownGracefully(); + localGroup.shutdownGracefully(); + } + } + /** * If the length of the content is unknown, {@link HttpContentEncoder} should not skip encoding the content * even if the actual length is turned out to be 0. @@ -272,7 +388,7 @@ public class HttpContentCompressorTest { assertEncodedResponse(ch); ch.writeOutbound(LastHttpContent.EMPTY_LAST_CONTENT); - HttpContent chunk = (HttpContent) ch.readOutbound(); + HttpContent chunk = ch.readOutbound(); assertThat(ByteBufUtil.hexDump(chunk.content()), is("1f8b080000000000000003000000000000000000")); assertThat(chunk, is(instanceOf(HttpContent.class))); chunk.release(); @@ -423,7 +539,7 @@ public class HttpContentCompressorTest { res.headers().set(HttpHeaderNames.CONTENT_ENCODING, HttpHeaderValues.IDENTITY); assertTrue(ch.writeOutbound(res)); - FullHttpResponse response = (FullHttpResponse) ch.readOutbound(); + FullHttpResponse response = ch.readOutbound(); assertEquals(String.valueOf(len), response.headers().get(HttpHeaderNames.CONTENT_LENGTH)); assertEquals(HttpHeaderValues.IDENTITY.toString(), response.headers().get(HttpHeaderNames.CONTENT_ENCODING)); assertEquals("Hello, World", response.content().toString(CharsetUtil.US_ASCII)); @@ -445,7 +561,7 @@ public class HttpContentCompressorTest { res.headers().set(HttpHeaderNames.CONTENT_ENCODING, "ascii"); assertTrue(ch.writeOutbound(res)); - FullHttpResponse response = (FullHttpResponse) ch.readOutbound(); + FullHttpResponse response = ch.readOutbound(); assertEquals(String.valueOf(len), response.headers().get(HttpHeaderNames.CONTENT_LENGTH)); assertEquals("ascii", response.headers().get(HttpHeaderNames.CONTENT_ENCODING)); assertEquals("Hello, World", response.content().toString(CharsetUtil.US_ASCII)); @@ -500,6 +616,39 @@ public class HttpContentCompressorTest { assertTrue(ch.finishAndReleaseAll()); } + @Test + public void testMultipleAcceptEncodingHeaders() { + FullHttpRequest request = newRequest(); + request.headers().set(HttpHeaderNames.ACCEPT_ENCODING, "unknown; q=1.0") + .add(HttpHeaderNames.ACCEPT_ENCODING, "gzip; q=0.5") + .add(HttpHeaderNames.ACCEPT_ENCODING, "deflate; q=0"); + + EmbeddedChannel ch = new EmbeddedChannel(new HttpContentCompressor()); + + assertTrue(ch.writeInbound(request)); + + FullHttpResponse res = new DefaultFullHttpResponse( + HttpVersion.HTTP_1_1, HttpResponseStatus.OK, + Unpooled.copiedBuffer("Gzip Win", CharsetUtil.US_ASCII)); + assertTrue(ch.writeOutbound(res)); + + assertEncodedResponse(ch); + HttpContent c = ch.readOutbound(); + assertThat(ByteBufUtil.hexDump(c.content()), is("1f8b080000000000000072afca2c5008cfcc03000000ffff")); + c.release(); + + c = ch.readOutbound(); + assertThat(ByteBufUtil.hexDump(c.content()), is("03001f2ebf0f08000000")); + c.release(); + + LastHttpContent last = ch.readOutbound(); + assertThat(last.content().readableBytes(), is(0)); + last.release(); + + assertThat(ch.readOutbound(), is(nullValue())); + assertTrue(ch.finishAndReleaseAll()); + } + private static FullHttpRequest newRequest() { FullHttpRequest req = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/"); req.headers().set(HttpHeaderNames.ACCEPT_ENCODING, "gzip"); @@ -510,7 +659,10 @@ public class HttpContentCompressorTest { Object o = ch.readOutbound(); assertThat(o, is(instanceOf(HttpResponse.class))); - HttpResponse res = (HttpResponse) o; + assertEncodedResponse((HttpResponse) o); + } + + private static void assertEncodedResponse(HttpResponse res) { assertThat(res, is(not(instanceOf(HttpContent.class)))); assertThat(res.headers().get(HttpHeaderNames.TRANSFER_ENCODING), is("chunked")); assertThat(res.headers().get(HttpHeaderNames.CONTENT_LENGTH), is(nullValue())); diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/HttpContentDecoderTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/HttpContentDecoderTest.java index ff06285..24833a7 100644 --- a/codec-http/src/test/java/io/netty/handler/codec/http/HttpContentDecoderTest.java +++ b/codec-http/src/test/java/io/netty/handler/codec/http/HttpContentDecoderTest.java @@ -178,7 +178,7 @@ public class HttpContentDecoderTest { assertThat(o, is(instanceOf(FullHttpResponse.class))); FullHttpResponse r = (FullHttpResponse) o; assertEquals(100, r.status().code()); - assertTrue(channel.writeInbound(Unpooled.wrappedBuffer(GZ_HELLO_WORLD))); + assertTrue(channel.writeInbound(Unpooled.copiedBuffer(GZ_HELLO_WORLD))); r.release(); assertHasInboundMessages(channel, true); @@ -205,7 +205,7 @@ public class HttpContentDecoderTest { FullHttpResponse r = (FullHttpResponse) o; assertEquals(100, r.status().code()); r.release(); - assertTrue(channel.writeInbound(Unpooled.wrappedBuffer(GZ_HELLO_WORLD))); + assertTrue(channel.writeInbound(Unpooled.copiedBuffer(GZ_HELLO_WORLD))); assertHasInboundMessages(channel, true); assertHasOutboundMessages(channel, false); @@ -232,7 +232,7 @@ public class HttpContentDecoderTest { FullHttpResponse r = (FullHttpResponse) o; assertEquals(100, r.status().code()); r.release(); - assertTrue(channel.writeInbound(Unpooled.wrappedBuffer(GZ_HELLO_WORLD))); + assertTrue(channel.writeInbound(Unpooled.copiedBuffer(GZ_HELLO_WORLD))); assertHasInboundMessages(channel, true); assertHasOutboundMessages(channel, false); @@ -259,7 +259,7 @@ public class HttpContentDecoderTest { FullHttpResponse r = (FullHttpResponse) o; assertEquals(100, r.status().code()); r.release(); - assertTrue(channel.writeInbound(Unpooled.wrappedBuffer(GZ_HELLO_WORLD))); + assertTrue(channel.writeInbound(Unpooled.copiedBuffer(GZ_HELLO_WORLD))); assertHasInboundMessages(channel, true); assertHasOutboundMessages(channel, false); @@ -566,7 +566,7 @@ public class HttpContentDecoderTest { private static byte[] gzDecompress(byte[] input) { ZlibDecoder decoder = ZlibCodecFactory.newZlibDecoder(ZlibWrapper.GZIP); EmbeddedChannel channel = new EmbeddedChannel(decoder); - assertTrue(channel.writeInbound(Unpooled.wrappedBuffer(input))); + assertTrue(channel.writeInbound(Unpooled.copiedBuffer(input))); assertTrue(channel.finish()); // close the channel to indicate end-of-data int outputSize = 0; diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/HttpContentDecompressorTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/HttpContentDecompressorTest.java new file mode 100644 index 0000000..4a659fa --- /dev/null +++ b/codec-http/src/test/java/io/netty/handler/codec/http/HttpContentDecompressorTest.java @@ -0,0 +1,70 @@ +/* + * Copyright 2019 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.codec.http; + +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.ChannelOutboundHandlerAdapter; +import io.netty.channel.embedded.EmbeddedChannel; +import org.junit.Assert; +import org.junit.Test; + +import java.util.concurrent.atomic.AtomicInteger; + +public class HttpContentDecompressorTest { + + // See https://github.com/netty/netty/issues/8915. + @Test + public void testInvokeReadWhenNotProduceMessage() { + final AtomicInteger readCalled = new AtomicInteger(); + EmbeddedChannel channel = new EmbeddedChannel(new ChannelOutboundHandlerAdapter() { + @Override + public void read(ChannelHandlerContext ctx) { + readCalled.incrementAndGet(); + ctx.read(); + } + }, new HttpContentDecompressor(), new ChannelInboundHandlerAdapter() { + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + ctx.fireChannelRead(msg); + ctx.read(); + } + }); + + channel.config().setAutoRead(false); + + readCalled.set(0); + HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK); + response.headers().set(HttpHeaderNames.CONTENT_ENCODING, "gzip"); + response.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/json;charset=UTF-8"); + response.headers().set(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.CHUNKED); + + Assert.assertTrue(channel.writeInbound(response)); + + // we triggered read explicitly + Assert.assertEquals(1, readCalled.get()); + + Assert.assertTrue(channel.readInbound() instanceof HttpResponse); + + Assert.assertFalse(channel.writeInbound(new DefaultHttpContent(Unpooled.EMPTY_BUFFER))); + + // read was triggered by the HttpContentDecompressor itself as it did not produce any message to the next + // inbound handler. + Assert.assertEquals(2, readCalled.get()); + Assert.assertFalse(channel.finishAndReleaseAll()); + } +} diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/HttpContentEncoderTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/HttpContentEncoderTest.java index 6301ee8..9bb8838 100644 --- a/codec-http/src/test/java/io/netty/handler/codec/http/HttpContentEncoderTest.java +++ b/codec-http/src/test/java/io/netty/handler/codec/http/HttpContentEncoderTest.java @@ -41,7 +41,7 @@ public class HttpContentEncoderTest { private static final class TestEncoder extends HttpContentEncoder { @Override - protected Result beginEncode(HttpResponse headers, String acceptEncoding) { + protected Result beginEncode(HttpResponse httpResponse, String acceptEncoding) { return new Result("test", new EmbeddedChannel(new MessageToByteEncoder<ByteBuf>() { @Override protected void encode(ChannelHandlerContext ctx, ByteBuf in, ByteBuf out) throws Exception { @@ -395,7 +395,7 @@ public class HttpContentEncoderTest { public void testCleanupThrows() { HttpContentEncoder encoder = new HttpContentEncoder() { @Override - protected Result beginEncode(HttpResponse headers, String acceptEncoding) throws Exception { + protected Result beginEncode(HttpResponse httpResponse, String acceptEncoding) throws Exception { return new Result("myencoding", new EmbeddedChannel( new ChannelInboundHandlerAdapter() { @Override diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/HttpObjectAggregatorTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/HttpObjectAggregatorTest.java index 7dc0ac5..1a97659 100644 --- a/codec-http/src/test/java/io/netty/handler/codec/http/HttpObjectAggregatorTest.java +++ b/codec-http/src/test/java/io/netty/handler/codec/http/HttpObjectAggregatorTest.java @@ -23,7 +23,10 @@ import io.netty.channel.embedded.EmbeddedChannel; import io.netty.handler.codec.DecoderResult; import io.netty.handler.codec.DecoderResultProvider; import io.netty.handler.codec.TooLongFrameException; +import io.netty.util.AsciiString; import io.netty.util.CharsetUtil; +import io.netty.util.ReferenceCountUtil; + import org.junit.Test; import org.mockito.Mockito; @@ -33,6 +36,7 @@ import java.util.List; import static io.netty.handler.codec.http.HttpHeadersTestUtils.of; import static org.hamcrest.CoreMatchers.instanceOf; import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; @@ -40,6 +44,7 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import static org.junit.Assert.assertSame; public class HttpObjectAggregatorTest { @@ -139,9 +144,60 @@ public class HttpObjectAggregatorTest { assertFalse(embedder.finish()); } + @Test + public void testOversizedRequestWithContentLengthAndDecoder() { + EmbeddedChannel embedder = new EmbeddedChannel(new HttpRequestDecoder(), new HttpObjectAggregator(4, false)); + assertFalse(embedder.writeInbound(Unpooled.copiedBuffer( + "PUT /upload HTTP/1.1\r\n" + + "Content-Length: 5\r\n\r\n", CharsetUtil.US_ASCII))); + + assertNull(embedder.readInbound()); + + FullHttpResponse response = embedder.readOutbound(); + assertEquals(HttpResponseStatus.REQUEST_ENTITY_TOO_LARGE, response.status()); + assertEquals("0", response.headers().get(HttpHeaderNames.CONTENT_LENGTH)); + + assertTrue(embedder.isOpen()); + + assertFalse(embedder.writeInbound(Unpooled.wrappedBuffer(new byte[] { 1, 2, 3, 4 }))); + assertFalse(embedder.writeInbound(Unpooled.wrappedBuffer(new byte[] { 5 }))); + + assertNull(embedder.readOutbound()); + + assertFalse(embedder.writeInbound(Unpooled.copiedBuffer( + "PUT /upload HTTP/1.1\r\n" + + "Content-Length: 2\r\n\r\n", CharsetUtil.US_ASCII))); + + assertEquals(HttpResponseStatus.REQUEST_ENTITY_TOO_LARGE, response.status()); + assertEquals("0", response.headers().get(HttpHeaderNames.CONTENT_LENGTH)); + + assertThat(response, instanceOf(LastHttpContent.class)); + ReferenceCountUtil.release(response); + + assertTrue(embedder.isOpen()); + + assertFalse(embedder.writeInbound(Unpooled.copiedBuffer(new byte[] { 1 }))); + assertNull(embedder.readOutbound()); + assertTrue(embedder.writeInbound(Unpooled.copiedBuffer(new byte[] { 2 }))); + assertNull(embedder.readOutbound()); + + FullHttpRequest request = embedder.readInbound(); + assertEquals(HttpVersion.HTTP_1_1, request.protocolVersion()); + assertEquals(HttpMethod.PUT, request.method()); + assertEquals("/upload", request.uri()); + assertEquals(2, HttpUtil.getContentLength(request)); + + byte[] actual = new byte[request.content().readableBytes()]; + request.content().readBytes(actual); + assertArrayEquals(new byte[] { 1, 2 }, actual); + request.release(); + + assertFalse(embedder.finish()); + } + @Test public void testOversizedRequestWithoutKeepAlive() { - // send a HTTP/1.0 request with no keep-alive header + // send an HTTP/1.0 request with no keep-alive header HttpRequest message = new DefaultHttpRequest(HttpVersion.HTTP_1_0, HttpMethod.PUT, "http://localhost"); HttpUtil.setContentLength(message, 5); checkOversizedRequest(message); @@ -162,11 +218,48 @@ public class HttpObjectAggregatorTest { assertEquals(HttpResponseStatus.REQUEST_ENTITY_TOO_LARGE, response.status()); assertEquals("0", response.headers().get(HttpHeaderNames.CONTENT_LENGTH)); + assertThat(response, instanceOf(LastHttpContent.class)); + ReferenceCountUtil.release(response); + if (serverShouldCloseConnection(message, response)) { assertFalse(embedder.isOpen()); + + try { + embedder.writeInbound(new DefaultHttpContent(Unpooled.EMPTY_BUFFER)); + fail(); + } catch (Exception e) { + assertThat(e, instanceOf(ClosedChannelException.class)); + // expected + } assertFalse(embedder.finish()); } else { assertTrue(embedder.isOpen()); + assertFalse(embedder.writeInbound(new DefaultHttpContent(Unpooled.copiedBuffer(new byte[8])))); + assertFalse(embedder.writeInbound(new DefaultHttpContent(Unpooled.copiedBuffer(new byte[8])))); + + // Now start a new message and ensure we will not reject it again. + HttpRequest message2 = new DefaultHttpRequest(HttpVersion.HTTP_1_0, HttpMethod.PUT, "http://localhost"); + HttpUtil.setContentLength(message, 2); + + assertFalse(embedder.writeInbound(message2)); + assertNull(embedder.readOutbound()); + assertFalse(embedder.writeInbound(new DefaultHttpContent(Unpooled.copiedBuffer(new byte[] { 1 })))); + assertNull(embedder.readOutbound()); + assertTrue(embedder.writeInbound(new DefaultLastHttpContent(Unpooled.copiedBuffer(new byte[] { 2 })))); + assertNull(embedder.readOutbound()); + + FullHttpRequest request = embedder.readInbound(); + assertEquals(message2.protocolVersion(), request.protocolVersion()); + assertEquals(message2.method(), request.method()); + assertEquals(message2.uri(), request.uri()); + assertEquals(2, HttpUtil.getContentLength(request)); + + byte[] actual = new byte[request.content().readableBytes()]; + request.content().readBytes(actual); + assertArrayEquals(new byte[] { 1, 2 }, actual); + request.release(); + + assertFalse(embedder.finish()); } } @@ -517,4 +610,123 @@ public class HttpObjectAggregatorTest { aggregatedRep.release(); replacedRep.release(); } + + @Test + public void testSelectiveRequestAggregation() { + HttpObjectAggregator myPostAggregator = new HttpObjectAggregator(1024 * 1024) { + @Override + protected boolean isStartMessage(HttpObject msg) throws Exception { + if (msg instanceof HttpRequest) { + HttpRequest request = (HttpRequest) msg; + HttpMethod method = request.method(); + + if (method.equals(HttpMethod.POST)) { + return true; + } + } + + return false; + } + }; + + EmbeddedChannel channel = new EmbeddedChannel(myPostAggregator); + + try { + // Aggregate: POST + HttpRequest request1 = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, "/"); + HttpContent content1 = new DefaultHttpContent(Unpooled.copiedBuffer("Hello, World!", CharsetUtil.UTF_8)); + request1.headers().set(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.TEXT_PLAIN); + + assertTrue(channel.writeInbound(request1, content1, LastHttpContent.EMPTY_LAST_CONTENT)); + + // Getting an aggregated response out + Object msg1 = channel.readInbound(); + try { + assertTrue(msg1 instanceof FullHttpRequest); + } finally { + ReferenceCountUtil.release(msg1); + } + + // Don't aggregate: non-POST + HttpRequest request2 = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.PUT, "/"); + HttpContent content2 = new DefaultHttpContent(Unpooled.copiedBuffer("Hello, World!", CharsetUtil.UTF_8)); + request2.headers().set(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.TEXT_PLAIN); + + try { + assertTrue(channel.writeInbound(request2, content2, LastHttpContent.EMPTY_LAST_CONTENT)); + + // Getting the same response objects out + assertSame(request2, channel.readInbound()); + assertSame(content2, channel.readInbound()); + assertSame(LastHttpContent.EMPTY_LAST_CONTENT, channel.readInbound()); + } finally { + ReferenceCountUtil.release(request2); + ReferenceCountUtil.release(content2); + } + + assertFalse(channel.finish()); + } finally { + channel.close(); + } + } + + @Test + public void testSelectiveResponseAggregation() { + HttpObjectAggregator myTextAggregator = new HttpObjectAggregator(1024 * 1024) { + @Override + protected boolean isStartMessage(HttpObject msg) throws Exception { + if (msg instanceof HttpResponse) { + HttpResponse response = (HttpResponse) msg; + HttpHeaders headers = response.headers(); + + String contentType = headers.get(HttpHeaderNames.CONTENT_TYPE); + if (AsciiString.contentEqualsIgnoreCase(contentType, HttpHeaderValues.TEXT_PLAIN)) { + return true; + } + } + + return false; + } + }; + + EmbeddedChannel channel = new EmbeddedChannel(myTextAggregator); + + try { + // Aggregate: text/plain + HttpResponse response1 = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK); + HttpContent content1 = new DefaultHttpContent(Unpooled.copiedBuffer("Hello, World!", CharsetUtil.UTF_8)); + response1.headers().set(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.TEXT_PLAIN); + + assertTrue(channel.writeInbound(response1, content1, LastHttpContent.EMPTY_LAST_CONTENT)); + + // Getting an aggregated response out + Object msg1 = channel.readInbound(); + try { + assertTrue(msg1 instanceof FullHttpResponse); + } finally { + ReferenceCountUtil.release(msg1); + } + + // Don't aggregate: application/json + HttpResponse response2 = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK); + HttpContent content2 = new DefaultHttpContent(Unpooled.copiedBuffer("{key: 'value'}", CharsetUtil.UTF_8)); + response2.headers().set(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.APPLICATION_JSON); + + try { + assertTrue(channel.writeInbound(response2, content2, LastHttpContent.EMPTY_LAST_CONTENT)); + + // Getting the same response objects out + assertSame(response2, channel.readInbound()); + assertSame(content2, channel.readInbound()); + assertSame(LastHttpContent.EMPTY_LAST_CONTENT, channel.readInbound()); + } finally { + ReferenceCountUtil.release(response2); + ReferenceCountUtil.release(content2); + } + + assertFalse(channel.finish()); + } finally { + channel.close(); + } + } } diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/HttpRequestDecoderTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/HttpRequestDecoderTest.java index 4572063..4f83d82 100644 --- a/codec-http/src/test/java/io/netty/handler/codec/http/HttpRequestDecoderTest.java +++ b/codec-http/src/test/java/io/netty/handler/codec/http/HttpRequestDecoderTest.java @@ -81,7 +81,7 @@ public class HttpRequestDecoderTest { private static void testDecodeWholeRequestAtOnce(byte[] content) { EmbeddedChannel channel = new EmbeddedChannel(new HttpRequestDecoder()); - assertTrue(channel.writeInbound(Unpooled.wrappedBuffer(content))); + assertTrue(channel.writeInbound(Unpooled.copiedBuffer(content))); HttpRequest req = channel.readInbound(); assertNotNull(req); checkHeaders(req.headers()); @@ -145,14 +145,14 @@ public class HttpRequestDecoderTest { amount = headerLength - a; } - // if header is done it should produce a HttpRequest - channel.writeInbound(Unpooled.wrappedBuffer(content, a, amount)); + // if header is done it should produce an HttpRequest + channel.writeInbound(Unpooled.copiedBuffer(content, a, amount)); a += amount; } for (int i = CONTENT_LENGTH; i > 0; i --) { // Should produce HttpContent - channel.writeInbound(Unpooled.wrappedBuffer(content, content.length - i, 1)); + channel.writeInbound(Unpooled.copiedBuffer(content, content.length - i, 1)); } HttpRequest req = channel.readInbound(); @@ -308,6 +308,42 @@ public class HttpRequestDecoderTest { assertFalse(channel.finish()); } + @Test + public void testTooLargeInitialLineWithWSOnly() { + testTooLargeInitialLineWithControlCharsOnly(" "); + } + + @Test + public void testTooLargeInitialLineWithCRLFOnly() { + testTooLargeInitialLineWithControlCharsOnly("\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n"); + } + + private static void testTooLargeInitialLineWithControlCharsOnly(String controlChars) { + EmbeddedChannel channel = new EmbeddedChannel(new HttpRequestDecoder(15, 1024, 1024)); + String requestStr = controlChars + "GET / HTTP/1.1\r\n" + + "Host: localhost1\r\n\r\n"; + + assertTrue(channel.writeInbound(Unpooled.copiedBuffer(requestStr, CharsetUtil.US_ASCII))); + HttpRequest request = channel.readInbound(); + assertTrue(request.decoderResult().isFailure()); + assertTrue(request.decoderResult().cause() instanceof TooLongFrameException); + assertFalse(channel.finish()); + } + + @Test + public void testInitialLineWithLeadingControlChars() { + EmbeddedChannel channel = new EmbeddedChannel(new HttpRequestDecoder()); + String crlf = "\r\n"; + String request = crlf + "GET /some/path HTTP/1.1" + crlf + + "Host: localhost" + crlf + crlf; + assertTrue(channel.writeInbound(Unpooled.copiedBuffer(request, CharsetUtil.US_ASCII))); + HttpRequest req = channel.readInbound(); + assertEquals(HttpMethod.GET, req.method()); + assertEquals("/some/path", req.uri()); + assertEquals(HttpVersion.HTTP_1_1, req.protocolVersion()); + assertTrue(channel.finishAndReleaseAll()); + } + @Test public void testTooLargeHeaders() { EmbeddedChannel channel = new EmbeddedChannel(new HttpRequestDecoder(1024, 10, 1024)); @@ -320,4 +356,134 @@ public class HttpRequestDecoderTest { assertTrue(request.decoderResult().cause() instanceof TooLongFrameException); assertFalse(channel.finish()); } + + @Test + public void testWhitespace() { + String requestStr = "GET /some/path HTTP/1.1\r\n" + + "Transfer-Encoding : chunked\r\n" + + "Host: netty.io\r\n\r\n"; + testInvalidHeaders0(requestStr); + } + + @Test + public void testWhitespaceBeforeTransferEncoding01() { + String requestStr = "GET /some/path HTTP/1.1\r\n" + + " Transfer-Encoding : chunked\r\n" + + "Content-Length: 1\r\n" + + "Host: netty.io\r\n\r\n" + + "a"; + testInvalidHeaders0(requestStr); + } + + @Test + public void testWhitespaceBeforeTransferEncoding02() { + String requestStr = "POST / HTTP/1.1" + + " Transfer-Encoding : chunked\r\n" + + "Host: target.com" + + "Content-Length: 65\r\n\r\n" + + "0\r\n\r\n" + + "GET /maliciousRequest HTTP/1.1\r\n" + + "Host: evilServer.com\r\n" + + "Foo: x"; + testInvalidHeaders0(requestStr); + } + + @Test + public void testHeaderWithNoValueAndMissingColon() { + String requestStr = "GET /some/path HTTP/1.1\r\n" + + "Content-Length: 0\r\n" + + "Host:\r\n" + + "netty.io\r\n\r\n"; + testInvalidHeaders0(requestStr); + } + + @Test + public void testMultipleContentLengthHeaders() { + String requestStr = "GET /some/path HTTP/1.1\r\n" + + "Content-Length: 1\r\n" + + "Content-Length: 0\r\n\r\n" + + "b"; + testInvalidHeaders0(requestStr); + } + + @Test + public void testMultipleContentLengthHeaders2() { + String requestStr = "GET /some/path HTTP/1.1\r\n" + + "Content-Length: 1\r\n" + + "Connection: close\r\n" + + "Content-Length: 0\r\n\r\n" + + "b"; + testInvalidHeaders0(requestStr); + } + + @Test + public void testContentLengthHeaderWithCommaValue() { + String requestStr = "GET /some/path HTTP/1.1\r\n" + + "Content-Length: 1,1\r\n\r\n" + + "b"; + testInvalidHeaders0(requestStr); + } + + @Test + public void testMultipleContentLengthHeadersWithFolding() { + String requestStr = "POST / HTTP/1.1\r\n" + + "Host: example.com\r\n" + + "Connection: close\r\n" + + "Content-Length: 5\r\n" + + "Content-Length:\r\n" + + "\t6\r\n\r\n" + + "123456"; + testInvalidHeaders0(requestStr); + } + + @Test + public void testContentLengthAndTransferEncodingHeadersWithVerticalTab() { + testContentLengthAndTransferEncodingHeadersWithInvalidSeparator((char) 0x0b, false); + testContentLengthAndTransferEncodingHeadersWithInvalidSeparator((char) 0x0b, true); + } + + @Test + public void testContentLengthAndTransferEncodingHeadersWithCR() { + testContentLengthAndTransferEncodingHeadersWithInvalidSeparator((char) 0x0d, false); + testContentLengthAndTransferEncodingHeadersWithInvalidSeparator((char) 0x0d, true); + } + + private static void testContentLengthAndTransferEncodingHeadersWithInvalidSeparator( + char separator, boolean extraLine) { + String requestStr = "POST / HTTP/1.1\r\n" + + "Host: example.com\r\n" + + "Connection: close\r\n" + + "Content-Length: 9\r\n" + + "Transfer-Encoding:" + separator + "chunked\r\n\r\n" + + (extraLine ? "0\r\n\r\n" : "") + + "something\r\n\r\n"; + testInvalidHeaders0(requestStr); + } + + @Test + public void testContentLengthHeaderAndChunked() { + String requestStr = "POST / HTTP/1.1\r\n" + + "Host: example.com\r\n" + + "Connection: close\r\n" + + "Content-Length: 5\r\n" + + "Transfer-Encoding: chunked\r\n\r\n" + + "0\r\n\r\n"; + EmbeddedChannel channel = new EmbeddedChannel(new HttpRequestDecoder()); + assertTrue(channel.writeInbound(Unpooled.copiedBuffer(requestStr, CharsetUtil.US_ASCII))); + HttpRequest request = channel.readInbound(); + assertFalse(request.decoderResult().isFailure()); + assertTrue(request.headers().contains("Transfer-Encoding", "chunked", false)); + assertFalse(request.headers().contains("Content-Length")); + LastHttpContent c = channel.readInbound(); + assertFalse(channel.finish()); + } + + private static void testInvalidHeaders0(String requestStr) { + EmbeddedChannel channel = new EmbeddedChannel(new HttpRequestDecoder()); + assertTrue(channel.writeInbound(Unpooled.copiedBuffer(requestStr, CharsetUtil.US_ASCII))); + HttpRequest request = channel.readInbound(); + assertTrue(request.decoderResult().isFailure()); + assertTrue(request.decoderResult().cause() instanceof IllegalArgumentException); + assertFalse(channel.finish()); + } } diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/HttpResponseDecoderTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/HttpResponseDecoderTest.java index 017dbd5..b0ccd0c 100644 --- a/codec-http/src/test/java/io/netty/handler/codec/http/HttpResponseDecoderTest.java +++ b/codec-http/src/test/java/io/netty/handler/codec/http/HttpResponseDecoderTest.java @@ -50,7 +50,7 @@ public class HttpResponseDecoderTest { final int maxHeaderSize = 8192; final EmbeddedChannel ch = new EmbeddedChannel(new HttpResponseDecoder(4096, maxHeaderSize, 8192)); - final char[] bytes = new char[maxHeaderSize / 2 - 2]; + final char[] bytes = new char[maxHeaderSize / 2 - 4]; Arrays.fill(bytes, 'a'); ch.writeInbound(Unpooled.copiedBuffer("HTTP/1.1 200 OK\r\n", CharsetUtil.US_ASCII)); @@ -122,7 +122,7 @@ public class HttpResponseDecoderTest { for (int i = 0; i < 10; i++) { assertFalse(ch.writeInbound(Unpooled.copiedBuffer(Integer.toHexString(data.length) + "\r\n", CharsetUtil.US_ASCII))); - assertTrue(ch.writeInbound(Unpooled.wrappedBuffer(data))); + assertTrue(ch.writeInbound(Unpooled.copiedBuffer(data))); HttpContent content = ch.readInbound(); assertEquals(data.length, content.content().readableBytes()); @@ -164,7 +164,7 @@ public class HttpResponseDecoderTest { for (int i = 0; i < 10; i++) { assertFalse(ch.writeInbound(Unpooled.copiedBuffer(Integer.toHexString(data.length) + "\r\n", CharsetUtil.US_ASCII))); - assertTrue(ch.writeInbound(Unpooled.wrappedBuffer(data))); + assertTrue(ch.writeInbound(Unpooled.copiedBuffer(data))); byte[] decodedData = new byte[data.length]; HttpContent content = ch.readInbound(); @@ -446,13 +446,13 @@ public class HttpResponseDecoderTest { amount = headerLength - a; } - // if header is done it should produce a HttpRequest + // if header is done it should produce an HttpRequest boolean headerDone = a + amount == headerLength; - assertEquals(headerDone, ch.writeInbound(Unpooled.wrappedBuffer(content, a, amount))); + assertEquals(headerDone, ch.writeInbound(Unpooled.copiedBuffer(content, a, amount))); a += amount; } - ch.writeInbound(Unpooled.wrappedBuffer(content, headerLength, content.length - headerLength)); + ch.writeInbound(Unpooled.copiedBuffer(content, headerLength, content.length - headerLength)); HttpResponse res = ch.readInbound(); assertThat(res.protocolVersion(), sameInstance(HttpVersion.HTTP_1_1)); assertThat(res.status(), is(HttpResponseStatus.OK)); @@ -483,8 +483,8 @@ public class HttpResponseDecoderTest { for (int i = 0; i < data.length; i++) { data[i] = (byte) i; } - ch.writeInbound(Unpooled.wrappedBuffer(data, 0, data.length / 2)); - ch.writeInbound(Unpooled.wrappedBuffer(data, 5, data.length / 2)); + ch.writeInbound(Unpooled.copiedBuffer(data, 0, data.length / 2)); + ch.writeInbound(Unpooled.copiedBuffer(data, 5, data.length / 2)); HttpResponse res = ch.readInbound(); assertThat(res.protocolVersion(), sameInstance(HttpVersion.HTTP_1_1)); @@ -492,12 +492,12 @@ public class HttpResponseDecoderTest { HttpContent firstContent = ch.readInbound(); assertThat(firstContent.content().readableBytes(), is(5)); - assertEquals(Unpooled.wrappedBuffer(data, 0, 5), firstContent.content()); + assertEquals(Unpooled.copiedBuffer(data, 0, 5), firstContent.content()); firstContent.release(); LastHttpContent lastContent = ch.readInbound(); assertEquals(5, lastContent.content().readableBytes()); - assertEquals(Unpooled.wrappedBuffer(data, 5, 5), lastContent.content()); + assertEquals(Unpooled.copiedBuffer(data, 5, 5), lastContent.content()); lastContent.release(); assertThat(ch.finish(), is(false)); @@ -524,15 +524,15 @@ public class HttpResponseDecoderTest { amount = header.length - a; } - ch.writeInbound(Unpooled.wrappedBuffer(header, a, amount)); + ch.writeInbound(Unpooled.copiedBuffer(header, a, amount)); a += amount; } byte[] data = new byte[10]; for (int i = 0; i < data.length; i++) { data[i] = (byte) i; } - ch.writeInbound(Unpooled.wrappedBuffer(data, 0, data.length / 2)); - ch.writeInbound(Unpooled.wrappedBuffer(data, 5, data.length / 2)); + ch.writeInbound(Unpooled.copiedBuffer(data, 0, data.length / 2)); + ch.writeInbound(Unpooled.copiedBuffer(data, 5, data.length / 2)); HttpResponse res = ch.readInbound(); assertThat(res.protocolVersion(), sameInstance(HttpVersion.HTTP_1_1)); @@ -589,7 +589,7 @@ public class HttpResponseDecoderTest { byte[] otherData = {1, 2, 3, 4}; EmbeddedChannel ch = new EmbeddedChannel(new HttpResponseDecoder()); - ch.writeInbound(Unpooled.wrappedBuffer(data, otherData)); + ch.writeInbound(Unpooled.copiedBuffer(data, otherData)); HttpResponse res = ch.readInbound(); assertThat(res.protocolVersion(), sameInstance(HttpVersion.HTTP_1_1)); @@ -625,7 +625,7 @@ public class HttpResponseDecoderTest { EmbeddedChannel ch = new EmbeddedChannel(new HttpResponseDecoder()); - ch.writeInbound(Unpooled.wrappedBuffer(data)); + ch.writeInbound(Unpooled.copiedBuffer(data)); // Garbage input should generate the 999 Unknown response. HttpResponse res = ch.readInbound(); @@ -636,7 +636,7 @@ public class HttpResponseDecoderTest { assertThat(ch.readInbound(), is(nullValue())); // More garbage should not generate anything (i.e. the decoder discards anything beyond this point.) - ch.writeInbound(Unpooled.wrappedBuffer(data)); + ch.writeInbound(Unpooled.copiedBuffer(data)); assertThat(ch.readInbound(), is(nullValue())); // Closing the connection should not generate anything since the protocol has been violated. @@ -683,4 +683,48 @@ public class HttpResponseDecoderTest { assertThat(message.decoderResult().cause(), instanceOf(PrematureChannelClosureException.class)); assertNull(channel.readInbound()); } + + @Test + public void testTrailerWithEmptyLineInSeparateBuffer() { + HttpResponseDecoder decoder = new HttpResponseDecoder(); + EmbeddedChannel channel = new EmbeddedChannel(decoder); + + String headers = "HTTP/1.1 200 OK\r\n" + + "Transfer-Encoding: chunked\r\n" + + "Trailer: My-Trailer\r\n"; + assertFalse(channel.writeInbound(Unpooled.copiedBuffer(headers.getBytes(CharsetUtil.US_ASCII)))); + assertTrue(channel.writeInbound(Unpooled.copiedBuffer("\r\n".getBytes(CharsetUtil.US_ASCII)))); + + assertTrue(channel.writeInbound(Unpooled.copiedBuffer("0\r\n", CharsetUtil.US_ASCII))); + assertTrue(channel.writeInbound(Unpooled.copiedBuffer("My-Trailer: 42\r\n", CharsetUtil.US_ASCII))); + assertTrue(channel.writeInbound(Unpooled.copiedBuffer("\r\n", CharsetUtil.US_ASCII))); + + HttpResponse response = channel.readInbound(); + assertEquals(2, response.headers().size()); + assertEquals("chunked", response.headers().get(HttpHeaderNames.TRANSFER_ENCODING)); + assertEquals("My-Trailer", response.headers().get(HttpHeaderNames.TRAILER)); + + LastHttpContent lastContent = channel.readInbound(); + assertEquals(1, lastContent.trailingHeaders().size()); + assertEquals("42", lastContent.trailingHeaders().get("My-Trailer")); + assertEquals(0, lastContent.content().readableBytes()); + lastContent.release(); + + assertFalse(channel.finish()); + } + + @Test + public void testWhitespace() { + EmbeddedChannel channel = new EmbeddedChannel(new HttpResponseDecoder()); + String requestStr = "HTTP/1.1 200 OK\r\n" + + "Transfer-Encoding : chunked\r\n" + + "Host: netty.io\n\r\n"; + + assertTrue(channel.writeInbound(Unpooled.copiedBuffer(requestStr, CharsetUtil.US_ASCII))); + HttpResponse response = channel.readInbound(); + assertFalse(response.decoderResult().isFailure()); + assertEquals(HttpHeaderValues.CHUNKED.toString(), response.headers().get(HttpHeaderNames.TRANSFER_ENCODING)); + assertEquals("netty.io", response.headers().get(HttpHeaderNames.HOST)); + assertFalse(channel.finish()); + } } diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/HttpUtilTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/HttpUtilTest.java index 3159606..186b498 100644 --- a/codec-http/src/test/java/io/netty/handler/codec/http/HttpUtilTest.java +++ b/codec-http/src/test/java/io/netty/handler/codec/http/HttpUtilTest.java @@ -109,6 +109,7 @@ public class HttpUtilTest { public void testGetCharset_defaultValue() { final String SIMPLE_CONTENT_TYPE = "text/html"; final String CONTENT_TYPE_WITH_INCORRECT_CHARSET = "text/html; charset=UTFFF"; + final String CONTENT_TYPE_WITH_ILLEGAL_CHARSET_NAME = "text/html; charset=!illegal!"; HttpMessage message = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK); message.headers().set(HttpHeaderNames.CONTENT_TYPE, SIMPLE_CONTENT_TYPE); @@ -127,6 +128,15 @@ public class HttpUtilTest { assertEquals(CharsetUtil.UTF_8, HttpUtil.getCharset(message, StandardCharsets.UTF_8)); assertEquals(CharsetUtil.UTF_8, HttpUtil.getCharset(CONTENT_TYPE_WITH_INCORRECT_CHARSET, StandardCharsets.UTF_8)); + + message.headers().set(HttpHeaderNames.CONTENT_TYPE, CONTENT_TYPE_WITH_ILLEGAL_CHARSET_NAME); + assertEquals(CharsetUtil.ISO_8859_1, HttpUtil.getCharset(message)); + assertEquals(CharsetUtil.ISO_8859_1, HttpUtil.getCharset(CONTENT_TYPE_WITH_ILLEGAL_CHARSET_NAME)); + + message.headers().set(HttpHeaderNames.CONTENT_TYPE, CONTENT_TYPE_WITH_ILLEGAL_CHARSET_NAME); + assertEquals(CharsetUtil.UTF_8, HttpUtil.getCharset(message, StandardCharsets.UTF_8)); + assertEquals(CharsetUtil.UTF_8, + HttpUtil.getCharset(CONTENT_TYPE_WITH_ILLEGAL_CHARSET_NAME, StandardCharsets.UTF_8)); } @Test @@ -317,4 +327,25 @@ public class HttpUtilTest { "http:localhost/http_1_0"); assertFalse(HttpUtil.isKeepAlive(http10Message)); } + + @Test + public void testKeepAliveIfConnectionHeaderMultipleValues() { + HttpMessage http11Message = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, + "http:localhost/http_1_1"); + http11Message.headers().set( + HttpHeaderNames.CONNECTION, HttpHeaderValues.UPGRADE + ", " + HttpHeaderValues.CLOSE); + assertFalse(HttpUtil.isKeepAlive(http11Message)); + + http11Message.headers().set( + HttpHeaderNames.CONNECTION, HttpHeaderValues.UPGRADE + ", Close"); + assertFalse(HttpUtil.isKeepAlive(http11Message)); + + http11Message.headers().set( + HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE + ", " + HttpHeaderValues.UPGRADE); + assertFalse(HttpUtil.isKeepAlive(http11Message)); + + http11Message.headers().set( + HttpHeaderNames.CONNECTION, HttpHeaderValues.UPGRADE + ", " + HttpHeaderValues.KEEP_ALIVE); + assertTrue(HttpUtil.isKeepAlive(http11Message)); + } } diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/QueryStringDecoderTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/QueryStringDecoderTest.java index a0071c4..5937d66 100644 --- a/codec-http/src/test/java/io/netty/handler/codec/http/QueryStringDecoderTest.java +++ b/codec-http/src/test/java/io/netty/handler/codec/http/QueryStringDecoderTest.java @@ -129,6 +129,13 @@ public class QueryStringDecoderTest { assertQueryString("/foo?a=1&a=&a=", "/foo?a=1&a&a="); } + @Test + public void testSemicolon() { + assertQueryString("/foo?a=1;2", "/foo?a=1;2", false); + // ";" should be treated as a normal character, see #8855 + assertQueryString("/foo?a=1;2", "/foo?a=1%3B2", true); + } + @Test public void testPathSpecific() { // decode escaped characters @@ -225,8 +232,14 @@ public class QueryStringDecoderTest { } private static void assertQueryString(String expected, String actual) { - QueryStringDecoder ed = new QueryStringDecoder(expected, CharsetUtil.UTF_8); - QueryStringDecoder ad = new QueryStringDecoder(actual, CharsetUtil.UTF_8); + assertQueryString(expected, actual, false); + } + + private static void assertQueryString(String expected, String actual, boolean semicolonIsNormalChar) { + QueryStringDecoder ed = new QueryStringDecoder(expected, CharsetUtil.UTF_8, true, + 1024, semicolonIsNormalChar); + QueryStringDecoder ad = new QueryStringDecoder(actual, CharsetUtil.UTF_8, true, + 1024, semicolonIsNormalChar); Assert.assertEquals(ed.path(), ad.path()); Assert.assertEquals(ed.parameters(), ad.parameters()); } diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/QueryStringEncoderTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/QueryStringEncoderTest.java index a9f6f90..ba4d2b6 100644 --- a/codec-http/src/test/java/io/netty/handler/codec/http/QueryStringEncoderTest.java +++ b/codec-http/src/test/java/io/netty/handler/codec/http/QueryStringEncoderTest.java @@ -15,12 +15,12 @@ */ package io.netty.handler.codec.http; -import java.net.URI; -import java.nio.charset.Charset; - import org.junit.Assert; import org.junit.Test; +import java.net.URI; +import java.nio.charset.Charset; + public class QueryStringEncoderTest { @Test @@ -37,6 +37,11 @@ public class QueryStringEncoderTest { Assert.assertEquals("/foo/\u00A5?a=%C2%A5", e.toString()); Assert.assertEquals(new URI("/foo/\u00A5?a=%C2%A5"), e.toUri()); + e = new QueryStringEncoder("/foo/\u00A5"); + e.addParam("a", "abc\u00A5"); + Assert.assertEquals("/foo/\u00A5?a=abc%C2%A5", e.toString()); + Assert.assertEquals(new URI("/foo/\u00A5?a=abc%C2%A5"), e.toUri()); + e = new QueryStringEncoder("/foo"); e.addParam("a", "1"); e.addParam("b", "2"); diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/cookie/ClientCookieDecoderTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/cookie/ClientCookieDecoderTest.java index 2cd69f9..24e3361 100644 --- a/codec-http/src/test/java/io/netty/handler/codec/http/cookie/ClientCookieDecoderTest.java +++ b/codec-http/src/test/java/io/netty/handler/codec/http/cookie/ClientCookieDecoderTest.java @@ -16,6 +16,7 @@ package io.netty.handler.codec.http.cookie; import io.netty.handler.codec.DateFormatter; +import io.netty.handler.codec.http.cookie.CookieHeaderNames.SameSite; import org.junit.Test; import java.util.ArrayList; @@ -25,6 +26,8 @@ import java.util.Date; import java.util.Iterator; import java.util.TimeZone; +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.*; public class ClientCookieDecoderTest { @@ -32,7 +35,7 @@ public class ClientCookieDecoderTest { public void testDecodingSingleCookieV0() { String cookieString = "myCookie=myValue;expires=" + DateFormatter.format(new Date(System.currentTimeMillis() + 50000)) - + ";path=/apathsomewhere;domain=.adomainsomewhere;secure;"; + + ";path=/apathsomewhere;domain=.adomainsomewhere;secure;SameSite=None"; Cookie cookie = ClientCookieDecoder.STRICT.decode(cookieString); assertNotNull(cookie); @@ -44,6 +47,9 @@ public class ClientCookieDecoderTest { cookie.maxAge() >= 40 && cookie.maxAge() <= 60); assertEquals("/apathsomewhere", cookie.path()); assertTrue(cookie.isSecure()); + + assertThat(cookie, is(instanceOf(DefaultCookie.class))); + assertEquals(SameSite.None, ((DefaultCookie) cookie).sameSite()); } @Test @@ -259,7 +265,7 @@ public class ClientCookieDecoderTest { "'=KqtH"; Cookie cookie = ClientCookieDecoder.STRICT.decode("bh=\"" + longValue - + "\";"); + + "\";"); assertEquals("bh", cookie.name()); assertEquals(longValue, cookie.value()); } diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/cookie/ClientCookieEncoderTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/cookie/ClientCookieEncoderTest.java index af81059..8fbe544 100644 --- a/codec-http/src/test/java/io/netty/handler/codec/http/cookie/ClientCookieEncoderTest.java +++ b/codec-http/src/test/java/io/netty/handler/codec/http/cookie/ClientCookieEncoderTest.java @@ -51,4 +51,16 @@ public class ClientCookieEncoderTest { public void testRejectCookieValueWithSemicolon() { ClientCookieEncoder.STRICT.encode(new DefaultCookie("myCookie", "foo;bar")); } + + @Test + public void testComparatorForSamePathLength() { + Cookie cookie = new DefaultCookie("test", "value"); + cookie.setPath("1"); + + Cookie cookie2 = new DefaultCookie("test", "value"); + cookie2.setPath("2"); + + assertEquals(0, ClientCookieEncoder.COOKIE_COMPARATOR.compare(cookie, cookie2)); + assertEquals(0, ClientCookieEncoder.COOKIE_COMPARATOR.compare(cookie2, cookie)); + } } diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/cookie/ServerCookieDecoderTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/cookie/ServerCookieDecoderTest.java index b157bc3..b6b3365 100644 --- a/codec-http/src/test/java/io/netty/handler/codec/http/cookie/ServerCookieDecoderTest.java +++ b/codec-http/src/test/java/io/netty/handler/codec/http/cookie/ServerCookieDecoderTest.java @@ -15,6 +15,7 @@ */ package io.netty.handler.codec.http.cookie; +import java.util.List; import org.junit.Test; import java.util.Iterator; @@ -53,6 +54,26 @@ public class ServerCookieDecoderTest { assertEquals("myValue3", cookie.value()); } + @Test + public void testDecodingAllMultipleCookies() { + String c1 = "myCookie=myValue;"; + String c2 = "myCookie=myValue2;"; + String c3 = "myCookie=myValue3;"; + + List<Cookie> cookies = ServerCookieDecoder.STRICT.decodeAll(c1 + c2 + c3); + assertEquals(3, cookies.size()); + Iterator<Cookie> it = cookies.iterator(); + Cookie cookie = it.next(); + assertNotNull(cookie); + assertEquals("myValue", cookie.value()); + cookie = it.next(); + assertNotNull(cookie); + assertEquals("myValue2", cookie.value()); + cookie = it.next(); + assertNotNull(cookie); + assertEquals("myValue3", cookie.value()); + } + @Test public void testDecodingGoogleAnalyticsCookie() { String source = diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/cookie/ServerCookieEncoderTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/cookie/ServerCookieEncoderTest.java index 82f813f..42f9c21 100644 --- a/codec-http/src/test/java/io/netty/handler/codec/http/cookie/ServerCookieEncoderTest.java +++ b/codec-http/src/test/java/io/netty/handler/codec/http/cookie/ServerCookieEncoderTest.java @@ -32,6 +32,7 @@ import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; +import io.netty.handler.codec.http.cookie.CookieHeaderNames.SameSite; import org.junit.Test; public class ServerCookieEncoderTest { @@ -41,13 +42,14 @@ public class ServerCookieEncoderTest { int maxAge = 50; - String result = - "myCookie=myValue; Max-Age=50; Expires=(.+?); Path=/apathsomewhere; Domain=.adomainsomewhere; Secure"; - Cookie cookie = new DefaultCookie("myCookie", "myValue"); + String result = "myCookie=myValue; Max-Age=50; Expires=(.+?); Path=/apathsomewhere;" + + " Domain=.adomainsomewhere; Secure; SameSite=Lax"; + DefaultCookie cookie = new DefaultCookie("myCookie", "myValue"); cookie.setDomain(".adomainsomewhere"); cookie.setMaxAge(maxAge); cookie.setPath("/apathsomewhere"); cookie.setSecure(true); + cookie.setSameSite(SameSite.Lax); String encodedCookie = ServerCookieEncoder.STRICT.encode(cookie); diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/cors/CorsHandlerTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/cors/CorsHandlerTest.java index c4975e3..12685df 100644 --- a/codec-http/src/test/java/io/netty/handler/codec/http/cors/CorsHandlerTest.java +++ b/codec-http/src/test/java/io/netty/handler/codec/http/cors/CorsHandlerTest.java @@ -53,6 +53,7 @@ public class CorsHandlerTest { public void nonCorsRequest() { final HttpResponse response = simpleRequest(forAnyOrigin().build(), null); assertThat(response.headers().contains(ACCESS_CONTROL_ALLOW_ORIGIN), is(false)); + assertThat(ReferenceCountUtil.release(response), is(true)); } @Test @@ -60,6 +61,7 @@ public class CorsHandlerTest { final HttpResponse response = simpleRequest(forAnyOrigin().build(), "http://localhost:7777"); assertThat(response.headers().get(ACCESS_CONTROL_ALLOW_ORIGIN), is("*")); assertThat(response.headers().get(ACCESS_CONTROL_ALLOW_HEADERS), is(nullValue())); + assertThat(ReferenceCountUtil.release(response), is(true)); } @Test @@ -70,6 +72,7 @@ public class CorsHandlerTest { assertThat(response.headers().get(ACCESS_CONTROL_ALLOW_ORIGIN), is("null")); assertThat(response.headers().get(ACCESS_CONTROL_ALLOW_CREDENTIALS), is(equalTo("true"))); assertThat(response.headers().get(ACCESS_CONTROL_ALLOW_HEADERS), is(nullValue())); + assertThat(ReferenceCountUtil.release(response), is(true)); } @Test @@ -78,6 +81,7 @@ public class CorsHandlerTest { final HttpResponse response = simpleRequest(forOrigin(origin).build(), origin); assertThat(response.headers().get(ACCESS_CONTROL_ALLOW_ORIGIN), is(origin)); assertThat(response.headers().get(ACCESS_CONTROL_ALLOW_HEADERS), is(nullValue())); + assertThat(ReferenceCountUtil.release(response), is(true)); } @Test @@ -88,9 +92,12 @@ public class CorsHandlerTest { final HttpResponse response1 = simpleRequest(forOrigins(origins).build(), origin1); assertThat(response1.headers().get(ACCESS_CONTROL_ALLOW_ORIGIN), is(origin1)); assertThat(response1.headers().get(ACCESS_CONTROL_ALLOW_HEADERS), is(nullValue())); + assertThat(ReferenceCountUtil.release(response1), is(true)); + final HttpResponse response2 = simpleRequest(forOrigins(origins).build(), origin2); assertThat(response2.headers().get(ACCESS_CONTROL_ALLOW_ORIGIN), is(origin2)); assertThat(response2.headers().get(ACCESS_CONTROL_ALLOW_HEADERS), is(nullValue())); + assertThat(ReferenceCountUtil.release(response2), is(true)); } @Test @@ -100,6 +107,7 @@ public class CorsHandlerTest { forOrigins("https://localhost:8888").build(), origin); assertThat(response.headers().get(ACCESS_CONTROL_ALLOW_ORIGIN), is(nullValue())); assertThat(response.headers().get(ACCESS_CONTROL_ALLOW_HEADERS), is(nullValue())); + assertThat(ReferenceCountUtil.release(response), is(true)); } @Test @@ -112,6 +120,7 @@ public class CorsHandlerTest { assertThat(response.headers().get(ACCESS_CONTROL_ALLOW_METHODS), containsString("GET")); assertThat(response.headers().get(ACCESS_CONTROL_ALLOW_METHODS), containsString("DELETE")); assertThat(response.headers().get(VARY), equalTo(ORIGIN.toString())); + assertThat(ReferenceCountUtil.release(response), is(true)); } @Test @@ -127,6 +136,7 @@ public class CorsHandlerTest { assertThat(response.headers().get(ACCESS_CONTROL_ALLOW_HEADERS), containsString("content-type")); assertThat(response.headers().get(ACCESS_CONTROL_ALLOW_HEADERS), containsString("xheader1")); assertThat(response.headers().get(VARY), equalTo(ORIGIN.toString())); + assertThat(ReferenceCountUtil.release(response), is(true)); } @Test @@ -136,6 +146,7 @@ public class CorsHandlerTest { assertThat(response.headers().get(CONTENT_LENGTH), is("0")); assertThat(response.headers().get(DATE), is(notNullValue())); assertThat(response.headers().get(VARY), equalTo(ORIGIN.toString())); + assertThat(ReferenceCountUtil.release(response), is(true)); } @Test @@ -147,6 +158,7 @@ public class CorsHandlerTest { assertThat(response.headers().get(of("CustomHeader")), equalTo("somevalue")); assertThat(response.headers().get(VARY), equalTo(ORIGIN.toString())); assertThat(response.headers().get(CONTENT_LENGTH), is("0")); + assertThat(ReferenceCountUtil.release(response), is(true)); } @Test @@ -155,6 +167,7 @@ public class CorsHandlerTest { final CorsConfig config = forOrigin("http://localhost").build(); final HttpResponse response = preflightRequest(config, origin, "xheader1"); assertThat(response.headers().contains(ACCESS_CONTROL_ALLOW_ORIGIN), is(false)); + assertThat(ReferenceCountUtil.release(response), is(true)); } @Test @@ -168,6 +181,7 @@ public class CorsHandlerTest { final HttpResponse response = preflightRequest(config, "http://localhost:8888", "content-type, xheader1"); assertValues(response, headerName, value1, value2); assertThat(response.headers().get(VARY), equalTo(ORIGIN.toString())); + assertThat(ReferenceCountUtil.release(response), is(true)); } @Test @@ -181,6 +195,7 @@ public class CorsHandlerTest { final HttpResponse response = preflightRequest(config, "http://localhost:8888", "content-type, xheader1"); assertValues(response, headerName, value1, value2); assertThat(response.headers().get(VARY), equalTo(ORIGIN.toString())); + assertThat(ReferenceCountUtil.release(response), is(true)); } @Test @@ -195,6 +210,7 @@ public class CorsHandlerTest { final HttpResponse response = preflightRequest(config, "http://localhost:8888", "content-type, xheader1"); assertThat(response.headers().get(of("GenHeader")), equalTo("generatedValue")); assertThat(response.headers().get(VARY), equalTo(ORIGIN.toString())); + assertThat(ReferenceCountUtil.release(response), is(true)); } @Test @@ -207,6 +223,7 @@ public class CorsHandlerTest { final HttpResponse response = preflightRequest(config, origin, "content-type, xheader1"); assertThat(response.headers().get(ACCESS_CONTROL_ALLOW_ORIGIN), is(equalTo("null"))); assertThat(response.headers().get(ACCESS_CONTROL_ALLOW_CREDENTIALS), is(equalTo("true"))); + assertThat(ReferenceCountUtil.release(response), is(true)); } @Test @@ -215,6 +232,7 @@ public class CorsHandlerTest { final CorsConfig config = forOrigin(origin).allowCredentials().build(); final HttpResponse response = preflightRequest(config, origin, "content-type, xheader1"); assertThat(response.headers().get(ACCESS_CONTROL_ALLOW_CREDENTIALS), is(equalTo("true"))); + assertThat(ReferenceCountUtil.release(response), is(true)); } @Test @@ -223,6 +241,7 @@ public class CorsHandlerTest { final HttpResponse response = preflightRequest(config, "http://localhost:8888", ""); // the only valid value for Access-Control-Allow-Credentials is true. assertThat(response.headers().contains(ACCESS_CONTROL_ALLOW_CREDENTIALS), is(false)); + assertThat(ReferenceCountUtil.release(response), is(true)); } @Test @@ -232,6 +251,7 @@ public class CorsHandlerTest { assertThat(response.headers().get(ACCESS_CONTROL_ALLOW_ORIGIN), equalTo("*")); assertThat(response.headers().get(ACCESS_CONTROL_EXPOSE_HEADERS), containsString("custom1")); assertThat(response.headers().get(ACCESS_CONTROL_EXPOSE_HEADERS), containsString("custom2")); + assertThat(ReferenceCountUtil.release(response), is(true)); } @Test @@ -239,6 +259,7 @@ public class CorsHandlerTest { final CorsConfig config = forAnyOrigin().allowCredentials().build(); final HttpResponse response = simpleRequest(config, "http://localhost:7777"); assertThat(response.headers().get(ACCESS_CONTROL_ALLOW_CREDENTIALS), equalTo("true")); + assertThat(ReferenceCountUtil.release(response), is(true)); } @Test @@ -246,6 +267,7 @@ public class CorsHandlerTest { final CorsConfig config = forAnyOrigin().build(); final HttpResponse response = simpleRequest(config, "http://localhost:7777"); assertThat(response.headers().contains(ACCESS_CONTROL_ALLOW_CREDENTIALS), is(false)); + assertThat(ReferenceCountUtil.release(response), is(true)); } @Test @@ -255,6 +277,7 @@ public class CorsHandlerTest { assertThat(response.headers().get(ACCESS_CONTROL_ALLOW_CREDENTIALS), equalTo("true")); assertThat(response.headers().get(ACCESS_CONTROL_ALLOW_ORIGIN), equalTo("http://localhost:7777")); assertThat(response.headers().get(VARY), equalTo(ORIGIN.toString())); + assertThat(ReferenceCountUtil.release(response), is(true)); } @Test @@ -263,6 +286,7 @@ public class CorsHandlerTest { final HttpResponse response = simpleRequest(config, "http://localhost:7777"); assertThat(response.headers().get(ACCESS_CONTROL_EXPOSE_HEADERS), containsString("one")); assertThat(response.headers().get(ACCESS_CONTROL_EXPOSE_HEADERS), containsString("two")); + assertThat(ReferenceCountUtil.release(response), is(true)); } @Test @@ -271,6 +295,7 @@ public class CorsHandlerTest { final HttpResponse response = simpleRequest(config, "http://localhost:7777"); assertThat(response.status(), is(FORBIDDEN)); assertThat(response.headers().get(CONTENT_LENGTH), is("0")); + assertThat(ReferenceCountUtil.release(response), is(true)); } @Test @@ -279,6 +304,7 @@ public class CorsHandlerTest { final HttpResponse response = simpleRequest(config, "http://localhost:7777"); assertThat(response.status(), is(OK)); assertThat(response.headers().get(ACCESS_CONTROL_ALLOW_ORIGIN), is(nullValue())); + assertThat(ReferenceCountUtil.release(response), is(true)); } @Test @@ -287,6 +313,7 @@ public class CorsHandlerTest { final HttpResponse response = simpleRequest(config, null); assertThat(response.status(), is(OK)); assertThat(response.headers().get(ACCESS_CONTROL_ALLOW_ORIGIN), is(nullValue())); + assertThat(ReferenceCountUtil.release(response), is(true)); } @Test @@ -478,7 +505,9 @@ public class CorsHandlerTest { httpRequest.headers().set(ACCESS_CONTROL_REQUEST_HEADERS, requestHeaders); } assertThat(channel.writeInbound(httpRequest), is(false)); - return (HttpResponse) channel.readOutbound(); + HttpResponse response = channel.readOutbound(); + assertThat(channel.finish(), is(false)); + return response; } private static HttpResponse preflightRequest(final CorsConfig config, diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/multipart/HttpPostRequestDecoderTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/multipart/HttpPostRequestDecoderTest.java index f8b756c..40771e0 100644 --- a/codec-http/src/test/java/io/netty/handler/codec/http/multipart/HttpPostRequestDecoderTest.java +++ b/codec-http/src/test/java/io/netty/handler/codec/http/multipart/HttpPostRequestDecoderTest.java @@ -24,10 +24,12 @@ import io.netty.handler.codec.http.DefaultFullHttpRequest; import io.netty.handler.codec.http.DefaultHttpContent; import io.netty.handler.codec.http.DefaultHttpRequest; import io.netty.handler.codec.http.DefaultLastHttpContent; +import io.netty.handler.codec.http.FullHttpRequest; import io.netty.handler.codec.http.HttpContent; import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpHeaderValues; import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpVersion; import io.netty.handler.codec.http.LastHttpContent; import io.netty.util.CharsetUtil; @@ -413,6 +415,39 @@ public class HttpPostRequestDecoderTest { decoder.destroy(); } + @Test + public void testDecodeOtherMimeHeaderFields() throws Exception { + final String boundary = "74e78d11b0214bdcbc2f86491eeb4902"; + String filecontent = "123456"; + + final String body = "--" + boundary + "\r\n" + + "Content-Disposition: form-data; name=\"file\"; filename=" + "\"" + "attached.txt" + "\"" + + "\r\n" + + "Content-Type: application/octet-stream" + "\r\n" + + "Content-Encoding: gzip" + "\r\n" + + "\r\n" + + filecontent + + "\r\n" + + "--" + boundary + "--"; + + final DefaultFullHttpRequest req = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, + HttpMethod.POST, + "http://localhost", + Unpooled.wrappedBuffer(body.getBytes())); + req.headers().add(HttpHeaderNames.CONTENT_TYPE, "multipart/form-data; boundary=" + boundary); + req.headers().add(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.CHUNKED); + final DefaultHttpDataFactory inMemoryFactory = new DefaultHttpDataFactory(false); + final HttpPostRequestDecoder decoder = new HttpPostRequestDecoder(inMemoryFactory, req); + assertFalse(decoder.getBodyHttpDatas().isEmpty()); + InterfaceHttpData part1 = decoder.getBodyHttpDatas().get(0); + assertTrue("the item should be a FileUpload", part1 instanceof FileUpload); + FileUpload fileUpload = (FileUpload) part1; + byte[] fileBytes = fileUpload.get(); + assertTrue("the filecontent should not be decoded", filecontent.equals(new String(fileBytes))); + decoder.destroy(); + req.release(); + } + @Test public void testMultipartRequestWithFileInvalidCharset() throws Exception { final String boundary = "dLV9Wyq26L_-JQxk6ferf-RT153LhOO"; @@ -656,4 +691,116 @@ public class HttpPostRequestDecoderTest { assertEquals("tmp-0.txt", fileUpload.getFilename()); decoder.destroy(); } + + // https://github.com/netty/netty/issues/8575 + @Test + public void testMultipartRequest() throws Exception { + String BOUNDARY = "01f136d9282f"; + + ByteBuf byteBuf = Unpooled.wrappedBuffer(("--" + BOUNDARY + "\n" + + "Content-Disposition: form-data; name=\"msg_id\"\n" + + "\n" + + "15200\n" + + "--" + BOUNDARY + "\n" + + "Content-Disposition: form-data; name=\"msg\"\n" + + "\n" + + "test message\n" + + "--" + BOUNDARY + "--").getBytes()); + + FullHttpRequest req = new DefaultFullHttpRequest(HttpVersion.HTTP_1_0, HttpMethod.POST, "/up", byteBuf); + req.headers().add(HttpHeaderNames.CONTENT_TYPE, "multipart/form-data; boundary=" + BOUNDARY); + + HttpPostRequestDecoder decoder = + new HttpPostRequestDecoder(new DefaultHttpDataFactory(DefaultHttpDataFactory.MINSIZE), + req, + CharsetUtil.UTF_8); + + assertTrue(decoder.isMultipart()); + assertFalse(decoder.getBodyHttpDatas().isEmpty()); + assertEquals(2, decoder.getBodyHttpDatas().size()); + assertEquals("test message", ((Attribute) decoder.getBodyHttpData("msg")).getValue()); + assertEquals("15200", ((Attribute) decoder.getBodyHttpData("msg_id")).getValue()); + + decoder.destroy(); + assertEquals(1, req.refCnt()); + } + + @Test(expected = HttpPostRequestDecoder.ErrorDataDecoderException.class) + public void testNotLeak() { + FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, "/", + Unpooled.copiedBuffer("a=1&&b=2", CharsetUtil.US_ASCII)); + try { + new HttpPostStandardRequestDecoder(request); + } finally { + assertTrue(request.release()); + } + } + + @Test(expected = HttpPostRequestDecoder.ErrorDataDecoderException.class) + public void testNotLeakDirectBufferWhenWrapIllegalArgumentException() { + testNotLeakWhenWrapIllegalArgumentException(Unpooled.directBuffer()); + } + + @Test(expected = HttpPostRequestDecoder.ErrorDataDecoderException.class) + public void testNotLeakHeapBufferWhenWrapIllegalArgumentException() { + testNotLeakWhenWrapIllegalArgumentException(Unpooled.buffer()); + } + + private static void testNotLeakWhenWrapIllegalArgumentException(ByteBuf buf) { + buf.writeCharSequence("==", CharsetUtil.US_ASCII); + FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, "/", buf); + try { + new HttpPostStandardRequestDecoder(request); + } finally { + assertTrue(request.release()); + } + } + + @Test + public void testMultipartFormDataContentType() { + HttpRequest request = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, "/"); + assertFalse(HttpPostRequestDecoder.isMultipart(request)); + + String multipartDataValue = HttpHeaderValues.MULTIPART_FORM_DATA + ";" + "boundary=gc0p4Jq0M2Yt08jU534c0p"; + request.headers().set(HttpHeaderNames.CONTENT_TYPE, ";" + multipartDataValue); + assertFalse(HttpPostRequestDecoder.isMultipart(request)); + + request.headers().set(HttpHeaderNames.CONTENT_TYPE, multipartDataValue); + assertTrue(HttpPostRequestDecoder.isMultipart(request)); + } + + // see https://github.com/netty/netty/issues/10087 + @Test + public void testDecodeWithLanguageContentDispositionFieldParametersForFix() throws Exception { + + final String boundary = "952178786863262625034234"; + + String encoding = "UTF-8"; + String filename = "测试test.txt"; + String filenameEncoded = URLEncoder.encode(filename, encoding); + + final String body = "--" + boundary + "\r\n" + + "Content-Disposition: form-data; name=\"file\"; filename*=\"" + + encoding + "''" + filenameEncoded + "\"\r\n" + + "\r\n" + + "foo\r\n" + + "\r\n" + + "--" + boundary + "--"; + + final DefaultFullHttpRequest req = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, + HttpMethod.POST, + "http://localhost", + Unpooled.wrappedBuffer(body.getBytes())); + + req.headers().add(HttpHeaderNames.CONTENT_TYPE, "multipart/form-data; boundary=" + boundary); + final DefaultHttpDataFactory inMemoryFactory = new DefaultHttpDataFactory(false); + final HttpPostRequestDecoder decoder = new HttpPostRequestDecoder(inMemoryFactory, req); + assertFalse(decoder.getBodyHttpDatas().isEmpty()); + InterfaceHttpData part1 = decoder.getBodyHttpDatas().get(0); + assertTrue("the item should be a FileUpload", part1 instanceof FileUpload); + FileUpload fileUpload = (FileUpload) part1; + assertEquals("the filename should be decoded", filename, fileUpload.getFilename()); + decoder.destroy(); + req.release(); + } } diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/multipart/HttpPostRequestEncoderTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/multipart/HttpPostRequestEncoderTest.java index 7669f97..389fffa 100755 --- a/codec-http/src/test/java/io/netty/handler/codec/http/multipart/HttpPostRequestEncoderTest.java +++ b/codec-http/src/test/java/io/netty/handler/codec/http/multipart/HttpPostRequestEncoderTest.java @@ -18,10 +18,12 @@ package io.netty.handler.codec.http.multipart; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.Unpooled; +import io.netty.handler.codec.http.DefaultHttpRequest; import io.netty.handler.codec.http.DefaultFullHttpRequest; import io.netty.handler.codec.http.HttpConstants; import io.netty.handler.codec.http.HttpContent; import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpVersion; import io.netty.handler.codec.http.LastHttpContent; import io.netty.handler.codec.http.multipart.HttpPostRequestEncoder.EncoderMode; @@ -32,7 +34,6 @@ import org.junit.Test; import java.io.ByteArrayInputStream; import java.io.File; -import java.nio.charset.Charset; import java.util.Arrays; import java.util.List; @@ -139,9 +140,11 @@ public class HttpPostRequestEncoderTest { HttpPostRequestEncoder encoder = new HttpPostRequestEncoder(request, true); File file1 = new File(getClass().getResource("/file-01.txt").toURI()); File file2 = new File(getClass().getResource("/file-02.txt").toURI()); + File file3 = new File(getClass().getResource("/file-03.txt").toURI()); encoder.addBodyAttribute("foo", "bar"); encoder.addBodyFileUpload("quux", file1, "text/plain", false); encoder.addBodyFileUpload("quux", file2, "text/plain", false); + encoder.addBodyFileUpload("quux", file3, "text/plain", false); // We have to query the value of these two fields before finalizing // the request, which unsets one of them. @@ -160,7 +163,7 @@ public class HttpPostRequestEncoderTest { CONTENT_TYPE + ": multipart/mixed; boundary=" + multipartMixedBoundary + "\r\n" + "\r\n" + "--" + multipartMixedBoundary + "\r\n" + - CONTENT_DISPOSITION + ": attachment; filename=\"file-02.txt\"" + "\r\n" + + CONTENT_DISPOSITION + ": attachment; filename=\"file-01.txt\"" + "\r\n" + CONTENT_LENGTH + ": " + file1.length() + "\r\n" + CONTENT_TYPE + ": text/plain" + "\r\n" + CONTENT_TRANSFER_ENCODING + ": binary" + "\r\n" + @@ -175,6 +178,14 @@ public class HttpPostRequestEncoderTest { "\r\n" + "File 02" + StringUtil.NEWLINE + "\r\n" + + "--" + multipartMixedBoundary + "\r\n" + + CONTENT_DISPOSITION + ": attachment; filename=\"file-03.txt\"" + "\r\n" + + CONTENT_LENGTH + ": " + file3.length() + "\r\n" + + CONTENT_TYPE + ": text/plain" + "\r\n" + + CONTENT_TRANSFER_ENCODING + ": binary" + "\r\n" + + "\r\n" + + "File 03" + StringUtil.NEWLINE + + "\r\n" + "--" + multipartMixedBoundary + "--" + "\r\n" + "--" + multipartDataBoundary + "--" + "\r\n"; @@ -434,4 +445,27 @@ public class HttpPostRequestEncoderTest { + readable, expectedSize); httpContent.release(); } + + @Test + public void testEncodeChunkedContent() throws Exception { + HttpRequest req = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, "/"); + HttpPostRequestEncoder encoder = new HttpPostRequestEncoder(req, false); + + int length = 8077 + 8096; + char[] array = new char[length]; + Arrays.fill(array, 'a'); + String longText = new String(array); + + encoder.addBodyAttribute("data", longText); + encoder.addBodyAttribute("moreData", "abcd"); + + assertNotNull(encoder.finalizeRequest()); + + while (!encoder.isEndOfInput()) { + encoder.readChunk((ByteBufAllocator) null).release(); + } + + assertTrue(encoder.isEndOfInput()); + encoder.cleanFiles(); + } } diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocket08EncoderDecoderTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocket08EncoderDecoderTest.java index a84a81a..543867d 100644 --- a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocket08EncoderDecoderTest.java +++ b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocket08EncoderDecoderTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 The Netty Project + * Copyright 2019 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 @@ -53,6 +53,72 @@ public class WebSocket08EncoderDecoderTest { strTestData = s.toString(); } + @Test + public void testWebSocketProtocolViolation() { + // Given + initTestData(); + + int maxPayloadLength = 255; + String errorMessage = "Max frame length of " + maxPayloadLength + " has been exceeded."; + WebSocketCloseStatus expectedStatus = WebSocketCloseStatus.MESSAGE_TOO_BIG; + + // With auto-close + WebSocketDecoderConfig config = WebSocketDecoderConfig.newBuilder() + .maxFramePayloadLength(maxPayloadLength) + .closeOnProtocolViolation(true) + .build(); + EmbeddedChannel inChannel = new EmbeddedChannel(new WebSocket08FrameDecoder(config)); + EmbeddedChannel outChannel = new EmbeddedChannel(new WebSocket08FrameEncoder(true)); + + executeProtocolViolationTest(outChannel, inChannel, maxPayloadLength + 1, expectedStatus, errorMessage); + + CloseWebSocketFrame response = inChannel.readOutbound(); + Assert.assertNotNull(response); + Assert.assertEquals(expectedStatus.code(), response.statusCode()); + Assert.assertEquals(errorMessage, response.reasonText()); + response.release(); + + Assert.assertFalse(inChannel.finish()); + Assert.assertFalse(outChannel.finish()); + + // Without auto-close + config = WebSocketDecoderConfig.newBuilder() + .maxFramePayloadLength(maxPayloadLength) + .closeOnProtocolViolation(false) + .build(); + inChannel = new EmbeddedChannel(new WebSocket08FrameDecoder(config)); + outChannel = new EmbeddedChannel(new WebSocket08FrameEncoder(true)); + + executeProtocolViolationTest(outChannel, inChannel, maxPayloadLength + 1, expectedStatus, errorMessage); + + response = inChannel.readOutbound(); + Assert.assertNull(response); + + Assert.assertFalse(inChannel.finish()); + Assert.assertFalse(outChannel.finish()); + + // Release test data + binTestData.release(); + } + + private void executeProtocolViolationTest(EmbeddedChannel outChannel, EmbeddedChannel inChannel, + int testDataLength, WebSocketCloseStatus expectedStatus, String errorMessage) { + CorruptedWebSocketFrameException corrupted = null; + + try { + testBinaryWithLen(outChannel, inChannel, testDataLength); + } catch (CorruptedWebSocketFrameException e) { + corrupted = e; + } + + BinaryWebSocketFrame exceedingFrame = inChannel.readInbound(); + Assert.assertNull(exceedingFrame); + + Assert.assertNotNull(corrupted); + Assert.assertEquals(expectedStatus, corrupted.closeStatus()); + Assert.assertEquals(errorMessage, corrupted.getMessage()); + } + @Test public void testWebSocketEncodingAndDecoding() { initTestData(); @@ -108,16 +174,7 @@ public class WebSocket08EncoderDecoderTest { String testStr = strTestData.substring(0, testDataLength); outChannel.writeOutbound(new TextWebSocketFrame(testStr)); - // Transfer encoded data into decoder - // Loop because there might be multiple frames (gathering write) - while (true) { - ByteBuf encoded = outChannel.readOutbound(); - if (encoded != null) { - inChannel.writeInbound(encoded); - } else { - break; - } - } + transfer(outChannel, inChannel); Object decoded = inChannel.readInbound(); Assert.assertNotNull(decoded); @@ -132,16 +189,7 @@ public class WebSocket08EncoderDecoderTest { binTestData.setIndex(0, testDataLength); // Send only len bytes outChannel.writeOutbound(new BinaryWebSocketFrame(binTestData)); - // Transfer encoded data into decoder - // Loop because there might be multiple frames (gathering write) - while (true) { - ByteBuf encoded = outChannel.readOutbound(); - if (encoded != null) { - inChannel.writeInbound(encoded); - } else { - break; - } - } + transfer(outChannel, inChannel); Object decoded = inChannel.readInbound(); Assert.assertNotNull(decoded); @@ -154,4 +202,16 @@ public class WebSocket08EncoderDecoderTest { } binFrame.release(); } + + private void transfer(EmbeddedChannel outChannel, EmbeddedChannel inChannel) { + // Transfer encoded data into decoder + // Loop because there might be multiple frames (gathering write) + for (;;) { + ByteBuf encoded = outChannel.readOutbound(); + if (encoded == null) { + return; + } + inChannel.writeInbound(encoded); + } + } } diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker00Test.java b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker00Test.java index 33c6ce6..efbe22e 100644 --- a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker00Test.java +++ b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker00Test.java @@ -22,8 +22,10 @@ import java.net.URI; public class WebSocketClientHandshaker00Test extends WebSocketClientHandshakerTest { @Override - protected WebSocketClientHandshaker newHandshaker(URI uri, String subprotocol, HttpHeaders headers) { - return new WebSocketClientHandshaker00(uri, WebSocketVersion.V00, subprotocol, headers, 1024); + protected WebSocketClientHandshaker newHandshaker(URI uri, String subprotocol, HttpHeaders headers, + boolean absoluteUpgradeUrl) { + return new WebSocketClientHandshaker00(uri, WebSocketVersion.V00, subprotocol, headers, + 1024, 10000, absoluteUpgradeUrl); } @Override @@ -37,12 +39,11 @@ public class WebSocketClientHandshaker00Test extends WebSocketClientHandshakerTe } @Override - protected CharSequence[] getHandshakeHeaderNames() { + protected CharSequence[] getHandshakeRequiredHeaderNames() { return new CharSequence[] { HttpHeaderNames.CONNECTION, HttpHeaderNames.UPGRADE, HttpHeaderNames.HOST, - HttpHeaderNames.ORIGIN, HttpHeaderNames.SEC_WEBSOCKET_KEY1, HttpHeaderNames.SEC_WEBSOCKET_KEY2, }; diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker07Test.java b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker07Test.java index 9ff3e84..dea8b94 100644 --- a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker07Test.java +++ b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker07Test.java @@ -15,15 +15,39 @@ */ package io.netty.handler.codec.http.websocketx; +import io.netty.handler.codec.http.DefaultHttpHeaders; +import io.netty.handler.codec.http.FullHttpRequest; import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpHeaders; +import org.junit.Test; import java.net.URI; +import static org.junit.Assert.assertEquals; + public class WebSocketClientHandshaker07Test extends WebSocketClientHandshakerTest { + + @Test + public void testHostHeaderPreserved() { + URI uri = URI.create("ws://localhost:9999"); + WebSocketClientHandshaker handshaker = newHandshaker(uri, null, + new DefaultHttpHeaders().set(HttpHeaderNames.HOST, "test.netty.io"), false); + + FullHttpRequest request = handshaker.newHandshakeRequest(); + try { + assertEquals("/", request.uri()); + assertEquals("test.netty.io", request.headers().get(HttpHeaderNames.HOST)); + } finally { + request.release(); + } + } + @Override - protected WebSocketClientHandshaker newHandshaker(URI uri, String subprotocol, HttpHeaders headers) { - return new WebSocketClientHandshaker07(uri, WebSocketVersion.V07, subprotocol, false, headers, 1024); + protected WebSocketClientHandshaker newHandshaker(URI uri, String subprotocol, HttpHeaders headers, + boolean absoluteUpgradeUrl) { + return new WebSocketClientHandshaker07(uri, WebSocketVersion.V07, subprotocol, false, headers, + 1024, true, false, 10000, + absoluteUpgradeUrl); } @Override @@ -37,13 +61,12 @@ public class WebSocketClientHandshaker07Test extends WebSocketClientHandshakerTe } @Override - protected CharSequence[] getHandshakeHeaderNames() { + protected CharSequence[] getHandshakeRequiredHeaderNames() { return new CharSequence[] { HttpHeaderNames.UPGRADE, HttpHeaderNames.CONNECTION, HttpHeaderNames.SEC_WEBSOCKET_KEY, HttpHeaderNames.HOST, - HttpHeaderNames.SEC_WEBSOCKET_ORIGIN, HttpHeaderNames.SEC_WEBSOCKET_VERSION, }; } diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker08Test.java b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker08Test.java index 1efb682..79c6dd4 100644 --- a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker08Test.java +++ b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker08Test.java @@ -21,7 +21,10 @@ import java.net.URI; public class WebSocketClientHandshaker08Test extends WebSocketClientHandshaker07Test { @Override - protected WebSocketClientHandshaker newHandshaker(URI uri, String subprotocol, HttpHeaders headers) { - return new WebSocketClientHandshaker08(uri, WebSocketVersion.V08, subprotocol, false, headers, 1024); + protected WebSocketClientHandshaker newHandshaker(URI uri, String subprotocol, HttpHeaders headers, + boolean absoluteUpgradeUrl) { + return new WebSocketClientHandshaker08(uri, WebSocketVersion.V08, subprotocol, false, headers, + 1024, true, true, 10000, + absoluteUpgradeUrl); } } diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker13Test.java b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker13Test.java index 1727178..cdd9bd7 100644 --- a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker13Test.java +++ b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker13Test.java @@ -15,13 +15,24 @@ */ package io.netty.handler.codec.http.websocketx; +import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpHeaders; import java.net.URI; public class WebSocketClientHandshaker13Test extends WebSocketClientHandshaker07Test { + @Override - protected WebSocketClientHandshaker newHandshaker(URI uri, String subprotocol, HttpHeaders headers) { - return new WebSocketClientHandshaker13(uri, WebSocketVersion.V13, subprotocol, false, headers, 1024); + protected WebSocketClientHandshaker newHandshaker(URI uri, String subprotocol, HttpHeaders headers, + boolean absoluteUpgradeUrl) { + return new WebSocketClientHandshaker13(uri, WebSocketVersion.V13, subprotocol, false, headers, + 1024, true, true, 10000, + absoluteUpgradeUrl); } + + @Override + protected CharSequence getOriginHeaderName() { + return HttpHeaderNames.ORIGIN; + } + } diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshakerTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshakerTest.java index 2054af5..e74a7bf 100644 --- a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshakerTest.java +++ b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshakerTest.java @@ -32,7 +32,7 @@ import io.netty.handler.codec.http.HttpObjectAggregator; import io.netty.handler.codec.http.HttpRequestEncoder; import io.netty.handler.codec.http.HttpResponseDecoder; import io.netty.util.CharsetUtil; -import io.netty.util.internal.PlatformDependent; + import org.junit.Test; import java.net.URI; @@ -40,17 +40,18 @@ import java.net.URI; import static org.junit.Assert.*; public abstract class WebSocketClientHandshakerTest { - protected abstract WebSocketClientHandshaker newHandshaker(URI uri, String subprotocol, HttpHeaders headers); + protected abstract WebSocketClientHandshaker newHandshaker(URI uri, String subprotocol, HttpHeaders headers, + boolean absoluteUpgradeUrl); protected WebSocketClientHandshaker newHandshaker(URI uri) { - return newHandshaker(uri, null, null); + return newHandshaker(uri, null, null, false); } protected abstract CharSequence getOriginHeaderName(); protected abstract CharSequence getProtocolHeaderName(); - protected abstract CharSequence[] getHandshakeHeaderNames(); + protected abstract CharSequence[] getHandshakeRequiredHeaderNames(); @Test public void hostHeaderWs() { @@ -160,6 +161,19 @@ public abstract class WebSocketClientHandshakerTest { testOriginHeader("//LOCALHOST/", "http://localhost"); } + @Test + public void testSetOriginFromCustomHeaders() { + HttpHeaders customHeaders = new DefaultHttpHeaders().set(getOriginHeaderName(), "http://example.com"); + WebSocketClientHandshaker handshaker = newHandshaker(URI.create("ws://server.example.com/chat"), null, + customHeaders, false); + FullHttpRequest request = handshaker.newHandshakeRequest(); + try { + assertEquals("http://example.com", request.headers().get(getOriginHeaderName())); + } finally { + request.release(); + } + } + private void testHostHeader(String uri, String expected) { testHeaderDefaultHttp(uri, HttpHeaderNames.HOST, expected); } @@ -180,7 +194,7 @@ public abstract class WebSocketClientHandshakerTest { @Test @SuppressWarnings("deprecation") - public void testRawPath() { + public void testUpgradeUrl() { URI uri = URI.create("ws://localhost:9999/path%20with%20ws"); WebSocketClientHandshaker handshaker = newHandshaker(uri); FullHttpRequest request = handshaker.newHandshakeRequest(); @@ -192,7 +206,7 @@ public abstract class WebSocketClientHandshakerTest { } @Test - public void testRawPathWithQuery() { + public void testUpgradeUrlWithQuery() { URI uri = URI.create("ws://localhost:9999/path%20with%20ws?a=b%20c"); WebSocketClientHandshaker handshaker = newHandshaker(uri); FullHttpRequest request = handshaker.newHandshakeRequest(); @@ -203,6 +217,42 @@ public abstract class WebSocketClientHandshakerTest { } } + @Test + public void testUpgradeUrlWithoutPath() { + URI uri = URI.create("ws://localhost:9999"); + WebSocketClientHandshaker handshaker = newHandshaker(uri); + FullHttpRequest request = handshaker.newHandshakeRequest(); + try { + assertEquals("/", request.uri()); + } finally { + request.release(); + } + } + + @Test + public void testUpgradeUrlWithoutPathWithQuery() { + URI uri = URI.create("ws://localhost:9999?a=b%20c"); + WebSocketClientHandshaker handshaker = newHandshaker(uri); + FullHttpRequest request = handshaker.newHandshakeRequest(); + try { + assertEquals("/?a=b%20c", request.uri()); + } finally { + request.release(); + } + } + + @Test + public void testAbsoluteUpgradeUrlWithQuery() { + URI uri = URI.create("ws://localhost:9999/path%20with%20ws?a=b%20c"); + WebSocketClientHandshaker handshaker = newHandshaker(uri, null, null, true); + FullHttpRequest request = handshaker.newHandshakeRequest(); + try { + assertEquals("ws://localhost:9999/path%20with%20ws?a=b%20c", request.uri()); + } finally { + request.release(); + } + } + @Test(timeout = 3000) public void testHttpResponseAndFrameInSameBuffer() { testHttpResponseAndFrameInSameBuffer(false); @@ -217,7 +267,7 @@ public abstract class WebSocketClientHandshakerTest { String url = "ws://localhost:9999/ws"; final WebSocketClientHandshaker shaker = newHandshaker(URI.create(url)); final WebSocketClientHandshaker handshaker = new WebSocketClientHandshaker( - shaker.uri(), shaker.version(), null, EmptyHttpHeaders.INSTANCE, Integer.MAX_VALUE) { + shaker.uri(), shaker.version(), null, EmptyHttpHeaders.INSTANCE, Integer.MAX_VALUE, -1) { @Override protected FullHttpRequest newHandshakeRequest() { return shaker.newHandshakeRequest(); @@ -246,7 +296,9 @@ public abstract class WebSocketClientHandshakerTest { // Create a EmbeddedChannel which we will use to encode a BinaryWebsocketFrame to bytes and so use these // to test the actual handshaker. WebSocketServerHandshakerFactory factory = new WebSocketServerHandshakerFactory(url, null, false); - WebSocketServerHandshaker socketServerHandshaker = factory.newHandshaker(shaker.newHandshakeRequest()); + FullHttpRequest request = shaker.newHandshakeRequest(); + WebSocketServerHandshaker socketServerHandshaker = factory.newHandshaker(request); + request.release(); EmbeddedChannel websocketChannel = new EmbeddedChannel(socketServerHandshaker.newWebSocketEncoder(), socketServerHandshaker.newWebsocketDecoder()); assertTrue(websocketChannel.writeOutbound(new BinaryWebSocketFrame(Unpooled.wrappedBuffer(data)))); @@ -311,18 +363,20 @@ public abstract class WebSocketClientHandshakerTest { String bogusHeaderValue = "bogusHeaderValue"; // add values for the headers that are reserved for use in the websockets handshake - for (CharSequence header : getHandshakeHeaderNames()) { - inputHeaders.add(header, bogusHeaderValue); + for (CharSequence header : getHandshakeRequiredHeaderNames()) { + if (!HttpHeaderNames.HOST.equals(header)) { + inputHeaders.add(header, bogusHeaderValue); + } } inputHeaders.add(getProtocolHeaderName(), bogusSubProtocol); String realSubProtocol = "realSubProtocol"; - WebSocketClientHandshaker handshaker = newHandshaker(uri, realSubProtocol, inputHeaders); + WebSocketClientHandshaker handshaker = newHandshaker(uri, realSubProtocol, inputHeaders, false); FullHttpRequest request = handshaker.newHandshakeRequest(); HttpHeaders outputHeaders = request.headers(); // the header values passed in originally have been replaced with values generated by the Handshaker - for (CharSequence header : getHandshakeHeaderNames()) { + for (CharSequence header : getHandshakeRequiredHeaderNames()) { assertEquals(1, outputHeaders.getAll(header).size()); assertNotEquals(bogusHeaderValue, outputHeaders.get(header)); } diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketCloseStatusTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketCloseStatusTest.java new file mode 100644 index 0000000..05cb07d --- /dev/null +++ b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketCloseStatusTest.java @@ -0,0 +1,127 @@ +/* + * Copyright 2019 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.codec.http.websocketx; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.SortedSet; +import java.util.TreeSet; + +import org.hamcrest.Matchers; +import org.junit.Assert; +import org.junit.Test; + +import static io.netty.handler.codec.http.websocketx.WebSocketCloseStatus.*; + +public class WebSocketCloseStatusTest { + + private final List<WebSocketCloseStatus> validCodes = Arrays.asList( + NORMAL_CLOSURE, + ENDPOINT_UNAVAILABLE, + PROTOCOL_ERROR, + INVALID_MESSAGE_TYPE, + INVALID_PAYLOAD_DATA, + POLICY_VIOLATION, + MESSAGE_TOO_BIG, + MANDATORY_EXTENSION, + INTERNAL_SERVER_ERROR, + SERVICE_RESTART, + TRY_AGAIN_LATER, + BAD_GATEWAY + ); + + @Test + public void testToString() { + Assert.assertEquals("1000 Bye", NORMAL_CLOSURE.toString()); + } + + @Test + public void testKnownStatuses() { + Assert.assertSame(NORMAL_CLOSURE, valueOf(1000)); + Assert.assertSame(ENDPOINT_UNAVAILABLE, valueOf(1001)); + Assert.assertSame(PROTOCOL_ERROR, valueOf(1002)); + Assert.assertSame(INVALID_MESSAGE_TYPE, valueOf(1003)); + Assert.assertSame(INVALID_PAYLOAD_DATA, valueOf(1007)); + Assert.assertSame(POLICY_VIOLATION, valueOf(1008)); + Assert.assertSame(MESSAGE_TOO_BIG, valueOf(1009)); + Assert.assertSame(MANDATORY_EXTENSION, valueOf(1010)); + Assert.assertSame(INTERNAL_SERVER_ERROR, valueOf(1011)); + Assert.assertSame(SERVICE_RESTART, valueOf(1012)); + Assert.assertSame(TRY_AGAIN_LATER, valueOf(1013)); + Assert.assertSame(BAD_GATEWAY, valueOf(1014)); + } + + @Test + public void testNaturalOrder() { + Assert.assertThat(PROTOCOL_ERROR, Matchers.greaterThan(NORMAL_CLOSURE)); + Assert.assertThat(PROTOCOL_ERROR, Matchers.greaterThan(valueOf(1001))); + Assert.assertThat(PROTOCOL_ERROR, Matchers.comparesEqualTo(PROTOCOL_ERROR)); + Assert.assertThat(PROTOCOL_ERROR, Matchers.comparesEqualTo(valueOf(1002))); + Assert.assertThat(PROTOCOL_ERROR, Matchers.lessThan(INVALID_MESSAGE_TYPE)); + Assert.assertThat(PROTOCOL_ERROR, Matchers.lessThan(valueOf(1007))); + } + + @Test + public void testUserDefinedStatuses() { + // Given, when + WebSocketCloseStatus feedTimeot = new WebSocketCloseStatus(6033, "Feed timed out"); + WebSocketCloseStatus untradablePrice = new WebSocketCloseStatus(6034, "Untradable price"); + + // Then + Assert.assertNotSame(feedTimeot, valueOf(6033)); + Assert.assertEquals(feedTimeot.code(), 6033); + Assert.assertEquals(feedTimeot.reasonText(), "Feed timed out"); + + Assert.assertNotSame(untradablePrice, valueOf(6034)); + Assert.assertEquals(untradablePrice.code(), 6034); + Assert.assertEquals(untradablePrice.reasonText(), "Untradable price"); + } + + @Test + public void testRfc6455CodeValidation() { + // Given + List<Integer> knownCodes = Arrays.asList( + NORMAL_CLOSURE.code(), + ENDPOINT_UNAVAILABLE.code(), + PROTOCOL_ERROR.code(), + INVALID_MESSAGE_TYPE.code(), + INVALID_PAYLOAD_DATA.code(), + POLICY_VIOLATION.code(), + MESSAGE_TOO_BIG.code(), + MANDATORY_EXTENSION.code(), + INTERNAL_SERVER_ERROR.code(), + SERVICE_RESTART.code(), + TRY_AGAIN_LATER.code(), + BAD_GATEWAY.code() + ); + + SortedSet<Integer> invalidCodes = new TreeSet<Integer>(); + + // When + for (int statusCode = Short.MIN_VALUE; statusCode < Short.MAX_VALUE; statusCode++) { + if (!isValidStatusCode(statusCode)) { + invalidCodes.add(statusCode); + } + } + + // Then + Assert.assertEquals(0, invalidCodes.first().intValue()); + Assert.assertEquals(2999, invalidCodes.last().intValue()); + Assert.assertEquals(3000 - validCodes.size(), invalidCodes.size()); + + invalidCodes.retainAll(knownCodes); + Assert.assertEquals(invalidCodes, Collections.emptySet()); + } + +} diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketHandshakeHandOverTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketHandshakeHandOverTest.java index 10ad774..0245078 100644 --- a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketHandshakeHandOverTest.java +++ b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketHandshakeHandOverTest.java @@ -17,10 +17,13 @@ package io.netty.handler.codec.http.websocketx; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.handler.codec.http.EmptyHttpHeaders; import io.netty.handler.codec.http.HttpClientCodec; import io.netty.handler.codec.http.HttpObjectAggregator; import io.netty.handler.codec.http.HttpServerCodec; @@ -30,6 +33,7 @@ import org.junit.Before; import org.junit.Test; import java.net.URI; +import java.util.List; import static org.junit.Assert.*; @@ -39,6 +43,28 @@ public class WebSocketHandshakeHandOverTest { private WebSocketServerProtocolHandler.HandshakeComplete serverHandshakeComplete; private boolean clientReceivedHandshake; private boolean clientReceivedMessage; + private boolean serverReceivedCloseHandshake; + private boolean clientForceClosed; + private boolean clientHandshakeTimeout; + + private final class CloseNoOpServerProtocolHandler extends WebSocketServerProtocolHandler { + CloseNoOpServerProtocolHandler(String websocketPath) { + super(WebSocketServerProtocolConfig.newBuilder() + .websocketPath(websocketPath) + .allowExtensions(false) + .sendCloseFrame(null) + .build()); + } + + @Override + protected void decode(ChannelHandlerContext ctx, WebSocketFrame frame, List<Object> out) throws Exception { + if (frame instanceof CloseWebSocketFrame) { + serverReceivedCloseHandshake = true; + return; + } + super.decode(ctx, frame, out); + } + } @Before public void setUp() { @@ -46,6 +72,9 @@ public class WebSocketHandshakeHandOverTest { serverHandshakeComplete = null; clientReceivedHandshake = false; clientReceivedMessage = false; + serverReceivedCloseHandshake = false; + clientForceClosed = false; + clientHandshakeTimeout = false; } @Test @@ -95,6 +124,124 @@ public class WebSocketHandshakeHandOverTest { assertTrue(clientReceivedMessage); } + @Test(expected = WebSocketHandshakeException.class) + public void testClientHandshakeTimeout() throws Exception { + EmbeddedChannel serverChannel = createServerChannel(new SimpleChannelInboundHandler<Object>() { + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { + if (evt == ServerHandshakeStateEvent.HANDSHAKE_COMPLETE) { + serverReceivedHandshake = true; + // immediately send a message to the client on connect + ctx.writeAndFlush(new TextWebSocketFrame("abc")); + } else if (evt instanceof WebSocketServerProtocolHandler.HandshakeComplete) { + serverHandshakeComplete = (WebSocketServerProtocolHandler.HandshakeComplete) evt; + } + } + + @Override + protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception { + } + }); + + EmbeddedChannel clientChannel = createClientChannel(new SimpleChannelInboundHandler<Object>() { + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { + if (evt == ClientHandshakeStateEvent.HANDSHAKE_COMPLETE) { + clientReceivedHandshake = true; + } else if (evt == ClientHandshakeStateEvent.HANDSHAKE_TIMEOUT) { + clientHandshakeTimeout = true; + } + } + + @Override + protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception { + if (msg instanceof TextWebSocketFrame) { + clientReceivedMessage = true; + } + } + }, 100); + // Client send the handshake request to server + transferAllDataWithMerge(clientChannel, serverChannel); + // Server do not send the response back + // transferAllDataWithMerge(serverChannel, clientChannel); + WebSocketClientProtocolHandshakeHandler handshakeHandler = + (WebSocketClientProtocolHandshakeHandler) clientChannel + .pipeline().get(WebSocketClientProtocolHandshakeHandler.class.getName()); + + while (!handshakeHandler.getHandshakeFuture().isDone()) { + Thread.sleep(10); + // We need to run all pending tasks as the handshake timeout is scheduled on the EventLoop. + clientChannel.runScheduledPendingTasks(); + } + assertTrue(clientHandshakeTimeout); + assertFalse(clientReceivedHandshake); + assertFalse(clientReceivedMessage); + // Should throw WebSocketHandshakeException + try { + handshakeHandler.getHandshakeFuture().syncUninterruptibly(); + } finally { + serverChannel.finishAndReleaseAll(); + } + } + + @Test(timeout = 10000) + public void testClientHandshakerForceClose() throws Exception { + final WebSocketClientHandshaker handshaker = WebSocketClientHandshakerFactory.newHandshaker( + new URI("ws://localhost:1234/test"), WebSocketVersion.V13, null, true, + EmptyHttpHeaders.INSTANCE, Integer.MAX_VALUE, true, false, 20); + + EmbeddedChannel serverChannel = createServerChannel( + new CloseNoOpServerProtocolHandler("/test"), + new SimpleChannelInboundHandler<Object>() { + @Override + protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception { + } + }); + + EmbeddedChannel clientChannel = createClientChannel(handshaker, new SimpleChannelInboundHandler<Object>() { + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { + if (evt == ClientHandshakeStateEvent.HANDSHAKE_COMPLETE) { + ctx.channel().closeFuture().addListener(new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture future) throws Exception { + clientForceClosed = true; + } + }); + handshaker.close(ctx.channel(), new CloseWebSocketFrame()); + } + } + @Override + protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception { + } + }); + + // Transfer the handshake from the client to the server + transferAllDataWithMerge(clientChannel, serverChannel); + // Transfer the handshake from the server to client + transferAllDataWithMerge(serverChannel, clientChannel); + + // Transfer closing handshake + transferAllDataWithMerge(clientChannel, serverChannel); + assertTrue(serverReceivedCloseHandshake); + // Should not be closed yet as we disabled closing the connection on the server + assertFalse(clientForceClosed); + + while (!clientForceClosed) { + Thread.sleep(10); + // We need to run all pending tasks as the force close timeout is scheduled on the EventLoop. + clientChannel.runPendingTasks(); + } + + // clientForceClosed would be set to TRUE after any close, + // so check here that force close timeout was actually fired + assertTrue(handshaker.isForceCloseComplete()); + + // Both should be empty + assertFalse(serverChannel.finishAndReleaseAll()); + assertFalse(clientChannel.finishAndReleaseAll()); + } + /** * Transfers all pending data from the source channel into the destination channel.<br> * Merges all data into a single buffer before transmission into the destination. @@ -128,12 +275,35 @@ public class WebSocketHandshakeHandOverTest { } private static EmbeddedChannel createClientChannel(ChannelHandler handler) throws Exception { + return createClientChannel(handler, WebSocketClientProtocolConfig.newBuilder() + .webSocketUri("ws://localhost:1234/test") + .subprotocol("test-proto-2") + .build()); + } + + private static EmbeddedChannel createClientChannel(ChannelHandler handler, long timeoutMillis) throws Exception { + return createClientChannel(handler, WebSocketClientProtocolConfig.newBuilder() + .webSocketUri("ws://localhost:1234/test") + .subprotocol("test-proto-2") + .handshakeTimeoutMillis(timeoutMillis) + .build()); + } + + private static EmbeddedChannel createClientChannel(ChannelHandler handler, WebSocketClientProtocolConfig config) { + return new EmbeddedChannel( + new HttpClientCodec(), + new HttpObjectAggregator(8192), + new WebSocketClientProtocolHandler(config), + handler); + } + + private static EmbeddedChannel createClientChannel(WebSocketClientHandshaker handshaker, + ChannelHandler handler) throws Exception { return new EmbeddedChannel( new HttpClientCodec(), new HttpObjectAggregator(8192), - new WebSocketClientProtocolHandler(new URI("ws://localhost:1234/test"), - WebSocketVersion.V13, "test-proto-2", - false, null, 65536), + // Note that we're switching off close frames handling on purpose to test forced close on timeout. + new WebSocketClientProtocolHandler(handshaker, false, false), handler); } @@ -144,4 +314,13 @@ public class WebSocketHandshakeHandOverTest { new WebSocketServerProtocolHandler("/test", "test-proto-1, test-proto-2", false), handler); } + + private static EmbeddedChannel createServerChannel(WebSocketServerProtocolHandler webSocketHandler, + ChannelHandler handler) { + return new EmbeddedChannel( + new HttpServerCodec(), + new HttpObjectAggregator(8192), + webSocketHandler, + handler); + } } diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketProtocolHandlerTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketProtocolHandlerTest.java index af74982..b59cc1e 100644 --- a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketProtocolHandlerTest.java +++ b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketProtocolHandlerTest.java @@ -19,9 +19,10 @@ package io.netty.handler.codec.http.websocketx; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.embedded.EmbeddedChannel; -import io.netty.util.CharsetUtil; +import io.netty.handler.flow.FlowControlHandler; import org.junit.Test; +import static io.netty.util.CharsetUtil.UTF_8; import static org.junit.Assert.*; /** @@ -31,7 +32,7 @@ public class WebSocketProtocolHandlerTest { @Test public void testPingFrame() { - ByteBuf pingData = Unpooled.copiedBuffer("Hello, world", CharsetUtil.UTF_8); + ByteBuf pingData = Unpooled.copiedBuffer("Hello, world", UTF_8); EmbeddedChannel channel = new EmbeddedChannel(new WebSocketProtocolHandler() { }); PingWebSocketFrame inputMessage = new PingWebSocketFrame(pingData); @@ -45,6 +46,65 @@ public class WebSocketProtocolHandlerTest { assertFalse(channel.finish()); } + @Test + public void testPingPongFlowControlWhenAutoReadIsDisabled() { + String text1 = "Hello, world #1"; + String text2 = "Hello, world #2"; + String text3 = "Hello, world #3"; + String text4 = "Hello, world #4"; + + EmbeddedChannel channel = new EmbeddedChannel(); + channel.config().setAutoRead(false); + channel.pipeline().addLast(new FlowControlHandler()); + channel.pipeline().addLast(new WebSocketProtocolHandler() { }); + + // When + assertFalse(channel.writeInbound( + new PingWebSocketFrame(Unpooled.copiedBuffer(text1, UTF_8)), + new TextWebSocketFrame(text2), + new TextWebSocketFrame(text3), + new PingWebSocketFrame(Unpooled.copiedBuffer(text4, UTF_8)) + )); + + // Then - no messages were handled or propagated + assertNull(channel.readInbound()); + assertNull(channel.readOutbound()); + + // When + channel.read(); + + // Then - pong frame was written to the outbound + PongWebSocketFrame response1 = channel.readOutbound(); + assertEquals(text1, response1.content().toString(UTF_8)); + + // And - one requested message was handled and propagated inbound + TextWebSocketFrame message2 = channel.readInbound(); + assertEquals(text2, message2.text()); + + // And - no more messages were handled or propagated + assertNull(channel.readInbound()); + assertNull(channel.readOutbound()); + + // When + channel.read(); + + // Then - one requested message was handled and propagated inbound + TextWebSocketFrame message3 = channel.readInbound(); + assertEquals(text3, message3.text()); + + // And - no more messages were handled or propagated + // Precisely, ping frame 'text4' was NOT read or handled. + // It would be handle ONLY on the next 'channel.read()' call. + assertNull(channel.readInbound()); + assertNull(channel.readOutbound()); + + // Cleanup + response1.release(); + message2.release(); + message3.release(); + assertFalse(channel.finish()); + } + @Test public void testPongFrameDropFrameFalse() { EmbeddedChannel channel = new EmbeddedChannel(new WebSocketProtocolHandler(false) { }); diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketRequestBuilder.java b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketRequestBuilder.java index fd199b8..65ef489 100644 --- a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketRequestBuilder.java +++ b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketRequestBuilder.java @@ -138,7 +138,11 @@ public class WebSocketRequestBuilder { headers.set(HttpHeaderNames.SEC_WEBSOCKET_KEY, key); } if (origin != null) { - headers.set(HttpHeaderNames.SEC_WEBSOCKET_ORIGIN, origin); + if (version == WebSocketVersion.V13 || version == WebSocketVersion.V00) { + headers.set(HttpHeaderNames.ORIGIN, origin); + } else { + headers.set(HttpHeaderNames.SEC_WEBSOCKET_ORIGIN, origin); + } } if (version != null) { headers.set(HttpHeaderNames.SEC_WEBSOCKET_VERSION, version.toHttpHeaderValue()); diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshaker00Test.java b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshaker00Test.java index 76826ab..8783e0b 100644 --- a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshaker00Test.java +++ b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshaker00Test.java @@ -32,7 +32,9 @@ import io.netty.util.CharsetUtil; import org.junit.Assert; import org.junit.Test; -import static io.netty.handler.codec.http.HttpVersion.*; +import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; public class WebSocketServerHandshaker00Test { @@ -46,6 +48,34 @@ public class WebSocketServerHandshaker00Test { testPerformOpeningHandshake0(false); } + @Test + public void testPerformHandshakeWithoutOriginHeader() { + EmbeddedChannel ch = new EmbeddedChannel( + new HttpObjectAggregator(42), new HttpRequestDecoder(), new HttpResponseEncoder()); + + FullHttpRequest req = new DefaultFullHttpRequest( + HTTP_1_1, HttpMethod.GET, "/chat", Unpooled.copiedBuffer("^n:ds[4U", CharsetUtil.US_ASCII)); + + req.headers().set(HttpHeaderNames.HOST, "server.example.com"); + req.headers().set(HttpHeaderNames.UPGRADE, HttpHeaderValues.WEBSOCKET); + req.headers().set(HttpHeaderNames.CONNECTION, "Upgrade"); + req.headers().set(HttpHeaderNames.SEC_WEBSOCKET_KEY1, "4 @1 46546xW%0l 1 5"); + req.headers().set(HttpHeaderNames.SEC_WEBSOCKET_PROTOCOL, "chat, superchat"); + + WebSocketServerHandshaker00 handshaker00 = new WebSocketServerHandshaker00( + "ws://example.com/chat", "chat", Integer.MAX_VALUE); + try { + handshaker00.handshake(ch, req); + fail("Expecting WebSocketHandshakeException"); + } catch (WebSocketHandshakeException e) { + assertEquals("Missing origin header, got only " + + "[host, upgrade, connection, sec-websocket-key1, sec-websocket-protocol]", + e.getMessage()); + } finally { + req.release(); + } + } + private static void testPerformOpeningHandshake0(boolean subProtocol) { EmbeddedChannel ch = new EmbeddedChannel( new HttpObjectAggregator(42), new HttpRequestDecoder(), new HttpResponseEncoder()); diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshaker13Test.java b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshaker13Test.java index b66851e..25340df 100644 --- a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshaker13Test.java +++ b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshaker13Test.java @@ -16,6 +16,8 @@ package io.netty.handler.codec.http.websocketx; import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandler; import io.netty.channel.embedded.EmbeddedChannel; import io.netty.handler.codec.http.DefaultFullHttpRequest; import io.netty.handler.codec.http.FullHttpRequest; @@ -27,10 +29,15 @@ import io.netty.handler.codec.http.HttpRequestDecoder; import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.HttpResponseDecoder; import io.netty.handler.codec.http.HttpResponseEncoder; +import io.netty.handler.codec.http.HttpServerCodec; import io.netty.util.ReferenceCountUtil; +import io.netty.util.ReferenceCounted; +import org.hamcrest.CoreMatchers; import org.junit.Assert; import org.junit.Test; +import java.util.Iterator; + import static io.netty.handler.codec.http.HttpVersion.*; public class WebSocketServerHandshaker13Test { @@ -47,8 +54,50 @@ public class WebSocketServerHandshaker13Test { private static void testPerformOpeningHandshake0(boolean subProtocol) { EmbeddedChannel ch = new EmbeddedChannel( - new HttpObjectAggregator(42), new HttpRequestDecoder(), new HttpResponseEncoder()); + new HttpObjectAggregator(42), new HttpResponseEncoder(), new HttpRequestDecoder()); + + if (subProtocol) { + testUpgrade0(ch, new WebSocketServerHandshaker13( + "ws://example.com/chat", "chat", false, Integer.MAX_VALUE, false)); + } else { + testUpgrade0(ch, new WebSocketServerHandshaker13( + "ws://example.com/chat", null, false, Integer.MAX_VALUE, false)); + } + Assert.assertFalse(ch.finish()); + } + @Test + public void testCloseReasonWithEncoderAndDecoder() { + testCloseReason0(new HttpResponseEncoder(), new HttpRequestDecoder()); + } + + @Test + public void testCloseReasonWithCodec() { + testCloseReason0(new HttpServerCodec()); + } + + private static void testCloseReason0(ChannelHandler... handlers) { + EmbeddedChannel ch = new EmbeddedChannel( + new HttpObjectAggregator(42)); + ch.pipeline().addLast(handlers); + testUpgrade0(ch, new WebSocketServerHandshaker13("ws://example.com/chat", "chat", + WebSocketDecoderConfig.newBuilder().maxFramePayloadLength(4).closeOnProtocolViolation(true).build())); + + ch.writeOutbound(new BinaryWebSocketFrame(Unpooled.wrappedBuffer(new byte[8]))); + ByteBuf buffer = ch.readOutbound(); + try { + ch.writeInbound(buffer); + Assert.fail(); + } catch (CorruptedWebSocketFrameException expected) { + // expected + } + ReferenceCounted closeMessage = ch.readOutbound(); + Assert.assertThat(closeMessage, CoreMatchers.instanceOf(ByteBuf.class)); + closeMessage.release(); + Assert.assertFalse(ch.finish()); + } + + private static void testUpgrade0(EmbeddedChannel ch, WebSocketServerHandshaker13 handshaker) { FullHttpRequest req = new DefaultFullHttpRequest(HTTP_1_1, HttpMethod.GET, "/chat"); req.headers().set(HttpHeaderNames.HOST, "server.example.com"); req.headers().set(HttpHeaderNames.UPGRADE, HttpHeaderValues.WEBSOCKET); @@ -58,13 +107,7 @@ public class WebSocketServerHandshaker13Test { req.headers().set(HttpHeaderNames.SEC_WEBSOCKET_PROTOCOL, "chat, superchat"); req.headers().set(HttpHeaderNames.SEC_WEBSOCKET_VERSION, "13"); - if (subProtocol) { - new WebSocketServerHandshaker13( - "ws://example.com/chat", "chat", false, Integer.MAX_VALUE, false).handshake(ch, req); - } else { - new WebSocketServerHandshaker13( - "ws://example.com/chat", null, false, Integer.MAX_VALUE, false).handshake(ch, req); - } + handshaker.handshake(ch, req); ByteBuf resBuf = ch.readOutbound(); @@ -74,8 +117,10 @@ public class WebSocketServerHandshaker13Test { Assert.assertEquals( "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=", res.headers().get(HttpHeaderNames.SEC_WEBSOCKET_ACCEPT)); - if (subProtocol) { - Assert.assertEquals("chat", res.headers().get(HttpHeaderNames.SEC_WEBSOCKET_PROTOCOL)); + Iterator<String> subProtocols = handshaker.subprotocols().iterator(); + if (subProtocols.hasNext()) { + Assert.assertEquals(subProtocols.next(), + res.headers().get(HttpHeaderNames.SEC_WEBSOCKET_PROTOCOL)); } else { Assert.assertNull(res.headers().get(HttpHeaderNames.SEC_WEBSOCKET_PROTOCOL)); } diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketServerProtocolHandlerTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketServerProtocolHandlerTest.java index 50bd6be..69cda51 100644 --- a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketServerProtocolHandlerTest.java +++ b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketServerProtocolHandlerTest.java @@ -15,6 +15,7 @@ */ package io.netty.handler.codec.http.websocketx; +import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; @@ -24,11 +25,15 @@ import io.netty.channel.embedded.EmbeddedChannel; import io.netty.handler.codec.http.DefaultFullHttpRequest; import io.netty.handler.codec.http.FullHttpRequest; import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.HttpClientCodec; import io.netty.handler.codec.http.HttpHeaderValues; import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpObjectAggregator; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpRequestDecoder; import io.netty.handler.codec.http.HttpResponseEncoder; +import io.netty.handler.codec.http.HttpServerCodec; +import io.netty.util.CharsetUtil; import io.netty.util.ReferenceCountUtil; import org.junit.Before; import org.junit.Test; @@ -50,7 +55,7 @@ public class WebSocketServerProtocolHandlerTest { } @Test - public void testHttpUpgradeRequest() throws Exception { + public void testHttpUpgradeRequest() { EmbeddedChannel ch = createChannel(new MockOutboundHandler()); ChannelHandlerContext handshakerCtx = ch.pipeline().context(WebSocketServerProtocolHandshakeHandler.class); writeUpgradeRequest(ch); @@ -59,22 +64,29 @@ public class WebSocketServerProtocolHandlerTest { assertEquals(SWITCHING_PROTOCOLS, response.status()); response.release(); assertNotNull(WebSocketServerProtocolHandler.getHandshaker(handshakerCtx.channel())); + assertFalse(ch.finish()); } @Test - public void testSubsequentHttpRequestsAfterUpgradeShouldReturn403() throws Exception { - EmbeddedChannel ch = createChannel(); - + public void testWebSocketServerProtocolHandshakeHandlerReplacedBeforeHandshake() { + EmbeddedChannel ch = createChannel(new MockOutboundHandler()); + ChannelHandlerContext handshakerCtx = ch.pipeline().context(WebSocketServerProtocolHandshakeHandler.class); + ch.pipeline().addLast(new ChannelInboundHandlerAdapter() { + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { + if (evt instanceof WebSocketServerProtocolHandler.HandshakeComplete) { + // We should have removed the handler already. + assertNull(ctx.pipeline().context(WebSocketServerProtocolHandshakeHandler.class)); + } + } + }); writeUpgradeRequest(ch); FullHttpResponse response = responses.remove(); assertEquals(SWITCHING_PROTOCOLS, response.status()); response.release(); - - ch.writeInbound(new DefaultFullHttpRequest(HTTP_1_1, HttpMethod.GET, "/test")); - response = responses.remove(); - assertEquals(FORBIDDEN, response.status()); - response.release(); + assertNotNull(WebSocketServerProtocolHandler.getHandshaker(handshakerCtx.channel())); + assertFalse(ch.finish()); } @Test @@ -94,6 +106,7 @@ public class WebSocketServerProtocolHandlerTest { assertEquals(BAD_REQUEST, response.status()); assertEquals("not a WebSocket handshake request: missing upgrade", getResponseMessage(response)); response.release(); + assertFalse(ch.finish()); } @Test @@ -114,6 +127,49 @@ public class WebSocketServerProtocolHandlerTest { assertEquals(BAD_REQUEST, response.status()); assertEquals("not a WebSocket request: missing key", getResponseMessage(response)); response.release(); + assertFalse(ch.finish()); + } + + @Test + public void testCreateUTF8Validator() { + WebSocketServerProtocolConfig config = WebSocketServerProtocolConfig.newBuilder() + .websocketPath("/test") + .withUTF8Validator(true) + .build(); + + EmbeddedChannel ch = new EmbeddedChannel( + new WebSocketServerProtocolHandler(config), + new HttpRequestDecoder(), + new HttpResponseEncoder(), + new MockOutboundHandler()); + writeUpgradeRequest(ch); + + FullHttpResponse response = responses.remove(); + assertEquals(SWITCHING_PROTOCOLS, response.status()); + response.release(); + + assertNotNull(ch.pipeline().get(Utf8FrameValidator.class)); + } + + @Test + public void testDoNotCreateUTF8Validator() { + WebSocketServerProtocolConfig config = WebSocketServerProtocolConfig.newBuilder() + .websocketPath("/test") + .withUTF8Validator(false) + .build(); + + EmbeddedChannel ch = new EmbeddedChannel( + new WebSocketServerProtocolHandler(config), + new HttpRequestDecoder(), + new HttpResponseEncoder(), + new MockOutboundHandler()); + writeUpgradeRequest(ch); + + FullHttpResponse response = responses.remove(); + assertEquals(SWITCHING_PROTOCOLS, response.status()); + response.release(); + + assertNull(ch.pipeline().get(Utf8FrameValidator.class)); } @Test @@ -122,6 +178,10 @@ public class WebSocketServerProtocolHandlerTest { EmbeddedChannel ch = createChannel(customTextFrameHandler); writeUpgradeRequest(ch); + FullHttpResponse response = responses.remove(); + assertEquals(SWITCHING_PROTOCOLS, response.status()); + response.release(); + if (ch.pipeline().context(HttpRequestDecoder.class) != null) { // Removing the HttpRequestDecoder because we are writing a TextWebSocketFrame and thus // decoding is not necessary. @@ -131,6 +191,150 @@ public class WebSocketServerProtocolHandlerTest { ch.writeInbound(new TextWebSocketFrame("payload")); assertEquals("processed: payload", customTextFrameHandler.getContent()); + assertFalse(ch.finish()); + } + + @Test + public void testExplicitCloseFrameSentWhenServerChannelClosed() throws Exception { + WebSocketCloseStatus closeStatus = WebSocketCloseStatus.ENDPOINT_UNAVAILABLE; + EmbeddedChannel client = createClient(); + EmbeddedChannel server = createServer(); + + assertFalse(server.writeInbound(client.readOutbound())); + assertFalse(client.writeInbound(server.readOutbound())); + + // When server channel closed with explicit close-frame + assertTrue(server.writeOutbound(new CloseWebSocketFrame(closeStatus))); + server.close(); + + // Then client receives provided close-frame + assertTrue(client.writeInbound(server.readOutbound())); + assertFalse(server.isOpen()); + + CloseWebSocketFrame closeMessage = client.readInbound(); + assertEquals(closeMessage.statusCode(), closeStatus.code()); + closeMessage.release(); + + client.close(); + assertTrue(ReferenceCountUtil.release(client.readOutbound())); + assertFalse(client.finishAndReleaseAll()); + assertFalse(server.finishAndReleaseAll()); + } + + @Test + public void testCloseFrameSentWhenServerChannelClosedSilently() throws Exception { + EmbeddedChannel client = createClient(); + EmbeddedChannel server = createServer(); + + assertFalse(server.writeInbound(client.readOutbound())); + assertFalse(client.writeInbound(server.readOutbound())); + + // When server channel closed without explicit close-frame + server.close(); + + // Then client receives NORMAL_CLOSURE close-frame + assertTrue(client.writeInbound(server.readOutbound())); + assertFalse(server.isOpen()); + + CloseWebSocketFrame closeMessage = client.readInbound(); + assertEquals(closeMessage.statusCode(), WebSocketCloseStatus.NORMAL_CLOSURE.code()); + closeMessage.release(); + + client.close(); + assertTrue(ReferenceCountUtil.release(client.readOutbound())); + assertFalse(client.finishAndReleaseAll()); + assertFalse(server.finishAndReleaseAll()); + } + + @Test + public void testExplicitCloseFrameSentWhenClientChannelClosed() throws Exception { + WebSocketCloseStatus closeStatus = WebSocketCloseStatus.INVALID_PAYLOAD_DATA; + EmbeddedChannel client = createClient(); + EmbeddedChannel server = createServer(); + + assertFalse(server.writeInbound(client.readOutbound())); + assertFalse(client.writeInbound(server.readOutbound())); + + // When client channel closed with explicit close-frame + assertTrue(client.writeOutbound(new CloseWebSocketFrame(closeStatus))); + client.close(); + + // Then client receives provided close-frame + assertFalse(server.writeInbound(client.readOutbound())); + assertFalse(client.isOpen()); + assertFalse(server.isOpen()); + + CloseWebSocketFrame closeMessage = decode(server.<ByteBuf>readOutbound(), CloseWebSocketFrame.class); + assertEquals(closeMessage.statusCode(), closeStatus.code()); + closeMessage.release(); + + assertFalse(client.finishAndReleaseAll()); + assertFalse(server.finishAndReleaseAll()); + } + + @Test + public void testCloseFrameSentWhenClientChannelClosedSilently() throws Exception { + EmbeddedChannel client = createClient(); + EmbeddedChannel server = createServer(); + + assertFalse(server.writeInbound(client.readOutbound())); + assertFalse(client.writeInbound(server.readOutbound())); + + // When client channel closed without explicit close-frame + client.close(); + + // Then server receives NORMAL_CLOSURE close-frame + assertFalse(server.writeInbound(client.readOutbound())); + assertFalse(client.isOpen()); + assertFalse(server.isOpen()); + + CloseWebSocketFrame closeMessage = decode(server.<ByteBuf>readOutbound(), CloseWebSocketFrame.class); + assertEquals(closeMessage, new CloseWebSocketFrame(WebSocketCloseStatus.NORMAL_CLOSURE)); + closeMessage.release(); + + assertFalse(client.finishAndReleaseAll()); + assertFalse(server.finishAndReleaseAll()); + } + + private EmbeddedChannel createClient(ChannelHandler... handlers) throws Exception { + WebSocketClientProtocolConfig clientConfig = WebSocketClientProtocolConfig.newBuilder() + .webSocketUri("http://test/test") + .dropPongFrames(false) + .handleCloseFrames(false) + .build(); + EmbeddedChannel ch = new EmbeddedChannel(false, false, + new HttpClientCodec(), + new HttpObjectAggregator(8192), + new WebSocketClientProtocolHandler(clientConfig) + ); + ch.pipeline().addLast(handlers); + ch.register(); + return ch; + } + + private EmbeddedChannel createServer(ChannelHandler... handlers) throws Exception { + WebSocketServerProtocolConfig serverConfig = WebSocketServerProtocolConfig.newBuilder() + .websocketPath("/test") + .dropPongFrames(false) + .build(); + EmbeddedChannel ch = new EmbeddedChannel(false, false, + new HttpServerCodec(), + new HttpObjectAggregator(8192), + new WebSocketServerProtocolHandler(serverConfig) + ); + ch.pipeline().addLast(handlers); + ch.register(); + return ch; + } + + @SuppressWarnings("SameParameterValue") + private <T> T decode(ByteBuf input, Class<T> clazz) { + EmbeddedChannel ch = new EmbeddedChannel(new WebSocket13FrameDecoder(true, false, 65536, true)); + assertTrue(ch.writeInbound(input)); + Object decoded = ch.readInbound(); + assertNotNull(decoded); + assertFalse(ch.finish()); + return clazz.cast(decoded); } private EmbeddedChannel createChannel() { @@ -138,8 +342,12 @@ public class WebSocketServerProtocolHandlerTest { } private EmbeddedChannel createChannel(ChannelHandler handler) { + WebSocketServerProtocolConfig serverConfig = WebSocketServerProtocolConfig.newBuilder() + .websocketPath("/test") + .sendCloseFrame(null) + .build(); return new EmbeddedChannel( - new WebSocketServerProtocolHandler("/test", null, false), + new WebSocketServerProtocolHandler(serverConfig), new HttpRequestDecoder(), new HttpResponseEncoder(), new MockOutboundHandler(), @@ -151,19 +359,19 @@ public class WebSocketServerProtocolHandlerTest { } private static String getResponseMessage(FullHttpResponse response) { - return new String(response.content().array()); + return response.content().toString(CharsetUtil.UTF_8); } private class MockOutboundHandler extends ChannelOutboundHandlerAdapter { @Override - public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) { responses.add((FullHttpResponse) msg); promise.setSuccess(); } @Override - public void flush(ChannelHandlerContext ctx) throws Exception { + public void flush(ChannelHandlerContext ctx) { } } @@ -171,7 +379,7 @@ public class WebSocketServerProtocolHandlerTest { private String content; @Override - public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + public void channelRead(ChannelHandlerContext ctx, Object msg) { assertNull(content); content = "processed: " + ((TextWebSocketFrame) msg).text(); ReferenceCountUtil.release(msg); diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketUtf8FrameValidatorTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketUtf8FrameValidatorTest.java index c3bb0ed..84428f3 100644 --- a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketUtf8FrameValidatorTest.java +++ b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketUtf8FrameValidatorTest.java @@ -36,8 +36,9 @@ public class WebSocketUtf8FrameValidatorTest { private void assertCorruptedFrameExceptionHandling(byte[] data) { EmbeddedChannel channel = new EmbeddedChannel(new Utf8FrameValidator()); + TextWebSocketFrame frame = new TextWebSocketFrame(Unpooled.copiedBuffer(data)); try { - channel.writeInbound(new TextWebSocketFrame(Unpooled.copiedBuffer(data))); + channel.writeInbound(frame); Assert.fail(); } catch (CorruptedFrameException e) { // expected exception @@ -51,5 +52,6 @@ public class WebSocketUtf8FrameValidatorTest { buf.release(); } Assert.assertNull(channel.readOutbound()); + Assert.assertEquals(0, frame.refCnt()); } } diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketExtensionFilterProviderTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketExtensionFilterProviderTest.java new file mode 100644 index 0000000..66d5dbf --- /dev/null +++ b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketExtensionFilterProviderTest.java @@ -0,0 +1,32 @@ +/* + * Copyright 2019 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.codec.http.websocketx.extensions; + +import org.junit.Test; + +import static org.junit.Assert.*; + +public class WebSocketExtensionFilterProviderTest { + + @Test + public void testDefaultExtensionFilterProvider() { + WebSocketExtensionFilterProvider defaultProvider = WebSocketExtensionFilterProvider.DEFAULT; + assertNotNull(defaultProvider); + + assertEquals(WebSocketExtensionFilter.NEVER_SKIP, defaultProvider.decoderFilter()); + assertEquals(WebSocketExtensionFilter.NEVER_SKIP, defaultProvider.encoderFilter()); + } +} diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketExtensionFilterTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketExtensionFilterTest.java new file mode 100644 index 0000000..4d2a122 --- /dev/null +++ b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketExtensionFilterTest.java @@ -0,0 +1,87 @@ +/* + * Copyright 2019 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.codec.http.websocketx.extensions; + +import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame; +import io.netty.handler.codec.http.websocketx.CloseWebSocketFrame; +import io.netty.handler.codec.http.websocketx.ContinuationWebSocketFrame; +import io.netty.handler.codec.http.websocketx.PingWebSocketFrame; +import io.netty.handler.codec.http.websocketx.PongWebSocketFrame; +import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; +import org.junit.Test; + +import static org.junit.Assert.*; + +public class WebSocketExtensionFilterTest { + + @Test + public void testNeverSkip() { + WebSocketExtensionFilter neverSkip = WebSocketExtensionFilter.NEVER_SKIP; + + BinaryWebSocketFrame binaryFrame = new BinaryWebSocketFrame(); + assertFalse(neverSkip.mustSkip(binaryFrame)); + assertTrue(binaryFrame.release()); + + TextWebSocketFrame textFrame = new TextWebSocketFrame(); + assertFalse(neverSkip.mustSkip(textFrame)); + assertTrue(textFrame.release()); + + PingWebSocketFrame pingFrame = new PingWebSocketFrame(); + assertFalse(neverSkip.mustSkip(pingFrame)); + assertTrue(pingFrame.release()); + + PongWebSocketFrame pongFrame = new PongWebSocketFrame(); + assertFalse(neverSkip.mustSkip(pongFrame)); + assertTrue(pongFrame.release()); + + CloseWebSocketFrame closeFrame = new CloseWebSocketFrame(); + assertFalse(neverSkip.mustSkip(closeFrame)); + assertTrue(closeFrame.release()); + + ContinuationWebSocketFrame continuationFrame = new ContinuationWebSocketFrame(); + assertFalse(neverSkip.mustSkip(continuationFrame)); + assertTrue(continuationFrame.release()); + } + + @Test + public void testAlwaysSkip() { + WebSocketExtensionFilter neverSkip = WebSocketExtensionFilter.ALWAYS_SKIP; + + BinaryWebSocketFrame binaryFrame = new BinaryWebSocketFrame(); + assertTrue(neverSkip.mustSkip(binaryFrame)); + assertTrue(binaryFrame.release()); + + TextWebSocketFrame textFrame = new TextWebSocketFrame(); + assertTrue(neverSkip.mustSkip(textFrame)); + assertTrue(textFrame.release()); + + PingWebSocketFrame pingFrame = new PingWebSocketFrame(); + assertTrue(neverSkip.mustSkip(pingFrame)); + assertTrue(pingFrame.release()); + + PongWebSocketFrame pongFrame = new PongWebSocketFrame(); + assertTrue(neverSkip.mustSkip(pongFrame)); + assertTrue(pongFrame.release()); + + CloseWebSocketFrame closeFrame = new CloseWebSocketFrame(); + assertTrue(neverSkip.mustSkip(closeFrame)); + assertTrue(closeFrame.release()); + + ContinuationWebSocketFrame continuationFrame = new ContinuationWebSocketFrame(); + assertTrue(neverSkip.mustSkip(continuationFrame)); + assertTrue(continuationFrame.release()); + } +} diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketExtensionTestUtil.java b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketExtensionTestUtil.java index 411b167..38867fe 100644 --- a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketExtensionTestUtil.java +++ b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketExtensionTestUtil.java @@ -69,7 +69,7 @@ public final class WebSocketExtensionTestUtil { private final String name; - public WebSocketExtensionDataMatcher(String name) { + WebSocketExtensionDataMatcher(String name) { this.name = name; } diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketServerExtensionHandlerTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketServerExtensionHandlerTest.java index c02853f..ba4e3cc 100644 --- a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketServerExtensionHandlerTest.java +++ b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketServerExtensionHandlerTest.java @@ -15,11 +15,13 @@ */ package io.netty.handler.codec.http.websocketx.extensions; +import io.netty.channel.ChannelPromise; import io.netty.channel.embedded.EmbeddedChannel; import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponse; +import java.io.IOException; import java.util.Collections; import java.util.List; @@ -62,8 +64,9 @@ public class WebSocketServerExtensionHandlerTest { when(fallbackExtensionMock.rsv()).thenReturn(WebSocketExtension.RSV1); // execute - EmbeddedChannel ch = new EmbeddedChannel(new WebSocketServerExtensionHandler( - mainHandshakerMock, fallbackHandshakerMock)); + WebSocketServerExtensionHandler extensionHandler = + new WebSocketServerExtensionHandler(mainHandshakerMock, fallbackHandshakerMock); + EmbeddedChannel ch = new EmbeddedChannel(extensionHandler); HttpRequest req = newUpgradeRequest("main, fallback"); ch.writeInbound(req); @@ -76,6 +79,7 @@ public class WebSocketServerExtensionHandlerTest { res2.headers().get(HttpHeaderNames.SEC_WEBSOCKET_EXTENSIONS)); // test + assertNull(ch.pipeline().context(extensionHandler)); assertEquals(1, resExts.size()); assertEquals("main", resExts.get(0).name()); assertTrue(resExts.get(0).parameters().isEmpty()); @@ -119,8 +123,9 @@ public class WebSocketServerExtensionHandlerTest { when(fallbackExtensionMock.newExtensionDecoder()).thenReturn(new Dummy2Decoder()); // execute - EmbeddedChannel ch = new EmbeddedChannel(new WebSocketServerExtensionHandler( - mainHandshakerMock, fallbackHandshakerMock)); + WebSocketServerExtensionHandler extensionHandler = + new WebSocketServerExtensionHandler(mainHandshakerMock, fallbackHandshakerMock); + EmbeddedChannel ch = new EmbeddedChannel(extensionHandler); HttpRequest req = newUpgradeRequest("main, fallback"); ch.writeInbound(req); @@ -133,6 +138,7 @@ public class WebSocketServerExtensionHandlerTest { res2.headers().get(HttpHeaderNames.SEC_WEBSOCKET_EXTENSIONS)); // test + assertNull(ch.pipeline().context(extensionHandler)); assertEquals(2, resExts.size()); assertEquals("main", resExts.get(0).name()); assertEquals("fallback", resExts.get(1).name()); @@ -170,8 +176,9 @@ public class WebSocketServerExtensionHandlerTest { thenReturn(null); // execute - EmbeddedChannel ch = new EmbeddedChannel(new WebSocketServerExtensionHandler( - mainHandshakerMock, fallbackHandshakerMock)); + WebSocketServerExtensionHandler extensionHandler = + new WebSocketServerExtensionHandler(mainHandshakerMock, fallbackHandshakerMock); + EmbeddedChannel ch = new EmbeddedChannel(extensionHandler); HttpRequest req = newUpgradeRequest("unknown, unknown2"); ch.writeInbound(req); @@ -182,6 +189,7 @@ public class WebSocketServerExtensionHandlerTest { HttpResponse res2 = ch.readOutbound(); // test + assertNull(ch.pipeline().context(extensionHandler)); assertFalse(res2.headers().contains(HttpHeaderNames.SEC_WEBSOCKET_EXTENSIONS)); verify(mainHandshakerMock).handshakeExtension(webSocketExtensionDataMatcher("unknown")); @@ -190,4 +198,31 @@ public class WebSocketServerExtensionHandlerTest { verify(fallbackHandshakerMock).handshakeExtension(webSocketExtensionDataMatcher("unknown")); verify(fallbackHandshakerMock).handshakeExtension(webSocketExtensionDataMatcher("unknown2")); } + + @Test + public void testExtensionHandlerNotRemovedByFailureWritePromise() { + // initialize + when(mainHandshakerMock.handshakeExtension(webSocketExtensionDataMatcher("main"))) + .thenReturn(mainExtensionMock); + when(mainExtensionMock.newReponseData()).thenReturn( + new WebSocketExtensionData("main", Collections.<String, String>emptyMap())); + + // execute + WebSocketServerExtensionHandler extensionHandler = + new WebSocketServerExtensionHandler(mainHandshakerMock); + EmbeddedChannel ch = new EmbeddedChannel(extensionHandler); + + HttpRequest req = newUpgradeRequest("main"); + ch.writeInbound(req); + + HttpResponse res = newUpgradeResponse(null); + ChannelPromise failurePromise = ch.newPromise(); + ch.writeOneOutbound(res, failurePromise); + failurePromise.setFailure(new IOException("Cannot write response")); + + // test + assertNull(ch.readOutbound()); + assertNotNull(ch.pipeline().context(extensionHandler)); + assertTrue(ch.finish()); + } } diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerFrameDeflateDecoderTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerFrameDeflateDecoderTest.java index 67baa51..1d153f3 100644 --- a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerFrameDeflateDecoderTest.java +++ b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerFrameDeflateDecoderTest.java @@ -15,22 +15,20 @@ */ package io.netty.handler.codec.http.websocketx.extensions.compression; -import static io.netty.handler.codec.http.websocketx.extensions.WebSocketExtension.RSV1; -import static io.netty.handler.codec.http.websocketx.extensions.WebSocketExtension.RSV3; -import static org.junit.Assert.*; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.embedded.EmbeddedChannel; import io.netty.handler.codec.compression.ZlibCodecFactory; import io.netty.handler.codec.compression.ZlibWrapper; import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame; -import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtension; +import org.junit.Test; -import java.util.Arrays; import java.util.Random; -import org.junit.Test; +import static io.netty.handler.codec.http.websocketx.extensions.WebSocketExtension.*; +import static io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionFilter.*; +import static org.junit.Assert.*; public class PerFrameDeflateDecoderTest { @@ -46,7 +44,7 @@ public class PerFrameDeflateDecoderTest { byte[] payload = new byte[300]; random.nextBytes(payload); - encoderChannel.writeOutbound(Unpooled.wrappedBuffer(payload)); + assertTrue(encoderChannel.writeOutbound(Unpooled.wrappedBuffer(payload))); ByteBuf compressedPayload = encoderChannel.readOutbound(); BinaryWebSocketFrame compressedFrame = new BinaryWebSocketFrame(true, @@ -54,19 +52,18 @@ public class PerFrameDeflateDecoderTest { compressedPayload.slice(0, compressedPayload.readableBytes() - 4)); // execute - decoderChannel.writeInbound(compressedFrame); + assertTrue(decoderChannel.writeInbound(compressedFrame)); BinaryWebSocketFrame uncompressedFrame = decoderChannel.readInbound(); // test assertNotNull(uncompressedFrame); assertNotNull(uncompressedFrame.content()); - assertTrue(uncompressedFrame instanceof BinaryWebSocketFrame); assertEquals(RSV3, uncompressedFrame.rsv()); assertEquals(300, uncompressedFrame.content().readableBytes()); byte[] finalPayload = new byte[300]; uncompressedFrame.content().readBytes(finalPayload); - assertTrue(Arrays.equals(finalPayload, payload)); + assertArrayEquals(finalPayload, payload); uncompressedFrame.release(); } @@ -82,19 +79,18 @@ public class PerFrameDeflateDecoderTest { RSV3, Unpooled.wrappedBuffer(payload)); // execute - decoderChannel.writeInbound(frame); + assertTrue(decoderChannel.writeInbound(frame)); BinaryWebSocketFrame newFrame = decoderChannel.readInbound(); // test assertNotNull(newFrame); assertNotNull(newFrame.content()); - assertTrue(newFrame instanceof BinaryWebSocketFrame); assertEquals(RSV3, newFrame.rsv()); assertEquals(300, newFrame.content().readableBytes()); byte[] finalPayload = new byte[300]; newFrame.content().readBytes(finalPayload); - assertTrue(Arrays.equals(finalPayload, payload)); + assertArrayEquals(finalPayload, payload); newFrame.release(); } @@ -105,21 +101,51 @@ public class PerFrameDeflateDecoderTest { ZlibCodecFactory.newZlibEncoder(ZlibWrapper.NONE, 9, 15, 8)); EmbeddedChannel decoderChannel = new EmbeddedChannel(new PerFrameDeflateDecoder(false)); - encoderChannel.writeOutbound(Unpooled.EMPTY_BUFFER); + assertTrue(encoderChannel.writeOutbound(Unpooled.EMPTY_BUFFER)); ByteBuf compressedPayload = encoderChannel.readOutbound(); BinaryWebSocketFrame compressedFrame = new BinaryWebSocketFrame(true, RSV1 | RSV3, compressedPayload); // execute - decoderChannel.writeInbound(compressedFrame); + assertTrue(decoderChannel.writeInbound(compressedFrame)); BinaryWebSocketFrame uncompressedFrame = decoderChannel.readInbound(); // test assertNotNull(uncompressedFrame); assertNotNull(uncompressedFrame.content()); - assertTrue(uncompressedFrame instanceof BinaryWebSocketFrame); assertEquals(RSV3, uncompressedFrame.rsv()); assertEquals(0, uncompressedFrame.content().readableBytes()); uncompressedFrame.release(); } + + @Test + public void testDecompressionSkip() { + EmbeddedChannel encoderChannel = new EmbeddedChannel( + ZlibCodecFactory.newZlibEncoder(ZlibWrapper.NONE, 9, 15, 8)); + EmbeddedChannel decoderChannel = new EmbeddedChannel(new PerFrameDeflateDecoder(false, ALWAYS_SKIP)); + + byte[] payload = new byte[300]; + random.nextBytes(payload); + + assertTrue(encoderChannel.writeOutbound(Unpooled.wrappedBuffer(payload))); + ByteBuf compressedPayload = encoderChannel.readOutbound(); + + BinaryWebSocketFrame compressedBinaryFrame = new BinaryWebSocketFrame( + true, WebSocketExtension.RSV1 | WebSocketExtension.RSV3, compressedPayload); + + assertTrue(decoderChannel.writeInbound(compressedBinaryFrame)); + + BinaryWebSocketFrame inboundBinaryFrame = decoderChannel.readInbound(); + + assertNotNull(inboundBinaryFrame); + assertNotNull(inboundBinaryFrame.content()); + assertEquals(compressedPayload, inboundBinaryFrame.content()); + assertEquals(5, inboundBinaryFrame.rsv()); + + assertTrue(inboundBinaryFrame.release()); + + assertTrue(encoderChannel.finishAndReleaseAll()); + assertFalse(decoderChannel.finish()); + } + } diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerFrameDeflateEncoderTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerFrameDeflateEncoderTest.java index 5c085e9..4041f5c 100644 --- a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerFrameDeflateEncoderTest.java +++ b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerFrameDeflateEncoderTest.java @@ -15,8 +15,8 @@ */ package io.netty.handler.codec.http.websocketx.extensions.compression; -import static org.junit.Assert.*; import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; import io.netty.buffer.Unpooled; import io.netty.channel.embedded.EmbeddedChannel; import io.netty.handler.codec.compression.ZlibCodecFactory; @@ -24,11 +24,12 @@ import io.netty.handler.codec.compression.ZlibWrapper; import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame; import io.netty.handler.codec.http.websocketx.ContinuationWebSocketFrame; import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtension; +import org.junit.Test; -import java.util.Arrays; import java.util.Random; -import org.junit.Test; +import static io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionFilter.*; +import static org.junit.Assert.*; public class PerFrameDeflateEncoderTest { @@ -47,23 +48,22 @@ public class PerFrameDeflateEncoderTest { WebSocketExtension.RSV3, Unpooled.wrappedBuffer(payload)); // execute - encoderChannel.writeOutbound(frame); + assertTrue(encoderChannel.writeOutbound(frame)); BinaryWebSocketFrame compressedFrame = encoderChannel.readOutbound(); // test assertNotNull(compressedFrame); assertNotNull(compressedFrame.content()); - assertTrue(compressedFrame instanceof BinaryWebSocketFrame); assertEquals(WebSocketExtension.RSV1 | WebSocketExtension.RSV3, compressedFrame.rsv()); - decoderChannel.writeInbound(compressedFrame.content()); - decoderChannel.writeInbound(DeflateDecoder.FRAME_TAIL); + assertTrue(decoderChannel.writeInbound(compressedFrame.content())); + assertTrue(decoderChannel.writeInbound(DeflateDecoder.FRAME_TAIL.duplicate())); ByteBuf uncompressedPayload = decoderChannel.readInbound(); assertEquals(300, uncompressedPayload.readableBytes()); byte[] finalPayload = new byte[300]; uncompressedPayload.readBytes(finalPayload); - assertTrue(Arrays.equals(finalPayload, payload)); + assertArrayEquals(finalPayload, payload); uncompressedPayload.release(); } @@ -79,19 +79,18 @@ public class PerFrameDeflateEncoderTest { WebSocketExtension.RSV3 | WebSocketExtension.RSV1, Unpooled.wrappedBuffer(payload)); // execute - encoderChannel.writeOutbound(frame); + assertTrue(encoderChannel.writeOutbound(frame)); BinaryWebSocketFrame newFrame = encoderChannel.readOutbound(); // test assertNotNull(newFrame); assertNotNull(newFrame.content()); - assertTrue(newFrame instanceof BinaryWebSocketFrame); assertEquals(WebSocketExtension.RSV3 | WebSocketExtension.RSV1, newFrame.rsv()); assertEquals(300, newFrame.content().readableBytes()); byte[] finalPayload = new byte[300]; newFrame.content().readBytes(finalPayload); - assertTrue(Arrays.equals(finalPayload, payload)); + assertArrayEquals(finalPayload, payload); newFrame.release(); } @@ -117,9 +116,9 @@ public class PerFrameDeflateEncoderTest { WebSocketExtension.RSV3, Unpooled.wrappedBuffer(payload3)); // execute - encoderChannel.writeOutbound(frame1); - encoderChannel.writeOutbound(frame2); - encoderChannel.writeOutbound(frame3); + assertTrue(encoderChannel.writeOutbound(frame1)); + assertTrue(encoderChannel.writeOutbound(frame2)); + assertTrue(encoderChannel.writeOutbound(frame3)); BinaryWebSocketFrame compressedFrame1 = encoderChannel.readOutbound(); ContinuationWebSocketFrame compressedFrame2 = encoderChannel.readOutbound(); ContinuationWebSocketFrame compressedFrame3 = encoderChannel.readOutbound(); @@ -135,28 +134,52 @@ public class PerFrameDeflateEncoderTest { assertFalse(compressedFrame2.isFinalFragment()); assertTrue(compressedFrame3.isFinalFragment()); - decoderChannel.writeInbound(compressedFrame1.content()); - decoderChannel.writeInbound(Unpooled.wrappedBuffer(DeflateDecoder.FRAME_TAIL)); + assertTrue(decoderChannel.writeInbound(compressedFrame1.content())); + assertTrue(decoderChannel.writeInbound(DeflateDecoder.FRAME_TAIL.duplicate())); ByteBuf uncompressedPayload1 = decoderChannel.readInbound(); byte[] finalPayload1 = new byte[100]; uncompressedPayload1.readBytes(finalPayload1); - assertTrue(Arrays.equals(finalPayload1, payload1)); + assertArrayEquals(finalPayload1, payload1); uncompressedPayload1.release(); - decoderChannel.writeInbound(compressedFrame2.content()); - decoderChannel.writeInbound(Unpooled.wrappedBuffer(DeflateDecoder.FRAME_TAIL)); + assertTrue(decoderChannel.writeInbound(compressedFrame2.content())); + assertTrue(decoderChannel.writeInbound(DeflateDecoder.FRAME_TAIL.duplicate())); ByteBuf uncompressedPayload2 = decoderChannel.readInbound(); byte[] finalPayload2 = new byte[100]; uncompressedPayload2.readBytes(finalPayload2); - assertTrue(Arrays.equals(finalPayload2, payload2)); + assertArrayEquals(finalPayload2, payload2); uncompressedPayload2.release(); - decoderChannel.writeInbound(compressedFrame3.content()); - decoderChannel.writeInbound(Unpooled.wrappedBuffer(DeflateDecoder.FRAME_TAIL)); + assertTrue(decoderChannel.writeInbound(compressedFrame3.content())); + assertTrue(decoderChannel.writeInbound(DeflateDecoder.FRAME_TAIL.duplicate())); ByteBuf uncompressedPayload3 = decoderChannel.readInbound(); byte[] finalPayload3 = new byte[100]; uncompressedPayload3.readBytes(finalPayload3); - assertTrue(Arrays.equals(finalPayload3, payload3)); + assertArrayEquals(finalPayload3, payload3); uncompressedPayload3.release(); } + + @Test + public void testCompressionSkip() { + EmbeddedChannel encoderChannel = new EmbeddedChannel( + new PerFrameDeflateEncoder(9, 15, false, ALWAYS_SKIP)); + byte[] payload = new byte[300]; + random.nextBytes(payload); + BinaryWebSocketFrame binaryFrame = new BinaryWebSocketFrame(true, + 0, Unpooled.wrappedBuffer(payload)); + + // execute + assertTrue(encoderChannel.writeOutbound(binaryFrame.copy())); + BinaryWebSocketFrame outboundFrame = encoderChannel.readOutbound(); + + // test + assertNotNull(outboundFrame); + assertNotNull(outboundFrame.content()); + assertArrayEquals(payload, ByteBufUtil.getBytes(outboundFrame.content())); + assertEquals(0, outboundFrame.rsv()); + assertTrue(outboundFrame.release()); + + assertFalse(encoderChannel.finish()); + } + } diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateDecoderTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateDecoderTest.java index d5e5868..9529b44 100644 --- a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateDecoderTest.java +++ b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateDecoderTest.java @@ -15,20 +15,27 @@ */ package io.netty.handler.codec.http.websocketx.extensions.compression; -import static org.junit.Assert.*; import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; import io.netty.buffer.Unpooled; import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.handler.codec.DecoderException; import io.netty.handler.codec.compression.ZlibCodecFactory; import io.netty.handler.codec.compression.ZlibWrapper; import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame; import io.netty.handler.codec.http.websocketx.ContinuationWebSocketFrame; +import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; +import io.netty.handler.codec.http.websocketx.WebSocketFrame; import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtension; +import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionFilter; +import org.junit.Test; -import java.util.Arrays; import java.util.Random; -import org.junit.Test; +import static io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionFilter.*; +import static io.netty.handler.codec.http.websocketx.extensions.compression.DeflateDecoder.*; +import static io.netty.util.CharsetUtil.*; +import static org.junit.Assert.*; public class PerMessageDeflateDecoderTest { @@ -44,7 +51,7 @@ public class PerMessageDeflateDecoderTest { byte[] payload = new byte[300]; random.nextBytes(payload); - encoderChannel.writeOutbound(Unpooled.wrappedBuffer(payload)); + assertTrue(encoderChannel.writeOutbound(Unpooled.wrappedBuffer(payload))); ByteBuf compressedPayload = encoderChannel.readOutbound(); BinaryWebSocketFrame compressedFrame = new BinaryWebSocketFrame(true, @@ -52,19 +59,18 @@ public class PerMessageDeflateDecoderTest { compressedPayload.slice(0, compressedPayload.readableBytes() - 4)); // execute - decoderChannel.writeInbound(compressedFrame); + assertTrue(decoderChannel.writeInbound(compressedFrame)); BinaryWebSocketFrame uncompressedFrame = decoderChannel.readInbound(); // test assertNotNull(uncompressedFrame); assertNotNull(uncompressedFrame.content()); - assertTrue(uncompressedFrame instanceof BinaryWebSocketFrame); assertEquals(WebSocketExtension.RSV3, uncompressedFrame.rsv()); assertEquals(300, uncompressedFrame.content().readableBytes()); byte[] finalPayload = new byte[300]; uncompressedFrame.content().readBytes(finalPayload); - assertTrue(Arrays.equals(finalPayload, payload)); + assertArrayEquals(finalPayload, payload); uncompressedFrame.release(); } @@ -80,24 +86,23 @@ public class PerMessageDeflateDecoderTest { WebSocketExtension.RSV3, Unpooled.wrappedBuffer(payload)); // execute - decoderChannel.writeInbound(frame); + assertTrue(decoderChannel.writeInbound(frame)); BinaryWebSocketFrame newFrame = decoderChannel.readInbound(); // test assertNotNull(newFrame); assertNotNull(newFrame.content()); - assertTrue(newFrame instanceof BinaryWebSocketFrame); assertEquals(WebSocketExtension.RSV3, newFrame.rsv()); assertEquals(300, newFrame.content().readableBytes()); byte[] finalPayload = new byte[300]; newFrame.content().readBytes(finalPayload); - assertTrue(Arrays.equals(finalPayload, payload)); + assertArrayEquals(finalPayload, payload); newFrame.release(); } @Test - public void testFramementedFrame() { + public void testFragmentedFrame() { EmbeddedChannel encoderChannel = new EmbeddedChannel( ZlibCodecFactory.newZlibEncoder(ZlibWrapper.NONE, 9, 15, 8)); EmbeddedChannel decoderChannel = new EmbeddedChannel(new PerMessageDeflateDecoder(false)); @@ -106,7 +111,7 @@ public class PerMessageDeflateDecoderTest { byte[] payload = new byte[300]; random.nextBytes(payload); - encoderChannel.writeOutbound(Unpooled.wrappedBuffer(payload)); + assertTrue(encoderChannel.writeOutbound(Unpooled.wrappedBuffer(payload))); ByteBuf compressedPayload = encoderChannel.readOutbound(); compressedPayload = compressedPayload.slice(0, compressedPayload.readableBytes() - 4); @@ -121,9 +126,9 @@ public class PerMessageDeflateDecoderTest { compressedPayload.readableBytes() - oneThird * 2)); // execute - decoderChannel.writeInbound(compressedFrame1.retain()); - decoderChannel.writeInbound(compressedFrame2.retain()); - decoderChannel.writeInbound(compressedFrame3); + assertTrue(decoderChannel.writeInbound(compressedFrame1.retain())); + assertTrue(decoderChannel.writeInbound(compressedFrame2.retain())); + assertTrue(decoderChannel.writeInbound(compressedFrame3)); BinaryWebSocketFrame uncompressedFrame1 = decoderChannel.readInbound(); ContinuationWebSocketFrame uncompressedFrame2 = decoderChannel.readInbound(); ContinuationWebSocketFrame uncompressedFrame3 = decoderChannel.readInbound(); @@ -142,7 +147,7 @@ public class PerMessageDeflateDecoderTest { byte[] finalPayload = new byte[300]; finalPayloadWrapped.readBytes(finalPayload); - assertTrue(Arrays.equals(finalPayload, payload)); + assertArrayEquals(finalPayload, payload); finalPayloadWrapped.release(); } @@ -158,9 +163,9 @@ public class PerMessageDeflateDecoderTest { byte[] payload2 = new byte[100]; random.nextBytes(payload2); - encoderChannel.writeOutbound(Unpooled.wrappedBuffer(payload1)); + assertTrue(encoderChannel.writeOutbound(Unpooled.wrappedBuffer(payload1))); ByteBuf compressedPayload1 = encoderChannel.readOutbound(); - encoderChannel.writeOutbound(Unpooled.wrappedBuffer(payload2)); + assertTrue(encoderChannel.writeOutbound(Unpooled.wrappedBuffer(payload2))); ByteBuf compressedPayload2 = encoderChannel.readOutbound(); BinaryWebSocketFrame compressedFrame = new BinaryWebSocketFrame(true, @@ -170,23 +175,215 @@ public class PerMessageDeflateDecoderTest { compressedPayload2.slice(0, compressedPayload2.readableBytes() - 4))); // execute - decoderChannel.writeInbound(compressedFrame); + assertTrue(decoderChannel.writeInbound(compressedFrame)); BinaryWebSocketFrame uncompressedFrame = decoderChannel.readInbound(); // test assertNotNull(uncompressedFrame); assertNotNull(uncompressedFrame.content()); - assertTrue(uncompressedFrame instanceof BinaryWebSocketFrame); assertEquals(WebSocketExtension.RSV3, uncompressedFrame.rsv()); assertEquals(200, uncompressedFrame.content().readableBytes()); byte[] finalPayload1 = new byte[100]; uncompressedFrame.content().readBytes(finalPayload1); - assertTrue(Arrays.equals(finalPayload1, payload1)); + assertArrayEquals(finalPayload1, payload1); byte[] finalPayload2 = new byte[100]; uncompressedFrame.content().readBytes(finalPayload2); - assertTrue(Arrays.equals(finalPayload2, payload2)); + assertArrayEquals(finalPayload2, payload2); uncompressedFrame.release(); } + @Test + public void testDecompressionSkipForBinaryFrame() { + EmbeddedChannel encoderChannel = new EmbeddedChannel( + ZlibCodecFactory.newZlibEncoder(ZlibWrapper.NONE, 9, 15, 8)); + EmbeddedChannel decoderChannel = new EmbeddedChannel(new PerMessageDeflateDecoder(false, ALWAYS_SKIP)); + + byte[] payload = new byte[300]; + random.nextBytes(payload); + + assertTrue(encoderChannel.writeOutbound(Unpooled.wrappedBuffer(payload))); + ByteBuf compressedPayload = encoderChannel.readOutbound(); + + BinaryWebSocketFrame compressedBinaryFrame = new BinaryWebSocketFrame(true, WebSocketExtension.RSV1, + compressedPayload); + assertTrue(decoderChannel.writeInbound(compressedBinaryFrame)); + + WebSocketFrame inboundFrame = decoderChannel.readInbound(); + + assertEquals(WebSocketExtension.RSV1, inboundFrame.rsv()); + assertEquals(compressedPayload, inboundFrame.content()); + assertTrue(inboundFrame.release()); + + assertTrue(encoderChannel.finishAndReleaseAll()); + assertFalse(decoderChannel.finish()); + } + + @Test + public void testSelectivityDecompressionSkip() { + WebSocketExtensionFilter selectivityDecompressionFilter = new WebSocketExtensionFilter() { + @Override + public boolean mustSkip(WebSocketFrame frame) { + return frame instanceof TextWebSocketFrame && frame.content().readableBytes() < 100; + } + }; + EmbeddedChannel encoderChannel = new EmbeddedChannel( + ZlibCodecFactory.newZlibEncoder(ZlibWrapper.NONE, 9, 15, 8)); + EmbeddedChannel decoderChannel = new EmbeddedChannel( + new PerMessageDeflateDecoder(false, selectivityDecompressionFilter)); + + String textPayload = "compressed payload"; + byte[] binaryPayload = new byte[300]; + random.nextBytes(binaryPayload); + + assertTrue(encoderChannel.writeOutbound(Unpooled.wrappedBuffer(textPayload.getBytes(UTF_8)))); + assertTrue(encoderChannel.writeOutbound(Unpooled.wrappedBuffer(binaryPayload))); + ByteBuf compressedTextPayload = encoderChannel.readOutbound(); + ByteBuf compressedBinaryPayload = encoderChannel.readOutbound(); + + TextWebSocketFrame compressedTextFrame = new TextWebSocketFrame(true, WebSocketExtension.RSV1, + compressedTextPayload); + BinaryWebSocketFrame compressedBinaryFrame = new BinaryWebSocketFrame(true, WebSocketExtension.RSV1, + compressedBinaryPayload); + + assertTrue(decoderChannel.writeInbound(compressedTextFrame)); + assertTrue(decoderChannel.writeInbound(compressedBinaryFrame)); + + TextWebSocketFrame inboundTextFrame = decoderChannel.readInbound(); + BinaryWebSocketFrame inboundBinaryFrame = decoderChannel.readInbound(); + + assertEquals(WebSocketExtension.RSV1, inboundTextFrame.rsv()); + assertEquals(compressedTextPayload, inboundTextFrame.content()); + assertTrue(inboundTextFrame.release()); + + assertEquals(0, inboundBinaryFrame.rsv()); + assertArrayEquals(binaryPayload, ByteBufUtil.getBytes(inboundBinaryFrame.content())); + assertTrue(inboundBinaryFrame.release()); + + assertTrue(encoderChannel.finishAndReleaseAll()); + assertFalse(decoderChannel.finish()); + } + + @Test(expected = DecoderException.class) + public void testIllegalStateWhenDecompressionInProgress() { + WebSocketExtensionFilter selectivityDecompressionFilter = new WebSocketExtensionFilter() { + @Override + public boolean mustSkip(WebSocketFrame frame) { + return frame.content().readableBytes() < 100; + } + }; + + EmbeddedChannel encoderChannel = new EmbeddedChannel( + ZlibCodecFactory.newZlibEncoder(ZlibWrapper.NONE, 9, 15, 8)); + EmbeddedChannel decoderChannel = new EmbeddedChannel( + new PerMessageDeflateDecoder(false, selectivityDecompressionFilter)); + + byte[] firstPayload = new byte[200]; + random.nextBytes(firstPayload); + + byte[] finalPayload = new byte[50]; + random.nextBytes(finalPayload); + + assertTrue(encoderChannel.writeOutbound(Unpooled.wrappedBuffer(firstPayload))); + assertTrue(encoderChannel.writeOutbound(Unpooled.wrappedBuffer(finalPayload))); + ByteBuf compressedFirstPayload = encoderChannel.readOutbound(); + ByteBuf compressedFinalPayload = encoderChannel.readOutbound(); + assertTrue(encoderChannel.finishAndReleaseAll()); + + BinaryWebSocketFrame firstPart = new BinaryWebSocketFrame(false, WebSocketExtension.RSV1, + compressedFirstPayload); + ContinuationWebSocketFrame finalPart = new ContinuationWebSocketFrame(true, WebSocketExtension.RSV1, + compressedFinalPayload); + assertTrue(decoderChannel.writeInbound(firstPart)); + + BinaryWebSocketFrame outboundFirstPart = decoderChannel.readInbound(); + //first part is decompressed + assertEquals(0, outboundFirstPart.rsv()); + assertArrayEquals(firstPayload, ByteBufUtil.getBytes(outboundFirstPart.content())); + assertTrue(outboundFirstPart.release()); + + //final part throwing exception + try { + decoderChannel.writeInbound(finalPart); + } finally { + assertTrue(finalPart.release()); + assertFalse(encoderChannel.finishAndReleaseAll()); + } + } + + @Test + public void testEmptyFrameDecompression() { + EmbeddedChannel decoderChannel = new EmbeddedChannel(new PerMessageDeflateDecoder(false)); + + TextWebSocketFrame emptyDeflateBlockFrame = new TextWebSocketFrame(true, WebSocketExtension.RSV1, + EMPTY_DEFLATE_BLOCK); + + assertTrue(decoderChannel.writeInbound(emptyDeflateBlockFrame)); + TextWebSocketFrame emptyBufferFrame = decoderChannel.readInbound(); + + assertFalse(emptyBufferFrame.content().isReadable()); + + // Composite empty buffer + assertTrue(emptyBufferFrame.release()); + assertFalse(decoderChannel.finish()); + } + + @Test + public void testFragmentedFrameWithLeftOverInLastFragment() { + String hexDump = "677170647a777a737574656b707a787a6f6a7561756578756f6b7868616371716c657a6d64697479766d726f6" + + "269746c6376777464776f6f72767a726f64667278676764687775786f6762766d776d706b76697773777a7072" + + "6a6a737279707a7078697a6c69616d7461656d646278626d786f66666e686e776a7a7461746d7a776668776b6" + + "f6f736e73746575637a6d727a7175707a6e74627578687871767771697a71766c64626d78726d6d7675756877" + + "62667963626b687a726d676e646263776e67797264706d6c6863626577616967706a78636a72697464756e627" + + "977616f79736475676f76736f7178746a7a7479626c64636b6b6778637768746c62"; + EmbeddedChannel encoderChannel = new EmbeddedChannel( + ZlibCodecFactory.newZlibEncoder(ZlibWrapper.NONE, 9, 15, 8)); + EmbeddedChannel decoderChannel = new EmbeddedChannel(new PerMessageDeflateDecoder(false)); + + ByteBuf originPayload = Unpooled.wrappedBuffer(ByteBufUtil.decodeHexDump(hexDump)); + assertTrue(encoderChannel.writeOutbound(originPayload.duplicate().retain())); + + ByteBuf compressedPayload = encoderChannel.readOutbound(); + compressedPayload = compressedPayload.slice(0, compressedPayload.readableBytes() - 4); + + int oneThird = compressedPayload.readableBytes() / 3; + + TextWebSocketFrame compressedFrame1 = new TextWebSocketFrame( + false, WebSocketExtension.RSV1, compressedPayload.slice(0, oneThird)); + ContinuationWebSocketFrame compressedFrame2 = new ContinuationWebSocketFrame( + false, WebSocketExtension.RSV3, compressedPayload.slice(oneThird, oneThird)); + ContinuationWebSocketFrame compressedFrame3 = new ContinuationWebSocketFrame( + false, WebSocketExtension.RSV3, compressedPayload.slice(oneThird * 2, oneThird)); + int offset = oneThird * 3; + ContinuationWebSocketFrame compressedFrameWithExtraData = new ContinuationWebSocketFrame( + true, WebSocketExtension.RSV3, compressedPayload.slice(offset, + compressedPayload.readableBytes() - offset)); + + // check that last fragment contains only one extra byte + assertEquals(1, compressedFrameWithExtraData.content().readableBytes()); + assertEquals(1, compressedFrameWithExtraData.content().getByte(0)); + + // write compressed frames + assertTrue(decoderChannel.writeInbound(compressedFrame1.retain())); + assertTrue(decoderChannel.writeInbound(compressedFrame2.retain())); + assertTrue(decoderChannel.writeInbound(compressedFrame3.retain())); + assertTrue(decoderChannel.writeInbound(compressedFrameWithExtraData)); + + // read uncompressed frames + TextWebSocketFrame uncompressedFrame1 = decoderChannel.readInbound(); + ContinuationWebSocketFrame uncompressedFrame2 = decoderChannel.readInbound(); + ContinuationWebSocketFrame uncompressedFrame3 = decoderChannel.readInbound(); + ContinuationWebSocketFrame uncompressedExtraData = decoderChannel.readInbound(); + assertFalse(uncompressedExtraData.content().isReadable()); + + ByteBuf uncompressedPayload = Unpooled.wrappedBuffer(uncompressedFrame1.content(), uncompressedFrame2.content(), + uncompressedFrame3.content(), uncompressedExtraData.content()); + assertEquals(originPayload, uncompressedPayload); + + assertTrue(originPayload.release()); + assertTrue(uncompressedPayload.release()); + + assertTrue(encoderChannel.finishAndReleaseAll()); + assertFalse(decoderChannel.finish()); + } } diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateEncoderTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateEncoderTest.java index 66ae962..1f8b477 100644 --- a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateEncoderTest.java +++ b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateEncoderTest.java @@ -15,20 +15,28 @@ */ package io.netty.handler.codec.http.websocketx.extensions.compression; -import static org.junit.Assert.*; import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; import io.netty.buffer.Unpooled; import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.handler.codec.EncoderException; import io.netty.handler.codec.compression.ZlibCodecFactory; import io.netty.handler.codec.compression.ZlibWrapper; import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame; import io.netty.handler.codec.http.websocketx.ContinuationWebSocketFrame; +import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; +import io.netty.handler.codec.http.websocketx.WebSocketFrame; import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtension; +import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionFilter; +import org.junit.Test; import java.util.Arrays; import java.util.Random; -import org.junit.Test; +import static io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionFilter.*; +import static io.netty.handler.codec.http.websocketx.extensions.compression.DeflateDecoder.*; +import static io.netty.util.CharsetUtil.*; +import static org.junit.Assert.*; public class PerMessageDeflateEncoderTest { @@ -44,26 +52,25 @@ public class PerMessageDeflateEncoderTest { byte[] payload = new byte[300]; random.nextBytes(payload); BinaryWebSocketFrame frame = new BinaryWebSocketFrame(true, - WebSocketExtension.RSV3, Unpooled.wrappedBuffer(payload)); + WebSocketExtension.RSV3, Unpooled.wrappedBuffer(payload)); // execute - encoderChannel.writeOutbound(frame); + assertTrue(encoderChannel.writeOutbound(frame)); BinaryWebSocketFrame compressedFrame = encoderChannel.readOutbound(); // test assertNotNull(compressedFrame); assertNotNull(compressedFrame.content()); - assertTrue(compressedFrame instanceof BinaryWebSocketFrame); assertEquals(WebSocketExtension.RSV1 | WebSocketExtension.RSV3, compressedFrame.rsv()); - decoderChannel.writeInbound(compressedFrame.content()); - decoderChannel.writeInbound(DeflateDecoder.FRAME_TAIL); + assertTrue(decoderChannel.writeInbound(compressedFrame.content())); + assertTrue(decoderChannel.writeInbound(DeflateDecoder.FRAME_TAIL.duplicate())); ByteBuf uncompressedPayload = decoderChannel.readInbound(); assertEquals(300, uncompressedPayload.readableBytes()); byte[] finalPayload = new byte[300]; uncompressedPayload.readBytes(finalPayload); - assertTrue(Arrays.equals(finalPayload, payload)); + assertArrayEquals(finalPayload, payload); uncompressedPayload.release(); } @@ -76,28 +83,29 @@ public class PerMessageDeflateEncoderTest { random.nextBytes(payload); BinaryWebSocketFrame frame = new BinaryWebSocketFrame(true, - WebSocketExtension.RSV3 | WebSocketExtension.RSV1, Unpooled.wrappedBuffer(payload)); + WebSocketExtension.RSV3 | WebSocketExtension.RSV1, + Unpooled.wrappedBuffer(payload)); // execute - encoderChannel.writeOutbound(frame); + assertTrue(encoderChannel.writeOutbound(frame)); BinaryWebSocketFrame newFrame = encoderChannel.readOutbound(); // test assertNotNull(newFrame); assertNotNull(newFrame.content()); - assertTrue(newFrame instanceof BinaryWebSocketFrame); assertEquals(WebSocketExtension.RSV3 | WebSocketExtension.RSV1, newFrame.rsv()); assertEquals(300, newFrame.content().readableBytes()); byte[] finalPayload = new byte[300]; newFrame.content().readBytes(finalPayload); - assertTrue(Arrays.equals(finalPayload, payload)); + assertArrayEquals(finalPayload, payload); newFrame.release(); } @Test - public void testFramementedFrame() { - EmbeddedChannel encoderChannel = new EmbeddedChannel(new PerMessageDeflateEncoder(9, 15, false)); + public void testFragmentedFrame() { + EmbeddedChannel encoderChannel = new EmbeddedChannel(new PerMessageDeflateEncoder(9, 15, false, + NEVER_SKIP)); EmbeddedChannel decoderChannel = new EmbeddedChannel( ZlibCodecFactory.newZlibDecoder(ZlibWrapper.NONE)); @@ -110,16 +118,19 @@ public class PerMessageDeflateEncoderTest { random.nextBytes(payload3); BinaryWebSocketFrame frame1 = new BinaryWebSocketFrame(false, - WebSocketExtension.RSV3, Unpooled.wrappedBuffer(payload1)); + WebSocketExtension.RSV3, + Unpooled.wrappedBuffer(payload1)); ContinuationWebSocketFrame frame2 = new ContinuationWebSocketFrame(false, - WebSocketExtension.RSV3, Unpooled.wrappedBuffer(payload2)); + WebSocketExtension.RSV3, + Unpooled.wrappedBuffer(payload2)); ContinuationWebSocketFrame frame3 = new ContinuationWebSocketFrame(true, - WebSocketExtension.RSV3, Unpooled.wrappedBuffer(payload3)); + WebSocketExtension.RSV3, + Unpooled.wrappedBuffer(payload3)); // execute - encoderChannel.writeOutbound(frame1); - encoderChannel.writeOutbound(frame2); - encoderChannel.writeOutbound(frame3); + assertTrue(encoderChannel.writeOutbound(frame1)); + assertTrue(encoderChannel.writeOutbound(frame2)); + assertTrue(encoderChannel.writeOutbound(frame3)); BinaryWebSocketFrame compressedFrame1 = encoderChannel.readOutbound(); ContinuationWebSocketFrame compressedFrame2 = encoderChannel.readOutbound(); ContinuationWebSocketFrame compressedFrame3 = encoderChannel.readOutbound(); @@ -135,26 +146,163 @@ public class PerMessageDeflateEncoderTest { assertFalse(compressedFrame2.isFinalFragment()); assertTrue(compressedFrame3.isFinalFragment()); - decoderChannel.writeInbound(compressedFrame1.content()); + assertTrue(decoderChannel.writeInbound(compressedFrame1.content())); ByteBuf uncompressedPayload1 = decoderChannel.readInbound(); byte[] finalPayload1 = new byte[100]; uncompressedPayload1.readBytes(finalPayload1); - assertTrue(Arrays.equals(finalPayload1, payload1)); + assertArrayEquals(finalPayload1, payload1); uncompressedPayload1.release(); - decoderChannel.writeInbound(compressedFrame2.content()); + assertTrue(decoderChannel.writeInbound(compressedFrame2.content())); ByteBuf uncompressedPayload2 = decoderChannel.readInbound(); byte[] finalPayload2 = new byte[100]; uncompressedPayload2.readBytes(finalPayload2); - assertTrue(Arrays.equals(finalPayload2, payload2)); + assertArrayEquals(finalPayload2, payload2); uncompressedPayload2.release(); - decoderChannel.writeInbound(compressedFrame3.content()); - decoderChannel.writeInbound(DeflateDecoder.FRAME_TAIL); + assertTrue(decoderChannel.writeInbound(compressedFrame3.content())); + assertTrue(decoderChannel.writeInbound(DeflateDecoder.FRAME_TAIL.duplicate())); ByteBuf uncompressedPayload3 = decoderChannel.readInbound(); byte[] finalPayload3 = new byte[100]; uncompressedPayload3.readBytes(finalPayload3); - assertTrue(Arrays.equals(finalPayload3, payload3)); + assertArrayEquals(finalPayload3, payload3); uncompressedPayload3.release(); } + + @Test + public void testCompressionSkipForBinaryFrame() { + EmbeddedChannel encoderChannel = new EmbeddedChannel(new PerMessageDeflateEncoder(9, 15, false, + ALWAYS_SKIP)); + byte[] payload = new byte[300]; + random.nextBytes(payload); + + WebSocketFrame binaryFrame = new BinaryWebSocketFrame(Unpooled.wrappedBuffer(payload)); + + assertTrue(encoderChannel.writeOutbound(binaryFrame.copy())); + WebSocketFrame outboundFrame = encoderChannel.readOutbound(); + + assertEquals(0, outboundFrame.rsv()); + assertArrayEquals(payload, ByteBufUtil.getBytes(outboundFrame.content())); + assertTrue(outboundFrame.release()); + + assertFalse(encoderChannel.finish()); + } + + @Test + public void testSelectivityCompressionSkip() { + WebSocketExtensionFilter selectivityCompressionFilter = new WebSocketExtensionFilter() { + @Override + public boolean mustSkip(WebSocketFrame frame) { + return (frame instanceof TextWebSocketFrame || frame instanceof BinaryWebSocketFrame) + && frame.content().readableBytes() < 100; + } + }; + EmbeddedChannel encoderChannel = new EmbeddedChannel( + new PerMessageDeflateEncoder(9, 15, false, selectivityCompressionFilter)); + EmbeddedChannel decoderChannel = new EmbeddedChannel( + ZlibCodecFactory.newZlibDecoder(ZlibWrapper.NONE)); + + String textPayload = "not compressed payload"; + byte[] binaryPayload = new byte[101]; + random.nextBytes(binaryPayload); + + WebSocketFrame textFrame = new TextWebSocketFrame(textPayload); + BinaryWebSocketFrame binaryFrame = new BinaryWebSocketFrame(Unpooled.wrappedBuffer(binaryPayload)); + + assertTrue(encoderChannel.writeOutbound(textFrame)); + assertTrue(encoderChannel.writeOutbound(binaryFrame)); + + WebSocketFrame outboundTextFrame = encoderChannel.readOutbound(); + + //compression skipped for textFrame + assertEquals(0, outboundTextFrame.rsv()); + assertEquals(textPayload, outboundTextFrame.content().toString(UTF_8)); + assertTrue(outboundTextFrame.release()); + + WebSocketFrame outboundBinaryFrame = encoderChannel.readOutbound(); + + //compression not skipped for binaryFrame + assertEquals(WebSocketExtension.RSV1, outboundBinaryFrame.rsv()); + + assertTrue(decoderChannel.writeInbound(outboundBinaryFrame.content().retain())); + ByteBuf uncompressedBinaryPayload = decoderChannel.readInbound(); + + assertArrayEquals(binaryPayload, ByteBufUtil.getBytes(uncompressedBinaryPayload)); + + assertTrue(outboundBinaryFrame.release()); + assertTrue(uncompressedBinaryPayload.release()); + + assertFalse(encoderChannel.finish()); + assertFalse(decoderChannel.finish()); + } + + @Test(expected = EncoderException.class) + public void testIllegalStateWhenCompressionInProgress() { + WebSocketExtensionFilter selectivityCompressionFilter = new WebSocketExtensionFilter() { + @Override + public boolean mustSkip(WebSocketFrame frame) { + return frame.content().readableBytes() < 100; + } + }; + EmbeddedChannel encoderChannel = new EmbeddedChannel( + new PerMessageDeflateEncoder(9, 15, false, selectivityCompressionFilter)); + + byte[] firstPayload = new byte[200]; + random.nextBytes(firstPayload); + + byte[] finalPayload = new byte[90]; + random.nextBytes(finalPayload); + + BinaryWebSocketFrame firstPart = new BinaryWebSocketFrame(false, 0, Unpooled.wrappedBuffer(firstPayload)); + ContinuationWebSocketFrame finalPart = new ContinuationWebSocketFrame(true, 0, + Unpooled.wrappedBuffer(finalPayload)); + assertTrue(encoderChannel.writeOutbound(firstPart)); + + BinaryWebSocketFrame outboundFirstPart = encoderChannel.readOutbound(); + //first part is compressed + assertEquals(WebSocketExtension.RSV1, outboundFirstPart.rsv()); + assertFalse(Arrays.equals(firstPayload, ByteBufUtil.getBytes(outboundFirstPart.content()))); + assertTrue(outboundFirstPart.release()); + + //final part throwing exception + try { + encoderChannel.writeOutbound(finalPart); + } finally { + assertTrue(finalPart.release()); + assertFalse(encoderChannel.finishAndReleaseAll()); + } + } + + @Test + public void testEmptyFrameCompression() { + EmbeddedChannel encoderChannel = new EmbeddedChannel(new PerMessageDeflateEncoder(9, 15, false)); + + TextWebSocketFrame emptyFrame = new TextWebSocketFrame(""); + + assertTrue(encoderChannel.writeOutbound(emptyFrame)); + TextWebSocketFrame emptyDeflateFrame = encoderChannel.readOutbound(); + + assertEquals(WebSocketExtension.RSV1, emptyDeflateFrame.rsv()); + assertTrue(ByteBufUtil.equals(EMPTY_DEFLATE_BLOCK, emptyDeflateFrame.content())); + // Unreleasable buffer + assertFalse(emptyDeflateFrame.release()); + + assertFalse(encoderChannel.finish()); + } + + @Test(expected = EncoderException.class) + public void testCodecExceptionForNotFinEmptyFrame() { + EmbeddedChannel encoderChannel = new EmbeddedChannel(new PerMessageDeflateEncoder(9, 15, false)); + + TextWebSocketFrame emptyNotFinFrame = new TextWebSocketFrame(false, 0, ""); + + try { + encoderChannel.writeOutbound(emptyNotFinFrame); + } finally { + // EmptyByteBuf buffer + assertFalse(emptyNotFinFrame.release()); + assertFalse(encoderChannel.finish()); + } + } + } diff --git a/codec-http/src/test/java/io/netty/handler/codec/rtsp/RtspDecoderTest.java b/codec-http/src/test/java/io/netty/handler/codec/rtsp/RtspDecoderTest.java index d416720..631ba2a 100644 --- a/codec-http/src/test/java/io/netty/handler/codec/rtsp/RtspDecoderTest.java +++ b/codec-http/src/test/java/io/netty/handler/codec/rtsp/RtspDecoderTest.java @@ -65,7 +65,6 @@ public class RtspDecoderTest { ((FullHttpRequest) res1).release(); HttpObject res2 = ch.readInbound(); - System.out.println(res2); assertNotNull(res2); assertTrue(res2 instanceof FullHttpResponse); ((FullHttpResponse) res2).release(); diff --git a/codec-http/src/test/resources/file-03.txt b/codec-http/src/test/resources/file-03.txt new file mode 100644 index 0000000..b545f1b --- /dev/null +++ b/codec-http/src/test/resources/file-03.txt @@ -0,0 +1 @@ +File 03 diff --git a/codec-http2/pom.xml b/codec-http2/pom.xml index 695f3b8..1818182 100644 --- a/codec-http2/pom.xml +++ b/codec-http2/pom.xml @@ -20,7 +20,7 @@ <parent> <groupId>io.netty</groupId> <artifactId>netty-parent</artifactId> - <version>4.1.33.Final</version> + <version>4.1.48.Final</version> </parent> <artifactId>netty-codec-http2</artifactId> diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/AbstractHttp2ConnectionHandlerBuilder.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/AbstractHttp2ConnectionHandlerBuilder.java index 7c52cd2..f262b11 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/AbstractHttp2ConnectionHandlerBuilder.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/AbstractHttp2ConnectionHandlerBuilder.java @@ -16,17 +16,16 @@ package io.netty.handler.codec.http2; +import io.netty.channel.Channel; import io.netty.handler.codec.http2.Http2HeadersEncoder.SensitivityDetector; +import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.UnstableApi; import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_HEADER_LIST_SIZE; -import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_INITIAL_HUFFMAN_DECODE_CAPACITY; import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_MAX_RESERVED_STREAMS; +import static io.netty.handler.codec.http2.Http2PromisedRequestVerifier.ALWAYS_VERIFY; import static io.netty.util.internal.ObjectUtil.checkNotNull; -import static io.netty.util.internal.ObjectUtil.checkPositive; import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero; -import static java.util.concurrent.TimeUnit.MILLISECONDS; -import static java.util.concurrent.TimeUnit.SECONDS; /** * Abstract base class which defines commonly used features required to build {@link Http2ConnectionHandler} instances. @@ -64,7 +63,6 @@ import static java.util.concurrent.TimeUnit.SECONDS; * <li>{@link #headerSensitivityDetector(SensitivityDetector)}</li> * <li>{@link #encoderEnforceMaxConcurrentStreams(boolean)}</li> * <li>{@link #encoderIgnoreMaxHeaderListSize(boolean)}</li> - * <li>{@link #initialHuffmanDecodeCapacity(int)}</li> * </ul> * * <h3>Exposing necessary methods in a subclass</h3> @@ -84,9 +82,10 @@ public abstract class AbstractHttp2ConnectionHandlerBuilder<T extends Http2Conne private Http2Settings initialSettings = Http2Settings.defaultSettings(); private Http2FrameListener frameListener; private long gracefulShutdownTimeoutMillis = Http2CodecUtil.DEFAULT_GRACEFUL_SHUTDOWN_TIMEOUT_MILLIS; + private boolean decoupleCloseAndGoAway; // The property that will prohibit connection() and codec() if set by server(), - // because this property is used only when this builder creates a Http2Connection. + // because this property is used only when this builder creates an Http2Connection. private Boolean isServer; private Integer maxReservedStreams; @@ -105,7 +104,11 @@ public abstract class AbstractHttp2ConnectionHandlerBuilder<T extends Http2Conne private SensitivityDetector headerSensitivityDetector; private Boolean encoderEnforceMaxConcurrentStreams; private Boolean encoderIgnoreMaxHeaderListSize; - private int initialHuffmanDecodeCapacity = DEFAULT_INITIAL_HUFFMAN_DECODE_CAPACITY; + private Http2PromisedRequestVerifier promisedRequestVerifier = ALWAYS_VERIFY; + private boolean autoAckSettingsFrame = true; + private boolean autoAckPingFrame = true; + private int maxQueuedControlFrames = Http2CodecUtil.DEFAULT_MAX_QUEUED_CONTROL_FRAMES; + private int maxConsecutiveEmptyFrames = 2; /** * Sets the {@link Http2Settings} to use for the initial connection settings exchange. @@ -324,6 +327,30 @@ public abstract class AbstractHttp2ConnectionHandlerBuilder<T extends Http2Conne return self(); } + /** + * Returns the maximum number of queued control frames that are allowed before the connection is closed. + * This allows to protected against various attacks that can lead to high CPU / memory usage if the remote-peer + * floods us with frames that would have us produce control frames, but stops to read from the underlying socket. + * + * {@code 0} means no protection is in place. + */ + protected int encoderEnforceMaxQueuedControlFrames() { + return maxQueuedControlFrames; + } + + /** + * Sets the maximum number of queued control frames that are allowed before the connection is closed. + * This allows to protected against various attacks that can lead to high CPU / memory usage if the remote-peer + * floods us with frames that would have us produce control frames, but stops to read from the underlying socket. + * + * {@code 0} means no protection should be applied. + */ + protected B encoderEnforceMaxQueuedControlFrames(int maxQueuedControlFrames) { + enforceNonCodecConstraints("encoderEnforceMaxQueuedControlFrames"); + this.maxQueuedControlFrames = ObjectUtil.checkPositiveOrZero(maxQueuedControlFrames, "maxQueuedControlFrames"); + return self(); + } + /** * Returns the {@link SensitivityDetector} to use. */ @@ -354,16 +381,112 @@ public abstract class AbstractHttp2ConnectionHandlerBuilder<T extends Http2Conne } /** - * Sets the initial size of an intermediate buffer used during HPACK huffman decoding. - * @param initialHuffmanDecodeCapacity initial size of an intermediate buffer used during HPACK huffman decoding. - * @return this. + * Does nothing, do not call. + * + * @deprecated Huffman decoding no longer depends on having a decode capacity. */ + @Deprecated protected B initialHuffmanDecodeCapacity(int initialHuffmanDecodeCapacity) { - enforceNonCodecConstraints("initialHuffmanDecodeCapacity"); - this.initialHuffmanDecodeCapacity = checkPositive(initialHuffmanDecodeCapacity, "initialHuffmanDecodeCapacity"); return self(); } + /** + * Set the {@link Http2PromisedRequestVerifier} to use. + * @return this. + */ + protected B promisedRequestVerifier(Http2PromisedRequestVerifier promisedRequestVerifier) { + enforceNonCodecConstraints("promisedRequestVerifier"); + this.promisedRequestVerifier = checkNotNull(promisedRequestVerifier, "promisedRequestVerifier"); + return self(); + } + + /** + * Get the {@link Http2PromisedRequestVerifier} to use. + * @return the {@link Http2PromisedRequestVerifier} to use. + */ + protected Http2PromisedRequestVerifier promisedRequestVerifier() { + return promisedRequestVerifier; + } + + /** + * Returns the maximum number of consecutive empty DATA frames (without end_of_stream flag) that are allowed before + * the connection is closed. This allows to protected against the remote peer flooding us with such frames and + * so use up a lot of CPU. There is no valid use-case for empty DATA frames without end_of_stream flag. + * + * {@code 0} means no protection is in place. + */ + protected int decoderEnforceMaxConsecutiveEmptyDataFrames() { + return maxConsecutiveEmptyFrames; + } + + /** + * Sets the maximum number of consecutive empty DATA frames (without end_of_stream flag) that are allowed before + * the connection is closed. This allows to protected against the remote peer flooding us with such frames and + * so use up a lot of CPU. There is no valid use-case for empty DATA frames without end_of_stream flag. + * + * {@code 0} means no protection should be applied. + */ + protected B decoderEnforceMaxConsecutiveEmptyDataFrames(int maxConsecutiveEmptyFrames) { + enforceNonCodecConstraints("maxConsecutiveEmptyFrames"); + this.maxConsecutiveEmptyFrames = ObjectUtil.checkPositiveOrZero( + maxConsecutiveEmptyFrames, "maxConsecutiveEmptyFrames"); + return self(); + } + + /** + * Determine if settings frame should automatically be acknowledged and applied. + * @return this. + */ + protected B autoAckSettingsFrame(boolean autoAckSettings) { + enforceNonCodecConstraints("autoAckSettingsFrame"); + this.autoAckSettingsFrame = autoAckSettings; + return self(); + } + + /** + * Determine if the SETTINGS frames should be automatically acknowledged and applied. + * @return {@code true} if the SETTINGS frames should be automatically acknowledged and applied. + */ + protected boolean isAutoAckSettingsFrame() { + return autoAckSettingsFrame; + } + + /** + * Determine if PING frame should automatically be acknowledged or not. + * @return this. + */ + protected B autoAckPingFrame(boolean autoAckPingFrame) { + enforceNonCodecConstraints("autoAckPingFrame"); + this.autoAckPingFrame = autoAckPingFrame; + return self(); + } + + /** + * Determine if the PING frames should be automatically acknowledged or not. + * @return {@code true} if the PING frames should be automatically acknowledged. + */ + protected boolean isAutoAckPingFrame() { + return autoAckPingFrame; + } + + /** + * Determine if the {@link Channel#close()} should be coupled with goaway and graceful close. + * @param decoupleCloseAndGoAway {@code true} to make {@link Channel#close()} directly close the underlying + * transport, and not attempt graceful closure via GOAWAY. + * @return {@code this}. + */ + protected B decoupleCloseAndGoAway(boolean decoupleCloseAndGoAway) { + this.decoupleCloseAndGoAway = decoupleCloseAndGoAway; + return self(); + } + + /** + * Determine if the {@link Channel#close()} should be coupled with goaway and graceful close. + */ + protected boolean decoupleCloseAndGoAway() { + return decoupleCloseAndGoAway; + } + /** * Create a new {@link Http2ConnectionHandler}. */ @@ -385,7 +508,7 @@ public abstract class AbstractHttp2ConnectionHandlerBuilder<T extends Http2Conne Long maxHeaderListSize = initialSettings.maxHeaderListSize(); Http2FrameReader reader = new DefaultHttp2FrameReader(new DefaultHttp2HeadersDecoder(isValidateHeaders(), maxHeaderListSize == null ? DEFAULT_HEADER_LIST_SIZE : maxHeaderListSize, - initialHuffmanDecodeCapacity)); + /* initialHuffmanDecodeCapacity= */ -1)); Http2FrameWriter writer = encoderIgnoreMaxHeaderListSize == null ? new DefaultHttp2FrameWriter(headerSensitivityDetector()) : new DefaultHttp2FrameWriter(headerSensitivityDetector(), encoderIgnoreMaxHeaderListSize); @@ -398,6 +521,9 @@ public abstract class AbstractHttp2ConnectionHandlerBuilder<T extends Http2Conne Http2ConnectionEncoder encoder = new DefaultHttp2ConnectionEncoder(connection, writer); boolean encoderEnforceMaxConcurrentStreams = encoderEnforceMaxConcurrentStreams(); + if (maxQueuedControlFrames != 0) { + encoder = new Http2ControlFrameLimitEncoder(encoder, maxQueuedControlFrames); + } if (encoderEnforceMaxConcurrentStreams) { if (connection.isServer()) { encoder.close(); @@ -409,11 +535,16 @@ public abstract class AbstractHttp2ConnectionHandlerBuilder<T extends Http2Conne encoder = new StreamBufferingEncoder(encoder); } - Http2ConnectionDecoder decoder = new DefaultHttp2ConnectionDecoder(connection, encoder, reader); + DefaultHttp2ConnectionDecoder decoder = new DefaultHttp2ConnectionDecoder(connection, encoder, reader, + promisedRequestVerifier(), isAutoAckSettingsFrame(), isAutoAckPingFrame()); return buildFromCodec(decoder, encoder); } private T buildFromCodec(Http2ConnectionDecoder decoder, Http2ConnectionEncoder encoder) { + int maxConsecutiveEmptyDataFrames = decoderEnforceMaxConsecutiveEmptyDataFrames(); + if (maxConsecutiveEmptyDataFrames > 0) { + decoder = new Http2EmptyDataFrameConnectionDecoder(decoder, maxConsecutiveEmptyDataFrames); + } final T handler; try { // Call the abstract build method @@ -421,7 +552,7 @@ public abstract class AbstractHttp2ConnectionHandlerBuilder<T extends Http2Conne } catch (Throwable t) { encoder.close(); decoder.close(); - throw new IllegalStateException("failed to build a Http2ConnectionHandler", t); + throw new IllegalStateException("failed to build an Http2ConnectionHandler", t); } // Setup post build options diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/AbstractHttp2StreamChannel.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/AbstractHttp2StreamChannel.java new file mode 100644 index 0000000..92abf19 --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/AbstractHttp2StreamChannel.java @@ -0,0 +1,1106 @@ +/* + * Copyright 2019 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.codec.http2; + +import io.netty.buffer.ByteBufAllocator; +import io.netty.channel.Channel; +import io.netty.channel.ChannelConfig; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelId; +import io.netty.channel.ChannelMetadata; +import io.netty.channel.ChannelOutboundBuffer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.ChannelProgressivePromise; +import io.netty.channel.ChannelPromise; +import io.netty.channel.DefaultChannelConfig; +import io.netty.channel.DefaultChannelPipeline; +import io.netty.channel.EventLoop; +import io.netty.channel.MessageSizeEstimator; +import io.netty.channel.RecvByteBufAllocator; +import io.netty.channel.VoidChannelPromise; +import io.netty.channel.WriteBufferWaterMark; +import io.netty.handler.codec.http2.Http2FrameCodec.DefaultHttp2FrameStream; +import io.netty.util.DefaultAttributeMap; +import io.netty.util.ReferenceCountUtil; +import io.netty.util.internal.StringUtil; +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.channels.ClosedChannelException; +import java.util.ArrayDeque; +import java.util.Queue; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; +import java.util.concurrent.atomic.AtomicLongFieldUpdater; + +import static io.netty.handler.codec.http2.Http2CodecUtil.isStreamIdValid; +import static java.lang.Math.min; + +abstract class AbstractHttp2StreamChannel extends DefaultAttributeMap implements Http2StreamChannel { + + static final Http2FrameStreamVisitor WRITABLE_VISITOR = new Http2FrameStreamVisitor() { + @Override + public boolean visit(Http2FrameStream stream) { + final AbstractHttp2StreamChannel childChannel = (AbstractHttp2StreamChannel) + ((DefaultHttp2FrameStream) stream).attachment; + childChannel.trySetWritable(); + return true; + } + }; + + private static final InternalLogger logger = InternalLoggerFactory.getInstance(AbstractHttp2StreamChannel.class); + + private static final ChannelMetadata METADATA = new ChannelMetadata(false, 16); + + /** + * Number of bytes to consider non-payload messages. 9 is arbitrary, but also the minimum size of an HTTP/2 frame. + * Primarily is non-zero. + */ + private static final int MIN_HTTP2_FRAME_SIZE = 9; + + /** + * Returns the flow-control size for DATA frames, and {@value MIN_HTTP2_FRAME_SIZE} for all other frames. + */ + private static final class FlowControlledFrameSizeEstimator implements MessageSizeEstimator { + + static final FlowControlledFrameSizeEstimator INSTANCE = new FlowControlledFrameSizeEstimator(); + + private static final Handle HANDLE_INSTANCE = new Handle() { + @Override + public int size(Object msg) { + return msg instanceof Http2DataFrame ? + // Guard against overflow. + (int) min(Integer.MAX_VALUE, ((Http2DataFrame) msg).initialFlowControlledBytes() + + (long) MIN_HTTP2_FRAME_SIZE) : MIN_HTTP2_FRAME_SIZE; + } + }; + + @Override + public Handle newHandle() { + return HANDLE_INSTANCE; + } + } + + private static final AtomicLongFieldUpdater<AbstractHttp2StreamChannel> TOTAL_PENDING_SIZE_UPDATER = + AtomicLongFieldUpdater.newUpdater(AbstractHttp2StreamChannel.class, "totalPendingSize"); + + private static final AtomicIntegerFieldUpdater<AbstractHttp2StreamChannel> UNWRITABLE_UPDATER = + AtomicIntegerFieldUpdater.newUpdater(AbstractHttp2StreamChannel.class, "unwritable"); + + private static void windowUpdateFrameWriteComplete(ChannelFuture future, Channel streamChannel) { + Throwable cause = future.cause(); + if (cause != null) { + Throwable unwrappedCause; + // Unwrap if needed + if (cause instanceof Http2FrameStreamException && ((unwrappedCause = cause.getCause()) != null)) { + cause = unwrappedCause; + } + + // Notify the child-channel and close it. + streamChannel.pipeline().fireExceptionCaught(cause); + streamChannel.unsafe().close(streamChannel.unsafe().voidPromise()); + } + } + + private final ChannelFutureListener windowUpdateFrameWriteListener = new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture future) { + windowUpdateFrameWriteComplete(future, AbstractHttp2StreamChannel.this); + } + }; + + /** + * The current status of the read-processing for a {@link AbstractHttp2StreamChannel}. + */ + private enum ReadStatus { + /** + * No read in progress and no read was requested (yet) + */ + IDLE, + + /** + * Reading in progress + */ + IN_PROGRESS, + + /** + * A read operation was requested. + */ + REQUESTED + } + + private final AbstractHttp2StreamChannel.Http2StreamChannelConfig config = new Http2StreamChannelConfig(this); + private final AbstractHttp2StreamChannel.Http2ChannelUnsafe unsafe = new Http2ChannelUnsafe(); + private final ChannelId channelId; + private final ChannelPipeline pipeline; + private final DefaultHttp2FrameStream stream; + private final ChannelPromise closePromise; + + private volatile boolean registered; + + private volatile long totalPendingSize; + private volatile int unwritable; + + // Cached to reduce GC + private Runnable fireChannelWritabilityChangedTask; + + private boolean outboundClosed; + private int flowControlledBytes; + + /** + * This variable represents if a read is in progress for the current channel or was requested. + * Note that depending upon the {@link RecvByteBufAllocator} behavior a read may extend beyond the + * {@link Http2ChannelUnsafe#beginRead()} method scope. The {@link Http2ChannelUnsafe#beginRead()} loop may + * drain all pending data, and then if the parent channel is reading this channel may still accept frames. + */ + private ReadStatus readStatus = ReadStatus.IDLE; + + private Queue<Object> inboundBuffer; + + /** {@code true} after the first HEADERS frame has been written **/ + private boolean firstFrameWritten; + private boolean readCompletePending; + + AbstractHttp2StreamChannel(DefaultHttp2FrameStream stream, int id, ChannelHandler inboundHandler) { + this.stream = stream; + stream.attachment = this; + pipeline = new DefaultChannelPipeline(this) { + @Override + protected void incrementPendingOutboundBytes(long size) { + AbstractHttp2StreamChannel.this.incrementPendingOutboundBytes(size, true); + } + + @Override + protected void decrementPendingOutboundBytes(long size) { + AbstractHttp2StreamChannel.this.decrementPendingOutboundBytes(size, true); + } + }; + + closePromise = pipeline.newPromise(); + channelId = new Http2StreamChannelId(parent().id(), id); + + if (inboundHandler != null) { + // Add the handler to the pipeline now that we are registered. + pipeline.addLast(inboundHandler); + } + } + + private void incrementPendingOutboundBytes(long size, boolean invokeLater) { + if (size == 0) { + return; + } + + long newWriteBufferSize = TOTAL_PENDING_SIZE_UPDATER.addAndGet(this, size); + if (newWriteBufferSize > config().getWriteBufferHighWaterMark()) { + setUnwritable(invokeLater); + } + } + + private void decrementPendingOutboundBytes(long size, boolean invokeLater) { + if (size == 0) { + return; + } + + long newWriteBufferSize = TOTAL_PENDING_SIZE_UPDATER.addAndGet(this, -size); + // Once the totalPendingSize dropped below the low water-mark we can mark the child channel + // as writable again. Before doing so we also need to ensure the parent channel is writable to + // prevent excessive buffering in the parent outbound buffer. If the parent is not writable + // we will mark the child channel as writable once the parent becomes writable by calling + // trySetWritable() later. + if (newWriteBufferSize < config().getWriteBufferLowWaterMark() && parent().isWritable()) { + setWritable(invokeLater); + } + } + + final void trySetWritable() { + // The parent is writable again but the child channel itself may still not be writable. + // Lets try to set the child channel writable to match the state of the parent channel + // if (and only if) the totalPendingSize is smaller then the low water-mark. + // If this is not the case we will try again later once we drop under it. + if (totalPendingSize < config().getWriteBufferLowWaterMark()) { + setWritable(false); + } + } + + private void setWritable(boolean invokeLater) { + for (;;) { + final int oldValue = unwritable; + final int newValue = oldValue & ~1; + if (UNWRITABLE_UPDATER.compareAndSet(this, oldValue, newValue)) { + if (oldValue != 0 && newValue == 0) { + fireChannelWritabilityChanged(invokeLater); + } + break; + } + } + } + + private void setUnwritable(boolean invokeLater) { + for (;;) { + final int oldValue = unwritable; + final int newValue = oldValue | 1; + if (UNWRITABLE_UPDATER.compareAndSet(this, oldValue, newValue)) { + if (oldValue == 0 && newValue != 0) { + fireChannelWritabilityChanged(invokeLater); + } + break; + } + } + } + + private void fireChannelWritabilityChanged(boolean invokeLater) { + final ChannelPipeline pipeline = pipeline(); + if (invokeLater) { + Runnable task = fireChannelWritabilityChangedTask; + if (task == null) { + fireChannelWritabilityChangedTask = task = new Runnable() { + @Override + public void run() { + pipeline.fireChannelWritabilityChanged(); + } + }; + } + eventLoop().execute(task); + } else { + pipeline.fireChannelWritabilityChanged(); + } + } + @Override + public Http2FrameStream stream() { + return stream; + } + + void closeOutbound() { + outboundClosed = true; + } + + void streamClosed() { + unsafe.readEOS(); + // Attempt to drain any queued data from the queue and deliver it to the application before closing this + // channel. + unsafe.doBeginRead(); + } + + @Override + public ChannelMetadata metadata() { + return METADATA; + } + + @Override + public ChannelConfig config() { + return config; + } + + @Override + public boolean isOpen() { + return !closePromise.isDone(); + } + + @Override + public boolean isActive() { + return isOpen(); + } + + @Override + public boolean isWritable() { + return unwritable == 0; + } + + @Override + public ChannelId id() { + return channelId; + } + + @Override + public EventLoop eventLoop() { + return parent().eventLoop(); + } + + @Override + public Channel parent() { + return parentContext().channel(); + } + + @Override + public boolean isRegistered() { + return registered; + } + + @Override + public SocketAddress localAddress() { + return parent().localAddress(); + } + + @Override + public SocketAddress remoteAddress() { + return parent().remoteAddress(); + } + + @Override + public ChannelFuture closeFuture() { + return closePromise; + } + + @Override + public long bytesBeforeUnwritable() { + long bytes = config().getWriteBufferHighWaterMark() - totalPendingSize; + // If bytes is negative we know we are not writable, but if bytes is non-negative we have to check + // writability. Note that totalPendingSize and isWritable() use different volatile variables that are not + // synchronized together. totalPendingSize will be updated before isWritable(). + if (bytes > 0) { + return isWritable() ? bytes : 0; + } + return 0; + } + + @Override + public long bytesBeforeWritable() { + long bytes = totalPendingSize - config().getWriteBufferLowWaterMark(); + // If bytes is negative we know we are writable, but if bytes is non-negative we have to check writability. + // Note that totalPendingSize and isWritable() use different volatile variables that are not synchronized + // together. totalPendingSize will be updated before isWritable(). + if (bytes > 0) { + return isWritable() ? 0 : bytes; + } + return 0; + } + + @Override + public Unsafe unsafe() { + return unsafe; + } + + @Override + public ChannelPipeline pipeline() { + return pipeline; + } + + @Override + public ByteBufAllocator alloc() { + return config().getAllocator(); + } + + @Override + public Channel read() { + pipeline().read(); + return this; + } + + @Override + public Channel flush() { + pipeline().flush(); + return this; + } + + @Override + public ChannelFuture bind(SocketAddress localAddress) { + return pipeline().bind(localAddress); + } + + @Override + public ChannelFuture connect(SocketAddress remoteAddress) { + return pipeline().connect(remoteAddress); + } + + @Override + public ChannelFuture connect(SocketAddress remoteAddress, SocketAddress localAddress) { + return pipeline().connect(remoteAddress, localAddress); + } + + @Override + public ChannelFuture disconnect() { + return pipeline().disconnect(); + } + + @Override + public ChannelFuture close() { + return pipeline().close(); + } + + @Override + public ChannelFuture deregister() { + return pipeline().deregister(); + } + + @Override + public ChannelFuture bind(SocketAddress localAddress, ChannelPromise promise) { + return pipeline().bind(localAddress, promise); + } + + @Override + public ChannelFuture connect(SocketAddress remoteAddress, ChannelPromise promise) { + return pipeline().connect(remoteAddress, promise); + } + + @Override + public ChannelFuture connect(SocketAddress remoteAddress, SocketAddress localAddress, ChannelPromise promise) { + return pipeline().connect(remoteAddress, localAddress, promise); + } + + @Override + public ChannelFuture disconnect(ChannelPromise promise) { + return pipeline().disconnect(promise); + } + + @Override + public ChannelFuture close(ChannelPromise promise) { + return pipeline().close(promise); + } + + @Override + public ChannelFuture deregister(ChannelPromise promise) { + return pipeline().deregister(promise); + } + + @Override + public ChannelFuture write(Object msg) { + return pipeline().write(msg); + } + + @Override + public ChannelFuture write(Object msg, ChannelPromise promise) { + return pipeline().write(msg, promise); + } + + @Override + public ChannelFuture writeAndFlush(Object msg, ChannelPromise promise) { + return pipeline().writeAndFlush(msg, promise); + } + + @Override + public ChannelFuture writeAndFlush(Object msg) { + return pipeline().writeAndFlush(msg); + } + + @Override + public ChannelPromise newPromise() { + return pipeline().newPromise(); + } + + @Override + public ChannelProgressivePromise newProgressivePromise() { + return pipeline().newProgressivePromise(); + } + + @Override + public ChannelFuture newSucceededFuture() { + return pipeline().newSucceededFuture(); + } + + @Override + public ChannelFuture newFailedFuture(Throwable cause) { + return pipeline().newFailedFuture(cause); + } + + @Override + public ChannelPromise voidPromise() { + return pipeline().voidPromise(); + } + + @Override + public int hashCode() { + return id().hashCode(); + } + + @Override + public boolean equals(Object o) { + return this == o; + } + + @Override + public int compareTo(Channel o) { + if (this == o) { + return 0; + } + + return id().compareTo(o.id()); + } + + @Override + public String toString() { + return parent().toString() + "(H2 - " + stream + ')'; + } + + /** + * Receive a read message. This does not notify handlers unless a read is in progress on the + * channel. + */ + void fireChildRead(Http2Frame frame) { + assert eventLoop().inEventLoop(); + if (!isActive()) { + ReferenceCountUtil.release(frame); + } else if (readStatus != ReadStatus.IDLE) { + // If a read is in progress or has been requested, there cannot be anything in the queue, + // otherwise we would have drained it from the queue and processed it during the read cycle. + assert inboundBuffer == null || inboundBuffer.isEmpty(); + final RecvByteBufAllocator.Handle allocHandle = unsafe.recvBufAllocHandle(); + unsafe.doRead0(frame, allocHandle); + // We currently don't need to check for readEOS because the parent channel and child channel are limited + // to the same EventLoop thread. There are a limited number of frame types that may come after EOS is + // read (unknown, reset) and the trade off is less conditionals for the hot path (headers/data) at the + // cost of additional readComplete notifications on the rare path. + if (allocHandle.continueReading()) { + maybeAddChannelToReadCompletePendingQueue(); + } else { + unsafe.notifyReadComplete(allocHandle, true); + } + } else { + if (inboundBuffer == null) { + inboundBuffer = new ArrayDeque<Object>(4); + } + inboundBuffer.add(frame); + } + } + + void fireChildReadComplete() { + assert eventLoop().inEventLoop(); + assert readStatus != ReadStatus.IDLE || !readCompletePending; + unsafe.notifyReadComplete(unsafe.recvBufAllocHandle(), false); + } + + private final class Http2ChannelUnsafe implements Unsafe { + private final VoidChannelPromise unsafeVoidPromise = + new VoidChannelPromise(AbstractHttp2StreamChannel.this, false); + @SuppressWarnings("deprecation") + private RecvByteBufAllocator.Handle recvHandle; + private boolean writeDoneAndNoFlush; + private boolean closeInitiated; + private boolean readEOS; + + @Override + public void connect(final SocketAddress remoteAddress, + SocketAddress localAddress, final ChannelPromise promise) { + if (!promise.setUncancellable()) { + return; + } + promise.setFailure(new UnsupportedOperationException()); + } + + @Override + public RecvByteBufAllocator.Handle recvBufAllocHandle() { + if (recvHandle == null) { + recvHandle = config().getRecvByteBufAllocator().newHandle(); + recvHandle.reset(config()); + } + return recvHandle; + } + + @Override + public SocketAddress localAddress() { + return parent().unsafe().localAddress(); + } + + @Override + public SocketAddress remoteAddress() { + return parent().unsafe().remoteAddress(); + } + + @Override + public void register(EventLoop eventLoop, ChannelPromise promise) { + if (!promise.setUncancellable()) { + return; + } + if (registered) { + promise.setFailure(new UnsupportedOperationException("Re-register is not supported")); + return; + } + + registered = true; + + promise.setSuccess(); + + pipeline().fireChannelRegistered(); + if (isActive()) { + pipeline().fireChannelActive(); + } + } + + @Override + public void bind(SocketAddress localAddress, ChannelPromise promise) { + if (!promise.setUncancellable()) { + return; + } + promise.setFailure(new UnsupportedOperationException()); + } + + @Override + public void disconnect(ChannelPromise promise) { + close(promise); + } + + @Override + public void close(final ChannelPromise promise) { + if (!promise.setUncancellable()) { + return; + } + if (closeInitiated) { + if (closePromise.isDone()) { + // Closed already. + promise.setSuccess(); + } else if (!(promise instanceof VoidChannelPromise)) { // Only needed if no VoidChannelPromise. + // This means close() was called before so we just register a listener and return + closePromise.addListener(new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture future) { + promise.setSuccess(); + } + }); + } + return; + } + closeInitiated = true; + // Just set to false as removing from an underlying queue would even be more expensive. + readCompletePending = false; + + final boolean wasActive = isActive(); + + // There is no need to update the local window as once the stream is closed all the pending bytes will be + // given back to the connection window by the controller itself. + + // Only ever send a reset frame if the connection is still alive and if the stream was created before + // as otherwise we may send a RST on a stream in an invalid state and cause a connection error. + if (parent().isActive() && !readEOS && Http2CodecUtil.isStreamIdValid(stream.id())) { + Http2StreamFrame resetFrame = new DefaultHttp2ResetFrame(Http2Error.CANCEL).stream(stream()); + write(resetFrame, unsafe().voidPromise()); + flush(); + } + + if (inboundBuffer != null) { + for (;;) { + Object msg = inboundBuffer.poll(); + if (msg == null) { + break; + } + ReferenceCountUtil.release(msg); + } + inboundBuffer = null; + } + + // The promise should be notified before we call fireChannelInactive(). + outboundClosed = true; + closePromise.setSuccess(); + promise.setSuccess(); + + fireChannelInactiveAndDeregister(voidPromise(), wasActive); + } + + @Override + public void closeForcibly() { + close(unsafe().voidPromise()); + } + + @Override + public void deregister(ChannelPromise promise) { + fireChannelInactiveAndDeregister(promise, false); + } + + private void fireChannelInactiveAndDeregister(final ChannelPromise promise, + final boolean fireChannelInactive) { + if (!promise.setUncancellable()) { + return; + } + + if (!registered) { + promise.setSuccess(); + return; + } + + // As a user may call deregister() from within any method while doing processing in the ChannelPipeline, + // we need to ensure we do the actual deregister operation later. This is necessary to preserve the + // behavior of the AbstractChannel, which always invokes channelUnregistered and channelInactive + // events 'later' to ensure the current events in the handler are completed before these events. + // + // See: + // https://github.com/netty/netty/issues/4435 + invokeLater(new Runnable() { + @Override + public void run() { + if (fireChannelInactive) { + pipeline.fireChannelInactive(); + } + // The user can fire `deregister` events multiple times but we only want to fire the pipeline + // event if the channel was actually registered. + if (registered) { + registered = false; + pipeline.fireChannelUnregistered(); + } + safeSetSuccess(promise); + } + }); + } + + private void safeSetSuccess(ChannelPromise promise) { + if (!(promise instanceof VoidChannelPromise) && !promise.trySuccess()) { + logger.warn("Failed to mark a promise as success because it is done already: {}", promise); + } + } + + private void invokeLater(Runnable task) { + try { + // This method is used by outbound operation implementations to trigger an inbound event later. + // They do not trigger an inbound event immediately because an outbound operation might have been + // triggered by another inbound event handler method. If fired immediately, the call stack + // will look like this for example: + // + // handlerA.inboundBufferUpdated() - (1) an inbound handler method closes a connection. + // -> handlerA.ctx.close() + // -> channel.unsafe.close() + // -> handlerA.channelInactive() - (2) another inbound handler method called while in (1) yet + // + // which means the execution of two inbound handler methods of the same handler overlap undesirably. + eventLoop().execute(task); + } catch (RejectedExecutionException e) { + logger.warn("Can't invoke task later as EventLoop rejected it", e); + } + } + + @Override + public void beginRead() { + if (!isActive()) { + return; + } + updateLocalWindowIfNeeded(); + + switch (readStatus) { + case IDLE: + readStatus = ReadStatus.IN_PROGRESS; + doBeginRead(); + break; + case IN_PROGRESS: + readStatus = ReadStatus.REQUESTED; + break; + default: + break; + } + } + + private Object pollQueuedMessage() { + return inboundBuffer == null ? null : inboundBuffer.poll(); + } + + void doBeginRead() { + // Process messages until there are none left (or the user stopped requesting) and also handle EOS. + while (readStatus != ReadStatus.IDLE) { + Object message = pollQueuedMessage(); + if (message == null) { + if (readEOS) { + unsafe.closeForcibly(); + } + // We need to double check that there is nothing left to flush such as a + // window update frame. + flush(); + break; + } + final RecvByteBufAllocator.Handle allocHandle = recvBufAllocHandle(); + allocHandle.reset(config()); + boolean continueReading = false; + do { + doRead0((Http2Frame) message, allocHandle); + } while ((readEOS || (continueReading = allocHandle.continueReading())) + && (message = pollQueuedMessage()) != null); + + if (continueReading && isParentReadInProgress() && !readEOS) { + // Currently the parent and child channel are on the same EventLoop thread. If the parent is + // currently reading it is possible that more frames will be delivered to this child channel. In + // the case that this child channel still wants to read we delay the channelReadComplete on this + // child channel until the parent is done reading. + maybeAddChannelToReadCompletePendingQueue(); + } else { + notifyReadComplete(allocHandle, true); + } + } + } + + void readEOS() { + readEOS = true; + } + + private void updateLocalWindowIfNeeded() { + if (flowControlledBytes != 0) { + int bytes = flowControlledBytes; + flowControlledBytes = 0; + ChannelFuture future = write0(parentContext(), new DefaultHttp2WindowUpdateFrame(bytes).stream(stream)); + // window update frames are commonly swallowed by the Http2FrameCodec and the promise is synchronously + // completed but the flow controller _may_ have generated a wire level WINDOW_UPDATE. Therefore we need, + // to assume there was a write done that needs to be flushed or we risk flow control starvation. + writeDoneAndNoFlush = true; + // Add a listener which will notify and teardown the stream + // when a window update fails if needed or check the result of the future directly if it was completed + // already. + // See https://github.com/netty/netty/issues/9663 + if (future.isDone()) { + windowUpdateFrameWriteComplete(future, AbstractHttp2StreamChannel.this); + } else { + future.addListener(windowUpdateFrameWriteListener); + } + } + } + + void notifyReadComplete(RecvByteBufAllocator.Handle allocHandle, boolean forceReadComplete) { + if (!readCompletePending && !forceReadComplete) { + return; + } + // Set to false just in case we added the channel multiple times before. + readCompletePending = false; + + if (readStatus == ReadStatus.REQUESTED) { + readStatus = ReadStatus.IN_PROGRESS; + } else { + readStatus = ReadStatus.IDLE; + } + + allocHandle.readComplete(); + pipeline().fireChannelReadComplete(); + // Reading data may result in frames being written (e.g. WINDOW_UPDATE, RST, etc..). If the parent + // channel is not currently reading we need to force a flush at the child channel, because we cannot + // rely upon flush occurring in channelReadComplete on the parent channel. + flush(); + if (readEOS) { + unsafe.closeForcibly(); + } + } + + @SuppressWarnings("deprecation") + void doRead0(Http2Frame frame, RecvByteBufAllocator.Handle allocHandle) { + final int bytes; + if (frame instanceof Http2DataFrame) { + bytes = ((Http2DataFrame) frame).initialFlowControlledBytes(); + + // It is important that we increment the flowControlledBytes before we call fireChannelRead(...) + // as it may cause a read() that will call updateLocalWindowIfNeeded() and we need to ensure + // in this case that we accounted for it. + // + // See https://github.com/netty/netty/issues/9663 + flowControlledBytes += bytes; + } else { + bytes = MIN_HTTP2_FRAME_SIZE; + } + // Update before firing event through the pipeline to be consistent with other Channel implementation. + allocHandle.attemptedBytesRead(bytes); + allocHandle.lastBytesRead(bytes); + allocHandle.incMessagesRead(1); + + pipeline().fireChannelRead(frame); + } + + @Override + public void write(Object msg, final ChannelPromise promise) { + // After this point its not possible to cancel a write anymore. + if (!promise.setUncancellable()) { + ReferenceCountUtil.release(msg); + return; + } + + if (!isActive() || + // Once the outbound side was closed we should not allow header / data frames + outboundClosed && (msg instanceof Http2HeadersFrame || msg instanceof Http2DataFrame)) { + ReferenceCountUtil.release(msg); + promise.setFailure(new ClosedChannelException()); + return; + } + + try { + if (msg instanceof Http2StreamFrame) { + Http2StreamFrame frame = validateStreamFrame((Http2StreamFrame) msg).stream(stream()); + writeHttp2StreamFrame(frame, promise); + } else { + String msgStr = msg.toString(); + ReferenceCountUtil.release(msg); + promise.setFailure(new IllegalArgumentException( + "Message must be an " + StringUtil.simpleClassName(Http2StreamFrame.class) + + ": " + msgStr)); + } + } catch (Throwable t) { + promise.tryFailure(t); + } + } + + private void writeHttp2StreamFrame(Http2StreamFrame frame, final ChannelPromise promise) { + if (!firstFrameWritten && !isStreamIdValid(stream().id()) && !(frame instanceof Http2HeadersFrame)) { + ReferenceCountUtil.release(frame); + promise.setFailure( + new IllegalArgumentException("The first frame must be a headers frame. Was: " + + frame.name())); + return; + } + + final boolean firstWrite; + if (firstFrameWritten) { + firstWrite = false; + } else { + firstWrite = firstFrameWritten = true; + } + + ChannelFuture f = write0(parentContext(), frame); + if (f.isDone()) { + if (firstWrite) { + firstWriteComplete(f, promise); + } else { + writeComplete(f, promise); + } + } else { + final long bytes = FlowControlledFrameSizeEstimator.HANDLE_INSTANCE.size(frame); + incrementPendingOutboundBytes(bytes, false); + f.addListener(new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture future) { + if (firstWrite) { + firstWriteComplete(future, promise); + } else { + writeComplete(future, promise); + } + decrementPendingOutboundBytes(bytes, false); + } + }); + writeDoneAndNoFlush = true; + } + } + + private void firstWriteComplete(ChannelFuture future, ChannelPromise promise) { + Throwable cause = future.cause(); + if (cause == null) { + promise.setSuccess(); + } else { + // If the first write fails there is not much we can do, just close + closeForcibly(); + promise.setFailure(wrapStreamClosedError(cause)); + } + } + + private void writeComplete(ChannelFuture future, ChannelPromise promise) { + Throwable cause = future.cause(); + if (cause == null) { + promise.setSuccess(); + } else { + Throwable error = wrapStreamClosedError(cause); + // To make it more consistent with AbstractChannel we handle all IOExceptions here. + if (error instanceof IOException) { + if (config.isAutoClose()) { + // Close channel if needed. + closeForcibly(); + } else { + // TODO: Once Http2StreamChannel extends DuplexChannel we should call shutdownOutput(...) + outboundClosed = true; + } + } + promise.setFailure(error); + } + } + + private Throwable wrapStreamClosedError(Throwable cause) { + // If the error was caused by STREAM_CLOSED we should use a ClosedChannelException to better + // mimic other transports and make it easier to reason about what exceptions to expect. + if (cause instanceof Http2Exception && ((Http2Exception) cause).error() == Http2Error.STREAM_CLOSED) { + return new ClosedChannelException().initCause(cause); + } + return cause; + } + + private Http2StreamFrame validateStreamFrame(Http2StreamFrame frame) { + if (frame.stream() != null && frame.stream() != stream) { + String msgString = frame.toString(); + ReferenceCountUtil.release(frame); + throw new IllegalArgumentException( + "Stream " + frame.stream() + " must not be set on the frame: " + msgString); + } + return frame; + } + + @Override + public void flush() { + // If we are currently in the parent channel's read loop we should just ignore the flush. + // We will ensure we trigger ctx.flush() after we processed all Channels later on and + // so aggregate the flushes. This is done as ctx.flush() is expensive when as it may trigger an + // write(...) or writev(...) operation on the socket. + if (!writeDoneAndNoFlush || isParentReadInProgress()) { + // There is nothing to flush so this is a NOOP. + return; + } + // We need to set this to false before we call flush0(...) as ChannelFutureListener may produce more data + // that are explicit flushed. + writeDoneAndNoFlush = false; + flush0(parentContext()); + } + + @Override + public ChannelPromise voidPromise() { + return unsafeVoidPromise; + } + + @Override + public ChannelOutboundBuffer outboundBuffer() { + // Always return null as we not use the ChannelOutboundBuffer and not even support it. + return null; + } + } + + /** + * {@link ChannelConfig} so that the high and low writebuffer watermarks can reflect the outbound flow control + * window, without having to create a new {@link WriteBufferWaterMark} object whenever the flow control window + * changes. + */ + private static final class Http2StreamChannelConfig extends DefaultChannelConfig { + Http2StreamChannelConfig(Channel channel) { + super(channel); + } + + @Override + public MessageSizeEstimator getMessageSizeEstimator() { + return FlowControlledFrameSizeEstimator.INSTANCE; + } + + @Override + public ChannelConfig setMessageSizeEstimator(MessageSizeEstimator estimator) { + throw new UnsupportedOperationException(); + } + + @Override + public ChannelConfig setRecvByteBufAllocator(RecvByteBufAllocator allocator) { + if (!(allocator.newHandle() instanceof RecvByteBufAllocator.ExtendedHandle)) { + throw new IllegalArgumentException("allocator.newHandle() must return an object of type: " + + RecvByteBufAllocator.ExtendedHandle.class); + } + super.setRecvByteBufAllocator(allocator); + return this; + } + } + + private void maybeAddChannelToReadCompletePendingQueue() { + if (!readCompletePending) { + readCompletePending = true; + addChannelToReadCompletePendingQueue(); + } + } + + protected void flush0(ChannelHandlerContext ctx) { + ctx.flush(); + } + + protected ChannelFuture write0(ChannelHandlerContext ctx, Object msg) { + ChannelPromise promise = ctx.newPromise(); + ctx.write(msg, promise); + return promise; + } + + protected abstract boolean isParentReadInProgress(); + protected abstract void addChannelToReadCompletePendingQueue(); + protected abstract ChannelHandlerContext parentContext(); +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/CleartextHttp2ServerUpgradeHandler.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/CleartextHttp2ServerUpgradeHandler.java index c70b343..fe9353b 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/CleartextHttp2ServerUpgradeHandler.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/CleartextHttp2ServerUpgradeHandler.java @@ -18,7 +18,6 @@ package io.netty.handler.codec.http2; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufUtil; import io.netty.channel.ChannelHandler; -import io.netty.channel.ChannelHandlerAdapter; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.ByteToMessageDecoder; import io.netty.handler.codec.http.HttpServerCodec; @@ -39,7 +38,7 @@ import static io.netty.util.internal.ObjectUtil.checkNotNull; * prior knowledge or not. */ @UnstableApi -public final class CleartextHttp2ServerUpgradeHandler extends ChannelHandlerAdapter { +public final class CleartextHttp2ServerUpgradeHandler extends ByteToMessageDecoder { private static final ByteBuf CONNECTION_PREFACE = unreleasableBuffer(connectionPrefaceBuf()); private final HttpServerCodec httpServerCodec; @@ -66,36 +65,33 @@ public final class CleartextHttp2ServerUpgradeHandler extends ChannelHandlerAdap @Override public void handlerAdded(ChannelHandlerContext ctx) throws Exception { ctx.pipeline() - .addBefore(ctx.name(), null, new PriorKnowledgeHandler()) - .addBefore(ctx.name(), null, httpServerCodec) - .replace(this, null, httpServerUpgradeHandler); + .addAfter(ctx.name(), null, httpServerUpgradeHandler) + .addAfter(ctx.name(), null, httpServerCodec); } /** * Peek inbound message to determine current connection wants to start HTTP/2 * by HTTP upgrade or prior knowledge */ - private final class PriorKnowledgeHandler extends ByteToMessageDecoder { - @Override - protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception { - int prefaceLength = CONNECTION_PREFACE.readableBytes(); - int bytesRead = Math.min(in.readableBytes(), prefaceLength); + @Override + protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception { + int prefaceLength = CONNECTION_PREFACE.readableBytes(); + int bytesRead = Math.min(in.readableBytes(), prefaceLength); - if (!ByteBufUtil.equals(CONNECTION_PREFACE, CONNECTION_PREFACE.readerIndex(), - in, in.readerIndex(), bytesRead)) { - ctx.pipeline().remove(this); - } else if (bytesRead == prefaceLength) { - // Full h2 preface match, removed source codec, using http2 codec to handle - // following network traffic - ctx.pipeline() - .remove(httpServerCodec) - .remove(httpServerUpgradeHandler); + if (!ByteBufUtil.equals(CONNECTION_PREFACE, CONNECTION_PREFACE.readerIndex(), + in, in.readerIndex(), bytesRead)) { + ctx.pipeline().remove(this); + } else if (bytesRead == prefaceLength) { + // Full h2 preface match, removed source codec, using http2 codec to handle + // following network traffic + ctx.pipeline() + .remove(httpServerCodec) + .remove(httpServerUpgradeHandler); - ctx.pipeline().addAfter(ctx.name(), null, http2ServerHandler); - ctx.pipeline().remove(this); + ctx.pipeline().addAfter(ctx.name(), null, http2ServerHandler); + ctx.pipeline().remove(this); - ctx.fireUserEventTriggered(PriorKnowledgeUpgradeEvent.INSTANCE); - } + ctx.fireUserEventTriggered(PriorKnowledgeUpgradeEvent.INSTANCE); } } diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/CompressorHttp2ConnectionEncoder.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/CompressorHttp2ConnectionEncoder.java index 3137da2..213aa7b 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/CompressorHttp2ConnectionEncoder.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/CompressorHttp2ConnectionEncoder.java @@ -108,7 +108,7 @@ public class CompressorHttp2ConnectionEncoder extends DecoratingHttp2ConnectionE return promise; } - PromiseCombiner combiner = new PromiseCombiner(); + PromiseCombiner combiner = new PromiseCombiner(ctx.executor()); for (;;) { ByteBuf nextBuf = nextReadableBuf(channel); boolean compressedEndOfStream = nextBuf == null && endOfStream; @@ -284,16 +284,7 @@ public class CompressorHttp2ConnectionEncoder extends DecoratingHttp2ConnectionE * @param compressor The compressor for {@code stream} */ void cleanup(Http2Stream stream, EmbeddedChannel compressor) { - if (compressor.finish()) { - for (;;) { - final ByteBuf buf = compressor.readOutbound(); - if (buf == null) { - break; - } - - buf.release(); - } - } + compressor.finishAndReleaseAll(); stream.removeProperty(propertyKey); } diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/DecoratingHttp2ConnectionEncoder.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/DecoratingHttp2ConnectionEncoder.java index 9d591ab..43f3c3b 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/DecoratingHttp2ConnectionEncoder.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/DecoratingHttp2ConnectionEncoder.java @@ -22,7 +22,8 @@ import static io.netty.util.internal.ObjectUtil.checkNotNull; * A decorator around another {@link Http2ConnectionEncoder} instance. */ @UnstableApi -public class DecoratingHttp2ConnectionEncoder extends DecoratingHttp2FrameWriter implements Http2ConnectionEncoder { +public class DecoratingHttp2ConnectionEncoder extends DecoratingHttp2FrameWriter implements Http2ConnectionEncoder, + Http2SettingsReceivedConsumer { private final Http2ConnectionEncoder delegate; public DecoratingHttp2ConnectionEncoder(Http2ConnectionEncoder delegate) { @@ -59,4 +60,14 @@ public class DecoratingHttp2ConnectionEncoder extends DecoratingHttp2FrameWriter public void remoteSettings(Http2Settings settings) throws Http2Exception { delegate.remoteSettings(settings); } + + @Override + public void consumeReceivedSettings(Http2Settings settings) { + if (delegate instanceof Http2SettingsReceivedConsumer) { + ((Http2SettingsReceivedConsumer) delegate).consumeReceivedSettings(settings); + } else { + throw new IllegalStateException("delegate " + delegate + " is not an instance of " + + Http2SettingsReceivedConsumer.class); + } + } } diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2Connection.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2Connection.java index 8590e06..4d866eb 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2Connection.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2Connection.java @@ -930,7 +930,7 @@ public class DefaultHttp2Connection implements Http2Connection { private final Set<Http2Stream> streams = new LinkedHashSet<Http2Stream>(); private int pendingIterations; - public ActiveStreams(List<Listener> listeners) { + ActiveStreams(List<Listener> listeners) { this.listeners = listeners; } diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2ConnectionDecoder.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2ConnectionDecoder.java index 2d78fc9..10da347 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2ConnectionDecoder.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2ConnectionDecoder.java @@ -57,6 +57,8 @@ public class DefaultHttp2ConnectionDecoder implements Http2ConnectionDecoder { private final Http2FrameReader frameReader; private Http2FrameListener listener; private final Http2PromisedRequestVerifier requestVerifier; + private final Http2SettingsReceivedConsumer settingsReceivedConsumer; + private final boolean autoAckPing; public DefaultHttp2ConnectionDecoder(Http2Connection connection, Http2ConnectionEncoder encoder, @@ -68,6 +70,60 @@ public class DefaultHttp2ConnectionDecoder implements Http2ConnectionDecoder { Http2ConnectionEncoder encoder, Http2FrameReader frameReader, Http2PromisedRequestVerifier requestVerifier) { + this(connection, encoder, frameReader, requestVerifier, true); + } + + /** + * Create a new instance. + * @param connection The {@link Http2Connection} associated with this decoder. + * @param encoder The {@link Http2ConnectionEncoder} associated with this decoder. + * @param frameReader Responsible for reading/parsing the raw frames. As opposed to this object which applies + * h2 semantics on top of the frames. + * @param requestVerifier Determines if push promised streams are valid. + * @param autoAckSettings {@code false} to disable automatically applying and sending settings acknowledge frame. + * The {@code Http2ConnectionEncoder} is expected to be an instance of {@link Http2SettingsReceivedConsumer} and + * will apply the earliest received but not yet ACKed SETTINGS when writing the SETTINGS ACKs. + * {@code true} to enable automatically applying and sending settings acknowledge frame. + */ + public DefaultHttp2ConnectionDecoder(Http2Connection connection, + Http2ConnectionEncoder encoder, + Http2FrameReader frameReader, + Http2PromisedRequestVerifier requestVerifier, + boolean autoAckSettings) { + this(connection, encoder, frameReader, requestVerifier, autoAckSettings, true); + } + + /** + * Create a new instance. + * @param connection The {@link Http2Connection} associated with this decoder. + * @param encoder The {@link Http2ConnectionEncoder} associated with this decoder. + * @param frameReader Responsible for reading/parsing the raw frames. As opposed to this object which applies + * h2 semantics on top of the frames. + * @param requestVerifier Determines if push promised streams are valid. + * @param autoAckSettings {@code false} to disable automatically applying and sending settings acknowledge frame. + * The {@code Http2ConnectionEncoder} is expected to be an instance of + * {@link Http2SettingsReceivedConsumer} and will apply the earliest received but not yet + * ACKed SETTINGS when writing the SETTINGS ACKs. {@code true} to enable automatically + * applying and sending settings acknowledge frame. + * @param autoAckPing {@code false} to disable automatically sending ping acknowledge frame. {@code true} to enable + * automatically sending ping ack frame. + */ + public DefaultHttp2ConnectionDecoder(Http2Connection connection, + Http2ConnectionEncoder encoder, + Http2FrameReader frameReader, + Http2PromisedRequestVerifier requestVerifier, + boolean autoAckSettings, + boolean autoAckPing) { + this.autoAckPing = autoAckPing; + if (autoAckSettings) { + settingsReceivedConsumer = null; + } else { + if (!(encoder instanceof Http2SettingsReceivedConsumer)) { + throw new IllegalArgumentException("disabling autoAckSettings requires the encoder to be a " + + Http2SettingsReceivedConsumer.class); + } + settingsReceivedConsumer = (Http2SettingsReceivedConsumer) encoder; + } this.connection = checkNotNull(connection, "connection"); this.frameReader = checkNotNull(frameReader, "frameReader"); this.encoder = checkNotNull(encoder, "encoder"); @@ -158,8 +214,8 @@ public class DefaultHttp2ConnectionDecoder implements Http2ConnectionDecoder { void onGoAwayRead0(ChannelHandlerContext ctx, int lastStreamId, long errorCode, ByteBuf debugData) throws Http2Exception { - connection.goAwayReceived(lastStreamId, errorCode, debugData); listener.onGoAwayRead(ctx, lastStreamId, errorCode, debugData); + connection.goAwayReceived(lastStreamId, errorCode, debugData); } void onUnknownFrame0(ChannelHandlerContext ctx, byte frameType, int streamId, Http2Flags flags, @@ -408,23 +464,27 @@ public class DefaultHttp2ConnectionDecoder implements Http2ConnectionDecoder { } @Override - public void onSettingsRead(ChannelHandlerContext ctx, Http2Settings settings) throws Http2Exception { - // Acknowledge receipt of the settings. We should do this before we process the settings to ensure our - // remote peer applies these settings before any subsequent frames that we may send which depend upon these - // new settings. See https://github.com/netty/netty/issues/6520. - encoder.writeSettingsAck(ctx, ctx.newPromise()); - - encoder.remoteSettings(settings); + public void onSettingsRead(final ChannelHandlerContext ctx, Http2Settings settings) throws Http2Exception { + if (settingsReceivedConsumer == null) { + // Acknowledge receipt of the settings. We should do this before we process the settings to ensure our + // remote peer applies these settings before any subsequent frames that we may send which depend upon + // these new settings. See https://github.com/netty/netty/issues/6520. + encoder.writeSettingsAck(ctx, ctx.newPromise()); + + encoder.remoteSettings(settings); + } else { + settingsReceivedConsumer.consumeReceivedSettings(settings); + } listener.onSettingsRead(ctx, settings); } @Override public void onPingRead(ChannelHandlerContext ctx, long data) throws Http2Exception { - // Send an ack back to the remote client. - // Need to retain the buffer here since it will be released after the write completes. - encoder.writePing(ctx, true, data, ctx.newPromise()); - + if (autoAckPing) { + // Send an ack back to the remote client. + encoder.writePing(ctx, true, data, ctx.newPromise()); + } listener.onPingRead(ctx, data); } @@ -525,6 +585,11 @@ public class DefaultHttp2ConnectionDecoder implements Http2ConnectionDecoder { ctx.channel(), frameName, streamId); return true; } + + // Make sure it's not an out-of-order frame, like a rogue DATA frame, for a stream that could + // never have existed. + verifyStreamMayHaveExisted(streamId); + // Its possible that this frame would result in stream ID out of order creation (PROTOCOL ERROR) and its // also possible that this frame is received on a CLOSED stream (STREAM_CLOSED after a RST_STREAM is // sent). We don't have enough information to know for sure, so we choose the lesser of the two errors. diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2ConnectionEncoder.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2ConnectionEncoder.java index ce90e24..c1bdbc6 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2ConnectionEncoder.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2ConnectionEncoder.java @@ -21,15 +21,19 @@ import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelPromise; import io.netty.channel.CoalescingBufferQueue; import io.netty.handler.codec.http.HttpStatusClass; +import io.netty.handler.codec.http2.Http2CodecUtil.SimpleChannelPromiseAggregator; import io.netty.util.internal.UnstableApi; import java.util.ArrayDeque; +import java.util.Queue; import static io.netty.handler.codec.http.HttpStatusClass.INFORMATIONAL; import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_PRIORITY_WEIGHT; +import static io.netty.handler.codec.http2.Http2Error.INTERNAL_ERROR; import static io.netty.handler.codec.http2.Http2Error.PROTOCOL_ERROR; import static io.netty.handler.codec.http2.Http2Exception.connectionError; import static io.netty.util.internal.ObjectUtil.checkNotNull; +import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero; import static java.lang.Integer.MAX_VALUE; import static java.lang.Math.min; @@ -37,13 +41,14 @@ import static java.lang.Math.min; * Default implementation of {@link Http2ConnectionEncoder}. */ @UnstableApi -public class DefaultHttp2ConnectionEncoder implements Http2ConnectionEncoder { +public class DefaultHttp2ConnectionEncoder implements Http2ConnectionEncoder, Http2SettingsReceivedConsumer { private final Http2FrameWriter frameWriter; private final Http2Connection connection; private Http2LifecycleManager lifecycleManager; // We prefer ArrayDeque to LinkedList because later will produce more GC. // This initial capacity is plenty for SETTINGS traffic. - private final ArrayDeque<Http2Settings> outstandingLocalSettingsQueue = new ArrayDeque<Http2Settings>(4); + private final Queue<Http2Settings> outstandingLocalSettingsQueue = new ArrayDeque<Http2Settings>(4); + private Queue<Http2Settings> outstandingRemoteSettingsQueue; public DefaultHttp2ConnectionEncoder(Http2Connection connection, Http2FrameWriter frameWriter) { this.connection = checkNotNull(connection, "connection"); @@ -143,7 +148,7 @@ public class DefaultHttp2ConnectionEncoder implements Http2ConnectionEncoder { @Override public ChannelFuture writeHeaders(ChannelHandlerContext ctx, int streamId, Http2Headers headers, int padding, boolean endStream, ChannelPromise promise) { - return writeHeaders(ctx, streamId, headers, 0, DEFAULT_PRIORITY_WEIGHT, false, padding, endStream, promise); + return writeHeaders0(ctx, streamId, headers, false, 0, (short) 0, false, padding, endStream, promise); } private static boolean validateHeadersSentState(Http2Stream stream, Http2Headers headers, boolean isServer, @@ -159,6 +164,31 @@ public class DefaultHttp2ConnectionEncoder implements Http2ConnectionEncoder { public ChannelFuture writeHeaders(final ChannelHandlerContext ctx, final int streamId, final Http2Headers headers, final int streamDependency, final short weight, final boolean exclusive, final int padding, final boolean endOfStream, ChannelPromise promise) { + return writeHeaders0(ctx, streamId, headers, true, streamDependency, + weight, exclusive, padding, endOfStream, promise); + } + + /** + * Write headers via {@link Http2FrameWriter}. If {@code hasPriority} is {@code false} it will ignore the + * {@code streamDependency}, {@code weight} and {@code exclusive} parameters. + */ + private static ChannelFuture sendHeaders(Http2FrameWriter frameWriter, ChannelHandlerContext ctx, int streamId, + Http2Headers headers, final boolean hasPriority, + int streamDependency, final short weight, + boolean exclusive, final int padding, + boolean endOfStream, ChannelPromise promise) { + if (hasPriority) { + return frameWriter.writeHeaders(ctx, streamId, headers, streamDependency, + weight, exclusive, padding, endOfStream, promise); + } + return frameWriter.writeHeaders(ctx, streamId, headers, padding, endOfStream, promise); + } + + private ChannelFuture writeHeaders0(final ChannelHandlerContext ctx, final int streamId, + final Http2Headers headers, final boolean hasPriority, + final int streamDependency, final short weight, + final boolean exclusive, final int padding, + final boolean endOfStream, ChannelPromise promise) { try { Http2Stream stream = connection.stream(streamId); if (stream == null) { @@ -200,8 +230,9 @@ public class DefaultHttp2ConnectionEncoder implements Http2ConnectionEncoder { promise = promise.unvoid(); boolean isInformational = validateHeadersSentState(stream, headers, connection.isServer(), endOfStream); - ChannelFuture future = frameWriter.writeHeaders(ctx, streamId, headers, streamDependency, - weight, exclusive, padding, endOfStream, promise); + ChannelFuture future = sendHeaders(frameWriter, ctx, streamId, headers, hasPriority, streamDependency, + weight, exclusive, padding, endOfStream, promise); + // Writing headers may fail during the encode state if they violate HPACK limits. Throwable failureCause = future.cause(); if (failureCause == null) { @@ -231,8 +262,8 @@ public class DefaultHttp2ConnectionEncoder implements Http2ConnectionEncoder { } else { // Pass headers to the flow-controller so it can maintain their sequence relative to DATA frames. flowController.addFlowControlled(stream, - new FlowControlledHeaders(stream, headers, streamDependency, weight, exclusive, padding, - true, promise)); + new FlowControlledHeaders(stream, headers, hasPriority, streamDependency, + weight, exclusive, padding, true, promise)); return promise; } } catch (Throwable t) { @@ -273,7 +304,32 @@ public class DefaultHttp2ConnectionEncoder implements Http2ConnectionEncoder { @Override public ChannelFuture writeSettingsAck(ChannelHandlerContext ctx, ChannelPromise promise) { - return frameWriter.writeSettingsAck(ctx, promise); + if (outstandingRemoteSettingsQueue == null) { + return frameWriter.writeSettingsAck(ctx, promise); + } + Http2Settings settings = outstandingRemoteSettingsQueue.poll(); + if (settings == null) { + return promise.setFailure(new Http2Exception(INTERNAL_ERROR, "attempted to write a SETTINGS ACK with no " + + " pending SETTINGS")); + } + SimpleChannelPromiseAggregator aggregator = new SimpleChannelPromiseAggregator(promise, ctx.channel(), + ctx.executor()); + // Acknowledge receipt of the settings. We should do this before we process the settings to ensure our + // remote peer applies these settings before any subsequent frames that we may send which depend upon + // these new settings. See https://github.com/netty/netty/issues/6520. + frameWriter.writeSettingsAck(ctx, aggregator.newPromise()); + + // We create a "new promise" to make sure that status from both the write and the application are taken into + // account independently. + ChannelPromise applySettingsPromise = aggregator.newPromise(); + try { + remoteSettings(settings); + applySettingsPromise.setSuccess(); + } catch (Throwable e) { + applySettingsPromise.setFailure(e); + lifecycleManager.onError(ctx, true, e); + } + return aggregator.doneAllocatingPromises(); } @Override @@ -366,6 +422,14 @@ public class DefaultHttp2ConnectionEncoder implements Http2ConnectionEncoder { return stream; } + @Override + public void consumeReceivedSettings(Http2Settings settings) { + if (outstandingRemoteSettingsQueue == null) { + outstandingRemoteSettingsQueue = new ArrayDeque<Http2Settings>(2); + } + outstandingRemoteSettingsQueue.add(settings); + } + /** * Wrap a DATA frame so it can be written subject to flow-control. Note that this implementation assumes it * only writes padding once for the entire payload as opposed to writing it once per-frame. This makes the @@ -481,14 +545,17 @@ public class DefaultHttp2ConnectionEncoder implements Http2ConnectionEncoder { */ private final class FlowControlledHeaders extends FlowControlledBase { private final Http2Headers headers; + private final boolean hasPriorty; private final int streamDependency; private final short weight; private final boolean exclusive; - FlowControlledHeaders(Http2Stream stream, Http2Headers headers, int streamDependency, short weight, - boolean exclusive, int padding, boolean endOfStream, ChannelPromise promise) { + FlowControlledHeaders(Http2Stream stream, Http2Headers headers, boolean hasPriority, + int streamDependency, short weight, boolean exclusive, + int padding, boolean endOfStream, ChannelPromise promise) { super(stream, padding, endOfStream, promise.unvoid()); this.headers = headers; + this.hasPriorty = hasPriority; this.streamDependency = streamDependency; this.weight = weight; this.exclusive = exclusive; @@ -514,8 +581,8 @@ public class DefaultHttp2ConnectionEncoder implements Http2ConnectionEncoder { // closeStreamLocal(). promise.addListener(this); - ChannelFuture f = frameWriter.writeHeaders(ctx, stream.id(), headers, streamDependency, weight, exclusive, - padding, endOfStream, promise); + ChannelFuture f = sendHeaders(frameWriter, ctx, stream.id(), headers, hasPriorty, streamDependency, + weight, exclusive, padding, endOfStream, promise); // Writing headers may fail during the encode state if they violate HPACK limits. Throwable failureCause = f.cause(); if (failureCause == null) { @@ -543,9 +610,7 @@ public class DefaultHttp2ConnectionEncoder implements Http2ConnectionEncoder { FlowControlledBase(final Http2Stream stream, int padding, boolean endOfStream, final ChannelPromise promise) { - if (padding < 0) { - throw new IllegalArgumentException("padding must be >= 0"); - } + checkPositiveOrZero(padding, "padding"); this.padding = padding; this.endOfStream = endOfStream; this.stream = stream; diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2FrameWriter.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2FrameWriter.java index 1e4cdcd..71a6653 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2FrameWriter.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2FrameWriter.java @@ -61,6 +61,8 @@ import static io.netty.handler.codec.http2.Http2FrameTypes.RST_STREAM; import static io.netty.handler.codec.http2.Http2FrameTypes.SETTINGS; import static io.netty.handler.codec.http2.Http2FrameTypes.WINDOW_UPDATE; import static io.netty.util.internal.ObjectUtil.checkNotNull; +import static io.netty.util.internal.ObjectUtil.checkPositive; +import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero; import static java.lang.Math.max; import static java.lang.Math.min; @@ -384,7 +386,7 @@ public class DefaultHttp2FrameWriter implements Http2FrameWriter, Http2FrameSize } if (!flags.endOfHeaders()) { - writeContinuationFrames(ctx, streamId, headerBlock, padding, promiseAggregator); + writeContinuationFrames(ctx, streamId, headerBlock, promiseAggregator); } } catch (Http2Exception e) { promiseAggregator.setFailure(e); @@ -531,7 +533,7 @@ public class DefaultHttp2FrameWriter implements Http2FrameWriter, Http2FrameSize } if (!flags.endOfHeaders()) { - writeContinuationFrames(ctx, streamId, headerBlock, padding, promiseAggregator); + writeContinuationFrames(ctx, streamId, headerBlock, promiseAggregator); } } catch (Http2Exception e) { promiseAggregator.setFailure(e); @@ -551,28 +553,19 @@ public class DefaultHttp2FrameWriter implements Http2FrameWriter, Http2FrameSize * Writes as many continuation frames as needed until {@code padding} and {@code headerBlock} are consumed. */ private ChannelFuture writeContinuationFrames(ChannelHandlerContext ctx, int streamId, - ByteBuf headerBlock, int padding, SimpleChannelPromiseAggregator promiseAggregator) { - Http2Flags flags = new Http2Flags().paddingPresent(padding > 0); - int maxFragmentLength = maxFrameSize - padding; - // TODO: same padding is applied to all frames, is this desired? - if (maxFragmentLength <= 0) { - return promiseAggregator.setFailure(new IllegalArgumentException( - "Padding [" + padding + "] is too large for max frame size [" + maxFrameSize + "]")); - } + ByteBuf headerBlock, SimpleChannelPromiseAggregator promiseAggregator) { + Http2Flags flags = new Http2Flags(); if (headerBlock.isReadable()) { // The frame header (and padding) only changes on the last frame, so allocate it once and re-use - int fragmentReadableBytes = min(headerBlock.readableBytes(), maxFragmentLength); - int payloadLength = fragmentReadableBytes + padding; + int fragmentReadableBytes = min(headerBlock.readableBytes(), maxFrameSize); ByteBuf buf = ctx.alloc().buffer(CONTINUATION_FRAME_HEADER_LENGTH); - writeFrameHeaderInternal(buf, payloadLength, CONTINUATION, flags, streamId); - writePaddingLength(buf, padding); + writeFrameHeaderInternal(buf, fragmentReadableBytes, CONTINUATION, flags, streamId); do { - fragmentReadableBytes = min(headerBlock.readableBytes(), maxFragmentLength); + fragmentReadableBytes = min(headerBlock.readableBytes(), maxFrameSize); ByteBuf fragment = headerBlock.readRetainedSlice(fragmentReadableBytes); - payloadLength = fragmentReadableBytes + padding; if (headerBlock.isReadable()) { ctx.write(buf.retain(), promiseAggregator.newPromise()); } else { @@ -580,18 +573,13 @@ public class DefaultHttp2FrameWriter implements Http2FrameWriter, Http2FrameSize flags = flags.endOfHeaders(true); buf.release(); buf = ctx.alloc().buffer(CONTINUATION_FRAME_HEADER_LENGTH); - writeFrameHeaderInternal(buf, payloadLength, CONTINUATION, flags, streamId); - writePaddingLength(buf, padding); + writeFrameHeaderInternal(buf, fragmentReadableBytes, CONTINUATION, flags, streamId); ctx.write(buf, promiseAggregator.newPromise()); } ctx.write(fragment, promiseAggregator.newPromise()); - // Write out the padding, if any. - if (paddingBytes(padding) > 0) { - ctx.write(ZERO_BUFFER.slice(0, paddingBytes(padding)), promiseAggregator.newPromise()); - } - } while(headerBlock.isReadable()); + } while (headerBlock.isReadable()); } return promiseAggregator; } @@ -614,15 +602,11 @@ public class DefaultHttp2FrameWriter implements Http2FrameWriter, Http2FrameSize } private static void verifyStreamId(int streamId, String argumentName) { - if (streamId <= 0) { - throw new IllegalArgumentException(argumentName + " must be > 0"); - } + checkPositive(streamId, "streamId"); } private static void verifyStreamOrConnectionId(int streamId, String argumentName) { - if (streamId < 0) { - throw new IllegalArgumentException(argumentName + " must be >= 0"); - } + checkPositiveOrZero(streamId, "streamId"); } private static void verifyWeight(short weight) { @@ -638,9 +622,7 @@ public class DefaultHttp2FrameWriter implements Http2FrameWriter, Http2FrameSize } private static void verifyWindowSizeIncrement(int windowSizeIncrement) { - if (windowSizeIncrement < 0) { - throw new IllegalArgumentException("WindowSizeIncrement must be >= 0"); - } + checkPositiveOrZero(windowSizeIncrement, "windowSizeIncrement"); } private static void verifyPingPayload(ByteBuf data) { diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2GoAwayFrame.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2GoAwayFrame.java index 7720767..2dbd738 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2GoAwayFrame.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2GoAwayFrame.java @@ -15,6 +15,8 @@ */ package io.netty.handler.codec.http2; +import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero; + import io.netty.buffer.ByteBuf; import io.netty.buffer.DefaultByteBufHolder; import io.netty.buffer.Unpooled; @@ -98,9 +100,7 @@ public final class DefaultHttp2GoAwayFrame extends DefaultByteBufHolder implemen @Override public Http2GoAwayFrame setExtraStreamIds(int extraStreamIds) { - if (extraStreamIds < 0) { - throw new IllegalArgumentException("extraStreamIds must be non-negative"); - } + checkPositiveOrZero(extraStreamIds, "extraStreamIds"); this.extraStreamIds = extraStreamIds; return this; } diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2HeadersDecoder.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2HeadersDecoder.java index 5d63209..593e948 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2HeadersDecoder.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2HeadersDecoder.java @@ -20,7 +20,6 @@ import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.UnstableApi; import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_HEADER_LIST_SIZE; -import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_INITIAL_HUFFMAN_DECODE_CAPACITY; import static io.netty.handler.codec.http2.Http2Error.COMPRESSION_ERROR; import static io.netty.handler.codec.http2.Http2Error.INTERNAL_ERROR; import static io.netty.handler.codec.http2.Http2Exception.connectionError; @@ -57,7 +56,7 @@ public class DefaultHttp2HeadersDecoder implements Http2HeadersDecoder, Http2Hea * (which is dangerous). */ public DefaultHttp2HeadersDecoder(boolean validateHeaders, long maxHeaderListSize) { - this(validateHeaders, maxHeaderListSize, DEFAULT_INITIAL_HUFFMAN_DECODE_CAPACITY); + this(validateHeaders, maxHeaderListSize, /* initialHuffmanDecodeCapacity= */ -1); } /** @@ -67,11 +66,11 @@ public class DefaultHttp2HeadersDecoder implements Http2HeadersDecoder, Http2Hea * This is because <a href="https://tools.ietf.org/html/rfc7540#section-6.5.1">SETTINGS_MAX_HEADER_LIST_SIZE</a> * allows a lower than advertised limit from being enforced, and the default limit is unlimited * (which is dangerous). - * @param initialHuffmanDecodeCapacity Size of an intermediate buffer used during huffman decode. + * @param initialHuffmanDecodeCapacity Does nothing, do not use. */ public DefaultHttp2HeadersDecoder(boolean validateHeaders, long maxHeaderListSize, - int initialHuffmanDecodeCapacity) { - this(validateHeaders, new HpackDecoder(maxHeaderListSize, initialHuffmanDecodeCapacity)); + @Deprecated int initialHuffmanDecodeCapacity) { + this(validateHeaders, new HpackDecoder(maxHeaderListSize)); } /** diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2HeadersEncoder.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2HeadersEncoder.java index 5e0a9f6..7b48b96 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2HeadersEncoder.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2HeadersEncoder.java @@ -43,7 +43,13 @@ public class DefaultHttp2HeadersEncoder implements Http2HeadersEncoder, Http2Hea public DefaultHttp2HeadersEncoder(SensitivityDetector sensitivityDetector, boolean ignoreMaxHeaderListSize, int dynamicTableArraySizeHint) { - this(sensitivityDetector, new HpackEncoder(ignoreMaxHeaderListSize, dynamicTableArraySizeHint)); + this(sensitivityDetector, ignoreMaxHeaderListSize, dynamicTableArraySizeHint, HpackEncoder.HUFF_CODE_THRESHOLD); + } + + public DefaultHttp2HeadersEncoder(SensitivityDetector sensitivityDetector, boolean ignoreMaxHeaderListSize, + int dynamicTableArraySizeHint, int huffCodeThreshold) { + this(sensitivityDetector, + new HpackEncoder(ignoreMaxHeaderListSize, dynamicTableArraySizeHint, huffCodeThreshold)); } /** diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2LocalFlowController.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2LocalFlowController.java index 74dc3ae..12116c8 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2LocalFlowController.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2LocalFlowController.java @@ -24,12 +24,14 @@ import static io.netty.handler.codec.http2.Http2Error.INTERNAL_ERROR; import static io.netty.handler.codec.http2.Http2Exception.connectionError; import static io.netty.handler.codec.http2.Http2Exception.streamError; import static io.netty.util.internal.ObjectUtil.checkNotNull; +import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero; import static java.lang.Math.max; import static java.lang.Math.min; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http2.Http2Exception.CompositeStreamException; import io.netty.handler.codec.http2.Http2Exception.StreamException; +import io.netty.handler.codec.http2.Http2Stream.State; import io.netty.util.internal.PlatformDependent; import io.netty.util.internal.UnstableApi; @@ -108,8 +110,11 @@ public class DefaultHttp2LocalFlowController implements Http2LocalFlowController FlowState state = state(stream); int unconsumedBytes = state.unconsumedBytes(); if (ctx != null && unconsumedBytes > 0) { - connectionState().consumeBytes(unconsumedBytes); - state.consumeBytes(unconsumedBytes); + if (consumeAllBytes(state, unconsumedBytes)) { + // As the user has no real control on when this callback is used we should better + // call flush() if we produced any window update to ensure we not stale. + ctx.flush(); + } } } catch (Http2Exception e) { PlatformDependent.throwException(e); @@ -173,9 +178,7 @@ public class DefaultHttp2LocalFlowController implements Http2LocalFlowController @Override public boolean consumeBytes(Http2Stream stream, int numBytes) throws Http2Exception { assert ctx != null && ctx.executor().inEventLoop(); - if (numBytes < 0) { - throw new IllegalArgumentException("numBytes must not be negative"); - } + checkPositiveOrZero(numBytes, "numBytes"); if (numBytes == 0) { return false; } @@ -187,13 +190,15 @@ public class DefaultHttp2LocalFlowController implements Http2LocalFlowController throw new UnsupportedOperationException("Returning bytes for the connection window is not supported"); } - boolean windowUpdateSent = connectionState().consumeBytes(numBytes); - windowUpdateSent |= state(stream).consumeBytes(numBytes); - return windowUpdateSent; + return consumeAllBytes(state(stream), numBytes); } return false; } + private boolean consumeAllBytes(FlowState state, int numBytes) throws Http2Exception { + return connectionState().consumeBytes(numBytes) | state.consumeBytes(numBytes); + } + @Override public int unconsumedBytes(Http2Stream stream) { return state(stream).unconsumedBytes(); @@ -296,7 +301,7 @@ public class DefaultHttp2LocalFlowController implements Http2LocalFlowController * received. */ private final class AutoRefillState extends DefaultState { - public AutoRefillState(Http2Stream stream, int initialWindowSize) { + AutoRefillState(Http2Stream stream, int initialWindowSize) { super(stream, initialWindowSize); } @@ -349,7 +354,7 @@ public class DefaultHttp2LocalFlowController implements Http2LocalFlowController private int lowerBound; private boolean endOfStream; - public DefaultState(Http2Stream stream, int initialWindowSize) { + DefaultState(Http2Stream stream, int initialWindowSize) { this.stream = stream; window(initialWindowSize); streamWindowUpdateRatio = windowUpdateRatio; @@ -449,7 +454,9 @@ public class DefaultHttp2LocalFlowController implements Http2LocalFlowController @Override public boolean writeWindowUpdateIfNeeded() throws Http2Exception { - if (endOfStream || initialStreamWindowSize <= 0) { + if (endOfStream || initialStreamWindowSize <= 0 || + // If the stream is already closed there is no need to try to write a window update for it. + isClosed(stream)) { return false; } @@ -613,7 +620,7 @@ public class DefaultHttp2LocalFlowController implements Http2LocalFlowController private CompositeStreamException compositeException; private final int delta; - public WindowUpdateVisitor(int delta) { + WindowUpdateVisitor(int delta) { this.delta = delta; } diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2PingFrame.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2PingFrame.java index be5f2da..984f65d 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2PingFrame.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2PingFrame.java @@ -32,10 +32,7 @@ public class DefaultHttp2PingFrame implements Http2PingFrame { this(content, false); } - /** - * A user cannot send a ping ack, as this is done automatically when a ping is received. - */ - DefaultHttp2PingFrame(long content, boolean ack) { + public DefaultHttp2PingFrame(long content, boolean ack) { this.content = content; this.ack = ack; } diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2RemoteFlowController.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2RemoteFlowController.java index 217cf8d..ef6ec98 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2RemoteFlowController.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2RemoteFlowController.java @@ -31,6 +31,7 @@ import static io.netty.handler.codec.http2.Http2Error.STREAM_CLOSED; import static io.netty.handler.codec.http2.Http2Exception.streamError; import static io.netty.handler.codec.http2.Http2Stream.State.HALF_CLOSED_LOCAL; import static io.netty.util.internal.ObjectUtil.checkNotNull; +import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero; import static java.lang.Math.max; import static java.lang.Math.min; @@ -635,9 +636,7 @@ public class DefaultHttp2RemoteFlowController implements Http2RemoteFlowControll } void initialWindowSize(int newWindowSize) throws Http2Exception { - if (newWindowSize < 0) { - throw new IllegalArgumentException("Invalid initial window size: " + newWindowSize); - } + checkPositiveOrZero(newWindowSize, "newWindowSize"); final int delta = newWindowSize - initialWindowSize; initialWindowSize = newWindowSize; diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2SettingsAckFrame.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2SettingsAckFrame.java new file mode 100644 index 0000000..259b4a0 --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2SettingsAckFrame.java @@ -0,0 +1,33 @@ +/* + * Copyright 2019 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.codec.http2; + +import io.netty.util.internal.StringUtil; + +/** + * The default {@link Http2SettingsAckFrame} implementation. + */ +final class DefaultHttp2SettingsAckFrame implements Http2SettingsAckFrame { + @Override + public String name() { + return "SETTINGS(ACK)"; + } + + @Override + public String toString() { + return StringUtil.simpleClassName(this); + } +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2SettingsFrame.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2SettingsFrame.java index c60f59f..f0b6b94 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2SettingsFrame.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2SettingsFrame.java @@ -42,6 +42,20 @@ public class DefaultHttp2SettingsFrame implements Http2SettingsFrame { return "SETTINGS"; } + @Override + public boolean equals(Object o) { + if (!(o instanceof Http2SettingsFrame)) { + return false; + } + Http2SettingsFrame other = (Http2SettingsFrame) o; + return settings.equals(other.settings()); + } + + @Override + public int hashCode() { + return settings.hashCode(); + } + @Override public String toString() { return StringUtil.simpleClassName(this) + "(settings=" + settings + ')'; diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2UnknownFrame.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2UnknownFrame.java index 65289d4..4bc77f8 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2UnknownFrame.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2UnknownFrame.java @@ -80,7 +80,7 @@ public final class DefaultHttp2UnknownFrame extends DefaultByteBufHolder impleme @Override public DefaultHttp2UnknownFrame replace(ByteBuf content) { - return new DefaultHttp2UnknownFrame(frameType, flags, content).stream(stream()); + return new DefaultHttp2UnknownFrame(frameType, flags, content).stream(stream); } @Override @@ -97,8 +97,8 @@ public final class DefaultHttp2UnknownFrame extends DefaultByteBufHolder impleme @Override public String toString() { - return StringUtil.simpleClassName(this) + "(frameType=" + frameType() + ", stream=" + stream() + - ", flags=" + flags() + ", content=" + contentToString() + ')'; + return StringUtil.simpleClassName(this) + "(frameType=" + frameType + ", stream=" + stream + + ", flags=" + flags + ", content=" + contentToString() + ')'; } @Override @@ -119,18 +119,20 @@ public final class DefaultHttp2UnknownFrame extends DefaultByteBufHolder impleme return false; } DefaultHttp2UnknownFrame other = (DefaultHttp2UnknownFrame) o; - return super.equals(other) && flags().equals(other.flags()) - && frameType() == other.frameType() && (stream() == null && other.stream() == null) || - stream().equals(other.stream()); + Http2FrameStream otherStream = other.stream(); + return (stream == otherStream || otherStream != null && otherStream.equals(stream)) + && flags.equals(other.flags()) + && frameType == other.frameType() + && super.equals(other); } @Override public int hashCode() { int hash = super.hashCode(); - hash = hash * 31 + frameType(); - hash = hash * 31 + flags().hashCode(); - if (stream() != null) { - hash = hash * 31 + stream().hashCode(); + hash = hash * 31 + frameType; + hash = hash * 31 + flags.hashCode(); + if (stream != null) { + hash = hash * 31 + stream.hashCode(); } return hash; diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/DelegatingDecompressorFrameListener.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/DelegatingDecompressorFrameListener.java index 78ef230..6793f28 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/DelegatingDecompressorFrameListener.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/DelegatingDecompressorFrameListener.java @@ -33,9 +33,10 @@ import static io.netty.handler.codec.http.HttpHeaderValues.X_GZIP; import static io.netty.handler.codec.http2.Http2Error.INTERNAL_ERROR; import static io.netty.handler.codec.http2.Http2Exception.streamError; import static io.netty.util.internal.ObjectUtil.checkNotNull; +import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero; /** - * A HTTP2 frame listener that will decompress data frames according to the {@code content-encoding} header for each + * An HTTP2 frame listener that will decompress data frames according to the {@code content-encoding} header for each * stream. The decompression provided by this class will be applied to the data for the entire stream. */ @UnstableApi @@ -398,9 +399,7 @@ public class DelegatingDecompressorFrameListener extends Http2FrameListenerDecor * @return The number of pre-decompressed bytes that have been consumed. */ int consumeBytes(int streamId, int decompressedBytes) throws Http2Exception { - if (decompressedBytes < 0) { - throw new IllegalArgumentException("decompressedBytes must not be negative: " + decompressedBytes); - } + checkPositiveOrZero(decompressedBytes, "decompressedBytes"); if (decompressed - decompressedBytes < 0) { throw streamError(streamId, INTERNAL_ERROR, "Attempting to return too many bytes for stream %d. decompressed: %d " + diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/HpackDecoder.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/HpackDecoder.java index 9c680e6..6070bc5 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/HpackDecoder.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/HpackDecoder.java @@ -53,24 +53,31 @@ import static io.netty.util.internal.ThrowableUtil.unknownStackTrace; final class HpackDecoder { private static final Http2Exception DECODE_ULE_128_DECOMPRESSION_EXCEPTION = unknownStackTrace( - connectionError(COMPRESSION_ERROR, "HPACK - decompression failure"), HpackDecoder.class, + Http2Exception.newStatic(COMPRESSION_ERROR, "HPACK - decompression failure", + Http2Exception.ShutdownHint.HARD_SHUTDOWN), HpackDecoder.class, "decodeULE128(..)"); private static final Http2Exception DECODE_ULE_128_TO_LONG_DECOMPRESSION_EXCEPTION = unknownStackTrace( - connectionError(COMPRESSION_ERROR, "HPACK - long overflow"), HpackDecoder.class, "decodeULE128(..)"); + Http2Exception.newStatic(COMPRESSION_ERROR, "HPACK - long overflow", + Http2Exception.ShutdownHint.HARD_SHUTDOWN), HpackDecoder.class, "decodeULE128(..)"); private static final Http2Exception DECODE_ULE_128_TO_INT_DECOMPRESSION_EXCEPTION = unknownStackTrace( - connectionError(COMPRESSION_ERROR, "HPACK - int overflow"), HpackDecoder.class, "decodeULE128ToInt(..)"); + Http2Exception.newStatic(COMPRESSION_ERROR, "HPACK - int overflow", + Http2Exception.ShutdownHint.HARD_SHUTDOWN), HpackDecoder.class, "decodeULE128ToInt(..)"); private static final Http2Exception DECODE_ILLEGAL_INDEX_VALUE = unknownStackTrace( - connectionError(COMPRESSION_ERROR, "HPACK - illegal index value"), HpackDecoder.class, "decode(..)"); + Http2Exception.newStatic(COMPRESSION_ERROR, "HPACK - illegal index value", + Http2Exception.ShutdownHint.HARD_SHUTDOWN), HpackDecoder.class, "decode(..)"); private static final Http2Exception INDEX_HEADER_ILLEGAL_INDEX_VALUE = unknownStackTrace( - connectionError(COMPRESSION_ERROR, "HPACK - illegal index value"), HpackDecoder.class, "indexHeader(..)"); + Http2Exception.newStatic(COMPRESSION_ERROR, "HPACK - illegal index value", + Http2Exception.ShutdownHint.HARD_SHUTDOWN), HpackDecoder.class, "indexHeader(..)"); private static final Http2Exception READ_NAME_ILLEGAL_INDEX_VALUE = unknownStackTrace( - connectionError(COMPRESSION_ERROR, "HPACK - illegal index value"), HpackDecoder.class, "readName(..)"); + Http2Exception.newStatic(COMPRESSION_ERROR, "HPACK - illegal index value", + Http2Exception.ShutdownHint.HARD_SHUTDOWN), HpackDecoder.class, "readName(..)"); private static final Http2Exception INVALID_MAX_DYNAMIC_TABLE_SIZE = unknownStackTrace( - connectionError(COMPRESSION_ERROR, "HPACK - invalid max dynamic table size"), HpackDecoder.class, + Http2Exception.newStatic(COMPRESSION_ERROR, "HPACK - invalid max dynamic table size", + Http2Exception.ShutdownHint.HARD_SHUTDOWN), HpackDecoder.class, "setDynamicTableSize(..)"); private static final Http2Exception MAX_DYNAMIC_TABLE_SIZE_CHANGE_REQUIRED = unknownStackTrace( - connectionError(COMPRESSION_ERROR, "HPACK - max dynamic table size change required"), HpackDecoder.class, - "decode(..)"); + Http2Exception.newStatic(COMPRESSION_ERROR, "HPACK - max dynamic table size change required", + Http2Exception.ShutdownHint.HARD_SHUTDOWN), HpackDecoder.class, "decode(..)"); private static final byte READ_HEADER_REPRESENTATION = 0; private static final byte READ_MAX_DYNAMIC_TABLE_SIZE = 1; private static final byte READ_INDEXED_HEADER = 2; @@ -82,8 +89,8 @@ final class HpackDecoder { private static final byte READ_LITERAL_HEADER_VALUE_LENGTH = 8; private static final byte READ_LITERAL_HEADER_VALUE = 9; + private final HpackHuffmanDecoder huffmanDecoder = new HpackHuffmanDecoder(); private final HpackDynamicTable hpackDynamicTable; - private final HpackHuffmanDecoder hpackHuffmanDecoder; private long maxHeaderListSize; private long maxDynamicTableSize; private long encoderMaxDynamicTableSize; @@ -95,23 +102,21 @@ final class HpackDecoder { * This is because <a href="https://tools.ietf.org/html/rfc7540#section-6.5.1">SETTINGS_MAX_HEADER_LIST_SIZE</a> * allows a lower than advertised limit from being enforced, and the default limit is unlimited * (which is dangerous). - * @param initialHuffmanDecodeCapacity Size of an intermediate buffer used during huffman decode. */ - HpackDecoder(long maxHeaderListSize, int initialHuffmanDecodeCapacity) { - this(maxHeaderListSize, initialHuffmanDecodeCapacity, DEFAULT_HEADER_TABLE_SIZE); + HpackDecoder(long maxHeaderListSize) { + this(maxHeaderListSize, DEFAULT_HEADER_TABLE_SIZE); } /** * Exposed Used for testing only! Default values used in the initial settings frame are overridden intentionally * for testing but violate the RFC if used outside the scope of testing. */ - HpackDecoder(long maxHeaderListSize, int initialHuffmanDecodeCapacity, int maxHeaderTableSize) { + HpackDecoder(long maxHeaderListSize, int maxHeaderTableSize) { this.maxHeaderListSize = checkPositive(maxHeaderListSize, "maxHeaderListSize"); maxDynamicTableSize = encoderMaxDynamicTableSize = maxHeaderTableSize; maxDynamicTableSizeChangeRequired = false; hpackDynamicTable = new HpackDynamicTable(maxHeaderTableSize); - hpackHuffmanDecoder = new HpackHuffmanDecoder(initialHuffmanDecodeCapacity); } /** @@ -441,7 +446,7 @@ final class HpackDecoder { private CharSequence readStringLiteral(ByteBuf in, int length, boolean huffmanEncoded) throws Http2Exception { if (huffmanEncoded) { - return hpackHuffmanDecoder.decode(in, length); + return huffmanDecoder.decode(in, length); } byte[] buf = new byte[length]; in.readBytes(buf); @@ -528,7 +533,7 @@ final class HpackDecoder { private HeaderType previousType; private Http2Exception validationException; - public Http2HeadersSink(int streamId, Http2Headers headers, long maxHeaderListSize, boolean validate) { + Http2HeadersSink(int streamId, Http2Headers headers, long maxHeaderListSize, boolean validate) { this.headers = headers; this.maxHeaderListSize = maxHeaderListSize; this.streamId = streamId; diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/HpackEncoder.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/HpackEncoder.java index 7719072..5dab8f8 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/HpackEncoder.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/HpackEncoder.java @@ -41,6 +41,7 @@ import java.util.Arrays; import java.util.Map; import static io.netty.handler.codec.http2.HpackUtil.equalsConstantTime; +import static io.netty.handler.codec.http2.HpackUtil.equalsVariableTime; import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_HEADER_TABLE_SIZE; import static io.netty.handler.codec.http2.Http2CodecUtil.MAX_HEADER_LIST_SIZE; import static io.netty.handler.codec.http2.Http2CodecUtil.MAX_HEADER_TABLE_SIZE; @@ -53,7 +54,15 @@ import static io.netty.util.internal.MathUtil.findNextPositivePowerOfTwo; import static java.lang.Math.max; import static java.lang.Math.min; +/** + * An HPACK encoder. + * + * <p>Implementation note: This class is security sensitive, and depends on users correctly identifying their headers + * as security sensitive or not. If a header is considered not sensitive, methods names "insensitive" are used which + * are fast, but don't provide any security guarantees. + */ final class HpackEncoder { + static final int HUFF_CODE_THRESHOLD = 512; // a linked hash map of header fields private final HeaderEntry[] headerFields; private final HeaderEntry head = new HeaderEntry(-1, AsciiString.EMPTY_STRING, @@ -61,6 +70,7 @@ final class HpackEncoder { private final HpackHuffmanEncoder hpackHuffmanEncoder = new HpackHuffmanEncoder(); private final byte hashMask; private final boolean ignoreMaxHeaderListSize; + private final int huffCodeThreshold; private long size; private long maxHeaderTableSize; private long maxHeaderListSize; @@ -75,14 +85,14 @@ final class HpackEncoder { /** * Creates a new encoder. */ - public HpackEncoder(boolean ignoreMaxHeaderListSize) { - this(ignoreMaxHeaderListSize, 16); + HpackEncoder(boolean ignoreMaxHeaderListSize) { + this(ignoreMaxHeaderListSize, 16, HUFF_CODE_THRESHOLD); } /** * Creates a new encoder. */ - public HpackEncoder(boolean ignoreMaxHeaderListSize, int arraySizeHint) { + HpackEncoder(boolean ignoreMaxHeaderListSize, int arraySizeHint, int huffCodeThreshold) { this.ignoreMaxHeaderListSize = ignoreMaxHeaderListSize; maxHeaderTableSize = DEFAULT_HEADER_TABLE_SIZE; maxHeaderListSize = MAX_HEADER_LIST_SIZE; @@ -91,6 +101,7 @@ final class HpackEncoder { headerFields = new HeaderEntry[findNextPositivePowerOfTwo(max(2, min(arraySizeHint, 128)))]; hashMask = (byte) (headerFields.length - 1); head.before = head.after = head; + this.huffCodeThreshold = huffCodeThreshold; } /** @@ -150,7 +161,7 @@ final class HpackEncoder { // If the peer will only use the static table if (maxHeaderTableSize == 0) { - int staticTableIndex = HpackStaticTable.getIndex(name, value); + int staticTableIndex = HpackStaticTable.getIndexInsensitive(name, value); if (staticTableIndex == -1) { int nameIndex = HpackStaticTable.getIndex(name); encodeLiteral(out, name, value, IndexType.NONE, nameIndex); @@ -167,13 +178,13 @@ final class HpackEncoder { return; } - HeaderEntry headerField = getEntry(name, value); + HeaderEntry headerField = getEntryInsensitive(name, value); if (headerField != null) { int index = getIndex(headerField.index) + HpackStaticTable.length; // Section 6.1. Indexed Header Field Representation encodeInteger(out, 0x80, 7, index); } else { - int staticTableIndex = HpackStaticTable.getIndex(name, value); + int staticTableIndex = HpackStaticTable.getIndexInsensitive(name, value); if (staticTableIndex != -1) { // Section 6.1. Indexed Header Field Representation encodeInteger(out, 0x80, 7, staticTableIndex); @@ -250,8 +261,9 @@ final class HpackEncoder { * Encode string literal according to Section 5.2. */ private void encodeStringLiteral(ByteBuf out, CharSequence string) { - int huffmanLength = hpackHuffmanEncoder.getEncodedLength(string); - if (huffmanLength < string.length()) { + int huffmanLength; + if (string.length() >= huffCodeThreshold + && (huffmanLength = hpackHuffmanEncoder.getEncodedLength(string)) < string.length()) { encodeInteger(out, 0x80, 7, huffmanLength); hpackHuffmanEncoder.encode(out, string); } else { @@ -347,15 +359,16 @@ final class HpackEncoder { * Returns the header entry with the lowest index value for the header field. Returns null if * header field is not in the dynamic table. */ - private HeaderEntry getEntry(CharSequence name, CharSequence value) { + private HeaderEntry getEntryInsensitive(CharSequence name, CharSequence value) { if (length() == 0 || name == null || value == null) { return null; } int h = AsciiString.hashCode(name); int i = index(h); for (HeaderEntry e = headerFields[i]; e != null; e = e.next) { - // To avoid short circuit behavior a bitwise operator is used instead of a boolean operator. - if (e.hash == h && (equalsConstantTime(name, e.name) & equalsConstantTime(value, e.value)) != 0) { + // Check the value before then name, as it is more likely the value will be different incase there is no + // match. + if (e.hash == h && equalsVariableTime(value, e.value) && equalsVariableTime(name, e.name)) { return e; } } diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/HpackHeaderField.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/HpackHeaderField.java index 0b0d646..0cfddba 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/HpackHeaderField.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/HpackHeaderField.java @@ -31,6 +31,7 @@ */ package io.netty.handler.codec.http2; +import static io.netty.handler.codec.http2.HpackUtil.equalsVariableTime; import static io.netty.util.internal.ObjectUtil.checkNotNull; class HpackHeaderField { @@ -57,23 +58,8 @@ class HpackHeaderField { return name.length() + value.length() + HEADER_ENTRY_OVERHEAD; } - @Override - public final int hashCode() { - // TODO(nmittler): Netty's build rules require this. Probably need a better implementation. - return super.hashCode(); - } - - @Override - public final boolean equals(Object obj) { - if (obj == this) { - return true; - } - if (!(obj instanceof HpackHeaderField)) { - return false; - } - HpackHeaderField other = (HpackHeaderField) obj; - // To avoid short circuit behavior a bitwise operator is used instead of a boolean operator. - return (HpackUtil.equalsConstantTime(name, other.name) & HpackUtil.equalsConstantTime(value, other.value)) != 0; + public final boolean equalsForTest(HpackHeaderField other) { + return equalsVariableTime(name, other.name) && equalsVariableTime(value, other.value); } @Override diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/HpackHuffmanDecoder.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/HpackHuffmanDecoder.java index 9549c66..0b4a42c 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/HpackHuffmanDecoder.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/HpackHuffmanDecoder.java @@ -34,212 +34,4704 @@ package io.netty.handler.codec.http2; import io.netty.buffer.ByteBuf; import io.netty.util.AsciiString; import io.netty.util.ByteProcessor; -import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.ThrowableUtil; import static io.netty.handler.codec.http2.Http2Error.COMPRESSION_ERROR; -import static io.netty.handler.codec.http2.Http2Exception.connectionError; -final class HpackHuffmanDecoder { +final class HpackHuffmanDecoder implements ByteProcessor { - private static final Http2Exception EOS_DECODED = ThrowableUtil.unknownStackTrace( - connectionError(COMPRESSION_ERROR, "HPACK - EOS Decoded"), HpackHuffmanDecoder.class, "decode(..)"); - private static final Http2Exception INVALID_PADDING = ThrowableUtil.unknownStackTrace( - connectionError(COMPRESSION_ERROR, "HPACK - Invalid Padding"), HpackHuffmanDecoder.class, "decode(..)"); + /* Scroll to the bottom! */ - private static final Node ROOT = buildTree(HpackUtil.HUFFMAN_CODES, HpackUtil.HUFFMAN_CODE_LENGTHS); + private static final byte HUFFMAN_COMPLETE = 1; + private static final byte HUFFMAN_EMIT_SYMBOL = 1 << 1; + private static final byte HUFFMAN_FAIL = 1 << 2; - private final DecoderProcessor processor; - - HpackHuffmanDecoder(int initialCapacity) { - processor = new DecoderProcessor(initialCapacity); - } + private static final int HUFFMAN_COMPLETE_SHIFT = HUFFMAN_COMPLETE << 8; + private static final int HUFFMAN_EMIT_SYMBOL_SHIFT = HUFFMAN_EMIT_SYMBOL << 8; + private static final int HUFFMAN_FAIL_SHIFT = HUFFMAN_FAIL << 8; /** - * Decompresses the given Huffman coded string literal. + * A table of byte tuples (state, flags, output). They are packed together as: * - * @param buf the string literal to be decoded - * @return the output stream for the compressed data - * @throws Http2Exception EOS Decoded + * state<<16 + flags<<8 + output */ - public AsciiString decode(ByteBuf buf, int length) throws Http2Exception { - processor.reset(); - buf.forEachByte(buf.readerIndex(), length, processor); - buf.skipBytes(length); - return processor.end(); - } + private static final int[] HUFFS = new int[] { + // Node 0 (Root Node, never emits symbols.) + (4 << 16) + (0 << 8) + 0, + (5 << 16) + (0 << 8) + 0, + (7 << 16) + (0 << 8) + 0, + (8 << 16) + (0 << 8) + 0, + (11 << 16) + (0 << 8) + 0, + (12 << 16) + (0 << 8) + 0, + (16 << 16) + (0 << 8) + 0, + (19 << 16) + (0 << 8) + 0, + (25 << 16) + (0 << 8) + 0, + (28 << 16) + (0 << 8) + 0, + (32 << 16) + (0 << 8) + 0, + (35 << 16) + (0 << 8) + 0, + (42 << 16) + (0 << 8) + 0, + (49 << 16) + (0 << 8) + 0, + (57 << 16) + (0 << 8) + 0, + (64 << 16) + (HUFFMAN_COMPLETE << 8) + 0, - private static final class Node { + // Node 1 + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 48, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 49, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 50, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 97, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 99, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 101, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 105, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 111, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 115, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 116, + (13 << 16) + (0 << 8) + 0, + (14 << 16) + (0 << 8) + 0, + (17 << 16) + (0 << 8) + 0, + (18 << 16) + (0 << 8) + 0, + (20 << 16) + (0 << 8) + 0, + (21 << 16) + (0 << 8) + 0, - private final int symbol; // terminal nodes have a symbol - private final int bits; // number of bits matched by the node - private final Node[] children; // internal nodes have children + // Node 2 + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 48, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 48, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 49, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 49, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 50, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 50, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 97, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 97, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 99, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 99, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 101, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 101, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 105, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 105, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 111, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 111, - /** - * Construct an internal node - */ - Node() { - symbol = 0; - bits = 8; - children = new Node[256]; - } + // Node 3 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 48, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 48, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 48, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 48, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 49, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 49, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 49, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 49, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 50, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 50, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 50, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 50, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 97, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 97, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 97, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 97, - /** - * Construct a terminal node - * - * @param symbol the symbol the node represents - * @param bits the number of bits matched by this node - */ - Node(int symbol, int bits) { - assert bits > 0 && bits <= 8; - this.symbol = symbol; - this.bits = bits; - children = null; - } + // Node 4 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 48, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 48, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 48, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 48, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 48, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 48, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 48, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 48, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 49, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 49, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 49, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 49, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 49, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 49, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 49, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 49, - private boolean isTerminal() { - return children == null; - } - } + // Node 5 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 50, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 50, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 50, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 50, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 50, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 50, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 50, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 50, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 97, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 97, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 97, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 97, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 97, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 97, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 97, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 97, - private static Node buildTree(int[] codes, byte[] lengths) { - Node root = new Node(); - for (int i = 0; i < codes.length; i++) { - insert(root, i, codes[i], lengths[i]); - } - return root; - } + // Node 6 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 99, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 99, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 99, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 99, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 101, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 101, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 101, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 101, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 105, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 105, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 105, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 105, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 111, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 111, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 111, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 111, - private static void insert(Node root, int symbol, int code, byte length) { - // traverse tree using the most significant bytes of code - Node current = root; - while (length > 8) { - if (current.isTerminal()) { - throw new IllegalStateException("invalid Huffman code: prefix not unique"); - } - length -= 8; - int i = (code >>> length) & 0xFF; - if (current.children[i] == null) { - current.children[i] = new Node(); - } - current = current.children[i]; - } + // Node 7 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 99, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 99, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 99, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 99, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 99, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 99, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 99, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 99, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 101, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 101, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 101, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 101, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 101, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 101, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 101, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 101, - Node terminal = new Node(symbol, length); - int shift = 8 - length; - int start = (code << shift) & 0xFF; - int end = 1 << shift; - for (int i = start; i < start + end; i++) { - current.children[i] = terminal; - } - } + // Node 8 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 105, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 105, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 105, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 105, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 105, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 105, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 105, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 105, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 111, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 111, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 111, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 111, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 111, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 111, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 111, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 111, - private static final class DecoderProcessor implements ByteProcessor { - private final int initialCapacity; - private byte[] bytes; - private int index; - private Node node; - private int current; - private int currentBits; - private int symbolBits; - - DecoderProcessor(int initialCapacity) { - this.initialCapacity = ObjectUtil.checkPositive(initialCapacity, "initialCapacity"); - } + // Node 9 + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 115, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 115, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 116, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 116, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 32, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 37, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 45, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 46, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 47, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 51, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 52, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 53, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 54, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 55, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 56, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 57, - void reset() { - node = ROOT; - current = 0; - currentBits = 0; - symbolBits = 0; - bytes = new byte[initialCapacity]; - index = 0; - } + // Node 10 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 115, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 115, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 115, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 115, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 116, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 116, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 116, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 116, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 32, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 32, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 37, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 37, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 45, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 45, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 46, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 46, - /* - * The idea here is to consume whole bytes at a time rather than individual bits. node - * represents the Huffman tree, with all bit patterns denormalized as 256 children. Each - * child represents the last 8 bits of the huffman code. The parents of each child each - * represent the successive 8 bit chunks that lead up to the last most part. 8 bit bytes - * from buf are used to traverse these tree until a terminal node is found. - * - * current is a bit buffer. The low order bits represent how much of the huffman code has - * not been used to traverse the tree. Thus, the high order bits are just garbage. - * currentBits represents how many of the low order bits of current are actually valid. - * currentBits will vary between 0 and 15. - * - * symbolBits is the number of bits of the symbol being decoded, *including* all those of - * the parent nodes. symbolBits tells how far down the tree we are. For example, when - * decoding the invalid sequence {0xff, 0xff}, currentBits will be 0, but symbolBits will be - * 16. This is used to know if buf ended early (before consuming a whole symbol) or if - * there is too much padding. - */ - @Override - public boolean process(byte value) throws Http2Exception { - current = (current << 8) | (value & 0xFF); - currentBits += 8; - symbolBits += 8; - // While there are unconsumed bits in current, keep consuming symbols. - do { - node = node.children[(current >>> (currentBits - 8)) & 0xFF]; - currentBits -= node.bits; - if (node.isTerminal()) { - if (node.symbol == HpackUtil.HUFFMAN_EOS) { - throw EOS_DECODED; - } - append(node.symbol); - node = ROOT; - // Upon consuming a whole symbol, reset the symbol bits to the number of bits - // left over in the byte. - symbolBits = currentBits; - } - } while (currentBits >= 8); - return true; - } + // Node 11 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 115, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 115, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 115, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 115, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 115, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 115, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 115, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 115, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 116, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 116, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 116, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 116, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 116, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 116, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 116, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 116, - AsciiString end() throws Http2Exception { - /* - * We have consumed all the bytes in buf, but haven't consumed all the symbols. We may be on - * a partial symbol, so consume until there is nothing left. This will loop at most 2 times. - */ - while (currentBits > 0) { - node = node.children[(current << (8 - currentBits)) & 0xFF]; - if (node.isTerminal() && node.bits <= currentBits) { - if (node.symbol == HpackUtil.HUFFMAN_EOS) { - throw EOS_DECODED; - } - currentBits -= node.bits; - append(node.symbol); - node = ROOT; - symbolBits = currentBits; - } else { - break; - } - } + // Node 12 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 32, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 32, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 32, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 32, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 37, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 37, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 37, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 37, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 45, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 45, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 45, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 45, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 46, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 46, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 46, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 46, + + // Node 13 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 32, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 32, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 32, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 32, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 32, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 32, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 32, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 32, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 37, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 37, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 37, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 37, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 37, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 37, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 37, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 37, + + // Node 14 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 45, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 45, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 45, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 45, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 45, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 45, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 45, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 45, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 46, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 46, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 46, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 46, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 46, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 46, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 46, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 46, + + // Node 15 + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 47, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 47, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 51, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 51, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 52, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 52, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 53, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 53, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 54, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 54, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 55, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 55, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 56, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 56, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 57, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 57, + + // Node 16 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 47, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 47, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 47, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 47, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 51, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 51, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 51, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 51, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 52, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 52, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 52, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 52, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 53, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 53, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 53, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 53, + + // Node 17 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 47, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 47, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 47, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 47, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 47, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 47, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 47, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 47, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 51, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 51, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 51, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 51, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 51, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 51, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 51, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 51, + + // Node 18 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 52, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 52, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 52, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 52, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 52, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 52, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 52, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 52, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 53, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 53, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 53, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 53, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 53, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 53, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 53, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 53, + + // Node 19 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 54, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 54, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 54, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 54, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 55, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 55, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 55, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 55, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 56, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 56, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 56, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 56, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 57, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 57, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 57, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 57, + + // Node 20 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 54, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 54, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 54, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 54, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 54, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 54, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 54, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 54, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 55, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 55, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 55, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 55, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 55, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 55, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 55, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 55, + + // Node 21 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 56, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 56, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 56, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 56, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 56, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 56, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 56, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 56, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 57, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 57, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 57, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 57, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 57, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 57, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 57, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 57, + + // Node 22 + (26 << 16) + (0 << 8) + 0, + (27 << 16) + (0 << 8) + 0, + (29 << 16) + (0 << 8) + 0, + (30 << 16) + (0 << 8) + 0, + (33 << 16) + (0 << 8) + 0, + (34 << 16) + (0 << 8) + 0, + (36 << 16) + (0 << 8) + 0, + (37 << 16) + (0 << 8) + 0, + (43 << 16) + (0 << 8) + 0, + (46 << 16) + (0 << 8) + 0, + (50 << 16) + (0 << 8) + 0, + (53 << 16) + (0 << 8) + 0, + (58 << 16) + (0 << 8) + 0, + (61 << 16) + (0 << 8) + 0, + (65 << 16) + (0 << 8) + 0, + (68 << 16) + (HUFFMAN_COMPLETE << 8) + 0, + + // Node 23 + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 61, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 65, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 95, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 98, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 100, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 102, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 103, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 104, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 108, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 109, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 110, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 112, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 114, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 117, + (38 << 16) + (0 << 8) + 0, + (39 << 16) + (0 << 8) + 0, + + // Node 24 + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 61, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 61, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 65, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 65, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 95, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 95, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 98, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 98, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 100, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 100, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 102, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 102, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 103, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 103, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 104, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 104, + + // Node 25 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 61, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 61, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 61, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 61, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 65, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 65, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 65, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 65, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 95, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 95, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 95, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 95, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 98, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 98, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 98, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 98, + + // Node 26 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 61, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 61, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 61, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 61, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 61, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 61, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 61, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 61, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 65, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 65, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 65, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 65, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 65, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 65, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 65, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 65, + + // Node 27 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 95, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 95, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 95, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 95, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 95, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 95, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 95, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 95, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 98, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 98, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 98, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 98, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 98, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 98, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 98, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 98, + + // Node 28 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 100, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 100, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 100, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 100, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 102, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 102, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 102, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 102, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 103, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 103, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 103, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 103, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 104, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 104, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 104, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 104, + + // Node 29 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 100, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 100, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 100, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 100, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 100, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 100, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 100, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 100, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 102, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 102, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 102, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 102, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 102, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 102, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 102, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 102, + + // Node 30 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 103, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 103, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 103, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 103, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 103, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 103, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 103, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 103, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 104, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 104, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 104, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 104, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 104, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 104, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 104, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 104, + + // Node 31 + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 108, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 108, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 109, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 109, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 110, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 110, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 112, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 112, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 114, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 114, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 117, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 117, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 58, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 66, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 67, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 68, + + // Node 32 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 108, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 108, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 108, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 108, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 109, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 109, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 109, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 109, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 110, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 110, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 110, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 110, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 112, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 112, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 112, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 112, + + // Node 33 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 108, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 108, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 108, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 108, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 108, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 108, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 108, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 108, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 109, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 109, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 109, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 109, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 109, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 109, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 109, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 109, + + // Node 34 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 110, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 110, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 110, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 110, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 110, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 110, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 110, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 110, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 112, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 112, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 112, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 112, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 112, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 112, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 112, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 112, + + // Node 35 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 114, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 114, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 114, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 114, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 117, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 117, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 117, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 117, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 58, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 58, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 66, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 66, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 67, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 67, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 68, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 68, + + // Node 36 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 114, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 114, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 114, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 114, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 114, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 114, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 114, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 114, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 117, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 117, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 117, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 117, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 117, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 117, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 117, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 117, + + // Node 37 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 58, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 58, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 58, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 58, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 66, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 66, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 66, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 66, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 67, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 67, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 67, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 67, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 68, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 68, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 68, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 68, + + // Node 38 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 58, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 58, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 58, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 58, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 58, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 58, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 58, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 58, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 66, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 66, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 66, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 66, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 66, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 66, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 66, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 66, + + // Node 39 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 67, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 67, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 67, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 67, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 67, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 67, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 67, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 67, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 68, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 68, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 68, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 68, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 68, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 68, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 68, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 68, + + // Node 40 + (44 << 16) + (0 << 8) + 0, + (45 << 16) + (0 << 8) + 0, + (47 << 16) + (0 << 8) + 0, + (48 << 16) + (0 << 8) + 0, + (51 << 16) + (0 << 8) + 0, + (52 << 16) + (0 << 8) + 0, + (54 << 16) + (0 << 8) + 0, + (55 << 16) + (0 << 8) + 0, + (59 << 16) + (0 << 8) + 0, + (60 << 16) + (0 << 8) + 0, + (62 << 16) + (0 << 8) + 0, + (63 << 16) + (0 << 8) + 0, + (66 << 16) + (0 << 8) + 0, + (67 << 16) + (0 << 8) + 0, + (69 << 16) + (0 << 8) + 0, + (72 << 16) + (HUFFMAN_COMPLETE << 8) + 0, + + // Node 41 + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 69, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 70, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 71, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 72, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 73, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 74, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 75, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 76, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 77, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 78, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 79, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 80, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 81, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 82, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 83, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 84, + + // Node 42 + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 69, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 69, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 70, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 70, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 71, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 71, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 72, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 72, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 73, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 73, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 74, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 74, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 75, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 75, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 76, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 76, + + // Node 43 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 69, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 69, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 69, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 69, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 70, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 70, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 70, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 70, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 71, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 71, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 71, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 71, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 72, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 72, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 72, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 72, + + // Node 44 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 69, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 69, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 69, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 69, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 69, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 69, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 69, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 69, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 70, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 70, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 70, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 70, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 70, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 70, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 70, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 70, + + // Node 45 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 71, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 71, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 71, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 71, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 71, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 71, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 71, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 71, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 72, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 72, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 72, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 72, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 72, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 72, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 72, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 72, + + // Node 46 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 73, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 73, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 73, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 73, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 74, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 74, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 74, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 74, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 75, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 75, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 75, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 75, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 76, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 76, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 76, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 76, + + // Node 47 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 73, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 73, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 73, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 73, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 73, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 73, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 73, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 73, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 74, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 74, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 74, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 74, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 74, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 74, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 74, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 74, + + // Node 48 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 75, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 75, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 75, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 75, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 75, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 75, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 75, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 75, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 76, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 76, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 76, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 76, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 76, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 76, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 76, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 76, + + // Node 49 + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 77, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 77, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 78, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 78, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 79, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 79, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 80, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 80, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 81, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 81, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 82, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 82, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 83, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 83, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 84, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 84, + + // Node 50 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 77, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 77, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 77, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 77, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 78, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 78, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 78, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 78, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 79, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 79, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 79, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 79, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 80, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 80, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 80, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 80, + + // Node 51 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 77, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 77, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 77, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 77, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 77, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 77, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 77, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 77, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 78, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 78, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 78, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 78, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 78, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 78, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 78, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 78, + + // Node 52 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 79, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 79, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 79, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 79, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 79, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 79, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 79, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 79, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 80, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 80, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 80, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 80, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 80, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 80, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 80, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 80, + + // Node 53 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 81, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 81, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 81, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 81, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 82, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 82, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 82, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 82, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 83, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 83, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 83, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 83, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 84, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 84, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 84, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 84, + + // Node 54 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 81, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 81, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 81, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 81, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 81, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 81, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 81, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 81, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 82, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 82, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 82, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 82, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 82, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 82, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 82, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 82, + + // Node 55 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 83, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 83, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 83, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 83, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 83, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 83, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 83, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 83, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 84, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 84, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 84, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 84, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 84, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 84, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 84, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 84, + + // Node 56 + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 85, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 86, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 87, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 89, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 106, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 107, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 113, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 118, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 119, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 120, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 121, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 122, + (70 << 16) + (0 << 8) + 0, + (71 << 16) + (0 << 8) + 0, + (73 << 16) + (0 << 8) + 0, + (74 << 16) + (HUFFMAN_COMPLETE << 8) + 0, + + // Node 57 + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 85, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 85, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 86, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 86, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 87, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 87, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 89, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 89, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 106, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 106, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 107, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 107, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 113, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 113, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 118, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 118, + + // Node 58 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 85, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 85, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 85, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 85, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 86, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 86, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 86, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 86, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 87, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 87, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 87, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 87, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 89, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 89, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 89, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 89, + + // Node 59 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 85, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 85, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 85, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 85, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 85, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 85, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 85, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 85, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 86, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 86, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 86, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 86, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 86, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 86, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 86, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 86, + + // Node 60 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 87, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 87, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 87, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 87, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 87, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 87, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 87, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 87, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 89, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 89, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 89, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 89, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 89, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 89, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 89, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 89, + + // Node 61 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 106, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 106, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 106, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 106, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 107, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 107, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 107, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 107, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 113, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 113, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 113, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 113, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 118, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 118, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 118, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 118, + + // Node 62 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 106, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 106, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 106, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 106, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 106, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 106, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 106, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 106, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 107, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 107, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 107, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 107, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 107, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 107, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 107, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 107, + + // Node 63 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 113, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 113, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 113, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 113, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 113, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 113, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 113, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 113, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 118, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 118, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 118, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 118, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 118, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 118, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 118, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 118, + + // Node 64 + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 119, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 119, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 120, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 120, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 121, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 121, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 122, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 122, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 38, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 42, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 44, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 59, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 88, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 90, + (75 << 16) + (0 << 8) + 0, + (78 << 16) + (0 << 8) + 0, + + // Node 65 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 119, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 119, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 119, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 119, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 120, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 120, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 120, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 120, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 121, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 121, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 121, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 121, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 122, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 122, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 122, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 122, + + // Node 66 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 119, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 119, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 119, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 119, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 119, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 119, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 119, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 119, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 120, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 120, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 120, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 120, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 120, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 120, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 120, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 120, + + // Node 67 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 121, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 121, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 121, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 121, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 121, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 121, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 121, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 121, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 122, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 122, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 122, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 122, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 122, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 122, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 122, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 122, + + // Node 68 + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 38, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 38, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 42, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 42, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 44, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 44, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 59, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 59, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 88, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 88, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 90, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 90, + (76 << 16) + (0 << 8) + 0, + (77 << 16) + (0 << 8) + 0, + (79 << 16) + (0 << 8) + 0, + (81 << 16) + (0 << 8) + 0, + + // Node 69 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 38, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 38, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 38, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 38, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 42, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 42, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 42, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 42, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 44, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 44, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 44, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 44, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 59, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 59, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 59, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 59, + + // Node 70 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 38, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 38, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 38, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 38, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 38, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 38, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 38, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 38, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 42, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 42, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 42, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 42, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 42, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 42, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 42, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 42, + + // Node 71 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 44, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 44, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 44, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 44, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 44, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 44, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 44, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 44, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 59, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 59, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 59, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 59, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 59, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 59, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 59, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 59, + + // Node 72 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 88, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 88, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 88, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 88, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 90, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 90, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 90, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 90, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 33, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 34, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 40, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 41, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 63, + (80 << 16) + (0 << 8) + 0, + (82 << 16) + (0 << 8) + 0, + (84 << 16) + (0 << 8) + 0, + + // Node 73 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 88, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 88, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 88, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 88, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 88, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 88, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 88, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 88, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 90, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 90, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 90, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 90, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 90, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 90, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 90, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 90, + + // Node 74 + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 33, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 33, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 34, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 34, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 40, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 40, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 41, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 41, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 63, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 63, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 39, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 43, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 124, + (83 << 16) + (0 << 8) + 0, + (85 << 16) + (0 << 8) + 0, + (88 << 16) + (0 << 8) + 0, + + // Node 75 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 33, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 33, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 33, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 33, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 34, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 34, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 34, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 34, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 40, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 40, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 40, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 40, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 41, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 41, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 41, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 41, + + // Node 76 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 33, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 33, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 33, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 33, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 33, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 33, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 33, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 33, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 34, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 34, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 34, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 34, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 34, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 34, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 34, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 34, + + // Node 77 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 40, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 40, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 40, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 40, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 40, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 40, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 40, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 40, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 41, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 41, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 41, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 41, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 41, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 41, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 41, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 41, + + // Node 78 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 63, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 63, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 63, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 63, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 39, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 39, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 43, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 43, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 124, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 124, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 35, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 62, + (86 << 16) + (0 << 8) + 0, + (87 << 16) + (0 << 8) + 0, + (89 << 16) + (0 << 8) + 0, + (90 << 16) + (0 << 8) + 0, + + // Node 79 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 63, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 63, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 63, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 63, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 63, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 63, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 63, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 63, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 39, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 39, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 39, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 39, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 43, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 43, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 43, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 43, + + // Node 80 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 39, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 39, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 39, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 39, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 39, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 39, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 39, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 39, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 43, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 43, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 43, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 43, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 43, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 43, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 43, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 43, + + // Node 81 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 124, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 124, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 124, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 124, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 35, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 35, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 62, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 62, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 0, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 36, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 64, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 91, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 93, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 126, + (91 << 16) + (0 << 8) + 0, + (92 << 16) + (0 << 8) + 0, + + // Node 82 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 124, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 124, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 124, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 124, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 124, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 124, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 124, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 124, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 35, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 35, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 35, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 35, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 62, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 62, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 62, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 62, + + // Node 83 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 35, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 35, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 35, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 35, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 35, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 35, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 35, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 35, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 62, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 62, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 62, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 62, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 62, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 62, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 62, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 62, + + // Node 84 + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 0, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 0, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 36, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 36, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 64, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 64, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 91, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 91, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 93, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 93, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 126, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 126, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 94, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 125, + (93 << 16) + (0 << 8) + 0, + (94 << 16) + (0 << 8) + 0, + + // Node 85 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 0, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 0, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 0, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 0, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 36, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 36, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 36, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 36, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 64, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 64, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 64, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 64, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 91, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 91, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 91, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 91, + + // Node 86 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 0, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 0, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 0, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 0, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 0, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 0, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 0, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 0, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 36, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 36, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 36, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 36, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 36, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 36, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 36, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 36, + + // Node 87 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 64, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 64, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 64, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 64, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 64, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 64, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 64, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 64, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 91, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 91, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 91, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 91, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 91, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 91, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 91, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 91, + + // Node 88 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 93, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 93, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 93, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 93, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 126, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 126, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 126, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 126, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 94, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 94, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 125, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 125, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 60, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 96, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 123, + (95 << 16) + (0 << 8) + 0, + + // Node 89 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 93, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 93, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 93, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 93, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 93, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 93, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 93, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 93, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 126, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 126, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 126, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 126, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 126, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 126, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 126, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 126, + + // Node 90 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 94, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 94, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 94, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 94, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 125, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 125, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 125, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 125, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 60, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 60, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 96, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 96, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 123, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 123, + (96 << 16) + (0 << 8) + 0, + (110 << 16) + (0 << 8) + 0, + + // Node 91 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 94, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 94, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 94, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 94, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 94, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 94, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 94, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 94, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 125, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 125, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 125, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 125, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 125, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 125, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 125, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 125, + + // Node 92 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 60, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 60, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 60, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 60, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 96, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 96, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 96, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 96, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 123, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 123, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 123, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 123, + (97 << 16) + (0 << 8) + 0, + (101 << 16) + (0 << 8) + 0, + (111 << 16) + (0 << 8) + 0, + (133 << 16) + (0 << 8) + 0, + + // Node 93 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 60, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 60, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 60, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 60, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 60, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 60, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 60, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 60, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 96, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 96, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 96, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 96, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 96, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 96, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 96, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 96, + + // Node 94 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 123, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 123, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 123, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 123, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 123, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 123, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 123, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 123, + (98 << 16) + (0 << 8) + 0, + (99 << 16) + (0 << 8) + 0, + (102 << 16) + (0 << 8) + 0, + (105 << 16) + (0 << 8) + 0, + (112 << 16) + (0 << 8) + 0, + (119 << 16) + (0 << 8) + 0, + (134 << 16) + (0 << 8) + 0, + (153 << 16) + (0 << 8) + 0, + + // Node 95 + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 92, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 195, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 208, + (100 << 16) + (0 << 8) + 0, + (103 << 16) + (0 << 8) + 0, + (104 << 16) + (0 << 8) + 0, + (106 << 16) + (0 << 8) + 0, + (107 << 16) + (0 << 8) + 0, + (113 << 16) + (0 << 8) + 0, + (116 << 16) + (0 << 8) + 0, + (120 << 16) + (0 << 8) + 0, + (126 << 16) + (0 << 8) + 0, + (135 << 16) + (0 << 8) + 0, + (142 << 16) + (0 << 8) + 0, + (154 << 16) + (0 << 8) + 0, + (169 << 16) + (0 << 8) + 0, + + // Node 96 + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 92, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 92, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 195, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 195, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 208, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 208, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 128, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 130, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 131, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 162, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 184, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 194, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 224, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 226, + (108 << 16) + (0 << 8) + 0, + (109 << 16) + (0 << 8) + 0, + + // Node 97 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 92, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 92, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 92, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 92, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 195, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 195, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 195, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 195, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 208, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 208, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 208, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 208, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 128, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 128, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 130, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 130, + + // Node 98 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 92, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 92, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 92, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 92, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 92, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 92, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 92, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 92, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 195, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 195, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 195, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 195, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 195, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 195, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 195, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 195, + + // Node 99 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 208, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 208, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 208, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 208, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 208, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 208, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 208, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 208, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 128, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 128, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 128, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 128, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 130, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 130, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 130, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 130, + + // Node 100 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 128, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 128, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 128, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 128, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 128, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 128, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 128, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 128, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 130, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 130, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 130, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 130, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 130, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 130, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 130, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 130, + + // Node 101 + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 131, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 131, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 162, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 162, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 184, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 184, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 194, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 194, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 224, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 224, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 226, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 226, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 153, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 161, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 167, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 172, + + // Node 102 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 131, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 131, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 131, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 131, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 162, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 162, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 162, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 162, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 184, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 184, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 184, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 184, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 194, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 194, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 194, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 194, + + // Node 103 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 131, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 131, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 131, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 131, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 131, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 131, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 131, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 131, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 162, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 162, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 162, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 162, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 162, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 162, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 162, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 162, + + // Node 104 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 184, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 184, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 184, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 184, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 184, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 184, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 184, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 184, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 194, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 194, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 194, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 194, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 194, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 194, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 194, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 194, + + // Node 105 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 224, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 224, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 224, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 224, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 226, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 226, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 226, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 226, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 153, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 153, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 161, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 161, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 167, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 167, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 172, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 172, + + // Node 106 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 224, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 224, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 224, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 224, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 224, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 224, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 224, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 224, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 226, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 226, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 226, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 226, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 226, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 226, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 226, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 226, + + // Node 107 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 153, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 153, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 153, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 153, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 161, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 161, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 161, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 161, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 167, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 167, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 167, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 167, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 172, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 172, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 172, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 172, + + // Node 108 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 153, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 153, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 153, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 153, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 153, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 153, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 153, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 153, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 161, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 161, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 161, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 161, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 161, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 161, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 161, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 161, + + // Node 109 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 167, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 167, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 167, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 167, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 167, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 167, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 167, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 167, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 172, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 172, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 172, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 172, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 172, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 172, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 172, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 172, + + // Node 110 + (114 << 16) + (0 << 8) + 0, + (115 << 16) + (0 << 8) + 0, + (117 << 16) + (0 << 8) + 0, + (118 << 16) + (0 << 8) + 0, + (121 << 16) + (0 << 8) + 0, + (123 << 16) + (0 << 8) + 0, + (127 << 16) + (0 << 8) + 0, + (130 << 16) + (0 << 8) + 0, + (136 << 16) + (0 << 8) + 0, + (139 << 16) + (0 << 8) + 0, + (143 << 16) + (0 << 8) + 0, + (146 << 16) + (0 << 8) + 0, + (155 << 16) + (0 << 8) + 0, + (162 << 16) + (0 << 8) + 0, + (170 << 16) + (0 << 8) + 0, + (180 << 16) + (0 << 8) + 0, + + // Node 111 + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 176, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 177, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 179, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 209, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 216, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 217, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 227, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 229, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 230, + (122 << 16) + (0 << 8) + 0, + (124 << 16) + (0 << 8) + 0, + (125 << 16) + (0 << 8) + 0, + (128 << 16) + (0 << 8) + 0, + (129 << 16) + (0 << 8) + 0, + (131 << 16) + (0 << 8) + 0, + (132 << 16) + (0 << 8) + 0, + + // Node 112 + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 176, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 176, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 177, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 177, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 179, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 179, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 209, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 209, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 216, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 216, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 217, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 217, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 227, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 227, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 229, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 229, + + // Node 113 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 176, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 176, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 176, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 176, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 177, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 177, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 177, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 177, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 179, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 179, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 179, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 179, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 209, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 209, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 209, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 209, + + // Node 114 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 176, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 176, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 176, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 176, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 176, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 176, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 176, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 176, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 177, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 177, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 177, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 177, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 177, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 177, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 177, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 177, + + // Node 115 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 179, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 179, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 179, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 179, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 179, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 179, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 179, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 179, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 209, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 209, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 209, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 209, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 209, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 209, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 209, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 209, + + // Node 116 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 216, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 216, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 216, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 216, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 217, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 217, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 217, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 217, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 227, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 227, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 227, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 227, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 229, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 229, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 229, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 229, + + // Node 117 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 216, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 216, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 216, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 216, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 216, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 216, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 216, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 216, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 217, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 217, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 217, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 217, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 217, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 217, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 217, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 217, + + // Node 118 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 227, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 227, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 227, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 227, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 227, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 227, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 227, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 227, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 229, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 229, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 229, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 229, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 229, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 229, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 229, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 229, + + // Node 119 + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 230, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 230, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 129, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 132, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 133, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 134, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 136, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 146, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 154, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 156, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 160, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 163, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 164, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 169, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 170, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 173, + + // Node 120 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 230, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 230, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 230, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 230, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 129, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 129, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 132, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 132, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 133, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 133, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 134, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 134, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 136, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 136, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 146, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 146, + + // Node 121 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 230, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 230, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 230, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 230, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 230, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 230, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 230, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 230, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 129, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 129, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 129, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 129, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 132, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 132, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 132, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 132, + + // Node 122 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 129, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 129, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 129, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 129, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 129, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 129, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 129, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 129, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 132, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 132, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 132, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 132, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 132, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 132, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 132, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 132, + + // Node 123 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 133, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 133, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 133, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 133, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 134, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 134, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 134, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 134, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 136, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 136, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 136, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 136, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 146, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 146, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 146, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 146, + + // Node 124 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 133, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 133, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 133, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 133, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 133, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 133, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 133, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 133, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 134, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 134, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 134, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 134, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 134, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 134, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 134, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 134, + + // Node 125 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 136, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 136, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 136, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 136, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 136, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 136, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 136, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 136, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 146, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 146, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 146, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 146, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 146, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 146, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 146, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 146, + + // Node 126 + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 154, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 154, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 156, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 156, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 160, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 160, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 163, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 163, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 164, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 164, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 169, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 169, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 170, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 170, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 173, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 173, + + // Node 127 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 154, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 154, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 154, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 154, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 156, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 156, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 156, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 156, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 160, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 160, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 160, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 160, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 163, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 163, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 163, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 163, + + // Node 128 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 154, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 154, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 154, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 154, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 154, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 154, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 154, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 154, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 156, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 156, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 156, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 156, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 156, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 156, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 156, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 156, + + // Node 129 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 160, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 160, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 160, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 160, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 160, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 160, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 160, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 160, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 163, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 163, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 163, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 163, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 163, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 163, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 163, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 163, + + // Node 130 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 164, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 164, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 164, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 164, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 169, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 169, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 169, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 169, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 170, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 170, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 170, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 170, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 173, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 173, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 173, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 173, + + // Node 131 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 164, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 164, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 164, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 164, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 164, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 164, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 164, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 164, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 169, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 169, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 169, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 169, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 169, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 169, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 169, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 169, - // Section 5.2. String Literal Representation - // A padding strictly longer than 7 bits MUST be treated as a decoding error. - // Padding not corresponding to the most significant bits of the code - // for the EOS symbol (0xFF) MUST be treated as a decoding error. - int mask = (1 << symbolBits) - 1; - if (symbolBits > 7 || (current & mask) != mask) { - throw INVALID_PADDING; + // Node 132 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 170, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 170, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 170, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 170, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 170, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 170, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 170, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 170, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 173, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 173, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 173, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 173, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 173, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 173, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 173, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 173, + + // Node 133 + (137 << 16) + (0 << 8) + 0, + (138 << 16) + (0 << 8) + 0, + (140 << 16) + (0 << 8) + 0, + (141 << 16) + (0 << 8) + 0, + (144 << 16) + (0 << 8) + 0, + (145 << 16) + (0 << 8) + 0, + (147 << 16) + (0 << 8) + 0, + (150 << 16) + (0 << 8) + 0, + (156 << 16) + (0 << 8) + 0, + (159 << 16) + (0 << 8) + 0, + (163 << 16) + (0 << 8) + 0, + (166 << 16) + (0 << 8) + 0, + (171 << 16) + (0 << 8) + 0, + (174 << 16) + (0 << 8) + 0, + (181 << 16) + (0 << 8) + 0, + (190 << 16) + (0 << 8) + 0, + + // Node 134 + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 178, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 181, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 185, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 186, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 187, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 189, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 190, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 196, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 198, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 228, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 232, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 233, + (148 << 16) + (0 << 8) + 0, + (149 << 16) + (0 << 8) + 0, + (151 << 16) + (0 << 8) + 0, + (152 << 16) + (0 << 8) + 0, + + // Node 135 + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 178, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 178, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 181, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 181, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 185, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 185, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 186, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 186, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 187, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 187, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 189, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 189, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 190, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 190, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 196, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 196, + + // Node 136 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 178, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 178, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 178, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 178, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 181, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 181, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 181, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 181, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 185, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 185, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 185, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 185, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 186, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 186, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 186, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 186, + + // Node 137 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 178, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 178, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 178, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 178, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 178, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 178, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 178, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 178, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 181, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 181, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 181, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 181, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 181, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 181, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 181, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 181, + + // Node 138 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 185, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 185, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 185, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 185, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 185, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 185, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 185, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 185, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 186, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 186, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 186, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 186, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 186, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 186, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 186, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 186, + + // Node 139 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 187, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 187, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 187, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 187, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 189, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 189, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 189, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 189, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 190, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 190, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 190, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 190, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 196, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 196, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 196, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 196, + + // Node 140 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 187, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 187, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 187, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 187, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 187, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 187, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 187, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 187, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 189, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 189, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 189, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 189, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 189, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 189, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 189, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 189, + + // Node 141 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 190, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 190, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 190, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 190, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 190, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 190, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 190, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 190, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 196, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 196, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 196, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 196, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 196, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 196, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 196, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 196, + + // Node 142 + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 198, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 198, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 228, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 228, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 232, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 232, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 233, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 233, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 1, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 135, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 137, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 138, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 139, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 140, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 141, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 143, + + // Node 143 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 198, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 198, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 198, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 198, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 228, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 228, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 228, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 228, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 232, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 232, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 232, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 232, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 233, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 233, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 233, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 233, + + // Node 144 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 198, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 198, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 198, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 198, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 198, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 198, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 198, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 198, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 228, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 228, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 228, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 228, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 228, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 228, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 228, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 228, + + // Node 145 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 232, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 232, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 232, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 232, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 232, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 232, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 232, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 232, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 233, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 233, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 233, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 233, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 233, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 233, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 233, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 233, + + // Node 146 + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 1, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 1, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 135, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 135, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 137, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 137, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 138, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 138, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 139, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 139, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 140, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 140, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 141, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 141, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 143, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 143, + + // Node 147 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 1, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 1, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 1, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 1, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 135, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 135, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 135, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 135, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 137, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 137, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 137, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 137, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 138, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 138, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 138, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 138, + + // Node 148 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 1, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 1, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 1, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 1, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 1, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 1, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 1, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 1, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 135, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 135, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 135, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 135, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 135, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 135, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 135, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 135, + + // Node 149 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 137, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 137, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 137, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 137, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 137, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 137, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 137, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 137, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 138, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 138, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 138, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 138, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 138, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 138, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 138, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 138, + + // Node 150 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 139, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 139, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 139, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 139, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 140, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 140, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 140, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 140, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 141, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 141, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 141, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 141, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 143, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 143, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 143, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 143, + + // Node 151 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 139, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 139, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 139, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 139, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 139, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 139, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 139, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 139, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 140, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 140, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 140, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 140, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 140, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 140, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 140, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 140, + + // Node 152 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 141, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 141, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 141, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 141, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 141, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 141, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 141, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 141, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 143, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 143, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 143, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 143, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 143, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 143, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 143, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 143, + + // Node 153 + (157 << 16) + (0 << 8) + 0, + (158 << 16) + (0 << 8) + 0, + (160 << 16) + (0 << 8) + 0, + (161 << 16) + (0 << 8) + 0, + (164 << 16) + (0 << 8) + 0, + (165 << 16) + (0 << 8) + 0, + (167 << 16) + (0 << 8) + 0, + (168 << 16) + (0 << 8) + 0, + (172 << 16) + (0 << 8) + 0, + (173 << 16) + (0 << 8) + 0, + (175 << 16) + (0 << 8) + 0, + (177 << 16) + (0 << 8) + 0, + (182 << 16) + (0 << 8) + 0, + (185 << 16) + (0 << 8) + 0, + (191 << 16) + (0 << 8) + 0, + (207 << 16) + (0 << 8) + 0, + + // Node 154 + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 147, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 149, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 150, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 151, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 152, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 155, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 157, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 158, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 165, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 166, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 168, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 174, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 175, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 180, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 182, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 183, + + // Node 155 + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 147, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 147, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 149, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 149, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 150, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 150, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 151, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 151, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 152, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 152, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 155, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 155, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 157, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 157, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 158, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 158, + + // Node 156 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 147, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 147, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 147, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 147, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 149, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 149, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 149, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 149, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 150, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 150, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 150, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 150, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 151, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 151, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 151, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 151, + + // Node 157 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 147, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 147, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 147, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 147, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 147, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 147, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 147, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 147, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 149, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 149, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 149, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 149, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 149, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 149, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 149, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 149, + + // Node 158 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 150, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 150, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 150, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 150, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 150, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 150, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 150, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 150, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 151, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 151, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 151, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 151, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 151, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 151, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 151, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 151, + + // Node 159 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 152, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 152, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 152, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 152, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 155, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 155, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 155, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 155, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 157, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 157, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 157, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 157, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 158, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 158, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 158, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 158, + + // Node 160 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 152, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 152, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 152, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 152, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 152, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 152, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 152, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 152, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 155, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 155, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 155, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 155, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 155, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 155, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 155, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 155, + + // Node 161 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 157, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 157, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 157, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 157, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 157, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 157, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 157, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 157, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 158, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 158, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 158, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 158, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 158, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 158, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 158, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 158, + + // Node 162 + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 165, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 165, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 166, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 166, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 168, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 168, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 174, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 174, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 175, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 175, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 180, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 180, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 182, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 182, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 183, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 183, + + // Node 163 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 165, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 165, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 165, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 165, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 166, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 166, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 166, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 166, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 168, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 168, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 168, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 168, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 174, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 174, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 174, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 174, + + // Node 164 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 165, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 165, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 165, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 165, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 165, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 165, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 165, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 165, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 166, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 166, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 166, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 166, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 166, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 166, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 166, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 166, + + // Node 165 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 168, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 168, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 168, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 168, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 168, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 168, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 168, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 168, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 174, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 174, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 174, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 174, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 174, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 174, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 174, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 174, + + // Node 166 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 175, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 175, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 175, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 175, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 180, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 180, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 180, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 180, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 182, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 182, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 182, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 182, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 183, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 183, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 183, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 183, + + // Node 167 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 175, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 175, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 175, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 175, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 175, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 175, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 175, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 175, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 180, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 180, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 180, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 180, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 180, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 180, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 180, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 180, + + // Node 168 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 182, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 182, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 182, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 182, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 182, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 182, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 182, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 182, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 183, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 183, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 183, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 183, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 183, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 183, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 183, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 183, + + // Node 169 + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 188, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 191, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 197, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 231, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 239, + (176 << 16) + (0 << 8) + 0, + (178 << 16) + (0 << 8) + 0, + (179 << 16) + (0 << 8) + 0, + (183 << 16) + (0 << 8) + 0, + (184 << 16) + (0 << 8) + 0, + (186 << 16) + (0 << 8) + 0, + (187 << 16) + (0 << 8) + 0, + (192 << 16) + (0 << 8) + 0, + (199 << 16) + (0 << 8) + 0, + (208 << 16) + (0 << 8) + 0, + (223 << 16) + (0 << 8) + 0, + + // Node 170 + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 188, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 188, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 191, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 191, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 197, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 197, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 231, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 231, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 239, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 239, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 9, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 142, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 144, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 145, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 148, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 159, + + // Node 171 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 188, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 188, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 188, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 188, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 191, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 191, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 191, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 191, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 197, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 197, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 197, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 197, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 231, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 231, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 231, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 231, + + // Node 172 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 188, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 188, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 188, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 188, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 188, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 188, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 188, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 188, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 191, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 191, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 191, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 191, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 191, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 191, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 191, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 191, + + // Node 173 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 197, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 197, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 197, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 197, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 197, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 197, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 197, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 197, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 231, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 231, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 231, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 231, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 231, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 231, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 231, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 231, + + // Node 174 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 239, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 239, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 239, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 239, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 9, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 9, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 142, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 142, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 144, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 144, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 145, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 145, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 148, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 148, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 159, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 159, + + // Node 175 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 239, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 239, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 239, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 239, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 239, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 239, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 239, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 239, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 9, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 9, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 9, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 9, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 142, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 142, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 142, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 142, + + // Node 176 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 9, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 9, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 9, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 9, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 9, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 9, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 9, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 9, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 142, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 142, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 142, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 142, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 142, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 142, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 142, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 142, + + // Node 177 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 144, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 144, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 144, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 144, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 145, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 145, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 145, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 145, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 148, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 148, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 148, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 148, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 159, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 159, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 159, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 159, + + // Node 178 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 144, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 144, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 144, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 144, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 144, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 144, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 144, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 144, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 145, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 145, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 145, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 145, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 145, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 145, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 145, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 145, + + // Node 179 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 148, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 148, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 148, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 148, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 148, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 148, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 148, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 148, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 159, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 159, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 159, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 159, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 159, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 159, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 159, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 159, + + // Node 180 + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 171, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 206, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 215, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 225, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 236, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 237, + (188 << 16) + (0 << 8) + 0, + (189 << 16) + (0 << 8) + 0, + (193 << 16) + (0 << 8) + 0, + (196 << 16) + (0 << 8) + 0, + (200 << 16) + (0 << 8) + 0, + (203 << 16) + (0 << 8) + 0, + (209 << 16) + (0 << 8) + 0, + (216 << 16) + (0 << 8) + 0, + (224 << 16) + (0 << 8) + 0, + (238 << 16) + (0 << 8) + 0, + + // Node 181 + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 171, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 171, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 206, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 206, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 215, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 215, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 225, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 225, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 236, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 236, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 237, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 237, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 199, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 207, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 234, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 235, + + // Node 182 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 171, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 171, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 171, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 171, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 206, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 206, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 206, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 206, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 215, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 215, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 215, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 215, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 225, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 225, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 225, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 225, + + // Node 183 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 171, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 171, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 171, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 171, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 171, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 171, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 171, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 171, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 206, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 206, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 206, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 206, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 206, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 206, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 206, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 206, + + // Node 184 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 215, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 215, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 215, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 215, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 215, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 215, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 215, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 215, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 225, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 225, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 225, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 225, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 225, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 225, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 225, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 225, + + // Node 185 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 236, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 236, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 236, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 236, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 237, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 237, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 237, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 237, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 199, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 199, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 207, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 207, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 234, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 234, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 235, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 235, + + // Node 186 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 236, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 236, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 236, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 236, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 236, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 236, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 236, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 236, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 237, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 237, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 237, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 237, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 237, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 237, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 237, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 237, + + // Node 187 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 199, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 199, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 199, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 199, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 207, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 207, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 207, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 207, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 234, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 234, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 234, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 234, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 235, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 235, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 235, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 235, + + // Node 188 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 199, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 199, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 199, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 199, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 199, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 199, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 199, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 199, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 207, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 207, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 207, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 207, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 207, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 207, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 207, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 207, + + // Node 189 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 234, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 234, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 234, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 234, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 234, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 234, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 234, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 234, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 235, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 235, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 235, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 235, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 235, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 235, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 235, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 235, + + // Node 190 + (194 << 16) + (0 << 8) + 0, + (195 << 16) + (0 << 8) + 0, + (197 << 16) + (0 << 8) + 0, + (198 << 16) + (0 << 8) + 0, + (201 << 16) + (0 << 8) + 0, + (202 << 16) + (0 << 8) + 0, + (204 << 16) + (0 << 8) + 0, + (205 << 16) + (0 << 8) + 0, + (210 << 16) + (0 << 8) + 0, + (213 << 16) + (0 << 8) + 0, + (217 << 16) + (0 << 8) + 0, + (220 << 16) + (0 << 8) + 0, + (225 << 16) + (0 << 8) + 0, + (231 << 16) + (0 << 8) + 0, + (239 << 16) + (0 << 8) + 0, + (246 << 16) + (0 << 8) + 0, + + // Node 191 + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 192, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 193, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 200, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 201, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 202, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 205, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 210, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 213, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 218, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 219, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 238, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 240, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 242, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 243, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 255, + (206 << 16) + (0 << 8) + 0, + + // Node 192 + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 192, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 192, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 193, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 193, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 200, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 200, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 201, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 201, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 202, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 202, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 205, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 205, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 210, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 210, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 213, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 213, + + // Node 193 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 192, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 192, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 192, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 192, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 193, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 193, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 193, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 193, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 200, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 200, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 200, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 200, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 201, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 201, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 201, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 201, + + // Node 194 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 192, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 192, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 192, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 192, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 192, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 192, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 192, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 192, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 193, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 193, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 193, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 193, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 193, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 193, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 193, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 193, + + // Node 195 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 200, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 200, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 200, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 200, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 200, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 200, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 200, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 200, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 201, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 201, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 201, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 201, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 201, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 201, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 201, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 201, + + // Node 196 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 202, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 202, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 202, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 202, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 205, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 205, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 205, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 205, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 210, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 210, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 210, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 210, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 213, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 213, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 213, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 213, + + // Node 197 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 202, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 202, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 202, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 202, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 202, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 202, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 202, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 202, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 205, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 205, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 205, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 205, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 205, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 205, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 205, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 205, + + // Node 198 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 210, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 210, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 210, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 210, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 210, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 210, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 210, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 210, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 213, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 213, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 213, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 213, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 213, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 213, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 213, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 213, + + // Node 199 + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 218, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 218, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 219, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 219, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 238, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 238, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 240, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 240, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 242, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 242, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 243, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 243, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 255, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 255, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 203, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 204, + + // Node 200 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 218, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 218, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 218, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 218, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 219, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 219, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 219, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 219, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 238, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 238, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 238, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 238, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 240, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 240, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 240, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 240, + + // Node 201 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 218, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 218, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 218, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 218, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 218, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 218, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 218, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 218, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 219, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 219, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 219, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 219, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 219, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 219, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 219, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 219, + + // Node 202 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 238, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 238, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 238, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 238, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 238, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 238, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 238, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 238, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 240, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 240, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 240, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 240, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 240, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 240, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 240, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 240, + + // Node 203 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 242, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 242, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 242, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 242, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 243, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 243, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 243, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 243, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 255, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 255, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 255, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 255, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 203, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 203, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 204, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 204, + + // Node 204 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 242, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 242, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 242, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 242, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 242, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 242, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 242, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 242, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 243, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 243, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 243, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 243, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 243, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 243, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 243, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 243, + + // Node 205 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 255, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 255, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 255, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 255, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 255, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 255, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 255, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 255, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 203, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 203, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 203, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 203, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 204, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 204, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 204, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 204, + + // Node 206 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 203, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 203, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 203, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 203, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 203, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 203, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 203, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 203, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 204, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 204, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 204, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 204, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 204, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 204, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 204, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 204, + + // Node 207 + (211 << 16) + (0 << 8) + 0, + (212 << 16) + (0 << 8) + 0, + (214 << 16) + (0 << 8) + 0, + (215 << 16) + (0 << 8) + 0, + (218 << 16) + (0 << 8) + 0, + (219 << 16) + (0 << 8) + 0, + (221 << 16) + (0 << 8) + 0, + (222 << 16) + (0 << 8) + 0, + (226 << 16) + (0 << 8) + 0, + (228 << 16) + (0 << 8) + 0, + (232 << 16) + (0 << 8) + 0, + (235 << 16) + (0 << 8) + 0, + (240 << 16) + (0 << 8) + 0, + (243 << 16) + (0 << 8) + 0, + (247 << 16) + (0 << 8) + 0, + (250 << 16) + (0 << 8) + 0, + + // Node 208 + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 211, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 212, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 214, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 221, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 222, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 223, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 241, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 244, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 245, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 246, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 247, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 248, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 250, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 251, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 252, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 253, + + // Node 209 + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 211, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 211, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 212, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 212, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 214, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 214, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 221, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 221, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 222, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 222, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 223, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 223, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 241, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 241, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 244, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 244, + + // Node 210 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 211, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 211, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 211, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 211, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 212, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 212, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 212, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 212, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 214, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 214, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 214, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 214, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 221, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 221, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 221, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 221, + + // Node 211 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 211, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 211, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 211, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 211, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 211, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 211, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 211, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 211, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 212, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 212, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 212, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 212, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 212, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 212, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 212, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 212, + + // Node 212 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 214, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 214, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 214, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 214, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 214, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 214, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 214, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 214, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 221, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 221, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 221, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 221, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 221, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 221, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 221, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 221, + + // Node 213 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 222, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 222, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 222, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 222, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 223, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 223, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 223, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 223, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 241, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 241, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 241, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 241, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 244, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 244, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 244, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 244, + + // Node 214 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 222, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 222, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 222, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 222, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 222, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 222, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 222, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 222, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 223, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 223, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 223, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 223, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 223, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 223, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 223, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 223, + + // Node 215 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 241, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 241, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 241, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 241, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 241, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 241, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 241, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 241, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 244, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 244, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 244, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 244, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 244, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 244, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 244, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 244, + + // Node 216 + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 245, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 245, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 246, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 246, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 247, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 247, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 248, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 248, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 250, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 250, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 251, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 251, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 252, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 252, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 253, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 253, + + // Node 217 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 245, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 245, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 245, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 245, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 246, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 246, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 246, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 246, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 247, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 247, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 247, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 247, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 248, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 248, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 248, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 248, + + // Node 218 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 245, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 245, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 245, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 245, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 245, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 245, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 245, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 245, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 246, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 246, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 246, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 246, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 246, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 246, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 246, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 246, + + // Node 219 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 247, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 247, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 247, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 247, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 247, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 247, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 247, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 247, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 248, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 248, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 248, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 248, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 248, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 248, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 248, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 248, + + // Node 220 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 250, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 250, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 250, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 250, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 251, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 251, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 251, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 251, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 252, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 252, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 252, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 252, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 253, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 253, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 253, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 253, + + // Node 221 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 250, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 250, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 250, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 250, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 250, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 250, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 250, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 250, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 251, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 251, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 251, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 251, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 251, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 251, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 251, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 251, + + // Node 222 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 252, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 252, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 252, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 252, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 252, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 252, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 252, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 252, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 253, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 253, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 253, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 253, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 253, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 253, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 253, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 253, + + // Node 223 + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 254, + (227 << 16) + (0 << 8) + 0, + (229 << 16) + (0 << 8) + 0, + (230 << 16) + (0 << 8) + 0, + (233 << 16) + (0 << 8) + 0, + (234 << 16) + (0 << 8) + 0, + (236 << 16) + (0 << 8) + 0, + (237 << 16) + (0 << 8) + 0, + (241 << 16) + (0 << 8) + 0, + (242 << 16) + (0 << 8) + 0, + (244 << 16) + (0 << 8) + 0, + (245 << 16) + (0 << 8) + 0, + (248 << 16) + (0 << 8) + 0, + (249 << 16) + (0 << 8) + 0, + (251 << 16) + (0 << 8) + 0, + (252 << 16) + (0 << 8) + 0, + + // Node 224 + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 254, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 254, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 2, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 3, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 4, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 5, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 6, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 7, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 8, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 11, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 12, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 14, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 15, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 16, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 17, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 18, + + // Node 225 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 254, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 254, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 254, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 254, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 2, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 2, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 3, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 3, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 4, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 4, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 5, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 5, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 6, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 6, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 7, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 7, + + // Node 226 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 254, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 254, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 254, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 254, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 254, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 254, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 254, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 254, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 2, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 2, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 2, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 2, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 3, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 3, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 3, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 3, + + // Node 227 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 2, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 2, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 2, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 2, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 2, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 2, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 2, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 2, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 3, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 3, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 3, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 3, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 3, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 3, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 3, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 3, + + // Node 228 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 4, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 4, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 4, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 4, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 5, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 5, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 5, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 5, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 6, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 6, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 6, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 6, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 7, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 7, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 7, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 7, + + // Node 229 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 4, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 4, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 4, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 4, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 4, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 4, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 4, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 4, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 5, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 5, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 5, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 5, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 5, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 5, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 5, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 5, + + // Node 230 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 6, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 6, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 6, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 6, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 6, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 6, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 6, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 6, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 7, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 7, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 7, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 7, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 7, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 7, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 7, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 7, + + // Node 231 + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 8, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 8, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 11, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 11, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 12, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 12, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 14, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 14, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 15, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 15, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 16, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 16, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 17, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 17, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 18, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 18, + + // Node 232 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 8, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 8, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 8, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 8, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 11, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 11, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 11, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 11, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 12, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 12, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 12, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 12, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 14, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 14, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 14, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 14, + + // Node 233 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 8, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 8, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 8, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 8, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 8, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 8, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 8, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 8, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 11, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 11, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 11, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 11, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 11, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 11, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 11, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 11, + + // Node 234 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 12, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 12, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 12, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 12, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 12, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 12, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 12, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 12, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 14, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 14, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 14, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 14, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 14, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 14, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 14, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 14, + + // Node 235 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 15, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 15, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 15, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 15, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 16, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 16, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 16, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 16, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 17, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 17, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 17, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 17, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 18, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 18, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 18, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 18, + + // Node 236 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 15, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 15, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 15, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 15, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 15, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 15, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 15, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 15, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 16, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 16, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 16, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 16, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 16, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 16, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 16, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 16, + + // Node 237 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 17, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 17, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 17, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 17, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 17, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 17, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 17, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 17, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 18, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 18, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 18, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 18, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 18, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 18, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 18, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 18, + + // Node 238 + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 19, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 20, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 21, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 23, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 24, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 25, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 26, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 27, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 28, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 29, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 30, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 31, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 127, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 220, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 249, + (253 << 16) + (0 << 8) + 0, + + // Node 239 + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 19, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 19, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 20, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 20, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 21, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 21, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 23, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 23, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 24, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 24, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 25, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 25, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 26, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 26, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 27, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 27, + + // Node 240 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 19, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 19, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 19, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 19, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 20, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 20, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 20, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 20, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 21, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 21, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 21, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 21, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 23, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 23, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 23, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 23, + + // Node 241 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 19, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 19, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 19, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 19, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 19, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 19, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 19, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 19, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 20, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 20, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 20, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 20, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 20, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 20, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 20, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 20, + + // Node 242 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 21, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 21, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 21, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 21, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 21, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 21, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 21, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 21, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 23, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 23, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 23, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 23, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 23, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 23, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 23, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 23, + + // Node 243 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 24, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 24, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 24, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 24, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 25, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 25, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 25, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 25, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 26, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 26, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 26, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 26, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 27, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 27, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 27, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 27, + + // Node 244 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 24, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 24, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 24, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 24, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 24, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 24, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 24, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 24, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 25, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 25, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 25, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 25, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 25, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 25, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 25, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 25, + + // Node 245 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 26, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 26, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 26, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 26, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 26, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 26, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 26, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 26, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 27, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 27, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 27, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 27, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 27, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 27, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 27, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 27, + + // Node 246 + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 28, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 28, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 29, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 29, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 30, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 30, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 31, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 31, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 127, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 127, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 220, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 220, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 249, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 249, + (254 << 16) + (0 << 8) + 0, + (255 << 16) + (0 << 8) + 0, + + // Node 247 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 28, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 28, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 28, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 28, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 29, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 29, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 29, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 29, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 30, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 30, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 30, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 30, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 31, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 31, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 31, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 31, + + // Node 248 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 28, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 28, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 28, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 28, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 28, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 28, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 28, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 28, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 29, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 29, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 29, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 29, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 29, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 29, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 29, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 29, + + // Node 249 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 30, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 30, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 30, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 30, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 30, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 30, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 30, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 30, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 31, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 31, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 31, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 31, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 31, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 31, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 31, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 31, + + // Node 250 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 127, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 127, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 127, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 127, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 220, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 220, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 220, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 220, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 249, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 249, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 249, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 249, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 10, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 13, + (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 22, + (0 << 16) + (HUFFMAN_FAIL << 8) + 0, + + // Node 251 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 127, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 127, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 127, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 127, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 127, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 127, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 127, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 127, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 220, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 220, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 220, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 220, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 220, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 220, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 220, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 220, + + // Node 252 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 249, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 249, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 249, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 249, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 249, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 249, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 249, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 249, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 10, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 10, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 13, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 13, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 22, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 22, + (0 << 16) + (HUFFMAN_FAIL << 8) + 0, + (0 << 16) + (HUFFMAN_FAIL << 8) + 0, + + // Node 253 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 10, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 10, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 10, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 10, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 13, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 13, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 13, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 13, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 22, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 22, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 22, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 22, + (0 << 16) + (HUFFMAN_FAIL << 8) + 0, + (0 << 16) + (HUFFMAN_FAIL << 8) + 0, + (0 << 16) + (HUFFMAN_FAIL << 8) + 0, + (0 << 16) + (HUFFMAN_FAIL << 8) + 0, + + // Node 254 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 10, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 10, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 10, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 10, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 10, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 10, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 10, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 10, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 13, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 13, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 13, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 13, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 13, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 13, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 13, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 13, + + // Node 255 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 22, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 22, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 22, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 22, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 22, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 22, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 22, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 22, + (0 << 16) + (HUFFMAN_FAIL << 8) + 0, + (0 << 16) + (HUFFMAN_FAIL << 8) + 0, + (0 << 16) + (HUFFMAN_FAIL << 8) + 0, + (0 << 16) + (HUFFMAN_FAIL << 8) + 0, + (0 << 16) + (HUFFMAN_FAIL << 8) + 0, + (0 << 16) + (HUFFMAN_FAIL << 8) + 0, + (0 << 16) + (HUFFMAN_FAIL << 8) + 0, + (0 << 16) + (HUFFMAN_FAIL << 8) + 0, + }; + + private static final Http2Exception BAD_ENCODING = ThrowableUtil.unknownStackTrace( + Http2Exception.newStatic(COMPRESSION_ERROR, "HPACK - Bad Encoding", + Http2Exception.ShutdownHint.HARD_SHUTDOWN), HpackHuffmanDecoder.class, "decode(..)"); + + private byte[] dest; + private int k; + private int state; + + HpackHuffmanDecoder() { } + + /** + * Decompresses the given Huffman coded string literal. + * + * @param buf the string literal to be decoded + * @return the output stream for the compressed data + * @throws Http2Exception EOS Decoded + */ + public AsciiString decode(ByteBuf buf, int length) throws Http2Exception { + if (length == 0) { + return AsciiString.EMPTY_STRING; + } + dest = new byte[length * 8 / 5]; + try { + int readerIndex = buf.readerIndex(); + // Using ByteProcessor to reduce bounds-checking and reference-count checking during byte-by-byte + // processing of the ByteBuf. + int endIndex = buf.forEachByte(readerIndex, length, this); + if (endIndex == -1) { + // We did consume the requested length + buf.readerIndex(readerIndex + length); + if ((state & HUFFMAN_COMPLETE_SHIFT) != HUFFMAN_COMPLETE_SHIFT) { + throw BAD_ENCODING; + } + return new AsciiString(dest, 0, k, false); } - return new AsciiString(bytes, 0, index, false); + // The process(...) method returned before the requested length was requested. This means there + // was a bad encoding detected. + buf.readerIndex(endIndex); + throw BAD_ENCODING; + } finally { + dest = null; + k = 0; + state = 0; } + } - private void append(int i) { - if (bytes.length == index) { - // Choose an expanding strategy depending on how big the buffer already is. - // 1024 was choosen as a good guess and we may be able to investigate more if there are better choices. - // See also https://github.com/netty/netty/issues/6846 - final int newLength = bytes.length >= 1024 ? bytes.length + initialCapacity : bytes.length << 1; - byte[] newBytes = new byte[newLength]; - System.arraycopy(bytes, 0, newBytes, 0, bytes.length); - bytes = newBytes; - } - bytes[index++] = (byte) i; + /** + * <strong>This should never be called from anything but this class itself!</strong> + */ + @Override + public boolean process(byte input) { + return processNibble(input >> 4) && processNibble(input); + } + + private boolean processNibble(int input) { + // The high nibble of the flags byte of each row is always zero + // (low nibble after shifting row by 12), since there are only 3 flag bits + int index = state >> 12 | (input & 0x0F); + state = HUFFS[index]; + if ((state & HUFFMAN_FAIL_SHIFT) != 0) { + return false; + } + if ((state & HUFFMAN_EMIT_SYMBOL_SHIFT) != 0) { + // state is always positive so can cast without mask here + dest[k++] = (byte) state; } + return true; } } diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/HpackStaticTable.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/HpackStaticTable.java index 9621daf..2386d03 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/HpackStaticTable.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/HpackStaticTable.java @@ -38,6 +38,7 @@ import java.util.Arrays; import java.util.List; import static io.netty.handler.codec.http2.HpackUtil.equalsConstantTime; +import static io.netty.handler.codec.http2.HpackUtil.equalsVariableTime; final class HpackStaticTable { @@ -145,7 +146,7 @@ final class HpackStaticTable { * Returns the index value for the given header field in the static table. Returns -1 if the * header field is not in the static table. */ - static int getIndex(CharSequence name, CharSequence value) { + static int getIndexInsensitive(CharSequence name, CharSequence value) { int index = getIndex(name); if (index == -1) { return -1; @@ -154,10 +155,7 @@ final class HpackStaticTable { // Note this assumes all entries for a given header field are sequential. while (index <= length) { HpackHeaderField entry = getEntry(index); - if (equalsConstantTime(name, entry.name) == 0) { - break; - } - if (equalsConstantTime(value, entry.value) != 0) { + if (equalsVariableTime(name, entry.name) && equalsVariableTime(value, entry.value)) { return index; } index++; diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/HpackUtil.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/HpackUtil.java index 62c24aa..d0f0da0 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/HpackUtil.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/HpackUtil.java @@ -65,6 +65,16 @@ final class HpackUtil { return ConstantTimeUtils.equalsConstantTime(s1, s2); } + /** + * Compare two {@link CharSequence}s. + * @param s1 the first value. + * @param s2 the second value. + * @return {@code false} if not equal. {@code true} if equal. + */ + static boolean equalsVariableTime(CharSequence s1, CharSequence s2) { + return AsciiString.contentEquals(s1, s2); + } + // Section 6.2. Literal Header Field Representation enum IndexType { INCREMENTAL, // Section 6.2.1. Literal Header Field with Incremental Indexing diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2ClientUpgradeCodec.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2ClientUpgradeCodec.java index 6028a6f..6e5e55e 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2ClientUpgradeCodec.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2ClientUpgradeCodec.java @@ -47,13 +47,14 @@ public class Http2ClientUpgradeCodec implements HttpClientUpgradeHandler.Upgrade private final String handlerName; private final Http2ConnectionHandler connectionHandler; private final ChannelHandler upgradeToHandler; + private final ChannelHandler http2MultiplexHandler; public Http2ClientUpgradeCodec(Http2FrameCodec frameCodec, ChannelHandler upgradeToHandler) { this(null, frameCodec, upgradeToHandler); } public Http2ClientUpgradeCodec(String handlerName, Http2FrameCodec frameCodec, ChannelHandler upgradeToHandler) { - this(handlerName, (Http2ConnectionHandler) frameCodec, upgradeToHandler); + this(handlerName, (Http2ConnectionHandler) frameCodec, upgradeToHandler, null); } /** @@ -66,6 +67,18 @@ public class Http2ClientUpgradeCodec implements HttpClientUpgradeHandler.Upgrade this((String) null, connectionHandler); } + /** + * Creates the codec using a default name for the connection handler when adding to the + * pipeline. + * + * @param connectionHandler the HTTP/2 connection handler + * @param http2MultiplexHandler the Http2 Multiplexer handler to work with Http2FrameCodec + */ + public Http2ClientUpgradeCodec(Http2ConnectionHandler connectionHandler, + Http2MultiplexHandler http2MultiplexHandler) { + this((String) null, connectionHandler, http2MultiplexHandler); + } + /** * Creates the codec providing an upgrade to the given handler for HTTP/2. * @@ -74,14 +87,27 @@ public class Http2ClientUpgradeCodec implements HttpClientUpgradeHandler.Upgrade * @param connectionHandler the HTTP/2 connection handler */ public Http2ClientUpgradeCodec(String handlerName, Http2ConnectionHandler connectionHandler) { - this(handlerName, connectionHandler, connectionHandler); + this(handlerName, connectionHandler, connectionHandler, null); + } + + /** + * Creates the codec providing an upgrade to the given handler for HTTP/2. + * + * @param handlerName the name of the HTTP/2 connection handler to be used in the pipeline, + * or {@code null} to auto-generate the name + * @param connectionHandler the HTTP/2 connection handler + */ + public Http2ClientUpgradeCodec(String handlerName, Http2ConnectionHandler connectionHandler, + Http2MultiplexHandler http2MultiplexHandler) { + this(handlerName, connectionHandler, connectionHandler, http2MultiplexHandler); } private Http2ClientUpgradeCodec(String handlerName, Http2ConnectionHandler connectionHandler, ChannelHandler - upgradeToHandler) { + upgradeToHandler, Http2MultiplexHandler http2MultiplexHandler) { this.handlerName = handlerName; this.connectionHandler = checkNotNull(connectionHandler, "connectionHandler"); this.upgradeToHandler = checkNotNull(upgradeToHandler, "upgradeToHandler"); + this.http2MultiplexHandler = http2MultiplexHandler; } @Override @@ -91,7 +117,7 @@ public class Http2ClientUpgradeCodec implements HttpClientUpgradeHandler.Upgrade @Override public Collection<CharSequence> setUpgradeHeaders(ChannelHandlerContext ctx, - HttpRequest upgradeRequest) { + HttpRequest upgradeRequest) { CharSequence settingsValue = getSettingsHeaderValue(ctx); upgradeRequest.headers().set(HTTP_UPGRADE_SETTINGS_HEADER, settingsValue); return UPGRADE_HEADERS; @@ -99,12 +125,24 @@ public class Http2ClientUpgradeCodec implements HttpClientUpgradeHandler.Upgrade @Override public void upgradeTo(ChannelHandlerContext ctx, FullHttpResponse upgradeResponse) - throws Exception { - // Add the handler to the pipeline. - ctx.pipeline().addAfter(ctx.name(), handlerName, upgradeToHandler); + throws Exception { + try { + // Add the handler to the pipeline. + ctx.pipeline().addAfter(ctx.name(), handlerName, upgradeToHandler); + + // Add the Http2 Multiplex handler as this handler handle events produced by the connectionHandler. + // See https://github.com/netty/netty/issues/9495 + if (http2MultiplexHandler != null) { + final String name = ctx.pipeline().context(connectionHandler).name(); + ctx.pipeline().addAfter(name, null, http2MultiplexHandler); + } - // Reserve local stream 1 for the response. - connectionHandler.onHttpClientUpgrade(); + // Reserve local stream 1 for the response. + connectionHandler.onHttpClientUpgrade(); + } catch (Http2Exception e) { + ctx.fireExceptionCaught(e); + ctx.close(); + } } /** diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2CodecUtil.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2CodecUtil.java index 7ebc8fd..303527c 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2CodecUtil.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2CodecUtil.java @@ -117,7 +117,6 @@ public final class Http2CodecUtil { public static final int SMALLEST_MAX_CONCURRENT_STREAMS = 100; static final int DEFAULT_MAX_RESERVED_STREAMS = SMALLEST_MAX_CONCURRENT_STREAMS; static final int DEFAULT_MIN_ALLOCATION_CHUNK = 1024; - static final int DEFAULT_INITIAL_HUFFMAN_DECODE_CAPACITY = 32; /** * Calculate the threshold in bytes which should trigger a {@code GO_AWAY} if a set of headers exceeds this amount. @@ -133,6 +132,8 @@ public final class Http2CodecUtil { public static final long DEFAULT_GRACEFUL_SHUTDOWN_TIMEOUT_MILLIS = MILLISECONDS.convert(30, SECONDS); + public static final int DEFAULT_MAX_QUEUED_CONTROL_FRAMES = 10000; + /** * Returns {@code true} if the stream is an outbound stream. * @@ -151,6 +152,10 @@ public final class Http2CodecUtil { return streamId >= 0; } + static boolean isStreamIdValid(int streamId, boolean server) { + return isStreamIdValid(streamId) && server == ((streamId & 1) == 0); + } + /** * Indicates whether or not the given value for max frame size falls within the valid range. */ diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2ConnectionHandler.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2ConnectionHandler.java index 4f66e91..909ca74 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2ConnectionHandler.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2ConnectionHandler.java @@ -76,39 +76,27 @@ public class Http2ConnectionHandler extends ByteToMessageDecoder implements Http private final Http2ConnectionDecoder decoder; private final Http2ConnectionEncoder encoder; private final Http2Settings initialSettings; + private final boolean decoupleCloseAndGoAway; private ChannelFutureListener closeListener; private BaseDecoder byteDecoder; private long gracefulShutdownTimeoutMillis; protected Http2ConnectionHandler(Http2ConnectionDecoder decoder, Http2ConnectionEncoder encoder, Http2Settings initialSettings) { + this(decoder, encoder, initialSettings, false); + } + + protected Http2ConnectionHandler(Http2ConnectionDecoder decoder, Http2ConnectionEncoder encoder, + Http2Settings initialSettings, boolean decoupleCloseAndGoAway) { this.initialSettings = checkNotNull(initialSettings, "initialSettings"); this.decoder = checkNotNull(decoder, "decoder"); this.encoder = checkNotNull(encoder, "encoder"); + this.decoupleCloseAndGoAway = decoupleCloseAndGoAway; if (encoder.connection() != decoder.connection()) { throw new IllegalArgumentException("Encoder and Decoder do not share the same connection object"); } } - Http2ConnectionHandler(boolean server, Http2FrameWriter frameWriter, Http2FrameLogger frameLogger, - Http2Settings initialSettings) { - this.initialSettings = checkNotNull(initialSettings, "initialSettings"); - - Http2Connection connection = new DefaultHttp2Connection(server); - - Long maxHeaderListSize = initialSettings.maxHeaderListSize(); - Http2FrameReader frameReader = new DefaultHttp2FrameReader(maxHeaderListSize == null ? - new DefaultHttp2HeadersDecoder(true) : - new DefaultHttp2HeadersDecoder(true, maxHeaderListSize)); - - if (frameLogger != null) { - frameWriter = new Http2OutboundFrameLogger(frameWriter, frameLogger); - frameReader = new Http2InboundFrameLogger(frameReader, frameLogger); - } - encoder = new DefaultHttp2ConnectionEncoder(connection, frameWriter); - decoder = new DefaultHttp2ConnectionDecoder(connection, encoder, frameReader); - } - /** * Get the amount of time (in milliseconds) this endpoint will wait for all streams to be closed before closing * the connection during the graceful shutdown process. Returns -1 if this connection is configured to wait @@ -233,7 +221,7 @@ public class Http2ConnectionHandler extends ByteToMessageDecoder implements Http private ByteBuf clientPrefaceString; private boolean prefaceSent; - public PrefaceDecoder(ChannelHandlerContext ctx) throws Exception { + PrefaceDecoder(ChannelHandlerContext ctx) throws Exception { clientPrefaceString = clientPrefaceString(encoder.connection()); // This handler was just added to the context. In case it was handled after // the connection became active, send the connection preface now. @@ -334,7 +322,7 @@ public class Http2ConnectionHandler extends ByteToMessageDecoder implements Http * Peeks at that the next frame in the buffer and verifies that it is a non-ack {@code SETTINGS} frame. * * @param in the inbound buffer. - * @return {@code} true if the next frame is a non-ack {@code SETTINGS} frame, {@code false} if more + * @return {@code true} if the next frame is a non-ack {@code SETTINGS} frame, {@code false} if more * data is required before we can determine the next frame type. * @throws Http2Exception thrown if the next frame is NOT a non-ack {@code SETTINGS} frame. */ @@ -468,6 +456,10 @@ public class Http2ConnectionHandler extends ByteToMessageDecoder implements Http @Override public void close(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception { + if (decoupleCloseAndGoAway) { + ctx.close(promise); + return; + } promise = promise.unvoid(); // Avoid NotYetConnectedException if (!ctx.channel().isActive()) { @@ -480,22 +472,44 @@ public class Http2ConnectionHandler extends ByteToMessageDecoder implements Http // a GO_AWAY has been sent we send a empty buffer just so we can wait to close until all other data has been // flushed to the OS. // https://github.com/netty/netty/issues/5307 - final ChannelFuture future = connection().goAwaySent() ? ctx.write(EMPTY_BUFFER) : goAway(ctx, null); + ChannelFuture f = connection().goAwaySent() ? ctx.write(EMPTY_BUFFER) : goAway(ctx, null, ctx.newPromise()); ctx.flush(); - doGracefulShutdown(ctx, future, promise); + doGracefulShutdown(ctx, f, promise); } - private void doGracefulShutdown(ChannelHandlerContext ctx, ChannelFuture future, ChannelPromise promise) { + private ChannelFutureListener newClosingChannelFutureListener( + ChannelHandlerContext ctx, ChannelPromise promise) { + long gracefulShutdownTimeoutMillis = this.gracefulShutdownTimeoutMillis; + return gracefulShutdownTimeoutMillis < 0 ? + new ClosingChannelFutureListener(ctx, promise) : + new ClosingChannelFutureListener(ctx, promise, gracefulShutdownTimeoutMillis, MILLISECONDS); + } + + private void doGracefulShutdown(ChannelHandlerContext ctx, ChannelFuture future, final ChannelPromise promise) { + final ChannelFutureListener listener = newClosingChannelFutureListener(ctx, promise); if (isGracefulShutdownComplete()) { - // If there are no active streams, close immediately after the GO_AWAY write completes. - future.addListener(new ClosingChannelFutureListener(ctx, promise)); + // If there are no active streams, close immediately after the GO_AWAY write completes or the timeout + // elapsed. + future.addListener(listener); } else { // If there are active streams we should wait until they are all closed before closing the connection. - if (gracefulShutdownTimeoutMillis < 0) { - closeListener = new ClosingChannelFutureListener(ctx, promise); - } else { - closeListener = new ClosingChannelFutureListener(ctx, promise, - gracefulShutdownTimeoutMillis, MILLISECONDS); + + // The ClosingChannelFutureListener will cascade promise completion. We need to always notify the + // new ClosingChannelFutureListener when the graceful close completes if the promise is not null. + if (closeListener == null) { + closeListener = listener; + } else if (promise != null) { + final ChannelFutureListener oldCloseListener = closeListener; + closeListener = new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture future) throws Exception { + try { + oldCloseListener.operationComplete(future); + } finally { + listener.operationComplete(future); + } + } + }; } } } @@ -655,14 +669,11 @@ public class Http2ConnectionHandler extends ByteToMessageDecoder implements Http } ChannelPromise promise = ctx.newPromise(); - ChannelFuture future = goAway(ctx, http2Ex); - switch (http2Ex.shutdownHint()) { - case GRACEFUL_SHUTDOWN: + ChannelFuture future = goAway(ctx, http2Ex, ctx.newPromise()); + if (http2Ex.shutdownHint() == Http2Exception.ShutdownHint.GRACEFUL_SHUTDOWN) { doGracefulShutdown(ctx, future, promise); - break; - default: - future.addListener(new ClosingChannelFutureListener(ctx, promise)); - break; + } else { + future.addListener(newClosingChannelFutureListener(ctx, promise)); } } @@ -773,6 +784,13 @@ public class Http2ConnectionHandler extends ByteToMessageDecoder implements Http // Don't write a RST_STREAM frame if we have already written one. return promise.setSuccess(); } + // Synchronously set the resetSent flag to prevent any subsequent calls + // from resulting in multiple reset frames being sent. + // + // This needs to be done before we notify the promise as the promise may have a listener attached that + // call resetStream(...) again. + stream.resetSent(); + final ChannelFuture future; // If the remote peer is not aware of the steam, then we are not allowed to send a RST_STREAM // https://tools.ietf.org/html/rfc7540#section-6.4. @@ -782,11 +800,6 @@ public class Http2ConnectionHandler extends ByteToMessageDecoder implements Http } else { future = frameWriter().writeRstStream(ctx, stream.id(), errorCode, promise); } - - // Synchronously set the resetSent flag to prevent any subsequent calls - // from resulting in multiple reset frames being sent. - stream.resetSent(); - if (future.isDone()) { processRstStreamWriteResult(ctx, stream, future); } else { @@ -861,10 +874,10 @@ public class Http2ConnectionHandler extends ByteToMessageDecoder implements Http * Close the remote endpoint with with a {@code GO_AWAY} frame. Does <strong>not</strong> flush * immediately, this is the responsibility of the caller. */ - private ChannelFuture goAway(ChannelHandlerContext ctx, Http2Exception cause) { + private ChannelFuture goAway(ChannelHandlerContext ctx, Http2Exception cause, ChannelPromise promise) { long errorCode = cause != null ? cause.error().code() : NO_ERROR.code(); int lastKnownStream = connection().remote().lastStreamCreated(); - return goAway(ctx, lastKnownStream, errorCode, Http2CodecUtil.toByteBuf(ctx, cause), ctx.newPromise()); + return goAway(ctx, lastKnownStream, errorCode, Http2CodecUtil.toByteBuf(ctx, cause), promise); } private void processRstStreamWriteResult(ChannelHandlerContext ctx, Http2Stream stream, ChannelFuture future) { @@ -936,17 +949,25 @@ public class Http2ConnectionHandler extends ByteToMessageDecoder implements Http timeoutTask = ctx.executor().schedule(new Runnable() { @Override public void run() { - ctx.close(promise); + doClose(); } }, timeout, unit); } @Override - public void operationComplete(ChannelFuture sentGoAwayFuture) throws Exception { + public void operationComplete(ChannelFuture sentGoAwayFuture) { if (timeoutTask != null) { timeoutTask.cancel(false); } - ctx.close(promise); + doClose(); + } + + private void doClose() { + if (promise == null) { + ctx.close(); + } else { + ctx.close(promise); + } } } } diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2ConnectionHandlerBuilder.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2ConnectionHandlerBuilder.java index 3e30200..c6d1ce7 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2ConnectionHandlerBuilder.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2ConnectionHandlerBuilder.java @@ -88,10 +88,16 @@ public final class Http2ConnectionHandlerBuilder } @Override + @Deprecated public Http2ConnectionHandlerBuilder initialHuffmanDecodeCapacity(int initialHuffmanDecodeCapacity) { return super.initialHuffmanDecodeCapacity(initialHuffmanDecodeCapacity); } + @Override + public Http2ConnectionHandlerBuilder decoupleCloseAndGoAway(boolean decoupleCloseAndGoAway) { + return super.decoupleCloseAndGoAway(decoupleCloseAndGoAway); + } + @Override public Http2ConnectionHandler build() { return super.build(); @@ -100,6 +106,6 @@ public final class Http2ConnectionHandlerBuilder @Override protected Http2ConnectionHandler build(Http2ConnectionDecoder decoder, Http2ConnectionEncoder encoder, Http2Settings initialSettings) { - return new Http2ConnectionHandler(decoder, encoder, initialSettings); + return new Http2ConnectionHandler(decoder, encoder, initialSettings, decoupleCloseAndGoAway()); } } diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2ControlFrameLimitEncoder.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2ControlFrameLimitEncoder.java new file mode 100644 index 0000000..0d25123 --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2ControlFrameLimitEncoder.java @@ -0,0 +1,113 @@ +/* + * Copyright 2019 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.codec.http2; + +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPromise; +import io.netty.util.internal.ObjectUtil; +import io.netty.util.internal.logging.InternalLogger; +import io.netty.util.internal.logging.InternalLoggerFactory; + +/** + * {@link DecoratingHttp2ConnectionEncoder} which guards against a remote peer that will trigger a massive amount + * of control frames but will not consume our responses to these. + * This encoder will tear-down the connection once we reached the configured limit to reduce the risk of DDOS. + */ +final class Http2ControlFrameLimitEncoder extends DecoratingHttp2ConnectionEncoder { + private static final InternalLogger logger = InternalLoggerFactory.getInstance(Http2ControlFrameLimitEncoder.class); + + private final int maxOutstandingControlFrames; + private final ChannelFutureListener outstandingControlFramesListener = new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture future) { + outstandingControlFrames--; + } + }; + private Http2LifecycleManager lifecycleManager; + private int outstandingControlFrames; + private boolean limitReached; + + Http2ControlFrameLimitEncoder(Http2ConnectionEncoder delegate, int maxOutstandingControlFrames) { + super(delegate); + this.maxOutstandingControlFrames = ObjectUtil.checkPositive(maxOutstandingControlFrames, + "maxOutstandingControlFrames"); + } + + @Override + public void lifecycleManager(Http2LifecycleManager lifecycleManager) { + this.lifecycleManager = lifecycleManager; + super.lifecycleManager(lifecycleManager); + } + + @Override + public ChannelFuture writeSettingsAck(ChannelHandlerContext ctx, ChannelPromise promise) { + ChannelPromise newPromise = handleOutstandingControlFrames(ctx, promise); + if (newPromise == null) { + return promise; + } + return super.writeSettingsAck(ctx, newPromise); + } + + @Override + public ChannelFuture writePing(ChannelHandlerContext ctx, boolean ack, long data, ChannelPromise promise) { + // Only apply the limit to ping acks. + if (ack) { + ChannelPromise newPromise = handleOutstandingControlFrames(ctx, promise); + if (newPromise == null) { + return promise; + } + return super.writePing(ctx, ack, data, newPromise); + } + return super.writePing(ctx, ack, data, promise); + } + + @Override + public ChannelFuture writeRstStream( + ChannelHandlerContext ctx, int streamId, long errorCode, ChannelPromise promise) { + ChannelPromise newPromise = handleOutstandingControlFrames(ctx, promise); + if (newPromise == null) { + return promise; + } + return super.writeRstStream(ctx, streamId, errorCode, newPromise); + } + + private ChannelPromise handleOutstandingControlFrames(ChannelHandlerContext ctx, ChannelPromise promise) { + if (!limitReached) { + if (outstandingControlFrames == maxOutstandingControlFrames) { + // Let's try to flush once as we may be able to flush some of the control frames. + ctx.flush(); + } + if (outstandingControlFrames == maxOutstandingControlFrames) { + limitReached = true; + Http2Exception exception = Http2Exception.connectionError(Http2Error.ENHANCE_YOUR_CALM, + "Maximum number %d of outstanding control frames reached", maxOutstandingControlFrames); + logger.info("Maximum number {} of outstanding control frames reached. Closing channel {}", + maxOutstandingControlFrames, ctx.channel(), exception); + + // First notify the Http2LifecycleManager and then close the connection. + lifecycleManager.onError(ctx, true, exception); + ctx.close(); + } + outstandingControlFrames++; + + // We did not reach the limit yet, add the listener to decrement the number of outstanding control frames + // once the promise was completed + return promise.unvoid().addListener(outstandingControlFramesListener); + } + return promise; + } +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2EmptyDataFrameConnectionDecoder.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2EmptyDataFrameConnectionDecoder.java new file mode 100644 index 0000000..4f0e155 --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2EmptyDataFrameConnectionDecoder.java @@ -0,0 +1,56 @@ +/* + * Copyright 2019 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.codec.http2; + +import io.netty.util.internal.ObjectUtil; + +/** + * Enforce a limit on the maximum number of consecutive empty DATA frames (without end_of_stream flag) that are allowed + * before the connection will be closed. + */ +final class Http2EmptyDataFrameConnectionDecoder extends DecoratingHttp2ConnectionDecoder { + + private final int maxConsecutiveEmptyFrames; + + Http2EmptyDataFrameConnectionDecoder(Http2ConnectionDecoder delegate, int maxConsecutiveEmptyFrames) { + super(delegate); + this.maxConsecutiveEmptyFrames = ObjectUtil.checkPositive( + maxConsecutiveEmptyFrames, "maxConsecutiveEmptyFrames"); + } + + @Override + public void frameListener(Http2FrameListener listener) { + if (listener != null) { + super.frameListener(new Http2EmptyDataFrameListener(listener, maxConsecutiveEmptyFrames)); + } else { + super.frameListener(null); + } + } + + @Override + public Http2FrameListener frameListener() { + Http2FrameListener frameListener = frameListener0(); + // Unwrap the original Http2FrameListener as we add this decoder under the hood. + if (frameListener instanceof Http2EmptyDataFrameListener) { + return ((Http2EmptyDataFrameListener) frameListener).listener; + } + return frameListener; + } + + // Package-private for testing + Http2FrameListener frameListener0() { + return super.frameListener(); + } +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2EmptyDataFrameListener.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2EmptyDataFrameListener.java new file mode 100644 index 0000000..dcbd987 --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2EmptyDataFrameListener.java @@ -0,0 +1,65 @@ +/* + * Copyright 2019 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.codec.http2; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.util.internal.ObjectUtil; + +/** + * Enforce a limit on the maximum number of consecutive empty DATA frames (without end_of_stream flag) that are allowed + * before the connection will be closed. + */ +final class Http2EmptyDataFrameListener extends Http2FrameListenerDecorator { + private final int maxConsecutiveEmptyFrames; + + private boolean violationDetected; + private int emptyDataFrames; + + Http2EmptyDataFrameListener(Http2FrameListener listener, int maxConsecutiveEmptyFrames) { + super(listener); + this.maxConsecutiveEmptyFrames = ObjectUtil.checkPositive( + maxConsecutiveEmptyFrames, "maxConsecutiveEmptyFrames"); + } + + @Override + public int onDataRead(ChannelHandlerContext ctx, int streamId, ByteBuf data, int padding, boolean endOfStream) + throws Http2Exception { + if (endOfStream || data.isReadable()) { + emptyDataFrames = 0; + } else if (emptyDataFrames++ == maxConsecutiveEmptyFrames && !violationDetected) { + violationDetected = true; + throw Http2Exception.connectionError(Http2Error.ENHANCE_YOUR_CALM, + "Maximum number %d of empty data frames without end_of_stream flag received", + maxConsecutiveEmptyFrames); + } + + return super.onDataRead(ctx, streamId, data, padding, endOfStream); + } + + @Override + public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers, + int padding, boolean endStream) throws Http2Exception { + emptyDataFrames = 0; + super.onHeadersRead(ctx, streamId, headers, padding, endStream); + } + + @Override + public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers, int streamDependency, + short weight, boolean exclusive, int padding, boolean endStream) throws Http2Exception { + emptyDataFrames = 0; + super.onHeadersRead(ctx, streamId, headers, streamDependency, weight, exclusive, padding, endStream); + } +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2Exception.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2Exception.java index 258f871..cedfe0b 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2Exception.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2Exception.java @@ -15,6 +15,8 @@ package io.netty.handler.codec.http2; +import io.netty.util.internal.PlatformDependent; +import io.netty.util.internal.SuppressJava6Requirement; import io.netty.util.internal.UnstableApi; import java.util.ArrayList; @@ -62,6 +64,22 @@ public class Http2Exception extends Exception { this.shutdownHint = checkNotNull(shutdownHint, "shutdownHint"); } + static Http2Exception newStatic(Http2Error error, String message, ShutdownHint shutdownHint) { + if (PlatformDependent.javaVersion() >= 7) { + return new Http2Exception(error, message, shutdownHint, true); + } + return new Http2Exception(error, message, shutdownHint); + } + + @SuppressJava6Requirement(reason = "uses Java 7+ Exception.<init>(String, Throwable, boolean, boolean)" + + " but is guarded by version checks") + private Http2Exception(Http2Error error, String message, ShutdownHint shutdownHint, boolean shared) { + super(message, null, false, true); + assert shared; + this.error = checkNotNull(error, "error"); + this.shutdownHint = checkNotNull(shutdownHint, "shutdownHint"); + } + public Http2Error error() { return error; } @@ -79,7 +97,7 @@ public class Http2Exception extends Exception { * @param error The type of error as defined by the HTTP/2 specification. * @param fmt String with the content and format for the additional debug data. * @param args Objects which fit into the format defined by {@code fmt}. - * @return An exception which can be translated into a HTTP/2 error. + * @return An exception which can be translated into an HTTP/2 error. */ public static Http2Exception connectionError(Http2Error error, String fmt, Object... args) { return new Http2Exception(error, String.format(fmt, args)); @@ -92,7 +110,7 @@ public class Http2Exception extends Exception { * @param cause The object which caused the error. * @param fmt String with the content and format for the additional debug data. * @param args Objects which fit into the format defined by {@code fmt}. - * @return An exception which can be translated into a HTTP/2 error. + * @return An exception which can be translated into an HTTP/2 error. */ public static Http2Exception connectionError(Http2Error error, Throwable cause, String fmt, Object... args) { @@ -105,7 +123,7 @@ public class Http2Exception extends Exception { * @param error The type of error as defined by the HTTP/2 specification. * @param fmt String with the content and format for the additional debug data. * @param args Objects which fit into the format defined by {@code fmt}. - * @return An exception which can be translated into a HTTP/2 error. + * @return An exception which can be translated into an HTTP/2 error. */ public static Http2Exception closedStreamError(Http2Error error, String fmt, Object... args) { return new ClosedStreamCreationException(error, String.format(fmt, args)); @@ -194,7 +212,7 @@ public class Http2Exception extends Exception { /** * Provides a hint as to if shutdown is justified, what type of shutdown should be executed. */ - public static enum ShutdownHint { + public enum ShutdownHint { /** * Do not shutdown the underlying channel. */ @@ -207,7 +225,7 @@ public class Http2Exception extends Exception { /** * Close the channel immediately after a {@code GOAWAY} is sent. */ - HARD_SHUTDOWN; + HARD_SHUTDOWN } /** diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2FrameCodec.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2FrameCodec.java index cf756ca..d5fa758 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2FrameCodec.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2FrameCodec.java @@ -16,6 +16,7 @@ package io.netty.handler.codec.http2; import io.netty.buffer.ByteBuf; +import io.netty.channel.Channel; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; @@ -32,18 +33,21 @@ import io.netty.util.ReferenceCounted; import io.netty.util.collection.IntObjectHashMap; import io.netty.util.collection.IntObjectMap; import io.netty.util.internal.UnstableApi; +import io.netty.util.internal.logging.InternalLogLevel; import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; +import static io.netty.buffer.ByteBufUtil.writeAscii; import static io.netty.handler.codec.http2.Http2CodecUtil.HTTP_UPGRADE_STREAM_ID; import static io.netty.handler.codec.http2.Http2CodecUtil.isStreamIdValid; +import static io.netty.handler.codec.http2.Http2Error.NO_ERROR; /** * <p><em>This API is very immature.</em> The Http2Connection-based API is currently preferred over this API. * This API is targeted to eventually replace or reduce the need for the {@link Http2ConnectionHandler} API. * - * <p>A HTTP/2 handler that maps HTTP/2 frames to {@link Http2Frame} objects and vice versa. For every incoming HTTP/2 - * frame, a {@link Http2Frame} object is created and propagated via {@link #channelRead}. Outbound {@link Http2Frame} + * <p>An HTTP/2 handler that maps HTTP/2 frames to {@link Http2Frame} objects and vice versa. For every incoming HTTP/2 + * frame, an {@link Http2Frame} object is created and propagated via {@link #channelRead}. Outbound {@link Http2Frame} * objects received via {@link #write} are converted to the HTTP/2 wire format. HTTP/2 frames specific to a stream * implement the {@link Http2StreamFrame} interface. The {@link Http2FrameCodec} is instantiated using the * {@link Http2FrameCodecBuilder}. It's recommended for channel handlers to inherit from the @@ -76,7 +80,7 @@ import static io.netty.handler.codec.http2.Http2CodecUtil.isStreamIdValid; * * <h3>New inbound Streams</h3> * - * The first frame of a HTTP/2 stream must be a {@link Http2HeadersFrame}, which will have a {@link Http2FrameStream} + * The first frame of an HTTP/2 stream must be an {@link Http2HeadersFrame}, which will have an {@link Http2FrameStream} * object attached. * * <h3>New outbound Streams</h3> @@ -133,7 +137,7 @@ import static io.netty.handler.codec.http2.Http2CodecUtil.isStreamIdValid; * reference counted objects (e.g. {@link ByteBuf}s). The frame codec will call {@link ReferenceCounted#retain()} before * propagating a reference counted object through the pipeline, and thus an application handler needs to release such * an object after having consumed it. For more information on reference counting take a look at - * http://netty.io/wiki/reference-counted-objects.html + * https://netty.io/wiki/reference-counted-objects.html * * <h3>HTTP Upgrade</h3> * @@ -157,8 +161,9 @@ public class Http2FrameCodec extends Http2ConnectionHandler { private final IntObjectMap<DefaultHttp2FrameStream> frameStreamToInitializeMap = new IntObjectHashMap<DefaultHttp2FrameStream>(8); - Http2FrameCodec(Http2ConnectionEncoder encoder, Http2ConnectionDecoder decoder, Http2Settings initialSettings) { - super(decoder, encoder, initialSettings); + Http2FrameCodec(Http2ConnectionEncoder encoder, Http2ConnectionDecoder decoder, Http2Settings initialSettings, + boolean decoupleCloseAndGoAway) { + super(decoder, encoder, initialSettings, decoupleCloseAndGoAway); decoder.frameListener(new FrameListener()); connection().addListener(new ConnectionListener()); @@ -182,18 +187,28 @@ public class Http2FrameCodec extends Http2ConnectionHandler { */ final void forEachActiveStream(final Http2FrameStreamVisitor streamVisitor) throws Http2Exception { assert ctx.executor().inEventLoop(); - - connection().forEachActiveStream(new Http2StreamVisitor() { - @Override - public boolean visit(Http2Stream stream) { - try { - return streamVisitor.visit((Http2FrameStream) stream.getProperty(streamKey)); - } catch (Throwable cause) { - onError(ctx, false, cause); - return false; + if (connection().numActiveStreams() > 0) { + connection().forEachActiveStream(new Http2StreamVisitor() { + @Override + public boolean visit(Http2Stream stream) { + try { + return streamVisitor.visit((Http2FrameStream) stream.getProperty(streamKey)); + } catch (Throwable cause) { + onError(ctx, false, cause); + return false; + } } - } - }); + }); + } + } + + /** + * Retrieve the number of streams currently in the process of being initialized. + * + * This is package-private for testing only. + */ + int numInitializingStreams() { + return frameStreamToInitializeMap.size(); } @Override @@ -234,10 +249,20 @@ public class Http2FrameCodec extends Http2ConnectionHandler { * HTTP/2 on stream 1 (the stream specifically reserved for cleartext HTTP upgrade). */ @Override - public final void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + public final void userEventTriggered(final ChannelHandlerContext ctx, final Object evt) throws Exception { if (evt == Http2ConnectionPrefaceAndSettingsFrameWrittenEvent.INSTANCE) { // The user event implies that we are on the client. tryExpandConnectionFlowControlWindow(connection()); + + // We schedule this on the EventExecutor to allow to have any extra handlers added to the pipeline + // before we pass the event to the next handler. This is needed as the event may be called from within + // handlerAdded(...) which will be run before other handlers will be added to the pipeline. + ctx.executor().execute(new Runnable() { + @Override + public void run() { + ctx.fireUserEventTriggered(evt); + } + }); } else if (evt instanceof UpgradeEvent) { UpgradeEvent upgrade = (UpgradeEvent) evt; try { @@ -257,9 +282,9 @@ public class Http2FrameCodec extends Http2ConnectionHandler { } finally { upgrade.release(); } - return; + } else { + ctx.fireUserEventTriggered(evt); } - super.userEventTriggered(ctx, evt); } /** @@ -291,12 +316,25 @@ public class Http2FrameCodec extends Http2ConnectionHandler { } } else if (msg instanceof Http2ResetFrame) { Http2ResetFrame rstFrame = (Http2ResetFrame) msg; - encoder().writeRstStream(ctx, rstFrame.stream().id(), rstFrame.errorCode(), promise); + int id = rstFrame.stream().id(); + // Only ever send a reset frame if stream may have existed before as otherwise we may send a RST on a + // stream in an invalid state and cause a connection error. + if (connection().streamMayHaveExisted(id)) { + encoder().writeRstStream(ctx, rstFrame.stream().id(), rstFrame.errorCode(), promise); + } else { + ReferenceCountUtil.release(rstFrame); + promise.setFailure(Http2Exception.streamError( + rstFrame.stream().id(), Http2Error.PROTOCOL_ERROR, "Stream never existed")); + } } else if (msg instanceof Http2PingFrame) { Http2PingFrame frame = (Http2PingFrame) msg; encoder().writePing(ctx, frame.ack(), frame.content(), promise); } else if (msg instanceof Http2SettingsFrame) { encoder().writeSettings(ctx, ((Http2SettingsFrame) msg).settings(), promise); + } else if (msg instanceof Http2SettingsAckFrame) { + // In the event of manual SETTINGS ACK is is assumed the encoder will apply the earliest received but not + // yet ACKed settings. + encoder().writeSettingsAck(ctx, promise); } else if (msg instanceof Http2GoAwayFrame) { writeGoAwayFrame(ctx, (Http2GoAwayFrame) msg, promise); } else if (msg instanceof Http2UnknownFrame) { @@ -357,6 +395,12 @@ public class Http2FrameCodec extends Http2ConnectionHandler { final int streamId = connection.local().incrementAndGetNextStreamId(); if (streamId < 0) { promise.setFailure(new Http2NoMoreStreamIdsException()); + + // Simulate a GOAWAY being received due to stream exhaustion on this connection. We use the maximum + // valid stream ID for the current peer. + onHttp2Frame(ctx, new DefaultHttp2GoAwayFrame(connection.isServer() ? Integer.MAX_VALUE : + Integer.MAX_VALUE - 1, NO_ERROR.code(), + writeAscii(ctx.alloc(), "Stream IDs exhausted on local stream creation"))); return; } stream.id = streamId; @@ -371,56 +415,52 @@ public class Http2FrameCodec extends Http2ConnectionHandler { // We should not re-use ids. assert old == null; - // TODO(buchgr): Once Http2FrameStream and Http2Stream are merged this is no longer necessary. - final ChannelPromise writePromise = ctx.newPromise(); - encoder().writeHeaders(ctx, streamId, headersFrame.headers(), headersFrame.padding(), - headersFrame.isEndStream(), writePromise); - if (writePromise.isDone()) { - notifyHeaderWritePromise(writePromise, promise); - } else { - numBufferedStreams++; + headersFrame.isEndStream(), promise); - writePromise.addListener(new ChannelFutureListener() { + if (!promise.isDone()) { + numBufferedStreams++; + // Clean up the stream being initialized if writing the headers fails and also + // decrement the number of buffered streams. + promise.addListener(new ChannelFutureListener() { @Override - public void operationComplete(ChannelFuture future) throws Exception { + public void operationComplete(ChannelFuture channelFuture) { numBufferedStreams--; - notifyHeaderWritePromise(future, promise); + handleHeaderFuture(channelFuture, streamId); } }); + } else { + handleHeaderFuture(promise, streamId); } } } - private static void notifyHeaderWritePromise(ChannelFuture future, ChannelPromise promise) { - Throwable cause = future.cause(); - if (cause == null) { - promise.setSuccess(); - } else { - promise.setFailure(cause); + private void handleHeaderFuture(ChannelFuture channelFuture, int streamId) { + if (!channelFuture.isSuccess()) { + frameStreamToInitializeMap.remove(streamId); } } private void onStreamActive0(Http2Stream stream) { - if (connection().local().isValidStreamId(stream.id())) { + if (stream.id() != Http2CodecUtil.HTTP_UPGRADE_STREAM_ID && + connection().local().isValidStreamId(stream.id())) { return; } - Http2FrameStream stream2 = newStream().setStreamAndProperty(streamKey, stream); + DefaultHttp2FrameStream stream2 = newStream().setStreamAndProperty(streamKey, stream); onHttp2StreamStateChanged(ctx, stream2); } private final class ConnectionListener extends Http2ConnectionAdapter { - @Override public void onStreamAdded(Http2Stream stream) { DefaultHttp2FrameStream frameStream = frameStreamToInitializeMap.remove(stream.id()); - if (frameStream != null) { - frameStream.setStreamAndProperty(streamKey, stream); - } - } + if (frameStream != null) { + frameStream.setStreamAndProperty(streamKey, stream); + } + } @Override public void onStreamActive(Http2Stream stream) { @@ -429,15 +469,16 @@ public class Http2FrameCodec extends Http2ConnectionHandler { @Override public void onStreamClosed(Http2Stream stream) { - Http2FrameStream stream2 = stream.getProperty(streamKey); - if (stream2 != null) { - onHttp2StreamStateChanged(ctx, stream2); - } + onHttp2StreamStateChanged0(stream); } @Override public void onStreamHalfClosed(Http2Stream stream) { - Http2FrameStream stream2 = stream.getProperty(streamKey); + onHttp2StreamStateChanged0(stream); + } + + private void onHttp2StreamStateChanged0(Http2Stream stream) { + DefaultHttp2FrameStream stream2 = stream.getProperty(streamKey); if (stream2 != null) { onHttp2StreamStateChanged(ctx, stream2); } @@ -487,10 +528,14 @@ public class Http2FrameCodec extends Http2ConnectionHandler { } } - void onHttp2UnknownStreamError(@SuppressWarnings("unused") ChannelHandlerContext ctx, Throwable cause, + private void onHttp2UnknownStreamError(@SuppressWarnings("unused") ChannelHandlerContext ctx, Throwable cause, Http2Exception.StreamException streamException) { - // Just log.... - LOG.warn("Stream exception thrown for unkown stream {}.", streamException.streamId(), cause); + // It is normal to hit a race condition where we still receive frames for a stream that this + // peer has deemed closed, such as if this peer sends a RST(CANCEL) to discard the request. + // Since this is likely to be normal we log at DEBUG level. + InternalLogLevel level = + streamException.error() == Http2Error.STREAM_CLOSED ? InternalLogLevel.DEBUG : InternalLogLevel.WARN; + LOG.log(level, "Stream exception thrown for unknown stream {}.", streamException.streamId(), cause); } @Override @@ -572,7 +617,7 @@ public class Http2FrameCodec extends Http2ConnectionHandler { @Override public void onSettingsAckRead(ChannelHandlerContext ctx) { - // TODO: Maybe handle me + onHttp2Frame(ctx, Http2SettingsAckFrame.INSTANCE); } @Override @@ -590,17 +635,17 @@ public class Http2FrameCodec extends Http2ConnectionHandler { } } - void onUpgradeEvent(ChannelHandlerContext ctx, UpgradeEvent evt) { + private void onUpgradeEvent(ChannelHandlerContext ctx, UpgradeEvent evt) { ctx.fireUserEventTriggered(evt); } - void onHttp2StreamWritabilityChanged(ChannelHandlerContext ctx, Http2FrameStream stream, + private void onHttp2StreamWritabilityChanged(ChannelHandlerContext ctx, DefaultHttp2FrameStream stream, @SuppressWarnings("unused") boolean writable) { - ctx.fireUserEventTriggered(Http2FrameStreamEvent.writabilityChanged(stream)); + ctx.fireUserEventTriggered(stream.writabilityChanged); } - void onHttp2StreamStateChanged(ChannelHandlerContext ctx, Http2FrameStream stream) { - ctx.fireUserEventTriggered(Http2FrameStreamEvent.stateChanged(stream)); + void onHttp2StreamStateChanged(ChannelHandlerContext ctx, DefaultHttp2FrameStream stream) { + ctx.fireUserEventTriggered(stream.stateChanged); } void onHttp2Frame(ChannelHandlerContext ctx, Http2Frame frame) { @@ -611,15 +656,10 @@ public class Http2FrameCodec extends Http2ConnectionHandler { ctx.fireExceptionCaught(cause); } - final boolean isWritable(DefaultHttp2FrameStream stream) { - Http2Stream s = stream.stream; - return s != null && connection().remote().flowController().isWritable(s); - } - private final class Http2RemoteFlowControllerListener implements Http2RemoteFlowController.Listener { @Override public void writabilityChanged(Http2Stream stream) { - Http2FrameStream frameStream = stream.getProperty(streamKey); + DefaultHttp2FrameStream frameStream = stream.getProperty(streamKey); if (frameStream == null) { return; } @@ -637,6 +677,11 @@ public class Http2FrameCodec extends Http2ConnectionHandler { private volatile int id = -1; volatile Http2Stream stream; + final Http2FrameStreamEvent stateChanged = Http2FrameStreamEvent.stateChanged(this); + final Http2FrameStreamEvent writabilityChanged = Http2FrameStreamEvent.writabilityChanged(this); + + Channel attachment; + DefaultHttp2FrameStream setStreamAndProperty(PropertyKey streamKey, Http2Stream stream) { assert id == -1 || stream.id() == id; this.stream = stream; diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2FrameCodecBuilder.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2FrameCodecBuilder.java index eb45723..fad31b2 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2FrameCodecBuilder.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2FrameCodecBuilder.java @@ -31,17 +31,19 @@ public class Http2FrameCodecBuilder extends Http2FrameCodecBuilder(boolean server) { server(server); + // For backwards compatibility we should disable to timeout by default at this layer. + gracefulShutdownTimeoutMillis(0); } /** - * Creates a builder for a HTTP/2 client. + * Creates a builder for an HTTP/2 client. */ public static Http2FrameCodecBuilder forClient() { return new Http2FrameCodecBuilder(false); } /** - * Creates a builder for a HTTP/2 server. + * Creates a builder for an HTTP/2 server. */ public static Http2FrameCodecBuilder forServer() { return new Http2FrameCodecBuilder(true); @@ -118,6 +120,16 @@ public class Http2FrameCodecBuilder extends return super.encoderEnforceMaxConcurrentStreams(encoderEnforceMaxConcurrentStreams); } + @Override + public int encoderEnforceMaxQueuedControlFrames() { + return super.encoderEnforceMaxQueuedControlFrames(); + } + + @Override + public Http2FrameCodecBuilder encoderEnforceMaxQueuedControlFrames(int maxQueuedControlFrames) { + return super.encoderEnforceMaxQueuedControlFrames(maxQueuedControlFrames); + } + @Override public Http2HeadersEncoder.SensitivityDetector headerSensitivityDetector() { return super.headerSensitivityDetector(); @@ -135,10 +147,36 @@ public class Http2FrameCodecBuilder extends } @Override + @Deprecated public Http2FrameCodecBuilder initialHuffmanDecodeCapacity(int initialHuffmanDecodeCapacity) { return super.initialHuffmanDecodeCapacity(initialHuffmanDecodeCapacity); } + @Override + public Http2FrameCodecBuilder autoAckSettingsFrame(boolean autoAckSettings) { + return super.autoAckSettingsFrame(autoAckSettings); + } + + @Override + public Http2FrameCodecBuilder autoAckPingFrame(boolean autoAckPingFrame) { + return super.autoAckPingFrame(autoAckPingFrame); + } + + @Override + public Http2FrameCodecBuilder decoupleCloseAndGoAway(boolean decoupleCloseAndGoAway) { + return super.decoupleCloseAndGoAway(decoupleCloseAndGoAway); + } + + @Override + public int decoderEnforceMaxConsecutiveEmptyDataFrames() { + return super.decoderEnforceMaxConsecutiveEmptyDataFrames(); + } + + @Override + public Http2FrameCodecBuilder decoderEnforceMaxConsecutiveEmptyDataFrames(int maxConsecutiveEmptyFrames) { + return super.decoderEnforceMaxConsecutiveEmptyDataFrames(maxConsecutiveEmptyFrames); + } + /** * Build a {@link Http2FrameCodec} object. */ @@ -151,8 +189,8 @@ public class Http2FrameCodecBuilder extends DefaultHttp2Connection connection = new DefaultHttp2Connection(isServer(), maxReservedStreams()); Long maxHeaderListSize = initialSettings().maxHeaderListSize(); Http2FrameReader frameReader = new DefaultHttp2FrameReader(maxHeaderListSize == null ? - new DefaultHttp2HeadersDecoder(true) : - new DefaultHttp2HeadersDecoder(true, maxHeaderListSize)); + new DefaultHttp2HeadersDecoder(isValidateHeaders()) : + new DefaultHttp2HeadersDecoder(isValidateHeaders(), maxHeaderListSize)); if (frameLogger() != null) { frameWriter = new Http2OutboundFrameLogger(frameWriter, frameLogger()); @@ -162,8 +200,12 @@ public class Http2FrameCodecBuilder extends if (encoderEnforceMaxConcurrentStreams()) { encoder = new StreamBufferingEncoder(encoder); } - Http2ConnectionDecoder decoder = new DefaultHttp2ConnectionDecoder(connection, encoder, frameReader); - + Http2ConnectionDecoder decoder = new DefaultHttp2ConnectionDecoder(connection, encoder, frameReader, + promisedRequestVerifier(), isAutoAckSettingsFrame(), isAutoAckPingFrame()); + int maxConsecutiveEmptyDataFrames = decoderEnforceMaxConsecutiveEmptyDataFrames(); + if (maxConsecutiveEmptyDataFrames > 0) { + decoder = new Http2EmptyDataFrameConnectionDecoder(decoder, maxConsecutiveEmptyDataFrames); + } return build(decoder, encoder, initialSettings()); } return super.build(); @@ -172,6 +214,8 @@ public class Http2FrameCodecBuilder extends @Override protected Http2FrameCodec build( Http2ConnectionDecoder decoder, Http2ConnectionEncoder encoder, Http2Settings initialSettings) { - return new Http2FrameCodec(encoder, decoder, initialSettings); + Http2FrameCodec codec = new Http2FrameCodec(encoder, decoder, initialSettings, decoupleCloseAndGoAway()); + codec.gracefulShutdownTimeoutMillis(gracefulShutdownTimeoutMillis()); + return codec; } } diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2FrameListener.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2FrameListener.java index e48923b..c8d3518 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2FrameListener.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2FrameListener.java @@ -160,7 +160,7 @@ public interface Http2FrameListener { * Handles an inbound {@code PUSH_PROMISE} frame. Only called if {@code END_HEADERS} encountered. * <p> * Promised requests MUST be authoritative, cacheable, and safe. - * See <a href="https://tools.ietf.org/html/draft-ietf-httpbis-http2-17#section-8.2">[RFC http2], Section 8.2</a>. + * See <a href="https://tools.ietf.org/html/rfc7540#section-8.2">[RFC 7540], Section 8.2</a>. * <p> * Only one of the following methods will be called for each {@code HEADERS} frame sequence. * One will be called when the {@code END_HEADERS} flag has been received. diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2FrameStream.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2FrameStream.java index 13f8634..533800f 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2FrameStream.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2FrameStream.java @@ -20,7 +20,7 @@ import io.netty.handler.codec.http2.Http2Stream.State; import io.netty.util.internal.UnstableApi; /** - * A single stream within a HTTP/2 connection. To be used with the {@link Http2FrameCodec}. + * A single stream within an HTTP/2 connection. To be used with the {@link Http2FrameCodec}. */ @UnstableApi public interface Http2FrameStream { diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2FrameStreamException.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2FrameStreamException.java index e70a0f1..bd65a4a 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2FrameStreamException.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2FrameStreamException.java @@ -21,7 +21,7 @@ import io.netty.util.internal.UnstableApi; import static io.netty.util.internal.ObjectUtil.checkNotNull; /** - * A HTTP/2 exception for a specific {@link Http2FrameStream}. + * An HTTP/2 exception for a specific {@link Http2FrameStream}. */ @UnstableApi public final class Http2FrameStreamException extends Exception { diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2Headers.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2Headers.java index f0999bf..fc5e11f 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2Headers.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2Headers.java @@ -136,7 +136,7 @@ public interface Http2Headers extends Headers<CharSequence, CharSequence, Http2H Iterator<CharSequence> valueIterator(CharSequence name); /** - * Sets the {@link PseudoHeaderName#METHOD} header or {@code null} if there is no such header + * Sets the {@link PseudoHeaderName#METHOD} header */ Http2Headers method(CharSequence value); diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2HeadersEncoder.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2HeadersEncoder.java index 9e88efc..9e96a79 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2HeadersEncoder.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2HeadersEncoder.java @@ -57,17 +57,18 @@ public interface Http2HeadersEncoder { /** * Determine if a header name/value pair is treated as - * <a href="http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-12#section-7.1.3">sensitive</a>. + * <a href="https://tools.ietf.org/html/rfc7541#section-7.1.3">sensitive</a>. * If the object can be dynamically modified and shared across multiple connections it may need to be thread safe. */ interface SensitivityDetector { /** * Determine if a header {@code name}/{@code value} pair should be treated as - * <a href="http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-12#section-7.1.3">sensitive</a>. + * <a href="https://tools.ietf.org/html/rfc7541#section-7.1.3">sensitive</a>. + * * @param name The name for the header. * @param value The value of the header. * @return {@code true} if a header {@code name}/{@code value} pair should be treated as - * <a href="http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-12#section-7.1.3">sensitive</a>. + * <a href="https://tools.ietf.org/html/rfc7541#section-7.1.3">sensitive</a>. * {@code false} otherwise. */ boolean isSensitive(CharSequence name, CharSequence value); diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2MultiplexCodec.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2MultiplexCodec.java index d19ce2b..07c8fca 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2MultiplexCodec.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2MultiplexCodec.java @@ -16,47 +16,23 @@ package io.netty.handler.codec.http2; import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufAllocator; import io.netty.channel.Channel; import io.netty.channel.ChannelConfig; import io.netty.channel.ChannelFuture; -import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelId; -import io.netty.channel.ChannelMetadata; -import io.netty.channel.ChannelOutboundBuffer; -import io.netty.channel.ChannelPipeline; -import io.netty.channel.ChannelProgressivePromise; import io.netty.channel.ChannelPromise; -import io.netty.channel.DefaultChannelConfig; -import io.netty.channel.DefaultChannelPipeline; import io.netty.channel.EventLoop; -import io.netty.channel.MessageSizeEstimator; -import io.netty.channel.RecvByteBufAllocator; -import io.netty.channel.RecvByteBufAllocator.Handle; -import io.netty.channel.VoidChannelPromise; -import io.netty.channel.WriteBufferWaterMark; -import io.netty.util.DefaultAttributeMap; -import io.netty.util.ReferenceCountUtil; import io.netty.util.ReferenceCounted; -import io.netty.util.internal.StringUtil; -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.net.SocketAddress; -import java.nio.channels.ClosedChannelException; import java.util.ArrayDeque; import java.util.Queue; -import java.util.concurrent.RejectedExecutionException; import static io.netty.handler.codec.http2.Http2CodecUtil.HTTP_UPGRADE_STREAM_ID; -import static io.netty.handler.codec.http2.Http2CodecUtil.isStreamIdValid; import static io.netty.handler.codec.http2.Http2Error.INTERNAL_ERROR; import static io.netty.handler.codec.http2.Http2Exception.connectionError; -import static java.lang.Math.min; /** * An HTTP/2 handler that creates child channels for each stream. @@ -85,7 +61,7 @@ import static java.lang.Math.min; * reference counted objects (e.g. {@link ByteBuf}s). The multiplex codec will call {@link ReferenceCounted#retain()} * before propagating a reference counted object through the pipeline, and thus an application handler needs to release * such an object after having consumed it. For more information on reference counting take a look at - * http://netty.io/wiki/reference-counted-objects.html + * https://netty.io/wiki/reference-counted-objects.html * * <h3>Channel Events</h3> * @@ -102,71 +78,32 @@ import static java.lang.Math.min; * does not know about the connection-level flow control window. {@link ChannelHandler}s are free to ignore the * channel's writability, in which case the excessive writes will be buffered by the parent channel. It's important to * note that only {@link Http2DataFrame}s are subject to HTTP/2 flow control. + * + * @deprecated use {@link Http2FrameCodecBuilder} together with {@link Http2MultiplexHandler}. */ +@Deprecated @UnstableApi public class Http2MultiplexCodec extends Http2FrameCodec { - private static final InternalLogger logger = InternalLoggerFactory.getInstance(DefaultHttp2StreamChannel.class); - - private static final ChannelFutureListener CHILD_CHANNEL_REGISTRATION_LISTENER = new ChannelFutureListener() { - @Override - public void operationComplete(ChannelFuture future) { - registerDone(future); - } - }; - - private static final ChannelMetadata METADATA = new ChannelMetadata(false, 16); - private static final ClosedChannelException CLOSED_CHANNEL_EXCEPTION = ThrowableUtil.unknownStackTrace( - new ClosedChannelException(), DefaultHttp2StreamChannel.Http2ChannelUnsafe.class, "write(...)"); - /** - * Number of bytes to consider non-payload messages. 9 is arbitrary, but also the minimum size of an HTTP/2 frame. - * Primarily is non-zero. - */ - private static final int MIN_HTTP2_FRAME_SIZE = 9; - - /** - * Returns the flow-control size for DATA frames, and 0 for all other frames. - */ - private static final class FlowControlledFrameSizeEstimator implements MessageSizeEstimator { - - static final FlowControlledFrameSizeEstimator INSTANCE = new FlowControlledFrameSizeEstimator(); - - static final MessageSizeEstimator.Handle HANDLE_INSTANCE = new MessageSizeEstimator.Handle() { - @Override - public int size(Object msg) { - return msg instanceof Http2DataFrame ? - // Guard against overflow. - (int) min(Integer.MAX_VALUE, ((Http2DataFrame) msg).initialFlowControlledBytes() + - (long) MIN_HTTP2_FRAME_SIZE) : MIN_HTTP2_FRAME_SIZE; - } - }; - - @Override - public Handle newHandle() { - return HANDLE_INSTANCE; - } - } - private final ChannelHandler inboundStreamHandler; private final ChannelHandler upgradeStreamHandler; + private final Queue<AbstractHttp2StreamChannel> readCompletePendingQueue = + new MaxCapacityQueue<AbstractHttp2StreamChannel>(new ArrayDeque<AbstractHttp2StreamChannel>(8), + // Choose 100 which is what is used most of the times as default. + Http2CodecUtil.SMALLEST_MAX_CONCURRENT_STREAMS); - private int initialOutboundStreamWindow = Http2CodecUtil.DEFAULT_WINDOW_SIZE; private boolean parentReadInProgress; private int idCount; - // Linked-List for DefaultHttp2StreamChannel instances that need to be processed by channelReadComplete(...) - private DefaultHttp2StreamChannel head; - private DefaultHttp2StreamChannel tail; - - // Need to be volatile as accessed from within the DefaultHttp2StreamChannel in a multi-threaded fashion. + // Need to be volatile as accessed from within the Http2MultiplexCodecStreamChannel in a multi-threaded fashion. volatile ChannelHandlerContext ctx; Http2MultiplexCodec(Http2ConnectionEncoder encoder, Http2ConnectionDecoder decoder, Http2Settings initialSettings, ChannelHandler inboundStreamHandler, - ChannelHandler upgradeStreamHandler) { - super(encoder, decoder, initialSettings); + ChannelHandler upgradeStreamHandler, boolean decoupleCloseAndGoAway) { + super(encoder, decoder, initialSettings, decoupleCloseAndGoAway); this.inboundStreamHandler = inboundStreamHandler; this.upgradeStreamHandler = upgradeStreamHandler; } @@ -179,24 +116,6 @@ public class Http2MultiplexCodec extends Http2FrameCodec { } // Creates the Http2Stream in the Connection. super.onHttpClientUpgrade(); - // Now make a new FrameStream, set it's underlying Http2Stream, and initialize it. - Http2MultiplexCodecStream codecStream = newStream(); - codecStream.setStreamAndProperty(streamKey, connection().stream(HTTP_UPGRADE_STREAM_ID)); - onHttp2UpgradeStreamInitialized(ctx, codecStream); - } - - private static void registerDone(ChannelFuture future) { - // Handle any errors that occurred on the local thread while registering. Even though - // failures can happen after this point, they will be handled by the channel by closing the - // childChannel. - if (!future.isSuccess()) { - Channel childChannel = future.channel(); - if (childChannel.isRegistered()) { - childChannel.close(); - } else { - childChannel.unsafe().closeForcibly(); - } - } } @Override @@ -211,80 +130,61 @@ public class Http2MultiplexCodec extends Http2FrameCodec { public final void handlerRemoved0(ChannelHandlerContext ctx) throws Exception { super.handlerRemoved0(ctx); - // Unlink the linked list to guard against GC nepotism. - DefaultHttp2StreamChannel ch = head; - while (ch != null) { - DefaultHttp2StreamChannel curr = ch; - ch = curr.next; - curr.next = curr.previous = null; - } - head = tail = null; - } - - @Override - Http2MultiplexCodecStream newStream() { - return new Http2MultiplexCodecStream(); + readCompletePendingQueue.clear(); } @Override final void onHttp2Frame(ChannelHandlerContext ctx, Http2Frame frame) { if (frame instanceof Http2StreamFrame) { Http2StreamFrame streamFrame = (Http2StreamFrame) frame; - ((Http2MultiplexCodecStream) streamFrame.stream()).channel.fireChildRead(streamFrame); - } else if (frame instanceof Http2GoAwayFrame) { - onHttp2GoAwayFrame(ctx, (Http2GoAwayFrame) frame); - // Allow other handlers to act on GOAWAY frame - ctx.fireChannelRead(frame); - } else if (frame instanceof Http2SettingsFrame) { - Http2Settings settings = ((Http2SettingsFrame) frame).settings(); - if (settings.initialWindowSize() != null) { - initialOutboundStreamWindow = settings.initialWindowSize(); - } - // Allow other handlers to act on SETTINGS frame - ctx.fireChannelRead(frame); - } else { - // Send any other frames down the pipeline - ctx.fireChannelRead(frame); + AbstractHttp2StreamChannel channel = (AbstractHttp2StreamChannel) + ((DefaultHttp2FrameStream) streamFrame.stream()).attachment; + channel.fireChildRead(streamFrame); + return; } - } - - private void onHttp2UpgradeStreamInitialized(ChannelHandlerContext ctx, Http2MultiplexCodecStream stream) { - assert stream.state() == Http2Stream.State.HALF_CLOSED_LOCAL; - DefaultHttp2StreamChannel ch = new DefaultHttp2StreamChannel(stream, true); - ch.outboundClosed = true; - - // Add our upgrade handler to the channel and then register the channel. - // The register call fires the channelActive, etc. - ch.pipeline().addLast(upgradeStreamHandler); - ChannelFuture future = ctx.channel().eventLoop().register(ch); - if (future.isDone()) { - registerDone(future); - } else { - future.addListener(CHILD_CHANNEL_REGISTRATION_LISTENER); + if (frame instanceof Http2GoAwayFrame) { + onHttp2GoAwayFrame(ctx, (Http2GoAwayFrame) frame); } + // Send frames down the pipeline + ctx.fireChannelRead(frame); } @Override - final void onHttp2StreamStateChanged(ChannelHandlerContext ctx, Http2FrameStream stream) { - Http2MultiplexCodecStream s = (Http2MultiplexCodecStream) stream; - + final void onHttp2StreamStateChanged(ChannelHandlerContext ctx, DefaultHttp2FrameStream stream) { switch (stream.state()) { + case HALF_CLOSED_LOCAL: + if (stream.id() != HTTP_UPGRADE_STREAM_ID) { + // Ignore everything which was not caused by an upgrade + break; + } + // fall-through case HALF_CLOSED_REMOTE: + // fall-through case OPEN: - if (s.channel != null) { + if (stream.attachment != null) { // ignore if child channel was already created. break; } - // fall-trough - ChannelFuture future = ctx.channel().eventLoop().register(new DefaultHttp2StreamChannel(s, false)); + final Http2MultiplexCodecStreamChannel streamChannel; + // We need to handle upgrades special when on the client side. + if (stream.id() == HTTP_UPGRADE_STREAM_ID && !connection().isServer()) { + // Add our upgrade handler to the channel and then register the channel. + // The register call fires the channelActive, etc. + assert upgradeStreamHandler != null; + streamChannel = new Http2MultiplexCodecStreamChannel(stream, upgradeStreamHandler); + streamChannel.closeOutbound(); + } else { + streamChannel = new Http2MultiplexCodecStreamChannel(stream, inboundStreamHandler); + } + ChannelFuture future = ctx.channel().eventLoop().register(streamChannel); if (future.isDone()) { - registerDone(future); + Http2MultiplexHandler.registerDone(future); } else { - future.addListener(CHILD_CHANNEL_REGISTRATION_LISTENER); + future.addListener(Http2MultiplexHandler.CHILD_CHANNEL_REGISTRATION_LISTENER); } break; case CLOSED: - DefaultHttp2StreamChannel channel = s.channel; + AbstractHttp2StreamChannel channel = (AbstractHttp2StreamChannel) stream.attachment; if (channel != null) { channel.streamClosed(); } @@ -295,79 +195,33 @@ public class Http2MultiplexCodec extends Http2FrameCodec { } } - @Override - final void onHttp2StreamWritabilityChanged(ChannelHandlerContext ctx, Http2FrameStream stream, boolean writable) { - (((Http2MultiplexCodecStream) stream).channel).writabilityChanged(writable); - } - // TODO: This is most likely not the best way to expose this, need to think more about it. final Http2StreamChannel newOutboundStream() { - return new DefaultHttp2StreamChannel(newStream(), true); + return new Http2MultiplexCodecStreamChannel(newStream(), null); } @Override final void onHttp2FrameStreamException(ChannelHandlerContext ctx, Http2FrameStreamException cause) { Http2FrameStream stream = cause.stream(); - DefaultHttp2StreamChannel childChannel = ((Http2MultiplexCodecStream) stream).channel; + AbstractHttp2StreamChannel channel = (AbstractHttp2StreamChannel) ((DefaultHttp2FrameStream) stream).attachment; try { - childChannel.pipeline().fireExceptionCaught(cause.getCause()); + channel.pipeline().fireExceptionCaught(cause.getCause()); } finally { - childChannel.unsafe().closeForcibly(); + channel.unsafe().closeForcibly(); } } - private boolean isChildChannelInReadPendingQueue(DefaultHttp2StreamChannel childChannel) { - return childChannel.previous != null || childChannel.next != null || head == childChannel; - } - - final void tryAddChildChannelToReadPendingQueue(DefaultHttp2StreamChannel childChannel) { - if (!isChildChannelInReadPendingQueue(childChannel)) { - addChildChannelToReadPendingQueue(childChannel); - } - } - - final void addChildChannelToReadPendingQueue(DefaultHttp2StreamChannel childChannel) { - if (tail == null) { - assert head == null; - tail = head = childChannel; - } else { - childChannel.previous = tail; - tail.next = childChannel; - tail = childChannel; - } - } - - private void tryRemoveChildChannelFromReadPendingQueue(DefaultHttp2StreamChannel childChannel) { - if (isChildChannelInReadPendingQueue(childChannel)) { - removeChildChannelFromReadPendingQueue(childChannel); - } - } - - private void removeChildChannelFromReadPendingQueue(DefaultHttp2StreamChannel childChannel) { - DefaultHttp2StreamChannel previous = childChannel.previous; - if (childChannel.next != null) { - childChannel.next.previous = previous; - } else { - tail = tail.previous; // If there is no next, this childChannel is the tail, so move the tail back. - } - if (previous != null) { - previous.next = childChannel.next; - } else { - head = head.next; // If there is no previous, this childChannel is the head, so move the tail forward. - } - childChannel.next = childChannel.previous = null; - } - private void onHttp2GoAwayFrame(ChannelHandlerContext ctx, final Http2GoAwayFrame goAwayFrame) { try { forEachActiveStream(new Http2FrameStreamVisitor() { @Override public boolean visit(Http2FrameStream stream) { final int streamId = stream.id(); - final DefaultHttp2StreamChannel childChannel = ((Http2MultiplexCodecStream) stream).channel; + AbstractHttp2StreamChannel channel = (AbstractHttp2StreamChannel) + ((DefaultHttp2FrameStream) stream).attachment; if (streamId > goAwayFrame.lastStreamId() && connection().local().isValidStreamId(streamId)) { - childChannel.pipeline().fireUserEventTriggered(goAwayFrame.retainedDuplicate()); + channel.pipeline().fireUserEventTriggered(goAwayFrame.retainedDuplicate()); } return true; } @@ -383,917 +237,86 @@ public class Http2MultiplexCodec extends Http2FrameCodec { */ @Override public final void channelReadComplete(ChannelHandlerContext ctx) throws Exception { + processPendingReadCompleteQueue(); + channelReadComplete0(ctx); + } + + private void processPendingReadCompleteQueue() { + parentReadInProgress = true; try { - onChannelReadComplete(ctx); + // If we have many child channel we can optimize for the case when multiple call flush() in + // channelReadComplete(...) callbacks and only do it once as otherwise we will end-up with multiple + // write calls on the socket which is expensive. + for (;;) { + AbstractHttp2StreamChannel childChannel = readCompletePendingQueue.poll(); + if (childChannel == null) { + break; + } + childChannel.fireChildReadComplete(); + } } finally { parentReadInProgress = false; - tail = head = null; + readCompletePendingQueue.clear(); // We always flush as this is what Http2ConnectionHandler does for now. flush0(ctx); } - channelReadComplete0(ctx); } - @Override public final void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { parentReadInProgress = true; super.channelRead(ctx, msg); } - final void onChannelReadComplete(ChannelHandlerContext ctx) { - // If we have many child channel we can optimize for the case when multiple call flush() in - // channelReadComplete(...) callbacks and only do it once as otherwise we will end-up with multiple - // write calls on the socket which is expensive. - DefaultHttp2StreamChannel current = head; - while (current != null) { - DefaultHttp2StreamChannel childChannel = current; - // Clear early in case fireChildReadComplete() causes it to need to be re-processed - current = current.next; - childChannel.next = childChannel.previous = null; - childChannel.fireChildReadComplete(); + @Override + public final void channelWritabilityChanged(final ChannelHandlerContext ctx) throws Exception { + if (ctx.channel().isWritable()) { + // While the writability state may change during iterating of the streams we just set all of the streams + // to writable to not affect fairness. These will be "limited" by their own watermarks in any case. + forEachActiveStream(AbstractHttp2StreamChannel.WRITABLE_VISITOR); } + + super.channelWritabilityChanged(ctx); } final void flush0(ChannelHandlerContext ctx) { flush(ctx); } - static final class Http2MultiplexCodecStream extends DefaultHttp2FrameStream { - DefaultHttp2StreamChannel channel; - } - - private boolean initialWritability(DefaultHttp2FrameStream stream) { - // If the stream id is not valid yet we will just mark the channel as writable as we will be notified - // about non-writability state as soon as the first Http2HeaderFrame is written (if needed). - // This should be good enough and simplify things a lot. - return !isStreamIdValid(stream.id()) || isWritable(stream); - } - - /** - * The current status of the read-processing for a {@link Http2StreamChannel}. - */ - private enum ReadStatus { - /** - * No read in progress and no read was requested (yet) - */ - IDLE, - - /** - * Reading in progress - */ - IN_PROGRESS, - - /** - * A read operation was requested. - */ - REQUESTED - } - - // TODO: Handle writability changes due writing from outside the eventloop. - private final class DefaultHttp2StreamChannel extends DefaultAttributeMap implements Http2StreamChannel { - private final Http2StreamChannelConfig config = new Http2StreamChannelConfig(this); - private final Http2ChannelUnsafe unsafe = new Http2ChannelUnsafe(); - private final ChannelId channelId; - private final ChannelPipeline pipeline; - private final DefaultHttp2FrameStream stream; - private final ChannelPromise closePromise; - private final boolean outbound; - - private volatile boolean registered; - // We start with the writability of the channel when creating the StreamChannel. - private volatile boolean writable; - - private boolean outboundClosed; - - /** - * This variable represents if a read is in progress for the current channel or was requested. - * Note that depending upon the {@link RecvByteBufAllocator} behavior a read may extend beyond the - * {@link Http2ChannelUnsafe#beginRead()} method scope. The {@link Http2ChannelUnsafe#beginRead()} loop may - * drain all pending data, and then if the parent channel is reading this channel may still accept frames. - */ - private ReadStatus readStatus = ReadStatus.IDLE; - - private Queue<Object> inboundBuffer; - - /** {@code true} after the first HEADERS frame has been written **/ - private boolean firstFrameWritten; - - // Currently the child channel and parent channel are always on the same EventLoop thread. This allows us to - // extend the read loop of a child channel if the child channel drains its queued data during read, and the - // parent channel is still in its read loop. The next/previous links build a doubly linked list that the parent - // channel will iterate in its channelReadComplete to end the read cycle for each child channel in the list. - DefaultHttp2StreamChannel next; - DefaultHttp2StreamChannel previous; - - DefaultHttp2StreamChannel(DefaultHttp2FrameStream stream, boolean outbound) { - this.stream = stream; - this.outbound = outbound; - writable = initialWritability(stream); - ((Http2MultiplexCodecStream) stream).channel = this; - pipeline = new DefaultChannelPipeline(this) { - @Override - protected void incrementPendingOutboundBytes(long size) { - // Do thing for now - } - - @Override - protected void decrementPendingOutboundBytes(long size) { - // Do thing for now - } - }; - closePromise = pipeline.newPromise(); - channelId = new Http2StreamChannelId(parent().id(), ++idCount); - } - - @Override - public Http2FrameStream stream() { - return stream; - } - - void streamClosed() { - unsafe.readEOS(); - // Attempt to drain any queued data from the queue and deliver it to the application before closing this - // channel. - unsafe.doBeginRead(); - } - - @Override - public ChannelMetadata metadata() { - return METADATA; - } - - @Override - public ChannelConfig config() { - return config; - } - - @Override - public boolean isOpen() { - return !closePromise.isDone(); - } - - @Override - public boolean isActive() { - return isOpen(); - } - - @Override - public boolean isWritable() { - return writable; - } - - @Override - public ChannelId id() { - return channelId; - } - - @Override - public EventLoop eventLoop() { - return parent().eventLoop(); - } - - @Override - public Channel parent() { - return ctx.channel(); - } - - @Override - public boolean isRegistered() { - return registered; - } - - @Override - public SocketAddress localAddress() { - return parent().localAddress(); - } - - @Override - public SocketAddress remoteAddress() { - return parent().remoteAddress(); - } - - @Override - public ChannelFuture closeFuture() { - return closePromise; - } - - @Override - public long bytesBeforeUnwritable() { - // TODO: Do a proper impl - return config().getWriteBufferHighWaterMark(); - } - - @Override - public long bytesBeforeWritable() { - // TODO: Do a proper impl - return 0; - } - - @Override - public Unsafe unsafe() { - return unsafe; - } - - @Override - public ChannelPipeline pipeline() { - return pipeline; - } - - @Override - public ByteBufAllocator alloc() { - return config().getAllocator(); - } - - @Override - public Channel read() { - pipeline().read(); - return this; - } - - @Override - public Channel flush() { - pipeline().flush(); - return this; - } - - @Override - public ChannelFuture bind(SocketAddress localAddress) { - return pipeline().bind(localAddress); - } - - @Override - public ChannelFuture connect(SocketAddress remoteAddress) { - return pipeline().connect(remoteAddress); - } - - @Override - public ChannelFuture connect(SocketAddress remoteAddress, SocketAddress localAddress) { - return pipeline().connect(remoteAddress, localAddress); - } - - @Override - public ChannelFuture disconnect() { - return pipeline().disconnect(); - } - - @Override - public ChannelFuture close() { - return pipeline().close(); - } - - @Override - public ChannelFuture deregister() { - return pipeline().deregister(); - } - - @Override - public ChannelFuture bind(SocketAddress localAddress, ChannelPromise promise) { - return pipeline().bind(localAddress, promise); - } - - @Override - public ChannelFuture connect(SocketAddress remoteAddress, ChannelPromise promise) { - return pipeline().connect(remoteAddress, promise); - } - - @Override - public ChannelFuture connect(SocketAddress remoteAddress, SocketAddress localAddress, ChannelPromise promise) { - return pipeline().connect(remoteAddress, localAddress, promise); - } - - @Override - public ChannelFuture disconnect(ChannelPromise promise) { - return pipeline().disconnect(promise); - } - - @Override - public ChannelFuture close(ChannelPromise promise) { - return pipeline().close(promise); - } - - @Override - public ChannelFuture deregister(ChannelPromise promise) { - return pipeline().deregister(promise); - } - - @Override - public ChannelFuture write(Object msg) { - return pipeline().write(msg); - } - - @Override - public ChannelFuture write(Object msg, ChannelPromise promise) { - return pipeline().write(msg, promise); - } - - @Override - public ChannelFuture writeAndFlush(Object msg, ChannelPromise promise) { - return pipeline().writeAndFlush(msg, promise); - } - - @Override - public ChannelFuture writeAndFlush(Object msg) { - return pipeline().writeAndFlush(msg); - } - - @Override - public ChannelPromise newPromise() { - return pipeline().newPromise(); - } - - @Override - public ChannelProgressivePromise newProgressivePromise() { - return pipeline().newProgressivePromise(); - } - - @Override - public ChannelFuture newSucceededFuture() { - return pipeline().newSucceededFuture(); - } + private final class Http2MultiplexCodecStreamChannel extends AbstractHttp2StreamChannel { - @Override - public ChannelFuture newFailedFuture(Throwable cause) { - return pipeline().newFailedFuture(cause); + Http2MultiplexCodecStreamChannel(DefaultHttp2FrameStream stream, ChannelHandler inboundHandler) { + super(stream, ++idCount, inboundHandler); } @Override - public ChannelPromise voidPromise() { - return pipeline().voidPromise(); + protected boolean isParentReadInProgress() { + return parentReadInProgress; } @Override - public int hashCode() { - return id().hashCode(); + protected void addChannelToReadCompletePendingQueue() { + // If there is no space left in the queue, just keep on processing everything that is already + // stored there and try again. + while (!readCompletePendingQueue.offer(this)) { + processPendingReadCompleteQueue(); + } } @Override - public boolean equals(Object o) { - return this == o; + protected ChannelHandlerContext parentContext() { + return ctx; } @Override - public int compareTo(Channel o) { - if (this == o) { - return 0; - } - - return id().compareTo(o.id()); + protected ChannelFuture write0(ChannelHandlerContext ctx, Object msg) { + ChannelPromise promise = ctx.newPromise(); + Http2MultiplexCodec.this.write(ctx, msg, promise); + return promise; } @Override - public String toString() { - return parent().toString() + "(H2 - " + stream + ')'; - } - - void writabilityChanged(boolean writable) { - assert eventLoop().inEventLoop(); - if (writable != this.writable && isActive()) { - // Only notify if we received a state change. - this.writable = writable; - pipeline().fireChannelWritabilityChanged(); - } - } - - /** - * Receive a read message. This does not notify handlers unless a read is in progress on the - * channel. - */ - void fireChildRead(Http2Frame frame) { - assert eventLoop().inEventLoop(); - if (!isActive()) { - ReferenceCountUtil.release(frame); - } else if (readStatus != ReadStatus.IDLE) { - // If a read is in progress or has been requested, there cannot be anything in the queue, - // otherwise we would have drained it from the queue and processed it during the read cycle. - assert inboundBuffer == null || inboundBuffer.isEmpty(); - final Handle allocHandle = unsafe.recvBufAllocHandle(); - unsafe.doRead0(frame, allocHandle); - // We currently don't need to check for readEOS because the parent channel and child channel are limited - // to the same EventLoop thread. There are a limited number of frame types that may come after EOS is - // read (unknown, reset) and the trade off is less conditionals for the hot path (headers/data) at the - // cost of additional readComplete notifications on the rare path. - if (allocHandle.continueReading()) { - tryAddChildChannelToReadPendingQueue(this); - } else { - tryRemoveChildChannelFromReadPendingQueue(this); - unsafe.notifyReadComplete(allocHandle); - } - } else { - if (inboundBuffer == null) { - inboundBuffer = new ArrayDeque<Object>(4); - } - inboundBuffer.add(frame); - } - } - - void fireChildReadComplete() { - assert eventLoop().inEventLoop(); - assert readStatus != ReadStatus.IDLE; - unsafe.notifyReadComplete(unsafe.recvBufAllocHandle()); - } - - private final class Http2ChannelUnsafe implements Unsafe { - private final VoidChannelPromise unsafeVoidPromise = - new VoidChannelPromise(DefaultHttp2StreamChannel.this, false); - @SuppressWarnings("deprecation") - private Handle recvHandle; - private boolean writeDoneAndNoFlush; - private boolean closeInitiated; - private boolean readEOS; - - @Override - public void connect(final SocketAddress remoteAddress, - SocketAddress localAddress, final ChannelPromise promise) { - if (!promise.setUncancellable()) { - return; - } - promise.setFailure(new UnsupportedOperationException()); - } - - @Override - public Handle recvBufAllocHandle() { - if (recvHandle == null) { - recvHandle = config().getRecvByteBufAllocator().newHandle(); - recvHandle.reset(config()); - } - return recvHandle; - } - - @Override - public SocketAddress localAddress() { - return parent().unsafe().localAddress(); - } - - @Override - public SocketAddress remoteAddress() { - return parent().unsafe().remoteAddress(); - } - - @Override - public void register(EventLoop eventLoop, ChannelPromise promise) { - if (!promise.setUncancellable()) { - return; - } - if (registered) { - throw new UnsupportedOperationException("Re-register is not supported"); - } - - registered = true; - - if (!outbound) { - // Add the handler to the pipeline now that we are registered. - pipeline().addLast(inboundStreamHandler); - } - - promise.setSuccess(); - - pipeline().fireChannelRegistered(); - if (isActive()) { - pipeline().fireChannelActive(); - } - } - - @Override - public void bind(SocketAddress localAddress, ChannelPromise promise) { - if (!promise.setUncancellable()) { - return; - } - promise.setFailure(new UnsupportedOperationException()); - } - - @Override - public void disconnect(ChannelPromise promise) { - close(promise); - } - - @Override - public void close(final ChannelPromise promise) { - if (!promise.setUncancellable()) { - return; - } - if (closeInitiated) { - if (closePromise.isDone()) { - // Closed already. - promise.setSuccess(); - } else if (!(promise instanceof VoidChannelPromise)) { // Only needed if no VoidChannelPromise. - // This means close() was called before so we just register a listener and return - closePromise.addListener(new ChannelFutureListener() { - @Override - public void operationComplete(ChannelFuture future) { - promise.setSuccess(); - } - }); - } - return; - } - closeInitiated = true; - - tryRemoveChildChannelFromReadPendingQueue(DefaultHttp2StreamChannel.this); - - final boolean wasActive = isActive(); - - // Only ever send a reset frame if the connection is still alive and if the stream may have existed - // as otherwise we may send a RST on a stream in an invalid state and cause a connection error. - if (parent().isActive() && !readEOS && connection().streamMayHaveExisted(stream().id())) { - Http2StreamFrame resetFrame = new DefaultHttp2ResetFrame(Http2Error.CANCEL).stream(stream()); - write(resetFrame, unsafe().voidPromise()); - flush(); - } - - if (inboundBuffer != null) { - for (;;) { - Object msg = inboundBuffer.poll(); - if (msg == null) { - break; - } - ReferenceCountUtil.release(msg); - } - } - - // The promise should be notified before we call fireChannelInactive(). - outboundClosed = true; - closePromise.setSuccess(); - promise.setSuccess(); - - fireChannelInactiveAndDeregister(voidPromise(), wasActive); - } - - @Override - public void closeForcibly() { - close(unsafe().voidPromise()); - } - - @Override - public void deregister(ChannelPromise promise) { - fireChannelInactiveAndDeregister(promise, false); - } - - private void fireChannelInactiveAndDeregister(final ChannelPromise promise, - final boolean fireChannelInactive) { - if (!promise.setUncancellable()) { - return; - } - - if (!registered) { - promise.setSuccess(); - return; - } - - // As a user may call deregister() from within any method while doing processing in the ChannelPipeline, - // we need to ensure we do the actual deregister operation later. This is necessary to preserve the - // behavior of the AbstractChannel, which always invokes channelUnregistered and channelInactive - // events 'later' to ensure the current events in the handler are completed before these events. - // - // See: - // https://github.com/netty/netty/issues/4435 - invokeLater(new Runnable() { - @Override - public void run() { - if (fireChannelInactive) { - pipeline.fireChannelInactive(); - } - // The user can fire `deregister` events multiple times but we only want to fire the pipeline - // event if the channel was actually registered. - if (registered) { - registered = false; - pipeline.fireChannelUnregistered(); - } - safeSetSuccess(promise); - } - }); - } - - private void safeSetSuccess(ChannelPromise promise) { - if (!(promise instanceof VoidChannelPromise) && !promise.trySuccess()) { - logger.warn("Failed to mark a promise as success because it is done already: {}", promise); - } - } - - private void invokeLater(Runnable task) { - try { - // This method is used by outbound operation implementations to trigger an inbound event later. - // They do not trigger an inbound event immediately because an outbound operation might have been - // triggered by another inbound event handler method. If fired immediately, the call stack - // will look like this for example: - // - // handlerA.inboundBufferUpdated() - (1) an inbound handler method closes a connection. - // -> handlerA.ctx.close() - // -> channel.unsafe.close() - // -> handlerA.channelInactive() - (2) another inbound handler method called while in (1) yet - // - // which means the execution of two inbound handler methods of the same handler overlap undesirably. - eventLoop().execute(task); - } catch (RejectedExecutionException e) { - logger.warn("Can't invoke task later as EventLoop rejected it", e); - } - } - - @Override - public void beginRead() { - if (!isActive()) { - return; - } - switch (readStatus) { - case IDLE: - readStatus = ReadStatus.IN_PROGRESS; - doBeginRead(); - break; - case IN_PROGRESS: - readStatus = ReadStatus.REQUESTED; - break; - default: - break; - } - } - - void doBeginRead() { - Object message; - if (inboundBuffer == null || (message = inboundBuffer.poll()) == null) { - if (readEOS) { - unsafe.closeForcibly(); - } - } else { - final Handle allocHandle = recvBufAllocHandle(); - allocHandle.reset(config()); - boolean continueReading = false; - do { - doRead0((Http2Frame) message, allocHandle); - } while ((readEOS || (continueReading = allocHandle.continueReading())) && - (message = inboundBuffer.poll()) != null); - - if (continueReading && parentReadInProgress && !readEOS) { - // Currently the parent and child channel are on the same EventLoop thread. If the parent is - // currently reading it is possile that more frames will be delivered to this child channel. In - // the case that this child channel still wants to read we delay the channelReadComplete on this - // child channel until the parent is done reading. - assert !isChildChannelInReadPendingQueue(DefaultHttp2StreamChannel.this); - addChildChannelToReadPendingQueue(DefaultHttp2StreamChannel.this); - } else { - notifyReadComplete(allocHandle); - } - } - } - - void readEOS() { - readEOS = true; - } - - void notifyReadComplete(Handle allocHandle) { - assert next == null && previous == null; - if (readStatus == ReadStatus.REQUESTED) { - readStatus = ReadStatus.IN_PROGRESS; - } else { - readStatus = ReadStatus.IDLE; - } - allocHandle.readComplete(); - pipeline().fireChannelReadComplete(); - // Reading data may result in frames being written (e.g. WINDOW_UPDATE, RST, etc..). If the parent - // channel is not currently reading we need to force a flush at the child channel, because we cannot - // rely upon flush occurring in channelReadComplete on the parent channel. - flush(); - if (readEOS) { - unsafe.closeForcibly(); - } - } - - @SuppressWarnings("deprecation") - void doRead0(Http2Frame frame, Handle allocHandle) { - pipeline().fireChannelRead(frame); - allocHandle.incMessagesRead(1); - - if (frame instanceof Http2DataFrame) { - final int numBytesToBeConsumed = ((Http2DataFrame) frame).initialFlowControlledBytes(); - allocHandle.attemptedBytesRead(numBytesToBeConsumed); - allocHandle.lastBytesRead(numBytesToBeConsumed); - if (numBytesToBeConsumed != 0) { - try { - writeDoneAndNoFlush |= consumeBytes(stream.id(), numBytesToBeConsumed); - } catch (Http2Exception e) { - pipeline().fireExceptionCaught(e); - } - } - } else { - allocHandle.attemptedBytesRead(MIN_HTTP2_FRAME_SIZE); - allocHandle.lastBytesRead(MIN_HTTP2_FRAME_SIZE); - } - } - - @Override - public void write(Object msg, final ChannelPromise promise) { - // After this point its not possible to cancel a write anymore. - if (!promise.setUncancellable()) { - ReferenceCountUtil.release(msg); - return; - } - - if (!isActive() || - // Once the outbound side was closed we should not allow header / data frames - outboundClosed && (msg instanceof Http2HeadersFrame || msg instanceof Http2DataFrame)) { - ReferenceCountUtil.release(msg); - promise.setFailure(CLOSED_CHANNEL_EXCEPTION); - return; - } - - try { - if (msg instanceof Http2StreamFrame) { - Http2StreamFrame frame = validateStreamFrame((Http2StreamFrame) msg).stream(stream()); - if (!firstFrameWritten && !isStreamIdValid(stream().id())) { - if (!(frame instanceof Http2HeadersFrame)) { - ReferenceCountUtil.release(frame); - promise.setFailure( - new IllegalArgumentException("The first frame must be a headers frame. Was: " - + frame.name())); - return; - } - firstFrameWritten = true; - ChannelFuture future = write0(frame); - if (future.isDone()) { - firstWriteComplete(future, promise); - } else { - future.addListener(new ChannelFutureListener() { - @Override - public void operationComplete(ChannelFuture future) { - firstWriteComplete(future, promise); - } - }); - } - return; - } - } else { - String msgStr = msg.toString(); - ReferenceCountUtil.release(msg); - promise.setFailure(new IllegalArgumentException( - "Message must be an " + StringUtil.simpleClassName(Http2StreamFrame.class) + - ": " + msgStr)); - return; - } - - ChannelFuture future = write0(msg); - if (future.isDone()) { - writeComplete(future, promise); - } else { - future.addListener(new ChannelFutureListener() { - @Override - public void operationComplete(ChannelFuture future) { - writeComplete(future, promise); - } - }); - } - } catch (Throwable t) { - promise.tryFailure(t); - } finally { - writeDoneAndNoFlush = true; - } - } - - private void firstWriteComplete(ChannelFuture future, ChannelPromise promise) { - Throwable cause = future.cause(); - if (cause == null) { - // As we just finished our first write which made the stream-id valid we need to re-evaluate - // the writability of the channel. - writabilityChanged(Http2MultiplexCodec.this.isWritable(stream)); - promise.setSuccess(); - } else { - // If the first write fails there is not much we can do, just close - closeForcibly(); - promise.setFailure(wrapStreamClosedError(cause)); - } - } - - private void writeComplete(ChannelFuture future, ChannelPromise promise) { - Throwable cause = future.cause(); - if (cause == null) { - promise.setSuccess(); - } else { - Throwable error = wrapStreamClosedError(cause); - if (error instanceof ClosedChannelException) { - if (config.isAutoClose()) { - // Close channel if needed. - closeForcibly(); - } else { - outboundClosed = true; - } - } - promise.setFailure(error); - } - } - - private Throwable wrapStreamClosedError(Throwable cause) { - // If the error was caused by STREAM_CLOSED we should use a ClosedChannelException to better - // mimic other transports and make it easier to reason about what exceptions to expect. - if (cause instanceof Http2Exception && ((Http2Exception) cause).error() == Http2Error.STREAM_CLOSED) { - return new ClosedChannelException().initCause(cause); - } - return cause; - } - - private Http2StreamFrame validateStreamFrame(Http2StreamFrame frame) { - if (frame.stream() != null && frame.stream() != stream) { - String msgString = frame.toString(); - ReferenceCountUtil.release(frame); - throw new IllegalArgumentException( - "Stream " + frame.stream() + " must not be set on the frame: " + msgString); - } - return frame; - } - - private ChannelFuture write0(Object msg) { - ChannelPromise promise = ctx.newPromise(); - Http2MultiplexCodec.this.write(ctx, msg, promise); - return promise; - } - - @Override - public void flush() { - // If we are currently in the parent channel's read loop we should just ignore the flush. - // We will ensure we trigger ctx.flush() after we processed all Channels later on and - // so aggregate the flushes. This is done as ctx.flush() is expensive when as it may trigger an - // write(...) or writev(...) operation on the socket. - if (!writeDoneAndNoFlush || parentReadInProgress) { - // There is nothing to flush so this is a NOOP. - return; - } - try { - flush0(ctx); - } finally { - writeDoneAndNoFlush = false; - } - } - - @Override - public ChannelPromise voidPromise() { - return unsafeVoidPromise; - } - - @Override - public ChannelOutboundBuffer outboundBuffer() { - // Always return null as we not use the ChannelOutboundBuffer and not even support it. - return null; - } - } - - /** - * {@link ChannelConfig} so that the high and low writebuffer watermarks can reflect the outbound flow control - * window, without having to create a new {@link WriteBufferWaterMark} object whenever the flow control window - * changes. - */ - private final class Http2StreamChannelConfig extends DefaultChannelConfig { - Http2StreamChannelConfig(Channel channel) { - super(channel); - } - - @Override - public int getWriteBufferHighWaterMark() { - return min(parent().config().getWriteBufferHighWaterMark(), initialOutboundStreamWindow); - } - - @Override - public int getWriteBufferLowWaterMark() { - return min(parent().config().getWriteBufferLowWaterMark(), initialOutboundStreamWindow); - } - - @Override - public MessageSizeEstimator getMessageSizeEstimator() { - return FlowControlledFrameSizeEstimator.INSTANCE; - } - - @Override - public WriteBufferWaterMark getWriteBufferWaterMark() { - int mark = getWriteBufferHighWaterMark(); - return new WriteBufferWaterMark(mark, mark); - } - - @Override - public ChannelConfig setMessageSizeEstimator(MessageSizeEstimator estimator) { - throw new UnsupportedOperationException(); - } - - @Override - @Deprecated - public ChannelConfig setWriteBufferHighWaterMark(int writeBufferHighWaterMark) { - throw new UnsupportedOperationException(); - } - - @Override - @Deprecated - public ChannelConfig setWriteBufferLowWaterMark(int writeBufferLowWaterMark) { - throw new UnsupportedOperationException(); - } - - @Override - public ChannelConfig setWriteBufferWaterMark(WriteBufferWaterMark writeBufferWaterMark) { - throw new UnsupportedOperationException(); - } - - @Override - public ChannelConfig setRecvByteBufAllocator(RecvByteBufAllocator allocator) { - if (!(allocator.newHandle() instanceof RecvByteBufAllocator.ExtendedHandle)) { - throw new IllegalArgumentException("allocator.newHandle() must return an object of type: " + - RecvByteBufAllocator.ExtendedHandle.class); - } - super.setRecvByteBufAllocator(allocator); - return this; - } + protected void flush0(ChannelHandlerContext ctx) { + Http2MultiplexCodec.this.flush0(ctx); } } } diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2MultiplexCodecBuilder.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2MultiplexCodecBuilder.java index c5732ec..5d0829e 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2MultiplexCodecBuilder.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2MultiplexCodecBuilder.java @@ -23,7 +23,10 @@ import static io.netty.util.internal.ObjectUtil.checkNotNull; /** * A builder for {@link Http2MultiplexCodec}. + * + * @deprecated use {@link Http2FrameCodecBuilder} together with {@link Http2MultiplexHandler}. */ +@Deprecated @UnstableApi public class Http2MultiplexCodecBuilder extends AbstractHttp2ConnectionHandlerBuilder<Http2MultiplexCodec, Http2MultiplexCodecBuilder> { @@ -35,6 +38,8 @@ public class Http2MultiplexCodecBuilder Http2MultiplexCodecBuilder(boolean server, ChannelHandler childHandler) { server(server); this.childHandler = checkSharable(checkNotNull(childHandler, "childHandler")); + // For backwards compatibility we should disable to timeout by default at this layer. + gracefulShutdownTimeoutMillis(0); } private static ChannelHandler checkSharable(ChannelHandler handler) { @@ -52,7 +57,7 @@ public class Http2MultiplexCodecBuilder } /** - * Creates a builder for a HTTP/2 client. + * Creates a builder for an HTTP/2 client. * * @param childHandler the handler added to channels for remotely-created streams. It must be * {@link ChannelHandler.Sharable}. @@ -62,7 +67,7 @@ public class Http2MultiplexCodecBuilder } /** - * Creates a builder for a HTTP/2 server. + * Creates a builder for an HTTP/2 server. * * @param childHandler the handler added to channels for remotely-created streams. It must be * {@link ChannelHandler.Sharable}. @@ -71,6 +76,14 @@ public class Http2MultiplexCodecBuilder return new Http2MultiplexCodecBuilder(true, childHandler); } + public Http2MultiplexCodecBuilder withUpgradeStreamHandler(ChannelHandler upgradeStreamHandler) { + if (this.isServer()) { + throw new IllegalArgumentException("Server codecs don't use an extra handler for the upgrade stream"); + } + this.upgradeStreamHandler = upgradeStreamHandler; + return this; + } + @Override public Http2Settings initialSettings() { return super.initialSettings(); @@ -91,14 +104,6 @@ public class Http2MultiplexCodecBuilder return super.gracefulShutdownTimeoutMillis(gracefulShutdownTimeoutMillis); } - public Http2MultiplexCodecBuilder withUpgradeStreamHandler(ChannelHandler upgradeStreamHandler) { - if (this.isServer()) { - throw new IllegalArgumentException("Server codecs don't use an extra handler for the upgrade stream"); - } - this.upgradeStreamHandler = upgradeStreamHandler; - return this; - } - @Override public boolean isServer() { return super.isServer(); @@ -144,6 +149,16 @@ public class Http2MultiplexCodecBuilder return super.encoderEnforceMaxConcurrentStreams(encoderEnforceMaxConcurrentStreams); } + @Override + public int encoderEnforceMaxQueuedControlFrames() { + return super.encoderEnforceMaxQueuedControlFrames(); + } + + @Override + public Http2MultiplexCodecBuilder encoderEnforceMaxQueuedControlFrames(int maxQueuedControlFrames) { + return super.encoderEnforceMaxQueuedControlFrames(maxQueuedControlFrames); + } + @Override public Http2HeadersEncoder.SensitivityDetector headerSensitivityDetector() { return super.headerSensitivityDetector(); @@ -161,10 +176,36 @@ public class Http2MultiplexCodecBuilder } @Override + @Deprecated public Http2MultiplexCodecBuilder initialHuffmanDecodeCapacity(int initialHuffmanDecodeCapacity) { return super.initialHuffmanDecodeCapacity(initialHuffmanDecodeCapacity); } + @Override + public Http2MultiplexCodecBuilder autoAckSettingsFrame(boolean autoAckSettings) { + return super.autoAckSettingsFrame(autoAckSettings); + } + + @Override + public Http2MultiplexCodecBuilder autoAckPingFrame(boolean autoAckPingFrame) { + return super.autoAckPingFrame(autoAckPingFrame); + } + + @Override + public Http2MultiplexCodecBuilder decoupleCloseAndGoAway(boolean decoupleCloseAndGoAway) { + return super.decoupleCloseAndGoAway(decoupleCloseAndGoAway); + } + + @Override + public int decoderEnforceMaxConsecutiveEmptyDataFrames() { + return super.decoderEnforceMaxConsecutiveEmptyDataFrames(); + } + + @Override + public Http2MultiplexCodecBuilder decoderEnforceMaxConsecutiveEmptyDataFrames(int maxConsecutiveEmptyFrames) { + return super.decoderEnforceMaxConsecutiveEmptyDataFrames(maxConsecutiveEmptyFrames); + } + @Override public Http2MultiplexCodec build() { Http2FrameWriter frameWriter = this.frameWriter; @@ -174,8 +215,8 @@ public class Http2MultiplexCodecBuilder DefaultHttp2Connection connection = new DefaultHttp2Connection(isServer(), maxReservedStreams()); Long maxHeaderListSize = initialSettings().maxHeaderListSize(); Http2FrameReader frameReader = new DefaultHttp2FrameReader(maxHeaderListSize == null ? - new DefaultHttp2HeadersDecoder(true) : - new DefaultHttp2HeadersDecoder(true, maxHeaderListSize)); + new DefaultHttp2HeadersDecoder(isValidateHeaders()) : + new DefaultHttp2HeadersDecoder(isValidateHeaders(), maxHeaderListSize)); if (frameLogger() != null) { frameWriter = new Http2OutboundFrameLogger(frameWriter, frameLogger()); @@ -185,7 +226,13 @@ public class Http2MultiplexCodecBuilder if (encoderEnforceMaxConcurrentStreams()) { encoder = new StreamBufferingEncoder(encoder); } - Http2ConnectionDecoder decoder = new DefaultHttp2ConnectionDecoder(connection, encoder, frameReader); + Http2ConnectionDecoder decoder = new DefaultHttp2ConnectionDecoder(connection, encoder, frameReader, + promisedRequestVerifier(), isAutoAckSettingsFrame(), isAutoAckPingFrame()); + + int maxConsecutiveEmptyDataFrames = decoderEnforceMaxConsecutiveEmptyDataFrames(); + if (maxConsecutiveEmptyDataFrames > 0) { + decoder = new Http2EmptyDataFrameConnectionDecoder(decoder, maxConsecutiveEmptyDataFrames); + } return build(decoder, encoder, initialSettings()); } @@ -195,6 +242,9 @@ public class Http2MultiplexCodecBuilder @Override protected Http2MultiplexCodec build( Http2ConnectionDecoder decoder, Http2ConnectionEncoder encoder, Http2Settings initialSettings) { - return new Http2MultiplexCodec(encoder, decoder, initialSettings, childHandler, upgradeStreamHandler); + Http2MultiplexCodec codec = new Http2MultiplexCodec(encoder, decoder, initialSettings, childHandler, + upgradeStreamHandler, decoupleCloseAndGoAway()); + codec.gracefulShutdownTimeoutMillis(gracefulShutdownTimeoutMillis()); + return codec; } } diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2MultiplexHandler.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2MultiplexHandler.java new file mode 100644 index 0000000..f9e8b11 --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2MultiplexHandler.java @@ -0,0 +1,365 @@ +/* + * Copyright 2019 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.codec.http2; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.Channel; +import io.netty.channel.ChannelConfig; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.EventLoop; +import io.netty.channel.ServerChannel; +import io.netty.handler.codec.http2.Http2FrameCodec.DefaultHttp2FrameStream; +import io.netty.util.ReferenceCounted; +import io.netty.util.internal.ObjectUtil; +import io.netty.util.internal.UnstableApi; + +import java.util.ArrayDeque; +import java.util.Queue; + +import static io.netty.handler.codec.http2.Http2Error.INTERNAL_ERROR; +import static io.netty.handler.codec.http2.Http2Exception.connectionError; + +/** + * An HTTP/2 handler that creates child channels for each stream. This handler must be used in combination + * with {@link Http2FrameCodec}. + * + * <p>When a new stream is created, a new {@link Channel} is created for it. Applications send and + * receive {@link Http2StreamFrame}s on the created channel. {@link ByteBuf}s cannot be processed by the channel; + * all writes that reach the head of the pipeline must be an instance of {@link Http2StreamFrame}. Writes that reach + * the head of the pipeline are processed directly by this handler and cannot be intercepted. + * + * <p>The child channel will be notified of user events that impact the stream, such as {@link + * Http2GoAwayFrame} and {@link Http2ResetFrame}, as soon as they occur. Although {@code + * Http2GoAwayFrame} and {@code Http2ResetFrame} signify that the remote is ignoring further + * communication, closing of the channel is delayed until any inbound queue is drained with {@link + * Channel#read()}, which follows the default behavior of channels in Netty. Applications are + * free to close the channel in response to such events if they don't have use for any queued + * messages. Any connection level events like {@link Http2SettingsFrame} and {@link Http2GoAwayFrame} + * will be processed internally and also propagated down the pipeline for other handlers to act on. + * + * <p>Outbound streams are supported via the {@link Http2StreamChannelBootstrap}. + * + * <p>{@link ChannelConfig#setMaxMessagesPerRead(int)} and {@link ChannelConfig#setAutoRead(boolean)} are supported. + * + * <h3>Reference Counting</h3> + * + * Some {@link Http2StreamFrame}s implement the {@link ReferenceCounted} interface, as they carry + * reference counted objects (e.g. {@link ByteBuf}s). The multiplex codec will call {@link ReferenceCounted#retain()} + * before propagating a reference counted object through the pipeline, and thus an application handler needs to release + * such an object after having consumed it. For more information on reference counting take a look at + * https://netty.io/wiki/reference-counted-objects.html + * + * <h3>Channel Events</h3> + * + * A child channel becomes active as soon as it is registered to an {@link EventLoop}. Therefore, an active channel + * does not map to an active HTTP/2 stream immediately. Only once a {@link Http2HeadersFrame} has been successfully sent + * or received, does the channel map to an active HTTP/2 stream. In case it is not possible to open a new HTTP/2 stream + * (i.e. due to the maximum number of active streams being exceeded), the child channel receives an exception + * indicating the cause and is closed immediately thereafter. + * + * <h3>Writability and Flow Control</h3> + * + * A child channel observes outbound/remote flow control via the channel's writability. A channel only becomes writable + * when it maps to an active HTTP/2 stream . A child channel does not know about the connection-level flow control + * window. {@link ChannelHandler}s are free to ignore the channel's writability, in which case the excessive writes will + * be buffered by the parent channel. It's important to note that only {@link Http2DataFrame}s are subject to + * HTTP/2 flow control. + */ +@UnstableApi +public final class Http2MultiplexHandler extends Http2ChannelDuplexHandler { + + static final ChannelFutureListener CHILD_CHANNEL_REGISTRATION_LISTENER = new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture future) { + registerDone(future); + } + }; + + private final ChannelHandler inboundStreamHandler; + private final ChannelHandler upgradeStreamHandler; + private final Queue<AbstractHttp2StreamChannel> readCompletePendingQueue = + new MaxCapacityQueue<AbstractHttp2StreamChannel>(new ArrayDeque<AbstractHttp2StreamChannel>(8), + // Choose 100 which is what is used most of the times as default. + Http2CodecUtil.SMALLEST_MAX_CONCURRENT_STREAMS); + + private boolean parentReadInProgress; + private int idCount; + + // Need to be volatile as accessed from within the Http2MultiplexHandlerStreamChannel in a multi-threaded fashion. + private volatile ChannelHandlerContext ctx; + + /** + * Creates a new instance + * + * @param inboundStreamHandler the {@link ChannelHandler} that will be added to the {@link ChannelPipeline} of + * the {@link Channel}s created for new inbound streams. + */ + public Http2MultiplexHandler(ChannelHandler inboundStreamHandler) { + this(inboundStreamHandler, null); + } + + /** + * Creates a new instance + * + * @param inboundStreamHandler the {@link ChannelHandler} that will be added to the {@link ChannelPipeline} of + * the {@link Channel}s created for new inbound streams. + * @param upgradeStreamHandler the {@link ChannelHandler} that will be added to the {@link ChannelPipeline} of the + * upgraded {@link Channel}. + */ + public Http2MultiplexHandler(ChannelHandler inboundStreamHandler, ChannelHandler upgradeStreamHandler) { + this.inboundStreamHandler = ObjectUtil.checkNotNull(inboundStreamHandler, "inboundStreamHandler"); + this.upgradeStreamHandler = upgradeStreamHandler; + } + + static void registerDone(ChannelFuture future) { + // Handle any errors that occurred on the local thread while registering. Even though + // failures can happen after this point, they will be handled by the channel by closing the + // childChannel. + if (!future.isSuccess()) { + Channel childChannel = future.channel(); + if (childChannel.isRegistered()) { + childChannel.close(); + } else { + childChannel.unsafe().closeForcibly(); + } + } + } + + @Override + protected void handlerAdded0(ChannelHandlerContext ctx) { + if (ctx.executor() != ctx.channel().eventLoop()) { + throw new IllegalStateException("EventExecutor must be EventLoop of Channel"); + } + this.ctx = ctx; + } + + @Override + protected void handlerRemoved0(ChannelHandlerContext ctx) { + readCompletePendingQueue.clear(); + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + parentReadInProgress = true; + if (msg instanceof Http2StreamFrame) { + if (msg instanceof Http2WindowUpdateFrame) { + // We dont want to propagate update frames to the user + return; + } + Http2StreamFrame streamFrame = (Http2StreamFrame) msg; + DefaultHttp2FrameStream s = + (DefaultHttp2FrameStream) streamFrame.stream(); + + AbstractHttp2StreamChannel channel = (AbstractHttp2StreamChannel) s.attachment; + if (msg instanceof Http2ResetFrame) { + // Reset frames needs to be propagated via user events as these are not flow-controlled and so + // must not be controlled by suppressing channel.read() on the child channel. + channel.pipeline().fireUserEventTriggered(msg); + + // RST frames will also trigger closing of the streams which then will call + // AbstractHttp2StreamChannel.streamClosed() + } else { + channel.fireChildRead(streamFrame); + } + return; + } + + if (msg instanceof Http2GoAwayFrame) { + // goaway frames will also trigger closing of the streams which then will call + // AbstractHttp2StreamChannel.streamClosed() + onHttp2GoAwayFrame(ctx, (Http2GoAwayFrame) msg); + } + + // Send everything down the pipeline + ctx.fireChannelRead(msg); + } + + @Override + public void channelWritabilityChanged(final ChannelHandlerContext ctx) throws Exception { + if (ctx.channel().isWritable()) { + // While the writability state may change during iterating of the streams we just set all of the streams + // to writable to not affect fairness. These will be "limited" by their own watermarks in any case. + forEachActiveStream(AbstractHttp2StreamChannel.WRITABLE_VISITOR); + } + + ctx.fireChannelWritabilityChanged(); + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + if (evt instanceof Http2FrameStreamEvent) { + Http2FrameStreamEvent event = (Http2FrameStreamEvent) evt; + DefaultHttp2FrameStream stream = (DefaultHttp2FrameStream) event.stream(); + if (event.type() == Http2FrameStreamEvent.Type.State) { + switch (stream.state()) { + case HALF_CLOSED_LOCAL: + if (stream.id() != Http2CodecUtil.HTTP_UPGRADE_STREAM_ID) { + // Ignore everything which was not caused by an upgrade + break; + } + // fall-through + case HALF_CLOSED_REMOTE: + // fall-through + case OPEN: + if (stream.attachment != null) { + // ignore if child channel was already created. + break; + } + final AbstractHttp2StreamChannel ch; + // We need to handle upgrades special when on the client side. + if (stream.id() == Http2CodecUtil.HTTP_UPGRADE_STREAM_ID && !isServer(ctx)) { + // We must have an upgrade handler or else we can't handle the stream + if (upgradeStreamHandler == null) { + throw connectionError(INTERNAL_ERROR, + "Client is misconfigured for upgrade requests"); + } + ch = new Http2MultiplexHandlerStreamChannel(stream, upgradeStreamHandler); + ch.closeOutbound(); + } else { + ch = new Http2MultiplexHandlerStreamChannel(stream, inboundStreamHandler); + } + ChannelFuture future = ctx.channel().eventLoop().register(ch); + if (future.isDone()) { + registerDone(future); + } else { + future.addListener(CHILD_CHANNEL_REGISTRATION_LISTENER); + } + break; + case CLOSED: + AbstractHttp2StreamChannel channel = (AbstractHttp2StreamChannel) stream.attachment; + if (channel != null) { + channel.streamClosed(); + } + break; + default: + // ignore for now + break; + } + } + return; + } + ctx.fireUserEventTriggered(evt); + } + + // TODO: This is most likely not the best way to expose this, need to think more about it. + Http2StreamChannel newOutboundStream() { + return new Http2MultiplexHandlerStreamChannel((DefaultHttp2FrameStream) newStream(), null); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + if (cause instanceof Http2FrameStreamException) { + Http2FrameStreamException exception = (Http2FrameStreamException) cause; + Http2FrameStream stream = exception.stream(); + AbstractHttp2StreamChannel childChannel = (AbstractHttp2StreamChannel) + ((DefaultHttp2FrameStream) stream).attachment; + try { + childChannel.pipeline().fireExceptionCaught(cause.getCause()); + } finally { + childChannel.unsafe().closeForcibly(); + } + return; + } + ctx.fireExceptionCaught(cause); + } + + private static boolean isServer(ChannelHandlerContext ctx) { + return ctx.channel().parent() instanceof ServerChannel; + } + + private void onHttp2GoAwayFrame(ChannelHandlerContext ctx, final Http2GoAwayFrame goAwayFrame) { + try { + final boolean server = isServer(ctx); + forEachActiveStream(new Http2FrameStreamVisitor() { + @Override + public boolean visit(Http2FrameStream stream) { + final int streamId = stream.id(); + if (streamId > goAwayFrame.lastStreamId() && Http2CodecUtil.isStreamIdValid(streamId, server)) { + final AbstractHttp2StreamChannel childChannel = (AbstractHttp2StreamChannel) + ((DefaultHttp2FrameStream) stream).attachment; + childChannel.pipeline().fireUserEventTriggered(goAwayFrame.retainedDuplicate()); + } + return true; + } + }); + } catch (Http2Exception e) { + ctx.fireExceptionCaught(e); + ctx.close(); + } + } + + /** + * Notifies any child streams of the read completion. + */ + @Override + public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { + processPendingReadCompleteQueue(); + ctx.fireChannelReadComplete(); + } + + private void processPendingReadCompleteQueue() { + parentReadInProgress = true; + // If we have many child channel we can optimize for the case when multiple call flush() in + // channelReadComplete(...) callbacks and only do it once as otherwise we will end-up with multiple + // write calls on the socket which is expensive. + AbstractHttp2StreamChannel childChannel = readCompletePendingQueue.poll(); + if (childChannel != null) { + try { + do { + childChannel.fireChildReadComplete(); + childChannel = readCompletePendingQueue.poll(); + } while (childChannel != null); + } finally { + parentReadInProgress = false; + readCompletePendingQueue.clear(); + ctx.flush(); + } + } else { + parentReadInProgress = false; + } + } + + private final class Http2MultiplexHandlerStreamChannel extends AbstractHttp2StreamChannel { + + Http2MultiplexHandlerStreamChannel(DefaultHttp2FrameStream stream, ChannelHandler inboundHandler) { + super(stream, ++idCount, inboundHandler); + } + + @Override + protected boolean isParentReadInProgress() { + return parentReadInProgress; + } + + @Override + protected void addChannelToReadCompletePendingQueue() { + // If there is no space left in the queue, just keep on processing everything that is already + // stored there and try again. + while (!readCompletePendingQueue.offer(this)) { + processPendingReadCompleteQueue(); + } + } + + @Override + protected ChannelHandlerContext parentContext() { + return ctx; + } + } +} + diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2PromisedRequestVerifier.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2PromisedRequestVerifier.java index 43b8f07..fc8e65c 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2PromisedRequestVerifier.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2PromisedRequestVerifier.java @@ -19,7 +19,7 @@ import io.netty.util.internal.UnstableApi; /** * Provides an extensibility point for users to define the validity of push requests. - * @see <a href="https://tools.ietf.org/html/draft-ietf-httpbis-http2-17#section-8.2">[RFC http2], Section 8.2</a>. + * @see <a href="https://tools.ietf.org/html/rfc7540#section-8.2">[RFC 7540], Section 8.2</a>. */ @UnstableApi public interface Http2PromisedRequestVerifier { @@ -29,7 +29,7 @@ public interface Http2PromisedRequestVerifier { * @param headers The headers to be verified. * @return {@code true} if the {@code ctx} is authoritative for the {@code headers}, {@code false} otherwise. * @see - * <a href="https://tools.ietf.org/html/draft-ietf-httpbis-http2-17#section-10.1">[RFC http2], Section 10.1</a>. + * <a href="https://tools.ietf.org/html/rfc7540#section-10.1">[RFC 7540], Section 10.1</a>. */ boolean isAuthoritative(ChannelHandlerContext ctx, Http2Headers headers); diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2SecurityUtil.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2SecurityUtil.java index 6c08fee..897c771 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2SecurityUtil.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2SecurityUtil.java @@ -33,7 +33,7 @@ public final class Http2SecurityUtil { * Ciphers</a> and <a * href="https://wiki.mozilla.org/Security/Server_Side_TLS#Modern_compatibility">Mozilla Modern Cipher * Suites</a> in accordance with the <a - * href="https://tools.ietf.org/html/draft-ietf-httpbis-http2-16#section-9.2.2">HTTP/2 Specification</a>. + * href="https://tools.ietf.org/html/rfc7540#section-9.2.2">HTTP/2 Specification</a>. * * According to the <a href="http://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html"> * JSSE documentation</a> "the names mentioned in the TLS RFCs prefixed with TLS_ are functionally equivalent diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2ServerUpgradeCodec.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2ServerUpgradeCodec.java index f61dae3..84911ec 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2ServerUpgradeCodec.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2ServerUpgradeCodec.java @@ -17,7 +17,6 @@ package io.netty.handler.codec.http2; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufUtil; import io.netty.channel.ChannelHandler; -import io.netty.channel.ChannelHandlerAdapter; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.base64.Base64; import io.netty.handler.codec.http.FullHttpRequest; @@ -38,7 +37,6 @@ import static io.netty.handler.codec.http2.Http2CodecUtil.FRAME_HEADER_LENGTH; import static io.netty.handler.codec.http2.Http2CodecUtil.HTTP_UPGRADE_SETTINGS_HEADER; import static io.netty.handler.codec.http2.Http2CodecUtil.writeFrameHeader; import static io.netty.handler.codec.http2.Http2FrameTypes.SETTINGS; -import static io.netty.util.internal.ObjectUtil.checkNotNull; /** * Server-side codec for performing a cleartext upgrade from HTTP/1.x to HTTP/2. @@ -148,19 +146,19 @@ public class Http2ServerUpgradeCodec implements HttpServerUpgradeHandler.Upgrade try { // Add the HTTP/2 connection handler to the pipeline immediately following the current handler. ctx.pipeline().addAfter(ctx.name(), handlerName, connectionHandler); - connectionHandler.onHttpServerUpgrade(settings); + // Add also all extra handlers as these may handle events / messages produced by the connectionHandler. + // See https://github.com/netty/netty/issues/9314 + if (handlers != null) { + final String name = ctx.pipeline().context(connectionHandler).name(); + for (int i = handlers.length - 1; i >= 0; i--) { + ctx.pipeline().addAfter(name, null, handlers[i]); + } + } + connectionHandler.onHttpServerUpgrade(settings); } catch (Http2Exception e) { ctx.fireExceptionCaught(e); ctx.close(); - return; - } - - if (handlers != null) { - final String name = ctx.pipeline().context(connectionHandler).name(); - for (int i = handlers.length - 1; i >= 0; i--) { - ctx.pipeline().addAfter(name, null, handlers[i]); - } } } diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2SettingsAckFrame.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2SettingsAckFrame.java new file mode 100644 index 0000000..fc46ce5 --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2SettingsAckFrame.java @@ -0,0 +1,29 @@ +/* + * Copyright 2019 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.codec.http2; + +/** + * An ack for a previously received {@link Http2SettingsFrame}. + * <p> + * The <a href="https://tools.ietf.org/html/rfc7540#section-6.5">HTTP/2 protocol</a> enforces that ACKs are applied in + * order, so this ACK will apply to the earliest received and not yet ACKed {@link Http2SettingsFrame} frame. + */ +public interface Http2SettingsAckFrame extends Http2Frame { + Http2SettingsAckFrame INSTANCE = new DefaultHttp2SettingsAckFrame(); + + @Override + String name(); +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2SettingsReceivedConsumer.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2SettingsReceivedConsumer.java new file mode 100644 index 0000000..97dfd39 --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2SettingsReceivedConsumer.java @@ -0,0 +1,25 @@ +/* + * Copyright 2019 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.codec.http2; + +/** + * Provides a Consumer like interface to consume remote settings received but not yet ACKed. + */ +public interface Http2SettingsReceivedConsumer { + /** + * Consume the most recently received but not yet ACKed settings. + */ + void consumeReceivedSettings(Http2Settings settings); +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2StreamChannelBootstrap.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2StreamChannelBootstrap.java index 6e9b797..f3007ff 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2StreamChannelBootstrap.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2StreamChannelBootstrap.java @@ -35,16 +35,26 @@ import io.netty.util.internal.logging.InternalLoggerFactory; import java.nio.channels.ClosedChannelException; import java.util.LinkedHashMap; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; @UnstableApi public final class Http2StreamChannelBootstrap { private static final InternalLogger logger = InternalLoggerFactory.getInstance(Http2StreamChannelBootstrap.class); + @SuppressWarnings("unchecked") + private static final Map.Entry<ChannelOption<?>, Object>[] EMPTY_OPTION_ARRAY = new Map.Entry[0]; + @SuppressWarnings("unchecked") + private static final Map.Entry<AttributeKey<?>, Object>[] EMPTY_ATTRIBUTE_ARRAY = new Map.Entry[0]; + // The order in which ChannelOptions are applied is important they may depend on each other for validation + // purposes. private final Map<ChannelOption<?>, Object> options = new LinkedHashMap<ChannelOption<?>, Object>(); - private final Map<AttributeKey<?>, Object> attrs = new LinkedHashMap<AttributeKey<?>, Object>(); + private final Map<AttributeKey<?>, Object> attrs = new ConcurrentHashMap<AttributeKey<?>, Object>(); private final Channel channel; private volatile ChannelHandler handler; + // Cache the ChannelHandlerContext to speed up open(...) operations. + private volatile ChannelHandlerContext multiplexCtx; + public Http2StreamChannelBootstrap(Channel channel) { this.channel = ObjectUtil.checkNotNull(channel, "channel"); } @@ -55,15 +65,12 @@ public final class Http2StreamChannelBootstrap { */ @SuppressWarnings("unchecked") public <T> Http2StreamChannelBootstrap option(ChannelOption<T> option, T value) { - if (option == null) { - throw new NullPointerException("option"); - } - if (value == null) { - synchronized (options) { + ObjectUtil.checkNotNull(option, "option"); + + synchronized (options) { + if (value == null) { options.remove(option); - } - } else { - synchronized (options) { + } else { options.put(option, value); } } @@ -76,17 +83,11 @@ public final class Http2StreamChannelBootstrap { */ @SuppressWarnings("unchecked") public <T> Http2StreamChannelBootstrap attr(AttributeKey<T> key, T value) { - if (key == null) { - throw new NullPointerException("key"); - } + ObjectUtil.checkNotNull(key, "key"); if (value == null) { - synchronized (attrs) { - attrs.remove(key); - } + attrs.remove(key); } else { - synchronized (attrs) { - attrs.put(key, value); - } + attrs.put(key, value); } return this; } @@ -94,44 +95,84 @@ public final class Http2StreamChannelBootstrap { /** * the {@link ChannelHandler} to use for serving the requests. */ - @SuppressWarnings("unchecked") public Http2StreamChannelBootstrap handler(ChannelHandler handler) { this.handler = ObjectUtil.checkNotNull(handler, "handler"); return this; } + /** + * Open a new {@link Http2StreamChannel} to use. + * @return the {@link Future} that will be notified once the channel was opened successfully or it failed. + */ public Future<Http2StreamChannel> open() { return open(channel.eventLoop().<Http2StreamChannel>newPromise()); } + /** + * Open a new {@link Http2StreamChannel} to use and notifies the given {@link Promise}. + * @return the {@link Future} that will be notified once the channel was opened successfully or it failed. + */ + @SuppressWarnings("deprecation") public Future<Http2StreamChannel> open(final Promise<Http2StreamChannel> promise) { - final ChannelHandlerContext ctx = channel.pipeline().context(Http2MultiplexCodec.class); - if (ctx == null) { - if (channel.isActive()) { - promise.setFailure(new IllegalStateException(StringUtil.simpleClassName(Http2MultiplexCodec.class) + - " must be in the ChannelPipeline of Channel " + channel)); - } else { - promise.setFailure(new ClosedChannelException()); - } - } else { + try { + ChannelHandlerContext ctx = findCtx(); EventExecutor executor = ctx.executor(); if (executor.inEventLoop()) { open0(ctx, promise); } else { + final ChannelHandlerContext finalCtx = ctx; executor.execute(new Runnable() { @Override public void run() { - open0(ctx, promise); + open0(finalCtx, promise); } }); } + } catch (Throwable cause) { + promise.setFailure(cause); } return promise; } + private ChannelHandlerContext findCtx() throws ClosedChannelException { + // First try to use cached context and if this not work lets try to lookup the context. + ChannelHandlerContext ctx = this.multiplexCtx; + if (ctx != null && !ctx.isRemoved()) { + return ctx; + } + ChannelPipeline pipeline = channel.pipeline(); + ctx = pipeline.context(Http2MultiplexCodec.class); + if (ctx == null) { + ctx = pipeline.context(Http2MultiplexHandler.class); + } + if (ctx == null) { + if (channel.isActive()) { + throw new IllegalStateException(StringUtil.simpleClassName(Http2MultiplexCodec.class) + " or " + + StringUtil.simpleClassName(Http2MultiplexHandler.class) + + " must be in the ChannelPipeline of Channel " + channel); + } else { + throw new ClosedChannelException(); + } + } + this.multiplexCtx = ctx; + return ctx; + } + + /** + * @deprecated should not be used directly. Use {@link #open()} or {@link #open(Promise)} + */ + @Deprecated public void open0(ChannelHandlerContext ctx, final Promise<Http2StreamChannel> promise) { assert ctx.executor().inEventLoop(); - final Http2StreamChannel streamChannel = ((Http2MultiplexCodec) ctx.handler()).newOutboundStream(); + if (!promise.setUncancellable()) { + return; + } + final Http2StreamChannel streamChannel; + if (ctx.handler() instanceof Http2MultiplexCodec) { + streamChannel = ((Http2MultiplexCodec) ctx.handler()).newOutboundStream(); + } else { + streamChannel = ((Http2MultiplexHandler) ctx.handler()).newOutboundStream(); + } try { init(streamChannel); } catch (Exception e) { @@ -143,7 +184,7 @@ public final class Http2StreamChannelBootstrap { ChannelFuture future = ctx.channel().eventLoop().register(streamChannel); future.addListener(new ChannelFutureListener() { @Override - public void operationComplete(ChannelFuture future) throws Exception { + public void operationComplete(ChannelFuture future) { if (future.isSuccess()) { promise.setSuccess(streamChannel); } else if (future.isCancelled()) { @@ -161,36 +202,34 @@ public final class Http2StreamChannelBootstrap { }); } - @SuppressWarnings("unchecked") - private void init(Channel channel) throws Exception { + private void init(Channel channel) { ChannelPipeline p = channel.pipeline(); ChannelHandler handler = this.handler; if (handler != null) { p.addLast(handler); } + final Map.Entry<ChannelOption<?>, Object> [] optionArray; synchronized (options) { - setChannelOptions(channel, options, logger); + optionArray = options.entrySet().toArray(EMPTY_OPTION_ARRAY); } - synchronized (attrs) { - for (Map.Entry<AttributeKey<?>, Object> e: attrs.entrySet()) { - channel.attr((AttributeKey<Object>) e.getKey()).set(e.getValue()); - } - } + setChannelOptions(channel, optionArray); + setAttributes(channel, attrs.entrySet().toArray(EMPTY_ATTRIBUTE_ARRAY)); } private static void setChannelOptions( - Channel channel, Map<ChannelOption<?>, Object> options, InternalLogger logger) { - for (Map.Entry<ChannelOption<?>, Object> e: options.entrySet()) { - setChannelOption(channel, e.getKey(), e.getValue(), logger); + Channel channel, Map.Entry<ChannelOption<?>, Object>[] options) { + for (Map.Entry<ChannelOption<?>, Object> e: options) { + setChannelOption(channel, e.getKey(), e.getValue()); } } - @SuppressWarnings("unchecked") private static void setChannelOption( - Channel channel, ChannelOption<?> option, Object value, InternalLogger logger) { + Channel channel, ChannelOption<?> option, Object value) { try { - if (!channel.config().setOption((ChannelOption<Object>) option, value)) { + @SuppressWarnings("unchecked") + ChannelOption<Object> opt = (ChannelOption<Object>) option; + if (!channel.config().setOption(opt, value)) { logger.warn("Unknown channel option '{}' for channel '{}'", option, channel); } } catch (Throwable t) { @@ -198,4 +237,13 @@ public final class Http2StreamChannelBootstrap { "Failed to set channel option '{}' with value '{}' for channel '{}'", option, value, channel, t); } } + + private static void setAttributes( + Channel channel, Map.Entry<AttributeKey<?>, Object>[] options) { + for (Map.Entry<AttributeKey<?>, Object> e: options) { + @SuppressWarnings("unchecked") + AttributeKey<Object> key = (AttributeKey<Object>) e.getKey(); + channel.attr(key).set(e.getValue()); + } + } } diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/HttpConversionUtil.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/HttpConversionUtil.java index 5f808db..8ffbd28 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/HttpConversionUtil.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/HttpConversionUtil.java @@ -33,6 +33,7 @@ import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.HttpUtil; import io.netty.handler.codec.http.HttpVersion; import io.netty.util.AsciiString; +import io.netty.util.internal.InternalThreadLocalMap; import io.netty.util.internal.UnstableApi; import java.net.URI; @@ -91,24 +92,24 @@ public final class HttpConversionUtil { /** * This will be the method used for {@link HttpRequest} objects generated out of the HTTP message flow defined in <a - * href="http://tools.ietf.org/html/draft-ietf-httpbis-http2-16#section-8.1.">HTTP/2 Spec Message Flow</a> + * href="https://tools.ietf.org/html/rfc7540#section-8.1">[RFC 7540], Section 8.1</a> */ public static final HttpMethod OUT_OF_MESSAGE_SEQUENCE_METHOD = HttpMethod.OPTIONS; /** * This will be the path used for {@link HttpRequest} objects generated out of the HTTP message flow defined in <a - * href="http://tools.ietf.org/html/draft-ietf-httpbis-http2-16#section-8.1.">HTTP/2 Spec Message Flow</a> + * href="https://tools.ietf.org/html/rfc7540#section-8.1">[RFC 7540], Section 8.1</a> */ public static final String OUT_OF_MESSAGE_SEQUENCE_PATH = ""; /** * This will be the status code used for {@link HttpResponse} objects generated out of the HTTP message flow defined - * in <a href="http://tools.ietf.org/html/draft-ietf-httpbis-http2-16#section-8.1.">HTTP/2 Spec Message Flow</a> + * in <a href="https://tools.ietf.org/html/rfc7540#section-8.1">[RFC 7540], Section 8.1</a> */ public static final HttpResponseStatus OUT_OF_MESSAGE_SEQUENCE_RETURN_CODE = HttpResponseStatus.OK; /** - * <a href="https://tools.ietf.org/html/rfc7540#section-8.1.2.3">rfc7540, 8.1.2.3</a> states the path must not + * <a href="https://tools.ietf.org/html/rfc7540#section-8.1.2.3">[RFC 7540], 8.1.2.3</a> states the path must not * be empty, and instead should be {@code /}. */ private static final AsciiString EMPTY_REQUEST_PATH = AsciiString.cached("/"); @@ -121,28 +122,28 @@ public final class HttpConversionUtil { */ public enum ExtensionHeaderNames { /** - * HTTP extension header which will identify the stream id from the HTTP/2 event(s) responsible for generating a - * {@code HttpObject} + * HTTP extension header which will identify the stream id from the HTTP/2 event(s) responsible for + * generating an {@code HttpObject} * <p> * {@code "x-http2-stream-id"} */ STREAM_ID("x-http2-stream-id"), /** * HTTP extension header which will identify the scheme pseudo header from the HTTP/2 event(s) responsible for - * generating a {@code HttpObject} + * generating an {@code HttpObject} * <p> * {@code "x-http2-scheme"} */ SCHEME("x-http2-scheme"), /** * HTTP extension header which will identify the path pseudo header from the HTTP/2 event(s) responsible for - * generating a {@code HttpObject} + * generating an {@code HttpObject} * <p> * {@code "x-http2-path"} */ PATH("x-http2-path"), /** - * HTTP extension header which will identify the stream id used to create this stream in a HTTP/2 push promise + * HTTP extension header which will identify the stream id used to create this stream in an HTTP/2 push promise * frame * <p> * {@code "x-http2-stream-promise-id"} @@ -157,7 +158,7 @@ public final class HttpConversionUtil { STREAM_DEPENDENCY_ID("x-http2-stream-dependency-id"), /** * HTTP extension header which will identify the weight (if non-default and the priority is not on the default - * stream) of the associated HTTP/2 stream responsible responsible for generating a {@code HttpObject} + * stream) of the associated HTTP/2 stream responsible responsible for generating an {@code HttpObject} * <p> * {@code "x-http2-stream-weight"} */ @@ -360,9 +361,7 @@ public final class HttpConversionUtil { HttpVersion httpVersion, boolean isTrailer, boolean isRequest) throws Http2Exception { Http2ToHttpHeaderTranslator translator = new Http2ToHttpHeaderTranslator(streamId, outputHeaders, isRequest); try { - for (Entry<CharSequence, CharSequence> entry : inputHeaders) { - translator.translate(entry); - } + translator.translateHeaders(inputHeaders); } catch (Http2Exception ex) { throw ex; } catch (Throwable t) { @@ -520,7 +519,7 @@ public final class HttpConversionUtil { } /** - * Generate a HTTP/2 {code :path} from a URI in accordance with + * Generate an HTTP/2 {code :path} from a URI in accordance with * <a href="https://tools.ietf.org/html/rfc7230#section-5.3">rfc7230, 5.3</a>. */ private static AsciiString toHttp2Path(URI uri) { @@ -620,29 +619,43 @@ public final class HttpConversionUtil { translations = request ? REQUEST_HEADER_TRANSLATIONS : RESPONSE_HEADER_TRANSLATIONS; } - public void translate(Entry<CharSequence, CharSequence> entry) throws Http2Exception { - final CharSequence name = entry.getKey(); - final CharSequence value = entry.getValue(); - AsciiString translatedName = translations.get(name); - if (translatedName != null) { - output.add(translatedName, AsciiString.of(value)); - } else if (!Http2Headers.PseudoHeaderName.isPseudoHeader(name)) { - // https://tools.ietf.org/html/rfc7540#section-8.1.2.3 - // All headers that start with ':' are only valid in HTTP/2 context - if (name.length() == 0 || name.charAt(0) == ':') { - throw streamError(streamId, PROTOCOL_ERROR, - "Invalid HTTP/2 header '%s' encountered in translation to HTTP/1.x", name); - } - if (COOKIE.equals(name)) { - // combine the cookie values into 1 header entry. - // https://tools.ietf.org/html/rfc7540#section-8.1.2.5 - String existingCookie = output.get(COOKIE); - output.set(COOKIE, - (existingCookie != null) ? (existingCookie + "; " + value) : value); - } else { - output.add(name, value); + public void translateHeaders(Iterable<Entry<CharSequence, CharSequence>> inputHeaders) throws Http2Exception { + // lazily created as needed + StringBuilder cookies = null; + + for (Entry<CharSequence, CharSequence> entry : inputHeaders) { + final CharSequence name = entry.getKey(); + final CharSequence value = entry.getValue(); + AsciiString translatedName = translations.get(name); + if (translatedName != null) { + output.add(translatedName, AsciiString.of(value)); + } else if (!Http2Headers.PseudoHeaderName.isPseudoHeader(name)) { + // https://tools.ietf.org/html/rfc7540#section-8.1.2.3 + // All headers that start with ':' are only valid in HTTP/2 context + if (name.length() == 0 || name.charAt(0) == ':') { + throw streamError(streamId, PROTOCOL_ERROR, + "Invalid HTTP/2 header '%s' encountered in translation to HTTP/1.x", name); + } + if (COOKIE.equals(name)) { + // combine the cookie values into 1 header entry. + // https://tools.ietf.org/html/rfc7540#section-8.1.2.5 + if (cookies == null) { + cookies = InternalThreadLocalMap.get().stringBuilder(); + } else if (cookies.length() > 0) { + cookies.append("; "); + } + cookies.append(value); + } else { + output.add(name, value); + } } } + if (cookies != null) { + output.add(COOKIE, cookies.toString()); + } + } + + private void translateHeader(Entry<CharSequence, CharSequence> entry) throws Http2Exception { } } } diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/HttpToHttp2ConnectionHandler.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/HttpToHttp2ConnectionHandler.java index bdec231..84998c2 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/HttpToHttp2ConnectionHandler.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/HttpToHttp2ConnectionHandler.java @@ -45,6 +45,13 @@ public class HttpToHttp2ConnectionHandler extends Http2ConnectionHandler { this.validateHeaders = validateHeaders; } + protected HttpToHttp2ConnectionHandler(Http2ConnectionDecoder decoder, Http2ConnectionEncoder encoder, + Http2Settings initialSettings, boolean validateHeaders, + boolean decoupleCloseAndGoAway) { + super(decoder, encoder, initialSettings, decoupleCloseAndGoAway); + this.validateHeaders = validateHeaders; + } + /** * Get the next stream id either from the {@link HttpHeaders} object or HTTP/2 codec * @@ -103,8 +110,8 @@ public class HttpToHttp2ConnectionHandler extends Http2ConnectionHandler { // Write the data final ByteBuf content = ((HttpContent) msg).content(); endStream = isLastContent && trailers.isEmpty(); - release = false; encoder.writeData(ctx, currentStreamId, content, 0, endStream, promiseAggregator.newPromise()); + release = false; if (!trailers.isEmpty()) { // Write trailing headers. diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/HttpToHttp2ConnectionHandlerBuilder.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/HttpToHttp2ConnectionHandlerBuilder.java index 877c958..8d1df17 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/HttpToHttp2ConnectionHandlerBuilder.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/HttpToHttp2ConnectionHandlerBuilder.java @@ -80,10 +80,16 @@ public final class HttpToHttp2ConnectionHandlerBuilder extends } @Override + @Deprecated public HttpToHttp2ConnectionHandlerBuilder initialHuffmanDecodeCapacity(int initialHuffmanDecodeCapacity) { return super.initialHuffmanDecodeCapacity(initialHuffmanDecodeCapacity); } + @Override + public HttpToHttp2ConnectionHandlerBuilder decoupleCloseAndGoAway(boolean decoupleCloseAndGoAway) { + return super.decoupleCloseAndGoAway(decoupleCloseAndGoAway); + } + @Override public HttpToHttp2ConnectionHandler build() { return super.build(); @@ -92,6 +98,7 @@ public final class HttpToHttp2ConnectionHandlerBuilder extends @Override protected HttpToHttp2ConnectionHandler build(Http2ConnectionDecoder decoder, Http2ConnectionEncoder encoder, Http2Settings initialSettings) { - return new HttpToHttp2ConnectionHandler(decoder, encoder, initialSettings, isValidateHeaders()); + return new HttpToHttp2ConnectionHandler(decoder, encoder, initialSettings, isValidateHeaders(), + decoupleCloseAndGoAway()); } } diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/InboundHttp2ToHttpAdapter.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/InboundHttp2ToHttpAdapter.java index b5adf25..d8d5768 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/InboundHttp2ToHttpAdapter.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/InboundHttp2ToHttpAdapter.java @@ -16,7 +16,6 @@ package io.netty.handler.codec.http2; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; -import io.netty.buffer.Unpooled; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.FullHttpMessage; import io.netty.handler.codec.http.FullHttpRequest; @@ -34,7 +33,7 @@ import static io.netty.util.internal.ObjectUtil.checkNotNull; /** * This adapter provides just header/data events from the HTTP message flow defined - * here <a href="http://tools.ietf.org/html/draft-ietf-httpbis-http2-16#section-8.1.">HTTP/2 Spec Message Flow</a>. + * in <a href="https://tools.ietf.org/html/rfc7540#section-8.1">[RFC 7540], Section 8.1</a>. * <p> * See {@link HttpToHttp2ConnectionHandler} to get translation from HTTP/1.x objects to HTTP/2 frames for writes. */ @@ -53,9 +52,9 @@ public class InboundHttp2ToHttpAdapter extends Http2EventAdapter { } @Override - public FullHttpMessage copyIfNeeded(FullHttpMessage msg) { + public FullHttpMessage copyIfNeeded(ByteBufAllocator allocator, FullHttpMessage msg) { if (msg instanceof FullHttpRequest) { - FullHttpRequest copy = ((FullHttpRequest) msg).replace(Unpooled.buffer(0)); + FullHttpRequest copy = ((FullHttpRequest) msg).replace(allocator.buffer(0)); copy.headers().remove(HttpHeaderNames.EXPECT); return copy; } @@ -73,11 +72,10 @@ public class InboundHttp2ToHttpAdapter extends Http2EventAdapter { protected InboundHttp2ToHttpAdapter(Http2Connection connection, int maxContentLength, boolean validateHttpHeaders, boolean propagateSettings) { - checkNotNull(connection, "connection"); if (maxContentLength <= 0) { throw new IllegalArgumentException("maxContentLength: " + maxContentLength + " (expected: > 0)"); } - this.connection = connection; + this.connection = checkNotNull(connection, "connection"); this.maxContentLength = maxContentLength; this.validateHttpHeaders = validateHttpHeaders; this.propagateSettings = propagateSettings; @@ -200,7 +198,7 @@ public class InboundHttp2ToHttpAdapter extends Http2EventAdapter { if (sendDetector.mustSendImmediately(msg)) { // Copy the message (if necessary) before sending. The content is not expected to be copied (or used) in // this operation but just in case it is used do the copy before sending and the resource may be released - final FullHttpMessage copy = endOfStream ? null : sendDetector.copyIfNeeded(msg); + final FullHttpMessage copy = endOfStream ? null : sendDetector.copyIfNeeded(ctx.alloc(), msg); fireChannelRead(ctx, msg, release, stream); return copy; } @@ -354,9 +352,10 @@ public class InboundHttp2ToHttpAdapter extends Http2EventAdapter { * with a 'Expect: 100-continue' header. The message will be sent immediately, * and the data will be queued and sent at the end of the stream. * + * @param allocator The {@link ByteBufAllocator} that can be used to allocate * @param msg The message which has just been sent due to {@link #mustSendImmediately(FullHttpMessage)} * @return A modified copy of the {@code msg} or {@code null} if a copy is not needed. */ - FullHttpMessage copyIfNeeded(FullHttpMessage msg); + FullHttpMessage copyIfNeeded(ByteBufAllocator allocator, FullHttpMessage msg); } } diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/MaxCapacityQueue.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/MaxCapacityQueue.java new file mode 100644 index 0000000..7f72511 --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/MaxCapacityQueue.java @@ -0,0 +1,129 @@ +/* + * Copyright 2019 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.codec.http2; + +import java.util.Collection; +import java.util.Iterator; +import java.util.Queue; + +final class MaxCapacityQueue<E> implements Queue<E> { + private final Queue<E> queue; + private final int maxCapacity; + + MaxCapacityQueue(Queue<E> queue, int maxCapacity) { + this.queue = queue; + this.maxCapacity = maxCapacity; + } + + @Override + public boolean add(E element) { + if (offer(element)) { + return true; + } + throw new IllegalStateException(); + } + + @Override + public boolean offer(E element) { + if (maxCapacity <= queue.size()) { + return false; + } + return queue.offer(element); + } + + @Override + public E remove() { + return queue.remove(); + } + + @Override + public E poll() { + return queue.poll(); + } + + @Override + public E element() { + return queue.element(); + } + + @Override + public E peek() { + return queue.peek(); + } + + @Override + public int size() { + return queue.size(); + } + + @Override + public boolean isEmpty() { + return queue.isEmpty(); + } + + @Override + public boolean contains(Object o) { + return queue.contains(o); + } + + @Override + public Iterator<E> iterator() { + return queue.iterator(); + } + + @Override + public Object[] toArray() { + return queue.toArray(); + } + + @Override + public <T> T[] toArray(T[] a) { + return queue.toArray(a); + } + + @Override + public boolean remove(Object o) { + return queue.remove(o); + } + + @Override + public boolean containsAll(Collection<?> c) { + return queue.containsAll(c); + } + + @Override + public boolean addAll(Collection<? extends E> c) { + if (maxCapacity >= size() + c.size()) { + return queue.addAll(c); + } + throw new IllegalStateException(); + } + + @Override + public boolean removeAll(Collection<?> c) { + return queue.removeAll(c); + } + + @Override + public boolean retainAll(Collection<?> c) { + return queue.retainAll(c); + } + + @Override + public void clear() { + queue.clear(); + } +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/UniformStreamByteDistributor.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/UniformStreamByteDistributor.java index 421d348..c7e5791 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/UniformStreamByteDistributor.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/UniformStreamByteDistributor.java @@ -24,6 +24,7 @@ import static io.netty.handler.codec.http2.Http2CodecUtil.streamableBytes; import static io.netty.handler.codec.http2.Http2Error.INTERNAL_ERROR; import static io.netty.handler.codec.http2.Http2Exception.connectionError; import static io.netty.util.internal.ObjectUtil.checkNotNull; +import static io.netty.util.internal.ObjectUtil.checkPositive; import static java.lang.Math.max; import static java.lang.Math.min; @@ -72,9 +73,7 @@ public final class UniformStreamByteDistributor implements StreamByteDistributor * Must be > 0. */ public void minAllocationChunk(int minAllocationChunk) { - if (minAllocationChunk <= 0) { - throw new IllegalArgumentException("minAllocationChunk must be > 0"); - } + checkPositive(minAllocationChunk, "minAllocationChunk"); this.minAllocationChunk = minAllocationChunk; } diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/WeightedFairQueueByteDistributor.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/WeightedFairQueueByteDistributor.java index b7c2cdd..b0f3a25 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/WeightedFairQueueByteDistributor.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/WeightedFairQueueByteDistributor.java @@ -37,6 +37,8 @@ import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_PRIORITY_WEIGH import static io.netty.handler.codec.http2.Http2CodecUtil.streamableBytes; import static io.netty.handler.codec.http2.Http2Error.INTERNAL_ERROR; import static io.netty.handler.codec.http2.Http2Exception.connectionError; +import static io.netty.util.internal.ObjectUtil.checkPositive; +import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero; import static java.lang.Integer.MAX_VALUE; import static java.lang.Math.max; import static java.lang.Math.min; @@ -96,9 +98,8 @@ public final class WeightedFairQueueByteDistributor implements StreamByteDistrib } public WeightedFairQueueByteDistributor(Http2Connection connection, int maxStateOnlySize) { - if (maxStateOnlySize < 0) { - throw new IllegalArgumentException("maxStateOnlySize: " + maxStateOnlySize + " (expected: >0)"); - } else if (maxStateOnlySize == 0) { + checkPositiveOrZero(maxStateOnlySize, "maxStateOnlySize"); + if (maxStateOnlySize == 0) { stateOnlyMap = IntCollections.emptyMap(); stateOnlyRemovalQueue = EmptyPriorityQueue.instance(); } else { @@ -281,9 +282,7 @@ public final class WeightedFairQueueByteDistributor implements StreamByteDistrib * @param allocationQuantum the amount of bytes that will be allocated to each stream. Must be > 0. */ public void allocationQuantum(int allocationQuantum) { - if (allocationQuantum <= 0) { - throw new IllegalArgumentException("allocationQuantum must be > 0"); - } + checkPositive(allocationQuantum, "allocationQuantum"); this.allocationQuantum = allocationQuantum; } diff --git a/codec-http2/src/main/resources/META-INF/native-image/io.netty/codec-http2/native-image.properties b/codec-http2/src/main/resources/META-INF/native-image/io.netty/codec-http2/native-image.properties new file mode 100644 index 0000000..ed5e345 --- /dev/null +++ b/codec-http2/src/main/resources/META-INF/native-image/io.netty/codec-http2/native-image.properties @@ -0,0 +1,16 @@ +# Copyright 2019 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. + +Args = --initialize-at-build-time=io.netty \ + --initialize-at-run-time=io.netty.handler.codec.http2.Http2CodecUtil,io.netty.handler.codec.http2.Http2ClientUpgradeCodec,io.netty.handler.codec.http2.Http2ConnectionHandler,io.netty.handler.codec.http2.DefaultHttp2FrameWriter diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/CleartextHttp2ServerUpgradeHandlerTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/CleartextHttp2ServerUpgradeHandlerTest.java index a544054..129f62d 100644 --- a/codec-http2/src/test/java/io/netty/handler/codec/http2/CleartextHttp2ServerUpgradeHandlerTest.java +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/CleartextHttp2ServerUpgradeHandlerTest.java @@ -42,8 +42,15 @@ import org.junit.Test; import java.util.ArrayList; import java.util.List; -import static org.junit.Assert.*; -import static org.mockito.Mockito.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; /** * Tests for {@link CleartextHttp2ServerUpgradeHandler} @@ -112,47 +119,35 @@ public class CleartextHttp2ServerUpgradeHandlerTest { @Test public void upgrade() throws Exception { - setUpServerChannel(); - String upgradeString = "GET / HTTP/1.1\r\n" + - "Host: example.com\r\n" + - "Connection: Upgrade, HTTP2-Settings\r\n" + - "Upgrade: h2c\r\n" + - "HTTP2-Settings: AAMAAABkAAQAAP__\r\n\r\n"; - ByteBuf upgrade = Unpooled.copiedBuffer(upgradeString, CharsetUtil.US_ASCII); - - assertFalse(channel.writeInbound(upgrade)); - - assertEquals(1, userEvents.size()); - - Object userEvent = userEvents.get(0); - assertTrue(userEvent instanceof UpgradeEvent); - assertEquals("h2c", ((UpgradeEvent) userEvent).protocol()); - ReferenceCountUtil.release(userEvent); - - assertEquals(100, http2ConnectionHandler.connection().local().maxActiveStreams()); - assertEquals(65535, http2ConnectionHandler.connection().local().flowController().initialWindowSize()); - - assertEquals(1, http2ConnectionHandler.connection().numActiveStreams()); - assertNotNull(http2ConnectionHandler.connection().stream(1)); - - Http2Stream stream = http2ConnectionHandler.connection().stream(1); - assertEquals(State.HALF_CLOSED_REMOTE, stream.state()); - assertFalse(stream.isHeadersSent()); - - String expectedHttpResponse = "HTTP/1.1 101 Switching Protocols\r\n" + - "connection: upgrade\r\n" + - "upgrade: h2c\r\n\r\n"; - ByteBuf responseBuffer = channel.readOutbound(); - assertEquals(expectedHttpResponse, responseBuffer.toString(CharsetUtil.UTF_8)); - responseBuffer.release(); + "Host: example.com\r\n" + + "Connection: Upgrade, HTTP2-Settings\r\n" + + "Upgrade: h2c\r\n" + + "HTTP2-Settings: AAMAAABkAAQAAP__\r\n\r\n"; + validateClearTextUpgrade(upgradeString); + } - // Check that the preface was send (a.k.a the settings frame) - ByteBuf settingsBuffer = channel.readOutbound(); - assertNotNull(settingsBuffer); - settingsBuffer.release(); + @Test + public void upgradeWithMultipleConnectionHeaders() { + String upgradeString = "GET / HTTP/1.1\r\n" + + "Host: example.com\r\n" + + "Connection: keep-alive\r\n" + + "Connection: Upgrade, HTTP2-Settings\r\n" + + "Upgrade: h2c\r\n" + + "HTTP2-Settings: AAMAAABkAAQAAP__\r\n\r\n"; + validateClearTextUpgrade(upgradeString); + } - assertNull(channel.readOutbound()); + @Test + public void requiredHeadersInSeparateConnectionHeaders() { + String upgradeString = "GET / HTTP/1.1\r\n" + + "Host: example.com\r\n" + + "Connection: keep-alive\r\n" + + "Connection: HTTP2-Settings\r\n" + + "Connection: Upgrade\r\n" + + "Upgrade: h2c\r\n" + + "HTTP2-Settings: AAMAAABkAAQAAP__\r\n\r\n"; + validateClearTextUpgrade(upgradeString); } @Test @@ -254,4 +249,43 @@ public class CleartextHttp2ServerUpgradeHandlerTest { private static Http2Settings expectedSettings() { return new Http2Settings().maxConcurrentStreams(100).initialWindowSize(65535); } + + private void validateClearTextUpgrade(String upgradeString) { + setUpServerChannel(); + + ByteBuf upgrade = Unpooled.copiedBuffer(upgradeString, CharsetUtil.US_ASCII); + + assertFalse(channel.writeInbound(upgrade)); + + assertEquals(1, userEvents.size()); + + Object userEvent = userEvents.get(0); + assertTrue(userEvent instanceof UpgradeEvent); + assertEquals("h2c", ((UpgradeEvent) userEvent).protocol()); + ReferenceCountUtil.release(userEvent); + + assertEquals(100, http2ConnectionHandler.connection().local().maxActiveStreams()); + assertEquals(65535, http2ConnectionHandler.connection().local().flowController().initialWindowSize()); + + assertEquals(1, http2ConnectionHandler.connection().numActiveStreams()); + assertNotNull(http2ConnectionHandler.connection().stream(1)); + + Http2Stream stream = http2ConnectionHandler.connection().stream(1); + assertEquals(State.HALF_CLOSED_REMOTE, stream.state()); + assertFalse(stream.isHeadersSent()); + + String expectedHttpResponse = "HTTP/1.1 101 Switching Protocols\r\n" + + "connection: upgrade\r\n" + + "upgrade: h2c\r\n\r\n"; + ByteBuf responseBuffer = channel.readOutbound(); + assertEquals(expectedHttpResponse, responseBuffer.toString(CharsetUtil.UTF_8)); + responseBuffer.release(); + + // Check that the preface was send (a.k.a the settings frame) + ByteBuf settingsBuffer = channel.readOutbound(); + assertNotNull(settingsBuffer); + settingsBuffer.release(); + + assertNull(channel.readOutbound()); + } } diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/DecoratingHttp2ConnectionEncoderTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/DecoratingHttp2ConnectionEncoderTest.java new file mode 100644 index 0000000..0eb9a73 --- /dev/null +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/DecoratingHttp2ConnectionEncoderTest.java @@ -0,0 +1,45 @@ +/* + * Copyright 2019 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.codec.http2; + +import org.junit.Test; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.times; + +public class DecoratingHttp2ConnectionEncoderTest { + + @Test(expected = IllegalStateException.class) + public void testConsumeReceivedSettingsThrows() { + Http2ConnectionEncoder encoder = mock(Http2ConnectionEncoder.class); + DecoratingHttp2ConnectionEncoder decoratingHttp2ConnectionEncoder = + new DecoratingHttp2ConnectionEncoder(encoder); + decoratingHttp2ConnectionEncoder.consumeReceivedSettings(Http2Settings.defaultSettings()); + } + + @Test + public void testConsumeReceivedSettingsDelegate() { + TestHttp2ConnectionEncoder encoder = mock(TestHttp2ConnectionEncoder.class); + DecoratingHttp2ConnectionEncoder decoratingHttp2ConnectionEncoder = + new DecoratingHttp2ConnectionEncoder(encoder); + + Http2Settings settings = Http2Settings.defaultSettings(); + decoratingHttp2ConnectionEncoder.consumeReceivedSettings(Http2Settings.defaultSettings()); + verify(encoder, times(1)).consumeReceivedSettings(eq(settings)); + } + + private interface TestHttp2ConnectionEncoder extends Http2ConnectionEncoder, Http2SettingsReceivedConsumer { } +} diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2ConnectionDecoderTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2ConnectionDecoderTest.java index 7e87d52..9eb66bc 100644 --- a/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2ConnectionDecoderTest.java +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2ConnectionDecoderTest.java @@ -42,7 +42,11 @@ import static io.netty.handler.codec.http2.Http2Stream.State.IDLE; import static io.netty.handler.codec.http2.Http2Stream.State.OPEN; import static io.netty.handler.codec.http2.Http2Stream.State.RESERVED_REMOTE; import static io.netty.util.CharsetUtil.UTF_8; + +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.not; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.Mockito.any; @@ -249,9 +253,9 @@ public class DefaultHttp2ConnectionDecoderTest { } } - @Test(expected = Http2Exception.class) + @Test(expected = Http2Exception.StreamException.class) public void dataReadForUnknownStreamShouldApplyFlowControlAndFail() throws Exception { - when(connection.streamMayHaveExisted(STREAM_ID)).thenReturn(false); + when(connection.streamMayHaveExisted(STREAM_ID)).thenReturn(true); when(connection.stream(STREAM_ID)).thenReturn(null); final ByteBuf data = dummyData(); int padding = 10; @@ -272,6 +276,32 @@ public class DefaultHttp2ConnectionDecoderTest { } } + @Test(expected = Http2Exception.class) + public void dataReadForUnknownStreamThatCouldntExistFail() throws Exception { + when(connection.streamMayHaveExisted(STREAM_ID)).thenReturn(false); + when(connection.stream(STREAM_ID)).thenReturn(null); + final ByteBuf data = dummyData(); + int padding = 10; + int processedBytes = data.readableBytes() + padding; + try { + decode().onDataRead(ctx, STREAM_ID, data, padding, true); + } catch (Http2Exception ex) { + assertThat(ex, not(instanceOf(Http2Exception.StreamException.class))); + throw ex; + } finally { + try { + verify(localFlow) + .receiveFlowControlledFrame(eq((Http2Stream) null), eq(data), eq(padding), eq(true)); + verify(localFlow).consumeBytes(eq((Http2Stream) null), eq(processedBytes)); + verify(localFlow).frameWriter(any(Http2FrameWriter.class)); + verifyNoMoreInteractions(localFlow); + verify(listener, never()).onDataRead(eq(ctx), anyInt(), any(ByteBuf.class), anyInt(), anyBoolean()); + } finally { + data.release(); + } + } + } + @Test public void dataReadForUnknownStreamShouldApplyFlowControl() throws Exception { when(connection.stream(STREAM_ID)).thenReturn(null); diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2ConnectionEncoderTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2ConnectionEncoderTest.java index 9ca7b1f..aa4ffc3 100644 --- a/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2ConnectionEncoderTest.java +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2ConnectionEncoderTest.java @@ -177,12 +177,26 @@ public class DefaultHttp2ConnectionEncoderTest { anyInt(), anyBoolean(), any(ChannelPromise.class))) .then(new Answer<ChannelFuture>() { @Override - public ChannelFuture answer(InvocationOnMock invocationOnMock) throws Throwable { - ChannelPromise promise = (ChannelPromise) invocationOnMock.getArguments()[8]; + public ChannelFuture answer(InvocationOnMock invocationOnMock) { + ChannelPromise promise = invocationOnMock.getArgument(8); if (streamClosed) { fail("Stream already closed"); } else { - streamClosed = (Boolean) invocationOnMock.getArguments()[5]; + streamClosed = invocationOnMock.getArgument(5); + } + return promise.setSuccess(); + } + }); + when(writer.writeHeaders(eq(ctx), anyInt(), any(Http2Headers.class), + anyInt(), anyBoolean(), any(ChannelPromise.class))) + .then(new Answer<ChannelFuture>() { + @Override + public ChannelFuture answer(InvocationOnMock invocationOnMock) { + ChannelPromise promise = invocationOnMock.getArgument(5); + if (streamClosed) { + fail("Stream already closed"); + } else { + streamClosed = invocationOnMock.getArgument(4); } return promise.setSuccess(); } @@ -335,12 +349,12 @@ public class DefaultHttp2ConnectionEncoderTest { @Test public void writeHeadersUsingVoidPromise() throws Exception { final Throwable cause = new RuntimeException("fake exception"); - when(writer.writeHeaders(eq(ctx), eq(STREAM_ID), any(Http2Headers.class), anyInt(), anyShort(), anyBoolean(), + when(writer.writeHeaders(eq(ctx), eq(STREAM_ID), any(Http2Headers.class), anyInt(), anyBoolean(), any(ChannelPromise.class))) .then(new Answer<ChannelFuture>() { @Override public ChannelFuture answer(InvocationOnMock invocationOnMock) throws Throwable { - ChannelPromise promise = invocationOnMock.getArgument(8); + ChannelPromise promise = invocationOnMock.getArgument(5); assertFalse(promise.isVoid()); return promise.setFailure(cause); } @@ -349,7 +363,7 @@ public class DefaultHttp2ConnectionEncoderTest { // END_STREAM flag, so that a listener is added to the future. encoder.writeHeaders(ctx, STREAM_ID, EmptyHttp2Headers.INSTANCE, 0, true, newVoidPromise(channel)); - verify(writer).writeHeaders(eq(ctx), eq(STREAM_ID), any(Http2Headers.class), anyInt(), anyShort(), anyBoolean(), + verify(writer).writeHeaders(eq(ctx), eq(STREAM_ID), any(Http2Headers.class), anyInt(), anyBoolean(), any(ChannelPromise.class)); // When using a void promise, the error should be propagated via the channel pipeline. verify(pipeline).fireExceptionCaught(cause); @@ -377,7 +391,7 @@ public class DefaultHttp2ConnectionEncoderTest { ChannelPromise promise = newPromise(); encoder.writeHeaders(ctx, streamId, EmptyHttp2Headers.INSTANCE, 0, false, promise); verify(writer).writeHeaders(eq(ctx), eq(streamId), eq(EmptyHttp2Headers.INSTANCE), eq(0), - eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(false), eq(promise)); + eq(false), eq(promise)); assertTrue(promise.isSuccess()); } @@ -390,8 +404,8 @@ public class DefaultHttp2ConnectionEncoderTest { ChannelPromise promise = newPromise(); encoder.writeHeaders(ctx, PUSH_STREAM_ID, EmptyHttp2Headers.INSTANCE, 0, false, promise); assertEquals(HALF_CLOSED_REMOTE, stream(PUSH_STREAM_ID).state()); - verify(writer).writeHeaders(eq(ctx), eq(PUSH_STREAM_ID), eq(EmptyHttp2Headers.INSTANCE), eq(0), - eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(false), eq(promise)); + verify(writer).writeHeaders(eq(ctx), eq(PUSH_STREAM_ID), eq(EmptyHttp2Headers.INSTANCE), + eq(0), eq(false), eq(promise)); } @Test @@ -406,8 +420,8 @@ public class DefaultHttp2ConnectionEncoderTest { assertTrue(future.isDone()); assertFalse(future.isSuccess()); - verify(writer, times(1)).writeHeaders(eq(ctx), eq(streamId), eq(EmptyHttp2Headers.INSTANCE), eq(0), - eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(false), eq(promise)); + verify(writer, times(1)).writeHeaders(eq(ctx), eq(streamId), eq(EmptyHttp2Headers.INSTANCE), + eq(0), eq(false), eq(promise)); } @Test @@ -425,8 +439,8 @@ public class DefaultHttp2ConnectionEncoderTest { assertTrue(future.isDone()); assertFalse(future.isSuccess()); - verify(writer, times(1)).writeHeaders(eq(ctx), eq(streamId), eq(EmptyHttp2Headers.INSTANCE), eq(0), - eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(false), eq(promise)); + verify(writer, times(1)).writeHeaders(eq(ctx), eq(streamId), eq(EmptyHttp2Headers.INSTANCE), + eq(0), eq(false), eq(promise)); } @Test @@ -452,10 +466,10 @@ public class DefaultHttp2ConnectionEncoderTest { assertTrue(future.isDone()); assertFalse(future.isSuccess()); - verify(writer, times(1)).writeHeaders(eq(ctx), eq(streamId), eq(EmptyHttp2Headers.INSTANCE), eq(0), - eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(false), eq(promise)); - verify(writer, times(1)).writeHeaders(eq(ctx), eq(streamId), eq(EmptyHttp2Headers.INSTANCE), eq(0), - eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(true), eq(promise2)); + verify(writer, times(1)).writeHeaders(eq(ctx), eq(streamId), eq(EmptyHttp2Headers.INSTANCE), + eq(0), eq(false), eq(promise)); + verify(writer, times(1)).writeHeaders(eq(ctx), eq(streamId), eq(EmptyHttp2Headers.INSTANCE), + eq(0), eq(true), eq(promise2)); } @Test @@ -493,13 +507,13 @@ public class DefaultHttp2ConnectionEncoderTest { assertTrue(future.isDone()); assertEquals(eos, future.isSuccess()); - verify(writer, times(infoHeaderCount)).writeHeaders(eq(ctx), eq(streamId), eq(infoHeaders), eq(0), - eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(false), any(ChannelPromise.class)); - verify(writer, times(1)).writeHeaders(eq(ctx), eq(streamId), eq(EmptyHttp2Headers.INSTANCE), eq(0), - eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(false), eq(promise2)); + verify(writer, times(infoHeaderCount)).writeHeaders(eq(ctx), eq(streamId), eq(infoHeaders), + eq(0), eq(false), any(ChannelPromise.class)); + verify(writer, times(1)).writeHeaders(eq(ctx), eq(streamId), eq(EmptyHttp2Headers.INSTANCE), + eq(0), eq(false), eq(promise2)); if (eos) { - verify(writer, times(1)).writeHeaders(eq(ctx), eq(streamId), eq(EmptyHttp2Headers.INSTANCE), eq(0), - eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(true), eq(promise3)); + verify(writer, times(1)).writeHeaders(eq(ctx), eq(streamId), eq(EmptyHttp2Headers.INSTANCE), + eq(0), eq(true), eq(promise3)); } } @@ -536,10 +550,10 @@ public class DefaultHttp2ConnectionEncoderTest { assertTrue(future.isDone()); assertFalse(future.isSuccess()); - verify(writer, times(1)).writeHeaders(eq(ctx), eq(streamId), eq(EmptyHttp2Headers.INSTANCE), eq(0), - eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(false), eq(promise)); - verify(writer, times(1)).writeHeaders(eq(ctx), eq(streamId), eq(EmptyHttp2Headers.INSTANCE), eq(0), - eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(true), eq(promise2)); + verify(writer, times(1)).writeHeaders(eq(ctx), eq(streamId), eq(EmptyHttp2Headers.INSTANCE), + eq(0), eq(false), eq(promise)); + verify(writer, times(1)).writeHeaders(eq(ctx), eq(streamId), eq(EmptyHttp2Headers.INSTANCE), + eq(0), eq(true), eq(promise2)); } @Test @@ -581,13 +595,13 @@ public class DefaultHttp2ConnectionEncoderTest { assertTrue(future.isDone()); assertEquals(eos, future.isSuccess()); - verify(writer, times(infoHeaderCount)).writeHeaders(eq(ctx), eq(streamId), eq(infoHeaders), eq(0), - eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(false), any(ChannelPromise.class)); - verify(writer, times(1)).writeHeaders(eq(ctx), eq(streamId), eq(EmptyHttp2Headers.INSTANCE), eq(0), - eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(false), eq(promise2)); + verify(writer, times(infoHeaderCount)).writeHeaders(eq(ctx), eq(streamId), eq(infoHeaders), + eq(0), eq(false), any(ChannelPromise.class)); + verify(writer, times(1)).writeHeaders(eq(ctx), eq(streamId), eq(EmptyHttp2Headers.INSTANCE), + eq(0), eq(false), eq(promise2)); if (eos) { - verify(writer, times(1)).writeHeaders(eq(ctx), eq(streamId), eq(EmptyHttp2Headers.INSTANCE), eq(0), - eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(true), eq(promise3)); + verify(writer, times(1)).writeHeaders(eq(ctx), eq(streamId), eq(EmptyHttp2Headers.INSTANCE), + eq(0), eq(true), eq(promise3)); } } @@ -752,8 +766,7 @@ public class DefaultHttp2ConnectionEncoderTest { final ChannelPromise promise = newPromise(); final Throwable ex = new RuntimeException(); // Fake an encoding error, like HPACK's HeaderListSizeException - when(writer.writeHeaders(eq(ctx), eq(STREAM_ID), eq(EmptyHttp2Headers.INSTANCE), eq(0), - eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(true), eq(promise))) + when(writer.writeHeaders(eq(ctx), eq(STREAM_ID), eq(EmptyHttp2Headers.INSTANCE), eq(0), eq(true), eq(promise))) .thenAnswer(new Answer<ChannelFuture>() { @Override public ChannelFuture answer(InvocationOnMock invocation) { @@ -779,8 +792,7 @@ public class DefaultHttp2ConnectionEncoderTest { final ChannelPromise promise = newPromise(); final Throwable ex = new RuntimeException(); // Fake an encoding error, like HPACK's HeaderListSizeException - when(writer.writeHeaders(eq(ctx), eq(STREAM_ID), eq(EmptyHttp2Headers.INSTANCE), eq(0), - eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(true), eq(promise))) + when(writer.writeHeaders(eq(ctx), eq(STREAM_ID), eq(EmptyHttp2Headers.INSTANCE), eq(0), eq(true), eq(promise))) .thenAnswer(new Answer<ChannelFuture>() { @Override public ChannelFuture answer(InvocationOnMock invocation) { @@ -849,8 +861,8 @@ public class DefaultHttp2ConnectionEncoderTest { goAwaySent(0); ChannelPromise promise = newPromise(); encoder.writeHeaders(ctx, STREAM_ID, EmptyHttp2Headers.INSTANCE, 0, false, promise); - verify(writer).writeHeaders(eq(ctx), eq(STREAM_ID), eq(EmptyHttp2Headers.INSTANCE), eq(0), - eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(false), eq(promise)); + verify(writer).writeHeaders(eq(ctx), eq(STREAM_ID), eq(EmptyHttp2Headers.INSTANCE), + eq(0), eq(false), eq(promise)); } @Test @@ -868,8 +880,29 @@ public class DefaultHttp2ConnectionEncoderTest { goAwayReceived(STREAM_ID); ChannelPromise promise = newPromise(); encoder.writeHeaders(ctx, STREAM_ID, EmptyHttp2Headers.INSTANCE, 0, false, promise); - verify(writer).writeHeaders(eq(ctx), eq(STREAM_ID), eq(EmptyHttp2Headers.INSTANCE), eq(0), - eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(false), eq(promise)); + verify(writer).writeHeaders(eq(ctx), eq(STREAM_ID), eq(EmptyHttp2Headers.INSTANCE), + eq(0), eq(false), eq(promise)); + } + + @Test + public void headersWithNoPriority() { + writeAllFlowControlledFrames(); + final int streamId = 6; + ChannelPromise promise = newPromise(); + encoder.writeHeaders(ctx, streamId, EmptyHttp2Headers.INSTANCE, 0, false, promise); + verify(writer).writeHeaders(eq(ctx), eq(streamId), eq(EmptyHttp2Headers.INSTANCE), + eq(0), eq(false), eq(promise)); + } + + @Test + public void headersWithPriority() { + writeAllFlowControlledFrames(); + final int streamId = 6; + ChannelPromise promise = newPromise(); + encoder.writeHeaders(ctx, streamId, EmptyHttp2Headers.INSTANCE, 10, DEFAULT_PRIORITY_WEIGHT, + true, 1, false, promise); + verify(writer).writeHeaders(eq(ctx), eq(streamId), eq(EmptyHttp2Headers.INSTANCE), eq(10), + eq(DEFAULT_PRIORITY_WEIGHT), eq(true), eq(1), eq(false), eq(promise)); } private void writeAllFlowControlledFrames() { diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2ConnectionTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2ConnectionTest.java index 69183e0..b43c000 100644 --- a/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2ConnectionTest.java +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2ConnectionTest.java @@ -624,7 +624,7 @@ public class DefaultHttp2ConnectionTest { private final boolean[] array; private final int index; - public ListenerExceptionThrower(boolean[] array, int index) { + ListenerExceptionThrower(boolean[] array, int index) { this.array = array; this.index = index; } @@ -640,7 +640,7 @@ public class DefaultHttp2ConnectionTest { private final boolean[] array; private final int index; - public ListenerVerifyCallAnswer(boolean[] array, int index) { + ListenerVerifyCallAnswer(boolean[] array, int index) { this.array = array; this.index = index; } diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2FrameWriterTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2FrameWriterTest.java index b5a40d6..6d53c40 100644 --- a/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2FrameWriterTest.java +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2FrameWriterTest.java @@ -15,6 +15,7 @@ package io.netty.handler.codec.http2; import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; import io.netty.buffer.Unpooled; import io.netty.buffer.UnpooledByteBufAllocator; import io.netty.channel.Channel; @@ -37,7 +38,6 @@ import java.io.IOException; import java.util.Arrays; import static org.junit.Assert.*; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; /** @@ -66,8 +66,11 @@ public class DefaultHttp2FrameWriterTest { @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); + http2HeadersEncoder = new DefaultHttp2HeadersEncoder( + Http2HeadersEncoder.NEVER_SENSITIVE, new HpackEncoder(false, 16, 0)); - frameWriter = new DefaultHttp2FrameWriter(); + frameWriter = new DefaultHttp2FrameWriter(new DefaultHttp2HeadersEncoder( + Http2HeadersEncoder.NEVER_SENSITIVE, new HpackEncoder(false, 16, 0))); outbound = Unpooled.buffer(); @@ -75,8 +78,6 @@ public class DefaultHttp2FrameWriterTest { promise = new DefaultChannelPromise(channel, ImmediateEventExecutor.INSTANCE); - http2HeadersEncoder = new DefaultHttp2HeadersEncoder(); - Answer<Object> answer = new Answer<Object>() { @Override public Object answer(InvocationOnMock var1) throws Throwable { @@ -204,6 +205,48 @@ public class DefaultHttp2FrameWriterTest { secondPayload); } + @Test + public void writeLargeHeaderWithPadding() throws Exception { + int streamId = 1; + Http2Headers headers = new DefaultHttp2Headers() + .method("GET").path("/").authority("foo.com").scheme("https"); + headers = dummyHeaders(headers, 20); + + http2HeadersEncoder.configuration().maxHeaderListSize(Integer.MAX_VALUE); + frameWriter.headersConfiguration().maxHeaderListSize(Integer.MAX_VALUE); + frameWriter.maxFrameSize(Http2CodecUtil.MAX_FRAME_SIZE_LOWER_BOUND); + frameWriter.writeHeaders(ctx, streamId, headers, 5, true, promise); + + byte[] expectedPayload = buildLargeHeaderPayload(streamId, headers, (byte) 4, + Http2CodecUtil.MAX_FRAME_SIZE_LOWER_BOUND); + + // First frame: HEADER(length=0x4000, flags=0x09) + assertEquals(Http2CodecUtil.MAX_FRAME_SIZE_LOWER_BOUND, + outbound.readUnsignedMedium()); + assertEquals(0x01, outbound.readByte()); + assertEquals(0x09, outbound.readByte()); // 0x01 + 0x08 + assertEquals(streamId, outbound.readInt()); + + byte[] firstPayload = new byte[Http2CodecUtil.MAX_FRAME_SIZE_LOWER_BOUND]; + outbound.readBytes(firstPayload); + + int remainPayloadLength = expectedPayload.length - Http2CodecUtil.MAX_FRAME_SIZE_LOWER_BOUND; + // Second frame: CONTINUATION(length=remainPayloadLength, flags=0x04) + assertEquals(remainPayloadLength, outbound.readUnsignedMedium()); + assertEquals(0x09, outbound.readByte()); + assertEquals(0x04, outbound.readByte()); + assertEquals(streamId, outbound.readInt()); + + byte[] secondPayload = new byte[remainPayloadLength]; + outbound.readBytes(secondPayload); + + assertArrayEquals(Arrays.copyOfRange(expectedPayload, 0, firstPayload.length), + firstPayload); + assertArrayEquals(Arrays.copyOfRange(expectedPayload, firstPayload.length, + expectedPayload.length), + secondPayload); + } + @Test public void writeFrameZeroPayload() throws Exception { frameWriter.writeFrame(ctx, (byte) 0xf, 0, new Http2Flags(), Unpooled.EMPTY_BUFFER, promise); @@ -265,6 +308,22 @@ public class DefaultHttp2FrameWriterTest { } } + private byte[] buildLargeHeaderPayload(int streamId, Http2Headers headers, byte padding, int maxFrameSize) + throws Http2Exception, IOException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + try { + outputStream.write(padding); + byte[] payload = headerPayload(streamId, headers); + int firstPayloadSize = maxFrameSize - (padding + 1); //1 for padding length + outputStream.write(payload, 0, firstPayloadSize); + outputStream.write(new byte[padding]); + outputStream.write(payload, firstPayloadSize, payload.length - firstPayloadSize); + return outputStream.toByteArray(); + } finally { + outputStream.close(); + } + } + private static Http2Headers dummyHeaders(Http2Headers headers, int times) { final String largeValue = repeat("dummy-value", 100); for (int i = 0; i < times; i++) { diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2LocalFlowControllerTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2LocalFlowControllerTest.java index 6a3b80a..cea7793 100644 --- a/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2LocalFlowControllerTest.java +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2LocalFlowControllerTest.java @@ -20,12 +20,14 @@ import static io.netty.handler.codec.http2.Http2CodecUtil.CONNECTION_STREAM_ID; import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_WINDOW_SIZE; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.any; import static org.mockito.Mockito.anyInt; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; @@ -34,12 +36,15 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelPromise; +import io.netty.handler.codec.http2.Http2Stream.State; import io.netty.util.concurrent.EventExecutor; import junit.framework.AssertionFailedError; import org.junit.Before; import org.junit.Test; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; /** * Tests for {@link DefaultHttp2LocalFlowController}. @@ -66,15 +71,28 @@ public class DefaultHttp2LocalFlowControllerTest { @Before public void setup() throws Http2Exception { MockitoAnnotations.initMocks(this); - - when(ctx.newPromise()).thenReturn(promise); - when(ctx.flush()).thenThrow(new AssertionFailedError("forbidden")); - when(ctx.executor()).thenReturn(executor); + setupChannelHandlerContext(false); when(executor.inEventLoop()).thenReturn(true); initController(false); } + private void setupChannelHandlerContext(boolean allowFlush) { + reset(ctx); + when(ctx.newPromise()).thenReturn(promise); + if (allowFlush) { + when(ctx.flush()).then(new Answer<ChannelHandlerContext>() { + @Override + public ChannelHandlerContext answer(InvocationOnMock invocationOnMock) { + return ctx; + } + }); + } else { + when(ctx.flush()).thenThrow(new AssertionFailedError("forbidden")); + } + when(ctx.executor()).thenReturn(executor); + } + @Test public void dataFrameShouldBeAccepted() throws Http2Exception { receiveFlowControlledFrame(STREAM_ID, 10, 0, false); @@ -138,6 +156,46 @@ public class DefaultHttp2LocalFlowControllerTest { verifyWindowUpdateNotSent(STREAM_ID); } + @Test + public void windowUpdateShouldNotBeSentAfterStreamIsClosedForUnconsumedBytes() throws Http2Exception { + int dataSize = (int) (DEFAULT_WINDOW_SIZE * DEFAULT_WINDOW_UPDATE_RATIO) + 1; + + // Don't set end-of-stream on the frame as we want to verify that we not return the unconsumed bytes in this + // case once the stream was closed, + receiveFlowControlledFrame(STREAM_ID, dataSize, 0, false); + verifyWindowUpdateNotSent(CONNECTION_STREAM_ID); + verifyWindowUpdateNotSent(STREAM_ID); + + // Close the stream + Http2Stream stream = connection.stream(STREAM_ID); + stream.close(); + assertEquals(State.CLOSED, stream.state()); + assertNull(connection.stream(STREAM_ID)); + + // The window update for the connection should made it through but not the update for the already closed + // stream + verifyWindowUpdateSent(CONNECTION_STREAM_ID, dataSize); + verifyWindowUpdateNotSent(STREAM_ID); + } + + @Test + public void windowUpdateShouldBeWrittenWhenStreamIsClosedAndFlushed() throws Http2Exception { + int dataSize = (int) (DEFAULT_WINDOW_SIZE * DEFAULT_WINDOW_UPDATE_RATIO) + 1; + + setupChannelHandlerContext(true); + + receiveFlowControlledFrame(STREAM_ID, dataSize, 0, false); + verifyWindowUpdateNotSent(CONNECTION_STREAM_ID); + verifyWindowUpdateNotSent(STREAM_ID); + + connection.stream(STREAM_ID).close(); + + verifyWindowUpdateSent(CONNECTION_STREAM_ID, dataSize); + + // Verify we saw one flush. + verify(ctx).flush(); + } + @Test public void halfWindowRemainingShouldUpdateAllWindows() throws Http2Exception { int dataSize = (int) (DEFAULT_WINDOW_SIZE * DEFAULT_WINDOW_UPDATE_RATIO) + 1; diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/HpackDecoderTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/HpackDecoderTest.java index 994fef6..7b26d90 100644 --- a/codec-http2/src/test/java/io/netty/handler/codec/http2/HpackDecoderTest.java +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/HpackDecoderTest.java @@ -79,7 +79,7 @@ public class HpackDecoderTest { @Before public void setUp() { - hpackDecoder = new HpackDecoder(8192, 32); + hpackDecoder = new HpackDecoder(8192); mockHeaders = mock(Http2Headers.class); } diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/HpackEncoderTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/HpackEncoderTest.java index de049a6..b650035 100644 --- a/codec-http2/src/test/java/io/netty/handler/codec/http2/HpackEncoderTest.java +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/HpackEncoderTest.java @@ -33,7 +33,7 @@ public class HpackEncoderTest { @Before public void setUp() { hpackEncoder = new HpackEncoder(); - hpackDecoder = new HpackDecoder(DEFAULT_HEADER_LIST_SIZE, 32); + hpackDecoder = new HpackDecoder(DEFAULT_HEADER_LIST_SIZE); mockHeaders = mock(Http2Headers.class); } diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/HpackHuffmanTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/HpackHuffmanTest.java index 06e540f..f056436 100644 --- a/codec-http2/src/test/java/io/netty/handler/codec/http2/HpackHuffmanTest.java +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/HpackHuffmanTest.java @@ -61,56 +61,56 @@ public class HpackHuffmanTest { for (int i = 0; i < 4; i++) { buf[i] = (byte) 0xFF; } - decode(newHuffmanDecoder(), buf); + decode(buf); } @Test(expected = Http2Exception.class) public void testDecodeIllegalPadding() throws Http2Exception { byte[] buf = new byte[1]; buf[0] = 0x00; // '0', invalid padding - decode(newHuffmanDecoder(), buf); + decode(buf); } @Test(expected = Http2Exception.class) public void testDecodeExtraPadding() throws Http2Exception { byte[] buf = makeBuf(0x0f, 0xFF); // '1', 'EOS' - decode(newHuffmanDecoder(), buf); + decode(buf); } @Test(expected = Http2Exception.class) public void testDecodeExtraPadding1byte() throws Http2Exception { byte[] buf = makeBuf(0xFF); - decode(newHuffmanDecoder(), buf); + decode(buf); } @Test(expected = Http2Exception.class) public void testDecodeExtraPadding2byte() throws Http2Exception { byte[] buf = makeBuf(0x1F, 0xFF); // 'a' - decode(newHuffmanDecoder(), buf); + decode(buf); } @Test(expected = Http2Exception.class) public void testDecodeExtraPadding3byte() throws Http2Exception { byte[] buf = makeBuf(0x1F, 0xFF, 0xFF); // 'a' - decode(newHuffmanDecoder(), buf); + decode(buf); } @Test(expected = Http2Exception.class) public void testDecodeExtraPadding4byte() throws Http2Exception { byte[] buf = makeBuf(0x1F, 0xFF, 0xFF, 0xFF); // 'a' - decode(newHuffmanDecoder(), buf); + decode(buf); } @Test(expected = Http2Exception.class) public void testDecodeExtraPadding29bit() throws Http2Exception { byte[] buf = makeBuf(0xFF, 0x9F, 0xFF, 0xFF, 0xFF); // '|' - decode(newHuffmanDecoder(), buf); + decode(buf); } @Test(expected = Http2Exception.class) public void testDecodePartialSymbol() throws Http2Exception { byte[] buf = makeBuf(0x52, 0xBC, 0x30, 0xFF, 0xFF, 0xFF, 0xFF); // " pFA\x00", 31 bits of padding, a.k.a. EOS - decode(newHuffmanDecoder(), buf); + decode(buf); } private static byte[] makeBuf(int ... bytes) { @@ -122,19 +122,19 @@ public class HpackHuffmanTest { } private static void roundTrip(String s) throws Http2Exception { - roundTrip(new HpackHuffmanEncoder(), newHuffmanDecoder(), s); + roundTrip(new HpackHuffmanEncoder(), s); } - private static void roundTrip(HpackHuffmanEncoder encoder, HpackHuffmanDecoder decoder, String s) + private static void roundTrip(HpackHuffmanEncoder encoder, String s) throws Http2Exception { - roundTrip(encoder, decoder, s.getBytes()); + roundTrip(encoder, s.getBytes()); } private static void roundTrip(byte[] buf) throws Http2Exception { - roundTrip(new HpackHuffmanEncoder(), newHuffmanDecoder(), buf); + roundTrip(new HpackHuffmanEncoder(), buf); } - private static void roundTrip(HpackHuffmanEncoder encoder, HpackHuffmanDecoder decoder, byte[] buf) + private static void roundTrip(HpackHuffmanEncoder encoder, byte[] buf) throws Http2Exception { ByteBuf buffer = Unpooled.buffer(); try { @@ -142,7 +142,7 @@ public class HpackHuffmanTest { byte[] bytes = new byte[buffer.readableBytes()]; buffer.readBytes(bytes); - byte[] actualBytes = decode(decoder, bytes); + byte[] actualBytes = decode(bytes); Assert.assertTrue(Arrays.equals(buf, actualBytes)); } finally { @@ -150,18 +150,14 @@ public class HpackHuffmanTest { } } - private static byte[] decode(HpackHuffmanDecoder decoder, byte[] bytes) throws Http2Exception { + private static byte[] decode(byte[] bytes) throws Http2Exception { ByteBuf buffer = Unpooled.wrappedBuffer(bytes); try { - AsciiString decoded = decoder.decode(buffer, buffer.readableBytes()); + AsciiString decoded = new HpackHuffmanDecoder().decode(buffer, buffer.readableBytes()); Assert.assertFalse(buffer.isReadable()); return decoded.toByteArray(); } finally { buffer.release(); } } - - private static HpackHuffmanDecoder newHuffmanDecoder() { - return new HpackHuffmanDecoder(32); - } } diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/HpackTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/HpackTest.java index fe9fa32..5a7d45f 100644 --- a/codec-http2/src/test/java/io/netty/handler/codec/http2/HpackTest.java +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/HpackTest.java @@ -31,6 +31,7 @@ */ package io.netty.handler.codec.http2; +import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.ResourcesUtil; import org.junit.Test; import org.junit.runner.RunWith; @@ -57,9 +58,7 @@ public class HpackTest { @Parameters(name = "{0}") public static Collection<Object[]> data() { File[] files = ResourcesUtil.getFile(HpackTest.class, TEST_DIR).listFiles(); - if (files == null) { - throw new NullPointerException("files"); - } + ObjectUtil.checkNotNull(files, "files"); ArrayList<Object[]> data = new ArrayList<Object[]>(); for (File file : files) { diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/HpackTestCase.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/HpackTestCase.java index 33faa50..44c8231 100644 --- a/codec-http2/src/test/java/io/netty/handler/codec/http2/HpackTestCase.java +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/HpackTestCase.java @@ -102,7 +102,7 @@ final class HpackTestCase { List<HpackHeaderField> expectedDynamicTable = headerBlock.getDynamicTable(); - if (!expectedDynamicTable.equals(actualDynamicTable)) { + if (!headersEqual(expectedDynamicTable, actualDynamicTable)) { throw new AssertionError( "\nEXPECTED DYNAMIC TABLE:\n" + expectedDynamicTable + "\nACTUAL DYNAMIC TABLE:\n" + actualDynamicTable); @@ -128,7 +128,7 @@ final class HpackTestCase { expectedHeaders.add(new HpackHeaderField(h.name, h.value)); } - if (!expectedHeaders.equals(actualHeaders)) { + if (!headersEqual(expectedHeaders, actualHeaders)) { throw new AssertionError( "\nEXPECTED:\n" + expectedHeaders + "\nACTUAL:\n" + actualHeaders); @@ -141,7 +141,7 @@ final class HpackTestCase { List<HpackHeaderField> expectedDynamicTable = headerBlock.getDynamicTable(); - if (!expectedDynamicTable.equals(actualDynamicTable)) { + if (!headersEqual(expectedDynamicTable, actualDynamicTable)) { throw new AssertionError( "\nEXPECTED DYNAMIC TABLE:\n" + expectedDynamicTable + "\nACTUAL DYNAMIC TABLE:\n" + actualDynamicTable); @@ -174,7 +174,7 @@ final class HpackTestCase { maxHeaderTableSize = Integer.MAX_VALUE; } - return new HpackDecoder(DEFAULT_HEADER_LIST_SIZE, 32, maxHeaderTableSize); + return new HpackDecoder(DEFAULT_HEADER_LIST_SIZE, maxHeaderTableSize); } private static byte[] encode(HpackEncoder hpackEncoder, List<HpackHeaderField> headers, int maxHeaderTableSize, @@ -229,6 +229,18 @@ final class HpackTestCase { return ret.toString(); } + private static boolean headersEqual(List<HpackHeaderField> expected, List<HpackHeaderField> actual) { + if (expected.size() != actual.size()) { + return false; + } + for (int i = 0; i < expected.size(); i++) { + if (!expected.get(i).equalsForTest(actual.get(i))) { + return false; + } + } + return true; + } + static class HeaderBlock { private int maxHeaderTableSize = -1; private byte[] encodedBytes; diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2ClientUpgradeCodecTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2ClientUpgradeCodecTest.java index fcfdb4b..f0b4a58 100644 --- a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2ClientUpgradeCodecTest.java +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2ClientUpgradeCodecTest.java @@ -14,11 +14,9 @@ */ package io.netty.handler.codec.http2; -import io.netty.channel.Channel; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; -import io.netty.channel.ChannelInitializer; import io.netty.channel.embedded.EmbeddedChannel; import io.netty.handler.codec.http.DefaultFullHttpRequest; import io.netty.handler.codec.http.FullHttpRequest; @@ -35,26 +33,42 @@ public class Http2ClientUpgradeCodecTest { @Test public void testUpgradeToHttp2ConnectionHandler() throws Exception { - testUpgrade(new Http2ConnectionHandlerBuilder().server(false).frameListener(new Http2FrameAdapter()).build()); + testUpgrade(new Http2ConnectionHandlerBuilder().server(false).frameListener( + new Http2FrameAdapter()).build(), null); } @Test public void testUpgradeToHttp2FrameCodec() throws Exception { - testUpgrade(Http2FrameCodecBuilder.forClient().build()); + testUpgrade(Http2FrameCodecBuilder.forClient().build(), null); } @Test public void testUpgradeToHttp2MultiplexCodec() throws Exception { testUpgrade(Http2MultiplexCodecBuilder.forClient(new HttpInboundHandler()) - .withUpgradeStreamHandler(new ChannelInboundHandlerAdapter()).build()); + .withUpgradeStreamHandler(new ChannelInboundHandlerAdapter()).build(), null); } - private static void testUpgrade(Http2ConnectionHandler handler) throws Exception { + @Test + public void testUpgradeToHttp2FrameCodecWithMultiplexer() throws Exception { + testUpgrade(Http2FrameCodecBuilder.forClient().build(), + new Http2MultiplexHandler(new HttpInboundHandler(), new HttpInboundHandler())); + } + + private static void testUpgrade(Http2ConnectionHandler handler, Http2MultiplexHandler multiplexer) + throws Exception { FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.OPTIONS, "*"); EmbeddedChannel channel = new EmbeddedChannel(new ChannelInboundHandlerAdapter()); ChannelHandlerContext ctx = channel.pipeline().firstContext(); - Http2ClientUpgradeCodec codec = new Http2ClientUpgradeCodec("connectionHandler", handler); + + Http2ClientUpgradeCodec codec; + + if (multiplexer == null) { + codec = new Http2ClientUpgradeCodec("connectionHandler", handler); + } else { + codec = new Http2ClientUpgradeCodec("connectionHandler", handler, multiplexer); + } + codec.setUpgradeHeaders(ctx, request); // Flush the channel to ensure we write out all buffered data channel.flush(); @@ -62,6 +76,10 @@ public class Http2ClientUpgradeCodecTest { codec.upgradeTo(ctx, null); assertNotNull(channel.pipeline().get("connectionHandler")); + if (multiplexer != null) { + assertNotNull(channel.pipeline().get(Http2MultiplexHandler.class)); + } + assertTrue(channel.finishAndReleaseAll()); } diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2ConnectionHandlerTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2ConnectionHandlerTest.java index 9b8c624..0143edc 100644 --- a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2ConnectionHandlerTest.java +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2ConnectionHandlerTest.java @@ -29,6 +29,7 @@ import io.netty.channel.DefaultChannelConfig; import io.netty.channel.DefaultChannelPromise; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http2.Http2CodecUtil.SimpleChannelPromiseAggregator; +import io.netty.handler.codec.http2.Http2Exception.ShutdownHint; import io.netty.util.ReferenceCountUtil; import io.netty.util.concurrent.EventExecutor; import io.netty.util.concurrent.GenericFutureListener; @@ -40,6 +41,7 @@ import org.junit.Test; import org.mockito.ArgumentCaptor; import org.mockito.ArgumentMatchers; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; @@ -48,9 +50,11 @@ import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; import static io.netty.buffer.Unpooled.copiedBuffer; import static io.netty.handler.codec.http2.Http2CodecUtil.connectionPrefaceBuf; +import static io.netty.handler.codec.http2.Http2Error.CANCEL; import static io.netty.handler.codec.http2.Http2Error.PROTOCOL_ERROR; import static io.netty.handler.codec.http2.Http2Error.STREAM_CLOSED; import static io.netty.handler.codec.http2.Http2Stream.State.CLOSED; @@ -721,13 +725,42 @@ public class Http2ConnectionHandlerTest { writeRstStreamUsingVoidPromise(STREAM_ID); } + @Test + public void gracefulShutdownTimeoutWhenConnectionErrorHardShutdownTest() throws Exception { + gracefulShutdownTimeoutWhenConnectionErrorTest0(ShutdownHint.HARD_SHUTDOWN); + } + + @Test + public void gracefulShutdownTimeoutWhenConnectionErrorGracefulShutdownTest() throws Exception { + gracefulShutdownTimeoutWhenConnectionErrorTest0(ShutdownHint.GRACEFUL_SHUTDOWN); + } + + private void gracefulShutdownTimeoutWhenConnectionErrorTest0(ShutdownHint hint) throws Exception { + handler = newHandler(); + final long expectedMillis = 1234; + handler.gracefulShutdownTimeoutMillis(expectedMillis); + Http2Exception exception = new Http2Exception(PROTOCOL_ERROR, "Test error", hint); + handler.onConnectionError(ctx, false, exception, exception); + verify(executor, atLeastOnce()).schedule(any(Runnable.class), eq(expectedMillis), eq(TimeUnit.MILLISECONDS)); + } + @Test public void gracefulShutdownTimeoutTest() throws Exception { handler = newHandler(); final long expectedMillis = 1234; handler.gracefulShutdownTimeoutMillis(expectedMillis); handler.close(ctx, promise); - verify(executor).schedule(any(Runnable.class), eq(expectedMillis), eq(TimeUnit.MILLISECONDS)); + verify(executor, atLeastOnce()).schedule(any(Runnable.class), eq(expectedMillis), eq(TimeUnit.MILLISECONDS)); + } + + @Test + public void gracefulShutdownTimeoutNoActiveStreams() throws Exception { + handler = newHandler(); + when(connection.numActiveStreams()).thenReturn(0); + final long expectedMillis = 1234; + handler.gracefulShutdownTimeoutMillis(expectedMillis); + handler.close(ctx, promise); + verify(executor, atLeastOnce()).schedule(any(Runnable.class), eq(expectedMillis), eq(TimeUnit.MILLISECONDS)); } @Test @@ -738,6 +771,51 @@ public class Http2ConnectionHandlerTest { verify(executor, never()).schedule(any(Runnable.class), anyLong(), any(TimeUnit.class)); } + @Test + public void writeMultipleRstFramesForSameStream() throws Exception { + handler = newHandler(); + when(stream.id()).thenReturn(STREAM_ID); + + final AtomicBoolean resetSent = new AtomicBoolean(); + when(stream.resetSent()).then(new Answer<Http2Stream>() { + @Override + public Http2Stream answer(InvocationOnMock invocationOnMock) { + resetSent.set(true); + return stream; + } + }); + when(stream.isResetSent()).then(new Answer<Boolean>() { + @Override + public Boolean answer(InvocationOnMock invocationOnMock) { + return resetSent.get(); + } + }); + when(frameWriter.writeRstStream(eq(ctx), eq(STREAM_ID), anyLong(), any(ChannelPromise.class))) + .then(new Answer<ChannelFuture>() { + @Override + public ChannelFuture answer(InvocationOnMock invocationOnMock) throws Throwable { + ChannelPromise promise = invocationOnMock.getArgument(3); + return promise.setSuccess(); + } + }); + + ChannelPromise promise = + new DefaultChannelPromise(channel, ImmediateEventExecutor.INSTANCE); + final ChannelPromise promise2 = + new DefaultChannelPromise(channel, ImmediateEventExecutor.INSTANCE); + promise.addListener(new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture future) { + handler.resetStream(ctx, STREAM_ID, STREAM_CLOSED.code(), promise2); + } + }); + + handler.resetStream(ctx, STREAM_ID, CANCEL.code(), promise); + verify(frameWriter).writeRstStream(eq(ctx), eq(STREAM_ID), anyLong(), any(ChannelPromise.class)); + assertTrue(promise.isSuccess()); + assertTrue(promise2.isSuccess()); + } + private void writeRstStreamUsingVoidPromise(int streamId) throws Exception { handler = newHandler(); final Throwable cause = new RuntimeException("fake exception"); diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2ConnectionRoundtripTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2ConnectionRoundtripTest.java index 32febac..f9dee00 100644 --- a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2ConnectionRoundtripTest.java +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2ConnectionRoundtripTest.java @@ -52,6 +52,8 @@ import java.io.ByteArrayOutputStream; import java.util.Random; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import static io.netty.buffer.Unpooled.EMPTY_BUFFER; @@ -522,18 +524,7 @@ public class Http2ConnectionRoundtripTest { final CountDownLatch serverWriteHeadersLatch = new CountDownLatch(1); final AtomicReference<Throwable> serverWriteHeadersCauseRef = new AtomicReference<Throwable>(); - final Http2Headers headers = dummyHeaders(); final int streamId = 3; - runInChannel(clientChannel, new Http2Runnable() { - @Override - public void run() throws Http2Exception { - http2Client.encoder().writeHeaders(ctx(), streamId, headers, CONNECTION_STREAM_ID, - DEFAULT_PRIORITY_WEIGHT, false, 0, false, newPromise()); - http2Client.encoder().writeRstStream(ctx(), streamId, Http2Error.CANCEL.code(), newPromise()); - http2Client.flush(ctx()); - } - }); - doAnswer(new Answer<Void>() { @Override public Void answer(InvocationOnMock invocationOnMock) throws Throwable { @@ -544,6 +535,17 @@ public class Http2ConnectionRoundtripTest { } }).when(serverListener).onRstStreamRead(any(ChannelHandlerContext.class), eq(streamId), anyLong()); + final Http2Headers headers = dummyHeaders(); + runInChannel(clientChannel, new Http2Runnable() { + @Override + public void run() throws Http2Exception { + http2Client.encoder().writeHeaders(ctx(), streamId, headers, CONNECTION_STREAM_ID, + DEFAULT_PRIORITY_WEIGHT, false, 0, false, newPromise()); + http2Client.encoder().writeRstStream(ctx(), streamId, Http2Error.CANCEL.code(), newPromise()); + http2Client.flush(ctx()); + } + }); + assertTrue(serverSettingsAckLatch.await(DEFAULT_AWAIT_TIMEOUT_SECONDS, SECONDS)); assertTrue(serverGotRstLatch.await(DEFAULT_AWAIT_TIMEOUT_SECONDS, SECONDS)); @@ -945,47 +947,33 @@ public class Http2ConnectionRoundtripTest { } @Test - public void createStreamSynchronouslyAfterGoAwayReceivedShouldFailLocally() throws Exception { + public void listenerIsNotifiedOfGoawayBeforeStreamsAreRemovedFromTheConnection() throws Exception { bootstrapEnv(1, 1, 2, 1, 1); - final CountDownLatch clientGoAwayLatch = new CountDownLatch(1); - doAnswer(new Answer<Void>() { - @Override - public Void answer(InvocationOnMock invocationOnMock) throws Throwable { - clientGoAwayLatch.countDown(); - return null; - } - }).when(clientListener).onGoAwayRead(any(ChannelHandlerContext.class), anyInt(), anyLong(), any(ByteBuf.class)); - // We want both sides to do graceful shutdown during the test. setClientGracefulShutdownTime(10000); setServerGracefulShutdownTime(10000); - final Http2Headers headers = dummyHeaders(); - final AtomicReference<ChannelFuture> clientWriteAfterGoAwayFutureRef = new AtomicReference<ChannelFuture>(); - final CountDownLatch clientWriteAfterGoAwayLatch = new CountDownLatch(1); + final AtomicReference<Http2Stream.State> clientStream3State = new AtomicReference<Http2Stream.State>(); + final CountDownLatch clientGoAwayLatch = new CountDownLatch(1); doAnswer(new Answer<Void>() { @Override public Void answer(InvocationOnMock invocationOnMock) throws Throwable { - ChannelFuture f = http2Client.encoder().writeHeaders(ctx(), 5, headers, 0, (short) 16, false, 0, - true, newPromise()); - clientWriteAfterGoAwayFutureRef.set(f); - f.addListener(new ChannelFutureListener() { - @Override - public void operationComplete(ChannelFuture future) throws Exception { - clientWriteAfterGoAwayLatch.countDown(); - } - }); - http2Client.flush(ctx()); + clientStream3State.set(http2Client.connection().stream(3).state()); + clientGoAwayLatch.countDown(); return null; } }).when(clientListener).onGoAwayRead(any(ChannelHandlerContext.class), anyInt(), anyLong(), any(ByteBuf.class)); + // Create a single stream by sending a HEADERS frame to the server. + final Http2Headers headers = dummyHeaders(); runInChannel(clientChannel, new Http2Runnable() { @Override public void run() throws Http2Exception { + http2Client.encoder().writeHeaders(ctx(), 1, headers, 0, (short) 16, false, 0, + false, newPromise()); http2Client.encoder().writeHeaders(ctx(), 3, headers, 0, (short) 16, false, 0, - true, newPromise()); + false, newPromise()); http2Client.flush(ctx()); } }); @@ -998,24 +986,38 @@ public class Http2ConnectionRoundtripTest { runInChannel(serverChannel, new Http2Runnable() { @Override public void run() throws Http2Exception { - http2Server.encoder().writeGoAway(serverCtx(), 3, NO_ERROR.code(), EMPTY_BUFFER, serverNewPromise()); + http2Server.encoder().writeGoAway(serverCtx(), 1, NO_ERROR.code(), EMPTY_BUFFER, serverNewPromise()); http2Server.flush(serverCtx()); } }); - // Wait for the client's write operation to complete. - assertTrue(clientWriteAfterGoAwayLatch.await(DEFAULT_AWAIT_TIMEOUT_SECONDS, SECONDS)); - - ChannelFuture clientWriteAfterGoAwayFuture = clientWriteAfterGoAwayFutureRef.get(); - assertNotNull(clientWriteAfterGoAwayFuture); - Throwable clientCause = clientWriteAfterGoAwayFuture.cause(); - assertThat(clientCause, is(instanceOf(Http2Exception.StreamException.class))); - assertEquals(Http2Error.REFUSED_STREAM.code(), ((Http2Exception.StreamException) clientCause).error().code()); + // wait for the client to receive the GO_AWAY. + assertTrue(clientGoAwayLatch.await(DEFAULT_AWAIT_TIMEOUT_SECONDS, SECONDS)); + verify(clientListener).onGoAwayRead(any(ChannelHandlerContext.class), eq(1), eq(NO_ERROR.code()), + any(ByteBuf.class)); + assertEquals(Http2Stream.State.OPEN, clientStream3State.get()); + + // Make sure that stream 3 has been closed which is true if it's gone. + final CountDownLatch probeStreamCount = new CountDownLatch(1); + final AtomicBoolean stream3Exists = new AtomicBoolean(); + final AtomicInteger streamCount = new AtomicInteger(); + runInChannel(this.clientChannel, new Http2Runnable() { + @Override + public void run() throws Http2Exception { + stream3Exists.set(http2Client.connection().stream(3) != null); + streamCount.set(http2Client.connection().numActiveStreams()); + probeStreamCount.countDown(); + } + }); + // The stream should be closed right after + assertTrue(probeStreamCount.await(DEFAULT_AWAIT_TIMEOUT_SECONDS, SECONDS)); + assertEquals(1, streamCount.get()); + assertFalse(stream3Exists.get()); // Wait for the server to receive a GO_AWAY, but this is expected to timeout! assertFalse(goAwayLatch.await(1, SECONDS)); verify(serverListener, never()).onGoAwayRead(any(ChannelHandlerContext.class), anyInt(), anyLong(), - any(ByteBuf.class)); + any(ByteBuf.class)); // Shutdown shouldn't wait for the server to close streams setClientGracefulShutdownTime(0); diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2ControlFrameLimitEncoderTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2ControlFrameLimitEncoderTest.java new file mode 100644 index 0000000..bbae3af --- /dev/null +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2ControlFrameLimitEncoderTest.java @@ -0,0 +1,277 @@ +/* + * Copyright 2019 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.codec.http2; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.UnpooledByteBufAllocator; +import io.netty.channel.Channel; +import io.netty.channel.ChannelConfig; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelMetadata; +import io.netty.channel.ChannelPromise; +import io.netty.channel.DefaultChannelPromise; +import io.netty.channel.DefaultMessageSizeEstimator; +import io.netty.handler.codec.http2.Http2Exception.ShutdownHint; +import io.netty.util.ReferenceCountUtil; +import io.netty.util.concurrent.EventExecutor; +import io.netty.util.concurrent.ImmediateEventExecutor; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + + +import java.util.ArrayDeque; +import java.util.Queue; + +import static io.netty.handler.codec.http2.Http2CodecUtil.*; +import static io.netty.handler.codec.http2.Http2Error.CANCEL; +import static io.netty.handler.codec.http2.Http2Error.ENHANCE_YOUR_CALM; +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +/** + * Tests for {@link Http2ControlFrameLimitEncoder}. + */ +public class Http2ControlFrameLimitEncoderTest { + + private Http2ControlFrameLimitEncoder encoder; + + @Mock + private Http2FrameWriter writer; + + @Mock + private ChannelHandlerContext ctx; + + @Mock + private Channel channel; + + @Mock + private Channel.Unsafe unsafe; + + @Mock + private ChannelConfig config; + + @Mock + private EventExecutor executor; + + private int numWrites; + + private Queue<ChannelPromise> goAwayPromises = new ArrayDeque<ChannelPromise>(); + + /** + * Init fields and do mocking. + */ + @Before + public void setup() throws Exception { + MockitoAnnotations.initMocks(this); + + numWrites = 0; + + Http2FrameWriter.Configuration configuration = mock(Http2FrameWriter.Configuration.class); + Http2FrameSizePolicy frameSizePolicy = mock(Http2FrameSizePolicy.class); + when(writer.configuration()).thenReturn(configuration); + when(configuration.frameSizePolicy()).thenReturn(frameSizePolicy); + when(frameSizePolicy.maxFrameSize()).thenReturn(DEFAULT_MAX_FRAME_SIZE); + + when(writer.writeRstStream(eq(ctx), anyInt(), anyLong(), any(ChannelPromise.class))) + .thenAnswer(new Answer<ChannelFuture>() { + @Override + public ChannelFuture answer(InvocationOnMock invocationOnMock) { + return handlePromise(invocationOnMock, 3); + } + }); + when(writer.writeSettingsAck(any(ChannelHandlerContext.class), any(ChannelPromise.class))) + .thenAnswer(new Answer<ChannelFuture>() { + @Override + public ChannelFuture answer(InvocationOnMock invocationOnMock) { + return handlePromise(invocationOnMock, 1); + } + }); + when(writer.writePing(any(ChannelHandlerContext.class), anyBoolean(), anyLong(), any(ChannelPromise.class))) + .thenAnswer(new Answer<ChannelFuture>() { + @Override + public ChannelFuture answer(InvocationOnMock invocationOnMock) { + ChannelPromise promise = handlePromise(invocationOnMock, 3); + if (invocationOnMock.getArgument(1) == Boolean.FALSE) { + promise.trySuccess(); + } + return promise; + } + }); + when(writer.writeGoAway(any(ChannelHandlerContext.class), anyInt(), anyLong(), any(ByteBuf.class), + any(ChannelPromise.class))).thenAnswer(new Answer<ChannelFuture>() { + @Override + public ChannelFuture answer(InvocationOnMock invocationOnMock) { + ReferenceCountUtil.release(invocationOnMock.getArgument(3)); + ChannelPromise promise = invocationOnMock.getArgument(4); + goAwayPromises.offer(promise); + return promise; + } + }); + Http2Connection connection = new DefaultHttp2Connection(false); + connection.remote().flowController(new DefaultHttp2RemoteFlowController(connection)); + connection.local().flowController(new DefaultHttp2LocalFlowController(connection).frameWriter(writer)); + + DefaultHttp2ConnectionEncoder defaultEncoder = + new DefaultHttp2ConnectionEncoder(connection, writer); + encoder = new Http2ControlFrameLimitEncoder(defaultEncoder, 2); + DefaultHttp2ConnectionDecoder decoder = + new DefaultHttp2ConnectionDecoder(connection, encoder, mock(Http2FrameReader.class)); + Http2ConnectionHandler handler = new Http2ConnectionHandlerBuilder() + .frameListener(mock(Http2FrameListener.class)) + .codec(decoder, encoder).build(); + + // Set LifeCycleManager on encoder and decoder + when(ctx.channel()).thenReturn(channel); + when(ctx.alloc()).thenReturn(UnpooledByteBufAllocator.DEFAULT); + when(channel.alloc()).thenReturn(UnpooledByteBufAllocator.DEFAULT); + when(executor.inEventLoop()).thenReturn(true); + doAnswer(new Answer<ChannelPromise>() { + @Override + public ChannelPromise answer(InvocationOnMock invocation) throws Throwable { + return newPromise(); + } + }).when(ctx).newPromise(); + when(ctx.executor()).thenReturn(executor); + when(channel.isActive()).thenReturn(false); + when(channel.config()).thenReturn(config); + when(channel.isWritable()).thenReturn(true); + when(channel.bytesBeforeUnwritable()).thenReturn(Long.MAX_VALUE); + when(config.getWriteBufferHighWaterMark()).thenReturn(Integer.MAX_VALUE); + when(config.getMessageSizeEstimator()).thenReturn(DefaultMessageSizeEstimator.DEFAULT); + ChannelMetadata metadata = new ChannelMetadata(false, 16); + when(channel.metadata()).thenReturn(metadata); + when(channel.unsafe()).thenReturn(unsafe); + handler.handlerAdded(ctx); + } + + private ChannelPromise handlePromise(InvocationOnMock invocationOnMock, int promiseIdx) { + ChannelPromise promise = invocationOnMock.getArgument(promiseIdx); + if (++numWrites == 2) { + promise.setSuccess(); + } + return promise; + } + + @After + public void teardown() { + // Close and release any buffered frames. + encoder.close(); + + // Notify all goAway ChannelPromise instances now as these will also release the retained ByteBuf for the + // debugData. + for (;;) { + ChannelPromise promise = goAwayPromises.poll(); + if (promise == null) { + break; + } + promise.setSuccess(); + } + } + + @Test + public void testLimitSettingsAck() { + assertFalse(encoder.writeSettingsAck(ctx, newPromise()).isDone()); + // The second write is always marked as success by our mock, which means it will also not be queued and so + // not count to the number of queued frames. + assertTrue(encoder.writeSettingsAck(ctx, newPromise()).isSuccess()); + assertFalse(encoder.writeSettingsAck(ctx, newPromise()).isDone()); + + verifyFlushAndClose(0, false); + + assertFalse(encoder.writeSettingsAck(ctx, newPromise()).isDone()); + assertFalse(encoder.writeSettingsAck(ctx, newPromise()).isDone()); + + verifyFlushAndClose(1, true); + } + + @Test + public void testLimitPingAck() { + assertFalse(encoder.writePing(ctx, true, 8, newPromise()).isDone()); + // The second write is always marked as success by our mock, which means it will also not be queued and so + // not count to the number of queued frames. + assertTrue(encoder.writePing(ctx, true, 8, newPromise()).isSuccess()); + assertFalse(encoder.writePing(ctx, true, 8, newPromise()).isDone()); + + verifyFlushAndClose(0, false); + + assertFalse(encoder.writePing(ctx, true, 8, newPromise()).isDone()); + assertFalse(encoder.writePing(ctx, true, 8, newPromise()).isDone()); + + verifyFlushAndClose(1, true); + } + + @Test + public void testNotLimitPing() { + assertTrue(encoder.writePing(ctx, false, 8, newPromise()).isSuccess()); + assertTrue(encoder.writePing(ctx, false, 8, newPromise()).isSuccess()); + assertTrue(encoder.writePing(ctx, false, 8, newPromise()).isSuccess()); + assertTrue(encoder.writePing(ctx, false, 8, newPromise()).isSuccess()); + + verifyFlushAndClose(0, false); + } + + @Test + public void testLimitRst() { + assertFalse(encoder.writeRstStream(ctx, 1, CANCEL.code(), newPromise()).isDone()); + // The second write is always marked as success by our mock, which means it will also not be queued and so + // not count to the number of queued frames. + assertTrue(encoder.writeRstStream(ctx, 1, CANCEL.code(), newPromise()).isSuccess()); + assertFalse(encoder.writeRstStream(ctx, 1, CANCEL.code(), newPromise()).isDone()); + + verifyFlushAndClose(0, false); + + assertFalse(encoder.writeRstStream(ctx, 1, CANCEL.code(), newPromise()).isDone()); + assertFalse(encoder.writeRstStream(ctx, 1, CANCEL.code(), newPromise()).isDone()); + + verifyFlushAndClose(1, true); + } + + @Test + public void testLimit() { + assertFalse(encoder.writeRstStream(ctx, 1, CANCEL.code(), newPromise()).isDone()); + // The second write is always marked as success by our mock, which means it will also not be queued and so + // not count to the number of queued frames. + assertTrue(encoder.writePing(ctx, false, 8, newPromise()).isSuccess()); + assertFalse(encoder.writePing(ctx, true, 8, newPromise()).isSuccess()); + + verifyFlushAndClose(0, false); + + assertFalse(encoder.writeSettingsAck(ctx, newPromise()).isDone()); + assertFalse(encoder.writeRstStream(ctx, 1, CANCEL.code(), newPromise()).isDone()); + assertFalse(encoder.writePing(ctx, true, 8, newPromise()).isSuccess()); + + verifyFlushAndClose(1, true); + } + + private void verifyFlushAndClose(int invocations, boolean failed) { + verify(ctx, atLeast(invocations)).flush(); + verify(ctx, times(invocations)).close(); + if (failed) { + verify(writer, times(1)).writeGoAway(eq(ctx), eq(0), eq(ENHANCE_YOUR_CALM.code()), + any(ByteBuf.class), any(ChannelPromise.class)); + } + } + + private ChannelPromise newPromise() { + return new DefaultChannelPromise(channel, ImmediateEventExecutor.INSTANCE); + } +} diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2DefaultFramesTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2DefaultFramesTest.java new file mode 100644 index 0000000..86457d4 --- /dev/null +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2DefaultFramesTest.java @@ -0,0 +1,44 @@ +/* + * Copyright 2019 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.codec.http2; + +import io.netty.buffer.DefaultByteBufHolder; +import io.netty.buffer.Unpooled; +import org.junit.Test; + +import static org.junit.Assert.assertFalse; + +public class Http2DefaultFramesTest { + + @SuppressWarnings("SimplifiableJUnitAssertion") + @Test + public void testEqualOperation() { + // in this case, 'goAwayFrame' and 'unknownFrame' will also have an EMPTY_BUFFER data + // so we want to check that 'dflt' will not consider them equal. + DefaultHttp2GoAwayFrame goAwayFrame = new DefaultHttp2GoAwayFrame(1); + DefaultHttp2UnknownFrame unknownFrame = new DefaultHttp2UnknownFrame((byte) 1, new Http2Flags((short) 1)); + DefaultByteBufHolder dflt = new DefaultByteBufHolder(Unpooled.EMPTY_BUFFER); + try { + // not using 'assertNotEquals' to be explicit about which object we are calling .equals() on + assertFalse(dflt.equals(goAwayFrame)); + assertFalse(dflt.equals(unknownFrame)); + } finally { + goAwayFrame.release(); + unknownFrame.release(); + dflt.release(); + } + } +} diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2EmptyDataFrameConnectionDecoderTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2EmptyDataFrameConnectionDecoderTest.java new file mode 100644 index 0000000..346a09a --- /dev/null +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2EmptyDataFrameConnectionDecoderTest.java @@ -0,0 +1,60 @@ +/* + * Copyright 2019 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.codec.http2; + +import org.hamcrest.CoreMatchers; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class Http2EmptyDataFrameConnectionDecoderTest { + + @Test + public void testDecoration() { + Http2ConnectionDecoder delegate = mock(Http2ConnectionDecoder.class); + final ArgumentCaptor<Http2FrameListener> listenerArgumentCaptor = + ArgumentCaptor.forClass(Http2FrameListener.class); + when(delegate.frameListener()).then(new Answer<Http2FrameListener>() { + @Override + public Http2FrameListener answer(InvocationOnMock invocationOnMock) { + return listenerArgumentCaptor.getValue(); + } + }); + Http2FrameListener listener = mock(Http2FrameListener.class); + Http2EmptyDataFrameConnectionDecoder decoder = new Http2EmptyDataFrameConnectionDecoder(delegate, 2); + decoder.frameListener(listener); + verify(delegate).frameListener(listenerArgumentCaptor.capture()); + + assertThat(decoder.frameListener(), + CoreMatchers.not(CoreMatchers.instanceOf(Http2EmptyDataFrameListener.class))); + assertThat(decoder.frameListener0(), CoreMatchers.instanceOf(Http2EmptyDataFrameListener.class)); + } + + @Test + public void testDecorationWithNull() { + Http2ConnectionDecoder delegate = mock(Http2ConnectionDecoder.class); + + Http2EmptyDataFrameConnectionDecoder decoder = new Http2EmptyDataFrameConnectionDecoder(delegate, 2); + decoder.frameListener(null); + assertNull(decoder.frameListener()); + } +} diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2EmptyDataFrameListenerTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2EmptyDataFrameListenerTest.java new file mode 100644 index 0000000..e7b2ac4 --- /dev/null +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2EmptyDataFrameListenerTest.java @@ -0,0 +1,142 @@ +/* + * Copyright 2019 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.codec.http2; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandlerContext; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; + +import static org.junit.Assert.fail; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.mockito.MockitoAnnotations.initMocks; + +public class Http2EmptyDataFrameListenerTest { + + @Mock + private Http2FrameListener frameListener; + @Mock + private ChannelHandlerContext ctx; + + @Mock + private ByteBuf nonEmpty; + + private Http2EmptyDataFrameListener listener; + + @Before + public void setUp() { + initMocks(this); + when(nonEmpty.isReadable()).thenReturn(true); + listener = new Http2EmptyDataFrameListener(frameListener, 2); + } + + @Test + public void testEmptyDataFrames() throws Http2Exception { + listener.onDataRead(ctx, 1, Unpooled.EMPTY_BUFFER, 0, false); + listener.onDataRead(ctx, 1, Unpooled.EMPTY_BUFFER, 0, false); + + try { + listener.onDataRead(ctx, 1, Unpooled.EMPTY_BUFFER, 0, false); + fail(); + } catch (Http2Exception expected) { + // expected + } + verify(frameListener, times(2)).onDataRead(eq(ctx), eq(1), any(ByteBuf.class), eq(0), eq(false)); + } + + @Test + public void testEmptyDataFramesWithNonEmptyInBetween() throws Http2Exception { + Http2EmptyDataFrameListener listener = new Http2EmptyDataFrameListener(frameListener, 2); + listener.onDataRead(ctx, 1, Unpooled.EMPTY_BUFFER, 0, false); + listener.onDataRead(ctx, 1, nonEmpty, 0, false); + + listener.onDataRead(ctx, 1, Unpooled.EMPTY_BUFFER, 0, false); + listener.onDataRead(ctx, 1, Unpooled.EMPTY_BUFFER, 0, false); + + try { + listener.onDataRead(ctx, 1, Unpooled.EMPTY_BUFFER, 0, false); + fail(); + } catch (Http2Exception expected) { + // expected + } + verify(frameListener, times(4)).onDataRead(eq(ctx), eq(1), any(ByteBuf.class), eq(0), eq(false)); + } + + @Test + public void testEmptyDataFramesWithEndOfStreamInBetween() throws Http2Exception { + Http2EmptyDataFrameListener listener = new Http2EmptyDataFrameListener(frameListener, 2); + listener.onDataRead(ctx, 1, Unpooled.EMPTY_BUFFER, 0, false); + listener.onDataRead(ctx, 1, Unpooled.EMPTY_BUFFER, 0, true); + + listener.onDataRead(ctx, 1, Unpooled.EMPTY_BUFFER, 0, false); + listener.onDataRead(ctx, 1, Unpooled.EMPTY_BUFFER, 0, false); + + try { + listener.onDataRead(ctx, 1, Unpooled.EMPTY_BUFFER, 0, false); + fail(); + } catch (Http2Exception expected) { + // expected + } + verify(frameListener, times(1)).onDataRead(eq(ctx), eq(1), any(ByteBuf.class), eq(0), eq(true)); + verify(frameListener, times(3)).onDataRead(eq(ctx), eq(1), any(ByteBuf.class), eq(0), eq(false)); + } + + @Test + public void testEmptyDataFramesWithHeaderFrameInBetween() throws Http2Exception { + Http2EmptyDataFrameListener listener = new Http2EmptyDataFrameListener(frameListener, 2); + listener.onDataRead(ctx, 1, Unpooled.EMPTY_BUFFER, 0, false); + listener.onHeadersRead(ctx, 1, EmptyHttp2Headers.INSTANCE, 0, true); + + listener.onDataRead(ctx, 1, Unpooled.EMPTY_BUFFER, 0, false); + listener.onDataRead(ctx, 1, Unpooled.EMPTY_BUFFER, 0, false); + + try { + listener.onDataRead(ctx, 1, Unpooled.EMPTY_BUFFER, 0, false); + fail(); + } catch (Http2Exception expected) { + // expected + } + + verify(frameListener, times(1)).onHeadersRead(eq(ctx), eq(1), eq(EmptyHttp2Headers.INSTANCE), eq(0), eq(true)); + verify(frameListener, times(3)).onDataRead(eq(ctx), eq(1), any(ByteBuf.class), eq(0), eq(false)); + } + + @Test + public void testEmptyDataFramesWithHeaderFrameInBetween2() throws Http2Exception { + Http2EmptyDataFrameListener listener = new Http2EmptyDataFrameListener(frameListener, 2); + listener.onDataRead(ctx, 1, Unpooled.EMPTY_BUFFER, 0, false); + listener.onHeadersRead(ctx, 1, EmptyHttp2Headers.INSTANCE, 0, (short) 0, false, 0, true); + + listener.onDataRead(ctx, 1, Unpooled.EMPTY_BUFFER, 0, false); + listener.onDataRead(ctx, 1, Unpooled.EMPTY_BUFFER, 0, false); + + try { + listener.onDataRead(ctx, 1, Unpooled.EMPTY_BUFFER, 0, false); + fail(); + } catch (Http2Exception expected) { + // expected + } + + verify(frameListener, times(1)).onHeadersRead(eq(ctx), eq(1), + eq(EmptyHttp2Headers.INSTANCE), eq(0), eq((short) 0), eq(false), eq(0), eq(true)); + verify(frameListener, times(3)).onDataRead(eq(ctx), eq(1), any(ByteBuf.class), eq(0), eq(false)); + } +} diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2FrameCodecTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2FrameCodecTest.java index 27d13cf..f2e3c93 100644 --- a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2FrameCodecTest.java +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2FrameCodecTest.java @@ -55,11 +55,11 @@ import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import static io.netty.handler.codec.http2.Http2CodecUtil.isStreamIdValid; +import static io.netty.handler.codec.http2.Http2Error.NO_ERROR; import static io.netty.handler.codec.http2.Http2TestUtil.anyChannelPromise; import static io.netty.handler.codec.http2.Http2TestUtil.anyHttp2Settings; import static io.netty.handler.codec.http2.Http2TestUtil.assertEqualsAndRelease; import static io.netty.handler.codec.http2.Http2TestUtil.bb; - import static org.hamcrest.Matchers.instanceOf; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -152,6 +152,8 @@ public class Http2FrameCodecTest { Http2SettingsFrame settingsFrame = inboundHandler.readInbound(); assertNotNull(settingsFrame); + Http2SettingsAckFrame settingsAckFrame = inboundHandler.readInbound(); + assertNotNull(settingsAckFrame); } @Test @@ -174,7 +176,7 @@ public class Http2FrameCodecTest { channel.writeOutbound(new DefaultHttp2HeadersFrame(response, true, 27).stream(stream2)); verify(frameWriter).writeHeaders( - eqFrameCodecCtx(), eq(1), eq(response), anyInt(), anyShort(), anyBoolean(), + eqFrameCodecCtx(), eq(1), eq(response), eq(27), eq(true), anyChannelPromise()); verify(frameWriter, never()).writeRstStream( eqFrameCodecCtx(), anyInt(), anyLong(), anyChannelPromise()); @@ -203,7 +205,7 @@ public class Http2FrameCodecTest { channel.writeOutbound(new DefaultHttp2HeadersFrame(response, true, 27).stream(stream2)); verify(frameWriter).writeHeaders( - eqFrameCodecCtx(), eq(1), eq(response), anyInt(), anyShort(), anyBoolean(), + eqFrameCodecCtx(), eq(1), eq(response), eq(27), eq(true), anyChannelPromise()); verify(frameWriter, never()).writeRstStream( eqFrameCodecCtx(), anyInt(), anyLong(), anyChannelPromise()); @@ -217,7 +219,7 @@ public class Http2FrameCodecTest { Http2Connection conn = new DefaultHttp2Connection(true); Http2ConnectionEncoder enc = new DefaultHttp2ConnectionEncoder(conn, new DefaultHttp2FrameWriter()); Http2ConnectionDecoder dec = new DefaultHttp2ConnectionDecoder(conn, enc, new DefaultHttp2FrameReader()); - Http2FrameCodec codec = new Http2FrameCodec(enc, dec, new Http2Settings()); + Http2FrameCodec codec = new Http2FrameCodec(enc, dec, new Http2Settings(), false); EmbeddedChannel em = new EmbeddedChannel(codec); // We call #consumeBytes on a stream id which has not been seen yet to emulate the case @@ -250,8 +252,8 @@ public class Http2FrameCodecTest { assertNull(inboundHandler.readInbound()); channel.writeOutbound(new DefaultHttp2HeadersFrame(response, false).stream(stream2)); - verify(frameWriter).writeHeaders(eqFrameCodecCtx(), eq(1), eq(response), anyInt(), - anyShort(), anyBoolean(), eq(0), eq(false), anyChannelPromise()); + verify(frameWriter).writeHeaders(eqFrameCodecCtx(), eq(1), eq(response), + eq(0), eq(false), anyChannelPromise()); channel.writeOutbound(new DefaultHttp2DataFrame(bb("world"), true, 27).stream(stream2)); ArgumentCaptor<ByteBuf> outboundData = ArgumentCaptor.forClass(ByteBuf.class); @@ -302,9 +304,9 @@ public class Http2FrameCodecTest { Http2HeadersFrame actualHeaders = inboundHandler.readInbound(); assertEquals(expectedHeaders.stream(actualHeaders.stream()), actualHeaders); - frameInboundWriter.writeInboundRstStream(3, Http2Error.NO_ERROR.code()); + frameInboundWriter.writeInboundRstStream(3, NO_ERROR.code()); - Http2ResetFrame expectedRst = new DefaultHttp2ResetFrame(Http2Error.NO_ERROR).stream(actualHeaders.stream()); + Http2ResetFrame expectedRst = new DefaultHttp2ResetFrame(NO_ERROR).stream(actualHeaders.stream()); Http2ResetFrame actualRst = inboundHandler.readInbound(); assertEquals(expectedRst, actualRst); @@ -321,13 +323,13 @@ public class Http2FrameCodecTest { ByteBuf debugData = bb("debug"); ByteBuf expected = debugData.copy(); - Http2GoAwayFrame goAwayFrame = new DefaultHttp2GoAwayFrame(Http2Error.NO_ERROR.code(), debugData); + Http2GoAwayFrame goAwayFrame = new DefaultHttp2GoAwayFrame(NO_ERROR.code(), + debugData.retainedDuplicate()); goAwayFrame.setExtraStreamIds(2); channel.writeOutbound(goAwayFrame); verify(frameWriter).writeGoAway(eqFrameCodecCtx(), eq(7), - eq(Http2Error.NO_ERROR.code()), eq(expected), anyChannelPromise()); - assertEquals(1, debugData.refCnt()); + eq(NO_ERROR.code()), eq(expected), anyChannelPromise()); assertEquals(State.OPEN, stream.state()); assertTrue(channel.isActive()); expected.release(); @@ -337,8 +339,8 @@ public class Http2FrameCodecTest { @Test public void receiveGoaway() throws Exception { ByteBuf debugData = bb("foo"); - frameInboundWriter.writeInboundGoAway(2, Http2Error.NO_ERROR.code(), debugData); - Http2GoAwayFrame expectedFrame = new DefaultHttp2GoAwayFrame(2, Http2Error.NO_ERROR.code(), bb("foo")); + frameInboundWriter.writeInboundGoAway(2, NO_ERROR.code(), debugData); + Http2GoAwayFrame expectedFrame = new DefaultHttp2GoAwayFrame(2, NO_ERROR.code(), bb("foo")); Http2GoAwayFrame actualFrame = inboundHandler.readInbound(); assertEqualsAndRelease(expectedFrame, actualFrame); @@ -384,14 +386,15 @@ public class Http2FrameCodecTest { assertEquals(State.OPEN, stream.state()); ByteBuf debugData = bb("debug"); - Http2GoAwayFrame goAwayFrame = new DefaultHttp2GoAwayFrame(Http2Error.NO_ERROR.code(), debugData.slice()); + Http2GoAwayFrame goAwayFrame = new DefaultHttp2GoAwayFrame(NO_ERROR.code(), + debugData.retainedDuplicate()); goAwayFrame.setExtraStreamIds(Integer.MAX_VALUE); channel.writeOutbound(goAwayFrame); // When the last stream id computation overflows, the last stream id should just be set to 2^31 - 1. verify(frameWriter).writeGoAway(eqFrameCodecCtx(), eq(Integer.MAX_VALUE), - eq(Http2Error.NO_ERROR.code()), eq(debugData), anyChannelPromise()); - assertEquals(1, debugData.refCnt()); + eq(NO_ERROR.code()), eq(debugData), anyChannelPromise()); + debugData.release(); assertEquals(State.OPEN, stream.state()); assertTrue(channel.isActive()); } @@ -662,6 +665,33 @@ public class Http2FrameCodecTest { assertFalse(channel.finishAndReleaseAll()); } + @Test + public void doNotLeakOnFailedInitializationForChannels() throws Exception { + setUp(Http2FrameCodecBuilder.forServer(), new Http2Settings().maxConcurrentStreams(2)); + + Http2FrameStream stream1 = frameCodec.newStream(); + Http2FrameStream stream2 = frameCodec.newStream(); + + ChannelPromise stream1HeaderPromise = channel.newPromise(); + ChannelPromise stream2HeaderPromise = channel.newPromise(); + + channel.writeAndFlush(new DefaultHttp2HeadersFrame(new DefaultHttp2Headers()).stream(stream1), + stream1HeaderPromise); + channel.runPendingTasks(); + + frameInboundWriter.writeInboundGoAway(stream1.id(), 0L, Unpooled.EMPTY_BUFFER); + + channel.writeAndFlush(new DefaultHttp2HeadersFrame(new DefaultHttp2Headers()).stream(stream2), + stream2HeaderPromise); + channel.runPendingTasks(); + + assertTrue(stream1HeaderPromise.syncUninterruptibly().isSuccess()); + assertTrue(stream2HeaderPromise.isDone()); + + assertEquals(0, frameCodec.numInitializingStreams()); + assertFalse(channel.finishAndReleaseAll()); + } + @Test public void streamIdentifiersExhausted() throws Http2Exception { int maxServerStreamId = Integer.MAX_VALUE - 1; @@ -674,6 +704,11 @@ public class Http2FrameCodecTest { ChannelPromise writePromise = channel.newPromise(); channel.writeAndFlush(new DefaultHttp2HeadersFrame(new DefaultHttp2Headers()).stream(stream), writePromise); + Http2GoAwayFrame goAwayFrame = inboundHandler.readInbound(); + assertNotNull(goAwayFrame); + assertEquals(NO_ERROR.code(), goAwayFrame.errorCode()); + assertEquals(Integer.MAX_VALUE, goAwayFrame.lastStreamId()); + goAwayFrame.release(); assertThat(writePromise.cause(), instanceOf(Http2NoMoreStreamIdsException.class)); } @@ -751,6 +786,30 @@ public class Http2FrameCodecTest { assertEquals(expectedStreams, activeStreams); } + @Test + public void autoAckPingTrue() throws Exception { + setUp(Http2FrameCodecBuilder.forServer().autoAckPingFrame(true), new Http2Settings()); + frameInboundWriter.writeInboundPing(false, 8); + Http2PingFrame frame = inboundHandler.readInbound(); + assertFalse(frame.ack()); + assertEquals(8, frame.content()); + verify(frameWriter).writePing(eqFrameCodecCtx(), eq(true), eq(8L), anyChannelPromise()); + } + + @Test + public void autoAckPingFalse() throws Exception { + setUp(Http2FrameCodecBuilder.forServer().autoAckPingFrame(false), new Http2Settings()); + frameInboundWriter.writeInboundPing(false, 8); + verify(frameWriter, never()).writePing(eqFrameCodecCtx(), eq(true), eq(8L), anyChannelPromise()); + Http2PingFrame frame = inboundHandler.readInbound(); + assertFalse(frame.ack()); + assertEquals(8, frame.content()); + + // Now ack the frame manually. + channel.writeAndFlush(new DefaultHttp2PingFrame(8, true)); + verify(frameWriter).writePing(eqFrameCodecCtx(), eq(true), eq(8L), anyChannelPromise()); + } + @Test public void streamShouldBeOpenInListener() { final Http2FrameStream stream2 = frameCodec.newStream(); diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MultiplexClientUpgradeTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MultiplexClientUpgradeTest.java new file mode 100644 index 0000000..dcbece6 --- /dev/null +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MultiplexClientUpgradeTest.java @@ -0,0 +1,89 @@ +/* + * Copyright 2018 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.codec.http2; + +import org.junit.Test; + +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.embedded.EmbeddedChannel; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +public abstract class Http2MultiplexClientUpgradeTest<C extends Http2FrameCodec> { + + @ChannelHandler.Sharable + static final class NoopHandler extends ChannelInboundHandlerAdapter { + @Override + public void channelActive(ChannelHandlerContext ctx) { + ctx.channel().close(); + } + } + + private static final class UpgradeHandler extends ChannelInboundHandlerAdapter { + Http2Stream.State stateOnActive; + int streamId; + boolean channelInactiveCalled; + + @Override + public void channelActive(ChannelHandlerContext ctx) throws Exception { + Http2StreamChannel ch = (Http2StreamChannel) ctx.channel(); + stateOnActive = ch.stream().state(); + streamId = ch.stream().id(); + super.channelActive(ctx); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) throws Exception { + channelInactiveCalled = true; + super.channelInactive(ctx); + } + } + + protected abstract C newCodec(ChannelHandler upgradeHandler); + + protected abstract ChannelHandler newMultiplexer(ChannelHandler upgradeHandler); + + @Test + public void upgradeHandlerGetsActivated() throws Exception { + UpgradeHandler upgradeHandler = new UpgradeHandler(); + C codec = newCodec(upgradeHandler); + EmbeddedChannel ch = new EmbeddedChannel(codec, newMultiplexer(upgradeHandler)); + + codec.onHttpClientUpgrade(); + + assertFalse(upgradeHandler.stateOnActive.localSideOpen()); + assertTrue(upgradeHandler.stateOnActive.remoteSideOpen()); + assertNotNull(codec.connection().stream(Http2CodecUtil.HTTP_UPGRADE_STREAM_ID).getProperty(codec.streamKey)); + assertEquals(Http2CodecUtil.HTTP_UPGRADE_STREAM_ID, upgradeHandler.streamId); + assertTrue(ch.finishAndReleaseAll()); + assertTrue(upgradeHandler.channelInactiveCalled); + } + + @Test(expected = Http2Exception.class) + public void clientUpgradeWithoutUpgradeHandlerThrowsHttp2Exception() throws Http2Exception { + C codec = newCodec(null); + EmbeddedChannel ch = new EmbeddedChannel(codec, newMultiplexer(null)); + try { + codec.onHttpClientUpgrade(); + } finally { + assertTrue(ch.finishAndReleaseAll()); + } + } +} diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MultiplexCodecClientUpgradeTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MultiplexCodecClientUpgradeTest.java index 26b63ed..b917feb 100644 --- a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MultiplexCodecClientUpgradeTest.java +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MultiplexCodecClientUpgradeTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 The Netty Project + * Copyright 2019 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 @@ -14,68 +14,21 @@ */ package io.netty.handler.codec.http2; -import org.junit.Test; - import io.netty.channel.ChannelHandler; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelInboundHandlerAdapter; -import io.netty.channel.embedded.EmbeddedChannel; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -public class Http2MultiplexCodecClientUpgradeTest { - - @ChannelHandler.Sharable - private final class NoopHandler extends ChannelInboundHandlerAdapter { - @Override - public void channelActive(ChannelHandlerContext ctx) { - ctx.channel().close(); - } - } - private final class UpgradeHandler extends ChannelInboundHandlerAdapter { - Http2Stream.State stateOnActive; - int streamId; +public class Http2MultiplexCodecClientUpgradeTest extends Http2MultiplexClientUpgradeTest<Http2MultiplexCodec> { - @Override - public void channelActive(ChannelHandlerContext ctx) throws Exception { - Http2StreamChannel ch = (Http2StreamChannel) ctx.channel(); - stateOnActive = ch.stream().state(); - streamId = ch.stream().id(); - super.channelActive(ctx); - } - } - - private Http2MultiplexCodec newCodec(ChannelHandler upgradeHandler) { + @Override + protected Http2MultiplexCodec newCodec(ChannelHandler upgradeHandler) { Http2MultiplexCodecBuilder builder = Http2MultiplexCodecBuilder.forClient(new NoopHandler()); - builder.withUpgradeStreamHandler(upgradeHandler); + if (upgradeHandler != null) { + builder.withUpgradeStreamHandler(upgradeHandler); + } return builder.build(); } - @Test - public void upgradeHandlerGetsActivated() throws Exception { - UpgradeHandler upgradeHandler = new UpgradeHandler(); - Http2MultiplexCodec codec = newCodec(upgradeHandler); - EmbeddedChannel ch = new EmbeddedChannel(codec); - - codec.onHttpClientUpgrade(); - - assertFalse(upgradeHandler.stateOnActive.localSideOpen()); - assertTrue(upgradeHandler.stateOnActive.remoteSideOpen()); - assertEquals(1, upgradeHandler.streamId); - assertTrue(ch.finishAndReleaseAll()); - } - - @Test(expected = Http2Exception.class) - public void clientUpgradeWithoutUpgradeHandlerThrowsHttp2Exception() throws Http2Exception { - Http2MultiplexCodec codec = Http2MultiplexCodecBuilder.forClient(new NoopHandler()).build(); - EmbeddedChannel ch = new EmbeddedChannel(codec); - try { - codec.onHttpClientUpgrade(); - } finally { - assertTrue(ch.finishAndReleaseAll()); - } + @Override + protected ChannelHandler newMultiplexer(ChannelHandler upgradeHandler) { + return null; } } diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MultiplexCodecTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MultiplexCodecTest.java index 7788e6d..d9c20f9 100644 --- a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MultiplexCodecTest.java +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MultiplexCodecTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 The Netty Project + * Copyright 2019 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 @@ -14,994 +14,27 @@ */ package io.netty.handler.codec.http2; -import io.netty.buffer.ByteBuf; -import io.netty.buffer.Unpooled; -import io.netty.channel.Channel; -import io.netty.channel.ChannelFuture; -import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandler; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelInboundHandlerAdapter; -import io.netty.channel.ChannelPromise; -import io.netty.channel.embedded.EmbeddedChannel; -import io.netty.handler.codec.http.HttpMethod; -import io.netty.handler.codec.http.HttpScheme; -import io.netty.handler.codec.http2.Http2Exception.StreamException; -import io.netty.handler.codec.http2.LastInboundHandler.Consumer; -import io.netty.util.AsciiString; -import io.netty.util.AttributeKey; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.mockito.ArgumentMatcher; -import org.mockito.Mockito; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; -import java.net.InetSocketAddress; -import java.nio.channels.ClosedChannelException; -import java.util.ArrayDeque; -import java.util.Queue; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; +public class Http2MultiplexCodecTest extends Http2MultiplexTest<Http2FrameCodec> { -import static io.netty.util.ReferenceCountUtil.release; -import static io.netty.handler.codec.http2.Http2TestUtil.anyChannelPromise; -import static io.netty.handler.codec.http2.Http2TestUtil.anyHttp2Settings; -import static io.netty.handler.codec.http2.Http2TestUtil.assertEqualsAndRelease; -import static io.netty.handler.codec.http2.Http2TestUtil.bb; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.anyShort; -import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -/** - * Unit tests for {@link Http2MultiplexCodec}. - */ -public class Http2MultiplexCodecTest { - private final Http2Headers request = new DefaultHttp2Headers() - .method(HttpMethod.GET.asciiName()).scheme(HttpScheme.HTTPS.name()) - .authority(new AsciiString("example.org")).path(new AsciiString("/foo")); - - private EmbeddedChannel parentChannel; - private Http2FrameWriter frameWriter; - private Http2FrameInboundWriter frameInboundWriter; - private TestChannelInitializer childChannelInitializer; - private Http2MultiplexCodec codec; - - private static final int initialRemoteStreamWindow = 1024; - - @Before - public void setUp() { - childChannelInitializer = new TestChannelInitializer(); - parentChannel = new EmbeddedChannel(); - frameInboundWriter = new Http2FrameInboundWriter(parentChannel); - parentChannel.connect(new InetSocketAddress(0)); - frameWriter = Http2TestUtil.mockedFrameWriter(); - codec = new Http2MultiplexCodecBuilder(true, childChannelInitializer).frameWriter(frameWriter).build(); - parentChannel.pipeline().addLast(codec); - parentChannel.runPendingTasks(); - parentChannel.pipeline().fireChannelActive(); - - parentChannel.writeInbound(Http2CodecUtil.connectionPrefaceBuf()); - - Http2Settings settings = new Http2Settings().initialWindowSize(initialRemoteStreamWindow); - frameInboundWriter.writeInboundSettings(settings); - - verify(frameWriter).writeSettingsAck(eqMultiplexCodecCtx(), anyChannelPromise()); - - frameInboundWriter.writeInboundSettingsAck(); - - Http2SettingsFrame settingsFrame = parentChannel.readInbound(); - assertNotNull(settingsFrame); - - // Handshake - verify(frameWriter).writeSettings(eqMultiplexCodecCtx(), - anyHttp2Settings(), anyChannelPromise()); - } - - private ChannelHandlerContext eqMultiplexCodecCtx() { - return eq(codec.ctx); - } - - @After - public void tearDown() throws Exception { - if (childChannelInitializer.handler instanceof LastInboundHandler) { - ((LastInboundHandler) childChannelInitializer.handler).finishAndReleaseAll(); - } - parentChannel.finishAndReleaseAll(); - codec = null; - } - - // TODO(buchgr): Flush from child channel - // TODO(buchgr): ChildChannel.childReadComplete() - // TODO(buchgr): GOAWAY Logic - // TODO(buchgr): Test ChannelConfig.setMaxMessagesPerRead - - @Test - public void writeUnknownFrame() { - Http2StreamChannel childChannel = newOutboundStream(new ChannelInboundHandlerAdapter() { - @Override - public void channelActive(ChannelHandlerContext ctx) { - ctx.writeAndFlush(new DefaultHttp2HeadersFrame(new DefaultHttp2Headers())); - ctx.writeAndFlush(new DefaultHttp2UnknownFrame((byte) 99, new Http2Flags())); - ctx.fireChannelActive(); - } - }); - assertTrue(childChannel.isActive()); - - parentChannel.runPendingTasks(); - - verify(frameWriter).writeFrame(eq(codec.ctx), eq((byte) 99), eqStreamId(childChannel), any(Http2Flags.class), - any(ByteBuf.class), any(ChannelPromise.class)); - } - - private Http2StreamChannel newInboundStream(int streamId, boolean endStream, final ChannelHandler childHandler) { - return newInboundStream(streamId, endStream, null, childHandler); - } - - private Http2StreamChannel newInboundStream(int streamId, boolean endStream, - AtomicInteger maxReads, final ChannelHandler childHandler) { - final AtomicReference<Http2StreamChannel> streamChannelRef = new AtomicReference<Http2StreamChannel>(); - childChannelInitializer.maxReads = maxReads; - childChannelInitializer.handler = new ChannelInboundHandlerAdapter() { - @Override - public void channelRegistered(ChannelHandlerContext ctx) { - assertNull(streamChannelRef.get()); - streamChannelRef.set((Http2StreamChannel) ctx.channel()); - ctx.pipeline().addLast(childHandler); - ctx.fireChannelRegistered(); - } - }; - - frameInboundWriter.writeInboundHeaders(streamId, request, 0, endStream); - parentChannel.runPendingTasks(); - Http2StreamChannel channel = streamChannelRef.get(); - assertEquals(streamId, channel.stream().id()); - return channel; - } - - @Test - public void readUnkownFrame() { - LastInboundHandler handler = new LastInboundHandler(); - - Http2StreamChannel channel = newInboundStream(3, true, handler); - frameInboundWriter.writeInboundFrame((byte) 99, channel.stream().id(), new Http2Flags(), Unpooled.EMPTY_BUFFER); - - // header frame and unknown frame - verifyFramesMultiplexedToCorrectChannel(channel, handler, 2); - - Channel childChannel = newOutboundStream(new ChannelInboundHandlerAdapter()); - assertTrue(childChannel.isActive()); - } - - @Test - public void headerAndDataFramesShouldBeDelivered() { - LastInboundHandler inboundHandler = new LastInboundHandler(); - - Http2StreamChannel channel = newInboundStream(3, false, inboundHandler); - Http2HeadersFrame headersFrame = new DefaultHttp2HeadersFrame(request).stream(channel.stream()); - Http2DataFrame dataFrame1 = new DefaultHttp2DataFrame(bb("hello")).stream(channel.stream()); - Http2DataFrame dataFrame2 = new DefaultHttp2DataFrame(bb("world")).stream(channel.stream()); - - assertTrue(inboundHandler.isChannelActive()); - frameInboundWriter.writeInboundData(channel.stream().id(), bb("hello"), 0, false); - frameInboundWriter.writeInboundData(channel.stream().id(), bb("world"), 0, false); - - assertEquals(headersFrame, inboundHandler.readInbound()); - - assertEqualsAndRelease(dataFrame1, inboundHandler.<Http2Frame>readInbound()); - assertEqualsAndRelease(dataFrame2, inboundHandler.<Http2Frame>readInbound()); - - assertNull(inboundHandler.readInbound()); - } - - @Test - public void framesShouldBeMultiplexed() { - LastInboundHandler handler1 = new LastInboundHandler(); - Http2StreamChannel channel1 = newInboundStream(3, false, handler1); - LastInboundHandler handler2 = new LastInboundHandler(); - Http2StreamChannel channel2 = newInboundStream(5, false, handler2); - LastInboundHandler handler3 = new LastInboundHandler(); - Http2StreamChannel channel3 = newInboundStream(11, false, handler3); - - verifyFramesMultiplexedToCorrectChannel(channel1, handler1, 1); - verifyFramesMultiplexedToCorrectChannel(channel2, handler2, 1); - verifyFramesMultiplexedToCorrectChannel(channel3, handler3, 1); - - frameInboundWriter.writeInboundData(channel2.stream().id(), bb("hello"), 0, false); - frameInboundWriter.writeInboundData(channel1.stream().id(), bb("foo"), 0, true); - frameInboundWriter.writeInboundData(channel2.stream().id(), bb("world"), 0, true); - frameInboundWriter.writeInboundData(channel3.stream().id(), bb("bar"), 0, true); - - verifyFramesMultiplexedToCorrectChannel(channel1, handler1, 1); - verifyFramesMultiplexedToCorrectChannel(channel2, handler2, 2); - verifyFramesMultiplexedToCorrectChannel(channel3, handler3, 1); - } - - @Test - public void inboundDataFrameShouldUpdateLocalFlowController() throws Http2Exception { - Http2LocalFlowController flowController = Mockito.mock(Http2LocalFlowController.class); - codec.connection().local().flowController(flowController); - - LastInboundHandler handler = new LastInboundHandler(); - final Http2StreamChannel channel = newInboundStream(3, false, handler); - - ByteBuf tenBytes = bb("0123456789"); - - frameInboundWriter.writeInboundData(channel.stream().id(), tenBytes, 0, true); - - // Verify we marked the bytes as consumed - verify(flowController).consumeBytes(argThat(new ArgumentMatcher<Http2Stream>() { - @Override - public boolean matches(Http2Stream http2Stream) { - return http2Stream.id() == channel.stream().id(); - } - }), eq(10)); - - // headers and data frame - verifyFramesMultiplexedToCorrectChannel(channel, handler, 2); - } - - @Test - public void unhandledHttp2FramesShouldBePropagated() { - Http2PingFrame pingFrame = new DefaultHttp2PingFrame(0); - frameInboundWriter.writeInboundPing(false, 0); - assertEquals(parentChannel.readInbound(), pingFrame); - - DefaultHttp2GoAwayFrame goAwayFrame = new DefaultHttp2GoAwayFrame(1, - parentChannel.alloc().buffer().writeLong(8)); - frameInboundWriter.writeInboundGoAway(0, goAwayFrame.errorCode(), goAwayFrame.content().retainedDuplicate()); - - Http2GoAwayFrame frame = parentChannel.readInbound(); - assertEqualsAndRelease(frame, goAwayFrame); - } - - @Test - public void channelReadShouldRespectAutoRead() { - LastInboundHandler inboundHandler = new LastInboundHandler(); - Http2StreamChannel childChannel = newInboundStream(3, false, inboundHandler); - assertTrue(childChannel.config().isAutoRead()); - Http2HeadersFrame headersFrame = inboundHandler.readInbound(); - assertNotNull(headersFrame); - - childChannel.config().setAutoRead(false); - - frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("hello world"), 0, false); - Http2DataFrame dataFrame0 = inboundHandler.readInbound(); - assertNotNull(dataFrame0); - release(dataFrame0); - - frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("foo"), 0, false); - frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("bar"), 0, false); - - assertNull(inboundHandler.readInbound()); - - childChannel.config().setAutoRead(true); - verifyFramesMultiplexedToCorrectChannel(childChannel, inboundHandler, 2); - } - - @Test - public void readInChannelReadWithoutAutoRead() { - useReadWithoutAutoRead(false); - } - - @Test - public void readInChannelReadCompleteWithoutAutoRead() { - useReadWithoutAutoRead(true); - } - - private void useReadWithoutAutoRead(final boolean readComplete) { - LastInboundHandler inboundHandler = new LastInboundHandler(); - Http2StreamChannel childChannel = newInboundStream(3, false, inboundHandler); - assertTrue(childChannel.config().isAutoRead()); - childChannel.config().setAutoRead(false); - assertFalse(childChannel.config().isAutoRead()); - - Http2HeadersFrame headersFrame = inboundHandler.readInbound(); - assertNotNull(headersFrame); - - // Add a handler which will request reads. - childChannel.pipeline().addFirst(new ChannelInboundHandlerAdapter() { - @Override - public void channelRead(ChannelHandlerContext ctx, Object msg) { - ctx.fireChannelRead(msg); - if (!readComplete) { - ctx.read(); - } - } - - @Override - public void channelReadComplete(ChannelHandlerContext ctx) { - ctx.fireChannelReadComplete(); - if (readComplete) { - ctx.read(); - } - } - }); - - frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("hello world"), 0, false); - frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("foo"), 0, false); - frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("bar"), 0, false); - frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("hello world"), 0, false); - frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("foo"), 0, false); - frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("bar"), 0, true); - - verifyFramesMultiplexedToCorrectChannel(childChannel, inboundHandler, 6); - } - - private Http2StreamChannel newOutboundStream(ChannelHandler handler) { - return new Http2StreamChannelBootstrap(parentChannel).handler(handler) - .open().syncUninterruptibly().getNow(); - } - - /** - * A child channel for a HTTP/2 stream in IDLE state (that is no headers sent or received), - * should not emit a RST_STREAM frame on close, as this is a connection error of type protocol error. - */ - @Test - public void idleOutboundStreamShouldNotWriteResetFrameOnClose() { - LastInboundHandler handler = new LastInboundHandler(); - - Channel childChannel = newOutboundStream(handler); - assertTrue(childChannel.isActive()); - - childChannel.close(); - parentChannel.runPendingTasks(); - - assertFalse(childChannel.isOpen()); - assertFalse(childChannel.isActive()); - assertNull(parentChannel.readOutbound()); - } - - @Test - public void outboundStreamShouldWriteResetFrameOnClose_headersSent() { - ChannelHandler handler = new ChannelInboundHandlerAdapter() { - @Override - public void channelActive(ChannelHandlerContext ctx) { - ctx.writeAndFlush(new DefaultHttp2HeadersFrame(new DefaultHttp2Headers())); - ctx.fireChannelActive(); - } - }; - - Http2StreamChannel childChannel = newOutboundStream(handler); - assertTrue(childChannel.isActive()); - - childChannel.close(); - verify(frameWriter).writeRstStream(eqMultiplexCodecCtx(), - eqStreamId(childChannel), eq(Http2Error.CANCEL.code()), anyChannelPromise()); - } - - @Test - public void outboundStreamShouldNotWriteResetFrameOnClose_IfStreamDidntExist() { - when(frameWriter.writeHeaders(eqMultiplexCodecCtx(), anyInt(), - any(Http2Headers.class), anyInt(), anyShort(), anyBoolean(), anyInt(), anyBoolean(), - any(ChannelPromise.class))).thenAnswer(new Answer<ChannelFuture>() { - - private boolean headersWritten; - @Override - public ChannelFuture answer(InvocationOnMock invocationOnMock) { - // We want to fail to write the first headers frame. This is what happens if the connection - // refuses to allocate a new stream due to having received a GOAWAY. - if (!headersWritten) { - headersWritten = true; - return ((ChannelPromise) invocationOnMock.getArgument(8)).setFailure(new Exception("boom")); - } - return ((ChannelPromise) invocationOnMock.getArgument(8)).setSuccess(); - } - }); - - Http2StreamChannel childChannel = newOutboundStream(new ChannelInboundHandlerAdapter() { - @Override - public void channelActive(ChannelHandlerContext ctx) { - ctx.writeAndFlush(new DefaultHttp2HeadersFrame(new DefaultHttp2Headers())); - ctx.fireChannelActive(); - } - }); - - assertFalse(childChannel.isActive()); - - childChannel.close(); - parentChannel.runPendingTasks(); - // The channel was never active so we should not generate a RST frame. - verify(frameWriter, never()).writeRstStream(eqMultiplexCodecCtx(), eqStreamId(childChannel), anyLong(), - anyChannelPromise()); - - assertTrue(parentChannel.outboundMessages().isEmpty()); - } - - @Test - public void inboundRstStreamFireChannelInactive() { - LastInboundHandler inboundHandler = new LastInboundHandler(); - Http2StreamChannel channel = newInboundStream(3, false, inboundHandler); - assertTrue(inboundHandler.isChannelActive()); - frameInboundWriter.writeInboundRstStream(channel.stream().id(), Http2Error.INTERNAL_ERROR.code()); - - assertFalse(inboundHandler.isChannelActive()); - - // A RST_STREAM frame should NOT be emitted, as we received a RST_STREAM. - verify(frameWriter, Mockito.never()).writeRstStream(eqMultiplexCodecCtx(), eqStreamId(channel), - anyLong(), anyChannelPromise()); - } - - @Test(expected = StreamException.class) - public void streamExceptionTriggersChildChannelExceptionAndClose() throws Exception { - LastInboundHandler inboundHandler = new LastInboundHandler(); - Http2StreamChannel channel = newInboundStream(3, false, inboundHandler); - assertTrue(channel.isActive()); - StreamException cause = new StreamException(channel.stream().id(), Http2Error.PROTOCOL_ERROR, "baaam!"); - parentChannel.pipeline().fireExceptionCaught(cause); - - assertFalse(channel.isActive()); - inboundHandler.checkException(); - } - - @Test(expected = ClosedChannelException.class) - public void streamClosedErrorTranslatedToClosedChannelExceptionOnWrites() throws Exception { - LastInboundHandler inboundHandler = new LastInboundHandler(); - - final Http2StreamChannel childChannel = newOutboundStream(inboundHandler); - assertTrue(childChannel.isActive()); - - Http2Headers headers = new DefaultHttp2Headers(); - when(frameWriter.writeHeaders(eqMultiplexCodecCtx(), anyInt(), - eq(headers), anyInt(), anyShort(), anyBoolean(), anyInt(), anyBoolean(), - any(ChannelPromise.class))).thenAnswer(new Answer<ChannelFuture>() { - @Override - public ChannelFuture answer(InvocationOnMock invocationOnMock) { - return ((ChannelPromise) invocationOnMock.getArgument(8)).setFailure( - new StreamException(childChannel.stream().id(), Http2Error.STREAM_CLOSED, "Stream Closed")); - } - }); - ChannelFuture future = childChannel.writeAndFlush(new DefaultHttp2HeadersFrame(new DefaultHttp2Headers())); - - parentChannel.flush(); - - assertFalse(childChannel.isActive()); - assertFalse(childChannel.isOpen()); - - inboundHandler.checkException(); - - future.syncUninterruptibly(); - } - - @Test - public void creatingWritingReadingAndClosingOutboundStreamShouldWork() { - LastInboundHandler inboundHandler = new LastInboundHandler(); - Http2StreamChannel childChannel = newOutboundStream(inboundHandler); - assertTrue(childChannel.isActive()); - assertTrue(inboundHandler.isChannelActive()); - - // Write to the child channel - Http2Headers headers = new DefaultHttp2Headers().scheme("https").method("GET").path("/foo.txt"); - childChannel.writeAndFlush(new DefaultHttp2HeadersFrame(headers)); - - // Read from the child channel - frameInboundWriter.writeInboundHeaders(childChannel.stream().id(), headers, 0, false); - - Http2HeadersFrame headersFrame = inboundHandler.readInbound(); - assertNotNull(headersFrame); - assertEquals(headers, headersFrame.headers()); - - // Close the child channel. - childChannel.close(); - - parentChannel.runPendingTasks(); - // An active outbound stream should emit a RST_STREAM frame. - verify(frameWriter).writeRstStream(eqMultiplexCodecCtx(), eqStreamId(childChannel), - anyLong(), anyChannelPromise()); - - assertFalse(childChannel.isOpen()); - assertFalse(childChannel.isActive()); - assertFalse(inboundHandler.isChannelActive()); - } - - // Test failing the promise of the first headers frame of an outbound stream. In practice this error case would most - // likely happen due to the max concurrent streams limit being hit or the channel running out of stream identifiers. - // - @Test(expected = Http2NoMoreStreamIdsException.class) - public void failedOutboundStreamCreationThrowsAndClosesChannel() throws Exception { - LastInboundHandler handler = new LastInboundHandler(); - Http2StreamChannel childChannel = newOutboundStream(handler); - assertTrue(childChannel.isActive()); - - Http2Headers headers = new DefaultHttp2Headers(); - when(frameWriter.writeHeaders(eqMultiplexCodecCtx(), anyInt(), - eq(headers), anyInt(), anyShort(), anyBoolean(), anyInt(), anyBoolean(), - any(ChannelPromise.class))).thenAnswer(new Answer<ChannelFuture>() { - @Override - public ChannelFuture answer(InvocationOnMock invocationOnMock) { - return ((ChannelPromise) invocationOnMock.getArgument(8)).setFailure( - new Http2NoMoreStreamIdsException()); - } - }); - - ChannelFuture future = childChannel.writeAndFlush(new DefaultHttp2HeadersFrame(headers)); - parentChannel.flush(); - - assertFalse(childChannel.isActive()); - assertFalse(childChannel.isOpen()); - - handler.checkException(); - - future.syncUninterruptibly(); - } - - @Test - public void channelClosedWhenCloseListenerCompletes() { - LastInboundHandler inboundHandler = new LastInboundHandler(); - Http2StreamChannel childChannel = newInboundStream(3, false, inboundHandler); - - assertTrue(childChannel.isOpen()); - assertTrue(childChannel.isActive()); - - final AtomicBoolean channelOpen = new AtomicBoolean(true); - final AtomicBoolean channelActive = new AtomicBoolean(true); - - // Create a promise before actually doing the close, because otherwise we would be adding a listener to a future - // that is already completed because we are using EmbeddedChannel which executes code in the JUnit thread. - ChannelPromise p = childChannel.newPromise(); - p.addListener(new ChannelFutureListener() { - @Override - public void operationComplete(ChannelFuture future) { - channelOpen.set(future.channel().isOpen()); - channelActive.set(future.channel().isActive()); - } - }); - childChannel.close(p).syncUninterruptibly(); - - assertFalse(channelOpen.get()); - assertFalse(channelActive.get()); - assertFalse(childChannel.isActive()); - } - - @Test - public void channelClosedWhenChannelClosePromiseCompletes() { - LastInboundHandler inboundHandler = new LastInboundHandler(); - Http2StreamChannel childChannel = newInboundStream(3, false, inboundHandler); - - assertTrue(childChannel.isOpen()); - assertTrue(childChannel.isActive()); - - final AtomicBoolean channelOpen = new AtomicBoolean(true); - final AtomicBoolean channelActive = new AtomicBoolean(true); - - childChannel.closeFuture().addListener(new ChannelFutureListener() { - @Override - public void operationComplete(ChannelFuture future) { - channelOpen.set(future.channel().isOpen()); - channelActive.set(future.channel().isActive()); - } - }); - childChannel.close().syncUninterruptibly(); - - assertFalse(channelOpen.get()); - assertFalse(channelActive.get()); - assertFalse(childChannel.isActive()); - } - - @Test - public void channelClosedWhenWriteFutureFails() { - final Queue<ChannelPromise> writePromises = new ArrayDeque<ChannelPromise>(); - - LastInboundHandler inboundHandler = new LastInboundHandler(); - Http2StreamChannel childChannel = newInboundStream(3, false, inboundHandler); - - assertTrue(childChannel.isOpen()); - assertTrue(childChannel.isActive()); - - final AtomicBoolean channelOpen = new AtomicBoolean(true); - final AtomicBoolean channelActive = new AtomicBoolean(true); - - Http2Headers headers = new DefaultHttp2Headers(); - when(frameWriter.writeHeaders(eqMultiplexCodecCtx(), anyInt(), - eq(headers), anyInt(), anyShort(), anyBoolean(), anyInt(), anyBoolean(), - any(ChannelPromise.class))).thenAnswer(new Answer<ChannelFuture>() { - @Override - public ChannelFuture answer(InvocationOnMock invocationOnMock) { - ChannelPromise promise = invocationOnMock.getArgument(8); - writePromises.offer(promise); - return promise; - } - }); - - ChannelFuture f = childChannel.writeAndFlush(new DefaultHttp2HeadersFrame(headers)); - assertFalse(f.isDone()); - f.addListener(new ChannelFutureListener() { - @Override - public void operationComplete(ChannelFuture future) throws Exception { - channelOpen.set(future.channel().isOpen()); - channelActive.set(future.channel().isActive()); - } - }); - - ChannelPromise first = writePromises.poll(); - first.setFailure(new ClosedChannelException()); - f.awaitUninterruptibly(); - - assertFalse(channelOpen.get()); - assertFalse(channelActive.get()); - assertFalse(childChannel.isActive()); - } - - @Test - public void channelClosedTwiceMarksPromiseAsSuccessful() { - LastInboundHandler inboundHandler = new LastInboundHandler(); - Http2StreamChannel childChannel = newInboundStream(3, false, inboundHandler); - - assertTrue(childChannel.isOpen()); - assertTrue(childChannel.isActive()); - childChannel.close().syncUninterruptibly(); - childChannel.close().syncUninterruptibly(); - - assertFalse(childChannel.isOpen()); - assertFalse(childChannel.isActive()); - } - - @Test - public void settingChannelOptsAndAttrs() { - AttributeKey<String> key = AttributeKey.newInstance("foo"); - - Channel childChannel = newOutboundStream(new ChannelInboundHandlerAdapter()); - childChannel.config().setAutoRead(false).setWriteSpinCount(1000); - childChannel.attr(key).set("bar"); - assertFalse(childChannel.config().isAutoRead()); - assertEquals(1000, childChannel.config().getWriteSpinCount()); - assertEquals("bar", childChannel.attr(key).get()); - } - - @Test - public void outboundFlowControlWritability() { - Http2StreamChannel childChannel = newOutboundStream(new ChannelInboundHandlerAdapter()); - assertTrue(childChannel.isActive()); - - assertTrue(childChannel.isWritable()); - childChannel.writeAndFlush(new DefaultHttp2HeadersFrame(new DefaultHttp2Headers())); - parentChannel.flush(); - - // Test for initial window size - assertEquals(initialRemoteStreamWindow, childChannel.config().getWriteBufferHighWaterMark()); - - assertTrue(childChannel.isWritable()); - childChannel.write(new DefaultHttp2DataFrame(Unpooled.buffer().writeZero(16 * 1024 * 1024))); - assertFalse(childChannel.isWritable()); - } - - @Test - public void writabilityAndFlowControl() { - LastInboundHandler inboundHandler = new LastInboundHandler(); - Http2StreamChannel childChannel = newInboundStream(3, false, inboundHandler); - assertEquals("", inboundHandler.writabilityStates()); - - assertTrue(childChannel.isWritable()); - // HEADERS frames are not flow controlled, so they should not affect the flow control window. - childChannel.writeAndFlush(new DefaultHttp2HeadersFrame(new DefaultHttp2Headers())); - codec.onHttp2StreamWritabilityChanged(codec.ctx, childChannel.stream(), true); - - assertTrue(childChannel.isWritable()); - assertEquals("", inboundHandler.writabilityStates()); - - codec.onHttp2StreamWritabilityChanged(codec.ctx, childChannel.stream(), true); - assertTrue(childChannel.isWritable()); - assertEquals("", inboundHandler.writabilityStates()); - - codec.onHttp2StreamWritabilityChanged(codec.ctx, childChannel.stream(), false); - assertFalse(childChannel.isWritable()); - assertEquals("false", inboundHandler.writabilityStates()); - - codec.onHttp2StreamWritabilityChanged(codec.ctx, childChannel.stream(), false); - assertFalse(childChannel.isWritable()); - assertEquals("false", inboundHandler.writabilityStates()); - } - - @Test - public void channelClosedWhenInactiveFired() { - LastInboundHandler inboundHandler = new LastInboundHandler(); - Http2StreamChannel childChannel = newInboundStream(3, false, inboundHandler); - - final AtomicBoolean channelOpen = new AtomicBoolean(false); - final AtomicBoolean channelActive = new AtomicBoolean(false); - assertTrue(childChannel.isOpen()); - assertTrue(childChannel.isActive()); - - childChannel.pipeline().addLast(new ChannelInboundHandlerAdapter() { - @Override - public void channelInactive(ChannelHandlerContext ctx) throws Exception { - channelOpen.set(ctx.channel().isOpen()); - channelActive.set(ctx.channel().isActive()); - - super.channelInactive(ctx); - } - }); - - childChannel.close().syncUninterruptibly(); - assertFalse(channelOpen.get()); - assertFalse(channelActive.get()); - } - - @Test - public void channelInactiveHappensAfterExceptionCaughtEvents() throws Exception { - final AtomicInteger count = new AtomicInteger(0); - final AtomicInteger exceptionCaught = new AtomicInteger(-1); - final AtomicInteger channelInactive = new AtomicInteger(-1); - final AtomicInteger channelUnregistered = new AtomicInteger(-1); - Http2StreamChannel childChannel = newOutboundStream(new ChannelInboundHandlerAdapter() { - - @Override - public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { - ctx.close(); - throw new Exception("exception"); - } - }); - - childChannel.pipeline().addLast(new ChannelInboundHandlerAdapter() { - - @Override - public void channelInactive(ChannelHandlerContext ctx) throws Exception { - channelInactive.set(count.getAndIncrement()); - super.channelInactive(ctx); - } - - @Override - public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { - exceptionCaught.set(count.getAndIncrement()); - super.exceptionCaught(ctx, cause); - } - - @Override - public void channelUnregistered(ChannelHandlerContext ctx) throws Exception { - channelUnregistered.set(count.getAndIncrement()); - super.channelUnregistered(ctx); - } - }); - - childChannel.pipeline().fireUserEventTriggered(new Object()); - parentChannel.runPendingTasks(); - - // The events should have happened in this order because the inactive and deregistration events - // get deferred as they do in the AbstractChannel. - assertEquals(0, exceptionCaught.get()); - assertEquals(1, channelInactive.get()); - assertEquals(2, channelUnregistered.get()); - } - - @Test - public void callUnsafeCloseMultipleTimes() { - LastInboundHandler inboundHandler = new LastInboundHandler(); - Http2StreamChannel childChannel = newInboundStream(3, false, inboundHandler); - childChannel.unsafe().close(childChannel.voidPromise()); - - ChannelPromise promise = childChannel.newPromise(); - childChannel.unsafe().close(promise); - promise.syncUninterruptibly(); - childChannel.closeFuture().syncUninterruptibly(); - } - - @Test - public void endOfStreamDoesNotDiscardData() { - AtomicInteger numReads = new AtomicInteger(1); - final AtomicBoolean shouldDisableAutoRead = new AtomicBoolean(); - Consumer<ChannelHandlerContext> ctxConsumer = new Consumer<ChannelHandlerContext>() { - @Override - public void accept(ChannelHandlerContext obj) { - if (shouldDisableAutoRead.get()) { - obj.channel().config().setAutoRead(false); - } - } - }; - LastInboundHandler inboundHandler = new LastInboundHandler(ctxConsumer); - Http2StreamChannel childChannel = newInboundStream(3, false, numReads, inboundHandler); - childChannel.config().setAutoRead(false); - - Http2DataFrame dataFrame1 = new DefaultHttp2DataFrame(bb("1")).stream(childChannel.stream()); - Http2DataFrame dataFrame2 = new DefaultHttp2DataFrame(bb("2")).stream(childChannel.stream()); - Http2DataFrame dataFrame3 = new DefaultHttp2DataFrame(bb("3")).stream(childChannel.stream()); - Http2DataFrame dataFrame4 = new DefaultHttp2DataFrame(bb("4")).stream(childChannel.stream()); - - assertEquals(new DefaultHttp2HeadersFrame(request).stream(childChannel.stream()), inboundHandler.readInbound()); - - ChannelHandler readCompleteSupressHandler = new ChannelInboundHandlerAdapter() { - @Override - public void channelReadComplete(ChannelHandlerContext ctx) { - // We want to simulate the parent channel calling channelRead and delay calling channelReadComplete. - } - }; - - parentChannel.pipeline().addFirst(readCompleteSupressHandler); - frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("1"), 0, false); - - assertEqualsAndRelease(dataFrame1, inboundHandler.<Http2DataFrame>readInbound()); - - // Deliver frames, and then a stream closed while read is inactive. - frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("2"), 0, false); - frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("3"), 0, false); - frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("4"), 0, false); - - shouldDisableAutoRead.set(true); - childChannel.config().setAutoRead(true); - numReads.set(1); - - frameInboundWriter.writeInboundRstStream(childChannel.stream().id(), Http2Error.NO_ERROR.code()); - - // Detecting EOS should flush all pending data regardless of read calls. - assertEqualsAndRelease(dataFrame2, inboundHandler.<Http2DataFrame>readInbound()); - assertEqualsAndRelease(dataFrame3, inboundHandler.<Http2DataFrame>readInbound()); - assertEqualsAndRelease(dataFrame4, inboundHandler.<Http2DataFrame>readInbound()); - - Http2ResetFrame resetFrame = inboundHandler.readInbound(); - assertEquals(childChannel.stream(), resetFrame.stream()); - assertEquals(Http2Error.NO_ERROR.code(), resetFrame.errorCode()); - - assertNull(inboundHandler.readInbound()); - - // Now we want to call channelReadComplete and simulate the end of the read loop. - parentChannel.pipeline().remove(readCompleteSupressHandler); - parentChannel.flushInbound(); - - childChannel.closeFuture().syncUninterruptibly(); - } - - @Test - public void childQueueIsDrainedAndNewDataIsDispatchedInParentReadLoopAutoRead() { - AtomicInteger numReads = new AtomicInteger(1); - final AtomicInteger channelReadCompleteCount = new AtomicInteger(0); - final AtomicBoolean shouldDisableAutoRead = new AtomicBoolean(); - Consumer<ChannelHandlerContext> ctxConsumer = new Consumer<ChannelHandlerContext>() { - @Override - public void accept(ChannelHandlerContext obj) { - channelReadCompleteCount.incrementAndGet(); - if (shouldDisableAutoRead.get()) { - obj.channel().config().setAutoRead(false); - } - } - }; - LastInboundHandler inboundHandler = new LastInboundHandler(ctxConsumer); - Http2StreamChannel childChannel = newInboundStream(3, false, numReads, inboundHandler); - childChannel.config().setAutoRead(false); - - Http2DataFrame dataFrame1 = new DefaultHttp2DataFrame(bb("1")).stream(childChannel.stream()); - Http2DataFrame dataFrame2 = new DefaultHttp2DataFrame(bb("2")).stream(childChannel.stream()); - Http2DataFrame dataFrame3 = new DefaultHttp2DataFrame(bb("3")).stream(childChannel.stream()); - Http2DataFrame dataFrame4 = new DefaultHttp2DataFrame(bb("4")).stream(childChannel.stream()); - - assertEquals(new DefaultHttp2HeadersFrame(request).stream(childChannel.stream()), inboundHandler.readInbound()); - - ChannelHandler readCompleteSupressHandler = new ChannelInboundHandlerAdapter() { - @Override - public void channelReadComplete(ChannelHandlerContext ctx) { - // We want to simulate the parent channel calling channelRead and delay calling channelReadComplete. - } - }; - parentChannel.pipeline().addFirst(readCompleteSupressHandler); - - frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("1"), 0, false); - - assertEqualsAndRelease(dataFrame1, inboundHandler.<Http2DataFrame>readInbound()); - - // We want one item to be in the queue, and allow the numReads to be larger than 1. This will ensure that - // when beginRead() is called the child channel is added to the readPending queue of the parent channel. - frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("2"), 0, false); - - numReads.set(10); - shouldDisableAutoRead.set(true); - childChannel.config().setAutoRead(true); - - frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("3"), 0, false); - frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("4"), 0, false); - - // Detecting EOS should flush all pending data regardless of read calls. - assertEqualsAndRelease(dataFrame2, inboundHandler.<Http2DataFrame>readInbound()); - assertEqualsAndRelease(dataFrame3, inboundHandler.<Http2DataFrame>readInbound()); - assertEqualsAndRelease(dataFrame4, inboundHandler.<Http2DataFrame>readInbound()); - - assertNull(inboundHandler.readInbound()); - - // Now we want to call channelReadComplete and simulate the end of the read loop. - parentChannel.pipeline().remove(readCompleteSupressHandler); - parentChannel.flushInbound(); - - // 3 = 1 for initialization + 1 for read when auto read was off + 1 for when auto read was back on - assertEquals(3, channelReadCompleteCount.get()); + @Override + protected Http2FrameCodec newCodec(TestChannelInitializer childChannelInitializer, Http2FrameWriter frameWriter) { + return new Http2MultiplexCodecBuilder(true, childChannelInitializer).frameWriter(frameWriter).build(); } - @Test - public void childQueueIsDrainedAndNewDataIsDispatchedInParentReadLoopNoAutoRead() { - final AtomicInteger numReads = new AtomicInteger(1); - final AtomicInteger channelReadCompleteCount = new AtomicInteger(0); - final AtomicBoolean shouldDisableAutoRead = new AtomicBoolean(); - Consumer<ChannelHandlerContext> ctxConsumer = new Consumer<ChannelHandlerContext>() { - @Override - public void accept(ChannelHandlerContext obj) { - channelReadCompleteCount.incrementAndGet(); - if (shouldDisableAutoRead.get()) { - obj.channel().config().setAutoRead(false); - } - } - }; - final LastInboundHandler inboundHandler = new LastInboundHandler(ctxConsumer); - Http2StreamChannel childChannel = newInboundStream(3, false, numReads, inboundHandler); - childChannel.config().setAutoRead(false); - - Http2DataFrame dataFrame1 = new DefaultHttp2DataFrame(bb("1")).stream(childChannel.stream()); - Http2DataFrame dataFrame2 = new DefaultHttp2DataFrame(bb("2")).stream(childChannel.stream()); - Http2DataFrame dataFrame3 = new DefaultHttp2DataFrame(bb("3")).stream(childChannel.stream()); - Http2DataFrame dataFrame4 = new DefaultHttp2DataFrame(bb("4")).stream(childChannel.stream()); - - assertEquals(new DefaultHttp2HeadersFrame(request).stream(childChannel.stream()), inboundHandler.readInbound()); - - ChannelHandler readCompleteSupressHandler = new ChannelInboundHandlerAdapter() { - @Override - public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { - // We want to simulate the parent channel calling channelRead and delay calling channelReadComplete. - } - }; - parentChannel.pipeline().addFirst(readCompleteSupressHandler); - - frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("1"), 0, false); - - assertEqualsAndRelease(dataFrame1, inboundHandler.<Http2Frame>readInbound()); - - // We want one item to be in the queue, and allow the numReads to be larger than 1. This will ensure that - // when beginRead() is called the child channel is added to the readPending queue of the parent channel. - frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("2"), 0, false); - - numReads.set(2); - childChannel.read(); - - assertEqualsAndRelease(dataFrame2, inboundHandler.<Http2Frame>readInbound()); - - assertNull(inboundHandler.readInbound()); - - // This is the second item that was read, this should be the last until we call read() again. This should also - // notify of readComplete(). - frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("3"), 0, false); - - assertEqualsAndRelease(dataFrame3, inboundHandler.<Http2Frame>readInbound()); - - frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("4"), 0, false); - assertNull(inboundHandler.readInbound()); - - childChannel.read(); - - assertEqualsAndRelease(dataFrame4, inboundHandler.<Http2Frame>readInbound()); - - assertNull(inboundHandler.readInbound()); - - // Now we want to call channelReadComplete and simulate the end of the read loop. - parentChannel.pipeline().remove(readCompleteSupressHandler); - parentChannel.flushInbound(); - - // 3 = 1 for initialization + 1 for first read of 2 items + 1 for second read of 2 items + - // 1 for parent channel readComplete - assertEquals(4, channelReadCompleteCount.get()); + @Override + protected ChannelHandler newMultiplexer(TestChannelInitializer childChannelInitializer) { + return null; } - private static void verifyFramesMultiplexedToCorrectChannel(Http2StreamChannel streamChannel, - LastInboundHandler inboundHandler, - int numFrames) { - for (int i = 0; i < numFrames; i++) { - Http2StreamFrame frame = inboundHandler.readInbound(); - assertNotNull(frame); - assertEquals(streamChannel.stream(), frame.stream()); - release(frame); - } - assertNull(inboundHandler.readInbound()); + @Override + protected boolean useUserEventForResetFrame() { + return false; } - private static int eqStreamId(Http2StreamChannel channel) { - return eq(channel.stream().id()); + @Override + protected boolean ignoreWindowUpdateFrames() { + return false; } } diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MultiplexHandlerClientUpgradeTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MultiplexHandlerClientUpgradeTest.java new file mode 100644 index 0000000..281c8da --- /dev/null +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MultiplexHandlerClientUpgradeTest.java @@ -0,0 +1,30 @@ +/* + * Copyright 2019 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.codec.http2; + +import io.netty.channel.ChannelHandler; + +public class Http2MultiplexHandlerClientUpgradeTest extends Http2MultiplexClientUpgradeTest<Http2FrameCodec> { + + @Override + protected Http2FrameCodec newCodec(ChannelHandler upgradeHandler) { + return Http2FrameCodecBuilder.forClient().build(); + } + + @Override + protected ChannelHandler newMultiplexer(ChannelHandler upgradeHandler) { + return new Http2MultiplexHandler(new NoopHandler(), upgradeHandler); + } +} diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MultiplexHandlerTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MultiplexHandlerTest.java new file mode 100644 index 0000000..a37f74b --- /dev/null +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MultiplexHandlerTest.java @@ -0,0 +1,43 @@ +/* + * Copyright 2019 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.codec.http2; + +import io.netty.channel.ChannelHandler; + +/** + * Unit tests for {@link Http2MultiplexHandler}. + */ +public class Http2MultiplexHandlerTest extends Http2MultiplexTest<Http2FrameCodec> { + + @Override + protected Http2FrameCodec newCodec(TestChannelInitializer childChannelInitializer, Http2FrameWriter frameWriter) { + return new Http2FrameCodecBuilder(true).frameWriter(frameWriter).build(); + } + + @Override + protected ChannelHandler newMultiplexer(TestChannelInitializer childChannelInitializer) { + return new Http2MultiplexHandler(childChannelInitializer, null); + } + + @Override + protected boolean useUserEventForResetFrame() { + return true; + } + + @Override + protected boolean ignoreWindowUpdateFrames() { + return true; + } +} diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MultiplexTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MultiplexTest.java new file mode 100644 index 0000000..c1207f6 --- /dev/null +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MultiplexTest.java @@ -0,0 +1,1230 @@ +/* + * Copyright 2016 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.codec.http2; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.ChannelOutboundHandlerAdapter; +import io.netty.channel.ChannelPromise; +import io.netty.channel.WriteBufferWaterMark; +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpScheme; +import io.netty.handler.codec.http2.Http2Exception.StreamException; +import io.netty.handler.codec.http2.LastInboundHandler.Consumer; +import io.netty.util.AsciiString; +import io.netty.util.AttributeKey; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentMatcher; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import java.net.InetSocketAddress; +import java.nio.channels.ClosedChannelException; +import java.util.ArrayDeque; +import java.util.Queue; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import static io.netty.handler.codec.http2.Http2TestUtil.anyChannelPromise; +import static io.netty.handler.codec.http2.Http2TestUtil.anyHttp2Settings; +import static io.netty.handler.codec.http2.Http2TestUtil.assertEqualsAndRelease; +import static io.netty.handler.codec.http2.Http2TestUtil.bb; +import static io.netty.util.ReferenceCountUtil.release; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public abstract class Http2MultiplexTest<C extends Http2FrameCodec> { + private final Http2Headers request = new DefaultHttp2Headers() + .method(HttpMethod.GET.asciiName()).scheme(HttpScheme.HTTPS.name()) + .authority(new AsciiString("example.org")).path(new AsciiString("/foo")); + + private EmbeddedChannel parentChannel; + private Http2FrameWriter frameWriter; + private Http2FrameInboundWriter frameInboundWriter; + private TestChannelInitializer childChannelInitializer; + private C codec; + + private static final int initialRemoteStreamWindow = 1024; + + protected abstract C newCodec(TestChannelInitializer childChannelInitializer, Http2FrameWriter frameWriter); + protected abstract ChannelHandler newMultiplexer(TestChannelInitializer childChannelInitializer); + + @Before + public void setUp() { + childChannelInitializer = new TestChannelInitializer(); + parentChannel = new EmbeddedChannel(); + frameInboundWriter = new Http2FrameInboundWriter(parentChannel); + parentChannel.connect(new InetSocketAddress(0)); + frameWriter = Http2TestUtil.mockedFrameWriter(); + codec = newCodec(childChannelInitializer, frameWriter); + parentChannel.pipeline().addLast(codec); + ChannelHandler multiplexer = newMultiplexer(childChannelInitializer); + if (multiplexer != null) { + parentChannel.pipeline().addLast(multiplexer); + } + + parentChannel.runPendingTasks(); + parentChannel.pipeline().fireChannelActive(); + + parentChannel.writeInbound(Http2CodecUtil.connectionPrefaceBuf()); + + Http2Settings settings = new Http2Settings().initialWindowSize(initialRemoteStreamWindow); + frameInboundWriter.writeInboundSettings(settings); + + verify(frameWriter).writeSettingsAck(eqCodecCtx(), anyChannelPromise()); + + frameInboundWriter.writeInboundSettingsAck(); + + Http2SettingsFrame settingsFrame = parentChannel.readInbound(); + assertNotNull(settingsFrame); + Http2SettingsAckFrame settingsAckFrame = parentChannel.readInbound(); + assertNotNull(settingsAckFrame); + + // Handshake + verify(frameWriter).writeSettings(eqCodecCtx(), + anyHttp2Settings(), anyChannelPromise()); + } + + private ChannelHandlerContext eqCodecCtx() { + return eq(codec.ctx); + } + + @After + public void tearDown() throws Exception { + if (childChannelInitializer.handler instanceof LastInboundHandler) { + ((LastInboundHandler) childChannelInitializer.handler).finishAndReleaseAll(); + } + parentChannel.finishAndReleaseAll(); + codec = null; + } + + // TODO(buchgr): Flush from child channel + // TODO(buchgr): ChildChannel.childReadComplete() + // TODO(buchgr): GOAWAY Logic + // TODO(buchgr): Test ChannelConfig.setMaxMessagesPerRead + + @Test + public void writeUnknownFrame() { + Http2StreamChannel childChannel = newOutboundStream(new ChannelInboundHandlerAdapter() { + @Override + public void channelActive(ChannelHandlerContext ctx) { + ctx.writeAndFlush(new DefaultHttp2HeadersFrame(new DefaultHttp2Headers())); + ctx.writeAndFlush(new DefaultHttp2UnknownFrame((byte) 99, new Http2Flags())); + ctx.fireChannelActive(); + } + }); + assertTrue(childChannel.isActive()); + + parentChannel.runPendingTasks(); + + verify(frameWriter).writeFrame(eq(codec.ctx), eq((byte) 99), eqStreamId(childChannel), any(Http2Flags.class), + any(ByteBuf.class), any(ChannelPromise.class)); + } + + private Http2StreamChannel newInboundStream(int streamId, boolean endStream, final ChannelHandler childHandler) { + return newInboundStream(streamId, endStream, null, childHandler); + } + + private Http2StreamChannel newInboundStream(int streamId, boolean endStream, + AtomicInteger maxReads, final ChannelHandler childHandler) { + final AtomicReference<Http2StreamChannel> streamChannelRef = new AtomicReference<Http2StreamChannel>(); + childChannelInitializer.maxReads = maxReads; + childChannelInitializer.handler = new ChannelInboundHandlerAdapter() { + @Override + public void channelRegistered(ChannelHandlerContext ctx) { + assertNull(streamChannelRef.get()); + streamChannelRef.set((Http2StreamChannel) ctx.channel()); + ctx.pipeline().addLast(childHandler); + ctx.fireChannelRegistered(); + } + }; + + frameInboundWriter.writeInboundHeaders(streamId, request, 0, endStream); + parentChannel.runPendingTasks(); + Http2StreamChannel channel = streamChannelRef.get(); + assertEquals(streamId, channel.stream().id()); + return channel; + } + + @Test + public void readUnkownFrame() { + LastInboundHandler handler = new LastInboundHandler(); + + Http2StreamChannel channel = newInboundStream(3, true, handler); + frameInboundWriter.writeInboundFrame((byte) 99, channel.stream().id(), new Http2Flags(), Unpooled.EMPTY_BUFFER); + + // header frame and unknown frame + verifyFramesMultiplexedToCorrectChannel(channel, handler, 2); + + Channel childChannel = newOutboundStream(new ChannelInboundHandlerAdapter()); + assertTrue(childChannel.isActive()); + } + + @Test + public void headerAndDataFramesShouldBeDelivered() { + LastInboundHandler inboundHandler = new LastInboundHandler(); + + Http2StreamChannel channel = newInboundStream(3, false, inboundHandler); + Http2HeadersFrame headersFrame = new DefaultHttp2HeadersFrame(request).stream(channel.stream()); + Http2DataFrame dataFrame1 = new DefaultHttp2DataFrame(bb("hello")).stream(channel.stream()); + Http2DataFrame dataFrame2 = new DefaultHttp2DataFrame(bb("world")).stream(channel.stream()); + + assertTrue(inboundHandler.isChannelActive()); + frameInboundWriter.writeInboundData(channel.stream().id(), bb("hello"), 0, false); + frameInboundWriter.writeInboundData(channel.stream().id(), bb("world"), 0, false); + + assertEquals(headersFrame, inboundHandler.readInbound()); + + assertEqualsAndRelease(dataFrame1, inboundHandler.<Http2Frame>readInbound()); + assertEqualsAndRelease(dataFrame2, inboundHandler.<Http2Frame>readInbound()); + + assertNull(inboundHandler.readInbound()); + } + + @Test + public void framesShouldBeMultiplexed() { + LastInboundHandler handler1 = new LastInboundHandler(); + Http2StreamChannel channel1 = newInboundStream(3, false, handler1); + LastInboundHandler handler2 = new LastInboundHandler(); + Http2StreamChannel channel2 = newInboundStream(5, false, handler2); + LastInboundHandler handler3 = new LastInboundHandler(); + Http2StreamChannel channel3 = newInboundStream(11, false, handler3); + + verifyFramesMultiplexedToCorrectChannel(channel1, handler1, 1); + verifyFramesMultiplexedToCorrectChannel(channel2, handler2, 1); + verifyFramesMultiplexedToCorrectChannel(channel3, handler3, 1); + + frameInboundWriter.writeInboundData(channel2.stream().id(), bb("hello"), 0, false); + frameInboundWriter.writeInboundData(channel1.stream().id(), bb("foo"), 0, true); + frameInboundWriter.writeInboundData(channel2.stream().id(), bb("world"), 0, true); + frameInboundWriter.writeInboundData(channel3.stream().id(), bb("bar"), 0, true); + + verifyFramesMultiplexedToCorrectChannel(channel1, handler1, 1); + verifyFramesMultiplexedToCorrectChannel(channel2, handler2, 2); + verifyFramesMultiplexedToCorrectChannel(channel3, handler3, 1); + } + + @Test + public void inboundDataFrameShouldUpdateLocalFlowController() throws Http2Exception { + Http2LocalFlowController flowController = Mockito.mock(Http2LocalFlowController.class); + codec.connection().local().flowController(flowController); + + LastInboundHandler handler = new LastInboundHandler(); + final Http2StreamChannel channel = newInboundStream(3, false, handler); + + ByteBuf tenBytes = bb("0123456789"); + + frameInboundWriter.writeInboundData(channel.stream().id(), tenBytes, 0, true); + + // Verify we marked the bytes as consumed + verify(flowController).consumeBytes(argThat(new ArgumentMatcher<Http2Stream>() { + @Override + public boolean matches(Http2Stream http2Stream) { + return http2Stream.id() == channel.stream().id(); + } + }), eq(10)); + + // headers and data frame + verifyFramesMultiplexedToCorrectChannel(channel, handler, 2); + } + + @Test + public void unhandledHttp2FramesShouldBePropagated() { + Http2PingFrame pingFrame = new DefaultHttp2PingFrame(0); + frameInboundWriter.writeInboundPing(false, 0); + assertEquals(parentChannel.readInbound(), pingFrame); + + DefaultHttp2GoAwayFrame goAwayFrame = new DefaultHttp2GoAwayFrame(1, + parentChannel.alloc().buffer().writeLong(8)); + frameInboundWriter.writeInboundGoAway(0, goAwayFrame.errorCode(), goAwayFrame.content().retainedDuplicate()); + + Http2GoAwayFrame frame = parentChannel.readInbound(); + assertEqualsAndRelease(frame, goAwayFrame); + } + + @Test + public void channelReadShouldRespectAutoRead() { + LastInboundHandler inboundHandler = new LastInboundHandler(); + Http2StreamChannel childChannel = newInboundStream(3, false, inboundHandler); + assertTrue(childChannel.config().isAutoRead()); + Http2HeadersFrame headersFrame = inboundHandler.readInbound(); + assertNotNull(headersFrame); + + childChannel.config().setAutoRead(false); + + frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("hello world"), 0, false); + Http2DataFrame dataFrame0 = inboundHandler.readInbound(); + assertNotNull(dataFrame0); + release(dataFrame0); + + frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("foo"), 0, false); + frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("bar"), 0, false); + + assertNull(inboundHandler.readInbound()); + + childChannel.config().setAutoRead(true); + verifyFramesMultiplexedToCorrectChannel(childChannel, inboundHandler, 2); + } + + @Test + public void channelReadShouldRespectAutoReadAndNotProduceNPE() throws Exception { + LastInboundHandler inboundHandler = new LastInboundHandler(); + Http2StreamChannel childChannel = newInboundStream(3, false, inboundHandler); + assertTrue(childChannel.config().isAutoRead()); + Http2HeadersFrame headersFrame = inboundHandler.readInbound(); + assertNotNull(headersFrame); + + childChannel.config().setAutoRead(false); + childChannel.pipeline().addFirst(new ChannelInboundHandlerAdapter() { + private int count; + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + ctx.fireChannelRead(msg); + // Close channel after 2 reads so there is still something in the inboundBuffer when the close happens. + if (++count == 2) { + ctx.close(); + } + } + }); + frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("hello world"), 0, false); + Http2DataFrame dataFrame0 = inboundHandler.readInbound(); + assertNotNull(dataFrame0); + release(dataFrame0); + + frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("foo"), 0, false); + frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("bar"), 0, false); + frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("bar"), 0, false); + + assertNull(inboundHandler.readInbound()); + + childChannel.config().setAutoRead(true); + verifyFramesMultiplexedToCorrectChannel(childChannel, inboundHandler, 3); + inboundHandler.checkException(); + } + + @Test + public void readInChannelReadWithoutAutoRead() { + useReadWithoutAutoRead(false); + } + + @Test + public void readInChannelReadCompleteWithoutAutoRead() { + useReadWithoutAutoRead(true); + } + + private void useReadWithoutAutoRead(final boolean readComplete) { + LastInboundHandler inboundHandler = new LastInboundHandler(); + Http2StreamChannel childChannel = newInboundStream(3, false, inboundHandler); + assertTrue(childChannel.config().isAutoRead()); + childChannel.config().setAutoRead(false); + assertFalse(childChannel.config().isAutoRead()); + + Http2HeadersFrame headersFrame = inboundHandler.readInbound(); + assertNotNull(headersFrame); + + // Add a handler which will request reads. + childChannel.pipeline().addFirst(new ChannelInboundHandlerAdapter() { + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + ctx.fireChannelRead(msg); + if (!readComplete) { + ctx.read(); + } + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) { + ctx.fireChannelReadComplete(); + if (readComplete) { + ctx.read(); + } + } + }); + + frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("hello world"), 0, false); + frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("foo"), 0, false); + frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("bar"), 0, false); + frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("hello world"), 0, false); + frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("foo"), 0, false); + frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("bar"), 0, true); + + verifyFramesMultiplexedToCorrectChannel(childChannel, inboundHandler, 6); + } + + private Http2StreamChannel newOutboundStream(ChannelHandler handler) { + return new Http2StreamChannelBootstrap(parentChannel).handler(handler) + .open().syncUninterruptibly().getNow(); + } + + /** + * A child channel for an HTTP/2 stream in IDLE state (that is no headers sent or received), + * should not emit a RST_STREAM frame on close, as this is a connection error of type protocol error. + */ + @Test + public void idleOutboundStreamShouldNotWriteResetFrameOnClose() { + LastInboundHandler handler = new LastInboundHandler(); + + Channel childChannel = newOutboundStream(handler); + assertTrue(childChannel.isActive()); + + childChannel.close(); + parentChannel.runPendingTasks(); + + assertFalse(childChannel.isOpen()); + assertFalse(childChannel.isActive()); + assertNull(parentChannel.readOutbound()); + } + + @Test + public void outboundStreamShouldWriteResetFrameOnClose_headersSent() { + ChannelHandler handler = new ChannelInboundHandlerAdapter() { + @Override + public void channelActive(ChannelHandlerContext ctx) { + ctx.writeAndFlush(new DefaultHttp2HeadersFrame(new DefaultHttp2Headers())); + ctx.fireChannelActive(); + } + }; + + Http2StreamChannel childChannel = newOutboundStream(handler); + assertTrue(childChannel.isActive()); + + childChannel.close(); + verify(frameWriter).writeRstStream(eqCodecCtx(), + eqStreamId(childChannel), eq(Http2Error.CANCEL.code()), anyChannelPromise()); + } + + @Test + public void outboundStreamShouldNotWriteResetFrameOnClose_IfStreamDidntExist() { + when(frameWriter.writeHeaders(eqCodecCtx(), anyInt(), + any(Http2Headers.class), anyInt(), anyBoolean(), + any(ChannelPromise.class))).thenAnswer(new Answer<ChannelFuture>() { + + private boolean headersWritten; + @Override + public ChannelFuture answer(InvocationOnMock invocationOnMock) { + // We want to fail to write the first headers frame. This is what happens if the connection + // refuses to allocate a new stream due to having received a GOAWAY. + if (!headersWritten) { + headersWritten = true; + return ((ChannelPromise) invocationOnMock.getArgument(5)).setFailure(new Exception("boom")); + } + return ((ChannelPromise) invocationOnMock.getArgument(5)).setSuccess(); + } + }); + + Http2StreamChannel childChannel = newOutboundStream(new ChannelInboundHandlerAdapter() { + @Override + public void channelActive(ChannelHandlerContext ctx) { + ctx.writeAndFlush(new DefaultHttp2HeadersFrame(new DefaultHttp2Headers())); + ctx.fireChannelActive(); + } + }); + + assertFalse(childChannel.isActive()); + + childChannel.close(); + parentChannel.runPendingTasks(); + // The channel was never active so we should not generate a RST frame. + verify(frameWriter, never()).writeRstStream(eqCodecCtx(), eqStreamId(childChannel), anyLong(), + anyChannelPromise()); + + assertTrue(parentChannel.outboundMessages().isEmpty()); + } + + @Test + public void inboundRstStreamFireChannelInactive() { + LastInboundHandler inboundHandler = new LastInboundHandler(); + Http2StreamChannel channel = newInboundStream(3, false, inboundHandler); + assertTrue(inboundHandler.isChannelActive()); + frameInboundWriter.writeInboundRstStream(channel.stream().id(), Http2Error.INTERNAL_ERROR.code()); + + assertFalse(inboundHandler.isChannelActive()); + + // A RST_STREAM frame should NOT be emitted, as we received a RST_STREAM. + verify(frameWriter, Mockito.never()).writeRstStream(eqCodecCtx(), eqStreamId(channel), + anyLong(), anyChannelPromise()); + } + + @Test(expected = StreamException.class) + public void streamExceptionTriggersChildChannelExceptionAndClose() throws Exception { + LastInboundHandler inboundHandler = new LastInboundHandler(); + Http2StreamChannel channel = newInboundStream(3, false, inboundHandler); + assertTrue(channel.isActive()); + StreamException cause = new StreamException(channel.stream().id(), Http2Error.PROTOCOL_ERROR, "baaam!"); + parentChannel.pipeline().fireExceptionCaught(cause); + + assertFalse(channel.isActive()); + inboundHandler.checkException(); + } + + @Test(expected = ClosedChannelException.class) + public void streamClosedErrorTranslatedToClosedChannelExceptionOnWrites() throws Exception { + LastInboundHandler inboundHandler = new LastInboundHandler(); + + final Http2StreamChannel childChannel = newOutboundStream(inboundHandler); + assertTrue(childChannel.isActive()); + + Http2Headers headers = new DefaultHttp2Headers(); + when(frameWriter.writeHeaders(eqCodecCtx(), anyInt(), + eq(headers), anyInt(), anyBoolean(), + any(ChannelPromise.class))).thenAnswer(new Answer<ChannelFuture>() { + @Override + public ChannelFuture answer(InvocationOnMock invocationOnMock) { + return ((ChannelPromise) invocationOnMock.getArgument(5)).setFailure( + new StreamException(childChannel.stream().id(), Http2Error.STREAM_CLOSED, "Stream Closed")); + } + }); + ChannelFuture future = childChannel.writeAndFlush(new DefaultHttp2HeadersFrame(new DefaultHttp2Headers())); + + parentChannel.flush(); + + assertFalse(childChannel.isActive()); + assertFalse(childChannel.isOpen()); + + inboundHandler.checkException(); + + future.syncUninterruptibly(); + } + + @Test + public void creatingWritingReadingAndClosingOutboundStreamShouldWork() { + LastInboundHandler inboundHandler = new LastInboundHandler(); + Http2StreamChannel childChannel = newOutboundStream(inboundHandler); + assertTrue(childChannel.isActive()); + assertTrue(inboundHandler.isChannelActive()); + + // Write to the child channel + Http2Headers headers = new DefaultHttp2Headers().scheme("https").method("GET").path("/foo.txt"); + childChannel.writeAndFlush(new DefaultHttp2HeadersFrame(headers)); + + // Read from the child channel + frameInboundWriter.writeInboundHeaders(childChannel.stream().id(), headers, 0, false); + + Http2HeadersFrame headersFrame = inboundHandler.readInbound(); + assertNotNull(headersFrame); + assertEquals(headers, headersFrame.headers()); + + // Close the child channel. + childChannel.close(); + + parentChannel.runPendingTasks(); + // An active outbound stream should emit a RST_STREAM frame. + verify(frameWriter).writeRstStream(eqCodecCtx(), eqStreamId(childChannel), + anyLong(), anyChannelPromise()); + + assertFalse(childChannel.isOpen()); + assertFalse(childChannel.isActive()); + assertFalse(inboundHandler.isChannelActive()); + } + + // Test failing the promise of the first headers frame of an outbound stream. In practice this error case would most + // likely happen due to the max concurrent streams limit being hit or the channel running out of stream identifiers. + // + @Test(expected = Http2NoMoreStreamIdsException.class) + public void failedOutboundStreamCreationThrowsAndClosesChannel() throws Exception { + LastInboundHandler handler = new LastInboundHandler(); + Http2StreamChannel childChannel = newOutboundStream(handler); + assertTrue(childChannel.isActive()); + + Http2Headers headers = new DefaultHttp2Headers(); + when(frameWriter.writeHeaders(eqCodecCtx(), anyInt(), + eq(headers), anyInt(), anyBoolean(), + any(ChannelPromise.class))).thenAnswer(new Answer<ChannelFuture>() { + @Override + public ChannelFuture answer(InvocationOnMock invocationOnMock) { + return ((ChannelPromise) invocationOnMock.getArgument(5)).setFailure( + new Http2NoMoreStreamIdsException()); + } + }); + + ChannelFuture future = childChannel.writeAndFlush(new DefaultHttp2HeadersFrame(headers)); + parentChannel.flush(); + + assertFalse(childChannel.isActive()); + assertFalse(childChannel.isOpen()); + + handler.checkException(); + + future.syncUninterruptibly(); + } + + @Test + public void channelClosedWhenCloseListenerCompletes() { + LastInboundHandler inboundHandler = new LastInboundHandler(); + Http2StreamChannel childChannel = newInboundStream(3, false, inboundHandler); + + assertTrue(childChannel.isOpen()); + assertTrue(childChannel.isActive()); + + final AtomicBoolean channelOpen = new AtomicBoolean(true); + final AtomicBoolean channelActive = new AtomicBoolean(true); + + // Create a promise before actually doing the close, because otherwise we would be adding a listener to a future + // that is already completed because we are using EmbeddedChannel which executes code in the JUnit thread. + ChannelPromise p = childChannel.newPromise(); + p.addListener(new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture future) { + channelOpen.set(future.channel().isOpen()); + channelActive.set(future.channel().isActive()); + } + }); + childChannel.close(p).syncUninterruptibly(); + + assertFalse(channelOpen.get()); + assertFalse(channelActive.get()); + assertFalse(childChannel.isActive()); + } + + @Test + public void channelClosedWhenChannelClosePromiseCompletes() { + LastInboundHandler inboundHandler = new LastInboundHandler(); + Http2StreamChannel childChannel = newInboundStream(3, false, inboundHandler); + + assertTrue(childChannel.isOpen()); + assertTrue(childChannel.isActive()); + + final AtomicBoolean channelOpen = new AtomicBoolean(true); + final AtomicBoolean channelActive = new AtomicBoolean(true); + + childChannel.closeFuture().addListener(new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture future) { + channelOpen.set(future.channel().isOpen()); + channelActive.set(future.channel().isActive()); + } + }); + childChannel.close().syncUninterruptibly(); + + assertFalse(channelOpen.get()); + assertFalse(channelActive.get()); + assertFalse(childChannel.isActive()); + } + + @Test + public void channelClosedWhenWriteFutureFails() { + final Queue<ChannelPromise> writePromises = new ArrayDeque<ChannelPromise>(); + + LastInboundHandler inboundHandler = new LastInboundHandler(); + Http2StreamChannel childChannel = newInboundStream(3, false, inboundHandler); + + assertTrue(childChannel.isOpen()); + assertTrue(childChannel.isActive()); + + final AtomicBoolean channelOpen = new AtomicBoolean(true); + final AtomicBoolean channelActive = new AtomicBoolean(true); + + Http2Headers headers = new DefaultHttp2Headers(); + when(frameWriter.writeHeaders(eqCodecCtx(), anyInt(), + eq(headers), anyInt(), anyBoolean(), + any(ChannelPromise.class))).thenAnswer(new Answer<ChannelFuture>() { + @Override + public ChannelFuture answer(InvocationOnMock invocationOnMock) { + ChannelPromise promise = invocationOnMock.getArgument(5); + writePromises.offer(promise); + return promise; + } + }); + + ChannelFuture f = childChannel.writeAndFlush(new DefaultHttp2HeadersFrame(headers)); + assertFalse(f.isDone()); + f.addListener(new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture future) throws Exception { + channelOpen.set(future.channel().isOpen()); + channelActive.set(future.channel().isActive()); + } + }); + + ChannelPromise first = writePromises.poll(); + first.setFailure(new ClosedChannelException()); + f.awaitUninterruptibly(); + + assertFalse(channelOpen.get()); + assertFalse(channelActive.get()); + assertFalse(childChannel.isActive()); + } + + @Test + public void channelClosedTwiceMarksPromiseAsSuccessful() { + LastInboundHandler inboundHandler = new LastInboundHandler(); + Http2StreamChannel childChannel = newInboundStream(3, false, inboundHandler); + + assertTrue(childChannel.isOpen()); + assertTrue(childChannel.isActive()); + childChannel.close().syncUninterruptibly(); + childChannel.close().syncUninterruptibly(); + + assertFalse(childChannel.isOpen()); + assertFalse(childChannel.isActive()); + } + + @Test + public void settingChannelOptsAndAttrs() { + AttributeKey<String> key = AttributeKey.newInstance(UUID.randomUUID().toString()); + + Channel childChannel = newOutboundStream(new ChannelInboundHandlerAdapter()); + childChannel.config().setAutoRead(false).setWriteSpinCount(1000); + childChannel.attr(key).set("bar"); + assertFalse(childChannel.config().isAutoRead()); + assertEquals(1000, childChannel.config().getWriteSpinCount()); + assertEquals("bar", childChannel.attr(key).get()); + } + + @Test + public void outboundFlowControlWritability() { + Http2StreamChannel childChannel = newOutboundStream(new ChannelInboundHandlerAdapter()); + assertTrue(childChannel.isActive()); + + assertTrue(childChannel.isWritable()); + childChannel.writeAndFlush(new DefaultHttp2HeadersFrame(new DefaultHttp2Headers())); + parentChannel.flush(); + + // Test for initial window size + assertTrue(initialRemoteStreamWindow < childChannel.config().getWriteBufferHighWaterMark()); + + assertTrue(childChannel.isWritable()); + childChannel.write(new DefaultHttp2DataFrame(Unpooled.buffer().writeZero(16 * 1024 * 1024))); + assertEquals(0, childChannel.bytesBeforeUnwritable()); + assertFalse(childChannel.isWritable()); + } + + @Test + public void writabilityOfParentIsRespected() { + Http2StreamChannel childChannel = newOutboundStream(new ChannelInboundHandlerAdapter()); + childChannel.config().setWriteBufferWaterMark(new WriteBufferWaterMark(2048, 4096)); + parentChannel.config().setWriteBufferWaterMark(new WriteBufferWaterMark(256, 512)); + assertTrue(childChannel.isWritable()); + assertTrue(parentChannel.isActive()); + + childChannel.writeAndFlush(new DefaultHttp2HeadersFrame(new DefaultHttp2Headers())); + parentChannel.flush(); + + assertTrue(childChannel.isWritable()); + childChannel.write(new DefaultHttp2DataFrame(Unpooled.buffer().writeZero(256))); + assertTrue(childChannel.isWritable()); + childChannel.writeAndFlush(new DefaultHttp2DataFrame(Unpooled.buffer().writeZero(512))); + + long bytesBeforeUnwritable = childChannel.bytesBeforeUnwritable(); + assertNotEquals(0, bytesBeforeUnwritable); + // Add something to the ChannelOutboundBuffer of the parent to simulate queuing in the parents channel buffer + // and verify that this only affect the writability of the parent channel while the child stays writable + // until it used all of its credits. + parentChannel.unsafe().outboundBuffer().addMessage( + Unpooled.buffer().writeZero(800), 800, parentChannel.voidPromise()); + assertFalse(parentChannel.isWritable()); + + assertTrue(childChannel.isWritable()); + assertEquals(4096, childChannel.bytesBeforeUnwritable()); + + // Flush everything which simulate writing everything to the socket. + parentChannel.flush(); + assertTrue(parentChannel.isWritable()); + assertTrue(childChannel.isWritable()); + assertEquals(bytesBeforeUnwritable, childChannel.bytesBeforeUnwritable()); + + ChannelFuture future = childChannel.writeAndFlush(new DefaultHttp2DataFrame( + Unpooled.buffer().writeZero((int) bytesBeforeUnwritable))); + assertFalse(childChannel.isWritable()); + assertTrue(parentChannel.isWritable()); + + parentChannel.flush(); + assertFalse(future.isDone()); + assertTrue(parentChannel.isWritable()); + assertFalse(childChannel.isWritable()); + + // Now write an window update frame for the stream which then should ensure we will flush the bytes that were + // queued in the RemoteFlowController before for the stream. + frameInboundWriter.writeInboundWindowUpdate(childChannel.stream().id(), (int) bytesBeforeUnwritable); + assertTrue(childChannel.isWritable()); + assertTrue(future.isDone()); + } + + @Test + public void channelClosedWhenInactiveFired() { + LastInboundHandler inboundHandler = new LastInboundHandler(); + Http2StreamChannel childChannel = newInboundStream(3, false, inboundHandler); + + final AtomicBoolean channelOpen = new AtomicBoolean(false); + final AtomicBoolean channelActive = new AtomicBoolean(false); + assertTrue(childChannel.isOpen()); + assertTrue(childChannel.isActive()); + + childChannel.pipeline().addLast(new ChannelInboundHandlerAdapter() { + @Override + public void channelInactive(ChannelHandlerContext ctx) throws Exception { + channelOpen.set(ctx.channel().isOpen()); + channelActive.set(ctx.channel().isActive()); + + super.channelInactive(ctx); + } + }); + + childChannel.close().syncUninterruptibly(); + assertFalse(channelOpen.get()); + assertFalse(channelActive.get()); + } + + @Test + public void channelInactiveHappensAfterExceptionCaughtEvents() throws Exception { + final AtomicInteger count = new AtomicInteger(0); + final AtomicInteger exceptionCaught = new AtomicInteger(-1); + final AtomicInteger channelInactive = new AtomicInteger(-1); + final AtomicInteger channelUnregistered = new AtomicInteger(-1); + Http2StreamChannel childChannel = newOutboundStream(new ChannelInboundHandlerAdapter() { + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + ctx.close(); + throw new Exception("exception"); + } + }); + + childChannel.pipeline().addLast(new ChannelInboundHandlerAdapter() { + + @Override + public void channelInactive(ChannelHandlerContext ctx) throws Exception { + channelInactive.set(count.getAndIncrement()); + super.channelInactive(ctx); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + exceptionCaught.set(count.getAndIncrement()); + super.exceptionCaught(ctx, cause); + } + + @Override + public void channelUnregistered(ChannelHandlerContext ctx) throws Exception { + channelUnregistered.set(count.getAndIncrement()); + super.channelUnregistered(ctx); + } + }); + + childChannel.pipeline().fireUserEventTriggered(new Object()); + parentChannel.runPendingTasks(); + + // The events should have happened in this order because the inactive and deregistration events + // get deferred as they do in the AbstractChannel. + assertEquals(0, exceptionCaught.get()); + assertEquals(1, channelInactive.get()); + assertEquals(2, channelUnregistered.get()); + } + + @Test + public void callUnsafeCloseMultipleTimes() { + LastInboundHandler inboundHandler = new LastInboundHandler(); + Http2StreamChannel childChannel = newInboundStream(3, false, inboundHandler); + childChannel.unsafe().close(childChannel.voidPromise()); + + ChannelPromise promise = childChannel.newPromise(); + childChannel.unsafe().close(promise); + promise.syncUninterruptibly(); + childChannel.closeFuture().syncUninterruptibly(); + } + + @Test + public void endOfStreamDoesNotDiscardData() { + AtomicInteger numReads = new AtomicInteger(1); + final AtomicBoolean shouldDisableAutoRead = new AtomicBoolean(); + Consumer<ChannelHandlerContext> ctxConsumer = new Consumer<ChannelHandlerContext>() { + @Override + public void accept(ChannelHandlerContext obj) { + if (shouldDisableAutoRead.get()) { + obj.channel().config().setAutoRead(false); + } + } + }; + LastInboundHandler inboundHandler = new LastInboundHandler(ctxConsumer); + Http2StreamChannel childChannel = newInboundStream(3, false, numReads, inboundHandler); + childChannel.config().setAutoRead(false); + + Http2DataFrame dataFrame1 = new DefaultHttp2DataFrame(bb("1")).stream(childChannel.stream()); + Http2DataFrame dataFrame2 = new DefaultHttp2DataFrame(bb("2")).stream(childChannel.stream()); + Http2DataFrame dataFrame3 = new DefaultHttp2DataFrame(bb("3")).stream(childChannel.stream()); + Http2DataFrame dataFrame4 = new DefaultHttp2DataFrame(bb("4")).stream(childChannel.stream()); + + assertEquals(new DefaultHttp2HeadersFrame(request).stream(childChannel.stream()), inboundHandler.readInbound()); + + ChannelHandler readCompleteSupressHandler = new ChannelInboundHandlerAdapter() { + @Override + public void channelReadComplete(ChannelHandlerContext ctx) { + // We want to simulate the parent channel calling channelRead and delay calling channelReadComplete. + } + }; + + parentChannel.pipeline().addFirst(readCompleteSupressHandler); + frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("1"), 0, false); + + assertEqualsAndRelease(dataFrame1, inboundHandler.<Http2DataFrame>readInbound()); + + // Deliver frames, and then a stream closed while read is inactive. + frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("2"), 0, false); + frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("3"), 0, false); + frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("4"), 0, false); + + shouldDisableAutoRead.set(true); + childChannel.config().setAutoRead(true); + numReads.set(1); + + frameInboundWriter.writeInboundRstStream(childChannel.stream().id(), Http2Error.NO_ERROR.code()); + + // Detecting EOS should flush all pending data regardless of read calls. + assertEqualsAndRelease(dataFrame2, inboundHandler.<Http2DataFrame>readInbound()); + assertNull(inboundHandler.readInbound()); + + // As we limited the number to 1 we also need to call read() again. + childChannel.read(); + + assertEqualsAndRelease(dataFrame3, inboundHandler.<Http2DataFrame>readInbound()); + assertEqualsAndRelease(dataFrame4, inboundHandler.<Http2DataFrame>readInbound()); + + Http2ResetFrame resetFrame = useUserEventForResetFrame() ? inboundHandler.<Http2ResetFrame>readUserEvent() : + inboundHandler.<Http2ResetFrame>readInbound(); + + assertEquals(childChannel.stream(), resetFrame.stream()); + assertEquals(Http2Error.NO_ERROR.code(), resetFrame.errorCode()); + + assertNull(inboundHandler.readInbound()); + + // Now we want to call channelReadComplete and simulate the end of the read loop. + parentChannel.pipeline().remove(readCompleteSupressHandler); + parentChannel.flushInbound(); + + childChannel.closeFuture().syncUninterruptibly(); + } + + protected abstract boolean useUserEventForResetFrame(); + + protected abstract boolean ignoreWindowUpdateFrames(); + + @Test + public void windowUpdateFrames() { + AtomicInteger numReads = new AtomicInteger(1); + LastInboundHandler inboundHandler = new LastInboundHandler(); + Http2StreamChannel childChannel = newInboundStream(3, false, numReads, inboundHandler); + + assertEquals(new DefaultHttp2HeadersFrame(request).stream(childChannel.stream()), inboundHandler.readInbound()); + + frameInboundWriter.writeInboundWindowUpdate(childChannel.stream().id(), 4); + + Http2WindowUpdateFrame updateFrame = inboundHandler.readInbound(); + if (ignoreWindowUpdateFrames()) { + assertNull(updateFrame); + } else { + assertEquals(new DefaultHttp2WindowUpdateFrame(4).stream(childChannel.stream()), updateFrame); + } + + frameInboundWriter.writeInboundWindowUpdate(Http2CodecUtil.CONNECTION_STREAM_ID, 6); + + assertNull(parentChannel.readInbound()); + childChannel.close().syncUninterruptibly(); + } + + @Test + public void childQueueIsDrainedAndNewDataIsDispatchedInParentReadLoopAutoRead() { + AtomicInteger numReads = new AtomicInteger(1); + final AtomicInteger channelReadCompleteCount = new AtomicInteger(0); + final AtomicBoolean shouldDisableAutoRead = new AtomicBoolean(); + Consumer<ChannelHandlerContext> ctxConsumer = new Consumer<ChannelHandlerContext>() { + @Override + public void accept(ChannelHandlerContext obj) { + channelReadCompleteCount.incrementAndGet(); + if (shouldDisableAutoRead.get()) { + obj.channel().config().setAutoRead(false); + } + } + }; + LastInboundHandler inboundHandler = new LastInboundHandler(ctxConsumer); + Http2StreamChannel childChannel = newInboundStream(3, false, numReads, inboundHandler); + childChannel.config().setAutoRead(false); + + Http2DataFrame dataFrame1 = new DefaultHttp2DataFrame(bb("1")).stream(childChannel.stream()); + Http2DataFrame dataFrame2 = new DefaultHttp2DataFrame(bb("2")).stream(childChannel.stream()); + Http2DataFrame dataFrame3 = new DefaultHttp2DataFrame(bb("3")).stream(childChannel.stream()); + Http2DataFrame dataFrame4 = new DefaultHttp2DataFrame(bb("4")).stream(childChannel.stream()); + + assertEquals(new DefaultHttp2HeadersFrame(request).stream(childChannel.stream()), inboundHandler.readInbound()); + + ChannelHandler readCompleteSupressHandler = new ChannelInboundHandlerAdapter() { + @Override + public void channelReadComplete(ChannelHandlerContext ctx) { + // We want to simulate the parent channel calling channelRead and delay calling channelReadComplete. + } + }; + parentChannel.pipeline().addFirst(readCompleteSupressHandler); + + frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("1"), 0, false); + + assertEqualsAndRelease(dataFrame1, inboundHandler.<Http2DataFrame>readInbound()); + + // We want one item to be in the queue, and allow the numReads to be larger than 1. This will ensure that + // when beginRead() is called the child channel is added to the readPending queue of the parent channel. + frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("2"), 0, false); + + numReads.set(10); + shouldDisableAutoRead.set(true); + childChannel.config().setAutoRead(true); + + frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("3"), 0, false); + frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("4"), 0, false); + + // Detecting EOS should flush all pending data regardless of read calls. + assertEqualsAndRelease(dataFrame2, inboundHandler.<Http2DataFrame>readInbound()); + assertEqualsAndRelease(dataFrame3, inboundHandler.<Http2DataFrame>readInbound()); + assertEqualsAndRelease(dataFrame4, inboundHandler.<Http2DataFrame>readInbound()); + + assertNull(inboundHandler.readInbound()); + + // Now we want to call channelReadComplete and simulate the end of the read loop. + parentChannel.pipeline().remove(readCompleteSupressHandler); + parentChannel.flushInbound(); + + // 3 = 1 for initialization + 1 for read when auto read was off + 1 for when auto read was back on + assertEquals(3, channelReadCompleteCount.get()); + } + + @Test + public void childQueueIsDrainedAndNewDataIsDispatchedInParentReadLoopNoAutoRead() { + final AtomicInteger numReads = new AtomicInteger(1); + final AtomicInteger channelReadCompleteCount = new AtomicInteger(0); + final AtomicBoolean shouldDisableAutoRead = new AtomicBoolean(); + Consumer<ChannelHandlerContext> ctxConsumer = new Consumer<ChannelHandlerContext>() { + @Override + public void accept(ChannelHandlerContext obj) { + channelReadCompleteCount.incrementAndGet(); + if (shouldDisableAutoRead.get()) { + obj.channel().config().setAutoRead(false); + } + } + }; + final LastInboundHandler inboundHandler = new LastInboundHandler(ctxConsumer); + Http2StreamChannel childChannel = newInboundStream(3, false, numReads, inboundHandler); + childChannel.config().setAutoRead(false); + + Http2DataFrame dataFrame1 = new DefaultHttp2DataFrame(bb("1")).stream(childChannel.stream()); + Http2DataFrame dataFrame2 = new DefaultHttp2DataFrame(bb("2")).stream(childChannel.stream()); + Http2DataFrame dataFrame3 = new DefaultHttp2DataFrame(bb("3")).stream(childChannel.stream()); + Http2DataFrame dataFrame4 = new DefaultHttp2DataFrame(bb("4")).stream(childChannel.stream()); + + assertEquals(new DefaultHttp2HeadersFrame(request).stream(childChannel.stream()), inboundHandler.readInbound()); + + ChannelHandler readCompleteSupressHandler = new ChannelInboundHandlerAdapter() { + @Override + public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { + // We want to simulate the parent channel calling channelRead and delay calling channelReadComplete. + } + }; + parentChannel.pipeline().addFirst(readCompleteSupressHandler); + + frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("1"), 0, false); + + assertEqualsAndRelease(dataFrame1, inboundHandler.<Http2Frame>readInbound()); + + // We want one item to be in the queue, and allow the numReads to be larger than 1. This will ensure that + // when beginRead() is called the child channel is added to the readPending queue of the parent channel. + frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("2"), 0, false); + + numReads.set(2); + childChannel.read(); + + assertEqualsAndRelease(dataFrame2, inboundHandler.<Http2Frame>readInbound()); + + assertNull(inboundHandler.readInbound()); + + // This is the second item that was read, this should be the last until we call read() again. This should also + // notify of readComplete(). + frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("3"), 0, false); + + assertEqualsAndRelease(dataFrame3, inboundHandler.<Http2Frame>readInbound()); + + frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("4"), 0, false); + assertNull(inboundHandler.readInbound()); + + childChannel.read(); + + assertEqualsAndRelease(dataFrame4, inboundHandler.<Http2Frame>readInbound()); + + assertNull(inboundHandler.readInbound()); + + // Now we want to call channelReadComplete and simulate the end of the read loop. + parentChannel.pipeline().remove(readCompleteSupressHandler); + parentChannel.flushInbound(); + + // 3 = 1 for initialization + 1 for first read of 2 items + 1 for second read of 2 items + + // 1 for parent channel readComplete + assertEquals(4, channelReadCompleteCount.get()); + } + + @Test + public void useReadWithoutAutoReadInRead() { + useReadWithoutAutoReadBuffered(false); + } + + @Test + public void useReadWithoutAutoReadInReadComplete() { + useReadWithoutAutoReadBuffered(true); + } + + private void useReadWithoutAutoReadBuffered(final boolean triggerOnReadComplete) { + LastInboundHandler inboundHandler = new LastInboundHandler(); + Http2StreamChannel childChannel = newInboundStream(3, false, inboundHandler); + assertTrue(childChannel.config().isAutoRead()); + childChannel.config().setAutoRead(false); + assertFalse(childChannel.config().isAutoRead()); + + Http2HeadersFrame headersFrame = inboundHandler.readInbound(); + assertNotNull(headersFrame); + + // Write some bytes to get the channel into the idle state with buffered data and also verify we + // do not dispatch it until we receive a read() call. + frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("hello world"), 0, false); + frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("foo"), 0, false); + frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("bar"), 0, false); + + // Add a handler which will request reads. + childChannel.pipeline().addFirst(new ChannelInboundHandlerAdapter() { + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { + super.channelReadComplete(ctx); + if (triggerOnReadComplete) { + ctx.read(); + ctx.read(); + } + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + ctx.fireChannelRead(msg); + if (!triggerOnReadComplete) { + ctx.read(); + ctx.read(); + } + } + }); + + inboundHandler.channel().read(); + + verifyFramesMultiplexedToCorrectChannel(childChannel, inboundHandler, 3); + + frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("hello world2"), 0, false); + frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("foo2"), 0, false); + frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("bar2"), 0, true); + + verifyFramesMultiplexedToCorrectChannel(childChannel, inboundHandler, 3); + } + + private static final class FlushSniffer extends ChannelOutboundHandlerAdapter { + + private boolean didFlush; + + public boolean checkFlush() { + boolean r = didFlush; + didFlush = false; + return r; + } + + @Override + public void flush(ChannelHandlerContext ctx) throws Exception { + didFlush = true; + super.flush(ctx); + } + } + + @Test + public void windowUpdatesAreFlushed() { + LastInboundHandler inboundHandler = new LastInboundHandler(); + FlushSniffer flushSniffer = new FlushSniffer(); + parentChannel.pipeline().addFirst(flushSniffer); + + Http2StreamChannel childChannel = newInboundStream(3, false, inboundHandler); + assertTrue(childChannel.config().isAutoRead()); + childChannel.config().setAutoRead(false); + assertFalse(childChannel.config().isAutoRead()); + + Http2HeadersFrame headersFrame = inboundHandler.readInbound(); + assertNotNull(headersFrame); + + assertTrue(flushSniffer.checkFlush()); + + // Write some bytes to get the channel into the idle state with buffered data and also verify we + // do not dispatch it until we receive a read() call. + frameInboundWriter.writeInboundData(childChannel.stream().id(), bb(16 * 1024), 0, false); + frameInboundWriter.writeInboundData(childChannel.stream().id(), bb(16 * 1024), 0, false); + assertTrue(flushSniffer.checkFlush()); + + verify(frameWriter, never()).writeWindowUpdate(eqCodecCtx(), anyInt(), anyInt(), anyChannelPromise()); + // only the first one was read because it was legacy auto-read behavior. + verifyFramesMultiplexedToCorrectChannel(childChannel, inboundHandler, 1); + assertFalse(flushSniffer.checkFlush()); + + // Trigger a read of the second frame. + childChannel.read(); + verifyFramesMultiplexedToCorrectChannel(childChannel, inboundHandler, 1); + // We expect a flush here because the StreamChannel will flush the smaller increment but the + // connection will collect the bytes and decide not to send a wire level frame until more are consumed. + assertTrue(flushSniffer.checkFlush()); + verify(frameWriter, never()).writeWindowUpdate(eqCodecCtx(), anyInt(), anyInt(), anyChannelPromise()); + + // Call read one more time which should trigger the writing of the flow control update. + childChannel.read(); + verify(frameWriter).writeWindowUpdate(eqCodecCtx(), eq(0), eq(32 * 1024), anyChannelPromise()); + verify(frameWriter).writeWindowUpdate( + eqCodecCtx(), eq(childChannel.stream().id()), eq(32 * 1024), anyChannelPromise()); + assertTrue(flushSniffer.checkFlush()); + } + + private static void verifyFramesMultiplexedToCorrectChannel(Http2StreamChannel streamChannel, + LastInboundHandler inboundHandler, + int numFrames) { + for (int i = 0; i < numFrames; i++) { + Http2StreamFrame frame = inboundHandler.readInbound(); + assertNotNull(i + " out of " + numFrames + " received", frame); + assertEquals(streamChannel.stream(), frame.stream()); + release(frame); + } + assertNull(inboundHandler.readInbound()); + } + + private static int eqStreamId(Http2StreamChannel channel) { + return eq(channel.stream().id()); + } +} diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MultiplexTransportTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MultiplexTransportTest.java new file mode 100644 index 0000000..0484678 --- /dev/null +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MultiplexTransportTest.java @@ -0,0 +1,257 @@ +/* + * Copyright 2019 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.codec.http2; + +import io.netty.bootstrap.Bootstrap; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.util.CharsetUtil; +import io.netty.util.NetUtil; +import io.netty.util.ReferenceCountUtil; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.net.InetSocketAddress; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static org.junit.Assert.assertFalse; + +public class Http2MultiplexTransportTest { + private static final ChannelHandler DISCARD_HANDLER = new ChannelInboundHandlerAdapter() { + + @Override + public boolean isSharable() { + return true; + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + ReferenceCountUtil.release(msg); + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { + ReferenceCountUtil.release(evt); + } + }; + + private EventLoopGroup eventLoopGroup; + private Channel clientChannel; + private Channel serverChannel; + private Channel serverConnectedChannel; + + @Before + public void setup() { + eventLoopGroup = new NioEventLoopGroup(); + } + + @After + public void teardown() { + if (clientChannel != null) { + clientChannel.close(); + } + if (serverChannel != null) { + serverChannel.close(); + } + if (serverConnectedChannel != null) { + serverConnectedChannel.close(); + } + eventLoopGroup.shutdownGracefully(0, 0, MILLISECONDS); + } + + @Test(timeout = 10000) + public void asyncSettingsAckWithMultiplexCodec() throws InterruptedException { + asyncSettingsAck0(new Http2MultiplexCodecBuilder(true, DISCARD_HANDLER).build(), null); + } + + @Test(timeout = 10000) + public void asyncSettingsAckWithMultiplexHandler() throws InterruptedException { + asyncSettingsAck0(new Http2FrameCodecBuilder(true).build(), + new Http2MultiplexHandler(DISCARD_HANDLER)); + } + + private void asyncSettingsAck0(final Http2FrameCodec codec, final ChannelHandler multiplexer) + throws InterruptedException { + // The client expects 2 settings frames. One from the connection setup and one from this test. + final CountDownLatch serverAckOneLatch = new CountDownLatch(1); + final CountDownLatch serverAckAllLatch = new CountDownLatch(2); + final CountDownLatch clientSettingsLatch = new CountDownLatch(2); + final CountDownLatch serverConnectedChannelLatch = new CountDownLatch(1); + final AtomicReference<Channel> serverConnectedChannelRef = new AtomicReference<Channel>(); + ServerBootstrap sb = new ServerBootstrap(); + sb.group(eventLoopGroup); + sb.channel(NioServerSocketChannel.class); + sb.childHandler(new ChannelInitializer<Channel>() { + @Override + protected void initChannel(Channel ch) { + ch.pipeline().addLast(codec); + if (multiplexer != null) { + ch.pipeline().addLast(multiplexer); + } + ch.pipeline().addLast(new ChannelInboundHandlerAdapter() { + @Override + public void channelActive(ChannelHandlerContext ctx) { + serverConnectedChannelRef.set(ctx.channel()); + serverConnectedChannelLatch.countDown(); + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + if (msg instanceof Http2SettingsAckFrame) { + serverAckOneLatch.countDown(); + serverAckAllLatch.countDown(); + } + ReferenceCountUtil.release(msg); + } + }); + } + }); + serverChannel = sb.bind(new InetSocketAddress(NetUtil.LOCALHOST, 0)).awaitUninterruptibly().channel(); + + Bootstrap bs = new Bootstrap(); + bs.group(eventLoopGroup); + bs.channel(NioSocketChannel.class); + bs.handler(new ChannelInitializer<Channel>() { + @Override + protected void initChannel(Channel ch) { + ch.pipeline().addLast(Http2MultiplexCodecBuilder + .forClient(DISCARD_HANDLER).autoAckSettingsFrame(false).build()); + ch.pipeline().addLast(new ChannelInboundHandlerAdapter() { + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + if (msg instanceof Http2SettingsFrame) { + clientSettingsLatch.countDown(); + } + ReferenceCountUtil.release(msg); + } + }); + } + }); + clientChannel = bs.connect(serverChannel.localAddress()).awaitUninterruptibly().channel(); + serverConnectedChannelLatch.await(); + serverConnectedChannel = serverConnectedChannelRef.get(); + + serverConnectedChannel.writeAndFlush(new DefaultHttp2SettingsFrame(new Http2Settings() + .maxConcurrentStreams(10))).sync(); + + clientSettingsLatch.await(); + + // We expect a timeout here because we want to asynchronously generate the SETTINGS ACK below. + assertFalse(serverAckOneLatch.await(300, MILLISECONDS)); + + // We expect 2 settings frames, the initial settings frame during connection establishment and the setting frame + // written in this test. We should ack both of these settings frames. + clientChannel.writeAndFlush(Http2SettingsAckFrame.INSTANCE).sync(); + clientChannel.writeAndFlush(Http2SettingsAckFrame.INSTANCE).sync(); + + serverAckAllLatch.await(); + } + + @Test(timeout = 5000L) + public void testFlushNotDiscarded() + throws InterruptedException { + final ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1); + + try { + ServerBootstrap sb = new ServerBootstrap(); + sb.group(eventLoopGroup); + sb.channel(NioServerSocketChannel.class); + sb.childHandler(new ChannelInitializer<Channel>() { + @Override + protected void initChannel(Channel ch) { + ch.pipeline().addLast(new Http2FrameCodecBuilder(true).build()); + ch.pipeline().addLast(new Http2MultiplexHandler(new ChannelInboundHandlerAdapter() { + @Override + public void channelRead(final ChannelHandlerContext ctx, Object msg) { + if (msg instanceof Http2HeadersFrame && ((Http2HeadersFrame) msg).isEndStream()) { + executorService.schedule(new Runnable() { + @Override + public void run() { + ctx.writeAndFlush(new DefaultHttp2HeadersFrame( + new DefaultHttp2Headers(), false)).addListener( + new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture future) { + ctx.write(new DefaultHttp2DataFrame( + Unpooled.copiedBuffer("Hello World", CharsetUtil.US_ASCII), + true)); + ctx.channel().eventLoop().execute(new Runnable() { + @Override + public void run() { + ctx.flush(); + } + }); + } + }); + } + }, 500, TimeUnit.MILLISECONDS); + } + ReferenceCountUtil.release(msg); + } + })); + } + }); + serverChannel = sb.bind(new InetSocketAddress(NetUtil.LOCALHOST, 0)).syncUninterruptibly().channel(); + + final CountDownLatch latch = new CountDownLatch(1); + Bootstrap bs = new Bootstrap(); + bs.group(eventLoopGroup); + bs.channel(NioSocketChannel.class); + bs.handler(new ChannelInitializer<Channel>() { + @Override + protected void initChannel(Channel ch) { + ch.pipeline().addLast(new Http2FrameCodecBuilder(false).build()); + ch.pipeline().addLast(new Http2MultiplexHandler(DISCARD_HANDLER)); + } + }); + clientChannel = bs.connect(serverChannel.localAddress()).syncUninterruptibly().channel(); + Http2StreamChannelBootstrap h2Bootstrap = new Http2StreamChannelBootstrap(clientChannel); + h2Bootstrap.handler(new ChannelInboundHandlerAdapter() { + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + if (msg instanceof Http2DataFrame && ((Http2DataFrame) msg).isEndStream()) { + latch.countDown(); + } + ReferenceCountUtil.release(msg); + } + }); + Http2StreamChannel streamChannel = h2Bootstrap.open().syncUninterruptibly().getNow(); + streamChannel.writeAndFlush(new DefaultHttp2HeadersFrame(new DefaultHttp2Headers(), true)) + .syncUninterruptibly(); + + latch.await(); + } finally { + executorService.shutdown(); + } + } +} diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2ServerUpgradeCodecTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2ServerUpgradeCodecTest.java index fe93b6b..3350754 100644 --- a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2ServerUpgradeCodecTest.java +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2ServerUpgradeCodecTest.java @@ -18,6 +18,8 @@ import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.DefaultChannelId; +import io.netty.channel.ServerChannel; import io.netty.channel.embedded.EmbeddedChannel; import io.netty.handler.codec.http.DefaultFullHttpRequest; import io.netty.handler.codec.http.DefaultHttpHeaders; @@ -32,40 +34,59 @@ import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import org.junit.Test; +import org.mockito.Mockito; public class Http2ServerUpgradeCodecTest { @Test public void testUpgradeToHttp2ConnectionHandler() { - testUpgrade(new Http2ConnectionHandlerBuilder().frameListener(new Http2FrameAdapter()).build()); + testUpgrade(new Http2ConnectionHandlerBuilder().frameListener(new Http2FrameAdapter()).build(), null); } @Test public void testUpgradeToHttp2FrameCodec() { - testUpgrade(new Http2FrameCodecBuilder(true).build()); + testUpgrade(new Http2FrameCodecBuilder(true).build(), null); } @Test public void testUpgradeToHttp2MultiplexCodec() { - testUpgrade(new Http2MultiplexCodecBuilder(true, new HttpInboundHandler()).build()); + testUpgrade(new Http2MultiplexCodecBuilder(true, new HttpInboundHandler()).build(), null); } - private static void testUpgrade(Http2ConnectionHandler handler) { + @Test + public void testUpgradeToHttp2FrameCodecWithMultiplexer() { + testUpgrade(new Http2FrameCodecBuilder(true).build(), + new Http2MultiplexHandler(new HttpInboundHandler())); + } + + private static void testUpgrade(Http2ConnectionHandler handler, ChannelHandler multiplexer) { FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.OPTIONS, "*"); request.headers().set(HttpHeaderNames.HOST, "netty.io"); request.headers().set(HttpHeaderNames.CONNECTION, "Upgrade, HTTP2-Settings"); request.headers().set(HttpHeaderNames.UPGRADE, "h2c"); request.headers().set("HTTP2-Settings", "AAMAAABkAAQAAP__"); - EmbeddedChannel channel = new EmbeddedChannel(new ChannelInboundHandlerAdapter()); + ServerChannel parent = Mockito.mock(ServerChannel.class); + EmbeddedChannel channel = new EmbeddedChannel(parent, DefaultChannelId.newInstance(), true, false, + new ChannelInboundHandlerAdapter()); ChannelHandlerContext ctx = channel.pipeline().firstContext(); - Http2ServerUpgradeCodec codec = new Http2ServerUpgradeCodec("connectionHandler", handler); + Http2ServerUpgradeCodec codec; + if (multiplexer == null) { + codec = new Http2ServerUpgradeCodec(handler); + } else { + codec = new Http2ServerUpgradeCodec((Http2FrameCodec) handler, multiplexer); + } assertTrue(codec.prepareUpgradeResponse(ctx, request, new DefaultHttpHeaders())); codec.upgradeTo(ctx, request); // Flush the channel to ensure we write out all buffered data channel.flush(); - assertSame(handler, channel.pipeline().remove("connectionHandler")); + channel.writeInbound(Http2CodecUtil.connectionPrefaceBuf()); + Http2FrameInboundWriter writer = new Http2FrameInboundWriter(channel); + writer.writeInboundSettings(new Http2Settings()); + writer.writeInboundRstStream(Http2CodecUtil.HTTP_UPGRADE_STREAM_ID, Http2Error.CANCEL.code()); + + assertSame(handler, channel.pipeline().remove(handler.getClass())); assertNull(channel.pipeline().get(handler.getClass())); assertTrue(channel.finish()); @@ -74,6 +95,10 @@ public class Http2ServerUpgradeCodecTest { assertNotNull(settingsBuffer); settingsBuffer.release(); + ByteBuf buf = channel.readOutbound(); + assertNotNull(buf); + buf.release(); + assertNull(channel.readOutbound()); } diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2StreamChannelIdTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2StreamChannelIdTest.java new file mode 100644 index 0000000..dddeddc --- /dev/null +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2StreamChannelIdTest.java @@ -0,0 +1,60 @@ +/* + * Copyright 2020 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.codec.http2; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; + +import org.junit.Test; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufInputStream; +import io.netty.buffer.ByteBufOutputStream; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelId; +import io.netty.channel.DefaultChannelId; + +import static org.junit.Assert.*; + +public class Http2StreamChannelIdTest { + + @Test + public void testSerialization() throws Exception { + ChannelId normalInstance = new Http2StreamChannelId(DefaultChannelId.newInstance(), 0); + + ByteBuf buf = Unpooled.buffer(); + ObjectOutputStream outStream = new ObjectOutputStream(new ByteBufOutputStream(buf)); + try { + outStream.writeObject(normalInstance); + } finally { + outStream.close(); + } + + ObjectInputStream inStream = new ObjectInputStream(new ByteBufInputStream(buf, true)); + final ChannelId deserializedInstance; + try { + deserializedInstance = (ChannelId) inStream.readObject(); + } finally { + inStream.close(); + } + + assertEquals(normalInstance, deserializedInstance); + assertEquals(normalInstance.hashCode(), deserializedInstance.hashCode()); + assertEquals(0, normalInstance.compareTo(deserializedInstance)); + assertEquals(normalInstance.asLongText(), deserializedInstance.asLongText()); + assertEquals(normalInstance.asShortText(), deserializedInstance.asShortText()); + } +} diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2TestUtil.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2TestUtil.java index f660381..ff420bf 100644 --- a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2TestUtil.java +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2TestUtil.java @@ -119,7 +119,7 @@ public final class Http2TestUtil { public static HpackEncoder newTestEncoder(boolean ignoreMaxHeaderListSize, long maxHeaderListSize, long maxHeaderTableSize) throws Http2Exception { - HpackEncoder hpackEncoder = new HpackEncoder(); + HpackEncoder hpackEncoder = new HpackEncoder(false, 16, 0); ByteBuf buf = Unpooled.buffer(); try { hpackEncoder.setMaxHeaderTableSize(buf, maxHeaderTableSize); @@ -139,7 +139,7 @@ public final class Http2TestUtil { } public static HpackDecoder newTestDecoder(long maxHeaderListSize, long maxHeaderTableSize) throws Http2Exception { - HpackDecoder hpackDecoder = new HpackDecoder(maxHeaderListSize, 32); + HpackDecoder hpackDecoder = new HpackDecoder(maxHeaderListSize); hpackDecoder.setMaxHeaderTableSize(maxHeaderTableSize); return hpackDecoder; } @@ -687,6 +687,10 @@ public final class Http2TestUtil { return ByteBufUtil.writeUtf8(UnpooledByteBufAllocator.DEFAULT, s); } + static ByteBuf bb(int size) { + return UnpooledByteBufAllocator.DEFAULT.buffer().writeZero(size); + } + static void assertEqualsAndRelease(Http2Frame expected, Http2Frame actual) { try { assertEquals(expected, actual); diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/HttpConversionUtilTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/HttpConversionUtilTest.java index 200dd39..21530c9 100644 --- a/codec-http2/src/test/java/io/netty/handler/codec/http2/HttpConversionUtilTest.java +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/HttpConversionUtilTest.java @@ -17,11 +17,18 @@ package io.netty.handler.codec.http2; import io.netty.handler.codec.http.DefaultHttpHeaders; import io.netty.handler.codec.http.HttpHeaders; +import io.netty.handler.codec.http.HttpVersion; import io.netty.util.AsciiString; import org.junit.Test; import static io.netty.handler.codec.http.HttpHeaderNames.CONNECTION; +import static io.netty.handler.codec.http.HttpHeaderNames.COOKIE; +import static io.netty.handler.codec.http.HttpHeaderNames.HOST; +import static io.netty.handler.codec.http.HttpHeaderNames.KEEP_ALIVE; +import static io.netty.handler.codec.http.HttpHeaderNames.PROXY_CONNECTION; import static io.netty.handler.codec.http.HttpHeaderNames.TE; +import static io.netty.handler.codec.http.HttpHeaderNames.TRANSFER_ENCODING; +import static io.netty.handler.codec.http.HttpHeaderNames.UPGRADE; import static io.netty.handler.codec.http.HttpHeaderValues.GZIP; import static io.netty.handler.codec.http.HttpHeaderValues.TRAILERS; import static org.junit.Assert.assertEquals; @@ -143,4 +150,43 @@ public class HttpConversionUtilTest { assertEquals(1, out.size()); assertSame("world", out.get("hello")); } + + @Test + public void addHttp2ToHttpHeadersCombinesCookies() throws Http2Exception { + Http2Headers inHeaders = new DefaultHttp2Headers(); + inHeaders.add("yes", "no"); + inHeaders.add(COOKIE, "foo=bar"); + inHeaders.add(COOKIE, "bax=baz"); + + HttpHeaders outHeaders = new DefaultHttpHeaders(); + + HttpConversionUtil.addHttp2ToHttpHeaders(5, inHeaders, outHeaders, HttpVersion.HTTP_1_1, false, false); + assertEquals("no", outHeaders.get("yes")); + assertEquals("foo=bar; bax=baz", outHeaders.get(COOKIE.toString())); + } + + @Test + public void connectionSpecificHeadersShouldBeRemoved() { + HttpHeaders inHeaders = new DefaultHttpHeaders(); + inHeaders.add(CONNECTION, "keep-alive"); + inHeaders.add(HOST, "example.com"); + @SuppressWarnings("deprecation") + AsciiString keepAlive = KEEP_ALIVE; + inHeaders.add(keepAlive, "timeout=5, max=1000"); + @SuppressWarnings("deprecation") + AsciiString proxyConnection = PROXY_CONNECTION; + inHeaders.add(proxyConnection, "timeout=5, max=1000"); + inHeaders.add(TRANSFER_ENCODING, "chunked"); + inHeaders.add(UPGRADE, "h2c"); + + Http2Headers outHeaders = new DefaultHttp2Headers(); + HttpConversionUtil.toHttp2Headers(inHeaders, outHeaders); + + assertFalse(outHeaders.contains(CONNECTION)); + assertFalse(outHeaders.contains(HOST)); + assertFalse(outHeaders.contains(keepAlive)); + assertFalse(outHeaders.contains(proxyConnection)); + assertFalse(outHeaders.contains(TRANSFER_ENCODING)); + assertFalse(outHeaders.contains(UPGRADE)); + } } diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/LastInboundHandler.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/LastInboundHandler.java index 38f400a..dcb9fb3 100644 --- a/codec-http2/src/test/java/io/netty/handler/codec/http2/LastInboundHandler.java +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/LastInboundHandler.java @@ -99,7 +99,7 @@ public class LastInboundHandler extends ChannelDuplexHandler { @Override public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception { - if (writabilityStates == "") { + if ("".equals(writabilityStates)) { writabilityStates = String.valueOf(ctx.channel().isWritable()); } else { writabilityStates += "," + ctx.channel().isWritable(); diff --git a/codec-memcache/pom.xml b/codec-memcache/pom.xml index 40c2a68..a4a8e28 100644 --- a/codec-memcache/pom.xml +++ b/codec-memcache/pom.xml @@ -20,7 +20,7 @@ <parent> <groupId>io.netty</groupId> <artifactId>netty-parent</artifactId> - <version>4.1.33.Final</version> + <version>4.1.48.Final</version> </parent> <artifactId>netty-codec-memcache</artifactId> diff --git a/codec-memcache/src/main/java/io/netty/handler/codec/memcache/AbstractMemcacheObject.java b/codec-memcache/src/main/java/io/netty/handler/codec/memcache/AbstractMemcacheObject.java index 2ac6c29..04313b5 100644 --- a/codec-memcache/src/main/java/io/netty/handler/codec/memcache/AbstractMemcacheObject.java +++ b/codec-memcache/src/main/java/io/netty/handler/codec/memcache/AbstractMemcacheObject.java @@ -17,6 +17,7 @@ package io.netty.handler.codec.memcache; import io.netty.handler.codec.DecoderResult; import io.netty.util.AbstractReferenceCounted; +import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.UnstableApi; /** @@ -38,10 +39,6 @@ public abstract class AbstractMemcacheObject extends AbstractReferenceCounted im @Override public void setDecoderResult(DecoderResult result) { - if (result == null) { - throw new NullPointerException("DecoderResult should not be null."); - } - - decoderResult = result; + this.decoderResult = ObjectUtil.checkNotNull(result, "DecoderResult should not be null."); } } diff --git a/codec-memcache/src/main/java/io/netty/handler/codec/memcache/DefaultMemcacheContent.java b/codec-memcache/src/main/java/io/netty/handler/codec/memcache/DefaultMemcacheContent.java index 1ff7bcd..dccd957 100644 --- a/codec-memcache/src/main/java/io/netty/handler/codec/memcache/DefaultMemcacheContent.java +++ b/codec-memcache/src/main/java/io/netty/handler/codec/memcache/DefaultMemcacheContent.java @@ -16,6 +16,7 @@ package io.netty.handler.codec.memcache; import io.netty.buffer.ByteBuf; +import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.StringUtil; import io.netty.util.internal.UnstableApi; @@ -31,10 +32,7 @@ public class DefaultMemcacheContent extends AbstractMemcacheObject implements Me * Creates a new instance with the specified content. */ public DefaultMemcacheContent(ByteBuf content) { - if (content == null) { - throw new NullPointerException("Content cannot be null."); - } - this.content = content; + this.content = ObjectUtil.checkNotNull(content, "content"); } @Override diff --git a/codec-memcache/src/main/java/io/netty/handler/codec/memcache/binary/AbstractBinaryMemcacheDecoder.java b/codec-memcache/src/main/java/io/netty/handler/codec/memcache/binary/AbstractBinaryMemcacheDecoder.java index 2c90382..bec754a 100644 --- a/codec-memcache/src/main/java/io/netty/handler/codec/memcache/binary/AbstractBinaryMemcacheDecoder.java +++ b/codec-memcache/src/main/java/io/netty/handler/codec/memcache/binary/AbstractBinaryMemcacheDecoder.java @@ -15,6 +15,8 @@ */ package io.netty.handler.codec.memcache.binary; +import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero; + import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelHandlerContext; @@ -59,9 +61,7 @@ public abstract class AbstractBinaryMemcacheDecoder<M extends BinaryMemcacheMess * @param chunkSize the maximum chunk size of the payload. */ protected AbstractBinaryMemcacheDecoder(int chunkSize) { - if (chunkSize < 0) { - throw new IllegalArgumentException("chunkSize must be a positive integer: " + chunkSize); - } + checkPositiveOrZero(chunkSize, "chunkSize"); this.chunkSize = chunkSize; } diff --git a/codec-memcache/src/main/java/io/netty/handler/codec/memcache/binary/AbstractBinaryMemcacheMessage.java b/codec-memcache/src/main/java/io/netty/handler/codec/memcache/binary/AbstractBinaryMemcacheMessage.java index c40ddf4..c9417e9 100644 --- a/codec-memcache/src/main/java/io/netty/handler/codec/memcache/binary/AbstractBinaryMemcacheMessage.java +++ b/codec-memcache/src/main/java/io/netty/handler/codec/memcache/binary/AbstractBinaryMemcacheMessage.java @@ -78,7 +78,7 @@ public abstract class AbstractBinaryMemcacheMessage this.key = key; short oldKeyLength = keyLength; keyLength = key == null ? 0 : (short) key.readableBytes(); - totalBodyLength = totalBodyLength + keyLength - oldKeyLength; + totalBodyLength = totalBodyLength + keyLength - oldKeyLength; return this; } @@ -232,4 +232,20 @@ public abstract class AbstractBinaryMemcacheMessage } return this; } + + /** + * Copies special metadata hold by this instance to the provided instance + * + * @param dst The instance where to copy the metadata of this instance to + */ + void copyMeta(AbstractBinaryMemcacheMessage dst) { + dst.magic = magic; + dst.opcode = opcode; + dst.keyLength = keyLength; + dst.extrasLength = extrasLength; + dst.dataType = dataType; + dst.totalBodyLength = totalBodyLength; + dst.opaque = opaque; + dst.cas = cas; + } } diff --git a/codec-memcache/src/main/java/io/netty/handler/codec/memcache/binary/DefaultBinaryMemcacheRequest.java b/codec-memcache/src/main/java/io/netty/handler/codec/memcache/binary/DefaultBinaryMemcacheRequest.java index 68feb03..bac5845 100644 --- a/codec-memcache/src/main/java/io/netty/handler/codec/memcache/binary/DefaultBinaryMemcacheRequest.java +++ b/codec-memcache/src/main/java/io/netty/handler/codec/memcache/binary/DefaultBinaryMemcacheRequest.java @@ -92,4 +92,14 @@ public class DefaultBinaryMemcacheRequest extends AbstractBinaryMemcacheMessage super.touch(hint); return this; } + + /** + * Copies special metadata hold by this instance to the provided instance + * + * @param dst The instance where to copy the metadata of this instance to + */ + void copyMeta(DefaultBinaryMemcacheRequest dst) { + super.copyMeta(dst); + dst.reserved = reserved; + } } diff --git a/codec-memcache/src/main/java/io/netty/handler/codec/memcache/binary/DefaultBinaryMemcacheResponse.java b/codec-memcache/src/main/java/io/netty/handler/codec/memcache/binary/DefaultBinaryMemcacheResponse.java index b632dce..639913e 100644 --- a/codec-memcache/src/main/java/io/netty/handler/codec/memcache/binary/DefaultBinaryMemcacheResponse.java +++ b/codec-memcache/src/main/java/io/netty/handler/codec/memcache/binary/DefaultBinaryMemcacheResponse.java @@ -41,7 +41,7 @@ public class DefaultBinaryMemcacheResponse extends AbstractBinaryMemcacheMessage /** * Create a new {@link DefaultBinaryMemcacheResponse} with the header and key. * - * @param key the key to use + * @param key the key to use. */ public DefaultBinaryMemcacheResponse(ByteBuf key) { this(key, null); @@ -92,4 +92,14 @@ public class DefaultBinaryMemcacheResponse extends AbstractBinaryMemcacheMessage super.touch(hint); return this; } + + /** + * Copies special metadata hold by this instance to the provided instance + * + * @param dst The instance where to copy the metadata of this instance to + */ + void copyMeta(DefaultBinaryMemcacheResponse dst) { + super.copyMeta(dst); + dst.status = status; + } } diff --git a/codec-memcache/src/main/java/io/netty/handler/codec/memcache/binary/DefaultFullBinaryMemcacheRequest.java b/codec-memcache/src/main/java/io/netty/handler/codec/memcache/binary/DefaultFullBinaryMemcacheRequest.java index dbc5bac..d0f485a 100644 --- a/codec-memcache/src/main/java/io/netty/handler/codec/memcache/binary/DefaultFullBinaryMemcacheRequest.java +++ b/codec-memcache/src/main/java/io/netty/handler/codec/memcache/binary/DefaultFullBinaryMemcacheRequest.java @@ -17,6 +17,7 @@ package io.netty.handler.codec.memcache.binary; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; +import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.UnstableApi; /** @@ -48,11 +49,7 @@ public class DefaultFullBinaryMemcacheRequest extends DefaultBinaryMemcacheReque public DefaultFullBinaryMemcacheRequest(ByteBuf key, ByteBuf extras, ByteBuf content) { super(key, extras); - if (content == null) { - throw new NullPointerException("Supplied content is null."); - } - - this.content = content; + this.content = ObjectUtil.checkNotNull(content, "content"); setTotalBodyLength(keyLength() + extrasLength() + content.readableBytes()); } @@ -102,7 +99,7 @@ public class DefaultFullBinaryMemcacheRequest extends DefaultBinaryMemcacheReque if (extras != null) { extras = extras.copy(); } - return new DefaultFullBinaryMemcacheRequest(key, extras, content().copy()); + return newInstance(key, extras, content().copy()); } @Override @@ -115,7 +112,7 @@ public class DefaultFullBinaryMemcacheRequest extends DefaultBinaryMemcacheReque if (extras != null) { extras = extras.duplicate(); } - return new DefaultFullBinaryMemcacheRequest(key, extras, content().duplicate()); + return newInstance(key, extras, content().duplicate()); } @Override @@ -133,6 +130,12 @@ public class DefaultFullBinaryMemcacheRequest extends DefaultBinaryMemcacheReque if (extras != null) { extras = extras.retainedDuplicate(); } - return new DefaultFullBinaryMemcacheRequest(key, extras, content); + return newInstance(key, extras, content); + } + + private DefaultFullBinaryMemcacheRequest newInstance(ByteBuf key, ByteBuf extras, ByteBuf content) { + DefaultFullBinaryMemcacheRequest newInstance = new DefaultFullBinaryMemcacheRequest(key, extras, content); + copyMeta(newInstance); + return newInstance; } } diff --git a/codec-memcache/src/main/java/io/netty/handler/codec/memcache/binary/DefaultFullBinaryMemcacheResponse.java b/codec-memcache/src/main/java/io/netty/handler/codec/memcache/binary/DefaultFullBinaryMemcacheResponse.java index 734cba8..686a9ad 100644 --- a/codec-memcache/src/main/java/io/netty/handler/codec/memcache/binary/DefaultFullBinaryMemcacheResponse.java +++ b/codec-memcache/src/main/java/io/netty/handler/codec/memcache/binary/DefaultFullBinaryMemcacheResponse.java @@ -17,6 +17,7 @@ package io.netty.handler.codec.memcache.binary; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; +import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.UnstableApi; /** @@ -48,11 +49,7 @@ public class DefaultFullBinaryMemcacheResponse extends DefaultBinaryMemcacheResp public DefaultFullBinaryMemcacheResponse(ByteBuf key, ByteBuf extras, ByteBuf content) { super(key, extras); - if (content == null) { - throw new NullPointerException("Supplied content is null."); - } - - this.content = content; + this.content = ObjectUtil.checkNotNull(content, "content"); setTotalBodyLength(keyLength() + extrasLength() + content.readableBytes()); } @@ -102,7 +99,7 @@ public class DefaultFullBinaryMemcacheResponse extends DefaultBinaryMemcacheResp if (extras != null) { extras = extras.copy(); } - return new DefaultFullBinaryMemcacheResponse(key, extras, content().copy()); + return newInstance(key, extras, content().copy()); } @Override @@ -115,7 +112,7 @@ public class DefaultFullBinaryMemcacheResponse extends DefaultBinaryMemcacheResp if (extras != null) { extras = extras.duplicate(); } - return new DefaultFullBinaryMemcacheResponse(key, extras, content().duplicate()); + return newInstance(key, extras, content().duplicate()); } @Override @@ -133,6 +130,12 @@ public class DefaultFullBinaryMemcacheResponse extends DefaultBinaryMemcacheResp if (extras != null) { extras = extras.retainedDuplicate(); } - return new DefaultFullBinaryMemcacheResponse(key, extras, content); + return newInstance(key, extras, content); + } + + private FullBinaryMemcacheResponse newInstance(ByteBuf key, ByteBuf extras, ByteBuf content) { + DefaultFullBinaryMemcacheResponse newInstance = new DefaultFullBinaryMemcacheResponse(key, extras, content); + copyMeta(newInstance); + return newInstance; } } diff --git a/codec-memcache/src/test/java/io/netty/handler/codec/memcache/binary/DefaultFullBinaryMemcacheRequestTest.java b/codec-memcache/src/test/java/io/netty/handler/codec/memcache/binary/DefaultFullBinaryMemcacheRequestTest.java new file mode 100644 index 0000000..6b7e985 --- /dev/null +++ b/codec-memcache/src/test/java/io/netty/handler/codec/memcache/binary/DefaultFullBinaryMemcacheRequestTest.java @@ -0,0 +1,99 @@ +/* + * Copyright 2019 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.codec.memcache.binary; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.util.CharsetUtil; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotSame; + +public class DefaultFullBinaryMemcacheRequestTest { + + private DefaultFullBinaryMemcacheRequest request; + + @Before + public void setUp() { + request = new DefaultFullBinaryMemcacheRequest( + Unpooled.copiedBuffer("key", CharsetUtil.UTF_8), + Unpooled.wrappedBuffer(new byte[]{1, 3, 4, 9}), + Unpooled.copiedBuffer("some value", CharsetUtil.UTF_8)); + request.setReserved((short) 534); + request.setMagic((byte) 0x03); + request.setOpcode((byte) 0x02); + request.setKeyLength((short) 32); + request.setExtrasLength((byte) 34); + request.setDataType((byte) 43); + request.setTotalBodyLength(345); + request.setOpaque(3); + request.setCas(345345L); + } + + @Test + public void fullCopy() { + FullBinaryMemcacheRequest newInstance = request.copy(); + try { + assertCopy(request, request.content(), newInstance); + } finally { + request.release(); + newInstance.release(); + } + } + + @Test + public void fullDuplicate() { + FullBinaryMemcacheRequest newInstance = request.duplicate(); + try { + assertCopy(request, request.content(), newInstance); + } finally { + request.release(); + } + } + + @Test + public void fullReplace() { + ByteBuf newContent = Unpooled.copiedBuffer("new value", CharsetUtil.UTF_8); + FullBinaryMemcacheRequest newInstance = request.replace(newContent); + try { + assertCopy(request, newContent, newInstance); + } finally { + request.release(); + newInstance.release(); + } + } + + private void assertCopy(FullBinaryMemcacheRequest expected, ByteBuf expectedContent, + FullBinaryMemcacheRequest actual) { + assertNotSame(expected, actual); + + assertEquals(expected.key(), actual.key()); + assertEquals(expected.extras(), actual.extras()); + assertEquals(expectedContent, actual.content()); + + assertEquals(expected.reserved(), actual.reserved()); + assertEquals(expected.magic(), actual.magic()); + assertEquals(expected.opcode(), actual.opcode()); + assertEquals(expected.keyLength(), actual.keyLength()); + assertEquals(expected.extrasLength(), actual.extrasLength()); + assertEquals(expected.dataType(), actual.dataType()); + assertEquals(expected.totalBodyLength(), actual.totalBodyLength()); + assertEquals(expected.opaque(), actual.opaque()); + assertEquals(expected.cas(), actual.cas()); + } +} diff --git a/codec-memcache/src/test/java/io/netty/handler/codec/memcache/binary/DefaultFullBinaryMemcacheResponseTest.java b/codec-memcache/src/test/java/io/netty/handler/codec/memcache/binary/DefaultFullBinaryMemcacheResponseTest.java new file mode 100644 index 0000000..f94989b --- /dev/null +++ b/codec-memcache/src/test/java/io/netty/handler/codec/memcache/binary/DefaultFullBinaryMemcacheResponseTest.java @@ -0,0 +1,98 @@ +/* + * Copyright 2019 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.codec.memcache.binary; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.util.CharsetUtil; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotSame; + +public class DefaultFullBinaryMemcacheResponseTest { + + private DefaultFullBinaryMemcacheResponse response; + + @Before + public void setUp() { + response = new DefaultFullBinaryMemcacheResponse( + Unpooled.copiedBuffer("key", CharsetUtil.UTF_8), + Unpooled.wrappedBuffer(new byte[]{1, 3, 4, 9}), + Unpooled.copiedBuffer("some value", CharsetUtil.UTF_8)); + response.setStatus((short) 1); + response.setMagic((byte) 0x03); + response.setOpcode((byte) 0x02); + response.setKeyLength((short) 32); + response.setExtrasLength((byte) 34); + response.setDataType((byte) 43); + response.setTotalBodyLength(345); + response.setOpaque(3); + response.setCas(345345L); + } + + @Test + public void fullCopy() { + FullBinaryMemcacheResponse newInstance = response.copy(); + try { + assertResponseEquals(response, response.content(), newInstance); + } finally { + response.release(); + newInstance.release(); + } + } + + @Test + public void fullDuplicate() { + try { + assertResponseEquals(response, response.content(), response.duplicate()); + } finally { + response.release(); + } + } + + @Test + public void fullReplace() { + ByteBuf newContent = Unpooled.copiedBuffer("new value", CharsetUtil.UTF_8); + FullBinaryMemcacheResponse newInstance = response.replace(newContent); + try { + assertResponseEquals(response, newContent, newInstance); + } finally { + response.release(); + newInstance.release(); + } + } + + private void assertResponseEquals(FullBinaryMemcacheResponse expected, ByteBuf expectedContent, + FullBinaryMemcacheResponse actual) { + assertNotSame(expected, actual); + + assertEquals(expected.key(), actual.key()); + assertEquals(expected.extras(), actual.extras()); + assertEquals(expectedContent, actual.content()); + + assertEquals(expected.status(), actual.status()); + assertEquals(expected.magic(), actual.magic()); + assertEquals(expected.opcode(), actual.opcode()); + assertEquals(expected.keyLength(), actual.keyLength()); + assertEquals(expected.extrasLength(), actual.extrasLength()); + assertEquals(expected.dataType(), actual.dataType()); + assertEquals(expected.totalBodyLength(), actual.totalBodyLength()); + assertEquals(expected.opaque(), actual.opaque()); + assertEquals(expected.cas(), actual.cas()); + } +} diff --git a/codec-mqtt/pom.xml b/codec-mqtt/pom.xml index 1de30c7..f611818 100644 --- a/codec-mqtt/pom.xml +++ b/codec-mqtt/pom.xml @@ -20,7 +20,7 @@ <parent> <groupId>io.netty</groupId> <artifactId>netty-parent</artifactId> - <version>4.1.33.Final</version> + <version>4.1.48.Final</version> </parent> <artifactId>netty-codec-mqtt</artifactId> diff --git a/codec-mqtt/src/main/java/io/netty/handler/codec/mqtt/MqttConnectPayload.java b/codec-mqtt/src/main/java/io/netty/handler/codec/mqtt/MqttConnectPayload.java index b7a4d99..93f63e5 100644 --- a/codec-mqtt/src/main/java/io/netty/handler/codec/mqtt/MqttConnectPayload.java +++ b/codec-mqtt/src/main/java/io/netty/handler/codec/mqtt/MqttConnectPayload.java @@ -16,6 +16,8 @@ package io.netty.handler.codec.mqtt; +import java.util.Arrays; + import io.netty.util.CharsetUtil; import io.netty.util.internal.StringUtil; @@ -103,9 +105,9 @@ public final class MqttConnectPayload { .append('[') .append("clientIdentifier=").append(clientIdentifier) .append(", willTopic=").append(willTopic) - .append(", willMessage=").append(willMessage) + .append(", willMessage=").append(Arrays.toString(willMessage)) .append(", userName=").append(userName) - .append(", password=").append(password) + .append(", password=").append(Arrays.toString(password)) .append(']') .toString(); } diff --git a/codec-mqtt/src/main/java/io/netty/handler/codec/mqtt/MqttMessage.java b/codec-mqtt/src/main/java/io/netty/handler/codec/mqtt/MqttMessage.java index 9b1efa0..4a1a620 100644 --- a/codec-mqtt/src/main/java/io/netty/handler/codec/mqtt/MqttMessage.java +++ b/codec-mqtt/src/main/java/io/netty/handler/codec/mqtt/MqttMessage.java @@ -29,6 +29,17 @@ public class MqttMessage { private final Object payload; private final DecoderResult decoderResult; + // Constants for fixed-header only message types with all flags set to 0 (see + // http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Table_2.2_-) + public static final MqttMessage PINGREQ = new MqttMessage(new MqttFixedHeader(MqttMessageType.PINGREQ, false, + MqttQoS.AT_MOST_ONCE, false, 0)); + + public static final MqttMessage PINGRESP = new MqttMessage(new MqttFixedHeader(MqttMessageType.PINGRESP, false, + MqttQoS.AT_MOST_ONCE, false, 0)); + + public static final MqttMessage DISCONNECT = new MqttMessage(new MqttFixedHeader(MqttMessageType.DISCONNECT, false, + MqttQoS.AT_MOST_ONCE, false, 0)); + public MqttMessage(MqttFixedHeader mqttFixedHeader) { this(mqttFixedHeader, null, null); } diff --git a/codec-mqtt/src/main/java/io/netty/handler/codec/mqtt/MqttSubAckPayload.java b/codec-mqtt/src/main/java/io/netty/handler/codec/mqtt/MqttSubAckPayload.java index 0e8c909..0929b72 100644 --- a/codec-mqtt/src/main/java/io/netty/handler/codec/mqtt/MqttSubAckPayload.java +++ b/codec-mqtt/src/main/java/io/netty/handler/codec/mqtt/MqttSubAckPayload.java @@ -16,6 +16,7 @@ package io.netty.handler.codec.mqtt; +import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.StringUtil; import java.util.ArrayList; @@ -30,9 +31,7 @@ public class MqttSubAckPayload { private final List<Integer> grantedQoSLevels; public MqttSubAckPayload(int... grantedQoSLevels) { - if (grantedQoSLevels == null) { - throw new NullPointerException("grantedQoSLevels"); - } + ObjectUtil.checkNotNull(grantedQoSLevels, "grantedQoSLevels"); List<Integer> list = new ArrayList<Integer>(grantedQoSLevels.length); for (int v: grantedQoSLevels) { @@ -42,9 +41,7 @@ public class MqttSubAckPayload { } public MqttSubAckPayload(Iterable<Integer> grantedQoSLevels) { - if (grantedQoSLevels == null) { - throw new NullPointerException("grantedQoSLevels"); - } + ObjectUtil.checkNotNull(grantedQoSLevels, "grantedQoSLevels"); List<Integer> list = new ArrayList<Integer>(); for (Integer v: grantedQoSLevels) { if (v == null) { diff --git a/codec-mqtt/src/main/java/io/netty/handler/codec/mqtt/MqttSubscribePayload.java b/codec-mqtt/src/main/java/io/netty/handler/codec/mqtt/MqttSubscribePayload.java index eb1b9c9..aa3a324 100644 --- a/codec-mqtt/src/main/java/io/netty/handler/codec/mqtt/MqttSubscribePayload.java +++ b/codec-mqtt/src/main/java/io/netty/handler/codec/mqtt/MqttSubscribePayload.java @@ -39,11 +39,12 @@ public final class MqttSubscribePayload { @Override public String toString() { StringBuilder builder = new StringBuilder(StringUtil.simpleClassName(this)).append('['); - for (int i = 0; i < topicSubscriptions.size() - 1; i++) { + for (int i = 0; i < topicSubscriptions.size(); i++) { builder.append(topicSubscriptions.get(i)).append(", "); } - builder.append(topicSubscriptions.get(topicSubscriptions.size() - 1)); - builder.append(']'); - return builder.toString(); + if (!topicSubscriptions.isEmpty()) { + builder.setLength(builder.length() - 2); + } + return builder.append(']').toString(); } } diff --git a/codec-mqtt/src/main/java/io/netty/handler/codec/mqtt/MqttUnsubscribePayload.java b/codec-mqtt/src/main/java/io/netty/handler/codec/mqtt/MqttUnsubscribePayload.java index b032d12..9812bd9 100644 --- a/codec-mqtt/src/main/java/io/netty/handler/codec/mqtt/MqttUnsubscribePayload.java +++ b/codec-mqtt/src/main/java/io/netty/handler/codec/mqtt/MqttUnsubscribePayload.java @@ -39,11 +39,12 @@ public final class MqttUnsubscribePayload { @Override public String toString() { StringBuilder builder = new StringBuilder(StringUtil.simpleClassName(this)).append('['); - for (int i = 0; i < topics.size() - 1; i++) { + for (int i = 0; i < topics.size(); i++) { builder.append("topicName = ").append(topics.get(i)).append(", "); } - builder.append("topicName = ").append(topics.get(topics.size() - 1)) - .append(']'); - return builder.toString(); + if (!topics.isEmpty()) { + builder.setLength(builder.length() - 2); + } + return builder.append("]").toString(); } } diff --git a/codec-mqtt/src/test/java/io/netty/handler/codec/mqtt/MqttCodecTest.java b/codec-mqtt/src/test/java/io/netty/handler/codec/mqtt/MqttCodecTest.java index 927334c..2264dc4 100644 --- a/codec-mqtt/src/test/java/io/netty/handler/codec/mqtt/MqttCodecTest.java +++ b/codec-mqtt/src/test/java/io/netty/handler/codec/mqtt/MqttCodecTest.java @@ -266,17 +266,17 @@ public class MqttCodecTest { @Test public void testPingReqMessage() throws Exception { - testMessageWithOnlyFixedHeader(MqttMessageType.PINGREQ); + testMessageWithOnlyFixedHeader(MqttMessage.PINGREQ); } @Test public void testPingRespMessage() throws Exception { - testMessageWithOnlyFixedHeader(MqttMessageType.PINGRESP); + testMessageWithOnlyFixedHeader(MqttMessage.PINGRESP); } @Test public void testDisconnectMessage() throws Exception { - testMessageWithOnlyFixedHeader(MqttMessageType.DISCONNECT); + testMessageWithOnlyFixedHeader(MqttMessage.DISCONNECT); } @Test @@ -450,8 +450,7 @@ public class MqttCodecTest { } } - private void testMessageWithOnlyFixedHeader(MqttMessageType messageType) throws Exception { - MqttMessage message = createMessageWithFixedHeader(messageType); + private void testMessageWithOnlyFixedHeader(MqttMessage message) throws Exception { ByteBuf byteBuf = MqttEncoder.doEncode(ALLOCATOR, message); final List<Object> out = new LinkedList<Object>(); diff --git a/codec-mqtt/src/test/java/io/netty/handler/codec/mqtt/MqttConnectPayloadTest.java b/codec-mqtt/src/test/java/io/netty/handler/codec/mqtt/MqttConnectPayloadTest.java index 5a929dc..f1d9dc0 100644 --- a/codec-mqtt/src/test/java/io/netty/handler/codec/mqtt/MqttConnectPayloadTest.java +++ b/codec-mqtt/src/test/java/io/netty/handler/codec/mqtt/MqttConnectPayloadTest.java @@ -21,6 +21,8 @@ import static org.junit.Assert.assertNull; import io.netty.util.CharsetUtil; import org.junit.Test; +import java.util.Collections; + public class MqttConnectPayloadTest { @Test @@ -88,4 +90,11 @@ public class MqttConnectPayloadTest { assertNull(mqttConnectPayload.willMessageInBytes()); assertNull(mqttConnectPayload.willMessage()); } + + /* See https://github.com/netty/netty/pull/9202 */ + @Test + public void testEmptyTopicsToString() { + new MqttSubscribePayload(Collections.<MqttTopicSubscription>emptyList()).toString(); + new MqttUnsubscribePayload(Collections.<String>emptyList()).toString(); + } } diff --git a/codec-redis/pom.xml b/codec-redis/pom.xml index bbad7d9..07d3bfc 100644 --- a/codec-redis/pom.xml +++ b/codec-redis/pom.xml @@ -20,7 +20,7 @@ <parent> <groupId>io.netty</groupId> <artifactId>netty-parent</artifactId> - <version>4.1.33.Final</version> + <version>4.1.48.Final</version> </parent> <artifactId>netty-codec-redis</artifactId> diff --git a/codec-redis/src/test/java/io/netty/handler/codec/redis/RedisDecoderTest.java b/codec-redis/src/test/java/io/netty/handler/codec/redis/RedisDecoderTest.java index 6d9bd19..c62ab40 100644 --- a/codec-redis/src/test/java/io/netty/handler/codec/redis/RedisDecoderTest.java +++ b/codec-redis/src/test/java/io/netty/handler/codec/redis/RedisDecoderTest.java @@ -306,4 +306,12 @@ public class RedisDecoderTest { ReferenceCountUtil.release(msg); ReferenceCountUtil.release(childBuf); } + + @Test + public void testPredefinedMessagesNotEqual() { + // both EMPTY_INSTANCE and NULL_INSTANCE have EMPTY_BUFFER as their 'data', + // however we need to check that they are not equal between themselves. + assertNotEquals(FullBulkStringRedisMessage.EMPTY_INSTANCE, FullBulkStringRedisMessage.NULL_INSTANCE); + assertNotEquals(FullBulkStringRedisMessage.NULL_INSTANCE, FullBulkStringRedisMessage.EMPTY_INSTANCE); + } } diff --git a/codec-smtp/pom.xml b/codec-smtp/pom.xml index 1cd05fd..b9ded10 100644 --- a/codec-smtp/pom.xml +++ b/codec-smtp/pom.xml @@ -20,7 +20,7 @@ <parent> <groupId>io.netty</groupId> <artifactId>netty-parent</artifactId> - <version>4.1.33.Final</version> + <version>4.1.48.Final</version> </parent> <artifactId>netty-codec-smtp</artifactId> diff --git a/codec-smtp/src/main/java/io/netty/handler/codec/smtp/SmtpCommand.java b/codec-smtp/src/main/java/io/netty/handler/codec/smtp/SmtpCommand.java index 63ba3f5..a2c69b8 100644 --- a/codec-smtp/src/main/java/io/netty/handler/codec/smtp/SmtpCommand.java +++ b/codec-smtp/src/main/java/io/netty/handler/codec/smtp/SmtpCommand.java @@ -31,6 +31,7 @@ import java.util.Map; public final class SmtpCommand { public static final SmtpCommand EHLO = new SmtpCommand(AsciiString.cached("EHLO")); public static final SmtpCommand HELO = new SmtpCommand(AsciiString.cached("HELO")); + public static final SmtpCommand AUTH = new SmtpCommand(AsciiString.cached("AUTH")); public static final SmtpCommand MAIL = new SmtpCommand(AsciiString.cached("MAIL")); public static final SmtpCommand RCPT = new SmtpCommand(AsciiString.cached("RCPT")); public static final SmtpCommand DATA = new SmtpCommand(AsciiString.cached("DATA")); @@ -40,11 +41,13 @@ public final class SmtpCommand { public static final SmtpCommand VRFY = new SmtpCommand(AsciiString.cached("VRFY")); public static final SmtpCommand HELP = new SmtpCommand(AsciiString.cached("HELP")); public static final SmtpCommand QUIT = new SmtpCommand(AsciiString.cached("QUIT")); + public static final SmtpCommand EMPTY = new SmtpCommand(AsciiString.cached("")); private static final Map<String, SmtpCommand> COMMANDS = new HashMap<String, SmtpCommand>(); static { COMMANDS.put(EHLO.name().toString(), EHLO); COMMANDS.put(HELO.name().toString(), HELO); + COMMANDS.put(AUTH.name().toString(), AUTH); COMMANDS.put(MAIL.name().toString(), MAIL); COMMANDS.put(RCPT.name().toString(), RCPT); COMMANDS.put(DATA.name().toString(), DATA); @@ -54,6 +57,7 @@ public final class SmtpCommand { COMMANDS.put(VRFY.name().toString(), VRFY); COMMANDS.put(HELP.name().toString(), HELP); COMMANDS.put(QUIT.name().toString(), QUIT); + COMMANDS.put(EMPTY.name().toString(), EMPTY); } /** diff --git a/codec-smtp/src/main/java/io/netty/handler/codec/smtp/SmtpRequestEncoder.java b/codec-smtp/src/main/java/io/netty/handler/codec/smtp/SmtpRequestEncoder.java index 9fb567f..0355a29 100644 --- a/codec-smtp/src/main/java/io/netty/handler/codec/smtp/SmtpRequestEncoder.java +++ b/codec-smtp/src/main/java/io/netty/handler/codec/smtp/SmtpRequestEncoder.java @@ -58,7 +58,8 @@ public final class SmtpRequestEncoder extends MessageToMessageEncoder<Object> { final ByteBuf buffer = ctx.alloc().buffer(); try { req.command().encode(buffer); - writeParameters(req.parameters(), buffer); + boolean notEmpty = req.command() != SmtpCommand.EMPTY; + writeParameters(req.parameters(), buffer, notEmpty); ByteBufUtil.writeShortBE(buffer, CRLF_SHORT); out.add(buffer); release = false; @@ -85,11 +86,13 @@ public final class SmtpRequestEncoder extends MessageToMessageEncoder<Object> { } } - private static void writeParameters(List<CharSequence> parameters, ByteBuf out) { + private static void writeParameters(List<CharSequence> parameters, ByteBuf out, boolean commandNotEmpty) { if (parameters.isEmpty()) { return; } - out.writeByte(SP); + if (commandNotEmpty) { + out.writeByte(SP); + } if (parameters instanceof RandomAccess) { final int sizeMinusOne = parameters.size() - 1; for (int i = 0; i < sizeMinusOne; i++) { diff --git a/codec-smtp/src/main/java/io/netty/handler/codec/smtp/SmtpRequests.java b/codec-smtp/src/main/java/io/netty/handler/codec/smtp/SmtpRequests.java index 2c42c5d..b9da1a3 100644 --- a/codec-smtp/src/main/java/io/netty/handler/codec/smtp/SmtpRequests.java +++ b/codec-smtp/src/main/java/io/netty/handler/codec/smtp/SmtpRequests.java @@ -20,6 +20,7 @@ import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.UnstableApi; import java.util.ArrayList; +import java.util.Collections; import java.util.List; /** @@ -49,6 +50,20 @@ public final class SmtpRequests { return new DefaultSmtpRequest(SmtpCommand.EHLO, hostname); } + /** + * Creates a {@code EMPTY} request. + */ + public static SmtpRequest empty(CharSequence... parameter) { + return new DefaultSmtpRequest(SmtpCommand.EMPTY, parameter); + } + + /** + * Creates a {@code AUTH} request. + */ + public static SmtpRequest auth(CharSequence... parameter) { + return new DefaultSmtpRequest(SmtpCommand.AUTH, parameter); + } + /** * Creates a {@code NOOP} request. */ @@ -94,9 +109,7 @@ public final class SmtpRequests { } else { List<CharSequence> params = new ArrayList<CharSequence>(mailParameters.length + 1); params.add(sender != null? "FROM:<" + sender + '>' : FROM_NULL_SENDER); - for (CharSequence param : mailParameters) { - params.add(param); - } + Collections.addAll(params, mailParameters); return new DefaultSmtpRequest(SmtpCommand.MAIL, params); } } @@ -111,9 +124,7 @@ public final class SmtpRequests { } else { List<CharSequence> params = new ArrayList<CharSequence>(rcptParameters.length + 1); params.add("TO:<" + recipient + '>'); - for (CharSequence param : rcptParameters) { - params.add(param); - } + Collections.addAll(params, rcptParameters); return new DefaultSmtpRequest(SmtpCommand.RCPT, params); } } diff --git a/codec-smtp/src/test/java/io/netty/handler/codec/smtp/SmtpRequestEncoderTest.java b/codec-smtp/src/test/java/io/netty/handler/codec/smtp/SmtpRequestEncoderTest.java index 7717b15..320bae4 100644 --- a/codec-smtp/src/test/java/io/netty/handler/codec/smtp/SmtpRequestEncoderTest.java +++ b/codec-smtp/src/test/java/io/netty/handler/codec/smtp/SmtpRequestEncoderTest.java @@ -22,7 +22,9 @@ import io.netty.handler.codec.EncoderException; import io.netty.util.CharsetUtil; import org.junit.Test; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; public class SmtpRequestEncoderTest { @@ -36,6 +38,21 @@ public class SmtpRequestEncoderTest { testEncode(SmtpRequests.helo("localhost"), "HELO localhost\r\n"); } + @Test + public void testEncodeAuth() { + testEncode(SmtpRequests.auth("LOGIN"), "AUTH LOGIN\r\n"); + } + + @Test + public void testEncodeAuthWithParameter() { + testEncode(SmtpRequests.auth("PLAIN", "dGVzdAB0ZXN0ADEyMzQ="), "AUTH PLAIN dGVzdAB0ZXN0ADEyMzQ=\r\n"); + } + + @Test + public void testEncodeEmpty() { + testEncode(SmtpRequests.empty("dGVzdAB0ZXN0ADEyMzQ="), "dGVzdAB0ZXN0ADEyMzQ=\r\n"); + } + @Test public void testEncodeMail() { testEncode(SmtpRequests.mail("me@netty.io"), "MAIL FROM:<me@netty.io>\r\n"); @@ -92,8 +109,12 @@ public class SmtpRequestEncoderTest { @Test(expected = EncoderException.class) public void testThrowsIfContentExpected() { EmbeddedChannel channel = new EmbeddedChannel(new SmtpRequestEncoder()); - assertTrue(channel.writeOutbound(SmtpRequests.data())); - channel.writeOutbound(SmtpRequests.noop()); + try { + assertTrue(channel.writeOutbound(SmtpRequests.data())); + channel.writeOutbound(SmtpRequests.noop()); + } finally { + channel.finishAndReleaseAll(); + } } @Test diff --git a/codec-socks/pom.xml b/codec-socks/pom.xml index d2c20ef..8007e41 100644 --- a/codec-socks/pom.xml +++ b/codec-socks/pom.xml @@ -20,7 +20,7 @@ <parent> <groupId>io.netty</groupId> <artifactId>netty-parent</artifactId> - <version>4.1.33.Final</version> + <version>4.1.48.Final</version> </parent> <artifactId>netty-codec-socks</artifactId> diff --git a/codec-socks/src/main/java/io/netty/handler/codec/socks/SocksAuthRequest.java b/codec-socks/src/main/java/io/netty/handler/codec/socks/SocksAuthRequest.java index 277e0d3..0b932a6 100644 --- a/codec-socks/src/main/java/io/netty/handler/codec/socks/SocksAuthRequest.java +++ b/codec-socks/src/main/java/io/netty/handler/codec/socks/SocksAuthRequest.java @@ -17,6 +17,7 @@ package io.netty.handler.codec.socks; import io.netty.buffer.ByteBuf; import io.netty.util.CharsetUtil; +import io.netty.util.internal.ObjectUtil; import java.nio.charset.CharsetEncoder; @@ -27,19 +28,15 @@ import java.nio.charset.CharsetEncoder; * @see SocksAuthRequestDecoder */ public final class SocksAuthRequest extends SocksRequest { - private static final CharsetEncoder asciiEncoder = CharsetUtil.encoder(CharsetUtil.US_ASCII); private static final SocksSubnegotiationVersion SUBNEGOTIATION_VERSION = SocksSubnegotiationVersion.AUTH_PASSWORD; private final String username; private final String password; public SocksAuthRequest(String username, String password) { super(SocksRequestType.AUTH); - if (username == null) { - throw new NullPointerException("username"); - } - if (password == null) { - throw new NullPointerException("username"); - } + ObjectUtil.checkNotNull(username, "username"); + ObjectUtil.checkNotNull(password, "password"); + final CharsetEncoder asciiEncoder = CharsetUtil.encoder(CharsetUtil.US_ASCII); if (!asciiEncoder.canEncode(username) || !asciiEncoder.canEncode(password)) { throw new IllegalArgumentException( "username: " + username + " or password: **** values should be in pure ascii"); diff --git a/codec-socks/src/main/java/io/netty/handler/codec/socks/SocksAuthResponse.java b/codec-socks/src/main/java/io/netty/handler/codec/socks/SocksAuthResponse.java index 5cacdde..64dda05 100644 --- a/codec-socks/src/main/java/io/netty/handler/codec/socks/SocksAuthResponse.java +++ b/codec-socks/src/main/java/io/netty/handler/codec/socks/SocksAuthResponse.java @@ -16,6 +16,7 @@ package io.netty.handler.codec.socks; import io.netty.buffer.ByteBuf; +import io.netty.util.internal.ObjectUtil; /** * An socks auth response. @@ -29,10 +30,7 @@ public final class SocksAuthResponse extends SocksResponse { public SocksAuthResponse(SocksAuthStatus authStatus) { super(SocksResponseType.AUTH); - if (authStatus == null) { - throw new NullPointerException("authStatus"); - } - this.authStatus = authStatus; + this.authStatus = ObjectUtil.checkNotNull(authStatus, "authStatus"); } /** diff --git a/codec-socks/src/main/java/io/netty/handler/codec/socks/SocksCmdRequest.java b/codec-socks/src/main/java/io/netty/handler/codec/socks/SocksCmdRequest.java index 18bd65f..997c71b 100644 --- a/codec-socks/src/main/java/io/netty/handler/codec/socks/SocksCmdRequest.java +++ b/codec-socks/src/main/java/io/netty/handler/codec/socks/SocksCmdRequest.java @@ -18,6 +18,7 @@ package io.netty.handler.codec.socks; import io.netty.buffer.ByteBuf; import io.netty.util.CharsetUtil; import io.netty.util.NetUtil; +import io.netty.util.internal.ObjectUtil; import java.net.IDN; @@ -35,15 +36,10 @@ public final class SocksCmdRequest extends SocksRequest { public SocksCmdRequest(SocksCmdType cmdType, SocksAddressType addressType, String host, int port) { super(SocksRequestType.CMD); - if (cmdType == null) { - throw new NullPointerException("cmdType"); - } - if (addressType == null) { - throw new NullPointerException("addressType"); - } - if (host == null) { - throw new NullPointerException("host"); - } + ObjectUtil.checkNotNull(cmdType, "cmdType"); + ObjectUtil.checkNotNull(addressType, "addressType"); + ObjectUtil.checkNotNull(host, "host"); + switch (addressType) { case IPv4: if (!NetUtil.isValidIpV4Address(host)) { diff --git a/codec-socks/src/main/java/io/netty/handler/codec/socks/SocksCmdResponse.java b/codec-socks/src/main/java/io/netty/handler/codec/socks/SocksCmdResponse.java index 94ee516..d4a0d1b 100644 --- a/codec-socks/src/main/java/io/netty/handler/codec/socks/SocksCmdResponse.java +++ b/codec-socks/src/main/java/io/netty/handler/codec/socks/SocksCmdResponse.java @@ -18,6 +18,7 @@ package io.netty.handler.codec.socks; import io.netty.buffer.ByteBuf; import io.netty.util.CharsetUtil; import io.netty.util.NetUtil; +import io.netty.util.internal.ObjectUtil; import java.net.IDN; @@ -61,12 +62,8 @@ public final class SocksCmdResponse extends SocksResponse { */ public SocksCmdResponse(SocksCmdStatus cmdStatus, SocksAddressType addressType, String host, int port) { super(SocksResponseType.CMD); - if (cmdStatus == null) { - throw new NullPointerException("cmdStatus"); - } - if (addressType == null) { - throw new NullPointerException("addressType"); - } + ObjectUtil.checkNotNull(cmdStatus, "cmdStatus"); + ObjectUtil.checkNotNull(addressType, "addressType"); if (host != null) { switch (addressType) { case IPv4: diff --git a/codec-socks/src/main/java/io/netty/handler/codec/socks/SocksInitRequest.java b/codec-socks/src/main/java/io/netty/handler/codec/socks/SocksInitRequest.java index 2299d2b..df6d4d7 100644 --- a/codec-socks/src/main/java/io/netty/handler/codec/socks/SocksInitRequest.java +++ b/codec-socks/src/main/java/io/netty/handler/codec/socks/SocksInitRequest.java @@ -16,6 +16,7 @@ package io.netty.handler.codec.socks; import io.netty.buffer.ByteBuf; +import io.netty.util.internal.ObjectUtil; import java.util.Collections; import java.util.List; @@ -31,10 +32,7 @@ public final class SocksInitRequest extends SocksRequest { public SocksInitRequest(List<SocksAuthScheme> authSchemes) { super(SocksRequestType.INIT); - if (authSchemes == null) { - throw new NullPointerException("authSchemes"); - } - this.authSchemes = authSchemes; + this.authSchemes = ObjectUtil.checkNotNull(authSchemes, "authSchemes"); } /** diff --git a/codec-socks/src/main/java/io/netty/handler/codec/socks/SocksInitResponse.java b/codec-socks/src/main/java/io/netty/handler/codec/socks/SocksInitResponse.java index b4d9039..9857e64 100644 --- a/codec-socks/src/main/java/io/netty/handler/codec/socks/SocksInitResponse.java +++ b/codec-socks/src/main/java/io/netty/handler/codec/socks/SocksInitResponse.java @@ -16,6 +16,7 @@ package io.netty.handler.codec.socks; import io.netty.buffer.ByteBuf; +import io.netty.util.internal.ObjectUtil; /** * An socks init response. @@ -28,10 +29,7 @@ public final class SocksInitResponse extends SocksResponse { public SocksInitResponse(SocksAuthScheme authScheme) { super(SocksResponseType.INIT); - if (authScheme == null) { - throw new NullPointerException("authScheme"); - } - this.authScheme = authScheme; + this.authScheme = ObjectUtil.checkNotNull(authScheme, "authScheme"); } /** diff --git a/codec-socks/src/main/java/io/netty/handler/codec/socks/SocksMessage.java b/codec-socks/src/main/java/io/netty/handler/codec/socks/SocksMessage.java index 5f5f48b..46b0de0 100644 --- a/codec-socks/src/main/java/io/netty/handler/codec/socks/SocksMessage.java +++ b/codec-socks/src/main/java/io/netty/handler/codec/socks/SocksMessage.java @@ -16,6 +16,7 @@ package io.netty.handler.codec.socks; import io.netty.buffer.ByteBuf; +import io.netty.util.internal.ObjectUtil; /** * An abstract class that defines a SocksMessage, providing common properties for @@ -30,10 +31,7 @@ public abstract class SocksMessage { private final SocksProtocolVersion protocolVersion = SocksProtocolVersion.SOCKS5; protected SocksMessage(SocksMessageType type) { - if (type == null) { - throw new NullPointerException("type"); - } - this.type = type; + this.type = ObjectUtil.checkNotNull(type, "type"); } /** diff --git a/codec-socks/src/main/java/io/netty/handler/codec/socks/SocksRequest.java b/codec-socks/src/main/java/io/netty/handler/codec/socks/SocksRequest.java index 148353c..2dd29a0 100644 --- a/codec-socks/src/main/java/io/netty/handler/codec/socks/SocksRequest.java +++ b/codec-socks/src/main/java/io/netty/handler/codec/socks/SocksRequest.java @@ -15,6 +15,8 @@ */ package io.netty.handler.codec.socks; +import io.netty.util.internal.ObjectUtil; + /** * An abstract class that defines a SocksRequest, providing common properties for * {@link SocksInitRequest}, {@link SocksAuthRequest}, {@link SocksCmdRequest} and {@link UnknownSocksRequest}. @@ -29,10 +31,7 @@ public abstract class SocksRequest extends SocksMessage { protected SocksRequest(SocksRequestType requestType) { super(SocksMessageType.REQUEST); - if (requestType == null) { - throw new NullPointerException("requestType"); - } - this.requestType = requestType; + this.requestType = ObjectUtil.checkNotNull(requestType, "requestType"); } /** diff --git a/codec-socks/src/main/java/io/netty/handler/codec/socks/SocksResponse.java b/codec-socks/src/main/java/io/netty/handler/codec/socks/SocksResponse.java index 8bdd3cc..e4226d3 100644 --- a/codec-socks/src/main/java/io/netty/handler/codec/socks/SocksResponse.java +++ b/codec-socks/src/main/java/io/netty/handler/codec/socks/SocksResponse.java @@ -15,6 +15,8 @@ */ package io.netty.handler.codec.socks; +import io.netty.util.internal.ObjectUtil; + /** * An abstract class that defines a SocksResponse, providing common properties for * {@link SocksInitResponse}, {@link SocksAuthResponse}, {@link SocksCmdResponse} and {@link UnknownSocksResponse}. @@ -29,10 +31,7 @@ public abstract class SocksResponse extends SocksMessage { protected SocksResponse(SocksResponseType responseType) { super(SocksMessageType.RESPONSE); - if (responseType == null) { - throw new NullPointerException("responseType"); - } - this.responseType = responseType; + this.responseType = ObjectUtil.checkNotNull(responseType, "responseType"); } /** diff --git a/codec-socks/src/main/java/io/netty/handler/codec/socksx/AbstractSocksMessage.java b/codec-socks/src/main/java/io/netty/handler/codec/socksx/AbstractSocksMessage.java index fbb847a..e66c63c 100644 --- a/codec-socks/src/main/java/io/netty/handler/codec/socksx/AbstractSocksMessage.java +++ b/codec-socks/src/main/java/io/netty/handler/codec/socksx/AbstractSocksMessage.java @@ -17,6 +17,7 @@ package io.netty.handler.codec.socksx; import io.netty.handler.codec.DecoderResult; +import io.netty.util.internal.ObjectUtil; /** * An abstract {@link SocksMessage}. @@ -32,9 +33,6 @@ public abstract class AbstractSocksMessage implements SocksMessage { @Override public void setDecoderResult(DecoderResult decoderResult) { - if (decoderResult == null) { - throw new NullPointerException("decoderResult"); - } - this.decoderResult = decoderResult; + this.decoderResult = ObjectUtil.checkNotNull(decoderResult, "decoderResult"); } } diff --git a/codec-socks/src/main/java/io/netty/handler/codec/socksx/SocksPortUnificationServerHandler.java b/codec-socks/src/main/java/io/netty/handler/codec/socksx/SocksPortUnificationServerHandler.java index c4dfe3f..e826767 100644 --- a/codec-socks/src/main/java/io/netty/handler/codec/socksx/SocksPortUnificationServerHandler.java +++ b/codec-socks/src/main/java/io/netty/handler/codec/socksx/SocksPortUnificationServerHandler.java @@ -25,6 +25,7 @@ import io.netty.handler.codec.socksx.v4.Socks4ServerEncoder; import io.netty.handler.codec.socksx.v5.Socks5AddressEncoder; import io.netty.handler.codec.socksx.v5.Socks5InitialRequestDecoder; import io.netty.handler.codec.socksx.v5.Socks5ServerEncoder; +import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; @@ -53,11 +54,7 @@ public class SocksPortUnificationServerHandler extends ByteToMessageDecoder { * This constructor is useful when a user wants to use an alternative {@link Socks5AddressEncoder}. */ public SocksPortUnificationServerHandler(Socks5ServerEncoder socks5encoder) { - if (socks5encoder == null) { - throw new NullPointerException("socks5encoder"); - } - - this.socks5encoder = socks5encoder; + this.socks5encoder = ObjectUtil.checkNotNull(socks5encoder, "socks5encoder"); } @Override diff --git a/codec-socks/src/main/java/io/netty/handler/codec/socksx/v4/DefaultSocks4CommandRequest.java b/codec-socks/src/main/java/io/netty/handler/codec/socksx/v4/DefaultSocks4CommandRequest.java index 169768e..4fc527c 100755 --- a/codec-socks/src/main/java/io/netty/handler/codec/socksx/v4/DefaultSocks4CommandRequest.java +++ b/codec-socks/src/main/java/io/netty/handler/codec/socksx/v4/DefaultSocks4CommandRequest.java @@ -16,6 +16,7 @@ package io.netty.handler.codec.socksx.v4; import io.netty.handler.codec.DecoderResult; +import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.StringUtil; import java.net.IDN; @@ -50,22 +51,13 @@ public class DefaultSocks4CommandRequest extends AbstractSocks4Message implement * @param userId the {@code USERID} field of the request */ public DefaultSocks4CommandRequest(Socks4CommandType type, String dstAddr, int dstPort, String userId) { - if (type == null) { - throw new NullPointerException("type"); - } - if (dstAddr == null) { - throw new NullPointerException("dstAddr"); - } if (dstPort <= 0 || dstPort >= 65536) { throw new IllegalArgumentException("dstPort: " + dstPort + " (expected: 1~65535)"); } - if (userId == null) { - throw new NullPointerException("userId"); - } - - this.userId = userId; - this.type = type; - this.dstAddr = IDN.toASCII(dstAddr); + this.type = ObjectUtil.checkNotNull(type, "type"); + this.dstAddr = IDN.toASCII( + ObjectUtil.checkNotNull(dstAddr, "dstAddr")); + this.userId = ObjectUtil.checkNotNull(userId, "userId"); this.dstPort = dstPort; } diff --git a/codec-socks/src/main/java/io/netty/handler/codec/socksx/v4/DefaultSocks4CommandResponse.java b/codec-socks/src/main/java/io/netty/handler/codec/socksx/v4/DefaultSocks4CommandResponse.java index bd76265..f9ba3f9 100755 --- a/codec-socks/src/main/java/io/netty/handler/codec/socksx/v4/DefaultSocks4CommandResponse.java +++ b/codec-socks/src/main/java/io/netty/handler/codec/socksx/v4/DefaultSocks4CommandResponse.java @@ -17,6 +17,7 @@ package io.netty.handler.codec.socksx.v4; import io.netty.handler.codec.DecoderResult; import io.netty.util.NetUtil; +import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.StringUtil; /** @@ -45,9 +46,6 @@ public class DefaultSocks4CommandResponse extends AbstractSocks4Message implemen * @param dstPort the {@code DSTPORT} field of the response */ public DefaultSocks4CommandResponse(Socks4CommandStatus status, String dstAddr, int dstPort) { - if (status == null) { - throw new NullPointerException("cmdStatus"); - } if (dstAddr != null) { if (!NetUtil.isValidIpV4Address(dstAddr)) { throw new IllegalArgumentException( @@ -58,7 +56,7 @@ public class DefaultSocks4CommandResponse extends AbstractSocks4Message implemen throw new IllegalArgumentException("dstPort: " + dstPort + " (expected: 0~65535)"); } - this.status = status; + this.status = ObjectUtil.checkNotNull(status, "cmdStatus"); this.dstAddr = dstAddr; this.dstPort = dstPort; } diff --git a/codec-socks/src/main/java/io/netty/handler/codec/socksx/v4/Socks4CommandStatus.java b/codec-socks/src/main/java/io/netty/handler/codec/socksx/v4/Socks4CommandStatus.java index fdcfe0f..959168d 100755 --- a/codec-socks/src/main/java/io/netty/handler/codec/socksx/v4/Socks4CommandStatus.java +++ b/codec-socks/src/main/java/io/netty/handler/codec/socksx/v4/Socks4CommandStatus.java @@ -15,6 +15,8 @@ */ package io.netty.handler.codec.socksx.v4; +import io.netty.util.internal.ObjectUtil; + /** * The status of {@link Socks4CommandResponse}. */ @@ -49,12 +51,8 @@ public class Socks4CommandStatus implements Comparable<Socks4CommandStatus> { } public Socks4CommandStatus(int byteValue, String name) { - if (name == null) { - throw new NullPointerException("name"); - } - + this.name = ObjectUtil.checkNotNull(name, "name"); this.byteValue = (byte) byteValue; - this.name = name; } public byte byteValue() { diff --git a/codec-socks/src/main/java/io/netty/handler/codec/socksx/v4/Socks4CommandType.java b/codec-socks/src/main/java/io/netty/handler/codec/socksx/v4/Socks4CommandType.java index 0cd6fe4..8cb0786 100755 --- a/codec-socks/src/main/java/io/netty/handler/codec/socksx/v4/Socks4CommandType.java +++ b/codec-socks/src/main/java/io/netty/handler/codec/socksx/v4/Socks4CommandType.java @@ -15,6 +15,8 @@ */ package io.netty.handler.codec.socksx.v4; +import io.netty.util.internal.ObjectUtil; + /** * The type of {@link Socks4CommandRequest}. */ @@ -43,11 +45,8 @@ public class Socks4CommandType implements Comparable<Socks4CommandType> { } public Socks4CommandType(int byteValue, String name) { - if (name == null) { - throw new NullPointerException("name"); - } + this.name = ObjectUtil.checkNotNull(name, "name"); this.byteValue = (byte) byteValue; - this.name = name; } public byte byteValue() { diff --git a/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/DefaultSocks5CommandRequest.java b/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/DefaultSocks5CommandRequest.java index ddd35fd..83c39e2 100755 --- a/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/DefaultSocks5CommandRequest.java +++ b/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/DefaultSocks5CommandRequest.java @@ -17,6 +17,7 @@ package io.netty.handler.codec.socksx.v5; import io.netty.handler.codec.DecoderResult; import io.netty.util.NetUtil; +import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.StringUtil; import java.net.IDN; @@ -34,15 +35,9 @@ public final class DefaultSocks5CommandRequest extends AbstractSocks5Message imp public DefaultSocks5CommandRequest( Socks5CommandType type, Socks5AddressType dstAddrType, String dstAddr, int dstPort) { - if (type == null) { - throw new NullPointerException("type"); - } - if (dstAddrType == null) { - throw new NullPointerException("dstAddrType"); - } - if (dstAddr == null) { - throw new NullPointerException("dstAddr"); - } + this.type = ObjectUtil.checkNotNull(type, "type"); + ObjectUtil.checkNotNull(dstAddrType, "dstAddrType"); + ObjectUtil.checkNotNull(dstAddr, "dstAddr"); if (dstAddrType == Socks5AddressType.IPv4) { if (!NetUtil.isValidIpV4Address(dstAddr)) { @@ -63,7 +58,6 @@ public final class DefaultSocks5CommandRequest extends AbstractSocks5Message imp throw new IllegalArgumentException("dstPort: " + dstPort + " (expected: 0~65535)"); } - this.type = type; this.dstAddrType = dstAddrType; this.dstAddr = dstAddr; this.dstPort = dstPort; diff --git a/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/DefaultSocks5CommandResponse.java b/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/DefaultSocks5CommandResponse.java index 8d9719e..c46a77a 100755 --- a/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/DefaultSocks5CommandResponse.java +++ b/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/DefaultSocks5CommandResponse.java @@ -17,6 +17,7 @@ package io.netty.handler.codec.socksx.v5; import io.netty.handler.codec.DecoderResult; import io.netty.util.NetUtil; +import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.StringUtil; import java.net.IDN; @@ -38,12 +39,9 @@ public final class DefaultSocks5CommandResponse extends AbstractSocks5Message im public DefaultSocks5CommandResponse( Socks5CommandStatus status, Socks5AddressType bndAddrType, String bndAddr, int bndPort) { - if (status == null) { - throw new NullPointerException("status"); - } - if (bndAddrType == null) { - throw new NullPointerException("bndAddrType"); - } + ObjectUtil.checkNotNull(status, "status"); + ObjectUtil.checkNotNull(bndAddrType, "bndAddrType"); + if (bndAddr != null) { if (bndAddrType == Socks5AddressType.IPv4) { if (!NetUtil.isValidIpV4Address(bndAddr)) { diff --git a/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/DefaultSocks5InitialRequest.java b/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/DefaultSocks5InitialRequest.java index 0610e04..5cef490 100755 --- a/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/DefaultSocks5InitialRequest.java +++ b/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/DefaultSocks5InitialRequest.java @@ -16,6 +16,7 @@ package io.netty.handler.codec.socksx.v5; import io.netty.handler.codec.DecoderResult; +import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.StringUtil; import java.util.ArrayList; @@ -30,9 +31,7 @@ public class DefaultSocks5InitialRequest extends AbstractSocks5Message implement private final List<Socks5AuthMethod> authMethods; public DefaultSocks5InitialRequest(Socks5AuthMethod... authMethods) { - if (authMethods == null) { - throw new NullPointerException("authMethods"); - } + ObjectUtil.checkNotNull(authMethods, "authMethods"); List<Socks5AuthMethod> list = new ArrayList<Socks5AuthMethod>(authMethods.length); for (Socks5AuthMethod m: authMethods) { @@ -50,9 +49,7 @@ public class DefaultSocks5InitialRequest extends AbstractSocks5Message implement } public DefaultSocks5InitialRequest(Iterable<Socks5AuthMethod> authMethods) { - if (authMethods == null) { - throw new NullPointerException("authSchemes"); - } + ObjectUtil.checkNotNull(authMethods, "authSchemes"); List<Socks5AuthMethod> list = new ArrayList<Socks5AuthMethod>(); for (Socks5AuthMethod m: authMethods) { diff --git a/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/DefaultSocks5InitialResponse.java b/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/DefaultSocks5InitialResponse.java index 23d1c12..4fe878d 100755 --- a/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/DefaultSocks5InitialResponse.java +++ b/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/DefaultSocks5InitialResponse.java @@ -16,6 +16,7 @@ package io.netty.handler.codec.socksx.v5; import io.netty.handler.codec.DecoderResult; +import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.StringUtil; /** @@ -26,10 +27,7 @@ public class DefaultSocks5InitialResponse extends AbstractSocks5Message implemen private final Socks5AuthMethod authMethod; public DefaultSocks5InitialResponse(Socks5AuthMethod authMethod) { - if (authMethod == null) { - throw new NullPointerException("authMethod"); - } - this.authMethod = authMethod; + this.authMethod = ObjectUtil.checkNotNull(authMethod, "authMethod"); } @Override diff --git a/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/DefaultSocks5PasswordAuthRequest.java b/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/DefaultSocks5PasswordAuthRequest.java index 6917789..c404711 100755 --- a/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/DefaultSocks5PasswordAuthRequest.java +++ b/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/DefaultSocks5PasswordAuthRequest.java @@ -16,6 +16,7 @@ package io.netty.handler.codec.socksx.v5; import io.netty.handler.codec.DecoderResult; +import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.StringUtil; /** @@ -27,12 +28,8 @@ public class DefaultSocks5PasswordAuthRequest extends AbstractSocks5Message impl private final String password; public DefaultSocks5PasswordAuthRequest(String username, String password) { - if (username == null) { - throw new NullPointerException("username"); - } - if (password == null) { - throw new NullPointerException("password"); - } + ObjectUtil.checkNotNull(username, "username"); + ObjectUtil.checkNotNull(password, "password"); if (username.length() > 255) { throw new IllegalArgumentException("username: **** (expected: less than 256 chars)"); diff --git a/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/DefaultSocks5PasswordAuthResponse.java b/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/DefaultSocks5PasswordAuthResponse.java index 6cd9a30..07002c7 100755 --- a/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/DefaultSocks5PasswordAuthResponse.java +++ b/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/DefaultSocks5PasswordAuthResponse.java @@ -16,6 +16,7 @@ package io.netty.handler.codec.socksx.v5; import io.netty.handler.codec.DecoderResult; +import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.StringUtil; /** @@ -26,11 +27,7 @@ public class DefaultSocks5PasswordAuthResponse extends AbstractSocks5Message imp private final Socks5PasswordAuthStatus status; public DefaultSocks5PasswordAuthResponse(Socks5PasswordAuthStatus status) { - if (status == null) { - throw new NullPointerException("status"); - } - - this.status = status; + this.status = ObjectUtil.checkNotNull(status, "status"); } @Override diff --git a/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5AddressType.java b/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5AddressType.java index 554721a..da85114 100755 --- a/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5AddressType.java +++ b/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5AddressType.java @@ -16,6 +16,8 @@ package io.netty.handler.codec.socksx.v5; +import io.netty.util.internal.ObjectUtil; + /** * The type of address in {@link Socks5CommandRequest} and {@link Socks5CommandResponse}. */ @@ -47,12 +49,8 @@ public class Socks5AddressType implements Comparable<Socks5AddressType> { } public Socks5AddressType(int byteValue, String name) { - if (name == null) { - throw new NullPointerException("name"); - } - + this.name = ObjectUtil.checkNotNull(name, "name"); this.byteValue = (byte) byteValue; - this.name = name; } public byte byteValue() { diff --git a/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5AuthMethod.java b/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5AuthMethod.java index 8efceee..0f5bfbb 100755 --- a/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5AuthMethod.java +++ b/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5AuthMethod.java @@ -16,6 +16,8 @@ package io.netty.handler.codec.socksx.v5; +import io.netty.util.internal.ObjectUtil; + /** * The authentication method of SOCKS5. */ @@ -54,12 +56,8 @@ public class Socks5AuthMethod implements Comparable<Socks5AuthMethod> { } public Socks5AuthMethod(int byteValue, String name) { - if (name == null) { - throw new NullPointerException("name"); - } - + this.name = ObjectUtil.checkNotNull(name, "name"); this.byteValue = (byte) byteValue; - this.name = name; } public byte byteValue() { diff --git a/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5ClientEncoder.java b/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5ClientEncoder.java index ea6092a..f460fca 100644 --- a/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5ClientEncoder.java +++ b/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5ClientEncoder.java @@ -22,6 +22,7 @@ import io.netty.channel.ChannelHandler.Sharable; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.EncoderException; import io.netty.handler.codec.MessageToByteEncoder; +import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.StringUtil; import java.util.List; @@ -48,11 +49,7 @@ public class Socks5ClientEncoder extends MessageToByteEncoder<Socks5Message> { * Creates a new instance with the specified {@link Socks5AddressEncoder}. */ public Socks5ClientEncoder(Socks5AddressEncoder addressEncoder) { - if (addressEncoder == null) { - throw new NullPointerException("addressEncoder"); - } - - this.addressEncoder = addressEncoder; + this.addressEncoder = ObjectUtil.checkNotNull(addressEncoder, "addressEncoder"); } /** diff --git a/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5CommandRequestDecoder.java b/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5CommandRequestDecoder.java index eab1b15..444d8fc 100644 --- a/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5CommandRequestDecoder.java +++ b/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5CommandRequestDecoder.java @@ -23,6 +23,7 @@ import io.netty.handler.codec.DecoderResult; import io.netty.handler.codec.ReplayingDecoder; import io.netty.handler.codec.socksx.SocksVersion; import io.netty.handler.codec.socksx.v5.Socks5CommandRequestDecoder.State; +import io.netty.util.internal.ObjectUtil; import java.util.List; @@ -48,11 +49,7 @@ public class Socks5CommandRequestDecoder extends ReplayingDecoder<State> { public Socks5CommandRequestDecoder(Socks5AddressDecoder addressDecoder) { super(State.INIT); - if (addressDecoder == null) { - throw new NullPointerException("addressDecoder"); - } - - this.addressDecoder = addressDecoder; + this.addressDecoder = ObjectUtil.checkNotNull(addressDecoder, "addressDecoder"); } @Override diff --git a/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5CommandResponseDecoder.java b/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5CommandResponseDecoder.java index e3ac387..ce6c59e 100644 --- a/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5CommandResponseDecoder.java +++ b/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5CommandResponseDecoder.java @@ -23,6 +23,7 @@ import io.netty.handler.codec.DecoderResult; import io.netty.handler.codec.ReplayingDecoder; import io.netty.handler.codec.socksx.SocksVersion; import io.netty.handler.codec.socksx.v5.Socks5CommandResponseDecoder.State; +import io.netty.util.internal.ObjectUtil; import java.util.List; @@ -48,11 +49,7 @@ public class Socks5CommandResponseDecoder extends ReplayingDecoder<State> { public Socks5CommandResponseDecoder(Socks5AddressDecoder addressDecoder) { super(State.INIT); - if (addressDecoder == null) { - throw new NullPointerException("addressDecoder"); - } - - this.addressDecoder = addressDecoder; + this.addressDecoder = ObjectUtil.checkNotNull(addressDecoder, "addressDecoder"); } @Override diff --git a/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5CommandStatus.java b/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5CommandStatus.java index 7973c04..ff2ee3d 100755 --- a/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5CommandStatus.java +++ b/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5CommandStatus.java @@ -16,6 +16,8 @@ package io.netty.handler.codec.socksx.v5; +import io.netty.util.internal.ObjectUtil; + /** * The status of {@link Socks5CommandResponse}. */ @@ -65,12 +67,8 @@ public class Socks5CommandStatus implements Comparable<Socks5CommandStatus> { } public Socks5CommandStatus(int byteValue, String name) { - if (name == null) { - throw new NullPointerException("name"); - } - + this.name = ObjectUtil.checkNotNull(name, "name"); this.byteValue = (byte) byteValue; - this.name = name; } public byte byteValue() { diff --git a/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5CommandType.java b/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5CommandType.java index ecc4ca3..d0ac784 100755 --- a/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5CommandType.java +++ b/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5CommandType.java @@ -16,6 +16,8 @@ package io.netty.handler.codec.socksx.v5; +import io.netty.util.internal.ObjectUtil; + /** * The type of {@link Socks5CommandRequest}. */ @@ -47,12 +49,8 @@ public class Socks5CommandType implements Comparable<Socks5CommandType> { } public Socks5CommandType(int byteValue, String name) { - if (name == null) { - throw new NullPointerException("name"); - } - + this.name = ObjectUtil.checkNotNull(name, "name"); this.byteValue = (byte) byteValue; - this.name = name; } public byte byteValue() { diff --git a/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5InitialRequestDecoder.java b/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5InitialRequestDecoder.java index f9a663a..45682fa 100644 --- a/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5InitialRequestDecoder.java +++ b/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5InitialRequestDecoder.java @@ -56,9 +56,6 @@ public class Socks5InitialRequestDecoder extends ReplayingDecoder<State> { } final int authMethodCnt = in.readUnsignedByte(); - if (actualReadableBytes() < authMethodCnt) { - break; - } final Socks5AuthMethod[] authMethods = new Socks5AuthMethod[authMethodCnt]; for (int i = 0; i < authMethodCnt; i++) { diff --git a/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5PasswordAuthStatus.java b/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5PasswordAuthStatus.java index e7ea4a1..e26b26a 100755 --- a/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5PasswordAuthStatus.java +++ b/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5PasswordAuthStatus.java @@ -16,6 +16,8 @@ package io.netty.handler.codec.socksx.v5; +import io.netty.util.internal.ObjectUtil; + /** * The status of {@link Socks5PasswordAuthResponse}. */ @@ -44,12 +46,8 @@ public class Socks5PasswordAuthStatus implements Comparable<Socks5PasswordAuthSt } public Socks5PasswordAuthStatus(int byteValue, String name) { - if (name == null) { - throw new NullPointerException("name"); - } - + this.name = ObjectUtil.checkNotNull(name, "name"); this.byteValue = (byte) byteValue; - this.name = name; } public byte byteValue() { diff --git a/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5ServerEncoder.java b/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5ServerEncoder.java index d24e559..fe7a8ce 100644 --- a/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5ServerEncoder.java +++ b/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5ServerEncoder.java @@ -21,6 +21,7 @@ import io.netty.channel.ChannelHandler.Sharable; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.EncoderException; import io.netty.handler.codec.MessageToByteEncoder; +import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.StringUtil; /** @@ -44,11 +45,7 @@ public class Socks5ServerEncoder extends MessageToByteEncoder<Socks5Message> { * Creates a new instance with the specified {@link Socks5AddressEncoder}. */ public Socks5ServerEncoder(Socks5AddressEncoder addressEncoder) { - if (addressEncoder == null) { - throw new NullPointerException("addressEncoder"); - } - - this.addressEncoder = addressEncoder; + this.addressEncoder = ObjectUtil.checkNotNull(addressEncoder, "addressEncoder"); } /** diff --git a/codec-socks/src/test/java/io/netty/handler/codec/socksx/v5/Socks5InitialRequestDecoderTest.java b/codec-socks/src/test/java/io/netty/handler/codec/socksx/v5/Socks5InitialRequestDecoderTest.java new file mode 100644 index 0000000..e472c0f --- /dev/null +++ b/codec-socks/src/test/java/io/netty/handler/codec/socksx/v5/Socks5InitialRequestDecoderTest.java @@ -0,0 +1,38 @@ +/* + * Copyright 2019 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.codec.socksx.v5; + +import io.netty.buffer.Unpooled; +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.handler.codec.DecoderResult; +import org.junit.Test; + +import static org.junit.Assert.*; + +public class Socks5InitialRequestDecoderTest { + @Test + public void testUnpackingCausesDecodeFail() { + EmbeddedChannel e = new EmbeddedChannel(new Socks5InitialRequestDecoder()); + assertFalse(e.writeInbound(Unpooled.wrappedBuffer(new byte[]{5, 2, 0}))); + assertTrue(e.writeInbound(Unpooled.wrappedBuffer(new byte[]{1}))); + Object o = e.readInbound(); + + assertTrue(o instanceof DefaultSocks5InitialRequest); + DefaultSocks5InitialRequest req = (DefaultSocks5InitialRequest) o; + assertSame(req.decoderResult(), DecoderResult.SUCCESS); + assertFalse(e.finish()); + } +} diff --git a/codec-stomp/pom.xml b/codec-stomp/pom.xml index 7f5e47d..ec0cd09 100644 --- a/codec-stomp/pom.xml +++ b/codec-stomp/pom.xml @@ -20,7 +20,7 @@ <parent> <groupId>io.netty</groupId> <artifactId>netty-parent</artifactId> - <version>4.1.33.Final</version> + <version>4.1.48.Final</version> </parent> <artifactId>netty-codec-stomp</artifactId> diff --git a/codec-stomp/src/main/java/io/netty/handler/codec/stomp/DefaultStompFrame.java b/codec-stomp/src/main/java/io/netty/handler/codec/stomp/DefaultStompFrame.java index 8df4c65..0d0a972 100644 --- a/codec-stomp/src/main/java/io/netty/handler/codec/stomp/DefaultStompFrame.java +++ b/codec-stomp/src/main/java/io/netty/handler/codec/stomp/DefaultStompFrame.java @@ -18,6 +18,7 @@ package io.netty.handler.codec.stomp; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.util.CharsetUtil; +import io.netty.util.internal.ObjectUtil; /** * Default implementation of {@link StompFrame}. @@ -36,12 +37,7 @@ public class DefaultStompFrame extends DefaultStompHeadersSubframe implements St DefaultStompFrame(StompCommand command, ByteBuf content, DefaultStompHeaders headers) { super(command, headers); - - if (content == null) { - throw new NullPointerException("content"); - } - - this.content = content; + this.content = ObjectUtil.checkNotNull(content, "content"); } @Override diff --git a/codec-stomp/src/main/java/io/netty/handler/codec/stomp/DefaultStompHeadersSubframe.java b/codec-stomp/src/main/java/io/netty/handler/codec/stomp/DefaultStompHeadersSubframe.java index 728a889..9ece744 100644 --- a/codec-stomp/src/main/java/io/netty/handler/codec/stomp/DefaultStompHeadersSubframe.java +++ b/codec-stomp/src/main/java/io/netty/handler/codec/stomp/DefaultStompHeadersSubframe.java @@ -16,6 +16,7 @@ package io.netty.handler.codec.stomp; import io.netty.handler.codec.DecoderResult; +import io.netty.util.internal.ObjectUtil; /** * Default implementation of {@link StompHeadersSubframe}. @@ -31,11 +32,7 @@ public class DefaultStompHeadersSubframe implements StompHeadersSubframe { } DefaultStompHeadersSubframe(StompCommand command, DefaultStompHeaders headers) { - if (command == null) { - throw new NullPointerException("command"); - } - - this.command = command; + this.command = ObjectUtil.checkNotNull(command, "command"); this.headers = headers == null ? new DefaultStompHeaders() : headers; } diff --git a/codec-stomp/src/main/java/io/netty/handler/codec/stomp/StompSubframeDecoder.java b/codec-stomp/src/main/java/io/netty/handler/codec/stomp/StompSubframeDecoder.java index b4d6a14..5700bb1 100644 --- a/codec-stomp/src/main/java/io/netty/handler/codec/stomp/StompSubframeDecoder.java +++ b/codec-stomp/src/main/java/io/netty/handler/codec/stomp/StompSubframeDecoder.java @@ -15,9 +15,6 @@ */ package io.netty.handler.codec.stomp; -import java.util.List; -import java.util.Locale; - import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelHandlerContext; @@ -26,34 +23,32 @@ import io.netty.handler.codec.DecoderResult; import io.netty.handler.codec.ReplayingDecoder; import io.netty.handler.codec.TooLongFrameException; import io.netty.handler.codec.stomp.StompSubframeDecoder.State; +import io.netty.util.ByteProcessor; import io.netty.util.internal.AppendableCharSequence; +import io.netty.util.internal.StringUtil; -import static io.netty.buffer.ByteBufUtil.indexOf; -import static io.netty.buffer.ByteBufUtil.readBytes; +import java.util.List; + +import static io.netty.buffer.ByteBufUtil.*; +import static io.netty.util.internal.ObjectUtil.*; /** - * Decodes {@link ByteBuf}s into {@link StompHeadersSubframe}s and - * {@link StompContentSubframe}s. + * Decodes {@link ByteBuf}s into {@link StompHeadersSubframe}s and {@link StompContentSubframe}s. * * <h3>Parameters to control memory consumption: </h3> - * {@code maxLineLength} the maximum length of line - - * restricts length of command and header lines - * If the length of the initial line exceeds this value, a - * {@link TooLongFrameException} will be raised. + * {@code maxLineLength} the maximum length of line - restricts length of command and header lines If the length of the + * initial line exceeds this value, a {@link TooLongFrameException} will be raised. * <br> - * {@code maxChunkSize} - * The maximum length of the content or each chunk. If the content length - * (or the length of each chunk) exceeds this value, the content or chunk - * ill be split into multiple {@link StompContentSubframe}s whose length is - * {@code maxChunkSize} at maximum. + * {@code maxChunkSize} The maximum length of the content or each chunk. If the content length (or the length of each + * chunk) exceeds this value, the content or chunk ill be split into multiple {@link StompContentSubframe}s whose length + * is {@code maxChunkSize} at maximum. * * <h3>Chunked Content</h3> - * - * If the content of a stomp message is greater than {@code maxChunkSize} - * the transfer encoding of the HTTP message is 'chunked', this decoder - * generates multiple {@link StompContentSubframe} instances to avoid excessive memory - * consumption. Note, that every message, even with no content decodes with - * {@link LastStompContentSubframe} at the end to simplify upstream message parsing. + * <p> + * If the content of a stomp message is greater than {@code maxChunkSize} the transfer encoding of the HTTP message is + * 'chunked', this decoder generates multiple {@link StompContentSubframe} instances to avoid excessive memory + * consumption. Note, that every message, even with no content decodes with {@link LastStompContentSubframe} at the end + * to simplify upstream message parsing. */ public class StompSubframeDecoder extends ReplayingDecoder<State> { @@ -69,9 +64,9 @@ public class StompSubframeDecoder extends ReplayingDecoder<State> { INVALID_CHUNK } - private final int maxLineLength; + private final Utf8LineParser commandParser; + private final HeaderParser headerParser; private final int maxChunkSize; - private final boolean validateHeaders; private int alreadyReadChunkSize; private LastStompContentSubframe lastContent; private long contentLength = -1; @@ -90,19 +85,11 @@ public class StompSubframeDecoder extends ReplayingDecoder<State> { public StompSubframeDecoder(int maxLineLength, int maxChunkSize, boolean validateHeaders) { super(State.SKIP_CONTROL_CHARACTERS); - if (maxLineLength <= 0) { - throw new IllegalArgumentException( - "maxLineLength must be a positive integer: " + - maxLineLength); - } - if (maxChunkSize <= 0) { - throw new IllegalArgumentException( - "maxChunkSize must be a positive integer: " + - maxChunkSize); - } + checkPositive(maxLineLength, "maxLineLength"); + checkPositive(maxChunkSize, "maxChunkSize"); this.maxChunkSize = maxChunkSize; - this.maxLineLength = maxLineLength; - this.validateHeaders = validateHeaders; + commandParser = new Utf8LineParser(new AppendableCharSequence(16), maxLineLength); + headerParser = new HeaderParser(new AppendableCharSequence(128), maxLineLength, validateHeaders); } @Override @@ -196,34 +183,24 @@ public class StompSubframeDecoder extends ReplayingDecoder<State> { } private StompCommand readCommand(ByteBuf in) { - String commandStr = readLine(in, 16); - StompCommand command = null; + CharSequence commandSequence = commandParser.parse(in); + if (commandSequence == null) { + throw new DecoderException("Failed to read command from channel"); + } + String commandStr = commandSequence.toString(); try { - command = StompCommand.valueOf(commandStr); + return StompCommand.valueOf(commandStr); } catch (IllegalArgumentException iae) { - //do nothing - } - if (command == null) { - commandStr = commandStr.toUpperCase(Locale.US); - try { - command = StompCommand.valueOf(commandStr); - } catch (IllegalArgumentException iae) { - //do nothing - } - } - if (command == null) { - throw new DecoderException("failed to read command from channel"); + throw new DecoderException("Cannot to parse command " + commandStr); } - return command; } private State readHeaders(ByteBuf buffer, StompHeaders headers) { - AppendableCharSequence buf = new AppendableCharSequence(128); for (;;) { - boolean headerRead = readHeader(headers, buf, buffer); + boolean headerRead = headerParser.parseHeader(headers, buffer); if (!headerRead) { if (headers.contains(StompHeaders.CONTENT_LENGTH)) { - contentLength = getContentLength(headers, 0); + contentLength = getContentLength(headers); if (contentLength == 0) { return State.FINALIZE_FRAME_READ; } @@ -233,8 +210,8 @@ public class StompSubframeDecoder extends ReplayingDecoder<State> { } } - private static long getContentLength(StompHeaders headers, long defaultValue) { - long contentLength = headers.getLong(StompHeaders.CONTENT_LENGTH, defaultValue); + private static long getContentLength(StompHeaders headers) { + long contentLength = headers.getLong(StompHeaders.CONTENT_LENGTH, 0L); if (contentLength < 0) { throw new DecoderException(StompHeaders.CONTENT_LENGTH + " must be non-negative"); } @@ -259,75 +236,147 @@ public class StompSubframeDecoder extends ReplayingDecoder<State> { } } - private String readLine(ByteBuf buffer, int initialBufferSize) { - AppendableCharSequence buf = new AppendableCharSequence(initialBufferSize); - int lineLength = 0; - for (;;) { - byte nextByte = buffer.readByte(); + private void resetDecoder() { + checkpoint(State.SKIP_CONTROL_CHARACTERS); + contentLength = -1; + alreadyReadChunkSize = 0; + lastContent = null; + } + + private static class Utf8LineParser implements ByteProcessor { + + private final AppendableCharSequence charSeq; + private final int maxLineLength; + + private int lineLength; + private char interim; + private boolean nextRead; + + Utf8LineParser(AppendableCharSequence charSeq, int maxLineLength) { + this.charSeq = checkNotNull(charSeq, "charSeq"); + this.maxLineLength = maxLineLength; + } + + AppendableCharSequence parse(ByteBuf byteBuf) { + reset(); + int offset = byteBuf.forEachByte(this); + if (offset == -1) { + return null; + } + + byteBuf.readerIndex(offset + 1); + return charSeq; + } + + AppendableCharSequence charSequence() { + return charSeq; + } + + @Override + public boolean process(byte nextByte) throws Exception { if (nextByte == StompConstants.CR) { - //do nothing - } else if (nextByte == StompConstants.LF) { - return buf.toString(); + ++lineLength; + return true; + } + + if (nextByte == StompConstants.LF) { + return false; + } + + if (++lineLength > maxLineLength) { + throw new TooLongFrameException("An STOMP line is larger than " + maxLineLength + " bytes."); + } + + // 1 byte - 0xxxxxxx - 7 bits + // 2 byte - 110xxxxx 10xxxxxx - 11 bits + // 3 byte - 1110xxxx 10xxxxxx 10xxxxxx - 16 bits + if (nextRead) { + interim |= (nextByte & 0x3F) << 6; + nextRead = false; + } else if (interim != 0) { // flush 2 or 3 byte + charSeq.append((char) (interim | (nextByte & 0x3F))); + interim = 0; + } else if (nextByte >= 0) { // INITIAL BRANCH + // The first 128 characters (US-ASCII) need one byte. + charSeq.append((char) nextByte); + } else if ((nextByte & 0xE0) == 0xC0) { + // The next 1920 characters need two bytes and we can define + // a first byte by mask 110xxxxx. + interim = (char) ((nextByte & 0x1F) << 6); } else { - if (lineLength >= maxLineLength) { - invalidLineLength(); - } - lineLength ++; - buf.append((char) nextByte); + // The rest of characters need three bytes. + interim = (char) ((nextByte & 0x0F) << 12); + nextRead = true; } + + return true; + } + + protected void reset() { + charSeq.reset(); + lineLength = 0; + interim = 0; + nextRead = false; } } - private boolean readHeader(StompHeaders headers, AppendableCharSequence buf, ByteBuf buffer) { - buf.reset(); - int lineLength = 0; - String key = null; - boolean valid = false; - for (;;) { - byte nextByte = buffer.readByte(); - - if (nextByte == StompConstants.COLON && key == null) { - key = buf.toString(); - valid = true; - buf.reset(); - } else if (nextByte == StompConstants.CR) { - //do nothing - } else if (nextByte == StompConstants.LF) { - if (key == null && lineLength == 0) { - return false; - } else if (valid) { - headers.add(key, buf.toString()); - } else if (validateHeaders) { - invalidHeader(key, buf.toString()); - } - return true; - } else { - if (lineLength >= maxLineLength) { - invalidLineLength(); - } - if (nextByte == StompConstants.COLON && key != null) { - valid = false; + private static final class HeaderParser extends Utf8LineParser { + + private final boolean validateHeaders; + + private String name; + private boolean valid; + + HeaderParser(AppendableCharSequence charSeq, int maxLineLength, boolean validateHeaders) { + super(charSeq, maxLineLength); + this.validateHeaders = validateHeaders; + } + + boolean parseHeader(StompHeaders headers, ByteBuf buf) { + AppendableCharSequence value = super.parse(buf); + if (value == null || (name == null && value.length() == 0)) { + return false; + } + + if (valid) { + headers.add(name, value.toString()); + } else if (validateHeaders) { + if (StringUtil.isNullOrEmpty(name)) { + throw new IllegalArgumentException("received an invalid header line '" + value.toString() + '\''); } - lineLength ++; - buf.append((char) nextByte); + String line = name + ':' + value.toString(); + throw new IllegalArgumentException("a header value or name contains a prohibited character ':'" + + ", " + line); } + return true; } - } - private void invalidHeader(String key, String value) { - String line = key != null ? key + ":" + value : value; - throw new IllegalArgumentException("a header value or name contains a prohibited character ':'" - + ", " + line); - } + @Override + public boolean process(byte nextByte) throws Exception { + if (nextByte == StompConstants.COLON) { + if (name == null) { + AppendableCharSequence charSeq = charSequence(); + if (charSeq.length() != 0) { + name = charSeq.substring(0, charSeq.length()); + charSeq.reset(); + valid = true; + return true; + } else { + name = StringUtil.EMPTY_STRING; + } + } else { + valid = false; + } + } - private void invalidLineLength() { - throw new TooLongFrameException("An STOMP line is larger than " + maxLineLength + " bytes."); - } + return super.process(nextByte); + } - private void resetDecoder() { - checkpoint(State.SKIP_CONTROL_CHARACTERS); - contentLength = -1; - alreadyReadChunkSize = 0; - lastContent = null; + @Override + protected void reset() { + name = null; + valid = false; + super.reset(); + } } } diff --git a/codec-stomp/src/main/java/io/netty/handler/codec/stomp/StompSubframeEncoder.java b/codec-stomp/src/main/java/io/netty/handler/codec/stomp/StompSubframeEncoder.java index 7999279..fd99f7b 100644 --- a/codec-stomp/src/main/java/io/netty/handler/codec/stomp/StompSubframeEncoder.java +++ b/codec-stomp/src/main/java/io/netty/handler/codec/stomp/StompSubframeEncoder.java @@ -15,17 +15,15 @@ */ package io.netty.handler.codec.stomp; -import java.util.List; -import java.util.Map.Entry; - import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; import io.netty.channel.ChannelHandlerContext; -import io.netty.handler.codec.AsciiHeadersEncoder; -import io.netty.handler.codec.AsciiHeadersEncoder.NewlineType; -import io.netty.handler.codec.AsciiHeadersEncoder.SeparatorType; import io.netty.handler.codec.MessageToMessageEncoder; import io.netty.util.CharsetUtil; +import java.util.List; +import java.util.Map.Entry; + /** * Encodes a {@link StompFrame} or a {@link StompSubframe} into a {@link ByteBuf}. */ @@ -64,11 +62,13 @@ public class StompSubframeEncoder extends MessageToMessageEncoder<StompSubframe> private static ByteBuf encodeFrame(StompHeadersSubframe frame, ChannelHandlerContext ctx) { ByteBuf buf = ctx.alloc().buffer(); - buf.writeCharSequence(frame.command().toString(), CharsetUtil.US_ASCII); + buf.writeCharSequence(frame.command().toString(), CharsetUtil.UTF_8); buf.writeByte(StompConstants.LF); - AsciiHeadersEncoder headersEncoder = new AsciiHeadersEncoder(buf, SeparatorType.COLON, NewlineType.LF); for (Entry<CharSequence, CharSequence> entry : frame.headers()) { - headersEncoder.encode(entry); + ByteBufUtil.writeUtf8(buf, entry.getKey()); + buf.writeByte(StompConstants.COLON); + ByteBufUtil.writeUtf8(buf, entry.getValue()); + buf.writeByte(StompConstants.LF); } buf.writeByte(StompConstants.LF); return buf; diff --git a/codec-stomp/src/test/java/io/netty/handler/codec/stomp/StompSubframeDecoderTest.java b/codec-stomp/src/test/java/io/netty/handler/codec/stomp/StompSubframeDecoderTest.java index fe77212..69e2b00 100644 --- a/codec-stomp/src/test/java/io/netty/handler/codec/stomp/StompSubframeDecoderTest.java +++ b/codec-stomp/src/test/java/io/netty/handler/codec/stomp/StompSubframeDecoderTest.java @@ -22,15 +22,9 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; -import static io.netty.handler.codec.stomp.StompTestConstants.FRAME_WITH_INVALID_HEADER; -import static io.netty.util.CharsetUtil.US_ASCII; -import static io.netty.util.CharsetUtil.UTF_8; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertTrue; +import static io.netty.handler.codec.stomp.StompTestConstants.*; +import static io.netty.util.CharsetUtil.*; +import static org.junit.Assert.*; public class StompSubframeDecoderTest { @@ -165,7 +159,7 @@ public class StompSubframeDecoderTest { @Test public void testValidateHeadersDecodingDisabled() { - ByteBuf invalidIncoming = Unpooled.copiedBuffer(FRAME_WITH_INVALID_HEADER.getBytes(US_ASCII)); + ByteBuf invalidIncoming = Unpooled.copiedBuffer(FRAME_WITH_INVALID_HEADER.getBytes(UTF_8)); assertTrue(channel.writeInbound(invalidIncoming)); StompHeadersSubframe frame = channel.readInbound(); @@ -185,7 +179,7 @@ public class StompSubframeDecoderTest { public void testValidateHeadersDecodingEnabled() { channel = new EmbeddedChannel(new StompSubframeDecoder(true)); - ByteBuf invalidIncoming = Unpooled.copiedBuffer(FRAME_WITH_INVALID_HEADER.getBytes(US_ASCII)); + ByteBuf invalidIncoming = Unpooled.wrappedBuffer(FRAME_WITH_INVALID_HEADER.getBytes(UTF_8)); assertTrue(channel.writeInbound(invalidIncoming)); StompHeadersSubframe frame = channel.readInbound(); @@ -194,4 +188,37 @@ public class StompSubframeDecoderTest { assertEquals("a header value or name contains a prohibited character ':', current-time:2000-01-01T00:00:00", frame.decoderResult().cause().getMessage()); } + + @Test + public void testNotValidFrameWithEmptyHeaderName() { + channel = new EmbeddedChannel(new StompSubframeDecoder(true)); + + ByteBuf invalidIncoming = Unpooled.wrappedBuffer(FRAME_WITH_EMPTY_HEADER_NAME.getBytes(UTF_8)); + assertTrue(channel.writeInbound(invalidIncoming)); + + StompHeadersSubframe frame = channel.readInbound(); + assertNotNull(frame); + assertTrue(frame.decoderResult().isFailure()); + assertEquals("received an invalid header line ':header-value'", + frame.decoderResult().cause().getMessage()); + } + + @Test + public void testUtf8FrameDecoding() { + channel = new EmbeddedChannel(new StompSubframeDecoder(true)); + + ByteBuf incoming = Unpooled.wrappedBuffer(SEND_FRAME_UTF8.getBytes(UTF_8)); + assertTrue(channel.writeInbound(incoming)); + + StompHeadersSubframe headersSubFrame = channel.readInbound(); + assertNotNull(headersSubFrame); + assertFalse(headersSubFrame.decoderResult().isFailure()); + assertEquals("/queue/â„–11±♛нетти♕", headersSubFrame.headers().getAsString("destination")); + assertTrue(headersSubFrame.headers().contains("content-type")); + + StompContentSubframe contentSubFrame = channel.readInbound(); + assertNotNull(contentSubFrame); + assertEquals("body", contentSubFrame.content().toString(UTF_8)); + assertTrue(contentSubFrame.release()); + } } diff --git a/codec-stomp/src/test/java/io/netty/handler/codec/stomp/StompSubframeEncoderTest.java b/codec-stomp/src/test/java/io/netty/handler/codec/stomp/StompSubframeEncoderTest.java index 939f8b4..efbac17 100644 --- a/codec-stomp/src/test/java/io/netty/handler/codec/stomp/StompSubframeEncoderTest.java +++ b/codec-stomp/src/test/java/io/netty/handler/codec/stomp/StompSubframeEncoderTest.java @@ -18,11 +18,13 @@ package io.netty.handler.codec.stomp; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.util.AsciiString; import io.netty.util.CharsetUtil; import org.junit.After; import org.junit.Before; import org.junit.Test; +import static io.netty.handler.codec.stomp.StompTestConstants.*; import static org.junit.Assert.*; public class StompSubframeEncoderTest { @@ -63,4 +65,22 @@ public class StompSubframeEncoderTest { assertEquals(StompTestConstants.CONNECT_FRAME, content); aggregatedBuffer.release(); } + + @Test + public void testUtf8FrameEncoding() { + StompFrame frame = new DefaultStompFrame(StompCommand.SEND, + Unpooled.wrappedBuffer("body".getBytes(CharsetUtil.UTF_8))); + StompHeaders incoming = frame.headers(); + incoming.set(StompHeaders.DESTINATION, "/queue/â„–11±♛нетти♕"); + incoming.set(StompHeaders.CONTENT_TYPE, AsciiString.of("text/plain")); + + channel.writeOutbound(frame); + + ByteBuf headers = channel.readOutbound(); + ByteBuf content = channel.readOutbound(); + ByteBuf fullFrame = Unpooled.wrappedBuffer(headers, content); + assertEquals(SEND_FRAME_UTF8, fullFrame.toString(CharsetUtil.UTF_8)); + assertTrue(fullFrame.release()); + } + } diff --git a/codec-stomp/src/test/java/io/netty/handler/codec/stomp/StompTestConstants.java b/codec-stomp/src/test/java/io/netty/handler/codec/stomp/StompTestConstants.java index 0d89a5c..314a933 100644 --- a/codec-stomp/src/test/java/io/netty/handler/codec/stomp/StompTestConstants.java +++ b/codec-stomp/src/test/java/io/netty/handler/codec/stomp/StompTestConstants.java @@ -64,5 +64,18 @@ public final class StompTestConstants { '\n' + "some body\0"; + public static final String FRAME_WITH_EMPTY_HEADER_NAME = "SEND\n" + + "destination:/some-destination\n" + + "content-type:text/plain\n" + + ":header-value\n" + + '\n' + + "some body\0"; + + public static final String SEND_FRAME_UTF8 = "SEND\n" + + "destination:/queue/â„–11±♛нетти♕\n" + + "content-type:text/plain\n" + + '\n' + + "body\0"; + private StompTestConstants() { } } diff --git a/codec-xml/pom.xml b/codec-xml/pom.xml index 54ef3c5..664314d 100644 --- a/codec-xml/pom.xml +++ b/codec-xml/pom.xml @@ -20,7 +20,7 @@ <parent> <groupId>io.netty</groupId> <artifactId>netty-parent</artifactId> - <version>4.1.33.Final</version> + <version>4.1.48.Final</version> </parent> <artifactId>netty-codec-xml</artifactId> diff --git a/codec-xml/src/main/java/io/netty/handler/codec/xml/XmlAttribute.java b/codec-xml/src/main/java/io/netty/handler/codec/xml/XmlAttribute.java index d40ce13..968d306 100644 --- a/codec-xml/src/main/java/io/netty/handler/codec/xml/XmlAttribute.java +++ b/codec-xml/src/main/java/io/netty/handler/codec/xml/XmlAttribute.java @@ -57,16 +57,30 @@ public class XmlAttribute { @Override public boolean equals(Object o) { - if (this == o) { return true; } - if (o == null || getClass() != o.getClass()) { return false; } + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } XmlAttribute that = (XmlAttribute) o; - if (!name.equals(that.name)) { return false; } - if (namespace != null ? !namespace.equals(that.namespace) : that.namespace != null) { return false; } - if (prefix != null ? !prefix.equals(that.prefix) : that.prefix != null) { return false; } - if (type != null ? !type.equals(that.type) : that.type != null) { return false; } - if (value != null ? !value.equals(that.value) : that.value != null) { return false; } + if (!name.equals(that.name)) { + return false; + } + if (namespace != null ? !namespace.equals(that.namespace) : that.namespace != null) { + return false; + } + if (prefix != null ? !prefix.equals(that.prefix) : that.prefix != null) { + return false; + } + if (type != null ? !type.equals(that.type) : that.type != null) { + return false; + } + if (value != null ? !value.equals(that.value) : that.value != null) { + return false; + } return true; } diff --git a/codec-xml/src/main/java/io/netty/handler/codec/xml/XmlContent.java b/codec-xml/src/main/java/io/netty/handler/codec/xml/XmlContent.java index a47df33..275297c 100644 --- a/codec-xml/src/main/java/io/netty/handler/codec/xml/XmlContent.java +++ b/codec-xml/src/main/java/io/netty/handler/codec/xml/XmlContent.java @@ -32,12 +32,18 @@ public abstract class XmlContent { @Override public boolean equals(Object o) { - if (this == o) { return true; } - if (o == null || getClass() != o.getClass()) { return false; } + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } XmlContent that = (XmlContent) o; - if (data != null ? !data.equals(that.data) : that.data != null) { return false; } + if (data != null ? !data.equals(that.data) : that.data != null) { + return false; + } return true; } diff --git a/codec-xml/src/main/java/io/netty/handler/codec/xml/XmlDTD.java b/codec-xml/src/main/java/io/netty/handler/codec/xml/XmlDTD.java index 754539b..e36648f 100644 --- a/codec-xml/src/main/java/io/netty/handler/codec/xml/XmlDTD.java +++ b/codec-xml/src/main/java/io/netty/handler/codec/xml/XmlDTD.java @@ -32,12 +32,18 @@ public class XmlDTD { @Override public boolean equals(Object o) { - if (this == o) { return true; } - if (o == null || getClass() != o.getClass()) { return false; } + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } XmlDTD xmlDTD = (XmlDTD) o; - if (text != null ? !text.equals(xmlDTD.text) : xmlDTD.text != null) { return false; } + if (text != null ? !text.equals(xmlDTD.text) : xmlDTD.text != null) { + return false; + } return true; } diff --git a/codec-xml/src/main/java/io/netty/handler/codec/xml/XmlDecoder.java b/codec-xml/src/main/java/io/netty/handler/codec/xml/XmlDecoder.java index 7408848..e84e595 100644 --- a/codec-xml/src/main/java/io/netty/handler/codec/xml/XmlDecoder.java +++ b/codec-xml/src/main/java/io/netty/handler/codec/xml/XmlDecoder.java @@ -39,7 +39,7 @@ public class XmlDecoder extends ByteToMessageDecoder { private static final XmlDocumentEnd XML_DOCUMENT_END = XmlDocumentEnd.INSTANCE; private final AsyncXMLStreamReader<AsyncByteArrayFeeder> streamReader = XML_INPUT_FACTORY.createAsyncForByteArray(); - private final AsyncByteArrayFeeder streamFeeder = (AsyncByteArrayFeeder) streamReader.getInputFeeder(); + private final AsyncByteArrayFeeder streamFeeder = streamReader.getInputFeeder(); @Override protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception { diff --git a/codec-xml/src/main/java/io/netty/handler/codec/xml/XmlDocumentStart.java b/codec-xml/src/main/java/io/netty/handler/codec/xml/XmlDocumentStart.java index 311a1f5..98ce875 100644 --- a/codec-xml/src/main/java/io/netty/handler/codec/xml/XmlDocumentStart.java +++ b/codec-xml/src/main/java/io/netty/handler/codec/xml/XmlDocumentStart.java @@ -54,17 +54,27 @@ public class XmlDocumentStart { @Override public boolean equals(Object o) { - if (this == o) { return true; } - if (o == null || getClass() != o.getClass()) { return false; } + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } XmlDocumentStart that = (XmlDocumentStart) o; - if (standalone != that.standalone) { return false; } - if (encoding != null ? !encoding.equals(that.encoding) : that.encoding != null) { return false; } + if (standalone != that.standalone) { + return false; + } + if (encoding != null ? !encoding.equals(that.encoding) : that.encoding != null) { + return false; + } if (encodingScheme != null ? !encodingScheme.equals(that.encodingScheme) : that.encodingScheme != null) { return false; } - if (version != null ? !version.equals(that.version) : that.version != null) { return false; } + if (version != null ? !version.equals(that.version) : that.version != null) { + return false; + } return true; } diff --git a/codec-xml/src/main/java/io/netty/handler/codec/xml/XmlElement.java b/codec-xml/src/main/java/io/netty/handler/codec/xml/XmlElement.java index 885e814..8391bd0 100644 --- a/codec-xml/src/main/java/io/netty/handler/codec/xml/XmlElement.java +++ b/codec-xml/src/main/java/io/netty/handler/codec/xml/XmlElement.java @@ -54,15 +54,27 @@ public abstract class XmlElement { @Override public boolean equals(Object o) { - if (this == o) { return true; } - if (o == null || getClass() != o.getClass()) { return false; } + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } XmlElement that = (XmlElement) o; - if (!name.equals(that.name)) { return false; } - if (namespace != null ? !namespace.equals(that.namespace) : that.namespace != null) { return false; } - if (namespaces != null ? !namespaces.equals(that.namespaces) : that.namespaces != null) { return false; } - if (prefix != null ? !prefix.equals(that.prefix) : that.prefix != null) { return false; } + if (!name.equals(that.name)) { + return false; + } + if (namespace != null ? !namespace.equals(that.namespace) : that.namespace != null) { + return false; + } + if (namespaces != null ? !namespaces.equals(that.namespaces) : that.namespaces != null) { + return false; + } + if (prefix != null ? !prefix.equals(that.prefix) : that.prefix != null) { + return false; + } return true; } diff --git a/codec-xml/src/main/java/io/netty/handler/codec/xml/XmlElementStart.java b/codec-xml/src/main/java/io/netty/handler/codec/xml/XmlElementStart.java index 17d603e..1902423 100644 --- a/codec-xml/src/main/java/io/netty/handler/codec/xml/XmlElementStart.java +++ b/codec-xml/src/main/java/io/netty/handler/codec/xml/XmlElementStart.java @@ -35,13 +35,21 @@ public class XmlElementStart extends XmlElement { @Override public boolean equals(Object o) { - if (this == o) { return true; } - if (o == null || getClass() != o.getClass()) { return false; } - if (!super.equals(o)) { return false; } + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + if (!super.equals(o)) { + return false; + } XmlElementStart that = (XmlElementStart) o; - if (attributes != null ? !attributes.equals(that.attributes) : that.attributes != null) { return false; } + if (attributes != null ? !attributes.equals(that.attributes) : that.attributes != null) { + return false; + } return true; } diff --git a/codec-xml/src/main/java/io/netty/handler/codec/xml/XmlEntityReference.java b/codec-xml/src/main/java/io/netty/handler/codec/xml/XmlEntityReference.java index ba3cba9..78ed9e7 100644 --- a/codec-xml/src/main/java/io/netty/handler/codec/xml/XmlEntityReference.java +++ b/codec-xml/src/main/java/io/netty/handler/codec/xml/XmlEntityReference.java @@ -38,13 +38,21 @@ public class XmlEntityReference { @Override public boolean equals(Object o) { - if (this == o) { return true; } - if (o == null || getClass() != o.getClass()) { return false; } + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } XmlEntityReference that = (XmlEntityReference) o; - if (name != null ? !name.equals(that.name) : that.name != null) { return false; } - if (text != null ? !text.equals(that.text) : that.text != null) { return false; } + if (name != null ? !name.equals(that.name) : that.name != null) { + return false; + } + if (text != null ? !text.equals(that.text) : that.text != null) { + return false; + } return true; } diff --git a/codec-xml/src/main/java/io/netty/handler/codec/xml/XmlNamespace.java b/codec-xml/src/main/java/io/netty/handler/codec/xml/XmlNamespace.java index 9cbb86f..2d0ae56 100644 --- a/codec-xml/src/main/java/io/netty/handler/codec/xml/XmlNamespace.java +++ b/codec-xml/src/main/java/io/netty/handler/codec/xml/XmlNamespace.java @@ -38,13 +38,21 @@ public class XmlNamespace { @Override public boolean equals(Object o) { - if (this == o) { return true; } - if (o == null || getClass() != o.getClass()) { return false; } + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } XmlNamespace that = (XmlNamespace) o; - if (prefix != null ? !prefix.equals(that.prefix) : that.prefix != null) { return false; } - if (uri != null ? !uri.equals(that.uri) : that.uri != null) { return false; } + if (prefix != null ? !prefix.equals(that.prefix) : that.prefix != null) { + return false; + } + if (uri != null ? !uri.equals(that.uri) : that.uri != null) { + return false; + } return true; } diff --git a/codec-xml/src/main/java/io/netty/handler/codec/xml/XmlProcessingInstruction.java b/codec-xml/src/main/java/io/netty/handler/codec/xml/XmlProcessingInstruction.java index 27dc4de..6f75880 100644 --- a/codec-xml/src/main/java/io/netty/handler/codec/xml/XmlProcessingInstruction.java +++ b/codec-xml/src/main/java/io/netty/handler/codec/xml/XmlProcessingInstruction.java @@ -38,13 +38,21 @@ public class XmlProcessingInstruction { @Override public boolean equals(Object o) { - if (this == o) { return true; } - if (o == null || getClass() != o.getClass()) { return false; } + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } XmlProcessingInstruction that = (XmlProcessingInstruction) o; - if (data != null ? !data.equals(that.data) : that.data != null) { return false; } - if (target != null ? !target.equals(that.target) : that.target != null) { return false; } + if (data != null ? !data.equals(that.data) : that.data != null) { + return false; + } + if (target != null ? !target.equals(that.target) : that.target != null) { + return false; + } return true; } diff --git a/codec-xml/src/test/java/io/netty/handler/codec/xml/XmlDecoderTest.java b/codec-xml/src/test/java/io/netty/handler/codec/xml/XmlDecoderTest.java index 1d623d6..500de1d 100644 --- a/codec-xml/src/test/java/io/netty/handler/codec/xml/XmlDecoderTest.java +++ b/codec-xml/src/test/java/io/netty/handler/codec/xml/XmlDecoderTest.java @@ -37,12 +37,12 @@ public class XmlDecoderTest { "<!DOCTYPE employee SYSTEM \"employee.dtd\">" + "<?xml-stylesheet type=\"text/css\" href=\"netty.css\"?>" + "<?xml-test ?>" + - "<employee xmlns:nettya=\"http://netty.io/netty/a\">" + + "<employee xmlns:nettya=\"https://netty.io/netty/a\">" + "<nettya:id>±1</nettya:id>\n" + "<name "; private static final String XML2 = "type=\"given\">Alba</name><![CDATA[ <some data >/> ]]>" + - " <!-- namespaced --><nettyb:salary xmlns:nettyb=\"http://netty.io/netty/b\" nettyb:period=\"weekly\">" + + " <!-- namespaced --><nettyb:salary xmlns:nettyb=\"https://netty.io/netty/b\" nettyb:period=\"weekly\">" + "100</nettyb:salary><last/></employee>"; private static final String XML3 = "<?xml version=\"1.1\" encoding=\"UTf-8\" standalone=\"yes\"?><netty></netty>"; @@ -99,13 +99,13 @@ public class XmlDecoderTest { assertThat(((XmlElementStart) temp).attributes().size(), is(0)); assertThat(((XmlElementStart) temp).namespaces().size(), is(1)); assertThat(((XmlElementStart) temp).namespaces().get(0).prefix(), is("nettya")); - assertThat(((XmlElementStart) temp).namespaces().get(0).uri(), is("http://netty.io/netty/a")); + assertThat(((XmlElementStart) temp).namespaces().get(0).uri(), is("https://netty.io/netty/a")); temp = channel.readInbound(); assertThat(temp, instanceOf(XmlElementStart.class)); assertThat(((XmlElementStart) temp).name(), is("id")); assertThat(((XmlElementStart) temp).prefix(), is("nettya")); - assertThat(((XmlElementStart) temp).namespace(), is("http://netty.io/netty/a")); + assertThat(((XmlElementStart) temp).namespace(), is("https://netty.io/netty/a")); assertThat(((XmlElementStart) temp).attributes().size(), is(0)); assertThat(((XmlElementStart) temp).namespaces().size(), is(0)); @@ -122,7 +122,7 @@ public class XmlDecoderTest { assertThat(temp, instanceOf(XmlElementEnd.class)); assertThat(((XmlElementEnd) temp).name(), is("id")); assertThat(((XmlElementEnd) temp).prefix(), is("nettya")); - assertThat(((XmlElementEnd) temp).namespace(), is("http://netty.io/netty/a")); + assertThat(((XmlElementEnd) temp).namespace(), is("https://netty.io/netty/a")); temp = channel.readInbound(); assertThat(temp, instanceOf(XmlCharacters.class)); @@ -171,15 +171,15 @@ public class XmlDecoderTest { assertThat(temp, instanceOf(XmlElementStart.class)); assertThat(((XmlElementStart) temp).name(), is("salary")); assertThat(((XmlElementStart) temp).prefix(), is("nettyb")); - assertThat(((XmlElementStart) temp).namespace(), is("http://netty.io/netty/b")); + assertThat(((XmlElementStart) temp).namespace(), is("https://netty.io/netty/b")); assertThat(((XmlElementStart) temp).attributes().size(), is(1)); assertThat(((XmlElementStart) temp).attributes().get(0).name(), is("period")); assertThat(((XmlElementStart) temp).attributes().get(0).value(), is("weekly")); assertThat(((XmlElementStart) temp).attributes().get(0).prefix(), is("nettyb")); - assertThat(((XmlElementStart) temp).attributes().get(0).namespace(), is("http://netty.io/netty/b")); + assertThat(((XmlElementStart) temp).attributes().get(0).namespace(), is("https://netty.io/netty/b")); assertThat(((XmlElementStart) temp).namespaces().size(), is(1)); assertThat(((XmlElementStart) temp).namespaces().get(0).prefix(), is("nettyb")); - assertThat(((XmlElementStart) temp).namespaces().get(0).uri(), is("http://netty.io/netty/b")); + assertThat(((XmlElementStart) temp).namespaces().get(0).uri(), is("https://netty.io/netty/b")); temp = channel.readInbound(); assertThat(temp, instanceOf(XmlCharacters.class)); @@ -189,10 +189,10 @@ public class XmlDecoderTest { assertThat(temp, instanceOf(XmlElementEnd.class)); assertThat(((XmlElementEnd) temp).name(), is("salary")); assertThat(((XmlElementEnd) temp).prefix(), is("nettyb")); - assertThat(((XmlElementEnd) temp).namespace(), is("http://netty.io/netty/b")); + assertThat(((XmlElementEnd) temp).namespace(), is("https://netty.io/netty/b")); assertThat(((XmlElementEnd) temp).namespaces().size(), is(1)); assertThat(((XmlElementEnd) temp).namespaces().get(0).prefix(), is("nettyb")); - assertThat(((XmlElementEnd) temp).namespaces().get(0).uri(), is("http://netty.io/netty/b")); + assertThat(((XmlElementEnd) temp).namespaces().get(0).uri(), is("https://netty.io/netty/b")); temp = channel.readInbound(); assertThat(temp, instanceOf(XmlElementStart.class)); @@ -216,7 +216,7 @@ public class XmlDecoderTest { assertThat(((XmlElementEnd) temp).namespace(), is("")); assertThat(((XmlElementEnd) temp).namespaces().size(), is(1)); assertThat(((XmlElementEnd) temp).namespaces().get(0).prefix(), is("nettya")); - assertThat(((XmlElementEnd) temp).namespaces().get(0).uri(), is("http://netty.io/netty/a")); + assertThat(((XmlElementEnd) temp).namespaces().get(0).uri(), is("https://netty.io/netty/a")); temp = channel.readInbound(); assertThat(temp, nullValue()); diff --git a/codec/pom.xml b/codec/pom.xml index 01205ef..33f1475 100644 --- a/codec/pom.xml +++ b/codec/pom.xml @@ -20,7 +20,7 @@ <parent> <groupId>io.netty</groupId> <artifactId>netty-parent</artifactId> - <version>4.1.33.Final</version> + <version>4.1.48.Final</version> </parent> <artifactId>netty-codec</artifactId> diff --git a/codec/src/main/java/io/netty/handler/codec/AsciiHeadersEncoder.java b/codec/src/main/java/io/netty/handler/codec/AsciiHeadersEncoder.java index b8162e8..446ef38 100644 --- a/codec/src/main/java/io/netty/handler/codec/AsciiHeadersEncoder.java +++ b/codec/src/main/java/io/netty/handler/codec/AsciiHeadersEncoder.java @@ -23,6 +23,7 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufUtil; import io.netty.util.AsciiString; import io.netty.util.CharsetUtil; +import io.netty.util.internal.ObjectUtil; public final class AsciiHeadersEncoder { @@ -63,19 +64,9 @@ public final class AsciiHeadersEncoder { } public AsciiHeadersEncoder(ByteBuf buf, SeparatorType separatorType, NewlineType newlineType) { - if (buf == null) { - throw new NullPointerException("buf"); - } - if (separatorType == null) { - throw new NullPointerException("separatorType"); - } - if (newlineType == null) { - throw new NullPointerException("newlineType"); - } - - this.buf = buf; - this.separatorType = separatorType; - this.newlineType = newlineType; + this.buf = ObjectUtil.checkNotNull(buf, "buf"); + this.separatorType = ObjectUtil.checkNotNull(separatorType, "separatorType"); + this.newlineType = ObjectUtil.checkNotNull(newlineType, "newlineType"); } public void encode(Entry<CharSequence, CharSequence> entry) { diff --git a/codec/src/main/java/io/netty/handler/codec/ByteToMessageDecoder.java b/codec/src/main/java/io/netty/handler/codec/ByteToMessageDecoder.java index ddf3d9b..46bd380 100644 --- a/codec/src/main/java/io/netty/handler/codec/ByteToMessageDecoder.java +++ b/codec/src/main/java/io/netty/handler/codec/ByteToMessageDecoder.java @@ -19,13 +19,18 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.CompositeByteBuf; import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelConfig; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.channel.socket.ChannelInputShutdownEvent; +import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.StringUtil; import java.util.List; +import static io.netty.util.internal.ObjectUtil.checkPositive; +import static java.lang.Integer.MAX_VALUE; + /** * {@link ChannelInboundHandlerAdapter} which decodes bytes in a stream-like fashion from one {@link ByteBuf} to an * other Message type. @@ -75,23 +80,25 @@ public abstract class ByteToMessageDecoder extends ChannelInboundHandlerAdapter public static final Cumulator MERGE_CUMULATOR = new Cumulator() { @Override public ByteBuf cumulate(ByteBufAllocator alloc, ByteBuf cumulation, ByteBuf in) { + if (!cumulation.isReadable() && in.isContiguous()) { + // If cumulation is empty and input buffer is contiguous, use it directly + cumulation.release(); + return in; + } try { - final ByteBuf buffer; - if (cumulation.writerIndex() > cumulation.maxCapacity() - in.readableBytes() - || cumulation.refCnt() > 1 || cumulation.isReadOnly()) { - // Expand cumulation (by replace it) when either there is not more room in the buffer - // or if the refCnt is greater then 1 which may happen when the user use slice().retain() or - // duplicate().retain() or if its read-only. - // - // See: - // - https://github.com/netty/netty/issues/2327 - // - https://github.com/netty/netty/issues/1764 - buffer = expandCumulation(alloc, cumulation, in.readableBytes()); - } else { - buffer = cumulation; + final int required = in.readableBytes(); + if (required > cumulation.maxWritableBytes() || + (required > cumulation.maxFastWritableBytes() && cumulation.refCnt() > 1) || + cumulation.isReadOnly()) { + // Expand cumulation (by replacing it) under the following conditions: + // - cumulation cannot be resized to accommodate the additional data + // - cumulation can be expanded with a reallocation operation to accommodate but the buffer is + // assumed to be shared (e.g. refCnt() > 1) and the reallocation may not be safe. + return expandCumulation(alloc, cumulation, in); } - buffer.writeBytes(in); - return buffer; + cumulation.writeBytes(in, in.readerIndex(), required); + in.readerIndex(in.writerIndex()); + return cumulation; } finally { // We must release in in all cases as otherwise it may produce a leak if writeBytes(...) throw // for whatever release (for example because of OutOfMemoryError) @@ -108,35 +115,33 @@ public abstract class ByteToMessageDecoder extends ChannelInboundHandlerAdapter public static final Cumulator COMPOSITE_CUMULATOR = new Cumulator() { @Override public ByteBuf cumulate(ByteBufAllocator alloc, ByteBuf cumulation, ByteBuf in) { - ByteBuf buffer; + if (!cumulation.isReadable()) { + cumulation.release(); + return in; + } + CompositeByteBuf composite = null; try { - if (cumulation.refCnt() > 1) { - // Expand cumulation (by replace it) when the refCnt is greater then 1 which may happen when the - // user use slice().retain() or duplicate().retain(). - // - // See: - // - https://github.com/netty/netty/issues/2327 - // - https://github.com/netty/netty/issues/1764 - buffer = expandCumulation(alloc, cumulation, in.readableBytes()); - buffer.writeBytes(in); - } else { - CompositeByteBuf composite; - if (cumulation instanceof CompositeByteBuf) { - composite = (CompositeByteBuf) cumulation; - } else { - composite = alloc.compositeBuffer(Integer.MAX_VALUE); - composite.addComponent(true, cumulation); + if (cumulation instanceof CompositeByteBuf && cumulation.refCnt() == 1) { + composite = (CompositeByteBuf) cumulation; + // Writer index must equal capacity if we are going to "write" + // new components to the end + if (composite.writerIndex() != composite.capacity()) { + composite.capacity(composite.writerIndex()); } - composite.addComponent(true, in); - in = null; - buffer = composite; + } else { + composite = alloc.compositeBuffer(Integer.MAX_VALUE).addFlattenedComponents(true, cumulation); } - return buffer; + composite.addFlattenedComponents(true, in); + in = null; + return composite; } finally { if (in != null) { - // We must release if the ownership was not transferred as otherwise it may produce a leak if - // writeBytes(...) throw for whatever release (for example because of OutOfMemoryError). + // We must release if the ownership was not transferred as otherwise it may produce a leak in.release(); + // Also release any new buffer allocated if we're not returning it + if (composite != null && composite != cumulation) { + composite.release(); + } } } } @@ -149,8 +154,14 @@ public abstract class ByteToMessageDecoder extends ChannelInboundHandlerAdapter ByteBuf cumulation; private Cumulator cumulator = MERGE_CUMULATOR; private boolean singleDecode; - private boolean decodeWasNull; private boolean first; + + /** + * 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; + /** * A bitmask where the bits are defined as * <ul> @@ -191,10 +202,7 @@ public abstract class ByteToMessageDecoder extends ChannelInboundHandlerAdapter * Set the {@link Cumulator} to use for cumulate the received {@link ByteBuf}s. */ public void setCumulator(Cumulator cumulator) { - if (cumulator == null) { - throw new NullPointerException("cumulator"); - } - this.cumulator = cumulator; + this.cumulator = ObjectUtil.checkNotNull(cumulator, "cumulator"); } /** @@ -202,9 +210,7 @@ public abstract class ByteToMessageDecoder extends ChannelInboundHandlerAdapter * The default is {@code 16}. */ public void setDiscardAfterReads(int discardAfterReads) { - if (discardAfterReads <= 0) { - throw new IllegalArgumentException("discardAfterReads must be > 0"); - } + checkPositive(discardAfterReads, "discardAfterReads"); this.discardAfterReads = discardAfterReads; } @@ -241,18 +247,14 @@ public abstract class ByteToMessageDecoder extends ChannelInboundHandlerAdapter if (buf != null) { // Directly set this to null so we are sure we not access it in any other method here anymore. cumulation = null; - + numReads = 0; int readable = buf.readableBytes(); if (readable > 0) { - ByteBuf bytes = buf.readBytes(readable); - buf.release(); - ctx.fireChannelRead(bytes); + ctx.fireChannelRead(buf); + ctx.fireChannelReadComplete(); } else { buf.release(); } - - numReads = 0; - ctx.fireChannelReadComplete(); } handlerRemoved0(ctx); } @@ -268,13 +270,9 @@ public abstract class ByteToMessageDecoder extends ChannelInboundHandlerAdapter if (msg instanceof ByteBuf) { CodecOutputList out = CodecOutputList.newInstance(); try { - ByteBuf data = (ByteBuf) msg; first = cumulation == null; - if (first) { - cumulation = data; - } else { - cumulation = cumulator.cumulate(ctx.alloc(), cumulation, data); - } + cumulation = cumulator.cumulate(ctx.alloc(), + first ? Unpooled.EMPTY_BUFFER : cumulation, (ByteBuf) msg); callDecode(ctx, cumulation, out); } catch (DecoderException e) { throw e; @@ -293,7 +291,7 @@ public abstract class ByteToMessageDecoder extends ChannelInboundHandlerAdapter } int size = out.size(); - decodeWasNull = !out.insertSinceRecycled(); + firedChannelRead |= out.insertSinceRecycled(); fireChannelRead(ctx, out, size); out.recycle(); } @@ -328,12 +326,10 @@ public abstract class ByteToMessageDecoder extends ChannelInboundHandlerAdapter public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { numReads = 0; discardSomeReadBytes(); - if (decodeWasNull) { - decodeWasNull = false; - if (!ctx.channel().config().isAutoRead()) { - ctx.read(); - } + if (!firedChannelRead && !ctx.channel().config().isAutoRead()) { + ctx.read(); } + firedChannelRead = false; ctx.fireChannelReadComplete(); } @@ -366,7 +362,7 @@ public abstract class ByteToMessageDecoder extends ChannelInboundHandlerAdapter super.userEventTriggered(ctx, evt); } - private void channelInputClosed(ChannelHandlerContext ctx, boolean callChannelInactive) throws Exception { + private void channelInputClosed(ChannelHandlerContext ctx, boolean callChannelInactive) { CodecOutputList out = CodecOutputList.newInstance(); try { channelInputClosed(ctx, out); @@ -504,6 +500,8 @@ public abstract class ByteToMessageDecoder extends ChannelInboundHandlerAdapter boolean removePending = decodeState == STATE_HANDLER_REMOVED_PENDING; decodeState = STATE_INIT; if (removePending) { + fireChannelRead(ctx, out, out.size()); + out.clear(); handlerRemoved(ctx); } } @@ -524,12 +522,23 @@ public abstract class ByteToMessageDecoder extends ChannelInboundHandlerAdapter } } - static ByteBuf expandCumulation(ByteBufAllocator alloc, ByteBuf cumulation, int readable) { - ByteBuf oldCumulation = cumulation; - cumulation = alloc.buffer(oldCumulation.readableBytes() + readable); - cumulation.writeBytes(oldCumulation); - oldCumulation.release(); - return cumulation; + static ByteBuf expandCumulation(ByteBufAllocator alloc, ByteBuf oldCumulation, ByteBuf in) { + int oldBytes = oldCumulation.readableBytes(); + int newBytes = in.readableBytes(); + int totalBytes = oldBytes + newBytes; + ByteBuf newCumulation = alloc.buffer(alloc.calculateNewCapacity(totalBytes, MAX_VALUE)); + ByteBuf toRelease = newCumulation; + try { + // This avoids redundant checks and stack depth compared to calling writeBytes(...) + newCumulation.setBytes(0, oldCumulation, oldCumulation.readerIndex(), oldBytes) + .setBytes(oldBytes, in, in.readerIndex(), newBytes) + .writerIndex(totalBytes); + in.readerIndex(in.writerIndex()); + toRelease = oldCumulation; + return newCumulation; + } finally { + toRelease.release(); + } } /** diff --git a/codec/src/main/java/io/netty/handler/codec/DatagramPacketEncoder.java b/codec/src/main/java/io/netty/handler/codec/DatagramPacketEncoder.java index e8ef7ee..57f9a4e 100644 --- a/codec/src/main/java/io/netty/handler/codec/DatagramPacketEncoder.java +++ b/codec/src/main/java/io/netty/handler/codec/DatagramPacketEncoder.java @@ -63,7 +63,7 @@ public class DatagramPacketEncoder<M> extends MessageToMessageEncoder<AddressedE @SuppressWarnings("rawtypes") AddressedEnvelope envelope = (AddressedEnvelope) msg; return encoder.acceptOutboundMessage(envelope.content()) - && envelope.sender() instanceof InetSocketAddress + && (envelope.sender() instanceof InetSocketAddress || envelope.sender() == null) && envelope.recipient() instanceof InetSocketAddress; } return false; diff --git a/codec/src/main/java/io/netty/handler/codec/DateFormatter.java b/codec/src/main/java/io/netty/handler/codec/DateFormatter.java index 86df148..b07912a 100644 --- a/codec/src/main/java/io/netty/handler/codec/DateFormatter.java +++ b/codec/src/main/java/io/netty/handler/codec/DateFormatter.java @@ -261,10 +261,6 @@ public final class DateFormatter { return false; } - private static boolean matchMonth(String month, CharSequence txt, int tokenStart) { - return AsciiString.regionMatchesAscii(month, true, 0, txt, tokenStart, 3); - } - private boolean tryParseMonth(CharSequence txt, int tokenStart, int tokenEnd) { int len = tokenEnd - tokenStart; @@ -272,29 +268,33 @@ public final class DateFormatter { return false; } - if (matchMonth("Jan", txt, tokenStart)) { + char monthChar1 = AsciiString.toLowerCase(txt.charAt(tokenStart)); + char monthChar2 = AsciiString.toLowerCase(txt.charAt(tokenStart + 1)); + char monthChar3 = AsciiString.toLowerCase(txt.charAt(tokenStart + 2)); + + if (monthChar1 == 'j' && monthChar2 == 'a' && monthChar3 == 'n') { month = Calendar.JANUARY; - } else if (matchMonth("Feb", txt, tokenStart)) { + } else if (monthChar1 == 'f' && monthChar2 == 'e' && monthChar3 == 'b') { month = Calendar.FEBRUARY; - } else if (matchMonth("Mar", txt, tokenStart)) { + } else if (monthChar1 == 'm' && monthChar2 == 'a' && monthChar3 == 'r') { month = Calendar.MARCH; - } else if (matchMonth("Apr", txt, tokenStart)) { + } else if (monthChar1 == 'a' && monthChar2 == 'p' && monthChar3 == 'r') { month = Calendar.APRIL; - } else if (matchMonth("May", txt, tokenStart)) { + } else if (monthChar1 == 'm' && monthChar2 == 'a' && monthChar3 == 'y') { month = Calendar.MAY; - } else if (matchMonth("Jun", txt, tokenStart)) { + } else if (monthChar1 == 'j' && monthChar2 == 'u' && monthChar3 == 'n') { month = Calendar.JUNE; - } else if (matchMonth("Jul", txt, tokenStart)) { + } else if (monthChar1 == 'j' && monthChar2 == 'u' && monthChar3 == 'l') { month = Calendar.JULY; - } else if (matchMonth("Aug", txt, tokenStart)) { + } else if (monthChar1 == 'a' && monthChar2 == 'u' && monthChar3 == 'g') { month = Calendar.AUGUST; - } else if (matchMonth("Sep", txt, tokenStart)) { + } else if (monthChar1 == 's' && monthChar2 == 'e' && monthChar3 == 'p') { month = Calendar.SEPTEMBER; - } else if (matchMonth("Oct", txt, tokenStart)) { + } else if (monthChar1 == 'o' && monthChar2 == 'c' && monthChar3 == 't') { month = Calendar.OCTOBER; - } else if (matchMonth("Nov", txt, tokenStart)) { + } else if (monthChar1 == 'n' && monthChar2 == 'o' && monthChar3 == 'v') { month = Calendar.NOVEMBER; - } else if (matchMonth("Dec", txt, tokenStart)) { + } else if (monthChar1 == 'd' && monthChar2 == 'e' && monthChar3 == 'c') { month = Calendar.DECEMBER; } else { return false; diff --git a/codec/src/main/java/io/netty/handler/codec/DecoderResult.java b/codec/src/main/java/io/netty/handler/codec/DecoderResult.java index f666a3b..253d113 100644 --- a/codec/src/main/java/io/netty/handler/codec/DecoderResult.java +++ b/codec/src/main/java/io/netty/handler/codec/DecoderResult.java @@ -16,6 +16,7 @@ package io.netty.handler.codec; import io.netty.util.Signal; +import io.netty.util.internal.ObjectUtil; public class DecoderResult { @@ -26,19 +27,13 @@ public class DecoderResult { public static final DecoderResult SUCCESS = new DecoderResult(SIGNAL_SUCCESS); public static DecoderResult failure(Throwable cause) { - if (cause == null) { - throw new NullPointerException("cause"); - } - return new DecoderResult(cause); + return new DecoderResult(ObjectUtil.checkNotNull(cause, "cause")); } private final Throwable cause; protected DecoderResult(Throwable cause) { - if (cause == null) { - throw new NullPointerException("cause"); - } - this.cause = cause; + this.cause = ObjectUtil.checkNotNull(cause, "cause"); } public boolean isFinished() { diff --git a/codec/src/main/java/io/netty/handler/codec/DefaultHeaders.java b/codec/src/main/java/io/netty/handler/codec/DefaultHeaders.java index df7266d..4109037 100644 --- a/codec/src/main/java/io/netty/handler/codec/DefaultHeaders.java +++ b/codec/src/main/java/io/netty/handler/codec/DefaultHeaders.java @@ -1012,6 +1012,20 @@ public class DefaultHeaders<K, V, T extends Headers<K, V, T>> implements Headers return value; } + private HeaderEntry<K, V> remove0(HeaderEntry<K, V> entry, HeaderEntry<K, V> previous) { + int i = index(entry.hash); + HeaderEntry<K, V> e = entries[i]; + if (e == entry) { + entries[i] = entry.next; + previous = entries[i]; + } else { + previous.next = entry.next; + } + entry.remove(); + --size; + return previous; + } + @SuppressWarnings("unchecked") private T thisT() { return (T) this; @@ -1055,6 +1069,8 @@ public class DefaultHeaders<K, V, T extends Headers<K, V, T>> implements Headers private final class ValueIterator implements Iterator<V> { private final K name; private final int hash; + private HeaderEntry<K, V> removalPrevious; + private HeaderEntry<K, V> previous; private HeaderEntry<K, V> next; ValueIterator(K name) { @@ -1073,14 +1089,21 @@ public class DefaultHeaders<K, V, T extends Headers<K, V, T>> implements Headers if (!hasNext()) { throw new NoSuchElementException(); } - HeaderEntry<K, V> current = next; + if (previous != null) { + removalPrevious = previous; + } + previous = next; calculateNext(next.next); - return current.value; + return previous.value; } @Override public void remove() { - throw new UnsupportedOperationException("read only"); + if (previous == null) { + throw new IllegalStateException(); + } + removalPrevious = remove0(previous, removalPrevious); + previous = null; } private void calculateNext(HeaderEntry<K, V> entry) { diff --git a/codec/src/main/java/io/netty/handler/codec/DelimiterBasedFrameDecoder.java b/codec/src/main/java/io/netty/handler/codec/DelimiterBasedFrameDecoder.java index 27e8c20..1184f0f 100644 --- a/codec/src/main/java/io/netty/handler/codec/DelimiterBasedFrameDecoder.java +++ b/codec/src/main/java/io/netty/handler/codec/DelimiterBasedFrameDecoder.java @@ -15,8 +15,11 @@ */ package io.netty.handler.codec; +import static io.netty.util.internal.ObjectUtil.checkPositive; + import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; +import io.netty.util.internal.ObjectUtil; import java.util.List; @@ -164,12 +167,7 @@ public class DelimiterBasedFrameDecoder extends ByteToMessageDecoder { public DelimiterBasedFrameDecoder( int maxFrameLength, boolean stripDelimiter, boolean failFast, ByteBuf... delimiters) { validateMaxFrameLength(maxFrameLength); - if (delimiters == null) { - throw new NullPointerException("delimiters"); - } - if (delimiters.length == 0) { - throw new IllegalArgumentException("empty delimiters"); - } + ObjectUtil.checkNonEmpty(delimiters, "delimiters"); if (isLineBased(delimiters) && !isSubclass()) { lineBasedDecoder = new LineBasedFrameDecoder(maxFrameLength, stripDelimiter, failFast); @@ -337,19 +335,13 @@ public class DelimiterBasedFrameDecoder extends ByteToMessageDecoder { } private static void validateDelimiter(ByteBuf delimiter) { - if (delimiter == null) { - throw new NullPointerException("delimiter"); - } + ObjectUtil.checkNotNull(delimiter, "delimiter"); if (!delimiter.isReadable()) { throw new IllegalArgumentException("empty delimiter"); } } private static void validateMaxFrameLength(int maxFrameLength) { - if (maxFrameLength <= 0) { - throw new IllegalArgumentException( - "maxFrameLength must be a positive integer: " + - maxFrameLength); - } + checkPositive(maxFrameLength, "maxFrameLength"); } } diff --git a/codec/src/main/java/io/netty/handler/codec/FixedLengthFrameDecoder.java b/codec/src/main/java/io/netty/handler/codec/FixedLengthFrameDecoder.java index 5b4bb71..9475e5b 100644 --- a/codec/src/main/java/io/netty/handler/codec/FixedLengthFrameDecoder.java +++ b/codec/src/main/java/io/netty/handler/codec/FixedLengthFrameDecoder.java @@ -15,6 +15,8 @@ */ package io.netty.handler.codec; +import static io.netty.util.internal.ObjectUtil.checkPositive; + import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; @@ -46,10 +48,7 @@ public class FixedLengthFrameDecoder extends ByteToMessageDecoder { * @param frameLength the length of the frame */ public FixedLengthFrameDecoder(int frameLength) { - if (frameLength <= 0) { - throw new IllegalArgumentException( - "frameLength must be a positive integer: " + frameLength); - } + checkPositive(frameLength, "frameLength"); this.frameLength = frameLength; } diff --git a/codec/src/main/java/io/netty/handler/codec/LengthFieldBasedFrameDecoder.java b/codec/src/main/java/io/netty/handler/codec/LengthFieldBasedFrameDecoder.java index 4d94bdf..bed0c79 100644 --- a/codec/src/main/java/io/netty/handler/codec/LengthFieldBasedFrameDecoder.java +++ b/codec/src/main/java/io/netty/handler/codec/LengthFieldBasedFrameDecoder.java @@ -15,12 +15,15 @@ */ package io.netty.handler.codec; +import static io.netty.util.internal.ObjectUtil.checkNotNull; +import static io.netty.util.internal.ObjectUtil.checkPositive; +import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero; + import java.nio.ByteOrder; import java.util.List; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; -import io.netty.handler.codec.serialization.ObjectDecoder; /** * A decoder that splits the received {@link ByteBuf}s dynamically by the @@ -298,27 +301,14 @@ public class LengthFieldBasedFrameDecoder extends ByteToMessageDecoder { public LengthFieldBasedFrameDecoder( ByteOrder byteOrder, int maxFrameLength, int lengthFieldOffset, int lengthFieldLength, int lengthAdjustment, int initialBytesToStrip, boolean failFast) { - if (byteOrder == null) { - throw new NullPointerException("byteOrder"); - } - if (maxFrameLength <= 0) { - throw new IllegalArgumentException( - "maxFrameLength must be a positive integer: " + - maxFrameLength); - } + this.byteOrder = checkNotNull(byteOrder, "byteOrder"); - if (lengthFieldOffset < 0) { - throw new IllegalArgumentException( - "lengthFieldOffset must be a non-negative integer: " + - lengthFieldOffset); - } + checkPositive(maxFrameLength, "maxFrameLength"); - if (initialBytesToStrip < 0) { - throw new IllegalArgumentException( - "initialBytesToStrip must be a non-negative integer: " + - initialBytesToStrip); - } + checkPositiveOrZero(lengthFieldOffset, "lengthFieldOffset"); + + checkPositiveOrZero(initialBytesToStrip, "initialBytesToStrip"); if (lengthFieldOffset > maxFrameLength - lengthFieldLength) { throw new IllegalArgumentException( @@ -328,12 +318,11 @@ public class LengthFieldBasedFrameDecoder extends ByteToMessageDecoder { "lengthFieldLength (" + lengthFieldLength + ")."); } - this.byteOrder = byteOrder; this.maxFrameLength = maxFrameLength; this.lengthFieldOffset = lengthFieldOffset; this.lengthFieldLength = lengthFieldLength; this.lengthAdjustment = lengthAdjustment; - lengthFieldEndOffset = lengthFieldOffset + lengthFieldLength; + this.lengthFieldEndOffset = lengthFieldOffset + lengthFieldLength; this.initialBytesToStrip = initialBytesToStrip; this.failFast = failFast; } @@ -504,14 +493,6 @@ public class LengthFieldBasedFrameDecoder extends ByteToMessageDecoder { /** * Extract the sub-region of the specified buffer. - * <p> - * If you are sure that the frame and its content are not accessed after - * the current {@link #decode(ChannelHandlerContext, ByteBuf)} - * call returns, you can even avoid memory copy by returning the sliced - * sub-region (i.e. <tt>return buffer.slice(index, length)</tt>). - * It's often useful when you convert the extracted frame into an object. - * Refer to the source code of {@link ObjectDecoder} to see how this method - * is overridden to avoid memory copy. */ protected ByteBuf extractFrame(ChannelHandlerContext ctx, ByteBuf buffer, int index, int length) { return buffer.retainedSlice(index, length); diff --git a/codec/src/main/java/io/netty/handler/codec/LengthFieldPrepender.java b/codec/src/main/java/io/netty/handler/codec/LengthFieldPrepender.java index 4076e07..ecc9225 100644 --- a/codec/src/main/java/io/netty/handler/codec/LengthFieldPrepender.java +++ b/codec/src/main/java/io/netty/handler/codec/LengthFieldPrepender.java @@ -15,6 +15,8 @@ */ package io.netty.handler.codec; +import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero; + import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandler.Sharable; import io.netty.channel.ChannelHandlerContext; @@ -148,9 +150,7 @@ public class LengthFieldPrepender extends MessageToMessageEncoder<ByteBuf> { "lengthFieldLength must be either 1, 2, 3, 4, or 8: " + lengthFieldLength); } - ObjectUtil.checkNotNull(byteOrder, "byteOrder"); - - this.byteOrder = byteOrder; + this.byteOrder = ObjectUtil.checkNotNull(byteOrder, "byteOrder"); this.lengthFieldLength = lengthFieldLength; this.lengthIncludesLengthFieldLength = lengthIncludesLengthFieldLength; this.lengthAdjustment = lengthAdjustment; @@ -163,10 +163,7 @@ public class LengthFieldPrepender extends MessageToMessageEncoder<ByteBuf> { length += lengthFieldLength; } - if (length < 0) { - throw new IllegalArgumentException( - "Adjusted frame length (" + length + ") is less than zero"); - } + checkPositiveOrZero(length, "length"); switch (lengthFieldLength) { case 1: diff --git a/codec/src/main/java/io/netty/handler/codec/MessageAggregator.java b/codec/src/main/java/io/netty/handler/codec/MessageAggregator.java index 2cdb880..1e46e0d 100644 --- a/codec/src/main/java/io/netty/handler/codec/MessageAggregator.java +++ b/codec/src/main/java/io/netty/handler/codec/MessageAggregator.java @@ -28,6 +28,7 @@ import io.netty.util.ReferenceCountUtil; import java.util.List; import static io.netty.buffer.Unpooled.EMPTY_BUFFER; +import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero; /** * An abstract {@link ChannelHandler} that aggregates a series of message objects into a single aggregated message. @@ -61,6 +62,8 @@ public abstract class MessageAggregator<I, S, C extends ByteBufHolder, O extends private ChannelHandlerContext ctx; private ChannelFutureListener continueResponseWriteListener; + private boolean aggregating; + /** * Creates a new instance. * @@ -81,9 +84,7 @@ public abstract class MessageAggregator<I, S, C extends ByteBufHolder, O extends } private static void validateMaxContentLength(int maxContentLength) { - if (maxContentLength < 0) { - throw new IllegalArgumentException("maxContentLength: " + maxContentLength + " (expected: >= 0)"); - } + checkPositiveOrZero(maxContentLength, "maxContentLength"); } @Override @@ -96,7 +97,20 @@ public abstract class MessageAggregator<I, S, C extends ByteBufHolder, O extends @SuppressWarnings("unchecked") I in = (I) msg; - return (isContentMessage(in) || isStartMessage(in)) && !isAggregated(in); + if (isAggregated(in)) { + return false; + } + + // NOTE: It's tempting to make this check only if aggregating is false. There are however + // side conditions in decode(...) in respect to large messages. + if (isStartMessage(in)) { + aggregating = true; + return true; + } else if (aggregating && isContentMessage(in)) { + return true; + } + + return false; } /** @@ -192,6 +206,8 @@ public abstract class MessageAggregator<I, S, C extends ByteBufHolder, O extends @Override protected void decode(final ChannelHandlerContext ctx, I msg, List<Object> out) throws Exception { + assert aggregating; + if (isStartMessage(msg)) { handlingOversizedMessage = false; if (currentMessage != null) { @@ -246,7 +262,7 @@ public abstract class MessageAggregator<I, S, C extends ByteBufHolder, O extends } else { aggregated = beginAggregation(m, EMPTY_BUFFER); } - finishAggregation(aggregated); + finishAggregation0(aggregated); out.add(aggregated); return; } @@ -301,7 +317,7 @@ public abstract class MessageAggregator<I, S, C extends ByteBufHolder, O extends } if (last) { - finishAggregation(currentMessage); + finishAggregation0(currentMessage); // All done out.add(currentMessage); @@ -371,6 +387,11 @@ public abstract class MessageAggregator<I, S, C extends ByteBufHolder, O extends */ protected void aggregate(O aggregated, C content) throws Exception { } + private void finishAggregation0(O aggregated) throws Exception { + aggregating = false; + finishAggregation(aggregated); + } + /** * Invoked when the specified {@code aggregated} message is about to be passed to the next handler in the pipeline. */ @@ -441,6 +462,7 @@ public abstract class MessageAggregator<I, S, C extends ByteBufHolder, O extends currentMessage.release(); currentMessage = null; handlingOversizedMessage = false; + aggregating = false; } } } diff --git a/codec/src/main/java/io/netty/handler/codec/MessageToMessageEncoder.java b/codec/src/main/java/io/netty/handler/codec/MessageToMessageEncoder.java index 6b47b6f..439dc8c 100644 --- a/codec/src/main/java/io/netty/handler/codec/MessageToMessageEncoder.java +++ b/codec/src/main/java/io/netty/handler/codec/MessageToMessageEncoder.java @@ -132,7 +132,7 @@ public abstract class MessageToMessageEncoder<I> extends ChannelOutboundHandlerA } private static void writePromiseCombiner(ChannelHandlerContext ctx, CodecOutputList out, ChannelPromise promise) { - final PromiseCombiner combiner = new PromiseCombiner(); + final PromiseCombiner combiner = new PromiseCombiner(ctx.executor()); for (int i = 0; i < out.size(); i++) { combiner.add(ctx.write(out.getUnsafe(i))); } diff --git a/codec/src/main/java/io/netty/handler/codec/ProtocolDetectionResult.java b/codec/src/main/java/io/netty/handler/codec/ProtocolDetectionResult.java index d4b4359..1578386 100644 --- a/codec/src/main/java/io/netty/handler/codec/ProtocolDetectionResult.java +++ b/codec/src/main/java/io/netty/handler/codec/ProtocolDetectionResult.java @@ -25,7 +25,7 @@ import static io.netty.util.internal.ObjectUtil.checkNotNull; public final class ProtocolDetectionResult<T> { @SuppressWarnings({ "rawtypes", "unchecked" }) - private static final ProtocolDetectionResult NEEDS_MORE_DATE = + private static final ProtocolDetectionResult NEEDS_MORE_DATA = new ProtocolDetectionResult(ProtocolDetectionState.NEEDS_MORE_DATA, null); @SuppressWarnings({ "rawtypes", "unchecked" }) private static final ProtocolDetectionResult INVALID = @@ -39,7 +39,7 @@ public final class ProtocolDetectionResult<T> { */ @SuppressWarnings("unchecked") public static <T> ProtocolDetectionResult<T> needsMoreData() { - return NEEDS_MORE_DATE; + return NEEDS_MORE_DATA; } /** diff --git a/codec/src/main/java/io/netty/handler/codec/ReplayingDecoderByteBuf.java b/codec/src/main/java/io/netty/handler/codec/ReplayingDecoderByteBuf.java index 5e71b7d..26f4186 100644 --- a/codec/src/main/java/io/netty/handler/codec/ReplayingDecoderByteBuf.java +++ b/codec/src/main/java/io/netty/handler/codec/ReplayingDecoderByteBuf.java @@ -15,14 +15,6 @@ */ package io.netty.handler.codec; -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufAllocator; -import io.netty.buffer.SwappedByteBuf; -import io.netty.buffer.Unpooled; -import io.netty.util.ByteProcessor; -import io.netty.util.Signal; -import io.netty.util.internal.StringUtil; - import java.io.InputStream; import java.io.OutputStream; import java.nio.ByteBuffer; @@ -32,6 +24,15 @@ import java.nio.channels.GatheringByteChannel; import java.nio.channels.ScatteringByteChannel; import java.nio.charset.Charset; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.SwappedByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.util.ByteProcessor; +import io.netty.util.Signal; +import io.netty.util.internal.ObjectUtil; +import io.netty.util.internal.StringUtil; + /** * Special {@link ByteBuf} implementation which is used by the {@link ReplayingDecoder} */ @@ -472,10 +473,7 @@ final class ReplayingDecoderByteBuf extends ByteBuf { @Override public ByteBuf order(ByteOrder endianness) { - if (endianness == null) { - throw new NullPointerException("endianness"); - } - if (endianness == order()) { + if (ObjectUtil.checkNotNull(endianness, "endianness") == order()) { return this; } @@ -488,12 +486,12 @@ final class ReplayingDecoderByteBuf extends ByteBuf { @Override public boolean isReadable() { - return terminated? buffer.isReadable() : true; + return !terminated || buffer.isReadable(); } @Override public boolean isReadable(int size) { - return terminated? buffer.isReadable(size) : true; + return !terminated || buffer.isReadable(size); } @Override diff --git a/codec/src/main/java/io/netty/handler/codec/base64/Base64.java b/codec/src/main/java/io/netty/handler/codec/base64/Base64.java index a4efca2..ace2582 100644 --- a/codec/src/main/java/io/netty/handler/codec/base64/Base64.java +++ b/codec/src/main/java/io/netty/handler/codec/base64/Base64.java @@ -22,6 +22,7 @@ package io.netty.handler.codec.base64; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.util.ByteProcessor; +import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.PlatformDependent; import java.nio.ByteOrder; @@ -50,24 +51,15 @@ public final class Base64 { private static final byte EQUALS_SIGN_ENC = -1; // Indicates equals sign in encoding private static byte[] alphabet(Base64Dialect dialect) { - if (dialect == null) { - throw new NullPointerException("dialect"); - } - return dialect.alphabet; + return ObjectUtil.checkNotNull(dialect, "dialect").alphabet; } private static byte[] decodabet(Base64Dialect dialect) { - if (dialect == null) { - throw new NullPointerException("dialect"); - } - return dialect.decodabet; + return ObjectUtil.checkNotNull(dialect, "dialect").decodabet; } private static boolean breakLines(Base64Dialect dialect) { - if (dialect == null) { - throw new NullPointerException("dialect"); - } - return dialect.breakLinesByDefault; + return ObjectUtil.checkNotNull(dialect, "dialect").breakLinesByDefault; } public static ByteBuf encode(ByteBuf src) { @@ -83,10 +75,7 @@ public final class Base64 { } public static ByteBuf encode(ByteBuf src, boolean breakLines, Base64Dialect dialect) { - - if (src == null) { - throw new NullPointerException("src"); - } + ObjectUtil.checkNotNull(src, "src"); ByteBuf dest = encode(src, src.readerIndex(), src.readableBytes(), breakLines, dialect); src.readerIndex(src.writerIndex()); @@ -113,12 +102,8 @@ public final class Base64 { public static ByteBuf encode( ByteBuf src, int off, int len, boolean breakLines, Base64Dialect dialect, ByteBufAllocator allocator) { - if (src == null) { - throw new NullPointerException("src"); - } - if (dialect == null) { - throw new NullPointerException("dialect"); - } + ObjectUtil.checkNotNull(src, "src"); + ObjectUtil.checkNotNull(dialect, "dialect"); ByteBuf dest = allocator.buffer(encodedBufferSize(len, breakLines)).order(src.order()); byte[] alphabet = alphabet(dialect); @@ -291,9 +276,7 @@ public final class Base64 { } public static ByteBuf decode(ByteBuf src, Base64Dialect dialect) { - if (src == null) { - throw new NullPointerException("src"); - } + ObjectUtil.checkNotNull(src, "src"); ByteBuf dest = decode(src, src.readerIndex(), src.readableBytes(), dialect); src.readerIndex(src.writerIndex()); @@ -312,12 +295,8 @@ public final class Base64 { public static ByteBuf decode( ByteBuf src, int off, int len, Base64Dialect dialect, ByteBufAllocator allocator) { - if (src == null) { - throw new NullPointerException("src"); - } - if (dialect == null) { - throw new NullPointerException("dialect"); - } + ObjectUtil.checkNotNull(src, "src"); + ObjectUtil.checkNotNull(dialect, "dialect"); // Using a ByteProcessor to reduce bound and reference count checking. return new Decoder().decode(src, off, len, allocator, dialect); @@ -331,8 +310,6 @@ public final class Base64 { private static final class Decoder implements ByteProcessor { private final byte[] b4 = new byte[4]; private int b4Posn; - private byte sbiCrop; - private byte sbiDecode; private byte[] decodabet; private int outBuffPosn; private ByteBuf dest; @@ -353,26 +330,24 @@ public final class Base64 { @Override public boolean process(byte value) throws Exception { - sbiCrop = (byte) (value & 0x7f); // Only the low seven bits - sbiDecode = decodabet[sbiCrop]; - - if (sbiDecode >= WHITE_SPACE_ENC) { // White space, Equals sign or better - if (sbiDecode >= EQUALS_SIGN_ENC) { // Equals sign or better - b4[b4Posn ++] = sbiCrop; - if (b4Posn > 3) { // Quartet built - outBuffPosn += decode4to3(b4, dest, outBuffPosn, decodabet); - b4Posn = 0; - - // If that was the equals sign, break out of 'for' loop - if (sbiCrop == EQUALS_SIGN) { - return false; + if (value > 0) { + byte sbiDecode = decodabet[value]; + if (sbiDecode >= WHITE_SPACE_ENC) { // White space, Equals sign or better + if (sbiDecode >= EQUALS_SIGN_ENC) { // Equals sign or better + b4[b4Posn ++] = value; + if (b4Posn > 3) { // Quartet built + outBuffPosn += decode4to3(b4, dest, outBuffPosn, decodabet); + b4Posn = 0; + + // If that was the equals sign, break out of 'for' loop + return value != EQUALS_SIGN; } } + return true; } - return true; } throw new IllegalArgumentException( - "invalid bad Base64 input character: " + (short) (value & 0xFF) + " (decimal)"); + "invalid Base64 input character: " + (short) (value & 0xFF) + " (decimal)"); } private static int decode4to3(byte[] src, ByteBuf dest, int destOffset, byte[] decodabet) { diff --git a/codec/src/main/java/io/netty/handler/codec/base64/Base64Decoder.java b/codec/src/main/java/io/netty/handler/codec/base64/Base64Decoder.java index b8d7279..6759483 100644 --- a/codec/src/main/java/io/netty/handler/codec/base64/Base64Decoder.java +++ b/codec/src/main/java/io/netty/handler/codec/base64/Base64Decoder.java @@ -23,6 +23,7 @@ import io.netty.handler.codec.ByteToMessageDecoder; import io.netty.handler.codec.DelimiterBasedFrameDecoder; import io.netty.handler.codec.Delimiters; import io.netty.handler.codec.MessageToMessageDecoder; +import io.netty.util.internal.ObjectUtil; import java.util.List; @@ -53,10 +54,7 @@ public class Base64Decoder extends MessageToMessageDecoder<ByteBuf> { } public Base64Decoder(Base64Dialect dialect) { - if (dialect == null) { - throw new NullPointerException("dialect"); - } - this.dialect = dialect; + this.dialect = ObjectUtil.checkNotNull(dialect, "dialect"); } @Override diff --git a/codec/src/main/java/io/netty/handler/codec/base64/Base64Dialect.java b/codec/src/main/java/io/netty/handler/codec/base64/Base64Dialect.java index 27aecf2..716b0e2 100644 --- a/codec/src/main/java/io/netty/handler/codec/base64/Base64Dialect.java +++ b/codec/src/main/java/io/netty/handler/codec/base64/Base64Dialect.java @@ -67,17 +67,17 @@ public enum Base64Dialect { -9, -9, -9, -9, -9, -9, // Decimal 91 - 96 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm' 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z' - -9, -9, -9, -9, // Decimal 123 - 126 - /* -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 127 - 139 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */ + -9, -9, -9, -9, -9 // Decimal 123 - 127 + /* -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 140 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 141 - 153 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 154 - 166 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 167 - 179 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 180 - 192 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 193 - 205 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 206 - 218 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 219 - 231 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 232 - 244 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 245 - 255 */ }, true), /** * Base64-like encoding that is URL-safe as described in the Section 4 of @@ -126,17 +126,17 @@ public enum Base64Dialect { -9, // Decimal 96 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm' 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z' - -9, -9, -9, -9, // Decimal 123 - 126 - /*-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 127 - 139 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */ + -9, -9, -9, -9, -9, // Decimal 123 - 127 + /* -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 140 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 141 - 153 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 154 - 166 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 167 - 179 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 180 - 192 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 193 - 205 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 206 - 218 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 219 - 231 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 232 - 244 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 245 - 255 */ }, false), /** * Special "ordered" dialect of Base64 described in @@ -182,17 +182,17 @@ public enum Base64Dialect { -9, // Decimal 96 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, // Letters 'a' through 'm' 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, // Letters 'n' through 'z' - -9, -9, -9, -9, // Decimal 123 - 126 - /* -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 127 - 139 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */ + -9, -9, -9, -9, -9 // Decimal 123 - 127 + /* -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 140 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 141 - 153 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 154 - 166 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 167 - 179 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 180 - 192 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 193 - 205 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 206 - 218 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 219 - 231 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 232 - 244 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 245 - 255 */ }, true); final byte[] alphabet; diff --git a/codec/src/main/java/io/netty/handler/codec/base64/Base64Encoder.java b/codec/src/main/java/io/netty/handler/codec/base64/Base64Encoder.java index 92c23a5..54afb1d 100644 --- a/codec/src/main/java/io/netty/handler/codec/base64/Base64Encoder.java +++ b/codec/src/main/java/io/netty/handler/codec/base64/Base64Encoder.java @@ -22,6 +22,7 @@ import io.netty.channel.ChannelPipeline; import io.netty.handler.codec.DelimiterBasedFrameDecoder; import io.netty.handler.codec.Delimiters; import io.netty.handler.codec.MessageToMessageEncoder; +import io.netty.util.internal.ObjectUtil; import java.util.List; @@ -54,12 +55,8 @@ public class Base64Encoder extends MessageToMessageEncoder<ByteBuf> { } public Base64Encoder(boolean breakLines, Base64Dialect dialect) { - if (dialect == null) { - throw new NullPointerException("dialect"); - } - + this.dialect = ObjectUtil.checkNotNull(dialect, "dialect"); this.breakLines = breakLines; - this.dialect = dialect; } @Override diff --git a/codec/src/main/java/io/netty/handler/codec/compression/ByteBufChecksum.java b/codec/src/main/java/io/netty/handler/codec/compression/ByteBufChecksum.java index 2aff623..05456b6 100644 --- a/codec/src/main/java/io/netty/handler/codec/compression/ByteBufChecksum.java +++ b/codec/src/main/java/io/netty/handler/codec/compression/ByteBufChecksum.java @@ -55,7 +55,7 @@ abstract class ByteBufChecksum implements Checksum { if (PlatformDependent.javaVersion() >= 8) { try { Method method = checksum.getClass().getDeclaredMethod("update", ByteBuffer.class); - method.invoke(method, ByteBuffer.allocate(1)); + method.invoke(checksum, ByteBuffer.allocate(1)); return method; } catch (Throwable ignore) { return null; @@ -66,6 +66,9 @@ abstract class ByteBufChecksum implements Checksum { static ByteBufChecksum wrapChecksum(Checksum checksum) { ObjectUtil.checkNotNull(checksum, "checksum"); + if (checksum instanceof ByteBufChecksum) { + return (ByteBufChecksum) checksum; + } if (checksum instanceof Adler32 && ADLER32_UPDATE_METHOD != null) { return new ReflectiveByteBufChecksum(checksum, ADLER32_UPDATE_METHOD); } @@ -100,7 +103,7 @@ abstract class ByteBufChecksum implements Checksum { update(b.array(), b.arrayOffset() + off, len); } else { try { - method.invoke(checksum, CompressionUtil.safeNioBuffer(b)); + method.invoke(checksum, CompressionUtil.safeNioBuffer(b, off, len)); } catch (Throwable cause) { throw new Error(); } diff --git a/codec/src/main/java/io/netty/handler/codec/compression/Bzip2DivSufSort.java b/codec/src/main/java/io/netty/handler/codec/compression/Bzip2DivSufSort.java index cdf92a6..8138729 100644 --- a/codec/src/main/java/io/netty/handler/codec/compression/Bzip2DivSufSort.java +++ b/codec/src/main/java/io/netty/handler/codec/compression/Bzip2DivSufSort.java @@ -568,7 +568,9 @@ final class Bzip2DivSufSort { SA[i++] = SA[k]; SA[k++] = SA[i]; if (last <= k) { - while (j < bufend) { SA[i++] = buf[j]; buf[j++] = SA[i]; } + while (j < bufend) { + SA[i++] = buf[j]; buf[j++] = SA[i]; + } SA[i] = buf[j]; buf[j] = t; return; } diff --git a/codec/src/main/java/io/netty/handler/codec/compression/CompressionUtil.java b/codec/src/main/java/io/netty/handler/codec/compression/CompressionUtil.java index 8b43e7f..f25f1ee 100644 --- a/codec/src/main/java/io/netty/handler/codec/compression/CompressionUtil.java +++ b/codec/src/main/java/io/netty/handler/codec/compression/CompressionUtil.java @@ -40,4 +40,9 @@ final class CompressionUtil { return buffer.nioBufferCount() == 1 ? buffer.internalNioBuffer(buffer.readerIndex(), buffer.readableBytes()) : buffer.nioBuffer(); } + + static ByteBuffer safeNioBuffer(ByteBuf buffer, int index, int length) { + return buffer.nioBufferCount() == 1 ? buffer.internalNioBuffer(index, length) + : buffer.nioBuffer(index, length); + } } diff --git a/codec/src/main/java/io/netty/handler/codec/compression/JZlibDecoder.java b/codec/src/main/java/io/netty/handler/codec/compression/JZlibDecoder.java index 5d23bb8..6c65cd5 100644 --- a/codec/src/main/java/io/netty/handler/codec/compression/JZlibDecoder.java +++ b/codec/src/main/java/io/netty/handler/codec/compression/JZlibDecoder.java @@ -18,7 +18,9 @@ package io.netty.handler.codec.compression; import com.jcraft.jzlib.Inflater; import com.jcraft.jzlib.JZlib; import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; import io.netty.channel.ChannelHandlerContext; +import io.netty.util.internal.ObjectUtil; import java.util.List; @@ -34,7 +36,21 @@ public class JZlibDecoder extends ZlibDecoder { * @throws DecompressionException if failed to initialize zlib */ public JZlibDecoder() { - this(ZlibWrapper.ZLIB); + this(ZlibWrapper.ZLIB, 0); + } + + /** + * Creates a new instance with the default wrapper ({@link ZlibWrapper#ZLIB}) + * and specified maximum buffer allocation. + * + * @param maxAllocation + * Maximum size of the decompression buffer. Must be >= 0. + * If zero, maximum size is decided by the {@link ByteBufAllocator}. + * + * @throws DecompressionException if failed to initialize zlib + */ + public JZlibDecoder(int maxAllocation) { + this(ZlibWrapper.ZLIB, maxAllocation); } /** @@ -43,9 +59,22 @@ public class JZlibDecoder extends ZlibDecoder { * @throws DecompressionException if failed to initialize zlib */ public JZlibDecoder(ZlibWrapper wrapper) { - if (wrapper == null) { - throw new NullPointerException("wrapper"); - } + this(wrapper, 0); + } + + /** + * Creates a new instance with the specified wrapper and maximum buffer allocation. + * + * @param maxAllocation + * Maximum size of the decompression buffer. Must be >= 0. + * If zero, maximum size is decided by the {@link ByteBufAllocator}. + * + * @throws DecompressionException if failed to initialize zlib + */ + public JZlibDecoder(ZlibWrapper wrapper, int maxAllocation) { + super(maxAllocation); + + ObjectUtil.checkNotNull(wrapper, "wrapper"); int resultCode = z.init(ZlibUtil.convertWrapperType(wrapper)); if (resultCode != JZlib.Z_OK) { @@ -61,11 +90,23 @@ public class JZlibDecoder extends ZlibDecoder { * @throws DecompressionException if failed to initialize zlib */ public JZlibDecoder(byte[] dictionary) { - if (dictionary == null) { - throw new NullPointerException("dictionary"); - } - this.dictionary = dictionary; + this(dictionary, 0); + } + /** + * Creates a new instance with the specified preset dictionary and maximum buffer allocation. + * The wrapper is always {@link ZlibWrapper#ZLIB} because it is the only format that + * supports the preset dictionary. + * + * @param maxAllocation + * Maximum size of the decompression buffer. Must be >= 0. + * If zero, maximum size is decided by the {@link ByteBufAllocator}. + * + * @throws DecompressionException if failed to initialize zlib + */ + public JZlibDecoder(byte[] dictionary, int maxAllocation) { + super(maxAllocation); + this.dictionary = ObjectUtil.checkNotNull(dictionary, "dictionary"); int resultCode; resultCode = z.inflateInit(JZlib.W_ZLIB); if (resultCode != JZlib.Z_OK) { @@ -110,11 +151,11 @@ public class JZlibDecoder extends ZlibDecoder { final int oldNextInIndex = z.next_in_index; // Configure output. - ByteBuf decompressed = ctx.alloc().heapBuffer(inputLength << 1); + ByteBuf decompressed = prepareDecompressBuffer(ctx, null, inputLength << 1); try { loop: for (;;) { - decompressed.ensureWritable(z.avail_in << 1); + decompressed = prepareDecompressBuffer(ctx, decompressed, z.avail_in << 1); z.avail_out = decompressed.writableBytes(); z.next_out = decompressed.array(); z.next_out_index = decompressed.arrayOffset() + decompressed.writerIndex(); @@ -170,4 +211,9 @@ public class JZlibDecoder extends ZlibDecoder { z.next_out = null; } } + + @Override + protected void decompressionBufferExhausted(ByteBuf buffer) { + finished = true; + } } diff --git a/codec/src/main/java/io/netty/handler/codec/compression/JZlibEncoder.java b/codec/src/main/java/io/netty/handler/codec/compression/JZlibEncoder.java index 3868bfb..882cac7 100644 --- a/codec/src/main/java/io/netty/handler/codec/compression/JZlibEncoder.java +++ b/codec/src/main/java/io/netty/handler/codec/compression/JZlibEncoder.java @@ -26,6 +26,7 @@ import io.netty.channel.ChannelPromise; import io.netty.channel.ChannelPromiseNotifier; import io.netty.util.concurrent.EventExecutor; import io.netty.util.internal.EmptyArrays; +import io.netty.util.internal.ObjectUtil; import java.util.concurrent.TimeUnit; @@ -130,9 +131,7 @@ public class JZlibEncoder extends ZlibEncoder { throw new IllegalArgumentException( "memLevel: " + memLevel + " (expected: 1-9)"); } - if (wrapper == null) { - throw new NullPointerException("wrapper"); - } + ObjectUtil.checkNotNull(wrapper, "wrapper"); if (wrapper == ZlibWrapper.ZLIB_OR_NONE) { throw new IllegalArgumentException( "wrapper '" + ZlibWrapper.ZLIB_OR_NONE + "' is not " + @@ -220,9 +219,8 @@ public class JZlibEncoder extends ZlibEncoder { throw new IllegalArgumentException( "memLevel: " + memLevel + " (expected: 1-9)"); } - if (dictionary == null) { - throw new NullPointerException("dictionary"); - } + ObjectUtil.checkNotNull(dictionary, "dictionary"); + int resultCode; resultCode = z.deflateInit( compressionLevel, windowBits, memLevel, diff --git a/codec/src/main/java/io/netty/handler/codec/compression/JdkZlibDecoder.java b/codec/src/main/java/io/netty/handler/codec/compression/JdkZlibDecoder.java index c90cc4b..7e69422 100644 --- a/codec/src/main/java/io/netty/handler/codec/compression/JdkZlibDecoder.java +++ b/codec/src/main/java/io/netty/handler/codec/compression/JdkZlibDecoder.java @@ -16,7 +16,9 @@ package io.netty.handler.codec.compression; import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; import io.netty.channel.ChannelHandlerContext; +import io.netty.util.internal.ObjectUtil; import java.util.List; import java.util.zip.CRC32; @@ -64,7 +66,19 @@ public class JdkZlibDecoder extends ZlibDecoder { * Creates a new instance with the default wrapper ({@link ZlibWrapper#ZLIB}). */ public JdkZlibDecoder() { - this(ZlibWrapper.ZLIB, null, false); + this(ZlibWrapper.ZLIB, null, false, 0); + } + + /** + * Creates a new instance with the default wrapper ({@link ZlibWrapper#ZLIB}) + * and the specified maximum buffer allocation. + * + * @param maxAllocation + * Maximum size of the decompression buffer. Must be >= 0. + * If zero, maximum size is decided by the {@link ByteBufAllocator}. + */ + public JdkZlibDecoder(int maxAllocation) { + this(ZlibWrapper.ZLIB, null, false, maxAllocation); } /** @@ -73,7 +87,20 @@ public class JdkZlibDecoder extends ZlibDecoder { * supports the preset dictionary. */ public JdkZlibDecoder(byte[] dictionary) { - this(ZlibWrapper.ZLIB, dictionary, false); + this(ZlibWrapper.ZLIB, dictionary, false, 0); + } + + /** + * Creates a new instance with the specified preset dictionary and maximum buffer allocation. + * The wrapper is always {@link ZlibWrapper#ZLIB} because it is the only format that + * supports the preset dictionary. + * + * @param maxAllocation + * Maximum size of the decompression buffer. Must be >= 0. + * If zero, maximum size is decided by the {@link ByteBufAllocator}. + */ + public JdkZlibDecoder(byte[] dictionary, int maxAllocation) { + this(ZlibWrapper.ZLIB, dictionary, false, maxAllocation); } /** @@ -82,21 +109,43 @@ public class JdkZlibDecoder extends ZlibDecoder { * supported atm. */ public JdkZlibDecoder(ZlibWrapper wrapper) { - this(wrapper, null, false); + this(wrapper, null, false, 0); + } + + /** + * Creates a new instance with the specified wrapper and maximum buffer allocation. + * Be aware that only {@link ZlibWrapper#GZIP}, {@link ZlibWrapper#ZLIB} and {@link ZlibWrapper#NONE} are + * supported atm. + * + * @param maxAllocation + * Maximum size of the decompression buffer. Must be >= 0. + * If zero, maximum size is decided by the {@link ByteBufAllocator}. + */ + public JdkZlibDecoder(ZlibWrapper wrapper, int maxAllocation) { + this(wrapper, null, false, maxAllocation); } public JdkZlibDecoder(ZlibWrapper wrapper, boolean decompressConcatenated) { - this(wrapper, null, decompressConcatenated); + this(wrapper, null, decompressConcatenated, 0); + } + + public JdkZlibDecoder(ZlibWrapper wrapper, boolean decompressConcatenated, int maxAllocation) { + this(wrapper, null, decompressConcatenated, maxAllocation); } public JdkZlibDecoder(boolean decompressConcatenated) { - this(ZlibWrapper.GZIP, null, decompressConcatenated); + this(ZlibWrapper.GZIP, null, decompressConcatenated, 0); } - private JdkZlibDecoder(ZlibWrapper wrapper, byte[] dictionary, boolean decompressConcatenated) { - if (wrapper == null) { - throw new NullPointerException("wrapper"); - } + public JdkZlibDecoder(boolean decompressConcatenated, int maxAllocation) { + this(ZlibWrapper.GZIP, null, decompressConcatenated, maxAllocation); + } + + private JdkZlibDecoder(ZlibWrapper wrapper, byte[] dictionary, boolean decompressConcatenated, int maxAllocation) { + super(maxAllocation); + + ObjectUtil.checkNotNull(wrapper, "wrapper"); + this.decompressConcatenated = decompressConcatenated; switch (wrapper) { case GZIP: @@ -177,7 +226,7 @@ public class JdkZlibDecoder extends ZlibDecoder { inflater.setInput(array); } - ByteBuf decompressed = ctx.alloc().heapBuffer(inflater.getRemaining() << 1); + ByteBuf decompressed = prepareDecompressBuffer(ctx, null, inflater.getRemaining() << 1); try { boolean readFooter = false; while (!inflater.needsInput()) { @@ -208,7 +257,7 @@ public class JdkZlibDecoder extends ZlibDecoder { } break; } else { - decompressed.ensureWritable(inflater.getRemaining() << 1); + decompressed = prepareDecompressBuffer(ctx, decompressed, inflater.getRemaining() << 1); } } @@ -238,6 +287,11 @@ public class JdkZlibDecoder extends ZlibDecoder { } } + @Override + protected void decompressionBufferExhausted(ByteBuf buffer) { + finished = true; + } + @Override protected void handlerRemoved0(ChannelHandlerContext ctx) throws Exception { super.handlerRemoved0(ctx); diff --git a/codec/src/main/java/io/netty/handler/codec/compression/JdkZlibEncoder.java b/codec/src/main/java/io/netty/handler/codec/compression/JdkZlibEncoder.java index 276d7f8..f1b603f 100644 --- a/codec/src/main/java/io/netty/handler/codec/compression/JdkZlibEncoder.java +++ b/codec/src/main/java/io/netty/handler/codec/compression/JdkZlibEncoder.java @@ -22,6 +22,9 @@ import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelPromise; import io.netty.channel.ChannelPromiseNotifier; import io.netty.util.concurrent.EventExecutor; +import io.netty.util.internal.ObjectUtil; +import io.netty.util.internal.PlatformDependent; +import io.netty.util.internal.SuppressJava6Requirement; import java.util.concurrent.TimeUnit; import java.util.zip.CRC32; @@ -95,9 +98,7 @@ public class JdkZlibEncoder extends ZlibEncoder { throw new IllegalArgumentException( "compressionLevel: " + compressionLevel + " (expected: 0-9)"); } - if (wrapper == null) { - throw new NullPointerException("wrapper"); - } + ObjectUtil.checkNotNull(wrapper, "wrapper"); if (wrapper == ZlibWrapper.ZLIB_OR_NONE) { throw new IllegalArgumentException( "wrapper '" + ZlibWrapper.ZLIB_OR_NONE + "' is not " + @@ -141,9 +142,7 @@ public class JdkZlibEncoder extends ZlibEncoder { throw new IllegalArgumentException( "compressionLevel: " + compressionLevel + " (expected: 0-9)"); } - if (dictionary == null) { - throw new NullPointerException("dictionary"); - } + ObjectUtil.checkNotNull(dictionary, "dictionary"); wrapper = ZlibWrapper.ZLIB; deflater = new Deflater(compressionLevel); @@ -320,7 +319,11 @@ public class JdkZlibEncoder extends ZlibEncoder { return ctx.writeAndFlush(footer, promise); } + @SuppressJava6Requirement(reason = "Usage guarded by java version check") private void deflate(ByteBuf out) { + if (PlatformDependent.javaVersion() < 7) { + deflateJdk6(out); + } int numBytes; do { int writerIndex = out.writerIndex(); @@ -330,6 +333,16 @@ public class JdkZlibEncoder extends ZlibEncoder { } while (numBytes > 0); } + private void deflateJdk6(ByteBuf out) { + int numBytes; + do { + int writerIndex = out.writerIndex(); + numBytes = deflater.deflate( + out.array(), out.arrayOffset() + writerIndex, out.writableBytes()); + out.writerIndex(writerIndex + numBytes); + } while (numBytes > 0); + } + @Override public void handlerAdded(ChannelHandlerContext ctx) throws Exception { this.ctx = ctx; diff --git a/codec/src/main/java/io/netty/handler/codec/compression/Lz4FrameDecoder.java b/codec/src/main/java/io/netty/handler/codec/compression/Lz4FrameDecoder.java index addf105..92d6e74 100644 --- a/codec/src/main/java/io/netty/handler/codec/compression/Lz4FrameDecoder.java +++ b/codec/src/main/java/io/netty/handler/codec/compression/Lz4FrameDecoder.java @@ -18,10 +18,10 @@ package io.netty.handler.codec.compression; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.ByteToMessageDecoder; +import io.netty.util.internal.ObjectUtil; import net.jpountz.lz4.LZ4Exception; import net.jpountz.lz4.LZ4Factory; import net.jpountz.lz4.LZ4FastDecompressor; -import net.jpountz.xxhash.XXHashFactory; import java.util.List; import java.util.zip.Checksum; @@ -124,9 +124,7 @@ public class Lz4FrameDecoder extends ByteToMessageDecoder { * <a href="https://github.com/Cyan4973/xxHash">Github</a>. */ public Lz4FrameDecoder(LZ4Factory factory, boolean validateChecksums) { - this(factory, validateChecksums ? - XXHashFactory.fastestInstance().newStreamingHash32(DEFAULT_SEED).asChecksum() - : null); + this(factory, validateChecksums ? new Lz4XXHash32(DEFAULT_SEED) : null); } /** @@ -139,10 +137,7 @@ public class Lz4FrameDecoder extends ByteToMessageDecoder { * You may set {@code null} if you do not want to validate checksum of each block */ public Lz4FrameDecoder(LZ4Factory factory, Checksum checksum) { - if (factory == null) { - throw new NullPointerException("factory"); - } - decompressor = factory.fastDecompressor(); + decompressor = ObjectUtil.checkNotNull(factory, "factory").fastDecompressor(); this.checksum = checksum == null ? null : ByteBufChecksum.wrapChecksum(checksum); } diff --git a/codec/src/main/java/io/netty/handler/codec/compression/Lz4FrameEncoder.java b/codec/src/main/java/io/netty/handler/codec/compression/Lz4FrameEncoder.java index b94c893..d6a29a7 100644 --- a/codec/src/main/java/io/netty/handler/codec/compression/Lz4FrameEncoder.java +++ b/codec/src/main/java/io/netty/handler/codec/compression/Lz4FrameEncoder.java @@ -31,7 +31,6 @@ import io.netty.util.internal.ObjectUtil; import net.jpountz.lz4.LZ4Compressor; import net.jpountz.lz4.LZ4Exception; import net.jpountz.lz4.LZ4Factory; -import net.jpountz.xxhash.XXHashFactory; import java.nio.ByteBuffer; import java.util.concurrent.TimeUnit; @@ -50,7 +49,6 @@ import static io.netty.handler.codec.compression.Lz4Constants.MAGIC_NUMBER; import static io.netty.handler.codec.compression.Lz4Constants.MAX_BLOCK_SIZE; import static io.netty.handler.codec.compression.Lz4Constants.MIN_BLOCK_SIZE; import static io.netty.handler.codec.compression.Lz4Constants.TOKEN_OFFSET; -import static io.netty.util.internal.ThrowableUtil.unknownStackTrace; /** * Compresses a {@link ByteBuf} using the LZ4 format. @@ -69,9 +67,6 @@ import static io.netty.util.internal.ThrowableUtil.unknownStackTrace; * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ public class Lz4FrameEncoder extends MessageToByteEncoder<ByteBuf> { - private static final EncoderException ENCODE_FINSHED_EXCEPTION = unknownStackTrace(new EncoderException( - new IllegalStateException("encode finished and not enough space to write remaining data")), - Lz4FrameEncoder.class, "encode"); static final int DEFAULT_MAX_ENCODE_SIZE = Integer.MAX_VALUE; private final int blockSize; @@ -129,8 +124,7 @@ public class Lz4FrameEncoder extends MessageToByteEncoder<ByteBuf> { * and is slower but compresses more efficiently */ public Lz4FrameEncoder(boolean highCompressor) { - this(LZ4Factory.fastestInstance(), highCompressor, DEFAULT_BLOCK_SIZE, - XXHashFactory.fastestInstance().newStreamingHash32(DEFAULT_SEED).asChecksum()); + this(LZ4Factory.fastestInstance(), highCompressor, DEFAULT_BLOCK_SIZE, new Lz4XXHash32(DEFAULT_SEED)); } /** @@ -164,12 +158,8 @@ public class Lz4FrameEncoder extends MessageToByteEncoder<ByteBuf> { */ public Lz4FrameEncoder(LZ4Factory factory, boolean highCompressor, int blockSize, Checksum checksum, int maxEncodeSize) { - if (factory == null) { - throw new NullPointerException("factory"); - } - if (checksum == null) { - throw new NullPointerException("checksum"); - } + ObjectUtil.checkNotNull(factory, "factory"); + ObjectUtil.checkNotNull(checksum, "checksum"); compressor = highCompressor ? factory.highCompressor() : factory.fastCompressor(); this.checksum = ByteBufChecksum.wrapChecksum(checksum); @@ -246,7 +236,7 @@ public class Lz4FrameEncoder extends MessageToByteEncoder<ByteBuf> { if (finished) { if (!out.isWritable(in.readableBytes())) { // out should be EMPTY_BUFFER because we should have allocated enough space above in allocateBuffer. - throw ENCODE_FINSHED_EXCEPTION; + throw new IllegalStateException("encode finished and not enough space to write remaining data"); } out.writeBytes(in); return; diff --git a/codec/src/main/java/io/netty/handler/codec/compression/Lz4XXHash32.java b/codec/src/main/java/io/netty/handler/codec/compression/Lz4XXHash32.java new file mode 100644 index 0000000..3d79f53 --- /dev/null +++ b/codec/src/main/java/io/netty/handler/codec/compression/Lz4XXHash32.java @@ -0,0 +1,107 @@ +/* + * Copyright 2019 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.codec.compression; + +import io.netty.buffer.ByteBuf; +import net.jpountz.xxhash.StreamingXXHash32; +import net.jpountz.xxhash.XXHash32; +import net.jpountz.xxhash.XXHashFactory; + +import java.nio.ByteBuffer; +import java.util.zip.Checksum; + +/** + * A special-purpose {@link ByteBufChecksum} implementation for use with + * {@link Lz4FrameEncoder} and {@link Lz4FrameDecoder}. + * + * {@link StreamingXXHash32#asChecksum()} has a particularly nasty implementation + * of {@link Checksum#update(int)} that allocates a single-element byte array for + * every invocation. + * + * In addition to that, it doesn't implement an overload that accepts a {@link ByteBuffer} + * as an argument. + * + * Combined, this means that we can't use {@code ReflectiveByteBufChecksum} at all, + * and can't use {@code SlowByteBufChecksum} because of its atrocious performance + * with direct byte buffers (allocating an array and making a JNI call for every byte + * checksummed might be considered sub-optimal by some). + * + * Block version of xxHash32 ({@link XXHash32}), however, does provide + * {@link XXHash32#hash(ByteBuffer, int)} method that is efficient and does exactly + * what we need, with a caveat that we can only invoke it once before having to reset. + * This, however, is fine for our purposes, given the way we use it in + * {@link Lz4FrameEncoder} and {@link Lz4FrameDecoder}: + * {@code reset()}, followed by one {@code update()}, followed by {@code getValue()}. + */ +public final class Lz4XXHash32 extends ByteBufChecksum { + + private static final XXHash32 XXHASH32 = XXHashFactory.fastestInstance().hash32(); + + private final int seed; + private boolean used; + private int value; + + @SuppressWarnings("WeakerAccess") + public Lz4XXHash32(int seed) { + this.seed = seed; + } + + @Override + public void update(int b) { + throw new UnsupportedOperationException(); + } + + @Override + public void update(byte[] b, int off, int len) { + if (used) { + throw new IllegalStateException(); + } + value = XXHASH32.hash(b, off, len, seed); + used = true; + } + + @Override + public void update(ByteBuf b, int off, int len) { + if (used) { + throw new IllegalStateException(); + } + if (b.hasArray()) { + value = XXHASH32.hash(b.array(), b.arrayOffset() + off, len, seed); + } else { + value = XXHASH32.hash(CompressionUtil.safeNioBuffer(b, off, len), seed); + } + used = true; + } + + @Override + public long getValue() { + if (!used) { + throw new IllegalStateException(); + } + /* + * If you look carefully, you'll notice that the most significant nibble + * is being discarded; we believe this to be a bug, but this is what + * StreamingXXHash32#asChecksum() implementation of getValue() does, + * so we have to retain this behaviour for compatibility reasons. + */ + return value & 0xFFFFFFFL; + } + + @Override + public void reset() { + used = false; + } +} diff --git a/codec/src/main/java/io/netty/handler/codec/compression/LzfEncoder.java b/codec/src/main/java/io/netty/handler/codec/compression/LzfEncoder.java index e541218..32ad151 100644 --- a/codec/src/main/java/io/netty/handler/codec/compression/LzfEncoder.java +++ b/codec/src/main/java/io/netty/handler/codec/compression/LzfEncoder.java @@ -17,27 +17,38 @@ package io.netty.handler.codec.compression; import com.ning.compress.BufferRecycler; import com.ning.compress.lzf.ChunkEncoder; +import com.ning.compress.lzf.LZFChunk; import com.ning.compress.lzf.LZFEncoder; import com.ning.compress.lzf.util.ChunkEncoderFactory; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.MessageToByteEncoder; -import static com.ning.compress.lzf.LZFChunk.*; +import static com.ning.compress.lzf.LZFChunk.MAX_CHUNK_LEN; /** * Compresses a {@link ByteBuf} using the LZF format. - * + * <p> * See original <a href="http://oldhome.schmorp.de/marc/liblzf.html">LZF package</a> * and <a href="https://github.com/ning/compress/wiki/LZFFormat">LZF format</a> for full description. */ public class LzfEncoder extends MessageToByteEncoder<ByteBuf> { + /** * Minimum block size ready for compression. Blocks with length * less than {@link #MIN_BLOCK_TO_COMPRESS} will write as uncompressed. */ private static final int MIN_BLOCK_TO_COMPRESS = 16; + /** + * Compress threshold for LZF format. When the amount of input data is less than compressThreshold, + * we will construct an uncompressed output according to the LZF format. + * <p> + * When the value is less than {@see ChunkEncoder#MIN_BLOCK_TO_COMPRESS}, since LZF will not compress data + * that is less than {@see ChunkEncoder#MIN_BLOCK_TO_COMPRESS}, compressThreshold will not work. + */ + private final int compressThreshold; + /** * Underlying decoder in use. */ @@ -55,29 +66,44 @@ public class LzfEncoder extends MessageToByteEncoder<ByteBuf> { * non-standard platforms it may be necessary to use {@link #LzfEncoder(boolean)} with {@code true} param. */ public LzfEncoder() { - this(false, MAX_CHUNK_LEN); + this(false); } /** * Creates a new LZF encoder with specified encoding instance. * - * @param safeInstance - * If {@code true} encoder will use {@link ChunkEncoder} that only uses standard JDK access methods, - * and should work on all Java platforms and JVMs. - * Otherwise encoder will try to use highly optimized {@link ChunkEncoder} implementation that uses - * Sun JDK's {@link sun.misc.Unsafe} class (which may be included by other JDK's as well). + * @param safeInstance If {@code true} encoder will use {@link ChunkEncoder} that only uses + * standard JDK access methods, and should work on all Java platforms and JVMs. + * Otherwise encoder will try to use highly optimized {@link ChunkEncoder} + * implementation that uses Sun JDK's {@link sun.misc.Unsafe} + * class (which may be included by other JDK's as well). */ public LzfEncoder(boolean safeInstance) { this(safeInstance, MAX_CHUNK_LEN); } + /** + * Creates a new LZF encoder with specified encoding instance and compressThreshold. + * + * @param safeInstance If {@code true} encoder will use {@link ChunkEncoder} that only uses standard + * JDK access methods, and should work on all Java platforms and JVMs. + * Otherwise encoder will try to use highly optimized {@link ChunkEncoder} + * implementation that uses Sun JDK's {@link sun.misc.Unsafe} + * class (which may be included by other JDK's as well). + * @param totalLength Expected total length of content to compress; only matters for outgoing messages + * that is smaller than maximum chunk size (64k), to optimize encoding hash tables. + */ + public LzfEncoder(boolean safeInstance, int totalLength) { + this(safeInstance, totalLength, MIN_BLOCK_TO_COMPRESS); + } + /** * Creates a new LZF encoder with specified total length of encoded chunk. You can configure it to encode * your data flow more efficient if you know the average size of messages that you send. * - * @param totalLength - * Expected total length of content to compress; only matters for outgoing messages that is smaller - * than maximum chunk size (64k), to optimize encoding hash tables. + * @param totalLength Expected total length of content to compress; + * only matters for outgoing messages that is smaller than maximum chunk size (64k), + * to optimize encoding hash tables. */ public LzfEncoder(int totalLength) { this(false, totalLength); @@ -86,27 +112,36 @@ public class LzfEncoder extends MessageToByteEncoder<ByteBuf> { /** * Creates a new LZF encoder with specified settings. * - * @param safeInstance - * If {@code true} encoder will use {@link ChunkEncoder} that only uses standard JDK access methods, - * and should work on all Java platforms and JVMs. - * Otherwise encoder will try to use highly optimized {@link ChunkEncoder} implementation that uses - * Sun JDK's {@link sun.misc.Unsafe} class (which may be included by other JDK's as well). - * @param totalLength - * Expected total length of content to compress; only matters for outgoing messages that is smaller - * than maximum chunk size (64k), to optimize encoding hash tables. + * @param safeInstance If {@code true} encoder will use {@link ChunkEncoder} that only uses standard JDK + * access methods, and should work on all Java platforms and JVMs. + * Otherwise encoder will try to use highly optimized {@link ChunkEncoder} + * implementation that uses Sun JDK's {@link sun.misc.Unsafe} + * class (which may be included by other JDK's as well). + * @param totalLength Expected total length of content to compress; only matters for outgoing messages + * that is smaller than maximum chunk size (64k), to optimize encoding hash tables. + * @param compressThreshold Compress threshold for LZF format. When the amount of input data is less than + * compressThreshold, we will construct an uncompressed output according + * to the LZF format. */ - public LzfEncoder(boolean safeInstance, int totalLength) { + public LzfEncoder(boolean safeInstance, int totalLength, int compressThreshold) { super(false); if (totalLength < MIN_BLOCK_TO_COMPRESS || totalLength > MAX_CHUNK_LEN) { throw new IllegalArgumentException("totalLength: " + totalLength + " (expected: " + MIN_BLOCK_TO_COMPRESS + '-' + MAX_CHUNK_LEN + ')'); } - encoder = safeInstance ? + if (compressThreshold < MIN_BLOCK_TO_COMPRESS) { + // not a suitable value. + throw new IllegalArgumentException("compressThreshold:" + compressThreshold + + " expected >=" + MIN_BLOCK_TO_COMPRESS); + } + this.compressThreshold = compressThreshold; + + this.encoder = safeInstance ? ChunkEncoderFactory.safeNonAllocatingInstance(totalLength) - : ChunkEncoderFactory.optimalNonAllocatingInstance(totalLength); + : ChunkEncoderFactory.optimalNonAllocatingInstance(totalLength); - recycler = BufferRecycler.instance(); + this.recycler = BufferRecycler.instance(); } @Override @@ -128,8 +163,16 @@ public class LzfEncoder extends MessageToByteEncoder<ByteBuf> { out.ensureWritable(maxOutputLength); final byte[] output = out.array(); final int outputPtr = out.arrayOffset() + out.writerIndex(); - final int outputLength = LZFEncoder.appendEncoded(encoder, - input, inputPtr, length, output, outputPtr) - outputPtr; + + final int outputLength; + if (length >= compressThreshold) { + // compress. + outputLength = encodeCompress(input, inputPtr, length, output, outputPtr); + } else { + // not compress. + outputLength = encodeNonCompress(input, inputPtr, length, output, outputPtr); + } + out.writerIndex(out.writerIndex() + outputLength); in.skipBytes(length); @@ -137,4 +180,40 @@ public class LzfEncoder extends MessageToByteEncoder<ByteBuf> { recycler.releaseInputBuffer(input); } } + + private int encodeCompress(byte[] input, int inputPtr, int length, byte[] output, int outputPtr) { + return LZFEncoder.appendEncoded(encoder, + input, inputPtr, length, output, outputPtr) - outputPtr; + } + + private static int lzfEncodeNonCompress(byte[] input, int inputPtr, int length, byte[] output, int outputPtr) { + int left = length; + int chunkLen = Math.min(LZFChunk.MAX_CHUNK_LEN, left); + outputPtr = LZFChunk.appendNonCompressed(input, inputPtr, chunkLen, output, outputPtr); + left -= chunkLen; + if (left < 1) { + return outputPtr; + } + inputPtr += chunkLen; + do { + chunkLen = Math.min(left, LZFChunk.MAX_CHUNK_LEN); + outputPtr = LZFChunk.appendNonCompressed(input, inputPtr, chunkLen, output, outputPtr); + inputPtr += chunkLen; + left -= chunkLen; + } while (left > 0); + return outputPtr; + } + + /** + * Use lzf uncompressed format to encode a piece of input. + */ + private static int encodeNonCompress(byte[] input, int inputPtr, int length, byte[] output, int outputPtr) { + return lzfEncodeNonCompress(input, inputPtr, length, output, outputPtr) - outputPtr; + } + + @Override + public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { + encoder.close(); + super.handlerRemoved(ctx); + } } diff --git a/codec/src/main/java/io/netty/handler/codec/compression/Snappy.java b/codec/src/main/java/io/netty/handler/codec/compression/Snappy.java index 9264244..8e1825d 100644 --- a/codec/src/main/java/io/netty/handler/codec/compression/Snappy.java +++ b/codec/src/main/java/io/netty/handler/codec/compression/Snappy.java @@ -607,7 +607,7 @@ public final class Snappy { Crc32c crc32 = new Crc32c(); try { crc32.update(data, offset, length); - return maskChecksum((int) crc32.getValue()); + return maskChecksum(crc32.getValue()); } finally { crc32.reset(); } @@ -655,7 +655,7 @@ public final class Snappy { * @param checksum The actual checksum of the data * @return The masked checksum */ - static int maskChecksum(int checksum) { - return (checksum >> 15 | checksum << 17) + 0xa282ead8; + static int maskChecksum(long checksum) { + return (int) ((checksum >> 15 | checksum << 17) + 0xa282ead8); } } diff --git a/codec/src/main/java/io/netty/handler/codec/compression/ZlibDecoder.java b/codec/src/main/java/io/netty/handler/codec/compression/ZlibDecoder.java index d01bc6b..26fd3e7 100644 --- a/codec/src/main/java/io/netty/handler/codec/compression/ZlibDecoder.java +++ b/codec/src/main/java/io/netty/handler/codec/compression/ZlibDecoder.java @@ -16,6 +16,8 @@ package io.netty.handler.codec.compression; import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.ByteToMessageDecoder; /** @@ -23,9 +25,72 @@ import io.netty.handler.codec.ByteToMessageDecoder; */ public abstract class ZlibDecoder extends ByteToMessageDecoder { + /** + * Maximum allowed size of the decompression buffer. + */ + protected final int maxAllocation; + + /** + * Same as {@link #ZlibDecoder(int)} with maxAllocation = 0. + */ + public ZlibDecoder() { + this(0); + } + + /** + * Construct a new ZlibDecoder. + * @param maxAllocation + * Maximum size of the decompression buffer. Must be >= 0. + * If zero, maximum size is decided by the {@link ByteBufAllocator}. + */ + public ZlibDecoder(int maxAllocation) { + if (maxAllocation < 0) { + throw new IllegalArgumentException("maxAllocation must be >= 0"); + } + this.maxAllocation = maxAllocation; + } + /** * Returns {@code true} if and only if the end of the compressed stream * has been reached. */ public abstract boolean isClosed(); + + /** + * Allocate or expand the decompression buffer, without exceeding the maximum allocation. + * Calls {@link #decompressionBufferExhausted(ByteBuf)} if the buffer is full and cannot be expanded further. + */ + protected ByteBuf prepareDecompressBuffer(ChannelHandlerContext ctx, ByteBuf buffer, int preferredSize) { + if (buffer == null) { + if (maxAllocation == 0) { + return ctx.alloc().heapBuffer(preferredSize); + } + + return ctx.alloc().heapBuffer(Math.min(preferredSize, maxAllocation), maxAllocation); + } + + // this always expands the buffer if possible, even if the expansion is less than preferredSize + // we throw the exception only if the buffer could not be expanded at all + // this means that one final attempt to deserialize will always be made with the buffer at maxAllocation + if (buffer.ensureWritable(preferredSize, true) == 1) { + // buffer must be consumed so subclasses don't add it to output + // we therefore duplicate it when calling decompressionBufferExhausted() to guarantee non-interference + // but wait until after to consume it so the subclass can tell how much output is really in the buffer + decompressionBufferExhausted(buffer.duplicate()); + buffer.skipBytes(buffer.readableBytes()); + throw new DecompressionException("Decompression buffer has reached maximum size: " + buffer.maxCapacity()); + } + + return buffer; + } + + /** + * Called when the decompression buffer cannot be expanded further. + * Default implementation is a no-op, but subclasses can override in case they want to + * do something before the {@link DecompressionException} is thrown, such as log the + * data that was decompressed so far. + */ + protected void decompressionBufferExhausted(ByteBuf buffer) { + } + } diff --git a/codec/src/main/java/io/netty/handler/codec/protobuf/ProtobufDecoder.java b/codec/src/main/java/io/netty/handler/codec/protobuf/ProtobufDecoder.java index 9ef56f1..a5dec53 100644 --- a/codec/src/main/java/io/netty/handler/codec/protobuf/ProtobufDecoder.java +++ b/codec/src/main/java/io/netty/handler/codec/protobuf/ProtobufDecoder.java @@ -28,6 +28,7 @@ import io.netty.handler.codec.ByteToMessageDecoder; import io.netty.handler.codec.LengthFieldBasedFrameDecoder; import io.netty.handler.codec.LengthFieldPrepender; import io.netty.handler.codec.MessageToMessageDecoder; +import io.netty.util.internal.ObjectUtil; import java.util.List; @@ -95,10 +96,7 @@ public class ProtobufDecoder extends MessageToMessageDecoder<ByteBuf> { } public ProtobufDecoder(MessageLite prototype, ExtensionRegistryLite extensionRegistry) { - if (prototype == null) { - throw new NullPointerException("prototype"); - } - this.prototype = prototype.getDefaultInstanceForType(); + this.prototype = ObjectUtil.checkNotNull(prototype, "prototype").getDefaultInstanceForType(); this.extensionRegistry = extensionRegistry; } diff --git a/codec/src/main/java/io/netty/handler/codec/serialization/CompatibleObjectEncoder.java b/codec/src/main/java/io/netty/handler/codec/serialization/CompatibleObjectEncoder.java index db97def..c56b499 100644 --- a/codec/src/main/java/io/netty/handler/codec/serialization/CompatibleObjectEncoder.java +++ b/codec/src/main/java/io/netty/handler/codec/serialization/CompatibleObjectEncoder.java @@ -19,8 +19,6 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufOutputStream; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.MessageToByteEncoder; -import io.netty.util.Attribute; -import io.netty.util.AttributeKey; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; diff --git a/codec/src/main/java/io/netty/handler/codec/serialization/ObjectDecoderInputStream.java b/codec/src/main/java/io/netty/handler/codec/serialization/ObjectDecoderInputStream.java index ef705ae..8e117ab 100644 --- a/codec/src/main/java/io/netty/handler/codec/serialization/ObjectDecoderInputStream.java +++ b/codec/src/main/java/io/netty/handler/codec/serialization/ObjectDecoderInputStream.java @@ -15,6 +15,8 @@ */ package io.netty.handler.codec.serialization; +import io.netty.util.internal.ObjectUtil; + import java.io.BufferedReader; import java.io.DataInputStream; import java.io.IOException; @@ -88,12 +90,9 @@ public class ObjectDecoderInputStream extends InputStream implements * a {@link StreamCorruptedException} will be raised. */ public ObjectDecoderInputStream(InputStream in, ClassLoader classLoader, int maxObjectSize) { - if (in == null) { - throw new NullPointerException("in"); - } - if (maxObjectSize <= 0) { - throw new IllegalArgumentException("maxObjectSize: " + maxObjectSize); - } + ObjectUtil.checkNotNull(in, "in"); + ObjectUtil.checkPositive(maxObjectSize, "maxObjectSize"); + if (in instanceof DataInputStream) { this.in = (DataInputStream) in; } else { diff --git a/codec/src/main/java/io/netty/handler/codec/serialization/ObjectEncoderOutputStream.java b/codec/src/main/java/io/netty/handler/codec/serialization/ObjectEncoderOutputStream.java index 769db41..76ec4a5 100644 --- a/codec/src/main/java/io/netty/handler/codec/serialization/ObjectEncoderOutputStream.java +++ b/codec/src/main/java/io/netty/handler/codec/serialization/ObjectEncoderOutputStream.java @@ -18,6 +18,7 @@ package io.netty.handler.codec.serialization; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufOutputStream; import io.netty.buffer.Unpooled; +import io.netty.util.internal.ObjectUtil; import java.io.DataOutputStream; import java.io.IOException; @@ -63,12 +64,8 @@ public class ObjectEncoderOutputStream extends OutputStream implements * cost, please specify the properly estimated value. */ public ObjectEncoderOutputStream(OutputStream out, int estimatedLength) { - if (out == null) { - throw new NullPointerException("out"); - } - if (estimatedLength < 0) { - throw new IllegalArgumentException("estimatedLength: " + estimatedLength); - } + ObjectUtil.checkNotNull(out, "out"); + ObjectUtil.checkPositiveOrZero(estimatedLength, "estimatedLength"); if (out instanceof DataOutputStream) { this.out = (DataOutputStream) out; diff --git a/codec/src/main/java/io/netty/handler/codec/string/StringDecoder.java b/codec/src/main/java/io/netty/handler/codec/string/StringDecoder.java index 0f219ae..5b68950 100644 --- a/codec/src/main/java/io/netty/handler/codec/string/StringDecoder.java +++ b/codec/src/main/java/io/netty/handler/codec/string/StringDecoder.java @@ -23,6 +23,7 @@ import io.netty.handler.codec.ByteToMessageDecoder; import io.netty.handler.codec.DelimiterBasedFrameDecoder; import io.netty.handler.codec.LineBasedFrameDecoder; import io.netty.handler.codec.MessageToMessageDecoder; +import io.netty.util.internal.ObjectUtil; import java.nio.charset.Charset; import java.util.List; @@ -68,10 +69,7 @@ public class StringDecoder extends MessageToMessageDecoder<ByteBuf> { * Creates a new instance with the specified character set. */ public StringDecoder(Charset charset) { - if (charset == null) { - throw new NullPointerException("charset"); - } - this.charset = charset; + this.charset = ObjectUtil.checkNotNull(charset, "charset"); } @Override diff --git a/codec/src/main/java/io/netty/handler/codec/string/StringEncoder.java b/codec/src/main/java/io/netty/handler/codec/string/StringEncoder.java index 9177fae..a04ac85 100644 --- a/codec/src/main/java/io/netty/handler/codec/string/StringEncoder.java +++ b/codec/src/main/java/io/netty/handler/codec/string/StringEncoder.java @@ -22,6 +22,7 @@ import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelPipeline; import io.netty.handler.codec.LineBasedFrameDecoder; import io.netty.handler.codec.MessageToMessageEncoder; +import io.netty.util.internal.ObjectUtil; import java.nio.CharBuffer; import java.nio.charset.Charset; @@ -51,7 +52,6 @@ import java.util.List; @Sharable public class StringEncoder extends MessageToMessageEncoder<CharSequence> { - // TODO Use CharsetEncoder instead. private final Charset charset; /** @@ -65,10 +65,7 @@ public class StringEncoder extends MessageToMessageEncoder<CharSequence> { * Creates a new instance with the specified character set. */ public StringEncoder(Charset charset) { - if (charset == null) { - throw new NullPointerException("charset"); - } - this.charset = charset; + this.charset = ObjectUtil.checkNotNull(charset, "charset"); } @Override diff --git a/codec/src/test/java/io/netty/handler/codec/ByteToMessageCodecTest.java b/codec/src/test/java/io/netty/handler/codec/ByteToMessageCodecTest.java index 556bd24..c837758 100644 --- a/codec/src/test/java/io/netty/handler/codec/ByteToMessageCodecTest.java +++ b/codec/src/test/java/io/netty/handler/codec/ByteToMessageCodecTest.java @@ -64,7 +64,7 @@ public class ByteToMessageCodecTest { assertTrue(ch.finish()); assertEquals(1, ch.readInbound()); - ByteBuf buf = (ByteBuf) ch.readInbound(); + ByteBuf buf = ch.readInbound(); assertEquals(Unpooled.wrappedBuffer(new byte[]{'0'}), buf); buf.release(); assertNull(ch.readInbound()); diff --git a/codec/src/test/java/io/netty/handler/codec/ByteToMessageDecoderTest.java b/codec/src/test/java/io/netty/handler/codec/ByteToMessageDecoderTest.java index 93dd221..3db8022 100644 --- a/codec/src/test/java/io/netty/handler/codec/ByteToMessageDecoderTest.java +++ b/codec/src/test/java/io/netty/handler/codec/ByteToMessageDecoderTest.java @@ -15,13 +15,16 @@ */ package io.netty.handler.codec; +import io.netty.buffer.AbstractByteBufAllocator; import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.CompositeByteBuf; import io.netty.buffer.Unpooled; import io.netty.buffer.UnpooledByteBufAllocator; import io.netty.buffer.UnpooledHeapByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.ChannelOutboundHandlerAdapter; import io.netty.channel.embedded.EmbeddedChannel; import io.netty.util.internal.PlatformDependent; import org.junit.Test; @@ -30,7 +33,13 @@ import java.util.List; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingDeque; -import static org.junit.Assert.*; +import static io.netty.buffer.Unpooled.wrappedBuffer; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; public class ByteToMessageDecoderTest { @@ -245,7 +254,7 @@ public class ByteToMessageDecoderTest { byte[] bytes = new byte[1024]; PlatformDependent.threadLocalRandom().nextBytes(bytes); - assertTrue(channel.writeInbound(Unpooled.wrappedBuffer(bytes))); + assertTrue(channel.writeInbound(Unpooled.copiedBuffer(bytes))); assertBuffer(Unpooled.wrappedBuffer(bytes), (ByteBuf) channel.readInbound()); assertNull(channel.readInbound()); assertFalse(channel.finish()); @@ -277,7 +286,7 @@ public class ByteToMessageDecoderTest { byte[] bytes = new byte[1024]; PlatformDependent.threadLocalRandom().nextBytes(bytes); - assertTrue(channel.writeInbound(Unpooled.wrappedBuffer(bytes))); + assertTrue(channel.writeInbound(Unpooled.copiedBuffer(bytes))); assertBuffer(Unpooled.wrappedBuffer(bytes, 0, bytes.length - 1), (ByteBuf) channel.readInbound()); assertNull(channel.readInbound()); assertTrue(channel.finish()); @@ -306,23 +315,95 @@ public class ByteToMessageDecoderTest { assertFalse(channel.finish()); } + static class WriteFailingByteBuf extends UnpooledHeapByteBuf { + private final Error error = new Error(); + private int untilFailure; + + WriteFailingByteBuf(int untilFailure, int capacity) { + super(UnpooledByteBufAllocator.DEFAULT, capacity, capacity); + this.untilFailure = untilFailure; + } + + @Override + public ByteBuf setBytes(int index, ByteBuf src, int srcIndex, int length) { + if (--untilFailure <= 0) { + throw error; + } + return super.setBytes(index, src, srcIndex, length); + } + + Error writeError() { + return error; + } + } + @Test public void releaseWhenMergeCumulateThrows() { - final Error error = new Error(); + WriteFailingByteBuf oldCumulation = new WriteFailingByteBuf(1, 64); + oldCumulation.writeZero(1); + ByteBuf in = Unpooled.buffer().writeZero(12); + + Throwable thrown = null; + try { + ByteToMessageDecoder.MERGE_CUMULATOR.cumulate(UnpooledByteBufAllocator.DEFAULT, oldCumulation, in); + } catch (Throwable t) { + thrown = t; + } + + assertSame(oldCumulation.writeError(), thrown); + assertEquals(0, in.refCnt()); + assertEquals(1, oldCumulation.refCnt()); + oldCumulation.release(); + } + + @Test + public void releaseWhenMergeCumulateThrowsInExpand() { + releaseWhenMergeCumulateThrowsInExpand(1, true); + releaseWhenMergeCumulateThrowsInExpand(2, true); + releaseWhenMergeCumulateThrowsInExpand(3, false); // sentinel test case + } + + private void releaseWhenMergeCumulateThrowsInExpand(int untilFailure, boolean shouldFail) { + ByteBuf oldCumulation = UnpooledByteBufAllocator.DEFAULT.heapBuffer(8, 8).writeZero(1); + final WriteFailingByteBuf newCumulation = new WriteFailingByteBuf(untilFailure, 16); - ByteBuf cumulation = new UnpooledHeapByteBuf(UnpooledByteBufAllocator.DEFAULT, 0, 64) { + ByteBufAllocator allocator = new AbstractByteBufAllocator(false) { @Override - public ByteBuf writeBytes(ByteBuf src) { - throw error; + public boolean isDirectBufferPooled() { + return false; + } + + @Override + protected ByteBuf newHeapBuffer(int initialCapacity, int maxCapacity) { + return newCumulation; + } + + @Override + protected ByteBuf newDirectBuffer(int initialCapacity, int maxCapacity) { + throw new UnsupportedOperationException(); } }; + ByteBuf in = Unpooled.buffer().writeZero(12); + Throwable thrown = null; try { - ByteToMessageDecoder.MERGE_CUMULATOR.cumulate(UnpooledByteBufAllocator.DEFAULT, cumulation, in); - fail(); - } catch (Error expected) { - assertSame(error, expected); - assertEquals(0, in.refCnt()); + ByteToMessageDecoder.MERGE_CUMULATOR.cumulate(allocator, oldCumulation, in); + } catch (Throwable t) { + thrown = t; + } + + assertEquals(0, in.refCnt()); + + if (shouldFail) { + assertSame(newCumulation.writeError(), thrown); + assertEquals(1, oldCumulation.refCnt()); + oldCumulation.release(); + assertEquals(0, newCumulation.refCnt()); + } else { + assertNull(thrown); + assertEquals(0, oldCumulation.refCnt()); + assertEquals(1, newCumulation.refCnt()); + newCumulation.release(); } } @@ -335,7 +416,11 @@ public class ByteToMessageDecoderTest { public CompositeByteBuf addComponent(boolean increaseWriterIndex, ByteBuf buffer) { throw error; } - }; + @Override + public CompositeByteBuf addFlattenedComponents(boolean increaseWriterIndex, ByteBuf buffer) { + throw error; + } + }.writeZero(1); ByteBuf in = Unpooled.buffer().writeZero(12); try { ByteToMessageDecoder.COMPOSITE_CUMULATOR.cumulate(UnpooledByteBufAllocator.DEFAULT, cumulation, in); @@ -343,6 +428,86 @@ public class ByteToMessageDecoderTest { } catch (Error expected) { assertSame(error, expected); assertEquals(0, in.refCnt()); + cumulation.release(); } } + + @Test + public void testDoesNotOverRead() { + class ReadInterceptingHandler extends ChannelOutboundHandlerAdapter { + private int readsTriggered; + + @Override + public void read(ChannelHandlerContext ctx) throws Exception { + readsTriggered++; + super.read(ctx); + } + } + ReadInterceptingHandler interceptor = new ReadInterceptingHandler(); + + EmbeddedChannel channel = new EmbeddedChannel(); + channel.config().setAutoRead(false); + channel.pipeline().addLast(interceptor, new FixedLengthFrameDecoder(3)); + assertEquals(0, interceptor.readsTriggered); + + // 0 complete frames, 1 partial frame: SHOULD trigger a read + channel.writeInbound(wrappedBuffer(new byte[] { 0, 1 })); + assertEquals(1, interceptor.readsTriggered); + + // 2 complete frames, 0 partial frames: should NOT trigger a read + channel.writeInbound(wrappedBuffer(new byte[] { 2 }), wrappedBuffer(new byte[] { 3, 4, 5 })); + assertEquals(1, interceptor.readsTriggered); + + // 1 complete frame, 1 partial frame: should NOT trigger a read + channel.writeInbound(wrappedBuffer(new byte[] { 6, 7, 8 }), wrappedBuffer(new byte[] { 9 })); + assertEquals(1, interceptor.readsTriggered); + + // 1 complete frame, 1 partial frame: should NOT trigger a read + channel.writeInbound(wrappedBuffer(new byte[] { 10, 11 }), wrappedBuffer(new byte[] { 12 })); + assertEquals(1, interceptor.readsTriggered); + + // 0 complete frames, 1 partial frame: SHOULD trigger a read + channel.writeInbound(wrappedBuffer(new byte[] { 13 })); + assertEquals(2, interceptor.readsTriggered); + + // 1 complete frame, 0 partial frames: should NOT trigger a read + channel.writeInbound(wrappedBuffer(new byte[] { 14 })); + assertEquals(2, interceptor.readsTriggered); + + for (int i = 0; i < 5; i++) { + ByteBuf read = channel.readInbound(); + assertEquals(i * 3 + 0, read.getByte(0)); + assertEquals(i * 3 + 1, read.getByte(1)); + assertEquals(i * 3 + 2, read.getByte(2)); + read.release(); + } + assertFalse(channel.finish()); + } + + @Test + public void testDisorder() { + ByteToMessageDecoder decoder = new ByteToMessageDecoder() { + int count; + + //read 4 byte then remove this decoder + @Override + protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) { + out.add(in.readByte()); + if (++count >= 4) { + ctx.pipeline().remove(this); + } + } + }; + EmbeddedChannel channel = new EmbeddedChannel(decoder); + assertTrue(channel.writeInbound(Unpooled.wrappedBuffer(new byte[]{1, 2, 3, 4, 5}))); + assertEquals((byte) 1, channel.readInbound()); + assertEquals((byte) 2, channel.readInbound()); + assertEquals((byte) 3, channel.readInbound()); + assertEquals((byte) 4, channel.readInbound()); + ByteBuf buffer5 = channel.readInbound(); + assertEquals((byte) 5, buffer5.readByte()); + assertFalse(buffer5.isReadable()); + assertTrue(buffer5.release()); + assertFalse(channel.finish()); + } } diff --git a/codec/src/test/java/io/netty/handler/codec/DatagramPacketEncoderTest.java b/codec/src/test/java/io/netty/handler/codec/DatagramPacketEncoderTest.java index fdf1cac..be200bf 100644 --- a/codec/src/test/java/io/netty/handler/codec/DatagramPacketEncoderTest.java +++ b/codec/src/test/java/io/netty/handler/codec/DatagramPacketEncoderTest.java @@ -51,8 +51,17 @@ public class DatagramPacketEncoderTest { @Test public void testEncode() { + testEncode(false); + } + + @Test + public void testEncodeWithSenderIsNull() { + testEncode(true); + } + + private void testEncode(boolean senderIsNull) { InetSocketAddress recipient = SocketUtils.socketAddress("127.0.0.1", 10000); - InetSocketAddress sender = SocketUtils.socketAddress("127.0.0.1", 20000); + InetSocketAddress sender = senderIsNull ? null : SocketUtils.socketAddress("127.0.0.1", 20000); assertTrue(channel.writeOutbound( new DefaultAddressedEnvelope<String, InetSocketAddress>("netty", recipient, sender))); DatagramPacket packet = channel.readOutbound(); diff --git a/codec/src/test/java/io/netty/handler/codec/DateFormatterTest.java b/codec/src/test/java/io/netty/handler/codec/DateFormatterTest.java index 99d1d38..e833fdf 100644 --- a/codec/src/test/java/io/netty/handler/codec/DateFormatterTest.java +++ b/codec/src/test/java/io/netty/handler/codec/DateFormatterTest.java @@ -17,6 +17,7 @@ package io.netty.handler.codec; import org.junit.Test; +import java.util.Calendar; import java.util.Date; import static org.junit.Assert.*; @@ -111,4 +112,26 @@ public class DateFormatterTest { public void testFormat() { assertEquals("Sun, 6 Nov 1994 08:49:37 GMT", format(DATE)); } + + @Test + public void testParseAllMonths() { + assertEquals(Calendar.JANUARY, getMonth(parseHttpDate("Sun, 6 Jan 1994 08:49:37 GMT"))); + assertEquals(Calendar.FEBRUARY, getMonth(parseHttpDate("Sun, 6 Feb 1994 08:49:37 GMT"))); + assertEquals(Calendar.MARCH, getMonth(parseHttpDate("Sun, 6 Mar 1994 08:49:37 GMT"))); + assertEquals(Calendar.APRIL, getMonth(parseHttpDate("Sun, 6 Apr 1994 08:49:37 GMT"))); + assertEquals(Calendar.MAY, getMonth(parseHttpDate("Sun, 6 May 1994 08:49:37 GMT"))); + assertEquals(Calendar.JUNE, getMonth(parseHttpDate("Sun, 6 Jun 1994 08:49:37 GMT"))); + assertEquals(Calendar.JULY, getMonth(parseHttpDate("Sun, 6 Jul 1994 08:49:37 GMT"))); + assertEquals(Calendar.AUGUST, getMonth(parseHttpDate("Sun, 6 Aug 1994 08:49:37 GMT"))); + assertEquals(Calendar.SEPTEMBER, getMonth(parseHttpDate("Sun, 6 Sep 1994 08:49:37 GMT"))); + assertEquals(Calendar.OCTOBER, getMonth(parseHttpDate("Sun Oct 6 08:49:37 1994"))); + assertEquals(Calendar.NOVEMBER, getMonth(parseHttpDate("Sun Nov 6 08:49:37 1994"))); + assertEquals(Calendar.DECEMBER, getMonth(parseHttpDate("Sun Dec 6 08:49:37 1994"))); + } + + private static int getMonth(Date referenceDate) { + Calendar cal = Calendar.getInstance(); + cal.setTime(referenceDate); + return cal.get(Calendar.MONTH); + } } diff --git a/codec/src/test/java/io/netty/handler/codec/DefaultHeadersTest.java b/codec/src/test/java/io/netty/handler/codec/DefaultHeadersTest.java index 073872b..6139b46 100644 --- a/codec/src/test/java/io/netty/handler/codec/DefaultHeadersTest.java +++ b/codec/src/test/java/io/netty/handler/codec/DefaultHeadersTest.java @@ -41,11 +41,11 @@ public class DefaultHeadersTest { private static final class TestDefaultHeaders extends DefaultHeaders<CharSequence, CharSequence, TestDefaultHeaders> { - public TestDefaultHeaders() { + TestDefaultHeaders() { this(CharSequenceValueConverter.INSTANCE); } - public TestDefaultHeaders(ValueConverter<CharSequence> converter) { + TestDefaultHeaders(ValueConverter<CharSequence> converter) { super(converter); } } @@ -109,21 +109,85 @@ public class DefaultHeadersTest { assertTrue(values.containsAll(asList(of("value1"), of("value2"), of("value3")))); } + @Test + public void multipleValuesPerNameIteratorWithOtherNames() { + TestDefaultHeaders headers = newInstance(); + headers.add(of("name1"), of("value1")); + headers.add(of("name1"), of("value2")); + headers.add(of("name2"), of("value4")); + headers.add(of("name1"), of("value3")); + assertEquals(4, headers.size()); + + List<CharSequence> values = new ArrayList<CharSequence>(); + Iterator<CharSequence> itr = headers.valueIterator(of("name1")); + while (itr.hasNext()) { + values.add(itr.next()); + itr.remove(); + } + assertEquals(3, values.size()); + assertEquals(1, headers.size()); + assertFalse(headers.isEmpty()); + assertTrue(values.containsAll(asList(of("value1"), of("value2"), of("value3")))); + itr = headers.valueIterator(of("name1")); + assertFalse(itr.hasNext()); + itr = headers.valueIterator(of("name2")); + assertTrue(itr.hasNext()); + assertEquals(of("value4"), itr.next()); + assertFalse(itr.hasNext()); + } + @Test public void multipleValuesPerNameIterator() { + TestDefaultHeaders headers = newInstance(); + headers.add(of("name1"), of("value1")); + headers.add(of("name1"), of("value2")); + assertEquals(2, headers.size()); + + List<CharSequence> values = new ArrayList<CharSequence>(); + Iterator<CharSequence> itr = headers.valueIterator(of("name1")); + while (itr.hasNext()) { + values.add(itr.next()); + itr.remove(); + } + assertEquals(2, values.size()); + assertEquals(0, headers.size()); + assertTrue(headers.isEmpty()); + assertTrue(values.containsAll(asList(of("value1"), of("value2")))); + itr = headers.valueIterator(of("name1")); + assertFalse(itr.hasNext()); + } + + @Test(expected = IllegalStateException.class) + public void valuesItrRemoveThrowsWhenEmpty() { + TestDefaultHeaders headers = newInstance(); + assertEquals(0, headers.size()); + assertTrue(headers.isEmpty()); + Iterator<CharSequence> itr = headers.valueIterator(of("name")); + itr.remove(); + } + + @Test + public void valuesItrRemoveThrowsAfterLastElement() { TestDefaultHeaders headers = newInstance(); headers.add(of("name"), of("value1")); - headers.add(of("name"), of("value2")); - headers.add(of("name"), of("value3")); - assertEquals(3, headers.size()); + assertEquals(1, headers.size()); List<CharSequence> values = new ArrayList<CharSequence>(); Iterator<CharSequence> itr = headers.valueIterator(of("name")); while (itr.hasNext()) { values.add(itr.next()); + itr.remove(); + } + assertEquals(1, values.size()); + assertEquals(0, headers.size()); + assertTrue(headers.isEmpty()); + assertTrue(values.contains(of("value1"))); + try { + itr.remove(); + fail(); + } catch (IllegalStateException ignored) { + // ignored } - assertEquals(3, values.size()); - assertTrue(values.containsAll(asList(of("value1"), of("value2"), of("value3")))); } @Test diff --git a/codec/src/test/java/io/netty/handler/codec/base64/Base64Test.java b/codec/src/test/java/io/netty/handler/codec/base64/Base64Test.java index b6cb66f..d3e548c 100644 --- a/codec/src/test/java/io/netty/handler/codec/base64/Base64Test.java +++ b/codec/src/test/java/io/netty/handler/codec/base64/Base64Test.java @@ -29,7 +29,7 @@ import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import static io.netty.buffer.Unpooled.copiedBuffer; -import static org.junit.Assert.assertEquals; +import static org.junit.Assert.*; public class Base64Test { @@ -94,7 +94,7 @@ public class Base64Test { "8i96YWK0VxcCMQC7pf6Wk3RhUU2Sg6S9e6CiirFLDyzLkaWxuCnXcOwTvuXTHUQSeUCp2Q6ygS5q\n" + "Kyc="; - ByteBuf src = Unpooled.wrappedBuffer(certFromString(cert).getEncoded()); + ByteBuf src = Unpooled.wrappedBuffer(certFromString(cert).getEncoded()); ByteBuf expectedEncoded = copiedBuffer(expected, CharsetUtil.US_ASCII); testEncode(src, expectedEncoded); } @@ -169,4 +169,20 @@ public class Base64Test { public void testOverflowDecodedBufferSize() { assertEquals(1610612736, Base64.decodedBufferSize(Integer.MAX_VALUE)); } + + @Test + public void decodingFailsOnInvalidInputByte() { + char[] invalidChars = {'\u007F', '\u0080', '\u00BD', '\u00FF'}; + for (char invalidChar : invalidChars) { + ByteBuf buf = copiedBuffer("eHh4" + invalidChar, CharsetUtil.ISO_8859_1); + try { + Base64.decode(buf); + fail("Invalid character in not detected: " + invalidChar); + } catch (IllegalArgumentException ignored) { + // as expected + } finally { + assertTrue(buf.release()); + } + } + } } diff --git a/codec/src/test/java/io/netty/handler/codec/compression/ByteBufChecksumTest.java b/codec/src/test/java/io/netty/handler/codec/compression/ByteBufChecksumTest.java new file mode 100644 index 0000000..d2df38b --- /dev/null +++ b/codec/src/test/java/io/netty/handler/codec/compression/ByteBufChecksumTest.java @@ -0,0 +1,90 @@ +/* + * Copyright 2019 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.codec.compression; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import net.jpountz.xxhash.XXHashFactory; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.Random; +import java.util.zip.Adler32; +import java.util.zip.CRC32; +import java.util.zip.Checksum; + +import static io.netty.handler.codec.compression.Lz4Constants.DEFAULT_SEED; +import static org.junit.Assert.*; + +public class ByteBufChecksumTest { + + private static final byte[] BYTE_ARRAY = new byte[1024]; + + @BeforeClass + public static void setUp() { + new Random().nextBytes(BYTE_ARRAY); + } + + @Test + public void testHeapByteBufUpdate() { + testUpdate(Unpooled.wrappedBuffer(BYTE_ARRAY)); + } + + @Test + public void testDirectByteBufUpdate() { + ByteBuf buf = Unpooled.directBuffer(BYTE_ARRAY.length); + buf.writeBytes(BYTE_ARRAY); + testUpdate(buf); + } + + private static void testUpdate(ByteBuf buf) { + try { + // all variations of xxHash32: slow and naive, optimised, wrapped optimised; + // the last two should be literally identical, but it's best to guard against + // an accidental regression in ByteBufChecksum#wrapChecksum(Checksum) + testUpdate(xxHash32(DEFAULT_SEED), ByteBufChecksum.wrapChecksum(xxHash32(DEFAULT_SEED)), buf); + testUpdate(xxHash32(DEFAULT_SEED), new Lz4XXHash32(DEFAULT_SEED), buf); + testUpdate(xxHash32(DEFAULT_SEED), ByteBufChecksum.wrapChecksum(new Lz4XXHash32(DEFAULT_SEED)), buf); + + // CRC32 and Adler32, special-cased to use ReflectiveByteBufChecksum + testUpdate(new CRC32(), ByteBufChecksum.wrapChecksum(new CRC32()), buf); + testUpdate(new Adler32(), ByteBufChecksum.wrapChecksum(new Adler32()), buf); + } finally { + buf.release(); + } + } + + private static void testUpdate(Checksum checksum, ByteBufChecksum wrapped, ByteBuf buf) { + testUpdate(checksum, wrapped, buf, 0, BYTE_ARRAY.length); + testUpdate(checksum, wrapped, buf, 0, BYTE_ARRAY.length - 1); + testUpdate(checksum, wrapped, buf, 1, BYTE_ARRAY.length - 1); + testUpdate(checksum, wrapped, buf, 1, BYTE_ARRAY.length - 2); + } + + private static void testUpdate(Checksum checksum, ByteBufChecksum wrapped, ByteBuf buf, int off, int len) { + checksum.reset(); + wrapped.reset(); + + checksum.update(BYTE_ARRAY, off, len); + wrapped.update(buf, off, len); + + assertEquals(checksum.getValue(), wrapped.getValue()); + } + + private static Checksum xxHash32(int seed) { + return XXHashFactory.fastestInstance().newStreamingHash32(seed).asChecksum(); + } +} diff --git a/codec/src/test/java/io/netty/handler/codec/compression/JZlibTest.java b/codec/src/test/java/io/netty/handler/codec/compression/JZlibTest.java index 28f3919..015559e 100644 --- a/codec/src/test/java/io/netty/handler/codec/compression/JZlibTest.java +++ b/codec/src/test/java/io/netty/handler/codec/compression/JZlibTest.java @@ -23,7 +23,7 @@ public class JZlibTest extends ZlibTest { } @Override - protected ZlibDecoder createDecoder(ZlibWrapper wrapper) { - return new JZlibDecoder(wrapper); + protected ZlibDecoder createDecoder(ZlibWrapper wrapper, int maxAllocation) { + return new JZlibDecoder(wrapper, maxAllocation); } } diff --git a/codec/src/test/java/io/netty/handler/codec/compression/JdkZlibTest.java b/codec/src/test/java/io/netty/handler/codec/compression/JdkZlibTest.java index 54a48a9..5ff19f1 100644 --- a/codec/src/test/java/io/netty/handler/codec/compression/JdkZlibTest.java +++ b/codec/src/test/java/io/netty/handler/codec/compression/JdkZlibTest.java @@ -38,8 +38,8 @@ public class JdkZlibTest extends ZlibTest { } @Override - protected ZlibDecoder createDecoder(ZlibWrapper wrapper) { - return new JdkZlibDecoder(wrapper); + protected ZlibDecoder createDecoder(ZlibWrapper wrapper, int maxAllocation) { + return new JdkZlibDecoder(wrapper, maxAllocation); } @Test(expected = DecompressionException.class) diff --git a/codec/src/test/java/io/netty/handler/codec/compression/LengthAwareLzfIntegrationTest.java b/codec/src/test/java/io/netty/handler/codec/compression/LengthAwareLzfIntegrationTest.java new file mode 100644 index 0000000..4230447 --- /dev/null +++ b/codec/src/test/java/io/netty/handler/codec/compression/LengthAwareLzfIntegrationTest.java @@ -0,0 +1,28 @@ +/* + * Copyright 2020 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.codec.compression; + +import io.netty.channel.embedded.EmbeddedChannel; + +import static com.ning.compress.lzf.LZFChunk.MAX_CHUNK_LEN; + +public class LengthAwareLzfIntegrationTest extends LzfIntegrationTest { + + @Override + protected EmbeddedChannel createEncoder() { + return new EmbeddedChannel(new LzfEncoder(false, MAX_CHUNK_LEN, 2 * 1024 * 1024)); + } +} diff --git a/codec/src/test/java/io/netty/handler/codec/compression/SnappyFrameDecoderTest.java b/codec/src/test/java/io/netty/handler/codec/compression/SnappyFrameDecoderTest.java index 474a695..6b31076 100644 --- a/codec/src/test/java/io/netty/handler/codec/compression/SnappyFrameDecoderTest.java +++ b/codec/src/test/java/io/netty/handler/codec/compression/SnappyFrameDecoderTest.java @@ -18,6 +18,7 @@ package io.netty.handler.codec.compression; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.embedded.EmbeddedChannel; +import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -31,6 +32,11 @@ public class SnappyFrameDecoderTest { channel = new EmbeddedChannel(new SnappyFrameDecoder()); } + @After + public void tearDown() { + assertFalse(channel.finishAndReleaseAll()); + } + @Test(expected = DecompressionException.class) public void testReservedUnskippableChunkTypeCausesError() throws Exception { ByteBuf in = Unpooled.wrappedBuffer(new byte[] { @@ -92,7 +98,7 @@ public class SnappyFrameDecoderTest { -0x7f, 0x05, 0x00, 0x00, 'n', 'e', 't', 't', 'y' }); - channel.writeInbound(in); + assertFalse(channel.writeInbound(in)); assertNull(channel.readInbound()); assertFalse(in.isReadable()); @@ -105,7 +111,7 @@ public class SnappyFrameDecoderTest { 0x01, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 'n', 'e', 't', 't', 'y' }); - channel.writeInbound(in); + assertTrue(channel.writeInbound(in)); ByteBuf expected = Unpooled.wrappedBuffer(new byte[] { 'n', 'e', 't', 't', 'y' }); ByteBuf actual = channel.readInbound(); @@ -125,7 +131,7 @@ public class SnappyFrameDecoderTest { 0x6e, 0x65, 0x74, 0x74, 0x79 // "netty" }); - channel.writeInbound(in); + assertTrue(channel.writeInbound(in)); ByteBuf expected = Unpooled.wrappedBuffer(new byte[] { 'n', 'e', 't', 't', 'y' }); ByteBuf actual = channel.readInbound(); @@ -142,26 +148,38 @@ public class SnappyFrameDecoderTest { @Test(expected = DecompressionException.class) public void testInvalidChecksumThrowsException() throws Exception { EmbeddedChannel channel = new EmbeddedChannel(new SnappyFrameDecoder(true)); - - // checksum here is presented as 0 - ByteBuf in = Unpooled.wrappedBuffer(new byte[] { - (byte) 0xff, 0x06, 0x00, 0x00, 0x73, 0x4e, 0x61, 0x50, 0x70, 0x59, - 0x01, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 'n', 'e', 't', 't', 'y' - }); - - channel.writeInbound(in); + try { + // checksum here is presented as 0 + ByteBuf in = Unpooled.wrappedBuffer(new byte[]{ + (byte) 0xff, 0x06, 0x00, 0x00, 0x73, 0x4e, 0x61, 0x50, 0x70, 0x59, + 0x01, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 'n', 'e', 't', 't', 'y' + }); + + channel.writeInbound(in); + } finally { + channel.finishAndReleaseAll(); + } } @Test public void testInvalidChecksumDoesNotThrowException() throws Exception { EmbeddedChannel channel = new EmbeddedChannel(new SnappyFrameDecoder(true)); - - // checksum here is presented as a282986f (little endian) - ByteBuf in = Unpooled.wrappedBuffer(new byte[] { - (byte) 0xff, 0x06, 0x00, 0x00, 0x73, 0x4e, 0x61, 0x50, 0x70, 0x59, - 0x01, 0x09, 0x00, 0x00, 0x6f, -0x68, -0x7e, -0x5e, 'n', 'e', 't', 't', 'y' - }); - - channel.writeInbound(in); + try { + // checksum here is presented as a282986f (little endian) + ByteBuf in = Unpooled.wrappedBuffer(new byte[]{ + (byte) 0xff, 0x06, 0x00, 0x00, 0x73, 0x4e, 0x61, 0x50, 0x70, 0x59, + 0x01, 0x09, 0x00, 0x00, 0x6f, -0x68, 0x2e, -0x47, 'n', 'e', 't', 't', 'y' + }); + + assertTrue(channel.writeInbound(in)); + ByteBuf expected = Unpooled.wrappedBuffer(new byte[] { 'n', 'e', 't', 't', 'y' }); + ByteBuf actual = channel.readInbound(); + assertEquals(expected, actual); + + expected.release(); + actual.release(); + } finally { + channel.finishAndReleaseAll(); + } } } diff --git a/codec/src/test/java/io/netty/handler/codec/compression/SnappyFrameEncoderTest.java b/codec/src/test/java/io/netty/handler/codec/compression/SnappyFrameEncoderTest.java index 7b61be1..78a046a 100644 --- a/codec/src/test/java/io/netty/handler/codec/compression/SnappyFrameEncoderTest.java +++ b/codec/src/test/java/io/netty/handler/codec/compression/SnappyFrameEncoderTest.java @@ -40,10 +40,9 @@ public class SnappyFrameEncoderTest { channel.writeOutbound(in); assertTrue(channel.finish()); - ByteBuf expected = Unpooled.wrappedBuffer(new byte[] { (byte) 0xff, 0x06, 0x00, 0x00, 0x73, 0x4e, 0x61, 0x50, 0x70, 0x59, - 0x01, 0x09, 0x00, 0x00, 0x6f, -0x68, -0x7e, -0x5e, 'n', 'e', 't', 't', 'y' + 0x01, 0x09, 0x00, 0x00, 0x6f, -0x68, 0x2e, -0x47, 'n', 'e', 't', 't', 'y' }); ByteBuf actual = channel.readOutbound(); assertEquals(expected, actual); @@ -89,8 +88,8 @@ public class SnappyFrameEncoderTest { ByteBuf expected = Unpooled.wrappedBuffer(new byte[] { (byte) 0xff, 0x06, 0x00, 0x00, 0x73, 0x4e, 0x61, 0x50, 0x70, 0x59, - 0x01, 0x09, 0x00, 0x00, 0x6f, -0x68, -0x7e, -0x5e, 'n', 'e', 't', 't', 'y', - 0x01, 0x09, 0x00, 0x00, 0x6f, -0x68, -0x7e, -0x5e, 'n', 'e', 't', 't', 'y', + 0x01, 0x09, 0x00, 0x00, 0x6f, -0x68, 0x2e, -0x47, 'n', 'e', 't', 't', 'y', + 0x01, 0x09, 0x00, 0x00, 0x6f, -0x68, 0x2e, -0x47, 'n', 'e', 't', 't', 'y', }); CompositeByteBuf actual = Unpooled.compositeBuffer(); diff --git a/codec/src/test/java/io/netty/handler/codec/compression/SnappyTest.java b/codec/src/test/java/io/netty/handler/codec/compression/SnappyTest.java index 115deef..1f2206a 100644 --- a/codec/src/test/java/io/netty/handler/codec/compression/SnappyTest.java +++ b/codec/src/test/java/io/netty/handler/codec/compression/SnappyTest.java @@ -234,7 +234,19 @@ public class SnappyTest { ByteBuf input = Unpooled.wrappedBuffer(new byte[] { 'n', 'e', 't', 't', 'y' }); - assertEquals(maskChecksum(0xd6cb8b55), calculateChecksum(input)); + + assertEquals(maskChecksum(0xd6cb8b55L), calculateChecksum(input)); + input.release(); + } + + @Test + public void testMaskChecksum() { + ByteBuf input = Unpooled.wrappedBuffer(new byte[] { + 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, + 0x5f, 0x68, 0x65, 0x61, 0x72, 0x74, 0x62, 0x65, + 0x61, 0x74, 0x5f, + }); + assertEquals(0x44a4301f, calculateChecksum(input)); input.release(); } diff --git a/codec/src/test/java/io/netty/handler/codec/compression/ZlibCrossTest1.java b/codec/src/test/java/io/netty/handler/codec/compression/ZlibCrossTest1.java index 9e16e1a..3c31274 100644 --- a/codec/src/test/java/io/netty/handler/codec/compression/ZlibCrossTest1.java +++ b/codec/src/test/java/io/netty/handler/codec/compression/ZlibCrossTest1.java @@ -23,7 +23,7 @@ public class ZlibCrossTest1 extends ZlibTest { } @Override - protected ZlibDecoder createDecoder(ZlibWrapper wrapper) { - return new JZlibDecoder(wrapper); + protected ZlibDecoder createDecoder(ZlibWrapper wrapper, int maxAllocation) { + return new JZlibDecoder(wrapper, maxAllocation); } } diff --git a/codec/src/test/java/io/netty/handler/codec/compression/ZlibCrossTest2.java b/codec/src/test/java/io/netty/handler/codec/compression/ZlibCrossTest2.java index 8717019..00c6e18 100644 --- a/codec/src/test/java/io/netty/handler/codec/compression/ZlibCrossTest2.java +++ b/codec/src/test/java/io/netty/handler/codec/compression/ZlibCrossTest2.java @@ -25,8 +25,8 @@ public class ZlibCrossTest2 extends ZlibTest { } @Override - protected ZlibDecoder createDecoder(ZlibWrapper wrapper) { - return new JdkZlibDecoder(wrapper); + protected ZlibDecoder createDecoder(ZlibWrapper wrapper, int maxAllocation) { + return new JdkZlibDecoder(wrapper, maxAllocation); } @Test(expected = DecompressionException.class) diff --git a/codec/src/test/java/io/netty/handler/codec/compression/ZlibTest.java b/codec/src/test/java/io/netty/handler/codec/compression/ZlibTest.java index 7c25ec4..5e9d128 100644 --- a/codec/src/test/java/io/netty/handler/codec/compression/ZlibTest.java +++ b/codec/src/test/java/io/netty/handler/codec/compression/ZlibTest.java @@ -15,7 +15,9 @@ */ package io.netty.handler.codec.compression; +import io.netty.buffer.AbstractByteBufAllocator; import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.ByteBufInputStream; import io.netty.buffer.Unpooled; import io.netty.channel.embedded.EmbeddedChannel; @@ -88,8 +90,12 @@ public abstract class ZlibTest { rand.nextBytes(BYTES_LARGE); } + protected ZlibDecoder createDecoder(ZlibWrapper wrapper) { + return createDecoder(wrapper, 0); + } + protected abstract ZlibEncoder createEncoder(ZlibWrapper wrapper); - protected abstract ZlibDecoder createDecoder(ZlibWrapper wrapper); + protected abstract ZlibDecoder createDecoder(ZlibWrapper wrapper, int maxAllocation); @Test public void testGZIP2() throws Exception { @@ -223,7 +229,7 @@ public abstract class ZlibTest { // Test for https://github.com/netty/netty/issues/2572 private void testDecompressOnly(ZlibWrapper decoderWrapper, byte[] compressed, byte[] data) throws Exception { EmbeddedChannel chDecoder = new EmbeddedChannel(createDecoder(decoderWrapper)); - chDecoder.writeInbound(Unpooled.wrappedBuffer(compressed)); + chDecoder.writeInbound(Unpooled.copiedBuffer(compressed)); assertTrue(chDecoder.finish()); ByteBuf decoded = Unpooled.buffer(data.length); @@ -236,7 +242,7 @@ public abstract class ZlibTest { decoded.writeBytes(buf); buf.release(); } - assertEquals(Unpooled.wrappedBuffer(data), decoded); + assertEquals(Unpooled.copiedBuffer(data), decoded); decoded.release(); } @@ -345,6 +351,25 @@ public abstract class ZlibTest { testCompressLarge(ZlibWrapper.GZIP, ZlibWrapper.ZLIB_OR_NONE); } + @Test + public void testMaxAllocation() throws Exception { + int maxAllocation = 1024; + ZlibDecoder decoder = createDecoder(ZlibWrapper.ZLIB, maxAllocation); + EmbeddedChannel chDecoder = new EmbeddedChannel(decoder); + TestByteBufAllocator alloc = new TestByteBufAllocator(chDecoder.alloc()); + chDecoder.config().setAllocator(alloc); + + try { + chDecoder.writeInbound(Unpooled.wrappedBuffer(deflate(BYTES_LARGE))); + fail("decompressed size > maxAllocation, so should have thrown exception"); + } catch (DecompressionException e) { + assertTrue(e.getMessage().startsWith("Decompression buffer has reached maximum size")); + assertEquals(maxAllocation, alloc.getMaxAllocation()); + assertTrue(decoder.isClosed()); + assertFalse(chDecoder.finish()); + } + } + private static byte[] gzip(byte[] bytes) throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); GZIPOutputStream stream = new GZIPOutputStream(out); @@ -360,4 +385,34 @@ public abstract class ZlibTest { stream.close(); return out.toByteArray(); } + + private static final class TestByteBufAllocator extends AbstractByteBufAllocator { + private ByteBufAllocator wrapped; + private int maxAllocation; + + TestByteBufAllocator(ByteBufAllocator wrapped) { + this.wrapped = wrapped; + } + + public int getMaxAllocation() { + return maxAllocation; + } + + @Override + public boolean isDirectBufferPooled() { + return wrapped.isDirectBufferPooled(); + } + + @Override + protected ByteBuf newHeapBuffer(int initialCapacity, int maxCapacity) { + maxAllocation = Math.max(maxAllocation, maxCapacity); + return wrapped.heapBuffer(initialCapacity, maxCapacity); + } + + @Override + protected ByteBuf newDirectBuffer(int initialCapacity, int maxCapacity) { + maxAllocation = Math.max(maxAllocation, maxCapacity); + return wrapped.directBuffer(initialCapacity, maxCapacity); + } + } } diff --git a/codec/src/test/java/io/netty/handler/codec/serialization/CompatibleObjectEncoderTest.java b/codec/src/test/java/io/netty/handler/codec/serialization/CompatibleObjectEncoderTest.java index 238b91e..60142eb 100644 --- a/codec/src/test/java/io/netty/handler/codec/serialization/CompatibleObjectEncoderTest.java +++ b/codec/src/test/java/io/netty/handler/codec/serialization/CompatibleObjectEncoderTest.java @@ -26,7 +26,6 @@ import java.io.Serializable; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; public class CompatibleObjectEncoderTest { @Test diff --git a/codec/src/test/resources/io/netty/handler/codec/xml/sample-04.xml b/codec/src/test/resources/io/netty/handler/codec/xml/sample-04.xml index 8f50d30..4550d31 100644 --- a/codec/src/test/resources/io/netty/handler/codec/xml/sample-04.xml +++ b/codec/src/test/resources/io/netty/handler/codec/xml/sample-04.xml @@ -29,7 +29,7 @@ <version>4.0.14.Final-SNAPSHOT</version> <name>Netty</name> - <url>http://netty.io/</url> + <url>https://netty.io/</url> <description> Netty is an asynchronous event-driven network application framework for rapid development of maintainable high performance protocol servers and @@ -38,7 +38,7 @@ <organization> <name>The Netty Project</name> - <url>http://netty.io/</url> + <url>https://netty.io/</url> </organization> <licenses> @@ -61,9 +61,9 @@ <id>netty.io</id> <name>The Netty Project Contributors</name> <email>netty@googlegroups.com</email> - <url>http://netty.io/</url> + <url>https://netty.io/</url> <organization>The Netty Project</organization> - <organizationUrl>http://netty.io/</organizationUrl> + <organizationUrl>https://netty.io/</organizationUrl> </developer> </developers> diff --git a/common/pom.xml b/common/pom.xml index 3d8aa99..618d160 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -21,7 +21,7 @@ <parent> <groupId>io.netty</groupId> <artifactId>netty-parent</artifactId> - <version>4.1.33.Final</version> + <version>4.1.48.Final</version> </parent> <artifactId>netty-common</artifactId> @@ -38,6 +38,13 @@ </properties> <dependencies> + <dependency> + <groupId>com.oracle.substratevm</groupId> + <artifactId>svm</artifactId> + <version>${graalvm.version}</version> + <!-- Provided scope as it is only needed for compiling the SVM substitution classes --> + <scope>provided</scope> + </dependency> <dependency> <groupId>org.jctools</groupId> <artifactId>jctools-core</artifactId> @@ -71,6 +78,11 @@ <artifactId>log4j-core</artifactId> <scope>test</scope> </dependency> + <dependency> + <groupId>io.projectreactor.tools</groupId> + <artifactId>blockhound</artifactId> + <optional>true</optional> + </dependency> <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> @@ -109,6 +121,8 @@ </relocation> </relocations> <minimizeJar>true</minimizeJar> + <createSourcesJar>true</createSourcesJar> + <shadeSourcesContent>true</shadeSourcesContent> </configuration> </execution> </executions> diff --git a/common/src/main/java/io/netty/util/AbstractReferenceCounted.java b/common/src/main/java/io/netty/util/AbstractReferenceCounted.java index b7480c4..b5ffb4f 100644 --- a/common/src/main/java/io/netty/util/AbstractReferenceCounted.java +++ b/common/src/main/java/io/netty/util/AbstractReferenceCounted.java @@ -15,85 +15,55 @@ */ package io.netty.util; -import static io.netty.util.internal.ObjectUtil.checkPositive; - import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; -import io.netty.util.internal.PlatformDependent; +import io.netty.util.internal.ReferenceCountUpdater; /** * Abstract base class for classes wants to implement {@link ReferenceCounted}. */ public abstract class AbstractReferenceCounted implements ReferenceCounted { - private static final long REFCNT_FIELD_OFFSET; - private static final AtomicIntegerFieldUpdater<AbstractReferenceCounted> refCntUpdater = + private static final long REFCNT_FIELD_OFFSET = + ReferenceCountUpdater.getUnsafeOffset(AbstractReferenceCounted.class, "refCnt"); + private static final AtomicIntegerFieldUpdater<AbstractReferenceCounted> AIF_UPDATER = AtomicIntegerFieldUpdater.newUpdater(AbstractReferenceCounted.class, "refCnt"); - // even => "real" refcount is (refCnt >>> 1); odd => "real" refcount is 0 - @SuppressWarnings("unused") - private volatile int refCnt = 2; - - static { - long refCntFieldOffset = -1; - try { - if (PlatformDependent.hasUnsafe()) { - refCntFieldOffset = PlatformDependent.objectFieldOffset( - AbstractReferenceCounted.class.getDeclaredField("refCnt")); - } - } catch (Throwable ignore) { - refCntFieldOffset = -1; + private static final ReferenceCountUpdater<AbstractReferenceCounted> updater = + new ReferenceCountUpdater<AbstractReferenceCounted>() { + @Override + protected AtomicIntegerFieldUpdater<AbstractReferenceCounted> updater() { + return AIF_UPDATER; } + @Override + protected long unsafeOffset() { + return REFCNT_FIELD_OFFSET; + } + }; - REFCNT_FIELD_OFFSET = refCntFieldOffset; - } - - private static int realRefCnt(int rawCnt) { - return (rawCnt & 1) != 0 ? 0 : rawCnt >>> 1; - } - - private int nonVolatileRawCnt() { - // TODO: Once we compile against later versions of Java we can replace the Unsafe usage here by varhandles. - return REFCNT_FIELD_OFFSET != -1 ? PlatformDependent.getInt(this, REFCNT_FIELD_OFFSET) - : refCntUpdater.get(this); - } + // Value might not equal "real" reference count, all access should be via the updater + @SuppressWarnings("unused") + private volatile int refCnt = updater.initialValue(); @Override public int refCnt() { - return realRefCnt(refCntUpdater.get(this)); + return updater.refCnt(this); } /** * An unsafe operation intended for use by a subclass that sets the reference count of the buffer directly */ - protected final void setRefCnt(int newRefCnt) { - refCntUpdater.set(this, newRefCnt << 1); // overflow OK here + protected final void setRefCnt(int refCnt) { + updater.setRefCnt(this, refCnt); } @Override public ReferenceCounted retain() { - return retain0(1); + return updater.retain(this); } @Override public ReferenceCounted retain(int increment) { - return retain0(checkPositive(increment, "increment")); - } - - private ReferenceCounted retain0(final int increment) { - // all changes to the raw count are 2x the "real" change - int adjustedIncrement = increment << 1; // overflow OK here - int oldRef = refCntUpdater.getAndAdd(this, adjustedIncrement); - if ((oldRef & 1) != 0) { - throw new IllegalReferenceCountException(0, increment); - } - // don't pass 0! - if ((oldRef <= 0 && oldRef + adjustedIncrement >= 0) - || (oldRef >= 0 && oldRef + adjustedIncrement < oldRef)) { - // overflow case - refCntUpdater.getAndAdd(this, -adjustedIncrement); - throw new IllegalReferenceCountException(realRefCnt(oldRef), increment); - } - return this; + return updater.retain(this, increment); } @Override @@ -103,64 +73,19 @@ public abstract class AbstractReferenceCounted implements ReferenceCounted { @Override public boolean release() { - return release0(1); + return handleRelease(updater.release(this)); } @Override public boolean release(int decrement) { - return release0(checkPositive(decrement, "decrement")); - } - - private boolean release0(int decrement) { - int rawCnt = nonVolatileRawCnt(), realCnt = toLiveRealCnt(rawCnt, decrement); - if (decrement == realCnt) { - if (refCntUpdater.compareAndSet(this, rawCnt, 1)) { - deallocate(); - return true; - } - return retryRelease0(decrement); - } - return releaseNonFinal0(decrement, rawCnt, realCnt); - } - - private boolean releaseNonFinal0(int decrement, int rawCnt, int realCnt) { - if (decrement < realCnt - // all changes to the raw count are 2x the "real" change - && refCntUpdater.compareAndSet(this, rawCnt, rawCnt - (decrement << 1))) { - return false; - } - return retryRelease0(decrement); - } - - private boolean retryRelease0(int decrement) { - for (;;) { - int rawCnt = refCntUpdater.get(this), realCnt = toLiveRealCnt(rawCnt, decrement); - if (decrement == realCnt) { - if (refCntUpdater.compareAndSet(this, rawCnt, 1)) { - deallocate(); - return true; - } - } else if (decrement < realCnt) { - // all changes to the raw count are 2x the "real" change - if (refCntUpdater.compareAndSet(this, rawCnt, rawCnt - (decrement << 1))) { - return false; - } - } else { - throw new IllegalReferenceCountException(realCnt, -decrement); - } - Thread.yield(); // this benefits throughput under high contention - } + return handleRelease(updater.release(this, decrement)); } - /** - * Like {@link #realRefCnt(int)} but throws if refCnt == 0 - */ - private static int toLiveRealCnt(int rawCnt, int decrement) { - if ((rawCnt & 1) == 0) { - return rawCnt >>> 1; + private boolean handleRelease(boolean result) { + if (result) { + deallocate(); } - // odd rawCnt => already deallocated - throw new IllegalReferenceCountException(0, -decrement); + return result; } /** diff --git a/common/src/main/java/io/netty/util/AsciiString.java b/common/src/main/java/io/netty/util/AsciiString.java index f537469..0154137 100644 --- a/common/src/main/java/io/netty/util/AsciiString.java +++ b/common/src/main/java/io/netty/util/AsciiString.java @@ -17,6 +17,7 @@ package io.netty.util; import io.netty.util.internal.EmptyArrays; import io.netty.util.internal.InternalThreadLocalMap; +import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.PlatformDependent; import java.nio.ByteBuffer; @@ -477,7 +478,7 @@ public final class AsciiString implements CharSequence, Comparable<CharSequence> return this; } - if (string.getClass() == AsciiString.class) { + if (string instanceof AsciiString) { AsciiString that = (AsciiString) string; if (isEmpty()) { return that; @@ -522,13 +523,17 @@ public final class AsciiString implements CharSequence, Comparable<CharSequence> * @return {@code true} if the specified string is equal to this string, {@code false} otherwise. */ public boolean contentEqualsIgnoreCase(CharSequence string) { + if (this == string) { + return true; + } + if (string == null || string.length() != length()) { return false; } - if (string.getClass() == AsciiString.class) { + if (string instanceof AsciiString) { AsciiString rhs = (AsciiString) string; - for (int i = arrayOffset(), j = rhs.arrayOffset(); i < length(); ++i, ++j) { + for (int i = arrayOffset(), j = rhs.arrayOffset(), end = i + length(); i < end; ++i, ++j) { if (!equalsIgnoreCase(value[i], rhs.value[j])) { return false; } @@ -536,7 +541,7 @@ public final class AsciiString implements CharSequence, Comparable<CharSequence> return true; } - for (int i = arrayOffset(), j = 0; i < length(); ++i, ++j) { + for (int i = arrayOffset(), j = 0, end = length(); j < end; ++i, ++j) { if (!equalsIgnoreCase(b2c(value[i]), string.charAt(j))) { return false; } @@ -585,9 +590,7 @@ public final class AsciiString implements CharSequence, Comparable<CharSequence> * @param length the number of characters to copy. */ public void copy(int srcIdx, char[] dst, int dstIdx, int length) { - if (dst == null) { - throw new NullPointerException("dst"); - } + ObjectUtil.checkNotNull(dst, "dst"); if (isOutOfBounds(srcIdx, length, length())) { throw new IndexOutOfBoundsException("expected: " + "0 <= srcIdx(" + srcIdx + ") <= srcIdx + length(" @@ -742,7 +745,7 @@ public final class AsciiString implements CharSequence, Comparable<CharSequence> */ public int lastIndexOf(CharSequence string) { // Use count instead of count - 1 so lastIndexOf("") answers count - return lastIndexOf(string, length()); + return lastIndexOf(string, length); } /** @@ -757,23 +760,20 @@ public final class AsciiString implements CharSequence, Comparable<CharSequence> */ public int lastIndexOf(CharSequence subString, int start) { final int subCount = subString.length(); + start = Math.min(start, length - subCount); if (start < 0) { - start = 0; - } - if (subCount <= 0) { - return start < length ? start : length; - } - if (subCount > length - start) { return INDEX_NOT_FOUND; } + if (subCount == 0) { + return start; + } final char firstChar = subString.charAt(0); if (firstChar > MAX_CHAR_VALUE) { return INDEX_NOT_FOUND; } final byte firstCharAsByte = c2b0(firstChar); - final int end = offset + start; - for (int i = offset + length - subCount; i >= end; --i) { + for (int i = offset + start; i >= 0; --i) { if (value[i] == firstCharAsByte) { int o1 = i, o2 = 0; while (++o2 < subCount && b2c(value[++o1]) == subString.charAt(o2)) { @@ -799,9 +799,7 @@ public final class AsciiString implements CharSequence, Comparable<CharSequence> * @throws NullPointerException if {@code string} is {@code null}. */ public boolean regionMatches(int thisStart, CharSequence string, int start, int length) { - if (string == null) { - throw new NullPointerException("string"); - } + ObjectUtil.checkNotNull(string, "string"); if (start < 0 || string.length() - start < length) { return false; @@ -842,9 +840,7 @@ public final class AsciiString implements CharSequence, Comparable<CharSequence> return regionMatches(thisStart, string, start, length); } - if (string == null) { - throw new NullPointerException("string"); - } + ObjectUtil.checkNotNull(string, "string"); final int thisLen = length(); if (thisStart < 0 || length > thisLen - thisStart) { @@ -988,7 +984,7 @@ public final class AsciiString implements CharSequence, Comparable<CharSequence> * @return a new string with characters {@code <= \\u0020} removed from the beginning and the end. */ public static CharSequence trim(CharSequence c) { - if (c.getClass() == AsciiString.class) { + if (c instanceof AsciiString) { return ((AsciiString) c).trim(); } if (c instanceof String) { @@ -1036,10 +1032,14 @@ public final class AsciiString implements CharSequence, Comparable<CharSequence> * @return {@code true} if equal, otherwise {@code false} */ public boolean contentEquals(CharSequence a) { + if (this == a) { + return true; + } + if (a == null || a.length() != length()) { return false; } - if (a.getClass() == AsciiString.class) { + if (a instanceof AsciiString) { return equals(a); } @@ -1388,7 +1388,7 @@ public final class AsciiString implements CharSequence, Comparable<CharSequence> * {@link AsciiString}, just returns the same instance. */ public static AsciiString of(CharSequence string) { - return string.getClass() == AsciiString.class ? (AsciiString) string : new AsciiString(string); + return string instanceof AsciiString ? (AsciiString) string : new AsciiString(string); } /** @@ -1412,7 +1412,7 @@ public final class AsciiString implements CharSequence, Comparable<CharSequence> if (value == null) { return 0; } - if (value.getClass() == AsciiString.class) { + if (value instanceof AsciiString) { return value.hashCode(); } @@ -1442,10 +1442,10 @@ public final class AsciiString implements CharSequence, Comparable<CharSequence> return a == b; } - if (a.getClass() == AsciiString.class) { + if (a instanceof AsciiString) { return ((AsciiString) a).contentEqualsIgnoreCase(b); } - if (b.getClass() == AsciiString.class) { + if (b instanceof AsciiString) { return ((AsciiString) b).contentEqualsIgnoreCase(a); } @@ -1504,11 +1504,11 @@ public final class AsciiString implements CharSequence, Comparable<CharSequence> return a == b; } - if (a.getClass() == AsciiString.class) { + if (a instanceof AsciiString) { return ((AsciiString) a).contentEquals(b); } - if (b.getClass() == AsciiString.class) { + if (b instanceof AsciiString) { return ((AsciiString) b).contentEquals(a); } @@ -1827,7 +1827,13 @@ public final class AsciiString implements CharSequence, Comparable<CharSequence> return isUpperCase(b) ? (byte) (b + 32) : b; } - private static char toLowerCase(char c) { + /** + * If the character is uppercase - converts the character to lowercase, + * otherwise returns the character as it is. Only for ASCII characters. + * + * @return lowercase ASCII character equivalent + */ + public static char toLowerCase(char c) { return isUpperCase(c) ? (char) (c + 32) : c; } diff --git a/common/src/main/java/io/netty/util/AttributeMap.java b/common/src/main/java/io/netty/util/AttributeMap.java index 826e695..06eba74 100644 --- a/common/src/main/java/io/netty/util/AttributeMap.java +++ b/common/src/main/java/io/netty/util/AttributeMap.java @@ -28,7 +28,7 @@ public interface AttributeMap { <T> Attribute<T> attr(AttributeKey<T> key); /** - * Returns {@code} true if and only if the given {@link Attribute} exists in this {@link AttributeMap}. + * Returns {@code true} if and only if the given {@link Attribute} exists in this {@link AttributeMap}. */ <T> boolean hasAttr(AttributeKey<T> key); } diff --git a/common/src/main/java/io/netty/util/CharsetUtil.java b/common/src/main/java/io/netty/util/CharsetUtil.java index 4d71b0a..a9317e5 100644 --- a/common/src/main/java/io/netty/util/CharsetUtil.java +++ b/common/src/main/java/io/netty/util/CharsetUtil.java @@ -65,7 +65,9 @@ public final class CharsetUtil { private static final Charset[] CHARSETS = new Charset[] { UTF_16, UTF_16BE, UTF_16LE, UTF_8, ISO_8859_1, US_ASCII }; - public static Charset[] values() { return CHARSETS; } + public static Charset[] values() { + return CHARSETS; + } /** * @deprecated Use {@link #encoder(Charset)}. diff --git a/common/src/main/java/io/netty/util/ConstantPool.java b/common/src/main/java/io/netty/util/ConstantPool.java index a05ab7e..e0bb5d6 100644 --- a/common/src/main/java/io/netty/util/ConstantPool.java +++ b/common/src/main/java/io/netty/util/ConstantPool.java @@ -37,14 +37,10 @@ public abstract class ConstantPool<T extends Constant<T>> { * Shortcut of {@link #valueOf(String) valueOf(firstNameComponent.getName() + "#" + secondNameComponent)}. */ public T valueOf(Class<?> firstNameComponent, String secondNameComponent) { - if (firstNameComponent == null) { - throw new NullPointerException("firstNameComponent"); - } - if (secondNameComponent == null) { - throw new NullPointerException("secondNameComponent"); - } - - return valueOf(firstNameComponent.getName() + '#' + secondNameComponent); + return valueOf( + ObjectUtil.checkNotNull(firstNameComponent, "firstNameComponent").getName() + + '#' + + ObjectUtil.checkNotNull(secondNameComponent, "secondNameComponent")); } /** diff --git a/common/src/main/java/io/netty/util/DefaultAttributeMap.java b/common/src/main/java/io/netty/util/DefaultAttributeMap.java index c685e4d..8624620 100644 --- a/common/src/main/java/io/netty/util/DefaultAttributeMap.java +++ b/common/src/main/java/io/netty/util/DefaultAttributeMap.java @@ -15,6 +15,8 @@ */ package io.netty.util; +import io.netty.util.internal.ObjectUtil; + import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReferenceArray; import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; @@ -39,9 +41,7 @@ public class DefaultAttributeMap implements AttributeMap { @SuppressWarnings("unchecked") @Override public <T> Attribute<T> attr(AttributeKey<T> key) { - if (key == null) { - throw new NullPointerException("key"); - } + ObjectUtil.checkNotNull(key, "key"); AtomicReferenceArray<DefaultAttribute<?>> attributes = this.attributes; if (attributes == null) { // Not using ConcurrentHashMap due to high memory consumption. @@ -90,9 +90,7 @@ public class DefaultAttributeMap implements AttributeMap { @Override public <T> boolean hasAttr(AttributeKey<T> key) { - if (key == null) { - throw new NullPointerException("key"); - } + ObjectUtil.checkNotNull(key, "key"); AtomicReferenceArray<DefaultAttribute<?>> attributes = this.attributes; if (attributes == null) { // no attribute exists diff --git a/common/src/main/java/io/netty/util/HashedWheelTimer.java b/common/src/main/java/io/netty/util/HashedWheelTimer.java index e8e72b6..c19dfe2 100644 --- a/common/src/main/java/io/netty/util/HashedWheelTimer.java +++ b/common/src/main/java/io/netty/util/HashedWheelTimer.java @@ -15,6 +15,7 @@ */ package io.netty.util; +import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.PlatformDependent; import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; @@ -242,18 +243,10 @@ public class HashedWheelTimer implements Timer { long tickDuration, TimeUnit unit, int ticksPerWheel, boolean leakDetection, long maxPendingTimeouts) { - if (threadFactory == null) { - throw new NullPointerException("threadFactory"); - } - if (unit == null) { - throw new NullPointerException("unit"); - } - if (tickDuration <= 0) { - throw new IllegalArgumentException("tickDuration must be greater than 0: " + tickDuration); - } - if (ticksPerWheel <= 0) { - throw new IllegalArgumentException("ticksPerWheel must be greater than 0: " + ticksPerWheel); - } + ObjectUtil.checkNotNull(threadFactory, "threadFactory"); + ObjectUtil.checkNotNull(unit, "unit"); + ObjectUtil.checkPositive(tickDuration, "tickDuration"); + ObjectUtil.checkPositive(ticksPerWheel, "ticksPerWheel"); // Normalize ticksPerWheel to power of two and initialize the wheel. wheel = createWheel(ticksPerWheel); @@ -270,10 +263,8 @@ public class HashedWheelTimer implements Timer { } if (duration < MILLISECOND_NANOS) { - if (logger.isWarnEnabled()) { - logger.warn("Configured tickDuration %d smaller then %d, using 1ms.", - tickDuration, MILLISECOND_NANOS); - } + logger.warn("Configured tickDuration {} smaller then {}, using 1ms.", + tickDuration, MILLISECOND_NANOS); this.tickDuration = MILLISECOND_NANOS; } else { this.tickDuration = duration; @@ -410,12 +401,8 @@ public class HashedWheelTimer implements Timer { @Override public Timeout newTimeout(TimerTask task, long delay, TimeUnit unit) { - if (task == null) { - throw new NullPointerException("task"); - } - if (unit == null) { - throw new NullPointerException("unit"); - } + ObjectUtil.checkNotNull(task, "task"); + ObjectUtil.checkNotNull(unit, "unit"); long pendingTimeoutsCount = pendingTimeouts.incrementAndGet(); @@ -573,6 +560,9 @@ public class HashedWheelTimer implements Timer { // See https://github.com/netty/netty/issues/356 if (PlatformDependent.isWindows()) { sleepTimeMs = sleepTimeMs / 10 * 10; + if (sleepTimeMs == 0) { + sleepTimeMs = 1; + } } try { diff --git a/common/src/main/java/io/netty/util/NetUtil.java b/common/src/main/java/io/netty/util/NetUtil.java index 088889d..8634351 100644 --- a/common/src/main/java/io/netty/util/NetUtil.java +++ b/common/src/main/java/io/netty/util/NetUtil.java @@ -291,7 +291,10 @@ public final class NetUtil { } } } catch (Exception e) { - logger.debug("Failed to get SOMAXCONN from sysctl and file {}. Default: {}", file, somaxconn, e); + if (logger.isDebugEnabled()) { + logger.debug("Failed to get SOMAXCONN from sysctl and file {}. Default: {}", + file, somaxconn, e); + } } finally { if (in != null) { try { @@ -320,10 +323,10 @@ public final class NetUtil { BufferedReader br = new BufferedReader(isr); try { String line = br.readLine(); - if (line.startsWith(sysctlKey)) { + if (line != null && line.startsWith(sysctlKey)) { for (int i = line.length() - 1; i > sysctlKey.length(); --i) { if (!Character.isDigit(line.charAt(i))) { - return Integer.valueOf(line.substring(i + 1, line.length())); + return Integer.valueOf(line.substring(i + 1)); } } } diff --git a/common/src/main/java/io/netty/util/Recycler.java b/common/src/main/java/io/netty/util/Recycler.java index 87b182e..ae2328e 100644 --- a/common/src/main/java/io/netty/util/Recycler.java +++ b/common/src/main/java/io/netty/util/Recycler.java @@ -17,6 +17,7 @@ package io.netty.util; import io.netty.util.concurrent.FastThreadLocal; +import io.netty.util.internal.ObjectPool; import io.netty.util.internal.SystemPropertyUtil; import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; @@ -105,14 +106,14 @@ public abstract class Recycler<T> { private final int maxCapacityPerThread; private final int maxSharedCapacityFactor; - private final int ratioMask; + private final int interval; private final int maxDelayedQueuesPerThread; private final FastThreadLocal<Stack<T>> threadLocal = new FastThreadLocal<Stack<T>>() { @Override protected Stack<T> initialValue() { return new Stack<T>(Recycler.this, Thread.currentThread(), maxCapacityPerThread, maxSharedCapacityFactor, - ratioMask, maxDelayedQueuesPerThread); + interval, maxDelayedQueuesPerThread); } @Override @@ -140,7 +141,7 @@ public abstract class Recycler<T> { protected Recycler(int maxCapacityPerThread, int maxSharedCapacityFactor, int ratio, int maxDelayedQueuesPerThread) { - ratioMask = safeFindNextPositivePowerOfTwo(ratio) - 1; + interval = safeFindNextPositivePowerOfTwo(ratio); if (maxCapacityPerThread <= 0) { this.maxCapacityPerThread = 0; this.maxSharedCapacityFactor = 1; @@ -194,18 +195,16 @@ public abstract class Recycler<T> { protected abstract T newObject(Handle<T> handle); - public interface Handle<T> { - void recycle(T object); - } + public interface Handle<T> extends ObjectPool.Handle<T> { } - static final class DefaultHandle<T> implements Handle<T> { - private int lastRecycledId; - private int recycleId; + private static final class DefaultHandle<T> implements Handle<T> { + int lastRecycledId; + int recycleId; boolean hasBeenRecycled; - private Stack<?> stack; - private Object value; + Stack<?> stack; + Object value; DefaultHandle(Stack<?> stack) { this.stack = stack; @@ -236,22 +235,21 @@ public abstract class Recycler<T> { // a queue that makes only moderate guarantees about visibility: items are seen in the correct order, // but we aren't absolutely guaranteed to ever see anything at all, thereby keeping the queue cheap to maintain - private static final class WeakOrderQueue { + private static final class WeakOrderQueue extends WeakReference<Thread> { static final WeakOrderQueue DUMMY = new WeakOrderQueue(); // Let Link extend AtomicInteger for intrinsics. The Link itself will be used as writerIndex. @SuppressWarnings("serial") static final class Link extends AtomicInteger { - private final DefaultHandle<?>[] elements = new DefaultHandle[LINK_CAPACITY]; + final DefaultHandle<?>[] elements = new DefaultHandle[LINK_CAPACITY]; - private int readIndex; + int readIndex; Link next; } - // This act as a place holder for the head Link but also will reclaim space once finalized. // Its important this does not hold any reference to either Stack or WeakOrderQueue. - static final class Head { + private static final class Head { private final AtomicInteger availableSharedCapacity; Link link; @@ -260,41 +258,49 @@ public abstract class Recycler<T> { this.availableSharedCapacity = availableSharedCapacity; } - /// TODO: In the future when we move to Java9+ we should use java.lang.ref.Cleaner. - @Override - protected void finalize() throws Throwable { - try { - super.finalize(); - } finally { - Link head = link; - link = null; - while (head != null) { - reclaimSpace(LINK_CAPACITY); - Link next = head.next; - // Unlink to help GC and guard against GC nepotism. - head.next = null; - head = next; - } + /** + * Reclaim all used space and also unlink the nodes to prevent GC nepotism. + */ + void reclaimAllSpaceAndUnlink() { + Link head = link; + link = null; + int reclaimSpace = 0; + while (head != null) { + reclaimSpace += LINK_CAPACITY; + Link next = head.next; + // Unlink to help GC and guard against GC nepotism. + head.next = null; + head = next; + } + if (reclaimSpace > 0) { + reclaimSpace(reclaimSpace); } } - void reclaimSpace(int space) { - assert space >= 0; + private void reclaimSpace(int space) { availableSharedCapacity.addAndGet(space); } - boolean reserveSpace(int space) { - return reserveSpace(availableSharedCapacity, space); + void relink(Link link) { + reclaimSpace(LINK_CAPACITY); + this.link = link; + } + + /** + * Creates a new {@link} and returns it if we can reserve enough space for it, otherwise it + * returns {@code null}. + */ + Link newLink() { + return reserveSpaceForLink(availableSharedCapacity) ? new Link() : null; } - static boolean reserveSpace(AtomicInteger availableSharedCapacity, int space) { - assert space >= 0; + static boolean reserveSpaceForLink(AtomicInteger availableSharedCapacity) { for (;;) { int available = availableSharedCapacity.get(); - if (available < space) { + if (available < LINK_CAPACITY) { return false; } - if (availableSharedCapacity.compareAndSet(available, available - space)) { + if (availableSharedCapacity.compareAndSet(available, available - LINK_CAPACITY)) { return true; } } @@ -306,15 +312,18 @@ public abstract class Recycler<T> { private Link tail; // pointer to another queue of delayed items for the same stack private WeakOrderQueue next; - private final WeakReference<Thread> owner; private final int id = ID_GENERATOR.getAndIncrement(); + private final int interval; + private int handleRecycleCount; private WeakOrderQueue() { - owner = null; + super(null); head = new Head(null); + interval = 0; } private WeakOrderQueue(Stack<?> stack, Thread thread) { + super(thread); tail = new Link(); // Its important that we not store the Stack itself in the WeakOrderQueue as the Stack also is used in @@ -322,10 +331,15 @@ public abstract class Recycler<T> { // Stack itself GCed. head = new Head(stack.availableSharedCapacity); head.link = tail; - owner = new WeakReference<Thread>(thread); + interval = stack.interval; + handleRecycleCount = interval; // Start at interval so the first one will be recycled. } static WeakOrderQueue newQueue(Stack<?> stack, Thread thread) { + // We allocated a Link so reserve the space + if (!Head.reserveSpaceForLink(stack.availableSharedCapacity)) { + return null; + } final WeakOrderQueue queue = new WeakOrderQueue(stack, thread); // Done outside of the constructor to ensure WeakOrderQueue.this does not escape the constructor and so // may be accessed while its still constructed. @@ -334,32 +348,43 @@ public abstract class Recycler<T> { return queue; } - private void setNext(WeakOrderQueue next) { + WeakOrderQueue getNext() { + return next; + } + + void setNext(WeakOrderQueue next) { assert next != this; this.next = next; } - /** - * Allocate a new {@link WeakOrderQueue} or return {@code null} if not possible. - */ - static WeakOrderQueue allocate(Stack<?> stack, Thread thread) { - // We allocated a Link so reserve the space - return Head.reserveSpace(stack.availableSharedCapacity, LINK_CAPACITY) - ? newQueue(stack, thread) : null; + void reclaimAllSpaceAndUnlink() { + head.reclaimAllSpaceAndUnlink(); + this.next = null; } void add(DefaultHandle<?> handle) { handle.lastRecycledId = id; + // While we also enforce the recycling ratio one we transfer objects from the WeakOrderQueue to the Stack + // we better should enforce it as well early. Missing to do so may let the WeakOrderQueue grow very fast + // without control if the Stack + if (handleRecycleCount < interval) { + handleRecycleCount++; + // Drop the item to prevent recycling to aggressive. + return; + } + handleRecycleCount = 0; + Link tail = this.tail; int writeIndex; if ((writeIndex = tail.get()) == LINK_CAPACITY) { - if (!head.reserveSpace(LINK_CAPACITY)) { + Link link = head.newLink(); + if (link == null) { // Drop it. return; } // We allocate a Link so reserve the space - this.tail = tail = tail.next = new Link(); + this.tail = tail = tail.next = link; writeIndex = tail.get(); } @@ -386,7 +411,8 @@ public abstract class Recycler<T> { if (head.next == null) { return false; } - this.head.link = head = head.next; + head = head.next; + this.head.relink(head); } final int srcStart = head.readIndex; @@ -409,7 +435,7 @@ public abstract class Recycler<T> { final DefaultHandle[] dstElems = dst.elements; int newDstSize = dstSize; for (int i = srcStart; i < srcEnd; i++) { - DefaultHandle element = srcElems[i]; + DefaultHandle<?> element = srcElems[i]; if (element.recycleId == 0) { element.recycleId = element.lastRecycledId; } else if (element.recycleId != element.lastRecycledId) { @@ -427,8 +453,7 @@ public abstract class Recycler<T> { if (srcEnd == LINK_CAPACITY && head.next != null) { // Add capacity back as the Link is GCed. - this.head.reclaimSpace(LINK_CAPACITY); - this.head.link = head.next; + this.head.relink(head.next); } head.readIndex = srcEnd; @@ -444,7 +469,7 @@ public abstract class Recycler<T> { } } - static final class Stack<T> { + private static final class Stack<T> { // we keep a queue of per-thread queues, which is appended to once only, each time a new thread other // than the stack owner recycles: when we run out of items in our stack we iterate this collection @@ -460,24 +485,25 @@ public abstract class Recycler<T> { // it in a timely manner). final WeakReference<Thread> threadRef; final AtomicInteger availableSharedCapacity; - final int maxDelayedQueues; + private final int maxDelayedQueues; private final int maxCapacity; - private final int ratioMask; - private DefaultHandle<?>[] elements; - private int size; - private int handleRecycleCount = -1; // Start with -1 so the first one will be recycled. + private final int interval; + DefaultHandle<?>[] elements; + int size; + private int handleRecycleCount; private WeakOrderQueue cursor, prev; private volatile WeakOrderQueue head; Stack(Recycler<T> parent, Thread thread, int maxCapacity, int maxSharedCapacityFactor, - int ratioMask, int maxDelayedQueues) { + int interval, int maxDelayedQueues) { this.parent = parent; threadRef = new WeakReference<Thread>(thread); this.maxCapacity = maxCapacity; availableSharedCapacity = new AtomicInteger(max(maxCapacity / maxSharedCapacityFactor, LINK_CAPACITY)); elements = new DefaultHandle[min(INITIAL_CAPACITY, maxCapacity)]; - this.ratioMask = ratioMask; + this.interval = interval; + handleRecycleCount = interval; // Start at interval so the first one will be recycled. this.maxDelayedQueues = maxDelayedQueues; } @@ -510,20 +536,28 @@ public abstract class Recycler<T> { return null; } size = this.size; + if (size <= 0) { + // double check, avoid races + return null; + } } size --; DefaultHandle ret = elements[size]; elements[size] = null; + // As we already set the element[size] to null we also need to store the updated size before we do + // any validation. Otherwise we may see a null value when later try to pop again without a new element + // added before. + this.size = size; + if (ret.lastRecycledId != ret.recycleId) { throw new IllegalStateException("recycled multiple times"); } ret.recycleId = 0; ret.lastRecycledId = 0; - this.size = size; return ret; } - boolean scavenge() { + private boolean scavenge() { // continue an existing scavenge, if any if (scavengeSome()) { return true; @@ -535,7 +569,7 @@ public abstract class Recycler<T> { return false; } - boolean scavengeSome() { + private boolean scavengeSome() { WeakOrderQueue prev; WeakOrderQueue cursor = this.cursor; if (cursor == null) { @@ -554,8 +588,8 @@ public abstract class Recycler<T> { success = true; break; } - WeakOrderQueue next = cursor.next; - if (cursor.owner.get() == null) { + WeakOrderQueue next = cursor.getNext(); + if (cursor.get() == null) { // If the thread associated with the queue is gone, unlink it, after // performing a volatile read to confirm there is no data left to collect. // We never unlink the first queue, as we don't want to synchronize on updating the head. @@ -570,6 +604,8 @@ public abstract class Recycler<T> { } if (prev != null) { + // Ensure we reclaim all space before dropping the WeakOrderQueue to be GC'ed. + cursor.reclaimAllSpaceAndUnlink(); prev.setNext(next); } } else { @@ -618,6 +654,11 @@ public abstract class Recycler<T> { } private void pushLater(DefaultHandle<?> item, Thread thread) { + if (maxDelayedQueues == 0) { + // We don't support recycling across threads and should just drop the item on the floor. + return; + } + // we don't want to have a ref to the queue as the value in our weak map // so we null it out; to ensure there are no races with restoring it later // we impose a memory ordering here (no-op on x86) @@ -630,7 +671,7 @@ public abstract class Recycler<T> { return; } // Check if we already reached the maximum number of delayed queues and if we can allocate at all. - if ((queue = WeakOrderQueue.allocate(this, thread)) == null) { + if ((queue = newWeakOrderQueue(thread)) == null) { // drop object return; } @@ -643,12 +684,21 @@ public abstract class Recycler<T> { queue.add(item); } + /** + * Allocate a new {@link WeakOrderQueue} or return {@code null} if not possible. + */ + private WeakOrderQueue newWeakOrderQueue(Thread thread) { + return WeakOrderQueue.newQueue(this, thread); + } + boolean dropHandle(DefaultHandle<?> handle) { if (!handle.hasBeenRecycled) { - if ((++handleRecycleCount & ratioMask) != 0) { + if (handleRecycleCount < interval) { + handleRecycleCount++; // Drop the object. return true; } + handleRecycleCount = 0; handle.hasBeenRecycled = true; } return false; diff --git a/common/src/main/java/io/netty/util/ResourceLeakDetector.java b/common/src/main/java/io/netty/util/ResourceLeakDetector.java index 436d793..5b9dd16 100644 --- a/common/src/main/java/io/netty/util/ResourceLeakDetector.java +++ b/common/src/main/java/io/netty/util/ResourceLeakDetector.java @@ -17,6 +17,7 @@ package io.netty.util; import io.netty.util.internal.EmptyArrays; +import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.PlatformDependent; import io.netty.util.internal.SystemPropertyUtil; import io.netty.util.internal.logging.InternalLogger; @@ -150,10 +151,7 @@ public class ResourceLeakDetector<T> { * Sets the resource leak detection level. */ public static void setLevel(Level level) { - if (level == null) { - throw new NullPointerException("level"); - } - ResourceLeakDetector.level = level; + ResourceLeakDetector.level = ObjectUtil.checkNotNull(level, "level"); } /** @@ -168,7 +166,8 @@ public class ResourceLeakDetector<T> { Collections.newSetFromMap(new ConcurrentHashMap<DefaultResourceLeak<?>, Boolean>()); private final ReferenceQueue<Object> refQueue = new ReferenceQueue<Object>(); - private final ConcurrentMap<String, Boolean> reportedLeaks = PlatformDependent.newConcurrentHashMap(); + private final Set<String> reportedLeaks = + Collections.newSetFromMap(new ConcurrentHashMap<String, Boolean>()); private final String resourceType; private final int samplingInterval; @@ -220,11 +219,7 @@ public class ResourceLeakDetector<T> { */ @Deprecated public ResourceLeakDetector(String resourceType, int samplingInterval, long maxActive) { - if (resourceType == null) { - throw new NullPointerException("resourceType"); - } - - this.resourceType = resourceType; + this.resourceType = ObjectUtil.checkNotNull(resourceType, "resourceType"); this.samplingInterval = samplingInterval; } @@ -271,7 +266,6 @@ public class ResourceLeakDetector<T> { private void clearRefQueue() { for (;;) { - @SuppressWarnings("unchecked") DefaultResourceLeak ref = (DefaultResourceLeak) refQueue.poll(); if (ref == null) { break; @@ -280,15 +274,24 @@ public class ResourceLeakDetector<T> { } } + /** + * When the return value is {@code true}, {@link #reportTracedLeak} and {@link #reportUntracedLeak} + * will be called once a leak is detected, otherwise not. + * + * @return {@code true} to enable leak reporting. + */ + protected boolean needReport() { + return logger.isErrorEnabled(); + } + private void reportLeak() { - if (!logger.isErrorEnabled()) { + if (!needReport()) { clearRefQueue(); return; } // Detect and report previous leaks. for (;;) { - @SuppressWarnings("unchecked") DefaultResourceLeak ref = (DefaultResourceLeak) refQueue.poll(); if (ref == null) { break; @@ -299,7 +302,7 @@ public class ResourceLeakDetector<T> { } String records = ref.toString(); - if (reportedLeaks.putIfAbsent(records, Boolean.TRUE) == null) { + if (reportedLeaks.add(records)) { if (records.isEmpty()) { reportUntracedLeak(resourceType); } else { @@ -316,7 +319,7 @@ public class ResourceLeakDetector<T> { protected void reportTracedLeak(String resourceType, String records) { logger.error( "LEAK: {}.release() was not called before it's garbage-collected. " + - "See http://netty.io/wiki/reference-counted-objects.html for more information.{}", + "See https://netty.io/wiki/reference-counted-objects.html for more information.{}", resourceType, records); } @@ -329,7 +332,7 @@ public class ResourceLeakDetector<T> { "Enable advanced leak reporting to find out where the leak occurred. " + "To enable advanced leak reporting, " + "specify the JVM option '-D{}={}' or call {}.setLevel() " + - "See http://netty.io/wiki/reference-counted-objects.html for more information.", + "See https://netty.io/wiki/reference-counted-objects.html for more information.", resourceType, PROP_LEVEL, Level.ADVANCED.name().toLowerCase(), simpleClassName(this)); } @@ -498,8 +501,9 @@ public class ResourceLeakDetector<T> { */ private static void reachabilityFence0(Object ref) { if (ref != null) { - // Empty synchronized is ok: https://stackoverflow.com/a/31933260/1151521 - synchronized (ref) { } + synchronized (ref) { + // Empty synchronized is ok: https://stackoverflow.com/a/31933260/1151521 + } } } diff --git a/common/src/main/java/io/netty/util/ResourceLeakDetectorFactory.java b/common/src/main/java/io/netty/util/ResourceLeakDetectorFactory.java index 65f1fc8..60c016d 100644 --- a/common/src/main/java/io/netty/util/ResourceLeakDetectorFactory.java +++ b/common/src/main/java/io/netty/util/ResourceLeakDetectorFactory.java @@ -23,8 +23,6 @@ import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; import java.lang.reflect.Constructor; -import java.security.AccessController; -import java.security.PrivilegedAction; /** * This static factory should be used to load {@link ResourceLeakDetector}s as needed @@ -103,12 +101,7 @@ public abstract class ResourceLeakDetectorFactory { DefaultResourceLeakDetectorFactory() { String customLeakDetector; try { - customLeakDetector = AccessController.doPrivileged(new PrivilegedAction<String>() { - @Override - public String run() { - return SystemPropertyUtil.get("io.netty.customResourceLeakDetector"); - } - }); + customLeakDetector = SystemPropertyUtil.get("io.netty.customResourceLeakDetector"); } catch (Throwable cause) { logger.error("Could not access System property: io.netty.customResourceLeakDetector", cause); customLeakDetector = null; diff --git a/common/src/main/java/io/netty/util/ThreadDeathWatcher.java b/common/src/main/java/io/netty/util/ThreadDeathWatcher.java index 8e755ed..7cc44e6 100644 --- a/common/src/main/java/io/netty/util/ThreadDeathWatcher.java +++ b/common/src/main/java/io/netty/util/ThreadDeathWatcher.java @@ -17,6 +17,7 @@ package io.netty.util; import io.netty.util.concurrent.DefaultThreadFactory; +import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.StringUtil; import io.netty.util.internal.SystemPropertyUtil; import io.netty.util.internal.logging.InternalLogger; @@ -77,12 +78,9 @@ public final class ThreadDeathWatcher { * @throws IllegalArgumentException if the specified {@code thread} is not alive */ public static void watch(Thread thread, Runnable task) { - if (thread == null) { - throw new NullPointerException("thread"); - } - if (task == null) { - throw new NullPointerException("task"); - } + ObjectUtil.checkNotNull(thread, "thread"); + ObjectUtil.checkNotNull(task, "task"); + if (!thread.isAlive()) { throw new IllegalArgumentException("thread must be alive."); } @@ -94,14 +92,9 @@ public final class ThreadDeathWatcher { * Cancels the task scheduled via {@link #watch(Thread, Runnable)}. */ public static void unwatch(Thread thread, Runnable task) { - if (thread == null) { - throw new NullPointerException("thread"); - } - if (task == null) { - throw new NullPointerException("task"); - } - - schedule(thread, task, false); + schedule(ObjectUtil.checkNotNull(thread, "thread"), + ObjectUtil.checkNotNull(task, "task"), + false); } private static void schedule(Thread thread, Runnable task, boolean isWatch) { @@ -137,9 +130,7 @@ public final class ThreadDeathWatcher { * @return {@code true} if and only if the watcher thread has been terminated */ public static boolean awaitInactivity(long timeout, TimeUnit unit) throws InterruptedException { - if (unit == null) { - throw new NullPointerException("unit"); - } + ObjectUtil.checkNotNull(unit, "unit"); Thread watcherThread = ThreadDeathWatcher.watcherThread; if (watcherThread != null) { diff --git a/common/src/main/java/io/netty/util/concurrent/AbstractEventExecutor.java b/common/src/main/java/io/netty/util/concurrent/AbstractEventExecutor.java index 6cfccbd..4799701 100644 --- a/common/src/main/java/io/netty/util/concurrent/AbstractEventExecutor.java +++ b/common/src/main/java/io/netty/util/concurrent/AbstractEventExecutor.java @@ -15,6 +15,7 @@ */ package io.netty.util.concurrent; +import io.netty.util.internal.UnstableApi; import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; @@ -165,4 +166,25 @@ public abstract class AbstractEventExecutor extends AbstractExecutorService impl logger.warn("A task raised an exception. Task: {}", task, t); } } + + /** + * Like {@link #execute(Runnable)} but does not guarantee the task will be run until either + * a non-lazy task is executed or the executor is shut down. + * + * This is equivalent to submitting a {@link EventExecutor.LazyRunnable} to + * {@link #execute(Runnable)} but for an arbitrary {@link Runnable}. + * + * The default implementation just delegates to {@link #execute(Runnable)}. + */ + @UnstableApi + public void lazyExecute(Runnable task) { + execute(task); + } + + /** + * Marker interface for {@link Runnable} to indicate that it should be queued for execution + * but does not need to run immediately. + */ + @UnstableApi + public interface LazyRunnable extends Runnable { } } diff --git a/common/src/main/java/io/netty/util/concurrent/AbstractScheduledEventExecutor.java b/common/src/main/java/io/netty/util/concurrent/AbstractScheduledEventExecutor.java index 41430c7..16166d3 100644 --- a/common/src/main/java/io/netty/util/concurrent/AbstractScheduledEventExecutor.java +++ b/common/src/main/java/io/netty/util/concurrent/AbstractScheduledEventExecutor.java @@ -19,17 +19,17 @@ import io.netty.util.internal.DefaultPriorityQueue; import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.PriorityQueue; +import static io.netty.util.concurrent.ScheduledFutureTask.deadlineNanos; + import java.util.Comparator; import java.util.Queue; import java.util.concurrent.Callable; -import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; /** * Abstract base class for {@link EventExecutor}s that want to support scheduling. */ public abstract class AbstractScheduledEventExecutor extends AbstractEventExecutor { - private static final Comparator<ScheduledFutureTask<?>> SCHEDULED_FUTURE_TASK_COMPARATOR = new Comparator<ScheduledFutureTask<?>>() { @Override @@ -38,8 +38,15 @@ public abstract class AbstractScheduledEventExecutor extends AbstractEventExecut } }; + static final Runnable WAKEUP_TASK = new Runnable() { + @Override + public void run() { } // Do nothing + }; + PriorityQueue<ScheduledFutureTask<?>> scheduledTaskQueue; + long nextTaskId; + protected AbstractScheduledEventExecutor() { } @@ -51,6 +58,24 @@ public abstract class AbstractScheduledEventExecutor extends AbstractEventExecut return ScheduledFutureTask.nanoTime(); } + /** + * Given an arbitrary deadline {@code deadlineNanos}, calculate the number of nano seconds from now + * {@code deadlineNanos} would expire. + * @param deadlineNanos An arbitrary deadline in nano seconds. + * @return the number of nano seconds from now {@code deadlineNanos} would expire. + */ + protected static long deadlineToDelayNanos(long deadlineNanos) { + return ScheduledFutureTask.deadlineToDelayNanos(deadlineNanos); + } + + /** + * The initial value used for delay and computations based upon a monatomic time source. + * @return initial value used for delay and computations based upon a monatomic time source. + */ + protected static long initialNanoTime() { + return ScheduledFutureTask.initialNanoTime(); + } + PriorityQueue<ScheduledFutureTask<?>> scheduledTaskQueue() { if (scheduledTaskQueue == null) { scheduledTaskQueue = new DefaultPriorityQueue<ScheduledFutureTask<?>>( @@ -101,45 +126,42 @@ public abstract class AbstractScheduledEventExecutor extends AbstractEventExecut protected final Runnable pollScheduledTask(long nanoTime) { assert inEventLoop(); - Queue<ScheduledFutureTask<?>> scheduledTaskQueue = this.scheduledTaskQueue; - ScheduledFutureTask<?> scheduledTask = scheduledTaskQueue == null ? null : scheduledTaskQueue.peek(); - if (scheduledTask == null) { + ScheduledFutureTask<?> scheduledTask = peekScheduledTask(); + if (scheduledTask == null || scheduledTask.deadlineNanos() - nanoTime > 0) { return null; } - - if (scheduledTask.deadlineNanos() <= nanoTime) { - scheduledTaskQueue.remove(); - return scheduledTask; - } - return null; + scheduledTaskQueue.remove(); + scheduledTask.setConsumed(); + return scheduledTask; } /** - * Return the nanoseconds when the next scheduled task is ready to be run or {@code -1} if no task is scheduled. + * Return the nanoseconds until the next scheduled task is ready to be run or {@code -1} if no task is scheduled. */ protected final long nextScheduledTaskNano() { - Queue<ScheduledFutureTask<?>> scheduledTaskQueue = this.scheduledTaskQueue; - ScheduledFutureTask<?> scheduledTask = scheduledTaskQueue == null ? null : scheduledTaskQueue.peek(); - if (scheduledTask == null) { - return -1; - } - return Math.max(0, scheduledTask.deadlineNanos() - nanoTime()); + ScheduledFutureTask<?> scheduledTask = peekScheduledTask(); + return scheduledTask != null ? scheduledTask.delayNanos() : -1; + } + + /** + * Return the deadline (in nanoseconds) when the next scheduled task is ready to be run or {@code -1} + * if no task is scheduled. + */ + protected final long nextScheduledTaskDeadlineNanos() { + ScheduledFutureTask<?> scheduledTask = peekScheduledTask(); + return scheduledTask != null ? scheduledTask.deadlineNanos() : -1; } final ScheduledFutureTask<?> peekScheduledTask() { Queue<ScheduledFutureTask<?>> scheduledTaskQueue = this.scheduledTaskQueue; - if (scheduledTaskQueue == null) { - return null; - } - return scheduledTaskQueue.peek(); + return scheduledTaskQueue != null ? scheduledTaskQueue.peek() : null; } /** * Returns {@code true} if a scheduled task is ready for processing. */ protected final boolean hasScheduledTasks() { - Queue<ScheduledFutureTask<?>> scheduledTaskQueue = this.scheduledTaskQueue; - ScheduledFutureTask<?> scheduledTask = scheduledTaskQueue == null ? null : scheduledTaskQueue.peek(); + ScheduledFutureTask<?> scheduledTask = peekScheduledTask(); return scheduledTask != null && scheduledTask.deadlineNanos() <= nanoTime(); } @@ -153,7 +175,9 @@ public abstract class AbstractScheduledEventExecutor extends AbstractEventExecut validateScheduled0(delay, unit); return schedule(new ScheduledFutureTask<Void>( - this, command, null, ScheduledFutureTask.deadlineNanos(unit.toNanos(delay)))); + this, + command, + deadlineNanos(unit.toNanos(delay)))); } @Override @@ -165,8 +189,7 @@ public abstract class AbstractScheduledEventExecutor extends AbstractEventExecut } validateScheduled0(delay, unit); - return schedule(new ScheduledFutureTask<V>( - this, callable, ScheduledFutureTask.deadlineNanos(unit.toNanos(delay)))); + return schedule(new ScheduledFutureTask<V>(this, callable, deadlineNanos(unit.toNanos(delay)))); } @Override @@ -185,8 +208,7 @@ public abstract class AbstractScheduledEventExecutor extends AbstractEventExecut validateScheduled0(period, unit); return schedule(new ScheduledFutureTask<Void>( - this, Executors.<Void>callable(command, null), - ScheduledFutureTask.deadlineNanos(unit.toNanos(initialDelay)), unit.toNanos(period))); + this, command, deadlineNanos(unit.toNanos(initialDelay)), unit.toNanos(period))); } @Override @@ -206,8 +228,7 @@ public abstract class AbstractScheduledEventExecutor extends AbstractEventExecut validateScheduled0(delay, unit); return schedule(new ScheduledFutureTask<Void>( - this, Executors.<Void>callable(command, null), - ScheduledFutureTask.deadlineNanos(unit.toNanos(initialDelay)), -unit.toNanos(delay))); + this, command, deadlineNanos(unit.toNanos(initialDelay)), -unit.toNanos(delay))); } @SuppressWarnings("deprecation") @@ -225,31 +246,65 @@ public abstract class AbstractScheduledEventExecutor extends AbstractEventExecut // NOOP } - <V> ScheduledFuture<V> schedule(final ScheduledFutureTask<V> task) { + final void scheduleFromEventLoop(final ScheduledFutureTask<?> task) { + // nextTaskId a long and so there is no chance it will overflow back to 0 + scheduledTaskQueue().add(task.setId(++nextTaskId)); + } + + private <V> ScheduledFuture<V> schedule(final ScheduledFutureTask<V> task) { if (inEventLoop()) { - scheduledTaskQueue().add(task); + scheduleFromEventLoop(task); } else { - execute(new Runnable() { - @Override - public void run() { - scheduledTaskQueue().add(task); + final long deadlineNanos = task.deadlineNanos(); + // task will add itself to scheduled task queue when run if not expired + if (beforeScheduledTaskSubmitted(deadlineNanos)) { + execute(task); + } else { + lazyExecute(task); + // Second hook after scheduling to facilitate race-avoidance + if (afterScheduledTaskSubmitted(deadlineNanos)) { + execute(WAKEUP_TASK); } - }); + } } return task; } final void removeScheduled(final ScheduledFutureTask<?> task) { + assert task.isCancelled(); if (inEventLoop()) { scheduledTaskQueue().removeTyped(task); } else { - execute(new Runnable() { - @Override - public void run() { - removeScheduled(task); - } - }); + // task will remove itself from scheduled task queue when it runs + lazyExecute(task); } } + + /** + * Called from arbitrary non-{@link EventExecutor} threads prior to scheduled task submission. + * Returns {@code true} if the {@link EventExecutor} thread should be woken immediately to + * process the scheduled task (if not already awake). + * <p> + * If {@code false} is returned, {@link #afterScheduledTaskSubmitted(long)} will be called with + * the same value <i>after</i> the scheduled task is enqueued, providing another opportunity + * to wake the {@link EventExecutor} thread if required. + * + * @param deadlineNanos deadline of the to-be-scheduled task + * relative to {@link AbstractScheduledEventExecutor#nanoTime()} + * @return {@code true} if the {@link EventExecutor} thread should be woken, {@code false} otherwise + */ + protected boolean beforeScheduledTaskSubmitted(long deadlineNanos) { + return true; + } + + /** + * See {@link #beforeScheduledTaskSubmitted(long)}. Called only after that method returns false. + * + * @param deadlineNanos relative to {@link AbstractScheduledEventExecutor#nanoTime()} + * @return {@code true} if the {@link EventExecutor} thread should be woken, {@code false} otherwise + */ + protected boolean afterScheduledTaskSubmitted(long deadlineNanos) { + return true; + } } diff --git a/common/src/main/java/io/netty/util/concurrent/CompleteFuture.java b/common/src/main/java/io/netty/util/concurrent/CompleteFuture.java index ed5755f..545c84e 100644 --- a/common/src/main/java/io/netty/util/concurrent/CompleteFuture.java +++ b/common/src/main/java/io/netty/util/concurrent/CompleteFuture.java @@ -16,6 +16,8 @@ package io.netty.util.concurrent; +import io.netty.util.internal.ObjectUtil; + import java.util.concurrent.TimeUnit; /** @@ -43,19 +45,15 @@ public abstract class CompleteFuture<V> extends AbstractFuture<V> { @Override public Future<V> addListener(GenericFutureListener<? extends Future<? super V>> listener) { - if (listener == null) { - throw new NullPointerException("listener"); - } - DefaultPromise.notifyListener(executor(), this, listener); + DefaultPromise.notifyListener(executor(), this, ObjectUtil.checkNotNull(listener, "listener")); return this; } @Override public Future<V> addListeners(GenericFutureListener<? extends Future<? super V>>... listeners) { - if (listeners == null) { - throw new NullPointerException("listeners"); - } - for (GenericFutureListener<? extends Future<? super V>> l: listeners) { + for (GenericFutureListener<? extends Future<? super V>> l: + ObjectUtil.checkNotNull(listeners, "listeners")) { + if (l == null) { break; } diff --git a/common/src/main/java/io/netty/util/concurrent/DefaultPromise.java b/common/src/main/java/io/netty/util/concurrent/DefaultPromise.java index a910e40..bc7a09b 100644 --- a/common/src/main/java/io/netty/util/concurrent/DefaultPromise.java +++ b/common/src/main/java/io/netty/util/concurrent/DefaultPromise.java @@ -24,7 +24,9 @@ import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; import static io.netty.util.internal.ObjectUtil.checkNotNull; @@ -43,6 +45,7 @@ public class DefaultPromise<V> extends AbstractFuture<V> implements Promise<V> { private static final Object UNCANCELLABLE = new Object(); private static final CauseHolder CANCELLATION_CAUSE_HOLDER = new CauseHolder(ThrowableUtil.unknownStackTrace( new CancellationException(), DefaultPromise.class, "cancel(...)")); + private static final StackTraceElement[] CANCELLATION_STACK = CANCELLATION_CAUSE_HOLDER.cause.getStackTrace(); private volatile Object result; private final EventExecutor executor; @@ -91,7 +94,6 @@ public class DefaultPromise<V> extends AbstractFuture<V> implements Promise<V> { @Override public Promise<V> setSuccess(V result) { if (setSuccess0(result)) { - notifyListeners(); return this; } throw new IllegalStateException("complete already: " + this); @@ -99,17 +101,12 @@ public class DefaultPromise<V> extends AbstractFuture<V> implements Promise<V> { @Override public boolean trySuccess(V result) { - if (setSuccess0(result)) { - notifyListeners(); - return true; - } - return false; + return setSuccess0(result); } @Override public Promise<V> setFailure(Throwable cause) { if (setFailure0(cause)) { - notifyListeners(); return this; } throw new IllegalStateException("complete already: " + this, cause); @@ -117,11 +114,7 @@ public class DefaultPromise<V> extends AbstractFuture<V> implements Promise<V> { @Override public boolean tryFailure(Throwable cause) { - if (setFailure0(cause)) { - notifyListeners(); - return true; - } - return false; + return setFailure0(cause); } @Override @@ -144,10 +137,38 @@ public class DefaultPromise<V> extends AbstractFuture<V> implements Promise<V> { return result == null; } + private static final class LeanCancellationException extends CancellationException { + private static final long serialVersionUID = 2794674970981187807L; + + @Override + public Throwable fillInStackTrace() { + setStackTrace(CANCELLATION_STACK); + return this; + } + + @Override + public String toString() { + return CancellationException.class.getName(); + } + } + @Override public Throwable cause() { - Object result = this.result; - return (result instanceof CauseHolder) ? ((CauseHolder) result).cause : null; + return cause0(result); + } + + private Throwable cause0(Object result) { + if (!(result instanceof CauseHolder)) { + return null; + } + if (result == CANCELLATION_CAUSE_HOLDER) { + CancellationException ce = new LeanCancellationException(); + if (RESULT_UPDATER.compareAndSet(this, CANCELLATION_CAUSE_HOLDER, new CauseHolder(ce))) { + return ce; + } + result = this.result; + } + return ((CauseHolder) result).cause; } @Override @@ -307,6 +328,50 @@ public class DefaultPromise<V> extends AbstractFuture<V> implements Promise<V> { return (V) result; } + @SuppressWarnings("unchecked") + @Override + public V get() throws InterruptedException, ExecutionException { + Object result = this.result; + if (!isDone0(result)) { + await(); + result = this.result; + } + if (result == SUCCESS || result == UNCANCELLABLE) { + return null; + } + Throwable cause = cause0(result); + if (cause == null) { + return (V) result; + } + if (cause instanceof CancellationException) { + throw (CancellationException) cause; + } + throw new ExecutionException(cause); + } + + @SuppressWarnings("unchecked") + @Override + public V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { + Object result = this.result; + if (!isDone0(result)) { + if (!await(timeout, unit)) { + throw new TimeoutException(); + } + result = this.result; + } + if (result == SUCCESS || result == UNCANCELLABLE) { + return null; + } + Throwable cause = cause0(result); + if (cause == null) { + return (V) result; + } + if (cause instanceof CancellationException) { + throw (CancellationException) cause; + } + throw new ExecutionException(cause); + } + /** * {@inheritDoc} * @@ -315,8 +380,9 @@ public class DefaultPromise<V> extends AbstractFuture<V> implements Promise<V> { @Override public boolean cancel(boolean mayInterruptIfRunning) { if (RESULT_UPDATER.compareAndSet(this, null, CANCELLATION_CAUSE_HOLDER)) { - checkNotifyWaiters(); - notifyListeners(); + if (checkNotifyWaiters()) { + notifyListeners(); + } return true; } return false; @@ -407,10 +473,10 @@ public class DefaultPromise<V> extends AbstractFuture<V> implements Promise<V> { */ protected static void notifyListener( EventExecutor eventExecutor, final Future<?> future, final GenericFutureListener<?> listener) { - checkNotNull(eventExecutor, "eventExecutor"); - checkNotNull(future, "future"); - checkNotNull(listener, "listener"); - notifyListenerWithStackOverFlowProtection(eventExecutor, future, listener); + notifyListenerWithStackOverFlowProtection( + checkNotNull(eventExecutor, "eventExecutor"), + checkNotNull(future, "future"), + checkNotNull(listener, "listener")); } private void notifyListeners() { @@ -545,16 +611,23 @@ public class DefaultPromise<V> extends AbstractFuture<V> implements Promise<V> { private boolean setValue0(Object objResult) { if (RESULT_UPDATER.compareAndSet(this, null, objResult) || RESULT_UPDATER.compareAndSet(this, UNCANCELLABLE, objResult)) { - checkNotifyWaiters(); + if (checkNotifyWaiters()) { + notifyListeners(); + } return true; } return false; } - private synchronized void checkNotifyWaiters() { + /** + * Check if there are any waiters and if so notify these. + * @return {@code true} if there are any listeners attached to the promise, {@code false} otherwise. + */ + private synchronized boolean checkNotifyWaiters() { if (waiters > 0) { notifyAll(); } + return listeners != null; } private void incWaiters() { diff --git a/common/src/main/java/io/netty/util/concurrent/DefaultThreadFactory.java b/common/src/main/java/io/netty/util/concurrent/DefaultThreadFactory.java index 15c7658..7c63270 100644 --- a/common/src/main/java/io/netty/util/concurrent/DefaultThreadFactory.java +++ b/common/src/main/java/io/netty/util/concurrent/DefaultThreadFactory.java @@ -16,6 +16,7 @@ package io.netty.util.concurrent; +import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.StringUtil; import java.util.Locale; @@ -64,9 +65,7 @@ public class DefaultThreadFactory implements ThreadFactory { } public static String toPoolName(Class<?> poolType) { - if (poolType == null) { - throw new NullPointerException("poolType"); - } + ObjectUtil.checkNotNull(poolType, "poolType"); String poolName = StringUtil.simpleClassName(poolType); switch (poolName.length()) { @@ -84,9 +83,8 @@ public class DefaultThreadFactory implements ThreadFactory { } public DefaultThreadFactory(String poolName, boolean daemon, int priority, ThreadGroup threadGroup) { - if (poolName == null) { - throw new NullPointerException("poolName"); - } + ObjectUtil.checkNotNull(poolName, "poolName"); + if (priority < Thread.MIN_PRIORITY || priority > Thread.MAX_PRIORITY) { throw new IllegalArgumentException( "priority: " + priority + " (expected: Thread.MIN_PRIORITY <= priority <= Thread.MAX_PRIORITY)"); diff --git a/common/src/main/java/io/netty/util/concurrent/FailedFuture.java b/common/src/main/java/io/netty/util/concurrent/FailedFuture.java index 31ce888..b246913 100644 --- a/common/src/main/java/io/netty/util/concurrent/FailedFuture.java +++ b/common/src/main/java/io/netty/util/concurrent/FailedFuture.java @@ -15,6 +15,7 @@ */ package io.netty.util.concurrent; +import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.PlatformDependent; /** @@ -34,10 +35,7 @@ public final class FailedFuture<V> extends CompleteFuture<V> { */ public FailedFuture(EventExecutor executor, Throwable cause) { super(executor); - if (cause == null) { - throw new NullPointerException("cause"); - } - this.cause = cause; + this.cause = ObjectUtil.checkNotNull(cause, "cause"); } @Override diff --git a/common/src/main/java/io/netty/util/concurrent/FastThreadLocal.java b/common/src/main/java/io/netty/util/concurrent/FastThreadLocal.java index 9d808c7..ed83fb4 100644 --- a/common/src/main/java/io/netty/util/concurrent/FastThreadLocal.java +++ b/common/src/main/java/io/netty/util/concurrent/FastThreadLocal.java @@ -139,33 +139,22 @@ public class FastThreadLocal<V> { return (V) v; } - V value = initialize(threadLocalMap); - registerCleaner(threadLocalMap); - return value; + return initialize(threadLocalMap); } - private void registerCleaner(final InternalThreadLocalMap threadLocalMap) { - Thread current = Thread.currentThread(); - if (FastThreadLocalThread.willCleanupFastThreadLocals(current) || threadLocalMap.isCleanerFlagSet(index)) { - return; - } - - threadLocalMap.setCleanerFlag(index); - - // TODO: We need to find a better way to handle this. - /* - // We will need to ensure we will trigger remove(InternalThreadLocalMap) so everything will be released - // and FastThreadLocal.onRemoval(...) will be called. - ObjectCleaner.register(current, new Runnable() { - @Override - public void run() { - remove(threadLocalMap); - - // It's fine to not call InternalThreadLocalMap.remove() here as this will only be triggered once - // the Thread is collected by GC. In this case the ThreadLocal will be gone away already. + /** + * Returns the current value for the current thread if it exists, {@code null} otherwise. + */ + @SuppressWarnings("unchecked") + public final V getIfExists() { + InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.getIfSet(); + if (threadLocalMap != null) { + Object v = threadLocalMap.indexedVariable(index); + if (v != InternalThreadLocalMap.UNSET) { + return (V) v; } - }); - */ + } + return null; } /** @@ -201,9 +190,7 @@ public class FastThreadLocal<V> { public final void set(V value) { if (value != InternalThreadLocalMap.UNSET) { InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.get(); - if (setKnownNotUnset(threadLocalMap, value)) { - registerCleaner(threadLocalMap); - } + setKnownNotUnset(threadLocalMap, value); } else { remove(); } @@ -223,12 +210,10 @@ public class FastThreadLocal<V> { /** * @return see {@link InternalThreadLocalMap#setIndexedVariable(int, Object)}. */ - private boolean setKnownNotUnset(InternalThreadLocalMap threadLocalMap, V value) { + private void setKnownNotUnset(InternalThreadLocalMap threadLocalMap, V value) { if (threadLocalMap.setIndexedVariable(index, value)) { addToVariablesToRemove(threadLocalMap, this); - return true; } - return false; } /** diff --git a/common/src/main/java/io/netty/util/concurrent/Future.java b/common/src/main/java/io/netty/util/concurrent/Future.java index 16ffa72..e455904 100644 --- a/common/src/main/java/io/netty/util/concurrent/Future.java +++ b/common/src/main/java/io/netty/util/concurrent/Future.java @@ -155,14 +155,14 @@ public interface Future<V> extends java.util.concurrent.Future<V> { * Return the result without blocking. If the future is not done yet this will return {@code null}. * * As it is possible that a {@code null} value is used to mark the future as successful you also need to check - * if the future is really done with {@link #isDone()} and not relay on the returned {@code null} value. + * if the future is really done with {@link #isDone()} and not rely on the returned {@code null} value. */ V getNow(); /** * {@inheritDoc} * - * If the cancellation was successful it will fail the future with an {@link CancellationException}. + * If the cancellation was successful it will fail the future with a {@link CancellationException}. */ @Override boolean cancel(boolean mayInterruptIfRunning); diff --git a/common/src/main/java/io/netty/util/concurrent/GlobalEventExecutor.java b/common/src/main/java/io/netty/util/concurrent/GlobalEventExecutor.java index 7e2527d..962fdff 100644 --- a/common/src/main/java/io/netty/util/concurrent/GlobalEventExecutor.java +++ b/common/src/main/java/io/netty/util/concurrent/GlobalEventExecutor.java @@ -15,6 +15,8 @@ */ package io.netty.util.concurrent; +import io.netty.util.internal.ObjectUtil; +import io.netty.util.internal.ThreadExecutorMap; import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; @@ -34,7 +36,7 @@ import java.util.concurrent.atomic.AtomicBoolean; * task pending in the task queue for 1 second. Please note it is not scalable to schedule large number of tasks to * this executor; use a dedicated executor. */ -public final class GlobalEventExecutor extends AbstractScheduledEventExecutor { +public final class GlobalEventExecutor extends AbstractScheduledEventExecutor implements OrderedEventExecutor { private static final InternalLogger logger = InternalLoggerFactory.getInstance(GlobalEventExecutor.class); @@ -55,8 +57,7 @@ public final class GlobalEventExecutor extends AbstractScheduledEventExecutor { // can trigger the creation of a thread from arbitrary thread groups; for this reason, the thread factory must not // be sticky about its thread group // visible for testing - final ThreadFactory threadFactory = - new DefaultThreadFactory(DefaultThreadFactory.toPoolName(getClass()), false, Thread.NORM_PRIORITY, null); + final ThreadFactory threadFactory; private final TaskRunner taskRunner = new TaskRunner(); private final AtomicBoolean started = new AtomicBoolean(); volatile Thread thread; @@ -65,6 +66,8 @@ public final class GlobalEventExecutor extends AbstractScheduledEventExecutor { private GlobalEventExecutor() { scheduledTaskQueue().add(quietPeriodTask); + threadFactory = ThreadExecutorMap.apply(new DefaultThreadFactory( + DefaultThreadFactory.toPoolName(getClass()), false, Thread.NORM_PRIORITY, null), this); } /** @@ -86,7 +89,7 @@ public final class GlobalEventExecutor extends AbstractScheduledEventExecutor { return task; } else { long delayNanos = scheduledTask.delayNanos(); - Runnable task; + Runnable task = null; if (delayNanos > 0) { try { task = taskQueue.poll(delayNanos, TimeUnit.NANOSECONDS); @@ -94,11 +97,12 @@ public final class GlobalEventExecutor extends AbstractScheduledEventExecutor { // Waken up. return null; } - } else { - task = taskQueue.poll(); } - if (task == null) { + // We need to fetch the scheduled tasks now as otherwise there may be a chance that + // scheduled tasks are never executed if there is always one task in the taskQueue. + // This is for example true for the read task of OIO Transport + // See https://github.com/netty/netty/issues/1614 fetchFromScheduledTaskQueue(); task = taskQueue.poll(); } @@ -134,10 +138,7 @@ public final class GlobalEventExecutor extends AbstractScheduledEventExecutor { * before. */ private void addTask(Runnable task) { - if (task == null) { - throw new NullPointerException("task"); - } - taskQueue.add(task); + taskQueue.add(ObjectUtil.checkNotNull(task, "task")); } @Override @@ -190,9 +191,7 @@ public final class GlobalEventExecutor extends AbstractScheduledEventExecutor { * @return {@code true} if and only if the worker thread has been terminated */ public boolean awaitInactivity(long timeout, TimeUnit unit) throws InterruptedException { - if (unit == null) { - throw new NullPointerException("unit"); - } + ObjectUtil.checkNotNull(unit, "unit"); final Thread thread = this.thread; if (thread == null) { @@ -204,11 +203,7 @@ public final class GlobalEventExecutor extends AbstractScheduledEventExecutor { @Override public void execute(Runnable task) { - if (task == null) { - throw new NullPointerException("task"); - } - - addTask(task); + addTask(ObjectUtil.checkNotNull(task, "task")); if (!inEventLoop()) { startThread(); } diff --git a/common/src/main/java/io/netty/util/concurrent/ImmediateEventExecutor.java b/common/src/main/java/io/netty/util/concurrent/ImmediateEventExecutor.java index f952cf2..9173711 100644 --- a/common/src/main/java/io/netty/util/concurrent/ImmediateEventExecutor.java +++ b/common/src/main/java/io/netty/util/concurrent/ImmediateEventExecutor.java @@ -15,6 +15,7 @@ */ package io.netty.util.concurrent; +import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; @@ -102,9 +103,7 @@ public final class ImmediateEventExecutor extends AbstractEventExecutor { @Override public void execute(Runnable command) { - if (command == null) { - throw new NullPointerException("command"); - } + ObjectUtil.checkNotNull(command, "command"); if (!RUNNING.get()) { RUNNING.set(true); try { diff --git a/common/src/main/java/io/netty/util/concurrent/ImmediateExecutor.java b/common/src/main/java/io/netty/util/concurrent/ImmediateExecutor.java index fa68e0b..6a92355 100644 --- a/common/src/main/java/io/netty/util/concurrent/ImmediateExecutor.java +++ b/common/src/main/java/io/netty/util/concurrent/ImmediateExecutor.java @@ -15,6 +15,8 @@ */ package io.netty.util.concurrent; +import io.netty.util.internal.ObjectUtil; + import java.util.concurrent.Executor; /** @@ -23,15 +25,12 @@ import java.util.concurrent.Executor; public final class ImmediateExecutor implements Executor { public static final ImmediateExecutor INSTANCE = new ImmediateExecutor(); - private ImmediateExecutor() { + private ImmediateExecutor() { // use static instance } @Override public void execute(Runnable command) { - if (command == null) { - throw new NullPointerException("command"); - } - command.run(); + ObjectUtil.checkNotNull(command, "command").run(); } } diff --git a/common/src/main/java/io/netty/util/concurrent/NonStickyEventExecutorGroup.java b/common/src/main/java/io/netty/util/concurrent/NonStickyEventExecutorGroup.java index bcc4b82..a2692f6 100644 --- a/common/src/main/java/io/netty/util/concurrent/NonStickyEventExecutorGroup.java +++ b/common/src/main/java/io/netty/util/concurrent/NonStickyEventExecutorGroup.java @@ -274,7 +274,7 @@ public final class NonStickyEventExecutorGroup implements EventExecutorGroup { // // The above cases can be distinguished by performing a // compareAndSet(NONE, RUNNING). If it returns "false", it is case 1; otherwise it is case 2. - if (tasks.peek() == null || !state.compareAndSet(NONE, RUNNING)) { + if (tasks.isEmpty() || !state.compareAndSet(NONE, RUNNING)) { return; // done } } diff --git a/common/src/main/java/io/netty/util/concurrent/PromiseAggregator.java b/common/src/main/java/io/netty/util/concurrent/PromiseAggregator.java index 7d908c0..e216b6e 100644 --- a/common/src/main/java/io/netty/util/concurrent/PromiseAggregator.java +++ b/common/src/main/java/io/netty/util/concurrent/PromiseAggregator.java @@ -16,11 +16,13 @@ package io.netty.util.concurrent; +import io.netty.util.internal.ObjectUtil; + import java.util.LinkedHashSet; import java.util.Set; /** - * @deprecated Use {@link PromiseCombiner} + * @deprecated Use {@link PromiseCombiner#PromiseCombiner(EventExecutor)}. * * {@link GenericFutureListener} implementation which consolidates multiple {@link Future}s * into one, by listening to individual {@link Future}s and producing an aggregated result @@ -43,10 +45,7 @@ public class PromiseAggregator<V, F extends Future<V>> implements GenericFutureL * @param failPending {@code true} to fail pending promises, false to leave them unaffected */ public PromiseAggregator(Promise<Void> aggregatePromise, boolean failPending) { - if (aggregatePromise == null) { - throw new NullPointerException("aggregatePromise"); - } - this.aggregatePromise = aggregatePromise; + this.aggregatePromise = ObjectUtil.checkNotNull(aggregatePromise, "aggregatePromise"); this.failPending = failPending; } @@ -63,9 +62,7 @@ public class PromiseAggregator<V, F extends Future<V>> implements GenericFutureL */ @SafeVarargs public final PromiseAggregator<V, F> add(Promise<V>... promises) { - if (promises == null) { - throw new NullPointerException("promises"); - } + ObjectUtil.checkNotNull(promises, "promises"); if (promises.length == 0) { return this; } diff --git a/common/src/main/java/io/netty/util/concurrent/PromiseCombiner.java b/common/src/main/java/io/netty/util/concurrent/PromiseCombiner.java index 6624f05..8895c1a 100644 --- a/common/src/main/java/io/netty/util/concurrent/PromiseCombiner.java +++ b/common/src/main/java/io/netty/util/concurrent/PromiseCombiner.java @@ -28,26 +28,62 @@ import io.netty.util.internal.ObjectUtil; * {@link PromiseCombiner#add(Future)} and {@link PromiseCombiner#addAll(Future[])} methods. When all futures to be * combined have been added, callers must provide an aggregate promise to be notified when all combined promises have * finished via the {@link PromiseCombiner#finish(Promise)} method.</p> + * + * <p>This implementation is <strong>NOT</strong> thread-safe and all methods must be called + * from the {@link EventExecutor} thread.</p> */ public final class PromiseCombiner { private int expectedCount; private int doneCount; - private boolean doneAdding; private Promise<Void> aggregatePromise; private Throwable cause; private final GenericFutureListener<Future<?>> listener = new GenericFutureListener<Future<?>>() { @Override - public void operationComplete(Future<?> future) throws Exception { + public void operationComplete(final Future<?> future) { + if (executor.inEventLoop()) { + operationComplete0(future); + } else { + executor.execute(new Runnable() { + @Override + public void run() { + operationComplete0(future); + } + }); + } + } + + private void operationComplete0(Future<?> future) { + assert executor.inEventLoop(); ++doneCount; if (!future.isSuccess() && cause == null) { cause = future.cause(); } - if (doneCount == expectedCount && doneAdding) { + if (doneCount == expectedCount && aggregatePromise != null) { tryPromise(); } } }; + private final EventExecutor executor; + + /** + * Deprecated use {@link PromiseCombiner#PromiseCombiner(EventExecutor)}. + */ + @Deprecated + public PromiseCombiner() { + this(ImmediateEventExecutor.INSTANCE); + } + + /** + * The {@link EventExecutor} to use for notifications. You must call {@link #add(Future)}, {@link #addAll(Future[])} + * and {@link #finish(Promise)} from within the {@link EventExecutor} thread. + * + * @param executor the {@link EventExecutor} to use for notifications. + */ + public PromiseCombiner(EventExecutor executor) { + this.executor = ObjectUtil.checkNotNull(executor, "executor"); + } + /** * Adds a new promise to be combined. New promises may be added until an aggregate promise is added via the * {@link PromiseCombiner#finish(Promise)} method. @@ -70,6 +106,7 @@ public final class PromiseCombiner { @SuppressWarnings({ "unchecked", "rawtypes" }) public void add(Future future) { checkAddAllowed(); + checkInEventLoop(); ++expectedCount; future.addListener(listener); } @@ -112,22 +149,29 @@ public final class PromiseCombiner { * @param aggregatePromise the promise to notify when all combined futures have finished */ public void finish(Promise<Void> aggregatePromise) { - if (doneAdding) { + ObjectUtil.checkNotNull(aggregatePromise, "aggregatePromise"); + checkInEventLoop(); + if (this.aggregatePromise != null) { throw new IllegalStateException("Already finished"); } - doneAdding = true; - this.aggregatePromise = ObjectUtil.checkNotNull(aggregatePromise, "aggregatePromise"); + this.aggregatePromise = aggregatePromise; if (doneCount == expectedCount) { tryPromise(); } } + private void checkInEventLoop() { + if (!executor.inEventLoop()) { + throw new IllegalStateException("Must be called from EventExecutor thread"); + } + } + private boolean tryPromise() { return (cause == null) ? aggregatePromise.trySuccess(null) : aggregatePromise.tryFailure(cause); } private void checkAddAllowed() { - if (doneAdding) { + if (aggregatePromise != null) { throw new IllegalStateException("Adding promises is not allowed after finished adding"); } } diff --git a/common/src/main/java/io/netty/util/concurrent/PromiseTask.java b/common/src/main/java/io/netty/util/concurrent/PromiseTask.java index 8cb23c7..0025815 100644 --- a/common/src/main/java/io/netty/util/concurrent/PromiseTask.java +++ b/common/src/main/java/io/netty/util/concurrent/PromiseTask.java @@ -20,10 +20,6 @@ import java.util.concurrent.RunnableFuture; class PromiseTask<V> extends DefaultPromise<V> implements RunnableFuture<V> { - static <T> Callable<T> toCallable(Runnable runnable, T result) { - return new RunnableAdapter<T>(runnable, result); - } - private static final class RunnableAdapter<T> implements Callable<T> { final Runnable task; final T result; @@ -45,10 +41,37 @@ class PromiseTask<V> extends DefaultPromise<V> implements RunnableFuture<V> { } } - protected final Callable<V> task; + private static final Runnable COMPLETED = new SentinelRunnable("COMPLETED"); + private static final Runnable CANCELLED = new SentinelRunnable("CANCELLED"); + private static final Runnable FAILED = new SentinelRunnable("FAILED"); + + private static class SentinelRunnable implements Runnable { + private final String name; + + SentinelRunnable(String name) { + this.name = name; + } + + @Override + public void run() { } // no-op + + @Override + public String toString() { + return name; + } + } + + // Strictly of type Callable<V> or Runnable + private Object task; PromiseTask(EventExecutor executor, Runnable runnable, V result) { - this(executor, toCallable(runnable, result)); + super(executor); + task = result == null ? runnable : new RunnableAdapter<V>(runnable, result); + } + + PromiseTask(EventExecutor executor, Runnable runnable) { + super(executor); + task = runnable; } PromiseTask(EventExecutor executor, Callable<V> callable) { @@ -66,11 +89,21 @@ class PromiseTask<V> extends DefaultPromise<V> implements RunnableFuture<V> { return this == obj; } + @SuppressWarnings("unchecked") + final V runTask() throws Exception { + final Object task = this.task; + if (task instanceof Callable) { + return ((Callable<V>) task).call(); + } + ((Runnable) task).run(); + return null; + } + @Override public void run() { try { if (setUncancellableInternal()) { - V result = task.call(); + V result = runTask(); setSuccessInternal(result); } } catch (Throwable e) { @@ -78,6 +111,17 @@ class PromiseTask<V> extends DefaultPromise<V> implements RunnableFuture<V> { } } + private boolean clearTaskAfterCompletion(boolean done, Runnable result) { + if (done) { + // The only time where it might be possible for the sentinel task + // to be called is in the case of a periodic ScheduledFutureTask, + // in which case it's a benign race with cancellation and the (null) + // return value is not used. + task = result; + } + return done; + } + @Override public final Promise<V> setFailure(Throwable cause) { throw new IllegalStateException(); @@ -85,6 +129,7 @@ class PromiseTask<V> extends DefaultPromise<V> implements RunnableFuture<V> { protected final Promise<V> setFailureInternal(Throwable cause) { super.setFailure(cause); + clearTaskAfterCompletion(true, FAILED); return this; } @@ -94,7 +139,7 @@ class PromiseTask<V> extends DefaultPromise<V> implements RunnableFuture<V> { } protected final boolean tryFailureInternal(Throwable cause) { - return super.tryFailure(cause); + return clearTaskAfterCompletion(super.tryFailure(cause), FAILED); } @Override @@ -104,6 +149,7 @@ class PromiseTask<V> extends DefaultPromise<V> implements RunnableFuture<V> { protected final Promise<V> setSuccessInternal(V result) { super.setSuccess(result); + clearTaskAfterCompletion(true, COMPLETED); return this; } @@ -113,7 +159,7 @@ class PromiseTask<V> extends DefaultPromise<V> implements RunnableFuture<V> { } protected final boolean trySuccessInternal(V result) { - return super.trySuccess(result); + return clearTaskAfterCompletion(super.trySuccess(result), COMPLETED); } @Override @@ -125,6 +171,11 @@ class PromiseTask<V> extends DefaultPromise<V> implements RunnableFuture<V> { return super.setUncancellable(); } + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + return clearTaskAfterCompletion(super.cancel(mayInterruptIfRunning), CANCELLED); + } + @Override protected StringBuilder toStringBuilder() { StringBuilder buf = super.toStringBuilder(); diff --git a/common/src/main/java/io/netty/util/concurrent/ScheduledFutureTask.java b/common/src/main/java/io/netty/util/concurrent/ScheduledFutureTask.java index 1eaa7b9..2278940 100644 --- a/common/src/main/java/io/netty/util/concurrent/ScheduledFutureTask.java +++ b/common/src/main/java/io/netty/util/concurrent/ScheduledFutureTask.java @@ -19,15 +19,12 @@ package io.netty.util.concurrent; import io.netty.util.internal.DefaultPriorityQueue; import io.netty.util.internal.PriorityQueueNode; -import java.util.Queue; import java.util.concurrent.Callable; import java.util.concurrent.Delayed; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicLong; @SuppressWarnings("ComparableImplementedButEqualsNotOverridden") final class ScheduledFutureTask<V> extends PromiseTask<V> implements ScheduledFuture<V>, PriorityQueueNode { - private static final AtomicLong nextTaskId = new AtomicLong(); private static final long START_TIME = System.nanoTime(); static long nanoTime() { @@ -40,34 +37,44 @@ final class ScheduledFutureTask<V> extends PromiseTask<V> implements ScheduledFu return deadlineNanos < 0 ? Long.MAX_VALUE : deadlineNanos; } - private final long id = nextTaskId.getAndIncrement(); + static long initialNanoTime() { + return START_TIME; + } + + // set once when added to priority queue + private long id; + private long deadlineNanos; /* 0 - no repeat, >0 - repeat at fixed rate, <0 - repeat with fixed delay */ private final long periodNanos; private int queueIndex = INDEX_NOT_IN_QUEUE; - ScheduledFutureTask( - AbstractScheduledEventExecutor executor, - Runnable runnable, V result, long nanoTime) { + ScheduledFutureTask(AbstractScheduledEventExecutor executor, + Runnable runnable, long nanoTime) { - this(executor, toCallable(runnable, result), nanoTime); + super(executor, runnable); + deadlineNanos = nanoTime; + periodNanos = 0; } - ScheduledFutureTask( - AbstractScheduledEventExecutor executor, + ScheduledFutureTask(AbstractScheduledEventExecutor executor, + Runnable runnable, long nanoTime, long period) { + + super(executor, runnable); + deadlineNanos = nanoTime; + periodNanos = validatePeriod(period); + } + + ScheduledFutureTask(AbstractScheduledEventExecutor executor, Callable<V> callable, long nanoTime, long period) { super(executor, callable); - if (period == 0) { - throw new IllegalArgumentException("period: 0 (expected: != 0)"); - } deadlineNanos = nanoTime; - periodNanos = period; + periodNanos = validatePeriod(period); } - ScheduledFutureTask( - AbstractScheduledEventExecutor executor, + ScheduledFutureTask(AbstractScheduledEventExecutor executor, Callable<V> callable, long nanoTime) { super(executor, callable); @@ -75,6 +82,20 @@ final class ScheduledFutureTask<V> extends PromiseTask<V> implements ScheduledFu periodNanos = 0; } + private static long validatePeriod(long period) { + if (period == 0) { + throw new IllegalArgumentException("period: 0 (expected: != 0)"); + } + return period; + } + + ScheduledFutureTask<V> setId(long id) { + if (this.id == 0L) { + this.id = id; + } + return this; + } + @Override protected EventExecutor executor() { return super.executor(); @@ -84,12 +105,26 @@ final class ScheduledFutureTask<V> extends PromiseTask<V> implements ScheduledFu return deadlineNanos; } + void setConsumed() { + // Optimization to avoid checking system clock again + // after deadline has passed and task has been dequeued + if (periodNanos == 0) { + assert nanoTime() >= deadlineNanos; + deadlineNanos = 0L; + } + } + public long delayNanos() { - return Math.max(0, deadlineNanos() - nanoTime()); + return deadlineToDelayNanos(deadlineNanos()); + } + + static long deadlineToDelayNanos(long deadlineNanos) { + return deadlineNanos == 0L ? 0L : Math.max(0L, deadlineNanos - nanoTime()); } public long delayNanos(long currentTimeNanos) { - return Math.max(0, deadlineNanos() - (currentTimeNanos - START_TIME)); + return deadlineNanos == 0L ? 0L + : Math.max(0L, deadlineNanos() - (currentTimeNanos - START_TIME)); } @Override @@ -111,9 +146,8 @@ final class ScheduledFutureTask<V> extends PromiseTask<V> implements ScheduledFu return 1; } else if (id < that.id) { return -1; - } else if (id == that.id) { - throw new Error(); } else { + assert id != that.id; return 1; } } @@ -122,28 +156,32 @@ final class ScheduledFutureTask<V> extends PromiseTask<V> implements ScheduledFu public void run() { assert executor().inEventLoop(); try { + if (delayNanos() > 0L) { + // Not yet expired, need to add or remove from queue + if (isCancelled()) { + scheduledExecutor().scheduledTaskQueue().removeTyped(this); + } else { + scheduledExecutor().scheduleFromEventLoop(this); + } + return; + } if (periodNanos == 0) { if (setUncancellableInternal()) { - V result = task.call(); + V result = runTask(); setSuccessInternal(result); } } else { // check if is done as it may was cancelled if (!isCancelled()) { - task.call(); + runTask(); if (!executor().isShutdown()) { - long p = periodNanos; - if (p > 0) { - deadlineNanos += p; + if (periodNanos > 0) { + deadlineNanos += periodNanos; } else { - deadlineNanos = nanoTime() - p; + deadlineNanos = nanoTime() - periodNanos; } if (!isCancelled()) { - // scheduledTaskQueue can never be null as we lazy init it before submit the task! - Queue<ScheduledFutureTask<?>> scheduledTaskQueue = - ((AbstractScheduledEventExecutor) executor()).scheduledTaskQueue; - assert scheduledTaskQueue != null; - scheduledTaskQueue.add(this); + scheduledExecutor().scheduledTaskQueue().add(this); } } } @@ -153,6 +191,10 @@ final class ScheduledFutureTask<V> extends PromiseTask<V> implements ScheduledFu } } + private AbstractScheduledEventExecutor scheduledExecutor() { + return (AbstractScheduledEventExecutor) executor(); + } + /** * {@inheritDoc} * @@ -162,7 +204,7 @@ final class ScheduledFutureTask<V> extends PromiseTask<V> implements ScheduledFu public boolean cancel(boolean mayInterruptIfRunning) { boolean canceled = super.cancel(mayInterruptIfRunning); if (canceled) { - ((AbstractScheduledEventExecutor) executor()).removeScheduled(this); + scheduledExecutor().removeScheduled(this); } return canceled; } @@ -176,9 +218,7 @@ final class ScheduledFutureTask<V> extends PromiseTask<V> implements ScheduledFu StringBuilder buf = super.toStringBuilder(); buf.setCharAt(buf.length() - 1, ','); - return buf.append(" id: ") - .append(id) - .append(", deadline: ") + return buf.append(" deadline: ") .append(deadlineNanos) .append(", period: ") .append(periodNanos) diff --git a/common/src/main/java/io/netty/util/concurrent/SingleThreadEventExecutor.java b/common/src/main/java/io/netty/util/concurrent/SingleThreadEventExecutor.java index ab2a914..f7ab9ac 100644 --- a/common/src/main/java/io/netty/util/concurrent/SingleThreadEventExecutor.java +++ b/common/src/main/java/io/netty/util/concurrent/SingleThreadEventExecutor.java @@ -18,6 +18,7 @@ package io.netty.util.concurrent; import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.PlatformDependent; import io.netty.util.internal.SystemPropertyUtil; +import io.netty.util.internal.ThreadExecutorMap; import io.netty.util.internal.UnstableApi; import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; @@ -31,11 +32,11 @@ import java.util.Queue; import java.util.Set; import java.util.concurrent.BlockingQueue; import java.util.concurrent.Callable; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.RejectedExecutionException; -import java.util.concurrent.Semaphore; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -60,12 +61,6 @@ public abstract class SingleThreadEventExecutor extends AbstractScheduledEventEx private static final int ST_SHUTDOWN = 4; private static final int ST_TERMINATED = 5; - private static final Runnable WAKEUP_TASK = new Runnable() { - @Override - public void run() { - // Do nothing. - } - }; private static final Runnable NOOP_TASK = new Runnable() { @Override public void run() { @@ -87,7 +82,7 @@ public abstract class SingleThreadEventExecutor extends AbstractScheduledEventEx private final Executor executor; private volatile boolean interrupted; - private final Semaphore threadLock = new Semaphore(0); + private final CountDownLatch threadLock = new CountDownLatch(1); private final Set<Runnable> shutdownHooks = new LinkedHashSet<Runnable>(); private final boolean addTaskWakesUp; private final int maxPendingTasks; @@ -161,11 +156,22 @@ public abstract class SingleThreadEventExecutor extends AbstractScheduledEventEx super(parent); this.addTaskWakesUp = addTaskWakesUp; this.maxPendingTasks = Math.max(16, maxPendingTasks); - this.executor = ObjectUtil.checkNotNull(executor, "executor"); + this.executor = ThreadExecutorMap.apply(executor, this); taskQueue = newTaskQueue(this.maxPendingTasks); rejectedExecutionHandler = ObjectUtil.checkNotNull(rejectedHandler, "rejectedHandler"); } + protected SingleThreadEventExecutor(EventExecutorGroup parent, Executor executor, + boolean addTaskWakesUp, Queue<Runnable> taskQueue, + RejectedExecutionHandler rejectedHandler) { + super(parent); + this.addTaskWakesUp = addTaskWakesUp; + this.maxPendingTasks = DEFAULT_MAX_PENDING_EXECUTOR_TASKS; + this.executor = ThreadExecutorMap.apply(executor, this); + this.taskQueue = ObjectUtil.checkNotNull(taskQueue, "taskQueue"); + this.rejectedExecutionHandler = ObjectUtil.checkNotNull(rejectedHandler, "rejectedHandler"); + } + /** * @deprecated Please use and override {@link #newTaskQueue(int)}. */ @@ -207,10 +213,9 @@ public abstract class SingleThreadEventExecutor extends AbstractScheduledEventEx protected static Runnable pollTaskFrom(Queue<Runnable> taskQueue) { for (;;) { Runnable task = taskQueue.poll(); - if (task == WAKEUP_TASK) { - continue; + if (task != WAKEUP_TASK) { + return task; } - return task; } } @@ -271,16 +276,38 @@ public abstract class SingleThreadEventExecutor extends AbstractScheduledEventEx } private boolean fetchFromScheduledTaskQueue() { + if (scheduledTaskQueue == null || scheduledTaskQueue.isEmpty()) { + return true; + } long nanoTime = AbstractScheduledEventExecutor.nanoTime(); - Runnable scheduledTask = pollScheduledTask(nanoTime); - while (scheduledTask != null) { + for (;;) { + Runnable scheduledTask = pollScheduledTask(nanoTime); + if (scheduledTask == null) { + return true; + } if (!taskQueue.offer(scheduledTask)) { // No space left in the task queue add it back to the scheduledTaskQueue so we pick it up again. - scheduledTaskQueue().add((ScheduledFutureTask<?>) scheduledTask); + scheduledTaskQueue.add((ScheduledFutureTask<?>) scheduledTask); return false; } - scheduledTask = pollScheduledTask(nanoTime); } + } + + /** + * @return {@code true} if at least one scheduled task was executed. + */ + private boolean executeExpiredScheduledTasks() { + if (scheduledTaskQueue == null || scheduledTaskQueue.isEmpty()) { + return false; + } + long nanoTime = AbstractScheduledEventExecutor.nanoTime(); + Runnable scheduledTask = pollScheduledTask(nanoTime); + if (scheduledTask == null) { + return false; + } + do { + safeExecute(scheduledTask); + } while ((scheduledTask = pollScheduledTask(nanoTime)) != null); return true; } @@ -315,9 +342,7 @@ public abstract class SingleThreadEventExecutor extends AbstractScheduledEventEx * before. */ protected void addTask(Runnable task) { - if (task == null) { - throw new NullPointerException("task"); - } + ObjectUtil.checkNotNull(task, "task"); if (!offerTask(task)) { reject(task); } @@ -334,10 +359,7 @@ public abstract class SingleThreadEventExecutor extends AbstractScheduledEventEx * @see Queue#remove(Object) */ protected boolean removeTask(Runnable task) { - if (task == null) { - throw new NullPointerException("task"); - } - return taskQueue.remove(task); + return taskQueue.remove(ObjectUtil.checkNotNull(task, "task")); } /** @@ -364,6 +386,32 @@ public abstract class SingleThreadEventExecutor extends AbstractScheduledEventEx return ranAtLeastOne; } + /** + * Execute all expired scheduled tasks and all current tasks in the executor queue until both queues are empty, + * or {@code maxDrainAttempts} has been exceeded. + * @param maxDrainAttempts The maximum amount of times this method attempts to drain from queues. This is to prevent + * continuous task execution and scheduling from preventing the EventExecutor thread to + * make progress and return to the selector mechanism to process inbound I/O events. + * @return {@code true} if at least one task was run. + */ + protected final boolean runScheduledAndExecutorTasks(final int maxDrainAttempts) { + assert inEventLoop(); + boolean ranAtLeastOneTask; + int drainAttempt = 0; + do { + // We must run the taskQueue tasks first, because the scheduled tasks from outside the EventLoop are queued + // here because the taskQueue is thread safe and the scheduledTaskQueue is not thread safe. + ranAtLeastOneTask = runExistingTasksFrom(taskQueue) | executeExpiredScheduledTasks(); + } while (ranAtLeastOneTask && ++drainAttempt < maxDrainAttempts); + + if (drainAttempt > 0) { + lastExecutionTime = ScheduledFutureTask.nanoTime(); + } + afterRunningAllTasks(); + + return drainAttempt > 0; + } + /** * Runs all tasks from the passed {@code taskQueue}. * @@ -385,6 +433,26 @@ public abstract class SingleThreadEventExecutor extends AbstractScheduledEventEx } } + /** + * What ever tasks are present in {@code taskQueue} when this method is invoked will be {@link Runnable#run()}. + * @param taskQueue the task queue to drain. + * @return {@code true} if at least {@link Runnable#run()} was called. + */ + private boolean runExistingTasksFrom(Queue<Runnable> taskQueue) { + Runnable task = pollTaskFrom(taskQueue); + if (task == null) { + return false; + } + int remaining = Math.min(maxPendingTasks, taskQueue.size()); + safeExecute(task); + // Use taskQueue.poll() directly rather than pollTaskFrom() since the latter may + // silently consume more than one item from the queue (skips over WAKEUP_TASK instances) + while (remaining-- > 0 && (task = taskQueue.poll()) != null) { + safeExecute(task); + } + return true; + } + /** * Poll all tasks from the task queue and run them via {@link Runnable#run()} method. This method stops running * the tasks in the task queue and returns if it ran longer than {@code timeoutNanos}. @@ -397,7 +465,7 @@ public abstract class SingleThreadEventExecutor extends AbstractScheduledEventEx return false; } - final long deadline = ScheduledFutureTask.nanoTime() + timeoutNanos; + final long deadline = timeoutNanos > 0 ? ScheduledFutureTask.nanoTime() + timeoutNanos : 0; long runTasks = 0; long lastExecutionTime; for (;;) { @@ -431,6 +499,7 @@ public abstract class SingleThreadEventExecutor extends AbstractScheduledEventEx */ @UnstableApi protected void afterRunningAllTasks() { } + /** * Returns the amount of time left until the scheduled task with the closest dead line is executed. */ @@ -480,7 +549,7 @@ public abstract class SingleThreadEventExecutor extends AbstractScheduledEventEx } protected void wakeup(boolean inEventLoop) { - if (!inEventLoop || state == ST_SHUTTING_DOWN) { + if (!inEventLoop) { // Use offer as we actually only need this to unblock the thread and if offer fails we do not care as there // is already something in the queue. taskQueue.offer(WAKEUP_TASK); @@ -550,16 +619,12 @@ public abstract class SingleThreadEventExecutor extends AbstractScheduledEventEx @Override public Future<?> shutdownGracefully(long quietPeriod, long timeout, TimeUnit unit) { - if (quietPeriod < 0) { - throw new IllegalArgumentException("quietPeriod: " + quietPeriod + " (expected >= 0)"); - } + ObjectUtil.checkPositiveOrZero(quietPeriod, "quietPeriod"); if (timeout < quietPeriod) { throw new IllegalArgumentException( "timeout: " + timeout + " (expected >= quietPeriod (" + quietPeriod + "))"); } - if (unit == null) { - throw new NullPointerException("unit"); - } + ObjectUtil.checkNotNull(unit, "unit"); if (isShuttingDown()) { return terminationFuture(); @@ -600,7 +665,10 @@ public abstract class SingleThreadEventExecutor extends AbstractScheduledEventEx } if (wakeup) { - wakeup(inEventLoop); + taskQueue.offer(WAKEUP_TASK); + if (!addTaskWakesUp) { + wakeup(inEventLoop); + } } return terminationFuture(); @@ -652,7 +720,10 @@ public abstract class SingleThreadEventExecutor extends AbstractScheduledEventEx } if (wakeup) { - wakeup(inEventLoop); + taskQueue.offer(WAKEUP_TASK); + if (!addTaskWakesUp) { + wakeup(inEventLoop); + } } } @@ -701,7 +772,7 @@ public abstract class SingleThreadEventExecutor extends AbstractScheduledEventEx if (gracefulShutdownQuietPeriod == 0) { return true; } - wakeup(true); + taskQueue.offer(WAKEUP_TASK); return false; } @@ -714,7 +785,7 @@ public abstract class SingleThreadEventExecutor extends AbstractScheduledEventEx if (nanoTime - lastExecutionTime <= gracefulShutdownQuietPeriod) { // Check if any tasks were added to the queue every 100ms. // TODO: Change the behavior of takeTask() so that it returns on timeout. - wakeup(true); + taskQueue.offer(WAKEUP_TASK); try { Thread.sleep(100); } catch (InterruptedException e) { @@ -731,27 +802,28 @@ public abstract class SingleThreadEventExecutor extends AbstractScheduledEventEx @Override public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException { - if (unit == null) { - throw new NullPointerException("unit"); - } - + ObjectUtil.checkNotNull(unit, "unit"); if (inEventLoop()) { throw new IllegalStateException("cannot await termination of the current thread"); } - if (threadLock.tryAcquire(timeout, unit)) { - threadLock.release(); - } + threadLock.await(timeout, unit); return isTerminated(); } @Override public void execute(Runnable task) { - if (task == null) { - throw new NullPointerException("task"); - } + ObjectUtil.checkNotNull(task, "task"); + execute(task, !(task instanceof LazyRunnable) && wakesUpForTask(task)); + } + + @Override + public void lazyExecute(Runnable task) { + execute(ObjectUtil.checkNotNull(task, "task"), false); + } + private void execute(Runnable task, boolean immediate) { boolean inEventLoop = inEventLoop(); addTask(task); if (!inEventLoop) { @@ -773,7 +845,7 @@ public abstract class SingleThreadEventExecutor extends AbstractScheduledEventEx } } - if (!addTaskWakesUp && wakesUpForTask(task)) { + if (!addTaskWakesUp && immediate) { wakeup(inEventLoop); } } @@ -813,7 +885,7 @@ public abstract class SingleThreadEventExecutor extends AbstractScheduledEventEx /** * Returns the {@link ThreadProperties} of the {@link Thread} that powers the {@link SingleThreadEventExecutor}. - * If the {@link SingleThreadEventExecutor} is not started yet, this operation will start it and block until the + * If the {@link SingleThreadEventExecutor} is not started yet, this operation will start it and block until * it is fully started. */ public final ThreadProperties threadProperties() { @@ -836,7 +908,16 @@ public abstract class SingleThreadEventExecutor extends AbstractScheduledEventEx return threadProperties; } - @SuppressWarnings("unused") + /** + * @deprecated use {@link AbstractEventExecutor.LazyRunnable} + */ + @Deprecated + protected interface NonWakeupRunnable extends LazyRunnable { } + + /** + * Can be overridden to control which tasks require waking the {@link EventExecutor} thread + * if it is waiting so that they can be run immediately. + */ protected boolean wakesUpForTask(Runnable task) { return true; } @@ -861,11 +942,14 @@ public abstract class SingleThreadEventExecutor extends AbstractScheduledEventEx private void startThread() { if (state == ST_NOT_STARTED) { if (STATE_UPDATER.compareAndSet(this, ST_NOT_STARTED, ST_STARTED)) { + boolean success = false; try { doStartThread(); - } catch (Throwable cause) { - STATE_UPDATER.set(this, ST_NOT_STARTED); - PlatformDependent.throwException(cause); + success = true; + } finally { + if (!success) { + STATE_UPDATER.compareAndSet(this, ST_STARTED, ST_NOT_STARTED); + } } } } @@ -925,12 +1009,28 @@ public abstract class SingleThreadEventExecutor extends AbstractScheduledEventEx } try { - // Run all remaining tasks and shutdown hooks. + // Run all remaining tasks and shutdown hooks. At this point the event loop + // is in ST_SHUTTING_DOWN state still accepting tasks which is needed for + // graceful shutdown with quietPeriod. for (;;) { if (confirmShutdown()) { break; } } + + // Now we want to make sure no more tasks can be added from this point. This is + // achieved by switching the state. Any new tasks beyond this point will be rejected. + for (;;) { + int oldState = state; + if (oldState >= ST_SHUTDOWN || STATE_UPDATER.compareAndSet( + SingleThreadEventExecutor.this, oldState, ST_SHUTDOWN)) { + break; + } + } + + // We have the final set of tasks in the queue now, no more can be added, run all remaining. + // No need to loop here, this is the final pass. + confirmShutdown(); } finally { try { cleanup(); @@ -942,12 +1042,11 @@ public abstract class SingleThreadEventExecutor extends AbstractScheduledEventEx FastThreadLocal.removeAll(); STATE_UPDATER.set(SingleThreadEventExecutor.this, ST_TERMINATED); - threadLock.release(); - if (!taskQueue.isEmpty()) { - if (logger.isWarnEnabled()) { - logger.warn("An event executor terminated with " + - "non-empty task queue (" + taskQueue.size() + ')'); - } + threadLock.countDown(); + int numUserTasks = drainTasks(); + if (numUserTasks > 0 && logger.isWarnEnabled()) { + logger.warn("An event executor terminated with " + + "non-empty task queue (" + numUserTasks + ')'); } terminationFuture.setSuccess(null); } @@ -957,6 +1056,22 @@ public abstract class SingleThreadEventExecutor extends AbstractScheduledEventEx }); } + final int drainTasks() { + int numTasks = 0; + for (;;) { + Runnable runnable = taskQueue.poll(); + if (runnable == null) { + break; + } + // WAKEUP_TASK should be just discarded as these are added internally. + // The important bit is that we not have any user tasks left. + if (WAKEUP_TASK != runnable) { + numTasks++; + } + } + return numTasks; + } + private static final class DefaultThreadProperties implements ThreadProperties { private final Thread t; diff --git a/common/src/main/java/io/netty/util/concurrent/ThreadPerTaskExecutor.java b/common/src/main/java/io/netty/util/concurrent/ThreadPerTaskExecutor.java index 21210ae..f1a2b19 100644 --- a/common/src/main/java/io/netty/util/concurrent/ThreadPerTaskExecutor.java +++ b/common/src/main/java/io/netty/util/concurrent/ThreadPerTaskExecutor.java @@ -15,6 +15,8 @@ */ package io.netty.util.concurrent; +import io.netty.util.internal.ObjectUtil; + import java.util.concurrent.Executor; import java.util.concurrent.ThreadFactory; @@ -22,10 +24,7 @@ public final class ThreadPerTaskExecutor implements Executor { private final ThreadFactory threadFactory; public ThreadPerTaskExecutor(ThreadFactory threadFactory) { - if (threadFactory == null) { - throw new NullPointerException("threadFactory"); - } - this.threadFactory = threadFactory; + this.threadFactory = ObjectUtil.checkNotNull(threadFactory, "threadFactory"); } @Override diff --git a/common/src/main/java/io/netty/util/concurrent/UnorderedThreadPoolEventExecutor.java b/common/src/main/java/io/netty/util/concurrent/UnorderedThreadPoolEventExecutor.java index 4ed94da..277c903 100644 --- a/common/src/main/java/io/netty/util/concurrent/UnorderedThreadPoolEventExecutor.java +++ b/common/src/main/java/io/netty/util/concurrent/UnorderedThreadPoolEventExecutor.java @@ -215,7 +215,7 @@ public final class UnorderedThreadPoolEventExecutor extends ScheduledThreadPoolE RunnableScheduledFutureTask(EventExecutor executor, Runnable runnable, RunnableScheduledFuture<V> future) { - super(executor, runnable, null); + super(executor, runnable); this.future = future; } @@ -232,7 +232,7 @@ public final class UnorderedThreadPoolEventExecutor extends ScheduledThreadPoolE } else if (!isDone()) { try { // Its a periodic task so we need to ignore the return value - task.call(); + runTask(); } catch (Throwable cause) { if (!tryFailureInternal(cause)) { logger.warn("Failure during execution of task", cause); diff --git a/common/src/main/java/io/netty/util/internal/AppendableCharSequence.java b/common/src/main/java/io/netty/util/internal/AppendableCharSequence.java index 408c32f..2e44b33 100644 --- a/common/src/main/java/io/netty/util/internal/AppendableCharSequence.java +++ b/common/src/main/java/io/netty/util/internal/AppendableCharSequence.java @@ -37,6 +37,13 @@ public final class AppendableCharSequence implements CharSequence, Appendable { pos = chars.length; } + public void setLength(int length) { + if (length < 0 || length > pos) { + throw new IllegalArgumentException("length: " + length + " (length: >= 0, <= " + pos + ')'); + } + this.pos = length; + } + @Override public int length() { return pos; @@ -63,6 +70,12 @@ public final class AppendableCharSequence implements CharSequence, Appendable { @Override public AppendableCharSequence subSequence(int start, int end) { + if (start == end) { + // If start and end index is the same we need to return an empty sequence to conform to the interface. + // As our expanding logic depends on the fact that we have a char[] with length > 0 we need to construct + // an instance for which this is true. + return new AppendableCharSequence(Math.min(16, chars.length)); + } return new AppendableCharSequence(Arrays.copyOfRange(chars, start, end)); } diff --git a/common/src/main/java/io/netty/util/internal/Hidden.java b/common/src/main/java/io/netty/util/internal/Hidden.java new file mode 100644 index 0000000..65887d3 --- /dev/null +++ b/common/src/main/java/io/netty/util/internal/Hidden.java @@ -0,0 +1,105 @@ +/* + * Copyright 2019 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.util.internal; + +import io.netty.util.concurrent.FastThreadLocalThread; +import reactor.blockhound.BlockHound; +import reactor.blockhound.integration.BlockHoundIntegration; + +import java.util.function.Function; +import java.util.function.Predicate; + +/** + * Contains classes that must be have public visibility but are not public API. + */ +class Hidden { + + /** + * This class integrates Netty with BlockHound. + * <p> + * It is public but only because of the ServiceLoader's limitations + * and SHOULD NOT be considered a public API. + */ + @UnstableApi + @SuppressJava6Requirement(reason = "BlockHound is Java 8+, but this class is only loaded by it's SPI") + public static final class NettyBlockHoundIntegration implements BlockHoundIntegration { + + @Override + public void applyTo(BlockHound.Builder builder) { + builder.allowBlockingCallsInside( + "io.netty.channel.nio.NioEventLoop", + "handleLoopException" + ); + + builder.allowBlockingCallsInside( + "io.netty.channel.kqueue.KQueueEventLoop", + "handleLoopException" + ); + + builder.allowBlockingCallsInside( + "io.netty.channel.epoll.EpollEventLoop", + "handleLoopException" + ); + + builder.allowBlockingCallsInside( + "io.netty.util.HashedWheelTimer$Worker", + "waitForNextTick" + ); + + builder.allowBlockingCallsInside( + "io.netty.util.concurrent.SingleThreadEventExecutor", + "confirmShutdown" + ); + + builder.allowBlockingCallsInside( + "io.netty.handler.ssl.SslHandler", + "handshake" + ); + + builder.allowBlockingCallsInside( + "io.netty.handler.ssl.SslHandler", + "runAllDelegatedTasks" + ); + + builder.allowBlockingCallsInside( + "io.netty.util.concurrent.GlobalEventExecutor", + "takeTask"); + + builder.allowBlockingCallsInside( + "io.netty.util.concurrent.SingleThreadEventExecutor", + "takeTask"); + + builder.nonBlockingThreadPredicate(new Function<Predicate<Thread>, Predicate<Thread>>() { + @Override + public Predicate<Thread> apply(final Predicate<Thread> p) { + return new Predicate<Thread>() { + @Override + @SuppressJava6Requirement(reason = "Predicate#test") + public boolean test(Thread thread) { + return p.test(thread) || thread instanceof FastThreadLocalThread; + } + }; + } + }); + } + + @Override + public int compareTo(BlockHoundIntegration o) { + return 0; + } + } +} diff --git a/common/src/main/java/io/netty/util/internal/InternalThreadLocalMap.java b/common/src/main/java/io/netty/util/internal/InternalThreadLocalMap.java index 0a6a6c5..c844b87 100644 --- a/common/src/main/java/io/netty/util/internal/InternalThreadLocalMap.java +++ b/common/src/main/java/io/netty/util/internal/InternalThreadLocalMap.java @@ -43,6 +43,8 @@ public final class InternalThreadLocalMap extends UnpaddedInternalThreadLocalMap private static final int DEFAULT_ARRAY_LIST_INITIAL_CAPACITY = 8; private static final int STRING_BUILDER_INITIAL_SIZE; private static final int STRING_BUILDER_MAX_SIZE; + private static final int HANDLER_SHARABLE_CACHE_INITIAL_CAPACITY = 4; + private static final int INDEXED_VARIABLE_TABLE_INITIAL_SIZE = 32; public static final Object UNSET = new Object(); @@ -127,7 +129,7 @@ public final class InternalThreadLocalMap extends UnpaddedInternalThreadLocalMap } private static Object[] newIndexedVariableTable() { - Object[] array = new Object[32]; + Object[] array = new Object[INDEXED_VARIABLE_TABLE_INITIAL_SIZE]; Arrays.fill(array, UNSET); return array; } @@ -271,7 +273,7 @@ public final class InternalThreadLocalMap extends UnpaddedInternalThreadLocalMap Map<Class<?>, Boolean> cache = handlerSharableCache; if (cache == null) { // Start with small capacity to keep memory overhead as low as possible. - handlerSharableCache = cache = new WeakHashMap<Class<?>, Boolean>(4); + handlerSharableCache = cache = new WeakHashMap<Class<?>, Boolean>(HANDLER_SHARABLE_CACHE_INITIAL_CAPACITY); } return cache; } diff --git a/common/src/main/java/io/netty/util/internal/LongAdderCounter.java b/common/src/main/java/io/netty/util/internal/LongAdderCounter.java index f7eeb81..b5dbdba 100644 --- a/common/src/main/java/io/netty/util/internal/LongAdderCounter.java +++ b/common/src/main/java/io/netty/util/internal/LongAdderCounter.java @@ -17,6 +17,7 @@ package io.netty.util.internal; import java.util.concurrent.atomic.LongAdder; +@SuppressJava6Requirement(reason = "Usage guarded by java version check") final class LongAdderCounter extends LongAdder implements LongCounter { @Override diff --git a/common/src/main/java/io/netty/util/internal/NativeLibraryLoader.java b/common/src/main/java/io/netty/util/internal/NativeLibraryLoader.java index 31b4a46..b059446 100644 --- a/common/src/main/java/io/netty/util/internal/NativeLibraryLoader.java +++ b/common/src/main/java/io/netty/util/internal/NativeLibraryLoader.java @@ -137,9 +137,11 @@ public final class NativeLibraryLoader { return; } catch (Throwable ex) { suppressed.add(ex); - logger.debug( - "{} cannot be loaded from java.libary.path, " - + "now trying export to -Dio.netty.native.workdir: {}", name, WORKDIR, ex); + if (logger.isDebugEnabled()) { + logger.debug( + "{} cannot be loaded from java.library.path, " + + "now trying export to -Dio.netty.native.workdir: {}", name, WORKDIR, ex); + } } String libname = System.mapLibraryName(name); @@ -178,34 +180,22 @@ public final class NativeLibraryLoader { int index = libname.lastIndexOf('.'); String prefix = libname.substring(0, index); - String suffix = libname.substring(index, libname.length()); + String suffix = libname.substring(index); tmpFile = File.createTempFile(prefix, suffix, WORKDIR); in = url.openStream(); out = new FileOutputStream(tmpFile); - byte[] buffer = new byte[8192]; - int length; - if (TRY_TO_PATCH_SHADED_ID && PlatformDependent.isOsx() && !packagePrefix.isEmpty()) { - // We read the whole native lib into memory to make it easier to monkey-patch the id. - ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(in.available()); - - while ((length = in.read(buffer)) > 0) { - byteArrayOutputStream.write(buffer, 0, length); - } - byteArrayOutputStream.flush(); - byte[] bytes = byteArrayOutputStream.toByteArray(); - byteArrayOutputStream.close(); - - // Try to patch the library id. - patchShadedLibraryId(bytes, originalName, name); - - out.write(bytes); + if (shouldShadedLibraryIdBePatched(packagePrefix)) { + patchShadedLibraryId(in, out, originalName, name); } else { + byte[] buffer = new byte[8192]; + int length; while ((length = in.read(buffer)) > 0) { out.write(buffer, 0, length); } } + out.flush(); // Close the output stream before loading the unpacked library, @@ -217,10 +207,13 @@ public final class NativeLibraryLoader { try { if (tmpFile != null && tmpFile.isFile() && tmpFile.canRead() && !NoexecVolumeDetector.canExecuteExecutable(tmpFile)) { + // Pass "io.netty.native.workdir" as an argument to allow shading tools to see + // the string. Since this is printed out to users to tell them what to do next, + // we want the value to be correct even when shading. logger.info("{} exists but cannot be executed even when execute permissions set; " + - "check volume for \"noexec\" flag; use -Dio.netty.native.workdir=[path] " + + "check volume for \"noexec\" flag; use -D{}=[path] " + "to set native working directory separately.", - tmpFile.getPath()); + tmpFile.getPath(), "io.netty.native.workdir"); } } catch (Throwable t) { suppressed.add(t); @@ -246,10 +239,50 @@ public final class NativeLibraryLoader { } } + // Package-private for testing. + static boolean patchShadedLibraryId(InputStream in, OutputStream out, String originalName, String name) + throws IOException { + byte[] buffer = new byte[8192]; + int length; + // We read the whole native lib into memory to make it easier to monkey-patch the id. + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(in.available()); + + while ((length = in.read(buffer)) > 0) { + byteArrayOutputStream.write(buffer, 0, length); + } + byteArrayOutputStream.flush(); + byte[] bytes = byteArrayOutputStream.toByteArray(); + byteArrayOutputStream.close(); + + final boolean patched; + // Try to patch the library id. + if (!patchShadedLibraryId(bytes, originalName, name)) { + // We did not find the Id, check if we used a originalName that has the os and arch as suffix. + // If this is the case we should also try to patch with the os and arch suffix removed. + String os = PlatformDependent.normalizedOs(); + String arch = PlatformDependent.normalizedArch(); + String osArch = "_" + os + "_" + arch; + if (originalName.endsWith(osArch)) { + patched = patchShadedLibraryId(bytes, + originalName.substring(0, originalName.length() - osArch.length()), name); + } else { + patched = false; + } + } else { + patched = true; + } + out.write(bytes, 0, bytes.length); + return patched; + } + + private static boolean shouldShadedLibraryIdBePatched(String packagePrefix) { + return TRY_TO_PATCH_SHADED_ID && PlatformDependent.isOsx() && !packagePrefix.isEmpty(); + } + /** * Try to patch shaded library to ensure it uses a unique ID. */ - private static void patchShadedLibraryId(byte[] bytes, String originalName, String name) { + private static boolean patchShadedLibraryId(byte[] bytes, String originalName, String name) { // Our native libs always have the name as part of their id so we can search for it and replace it // to make the ID unique if shading is used. byte[] nameBytes = originalName.getBytes(CharsetUtil.UTF_8); @@ -275,6 +308,7 @@ public final class NativeLibraryLoader { if (idIdx == -1) { logger.debug("Was not able to find the ID of the shaded native library {}, can't adjust it.", name); + return false; } else { // We found our ID... now monkey-patch it! for (int i = 0; i < nameBytes.length; i++) { @@ -288,6 +322,7 @@ public final class NativeLibraryLoader { "Found the ID of the shaded native library {}. Replacing ID part {} with {}", name, originalName, new String(bytes, idIdx, nameBytes.length, CharsetUtil.UTF_8)); } + return true; } } @@ -449,6 +484,7 @@ public final class NativeLibraryLoader { private static final class NoexecVolumeDetector { + @SuppressJava6Requirement(reason = "Usage guarded by java version check") private static boolean canExecuteExecutable(File file) throws IOException { if (PlatformDependent.javaVersion() < 7) { // Pre-JDK7, the Java API did not directly support POSIX permissions; instead of implementing a custom diff --git a/common/src/main/java/io/netty/util/internal/ObjectPool.java b/common/src/main/java/io/netty/util/internal/ObjectPool.java new file mode 100644 index 0000000..d0f0e03 --- /dev/null +++ b/common/src/main/java/io/netty/util/internal/ObjectPool.java @@ -0,0 +1,87 @@ +/* + * Copyright 2019 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.util.internal; + +import io.netty.util.Recycler; + +/** + * Light-weight object pool. + * + * @param <T> the type of the pooled object + */ +public abstract class ObjectPool<T> { + + ObjectPool() { } + + /** + * Get a {@link Object} from the {@link ObjectPool}. The returned {@link Object} may be created via + * {@link ObjectCreator#newObject(Handle)} if no pooled {@link Object} is ready to be reused. + */ + public abstract T get(); + + /** + * Handle for an pooled {@link Object} that will be used to notify the {@link ObjectPool} once it can + * reuse the pooled {@link Object} again. + * @param <T> + */ + public interface Handle<T> { + /** + * Recycle the {@link Object} if possible and so make it ready to be reused. + */ + void recycle(T self); + } + + /** + * Creates a new Object which references the given {@link Handle} and calls {@link Handle#recycle(Object)} once + * it can be re-used. + * + * @param <T> the type of the pooled object + */ + public interface ObjectCreator<T> { + + /** + * Creates an returns a new {@link Object} that can be used and later recycled via + * {@link Handle#recycle(Object)}. + */ + T newObject(Handle<T> handle); + } + + /** + * Creates a new {@link ObjectPool} which will use the given {@link ObjectCreator} to create the {@link Object} + * that should be pooled. + */ + public static <T> ObjectPool<T> newPool(final ObjectCreator<T> creator) { + return new RecyclerObjectPool<T>(ObjectUtil.checkNotNull(creator, "creator")); + } + + private static final class RecyclerObjectPool<T> extends ObjectPool<T> { + private final Recycler<T> recycler; + + RecyclerObjectPool(final ObjectCreator<T> creator) { + recycler = new Recycler<T>() { + @Override + protected T newObject(Handle<T> handle) { + return creator.newObject(handle); + } + }; + } + + @Override + public T get() { + return recycler.get(); + } + } +} diff --git a/common/src/main/java/io/netty/util/internal/PendingWrite.java b/common/src/main/java/io/netty/util/internal/PendingWrite.java index 6f27cf5..f84826c 100644 --- a/common/src/main/java/io/netty/util/internal/PendingWrite.java +++ b/common/src/main/java/io/netty/util/internal/PendingWrite.java @@ -15,20 +15,21 @@ */ package io.netty.util.internal; -import io.netty.util.Recycler; import io.netty.util.ReferenceCountUtil; import io.netty.util.concurrent.Promise; +import io.netty.util.internal.ObjectPool.Handle; +import io.netty.util.internal.ObjectPool.ObjectCreator; /** * Some pending write which should be picked up later. */ public final class PendingWrite { - private static final Recycler<PendingWrite> RECYCLER = new Recycler<PendingWrite>() { + private static final ObjectPool<PendingWrite> RECYCLER = ObjectPool.newPool(new ObjectCreator<PendingWrite>() { @Override - protected PendingWrite newObject(Handle<PendingWrite> handle) { + public PendingWrite newObject(Handle<PendingWrite> handle) { return new PendingWrite(handle); } - }; + }); /** * Create a new empty {@link RecyclableArrayList} instance @@ -40,11 +41,11 @@ public final class PendingWrite { return pending; } - private final Recycler.Handle<PendingWrite> handle; + private final Handle<PendingWrite> handle; private Object msg; private Promise<Void> promise; - private PendingWrite(Recycler.Handle<PendingWrite> handle) { + private PendingWrite(Handle<PendingWrite> handle) { this.handle = handle; } diff --git a/common/src/main/java/io/netty/util/internal/PlatformDependent.java b/common/src/main/java/io/netty/util/internal/PlatformDependent.java index 1baeecb..80808ed 100644 --- a/common/src/main/java/io/netty/util/internal/PlatformDependent.java +++ b/common/src/main/java/io/netty/util/internal/PlatformDependent.java @@ -15,6 +15,7 @@ */ package io.netty.util.internal; +import io.netty.util.CharsetUtil; import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; import org.jctools.queues.MpscArrayQueue; @@ -28,19 +29,28 @@ import org.jctools.queues.atomic.SpscLinkedAtomicQueue; import org.jctools.util.Pow2; import org.jctools.util.UnsafeAccess; +import java.io.BufferedReader; import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStreamReader; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.security.AccessController; import java.security.PrivilegedAction; +import java.util.Arrays; +import java.util.Collections; import java.util.Deque; +import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Queue; import java.util.Random; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedDeque; import java.util.concurrent.ConcurrentMap; @@ -74,6 +84,8 @@ public final class PlatformDependent { private static final boolean IS_WINDOWS = isWindows0(); private static final boolean IS_OSX = isOsx0(); + private static final boolean IS_J9_JVM = isJ9Jvm0(); + private static final boolean IS_IVKVM_DOT_NET = isIkvmDotNet0(); private static final boolean MAYBE_SUPER_USER; @@ -95,6 +107,10 @@ public final class PlatformDependent { private static final String NORMALIZED_ARCH = normalizeArch(SystemPropertyUtil.get("os.arch", "")); private static final String NORMALIZED_OS = normalizeOs(SystemPropertyUtil.get("os.name", "")); + // keep in sync with maven's pom.xml via os.detection.classifierWithLikes! + private static final String[] ALLOWED_LINUX_OS_CLASSIFIERS = {"fedora", "suse", "arch"}; + private static final Set<String> LINUX_OS_CLASSIFIERS; + private static final int ADDRESS_SIZE = addressSize0(); private static final boolean USE_DIRECT_BUFFER_NO_CLEANER; private static final AtomicLong DIRECT_MEMORY_COUNTER; @@ -102,7 +118,10 @@ public final class PlatformDependent { private static final ThreadLocalRandomProvider RANDOM_PROVIDER; private static final Cleaner CLEANER; private static final int UNINITIALIZED_ARRAY_ALLOCATION_THRESHOLD; - + // For specifications, see https://www.freedesktop.org/software/systemd/man/os-release.html + private static final String[] OS_RELEASE_FILES = {"/etc/os-release", "/usr/lib/os-release"}; + private static final String LINUX_ID_PREFIX = "ID="; + private static final String LINUX_ID_LIKE_PREFIX = "ID_LIKE="; public static final boolean BIG_ENDIAN_NATIVE_ORDER = ByteOrder.nativeOrder() == ByteOrder.BIG_ENDIAN; private static final Cleaner NOOP = new Cleaner() { @@ -116,6 +135,7 @@ public final class PlatformDependent { if (javaVersion() >= 7) { RANDOM_PROVIDER = new ThreadLocalRandomProvider() { @Override + @SuppressJava6Requirement(reason = "Usage guarded by java version check") public Random current() { return java.util.concurrent.ThreadLocalRandom.current(); } @@ -194,6 +214,63 @@ public final class PlatformDependent { "Unless explicitly requested, heap buffer will always be preferred to avoid potential system " + "instability."); } + + final Set<String> allowedClassifiers = Collections.unmodifiableSet( + new HashSet<String>(Arrays.asList(ALLOWED_LINUX_OS_CLASSIFIERS))); + final Set<String> availableClassifiers = new LinkedHashSet<String>(); + for (final String osReleaseFileName : OS_RELEASE_FILES) { + final File file = new File(osReleaseFileName); + boolean found = AccessController.doPrivileged(new PrivilegedAction<Boolean>() { + @Override + public Boolean run() { + try { + if (file.exists()) { + BufferedReader reader = null; + try { + reader = new BufferedReader( + new InputStreamReader( + new FileInputStream(file), CharsetUtil.UTF_8)); + + String line; + while ((line = reader.readLine()) != null) { + if (line.startsWith(LINUX_ID_PREFIX)) { + String id = normalizeOsReleaseVariableValue( + line.substring(LINUX_ID_PREFIX.length())); + addClassifier(allowedClassifiers, availableClassifiers, id); + } else if (line.startsWith(LINUX_ID_LIKE_PREFIX)) { + line = normalizeOsReleaseVariableValue( + line.substring(LINUX_ID_LIKE_PREFIX.length())); + addClassifier(allowedClassifiers, availableClassifiers, line.split("[ ]+")); + } + } + } catch (SecurityException e) { + logger.debug("Unable to read {}", osReleaseFileName, e); + } catch (IOException e) { + logger.debug("Error while reading content of {}", osReleaseFileName, e); + } finally { + if (reader != null) { + try { + reader.close(); + } catch (IOException ignored) { + // Ignore + } + } + } + // specification states we should only fall back if /etc/os-release does not exist + return true; + } + } catch (SecurityException e) { + logger.debug("Unable to check if {} exists", osReleaseFileName, e); + } + return false; + } + }); + + if (found) { + break; + } + } + LINUX_OS_CLASSIFIERS = Collections.unmodifiableSet(availableClassifiers); } public static boolean hasDirectBufferNoCleanerConstructor() { @@ -287,6 +364,16 @@ public final class PlatformDependent { return DIRECT_MEMORY_LIMIT; } + /** + * Returns the current memory reserved for direct buffer allocation. + * This method returns -1 in case that a value is not available. + * + * @see #maxDirectMemory() + */ + public static long usedDirectMemory() { + return DIRECT_MEMORY_COUNTER != null ? DIRECT_MEMORY_COUNTER.get() : -1; + } + /** * Returns the temporary directory. */ @@ -753,83 +840,43 @@ public final class PlatformDependent { * The resulting hash code will be case insensitive. */ public static int hashCodeAscii(CharSequence bytes) { + final int length = bytes.length(); + final int remainingBytes = length & 7; int hash = HASH_CODE_ASCII_SEED; - final int remainingBytes = bytes.length() & 7; // Benchmarking shows that by just naively looping for inputs 8~31 bytes long we incur a relatively large // performance penalty (only achieve about 60% performance of loop which iterates over each char). So because // of this we take special provisions to unroll the looping for these conditions. - switch (bytes.length()) { - case 31: - case 30: - case 29: - case 28: - case 27: - case 26: - case 25: - case 24: - hash = hashCodeAsciiCompute(bytes, bytes.length() - 24, - hashCodeAsciiCompute(bytes, bytes.length() - 16, - hashCodeAsciiCompute(bytes, bytes.length() - 8, hash))); - break; - case 23: - case 22: - case 21: - case 20: - case 19: - case 18: - case 17: - case 16: - hash = hashCodeAsciiCompute(bytes, bytes.length() - 16, - hashCodeAsciiCompute(bytes, bytes.length() - 8, hash)); - break; - case 15: - case 14: - case 13: - case 12: - case 11: - case 10: - case 9: - case 8: - hash = hashCodeAsciiCompute(bytes, bytes.length() - 8, hash); - break; - case 7: - case 6: - case 5: - case 4: - case 3: - case 2: - case 1: - case 0: - break; - default: - for (int i = bytes.length() - 8; i >= remainingBytes; i -= 8) { - hash = hashCodeAsciiCompute(bytes, i, hash); + if (length >= 32) { + for (int i = length - 8; i >= remainingBytes; i -= 8) { + hash = hashCodeAsciiCompute(bytes, i, hash); + } + } else if (length >= 8) { + hash = hashCodeAsciiCompute(bytes, length - 8, hash); + if (length >= 16) { + hash = hashCodeAsciiCompute(bytes, length - 16, hash); + if (length >= 24) { + hash = hashCodeAsciiCompute(bytes, length - 24, hash); } - break; + } } - switch(remainingBytes) { - case 7: - return ((hash * HASH_CODE_C1 + hashCodeAsciiSanitizeByte(bytes.charAt(0))) - * HASH_CODE_C2 + hashCodeAsciiSanitizeShort(bytes, 1)) - * HASH_CODE_C1 + hashCodeAsciiSanitizeInt(bytes, 3); - case 6: - return (hash * HASH_CODE_C1 + hashCodeAsciiSanitizeShort(bytes, 0)) - * HASH_CODE_C2 + hashCodeAsciiSanitizeInt(bytes, 2); - case 5: - return (hash * HASH_CODE_C1 + hashCodeAsciiSanitizeByte(bytes.charAt(0))) - * HASH_CODE_C2 + hashCodeAsciiSanitizeInt(bytes, 1); - case 4: - return hash * HASH_CODE_C1 + hashCodeAsciiSanitizeInt(bytes, 0); - case 3: - return (hash * HASH_CODE_C1 + hashCodeAsciiSanitizeByte(bytes.charAt(0))) - * HASH_CODE_C2 + hashCodeAsciiSanitizeShort(bytes, 1); - case 2: - return hash * HASH_CODE_C1 + hashCodeAsciiSanitizeShort(bytes, 0); - case 1: - return hash * HASH_CODE_C1 + hashCodeAsciiSanitizeByte(bytes.charAt(0)); - default: - return hash; + if (remainingBytes == 0) { + return hash; + } + int offset = 0; + if (remainingBytes != 2 & remainingBytes != 4 & remainingBytes != 6) { // 1, 3, 5, 7 + hash = hash * HASH_CODE_C1 + hashCodeAsciiSanitizeByte(bytes.charAt(0)); + offset = 1; + } + if (remainingBytes != 1 & remainingBytes != 4 & remainingBytes != 5) { // 2, 3, 6, 7 + hash = hash * (offset == 0 ? HASH_CODE_C1 : HASH_CODE_C2) + + hashCodeAsciiSanitize(hashCodeAsciiSanitizeShort(bytes, offset)); + offset += 2; } + if (remainingBytes >= 4) { // 4, 5, 6, 7 + return hash * ((offset == 0 | offset == 3) ? HASH_CODE_C1 : HASH_CODE_C2) + + hashCodeAsciiSanitizeInt(bytes, offset); + } + return hash; } private static final class Mpsc { @@ -934,6 +981,7 @@ public final class PlatformDependent { /** * Returns a new concurrent {@link Deque}. */ + @SuppressJava6Requirement(reason = "Usage guarded by java version check") public static <C> Deque<C> newConcurrentDeque() { if (javaVersion() < 7) { return new LinkedBlockingDeque<C>(); @@ -982,6 +1030,12 @@ public final class PlatformDependent { logger.debug("sun.misc.Unsafe: unavailable (Android)"); return new UnsupportedOperationException("sun.misc.Unsafe: unavailable (Android)"); } + + if (isIkvmDotNet()) { + logger.debug("sun.misc.Unsafe: unavailable (IKVM.NET)"); + return new UnsupportedOperationException("sun.misc.Unsafe: unavailable (IKVM.NET)"); + } + Throwable cause = PlatformDependent0.getUnsafeUnavailabilityCause(); if (cause != null) { return cause; @@ -998,6 +1052,31 @@ public final class PlatformDependent { } } + /** + * Returns {@code true} if the running JVM is either <a href="https://developer.ibm.com/javasdk/">IBM J9</a> or + * <a href="https://www.eclipse.org/openj9/">Eclipse OpenJ9</a>, {@code false} otherwise. + */ + public static boolean isJ9Jvm() { + return IS_J9_JVM; + } + + private static boolean isJ9Jvm0() { + String vmName = SystemPropertyUtil.get("java.vm.name", "").toLowerCase(); + return vmName.startsWith("ibm j9") || vmName.startsWith("eclipse openj9"); + } + + /** + * Returns {@code true} if the running JVM is <a href="https://www.ikvm.net">IKVM.NET</a>, {@code false} otherwise. + */ + public static boolean isIkvmDotNet() { + return IS_IVKVM_DOT_NET; + } + + private static boolean isIkvmDotNet0() { + String vmName = SystemPropertyUtil.get("java.vm.name", "").toUpperCase(Locale.US); + return vmName.equals("IKVM.NET"); + } + private static long maxDirectMemory0() { long maxDirectMemory = 0; @@ -1185,8 +1264,8 @@ public final class PlatformDependent { // Last resort: guess from VM name and then fall back to most common 64-bit mode. String vm = SystemPropertyUtil.get("java.vm.name", "").toLowerCase(Locale.US); - Pattern BIT_PATTERN = Pattern.compile("([1-9][0-9]+)-?bit"); - Matcher m = BIT_PATTERN.matcher(vm); + Pattern bitPattern = Pattern.compile("([1-9][0-9]+)-?bit"); + Matcher m = bitPattern.matcher(vm); if (m.find()) { return Integer.parseInt(m.group(1)); } else { @@ -1271,6 +1350,30 @@ public final class PlatformDependent { return NORMALIZED_OS; } + public static Set<String> normalizedLinuxClassifiers() { + return LINUX_OS_CLASSIFIERS; + } + + /** + * Adds only those classifier strings to <tt>dest</tt> which are present in <tt>allowed</tt>. + * + * @param allowed allowed classifiers + * @param dest destination set + * @param maybeClassifiers potential classifiers to add + */ + private static void addClassifier(Set<String> allowed, Set<String> dest, String... maybeClassifiers) { + for (String id : maybeClassifiers) { + if (allowed.contains(id)) { + dest.add(id); + } + } + } + + private static String normalizeOsReleaseVariableValue(String value) { + // Variable assignment values may be enclosed in double or single quotes. + return value.trim().replaceAll("[\"']", ""); + } + private static String normalize(String value) { return value.toLowerCase(Locale.US).replaceAll("[^a-z0-9]+", ""); } diff --git a/common/src/main/java/io/netty/util/internal/PlatformDependent0.java b/common/src/main/java/io/netty/util/internal/PlatformDependent0.java index df45d16..e6c8948 100644 --- a/common/src/main/java/io/netty/util/internal/PlatformDependent0.java +++ b/common/src/main/java/io/netty/util/internal/PlatformDependent0.java @@ -33,6 +33,7 @@ import static io.netty.util.internal.ObjectUtil.checkNotNull; /** * The {@link PlatformDependent} operations which requires access to {@code sun.misc.*}. */ +@SuppressJava6Requirement(reason = "Unsafe access is guarded") final class PlatformDependent0 { private static final InternalLogger logger = InternalLoggerFactory.getInstance(PlatformDependent0.class); @@ -622,72 +623,57 @@ final class PlatformDependent0 { } static boolean equals(byte[] bytes1, int startPos1, byte[] bytes2, int startPos2, int length) { - if (length <= 0) { - return true; - } - final long baseOffset1 = BYTE_ARRAY_BASE_OFFSET + startPos1; - final long baseOffset2 = BYTE_ARRAY_BASE_OFFSET + startPos2; int remainingBytes = length & 7; - final long end = baseOffset1 + remainingBytes; - for (long i = baseOffset1 - 8 + length, j = baseOffset2 - 8 + length; i >= end; i -= 8, j -= 8) { - if (UNSAFE.getLong(bytes1, i) != UNSAFE.getLong(bytes2, j)) { - return false; + final long baseOffset1 = BYTE_ARRAY_BASE_OFFSET + startPos1; + final long diff = startPos2 - startPos1; + if (length >= 8) { + final long end = baseOffset1 + remainingBytes; + for (long i = baseOffset1 - 8 + length; i >= end; i -= 8) { + if (UNSAFE.getLong(bytes1, i) != UNSAFE.getLong(bytes2, i + diff)) { + return false; + } } } - if (remainingBytes >= 4) { remainingBytes -= 4; - if (UNSAFE.getInt(bytes1, baseOffset1 + remainingBytes) != - UNSAFE.getInt(bytes2, baseOffset2 + remainingBytes)) { + long pos = baseOffset1 + remainingBytes; + if (UNSAFE.getInt(bytes1, pos) != UNSAFE.getInt(bytes2, pos + diff)) { return false; } } + final long baseOffset2 = baseOffset1 + diff; if (remainingBytes >= 2) { return UNSAFE.getChar(bytes1, baseOffset1) == UNSAFE.getChar(bytes2, baseOffset2) && - (remainingBytes == 2 || bytes1[startPos1 + 2] == bytes2[startPos2 + 2]); + (remainingBytes == 2 || + UNSAFE.getByte(bytes1, baseOffset1 + 2) == UNSAFE.getByte(bytes2, baseOffset2 + 2)); } - return bytes1[startPos1] == bytes2[startPos2]; + return remainingBytes == 0 || + UNSAFE.getByte(bytes1, baseOffset1) == UNSAFE.getByte(bytes2, baseOffset2); } static int equalsConstantTime(byte[] bytes1, int startPos1, byte[] bytes2, int startPos2, int length) { long result = 0; + long remainingBytes = length & 7; final long baseOffset1 = BYTE_ARRAY_BASE_OFFSET + startPos1; - final long baseOffset2 = BYTE_ARRAY_BASE_OFFSET + startPos2; - final int remainingBytes = length & 7; final long end = baseOffset1 + remainingBytes; - for (long i = baseOffset1 - 8 + length, j = baseOffset2 - 8 + length; i >= end; i -= 8, j -= 8) { - result |= UNSAFE.getLong(bytes1, i) ^ UNSAFE.getLong(bytes2, j); - } - switch (remainingBytes) { - case 7: - return ConstantTimeUtils.equalsConstantTime(result | - (UNSAFE.getInt(bytes1, baseOffset1 + 3) ^ UNSAFE.getInt(bytes2, baseOffset2 + 3)) | - (UNSAFE.getChar(bytes1, baseOffset1 + 1) ^ UNSAFE.getChar(bytes2, baseOffset2 + 1)) | - (UNSAFE.getByte(bytes1, baseOffset1) ^ UNSAFE.getByte(bytes2, baseOffset2)), 0); - case 6: - return ConstantTimeUtils.equalsConstantTime(result | - (UNSAFE.getInt(bytes1, baseOffset1 + 2) ^ UNSAFE.getInt(bytes2, baseOffset2 + 2)) | - (UNSAFE.getChar(bytes1, baseOffset1) ^ UNSAFE.getChar(bytes2, baseOffset2)), 0); - case 5: - return ConstantTimeUtils.equalsConstantTime(result | - (UNSAFE.getInt(bytes1, baseOffset1 + 1) ^ UNSAFE.getInt(bytes2, baseOffset2 + 1)) | - (UNSAFE.getByte(bytes1, baseOffset1) ^ UNSAFE.getByte(bytes2, baseOffset2)), 0); - case 4: - return ConstantTimeUtils.equalsConstantTime(result | - (UNSAFE.getInt(bytes1, baseOffset1) ^ UNSAFE.getInt(bytes2, baseOffset2)), 0); - case 3: - return ConstantTimeUtils.equalsConstantTime(result | - (UNSAFE.getChar(bytes1, baseOffset1 + 1) ^ UNSAFE.getChar(bytes2, baseOffset2 + 1)) | - (UNSAFE.getByte(bytes1, baseOffset1) ^ UNSAFE.getByte(bytes2, baseOffset2)), 0); - case 2: - return ConstantTimeUtils.equalsConstantTime(result | - (UNSAFE.getChar(bytes1, baseOffset1) ^ UNSAFE.getChar(bytes2, baseOffset2)), 0); - case 1: - return ConstantTimeUtils.equalsConstantTime(result | - (UNSAFE.getByte(bytes1, baseOffset1) ^ UNSAFE.getByte(bytes2, baseOffset2)), 0); - default: - return ConstantTimeUtils.equalsConstantTime(result, 0); + final long diff = startPos2 - startPos1; + for (long i = baseOffset1 - 8 + length; i >= end; i -= 8) { + result |= UNSAFE.getLong(bytes1, i) ^ UNSAFE.getLong(bytes2, i + diff); } + if (remainingBytes >= 4) { + result |= UNSAFE.getInt(bytes1, baseOffset1) ^ UNSAFE.getInt(bytes2, baseOffset1 + diff); + remainingBytes -= 4; + } + if (remainingBytes >= 2) { + long pos = end - remainingBytes; + result |= UNSAFE.getChar(bytes1, pos) ^ UNSAFE.getChar(bytes2, pos + diff); + remainingBytes -= 2; + } + if (remainingBytes == 1) { + long pos = end - 1; + result |= UNSAFE.getByte(bytes1, pos) ^ UNSAFE.getByte(bytes2, pos + diff); + } + return ConstantTimeUtils.equalsConstantTime(result, 0); } static boolean isZero(byte[] bytes, int startPos, int length) { @@ -718,35 +704,30 @@ final class PlatformDependent0 { static int hashCodeAscii(byte[] bytes, int startPos, int length) { int hash = HASH_CODE_ASCII_SEED; - final long baseOffset = BYTE_ARRAY_BASE_OFFSET + startPos; + long baseOffset = BYTE_ARRAY_BASE_OFFSET + startPos; final int remainingBytes = length & 7; final long end = baseOffset + remainingBytes; for (long i = baseOffset - 8 + length; i >= end; i -= 8) { hash = hashCodeAsciiCompute(UNSAFE.getLong(bytes, i), hash); } - switch(remainingBytes) { - case 7: - return ((hash * HASH_CODE_C1 + hashCodeAsciiSanitize(UNSAFE.getByte(bytes, baseOffset))) - * HASH_CODE_C2 + hashCodeAsciiSanitize(UNSAFE.getShort(bytes, baseOffset + 1))) - * HASH_CODE_C1 + hashCodeAsciiSanitize(UNSAFE.getInt(bytes, baseOffset + 3)); - case 6: - return (hash * HASH_CODE_C1 + hashCodeAsciiSanitize(UNSAFE.getShort(bytes, baseOffset))) - * HASH_CODE_C2 + hashCodeAsciiSanitize(UNSAFE.getInt(bytes, baseOffset + 2)); - case 5: - return (hash * HASH_CODE_C1 + hashCodeAsciiSanitize(UNSAFE.getByte(bytes, baseOffset))) - * HASH_CODE_C2 + hashCodeAsciiSanitize(UNSAFE.getInt(bytes, baseOffset + 1)); - case 4: - return hash * HASH_CODE_C1 + hashCodeAsciiSanitize(UNSAFE.getInt(bytes, baseOffset)); - case 3: - return (hash * HASH_CODE_C1 + hashCodeAsciiSanitize(UNSAFE.getByte(bytes, baseOffset))) - * HASH_CODE_C2 + hashCodeAsciiSanitize(UNSAFE.getShort(bytes, baseOffset + 1)); - case 2: - return hash * HASH_CODE_C1 + hashCodeAsciiSanitize(UNSAFE.getShort(bytes, baseOffset)); - case 1: - return hash * HASH_CODE_C1 + hashCodeAsciiSanitize(UNSAFE.getByte(bytes, baseOffset)); - default: + if (remainingBytes == 0) { return hash; } + int hcConst = HASH_CODE_C1; + if (remainingBytes != 2 & remainingBytes != 4 & remainingBytes != 6) { // 1, 3, 5, 7 + hash = hash * HASH_CODE_C1 + hashCodeAsciiSanitize(UNSAFE.getByte(bytes, baseOffset)); + hcConst = HASH_CODE_C2; + baseOffset++; + } + if (remainingBytes != 1 & remainingBytes != 4 & remainingBytes != 5) { // 2, 3, 6, 7 + hash = hash * hcConst + hashCodeAsciiSanitize(UNSAFE.getShort(bytes, baseOffset)); + hcConst = hcConst == HASH_CODE_C1 ? HASH_CODE_C2 : HASH_CODE_C1; + baseOffset += 2; + } + if (remainingBytes >= 4) { // 4, 5, 6, 7 + return hash * hcConst + hashCodeAsciiSanitize(UNSAFE.getInt(bytes, baseOffset)); + } + return hash; } static int hashCodeAsciiCompute(long value, int hash) { diff --git a/common/src/main/java/io/netty/util/internal/PromiseNotificationUtil.java b/common/src/main/java/io/netty/util/internal/PromiseNotificationUtil.java index 42872a8..90a7c89 100644 --- a/common/src/main/java/io/netty/util/internal/PromiseNotificationUtil.java +++ b/common/src/main/java/io/netty/util/internal/PromiseNotificationUtil.java @@ -65,7 +65,7 @@ public final class PromiseNotificationUtil { Throwable err = p.cause(); if (err == null) { logger.warn("Failed to mark a promise as failure because it has succeeded already: {}", p, cause); - } else { + } else if (logger.isWarnEnabled()) { logger.warn( "Failed to mark a promise as failure because it has failed already: {}, unnotified cause: {}", p, ThrowableUtil.stackTraceToString(err), cause); diff --git a/common/src/main/java/io/netty/util/internal/ReadOnlyIterator.java b/common/src/main/java/io/netty/util/internal/ReadOnlyIterator.java index f4843e5..59880b1 100644 --- a/common/src/main/java/io/netty/util/internal/ReadOnlyIterator.java +++ b/common/src/main/java/io/netty/util/internal/ReadOnlyIterator.java @@ -22,10 +22,7 @@ public final class ReadOnlyIterator<T> implements Iterator<T> { private final Iterator<? extends T> iterator; public ReadOnlyIterator(Iterator<? extends T> iterator) { - if (iterator == null) { - throw new NullPointerException("iterator"); - } - this.iterator = iterator; + this.iterator = ObjectUtil.checkNotNull(iterator, "iterator"); } @Override diff --git a/common/src/main/java/io/netty/util/internal/RecyclableArrayList.java b/common/src/main/java/io/netty/util/internal/RecyclableArrayList.java index bf98f22..6a1d2c1 100644 --- a/common/src/main/java/io/netty/util/internal/RecyclableArrayList.java +++ b/common/src/main/java/io/netty/util/internal/RecyclableArrayList.java @@ -16,8 +16,8 @@ package io.netty.util.internal; -import io.netty.util.Recycler; -import io.netty.util.Recycler.Handle; +import io.netty.util.internal.ObjectPool.Handle; +import io.netty.util.internal.ObjectPool.ObjectCreator; import java.util.ArrayList; import java.util.Collection; @@ -33,12 +33,13 @@ public final class RecyclableArrayList extends ArrayList<Object> { private static final int DEFAULT_INITIAL_CAPACITY = 8; - private static final Recycler<RecyclableArrayList> RECYCLER = new Recycler<RecyclableArrayList>() { + private static final ObjectPool<RecyclableArrayList> RECYCLER = ObjectPool.newPool( + new ObjectCreator<RecyclableArrayList>() { @Override - protected RecyclableArrayList newObject(Handle<RecyclableArrayList> handle) { + public RecyclableArrayList newObject(Handle<RecyclableArrayList> handle) { return new RecyclableArrayList(handle); } - }; + }); private boolean insertSinceRecycled; @@ -110,10 +111,7 @@ public final class RecyclableArrayList extends ArrayList<Object> { @Override public boolean add(Object element) { - if (element == null) { - throw new NullPointerException("element"); - } - if (super.add(element)) { + if (super.add(ObjectUtil.checkNotNull(element, "element"))) { insertSinceRecycled = true; return true; } @@ -122,19 +120,13 @@ public final class RecyclableArrayList extends ArrayList<Object> { @Override public void add(int index, Object element) { - if (element == null) { - throw new NullPointerException("element"); - } - super.add(index, element); + super.add(index, ObjectUtil.checkNotNull(element, "element")); insertSinceRecycled = true; } @Override public Object set(int index, Object element) { - if (element == null) { - throw new NullPointerException("element"); - } - Object old = super.set(index, element); + Object old = super.set(index, ObjectUtil.checkNotNull(element, "element")); insertSinceRecycled = true; return old; } diff --git a/common/src/main/java/io/netty/util/internal/ReferenceCountUpdater.java b/common/src/main/java/io/netty/util/internal/ReferenceCountUpdater.java new file mode 100644 index 0000000..d6d131b --- /dev/null +++ b/common/src/main/java/io/netty/util/internal/ReferenceCountUpdater.java @@ -0,0 +1,179 @@ +/* + * Copyright 2019 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.util.internal; + +import static io.netty.util.internal.ObjectUtil.checkPositive; + +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; + +import io.netty.util.IllegalReferenceCountException; +import io.netty.util.ReferenceCounted; + +/** + * Common logic for {@link ReferenceCounted} implementations + */ +public abstract class ReferenceCountUpdater<T extends ReferenceCounted> { + /* + * Implementation notes: + * + * For the updated int field: + * Even => "real" refcount is (refCnt >>> 1) + * Odd => "real" refcount is 0 + * + * (x & y) appears to be surprisingly expensive relative to (x == y). Thus this class uses + * a fast-path in some places for most common low values when checking for live (even) refcounts, + * for example: if (rawCnt == 2 || rawCnt == 4 || (rawCnt & 1) == 0) { ... + */ + + protected ReferenceCountUpdater() { } + + public static long getUnsafeOffset(Class<? extends ReferenceCounted> clz, String fieldName) { + try { + if (PlatformDependent.hasUnsafe()) { + return PlatformDependent.objectFieldOffset(clz.getDeclaredField(fieldName)); + } + } catch (Throwable ignore) { + // fall-back + } + return -1; + } + + protected abstract AtomicIntegerFieldUpdater<T> updater(); + + protected abstract long unsafeOffset(); + + public final int initialValue() { + return 2; + } + + private static int realRefCnt(int rawCnt) { + return rawCnt != 2 && rawCnt != 4 && (rawCnt & 1) != 0 ? 0 : rawCnt >>> 1; + } + + /** + * Like {@link #realRefCnt(int)} but throws if refCnt == 0 + */ + private static int toLiveRealRefCnt(int rawCnt, int decrement) { + if (rawCnt == 2 || rawCnt == 4 || (rawCnt & 1) == 0) { + return rawCnt >>> 1; + } + // odd rawCnt => already deallocated + throw new IllegalReferenceCountException(0, -decrement); + } + + private int nonVolatileRawCnt(T instance) { + // TODO: Once we compile against later versions of Java we can replace the Unsafe usage here by varhandles. + final long offset = unsafeOffset(); + return offset != -1 ? PlatformDependent.getInt(instance, offset) : updater().get(instance); + } + + public final int refCnt(T instance) { + return realRefCnt(updater().get(instance)); + } + + public final boolean isLiveNonVolatile(T instance) { + final long offset = unsafeOffset(); + final int rawCnt = offset != -1 ? PlatformDependent.getInt(instance, offset) : updater().get(instance); + + // The "real" ref count is > 0 if the rawCnt is even. + return rawCnt == 2 || rawCnt == 4 || rawCnt == 6 || rawCnt == 8 || (rawCnt & 1) == 0; + } + + /** + * An unsafe operation that sets the reference count directly + */ + public final void setRefCnt(T instance, int refCnt) { + updater().set(instance, refCnt > 0 ? refCnt << 1 : 1); // overflow OK here + } + + /** + * Resets the reference count to 1 + */ + public final void resetRefCnt(T instance) { + updater().set(instance, initialValue()); + } + + public final T retain(T instance) { + return retain0(instance, 1, 2); + } + + public final T retain(T instance, int increment) { + // all changes to the raw count are 2x the "real" change - overflow is OK + int rawIncrement = checkPositive(increment, "increment") << 1; + return retain0(instance, increment, rawIncrement); + } + + // rawIncrement == increment << 1 + private T retain0(T instance, final int increment, final int rawIncrement) { + int oldRef = updater().getAndAdd(instance, rawIncrement); + if (oldRef != 2 && oldRef != 4 && (oldRef & 1) != 0) { + throw new IllegalReferenceCountException(0, increment); + } + // don't pass 0! + if ((oldRef <= 0 && oldRef + rawIncrement >= 0) + || (oldRef >= 0 && oldRef + rawIncrement < oldRef)) { + // overflow case + updater().getAndAdd(instance, -rawIncrement); + throw new IllegalReferenceCountException(realRefCnt(oldRef), increment); + } + return instance; + } + + public final boolean release(T instance) { + int rawCnt = nonVolatileRawCnt(instance); + return rawCnt == 2 ? tryFinalRelease0(instance, 2) || retryRelease0(instance, 1) + : nonFinalRelease0(instance, 1, rawCnt, toLiveRealRefCnt(rawCnt, 1)); + } + + public final boolean release(T instance, int decrement) { + int rawCnt = nonVolatileRawCnt(instance); + int realCnt = toLiveRealRefCnt(rawCnt, checkPositive(decrement, "decrement")); + return decrement == realCnt ? tryFinalRelease0(instance, rawCnt) || retryRelease0(instance, decrement) + : nonFinalRelease0(instance, decrement, rawCnt, realCnt); + } + + private boolean tryFinalRelease0(T instance, int expectRawCnt) { + return updater().compareAndSet(instance, expectRawCnt, 1); // any odd number will work + } + + private boolean nonFinalRelease0(T instance, int decrement, int rawCnt, int realCnt) { + if (decrement < realCnt + // all changes to the raw count are 2x the "real" change - overflow is OK + && updater().compareAndSet(instance, rawCnt, rawCnt - (decrement << 1))) { + return false; + } + return retryRelease0(instance, decrement); + } + + private boolean retryRelease0(T instance, int decrement) { + for (;;) { + int rawCnt = updater().get(instance), realCnt = toLiveRealRefCnt(rawCnt, decrement); + if (decrement == realCnt) { + if (tryFinalRelease0(instance, rawCnt)) { + return true; + } + } else if (decrement < realCnt) { + // all changes to the raw count are 2x the "real" change + if (updater().compareAndSet(instance, rawCnt, rawCnt - (decrement << 1))) { + return false; + } + } else { + throw new IllegalReferenceCountException(realCnt, -decrement); + } + Thread.yield(); // this benefits throughput under high contention + } + } +} diff --git a/common/src/main/java/io/netty/util/internal/SocketUtils.java b/common/src/main/java/io/netty/util/internal/SocketUtils.java index 63fb742..02d9896 100644 --- a/common/src/main/java/io/netty/util/internal/SocketUtils.java +++ b/common/src/main/java/io/netty/util/internal/SocketUtils.java @@ -32,6 +32,7 @@ import java.security.AccessController; import java.security.PrivilegedAction; import java.security.PrivilegedActionException; import java.security.PrivilegedExceptionAction; +import java.util.Collections; import java.util.Enumeration; /** @@ -42,9 +43,16 @@ import java.util.Enumeration; */ public final class SocketUtils { + private static final Enumeration<Object> EMPTY = Collections.enumeration(Collections.emptyList()); + private SocketUtils() { } + @SuppressWarnings("unchecked") + private static <T> Enumeration<T> empty() { + return (Enumeration<T>) EMPTY; + } + public static void connect(final Socket socket, final SocketAddress remoteAddress, final int timeout) throws IOException { try { @@ -88,6 +96,7 @@ public final class SocketUtils { } } + @SuppressJava6Requirement(reason = "Usage guarded by java version check") public static void bind(final SocketChannel socketChannel, final SocketAddress address) throws IOException { try { AccessController.doPrivileged(new PrivilegedExceptionAction<Void>() { @@ -115,6 +124,7 @@ public final class SocketUtils { } } + @SuppressJava6Requirement(reason = "Usage guarded by java version check") public static void bind(final DatagramChannel networkChannel, final SocketAddress address) throws IOException { try { AccessController.doPrivileged(new PrivilegedExceptionAction<Void>() { @@ -174,14 +184,23 @@ public final class SocketUtils { } public static Enumeration<InetAddress> addressesFromNetworkInterface(final NetworkInterface intf) { - return AccessController.doPrivileged(new PrivilegedAction<Enumeration<InetAddress>>() { + Enumeration<InetAddress> addresses = + AccessController.doPrivileged(new PrivilegedAction<Enumeration<InetAddress>>() { @Override public Enumeration<InetAddress> run() { return intf.getInetAddresses(); } }); + // Android seems to sometimes return null even if this is not a valid return value by the api docs. + // Just return an empty Enumeration in this case. + // See https://github.com/netty/netty/issues/10045 + if (addresses == null) { + return empty(); + } + return addresses; } + @SuppressJava6Requirement(reason = "Usage guarded by java version check") public static InetAddress loopbackAddress() { return AccessController.doPrivileged(new PrivilegedAction<InetAddress>() { @Override diff --git a/common/src/main/java/io/netty/util/internal/StringUtil.java b/common/src/main/java/io/netty/util/internal/StringUtil.java index e81a024..7651129 100644 --- a/common/src/main/java/io/netty/util/internal/StringUtil.java +++ b/common/src/main/java/io/netty/util/internal/StringUtil.java @@ -17,6 +17,8 @@ package io.netty.util.internal; import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; import java.util.List; import static io.netty.util.internal.ObjectUtil.*; @@ -38,6 +40,7 @@ public final class StringUtil { private static final String[] BYTE2HEX_PAD = new String[256]; private static final String[] BYTE2HEX_NOPAD = new String[256]; + private static final byte[] HEX2B; /** * 2 - Quote character at beginning and end. @@ -53,6 +56,33 @@ public final class StringUtil { BYTE2HEX_PAD[i] = i > 0xf ? str : ('0' + str); BYTE2HEX_NOPAD[i] = str; } + // Generate the lookup table that converts an hex char into its decimal value: + // the size of the table is such that the JVM is capable of save any bounds-check + // if a char type is used as an index. + HEX2B = new byte[Character.MAX_VALUE + 1]; + Arrays.fill(HEX2B, (byte) -1); + HEX2B['0'] = (byte) 0; + HEX2B['1'] = (byte) 1; + HEX2B['2'] = (byte) 2; + HEX2B['3'] = (byte) 3; + HEX2B['4'] = (byte) 4; + HEX2B['5'] = (byte) 5; + HEX2B['6'] = (byte) 6; + HEX2B['7'] = (byte) 7; + HEX2B['8'] = (byte) 8; + HEX2B['9'] = (byte) 9; + HEX2B['A'] = (byte) 10; + HEX2B['B'] = (byte) 11; + HEX2B['C'] = (byte) 12; + HEX2B['D'] = (byte) 13; + HEX2B['E'] = (byte) 14; + HEX2B['F'] = (byte) 15; + HEX2B['a'] = (byte) 10; + HEX2B['b'] = (byte) 11; + HEX2B['c'] = (byte) 12; + HEX2B['d'] = (byte) 13; + HEX2B['e'] = (byte) 14; + HEX2B['f'] = (byte) 15; } private StringUtil() { @@ -210,18 +240,11 @@ public final class StringUtil { * given, or {@code -1} if the character is invalid. */ public static int decodeHexNibble(final char c) { + assert HEX2B.length == (Character.MAX_VALUE + 1); // Character.digit() is not used here, as it addresses a larger // set of characters (both ASCII and full-width latin letters). - if (c >= '0' && c <= '9') { - return c - '0'; - } - if (c >= 'A' && c <= 'F') { - return c - ('A' - 0xA); - } - if (c >= 'a' && c <= 'f') { - return c - ('a' - 0xA); - } - return -1; + final int index = c; + return HEX2B[index]; } /** @@ -541,7 +564,7 @@ public final class StringUtil { * * @param seq The string to search. * @param offset The offset to start searching at. - * @return the index of the first non-white space character or <{@code 0} if none was found. + * @return the index of the first non-white space character or <{@code -1} if none was found. */ public static int indexOfNonWhiteSpace(CharSequence seq, int offset) { for (; offset < seq.length(); ++offset) { @@ -552,6 +575,22 @@ public final class StringUtil { return -1; } + /** + * Find the index of the first white space character in {@code s} starting at {@code offset}. + * + * @param seq The string to search. + * @param offset The offset to start searching at. + * @return the index of the first white space character or <{@code -1} if none was found. + */ + public static int indexOfWhiteSpace(CharSequence seq, int offset) { + for (; offset < seq.length(); ++offset) { + if (Character.isWhitespace(seq.charAt(offset))) { + return offset; + } + } + return -1; + } + /** * Determine if {@code c} lies within the range of values defined for * <a href="http://unicode.org/glossary/#surrogate_code_point">Surrogate Code Point</a>. @@ -597,6 +636,36 @@ public final class StringUtil { return start == 0 && end == length - 1 ? value : value.subSequence(start, end + 1); } + /** + * Returns a char sequence that contains all {@code elements} joined by a given separator. + * + * @param separator for each element + * @param elements to join together + * + * @return a char sequence joined by a given separator. + */ + public static CharSequence join(CharSequence separator, Iterable<? extends CharSequence> elements) { + ObjectUtil.checkNotNull(separator, "separator"); + ObjectUtil.checkNotNull(elements, "elements"); + + Iterator<? extends CharSequence> iterator = elements.iterator(); + if (!iterator.hasNext()) { + return EMPTY_STRING; + } + + CharSequence firstElement = iterator.next(); + if (!iterator.hasNext()) { + return firstElement; + } + + StringBuilder builder = new StringBuilder(firstElement); + do { + builder.append(separator).append(iterator.next()); + } while (iterator.hasNext()); + + return builder; + } + /** * @return {@code length} if no OWS is found. */ @@ -622,4 +691,5 @@ public final class StringUtil { private static boolean isOws(char c) { return c == SPACE || c == TAB; } + } diff --git a/common/src/main/java/io/netty/util/internal/SuppressJava6Requirement.java b/common/src/main/java/io/netty/util/internal/SuppressJava6Requirement.java index 557d009..3724dc4 100644 --- a/common/src/main/java/io/netty/util/internal/SuppressJava6Requirement.java +++ b/common/src/main/java/io/netty/util/internal/SuppressJava6Requirement.java @@ -25,7 +25,7 @@ import java.lang.annotation.Target; * Annotation to suppress the Java 6 source code requirement checks for a method. */ @Retention(RetentionPolicy.CLASS) -@Target({ ElementType.METHOD }) +@Target({ ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.TYPE }) public @interface SuppressJava6Requirement { String reason(); diff --git a/common/src/main/java/io/netty/util/internal/SystemPropertyUtil.java b/common/src/main/java/io/netty/util/internal/SystemPropertyUtil.java index d13f387..f8632d9 100644 --- a/common/src/main/java/io/netty/util/internal/SystemPropertyUtil.java +++ b/common/src/main/java/io/netty/util/internal/SystemPropertyUtil.java @@ -56,9 +56,7 @@ public final class SystemPropertyUtil { * specified property is not allowed. */ public static String get(final String key, String def) { - if (key == null) { - throw new NullPointerException("key"); - } + ObjectUtil.checkNotNull(key, "key"); if (key.isEmpty()) { throw new IllegalArgumentException("key must not be empty."); } diff --git a/common/src/main/java/io/netty/util/internal/ThreadExecutorMap.java b/common/src/main/java/io/netty/util/internal/ThreadExecutorMap.java new file mode 100644 index 0000000..807698a --- /dev/null +++ b/common/src/main/java/io/netty/util/internal/ThreadExecutorMap.java @@ -0,0 +1,96 @@ +/* + * Copyright 2019 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.util.internal; + +import io.netty.util.concurrent.EventExecutor; +import io.netty.util.concurrent.FastThreadLocal; + +import java.util.concurrent.Executor; +import java.util.concurrent.ThreadFactory; + +/** + * Allow to retrieve the {@link EventExecutor} for the calling {@link Thread}. + */ +public final class ThreadExecutorMap { + + private static final FastThreadLocal<EventExecutor> mappings = new FastThreadLocal<EventExecutor>(); + + private ThreadExecutorMap() { } + + /** + * Returns the current {@link EventExecutor} that uses the {@link Thread}, or {@code null} if none / unknown. + */ + public static EventExecutor currentExecutor() { + return mappings.get(); + } + + /** + * Set the current {@link EventExecutor} that is used by the {@link Thread}. + */ + private static void setCurrentEventExecutor(EventExecutor executor) { + mappings.set(executor); + } + + /** + * Decorate the given {@link Executor} and ensure {@link #currentExecutor()} will return {@code eventExecutor} + * when called from within the {@link Runnable} during execution. + */ + public static Executor apply(final Executor executor, final EventExecutor eventExecutor) { + ObjectUtil.checkNotNull(executor, "executor"); + ObjectUtil.checkNotNull(eventExecutor, "eventExecutor"); + return new Executor() { + @Override + public void execute(final Runnable command) { + executor.execute(apply(command, eventExecutor)); + } + }; + } + + /** + * Decorate the given {@link Runnable} and ensure {@link #currentExecutor()} will return {@code eventExecutor} + * when called from within the {@link Runnable} during execution. + */ + public static Runnable apply(final Runnable command, final EventExecutor eventExecutor) { + ObjectUtil.checkNotNull(command, "command"); + ObjectUtil.checkNotNull(eventExecutor, "eventExecutor"); + return new Runnable() { + @Override + public void run() { + setCurrentEventExecutor(eventExecutor); + try { + command.run(); + } finally { + setCurrentEventExecutor(null); + } + } + }; + } + + /** + * Decorate the given {@link ThreadFactory} and ensure {@link #currentExecutor()} will return {@code eventExecutor} + * when called from within the {@link Runnable} during execution. + */ + public static ThreadFactory apply(final ThreadFactory threadFactory, final EventExecutor eventExecutor) { + ObjectUtil.checkNotNull(threadFactory, "command"); + ObjectUtil.checkNotNull(eventExecutor, "eventExecutor"); + return new ThreadFactory() { + @Override + public Thread newThread(Runnable r) { + return threadFactory.newThread(apply(r, eventExecutor)); + } + }; + } +} diff --git a/common/src/main/java/io/netty/util/internal/ThreadLocalRandom.java b/common/src/main/java/io/netty/util/internal/ThreadLocalRandom.java index 2282769..8981c10 100644 --- a/common/src/main/java/io/netty/util/internal/ThreadLocalRandom.java +++ b/common/src/main/java/io/netty/util/internal/ThreadLocalRandom.java @@ -271,6 +271,7 @@ public final class ThreadLocalRandom extends Random { * * @throws UnsupportedOperationException always */ + @Override public void setSeed(long seed) { if (initialized) { throw new UnsupportedOperationException(); @@ -278,6 +279,7 @@ public final class ThreadLocalRandom extends Random { rnd = (seed ^ multiplier) & mask; } + @Override protected int next(int bits) { rnd = (rnd * multiplier + addend) & mask; return (int) (rnd >>> (48 - bits)); diff --git a/common/src/main/java/io/netty/util/internal/logging/AbstractInternalLogger.java b/common/src/main/java/io/netty/util/internal/logging/AbstractInternalLogger.java index 6486efa..b29a415 100644 --- a/common/src/main/java/io/netty/util/internal/logging/AbstractInternalLogger.java +++ b/common/src/main/java/io/netty/util/internal/logging/AbstractInternalLogger.java @@ -15,6 +15,7 @@ */ package io.netty.util.internal.logging; +import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.StringUtil; import java.io.ObjectStreamException; @@ -37,10 +38,7 @@ public abstract class AbstractInternalLogger implements InternalLogger, Serializ * Creates a new instance. */ protected AbstractInternalLogger(String name) { - if (name == null) { - throw new NullPointerException("name"); - } - this.name = name; + this.name = ObjectUtil.checkNotNull(name, "name"); } @Override diff --git a/common/src/main/java/io/netty/util/internal/logging/CommonsLogger.java b/common/src/main/java/io/netty/util/internal/logging/CommonsLogger.java index 110f0f1..c31c830 100644 --- a/common/src/main/java/io/netty/util/internal/logging/CommonsLogger.java +++ b/common/src/main/java/io/netty/util/internal/logging/CommonsLogger.java @@ -39,6 +39,7 @@ */ package io.netty.util.internal.logging; +import io.netty.util.internal.ObjectUtil; import org.apache.commons.logging.Log; /** @@ -57,10 +58,7 @@ class CommonsLogger extends AbstractInternalLogger { CommonsLogger(Log logger, String name) { super(name); - if (logger == null) { - throw new NullPointerException("logger"); - } - this.logger = logger; + this.logger = ObjectUtil.checkNotNull(logger, "logger"); } /** diff --git a/common/src/main/java/io/netty/util/internal/logging/InternalLoggerFactory.java b/common/src/main/java/io/netty/util/internal/logging/InternalLoggerFactory.java index 12c1b5a..508f571 100644 --- a/common/src/main/java/io/netty/util/internal/logging/InternalLoggerFactory.java +++ b/common/src/main/java/io/netty/util/internal/logging/InternalLoggerFactory.java @@ -16,6 +16,8 @@ package io.netty.util.internal.logging; +import io.netty.util.internal.ObjectUtil; + /** * Creates an {@link InternalLogger} or changes the default factory * implementation. This factory allows you to choose what logging framework @@ -43,12 +45,12 @@ public abstract class InternalLoggerFactory { f.newInstance(name).debug("Using SLF4J as the default logging framework"); } catch (Throwable ignore1) { try { - f = Log4JLoggerFactory.INSTANCE; - f.newInstance(name).debug("Using Log4J as the default logging framework"); + f = Log4J2LoggerFactory.INSTANCE; + f.newInstance(name).debug("Using Log4J2 as the default logging framework"); } catch (Throwable ignore2) { try { - f = Log4J2LoggerFactory.INSTANCE; - f.newInstance(name).debug("Using Log4J2 as the default logging framework"); + f = Log4JLoggerFactory.INSTANCE; + f.newInstance(name).debug("Using Log4J as the default logging framework"); } catch (Throwable ignore3) { f = JdkLoggerFactory.INSTANCE; f.newInstance(name).debug("Using java.util.logging as the default logging framework"); @@ -73,10 +75,7 @@ public abstract class InternalLoggerFactory { * Changes the default factory. */ public static void setDefaultFactory(InternalLoggerFactory defaultFactory) { - if (defaultFactory == null) { - throw new NullPointerException("defaultFactory"); - } - InternalLoggerFactory.defaultFactory = defaultFactory; + InternalLoggerFactory.defaultFactory = ObjectUtil.checkNotNull(defaultFactory, "defaultFactory"); } /** diff --git a/common/src/main/java/io/netty/util/internal/logging/LocationAwareSlf4JLogger.java b/common/src/main/java/io/netty/util/internal/logging/LocationAwareSlf4JLogger.java index 33eb705..7f36283 100644 --- a/common/src/main/java/io/netty/util/internal/logging/LocationAwareSlf4JLogger.java +++ b/common/src/main/java/io/netty/util/internal/logging/LocationAwareSlf4JLogger.java @@ -79,7 +79,7 @@ final class LocationAwareSlf4JLogger extends AbstractInternalLogger { @Override public void trace(String format, Object... argArray) { if (isTraceEnabled()) { - log(TRACE_INT, org.slf4j.helpers.MessageFormatter.format(format, argArray)); + log(TRACE_INT, org.slf4j.helpers.MessageFormatter.arrayFormat(format, argArray)); } } @@ -119,7 +119,7 @@ final class LocationAwareSlf4JLogger extends AbstractInternalLogger { @Override public void debug(String format, Object... argArray) { if (isDebugEnabled()) { - log(DEBUG_INT, org.slf4j.helpers.MessageFormatter.format(format, argArray)); + log(DEBUG_INT, org.slf4j.helpers.MessageFormatter.arrayFormat(format, argArray)); } } @@ -159,7 +159,7 @@ final class LocationAwareSlf4JLogger extends AbstractInternalLogger { @Override public void info(String format, Object... argArray) { if (isInfoEnabled()) { - log(INFO_INT, org.slf4j.helpers.MessageFormatter.format(format, argArray)); + log(INFO_INT, org.slf4j.helpers.MessageFormatter.arrayFormat(format, argArray)); } } @@ -192,7 +192,7 @@ final class LocationAwareSlf4JLogger extends AbstractInternalLogger { @Override public void warn(String format, Object... argArray) { if (isWarnEnabled()) { - log(WARN_INT, org.slf4j.helpers.MessageFormatter.format(format, argArray)); + log(WARN_INT, org.slf4j.helpers.MessageFormatter.arrayFormat(format, argArray)); } } @@ -239,7 +239,7 @@ final class LocationAwareSlf4JLogger extends AbstractInternalLogger { @Override public void error(String format, Object... argArray) { if (isErrorEnabled()) { - log(ERROR_INT, org.slf4j.helpers.MessageFormatter.format(format, argArray)); + log(ERROR_INT, org.slf4j.helpers.MessageFormatter.arrayFormat(format, argArray)); } } diff --git a/common/src/main/java/io/netty/util/internal/svm/CleanerJava6Substitution.java b/common/src/main/java/io/netty/util/internal/svm/CleanerJava6Substitution.java new file mode 100644 index 0000000..cd78c67 --- /dev/null +++ b/common/src/main/java/io/netty/util/internal/svm/CleanerJava6Substitution.java @@ -0,0 +1,33 @@ +/* + * Copyright 2019 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.util.internal.svm; + +import com.oracle.svm.core.annotate.Alias; +import com.oracle.svm.core.annotate.RecomputeFieldValue; +import com.oracle.svm.core.annotate.TargetClass; + +@TargetClass(className = "io.netty.util.internal.CleanerJava6") +final class CleanerJava6Substitution { + private CleanerJava6Substitution() { + } + + @Alias + @RecomputeFieldValue( + kind = RecomputeFieldValue.Kind.FieldOffset, + declClassName = "java.nio.DirectByteBuffer", + name = "cleaner") + private static long CLEANER_FIELD_OFFSET; +} diff --git a/common/src/main/java/io/netty/util/internal/svm/PlatformDependent0Substitution.java b/common/src/main/java/io/netty/util/internal/svm/PlatformDependent0Substitution.java new file mode 100644 index 0000000..01b9db2 --- /dev/null +++ b/common/src/main/java/io/netty/util/internal/svm/PlatformDependent0Substitution.java @@ -0,0 +1,33 @@ +/* + * Copyright 2019 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.util.internal.svm; + +import com.oracle.svm.core.annotate.Alias; +import com.oracle.svm.core.annotate.RecomputeFieldValue; +import com.oracle.svm.core.annotate.TargetClass; + +@TargetClass(className = "io.netty.util.internal.PlatformDependent0") +final class PlatformDependent0Substitution { + private PlatformDependent0Substitution() { + } + + @Alias + @RecomputeFieldValue( + kind = RecomputeFieldValue.Kind.FieldOffset, + declClassName = "java.nio.Buffer", + name = "address") + private static long ADDRESS_FIELD_OFFSET; +} diff --git a/common/src/main/java/io/netty/util/internal/svm/PlatformDependentSubstitution.java b/common/src/main/java/io/netty/util/internal/svm/PlatformDependentSubstitution.java new file mode 100644 index 0000000..1afe0a0 --- /dev/null +++ b/common/src/main/java/io/netty/util/internal/svm/PlatformDependentSubstitution.java @@ -0,0 +1,39 @@ +/* + * Copyright 2019 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.util.internal.svm; + +import com.oracle.svm.core.annotate.Alias; +import com.oracle.svm.core.annotate.RecomputeFieldValue; +import com.oracle.svm.core.annotate.TargetClass; + +@TargetClass(className = "io.netty.util.internal.PlatformDependent") +final class PlatformDependentSubstitution { + private PlatformDependentSubstitution() { + } + + /** + * The class PlatformDependent caches the byte array base offset by reading the + * field from PlatformDependent0. The automatic recomputation of Substrate VM + * correctly recomputes the field in PlatformDependent0, but since the caching + * in PlatformDependent happens during image building, the non-recomputed value + * is cached. + */ + @Alias + @RecomputeFieldValue( + kind = RecomputeFieldValue.Kind.ArrayBaseOffset, + declClass = byte[].class) + private static long BYTE_ARRAY_BASE_OFFSET; +} diff --git a/common/src/main/java/io/netty/util/internal/svm/UnsafeRefArrayAccessSubstitution.java b/common/src/main/java/io/netty/util/internal/svm/UnsafeRefArrayAccessSubstitution.java new file mode 100644 index 0000000..732fe28 --- /dev/null +++ b/common/src/main/java/io/netty/util/internal/svm/UnsafeRefArrayAccessSubstitution.java @@ -0,0 +1,32 @@ +/* + * Copyright 2019 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.util.internal.svm; + +import com.oracle.svm.core.annotate.Alias; +import com.oracle.svm.core.annotate.RecomputeFieldValue; +import com.oracle.svm.core.annotate.TargetClass; + +@TargetClass(className = "io.netty.util.internal.shaded.org.jctools.util.UnsafeRefArrayAccess") +final class UnsafeRefArrayAccessSubstitution { + private UnsafeRefArrayAccessSubstitution() { + } + + @Alias + @RecomputeFieldValue( + kind = RecomputeFieldValue.Kind.ArrayIndexShift, + declClass = Object[].class) + public static int REF_ELEMENT_SHIFT; +} diff --git a/common/src/main/java/io/netty/util/internal/svm/package-info.java b/common/src/main/java/io/netty/util/internal/svm/package-info.java new file mode 100644 index 0000000..e9b82ec --- /dev/null +++ b/common/src/main/java/io/netty/util/internal/svm/package-info.java @@ -0,0 +1,21 @@ +/* + * Copyright 2019 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. + */ + +/** + * SVM substitutions for classes that will cause trouble while compiling + * into native image. + */ +package io.netty.util.internal.svm; diff --git a/common/src/main/resources/META-INF/native-image/io.netty/common/native-image.properties b/common/src/main/resources/META-INF/native-image/io.netty/common/native-image.properties new file mode 100644 index 0000000..5c11bf6 --- /dev/null +++ b/common/src/main/resources/META-INF/native-image/io.netty/common/native-image.properties @@ -0,0 +1,15 @@ +# Copyright 2019 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. + +Args = --initialize-at-run-time=io.netty.util.AbstractReferenceCounted,io.netty.util.concurrent.GlobalEventExecutor,io.netty.util.concurrent.ImmediateEventExecutor,io.netty.util.concurrent.ScheduledFutureTask,io.netty.util.internal.ThreadLocalRandom diff --git a/common/src/main/resources/META-INF/services/reactor.blockhound.integration.BlockHoundIntegration b/common/src/main/resources/META-INF/services/reactor.blockhound.integration.BlockHoundIntegration new file mode 100644 index 0000000..5cf376d --- /dev/null +++ b/common/src/main/resources/META-INF/services/reactor.blockhound.integration.BlockHoundIntegration @@ -0,0 +1,14 @@ +# Copyright 2019 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. +io.netty.util.internal.Hidden$NettyBlockHoundIntegration \ No newline at end of file diff --git a/common/src/main/templates/io/netty/util/collection/KObjectHashMap.template b/common/src/main/templates/io/netty/util/collection/KObjectHashMap.template index d8aeb1b..7db1ceb 100644 --- a/common/src/main/templates/io/netty/util/collection/KObjectHashMap.template +++ b/common/src/main/templates/io/netty/util/collection/KObjectHashMap.template @@ -236,7 +236,7 @@ public class @K@ObjectHashMap<V> implements @K@ObjectMap<V> { @Override public void remove() { - throw new UnsupportedOperationException(); + iter.remove(); } }; } diff --git a/common/src/test/java/io/netty/util/AsciiStringCharacterTest.java b/common/src/test/java/io/netty/util/AsciiStringCharacterTest.java index c2a835d..7534bb4 100644 --- a/common/src/test/java/io/netty/util/AsciiStringCharacterTest.java +++ b/common/src/test/java/io/netty/util/AsciiStringCharacterTest.java @@ -37,6 +37,15 @@ import static org.junit.Assert.assertTrue; public class AsciiStringCharacterTest { private static final Random r = new Random(); + @Test + public void testContentEqualsIgnoreCase() { + byte[] bytes = { 32, 'a' }; + AsciiString asciiString = new AsciiString(bytes, 1, 1, false); + // https://github.com/netty/netty/issues/9475 + assertFalse(asciiString.contentEqualsIgnoreCase("b")); + assertFalse(asciiString.contentEqualsIgnoreCase(AsciiString.of("b"))); + } + @Test public void testGetBytesStringBuilder() { final StringBuilder b = new StringBuilder(); @@ -356,15 +365,15 @@ public class AsciiStringCharacterTest { @Test public void testLastIndexOfCharSequence() { assertEquals(0, new AsciiString("abcd").lastIndexOf("abcd", 0)); - assertEquals(0, new AsciiString("abcd").lastIndexOf("abc", 0)); - assertEquals(1, new AsciiString("abcd").lastIndexOf("bcd", 0)); - assertEquals(1, new AsciiString("abcd").lastIndexOf("bc", 0)); - assertEquals(5, new AsciiString("abcdabcd").lastIndexOf("bcd", 0)); - assertEquals(0, new AsciiString("abcd", 1, 2).lastIndexOf("bc", 0)); - assertEquals(0, new AsciiString("abcd", 1, 3).lastIndexOf("bcd", 0)); - assertEquals(1, new AsciiString("abcdabcd", 4, 4).lastIndexOf("bcd", 0)); + assertEquals(0, new AsciiString("abcd").lastIndexOf("abc", 4)); + assertEquals(1, new AsciiString("abcd").lastIndexOf("bcd", 4)); + assertEquals(1, new AsciiString("abcd").lastIndexOf("bc", 4)); + assertEquals(5, new AsciiString("abcdabcd").lastIndexOf("bcd", 10)); + assertEquals(0, new AsciiString("abcd", 1, 2).lastIndexOf("bc", 2)); + assertEquals(0, new AsciiString("abcd", 1, 3).lastIndexOf("bcd", 3)); + assertEquals(1, new AsciiString("abcdabcd", 4, 4).lastIndexOf("bcd", 4)); assertEquals(3, new AsciiString("012345").lastIndexOf("345", 3)); - assertEquals(3, new AsciiString("012345").lastIndexOf("345", 0)); + assertEquals(3, new AsciiString("012345").lastIndexOf("345", 6)); // Test with empty string assertEquals(0, new AsciiString("abcd").lastIndexOf("", 0)); @@ -376,7 +385,7 @@ public class AsciiStringCharacterTest { assertEquals(-1, new AsciiString("abcdbc").lastIndexOf("bce", 0)); assertEquals(-1, new AsciiString("abcd", 1, 3).lastIndexOf("abc", 0)); assertEquals(-1, new AsciiString("abcd", 1, 2).lastIndexOf("bd", 0)); - assertEquals(-1, new AsciiString("012345").lastIndexOf("345", 4)); + assertEquals(-1, new AsciiString("012345").lastIndexOf("345", 2)); assertEquals(-1, new AsciiString("012345").lastIndexOf("abc", 3)); assertEquals(-1, new AsciiString("012345").lastIndexOf("abc", 0)); assertEquals(-1, new AsciiString("012345").lastIndexOf("abcdefghi", 0)); diff --git a/common/src/test/java/io/netty/util/RecyclerTest.java b/common/src/test/java/io/netty/util/RecyclerTest.java index e4bcbf1..8eaef8d 100644 --- a/common/src/test/java/io/netty/util/RecyclerTest.java +++ b/common/src/test/java/io/netty/util/RecyclerTest.java @@ -174,6 +174,7 @@ public class RecyclerTest { final HandledObject o = recycler.get(); final HandledObject o2 = recycler.get(); + final Thread thread = new Thread() { @Override public void run() { diff --git a/common/src/test/java/io/netty/util/concurrent/DefaultPromiseTest.java b/common/src/test/java/io/netty/util/concurrent/DefaultPromiseTest.java index fd9a3e9..84589bd 100644 --- a/common/src/test/java/io/netty/util/concurrent/DefaultPromiseTest.java +++ b/common/src/test/java/io/netty/util/concurrent/DefaultPromiseTest.java @@ -21,6 +21,7 @@ import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; import org.junit.BeforeClass; import org.junit.Test; +import org.mockito.Mockito; import java.util.HashMap; import java.util.Map; @@ -36,6 +37,7 @@ import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; import static java.lang.Math.max; +import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.lessThan; import static org.junit.Assert.*; @@ -63,10 +65,45 @@ public class DefaultPromiseTest { return max(stackOverflowDepth << 1, stackOverflowDepth); } + @Test + public void testCancelDoesNotScheduleWhenNoListeners() { + EventExecutor executor = Mockito.mock(EventExecutor.class); + Mockito.when(executor.inEventLoop()).thenReturn(false); + + Promise<Void> promise = new DefaultPromise<Void>(executor); + assertTrue(promise.cancel(false)); + Mockito.verify(executor, Mockito.never()).execute(Mockito.any(Runnable.class)); + assertTrue(promise.isCancelled()); + } + + @Test + public void testSuccessDoesNotScheduleWhenNoListeners() { + EventExecutor executor = Mockito.mock(EventExecutor.class); + Mockito.when(executor.inEventLoop()).thenReturn(false); + + Object value = new Object(); + Promise<Object> promise = new DefaultPromise<Object>(executor); + promise.setSuccess(value); + Mockito.verify(executor, Mockito.never()).execute(Mockito.any(Runnable.class)); + assertSame(value, promise.getNow()); + } + + @Test + public void testFailureDoesNotScheduleWhenNoListeners() { + EventExecutor executor = Mockito.mock(EventExecutor.class); + Mockito.when(executor.inEventLoop()).thenReturn(false); + + Exception cause = new Exception(); + Promise<Void> promise = new DefaultPromise<Void>(executor); + promise.setFailure(cause); + Mockito.verify(executor, Mockito.never()).execute(Mockito.any(Runnable.class)); + assertSame(cause, promise.cause()); + } + @Test(expected = CancellationException.class) public void testCancellationExceptionIsThrownWhenBlockingGet() throws InterruptedException, ExecutionException { final Promise<Void> promise = new DefaultPromise<Void>(ImmediateEventExecutor.INSTANCE); - promise.cancel(false); + assertTrue(promise.cancel(false)); promise.get(); } @@ -74,10 +111,18 @@ public class DefaultPromiseTest { public void testCancellationExceptionIsThrownWhenBlockingGetWithTimeout() throws InterruptedException, ExecutionException, TimeoutException { final Promise<Void> promise = new DefaultPromise<Void>(ImmediateEventExecutor.INSTANCE); - promise.cancel(false); + assertTrue(promise.cancel(false)); promise.get(1, TimeUnit.SECONDS); } + @Test + public void testCancellationExceptionIsReturnedAsCause() throws InterruptedException, + ExecutionException, TimeoutException { + final Promise<Void> promise = new DefaultPromise<Void>(ImmediateEventExecutor.INSTANCE); + assertTrue(promise.cancel(false)); + assertThat(promise.cause(), instanceOf(CancellationException.class)); + } + @Test public void testStackOverflowWithImmediateEventExecutorA() throws Exception { testStackOverFlowChainedFuturesA(stackOverflowTestDepth(), ImmediateEventExecutor.INSTANCE, true); diff --git a/common/src/test/java/io/netty/util/concurrent/FastThreadLocalTest.java b/common/src/test/java/io/netty/util/concurrent/FastThreadLocalTest.java index 6457de2..c097a34 100644 --- a/common/src/test/java/io/netty/util/concurrent/FastThreadLocalTest.java +++ b/common/src/test/java/io/netty/util/concurrent/FastThreadLocalTest.java @@ -27,7 +27,9 @@ import java.util.concurrent.atomic.AtomicReference; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.Matchers.nullValue; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; public class FastThreadLocalTest { @Before @@ -36,6 +38,23 @@ public class FastThreadLocalTest { assertThat(FastThreadLocal.size(), is(0)); } + @Test + public void testGetIfExists() { + FastThreadLocal<Boolean> threadLocal = new FastThreadLocal<Boolean>() { + @Override + protected Boolean initialValue() { + return Boolean.TRUE; + } + }; + + assertNull(threadLocal.getIfExists()); + assertTrue(threadLocal.get()); + assertTrue(threadLocal.getIfExists()); + + FastThreadLocal.removeAll(); + assertNull(threadLocal.getIfExists()); + } + @Test(timeout = 10000) public void testRemoveAll() throws Exception { final AtomicBoolean removed = new AtomicBoolean(); diff --git a/common/src/test/java/io/netty/util/concurrent/GlobalEventExecutorTest.java b/common/src/test/java/io/netty/util/concurrent/GlobalEventExecutorTest.java index 35c2a59..fcb2c42 100644 --- a/common/src/test/java/io/netty/util/concurrent/GlobalEventExecutorTest.java +++ b/common/src/test/java/io/netty/util/concurrent/GlobalEventExecutorTest.java @@ -46,7 +46,7 @@ public class GlobalEventExecutorTest { } } - @Test + @Test(timeout = 5000) public void testAutomaticStartStop() throws Exception { final TestRunnable task = new TestRunnable(500); e.execute(task); @@ -56,10 +56,7 @@ public class GlobalEventExecutorTest { assertThat(thread, is(not(nullValue()))); assertThat(thread.isAlive(), is(true)); - Thread.sleep(1500); - - // Ensure the thread stopped itself after running the task. - assertThat(thread.isAlive(), is(false)); + thread.join(); assertThat(task.ran.get(), is(true)); // Ensure another new thread starts again. @@ -68,14 +65,12 @@ public class GlobalEventExecutorTest { assertThat(e.thread, not(sameInstance(thread))); thread = e.thread; - Thread.sleep(1500); + thread.join(); - // Ensure the thread stopped itself after running the task. - assertThat(thread.isAlive(), is(false)); assertThat(task.ran.get(), is(true)); } - @Test + @Test(timeout = 5000) public void testScheduledTasks() throws Exception { TestRunnable task = new TestRunnable(0); ScheduledFuture<?> f = e.schedule(task, 1500, TimeUnit.MILLISECONDS); @@ -87,10 +82,7 @@ public class GlobalEventExecutorTest { assertThat(thread, is(not(nullValue()))); assertThat(thread.isAlive(), is(true)); - Thread.sleep(1500); - - // Now it should be stopped. - assertThat(thread.isAlive(), is(false)); + thread.join(); } // ensure that when a task submission causes a new thread to be created, the thread inherits the thread group of the @@ -116,6 +108,62 @@ public class GlobalEventExecutorTest { assertEquals(group, capturedGroup.get()); } + @Test(timeout = 5000) + public void testTakeTask() throws Exception { + //add task + TestRunnable beforeTask = new TestRunnable(0); + e.execute(beforeTask); + + //add scheduled task + TestRunnable scheduledTask = new TestRunnable(0); + ScheduledFuture<?> f = e.schedule(scheduledTask , 1500, TimeUnit.MILLISECONDS); + + //add task + TestRunnable afterTask = new TestRunnable(0); + e.execute(afterTask); + + f.sync(); + + assertThat(beforeTask.ran.get(), is(true)); + assertThat(scheduledTask.ran.get(), is(true)); + assertThat(afterTask.ran.get(), is(true)); + } + + @Test(timeout = 5000) + public void testTakeTaskAlwaysHasTask() throws Exception { + //for https://github.com/netty/netty/issues/1614 + //add scheduled task + TestRunnable t = new TestRunnable(0); + ScheduledFuture<?> f = e.schedule(t, 1500, TimeUnit.MILLISECONDS); + + final Runnable doNothing = new Runnable() { + @Override + public void run() { + //NOOP + } + }; + final AtomicBoolean stop = new AtomicBoolean(false); + + //ensure always has at least one task in taskQueue + //check if scheduled tasks are triggered + try { + new Thread(new Runnable() { + @Override + public void run() { + while (!stop.get()) { + e.execute(doNothing); + } + } + }).start(); + + f.sync(); + + assertThat(t.ran.get(), is(true)); + } finally { + stop.set(true); + } + } + private static final class TestRunnable implements Runnable { final AtomicBoolean ran = new AtomicBoolean(); final long delay; diff --git a/common/src/test/java/io/netty/util/concurrent/ImmediateExecutorTest.java b/common/src/test/java/io/netty/util/concurrent/ImmediateExecutorTest.java new file mode 100644 index 0000000..c1bd565 --- /dev/null +++ b/common/src/test/java/io/netty/util/concurrent/ImmediateExecutorTest.java @@ -0,0 +1,47 @@ +/* + * Copyright 2020 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.util.concurrent; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.util.concurrent.FutureTask; + +import org.junit.Test; + +public class ImmediateExecutorTest { + + @Test(expected = NullPointerException.class) + public void testExecuteNullRunnable() { + ImmediateExecutor.INSTANCE.execute(null); + } + + @Test + public void testExecuteNonNullRunnable() throws Exception { + FutureTask<Void> task = new FutureTask<Void>(new Runnable() { + @Override + public void run() { + // NOOP + } + }, null); + ImmediateExecutor.INSTANCE.execute(task); + assertTrue(task.isDone()); + assertFalse(task.isCancelled()); + assertNull(task.get()); + } +} diff --git a/common/src/test/java/io/netty/util/concurrent/PromiseCombinerTest.java b/common/src/test/java/io/netty/util/concurrent/PromiseCombinerTest.java index b46fa41..6194c5a 100644 --- a/common/src/test/java/io/netty/util/concurrent/PromiseCombinerTest.java +++ b/common/src/test/java/io/netty/util/concurrent/PromiseCombinerTest.java @@ -15,6 +15,7 @@ */ package io.netty.util.concurrent; +import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.mockito.Mock; @@ -25,6 +26,7 @@ import org.mockito.stubbing.Answer; import static org.mockito.Mockito.any; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -55,7 +57,19 @@ public class PromiseCombinerTest { @Before public void setup() { MockitoAnnotations.initMocks(this); - combiner = new PromiseCombiner(); + combiner = new PromiseCombiner(ImmediateEventExecutor.INSTANCE); + } + + @Test + public void testNullArgument() { + try { + combiner.finish(null); + Assert.fail(); + } catch (NullPointerException expected) { + // expected + } + combiner.finish(p1); + verify(p1).trySuccess(null); } @Test @@ -148,6 +162,38 @@ public class PromiseCombinerTest { verifyFail(p3, e1); } + @Test + public void testEventExecutor() { + EventExecutor executor = mock(EventExecutor.class); + when(executor.inEventLoop()).thenReturn(false); + combiner = new PromiseCombiner(executor); + + Future<?> future = mock(Future.class); + + try { + combiner.add(future); + Assert.fail(); + } catch (IllegalStateException expected) { + // expected + } + + try { + combiner.addAll(future); + Assert.fail(); + } catch (IllegalStateException expected) { + // expected + } + + @SuppressWarnings("unchecked") + Promise<Void> promise = (Promise<Void>) mock(Promise.class); + try { + combiner.finish(promise); + Assert.fail(); + } catch (IllegalStateException expected) { + // expected + } + } + private static void verifyFail(Promise<Void> p, Throwable cause) { verify(p).tryFailure(eq(cause)); } diff --git a/common/src/test/java/io/netty/util/concurrent/SingleThreadEventExecutorTest.java b/common/src/test/java/io/netty/util/concurrent/SingleThreadEventExecutorTest.java index 55981b2..16f4127 100644 --- a/common/src/test/java/io/netty/util/concurrent/SingleThreadEventExecutorTest.java +++ b/common/src/test/java/io/netty/util/concurrent/SingleThreadEventExecutorTest.java @@ -18,20 +18,29 @@ package io.netty.util.concurrent; import org.junit.Assert; import org.junit.Test; +import io.netty.util.concurrent.AbstractEventExecutor.LazyRunnable; + import java.util.Collections; import java.util.Set; import java.util.concurrent.Callable; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + public class SingleThreadEventExecutorTest { @Test - public void testWrappedExecutureIsShutdown() { + public void testWrappedExecutorIsShutdown() { ExecutorService executorService = Executors.newSingleThreadExecutor(); SingleThreadEventExecutor executor = new SingleThreadEventExecutor(null, executorService, false) { @@ -122,7 +131,7 @@ public class SingleThreadEventExecutorTest { private static void testInvokeInEventLoop(final boolean any, final boolean timeout) { final SingleThreadEventExecutor executor = new SingleThreadEventExecutor(null, - Executors.defaultThreadFactory(), false) { + Executors.defaultThreadFactory(), true) { @Override protected void run() { while (!confirmShutdown()) { @@ -170,4 +179,240 @@ public class SingleThreadEventExecutorTest { executor.shutdownGracefully(0, 0, TimeUnit.MILLISECONDS); } } + + static class LatchTask extends CountDownLatch implements Runnable { + LatchTask() { + super(1); + } + + @Override + public void run() { + countDown(); + } + } + + static class LazyLatchTask extends LatchTask implements LazyRunnable { } + + @Test + public void testLazyExecution() throws Exception { + final SingleThreadEventExecutor executor = new SingleThreadEventExecutor(null, + Executors.defaultThreadFactory(), false) { + @Override + protected void run() { + while (!confirmShutdown()) { + try { + synchronized (this) { + if (!hasTasks()) { + wait(); + } + } + runAllTasks(); + } catch (Exception e) { + e.printStackTrace(); + Assert.fail(e.toString()); + } + } + } + + @Override + protected void wakeup(boolean inEventLoop) { + if (!inEventLoop) { + synchronized (this) { + notifyAll(); + } + } + } + }; + + // Ensure event loop is started + LatchTask latch0 = new LatchTask(); + executor.execute(latch0); + assertTrue(latch0.await(100, TimeUnit.MILLISECONDS)); + // Pause to ensure it enters waiting state + Thread.sleep(100L); + + // Submit task via lazyExecute + LatchTask latch1 = new LatchTask(); + executor.lazyExecute(latch1); + // Sumbit lazy task via regular execute + LatchTask latch2 = new LazyLatchTask(); + executor.execute(latch2); + + // Neither should run yet + assertFalse(latch1.await(100, TimeUnit.MILLISECONDS)); + assertFalse(latch2.await(100, TimeUnit.MILLISECONDS)); + + // Submit regular task via regular execute + LatchTask latch3 = new LatchTask(); + executor.execute(latch3); + + // Should flush latch1 and latch2 and then run latch3 immediately + assertTrue(latch3.await(100, TimeUnit.MILLISECONDS)); + assertEquals(0, latch1.getCount()); + assertEquals(0, latch2.getCount()); + } + + @Test + public void testTaskAddedAfterShutdownNotAbandoned() throws Exception { + + // A queue that doesn't support remove, so tasks once added cannot be rejected anymore + LinkedBlockingQueue<Runnable> taskQueue = new LinkedBlockingQueue<Runnable>() { + @Override + public boolean remove(Object o) { + throw new UnsupportedOperationException(); + } + }; + + final Runnable dummyTask = new Runnable() { + @Override + public void run() { + } + }; + + final LinkedBlockingQueue<Future<?>> submittedTasks = new LinkedBlockingQueue<Future<?>>(); + final AtomicInteger attempts = new AtomicInteger(); + final AtomicInteger rejects = new AtomicInteger(); + + ExecutorService executorService = Executors.newSingleThreadExecutor(); + final SingleThreadEventExecutor executor = new SingleThreadEventExecutor(null, executorService, false, + taskQueue, RejectedExecutionHandlers.reject()) { + @Override + protected void run() { + while (!confirmShutdown()) { + Runnable task = takeTask(); + if (task != null) { + task.run(); + } + } + } + + @Override + protected boolean confirmShutdown() { + boolean result = super.confirmShutdown(); + // After shutdown is confirmed, scheduled one more task and record it + if (result) { + attempts.incrementAndGet(); + try { + submittedTasks.add(submit(dummyTask)); + } catch (RejectedExecutionException e) { + // ignore, tasks are either accepted or rejected + rejects.incrementAndGet(); + } + } + return result; + } + }; + + // Start the loop + executor.submit(dummyTask).sync(); + + // Shutdown without any quiet period + executor.shutdownGracefully(0, 100, TimeUnit.MILLISECONDS).sync(); + + // Ensure there are no user-tasks left. + assertEquals(0, executor.drainTasks()); + + // Verify that queue is empty and all attempts either succeeded or were rejected + assertTrue(taskQueue.isEmpty()); + assertTrue(attempts.get() > 0); + assertEquals(attempts.get(), submittedTasks.size() + rejects.get()); + for (Future<?> f : submittedTasks) { + assertTrue(f.isSuccess()); + } + } + + @Test(timeout = 5000) + public void testTakeTask() throws Exception { + final SingleThreadEventExecutor executor = + new SingleThreadEventExecutor(null, Executors.defaultThreadFactory(), true) { + @Override + protected void run() { + while (!confirmShutdown()) { + Runnable task = takeTask(); + if (task != null) { + task.run(); + } + } + } + }; + + //add task + TestRunnable beforeTask = new TestRunnable(); + executor.execute(beforeTask); + + //add scheduled task + TestRunnable scheduledTask = new TestRunnable(); + ScheduledFuture<?> f = executor.schedule(scheduledTask , 1500, TimeUnit.MILLISECONDS); + + //add task + TestRunnable afterTask = new TestRunnable(); + executor.execute(afterTask); + + f.sync(); + + assertThat(beforeTask.ran.get(), is(true)); + assertThat(scheduledTask.ran.get(), is(true)); + assertThat(afterTask.ran.get(), is(true)); + } + + @Test(timeout = 5000) + public void testTakeTaskAlwaysHasTask() throws Exception { + //for https://github.com/netty/netty/issues/1614 + + final SingleThreadEventExecutor executor = + new SingleThreadEventExecutor(null, Executors.defaultThreadFactory(), true) { + @Override + protected void run() { + while (!confirmShutdown()) { + Runnable task = takeTask(); + if (task != null) { + task.run(); + } + } + } + }; + + //add scheduled task + TestRunnable t = new TestRunnable(); + ScheduledFuture<?> f = executor.schedule(t, 1500, TimeUnit.MILLISECONDS); + + final Runnable doNothing = new Runnable() { + @Override + public void run() { + //NOOP + } + }; + final AtomicBoolean stop = new AtomicBoolean(false); + + //ensure always has at least one task in taskQueue + //check if scheduled tasks are triggered + try { + new Thread(new Runnable() { + @Override + public void run() { + while (!stop.get()) { + executor.execute(doNothing); + } + } + }).start(); + + f.sync(); + + assertThat(t.ran.get(), is(true)); + } finally { + stop.set(true); + } + } + + private static final class TestRunnable implements Runnable { + final AtomicBoolean ran = new AtomicBoolean(); + + TestRunnable() { + } + + @Override + public void run() { + ran.set(true); + } + } } diff --git a/common/src/test/java/io/netty/util/internal/AppendableCharSequenceTest.java b/common/src/test/java/io/netty/util/internal/AppendableCharSequenceTest.java index 2d7bab4..9d08c3e 100644 --- a/common/src/test/java/io/netty/util/internal/AppendableCharSequenceTest.java +++ b/common/src/test/java/io/netty/util/internal/AppendableCharSequenceTest.java @@ -64,6 +64,16 @@ public class AppendableCharSequenceTest { assertEquals("abcdefghij", master.subSequence(0, 10).toString()); } + @Test + public void testEmptySubSequence() { + AppendableCharSequence master = new AppendableCharSequence(26); + master.append("abcdefghijlkmonpqrstuvwxyz"); + AppendableCharSequence sub = master.subSequence(0, 0); + assertEquals(0, sub.length()); + sub.append('b'); + assertEquals('b', sub.charAt(0)); + } + private static void testSimpleAppend0(AppendableCharSequence seq) { String text = "testdata"; for (int i = 0; i < text.length(); i++) { diff --git a/common/src/test/java/io/netty/util/internal/DefaultPriorityQueueTest.java b/common/src/test/java/io/netty/util/internal/DefaultPriorityQueueTest.java index b4adc8e..a4e6900 100644 --- a/common/src/test/java/io/netty/util/internal/DefaultPriorityQueueTest.java +++ b/common/src/test/java/io/netty/util/internal/DefaultPriorityQueueTest.java @@ -15,9 +15,7 @@ */ package io.netty.util.internal; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; import java.io.Serializable; import java.util.ArrayList; diff --git a/common/src/test/java/io/netty/util/internal/MathUtilTest.java b/common/src/test/java/io/netty/util/internal/MathUtilTest.java new file mode 100644 index 0000000..16616b8 --- /dev/null +++ b/common/src/test/java/io/netty/util/internal/MathUtilTest.java @@ -0,0 +1,88 @@ +/* + * Copyright 2020 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.util.internal; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static io.netty.util.internal.MathUtil.*; + +import org.junit.Test; + +public class MathUtilTest { + + @Test + public void testFindNextPositivePowerOfTwo() { + assertEquals(1, findNextPositivePowerOfTwo(0)); + assertEquals(1, findNextPositivePowerOfTwo(1)); + assertEquals(1024, findNextPositivePowerOfTwo(1000)); + assertEquals(1024, findNextPositivePowerOfTwo(1023)); + assertEquals(2048, findNextPositivePowerOfTwo(2048)); + assertEquals(1 << 30, findNextPositivePowerOfTwo((1 << 30) - 1)); + assertEquals(1, findNextPositivePowerOfTwo(-1)); + assertEquals(1, findNextPositivePowerOfTwo(-10000)); + } + + @Test + public void testSafeFindNextPositivePowerOfTwo() { + assertEquals(1, safeFindNextPositivePowerOfTwo(0)); + assertEquals(1, safeFindNextPositivePowerOfTwo(1)); + assertEquals(1024, safeFindNextPositivePowerOfTwo(1000)); + assertEquals(1024, safeFindNextPositivePowerOfTwo(1023)); + assertEquals(2048, safeFindNextPositivePowerOfTwo(2048)); + assertEquals(1 << 30, safeFindNextPositivePowerOfTwo((1 << 30) - 1)); + assertEquals(1, safeFindNextPositivePowerOfTwo(-1)); + assertEquals(1, safeFindNextPositivePowerOfTwo(-10000)); + assertEquals(1 << 30, safeFindNextPositivePowerOfTwo(Integer.MAX_VALUE)); + assertEquals(1 << 30, safeFindNextPositivePowerOfTwo((1 << 30) + 1)); + assertEquals(1, safeFindNextPositivePowerOfTwo(Integer.MIN_VALUE)); + assertEquals(1, safeFindNextPositivePowerOfTwo(Integer.MIN_VALUE + 1)); + } + + @Test + public void testIsOutOfBounds() { + assertFalse(isOutOfBounds(0, 0, 0)); + assertFalse(isOutOfBounds(0, 0, 1)); + assertFalse(isOutOfBounds(0, 1, 1)); + assertTrue(isOutOfBounds(1, 1, 1)); + assertTrue(isOutOfBounds(Integer.MAX_VALUE, 1, 1)); + assertTrue(isOutOfBounds(Integer.MAX_VALUE, Integer.MAX_VALUE, 1)); + assertTrue(isOutOfBounds(Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE)); + assertFalse(isOutOfBounds(0, Integer.MAX_VALUE, Integer.MAX_VALUE)); + assertFalse(isOutOfBounds(0, Integer.MAX_VALUE - 1, Integer.MAX_VALUE)); + assertTrue(isOutOfBounds(0, Integer.MAX_VALUE, Integer.MAX_VALUE - 1)); + assertFalse(isOutOfBounds(Integer.MAX_VALUE - 1, 1, Integer.MAX_VALUE)); + assertTrue(isOutOfBounds(Integer.MAX_VALUE - 1, 1, Integer.MAX_VALUE - 1)); + assertTrue(isOutOfBounds(Integer.MAX_VALUE - 1, 2, Integer.MAX_VALUE)); + assertTrue(isOutOfBounds(1, Integer.MAX_VALUE, Integer.MAX_VALUE)); + } + + @Test + public void testCompare() { + assertEquals(-1, compare(0, 1)); + assertEquals(-1, compare(0L, 1L)); + assertEquals(-1, compare(0, Integer.MAX_VALUE)); + assertEquals(-1, compare(0L, Long.MAX_VALUE)); + assertEquals(0, compare(0, 0)); + assertEquals(0, compare(0L, 0L)); + assertEquals(0, compare(Integer.MIN_VALUE, Integer.MIN_VALUE)); + assertEquals(0, compare(Long.MIN_VALUE, Long.MIN_VALUE)); + assertEquals(1, compare(Integer.MAX_VALUE, 0)); + assertEquals(1, compare(Integer.MAX_VALUE, Integer.MAX_VALUE - 1)); + assertEquals(1, compare(Long.MAX_VALUE, 0L)); + assertEquals(1, compare(Long.MAX_VALUE, Long.MAX_VALUE - 1)); + } +} diff --git a/common/src/test/java/io/netty/util/internal/NativeLibraryLoaderTest.java b/common/src/test/java/io/netty/util/internal/NativeLibraryLoaderTest.java index de73b9e..e591b6c 100644 --- a/common/src/test/java/io/netty/util/internal/NativeLibraryLoaderTest.java +++ b/common/src/test/java/io/netty/util/internal/NativeLibraryLoaderTest.java @@ -15,11 +15,19 @@ */ package io.netty.util.internal; +import io.netty.util.CharsetUtil; import org.junit.Test; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; import java.util.UUID; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -65,4 +73,70 @@ public class NativeLibraryLoaderTest { throw new RuntimeException(e); } } + + @Test + public void testPatchingId() throws IOException { + testPatchingId0(true, false); + } + + @Test + public void testPatchingIdWithOsArch() throws IOException { + testPatchingId0(true, true); + } + + @Test + public void testPatchingIdNotMatch() throws IOException { + testPatchingId0(false, false); + } + + @Test + public void testPatchingIdWithOsArchNotMatch() throws IOException { + testPatchingId0(false, true); + } + + private static void testPatchingId0(boolean match, boolean withOsArch) throws IOException { + byte[] bytes = new byte[1024]; + PlatformDependent.threadLocalRandom().nextBytes(bytes); + byte[] idBytes = ("/workspace/netty-tcnative/boringssl-static/target/" + + "native-build/target/lib/libnetty_tcnative-2.0.20.Final.jnilib").getBytes(CharsetUtil.UTF_8); + + String originalName; + if (match) { + originalName = "netty-tcnative"; + } else { + originalName = "nonexist_tcnative"; + } + String name = "shaded_" + originalName; + if (withOsArch) { + name += "_osx_x86_64"; + } + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + out.write(bytes, 0, bytes.length); + out.write(idBytes, 0, idBytes.length); + out.write(bytes, 0 , bytes.length); + + out.flush(); + byte[] inBytes = out.toByteArray(); + out.close(); + + InputStream inputStream = new ByteArrayInputStream(inBytes); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + try { + assertEquals(match, + NativeLibraryLoader.patchShadedLibraryId(inputStream, outputStream, originalName, name)); + + outputStream.flush(); + byte[] outputBytes = outputStream.toByteArray(); + assertArrayEquals(bytes, Arrays.copyOfRange(outputBytes, 0, bytes.length)); + byte[] patchedId = Arrays.copyOfRange(outputBytes, bytes.length, bytes.length + idBytes.length); + assertEquals(!match, Arrays.equals(idBytes, patchedId)); + assertArrayEquals(bytes, + Arrays.copyOfRange(outputBytes, bytes.length + idBytes.length, outputBytes.length)); + assertEquals(inBytes.length, outputBytes.length); + } finally { + inputStream.close(); + outputStream.close(); + } + } } diff --git a/common/src/test/java/io/netty/util/internal/StringUtilTest.java b/common/src/test/java/io/netty/util/internal/StringUtilTest.java index 13fe499..c9f6b28 100644 --- a/common/src/test/java/io/netty/util/internal/StringUtilTest.java +++ b/common/src/test/java/io/netty/util/internal/StringUtilTest.java @@ -18,9 +18,13 @@ package io.netty.util.internal; import org.junit.Test; import java.util.Arrays; +import java.util.Collections; import static io.netty.util.internal.StringUtil.NEWLINE; import static io.netty.util.internal.StringUtil.commonSuffixOfLength; +import static io.netty.util.internal.StringUtil.indexOfWhiteSpace; +import static io.netty.util.internal.StringUtil.indexOfNonWhiteSpace; +import static io.netty.util.internal.StringUtil.isNullOrEmpty; import static io.netty.util.internal.StringUtil.simpleClassName; import static io.netty.util.internal.StringUtil.substringAfter; import static io.netty.util.internal.StringUtil.toHexString; @@ -446,15 +450,15 @@ public class StringUtilTest { @Test public void testUnescapeCsvFields() { - assertEquals(Arrays.asList(""), unescapeCsvFields("")); + assertEquals(Collections.singletonList(""), unescapeCsvFields("")); assertEquals(Arrays.asList("", ""), unescapeCsvFields(",")); assertEquals(Arrays.asList("a", ""), unescapeCsvFields("a,")); assertEquals(Arrays.asList("", "a"), unescapeCsvFields(",a")); - assertEquals(Arrays.asList("\""), unescapeCsvFields("\"\"\"\"")); + assertEquals(Collections.singletonList("\""), unescapeCsvFields("\"\"\"\"")); assertEquals(Arrays.asList("\"", "\""), unescapeCsvFields("\"\"\"\",\"\"\"\"")); - assertEquals(Arrays.asList("netty"), unescapeCsvFields("netty")); + assertEquals(Collections.singletonList("netty"), unescapeCsvFields("netty")); assertEquals(Arrays.asList("hello", "netty"), unescapeCsvFields("hello,netty")); - assertEquals(Arrays.asList("hello,netty"), unescapeCsvFields("\"hello,netty\"")); + assertEquals(Collections.singletonList("hello,netty"), unescapeCsvFields("\"hello,netty\"")); assertEquals(Arrays.asList("hello", "netty"), unescapeCsvFields("\"hello\",\"netty\"")); assertEquals(Arrays.asList("a\"b", "c\"d"), unescapeCsvFields("\"a\"\"b\",\"c\"\"d\"")); assertEquals(Arrays.asList("a\rb", "c\nd"), unescapeCsvFields("\"a\rb\",\"c\nd\"")); @@ -533,4 +537,58 @@ public class StringUtilTest { assertEquals("", StringUtil.trimOws("\t ").toString()); assertEquals("a b", StringUtil.trimOws("\ta b \t").toString()); } + + @Test + public void testJoin() { + assertEquals("", + StringUtil.join(",", Collections.<CharSequence>emptyList()).toString()); + assertEquals("a", + StringUtil.join(",", Collections.singletonList("a")).toString()); + assertEquals("a,b", + StringUtil.join(",", Arrays.asList("a", "b")).toString()); + assertEquals("a,b,c", + StringUtil.join(",", Arrays.asList("a", "b", "c")).toString()); + assertEquals("a,b,c,null,d", + StringUtil.join(",", Arrays.asList("a", "b", "c", null, "d")).toString()); + } + + @Test + public void testIsNullOrEmpty() { + assertTrue(isNullOrEmpty(null)); + assertTrue(isNullOrEmpty("")); + assertTrue(isNullOrEmpty(StringUtil.EMPTY_STRING)); + assertFalse(isNullOrEmpty(" ")); + assertFalse(isNullOrEmpty("\t")); + assertFalse(isNullOrEmpty("\n")); + assertFalse(isNullOrEmpty("foo")); + assertFalse(isNullOrEmpty(NEWLINE)); + } + + @Test + public void testIndexOfWhiteSpace() { + assertEquals(-1, indexOfWhiteSpace("", 0)); + assertEquals(0, indexOfWhiteSpace(" ", 0)); + assertEquals(-1, indexOfWhiteSpace(" ", 1)); + assertEquals(0, indexOfWhiteSpace("\n", 0)); + assertEquals(-1, indexOfWhiteSpace("\n", 1)); + assertEquals(0, indexOfWhiteSpace("\t", 0)); + assertEquals(-1, indexOfWhiteSpace("\t", 1)); + assertEquals(3, indexOfWhiteSpace("foo\r\nbar", 1)); + assertEquals(-1, indexOfWhiteSpace("foo\r\nbar", 10)); + assertEquals(7, indexOfWhiteSpace("foo\tbar\r\n", 6)); + assertEquals(-1, indexOfWhiteSpace("foo\tbar\r\n", Integer.MAX_VALUE)); + } + + @Test + public void testIndexOfNonWhiteSpace() { + assertEquals(-1, indexOfNonWhiteSpace("", 0)); + assertEquals(-1, indexOfNonWhiteSpace(" ", 0)); + assertEquals(-1, indexOfNonWhiteSpace(" \t", 0)); + assertEquals(-1, indexOfNonWhiteSpace(" \t\r\n", 0)); + assertEquals(2, indexOfNonWhiteSpace(" \tfoo\r\n", 0)); + assertEquals(2, indexOfNonWhiteSpace(" \tfoo\r\n", 1)); + assertEquals(4, indexOfNonWhiteSpace(" \tfoo\r\n", 4)); + assertEquals(-1, indexOfNonWhiteSpace(" \tfoo\r\n", 10)); + assertEquals(-1, indexOfNonWhiteSpace(" \tfoo\r\n", Integer.MAX_VALUE)); + } } diff --git a/common/src/test/java/io/netty/util/internal/ThreadExecutorMapTest.java b/common/src/test/java/io/netty/util/internal/ThreadExecutorMapTest.java new file mode 100644 index 0000000..22069e4 --- /dev/null +++ b/common/src/test/java/io/netty/util/internal/ThreadExecutorMapTest.java @@ -0,0 +1,63 @@ +/* + * Copyright 2019 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.util.internal; + +import io.netty.util.concurrent.ImmediateEventExecutor; +import io.netty.util.concurrent.ImmediateExecutor; +import org.junit.Assert; +import org.junit.Test; + +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; + +public class ThreadExecutorMapTest { + + @Test + public void testDecorateExecutor() { + Executor executor = ThreadExecutorMap.apply(ImmediateExecutor.INSTANCE, ImmediateEventExecutor.INSTANCE); + executor.execute(new Runnable() { + @Override + public void run() { + Assert.assertSame(ImmediateEventExecutor.INSTANCE, ThreadExecutorMap.currentExecutor()); + } + }); + } + + @Test + public void testDecorateRunnable() { + ThreadExecutorMap.apply(new Runnable() { + @Override + public void run() { + Assert.assertSame(ImmediateEventExecutor.INSTANCE, ThreadExecutorMap.currentExecutor()); + } + }, ImmediateEventExecutor.INSTANCE).run(); + } + + @Test + public void testDecorateThreadFactory() throws InterruptedException { + ThreadFactory threadFactory = + ThreadExecutorMap.apply(Executors.defaultThreadFactory(), ImmediateEventExecutor.INSTANCE); + Thread thread = threadFactory.newThread(new Runnable() { + @Override + public void run() { + Assert.assertSame(ImmediateEventExecutor.INSTANCE, ThreadExecutorMap.currentExecutor()); + } + }); + thread.start(); + thread.join(); + } +} diff --git a/common/src/test/java/io/netty/util/internal/logging/Log4J2LoggerTest.java b/common/src/test/java/io/netty/util/internal/logging/Log4J2LoggerTest.java index a72eb9a..9f86a85 100644 --- a/common/src/test/java/io/netty/util/internal/logging/Log4J2LoggerTest.java +++ b/common/src/test/java/io/netty/util/internal/logging/Log4J2LoggerTest.java @@ -50,7 +50,7 @@ public class Log4J2LoggerTest extends AbstractInternalLoggerTest<Logger> { result.put("level", level.name()); result.put("t", t); super.logMessage(fqcn, level, marker, message, t); - }; + } }; } diff --git a/common/src/test/java/io/netty/util/internal/logging/Slf4JLoggerFactoryTest.java b/common/src/test/java/io/netty/util/internal/logging/Slf4JLoggerFactoryTest.java index 8cb2f84..bf0f30b 100644 --- a/common/src/test/java/io/netty/util/internal/logging/Slf4JLoggerFactoryTest.java +++ b/common/src/test/java/io/netty/util/internal/logging/Slf4JLoggerFactoryTest.java @@ -27,7 +27,6 @@ import java.util.Iterator; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; public class Slf4JLoggerFactoryTest { @@ -71,46 +70,56 @@ public class Slf4JLoggerFactoryTest { InternalLogger internalLogger = Slf4JLoggerFactory.wrapLogger(logger); internalLogger.debug("{}", "debug"); internalLogger.debug("{} {}", "debug1", "debug2"); + internalLogger.debug("{} {} {}", "debug1", "debug2", "debug3"); internalLogger.error("{}", "error"); internalLogger.error("{} {}", "error1", "error2"); + internalLogger.error("{} {} {}", "error1", "error2", "error3"); internalLogger.info("{}", "info"); internalLogger.info("{} {}", "info1", "info2"); + internalLogger.info("{} {} {}", "info1", "info2", "info3"); internalLogger.trace("{}", "trace"); internalLogger.trace("{} {}", "trace1", "trace2"); + internalLogger.trace("{} {} {}", "trace1", "trace2", "trace3"); internalLogger.warn("{}", "warn"); internalLogger.warn("{} {}", "warn1", "warn2"); + internalLogger.warn("{} {} {}", "warn1", "warn2", "warn3"); - verify(logger, times(2)).log(ArgumentMatchers.<Marker>isNull(), eq(LocationAwareSlf4JLogger.FQCN), + verify(logger, times(3)).log(ArgumentMatchers.<Marker>isNull(), eq(LocationAwareSlf4JLogger.FQCN), eq(LocationAwareLogger.DEBUG_INT), captor.capture(), any(Object[].class), ArgumentMatchers.<Throwable>isNull()); - verify(logger, times(2)).log(ArgumentMatchers.<Marker>isNull(), eq(LocationAwareSlf4JLogger.FQCN), + verify(logger, times(3)).log(ArgumentMatchers.<Marker>isNull(), eq(LocationAwareSlf4JLogger.FQCN), eq(LocationAwareLogger.ERROR_INT), captor.capture(), any(Object[].class), ArgumentMatchers.<Throwable>isNull()); - verify(logger, times(2)).log(ArgumentMatchers.<Marker>isNull(), eq(LocationAwareSlf4JLogger.FQCN), + verify(logger, times(3)).log(ArgumentMatchers.<Marker>isNull(), eq(LocationAwareSlf4JLogger.FQCN), eq(LocationAwareLogger.INFO_INT), captor.capture(), any(Object[].class), ArgumentMatchers.<Throwable>isNull()); - verify(logger, times(2)).log(ArgumentMatchers.<Marker>isNull(), eq(LocationAwareSlf4JLogger.FQCN), + verify(logger, times(3)).log(ArgumentMatchers.<Marker>isNull(), eq(LocationAwareSlf4JLogger.FQCN), eq(LocationAwareLogger.TRACE_INT), captor.capture(), any(Object[].class), ArgumentMatchers.<Throwable>isNull()); - verify(logger, times(2)).log(ArgumentMatchers.<Marker>isNull(), eq(LocationAwareSlf4JLogger.FQCN), + verify(logger, times(3)).log(ArgumentMatchers.<Marker>isNull(), eq(LocationAwareSlf4JLogger.FQCN), eq(LocationAwareLogger.WARN_INT), captor.capture(), any(Object[].class), ArgumentMatchers.<Throwable>isNull()); Iterator<String> logMessages = captor.getAllValues().iterator(); assertEquals("debug", logMessages.next()); assertEquals("debug1 debug2", logMessages.next()); + assertEquals("debug1 debug2 debug3", logMessages.next()); assertEquals("error", logMessages.next()); assertEquals("error1 error2", logMessages.next()); + assertEquals("error1 error2 error3", logMessages.next()); assertEquals("info", logMessages.next()); assertEquals("info1 info2", logMessages.next()); + assertEquals("info1 info2 info3", logMessages.next()); assertEquals("trace", logMessages.next()); assertEquals("trace1 trace2", logMessages.next()); + assertEquals("trace1 trace2 trace3", logMessages.next()); assertEquals("warn", logMessages.next()); assertEquals("warn1 warn2", logMessages.next()); + assertEquals("warn1 warn2 warn3", logMessages.next()); assertFalse(logMessages.hasNext()); } } diff --git a/common/src/test/templates/io/netty/util/collection/KObjectHashMapTest.template b/common/src/test/templates/io/netty/util/collection/KObjectHashMapTest.template index 2be12a9..d4214f7 100644 --- a/common/src/test/templates/io/netty/util/collection/KObjectHashMapTest.template +++ b/common/src/test/templates/io/netty/util/collection/KObjectHashMapTest.template @@ -613,4 +613,34 @@ public class @K@ObjectHashMapTest { } assertTrue(map.isEmpty()); } + + @Test + public void valuesIteratorRemove() { + Value v1 = new Value("v1"); + Value v2 = new Value("v2"); + Value v3 = new Value("v3"); + map.put((@k@) 1, v1); + map.put((@k@) 2, v2); + map.put((@k@) 3, v3); + + Iterator<Value> it = map.values().iterator(); + + assertSame(v1, it.next()); + assertSame(v2, it.next()); + it.remove(); + + assertSame(v3, it.next()); + assertFalse(it.hasNext()); + + assertEquals(2, map.size()); + assertSame(v1, map.get((@k@) 1)); + assertNull(map.get((@k@) 2)); + assertSame(v3, map.get((@k@) 3)); + + it = map.values().iterator(); + + assertSame(v1, it.next()); + assertSame(v3, it.next()); + assertFalse(it.hasNext()); + } } diff --git a/dev-tools/pom.xml b/dev-tools/pom.xml index 41b5484..94313aa 100644 --- a/dev-tools/pom.xml +++ b/dev-tools/pom.xml @@ -25,7 +25,7 @@ <groupId>io.netty</groupId> <artifactId>netty-dev-tools</artifactId> - <version>4.1.33.Final</version> + <version>4.1.48.Final</version> <name>Netty/Dev-Tools</name> @@ -52,6 +52,6 @@ </build> <scm> - <tag>netty-4.1.33.Final</tag> + <tag>netty-4.1.48.Final</tag> </scm> </project> diff --git a/docker/Dockerfile.centos b/docker/Dockerfile.centos index 4d88820..e6df940 100644 --- a/docker/Dockerfile.centos +++ b/docker/Dockerfile.centos @@ -25,3 +25,6 @@ RUN curl -sL https://github.com/shyiko/jabba/raw/master/install.sh | JABBA_COMMA RUN echo 'export JAVA_HOME="/jdk"' >> ~/.bashrc RUN echo 'PATH=/jdk/bin:$PATH' >> ~/.bashrc + +# when the JDK is GraalVM install native-image +RUN if [ -O /jdk/bin/gu ]; then /jdk/bin/gu install native-image; else echo "Not GraalVM, skip installation of native-image" ; fi diff --git a/docker/README.md b/docker/README.md index 9970aae..d96242b 100644 --- a/docker/README.md +++ b/docker/README.md @@ -10,10 +10,10 @@ cd /path/to/netty/ docker-compose -f docker/docker-compose.yaml -f docker/docker-compose.centos-6.18.yaml run test ``` -## centos 7 with java 9 +## centos 7 with java 11 ``` -docker-compose -f docker/docker-compose.yaml -f docker/docker-compose.centos-7.19.yaml run test +docker-compose -f docker/docker-compose.yaml -f docker/docker-compose.centos-7.111.yaml run test ``` etc, etc diff --git a/docker/docker-compose.centos-6.110.yaml b/docker/docker-compose.centos-6.110.yaml index 144780d..7a60f46 100644 --- a/docker/docker-compose.centos-6.110.yaml +++ b/docker/docker-compose.centos-6.110.yaml @@ -7,7 +7,7 @@ services: build: args: centos_version : "6" - java_version : "openjdk@1.10.0-2" + java_version : "zulu@1.10.0-2" test: image: netty:centos-6-1.10 diff --git a/docker/docker-compose.centos-6.111.yaml b/docker/docker-compose.centos-6.111.yaml index 73618c7..d354a68 100644 --- a/docker/docker-compose.centos-6.111.yaml +++ b/docker/docker-compose.centos-6.111.yaml @@ -7,7 +7,7 @@ services: build: args: centos_version : "6" - java_version : "openjdk@1.11.0-2" + java_version : "adopt@1.11.0-6" test: image: netty:centos-6-1.11 diff --git a/docker/docker-compose.centos-6.112.yaml b/docker/docker-compose.centos-6.112.yaml index 08b5411..aee3e1f 100644 --- a/docker/docker-compose.centos-6.112.yaml +++ b/docker/docker-compose.centos-6.112.yaml @@ -7,7 +7,7 @@ services: build: args: centos_version : "6" - java_version : "openjdk@1.12.0-27" + java_version : "adopt@1.12.0-2" test: image: netty:centos-6-1.12 diff --git a/docker/docker-compose.centos-6.113.yaml b/docker/docker-compose.centos-6.113.yaml index c826067..82f53eb 100644 --- a/docker/docker-compose.centos-6.113.yaml +++ b/docker/docker-compose.centos-6.113.yaml @@ -7,7 +7,7 @@ services: build: args: centos_version : "6" - java_version : "openjdk@1.13.0-3" + java_version : "adopt@1.13.0-1" test: image: netty:centos-6-1.13 diff --git a/docker/docker-compose.centos-6.18.yaml b/docker/docker-compose.centos-6.18.yaml index ecde4da..f53b1cb 100644 --- a/docker/docker-compose.centos-6.18.yaml +++ b/docker/docker-compose.centos-6.18.yaml @@ -7,7 +7,7 @@ services: build: args: centos_version : "6" - java_version : "1.8.202" + java_version : "adopt@1.8.0-242" test: image: netty:centos-6-1.8 diff --git a/docker/docker-compose.centos-6.19.yaml b/docker/docker-compose.centos-6.19.yaml index 93588d6..ee34c53 100644 --- a/docker/docker-compose.centos-6.19.yaml +++ b/docker/docker-compose.centos-6.19.yaml @@ -7,7 +7,7 @@ services: build: args: centos_version : "6" - java_version : "openjdk@1.9.0-4" + java_version : "zulu@1.9.0-7" test: image: netty:centos-6-1.9 diff --git a/docker/docker-compose.centos-6.graalvm1.yaml b/docker/docker-compose.centos-6.graalvm1.yaml new file mode 100644 index 0000000..8b74216 --- /dev/null +++ b/docker/docker-compose.centos-6.graalvm1.yaml @@ -0,0 +1,22 @@ +version: "3" + +services: + + runtime-setup: + image: netty:centos-6-1.8 + build: + args: + centos_version : "6" + java_version : "graalvm@19.2.1" + + test: + image: netty:centos-6-1.8 + + test-leak: + image: netty:centos-6-1.8 + + test-boringssl-static: + image: netty:centos-6-1.8 + + shell: + image: netty:centos-6-1.8 diff --git a/docker/docker-compose.centos-6.openj9111.yaml b/docker/docker-compose.centos-6.openj9111.yaml new file mode 100644 index 0000000..6c1f2c5 --- /dev/null +++ b/docker/docker-compose.centos-6.openj9111.yaml @@ -0,0 +1,22 @@ +version: "3" + +services: + + runtime-setup: + image: netty:centos-6-openj9-1.11 + build: + args: + centos_version : "6" + java_version : "adopt-openj9@1.11.0-6" + + test: + image: netty:centos-6-openj9-1.11 + + test-leak: + image: netty:centos-6-openj9-1.11 + + test-boringssl-static: + image: netty:centos-6-openj9-1.11 + + shell: + image: netty:centos-6-openj9-1.11 diff --git a/docker/docker-compose.centos-7.110.yaml b/docker/docker-compose.centos-7.110.yaml index 7f16520..280ed73 100644 --- a/docker/docker-compose.centos-7.110.yaml +++ b/docker/docker-compose.centos-7.110.yaml @@ -7,7 +7,7 @@ services: build: args: centos_version : "7" - java_version : "openjdk@1.10.0-2" + java_version : "zulu@1.10.0-2" test: image: netty:centos-7-1.10 diff --git a/docker/docker-compose.centos-7.111.yaml b/docker/docker-compose.centos-7.111.yaml index 75d635d..391f915 100644 --- a/docker/docker-compose.centos-7.111.yaml +++ b/docker/docker-compose.centos-7.111.yaml @@ -7,7 +7,7 @@ services: build: args: centos_version : "7" - java_version : "openjdk@1.11.0-2" + java_version : "adopt@1.11.0-6" test: image: netty:centos-7-1.11 diff --git a/docker/docker-compose.centos-7.112.yaml b/docker/docker-compose.centos-7.112.yaml index b85a1d6..d628188 100644 --- a/docker/docker-compose.centos-7.112.yaml +++ b/docker/docker-compose.centos-7.112.yaml @@ -7,7 +7,7 @@ services: build: args: centos_version : "7" - java_version : "openjdk@1.12.0-27" + java_version : "adopt@1.12.0-2" test: image: netty:centos-7-1.12 diff --git a/docker/docker-compose.centos-7.113.yaml b/docker/docker-compose.centos-7.113.yaml index e49a045..2be6042 100644 --- a/docker/docker-compose.centos-7.113.yaml +++ b/docker/docker-compose.centos-7.113.yaml @@ -7,7 +7,7 @@ services: build: args: centos_version : "7" - java_version : "openjdk@1.13.0-3" + java_version : "adopt@1.13.0-1" test: image: netty:centos-7-1.13 diff --git a/docker/docker-compose.centos-7.18.yaml b/docker/docker-compose.centos-7.18.yaml index 3279201..04d60a4 100644 --- a/docker/docker-compose.centos-7.18.yaml +++ b/docker/docker-compose.centos-7.18.yaml @@ -7,7 +7,7 @@ services: build: args: centos_version : "7" - java_version : "1.8.202" + java_version : "adopt@1.8.0-242" test: image: netty:centos-7-1.8 diff --git a/docker/docker-compose.centos-7.19.yaml b/docker/docker-compose.centos-7.19.yaml index 5f1af4f..d1b6acf 100644 --- a/docker/docker-compose.centos-7.19.yaml +++ b/docker/docker-compose.centos-7.19.yaml @@ -7,7 +7,7 @@ services: build: args: centos_version : "7" - java_version : "openjdk@1.9.0-4" + java_version : "zulu@1.9.0-7" test: image: netty:centos-7-1.9 diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 3e3bd2c..5d9ec20 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -12,9 +12,9 @@ services: image: netty:default depends_on: [runtime-setup] volumes: - - ~/.ssh:/root/.ssh - - ~/.gnupg:/root/.gnupg - - ..:/code + - ~/.ssh:/root/.ssh:delegated + - ~/.gnupg:/root/.gnupg:delegated + - ..:/code:delegated working_dir: /code test-leak: @@ -35,8 +35,8 @@ services: - SANOTYPE_USER - SANOTYPE_PASSWORD volumes: - - ~/.ssh:/root/.ssh - - ~/.gnupg:/root/.gnupg - - ..:/code - - ~/.m2:/root/.m2 + - ~/.ssh:/root/.ssh:delegated + - ~/.gnupg:/root/.gnupg:delegated + - ..:/code:delegated + - ~/.m2:/root/.m2:delegated entrypoint: /bin/bash diff --git a/example/pom.xml b/example/pom.xml index aac14ac..5adfe2e 100644 --- a/example/pom.xml +++ b/example/pom.xml @@ -21,7 +21,7 @@ <parent> <groupId>io.netty</groupId> <artifactId>netty-parent</artifactId> - <version>4.1.33.Final</version> + <version>4.1.48.Final</version> </parent> <artifactId>netty-example</artifactId> @@ -29,6 +29,9 @@ <name>Netty/Example</name> + <properties> + <skipJapicmp>true</skipJapicmp> + </properties> <dependencies> <dependency> <groupId>${project.groupId}</groupId> @@ -95,6 +98,11 @@ <artifactId>netty-codec-stomp</artifactId> <version>${project.version}</version> </dependency> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>netty-codec-mqtt</artifactId> + <version>${project.version}</version> + </dependency> <dependency> <groupId>com.google.protobuf</groupId> diff --git a/example/src/main/java/io/netty/example/http/cors/OkResponseHandler.java b/example/src/main/java/io/netty/example/http/cors/OkResponseHandler.java index 4ad71e6..6616965 100644 --- a/example/src/main/java/io/netty/example/http/cors/OkResponseHandler.java +++ b/example/src/main/java/io/netty/example/http/cors/OkResponseHandler.java @@ -15,6 +15,7 @@ */ package io.netty.example.http.cors; +import io.netty.buffer.Unpooled; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; @@ -30,7 +31,8 @@ import io.netty.handler.codec.http.HttpVersion; public class OkResponseHandler extends SimpleChannelInboundHandler<Object> { @Override public void channelRead0(ChannelHandlerContext ctx, Object msg) { - final FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK); + final FullHttpResponse response = new DefaultFullHttpResponse( + HttpVersion.HTTP_1_1, HttpResponseStatus.OK, Unpooled.EMPTY_BUFFER); response.headers().set("custom-response-header", "Some value"); ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); } diff --git a/example/src/main/java/io/netty/example/http/file/HttpStaticFileServerHandler.java b/example/src/main/java/io/netty/example/http/file/HttpStaticFileServerHandler.java index e2a8013..8424a8c 100644 --- a/example/src/main/java/io/netty/example/http/file/HttpStaticFileServerHandler.java +++ b/example/src/main/java/io/netty/example/http/file/HttpStaticFileServerHandler.java @@ -110,36 +110,40 @@ public class HttpStaticFileServerHandler extends SimpleChannelInboundHandler<Ful public static final String HTTP_DATE_GMT_TIMEZONE = "GMT"; public static final int HTTP_CACHE_SECONDS = 60; + private FullHttpRequest request; + @Override public void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception { + this.request = request; if (!request.decoderResult().isSuccess()) { sendError(ctx, BAD_REQUEST); return; } - if (request.method() != GET) { - sendError(ctx, METHOD_NOT_ALLOWED); + if (!GET.equals(request.method())) { + this.sendError(ctx, METHOD_NOT_ALLOWED); return; } + final boolean keepAlive = HttpUtil.isKeepAlive(request); final String uri = request.uri(); final String path = sanitizeUri(uri); if (path == null) { - sendError(ctx, FORBIDDEN); + this.sendError(ctx, FORBIDDEN); return; } File file = new File(path); if (file.isHidden() || !file.exists()) { - sendError(ctx, NOT_FOUND); + this.sendError(ctx, NOT_FOUND); return; } if (file.isDirectory()) { if (uri.endsWith("/")) { - sendListing(ctx, file, uri); + this.sendListing(ctx, file, uri); } else { - sendRedirect(ctx, uri + '/'); + this.sendRedirect(ctx, uri + '/'); } return; } @@ -160,7 +164,7 @@ public class HttpStaticFileServerHandler extends SimpleChannelInboundHandler<Ful long ifModifiedSinceDateSeconds = ifModifiedSinceDate.getTime() / 1000; long fileLastModifiedSeconds = file.lastModified() / 1000; if (ifModifiedSinceDateSeconds == fileLastModifiedSeconds) { - sendNotModified(ctx); + this.sendNotModified(ctx); return; } } @@ -178,7 +182,10 @@ public class HttpStaticFileServerHandler extends SimpleChannelInboundHandler<Ful HttpUtil.setContentLength(response, fileLength); setContentTypeHeader(response, file); setDateAndCacheHeaders(response, file); - if (HttpUtil.isKeepAlive(request)) { + + if (!keepAlive) { + response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE); + } else if (request.protocolVersion().equals(HTTP_1_0)) { response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE); } @@ -218,7 +225,7 @@ public class HttpStaticFileServerHandler extends SimpleChannelInboundHandler<Ful }); // Decide whether to close the connection or not. - if (!HttpUtil.isKeepAlive(request)) { + if (!keepAlive) { // Close the connection when the whole content is written out. lastContentFuture.addListener(ChannelFutureListener.CLOSE); } @@ -264,10 +271,7 @@ public class HttpStaticFileServerHandler extends SimpleChannelInboundHandler<Ful private static final Pattern ALLOWED_FILE_NAME = Pattern.compile("[^-\\._]?[^<>&\\\"]*"); - private static void sendListing(ChannelHandlerContext ctx, File dir, String dirPath) { - FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, OK); - response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=UTF-8"); - + private void sendListing(ChannelHandlerContext ctx, File dir, String dirPath) { StringBuilder buf = new StringBuilder() .append("<!DOCTYPE html>\r\n") .append("<html><head><meta charset='utf-8' /><title>") @@ -282,47 +286,50 @@ public class HttpStaticFileServerHandler extends SimpleChannelInboundHandler<Ful .append("<ul>") .append("<li><a href=\"../\">..</a></li>\r\n"); - for (File f: dir.listFiles()) { - if (f.isHidden() || !f.canRead()) { - continue; - } + File[] files = dir.listFiles(); + if (files != null) { + for (File f: files) { + if (f.isHidden() || !f.canRead()) { + continue; + } - String name = f.getName(); - if (!ALLOWED_FILE_NAME.matcher(name).matches()) { - continue; - } + String name = f.getName(); + if (!ALLOWED_FILE_NAME.matcher(name).matches()) { + continue; + } - buf.append("<li><a href=\"") - .append(name) - .append("\">") - .append(name) - .append("</a></li>\r\n"); + buf.append("<li><a href=\"") + .append(name) + .append("\">") + .append(name) + .append("</a></li>\r\n"); + } } buf.append("</ul></body></html>\r\n"); - ByteBuf buffer = Unpooled.copiedBuffer(buf, CharsetUtil.UTF_8); - response.content().writeBytes(buffer); - buffer.release(); - // Close the connection as soon as the error message is sent. - ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); + ByteBuf buffer = ctx.alloc().buffer(buf.length()); + buffer.writeCharSequence(buf.toString(), CharsetUtil.UTF_8); + + FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, OK, buffer); + response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=UTF-8"); + + this.sendAndCleanupConnection(ctx, response); } - private static void sendRedirect(ChannelHandlerContext ctx, String newUri) { - FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, FOUND); + private void sendRedirect(ChannelHandlerContext ctx, String newUri) { + FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, FOUND, Unpooled.EMPTY_BUFFER); response.headers().set(HttpHeaderNames.LOCATION, newUri); - // Close the connection as soon as the error message is sent. - ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); + this.sendAndCleanupConnection(ctx, response); } - private static void sendError(ChannelHandlerContext ctx, HttpResponseStatus status) { + private void sendError(ChannelHandlerContext ctx, HttpResponseStatus status) { FullHttpResponse response = new DefaultFullHttpResponse( HTTP_1_1, status, Unpooled.copiedBuffer("Failure: " + status + "\r\n", CharsetUtil.UTF_8)); response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=UTF-8"); - // Close the connection as soon as the error message is sent. - ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); + this.sendAndCleanupConnection(ctx, response); } /** @@ -331,12 +338,35 @@ public class HttpStaticFileServerHandler extends SimpleChannelInboundHandler<Ful * @param ctx * Context */ - private static void sendNotModified(ChannelHandlerContext ctx) { - FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, NOT_MODIFIED); + private void sendNotModified(ChannelHandlerContext ctx) { + FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, NOT_MODIFIED, Unpooled.EMPTY_BUFFER); setDateHeader(response); - // Close the connection as soon as the error message is sent. - ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); + this.sendAndCleanupConnection(ctx, response); + } + + /** + * If Keep-Alive is disabled, attaches "Connection: close" header to the response + * and closes the connection after the response being sent. + */ + private void sendAndCleanupConnection(ChannelHandlerContext ctx, FullHttpResponse response) { + final FullHttpRequest request = this.request; + final boolean keepAlive = HttpUtil.isKeepAlive(request); + HttpUtil.setContentLength(response, response.content().readableBytes()); + if (!keepAlive) { + // We're going to close the connection as soon as the response is sent, + // so we should also make it clear for the client. + response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE); + } else if (request.protocolVersion().equals(HTTP_1_0)) { + response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE); + } + + ChannelFuture flushPromise = ctx.writeAndFlush(response); + + if (!keepAlive) { + // Close the connection as soon as the response is sent. + flushPromise.addListener(ChannelFutureListener.CLOSE); + } } /** diff --git a/example/src/main/java/io/netty/example/http/helloworld/HttpHelloWorldServerHandler.java b/example/src/main/java/io/netty/example/http/helloworld/HttpHelloWorldServerHandler.java index 3faf51a..ebc5be7 100644 --- a/example/src/main/java/io/netty/example/http/helloworld/HttpHelloWorldServerHandler.java +++ b/example/src/main/java/io/netty/example/http/helloworld/HttpHelloWorldServerHandler.java @@ -16,27 +16,27 @@ package io.netty.example.http.helloworld; import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.handler.codec.http.DefaultFullHttpResponse; import io.netty.handler.codec.http.FullHttpResponse; import io.netty.handler.codec.http.HttpObject; -import io.netty.handler.codec.http.HttpUtil; import io.netty.handler.codec.http.HttpRequest; -import io.netty.util.AsciiString; +import io.netty.handler.codec.http.HttpUtil; -import static io.netty.handler.codec.http.HttpResponseStatus.*; -import static io.netty.handler.codec.http.HttpVersion.*; +import static io.netty.handler.codec.http.HttpHeaderNames.CONNECTION; +import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_LENGTH; +import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE; +import static io.netty.handler.codec.http.HttpHeaderValues.CLOSE; +import static io.netty.handler.codec.http.HttpHeaderValues.KEEP_ALIVE; +import static io.netty.handler.codec.http.HttpHeaderValues.TEXT_PLAIN; +import static io.netty.handler.codec.http.HttpResponseStatus.OK; public class HttpHelloWorldServerHandler extends SimpleChannelInboundHandler<HttpObject> { private static final byte[] CONTENT = { 'H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd' }; - private static final AsciiString CONTENT_TYPE = AsciiString.cached("Content-Type"); - private static final AsciiString CONTENT_LENGTH = AsciiString.cached("Content-Length"); - private static final AsciiString CONNECTION = AsciiString.cached("Connection"); - private static final AsciiString KEEP_ALIVE = AsciiString.cached("keep-alive"); - @Override public void channelReadComplete(ChannelHandlerContext ctx) { ctx.flush(); @@ -48,15 +48,25 @@ public class HttpHelloWorldServerHandler extends SimpleChannelInboundHandler<Htt HttpRequest req = (HttpRequest) msg; boolean keepAlive = HttpUtil.isKeepAlive(req); - FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, OK, Unpooled.wrappedBuffer(CONTENT)); - response.headers().set(CONTENT_TYPE, "text/plain"); - response.headers().setInt(CONTENT_LENGTH, response.content().readableBytes()); + FullHttpResponse response = new DefaultFullHttpResponse(req.protocolVersion(), OK, + Unpooled.wrappedBuffer(CONTENT)); + response.headers() + .set(CONTENT_TYPE, TEXT_PLAIN) + .setInt(CONTENT_LENGTH, response.content().readableBytes()); - if (!keepAlive) { - ctx.write(response).addListener(ChannelFutureListener.CLOSE); + if (keepAlive) { + if (!req.protocolVersion().isKeepAliveDefault()) { + response.headers().set(CONNECTION, KEEP_ALIVE); + } } else { - response.headers().set(CONNECTION, KEEP_ALIVE); - ctx.write(response); + // Tell the client we're going to close the connection. + response.headers().set(CONNECTION, CLOSE); + } + + ChannelFuture f = ctx.write(response); + + if (!keepAlive) { + f.addListener(ChannelFutureListener.CLOSE); } } } diff --git a/example/src/main/java/io/netty/example/http/snoop/HttpSnoopClient.java b/example/src/main/java/io/netty/example/http/snoop/HttpSnoopClient.java index 2805559..7b10812 100644 --- a/example/src/main/java/io/netty/example/http/snoop/HttpSnoopClient.java +++ b/example/src/main/java/io/netty/example/http/snoop/HttpSnoopClient.java @@ -16,6 +16,7 @@ package io.netty.example.http.snoop; import io.netty.bootstrap.Bootstrap; +import io.netty.buffer.Unpooled; import io.netty.channel.Channel; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; @@ -83,7 +84,7 @@ public final class HttpSnoopClient { // Prepare the HTTP request. HttpRequest request = new DefaultFullHttpRequest( - HttpVersion.HTTP_1_1, HttpMethod.GET, uri.getRawPath()); + HttpVersion.HTTP_1_1, HttpMethod.GET, uri.getRawPath(), Unpooled.EMPTY_BUFFER); request.headers().set(HttpHeaderNames.HOST, host); request.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE); request.headers().set(HttpHeaderNames.ACCEPT_ENCODING, HttpHeaderValues.GZIP); diff --git a/example/src/main/java/io/netty/example/http/snoop/HttpSnoopServerHandler.java b/example/src/main/java/io/netty/example/http/snoop/HttpSnoopServerHandler.java index fc555d5..df5b64f 100644 --- a/example/src/main/java/io/netty/example/http/snoop/HttpSnoopServerHandler.java +++ b/example/src/main/java/io/netty/example/http/snoop/HttpSnoopServerHandler.java @@ -185,7 +185,7 @@ public class HttpSnoopServerHandler extends SimpleChannelInboundHandler<Object> } private static void send100Continue(ChannelHandlerContext ctx) { - FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, CONTINUE); + FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, CONTINUE, Unpooled.EMPTY_BUFFER); ctx.write(response); } diff --git a/example/src/main/java/io/netty/example/http/upload/HttpUploadServer.java b/example/src/main/java/io/netty/example/http/upload/HttpUploadServer.java index a004f75..342468c 100644 --- a/example/src/main/java/io/netty/example/http/upload/HttpUploadServer.java +++ b/example/src/main/java/io/netty/example/http/upload/HttpUploadServer.java @@ -27,7 +27,7 @@ import io.netty.handler.ssl.SslContextBuilder; import io.netty.handler.ssl.util.SelfSignedCertificate; /** - * A HTTP server showing how to use the HTTP multipart package for file uploads and decoding post data. + * An HTTP server showing how to use the HTTP multipart package for file uploads and decoding post data. */ public final class HttpUploadServer { diff --git a/example/src/main/java/io/netty/example/http/upload/HttpUploadServerHandler.java b/example/src/main/java/io/netty/example/http/upload/HttpUploadServerHandler.java index 43f2d13..7c1c378 100644 --- a/example/src/main/java/io/netty/example/http/upload/HttpUploadServerHandler.java +++ b/example/src/main/java/io/netty/example/http/upload/HttpUploadServerHandler.java @@ -69,8 +69,6 @@ public class HttpUploadServerHandler extends SimpleChannelInboundHandler<HttpObj private HttpRequest request; - private boolean readingChunks; - private HttpData partialContent; private final StringBuilder responseContent = new StringBuilder(); @@ -144,9 +142,9 @@ public class HttpUploadServerHandler extends SimpleChannelInboundHandler<HttpObj } responseContent.append("\r\n\r\n"); - // if GET Method: should not try to create a HttpPostRequestDecoder - if (request.method().equals(HttpMethod.GET)) { - // GET Method: should not try to create a HttpPostRequestDecoder + // if GET Method: should not try to create an HttpPostRequestDecoder + if (HttpMethod.GET.equals(request.method())) { + // GET Method: should not try to create an HttpPostRequestDecoder // So stop here responseContent.append("\r\n\r\nEND OF GET CONTENT\r\n"); // Not now: LastHttpContent will be sent writeResponse(ctx.channel()); @@ -157,18 +155,16 @@ public class HttpUploadServerHandler extends SimpleChannelInboundHandler<HttpObj } catch (ErrorDataDecoderException e1) { e1.printStackTrace(); responseContent.append(e1.getMessage()); - writeResponse(ctx.channel()); - ctx.channel().close(); + writeResponse(ctx.channel(), true); return; } - readingChunks = HttpUtil.isTransferEncodingChunked(request); + boolean readingChunks = HttpUtil.isTransferEncodingChunked(request); responseContent.append("Is Chunked: " + readingChunks + "\r\n"); responseContent.append("IsMultipart: " + decoder.isMultipart() + "\r\n"); if (readingChunks) { // Chunk version responseContent.append("Chunks: "); - readingChunks = true; } } @@ -183,8 +179,7 @@ public class HttpUploadServerHandler extends SimpleChannelInboundHandler<HttpObj } catch (ErrorDataDecoderException e1) { e1.printStackTrace(); responseContent.append(e1.getMessage()); - writeResponse(ctx.channel()); - ctx.channel().close(); + writeResponse(ctx.channel(), true); return; } responseContent.append('o'); @@ -194,7 +189,6 @@ public class HttpUploadServerHandler extends SimpleChannelInboundHandler<HttpObj // example of reading only if at the end if (chunk instanceof LastHttpContent) { writeResponse(ctx.channel()); - readingChunks = false; reset(); } @@ -311,24 +305,27 @@ public class HttpUploadServerHandler extends SimpleChannelInboundHandler<HttpObj } private void writeResponse(Channel channel) { + writeResponse(channel, false); + } + + private void writeResponse(Channel channel, boolean forceClose) { // Convert the response content to a ChannelBuffer. ByteBuf buf = copiedBuffer(responseContent.toString(), CharsetUtil.UTF_8); responseContent.setLength(0); // Decide whether to close the connection or not. - boolean close = request.headers().contains(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE, true) - || request.protocolVersion().equals(HttpVersion.HTTP_1_0) - && !request.headers().contains(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE, true); + boolean keepAlive = HttpUtil.isKeepAlive(request) && !forceClose; // Build the response object. FullHttpResponse response = new DefaultFullHttpResponse( HttpVersion.HTTP_1_1, HttpResponseStatus.OK, buf); response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=UTF-8"); + response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, buf.readableBytes()); - if (!close) { - // There's no need to add 'Content-Length' header - // if this is the last response. - response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, buf.readableBytes()); + if (!keepAlive) { + response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE); + } else if (request.protocolVersion().equals(HttpVersion.HTTP_1_0)) { + response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE); } Set<Cookie> cookies; @@ -347,7 +344,7 @@ public class HttpUploadServerHandler extends SimpleChannelInboundHandler<HttpObj // Write the response. ChannelFuture future = channel.writeAndFlush(response); // Close the connection after the write operation is done if necessary. - if (close) { + if (!keepAlive) { future.addListener(ChannelFutureListener.CLOSE); } } @@ -432,8 +429,20 @@ public class HttpUploadServerHandler extends SimpleChannelInboundHandler<HttpObj response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=UTF-8"); response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, buf.readableBytes()); + // Decide whether to close the connection or not. + boolean keepAlive = HttpUtil.isKeepAlive(request); + if (!keepAlive) { + response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE); + } else if (request.protocolVersion().equals(HttpVersion.HTTP_1_0)) { + response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE); + } + // Write the response. - ctx.channel().writeAndFlush(response); + ChannelFuture future = ctx.channel().writeAndFlush(response); + // Close the connection after the write operation is done if necessary. + if (!keepAlive) { + future.addListener(ChannelFutureListener.CLOSE); + } } @Override diff --git a/example/src/main/java/io/netty/example/http/websocketx/benchmarkserver/WebSocketServerHandler.java b/example/src/main/java/io/netty/example/http/websocketx/benchmarkserver/WebSocketServerHandler.java index 78f61ba..4d6c8f3 100644 --- a/example/src/main/java/io/netty/example/http/websocketx/benchmarkserver/WebSocketServerHandler.java +++ b/example/src/main/java/io/netty/example/http/websocketx/benchmarkserver/WebSocketServerHandler.java @@ -16,7 +16,7 @@ package io.netty.example.http.websocketx.benchmarkserver; import io.netty.buffer.ByteBuf; -import io.netty.buffer.Unpooled; +import io.netty.buffer.ByteBufUtil; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; @@ -25,6 +25,7 @@ import io.netty.handler.codec.http.DefaultFullHttpResponse; import io.netty.handler.codec.http.FullHttpRequest; import io.netty.handler.codec.http.FullHttpResponse; import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.HttpUtil; import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame; import io.netty.handler.codec.http.websocketx.CloseWebSocketFrame; @@ -34,11 +35,9 @@ import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; import io.netty.handler.codec.http.websocketx.WebSocketFrame; import io.netty.handler.codec.http.websocketx.WebSocketServerHandshaker; import io.netty.handler.codec.http.websocketx.WebSocketServerHandshakerFactory; -import io.netty.util.CharsetUtil; import static io.netty.handler.codec.http.HttpMethod.*; import static io.netty.handler.codec.http.HttpResponseStatus.*; -import static io.netty.handler.codec.http.HttpVersion.*; /** * Handles handshakes and messages @@ -66,20 +65,22 @@ public class WebSocketServerHandler extends SimpleChannelInboundHandler<Object> private void handleHttpRequest(ChannelHandlerContext ctx, FullHttpRequest req) { // Handle a bad request. if (!req.decoderResult().isSuccess()) { - sendHttpResponse(ctx, req, new DefaultFullHttpResponse(HTTP_1_1, BAD_REQUEST)); + sendHttpResponse(ctx, req, new DefaultFullHttpResponse(req.protocolVersion(), BAD_REQUEST, + ctx.alloc().buffer(0))); return; } // Allow only GET methods. - if (req.method() != GET) { - sendHttpResponse(ctx, req, new DefaultFullHttpResponse(HTTP_1_1, FORBIDDEN)); + if (!GET.equals(req.method())) { + sendHttpResponse(ctx, req, new DefaultFullHttpResponse(req.protocolVersion(), FORBIDDEN, + ctx.alloc().buffer(0))); return; } // Send the demo page and favicon.ico if ("/".equals(req.uri())) { ByteBuf content = WebSocketServerBenchmarkPage.getContent(getWebSocketLocation(req)); - FullHttpResponse res = new DefaultFullHttpResponse(HTTP_1_1, OK, content); + FullHttpResponse res = new DefaultFullHttpResponse(req.protocolVersion(), OK, content); res.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=UTF-8"); HttpUtil.setContentLength(res, content.readableBytes()); @@ -87,8 +88,10 @@ public class WebSocketServerHandler extends SimpleChannelInboundHandler<Object> sendHttpResponse(ctx, req, res); return; } + if ("/favicon.ico".equals(req.uri())) { - FullHttpResponse res = new DefaultFullHttpResponse(HTTP_1_1, NOT_FOUND); + FullHttpResponse res = new DefaultFullHttpResponse(req.protocolVersion(), NOT_FOUND, + ctx.alloc().buffer(0)); sendHttpResponse(ctx, req, res); return; } @@ -126,20 +129,19 @@ public class WebSocketServerHandler extends SimpleChannelInboundHandler<Object> } } - private static void sendHttpResponse( - ChannelHandlerContext ctx, FullHttpRequest req, FullHttpResponse res) { + private static void sendHttpResponse(ChannelHandlerContext ctx, FullHttpRequest req, FullHttpResponse res) { // Generate an error page if response getStatus code is not OK (200). - if (res.status().code() != 200) { - ByteBuf buf = Unpooled.copiedBuffer(res.status().toString(), CharsetUtil.UTF_8); - res.content().writeBytes(buf); - buf.release(); + HttpResponseStatus responseStatus = res.status(); + if (responseStatus.code() != 200) { + ByteBufUtil.writeUtf8(res.content(), responseStatus.toString()); HttpUtil.setContentLength(res, res.content().readableBytes()); } - // Send the response and close the connection if necessary. - ChannelFuture f = ctx.channel().writeAndFlush(res); - if (!HttpUtil.isKeepAlive(req) || res.status().code() != 200) { - f.addListener(ChannelFutureListener.CLOSE); + boolean keepAlive = HttpUtil.isKeepAlive(req) && responseStatus.code() == 200; + HttpUtil.setKeepAlive(res, keepAlive); + ChannelFuture future = ctx.write(res); // Flushed in channelReadComplete() + if (!keepAlive) { + future.addListener(ChannelFutureListener.CLOSE); } } diff --git a/example/src/main/java/io/netty/example/http/websocketx/server/WebSocketFrameHandler.java b/example/src/main/java/io/netty/example/http/websocketx/server/WebSocketFrameHandler.java index a453668..5c0e9e0 100644 --- a/example/src/main/java/io/netty/example/http/websocketx/server/WebSocketFrameHandler.java +++ b/example/src/main/java/io/netty/example/http/websocketx/server/WebSocketFrameHandler.java @@ -15,13 +15,13 @@ */ package io.netty.example.http.websocketx.server; -import java.util.Locale; - import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; import io.netty.handler.codec.http.websocketx.WebSocketFrame; +import java.util.Locale; + /** * Echoes uppercase content of text frames. */ diff --git a/example/src/main/java/io/netty/example/http/websocketx/server/WebSocketIndexPageHandler.java b/example/src/main/java/io/netty/example/http/websocketx/server/WebSocketIndexPageHandler.java index 7d543ce..a020ecf 100644 --- a/example/src/main/java/io/netty/example/http/websocketx/server/WebSocketIndexPageHandler.java +++ b/example/src/main/java/io/netty/example/http/websocketx/server/WebSocketIndexPageHandler.java @@ -16,7 +16,7 @@ package io.netty.example.http.websocketx.server; import io.netty.buffer.ByteBuf; -import io.netty.buffer.Unpooled; +import io.netty.buffer.ByteBufUtil; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; @@ -27,16 +27,13 @@ import io.netty.handler.codec.http.FullHttpRequest; import io.netty.handler.codec.http.FullHttpResponse; import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.HttpUtil; import io.netty.handler.ssl.SslHandler; -import io.netty.util.CharsetUtil; -import static io.netty.handler.codec.http.HttpMethod.GET; -import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST; -import static io.netty.handler.codec.http.HttpResponseStatus.FORBIDDEN; -import static io.netty.handler.codec.http.HttpResponseStatus.NOT_FOUND; -import static io.netty.handler.codec.http.HttpResponseStatus.OK; -import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1; +import static io.netty.handler.codec.http.HttpHeaderNames.*; +import static io.netty.handler.codec.http.HttpMethod.*; +import static io.netty.handler.codec.http.HttpResponseStatus.*; /** * Outputs index page content. @@ -53,13 +50,15 @@ public class WebSocketIndexPageHandler extends SimpleChannelInboundHandler<FullH protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest req) throws Exception { // Handle a bad request. if (!req.decoderResult().isSuccess()) { - sendHttpResponse(ctx, req, new DefaultFullHttpResponse(HTTP_1_1, BAD_REQUEST)); + sendHttpResponse(ctx, req, new DefaultFullHttpResponse(req.protocolVersion(), BAD_REQUEST, + ctx.alloc().buffer(0))); return; } // Allow only GET methods. - if (req.method() != GET) { - sendHttpResponse(ctx, req, new DefaultFullHttpResponse(HTTP_1_1, FORBIDDEN)); + if (!GET.equals(req.method())) { + sendHttpResponse(ctx, req, new DefaultFullHttpResponse(req.protocolVersion(), FORBIDDEN, + ctx.alloc().buffer(0))); return; } @@ -67,14 +66,15 @@ public class WebSocketIndexPageHandler extends SimpleChannelInboundHandler<FullH if ("/".equals(req.uri()) || "/index.html".equals(req.uri())) { String webSocketLocation = getWebSocketLocation(ctx.pipeline(), req, websocketPath); ByteBuf content = WebSocketServerIndexPage.getContent(webSocketLocation); - FullHttpResponse res = new DefaultFullHttpResponse(HTTP_1_1, OK, content); + FullHttpResponse res = new DefaultFullHttpResponse(req.protocolVersion(), OK, content); - res.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=UTF-8"); + res.headers().set(CONTENT_TYPE, "text/html; charset=UTF-8"); HttpUtil.setContentLength(res, content.readableBytes()); sendHttpResponse(ctx, req, res); } else { - sendHttpResponse(ctx, req, new DefaultFullHttpResponse(HTTP_1_1, NOT_FOUND)); + sendHttpResponse(ctx, req, new DefaultFullHttpResponse(req.protocolVersion(), NOT_FOUND, + ctx.alloc().buffer(0))); } } @@ -86,17 +86,17 @@ public class WebSocketIndexPageHandler extends SimpleChannelInboundHandler<FullH private static void sendHttpResponse(ChannelHandlerContext ctx, FullHttpRequest req, FullHttpResponse res) { // Generate an error page if response getStatus code is not OK (200). - if (res.status().code() != 200) { - ByteBuf buf = Unpooled.copiedBuffer(res.status().toString(), CharsetUtil.UTF_8); - res.content().writeBytes(buf); - buf.release(); + HttpResponseStatus responseStatus = res.status(); + if (responseStatus.code() != 200) { + ByteBufUtil.writeUtf8(res.content(), responseStatus.toString()); HttpUtil.setContentLength(res, res.content().readableBytes()); } - // Send the response and close the connection if necessary. - ChannelFuture f = ctx.channel().writeAndFlush(res); - if (!HttpUtil.isKeepAlive(req) || res.status().code() != 200) { - f.addListener(ChannelFutureListener.CLOSE); + boolean keepAlive = HttpUtil.isKeepAlive(req) && responseStatus.code() == 200; + HttpUtil.setKeepAlive(res, keepAlive); + ChannelFuture future = ctx.writeAndFlush(res); + if (!keepAlive) { + future.addListener(ChannelFutureListener.CLOSE); } } diff --git a/example/src/main/java/io/netty/example/http/websocketx/server/WebSocketServer.java b/example/src/main/java/io/netty/example/http/websocketx/server/WebSocketServer.java index 8e67854..9315aa7 100644 --- a/example/src/main/java/io/netty/example/http/websocketx/server/WebSocketServer.java +++ b/example/src/main/java/io/netty/example/http/websocketx/server/WebSocketServer.java @@ -27,7 +27,7 @@ import io.netty.handler.ssl.SslContextBuilder; import io.netty.handler.ssl.util.SelfSignedCertificate; /** - * A HTTP server which serves Web Socket requests at: + * An HTTP server which serves Web Socket requests at: * * http://localhost:8080/websocket * diff --git a/example/src/main/java/io/netty/example/http2/helloworld/client/Http2Client.java b/example/src/main/java/io/netty/example/http2/helloworld/client/Http2Client.java index bc477d5..80470d6 100644 --- a/example/src/main/java/io/netty/example/http2/helloworld/client/Http2Client.java +++ b/example/src/main/java/io/netty/example/http2/helloworld/client/Http2Client.java @@ -15,6 +15,7 @@ package io.netty.example.http2.helloworld.client; import io.netty.bootstrap.Bootstrap; +import io.netty.buffer.Unpooled; import io.netty.channel.Channel; import io.netty.channel.ChannelOption; import io.netty.channel.EventLoopGroup; @@ -49,9 +50,13 @@ import static io.netty.handler.codec.http.HttpMethod.POST; import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1; /** - * An HTTP2 client that allows you to send HTTP2 frames to a server. Inbound and outbound frames are - * logged. When run from the command-line, sends a single HEADERS frame to the server and gets back + * An HTTP2 client that allows you to send HTTP2 frames to a server using HTTP1-style approaches + * (via {@link io.netty.handler.codec.http2.InboundHttp2ToHttpAdapter}). Inbound and outbound + * frames are logged. + * When run from the command-line, sends a single HEADERS frame to the server and gets back * a "Hello World" response. + * See the ./http2/helloworld/frame/client/ example for a HTTP2 client example which does not use + * HTTP1-style objects and patterns. */ public final class Http2Client { @@ -113,7 +118,7 @@ public final class Http2Client { System.err.println("Sending request(s)..."); if (URL != null) { // Create a simple GET request. - FullHttpRequest request = new DefaultFullHttpRequest(HTTP_1_1, GET, URL); + FullHttpRequest request = new DefaultFullHttpRequest(HTTP_1_1, GET, URL, Unpooled.EMPTY_BUFFER); request.headers().add(HttpHeaderNames.HOST, hostName); request.headers().add(HttpConversionUtil.ExtensionHeaderNames.SCHEME.text(), scheme.name()); request.headers().add(HttpHeaderNames.ACCEPT_ENCODING, HttpHeaderValues.GZIP); diff --git a/example/src/main/java/io/netty/example/http2/helloworld/client/Http2ClientInitializer.java b/example/src/main/java/io/netty/example/http2/helloworld/client/Http2ClientInitializer.java index 182c156..f4fa9d2 100644 --- a/example/src/main/java/io/netty/example/http2/helloworld/client/Http2ClientInitializer.java +++ b/example/src/main/java/io/netty/example/http2/helloworld/client/Http2ClientInitializer.java @@ -14,6 +14,7 @@ */ package io.netty.example.http2.helloworld.client; +import io.netty.buffer.Unpooled; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.channel.ChannelInitializer; @@ -22,6 +23,7 @@ import io.netty.channel.socket.SocketChannel; import io.netty.handler.codec.http.DefaultFullHttpRequest; import io.netty.handler.codec.http.HttpClientCodec; import io.netty.handler.codec.http.HttpClientUpgradeHandler; +import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpMethod; import io.netty.handler.codec.http.HttpVersion; import io.netty.handler.codec.http2.DefaultHttp2Connection; @@ -36,6 +38,8 @@ import io.netty.handler.ssl.ApplicationProtocolNames; import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler; import io.netty.handler.ssl.SslContext; +import java.net.InetSocketAddress; + import static io.netty.handler.logging.LogLevel.INFO; /** @@ -94,7 +98,8 @@ public class Http2ClientInitializer extends ChannelInitializer<SocketChannel> { */ private void configureSsl(SocketChannel ch) { ChannelPipeline pipeline = ch.pipeline(); - pipeline.addLast(sslCtx.newHandler(ch.alloc())); + // Specify Host in SSLContext New Handler to add TLS SNI Extension + pipeline.addLast(sslCtx.newHandler(ch.alloc(), Http2Client.HOST, Http2Client.PORT)); // We must wait for the handshake to finish and the protocol to be negotiated before configuring // the HTTP/2 components of the pipeline. pipeline.addLast(new ApplicationProtocolNegotiationHandler("") { @@ -130,10 +135,20 @@ public class Http2ClientInitializer extends ChannelInitializer<SocketChannel> { * A handler that triggers the cleartext upgrade to HTTP/2 by sending an initial HTTP request. */ private final class UpgradeRequestHandler extends ChannelInboundHandlerAdapter { + @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { DefaultFullHttpRequest upgradeRequest = - new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/"); + new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/", Unpooled.EMPTY_BUFFER); + + // Set HOST header as the remote peer may require it. + InetSocketAddress remote = (InetSocketAddress) ctx.channel().remoteAddress(); + String hostString = remote.getHostString(); + if (hostString == null) { + hostString = remote.getAddress().getHostAddress(); + } + upgradeRequest.headers().set(HttpHeaderNames.HOST, hostString + ':' + remote.getPort()); + ctx.writeAndFlush(upgradeRequest); ctx.fireChannelActive(); diff --git a/example/src/main/java/io/netty/example/http2/helloworld/frame/client/Http2ClientFrameInitializer.java b/example/src/main/java/io/netty/example/http2/helloworld/frame/client/Http2ClientFrameInitializer.java new file mode 100644 index 0000000..b08111f --- /dev/null +++ b/example/src/main/java/io/netty/example/http2/helloworld/frame/client/Http2ClientFrameInitializer.java @@ -0,0 +1,57 @@ +/* + * Copyright 2020 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.example.http2.helloworld.frame.client; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.handler.codec.http2.Http2FrameCodec; +import io.netty.handler.codec.http2.Http2FrameCodecBuilder; +import io.netty.handler.codec.http2.Http2MultiplexHandler; +import io.netty.handler.codec.http2.Http2Settings; +import io.netty.handler.ssl.SslContext; + +/** + * Configures client pipeline to support HTTP/2 frames via {@link Http2FrameCodec} and {@link Http2MultiplexHandler}. + */ +public final class Http2ClientFrameInitializer extends ChannelInitializer<Channel> { + + private final SslContext sslCtx; + + public Http2ClientFrameInitializer(SslContext sslCtx) { + this.sslCtx = sslCtx; + } + + @Override + protected void initChannel(Channel ch) throws Exception { + // ensure that our 'trust all' SSL handler is the first in the pipeline if SSL is enabled. + if (sslCtx != null) { + ch.pipeline().addFirst(sslCtx.newHandler(ch.alloc())); + } + + final Http2FrameCodec http2FrameCodec = Http2FrameCodecBuilder.forClient() + .initialSettings(Http2Settings.defaultSettings()) // this is the default, but shows it can be changed. + .build(); + ch.pipeline().addLast(http2FrameCodec); + ch.pipeline().addLast(new Http2MultiplexHandler(new SimpleChannelInboundHandler() { + @Override + protected void channelRead0(ChannelHandlerContext ctx, Object msg) { + // NOOP (this is the handler for 'inbound' streams, which is not relevant in this example) + } + })); + } + +} diff --git a/example/src/main/java/io/netty/example/http2/helloworld/frame/client/Http2ClientStreamFrameResponseHandler.java b/example/src/main/java/io/netty/example/http2/helloworld/frame/client/Http2ClientStreamFrameResponseHandler.java new file mode 100644 index 0000000..2475723 --- /dev/null +++ b/example/src/main/java/io/netty/example/http2/helloworld/frame/client/Http2ClientStreamFrameResponseHandler.java @@ -0,0 +1,61 @@ +/* + * Copyright 2020 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.example.http2.helloworld.frame.client; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.handler.codec.http2.Http2DataFrame; +import io.netty.handler.codec.http2.Http2HeadersFrame; +import io.netty.handler.codec.http2.Http2StreamFrame; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +/** + * Handles HTTP/2 stream frame responses. This is a useful approach if you specifically want to check + * the main HTTP/2 response DATA/HEADERs, but in this example it's used purely to see whether + * our request (for a specific stream id) has had a final response (for that same stream id). + */ +public final class Http2ClientStreamFrameResponseHandler extends SimpleChannelInboundHandler<Http2StreamFrame> { + + private final CountDownLatch latch = new CountDownLatch(1); + + @Override + protected void channelRead0(ChannelHandlerContext ctx, Http2StreamFrame msg) throws Exception { + System.out.println("Received HTTP/2 'stream' frame: " + msg); + + // isEndStream() is not from a common interface, so we currently must check both + if (msg instanceof Http2DataFrame && ((Http2DataFrame) msg).isEndStream()) { + latch.countDown(); + } else if (msg instanceof Http2HeadersFrame && ((Http2HeadersFrame) msg).isEndStream()) { + latch.countDown(); + } + } + + /** + * Waits for the latch to be decremented (i.e. for an end of stream message to be received), or for + * the latch to expire after 5 seconds. + * @return true if a successful HTTP/2 end of stream message was received. + */ + public boolean responseSuccessfullyCompleted() { + try { + return latch.await(5, TimeUnit.SECONDS); + } catch (InterruptedException ie) { + System.err.println("Latch exception: " + ie.getMessage()); + return false; + } + } + +} diff --git a/example/src/main/java/io/netty/example/http2/helloworld/frame/client/Http2FrameClient.java b/example/src/main/java/io/netty/example/http2/helloworld/frame/client/Http2FrameClient.java new file mode 100644 index 0000000..014d488 --- /dev/null +++ b/example/src/main/java/io/netty/example/http2/helloworld/frame/client/Http2FrameClient.java @@ -0,0 +1,124 @@ +/* + * Copyright 2020 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.example.http2.helloworld.frame.client; + +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelOption; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.codec.http2.DefaultHttp2Headers; +import io.netty.handler.codec.http2.DefaultHttp2HeadersFrame; +import io.netty.handler.codec.http2.Http2HeadersFrame; +import io.netty.handler.codec.http2.Http2SecurityUtil; +import io.netty.handler.codec.http2.Http2StreamChannel; +import io.netty.handler.codec.http2.Http2StreamChannelBootstrap; +import io.netty.handler.ssl.ApplicationProtocolConfig; +import io.netty.handler.ssl.ApplicationProtocolConfig.Protocol; +import io.netty.handler.ssl.ApplicationProtocolConfig.SelectedListenerFailureBehavior; +import io.netty.handler.ssl.ApplicationProtocolConfig.SelectorFailureBehavior; +import io.netty.handler.ssl.ApplicationProtocolNames; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslContextBuilder; +import io.netty.handler.ssl.SslProvider; +import io.netty.handler.ssl.SupportedCipherSuiteFilter; +import io.netty.handler.ssl.util.InsecureTrustManagerFactory; + +/** + * An HTTP2 client that allows you to send HTTP2 frames to a server using the newer HTTP2 + * approach (via {@link io.netty.handler.codec.http2.Http2FrameCodec}). + * When run from the command-line, sends a single HEADERS frame (with prior knowledge) to + * the server configured at host:port/path. + * You should include {@link io.netty.handler.codec.http2.Http2ClientUpgradeCodec} if the + * HTTP/2 server you are hitting doesn't support h2c/prior knowledge. + */ +public final class Http2FrameClient { + + static final boolean SSL = System.getProperty("ssl") != null; + static final String HOST = System.getProperty("host", "127.0.0.1"); + static final int PORT = Integer.parseInt(System.getProperty("port", SSL? "8443" : "8080")); + static final String PATH = System.getProperty("path", "/"); + + private Http2FrameClient() { + } + + public static void main(String[] args) throws Exception { + final EventLoopGroup clientWorkerGroup = new NioEventLoopGroup(); + + // Configure SSL. + final SslContext sslCtx; + if (SSL) { + final SslProvider provider = + SslProvider.isAlpnSupported(SslProvider.OPENSSL)? SslProvider.OPENSSL : SslProvider.JDK; + sslCtx = SslContextBuilder.forClient() + .sslProvider(provider) + .ciphers(Http2SecurityUtil.CIPHERS, SupportedCipherSuiteFilter.INSTANCE) + // you probably won't want to use this in production, but it is fine for this example: + .trustManager(InsecureTrustManagerFactory.INSTANCE) + .applicationProtocolConfig(new ApplicationProtocolConfig( + Protocol.ALPN, + SelectorFailureBehavior.NO_ADVERTISE, + SelectedListenerFailureBehavior.ACCEPT, + ApplicationProtocolNames.HTTP_2, + ApplicationProtocolNames.HTTP_1_1)) + .build(); + } else { + sslCtx = null; + } + + try { + final Bootstrap b = new Bootstrap(); + b.group(clientWorkerGroup); + b.channel(NioSocketChannel.class); + b.option(ChannelOption.SO_KEEPALIVE, true); + b.remoteAddress(HOST, PORT); + b.handler(new Http2ClientFrameInitializer(sslCtx)); + + // Start the client. + final Channel channel = b.connect().syncUninterruptibly().channel(); + System.out.println("Connected to [" + HOST + ':' + PORT + ']'); + + final Http2ClientStreamFrameResponseHandler streamFrameResponseHandler = + new Http2ClientStreamFrameResponseHandler(); + + final Http2StreamChannelBootstrap streamChannelBootstrap = new Http2StreamChannelBootstrap(channel); + final Http2StreamChannel streamChannel = streamChannelBootstrap.open().syncUninterruptibly().getNow(); + streamChannel.pipeline().addLast(streamFrameResponseHandler); + + // Send request (a HTTP/2 HEADERS frame - with ':method = GET' in this case) + final DefaultHttp2Headers headers = new DefaultHttp2Headers(); + headers.method("GET"); + headers.path(PATH); + headers.scheme(SSL? "https" : "http"); + final Http2HeadersFrame headersFrame = new DefaultHttp2HeadersFrame(headers); + streamChannel.writeAndFlush(headersFrame); + System.out.println("Sent HTTP/2 GET request to " + PATH); + + // Wait for the responses (or for the latch to expire), then clean up the connections + if (!streamFrameResponseHandler.responseSuccessfullyCompleted()) { + System.err.println("Did not get HTTP/2 response in expected time."); + } + + System.out.println("Finished HTTP/2 request, will close the connection."); + + // Wait until the connection is closed. + channel.close().syncUninterruptibly(); + } finally { + clientWorkerGroup.shutdownGracefully(); + } + } + +} diff --git a/example/src/main/java/io/netty/example/http2/helloworld/frame/server/Http2OrHttpHandler.java b/example/src/main/java/io/netty/example/http2/helloworld/frame/server/Http2OrHttpHandler.java index 01ae593..0ec1da9 100644 --- a/example/src/main/java/io/netty/example/http2/helloworld/frame/server/Http2OrHttpHandler.java +++ b/example/src/main/java/io/netty/example/http2/helloworld/frame/server/Http2OrHttpHandler.java @@ -18,7 +18,6 @@ import io.netty.channel.ChannelHandlerContext; import io.netty.example.http2.helloworld.server.HelloWorldHttp1Handler; import io.netty.handler.codec.http.HttpObjectAggregator; import io.netty.handler.codec.http.HttpServerCodec; -import io.netty.handler.codec.http2.Http2FrameCodec; import io.netty.handler.codec.http2.Http2FrameCodecBuilder; import io.netty.handler.ssl.ApplicationProtocolNames; import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler; diff --git a/example/src/main/java/io/netty/example/http2/helloworld/frame/server/Http2Server.java b/example/src/main/java/io/netty/example/http2/helloworld/frame/server/Http2Server.java index f363721..25ceb46 100644 --- a/example/src/main/java/io/netty/example/http2/helloworld/frame/server/Http2Server.java +++ b/example/src/main/java/io/netty/example/http2/helloworld/frame/server/Http2Server.java @@ -38,7 +38,7 @@ import io.netty.handler.ssl.SupportedCipherSuiteFilter; import io.netty.handler.ssl.util.SelfSignedCertificate; /** - * A HTTP/2 Server that responds to requests with a Hello World. Once started, you can test the + * An HTTP/2 Server that responds to requests with a Hello World. Once started, you can test the * server with the example client. * * <p>This example is making use of the "multiplexing" http2 API, where streams are mapped to child diff --git a/example/src/main/java/io/netty/example/http2/helloworld/frame/server/Http2ServerInitializer.java b/example/src/main/java/io/netty/example/http2/helloworld/frame/server/Http2ServerInitializer.java index b2bb856..09b8399 100644 --- a/example/src/main/java/io/netty/example/http2/helloworld/frame/server/Http2ServerInitializer.java +++ b/example/src/main/java/io/netty/example/http2/helloworld/frame/server/Http2ServerInitializer.java @@ -100,8 +100,7 @@ public class Http2ServerInitializer extends ChannelInitializer<SocketChannel> { // If this handler is hit then no upgrade has been attempted and the client is just talking HTTP. System.err.println("Directly talking: " + msg.protocolVersion() + " (no upgrade was attempted)"); ChannelPipeline pipeline = ctx.pipeline(); - ChannelHandlerContext thisCtx = pipeline.context(this); - pipeline.addAfter(thisCtx.name(), null, new HelloWorldHttp1Handler("Direct. No Upgrade Attempted.")); + pipeline.addAfter(ctx.name(), null, new HelloWorldHttp1Handler("Direct. No Upgrade Attempted.")); pipeline.replace(this, null, new HttpObjectAggregator(maxHttpContentLength)); ctx.fireChannelRead(ReferenceCountUtil.retain(msg)); } diff --git a/example/src/main/java/io/netty/example/http2/helloworld/multiplex/server/Http2OrHttpHandler.java b/example/src/main/java/io/netty/example/http2/helloworld/multiplex/server/Http2OrHttpHandler.java index 4637e79..4099b04 100644 --- a/example/src/main/java/io/netty/example/http2/helloworld/multiplex/server/Http2OrHttpHandler.java +++ b/example/src/main/java/io/netty/example/http2/helloworld/multiplex/server/Http2OrHttpHandler.java @@ -18,7 +18,8 @@ import io.netty.channel.ChannelHandlerContext; import io.netty.example.http2.helloworld.server.HelloWorldHttp1Handler; import io.netty.handler.codec.http.HttpObjectAggregator; import io.netty.handler.codec.http.HttpServerCodec; -import io.netty.handler.codec.http2.Http2MultiplexCodecBuilder; +import io.netty.handler.codec.http2.Http2FrameCodecBuilder; +import io.netty.handler.codec.http2.Http2MultiplexHandler; import io.netty.handler.ssl.ApplicationProtocolNames; import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler; @@ -37,7 +38,8 @@ public class Http2OrHttpHandler extends ApplicationProtocolNegotiationHandler { @Override protected void configurePipeline(ChannelHandlerContext ctx, String protocol) throws Exception { if (ApplicationProtocolNames.HTTP_2.equals(protocol)) { - ctx.pipeline().addLast(Http2MultiplexCodecBuilder.forServer(new HelloWorldHttp2Handler()).build()); + ctx.pipeline().addLast(Http2FrameCodecBuilder.forServer().build()); + ctx.pipeline().addLast(new Http2MultiplexHandler(new HelloWorldHttp2Handler())); return; } diff --git a/example/src/main/java/io/netty/example/http2/helloworld/multiplex/server/Http2Server.java b/example/src/main/java/io/netty/example/http2/helloworld/multiplex/server/Http2Server.java index 61b3f08..89a488b 100644 --- a/example/src/main/java/io/netty/example/http2/helloworld/multiplex/server/Http2Server.java +++ b/example/src/main/java/io/netty/example/http2/helloworld/multiplex/server/Http2Server.java @@ -38,7 +38,7 @@ import io.netty.handler.ssl.SupportedCipherSuiteFilter; import io.netty.handler.ssl.util.SelfSignedCertificate; /** - * A HTTP/2 Server that responds to requests with a Hello World. Once started, you can test the + * An HTTP/2 Server that responds to requests with a Hello World. Once started, you can test the * server with the example client. * * <p>This example is making use of the "multiplexing" http2 API, where streams are mapped to child diff --git a/example/src/main/java/io/netty/example/http2/helloworld/multiplex/server/Http2ServerInitializer.java b/example/src/main/java/io/netty/example/http2/helloworld/multiplex/server/Http2ServerInitializer.java index 891becc..95190f4 100644 --- a/example/src/main/java/io/netty/example/http2/helloworld/multiplex/server/Http2ServerInitializer.java +++ b/example/src/main/java/io/netty/example/http2/helloworld/multiplex/server/Http2ServerInitializer.java @@ -29,8 +29,9 @@ import io.netty.handler.codec.http.HttpServerCodec; import io.netty.handler.codec.http.HttpServerUpgradeHandler; import io.netty.handler.codec.http.HttpServerUpgradeHandler.UpgradeCodec; import io.netty.handler.codec.http.HttpServerUpgradeHandler.UpgradeCodecFactory; -import io.netty.handler.codec.http2.Http2MultiplexCodecBuilder; +import io.netty.handler.codec.http2.Http2FrameCodecBuilder; import io.netty.handler.codec.http2.Http2CodecUtil; +import io.netty.handler.codec.http2.Http2MultiplexHandler; import io.netty.handler.codec.http2.Http2ServerUpgradeCodec; import io.netty.handler.ssl.SslContext; import io.netty.util.AsciiString; @@ -47,7 +48,8 @@ public class Http2ServerInitializer extends ChannelInitializer<SocketChannel> { public UpgradeCodec newUpgradeCodec(CharSequence protocol) { if (AsciiString.contentEquals(Http2CodecUtil.HTTP_UPGRADE_PROTOCOL_NAME, protocol)) { return new Http2ServerUpgradeCodec( - Http2MultiplexCodecBuilder.forServer(new HelloWorldHttp2Handler()).build()); + Http2FrameCodecBuilder.forServer().build(), + new Http2MultiplexHandler(new HelloWorldHttp2Handler())); } else { return null; } @@ -100,8 +102,7 @@ public class Http2ServerInitializer extends ChannelInitializer<SocketChannel> { // If this handler is hit then no upgrade has been attempted and the client is just talking HTTP. System.err.println("Directly talking: " + msg.protocolVersion() + " (no upgrade was attempted)"); ChannelPipeline pipeline = ctx.pipeline(); - ChannelHandlerContext thisCtx = pipeline.context(this); - pipeline.addAfter(thisCtx.name(), null, new HelloWorldHttp1Handler("Direct. No Upgrade Attempted.")); + pipeline.addAfter(ctx.name(), null, new HelloWorldHttp1Handler("Direct. No Upgrade Attempted.")); pipeline.replace(this, null, new HttpObjectAggregator(maxHttpContentLength)); ctx.fireChannelRead(ReferenceCountUtil.retain(msg)); } diff --git a/example/src/main/java/io/netty/example/http2/helloworld/server/HelloWorldHttp1Handler.java b/example/src/main/java/io/netty/example/http2/helloworld/server/HelloWorldHttp1Handler.java index f5cfbf1..cafb57d 100644 --- a/example/src/main/java/io/netty/example/http2/helloworld/server/HelloWorldHttp1Handler.java +++ b/example/src/main/java/io/netty/example/http2/helloworld/server/HelloWorldHttp1Handler.java @@ -18,13 +18,17 @@ package io.netty.example.http2.helloworld.server; import static io.netty.handler.codec.http.HttpHeaderNames.CONNECTION; import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_LENGTH; import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE; +import static io.netty.handler.codec.http.HttpHeaderValues.CLOSE; +import static io.netty.handler.codec.http.HttpHeaderValues.KEEP_ALIVE; import static io.netty.handler.codec.http.HttpResponseStatus.CONTINUE; import static io.netty.handler.codec.http.HttpResponseStatus.OK; +import static io.netty.handler.codec.http.HttpVersion.HTTP_1_0; import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1; import static io.netty.util.internal.ObjectUtil.checkNotNull; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.Unpooled; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; @@ -32,7 +36,6 @@ import io.netty.handler.codec.http.DefaultFullHttpResponse; import io.netty.handler.codec.http.FullHttpRequest; import io.netty.handler.codec.http.FullHttpResponse; import io.netty.handler.codec.http.HttpUtil; -import io.netty.handler.codec.http.HttpHeaderValues; /** * HTTP handler that responds with a "Hello World" @@ -47,7 +50,7 @@ public class HelloWorldHttp1Handler extends SimpleChannelInboundHandler<FullHttp @Override public void channelRead0(ChannelHandlerContext ctx, FullHttpRequest req) throws Exception { if (HttpUtil.is100ContinueExpected(req)) { - ctx.write(new DefaultFullHttpResponse(HTTP_1_1, CONTINUE)); + ctx.write(new DefaultFullHttpResponse(HTTP_1_1, CONTINUE, Unpooled.EMPTY_BUFFER)); } boolean keepAlive = HttpUtil.isKeepAlive(req); @@ -59,11 +62,15 @@ public class HelloWorldHttp1Handler extends SimpleChannelInboundHandler<FullHttp response.headers().set(CONTENT_TYPE, "text/plain; charset=UTF-8"); response.headers().setInt(CONTENT_LENGTH, response.content().readableBytes()); - if (!keepAlive) { - ctx.write(response).addListener(ChannelFutureListener.CLOSE); - } else { - response.headers().set(CONNECTION, HttpHeaderValues.KEEP_ALIVE); + if (keepAlive) { + if (req.protocolVersion().equals(HTTP_1_0)) { + response.headers().set(CONNECTION, KEEP_ALIVE); + } ctx.write(response); + } else { + // Tell the client we're going to close the connection. + response.headers().set(CONNECTION, CLOSE); + ctx.write(response).addListener(ChannelFutureListener.CLOSE); } } diff --git a/example/src/main/java/io/netty/example/http2/helloworld/server/Http2Server.java b/example/src/main/java/io/netty/example/http2/helloworld/server/Http2Server.java index b99b375..1b84afa 100644 --- a/example/src/main/java/io/netty/example/http2/helloworld/server/Http2Server.java +++ b/example/src/main/java/io/netty/example/http2/helloworld/server/Http2Server.java @@ -38,7 +38,7 @@ import io.netty.handler.ssl.SupportedCipherSuiteFilter; import io.netty.handler.ssl.util.SelfSignedCertificate; /** - * A HTTP/2 Server that responds to requests with a Hello World. Once started, you can test the + * An HTTP/2 Server that responds to requests with a Hello World. Once started, you can test the * server with the example client. */ public final class Http2Server { diff --git a/example/src/main/java/io/netty/example/http2/helloworld/server/Http2ServerInitializer.java b/example/src/main/java/io/netty/example/http2/helloworld/server/Http2ServerInitializer.java index b5dff01..ca429c6 100644 --- a/example/src/main/java/io/netty/example/http2/helloworld/server/Http2ServerInitializer.java +++ b/example/src/main/java/io/netty/example/http2/helloworld/server/Http2ServerInitializer.java @@ -101,8 +101,7 @@ public class Http2ServerInitializer extends ChannelInitializer<SocketChannel> { // If this handler is hit then no upgrade has been attempted and the client is just talking HTTP. System.err.println("Directly talking: " + msg.protocolVersion() + " (no upgrade was attempted)"); ChannelPipeline pipeline = ctx.pipeline(); - ChannelHandlerContext thisCtx = pipeline.context(this); - pipeline.addAfter(thisCtx.name(), null, new HelloWorldHttp1Handler("Direct. No Upgrade Attempted.")); + pipeline.addAfter(ctx.name(), null, new HelloWorldHttp1Handler("Direct. No Upgrade Attempted.")); pipeline.replace(this, null, new HttpObjectAggregator(maxHttpContentLength)); ctx.fireChannelRead(ReferenceCountUtil.retain(msg)); } diff --git a/example/src/main/java/io/netty/example/http2/tiles/FallbackRequestHandler.java b/example/src/main/java/io/netty/example/http2/tiles/FallbackRequestHandler.java index e9836dc..ee3e732 100644 --- a/example/src/main/java/io/netty/example/http2/tiles/FallbackRequestHandler.java +++ b/example/src/main/java/io/netty/example/http2/tiles/FallbackRequestHandler.java @@ -16,6 +16,7 @@ package io.netty.example.http2.tiles; +import static io.netty.buffer.Unpooled.EMPTY_BUFFER; import static io.netty.buffer.Unpooled.copiedBuffer; import static io.netty.buffer.Unpooled.unreleasableBuffer; import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_LENGTH; @@ -47,7 +48,7 @@ public final class FallbackRequestHandler extends SimpleChannelInboundHandler<Ht @Override protected void channelRead0(ChannelHandlerContext ctx, HttpRequest req) throws Exception { if (HttpUtil.is100ContinueExpected(req)) { - ctx.write(new DefaultFullHttpResponse(HTTP_1_1, CONTINUE)); + ctx.write(new DefaultFullHttpResponse(HTTP_1_1, CONTINUE, EMPTY_BUFFER)); } ByteBuf content = ctx.alloc().buffer(); diff --git a/example/src/main/java/io/netty/example/http2/tiles/Http1RequestHandler.java b/example/src/main/java/io/netty/example/http2/tiles/Http1RequestHandler.java index 49590f8..ebc6bf8 100644 --- a/example/src/main/java/io/netty/example/http2/tiles/Http1RequestHandler.java +++ b/example/src/main/java/io/netty/example/http2/tiles/Http1RequestHandler.java @@ -17,16 +17,20 @@ package io.netty.example.http2.tiles; import static io.netty.handler.codec.http.HttpHeaderNames.CONNECTION; +import static io.netty.handler.codec.http.HttpHeaderValues.CLOSE; +import static io.netty.handler.codec.http.HttpHeaderValues.KEEP_ALIVE; import static io.netty.handler.codec.http.HttpUtil.isKeepAlive; import static io.netty.handler.codec.http.HttpResponseStatus.CONTINUE; +import static io.netty.handler.codec.http.HttpVersion.HTTP_1_0; import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1; + +import io.netty.buffer.Unpooled; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.DefaultFullHttpResponse; import io.netty.handler.codec.http.FullHttpRequest; import io.netty.handler.codec.http.FullHttpResponse; import io.netty.handler.codec.http.HttpUtil; -import io.netty.handler.codec.http.HttpHeaderValues; import java.util.concurrent.TimeUnit; @@ -38,7 +42,7 @@ public final class Http1RequestHandler extends Http2RequestHandler { @Override protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception { if (HttpUtil.is100ContinueExpected(request)) { - ctx.write(new DefaultFullHttpResponse(HTTP_1_1, CONTINUE)); + ctx.write(new DefaultFullHttpResponse(HTTP_1_1, CONTINUE, Unpooled.EMPTY_BUFFER)); } super.channelRead0(ctx, request); } @@ -51,9 +55,13 @@ public final class Http1RequestHandler extends Http2RequestHandler { @Override public void run() { if (isKeepAlive(request)) { - response.headers().set(CONNECTION, HttpHeaderValues.KEEP_ALIVE); + if (request.protocolVersion().equals(HTTP_1_0)) { + response.headers().set(CONNECTION, KEEP_ALIVE); + } ctx.writeAndFlush(response); } else { + // Tell the client we're going to close the connection. + response.headers().set(CONNECTION, CLOSE); ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); } } diff --git a/example/src/main/java/io/netty/example/http2/tiles/Http2Server.java b/example/src/main/java/io/netty/example/http2/tiles/Http2Server.java index 2a50c5a..1a95f36 100644 --- a/example/src/main/java/io/netty/example/http2/tiles/Http2Server.java +++ b/example/src/main/java/io/netty/example/http2/tiles/Http2Server.java @@ -40,7 +40,7 @@ import java.security.cert.CertificateException; import javax.net.ssl.SSLException; /** - * Demonstrates a Http2 server using Netty to display a bunch of images and + * Demonstrates an Http2 server using Netty to display a bunch of images and * simulate latency. It is a Netty version of the <a href="https://http2.golang.org/gophertiles?latency=0"> * Go lang HTTP2 tiles demo</a>. */ diff --git a/example/src/main/java/io/netty/example/http2/tiles/HttpServer.java b/example/src/main/java/io/netty/example/http2/tiles/HttpServer.java index f46d01a..125509f 100644 --- a/example/src/main/java/io/netty/example/http2/tiles/HttpServer.java +++ b/example/src/main/java/io/netty/example/http2/tiles/HttpServer.java @@ -31,7 +31,7 @@ import io.netty.handler.logging.LogLevel; import io.netty.handler.logging.LoggingHandler; /** - * Demonstrates a http server using Netty to display a bunch of images, simulate + * Demonstrates an http server using Netty to display a bunch of images, simulate * latency and compare it against the http2 implementation. */ public final class HttpServer { diff --git a/example/src/main/java/io/netty/example/mqtt/heartBeat/MqttHeartBeatBroker.java b/example/src/main/java/io/netty/example/mqtt/heartBeat/MqttHeartBeatBroker.java new file mode 100644 index 0000000..47a16ad --- /dev/null +++ b/example/src/main/java/io/netty/example/mqtt/heartBeat/MqttHeartBeatBroker.java @@ -0,0 +1,64 @@ +/* + * Copyright 2019 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.example.mqtt.heartBeat; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelOption; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.codec.mqtt.MqttDecoder; +import io.netty.handler.codec.mqtt.MqttEncoder; +import io.netty.handler.timeout.IdleStateHandler; + +import java.util.concurrent.TimeUnit; + +public final class MqttHeartBeatBroker { + + private MqttHeartBeatBroker() { + } + + public static void main(String[] args) throws Exception { + EventLoopGroup bossGroup = new NioEventLoopGroup(1); + EventLoopGroup workerGroup = new NioEventLoopGroup(); + + try { + ServerBootstrap b = new ServerBootstrap(); + b.group(bossGroup, workerGroup); + b.option(ChannelOption.SO_BACKLOG, 1024); + b.channel(NioServerSocketChannel.class); + b.childHandler(new ChannelInitializer<SocketChannel>() { + protected void initChannel(SocketChannel ch) throws Exception { + ch.pipeline().addLast("encoder", MqttEncoder.INSTANCE); + ch.pipeline().addLast("decoder", new MqttDecoder()); + ch.pipeline().addLast("heartBeatHandler", new IdleStateHandler(45, 0, 0, TimeUnit.SECONDS)); + ch.pipeline().addLast("handler", MqttHeartBeatBrokerHandler.INSTANCE); + } + }); + + ChannelFuture f = b.bind(1883).sync(); + System.out.println("Broker initiated..."); + + f.channel().closeFuture().sync(); + } finally { + workerGroup.shutdownGracefully(); + bossGroup.shutdownGracefully(); + } + } +} diff --git a/example/src/main/java/io/netty/example/mqtt/heartBeat/MqttHeartBeatBrokerHandler.java b/example/src/main/java/io/netty/example/mqtt/heartBeat/MqttHeartBeatBrokerHandler.java new file mode 100644 index 0000000..5505fc2 --- /dev/null +++ b/example/src/main/java/io/netty/example/mqtt/heartBeat/MqttHeartBeatBrokerHandler.java @@ -0,0 +1,82 @@ +/* + * Copyright 2019 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.example.mqtt.heartBeat; + +import io.netty.channel.ChannelHandler.Sharable; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.handler.codec.mqtt.MqttConnAckMessage; +import io.netty.handler.codec.mqtt.MqttConnAckVariableHeader; +import io.netty.handler.codec.mqtt.MqttConnectReturnCode; +import io.netty.handler.codec.mqtt.MqttFixedHeader; +import io.netty.handler.codec.mqtt.MqttMessage; +import io.netty.handler.codec.mqtt.MqttMessageType; +import io.netty.handler.codec.mqtt.MqttQoS; +import io.netty.handler.timeout.IdleState; +import io.netty.handler.timeout.IdleStateEvent; +import io.netty.util.ReferenceCountUtil; + +@Sharable +public final class MqttHeartBeatBrokerHandler extends ChannelInboundHandlerAdapter { + + public static final MqttHeartBeatBrokerHandler INSTANCE = new MqttHeartBeatBrokerHandler(); + + private MqttHeartBeatBrokerHandler() { + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + MqttMessage mqttMessage = (MqttMessage) msg; + System.out.println("Received MQTT message: " + mqttMessage); + switch (mqttMessage.fixedHeader().messageType()) { + case CONNECT: + MqttFixedHeader connackFixedHeader = + new MqttFixedHeader(MqttMessageType.CONNACK, false, MqttQoS.AT_MOST_ONCE, false, 0); + MqttConnAckVariableHeader mqttConnAckVariableHeader = + new MqttConnAckVariableHeader(MqttConnectReturnCode.CONNECTION_ACCEPTED, false); + MqttConnAckMessage connack = new MqttConnAckMessage(connackFixedHeader, mqttConnAckVariableHeader); + ctx.writeAndFlush(connack); + break; + case PINGREQ: + MqttFixedHeader pingreqFixedHeader = new MqttFixedHeader(MqttMessageType.PINGRESP, false, + MqttQoS.AT_MOST_ONCE, false, 0); + MqttMessage pingResp = new MqttMessage(pingreqFixedHeader); + ctx.writeAndFlush(pingResp); + break; + case DISCONNECT: + ctx.close(); + break; + default: + System.out.println("Unexpected message type: " + mqttMessage.fixedHeader().messageType()); + ReferenceCountUtil.release(msg); + ctx.close(); + } + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + System.out.println("Channel heartBeat lost"); + if (evt instanceof IdleStateEvent && IdleState.READER_IDLE == ((IdleStateEvent) evt).state()) { + ctx.close(); + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + cause.printStackTrace(); + ctx.close(); + } +} diff --git a/example/src/main/java/io/netty/example/mqtt/heartBeat/MqttHeartBeatClient.java b/example/src/main/java/io/netty/example/mqtt/heartBeat/MqttHeartBeatClient.java new file mode 100644 index 0000000..b9729c7 --- /dev/null +++ b/example/src/main/java/io/netty/example/mqtt/heartBeat/MqttHeartBeatClient.java @@ -0,0 +1,64 @@ +/* + * Copyright 2019 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.example.mqtt.heartBeat; + +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.codec.mqtt.MqttDecoder; +import io.netty.handler.codec.mqtt.MqttEncoder; +import io.netty.handler.timeout.IdleStateHandler; + +import java.util.concurrent.TimeUnit; + +public final class MqttHeartBeatClient { + private MqttHeartBeatClient() { + } + + private static final String HOST = System.getProperty("host", "127.0.0.1"); + private static final int PORT = Integer.parseInt(System.getProperty("port", "1883")); + private static final String CLIENT_ID = System.getProperty("clientId", "guestClient"); + private static final String USER_NAME = System.getProperty("userName", "guest"); + private static final String PASSWORD = System.getProperty("password", "guest"); + + public static void main(String[] args) throws Exception { + EventLoopGroup workerGroup = new NioEventLoopGroup(); + + try { + Bootstrap b = new Bootstrap(); + b.group(workerGroup); + b.channel(NioSocketChannel.class); + b.handler(new ChannelInitializer<SocketChannel>() { + protected void initChannel(SocketChannel ch) throws Exception { + ch.pipeline().addLast("encoder", MqttEncoder.INSTANCE); + ch.pipeline().addLast("decoder", new MqttDecoder()); + ch.pipeline().addLast("heartBeatHandler", new IdleStateHandler(0, 20, 0, TimeUnit.SECONDS)); + ch.pipeline().addLast("handler", new MqttHeartBeatClientHandler(CLIENT_ID, USER_NAME, PASSWORD)); + } + }); + + ChannelFuture f = b.connect(HOST, PORT).sync(); + System.out.println("Client connected"); + f.channel().closeFuture().sync(); + } finally { + workerGroup.shutdownGracefully(); + } + } +} diff --git a/example/src/main/java/io/netty/example/mqtt/heartBeat/MqttHeartBeatClientHandler.java b/example/src/main/java/io/netty/example/mqtt/heartBeat/MqttHeartBeatClientHandler.java new file mode 100644 index 0000000..d04ecf1 --- /dev/null +++ b/example/src/main/java/io/netty/example/mqtt/heartBeat/MqttHeartBeatClientHandler.java @@ -0,0 +1,83 @@ +/* + * Copyright 2019 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.example.mqtt.heartBeat; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.handler.codec.mqtt.MqttConnectMessage; +import io.netty.handler.codec.mqtt.MqttConnectPayload; +import io.netty.handler.codec.mqtt.MqttConnectVariableHeader; +import io.netty.handler.codec.mqtt.MqttFixedHeader; +import io.netty.handler.codec.mqtt.MqttMessage; +import io.netty.handler.codec.mqtt.MqttMessageType; +import io.netty.handler.codec.mqtt.MqttQoS; +import io.netty.handler.timeout.IdleStateEvent; +import io.netty.util.ReferenceCountUtil; + +public class MqttHeartBeatClientHandler extends ChannelInboundHandlerAdapter { + + private static final String PROTOCOL_NAME_MQTT_3_1_1 = "MQTT"; + private static final int PROTOCOL_VERSION_MQTT_3_1_1 = 4; + + private final String clientId; + private final String userName; + private final byte[] password; + + public MqttHeartBeatClientHandler(String clientId, String userName, String password) { + this.clientId = clientId; + this.userName = userName; + this.password = password.getBytes(); + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + // discard all messages + ReferenceCountUtil.release(msg); + } + + @Override + public void channelActive(ChannelHandlerContext ctx) throws Exception { + MqttFixedHeader connectFixedHeader = + new MqttFixedHeader(MqttMessageType.CONNECT, false, MqttQoS.AT_MOST_ONCE, false, 0); + MqttConnectVariableHeader connectVariableHeader = + new MqttConnectVariableHeader(PROTOCOL_NAME_MQTT_3_1_1, PROTOCOL_VERSION_MQTT_3_1_1, true, true, false, + 0, false, false, 20); + MqttConnectPayload connectPayload = new MqttConnectPayload(clientId, null, null, userName, password); + MqttConnectMessage connectMessage = + new MqttConnectMessage(connectFixedHeader, connectVariableHeader, connectPayload); + ctx.writeAndFlush(connectMessage); + System.out.println("Sent CONNECT"); + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + if (evt instanceof IdleStateEvent) { + MqttFixedHeader pingreqFixedHeader = + new MqttFixedHeader(MqttMessageType.PINGREQ, false, MqttQoS.AT_MOST_ONCE, false, 0); + MqttMessage pingreqMessage = new MqttMessage(pingreqFixedHeader); + ctx.writeAndFlush(pingreqMessage); + System.out.println("Sent PINGREQ"); + } else { + super.userEventTriggered(ctx, evt); + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + cause.printStackTrace(); + ctx.close(); + } +} diff --git a/example/src/main/java/io/netty/example/ocsp/OcspClientExample.java b/example/src/main/java/io/netty/example/ocsp/OcspClientExample.java index f36001c..3546646 100644 --- a/example/src/main/java/io/netty/example/ocsp/OcspClientExample.java +++ b/example/src/main/java/io/netty/example/ocsp/OcspClientExample.java @@ -21,6 +21,7 @@ import java.math.BigInteger; import javax.net.ssl.SSLSession; import javax.security.cert.X509Certificate; +import io.netty.buffer.Unpooled; import org.bouncycastle.asn1.ocsp.OCSPResponseStatus; import org.bouncycastle.cert.ocsp.BasicOCSPResp; import org.bouncycastle.cert.ocsp.CertificateStatus; @@ -57,8 +58,8 @@ import io.netty.util.ReferenceCountUtil; import io.netty.util.concurrent.Promise; /** - * This is a very simple example for a HTTPS client that uses OCSP stapling. - * The client connects to a HTTPS server that has OCSP stapling enabled and + * This is a very simple example for an HTTPS client that uses OCSP stapling. + * The client connects to an HTTPS server that has OCSP stapling enabled and * then uses BC to parse and validate it. */ public class OcspClientExample { @@ -157,14 +158,15 @@ public class OcspClientExample { private final Promise<FullHttpResponse> promise; - public HttpClientHandler(String host, Promise<FullHttpResponse> promise) { + HttpClientHandler(String host, Promise<FullHttpResponse> promise) { this.host = host; this.promise = promise; } @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { - FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/"); + FullHttpRequest request = new DefaultFullHttpRequest( + HttpVersion.HTTP_1_1, HttpMethod.GET, "/", Unpooled.EMPTY_BUFFER); request.headers().set(HttpHeaderNames.HOST, host); request.headers().set(HttpHeaderNames.USER_AGENT, "netty-ocsp-example/1.0"); @@ -203,7 +205,7 @@ public class OcspClientExample { private static class ExampleOcspClientHandler extends OcspClientHandler { - public ExampleOcspClientHandler(ReferenceCountedOpenSslEngine engine) { + ExampleOcspClientHandler(ReferenceCountedOpenSslEngine engine) { super(engine); } diff --git a/example/src/main/java/io/netty/example/ocsp/OcspServerExample.java b/example/src/main/java/io/netty/example/ocsp/OcspServerExample.java index 5ff2210..b6a17e6 100644 --- a/example/src/main/java/io/netty/example/ocsp/OcspServerExample.java +++ b/example/src/main/java/io/netty/example/ocsp/OcspServerExample.java @@ -54,7 +54,7 @@ import io.netty.util.CharsetUtil; /** * ATTENTION: This is an incomplete example! In order to provide a fully functional - * end-to-end example we'd need a X.509 certificate and the matching PrivateKey. + * end-to-end example we'd need an X.509 certificate and the matching PrivateKey. */ @SuppressWarnings("unused") public class OcspServerExample { @@ -71,7 +71,7 @@ public class OcspServerExample { X509Certificate issuer = keyCertChain[keyCertChain.length - 1]; // Step 2: We need the URL of the CA's OCSP responder server. It's somewhere encoded - // into the certificate! Notice that it's a HTTP URL. + // into the certificate! Notice that it's an HTTP URL. URI uri = OcspUtils.ocspUri(certificate); System.out.println("OCSP Responder URI: " + uri); diff --git a/example/src/main/java/io/netty/example/spdy/client/SpdyClient.java b/example/src/main/java/io/netty/example/spdy/client/SpdyClient.java index 22a2acd..9f4128c 100644 --- a/example/src/main/java/io/netty/example/spdy/client/SpdyClient.java +++ b/example/src/main/java/io/netty/example/spdy/client/SpdyClient.java @@ -16,6 +16,7 @@ package io.netty.example.spdy.client; import io.netty.bootstrap.Bootstrap; +import io.netty.buffer.Unpooled; import io.netty.channel.Channel; import io.netty.channel.ChannelOption; import io.netty.channel.EventLoopGroup; @@ -84,7 +85,8 @@ public final class SpdyClient { System.out.println("Connected to " + HOST + ':' + PORT); // Create a GET request. - HttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, ""); + HttpRequest request = new DefaultFullHttpRequest( + HttpVersion.HTTP_1_1, HttpMethod.GET, "", Unpooled.EMPTY_BUFFER); request.headers().set(HttpHeaderNames.HOST, HOST); request.headers().set(HttpHeaderNames.ACCEPT_ENCODING, HttpHeaderValues.GZIP); diff --git a/example/src/main/java/io/netty/example/spdy/client/SpdyFrameLogger.java b/example/src/main/java/io/netty/example/spdy/client/SpdyFrameLogger.java index ab136d7..ccec0bf 100644 --- a/example/src/main/java/io/netty/example/spdy/client/SpdyFrameLogger.java +++ b/example/src/main/java/io/netty/example/spdy/client/SpdyFrameLogger.java @@ -19,6 +19,7 @@ import io.netty.channel.ChannelDuplexHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelPromise; import io.netty.handler.codec.spdy.SpdyFrame; +import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.logging.InternalLogLevel; import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; @@ -36,12 +37,8 @@ public class SpdyFrameLogger extends ChannelDuplexHandler { private final InternalLogLevel level; public SpdyFrameLogger(InternalLogLevel level) { - if (level == null) { - throw new NullPointerException("level"); - } - - logger = InternalLoggerFactory.getInstance(getClass()); - this.level = level; + this.level = ObjectUtil.checkNotNull(level, "level"); + this.logger = InternalLoggerFactory.getInstance(getClass()); } @Override diff --git a/example/src/main/java/io/netty/example/spdy/server/SpdyServerHandler.java b/example/src/main/java/io/netty/example/spdy/server/SpdyServerHandler.java index 93db15d..80cfce2 100644 --- a/example/src/main/java/io/netty/example/spdy/server/SpdyServerHandler.java +++ b/example/src/main/java/io/netty/example/spdy/server/SpdyServerHandler.java @@ -22,16 +22,22 @@ import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.handler.codec.http.DefaultFullHttpResponse; import io.netty.handler.codec.http.FullHttpResponse; -import io.netty.handler.codec.http.HttpHeaderNames; -import io.netty.handler.codec.http.HttpHeaderValues; import io.netty.handler.codec.http.HttpRequest; import io.netty.util.CharsetUtil; import java.util.Date; -import static io.netty.handler.codec.http.HttpUtil.*; -import static io.netty.handler.codec.http.HttpResponseStatus.*; -import static io.netty.handler.codec.http.HttpVersion.*; +import static io.netty.handler.codec.http.HttpHeaderNames.CONNECTION; +import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_LENGTH; +import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE; +import static io.netty.handler.codec.http.HttpHeaderValues.CLOSE; +import static io.netty.handler.codec.http.HttpHeaderValues.KEEP_ALIVE; +import static io.netty.handler.codec.http.HttpResponseStatus.CONTINUE; +import static io.netty.handler.codec.http.HttpResponseStatus.OK; +import static io.netty.handler.codec.http.HttpVersion.HTTP_1_0; +import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1; +import static io.netty.handler.codec.http.HttpUtil.is100ContinueExpected; +import static io.netty.handler.codec.http.HttpUtil.isKeepAlive; /** * HTTP handler that responds with a "Hello World" @@ -44,21 +50,25 @@ public class SpdyServerHandler extends SimpleChannelInboundHandler<Object> { HttpRequest req = (HttpRequest) msg; if (is100ContinueExpected(req)) { - ctx.write(new DefaultFullHttpResponse(HTTP_1_1, CONTINUE)); + ctx.write(new DefaultFullHttpResponse(HTTP_1_1, CONTINUE, Unpooled.EMPTY_BUFFER)); } boolean keepAlive = isKeepAlive(req); ByteBuf content = Unpooled.copiedBuffer("Hello World " + new Date(), CharsetUtil.UTF_8); FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, OK, content); - response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=UTF-8"); - response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes()); + response.headers().set(CONTENT_TYPE, "text/plain; charset=UTF-8"); + response.headers().setInt(CONTENT_LENGTH, response.content().readableBytes()); - if (!keepAlive) { - ctx.write(response).addListener(ChannelFutureListener.CLOSE); - } else { - response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE); + if (keepAlive) { + if (req.protocolVersion().equals(HTTP_1_0)) { + response.headers().set(CONNECTION, KEEP_ALIVE); + } ctx.write(response); + } else { + // Tell the client we're going to close the connection. + response.headers().set(CONNECTION, CLOSE); + ctx.write(response).addListener(ChannelFutureListener.CLOSE); } } } diff --git a/example/src/main/java/io/netty/example/worldclock/WorldClockProtocol.java b/example/src/main/java/io/netty/example/worldclock/WorldClockProtocol.java index fb85e29..6bbd27a 100644 --- a/example/src/main/java/io/netty/example/worldclock/WorldClockProtocol.java +++ b/example/src/main/java/io/netty/example/worldclock/WorldClockProtocol.java @@ -122,6 +122,7 @@ public final class WorldClockProtocol { public static final int PACIFIC_VALUE = 10; + @Override public final int getNumber() { return value; } public static Continent valueOf(int value) { @@ -148,15 +149,18 @@ public final class WorldClockProtocol { private static com.google.protobuf.Internal.EnumLiteMap<Continent> internalValueMap = new com.google.protobuf.Internal.EnumLiteMap<Continent>() { + @Override public Continent findValueByNumber(int number) { return Continent.valueOf(number); } }; + @Override public final com.google.protobuf.Descriptors.EnumValueDescriptor getValueDescriptor() { return getDescriptor().getValues().get(index); } + @Override public final com.google.protobuf.Descriptors.EnumDescriptor getDescriptorForType() { return getDescriptor(); @@ -253,6 +257,7 @@ public final class WorldClockProtocol { public static final int SATURDAY_VALUE = 7; + @Override public final int getNumber() { return value; } public static DayOfWeek valueOf(int value) { @@ -275,15 +280,18 @@ public final class WorldClockProtocol { private static com.google.protobuf.Internal.EnumLiteMap<DayOfWeek> internalValueMap = new com.google.protobuf.Internal.EnumLiteMap<DayOfWeek>() { + @Override public DayOfWeek findValueByNumber(int number) { return DayOfWeek.valueOf(number); } }; + @Override public final com.google.protobuf.Descriptors.EnumValueDescriptor getValueDescriptor() { return getDescriptor().getValues().get(index); } + @Override public final com.google.protobuf.Descriptors.EnumDescriptor getDescriptorForType() { return getDescriptor(); @@ -361,6 +369,7 @@ public final class WorldClockProtocol { return defaultInstance; } + @Override public Location getDefaultInstanceForType() { return defaultInstance; } @@ -427,6 +436,7 @@ public final class WorldClockProtocol { return io.netty.example.worldclock.WorldClockProtocol.internal_static_io_netty_example_worldclock_Location_descriptor; } + @Override protected com.google.protobuf.GeneratedMessage.FieldAccessorTable internalGetFieldAccessorTable() { return io.netty.example.worldclock.WorldClockProtocol.internal_static_io_netty_example_worldclock_Location_fieldAccessorTable @@ -436,6 +446,7 @@ public final class WorldClockProtocol { public static com.google.protobuf.Parser<Location> PARSER = new com.google.protobuf.AbstractParser<Location>() { + @Override public Location parsePartialFrom( com.google.protobuf.CodedInputStream input, com.google.protobuf.ExtensionRegistryLite extensionRegistry) @@ -456,12 +467,14 @@ public final class WorldClockProtocol { /** * <code>required .io.netty.example.worldclock.Continent continent = 1;</code> */ + @Override public boolean hasContinent() { return ((bitField0_ & 0x00000001) == 0x00000001); } /** * <code>required .io.netty.example.worldclock.Continent continent = 1;</code> */ + @Override public io.netty.example.worldclock.WorldClockProtocol.Continent getContinent() { return continent_; } @@ -472,12 +485,14 @@ public final class WorldClockProtocol { /** * <code>required string city = 2;</code> */ + @Override public boolean hasCity() { return ((bitField0_ & 0x00000002) == 0x00000002); } /** * <code>required string city = 2;</code> */ + @Override public java.lang.String getCity() { java.lang.Object ref = city_; if (ref instanceof java.lang.String) { @@ -495,6 +510,7 @@ public final class WorldClockProtocol { /** * <code>required string city = 2;</code> */ + @Override public com.google.protobuf.ByteString getCityBytes() { java.lang.Object ref = city_; @@ -514,6 +530,7 @@ public final class WorldClockProtocol { city_ = ""; } private byte memoizedIsInitialized = -1; + @Override public final boolean isInitialized() { byte isInitialized = memoizedIsInitialized; if (isInitialized != -1) return isInitialized == 1; @@ -530,6 +547,7 @@ public final class WorldClockProtocol { return true; } + @Override public void writeTo(com.google.protobuf.CodedOutputStream output) throws java.io.IOException { getSerializedSize(); @@ -543,6 +561,7 @@ public final class WorldClockProtocol { } private int memoizedSerializedSize = -1; + @Override public int getSerializedSize() { int size = memoizedSerializedSize; if (size != -1) return size; @@ -622,10 +641,12 @@ public final class WorldClockProtocol { } public static Builder newBuilder() { return Builder.create(); } + @Override public Builder newBuilderForType() { return newBuilder(); } public static Builder newBuilder(io.netty.example.worldclock.WorldClockProtocol.Location prototype) { return newBuilder().mergeFrom(prototype); } + @Override public Builder toBuilder() { return newBuilder(this); } @java.lang.Override @@ -645,6 +666,7 @@ public final class WorldClockProtocol { return io.netty.example.worldclock.WorldClockProtocol.internal_static_io_netty_example_worldclock_Location_descriptor; } + @Override protected com.google.protobuf.GeneratedMessage.FieldAccessorTable internalGetFieldAccessorTable() { return io.netty.example.worldclock.WorldClockProtocol.internal_static_io_netty_example_worldclock_Location_fieldAccessorTable @@ -670,6 +692,7 @@ public final class WorldClockProtocol { return new Builder(); } + @Override public Builder clear() { super.clear(); continent_ = io.netty.example.worldclock.WorldClockProtocol.Continent.AFRICA; @@ -679,19 +702,23 @@ public final class WorldClockProtocol { return this; } + @Override public Builder clone() { return create().mergeFrom(buildPartial()); } + @Override public com.google.protobuf.Descriptors.Descriptor getDescriptorForType() { return io.netty.example.worldclock.WorldClockProtocol.internal_static_io_netty_example_worldclock_Location_descriptor; } + @Override public io.netty.example.worldclock.WorldClockProtocol.Location getDefaultInstanceForType() { return io.netty.example.worldclock.WorldClockProtocol.Location.getDefaultInstance(); } + @Override public io.netty.example.worldclock.WorldClockProtocol.Location build() { io.netty.example.worldclock.WorldClockProtocol.Location result = buildPartial(); if (!result.isInitialized()) { @@ -700,6 +727,7 @@ public final class WorldClockProtocol { return result; } + @Override public io.netty.example.worldclock.WorldClockProtocol.Location buildPartial() { io.netty.example.worldclock.WorldClockProtocol.Location result = new io.netty.example.worldclock.WorldClockProtocol.Location(this); int from_bitField0_ = bitField0_; @@ -717,6 +745,7 @@ public final class WorldClockProtocol { return result; } + @Override public Builder mergeFrom(com.google.protobuf.Message other) { if (other instanceof io.netty.example.worldclock.WorldClockProtocol.Location) { return mergeFrom((io.netty.example.worldclock.WorldClockProtocol.Location)other); @@ -740,6 +769,7 @@ public final class WorldClockProtocol { return this; } + @Override public final boolean isInitialized() { if (!hasContinent()) { @@ -752,6 +782,7 @@ public final class WorldClockProtocol { return true; } + @Override public Builder mergeFrom( com.google.protobuf.CodedInputStream input, com.google.protobuf.ExtensionRegistryLite extensionRegistry) @@ -776,12 +807,14 @@ public final class WorldClockProtocol { /** * <code>required .io.netty.example.worldclock.Continent continent = 1;</code> */ + @Override public boolean hasContinent() { return ((bitField0_ & 0x00000001) == 0x00000001); } /** * <code>required .io.netty.example.worldclock.Continent continent = 1;</code> */ + @Override public io.netty.example.worldclock.WorldClockProtocol.Continent getContinent() { return continent_; } @@ -812,12 +845,14 @@ public final class WorldClockProtocol { /** * <code>required string city = 2;</code> */ + @Override public boolean hasCity() { return ((bitField0_ & 0x00000002) == 0x00000002); } /** * <code>required string city = 2;</code> */ + @Override public java.lang.String getCity() { java.lang.Object ref = city_; if (!(ref instanceof java.lang.String)) { @@ -832,6 +867,7 @@ public final class WorldClockProtocol { /** * <code>required string city = 2;</code> */ + @Override public com.google.protobuf.ByteString getCityBytes() { java.lang.Object ref = city_; @@ -938,6 +974,7 @@ public final class WorldClockProtocol { return defaultInstance; } + @Override public Locations getDefaultInstanceForType() { return defaultInstance; } @@ -999,6 +1036,7 @@ public final class WorldClockProtocol { return io.netty.example.worldclock.WorldClockProtocol.internal_static_io_netty_example_worldclock_Locations_descriptor; } + @Override protected com.google.protobuf.GeneratedMessage.FieldAccessorTable internalGetFieldAccessorTable() { return io.netty.example.worldclock.WorldClockProtocol.internal_static_io_netty_example_worldclock_Locations_fieldAccessorTable @@ -1008,6 +1046,7 @@ public final class WorldClockProtocol { public static com.google.protobuf.Parser<Locations> PARSER = new com.google.protobuf.AbstractParser<Locations>() { + @Override public Locations parsePartialFrom( com.google.protobuf.CodedInputStream input, com.google.protobuf.ExtensionRegistryLite extensionRegistry) @@ -1027,31 +1066,36 @@ public final class WorldClockProtocol { /** * <code>repeated .io.netty.example.worldclock.Location location = 1;</code> */ + @Override public java.util.List<io.netty.example.worldclock.WorldClockProtocol.Location> getLocationList() { return location_; } /** * <code>repeated .io.netty.example.worldclock.Location location = 1;</code> */ - public java.util.List<? extends io.netty.example.worldclock.WorldClockProtocol.LocationOrBuilder> + @Override + public java.util.List<? extends io.netty.example.worldclock.WorldClockProtocol.LocationOrBuilder> getLocationOrBuilderList() { return location_; } /** * <code>repeated .io.netty.example.worldclock.Location location = 1;</code> */ + @Override public int getLocationCount() { return location_.size(); } /** * <code>repeated .io.netty.example.worldclock.Location location = 1;</code> */ + @Override public io.netty.example.worldclock.WorldClockProtocol.Location getLocation(int index) { return location_.get(index); } /** * <code>repeated .io.netty.example.worldclock.Location location = 1;</code> */ + @Override public io.netty.example.worldclock.WorldClockProtocol.LocationOrBuilder getLocationOrBuilder( int index) { return location_.get(index); @@ -1061,6 +1105,7 @@ public final class WorldClockProtocol { location_ = java.util.Collections.emptyList(); } private byte memoizedIsInitialized = -1; + @Override public final boolean isInitialized() { byte isInitialized = memoizedIsInitialized; if (isInitialized != -1) return isInitialized == 1; @@ -1075,6 +1120,7 @@ public final class WorldClockProtocol { return true; } + @Override public void writeTo(com.google.protobuf.CodedOutputStream output) throws java.io.IOException { getSerializedSize(); @@ -1085,6 +1131,7 @@ public final class WorldClockProtocol { } private int memoizedSerializedSize = -1; + @Override public int getSerializedSize() { int size = memoizedSerializedSize; if (size != -1) return size; @@ -1160,10 +1207,12 @@ public final class WorldClockProtocol { } public static Builder newBuilder() { return Builder.create(); } + @Override public Builder newBuilderForType() { return newBuilder(); } public static Builder newBuilder(io.netty.example.worldclock.WorldClockProtocol.Locations prototype) { return newBuilder().mergeFrom(prototype); } + @Override public Builder toBuilder() { return newBuilder(this); } @java.lang.Override @@ -1183,6 +1232,7 @@ public final class WorldClockProtocol { return io.netty.example.worldclock.WorldClockProtocol.internal_static_io_netty_example_worldclock_Locations_descriptor; } + @Override protected com.google.protobuf.GeneratedMessage.FieldAccessorTable internalGetFieldAccessorTable() { return io.netty.example.worldclock.WorldClockProtocol.internal_static_io_netty_example_worldclock_Locations_fieldAccessorTable @@ -1209,6 +1259,7 @@ public final class WorldClockProtocol { return new Builder(); } + @Override public Builder clear() { super.clear(); if (locationBuilder_ == null) { @@ -1220,19 +1271,23 @@ public final class WorldClockProtocol { return this; } + @Override public Builder clone() { return create().mergeFrom(buildPartial()); } + @Override public com.google.protobuf.Descriptors.Descriptor getDescriptorForType() { return io.netty.example.worldclock.WorldClockProtocol.internal_static_io_netty_example_worldclock_Locations_descriptor; } + @Override public io.netty.example.worldclock.WorldClockProtocol.Locations getDefaultInstanceForType() { return io.netty.example.worldclock.WorldClockProtocol.Locations.getDefaultInstance(); } + @Override public io.netty.example.worldclock.WorldClockProtocol.Locations build() { io.netty.example.worldclock.WorldClockProtocol.Locations result = buildPartial(); if (!result.isInitialized()) { @@ -1241,6 +1296,7 @@ public final class WorldClockProtocol { return result; } + @Override public io.netty.example.worldclock.WorldClockProtocol.Locations buildPartial() { io.netty.example.worldclock.WorldClockProtocol.Locations result = new io.netty.example.worldclock.WorldClockProtocol.Locations(this); int from_bitField0_ = bitField0_; @@ -1257,6 +1313,7 @@ public final class WorldClockProtocol { return result; } + @Override public Builder mergeFrom(com.google.protobuf.Message other) { if (other instanceof io.netty.example.worldclock.WorldClockProtocol.Locations) { return mergeFrom((io.netty.example.worldclock.WorldClockProtocol.Locations)other); @@ -1298,6 +1355,7 @@ public final class WorldClockProtocol { return this; } + @Override public final boolean isInitialized() { for (int i = 0; i < getLocationCount(); i++) { if (!getLocation(i).isInitialized()) { @@ -1308,6 +1366,7 @@ public final class WorldClockProtocol { return true; } + @Override public Builder mergeFrom( com.google.protobuf.CodedInputStream input, com.google.protobuf.ExtensionRegistryLite extensionRegistry) @@ -1343,6 +1402,7 @@ public final class WorldClockProtocol { /** * <code>repeated .io.netty.example.worldclock.Location location = 1;</code> */ + @Override public java.util.List<io.netty.example.worldclock.WorldClockProtocol.Location> getLocationList() { if (locationBuilder_ == null) { return java.util.Collections.unmodifiableList(location_); @@ -1353,6 +1413,7 @@ public final class WorldClockProtocol { /** * <code>repeated .io.netty.example.worldclock.Location location = 1;</code> */ + @Override public int getLocationCount() { if (locationBuilder_ == null) { return location_.size(); @@ -1363,6 +1424,7 @@ public final class WorldClockProtocol { /** * <code>repeated .io.netty.example.worldclock.Location location = 1;</code> */ + @Override public io.netty.example.worldclock.WorldClockProtocol.Location getLocation(int index) { if (locationBuilder_ == null) { return location_.get(index); @@ -1512,6 +1574,7 @@ public final class WorldClockProtocol { /** * <code>repeated .io.netty.example.worldclock.Location location = 1;</code> */ + @Override public io.netty.example.worldclock.WorldClockProtocol.LocationOrBuilder getLocationOrBuilder( int index) { if (locationBuilder_ == null) { @@ -1522,7 +1585,8 @@ public final class WorldClockProtocol { /** * <code>repeated .io.netty.example.worldclock.Location location = 1;</code> */ - public java.util.List<? extends io.netty.example.worldclock.WorldClockProtocol.LocationOrBuilder> + @Override + public java.util.List<? extends io.netty.example.worldclock.WorldClockProtocol.LocationOrBuilder> getLocationOrBuilderList() { if (locationBuilder_ != null) { return locationBuilder_.getMessageOrBuilderList(); @@ -1669,6 +1733,7 @@ public final class WorldClockProtocol { return defaultInstance; } + @Override public LocalTime getDefaultInstanceForType() { return defaultInstance; } @@ -1760,6 +1825,7 @@ public final class WorldClockProtocol { return io.netty.example.worldclock.WorldClockProtocol.internal_static_io_netty_example_worldclock_LocalTime_descriptor; } + @Override protected com.google.protobuf.GeneratedMessage.FieldAccessorTable internalGetFieldAccessorTable() { return io.netty.example.worldclock.WorldClockProtocol.internal_static_io_netty_example_worldclock_LocalTime_fieldAccessorTable @@ -1769,6 +1835,7 @@ public final class WorldClockProtocol { public static com.google.protobuf.Parser<LocalTime> PARSER = new com.google.protobuf.AbstractParser<LocalTime>() { + @Override public LocalTime parsePartialFrom( com.google.protobuf.CodedInputStream input, com.google.protobuf.ExtensionRegistryLite extensionRegistry) @@ -1789,12 +1856,14 @@ public final class WorldClockProtocol { /** * <code>required uint32 year = 1;</code> */ + @Override public boolean hasYear() { return ((bitField0_ & 0x00000001) == 0x00000001); } /** * <code>required uint32 year = 1;</code> */ + @Override public int getYear() { return year_; } @@ -1805,12 +1874,14 @@ public final class WorldClockProtocol { /** * <code>required uint32 month = 2;</code> */ + @Override public boolean hasMonth() { return ((bitField0_ & 0x00000002) == 0x00000002); } /** * <code>required uint32 month = 2;</code> */ + @Override public int getMonth() { return month_; } @@ -1821,12 +1892,14 @@ public final class WorldClockProtocol { /** * <code>required uint32 dayOfMonth = 4;</code> */ + @Override public boolean hasDayOfMonth() { return ((bitField0_ & 0x00000004) == 0x00000004); } /** * <code>required uint32 dayOfMonth = 4;</code> */ + @Override public int getDayOfMonth() { return dayOfMonth_; } @@ -1837,12 +1910,14 @@ public final class WorldClockProtocol { /** * <code>required .io.netty.example.worldclock.DayOfWeek dayOfWeek = 5;</code> */ + @Override public boolean hasDayOfWeek() { return ((bitField0_ & 0x00000008) == 0x00000008); } /** * <code>required .io.netty.example.worldclock.DayOfWeek dayOfWeek = 5;</code> */ + @Override public io.netty.example.worldclock.WorldClockProtocol.DayOfWeek getDayOfWeek() { return dayOfWeek_; } @@ -1853,12 +1928,14 @@ public final class WorldClockProtocol { /** * <code>required uint32 hour = 6;</code> */ + @Override public boolean hasHour() { return ((bitField0_ & 0x00000010) == 0x00000010); } /** * <code>required uint32 hour = 6;</code> */ + @Override public int getHour() { return hour_; } @@ -1869,12 +1946,14 @@ public final class WorldClockProtocol { /** * <code>required uint32 minute = 7;</code> */ + @Override public boolean hasMinute() { return ((bitField0_ & 0x00000020) == 0x00000020); } /** * <code>required uint32 minute = 7;</code> */ + @Override public int getMinute() { return minute_; } @@ -1885,12 +1964,14 @@ public final class WorldClockProtocol { /** * <code>required uint32 second = 8;</code> */ + @Override public boolean hasSecond() { return ((bitField0_ & 0x00000040) == 0x00000040); } /** * <code>required uint32 second = 8;</code> */ + @Override public int getSecond() { return second_; } @@ -1905,6 +1986,7 @@ public final class WorldClockProtocol { second_ = 0; } private byte memoizedIsInitialized = -1; + @Override public final boolean isInitialized() { byte isInitialized = memoizedIsInitialized; if (isInitialized != -1) return isInitialized == 1; @@ -1941,6 +2023,7 @@ public final class WorldClockProtocol { return true; } + @Override public void writeTo(com.google.protobuf.CodedOutputStream output) throws java.io.IOException { getSerializedSize(); @@ -1969,6 +2052,7 @@ public final class WorldClockProtocol { } private int memoizedSerializedSize = -1; + @Override public int getSerializedSize() { int size = memoizedSerializedSize; if (size != -1) return size; @@ -2068,10 +2152,12 @@ public final class WorldClockProtocol { } public static Builder newBuilder() { return Builder.create(); } + @Override public Builder newBuilderForType() { return newBuilder(); } public static Builder newBuilder(io.netty.example.worldclock.WorldClockProtocol.LocalTime prototype) { return newBuilder().mergeFrom(prototype); } + @Override public Builder toBuilder() { return newBuilder(this); } @java.lang.Override @@ -2091,6 +2177,7 @@ public final class WorldClockProtocol { return io.netty.example.worldclock.WorldClockProtocol.internal_static_io_netty_example_worldclock_LocalTime_descriptor; } + @Override protected com.google.protobuf.GeneratedMessage.FieldAccessorTable internalGetFieldAccessorTable() { return io.netty.example.worldclock.WorldClockProtocol.internal_static_io_netty_example_worldclock_LocalTime_fieldAccessorTable @@ -2116,6 +2203,7 @@ public final class WorldClockProtocol { return new Builder(); } + @Override public Builder clear() { super.clear(); year_ = 0; @@ -2135,19 +2223,23 @@ public final class WorldClockProtocol { return this; } + @Override public Builder clone() { return create().mergeFrom(buildPartial()); } + @Override public com.google.protobuf.Descriptors.Descriptor getDescriptorForType() { return io.netty.example.worldclock.WorldClockProtocol.internal_static_io_netty_example_worldclock_LocalTime_descriptor; } + @Override public io.netty.example.worldclock.WorldClockProtocol.LocalTime getDefaultInstanceForType() { return io.netty.example.worldclock.WorldClockProtocol.LocalTime.getDefaultInstance(); } + @Override public io.netty.example.worldclock.WorldClockProtocol.LocalTime build() { io.netty.example.worldclock.WorldClockProtocol.LocalTime result = buildPartial(); if (!result.isInitialized()) { @@ -2156,6 +2248,7 @@ public final class WorldClockProtocol { return result; } + @Override public io.netty.example.worldclock.WorldClockProtocol.LocalTime buildPartial() { io.netty.example.worldclock.WorldClockProtocol.LocalTime result = new io.netty.example.worldclock.WorldClockProtocol.LocalTime(this); int from_bitField0_ = bitField0_; @@ -2193,6 +2286,7 @@ public final class WorldClockProtocol { return result; } + @Override public Builder mergeFrom(com.google.protobuf.Message other) { if (other instanceof io.netty.example.worldclock.WorldClockProtocol.LocalTime) { return mergeFrom((io.netty.example.worldclock.WorldClockProtocol.LocalTime)other); @@ -2229,6 +2323,7 @@ public final class WorldClockProtocol { return this; } + @Override public final boolean isInitialized() { if (!hasYear()) { @@ -2261,6 +2356,7 @@ public final class WorldClockProtocol { return true; } + @Override public Builder mergeFrom( com.google.protobuf.CodedInputStream input, com.google.protobuf.ExtensionRegistryLite extensionRegistry) @@ -2285,12 +2381,14 @@ public final class WorldClockProtocol { /** * <code>required uint32 year = 1;</code> */ + @Override public boolean hasYear() { return ((bitField0_ & 0x00000001) == 0x00000001); } /** * <code>required uint32 year = 1;</code> */ + @Override public int getYear() { return year_; } @@ -2318,12 +2416,14 @@ public final class WorldClockProtocol { /** * <code>required uint32 month = 2;</code> */ + @Override public boolean hasMonth() { return ((bitField0_ & 0x00000002) == 0x00000002); } /** * <code>required uint32 month = 2;</code> */ + @Override public int getMonth() { return month_; } @@ -2351,12 +2451,14 @@ public final class WorldClockProtocol { /** * <code>required uint32 dayOfMonth = 4;</code> */ + @Override public boolean hasDayOfMonth() { return ((bitField0_ & 0x00000004) == 0x00000004); } /** * <code>required uint32 dayOfMonth = 4;</code> */ + @Override public int getDayOfMonth() { return dayOfMonth_; } @@ -2384,12 +2486,14 @@ public final class WorldClockProtocol { /** * <code>required .io.netty.example.worldclock.DayOfWeek dayOfWeek = 5;</code> */ + @Override public boolean hasDayOfWeek() { return ((bitField0_ & 0x00000008) == 0x00000008); } /** * <code>required .io.netty.example.worldclock.DayOfWeek dayOfWeek = 5;</code> */ + @Override public io.netty.example.worldclock.WorldClockProtocol.DayOfWeek getDayOfWeek() { return dayOfWeek_; } @@ -2420,12 +2524,14 @@ public final class WorldClockProtocol { /** * <code>required uint32 hour = 6;</code> */ + @Override public boolean hasHour() { return ((bitField0_ & 0x00000010) == 0x00000010); } /** * <code>required uint32 hour = 6;</code> */ + @Override public int getHour() { return hour_; } @@ -2453,12 +2559,14 @@ public final class WorldClockProtocol { /** * <code>required uint32 minute = 7;</code> */ + @Override public boolean hasMinute() { return ((bitField0_ & 0x00000020) == 0x00000020); } /** * <code>required uint32 minute = 7;</code> */ + @Override public int getMinute() { return minute_; } @@ -2486,12 +2594,14 @@ public final class WorldClockProtocol { /** * <code>required uint32 second = 8;</code> */ + @Override public boolean hasSecond() { return ((bitField0_ & 0x00000040) == 0x00000040); } /** * <code>required uint32 second = 8;</code> */ + @Override public int getSecond() { return second_; } @@ -2571,6 +2681,7 @@ public final class WorldClockProtocol { return defaultInstance; } + @Override public LocalTimes getDefaultInstanceForType() { return defaultInstance; } @@ -2632,6 +2743,7 @@ public final class WorldClockProtocol { return io.netty.example.worldclock.WorldClockProtocol.internal_static_io_netty_example_worldclock_LocalTimes_descriptor; } + @Override protected com.google.protobuf.GeneratedMessage.FieldAccessorTable internalGetFieldAccessorTable() { return io.netty.example.worldclock.WorldClockProtocol.internal_static_io_netty_example_worldclock_LocalTimes_fieldAccessorTable @@ -2641,6 +2753,7 @@ public final class WorldClockProtocol { public static com.google.protobuf.Parser<LocalTimes> PARSER = new com.google.protobuf.AbstractParser<LocalTimes>() { + @Override public LocalTimes parsePartialFrom( com.google.protobuf.CodedInputStream input, com.google.protobuf.ExtensionRegistryLite extensionRegistry) @@ -2660,31 +2773,36 @@ public final class WorldClockProtocol { /** * <code>repeated .io.netty.example.worldclock.LocalTime localTime = 1;</code> */ + @Override public java.util.List<io.netty.example.worldclock.WorldClockProtocol.LocalTime> getLocalTimeList() { return localTime_; } /** * <code>repeated .io.netty.example.worldclock.LocalTime localTime = 1;</code> */ - public java.util.List<? extends io.netty.example.worldclock.WorldClockProtocol.LocalTimeOrBuilder> + @Override + public java.util.List<? extends io.netty.example.worldclock.WorldClockProtocol.LocalTimeOrBuilder> getLocalTimeOrBuilderList() { return localTime_; } /** * <code>repeated .io.netty.example.worldclock.LocalTime localTime = 1;</code> */ + @Override public int getLocalTimeCount() { return localTime_.size(); } /** * <code>repeated .io.netty.example.worldclock.LocalTime localTime = 1;</code> */ + @Override public io.netty.example.worldclock.WorldClockProtocol.LocalTime getLocalTime(int index) { return localTime_.get(index); } /** * <code>repeated .io.netty.example.worldclock.LocalTime localTime = 1;</code> */ + @Override public io.netty.example.worldclock.WorldClockProtocol.LocalTimeOrBuilder getLocalTimeOrBuilder( int index) { return localTime_.get(index); @@ -2694,6 +2812,7 @@ public final class WorldClockProtocol { localTime_ = java.util.Collections.emptyList(); } private byte memoizedIsInitialized = -1; + @Override public final boolean isInitialized() { byte isInitialized = memoizedIsInitialized; if (isInitialized != -1) return isInitialized == 1; @@ -2708,6 +2827,7 @@ public final class WorldClockProtocol { return true; } + @Override public void writeTo(com.google.protobuf.CodedOutputStream output) throws java.io.IOException { getSerializedSize(); @@ -2718,6 +2838,7 @@ public final class WorldClockProtocol { } private int memoizedSerializedSize = -1; + @Override public int getSerializedSize() { int size = memoizedSerializedSize; if (size != -1) return size; @@ -2793,10 +2914,12 @@ public final class WorldClockProtocol { } public static Builder newBuilder() { return Builder.create(); } + @Override public Builder newBuilderForType() { return newBuilder(); } public static Builder newBuilder(io.netty.example.worldclock.WorldClockProtocol.LocalTimes prototype) { return newBuilder().mergeFrom(prototype); } + @Override public Builder toBuilder() { return newBuilder(this); } @java.lang.Override @@ -2816,6 +2939,7 @@ public final class WorldClockProtocol { return io.netty.example.worldclock.WorldClockProtocol.internal_static_io_netty_example_worldclock_LocalTimes_descriptor; } + @Override protected com.google.protobuf.GeneratedMessage.FieldAccessorTable internalGetFieldAccessorTable() { return io.netty.example.worldclock.WorldClockProtocol.internal_static_io_netty_example_worldclock_LocalTimes_fieldAccessorTable @@ -2842,6 +2966,7 @@ public final class WorldClockProtocol { return new Builder(); } + @Override public Builder clear() { super.clear(); if (localTimeBuilder_ == null) { @@ -2853,19 +2978,23 @@ public final class WorldClockProtocol { return this; } + @Override public Builder clone() { return create().mergeFrom(buildPartial()); } + @Override public com.google.protobuf.Descriptors.Descriptor getDescriptorForType() { return io.netty.example.worldclock.WorldClockProtocol.internal_static_io_netty_example_worldclock_LocalTimes_descriptor; } + @Override public io.netty.example.worldclock.WorldClockProtocol.LocalTimes getDefaultInstanceForType() { return io.netty.example.worldclock.WorldClockProtocol.LocalTimes.getDefaultInstance(); } + @Override public io.netty.example.worldclock.WorldClockProtocol.LocalTimes build() { io.netty.example.worldclock.WorldClockProtocol.LocalTimes result = buildPartial(); if (!result.isInitialized()) { @@ -2874,6 +3003,7 @@ public final class WorldClockProtocol { return result; } + @Override public io.netty.example.worldclock.WorldClockProtocol.LocalTimes buildPartial() { io.netty.example.worldclock.WorldClockProtocol.LocalTimes result = new io.netty.example.worldclock.WorldClockProtocol.LocalTimes(this); int from_bitField0_ = bitField0_; @@ -2890,6 +3020,7 @@ public final class WorldClockProtocol { return result; } + @Override public Builder mergeFrom(com.google.protobuf.Message other) { if (other instanceof io.netty.example.worldclock.WorldClockProtocol.LocalTimes) { return mergeFrom((io.netty.example.worldclock.WorldClockProtocol.LocalTimes)other); @@ -2931,6 +3062,7 @@ public final class WorldClockProtocol { return this; } + @Override public final boolean isInitialized() { for (int i = 0; i < getLocalTimeCount(); i++) { if (!getLocalTime(i).isInitialized()) { @@ -2941,6 +3073,7 @@ public final class WorldClockProtocol { return true; } + @Override public Builder mergeFrom( com.google.protobuf.CodedInputStream input, com.google.protobuf.ExtensionRegistryLite extensionRegistry) @@ -2976,6 +3109,7 @@ public final class WorldClockProtocol { /** * <code>repeated .io.netty.example.worldclock.LocalTime localTime = 1;</code> */ + @Override public java.util.List<io.netty.example.worldclock.WorldClockProtocol.LocalTime> getLocalTimeList() { if (localTimeBuilder_ == null) { return java.util.Collections.unmodifiableList(localTime_); @@ -2986,6 +3120,7 @@ public final class WorldClockProtocol { /** * <code>repeated .io.netty.example.worldclock.LocalTime localTime = 1;</code> */ + @Override public int getLocalTimeCount() { if (localTimeBuilder_ == null) { return localTime_.size(); @@ -2996,6 +3131,7 @@ public final class WorldClockProtocol { /** * <code>repeated .io.netty.example.worldclock.LocalTime localTime = 1;</code> */ + @Override public io.netty.example.worldclock.WorldClockProtocol.LocalTime getLocalTime(int index) { if (localTimeBuilder_ == null) { return localTime_.get(index); @@ -3145,6 +3281,7 @@ public final class WorldClockProtocol { /** * <code>repeated .io.netty.example.worldclock.LocalTime localTime = 1;</code> */ + @Override public io.netty.example.worldclock.WorldClockProtocol.LocalTimeOrBuilder getLocalTimeOrBuilder( int index) { if (localTimeBuilder_ == null) { @@ -3155,7 +3292,8 @@ public final class WorldClockProtocol { /** * <code>repeated .io.netty.example.worldclock.LocalTime localTime = 1;</code> */ - public java.util.List<? extends io.netty.example.worldclock.WorldClockProtocol.LocalTimeOrBuilder> + @Override + public java.util.List<? extends io.netty.example.worldclock.WorldClockProtocol.LocalTimeOrBuilder> getLocalTimeOrBuilderList() { if (localTimeBuilder_ != null) { return localTimeBuilder_.getMessageOrBuilderList(); @@ -3262,6 +3400,7 @@ public final class WorldClockProtocol { }; com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner assigner = new com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner() { + @Override public com.google.protobuf.ExtensionRegistry assignDescriptors( com.google.protobuf.Descriptors.FileDescriptor root) { descriptor = root; diff --git a/handler-proxy/pom.xml b/handler-proxy/pom.xml index 0dcad25..452ece8 100644 --- a/handler-proxy/pom.xml +++ b/handler-proxy/pom.xml @@ -20,7 +20,7 @@ <parent> <groupId>io.netty</groupId> <artifactId>netty-parent</artifactId> - <version>4.1.33.Final</version> + <version>4.1.48.Final</version> </parent> <artifactId>netty-handler-proxy</artifactId> diff --git a/handler-proxy/src/main/java/io/netty/handler/proxy/HttpProxyHandler.java b/handler-proxy/src/main/java/io/netty/handler/proxy/HttpProxyHandler.java index 2c41fab..9751b05 100644 --- a/handler-proxy/src/main/java/io/netty/handler/proxy/HttpProxyHandler.java +++ b/handler-proxy/src/main/java/io/netty/handler/proxy/HttpProxyHandler.java @@ -19,7 +19,10 @@ package io.netty.handler.proxy; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; 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.handler.codec.base64.Base64; import io.netty.handler.codec.http.DefaultFullHttpRequest; import io.netty.handler.codec.http.FullHttpRequest; @@ -34,6 +37,7 @@ import io.netty.handler.codec.http.HttpVersion; import io.netty.handler.codec.http.LastHttpContent; import io.netty.util.AsciiString; import io.netty.util.CharsetUtil; +import io.netty.util.internal.ObjectUtil; import java.net.InetSocketAddress; import java.net.SocketAddress; @@ -43,13 +47,20 @@ public final class HttpProxyHandler extends ProxyHandler { private static final String PROTOCOL = "http"; private static final String AUTH_BASIC = "basic"; - private final HttpClientCodec codec = new HttpClientCodec(); + // Wrapper for the HttpClientCodec to prevent it to be removed by other handlers by mistake (for example the + // WebSocket*Handshaker. + // + // See: + // - https://github.com/netty/netty/issues/5201 + // - https://github.com/netty/netty/issues/5070 + private final HttpClientCodecWrapper codecWrapper = new HttpClientCodecWrapper(); private final String username; private final String password; private final CharSequence authorization; + private final HttpHeaders outboundHeaders; private final boolean ignoreDefaultPortsInConnectHostHeader; private HttpResponseStatus status; - private HttpHeaders headers; + private HttpHeaders inboundHeaders; public HttpProxyHandler(SocketAddress proxyAddress) { this(proxyAddress, null); @@ -66,7 +77,7 @@ public final class HttpProxyHandler extends ProxyHandler { username = null; password = null; authorization = null; - this.headers = headers; + this.outboundHeaders = headers; this.ignoreDefaultPortsInConnectHostHeader = ignoreDefaultPortsInConnectHostHeader; } @@ -85,14 +96,8 @@ public final class HttpProxyHandler extends ProxyHandler { HttpHeaders headers, boolean ignoreDefaultPortsInConnectHostHeader) { super(proxyAddress); - if (username == null) { - throw new NullPointerException("username"); - } - if (password == null) { - throw new NullPointerException("password"); - } - this.username = username; - this.password = password; + this.username = ObjectUtil.checkNotNull(username, "username"); + this.password = ObjectUtil.checkNotNull(password, "password"); ByteBuf authz = Unpooled.copiedBuffer(username + ':' + password, CharsetUtil.UTF_8); ByteBuf authzBase64 = Base64.encode(authz, false); @@ -102,7 +107,7 @@ public final class HttpProxyHandler extends ProxyHandler { authz.release(); authzBase64.release(); - this.headers = headers; + this.outboundHeaders = headers; this.ignoreDefaultPortsInConnectHostHeader = ignoreDefaultPortsInConnectHostHeader; } @@ -128,17 +133,17 @@ public final class HttpProxyHandler extends ProxyHandler { protected void addCodec(ChannelHandlerContext ctx) throws Exception { ChannelPipeline p = ctx.pipeline(); String name = ctx.name(); - p.addBefore(name, null, codec); + p.addBefore(name, null, codecWrapper); } @Override protected void removeEncoder(ChannelHandlerContext ctx) throws Exception { - codec.removeOutboundHandler(); + codecWrapper.codec.removeOutboundHandler(); } @Override protected void removeDecoder(ChannelHandlerContext ctx) throws Exception { - codec.removeInboundHandler(); + codecWrapper.codec.removeInboundHandler(); } @Override @@ -163,8 +168,8 @@ public final class HttpProxyHandler extends ProxyHandler { req.headers().set(HttpHeaderNames.PROXY_AUTHORIZATION, authorization); } - if (headers != null) { - req.headers().add(headers); + if (outboundHeaders != null) { + req.headers().add(outboundHeaders); } return req; @@ -174,21 +179,149 @@ public final class HttpProxyHandler extends ProxyHandler { protected boolean handleResponse(ChannelHandlerContext ctx, Object response) throws Exception { if (response instanceof HttpResponse) { if (status != null) { - throw new ProxyConnectException(exceptionMessage("too many responses")); + throw new HttpProxyConnectException(exceptionMessage("too many responses"), /*headers=*/ null); } - status = ((HttpResponse) response).status(); + HttpResponse res = (HttpResponse) response; + status = res.status(); + inboundHeaders = res.headers(); } boolean finished = response instanceof LastHttpContent; if (finished) { if (status == null) { - throw new ProxyConnectException(exceptionMessage("missing response")); + throw new HttpProxyConnectException(exceptionMessage("missing response"), inboundHeaders); } if (status.code() != 200) { - throw new ProxyConnectException(exceptionMessage("status: " + status)); + throw new HttpProxyConnectException(exceptionMessage("status: " + status), inboundHeaders); } } return finished; } + + /** + * Specific case of a connection failure, which may include headers from the proxy. + */ + public static final class HttpProxyConnectException extends ProxyConnectException { + private static final long serialVersionUID = -8824334609292146066L; + + private final HttpHeaders headers; + + /** + * @param message The failure message. + * @param headers Header associated with the connection failure. May be {@code null}. + */ + public HttpProxyConnectException(String message, HttpHeaders headers) { + super(message); + this.headers = headers; + } + + /** + * Returns headers, if any. May be {@code null}. + */ + public HttpHeaders headers() { + return headers; + } + } + + private static final class HttpClientCodecWrapper implements ChannelInboundHandler, ChannelOutboundHandler { + final HttpClientCodec codec = new HttpClientCodec(); + + @Override + public void handlerAdded(ChannelHandlerContext ctx) throws Exception { + codec.handlerAdded(ctx); + } + + @Override + public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { + codec.handlerRemoved(ctx); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + codec.exceptionCaught(ctx, cause); + } + + @Override + public void channelRegistered(ChannelHandlerContext ctx) throws Exception { + codec.channelRegistered(ctx); + } + + @Override + public void channelUnregistered(ChannelHandlerContext ctx) throws Exception { + codec.channelUnregistered(ctx); + } + + @Override + public void channelActive(ChannelHandlerContext ctx) throws Exception { + codec.channelActive(ctx); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) throws Exception { + codec.channelInactive(ctx); + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + codec.channelRead(ctx, msg); + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { + codec.channelReadComplete(ctx); + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + codec.userEventTriggered(ctx, evt); + } + + @Override + public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception { + codec.channelWritabilityChanged(ctx); + } + + @Override + public void bind(ChannelHandlerContext ctx, SocketAddress localAddress, + ChannelPromise promise) throws Exception { + codec.bind(ctx, localAddress, promise); + } + + @Override + public void connect(ChannelHandlerContext ctx, SocketAddress remoteAddress, SocketAddress localAddress, + ChannelPromise promise) throws Exception { + codec.connect(ctx, remoteAddress, localAddress, promise); + } + + @Override + public void disconnect(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception { + codec.disconnect(ctx, promise); + } + + @Override + public void close(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception { + codec.close(ctx, promise); + } + + @Override + public void deregister(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception { + codec.deregister(ctx, promise); + } + + @Override + public void read(ChannelHandlerContext ctx) throws Exception { + codec.read(ctx); + } + + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { + codec.write(ctx, msg, promise); + } + + @Override + public void flush(ChannelHandlerContext ctx) throws Exception { + codec.flush(ctx); + } + } } diff --git a/handler-proxy/src/main/java/io/netty/handler/proxy/ProxyConnectionEvent.java b/handler-proxy/src/main/java/io/netty/handler/proxy/ProxyConnectionEvent.java index 2d923fb..7928260 100644 --- a/handler-proxy/src/main/java/io/netty/handler/proxy/ProxyConnectionEvent.java +++ b/handler-proxy/src/main/java/io/netty/handler/proxy/ProxyConnectionEvent.java @@ -16,6 +16,7 @@ package io.netty.handler.proxy; +import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.StringUtil; import java.net.SocketAddress; @@ -33,23 +34,10 @@ public final class ProxyConnectionEvent { */ public ProxyConnectionEvent( String protocol, String authScheme, SocketAddress proxyAddress, SocketAddress destinationAddress) { - if (protocol == null) { - throw new NullPointerException("protocol"); - } - if (authScheme == null) { - throw new NullPointerException("authScheme"); - } - if (proxyAddress == null) { - throw new NullPointerException("proxyAddress"); - } - if (destinationAddress == null) { - throw new NullPointerException("destinationAddress"); - } - - this.protocol = protocol; - this.authScheme = authScheme; - this.proxyAddress = proxyAddress; - this.destinationAddress = destinationAddress; + this.protocol = ObjectUtil.checkNotNull(protocol, "protocol"); + this.authScheme = ObjectUtil.checkNotNull(authScheme, "authScheme"); + this.proxyAddress = ObjectUtil.checkNotNull(proxyAddress, "proxyAddress"); + this.destinationAddress = ObjectUtil.checkNotNull(destinationAddress, "destinationAddress"); } /** diff --git a/handler-proxy/src/main/java/io/netty/handler/proxy/ProxyHandler.java b/handler-proxy/src/main/java/io/netty/handler/proxy/ProxyHandler.java index 8d3271e..e15baa7 100644 --- a/handler-proxy/src/main/java/io/netty/handler/proxy/ProxyHandler.java +++ b/handler-proxy/src/main/java/io/netty/handler/proxy/ProxyHandler.java @@ -28,6 +28,7 @@ import io.netty.util.concurrent.DefaultPromise; import io.netty.util.concurrent.EventExecutor; import io.netty.util.concurrent.Future; import io.netty.util.concurrent.ScheduledFuture; +import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; @@ -70,10 +71,7 @@ public abstract class ProxyHandler extends ChannelDuplexHandler { }; protected ProxyHandler(SocketAddress proxyAddress) { - if (proxyAddress == null) { - throw new NullPointerException("proxyAddress"); - } - this.proxyAddress = proxyAddress; + this.proxyAddress = ObjectUtil.checkNotNull(proxyAddress, "proxyAddress"); } /** diff --git a/handler-proxy/src/test/java/io/netty/handler/proxy/HttpProxyHandlerTest.java b/handler-proxy/src/test/java/io/netty/handler/proxy/HttpProxyHandlerTest.java index 04747e4..5e5be9e 100644 --- a/handler-proxy/src/test/java/io/netty/handler/proxy/HttpProxyHandlerTest.java +++ b/handler-proxy/src/test/java/io/netty/handler/proxy/HttpProxyHandlerTest.java @@ -15,20 +15,39 @@ */ package io.netty.handler.proxy; +import io.netty.bootstrap.Bootstrap; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelPromise; +import io.netty.channel.DefaultEventLoopGroup; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.channel.local.LocalAddress; +import io.netty.channel.local.LocalChannel; +import io.netty.channel.local.LocalServerChannel; +import io.netty.handler.codec.http.DefaultFullHttpResponse; import io.netty.handler.codec.http.DefaultHttpHeaders; import io.netty.handler.codec.http.FullHttpRequest; +import io.netty.handler.codec.http.HttpClientCodec; import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpHeaders; +import io.netty.handler.codec.http.HttpResponseEncoder; +import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.HttpVersion; +import io.netty.handler.proxy.HttpProxyHandler.HttpProxyConnectException; import io.netty.util.NetUtil; + +import java.util.concurrent.atomic.AtomicReference; import org.junit.Test; import java.net.InetAddress; import java.net.InetSocketAddress; -import static org.junit.Assert.assertEquals; +import static org.junit.Assert.*; import static org.mockito.Mockito.*; public class HttpProxyHandlerTest { @@ -153,6 +172,65 @@ public class HttpProxyHandlerTest { true); } + @Test + public void testExceptionDuringConnect() throws Exception { + EventLoopGroup group = null; + Channel serverChannel = null; + Channel clientChannel = null; + try { + group = new DefaultEventLoopGroup(1); + final LocalAddress addr = new LocalAddress("a"); + final AtomicReference<Throwable> exception = new AtomicReference<Throwable>(); + ChannelFuture sf = + new ServerBootstrap().channel(LocalServerChannel.class).group(group).childHandler( + new ChannelInitializer<Channel>() { + + @Override + protected void initChannel(Channel ch) { + ch.pipeline().addFirst(new HttpResponseEncoder()); + DefaultFullHttpResponse response = new DefaultFullHttpResponse( + HttpVersion.HTTP_1_1, + HttpResponseStatus.BAD_GATEWAY); + response.headers().add("name", "value"); + response.headers().add(HttpHeaderNames.CONTENT_LENGTH, "0"); + ch.writeAndFlush(response); + } + }).bind(addr); + serverChannel = sf.sync().channel(); + ChannelFuture cf = new Bootstrap().channel(LocalChannel.class).group(group).handler( + new ChannelInitializer<Channel>() { + @Override + protected void initChannel(Channel ch) { + ch.pipeline().addFirst(new HttpProxyHandler(addr)); + ch.pipeline().addLast(new ChannelInboundHandlerAdapter() { + @Override + public void exceptionCaught(ChannelHandlerContext ctx, + Throwable cause) { + exception.set(cause); + } + }); + } + }).connect(new InetSocketAddress("localhost", 1234)); + clientChannel = cf.sync().channel(); + clientChannel.close().sync(); + + assertTrue(exception.get() instanceof HttpProxyConnectException); + HttpProxyConnectException actual = (HttpProxyConnectException) exception.get(); + assertNotNull(actual.headers()); + assertEquals("value", actual.headers().get("name")); + } finally { + if (clientChannel != null) { + clientChannel.close(); + } + if (serverChannel != null) { + serverChannel.close(); + } + if (group != null) { + group.shutdownGracefully(); + } + } + } + private static void testInitialMessage(InetSocketAddress socketAddress, String expectedUrl, String expectedHostHeader, @@ -190,4 +268,18 @@ public class HttpProxyHandlerTest { } verify(ctx).connect(proxyAddress, null, promise); } + + @Test + public void testHttpClientCodecIsInvisible() { + EmbeddedChannel channel = new EmbeddedChannel(new HttpProxyHandler( + new InetSocketAddress(NetUtil.LOCALHOST, 8080))) { + @Override + public boolean isActive() { + // We want to simulate that the Channel did not become active yet. + return false; + } + }; + assertNotNull(channel.pipeline().get(HttpProxyHandler.class)); + assertNull(channel.pipeline().get(HttpClientCodec.class)); + } } diff --git a/handler-proxy/src/test/java/io/netty/handler/proxy/ProxyHandlerTest.java b/handler-proxy/src/test/java/io/netty/handler/proxy/ProxyHandlerTest.java index 6f0f325..77e20fb 100644 --- a/handler-proxy/src/test/java/io/netty/handler/proxy/ProxyHandlerTest.java +++ b/handler-proxy/src/test/java/io/netty/handler/proxy/ProxyHandlerTest.java @@ -431,7 +431,9 @@ public class ProxyHandlerTest { private final TestItem testItem; - public ProxyHandlerTest(TestItem testItem) { this.testItem = testItem; } + public ProxyHandlerTest(TestItem testItem) { + this.testItem = testItem; + } @Before public void clearServerExceptions() throws Exception { diff --git a/handler/pom.xml b/handler/pom.xml index 4bddf4c..5e0d10f 100644 --- a/handler/pom.xml +++ b/handler/pom.xml @@ -20,7 +20,7 @@ <parent> <groupId>io.netty</groupId> <artifactId>netty-parent</artifactId> - <version>4.1.33.Final</version> + <version>4.1.48.Final</version> </parent> <artifactId>netty-handler</artifactId> @@ -40,6 +40,11 @@ <artifactId>netty-common</artifactId> <version>${project.version}</version> </dependency> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>netty-resolver</artifactId> + <version>${project.version}</version> + </dependency> <dependency> <groupId>${project.groupId}</groupId> <artifactId>netty-buffer</artifactId> @@ -86,6 +91,14 @@ <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> </dependency> + + <dependency> + <groupId>software.amazon.cryptools</groupId> + <artifactId>AmazonCorrettoCryptoProvider</artifactId> + <version>1.1.0</version> + <classifier>linux-x86_64</classifier> + <scope>test</scope> + </dependency> </dependencies> </project> diff --git a/handler/src/main/java/io/netty/handler/address/DynamicAddressConnectHandler.java b/handler/src/main/java/io/netty/handler/address/DynamicAddressConnectHandler.java new file mode 100644 index 0000000..6565ffc --- /dev/null +++ b/handler/src/main/java/io/netty/handler/address/DynamicAddressConnectHandler.java @@ -0,0 +1,82 @@ +/* + * Copyright 2019 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.address; + +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelOutboundHandler; +import io.netty.channel.ChannelOutboundHandlerAdapter; +import io.netty.channel.ChannelPromise; + +import java.net.NetworkInterface; +import java.net.SocketAddress; + +/** + * {@link ChannelOutboundHandler} implementation which allows to dynamically replace the used + * {@code remoteAddress} and / or {@code localAddress} when making a connection attempt. + * <p> + * This can be useful to for example bind to a specific {@link NetworkInterface} based on + * the {@code remoteAddress}. + */ +public abstract class DynamicAddressConnectHandler extends ChannelOutboundHandlerAdapter { + + @Override + public final void connect(ChannelHandlerContext ctx, SocketAddress remoteAddress, + SocketAddress localAddress, ChannelPromise promise) { + final SocketAddress remote; + final SocketAddress local; + try { + remote = remoteAddress(remoteAddress, localAddress); + local = localAddress(remoteAddress, localAddress); + } catch (Exception e) { + promise.setFailure(e); + return; + } + ctx.connect(remote, local, promise).addListener(new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture future) { + if (future.isSuccess()) { + // We only remove this handler from the pipeline once the connect was successful as otherwise + // the user may try to connect again. + future.channel().pipeline().remove(DynamicAddressConnectHandler.this); + } + } + }); + } + + /** + * Returns the local {@link SocketAddress} to use for + * {@link ChannelHandlerContext#connect(SocketAddress, SocketAddress)} based on the original {@code remoteAddress} + * and {@code localAddress}. + * By default, this method returns the given {@code localAddress}. + */ + protected SocketAddress localAddress( + @SuppressWarnings("unused") SocketAddress remoteAddress, SocketAddress localAddress) throws Exception { + return localAddress; + } + + /** + * Returns the remote {@link SocketAddress} to use for + * {@link ChannelHandlerContext#connect(SocketAddress, SocketAddress)} based on the original {@code remoteAddress} + * and {@code localAddress}. + * By default, this method returns the given {@code remoteAddress}. + */ + protected SocketAddress remoteAddress( + SocketAddress remoteAddress, @SuppressWarnings("unused") SocketAddress localAddress) throws Exception { + return remoteAddress; + } +} diff --git a/handler/src/main/java/io/netty/handler/address/ResolveAddressHandler.java b/handler/src/main/java/io/netty/handler/address/ResolveAddressHandler.java new file mode 100644 index 0000000..89f0067 --- /dev/null +++ b/handler/src/main/java/io/netty/handler/address/ResolveAddressHandler.java @@ -0,0 +1,66 @@ +/* + * Copyright 2020 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.address; + +import io.netty.channel.ChannelHandler.Sharable; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelOutboundHandlerAdapter; +import io.netty.channel.ChannelPromise; +import io.netty.resolver.AddressResolver; +import io.netty.resolver.AddressResolverGroup; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.FutureListener; +import io.netty.util.internal.ObjectUtil; + +import java.net.SocketAddress; + +/** + * {@link ChannelOutboundHandlerAdapter} which will resolve the {@link SocketAddress} that is passed to + * {@link #connect(ChannelHandlerContext, SocketAddress, SocketAddress, ChannelPromise)} if it is not already resolved + * and the {@link AddressResolver} supports the type of {@link SocketAddress}. + */ +@Sharable +public class ResolveAddressHandler extends ChannelOutboundHandlerAdapter { + + private final AddressResolverGroup<? extends SocketAddress> resolverGroup; + + public ResolveAddressHandler(AddressResolverGroup<? extends SocketAddress> resolverGroup) { + this.resolverGroup = ObjectUtil.checkNotNull(resolverGroup, "resolverGroup"); + } + + @Override + public void connect(final ChannelHandlerContext ctx, SocketAddress remoteAddress, + final SocketAddress localAddress, final ChannelPromise promise) { + AddressResolver<? extends SocketAddress> resolver = resolverGroup.getResolver(ctx.executor()); + if (resolver.isSupported(remoteAddress) && !resolver.isResolved(remoteAddress)) { + resolver.resolve(remoteAddress).addListener(new FutureListener<SocketAddress>() { + @Override + public void operationComplete(Future<SocketAddress> future) { + Throwable cause = future.cause(); + if (cause != null) { + promise.setFailure(cause); + } else { + ctx.connect(future.getNow(), localAddress, promise); + } + ctx.pipeline().remove(ResolveAddressHandler.this); + } + }); + } else { + ctx.connect(remoteAddress, localAddress, promise); + ctx.pipeline().remove(this); + } + } +} diff --git a/handler/src/main/java/io/netty/handler/address/package-info.java b/handler/src/main/java/io/netty/handler/address/package-info.java new file mode 100644 index 0000000..965faa8 --- /dev/null +++ b/handler/src/main/java/io/netty/handler/address/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2019 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 to dynamically replace local / remote {@link java.net.SocketAddress}. + */ +package io.netty.handler.address; diff --git a/handler/src/main/java/io/netty/handler/flow/FlowControlHandler.java b/handler/src/main/java/io/netty/handler/flow/FlowControlHandler.java index 576227c..c79c36a 100644 --- a/handler/src/main/java/io/netty/handler/flow/FlowControlHandler.java +++ b/handler/src/main/java/io/netty/handler/flow/FlowControlHandler.java @@ -24,9 +24,10 @@ import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.ByteToMessageDecoder; import io.netty.handler.codec.MessageToByteEncoder; -import io.netty.util.Recycler; -import io.netty.util.Recycler.Handle; import io.netty.util.ReferenceCountUtil; +import io.netty.util.internal.ObjectPool; +import io.netty.util.internal.ObjectPool.Handle; +import io.netty.util.internal.ObjectPool.ObjectCreator; import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; @@ -37,7 +38,7 @@ import io.netty.util.internal.logging.InternalLoggerFactory; * many events as they like for any given input. A channel's auto reading configuration doesn't usually * apply in these scenarios. This is causing problems in downstream {@link ChannelHandler}s that would * like to hold subsequent events while they're processing one event. It's a common problem with the - * {@code HttpObjectDecoder} that will very often fire a {@code HttpRequest} that is immediately followed + * {@code HttpObjectDecoder} that will very often fire an {@code HttpRequest} that is immediately followed * by a {@code LastHttpContent} event. * * <pre>{@code @@ -88,7 +89,7 @@ public class FlowControlHandler extends ChannelDuplexHandler { * testing, debugging and inspection purposes and it is not Thread safe! */ boolean isQueueEmpty() { - return queue.isEmpty(); + return queue == null || queue.isEmpty(); } /** @@ -154,9 +155,13 @@ public class FlowControlHandler extends ChannelDuplexHandler { @Override public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { - // Don't relay completion events from upstream as they - // make no sense in this context. See dequeue() where - // a new set of completion events is being produced. + if (isQueueEmpty()) { + ctx.fireChannelReadComplete(); + } else { + // Don't relay completion events from upstream as they + // make no sense in this context. See dequeue() where + // a new set of completion events is being produced. + } } /** @@ -172,32 +177,33 @@ public class FlowControlHandler extends ChannelDuplexHandler { * @see #channelRead(ChannelHandlerContext, Object) */ private int dequeue(ChannelHandlerContext ctx, int minConsume) { - if (queue != null) { - - int consumed = 0; + int consumed = 0; + + // fireChannelRead(...) may call ctx.read() and so this method may reentrance. Because of this we need to + // check if queue was set to null in the meantime and if so break the loop. + while (queue != null && (consumed < minConsume || config.isAutoRead())) { + Object msg = queue.poll(); + if (msg == null) { + break; + } - Object msg; - while ((consumed < minConsume) || config.isAutoRead()) { - msg = queue.poll(); - if (msg == null) { - break; - } + ++consumed; + ctx.fireChannelRead(msg); + } - ++consumed; - ctx.fireChannelRead(msg); - } + // We're firing a completion event every time one (or more) + // messages were consumed and the queue ended up being drained + // to an empty state. + if (queue != null && queue.isEmpty()) { + queue.recycle(); + queue = null; - // We're firing a completion event every time one (or more) - // messages were consumed and the queue ended up being drained - // to an empty state. - if (queue.isEmpty() && consumed > 0) { + if (consumed > 0) { ctx.fireChannelReadComplete(); } - - return consumed; } - return 0; + return consumed; } /** @@ -212,12 +218,13 @@ public class FlowControlHandler extends ChannelDuplexHandler { */ private static final int DEFAULT_NUM_ELEMENTS = 2; - private static final Recycler<RecyclableArrayDeque> RECYCLER = new Recycler<RecyclableArrayDeque>() { + private static final ObjectPool<RecyclableArrayDeque> RECYCLER = ObjectPool.newPool( + new ObjectCreator<RecyclableArrayDeque>() { @Override - protected RecyclableArrayDeque newObject(Handle<RecyclableArrayDeque> handle) { + public RecyclableArrayDeque newObject(Handle<RecyclableArrayDeque> handle) { return new RecyclableArrayDeque(DEFAULT_NUM_ELEMENTS, handle); } - }; + }); public static RecyclableArrayDeque newInstance() { return RECYCLER.get(); diff --git a/handler/src/main/java/io/netty/handler/flush/FlushConsolidationHandler.java b/handler/src/main/java/io/netty/handler/flush/FlushConsolidationHandler.java index 472a83b..b532f81 100644 --- a/handler/src/main/java/io/netty/handler/flush/FlushConsolidationHandler.java +++ b/handler/src/main/java/io/netty/handler/flush/FlushConsolidationHandler.java @@ -23,6 +23,7 @@ import io.netty.channel.ChannelOutboundHandler; import io.netty.channel.ChannelOutboundInvoker; import io.netty.channel.ChannelPipeline; import io.netty.channel.ChannelPromise; +import io.netty.util.internal.ObjectUtil; import java.util.concurrent.Future; @@ -95,20 +96,17 @@ public class FlushConsolidationHandler extends ChannelDuplexHandler { * ongoing. */ public FlushConsolidationHandler(int explicitFlushAfterFlushes, boolean consolidateWhenNoReadInProgress) { - if (explicitFlushAfterFlushes <= 0) { - throw new IllegalArgumentException("explicitFlushAfterFlushes: " - + explicitFlushAfterFlushes + " (expected: > 0)"); - } - this.explicitFlushAfterFlushes = explicitFlushAfterFlushes; + this.explicitFlushAfterFlushes = + ObjectUtil.checkPositive(explicitFlushAfterFlushes, "explicitFlushAfterFlushes"); this.consolidateWhenNoReadInProgress = consolidateWhenNoReadInProgress; - flushTask = consolidateWhenNoReadInProgress ? + this.flushTask = consolidateWhenNoReadInProgress ? new Runnable() { @Override public void run() { if (flushPendingCount > 0 && !readInProgress) { flushPendingCount = 0; - ctx.flush(); nextScheduledFlush = null; + ctx.flush(); } // else we'll flush when the read completes } } diff --git a/handler/src/main/java/io/netty/handler/ipfilter/IpSubnetFilterRule.java b/handler/src/main/java/io/netty/handler/ipfilter/IpSubnetFilterRule.java index b6c2e83..bc6b1ff 100644 --- a/handler/src/main/java/io/netty/handler/ipfilter/IpSubnetFilterRule.java +++ b/handler/src/main/java/io/netty/handler/ipfilter/IpSubnetFilterRule.java @@ -15,6 +15,7 @@ */ package io.netty.handler.ipfilter; +import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.SocketUtils; import java.math.BigInteger; @@ -45,13 +46,8 @@ public final class IpSubnetFilterRule implements IpFilterRule { } private static IpFilterRule selectFilterRule(InetAddress ipAddress, int cidrPrefix, IpFilterRuleType ruleType) { - if (ipAddress == null) { - throw new NullPointerException("ipAddress"); - } - - if (ruleType == null) { - throw new NullPointerException("ruleType"); - } + ObjectUtil.checkNotNull(ipAddress, "ipAddress"); + ObjectUtil.checkNotNull(ruleType, "ruleType"); if (ipAddress instanceof Inet4Address) { return new Ip4SubnetFilterRule((Inet4Address) ipAddress, cidrPrefix, ruleType); diff --git a/handler/src/main/java/io/netty/handler/ipfilter/RuleBasedIpFilter.java b/handler/src/main/java/io/netty/handler/ipfilter/RuleBasedIpFilter.java index 66ffa45..8d2b2f1 100644 --- a/handler/src/main/java/io/netty/handler/ipfilter/RuleBasedIpFilter.java +++ b/handler/src/main/java/io/netty/handler/ipfilter/RuleBasedIpFilter.java @@ -18,6 +18,7 @@ package io.netty.handler.ipfilter; import io.netty.channel.Channel; import io.netty.channel.ChannelHandler.Sharable; import io.netty.channel.ChannelHandlerContext; +import io.netty.util.internal.ObjectUtil; import java.net.InetSocketAddress; import java.net.SocketAddress; @@ -36,11 +37,7 @@ public class RuleBasedIpFilter extends AbstractRemoteAddressFilter<InetSocketAdd private final IpFilterRule[] rules; public RuleBasedIpFilter(IpFilterRule... rules) { - if (rules == null) { - throw new NullPointerException("rules"); - } - - this.rules = rules; + this.rules = ObjectUtil.checkNotNull(rules, "rules"); } @Override diff --git a/handler/src/main/java/io/netty/handler/logging/ByteBufFormat.java b/handler/src/main/java/io/netty/handler/logging/ByteBufFormat.java new file mode 100644 index 0000000..c643afd --- /dev/null +++ b/handler/src/main/java/io/netty/handler/logging/ByteBufFormat.java @@ -0,0 +1,36 @@ +/* + * Copyright 2020 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.logging; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufHolder; +import io.netty.buffer.ByteBufUtil; + +/** + * Used to control the format and verbosity of logging for {@link ByteBuf}s and {@link ByteBufHolder}s. + * + * @see LoggingHandler + */ +public enum ByteBufFormat { + /** + * {@link ByteBuf}s will be logged in a simple format, with no hex dump included. + */ + SIMPLE, + /** + * {@link ByteBuf}s will be logged using {@link ByteBufUtil#appendPrettyHexDump(StringBuilder, ByteBuf)}. + */ + HEX_DUMP +} diff --git a/handler/src/main/java/io/netty/handler/logging/LoggingHandler.java b/handler/src/main/java/io/netty/handler/logging/LoggingHandler.java index 3573fd3..5a84e9f 100644 --- a/handler/src/main/java/io/netty/handler/logging/LoggingHandler.java +++ b/handler/src/main/java/io/netty/handler/logging/LoggingHandler.java @@ -23,6 +23,7 @@ import io.netty.channel.ChannelHandler.Sharable; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelOutboundHandler; import io.netty.channel.ChannelPromise; +import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.logging.InternalLogLevel; import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; @@ -34,7 +35,7 @@ import static io.netty.util.internal.StringUtil.NEWLINE; /** * A {@link ChannelHandler} that logs all events using a logging framework. - * By default, all events are logged at <tt>DEBUG</tt> level. + * By default, all events are logged at <tt>DEBUG</tt> level and full hex dumps are recorded for ByteBufs. */ @Sharable @SuppressWarnings({ "StringConcatenationInsideStringBufferAppend", "StringBufferReplaceableByString" }) @@ -46,6 +47,7 @@ public class LoggingHandler extends ChannelDuplexHandler { protected final InternalLogLevel internalLevel; private final LogLevel level; + private final ByteBufFormat byteBufFormat; /** * Creates a new instance whose logger name is the fully qualified class @@ -62,12 +64,20 @@ public class LoggingHandler extends ChannelDuplexHandler { * @param level the log level */ public LoggingHandler(LogLevel level) { - if (level == null) { - throw new NullPointerException("level"); - } + this(level, ByteBufFormat.HEX_DUMP); + } + /** + * Creates a new instance whose logger name is the fully qualified class + * name of the instance. + * + * @param level the log level + * @param byteBufFormat the ByteBuf format + */ + public LoggingHandler(LogLevel level, ByteBufFormat byteBufFormat) { + this.level = ObjectUtil.checkNotNull(level, "level"); + this.byteBufFormat = ObjectUtil.checkNotNull(byteBufFormat, "byteBufFormat"); logger = InternalLoggerFactory.getInstance(getClass()); - this.level = level; internalLevel = level.toInternalLevel(); } @@ -88,15 +98,21 @@ public class LoggingHandler extends ChannelDuplexHandler { * @param level the log level */ public LoggingHandler(Class<?> clazz, LogLevel level) { - if (clazz == null) { - throw new NullPointerException("clazz"); - } - if (level == null) { - throw new NullPointerException("level"); - } + this(clazz, level, ByteBufFormat.HEX_DUMP); + } + /** + * Creates a new instance with the specified logger name. + * + * @param clazz the class type to generate the logger for + * @param level the log level + * @param byteBufFormat the ByteBuf format + */ + public LoggingHandler(Class<?> clazz, LogLevel level, ByteBufFormat byteBufFormat) { + ObjectUtil.checkNotNull(clazz, "clazz"); + this.level = ObjectUtil.checkNotNull(level, "level"); + this.byteBufFormat = ObjectUtil.checkNotNull(byteBufFormat, "byteBufFormat"); logger = InternalLoggerFactory.getInstance(clazz); - this.level = level; internalLevel = level.toInternalLevel(); } @@ -116,15 +132,22 @@ public class LoggingHandler extends ChannelDuplexHandler { * @param level the log level */ public LoggingHandler(String name, LogLevel level) { - if (name == null) { - throw new NullPointerException("name"); - } - if (level == null) { - throw new NullPointerException("level"); - } + this(name, level, ByteBufFormat.HEX_DUMP); + } + + /** + * Creates a new instance with the specified logger name. + * + * @param name the name of the class to use for the logger + * @param level the log level + * @param byteBufFormat the ByteBuf format + */ + public LoggingHandler(String name, LogLevel level, ByteBufFormat byteBufFormat) { + ObjectUtil.checkNotNull(name, "name"); + this.level = ObjectUtil.checkNotNull(level, "level"); + this.byteBufFormat = ObjectUtil.checkNotNull(byteBufFormat, "byteBufFormat"); logger = InternalLoggerFactory.getInstance(name); - this.level = level; internalLevel = level.toInternalLevel(); } @@ -135,6 +158,13 @@ public class LoggingHandler extends ChannelDuplexHandler { return level; } + /** + * Returns the {@link ByteBufFormat} that this handler uses to log + */ + public ByteBufFormat byteBufFormat() { + return byteBufFormat; + } + @Override public void channelRegistered(ChannelHandlerContext ctx) throws Exception { if (logger.isEnabled(internalLevel)) { @@ -320,7 +350,7 @@ public class LoggingHandler extends ChannelDuplexHandler { /** * Generates the default log message of the specified event whose argument is a {@link ByteBuf}. */ - private static String formatByteBuf(ChannelHandlerContext ctx, String eventName, ByteBuf msg) { + private String formatByteBuf(ChannelHandlerContext ctx, String eventName, ByteBuf msg) { String chStr = ctx.channel().toString(); int length = msg.readableBytes(); if (length == 0) { @@ -328,11 +358,18 @@ public class LoggingHandler extends ChannelDuplexHandler { buf.append(chStr).append(' ').append(eventName).append(": 0B"); return buf.toString(); } else { - int rows = length / 16 + (length % 15 == 0? 0 : 1) + 4; - StringBuilder buf = new StringBuilder(chStr.length() + 1 + eventName.length() + 2 + 10 + 1 + 2 + rows * 80); - - buf.append(chStr).append(' ').append(eventName).append(": ").append(length).append('B').append(NEWLINE); - appendPrettyHexDump(buf, msg); + int outputLength = chStr.length() + 1 + eventName.length() + 2 + 10 + 1; + if (byteBufFormat == ByteBufFormat.HEX_DUMP) { + int rows = length / 16 + (length % 15 == 0? 0 : 1) + 4; + int hexDumpLength = 2 + rows * 80; + outputLength += hexDumpLength; + } + StringBuilder buf = new StringBuilder(outputLength); + buf.append(chStr).append(' ').append(eventName).append(": ").append(length).append('B'); + if (byteBufFormat == ByteBufFormat.HEX_DUMP) { + buf.append(NEWLINE); + appendPrettyHexDump(buf, msg); + } return buf.toString(); } @@ -341,7 +378,7 @@ public class LoggingHandler extends ChannelDuplexHandler { /** * Generates the default log message of the specified event whose argument is a {@link ByteBufHolder}. */ - private static String formatByteBufHolder(ChannelHandlerContext ctx, String eventName, ByteBufHolder msg) { + private String formatByteBufHolder(ChannelHandlerContext ctx, String eventName, ByteBufHolder msg) { String chStr = ctx.channel().toString(); String msgStr = msg.toString(); ByteBuf content = msg.content(); @@ -351,13 +388,19 @@ public class LoggingHandler extends ChannelDuplexHandler { buf.append(chStr).append(' ').append(eventName).append(", ").append(msgStr).append(", 0B"); return buf.toString(); } else { - int rows = length / 16 + (length % 15 == 0? 0 : 1) + 4; - StringBuilder buf = new StringBuilder( - chStr.length() + 1 + eventName.length() + 2 + msgStr.length() + 2 + 10 + 1 + 2 + rows * 80); - + int outputLength = chStr.length() + 1 + eventName.length() + 2 + msgStr.length() + 2 + 10 + 1; + if (byteBufFormat == ByteBufFormat.HEX_DUMP) { + int rows = length / 16 + (length % 15 == 0? 0 : 1) + 4; + int hexDumpLength = 2 + rows * 80; + outputLength += hexDumpLength; + } + StringBuilder buf = new StringBuilder(outputLength); buf.append(chStr).append(' ').append(eventName).append(": ") - .append(msgStr).append(", ").append(length).append('B').append(NEWLINE); - appendPrettyHexDump(buf, content); + .append(msgStr).append(", ").append(length).append('B'); + if (byteBufFormat == ByteBufFormat.HEX_DUMP) { + buf.append(NEWLINE); + appendPrettyHexDump(buf, content); + } return buf.toString(); } diff --git a/handler/src/main/java/io/netty/handler/ssl/AbstractSniHandler.java b/handler/src/main/java/io/netty/handler/ssl/AbstractSniHandler.java index 05fcaff..dcba585 100644 --- a/handler/src/main/java/io/netty/handler/ssl/AbstractSniHandler.java +++ b/handler/src/main/java/io/netty/handler/ssl/AbstractSniHandler.java @@ -16,21 +16,10 @@ package io.netty.handler.ssl; import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufUtil; import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelOutboundHandler; -import io.netty.channel.ChannelPromise; -import io.netty.handler.codec.ByteToMessageDecoder; -import io.netty.handler.codec.DecoderException; import io.netty.util.CharsetUtil; import io.netty.util.concurrent.Future; -import io.netty.util.concurrent.FutureListener; -import io.netty.util.internal.PlatformDependent; -import io.netty.util.internal.logging.InternalLogger; -import io.netty.util.internal.logging.InternalLoggerFactory; -import java.net.SocketAddress; -import java.util.List; import java.util.Locale; /** @@ -40,228 +29,107 @@ import java.util.Locale; * The client will send host name in the handshake data so server could decide * which certificate to choose for the host name.</p> */ -public abstract class AbstractSniHandler<T> extends ByteToMessageDecoder implements ChannelOutboundHandler { - - // Maximal number of ssl records to inspect before fallback to the default SslContext. - private static final int MAX_SSL_RECORDS = 4; - - private static final InternalLogger logger = - InternalLoggerFactory.getInstance(AbstractSniHandler.class); - - private boolean handshakeFailed; - private boolean suppressRead; - private boolean readPending; - - @Override - protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception { - if (!suppressRead && !handshakeFailed) { - final int writerIndex = in.writerIndex(); - try { - loop: - for (int i = 0; i < MAX_SSL_RECORDS; i++) { - final int readerIndex = in.readerIndex(); - final int readableBytes = writerIndex - readerIndex; - if (readableBytes < SslUtils.SSL_RECORD_HEADER_LENGTH) { - // Not enough data to determine the record type and length. - return; +public abstract class AbstractSniHandler<T> extends SslClientHelloHandler<T> { + + private static String extractSniHostname(ByteBuf in) { + // See https://tools.ietf.org/html/rfc5246#section-7.4.1.2 + // + // Decode the ssl client hello packet. + // + // struct { + // ProtocolVersion client_version; + // Random random; + // SessionID session_id; + // CipherSuite cipher_suites<2..2^16-2>; + // CompressionMethod compression_methods<1..2^8-1>; + // select (extensions_present) { + // case false: + // struct {}; + // case true: + // Extension extensions<0..2^16-1>; + // }; + // } ClientHello; + // + + // We have to skip bytes until SessionID (which sum to 34 bytes in this case). + int offset = in.readerIndex(); + int endOffset = in.writerIndex(); + offset += 34; + + if (endOffset - offset >= 6) { + final int sessionIdLength = in.getUnsignedByte(offset); + offset += sessionIdLength + 1; + + final int cipherSuitesLength = in.getUnsignedShort(offset); + offset += cipherSuitesLength + 2; + + final int compressionMethodLength = in.getUnsignedByte(offset); + offset += compressionMethodLength + 1; + + final int extensionsLength = in.getUnsignedShort(offset); + offset += 2; + final int extensionsLimit = offset + extensionsLength; + + // Extensions should never exceed the record boundary. + if (extensionsLimit <= endOffset) { + while (extensionsLimit - offset >= 4) { + final int extensionType = in.getUnsignedShort(offset); + offset += 2; + + final int extensionLength = in.getUnsignedShort(offset); + offset += 2; + + if (extensionsLimit - offset < extensionLength) { + break; } - final int command = in.getUnsignedByte(readerIndex); - - // tls, but not handshake command - switch (command) { - case SslUtils.SSL_CONTENT_TYPE_CHANGE_CIPHER_SPEC: - case SslUtils.SSL_CONTENT_TYPE_ALERT: - final int len = SslUtils.getEncryptedPacketLength(in, readerIndex); - - // Not an SSL/TLS packet - if (len == SslUtils.NOT_ENCRYPTED) { - handshakeFailed = true; - NotSslRecordException e = new NotSslRecordException( - "not an SSL/TLS record: " + ByteBufUtil.hexDump(in)); - in.skipBytes(in.readableBytes()); - ctx.fireUserEventTriggered(new SniCompletionEvent(e)); - SslUtils.handleHandshakeFailure(ctx, e, true); - throw e; - } - if (len == SslUtils.NOT_ENOUGH_DATA || - writerIndex - readerIndex - SslUtils.SSL_RECORD_HEADER_LENGTH < len) { - // Not enough data - return; - } - // increase readerIndex and try again. - in.skipBytes(len); - continue; - case SslUtils.SSL_CONTENT_TYPE_HANDSHAKE: - final int majorVersion = in.getUnsignedByte(readerIndex + 1); - - // SSLv3 or TLS - if (majorVersion == 3) { - final int packetLength = in.getUnsignedShort(readerIndex + 3) + - SslUtils.SSL_RECORD_HEADER_LENGTH; - - if (readableBytes < packetLength) { - // client hello incomplete; try again to decode once more data is ready. - return; - } - - // See https://tools.ietf.org/html/rfc5246#section-7.4.1.2 - // - // Decode the ssl client hello packet. - // We have to skip bytes until SessionID (which sum to 43 bytes). - // - // struct { - // ProtocolVersion client_version; - // Random random; - // SessionID session_id; - // CipherSuite cipher_suites<2..2^16-2>; - // CompressionMethod compression_methods<1..2^8-1>; - // select (extensions_present) { - // case false: - // struct {}; - // case true: - // Extension extensions<0..2^16-1>; - // }; - // } ClientHello; - // - - final int endOffset = readerIndex + packetLength; - int offset = readerIndex + 43; - - if (endOffset - offset < 6) { - break loop; - } - - final int sessionIdLength = in.getUnsignedByte(offset); - offset += sessionIdLength + 1; - - final int cipherSuitesLength = in.getUnsignedShort(offset); - offset += cipherSuitesLength + 2; - - final int compressionMethodLength = in.getUnsignedByte(offset); - offset += compressionMethodLength + 1; - - final int extensionsLength = in.getUnsignedShort(offset); - offset += 2; - final int extensionsLimit = offset + extensionsLength; - - if (extensionsLimit > endOffset) { - // Extensions should never exceed the record boundary. - break loop; - } - - for (;;) { - if (extensionsLimit - offset < 4) { - break loop; - } - - final int extensionType = in.getUnsignedShort(offset); - offset += 2; - - final int extensionLength = in.getUnsignedShort(offset); - offset += 2; - - if (extensionsLimit - offset < extensionLength) { - break loop; - } - - // SNI - // See https://tools.ietf.org/html/rfc6066#page-6 - if (extensionType == 0) { - offset += 2; - if (extensionsLimit - offset < 3) { - break loop; - } - - final int serverNameType = in.getUnsignedByte(offset); - offset++; - - if (serverNameType == 0) { - final int serverNameLength = in.getUnsignedShort(offset); - offset += 2; - - if (extensionsLimit - offset < serverNameLength) { - break loop; - } + // SNI + // See https://tools.ietf.org/html/rfc6066#page-6 + if (extensionType == 0) { + offset += 2; + if (extensionsLimit - offset < 3) { + break; + } - final String hostname = in.toString(offset, serverNameLength, - CharsetUtil.US_ASCII); + final int serverNameType = in.getUnsignedByte(offset); + offset++; - try { - select(ctx, hostname.toLowerCase(Locale.US)); - } catch (Throwable t) { - PlatformDependent.throwException(t); - } - return; - } else { - // invalid enum value - break loop; - } - } + if (serverNameType == 0) { + final int serverNameLength = in.getUnsignedShort(offset); + offset += 2; - offset += extensionLength; - } + if (extensionsLimit - offset < serverNameLength) { + break; } - // Fall-through - default: - //not tls, ssl or application data, do not try sni - break loop; + + final String hostname = in.toString(offset, serverNameLength, CharsetUtil.US_ASCII); + return hostname.toLowerCase(Locale.US); + } else { + // invalid enum value + break; + } } - } - } catch (NotSslRecordException e) { - // Just rethrow as in this case we also closed the channel and this is consistent with SslHandler. - throw e; - } catch (Exception e) { - // unexpected encoding, ignore sni and use default - if (logger.isDebugEnabled()) { - logger.debug("Unexpected client hello packet: " + ByteBufUtil.hexDump(in), e); + + offset += extensionLength; } } - // Just select the default SslContext - select(ctx, null); } + return null; } - private void select(final ChannelHandlerContext ctx, final String hostname) throws Exception { - Future<T> future = lookup(ctx, hostname); - if (future.isDone()) { - fireSniCompletionEvent(ctx, hostname, future); - onLookupComplete(ctx, hostname, future); - } else { - suppressRead = true; - future.addListener(new FutureListener<T>() { - @Override - public void operationComplete(Future<T> future) throws Exception { - try { - suppressRead = false; - try { - fireSniCompletionEvent(ctx, hostname, future); - onLookupComplete(ctx, hostname, future); - } catch (DecoderException err) { - ctx.fireExceptionCaught(err); - } catch (Exception cause) { - ctx.fireExceptionCaught(new DecoderException(cause)); - } catch (Throwable cause) { - ctx.fireExceptionCaught(cause); - } - } finally { - if (readPending) { - readPending = false; - ctx.read(); - } - } - } - }); - } + private String hostname; + + @Override + protected Future<T> lookup(ChannelHandlerContext ctx, ByteBuf clientHello) throws Exception { + hostname = clientHello == null ? null : extractSniHostname(clientHello); + + return lookup(ctx, hostname); } - private void fireSniCompletionEvent(ChannelHandlerContext ctx, String hostname, Future<T> future) { - Throwable cause = future.cause(); - if (cause == null) { - ctx.fireUserEventTriggered(new SniCompletionEvent(hostname)); - } else { - ctx.fireUserEventTriggered(new SniCompletionEvent(hostname, cause)); - } + @Override + protected void onLookupComplete(ChannelHandlerContext ctx, Future<T> future) throws Exception { + fireSniCompletionEvent(ctx, hostname, future); + onLookupComplete(ctx, hostname, future); } /** @@ -280,48 +148,12 @@ public abstract class AbstractSniHandler<T> extends ByteToMessageDecoder impleme protected abstract void onLookupComplete(ChannelHandlerContext ctx, String hostname, Future<T> future) throws Exception; - @Override - public void read(ChannelHandlerContext ctx) throws Exception { - if (suppressRead) { - readPending = true; + private void fireSniCompletionEvent(ChannelHandlerContext ctx, String hostname, Future<T> future) { + Throwable cause = future.cause(); + if (cause == null) { + ctx.fireUserEventTriggered(new SniCompletionEvent(hostname)); } else { - ctx.read(); + ctx.fireUserEventTriggered(new SniCompletionEvent(hostname, cause)); } } - - @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 disconnect(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception { - ctx.disconnect(promise); - } - - @Override - public void close(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception { - ctx.close(promise); - } - - @Override - public void deregister(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception { - ctx.deregister(promise); - } - - @Override - public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { - ctx.write(msg, promise); - } - - @Override - public void flush(ChannelHandlerContext ctx) throws Exception { - ctx.flush(); - } } diff --git a/handler/src/main/java/io/netty/handler/ssl/ApplicationProtocolNegotiationHandler.java b/handler/src/main/java/io/netty/handler/ssl/ApplicationProtocolNegotiationHandler.java index 0e3ea0f..012463e 100644 --- a/handler/src/main/java/io/netty/handler/ssl/ApplicationProtocolNegotiationHandler.java +++ b/handler/src/main/java/io/netty/handler/ssl/ApplicationProtocolNegotiationHandler.java @@ -79,22 +79,29 @@ public abstract class ApplicationProtocolNegotiationHandler extends ChannelInbou @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { if (evt instanceof SslHandshakeCompletionEvent) { - ctx.pipeline().remove(this); - SslHandshakeCompletionEvent handshakeEvent = (SslHandshakeCompletionEvent) evt; - if (handshakeEvent.isSuccess()) { - SslHandler sslHandler = ctx.pipeline().get(SslHandler.class); - if (sslHandler == null) { - throw new IllegalStateException("cannot find a SslHandler in the pipeline (required for " + - "application-level protocol negotiation)"); + try { + SslHandshakeCompletionEvent handshakeEvent = (SslHandshakeCompletionEvent) evt; + if (handshakeEvent.isSuccess()) { + SslHandler sslHandler = ctx.pipeline().get(SslHandler.class); + if (sslHandler == null) { + throw new IllegalStateException("cannot find an SslHandler in the pipeline (required for " + + "application-level protocol negotiation)"); + } + String protocol = sslHandler.applicationProtocol(); + configurePipeline(ctx, protocol != null ? protocol : fallbackProtocol); + } else { + handshakeFailure(ctx, handshakeEvent.cause()); + } + } catch (Throwable cause) { + exceptionCaught(ctx, cause); + } finally { + ChannelPipeline pipeline = ctx.pipeline(); + if (pipeline.context(this) != null) { + pipeline.remove(this); } - String protocol = sslHandler.applicationProtocol(); - configurePipeline(ctx, protocol != null? protocol : fallbackProtocol); - } else { - handshakeFailure(ctx, handshakeEvent.cause()); } } - ctx.fireUserEventTriggered(evt); } @@ -119,6 +126,7 @@ public abstract class ApplicationProtocolNegotiationHandler extends ChannelInbou @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { logger.warn("{} Failed to select the application-level protocol:", ctx.channel(), cause); + ctx.fireExceptionCaught(cause); ctx.close(); } } diff --git a/handler/src/main/java/io/netty/handler/ssl/Conscrypt.java b/handler/src/main/java/io/netty/handler/ssl/Conscrypt.java index 4d7ec05..9c9e2bb 100644 --- a/handler/src/main/java/io/netty/handler/ssl/Conscrypt.java +++ b/handler/src/main/java/io/netty/handler/ssl/Conscrypt.java @@ -28,6 +28,7 @@ final class Conscrypt { // This class exists to avoid loading other conscrypt related classes using features only available in JDK8+, // because we need to maintain JDK6+ runtime compatibility. private static final Method IS_CONSCRYPT_SSLENGINE = loadIsConscryptEngine(); + private static final boolean CAN_INSTANCE_PROVIDER = canInstanceProvider(); private static Method loadIsConscryptEngine() { try { @@ -40,11 +41,22 @@ final class Conscrypt { } } + private static boolean canInstanceProvider() { + try { + Class<?> providerClass = Class.forName("org.conscrypt.OpenSSLProvider", true, + ConscryptAlpnSslEngine.class.getClassLoader()); + providerClass.newInstance(); + return true; + } catch (Throwable ignore) { + return false; + } + } + /** * Indicates whether or not conscrypt is available on the current system. */ static boolean isAvailable() { - return IS_CONSCRYPT_SSLENGINE != null && PlatformDependent.javaVersion() >= 8; + return CAN_INSTANCE_PROVIDER && IS_CONSCRYPT_SSLENGINE != null && PlatformDependent.javaVersion() >= 8; } static boolean isEngineSupported(SSLEngine engine) { diff --git a/handler/src/main/java/io/netty/handler/ssl/DelegatingSslContext.java b/handler/src/main/java/io/netty/handler/ssl/DelegatingSslContext.java index af02798..c79da85 100644 --- a/handler/src/main/java/io/netty/handler/ssl/DelegatingSslContext.java +++ b/handler/src/main/java/io/netty/handler/ssl/DelegatingSslContext.java @@ -21,6 +21,7 @@ import io.netty.util.internal.ObjectUtil; import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLSessionContext; import java.util.List; +import java.util.concurrent.Executor; /** * Adapter class which allows to wrap another {@link SslContext} and init {@link SSLEngine} instances. @@ -86,6 +87,21 @@ public abstract class DelegatingSslContext extends SslContext { return handler; } + @Override + protected SslHandler newHandler(ByteBufAllocator alloc, boolean startTls, Executor executor) { + SslHandler handler = ctx.newHandler(alloc, startTls, executor); + initHandler(handler); + return handler; + } + + @Override + protected SslHandler newHandler(ByteBufAllocator alloc, String peerHost, int peerPort, + boolean startTls, Executor executor) { + SslHandler handler = ctx.newHandler(alloc, peerHost, peerPort, startTls, executor); + initHandler(handler); + return handler; + } + @Override public final SSLSessionContext sessionContext() { return ctx.sessionContext(); diff --git a/handler/src/main/java/io/netty/handler/ssl/ExtendedOpenSslSession.java b/handler/src/main/java/io/netty/handler/ssl/ExtendedOpenSslSession.java index 184845a..879914c 100644 --- a/handler/src/main/java/io/netty/handler/ssl/ExtendedOpenSslSession.java +++ b/handler/src/main/java/io/netty/handler/ssl/ExtendedOpenSslSession.java @@ -15,6 +15,8 @@ */ package io.netty.handler.ssl; +import io.netty.util.internal.SuppressJava6Requirement; + import javax.net.ssl.ExtendedSSLSession; import javax.net.ssl.SSLException; import javax.net.ssl.SSLPeerUnverifiedException; @@ -29,6 +31,7 @@ import java.util.List; * Delegates all operations to a wrapped {@link OpenSslSession} except the methods defined by {@link ExtendedSSLSession} * itself. */ +@SuppressJava6Requirement(reason = "Usage guarded by java version check") abstract class ExtendedOpenSslSession extends ExtendedSSLSession implements OpenSslSession { // TODO: use OpenSSL API to actually fetch the real data but for now just do what Conscrypt does: @@ -46,6 +49,7 @@ abstract class ExtendedOpenSslSession extends ExtendedSSLSession implements Open } // Use rawtypes an unchecked override to be able to also work on java7. + @Override @SuppressWarnings({ "unchecked", "rawtypes" }) public abstract List getRequestedServerNames(); diff --git a/handler/src/main/java/io/netty/handler/ssl/Java7SslParametersUtils.java b/handler/src/main/java/io/netty/handler/ssl/Java7SslParametersUtils.java index 2255871..ed99802 100644 --- a/handler/src/main/java/io/netty/handler/ssl/Java7SslParametersUtils.java +++ b/handler/src/main/java/io/netty/handler/ssl/Java7SslParametersUtils.java @@ -15,6 +15,8 @@ */ package io.netty.handler.ssl; +import io.netty.util.internal.SuppressJava6Requirement; + import javax.net.ssl.SSLParameters; import java.security.AlgorithmConstraints; @@ -29,6 +31,7 @@ final class Java7SslParametersUtils { * {@link AlgorithmConstraints} in the code. This helps us to not get into trouble when using it in java * version < 7 and especially when using on android. */ + @SuppressJava6Requirement(reason = "Usage guarded by java version check") static void setAlgorithmConstraints(SSLParameters sslParameters, Object algorithmConstraints) { sslParameters.setAlgorithmConstraints((AlgorithmConstraints) algorithmConstraints); } diff --git a/handler/src/main/java/io/netty/handler/ssl/Java8SslUtils.java b/handler/src/main/java/io/netty/handler/ssl/Java8SslUtils.java index c47d965..0a5fbc7 100644 --- a/handler/src/main/java/io/netty/handler/ssl/Java8SslUtils.java +++ b/handler/src/main/java/io/netty/handler/ssl/Java8SslUtils.java @@ -15,6 +15,8 @@ */ package io.netty.handler.ssl; +import io.netty.util.internal.SuppressJava6Requirement; + import javax.net.ssl.SNIHostName; import javax.net.ssl.SNIMatcher; import javax.net.ssl.SNIServerName; @@ -25,6 +27,7 @@ import java.util.Collections; import java.util.Iterator; import java.util.List; +@SuppressJava6Requirement(reason = "Usage guarded by java version check") final class Java8SslUtils { private Java8SslUtils() { } diff --git a/handler/src/main/java/io/netty/handler/ssl/Java9SslEngine.java b/handler/src/main/java/io/netty/handler/ssl/Java9SslEngine.java index 5910855..4851522 100644 --- a/handler/src/main/java/io/netty/handler/ssl/Java9SslEngine.java +++ b/handler/src/main/java/io/netty/handler/ssl/Java9SslEngine.java @@ -16,6 +16,7 @@ package io.netty.handler.ssl; import io.netty.util.internal.StringUtil; +import io.netty.util.internal.SuppressJava6Requirement; import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLEngineResult; @@ -30,6 +31,7 @@ import static io.netty.handler.ssl.SslUtils.toSSLHandshakeException; import static io.netty.handler.ssl.JdkApplicationProtocolNegotiator.ProtocolSelectionListener; import static io.netty.handler.ssl.JdkApplicationProtocolNegotiator.ProtocolSelector; +@SuppressJava6Requirement(reason = "Usage guarded by java version check") final class Java9SslEngine extends JdkSslEngine { private final ProtocolSelectionListener selectionListener; private final AlpnSelector alpnSelector; diff --git a/handler/src/main/java/io/netty/handler/ssl/Java9SslUtils.java b/handler/src/main/java/io/netty/handler/ssl/Java9SslUtils.java index 2fd3bbc..b5886c1 100644 --- a/handler/src/main/java/io/netty/handler/ssl/Java9SslUtils.java +++ b/handler/src/main/java/io/netty/handler/ssl/Java9SslUtils.java @@ -27,9 +27,11 @@ import java.util.function.BiFunction; import io.netty.util.internal.EmptyArrays; import io.netty.util.internal.PlatformDependent; +import io.netty.util.internal.SuppressJava6Requirement; import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; +@SuppressJava6Requirement(reason = "Usage guarded by java version check") final class Java9SslUtils { private static final InternalLogger logger = InternalLoggerFactory.getInstance(Java9SslUtils.class); private static final Method SET_APPLICATION_PROTOCOLS; diff --git a/handler/src/main/java/io/netty/handler/ssl/JdkAlpnApplicationProtocolNegotiator.java b/handler/src/main/java/io/netty/handler/ssl/JdkAlpnApplicationProtocolNegotiator.java index 25f7ac8..7b9cbfb 100644 --- a/handler/src/main/java/io/netty/handler/ssl/JdkAlpnApplicationProtocolNegotiator.java +++ b/handler/src/main/java/io/netty/handler/ssl/JdkAlpnApplicationProtocolNegotiator.java @@ -148,4 +148,8 @@ public final class JdkAlpnApplicationProtocolNegotiator extends JdkBaseApplicati static boolean jdkAlpnSupported() { return PlatformDependent.javaVersion() >= 9 && Java9SslUtils.supportsAlpn(); } + + static boolean isAlpnSupported() { + return AVAILABLE; + } } diff --git a/handler/src/main/java/io/netty/handler/ssl/JdkSslClientContext.java b/handler/src/main/java/io/netty/handler/ssl/JdkSslClientContext.java index 6ed131e..35b5325 100644 --- a/handler/src/main/java/io/netty/handler/ssl/JdkSslClientContext.java +++ b/handler/src/main/java/io/netty/handler/ssl/JdkSslClientContext.java @@ -16,6 +16,7 @@ package io.netty.handler.ssl; +import java.security.KeyStore; import java.security.Provider; import javax.net.ssl.KeyManager; import javax.net.ssl.KeyManagerFactory; @@ -169,12 +170,12 @@ public final class JdkSslClientContext extends JdkSslContext { } JdkSslClientContext(Provider provider, - File trustCertCollectionFile, TrustManagerFactory trustManagerFactory, - Iterable<String> ciphers, CipherSuiteFilter cipherFilter, JdkApplicationProtocolNegotiator apn, - long sessionCacheSize, long sessionTimeout) throws SSLException { + File trustCertCollectionFile, TrustManagerFactory trustManagerFactory, + Iterable<String> ciphers, CipherSuiteFilter cipherFilter, JdkApplicationProtocolNegotiator apn, + long sessionCacheSize, long sessionTimeout) throws SSLException { super(newSSLContext(provider, toX509CertificatesInternal(trustCertCollectionFile), trustManagerFactory, null, null, - null, null, sessionCacheSize, sessionTimeout), true, + null, null, sessionCacheSize, sessionTimeout, KeyStore.getDefaultType()), true, ciphers, cipherFilter, apn, ClientAuth.NONE, null, false); } @@ -257,7 +258,7 @@ public final class JdkSslClientContext extends JdkSslContext { super(newSSLContext(null, toX509CertificatesInternal( trustCertCollectionFile), trustManagerFactory, toX509CertificatesInternal(keyCertChainFile), toPrivateKeyInternal(keyFile, keyPassword), - keyPassword, keyManagerFactory, sessionCacheSize, sessionTimeout), true, + keyPassword, keyManagerFactory, sessionCacheSize, sessionTimeout, KeyStore.getDefaultType()), true, ciphers, cipherFilter, apn, ClientAuth.NONE, null, false); } @@ -265,10 +266,12 @@ public final class JdkSslClientContext extends JdkSslContext { X509Certificate[] trustCertCollection, TrustManagerFactory trustManagerFactory, X509Certificate[] keyCertChain, PrivateKey key, String keyPassword, KeyManagerFactory keyManagerFactory, Iterable<String> ciphers, CipherSuiteFilter cipherFilter, - ApplicationProtocolConfig apn, String[] protocols, long sessionCacheSize, long sessionTimeout) + ApplicationProtocolConfig apn, String[] protocols, long sessionCacheSize, long sessionTimeout, + String keyStoreType) throws SSLException { super(newSSLContext(sslContextProvider, trustCertCollection, trustManagerFactory, - keyCertChain, key, keyPassword, keyManagerFactory, sessionCacheSize, sessionTimeout), + keyCertChain, key, keyPassword, keyManagerFactory, sessionCacheSize, + sessionTimeout, keyStoreType), true, ciphers, cipherFilter, toNegotiator(apn, false), ClientAuth.NONE, protocols, false); } @@ -276,13 +279,14 @@ public final class JdkSslClientContext extends JdkSslContext { X509Certificate[] trustCertCollection, TrustManagerFactory trustManagerFactory, X509Certificate[] keyCertChain, PrivateKey key, String keyPassword, KeyManagerFactory keyManagerFactory, - long sessionCacheSize, long sessionTimeout) throws SSLException { + long sessionCacheSize, long sessionTimeout, + String keyStore) throws SSLException { try { if (trustCertCollection != null) { - trustManagerFactory = buildTrustManagerFactory(trustCertCollection, trustManagerFactory); + trustManagerFactory = buildTrustManagerFactory(trustCertCollection, trustManagerFactory, keyStore); } if (keyCertChain != null) { - keyManagerFactory = buildKeyManagerFactory(keyCertChain, key, keyPassword, keyManagerFactory); + keyManagerFactory = buildKeyManagerFactory(keyCertChain, key, keyPassword, keyManagerFactory, keyStore); } SSLContext ctx = sslContextProvider == null ? SSLContext.getInstance(PROTOCOL) : SSLContext.getInstance(PROTOCOL, sslContextProvider); diff --git a/handler/src/main/java/io/netty/handler/ssl/JdkSslContext.java b/handler/src/main/java/io/netty/handler/ssl/JdkSslContext.java index d74bbde..0e9a127 100644 --- a/handler/src/main/java/io/netty/handler/ssl/JdkSslContext.java +++ b/handler/src/main/java/io/netty/handler/ssl/JdkSslContext.java @@ -25,6 +25,7 @@ import java.io.File; import java.io.IOException; import java.security.InvalidAlgorithmParameterException; import java.security.KeyException; +import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.Provider; @@ -78,7 +79,7 @@ public class JdkSslContext extends SslContext { DEFAULT_PROVIDER = context.getProvider(); SSLEngine engine = context.createSSLEngine(); - DEFAULT_PROTOCOLS = defaultProtocols(engine); + DEFAULT_PROTOCOLS = defaultProtocols(context, engine); SUPPORTED_CIPHERS = Collections.unmodifiableSet(supportedCiphers(engine)); DEFAULT_CIPHERS = Collections.unmodifiableList(defaultCiphers(engine, SUPPORTED_CIPHERS)); @@ -97,13 +98,11 @@ public class JdkSslContext extends SslContext { } } - private static String[] defaultProtocols(SSLEngine engine) { - // Choose the sensible default list of protocols. - final String[] supportedProtocols = engine.getSupportedProtocols(); + private static String[] defaultProtocols(SSLContext context, SSLEngine engine) { + // Choose the sensible default list of protocols that respects JDK flags, eg. jdk.tls.client.protocols + final String[] supportedProtocols = context.getDefaultSSLParameters().getProtocols(); Set<String> supportedProtocolsSet = new HashSet<String>(supportedProtocols.length); - for (int i = 0; i < supportedProtocols.length; ++i) { - supportedProtocolsSet.add(supportedProtocols[i]); - } + Collections.addAll(supportedProtocolsSet, supportedProtocols); List<String> protocols = new ArrayList<String>(); addIfSupported( supportedProtocolsSet, protocols, @@ -262,7 +261,7 @@ public class JdkSslContext extends SslContext { SSLEngine engine = sslContext.createSSLEngine(); try { if (protocols == null) { - this.protocols = defaultProtocols(engine); + this.protocols = defaultProtocols(sslContext, engine); } else { this.protocols = protocols; } @@ -435,17 +434,16 @@ public class JdkSslContext extends SslContext { /** * Build a {@link KeyManagerFactory} based upon a key file, key file password, and a certificate chain. - * @param certChainFile a X.509 certificate chain file in PEM format + * @param certChainFile an X.509 certificate chain file in PEM format * @param keyFile a PKCS#8 private key file in PEM format * @param keyPassword the password of the {@code keyFile}. * {@code null} if it's not password-protected. * @param kmf The existing {@link KeyManagerFactory} that will be used if not {@code null} + * @param keyStore the {@link KeyStore} that should be used in the {@link KeyManagerFactory} * @return A {@link KeyManagerFactory} based upon a key file, key file password, and a certificate chain. - * @deprecated will be removed. */ - @Deprecated - protected static KeyManagerFactory buildKeyManagerFactory(File certChainFile, File keyFile, String keyPassword, - KeyManagerFactory kmf) + static KeyManagerFactory buildKeyManagerFactory(File certChainFile, File keyFile, String keyPassword, + KeyManagerFactory kmf, String keyStore) throws UnrecoverableKeyException, KeyStoreException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeySpecException, InvalidAlgorithmParameterException, CertificateException, KeyException, IOException { @@ -453,30 +451,74 @@ public class JdkSslContext extends SslContext { if (algorithm == null) { algorithm = "SunX509"; } - return buildKeyManagerFactory(certChainFile, algorithm, keyFile, keyPassword, kmf); + return buildKeyManagerFactory(certChainFile, algorithm, keyFile, keyPassword, kmf, keyStore); + } + + /** + * Build a {@link KeyManagerFactory} based upon a key file, key file password, and a certificate chain. + * @param certChainFile an X.509 certificate chain file in PEM format + * @param keyFile a PKCS#8 private key file in PEM format + * @param keyPassword the password of the {@code keyFile}. + * {@code null} if it's not password-protected. + * @param kmf The existing {@link KeyManagerFactory} that will be used if not {@code null} + * @return A {@link KeyManagerFactory} based upon a key file, key file password, and a certificate chain. + * @deprecated will be removed. + */ + @Deprecated + protected static KeyManagerFactory buildKeyManagerFactory(File certChainFile, File keyFile, String keyPassword, + KeyManagerFactory kmf) + throws UnrecoverableKeyException, KeyStoreException, NoSuchAlgorithmException, + NoSuchPaddingException, InvalidKeySpecException, InvalidAlgorithmParameterException, + CertificateException, KeyException, IOException { + return buildKeyManagerFactory(certChainFile, keyFile, keyPassword, kmf, KeyStore.getDefaultType()); } /** * Build a {@link KeyManagerFactory} based upon a key algorithm, key file, key file password, * and a certificate chain. - * @param certChainFile a X.509 certificate chain file in PEM format + * @param certChainFile an X.509 certificate chain file in PEM format * @param keyAlgorithm the standard name of the requested algorithm. See the Java Secure Socket Extension - * Reference Guide for information about standard algorithm names. + * Reference Guide for information about standard algorithm names. * @param keyFile a PKCS#8 private key file in PEM format * @param keyPassword the password of the {@code keyFile}. * {@code null} if it's not password-protected. * @param kmf The existing {@link KeyManagerFactory} that will be used if not {@code null} + * @param keyStore the {@link KeyStore} that should be used in the {@link KeyManagerFactory} * @return A {@link KeyManagerFactory} based upon a key algorithm, key file, key file password, * and a certificate chain. - * @deprecated will be removed. */ - @Deprecated - protected static KeyManagerFactory buildKeyManagerFactory(File certChainFile, - String keyAlgorithm, File keyFile, String keyPassword, KeyManagerFactory kmf) + static KeyManagerFactory buildKeyManagerFactory(File certChainFile, + String keyAlgorithm, File keyFile, String keyPassword, KeyManagerFactory kmf, + String keyStore) throws KeyStoreException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeySpecException, InvalidAlgorithmParameterException, IOException, CertificateException, KeyException, UnrecoverableKeyException { return buildKeyManagerFactory(toX509Certificates(certChainFile), keyAlgorithm, - toPrivateKey(keyFile, keyPassword), keyPassword, kmf); + toPrivateKey(keyFile, keyPassword), keyPassword, kmf, keyStore); + } + + /** + * Build a {@link KeyManagerFactory} based upon a key algorithm, key file, key file password, + * and a certificate chain. + * @param certChainFile an buildKeyManagerFactory X.509 certificate chain file in PEM format + * @param keyAlgorithm the standard name of the requested algorithm. See the Java Secure Socket Extension + * Reference Guide for information about standard algorithm names. + * @param keyFile a PKCS#8 private key file in PEM format + * @param keyPassword the password of the {@code keyFile}. + * {@code null} if it's not password-protected. + * @param kmf The existing {@link KeyManagerFactory} that will be used if not {@code null} + * @return A {@link KeyManagerFactory} based upon a key algorithm, key file, key file password, + * and a certificate chain. + * @deprecated will be removed. + */ + @Deprecated + protected static KeyManagerFactory buildKeyManagerFactory(File certChainFile, + String keyAlgorithm, File keyFile, + String keyPassword, KeyManagerFactory kmf) + throws KeyStoreException, NoSuchAlgorithmException, NoSuchPaddingException, + InvalidKeySpecException, InvalidAlgorithmParameterException, IOException, + CertificateException, KeyException, UnrecoverableKeyException { + return buildKeyManagerFactory(toX509Certificates(certChainFile), keyAlgorithm, + toPrivateKey(keyFile, keyPassword), keyPassword, kmf, KeyStore.getDefaultType()); } } diff --git a/handler/src/main/java/io/netty/handler/ssl/JdkSslEngine.java b/handler/src/main/java/io/netty/handler/ssl/JdkSslEngine.java index 3304e38..80e8366 100644 --- a/handler/src/main/java/io/netty/handler/ssl/JdkSslEngine.java +++ b/handler/src/main/java/io/netty/handler/ssl/JdkSslEngine.java @@ -15,6 +15,8 @@ */ package io.netty.handler.ssl; +import io.netty.util.internal.SuppressJava6Requirement; + import java.nio.ByteBuffer; import javax.net.ssl.SSLEngine; @@ -145,6 +147,7 @@ class JdkSslEngine extends SSLEngine implements ApplicationProtocolAccessor { engine.setEnabledProtocols(strings); } + @SuppressJava6Requirement(reason = "Can only be called when running on JDK7+") @Override public SSLSession getHandshakeSession() { return engine.getHandshakeSession(); diff --git a/handler/src/main/java/io/netty/handler/ssl/JdkSslServerContext.java b/handler/src/main/java/io/netty/handler/ssl/JdkSslServerContext.java index 2dcbbc0..141126a 100644 --- a/handler/src/main/java/io/netty/handler/ssl/JdkSslServerContext.java +++ b/handler/src/main/java/io/netty/handler/ssl/JdkSslServerContext.java @@ -16,6 +16,7 @@ package io.netty.handler.ssl; +import java.security.KeyStore; import java.security.Provider; import javax.net.ssl.KeyManager; @@ -47,7 +48,8 @@ public final class JdkSslServerContext extends JdkSslContext { */ @Deprecated public JdkSslServerContext(File certChainFile, File keyFile) throws SSLException { - this(certChainFile, keyFile, null); + this(null, certChainFile, keyFile, null, null, IdentityCipherSuiteFilter.INSTANCE, + JdkDefaultApplicationProtocolNegotiator.INSTANCE, 0, 0, null); } /** @@ -87,8 +89,9 @@ public final class JdkSslServerContext extends JdkSslContext { File certChainFile, File keyFile, String keyPassword, Iterable<String> ciphers, Iterable<String> nextProtocols, long sessionCacheSize, long sessionTimeout) throws SSLException { - this(certChainFile, keyFile, keyPassword, ciphers, IdentityCipherSuiteFilter.INSTANCE, - toNegotiator(toApplicationProtocolConfig(nextProtocols), true), sessionCacheSize, sessionTimeout); + this(null, certChainFile, keyFile, keyPassword, ciphers, IdentityCipherSuiteFilter.INSTANCE, + toNegotiator(toApplicationProtocolConfig(nextProtocols), true), sessionCacheSize, + sessionTimeout, KeyStore.getDefaultType()); } /** @@ -113,8 +116,8 @@ public final class JdkSslServerContext extends JdkSslContext { File certChainFile, File keyFile, String keyPassword, Iterable<String> ciphers, CipherSuiteFilter cipherFilter, ApplicationProtocolConfig apn, long sessionCacheSize, long sessionTimeout) throws SSLException { - this(certChainFile, keyFile, keyPassword, ciphers, cipherFilter, - toNegotiator(apn, true), sessionCacheSize, sessionTimeout); + this(null, certChainFile, keyFile, keyPassword, ciphers, cipherFilter, + toNegotiator(apn, true), sessionCacheSize, sessionTimeout, KeyStore.getDefaultType()); } /** @@ -139,17 +142,18 @@ public final class JdkSslServerContext extends JdkSslContext { File certChainFile, File keyFile, String keyPassword, Iterable<String> ciphers, CipherSuiteFilter cipherFilter, JdkApplicationProtocolNegotiator apn, long sessionCacheSize, long sessionTimeout) throws SSLException { - this(null, certChainFile, keyFile, keyPassword, ciphers, cipherFilter, apn, sessionCacheSize, sessionTimeout); + this(null, certChainFile, keyFile, keyPassword, ciphers, cipherFilter, apn, + sessionCacheSize, sessionTimeout, KeyStore.getDefaultType()); } JdkSslServerContext(Provider provider, - File certChainFile, File keyFile, String keyPassword, - Iterable<String> ciphers, CipherSuiteFilter cipherFilter, JdkApplicationProtocolNegotiator apn, - long sessionCacheSize, long sessionTimeout) throws SSLException { + File certChainFile, File keyFile, String keyPassword, + Iterable<String> ciphers, CipherSuiteFilter cipherFilter, JdkApplicationProtocolNegotiator apn, + long sessionCacheSize, long sessionTimeout, String keyStore) throws SSLException { super(newSSLContext(provider, null, null, - toX509CertificatesInternal(certChainFile), toPrivateKeyInternal(keyFile, keyPassword), - keyPassword, null, sessionCacheSize, sessionTimeout), false, - ciphers, cipherFilter, apn, ClientAuth.NONE, null, false); + toX509CertificatesInternal(certChainFile), toPrivateKeyInternal(keyFile, keyPassword), + keyPassword, null, sessionCacheSize, sessionTimeout, keyStore), false, + ciphers, cipherFilter, apn, ClientAuth.NONE, null, false); } /** @@ -182,11 +186,14 @@ public final class JdkSslServerContext extends JdkSslContext { */ @Deprecated public JdkSslServerContext(File trustCertCollectionFile, TrustManagerFactory trustManagerFactory, - File keyCertChainFile, File keyFile, String keyPassword, KeyManagerFactory keyManagerFactory, - Iterable<String> ciphers, CipherSuiteFilter cipherFilter, ApplicationProtocolConfig apn, - long sessionCacheSize, long sessionTimeout) throws SSLException { - this(trustCertCollectionFile, trustManagerFactory, keyCertChainFile, keyFile, keyPassword, keyManagerFactory, - ciphers, cipherFilter, toNegotiator(apn, true), sessionCacheSize, sessionTimeout); + File keyCertChainFile, File keyFile, String keyPassword, + KeyManagerFactory keyManagerFactory, + Iterable<String> ciphers, CipherSuiteFilter cipherFilter, ApplicationProtocolConfig apn, + long sessionCacheSize, long sessionTimeout) throws SSLException { + super(newSSLContext(null, toX509CertificatesInternal(trustCertCollectionFile), trustManagerFactory, + toX509CertificatesInternal(keyCertChainFile), toPrivateKeyInternal(keyFile, keyPassword), + keyPassword, keyManagerFactory, sessionCacheSize, sessionTimeout, null), false, + ciphers, cipherFilter, apn, ClientAuth.NONE, null, false); } /** @@ -214,17 +221,19 @@ public final class JdkSslServerContext extends JdkSslContext { * @param sessionCacheSize the size of the cache used for storing SSL session objects. * {@code 0} to use the default value. * @param sessionTimeout the timeout for the cached SSL session objects, in seconds. - * {@code 0} to use the default value. + * {@code 0} to use the default value * @deprecated use {@link SslContextBuilder} */ @Deprecated public JdkSslServerContext(File trustCertCollectionFile, TrustManagerFactory trustManagerFactory, - File keyCertChainFile, File keyFile, String keyPassword, KeyManagerFactory keyManagerFactory, - Iterable<String> ciphers, CipherSuiteFilter cipherFilter, JdkApplicationProtocolNegotiator apn, - long sessionCacheSize, long sessionTimeout) throws SSLException { + File keyCertChainFile, File keyFile, String keyPassword, + KeyManagerFactory keyManagerFactory, + Iterable<String> ciphers, CipherSuiteFilter cipherFilter, + JdkApplicationProtocolNegotiator apn, + long sessionCacheSize, long sessionTimeout) throws SSLException { super(newSSLContext(null, toX509CertificatesInternal(trustCertCollectionFile), trustManagerFactory, toX509CertificatesInternal(keyCertChainFile), toPrivateKeyInternal(keyFile, keyPassword), - keyPassword, keyManagerFactory, sessionCacheSize, sessionTimeout), false, + keyPassword, keyManagerFactory, sessionCacheSize, sessionTimeout, KeyStore.getDefaultType()), false, ciphers, cipherFilter, apn, ClientAuth.NONE, null, false); } @@ -233,16 +242,17 @@ public final class JdkSslServerContext extends JdkSslContext { X509Certificate[] keyCertChain, PrivateKey key, String keyPassword, KeyManagerFactory keyManagerFactory, Iterable<String> ciphers, CipherSuiteFilter cipherFilter, ApplicationProtocolConfig apn, long sessionCacheSize, long sessionTimeout, - ClientAuth clientAuth, String[] protocols, boolean startTls) throws SSLException { + ClientAuth clientAuth, String[] protocols, boolean startTls, + String keyStore) throws SSLException { super(newSSLContext(provider, trustCertCollection, trustManagerFactory, keyCertChain, key, - keyPassword, keyManagerFactory, sessionCacheSize, sessionTimeout), false, + keyPassword, keyManagerFactory, sessionCacheSize, sessionTimeout, keyStore), false, ciphers, cipherFilter, toNegotiator(apn, true), clientAuth, protocols, startTls); } private static SSLContext newSSLContext(Provider sslContextProvider, X509Certificate[] trustCertCollection, TrustManagerFactory trustManagerFactory, X509Certificate[] keyCertChain, PrivateKey key, String keyPassword, KeyManagerFactory keyManagerFactory, - long sessionCacheSize, long sessionTimeout) + long sessionCacheSize, long sessionTimeout, String keyStore) throws SSLException { if (key == null && keyManagerFactory == null) { throw new NullPointerException("key, keyManagerFactory"); @@ -250,10 +260,10 @@ public final class JdkSslServerContext extends JdkSslContext { try { if (trustCertCollection != null) { - trustManagerFactory = buildTrustManagerFactory(trustCertCollection, trustManagerFactory); + trustManagerFactory = buildTrustManagerFactory(trustCertCollection, trustManagerFactory, keyStore); } if (key != null) { - keyManagerFactory = buildKeyManagerFactory(keyCertChain, key, keyPassword, keyManagerFactory); + keyManagerFactory = buildKeyManagerFactory(keyCertChain, key, keyPassword, keyManagerFactory, null); } // Initialize the SSLContext to work with our key managers. diff --git a/handler/src/main/java/io/netty/handler/ssl/KeyManagerFactoryWrapper.java b/handler/src/main/java/io/netty/handler/ssl/KeyManagerFactoryWrapper.java new file mode 100644 index 0000000..715b780 --- /dev/null +++ b/handler/src/main/java/io/netty/handler/ssl/KeyManagerFactoryWrapper.java @@ -0,0 +1,44 @@ +/* + * Copyright 2019 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.handler.ssl.util.SimpleKeyManagerFactory; +import io.netty.util.internal.ObjectUtil; + +import java.security.KeyStore; +import javax.net.ssl.KeyManager; +import javax.net.ssl.ManagerFactoryParameters; + +final class KeyManagerFactoryWrapper extends SimpleKeyManagerFactory { + private final KeyManager km; + + KeyManagerFactoryWrapper(KeyManager km) { + this.km = ObjectUtil.checkNotNull(km, "km"); + } + + @Override + protected void engineInit(KeyStore keyStore, char[] var2) throws Exception { } + + @Override + protected void engineInit(ManagerFactoryParameters managerFactoryParameters) + throws Exception { } + + @Override + protected KeyManager[] engineGetKeyManagers() { + return new KeyManager[] {km}; + } +} diff --git a/handler/src/main/java/io/netty/handler/ssl/OpenSsl.java b/handler/src/main/java/io/netty/handler/ssl/OpenSsl.java index 78a528f..de38c6f 100644 --- a/handler/src/main/java/io/netty/handler/ssl/OpenSsl.java +++ b/handler/src/main/java/io/netty/handler/ssl/OpenSsl.java @@ -18,10 +18,12 @@ package io.netty.handler.ssl; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.UnpooledByteBufAllocator; import io.netty.internal.tcnative.Buffer; import io.netty.internal.tcnative.Library; import io.netty.internal.tcnative.SSL; import io.netty.internal.tcnative.SSLContext; +import io.netty.util.CharsetUtil; import io.netty.util.ReferenceCountUtil; import io.netty.util.ReferenceCounted; import io.netty.util.internal.NativeLibraryLoader; @@ -31,10 +33,7 @@ import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; import java.io.ByteArrayInputStream; -import java.security.AccessController; -import java.security.PrivilegedAction; import java.security.cert.CertificateException; -import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Collections; @@ -45,7 +44,7 @@ import java.util.Set; import static io.netty.handler.ssl.SslUtils.*; /** - * Tells if <a href="http://netty.io/wiki/forked-tomcat-native.html">{@code netty-tcnative}</a> and its OpenSSL support + * Tells if <a href="https://netty.io/wiki/forked-tomcat-native.html">{@code netty-tcnative}</a> and its OpenSSL support * are available. */ public final class OpenSsl { @@ -58,13 +57,54 @@ public final class OpenSsl { private static final Set<String> AVAILABLE_OPENSSL_CIPHER_SUITES; private static final Set<String> AVAILABLE_JAVA_CIPHER_SUITES; private static final boolean SUPPORTS_KEYMANAGER_FACTORY; - private static final boolean SUPPORTS_HOSTNAME_VALIDATION; private static final boolean USE_KEYMANAGER_FACTORY; private static final boolean SUPPORTS_OCSP; private static final boolean TLSV13_SUPPORTED; private static final boolean IS_BORINGSSL; static final Set<String> SUPPORTED_PROTOCOLS_SET; + // self-signed certificate for netty.io and the matching private-key + private static final String CERT = "-----BEGIN CERTIFICATE-----\n" + + "MIICrjCCAZagAwIBAgIIdSvQPv1QAZQwDQYJKoZIhvcNAQELBQAwFjEUMBIGA1UEAxMLZXhhbXBs\n" + + "ZS5jb20wIBcNMTgwNDA2MjIwNjU5WhgPOTk5OTEyMzEyMzU5NTlaMBYxFDASBgNVBAMTC2V4YW1w\n" + + "bGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAggbWsmDQ6zNzRZ5AW8E3eoGl\n" + + "qWvOBDb5Fs1oBRrVQHuYmVAoaqwDzXYJ0LOwa293AgWEQ1jpcbZ2hpoYQzqEZBTLnFhMrhRFlH6K\n" + + "bJND8Y33kZ/iSVBBDuGbdSbJShlM+4WwQ9IAso4MZ4vW3S1iv5fGGpLgbtXRmBf/RU8omN0Gijlv\n" + + "WlLWHWijLN8xQtySFuBQ7ssW8RcKAary3pUm6UUQB+Co6lnfti0Tzag8PgjhAJq2Z3wbsGRnP2YS\n" + + "vYoaK6qzmHXRYlp/PxrjBAZAmkLJs4YTm/XFF+fkeYx4i9zqHbyone5yerRibsHaXZWLnUL+rFoe\n" + + "MdKvr0VS3sGmhQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQADQi441pKmXf9FvUV5EHU4v8nJT9Iq\n" + + "yqwsKwXnr7AsUlDGHBD7jGrjAXnG5rGxuNKBQ35wRxJATKrUtyaquFUL6H8O6aGQehiFTk6zmPbe\n" + + "12Gu44vqqTgIUxnv3JQJiox8S2hMxsSddpeCmSdvmalvD6WG4NthH6B9ZaBEiep1+0s0RUaBYn73\n" + + "I7CCUaAtbjfR6pcJjrFk5ei7uwdQZFSJtkP2z8r7zfeANJddAKFlkaMWn7u+OIVuB4XPooWicObk\n" + + "NAHFtP65bocUYnDpTVdiyvn8DdqyZ/EO8n1bBKBzuSLplk2msW4pdgaFgY7Vw/0wzcFXfUXmL1uy\n" + + "G8sQD/wx\n" + + "-----END CERTIFICATE-----"; + + private static final String KEY = "-----BEGIN PRIVATE KEY-----\n" + + "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCCBtayYNDrM3NFnkBbwTd6gaWp\n" + + "a84ENvkWzWgFGtVAe5iZUChqrAPNdgnQs7Brb3cCBYRDWOlxtnaGmhhDOoRkFMucWEyuFEWUfops\n" + + "k0PxjfeRn+JJUEEO4Zt1JslKGUz7hbBD0gCyjgxni9bdLWK/l8YakuBu1dGYF/9FTyiY3QaKOW9a\n" + + "UtYdaKMs3zFC3JIW4FDuyxbxFwoBqvLelSbpRRAH4KjqWd+2LRPNqDw+COEAmrZnfBuwZGc/ZhK9\n" + + "ihorqrOYddFiWn8/GuMEBkCaQsmzhhOb9cUX5+R5jHiL3OodvKid7nJ6tGJuwdpdlYudQv6sWh4x\n" + + "0q+vRVLewaaFAgMBAAECggEAP8tPJvFtTxhNJAkCloHz0D0vpDHqQBMgntlkgayqmBqLwhyb18pR\n" + + "i0qwgh7HHc7wWqOOQuSqlEnrWRrdcI6TSe8R/sErzfTQNoznKWIPYcI/hskk4sdnQ//Yn9/Jvnsv\n" + + "U/BBjOTJxtD+sQbhAl80JcA3R+5sArURQkfzzHOL/YMqzAsn5hTzp7HZCxUqBk3KaHRxV7NefeOE\n" + + "xlZuWSmxYWfbFIs4kx19/1t7h8CHQWezw+G60G2VBtSBBxDnhBWvqG6R/wpzJ3nEhPLLY9T+XIHe\n" + + "ipzdMOOOUZorfIg7M+pyYPji+ZIZxIpY5OjrOzXHciAjRtr5Y7l99K1CG1LguQKBgQDrQfIMxxtZ\n" + + "vxU/1cRmUV9l7pt5bjV5R6byXq178LxPKVYNjdZ840Q0/OpZEVqaT1xKVi35ohP1QfNjxPLlHD+K\n" + + "iDAR9z6zkwjIrbwPCnb5kuXy4lpwPcmmmkva25fI7qlpHtbcuQdoBdCfr/KkKaUCMPyY89LCXgEw\n" + + "5KTDj64UywKBgQCNfbO+eZLGzhiHhtNJurresCsIGWlInv322gL8CSfBMYl6eNfUTZvUDdFhPISL\n" + + "UljKWzXDrjw0ujFSPR0XhUGtiq89H+HUTuPPYv25gVXO+HTgBFZEPl4PpA+BUsSVZy0NddneyqLk\n" + + "42Wey9omY9Q8WsdNQS5cbUvy0uG6WFoX7wKBgQDZ1jpW8pa0x2bZsQsm4vo+3G5CRnZlUp+XlWt2\n" + + "dDcp5dC0xD1zbs1dc0NcLeGDOTDv9FSl7hok42iHXXq8AygjEm/QcuwwQ1nC2HxmQP5holAiUs4D\n" + + "WHM8PWs3wFYPzE459EBoKTxeaeP/uWAn+he8q7d5uWvSZlEcANs/6e77eQKBgD21Ar0hfFfj7mK8\n" + + "9E0FeRZBsqK3omkfnhcYgZC11Xa2SgT1yvs2Va2n0RcdM5kncr3eBZav2GYOhhAdwyBM55XuE/sO\n" + + "eokDVutNeuZ6d5fqV96TRaRBpvgfTvvRwxZ9hvKF4Vz+9wfn/JvCwANaKmegF6ejs7pvmF3whq2k\n" + + "drZVAoGAX5YxQ5XMTD0QbMAl7/6qp6S58xNoVdfCkmkj1ZLKaHKIjS/benkKGlySVQVPexPfnkZx\n" + + "p/Vv9yyphBoudiTBS9Uog66ueLYZqpgxlM/6OhYg86Gm3U2ycvMxYjBM1NFiyze21AqAhI+HX+Ot\n" + + "mraV2/guSgDgZAhukRZzeQ2RucI=\n" + + "-----END PRIVATE KEY-----"; + static { Throwable cause = null; @@ -97,7 +137,7 @@ public final class OpenSsl { "Failed to load netty-tcnative; " + OpenSslEngine.class.getSimpleName() + " will be unavailable, unless the " + "application has already loaded the symbols by some other means. " + - "See http://netty.io/wiki/forked-tomcat-native.html for more information.", t); + "See https://netty.io/wiki/forked-tomcat-native.html for more information.", t); } try { @@ -120,7 +160,7 @@ public final class OpenSsl { logger.debug( "Failed to initialize netty-tcnative; " + OpenSslEngine.class.getSimpleName() + " will be unavailable. " + - "See http://netty.io/wiki/forked-tomcat-native.html for more information.", t); + "See https://netty.io/wiki/forked-tomcat-native.html for more information.", t); } } } @@ -134,7 +174,6 @@ public final class OpenSsl { final Set<String> availableOpenSslCipherSuites = new LinkedHashSet<String>(128); boolean supportsKeyManagerFactory = false; boolean useKeyManagerFactory = false; - boolean supportsHostNameValidation = false; boolean tlsv13Supported = false; IS_BORINGSSL = "BoringSSL".equals(versionString()); @@ -142,6 +181,9 @@ public final class OpenSsl { try { final long sslCtx = SSLContext.make(SSL.SSL_PROTOCOL_ALL, SSL.SSL_MODE_SERVER); long certBio = 0; + long keyBio = 0; + long cert = 0; + long key = 0; try { try { StringBuilder tlsv13Ciphers = new StringBuilder(); @@ -188,36 +230,65 @@ public final class OpenSsl { "AEAD-AES256-GCM-SHA384", "AEAD-CHACHA20-POLY1305-SHA256"); } + + PemEncoded privateKey = PemPrivateKey.valueOf(KEY.getBytes(CharsetUtil.US_ASCII)); try { - SSL.setHostNameValidation(ssl, 0, "netty.io"); - supportsHostNameValidation = true; - } catch (Throwable ignore) { - logger.debug("Hostname Verification not supported."); - } - try { + // Let's check if we can set a callback, which may not work if the used OpenSSL version + // is to old. + SSLContext.setCertificateCallback(sslCtx, null); + X509Certificate certificate = selfSignedCertificate(); certBio = ReferenceCountedOpenSslContext.toBIO(ByteBufAllocator.DEFAULT, certificate); - SSL.setCertificateChainBio(ssl, certBio, false); + cert = SSL.parseX509Chain(certBio); + + keyBio = ReferenceCountedOpenSslContext.toBIO( + UnpooledByteBufAllocator.DEFAULT, privateKey.retain()); + key = SSL.parsePrivateKey(keyBio, null); + + SSL.setKeyMaterial(ssl, cert, key); supportsKeyManagerFactory = true; try { - useKeyManagerFactory = AccessController.doPrivileged(new PrivilegedAction<Boolean>() { - @Override - public Boolean run() { - return SystemPropertyUtil.getBoolean( - "io.netty.handler.ssl.openssl.useKeyManagerFactory", true); + boolean propertySet = SystemPropertyUtil.contains( + "io.netty.handler.ssl.openssl.useKeyManagerFactory"); + if (!IS_BORINGSSL) { + useKeyManagerFactory = SystemPropertyUtil.getBoolean( + "io.netty.handler.ssl.openssl.useKeyManagerFactory", true); + + if (propertySet) { + logger.info("System property " + + "'io.netty.handler.ssl.openssl.useKeyManagerFactory'" + + " is deprecated and so will be ignored in the future"); } - }); + } else { + useKeyManagerFactory = true; + if (propertySet) { + logger.info("System property " + + "'io.netty.handler.ssl.openssl.useKeyManagerFactory'" + + " is deprecated and will be ignored when using BoringSSL"); + } + } } catch (Throwable ignore) { logger.debug("Failed to get useKeyManagerFactory system property."); } - } catch (Throwable ignore) { + } catch (Error ignore) { logger.debug("KeyManagerFactory not supported."); + } finally { + privateKey.release(); } } finally { SSL.freeSSL(ssl); if (certBio != 0) { SSL.freeBIO(certBio); } + if (keyBio != 0) { + SSL.freeBIO(keyBio); + } + if (cert != 0) { + SSL.freeX509Chain(cert); + } + if (key != 0) { + SSL.freePrivateKey(key); + } } } finally { SSLContext.free(sslCtx); @@ -254,7 +325,6 @@ public final class OpenSsl { AVAILABLE_CIPHER_SUITES = availableCipherSuites; SUPPORTS_KEYMANAGER_FACTORY = supportsKeyManagerFactory; - SUPPORTS_HOSTNAME_VALIDATION = supportsHostNameValidation; USE_KEYMANAGER_FACTORY = useKeyManagerFactory; Set<String> protocols = new LinkedHashSet<String>(6); @@ -297,7 +367,6 @@ public final class OpenSsl { AVAILABLE_JAVA_CIPHER_SUITES = Collections.emptySet(); AVAILABLE_CIPHER_SUITES = Collections.emptySet(); SUPPORTS_KEYMANAGER_FACTORY = false; - SUPPORTS_HOSTNAME_VALIDATION = false; USE_KEYMANAGER_FACTORY = false; SUPPORTED_PROTOCOLS_SET = Collections.emptySet(); SUPPORTS_OCSP = false; @@ -310,39 +379,9 @@ public final class OpenSsl { * Returns a self-signed {@link X509Certificate} for {@code netty.io}. */ static X509Certificate selfSignedCertificate() throws CertificateException { - // Bytes of self-signed certificate for netty.io - byte[] certBytes = { - 48, -126, 1, -92, 48, -126, 1, 13, -96, 3, 2, 1, 2, 2, 9, 0, -9, 61, - 44, 121, -118, -4, -45, -120, 48, 13, 6, 9, 42, -122, 72, -122, - -9, 13, 1, 1, 5, 5, 0, 48, 19, 49, 17, 48, 15, 6, 3, 85, 4, 3, 19, - 8, 110, 101, 116, 116, 121, 46, 105, 111, 48, 32, 23, 13, 49, 55, - 49, 48, 50, 48, 49, 56, 49, 54, 51, 54, 90, 24, 15, 57, 57, 57, 57, - 49, 50, 51, 49, 50, 51, 53, 57, 53, 57, 90, 48, 19, 49, 17, 48, 15, - 6, 3, 85, 4, 3, 19, 8, 110, 101, 116, 116, 121, 46, 105, 111, 48, -127, - -97, 48, 13, 6, 9, 42, -122, 72, -122, -9, 13, 1, 1, 1, 5, 0, 3, -127, - -115, 0, 48, -127, -119, 2, -127, -127, 0, -116, 37, 122, -53, 28, 46, - 13, -90, -14, -33, 111, -108, -41, 59, 90, 124, 113, -112, -66, -17, - -102, 44, 13, 7, -33, -28, 24, -79, -126, -76, 40, 111, -126, -103, - -102, 34, 11, 45, 16, -38, 63, 24, 80, 24, 76, 88, -93, 96, 11, 38, - -19, -64, -11, 87, -49, -52, -65, 24, 36, -22, 53, 8, -42, 14, -121, - 114, 6, 17, -82, 10, 92, -91, -127, 81, -12, -75, 105, -10, -106, 91, - -38, 111, 50, 57, -97, -125, 109, 42, -87, -1, -19, 80, 78, 49, -97, -4, - 23, -2, -103, 122, -107, -43, 4, -31, -21, 90, 39, -9, -106, 34, -101, - -116, 31, -94, -84, 80, -6, -78, -33, 87, -90, 31, 103, 100, 56, -103, - -5, 11, 2, 3, 1, 0, 1, 48, 13, 6, 9, 42, -122, 72, -122, -9, 13, 1, 1, - 5, 5, 0, 3, -127, -127, 0, 112, 45, -73, 5, 64, 49, 59, 101, 51, 73, - -96, 62, 23, -84, 90, -41, -58, 83, -20, -72, 38, 123, -108, -45, 28, - 96, -122, -18, 30, 42, 86, 87, -87, -28, 107, 110, 11, -59, 91, 100, - 101, -18, 26, -103, -78, -80, -3, 38, 113, 83, -48, -108, 109, 41, -15, - 6, 112, 105, 7, -46, -11, -3, -51, 40, -66, -73, -83, -46, -94, -121, - -88, 51, -106, -77, 109, 53, -7, 123, 91, 75, -105, -22, 64, 121, -72, - -59, -21, -44, 84, 12, 9, 120, 21, -26, 13, 49, -81, -58, -47, 117, - -44, -18, -17, 124, 49, -48, 19, 16, -41, 71, -52, -107, 99, -19, -29, - 105, -93, -71, -38, -97, -128, -2, 118, 119, 49, -126, 109, 119 }; - - CertificateFactory cf = CertificateFactory.getInstance("X.509"); - return (X509Certificate) cf.generateCertificate( - new ByteArrayInputStream(certBytes)); + return (X509Certificate) SslContext.X509_CERT_FACTORY.generateCertificate( + new ByteArrayInputStream(CERT.getBytes(CharsetUtil.US_ASCII)) + ); } private static boolean doesSupportOcsp() { @@ -383,7 +422,7 @@ public final class OpenSsl { /** * Returns {@code true} if and only if - * <a href="http://netty.io/wiki/forked-tomcat-native.html">{@code netty-tcnative}</a> and its OpenSSL support + * <a href="https://netty.io/wiki/forked-tomcat-native.html">{@code netty-tcnative}</a> and its OpenSSL support * are available. */ public static boolean isAvailable() { @@ -393,7 +432,10 @@ public final class OpenSsl { /** * Returns {@code true} if the used version of openssl supports * <a href="https://tools.ietf.org/html/rfc7301">ALPN</a>. + * + * @deprecated use {@link SslProvider#isAlpnSupported(SslProvider)} with {@link SslProvider#OPENSSL}. */ + @Deprecated public static boolean isAlpnSupported() { return version() >= 0x10002000L; } @@ -422,7 +464,7 @@ public final class OpenSsl { } /** - * Ensure that <a href="http://netty.io/wiki/forked-tomcat-native.html">{@code netty-tcnative}</a> and + * Ensure that <a href="https://netty.io/wiki/forked-tomcat-native.html">{@code netty-tcnative}</a> and * its OpenSSL support are available. * * @throws UnsatisfiedLinkError if unavailable @@ -436,7 +478,7 @@ public final class OpenSsl { /** * Returns the cause of unavailability of - * <a href="http://netty.io/wiki/forked-tomcat-native.html">{@code netty-tcnative}</a> and its OpenSSL support. + * <a href="https://netty.io/wiki/forked-tomcat-native.html">{@code netty-tcnative}</a> and its OpenSSL support. * * @return the cause if unavailable. {@code null} if available. */ @@ -488,11 +530,14 @@ public final class OpenSsl { } /** - * Returns {@code true} if <a href="https://wiki.openssl.org/index.php/Hostname_validation">Hostname Validation</a> - * is supported when using OpenSSL. + * Always returns {@code true} if {@link #isAvailable()} returns {@code true}. + * + * @deprecated Will be removed because hostname validation is always done by a + * {@link javax.net.ssl.TrustManager} implementation. */ + @Deprecated public static boolean supportsHostnameValidation() { - return SUPPORTS_HOSTNAME_VALIDATION; + return isAvailable(); } static boolean useKeyManagerFactory() { @@ -510,15 +555,25 @@ public final class OpenSsl { String os = PlatformDependent.normalizedOs(); String arch = PlatformDependent.normalizedArch(); - Set<String> libNames = new LinkedHashSet<String>(4); + Set<String> libNames = new LinkedHashSet<String>(5); String staticLibName = "netty_tcnative"; // First, try loading the platform-specific library. Platform-specific // libraries will be available if using a tcnative uber jar. - libNames.add(staticLibName + "_" + os + '_' + arch); if ("linux".equalsIgnoreCase(os)) { - // Fedora SSL lib so naming (libssl.so.10 vs libssl.so.1.0.0).. + Set<String> classifiers = PlatformDependent.normalizedLinuxClassifiers(); + for (String classifier : classifiers) { + libNames.add(staticLibName + "_" + os + '_' + arch + "_" + classifier); + } + // generic arch-dependent library + libNames.add(staticLibName + "_" + os + '_' + arch); + + // Fedora SSL lib so naming (libssl.so.10 vs libssl.so.1.0.0). + // note: should already be included from the classifiers but if not, we use this as an + // additional fallback option here libNames.add(staticLibName + "_" + os + '_' + arch + "_fedora"); + } else { + libNames.add(staticLibName + "_" + os + '_' + arch); } libNames.add(staticLibName + "_" + arch); libNames.add(staticLibName); diff --git a/handler/src/main/java/io/netty/handler/ssl/OpenSslCachingKeyMaterialProvider.java b/handler/src/main/java/io/netty/handler/ssl/OpenSslCachingKeyMaterialProvider.java index db8779b..07b67d9 100644 --- a/handler/src/main/java/io/netty/handler/ssl/OpenSslCachingKeyMaterialProvider.java +++ b/handler/src/main/java/io/netty/handler/ssl/OpenSslCachingKeyMaterialProvider.java @@ -28,10 +28,13 @@ import java.util.concurrent.ConcurrentMap; */ final class OpenSslCachingKeyMaterialProvider extends OpenSslKeyMaterialProvider { + private final int maxCachedEntries; + private volatile boolean full; private final ConcurrentMap<String, OpenSslKeyMaterial> cache = new ConcurrentHashMap<String, OpenSslKeyMaterial>(); - OpenSslCachingKeyMaterialProvider(X509KeyManager keyManager, String password) { + OpenSslCachingKeyMaterialProvider(X509KeyManager keyManager, String password, int maxCachedEntries) { super(keyManager, password); + this.maxCachedEntries = maxCachedEntries; } @Override @@ -44,6 +47,14 @@ final class OpenSslCachingKeyMaterialProvider extends OpenSslKeyMaterialProvider return null; } + if (full) { + return material; + } + if (cache.size() > maxCachedEntries) { + full = true; + // Do not cache... + return material; + } OpenSslKeyMaterial old = cache.putIfAbsent(alias, material); if (old != null) { material.release(); diff --git a/handler/src/main/java/io/netty/handler/ssl/OpenSslCachingX509KeyManagerFactory.java b/handler/src/main/java/io/netty/handler/ssl/OpenSslCachingX509KeyManagerFactory.java index 6581d9b..7f67bc8 100644 --- a/handler/src/main/java/io/netty/handler/ssl/OpenSslCachingX509KeyManagerFactory.java +++ b/handler/src/main/java/io/netty/handler/ssl/OpenSslCachingX509KeyManagerFactory.java @@ -15,10 +15,13 @@ */ package io.netty.handler.ssl; +import io.netty.util.internal.ObjectUtil; + import javax.net.ssl.KeyManager; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.KeyManagerFactorySpi; import javax.net.ssl.ManagerFactoryParameters; +import javax.net.ssl.X509ExtendedKeyManager; import javax.net.ssl.X509KeyManager; import java.security.InvalidAlgorithmParameterException; import java.security.KeyStore; @@ -37,7 +40,13 @@ import java.security.cert.X509Certificate; */ public final class OpenSslCachingX509KeyManagerFactory extends KeyManagerFactory { + private final int maxCachedEntries; + public OpenSslCachingX509KeyManagerFactory(final KeyManagerFactory factory) { + this(factory, 1024); + } + + public OpenSslCachingX509KeyManagerFactory(final KeyManagerFactory factory, int maxCachedEntries) { super(new KeyManagerFactorySpi() { @Override protected void engineInit(KeyStore keyStore, char[] chars) @@ -56,5 +65,17 @@ public final class OpenSslCachingX509KeyManagerFactory extends KeyManagerFactory return factory.getKeyManagers(); } }, factory.getProvider(), factory.getAlgorithm()); + this.maxCachedEntries = ObjectUtil.checkPositive(maxCachedEntries, "maxCachedEntries"); + } + + OpenSslKeyMaterialProvider newProvider(String password) { + X509KeyManager keyManager = ReferenceCountedOpenSslContext.chooseX509KeyManager(getKeyManagers()); + if ("sun.security.ssl.X509KeyManagerImpl".equals(keyManager.getClass().getName())) { + // Don't do caching if X509KeyManagerImpl is used as the returned aliases are not stable and will change + // between invocations. + return new OpenSslKeyMaterialProvider(keyManager, password); + } + return new OpenSslCachingKeyMaterialProvider( + ReferenceCountedOpenSslContext.chooseX509KeyManager(getKeyManagers()), password, maxCachedEntries); } } diff --git a/handler/src/main/java/io/netty/handler/ssl/OpenSslClientContext.java b/handler/src/main/java/io/netty/handler/ssl/OpenSslClientContext.java index 6856c7f..7f9b39a 100644 --- a/handler/src/main/java/io/netty/handler/ssl/OpenSslClientContext.java +++ b/handler/src/main/java/io/netty/handler/ssl/OpenSslClientContext.java @@ -18,6 +18,7 @@ package io.netty.handler.ssl; import io.netty.internal.tcnative.SSL; import java.io.File; +import java.security.KeyStore; import java.security.PrivateKey; import java.security.cert.X509Certificate; @@ -42,7 +43,7 @@ public final class OpenSslClientContext extends OpenSslContext { */ @Deprecated public OpenSslClientContext() throws SSLException { - this((File) null, null, null, null, null, null, null, IdentityCipherSuiteFilter.INSTANCE, null, 0, 0); + this(null, null, null, null, null, null, null, IdentityCipherSuiteFilter.INSTANCE, null, 0, 0); } /** @@ -176,21 +177,22 @@ public final class OpenSslClientContext extends OpenSslContext { this(toX509CertificatesInternal(trustCertCollectionFile), trustManagerFactory, toX509CertificatesInternal(keyCertChainFile), toPrivateKeyInternal(keyFile, keyPassword), keyPassword, keyManagerFactory, ciphers, cipherFilter, apn, null, sessionCacheSize, - sessionTimeout, false); + sessionTimeout, false, KeyStore.getDefaultType()); } OpenSslClientContext(X509Certificate[] trustCertCollection, TrustManagerFactory trustManagerFactory, X509Certificate[] keyCertChain, PrivateKey key, String keyPassword, KeyManagerFactory keyManagerFactory, Iterable<String> ciphers, CipherSuiteFilter cipherFilter, ApplicationProtocolConfig apn, String[] protocols, - long sessionCacheSize, long sessionTimeout, boolean enableOcsp) + long sessionCacheSize, long sessionTimeout, boolean enableOcsp, String keyStore) throws SSLException { super(ciphers, cipherFilter, apn, sessionCacheSize, sessionTimeout, SSL.SSL_MODE_CLIENT, keyCertChain, ClientAuth.NONE, protocols, false, enableOcsp); boolean success = false; try { + OpenSslKeyMaterialProvider.validateKeyMaterialSupported(keyCertChain, key, keyPassword); sessionContext = newSessionContext(this, ctx, engineMap, trustCertCollection, trustManagerFactory, - keyCertChain, key, keyPassword, keyManagerFactory); + keyCertChain, key, keyPassword, keyManagerFactory, keyStore); success = true; } finally { if (!success) { diff --git a/handler/src/main/java/io/netty/handler/ssl/OpenSslJavaxX509Certificate.java b/handler/src/main/java/io/netty/handler/ssl/OpenSslJavaxX509Certificate.java index da10ded..af52ddc 100644 --- a/handler/src/main/java/io/netty/handler/ssl/OpenSslJavaxX509Certificate.java +++ b/handler/src/main/java/io/netty/handler/ssl/OpenSslJavaxX509Certificate.java @@ -32,7 +32,7 @@ final class OpenSslJavaxX509Certificate extends X509Certificate { private final byte[] bytes; private X509Certificate wrapped; - public OpenSslJavaxX509Certificate(byte[] bytes) { + OpenSslJavaxX509Certificate(byte[] bytes) { this.bytes = bytes; } diff --git a/handler/src/main/java/io/netty/handler/ssl/OpenSslKeyMaterialManager.java b/handler/src/main/java/io/netty/handler/ssl/OpenSslKeyMaterialManager.java index 9f0e019..7acbf70 100644 --- a/handler/src/main/java/io/netty/handler/ssl/OpenSslKeyMaterialManager.java +++ b/handler/src/main/java/io/netty/handler/ssl/OpenSslKeyMaterialManager.java @@ -15,8 +15,6 @@ */ package io.netty.handler.ssl; -import io.netty.internal.tcnative.SSL; - import javax.net.ssl.SSLException; import javax.net.ssl.X509ExtendedKeyManager; import javax.net.ssl.X509KeyManager; @@ -66,15 +64,19 @@ final class OpenSslKeyMaterialManager { } void setKeyMaterialServerSide(ReferenceCountedOpenSslEngine engine) throws SSLException { - long ssl = engine.sslPointer(); - String[] authMethods = SSL.authenticationMethods(ssl); + String[] authMethods = engine.authMethods(); + if (authMethods.length == 0) { + return; + } Set<String> aliases = new HashSet<String>(authMethods.length); for (String authMethod : authMethods) { String type = KEY_TYPES.get(authMethod); if (type != null) { String alias = chooseServerAlias(engine, type); if (alias != null && aliases.add(alias)) { - setKeyMaterial(engine, alias); + if (!setKeyMaterial(engine, alias)) { + return; + } } } } @@ -91,13 +93,11 @@ final class OpenSslKeyMaterialManager { } } - private void setKeyMaterial(ReferenceCountedOpenSslEngine engine, String alias) throws SSLException { + private boolean setKeyMaterial(ReferenceCountedOpenSslEngine engine, String alias) throws SSLException { OpenSslKeyMaterial keyMaterial = null; try { keyMaterial = provider.chooseKeyMaterial(engine.alloc, alias); - if (keyMaterial != null) { - engine.setKeyMaterial(keyMaterial); - } + return keyMaterial == null || engine.setKeyMaterial(keyMaterial); } catch (SSLException e) { throw e; } catch (Exception e) { diff --git a/handler/src/main/java/io/netty/handler/ssl/OpenSslKeyMaterialProvider.java b/handler/src/main/java/io/netty/handler/ssl/OpenSslKeyMaterialProvider.java index 72cd2e0..f931fcf 100644 --- a/handler/src/main/java/io/netty/handler/ssl/OpenSslKeyMaterialProvider.java +++ b/handler/src/main/java/io/netty/handler/ssl/OpenSslKeyMaterialProvider.java @@ -16,8 +16,10 @@ package io.netty.handler.ssl; import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.UnpooledByteBufAllocator; import io.netty.internal.tcnative.SSL; +import javax.net.ssl.SSLException; import javax.net.ssl.X509KeyManager; import java.security.PrivateKey; import java.security.cert.X509Certificate; @@ -37,6 +39,58 @@ class OpenSslKeyMaterialProvider { this.password = password; } + static void validateKeyMaterialSupported(X509Certificate[] keyCertChain, PrivateKey key, String keyPassword) + throws SSLException { + validateSupported(keyCertChain); + validateSupported(key, keyPassword); + } + + private static void validateSupported(PrivateKey key, String password) throws SSLException { + if (key == null) { + return; + } + + long pkeyBio = 0; + long pkey = 0; + + try { + pkeyBio = toBIO(UnpooledByteBufAllocator.DEFAULT, key); + pkey = SSL.parsePrivateKey(pkeyBio, password); + } catch (Exception e) { + throw new SSLException("PrivateKey type not supported " + key.getFormat(), e); + } finally { + SSL.freeBIO(pkeyBio); + if (pkey != 0) { + SSL.freePrivateKey(pkey); + } + } + } + + private static void validateSupported(X509Certificate[] certificates) throws SSLException { + if (certificates == null || certificates.length == 0) { + return; + } + + long chainBio = 0; + long chain = 0; + PemEncoded encoded = null; + try { + encoded = PemX509Certificate.toPEM(UnpooledByteBufAllocator.DEFAULT, true, certificates); + chainBio = toBIO(UnpooledByteBufAllocator.DEFAULT, encoded.retain()); + chain = SSL.parseX509Chain(chainBio); + } catch (Exception e) { + throw new SSLException("Certificate type not supported", e); + } finally { + SSL.freeBIO(chainBio); + if (chain != 0) { + SSL.freeX509Chain(chain); + } + if (encoded != null) { + encoded.release(); + } + } + } + /** * Returns the underlying {@link X509KeyManager} that is used. */ @@ -66,7 +120,7 @@ class OpenSslKeyMaterialProvider { OpenSslKeyMaterial keyMaterial; if (key instanceof OpenSslPrivateKey) { - keyMaterial = ((OpenSslPrivateKey) key).toKeyMaterial(chain, certificates); + keyMaterial = ((OpenSslPrivateKey) key).newKeyMaterial(chain, certificates); } else { pkeyBio = toBIO(allocator, key); pkey = key == null ? 0 : SSL.parsePrivateKey(pkeyBio, password); diff --git a/handler/src/main/java/io/netty/handler/ssl/OpenSslPrivateKey.java b/handler/src/main/java/io/netty/handler/ssl/OpenSslPrivateKey.java index 67639aa..c2e4f10 100644 --- a/handler/src/main/java/io/netty/handler/ssl/OpenSslPrivateKey.java +++ b/handler/src/main/java/io/netty/handler/ssl/OpenSslPrivateKey.java @@ -34,7 +34,7 @@ final class OpenSslPrivateKey extends AbstractReferenceCounted implements Privat @Override public String getAlgorithm() { - return "unkown"; + return "unknown"; } @Override @@ -48,10 +48,7 @@ final class OpenSslPrivateKey extends AbstractReferenceCounted implements Privat return null; } - /** - * Returns the pointer to the {@code EVP_PKEY}. - */ - long privateKeyAddress() { + private long privateKeyAddress() { if (refCnt() <= 0) { throw new IllegalReferenceCountException(); } @@ -94,6 +91,7 @@ final class OpenSslPrivateKey extends AbstractReferenceCounted implements Privat * * @see Destroyable#destroy() */ + @Override public void destroy() { release(refCnt()); } @@ -105,26 +103,33 @@ final class OpenSslPrivateKey extends AbstractReferenceCounted implements Privat * * @see Destroyable#isDestroyed() */ + @Override public boolean isDestroyed() { return refCnt() == 0; } /** - * Convert to a {@link OpenSslKeyMaterial}. Reference count of both is shared. + * Create a new {@link OpenSslKeyMaterial} which uses the private key that is held by {@link OpenSslPrivateKey}. + * + * When the material is created we increment the reference count of the enclosing {@link OpenSslPrivateKey} and + * decrement it again when the reference count of the {@link OpenSslKeyMaterial} reaches {@code 0}. */ - OpenSslKeyMaterial toKeyMaterial(long certificateChain, X509Certificate[] chain) { + OpenSslKeyMaterial newKeyMaterial(long certificateChain, X509Certificate[] chain) { return new OpenSslPrivateKeyMaterial(certificateChain, chain); } - private final class OpenSslPrivateKeyMaterial implements OpenSslKeyMaterial { + // Package-private for unit-test only + final class OpenSslPrivateKeyMaterial extends AbstractReferenceCounted implements OpenSslKeyMaterial { - private long certificateChain; + // Package-private for unit-test only + long certificateChain; private final X509Certificate[] x509CertificateChain; OpenSslPrivateKeyMaterial(long certificateChain, X509Certificate[] x509CertificateChain) { this.certificateChain = certificateChain; this.x509CertificateChain = x509CertificateChain == null ? EmptyArrays.EMPTY_X509_CERTIFICATES : x509CertificateChain; + OpenSslPrivateKey.this.retain(); } @Override @@ -142,59 +147,45 @@ final class OpenSslPrivateKey extends AbstractReferenceCounted implements Privat @Override public long privateKeyAddress() { + if (refCnt() <= 0) { + throw new IllegalReferenceCountException(); + } return OpenSslPrivateKey.this.privateKeyAddress(); } @Override - public OpenSslKeyMaterial retain() { - OpenSslPrivateKey.this.retain(); + public OpenSslKeyMaterial touch(Object hint) { + OpenSslPrivateKey.this.touch(hint); return this; } @Override - public OpenSslKeyMaterial retain(int increment) { - OpenSslPrivateKey.this.retain(increment); + public OpenSslKeyMaterial retain() { + super.retain(); return this; } @Override - public OpenSslKeyMaterial touch() { - OpenSslPrivateKey.this.touch(); + public OpenSslKeyMaterial retain(int increment) { + super.retain(increment); return this; } @Override - public OpenSslKeyMaterial touch(Object hint) { - OpenSslPrivateKey.this.touch(hint); + public OpenSslKeyMaterial touch() { + OpenSslPrivateKey.this.touch(); return this; } @Override - public boolean release() { - if (OpenSslPrivateKey.this.release()) { - releaseChain(); - return true; - } - return false; - } - - @Override - public boolean release(int decrement) { - if (OpenSslPrivateKey.this.release(decrement)) { - releaseChain(); - return true; - } - return false; + protected void deallocate() { + releaseChain(); + OpenSslPrivateKey.this.release(); } private void releaseChain() { SSL.freeX509Chain(certificateChain); certificateChain = 0; } - - @Override - public int refCnt() { - return OpenSslPrivateKey.this.refCnt(); - } } } diff --git a/handler/src/main/java/io/netty/handler/ssl/OpenSslPrivateKeyMethod.java b/handler/src/main/java/io/netty/handler/ssl/OpenSslPrivateKeyMethod.java new file mode 100644 index 0000000..d9fc877 --- /dev/null +++ b/handler/src/main/java/io/netty/handler/ssl/OpenSslPrivateKeyMethod.java @@ -0,0 +1,62 @@ +/* + * Copyright 2019 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.internal.tcnative.SSLPrivateKeyMethod; +import io.netty.util.internal.UnstableApi; + +import javax.net.ssl.SSLEngine; + +/** + * Allow to customize private key signing / decrypting (when using RSA). Only supported when using BoringSSL atm. + */ +@UnstableApi +public interface OpenSslPrivateKeyMethod { + int SSL_SIGN_RSA_PKCS1_SHA1 = SSLPrivateKeyMethod.SSL_SIGN_RSA_PKCS1_SHA1; + int SSL_SIGN_RSA_PKCS1_SHA256 = SSLPrivateKeyMethod.SSL_SIGN_RSA_PKCS1_SHA256; + int SSL_SIGN_RSA_PKCS1_SHA384 = SSLPrivateKeyMethod.SSL_SIGN_RSA_PKCS1_SHA384; + int SSL_SIGN_RSA_PKCS1_SHA512 = SSLPrivateKeyMethod.SSL_SIGN_RSA_PKCS1_SHA512; + int SSL_SIGN_ECDSA_SHA1 = SSLPrivateKeyMethod.SSL_SIGN_ECDSA_SHA1; + int SSL_SIGN_ECDSA_SECP256R1_SHA256 = SSLPrivateKeyMethod.SSL_SIGN_ECDSA_SECP256R1_SHA256; + int SSL_SIGN_ECDSA_SECP384R1_SHA384 = SSLPrivateKeyMethod.SSL_SIGN_ECDSA_SECP384R1_SHA384; + int SSL_SIGN_ECDSA_SECP521R1_SHA512 = SSLPrivateKeyMethod.SSL_SIGN_ECDSA_SECP521R1_SHA512; + int SSL_SIGN_RSA_PSS_RSAE_SHA256 = SSLPrivateKeyMethod.SSL_SIGN_RSA_PSS_RSAE_SHA256; + int SSL_SIGN_RSA_PSS_RSAE_SHA384 = SSLPrivateKeyMethod.SSL_SIGN_RSA_PSS_RSAE_SHA384; + int SSL_SIGN_RSA_PSS_RSAE_SHA512 = SSLPrivateKeyMethod.SSL_SIGN_RSA_PSS_RSAE_SHA512; + int SSL_SIGN_ED25519 = SSLPrivateKeyMethod.SSL_SIGN_ED25519; + int SSL_SIGN_RSA_PKCS1_MD5_SHA1 = SSLPrivateKeyMethod.SSL_SIGN_RSA_PKCS1_MD5_SHA1; + + /** + * Signs the input with the given key and returns the signed bytes. + * + * @param engine the {@link SSLEngine} + * @param signatureAlgorithm the algorithm to use for signing + * @param input the digest itself + * @return the signed data (must not be {@code null}) + * @throws Exception thrown if an error is encountered during the signing + */ + byte[] sign(SSLEngine engine, int signatureAlgorithm, byte[] input) throws Exception; + + /** + * Decrypts the input with the given key and returns the decrypted bytes. + * + * @param engine the {@link SSLEngine} + * @param input the input which should be decrypted + * @return the decrypted data (must not be {@code null}) + * @throws Exception thrown if an error is encountered during the decrypting + */ + byte[] decrypt(SSLEngine engine, byte[] input) throws Exception; +} diff --git a/handler/src/main/java/io/netty/handler/ssl/OpenSslServerContext.java b/handler/src/main/java/io/netty/handler/ssl/OpenSslServerContext.java index e27b05a..da342de 100644 --- a/handler/src/main/java/io/netty/handler/ssl/OpenSslServerContext.java +++ b/handler/src/main/java/io/netty/handler/ssl/OpenSslServerContext.java @@ -18,6 +18,7 @@ package io.netty.handler.ssl; import io.netty.internal.tcnative.SSL; import java.io.File; +import java.security.KeyStore; import java.security.PrivateKey; import java.security.cert.X509Certificate; @@ -321,7 +322,7 @@ public final class OpenSslServerContext extends OpenSslContext { this(toX509CertificatesInternal(trustCertCollectionFile), trustManagerFactory, toX509CertificatesInternal(keyCertChainFile), toPrivateKeyInternal(keyFile, keyPassword), keyPassword, keyManagerFactory, ciphers, cipherFilter, - apn, sessionCacheSize, sessionTimeout, ClientAuth.NONE, null, false, false); + apn, sessionCacheSize, sessionTimeout, ClientAuth.NONE, null, false, false, KeyStore.getDefaultType()); } OpenSslServerContext( @@ -329,10 +330,10 @@ public final class OpenSslServerContext extends OpenSslContext { X509Certificate[] keyCertChain, PrivateKey key, String keyPassword, KeyManagerFactory keyManagerFactory, Iterable<String> ciphers, CipherSuiteFilter cipherFilter, ApplicationProtocolConfig apn, long sessionCacheSize, long sessionTimeout, ClientAuth clientAuth, String[] protocols, boolean startTls, - boolean enableOcsp) throws SSLException { + boolean enableOcsp, String keyStore) throws SSLException { this(trustCertCollection, trustManagerFactory, keyCertChain, key, keyPassword, keyManagerFactory, ciphers, cipherFilter, toNegotiator(apn), sessionCacheSize, sessionTimeout, clientAuth, protocols, startTls, - enableOcsp); + enableOcsp, keyStore); } @SuppressWarnings("deprecation") @@ -341,14 +342,16 @@ public final class OpenSslServerContext extends OpenSslContext { X509Certificate[] keyCertChain, PrivateKey key, String keyPassword, KeyManagerFactory keyManagerFactory, Iterable<String> ciphers, CipherSuiteFilter cipherFilter, OpenSslApplicationProtocolNegotiator apn, long sessionCacheSize, long sessionTimeout, ClientAuth clientAuth, String[] protocols, boolean startTls, - boolean enableOcsp) throws SSLException { + boolean enableOcsp, String keyStore) throws SSLException { super(ciphers, cipherFilter, apn, sessionCacheSize, sessionTimeout, SSL.SSL_MODE_SERVER, keyCertChain, clientAuth, protocols, startTls, enableOcsp); + // Create a new SSL_CTX and configure it. boolean success = false; try { + OpenSslKeyMaterialProvider.validateKeyMaterialSupported(keyCertChain, key, keyPassword); sessionContext = newSessionContext(this, ctx, engineMap, trustCertCollection, trustManagerFactory, - keyCertChain, key, keyPassword, keyManagerFactory); + keyCertChain, key, keyPassword, keyManagerFactory, keyStore); success = true; } finally { if (!success) { diff --git a/handler/src/main/java/io/netty/handler/ssl/OpenSslSessionContext.java b/handler/src/main/java/io/netty/handler/ssl/OpenSslSessionContext.java index 9faefb1..f063352 100644 --- a/handler/src/main/java/io/netty/handler/ssl/OpenSslSessionContext.java +++ b/handler/src/main/java/io/netty/handler/ssl/OpenSslSessionContext.java @@ -15,10 +15,10 @@ */ package io.netty.handler.ssl; -import io.netty.util.internal.ObjectUtil; import io.netty.internal.tcnative.SSL; import io.netty.internal.tcnative.SSLContext; import io.netty.internal.tcnative.SessionTicketKey; +import io.netty.util.internal.ObjectUtil; import javax.net.ssl.SSLSession; import javax.net.ssl.SSLSessionContext; @@ -54,9 +54,7 @@ public abstract class OpenSslSessionContext implements SSLSessionContext { @Override public SSLSession getSession(byte[] bytes) { - if (bytes == null) { - throw new NullPointerException("bytes"); - } + ObjectUtil.checkNotNull(bytes, "bytes"); return null; } diff --git a/handler/src/main/java/io/netty/handler/ssl/OpenSslTlsv13X509ExtendedTrustManager.java b/handler/src/main/java/io/netty/handler/ssl/OpenSslTlsv13X509ExtendedTrustManager.java index 00c6886..a5a84f2 100644 --- a/handler/src/main/java/io/netty/handler/ssl/OpenSslTlsv13X509ExtendedTrustManager.java +++ b/handler/src/main/java/io/netty/handler/ssl/OpenSslTlsv13X509ExtendedTrustManager.java @@ -15,19 +15,15 @@ */ package io.netty.handler.ssl; -import io.netty.util.internal.EmptyArrays; import io.netty.util.internal.PlatformDependent; +import io.netty.util.internal.SuppressJava6Requirement; 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.SSLPeerUnverifiedException; import javax.net.ssl.SSLSession; import javax.net.ssl.SSLSessionContext; import javax.net.ssl.X509ExtendedTrustManager; import java.net.Socket; -import java.nio.ByteBuffer; import java.security.Principal; import java.security.cert.Certificate; import java.security.cert.CertificateException; @@ -40,6 +36,7 @@ import java.util.List; * default {@link X509ExtendedTrustManager} implementations provided by the JDK that can not handle a protocol version * of {@code TLSv1.3}. */ +@SuppressJava6Requirement(reason = "Usage guarded by java version check") final class OpenSslTlsv13X509ExtendedTrustManager extends X509ExtendedTrustManager { private final X509ExtendedTrustManager tm; @@ -48,22 +45,9 @@ final class OpenSslTlsv13X509ExtendedTrustManager extends X509ExtendedTrustManag this.tm = tm; } - static X509ExtendedTrustManager wrap(X509ExtendedTrustManager tm, boolean client) { - if (PlatformDependent.javaVersion() < 11) { - try { - X509Certificate[] certs = { OpenSsl.selfSignedCertificate() }; - if (client) { - tm.checkServerTrusted(certs, "RSA", new DummySSLEngine(true)); - } else { - tm.checkClientTrusted(certs, "RSA", new DummySSLEngine(false)); - } - } catch (IllegalArgumentException e) { - // If this happened we failed because our protocol version was not known by the implementation. - // See http://mail.openjdk.java.net/pipermail/security-dev/2018-September/018242.html. - return new OpenSslTlsv13X509ExtendedTrustManager(tm); - } catch (Throwable ignore) { - // Just assume we do not need to wrap. - } + static X509ExtendedTrustManager wrap(X509ExtendedTrustManager tm) { + if (PlatformDependent.javaVersion() < 11 && OpenSsl.isTlsv13Supported()) { + return new OpenSslTlsv13X509ExtendedTrustManager(tm); } return tm; } @@ -253,246 +237,4 @@ final class OpenSslTlsv13X509ExtendedTrustManager extends X509ExtendedTrustManag public X509Certificate[] getAcceptedIssuers() { return tm.getAcceptedIssuers(); } - - private static final class DummySSLEngine extends SSLEngine { - - private final boolean client; - - DummySSLEngine(boolean client) { - this.client = client; - } - - @Override - public SSLSession getHandshakeSession() { - return new SSLSession() { - @Override - public byte[] getId() { - return EmptyArrays.EMPTY_BYTES; - } - - @Override - public SSLSessionContext getSessionContext() { - return null; - } - - @Override - public long getCreationTime() { - return 0; - } - - @Override - public long getLastAccessedTime() { - return 0; - } - - @Override - public void invalidate() { - // NOOP - } - - @Override - public boolean isValid() { - return false; - } - - @Override - public void putValue(String s, Object o) { - // NOOP - } - - @Override - public Object getValue(String s) { - return null; - } - - @Override - public void removeValue(String s) { - // NOOP - } - - @Override - public String[] getValueNames() { - return EmptyArrays.EMPTY_STRINGS; - } - - @Override - public Certificate[] getPeerCertificates() throws SSLPeerUnverifiedException { - return EmptyArrays.EMPTY_CERTIFICATES; - } - - @Override - public Certificate[] getLocalCertificates() { - return EmptyArrays.EMPTY_CERTIFICATES; - } - - @Override - public javax.security.cert.X509Certificate[] getPeerCertificateChain() - throws SSLPeerUnverifiedException { - return EmptyArrays.EMPTY_JAVAX_X509_CERTIFICATES; - } - - @Override - public Principal getPeerPrincipal() throws SSLPeerUnverifiedException { - return null; - } - - @Override - public Principal getLocalPrincipal() { - return null; - } - - @Override - public String getCipherSuite() { - return null; - } - - @Override - public String getProtocol() { - return SslUtils.PROTOCOL_TLS_V1_3; - } - - @Override - public String getPeerHost() { - return null; - } - - @Override - public int getPeerPort() { - return 0; - } - - @Override - public int getPacketBufferSize() { - return 0; - } - - @Override - public int getApplicationBufferSize() { - return 0; - } - }; - } - - @Override - public SSLEngineResult wrap(ByteBuffer[] byteBuffers, int i, int i1, ByteBuffer byteBuffer) - throws SSLException { - throw new UnsupportedOperationException(); - } - - @Override - public SSLEngineResult unwrap(ByteBuffer byteBuffer, ByteBuffer[] byteBuffers, int i, int i1) - throws SSLException { - throw new UnsupportedOperationException(); - } - - @Override - public Runnable getDelegatedTask() { - return null; - } - - @Override - public void closeInbound() throws SSLException { - // NOOP - } - - @Override - public boolean isInboundDone() { - return true; - } - - @Override - public void closeOutbound() { - // NOOP - } - - @Override - public boolean isOutboundDone() { - return true; - } - - @Override - public String[] getSupportedCipherSuites() { - return EmptyArrays.EMPTY_STRINGS; - } - - @Override - public String[] getEnabledCipherSuites() { - return EmptyArrays.EMPTY_STRINGS; - } - - @Override - public void setEnabledCipherSuites(String[] strings) { - // NOOP - } - - @Override - public String[] getSupportedProtocols() { - return new String[] { SslUtils.PROTOCOL_TLS_V1_3 }; - } - - @Override - public String[] getEnabledProtocols() { - return new String[] { SslUtils.PROTOCOL_TLS_V1_3 }; - } - - @Override - public void setEnabledProtocols(String[] strings) { - // NOOP - } - - @Override - public SSLSession getSession() { - return getHandshakeSession(); - } - - @Override - public void beginHandshake() throws SSLException { - // NOOP - } - - @Override - public HandshakeStatus getHandshakeStatus() { - return HandshakeStatus.NEED_TASK; - } - - @Override - public void setUseClientMode(boolean b) { - // NOOP - } - - @Override - public boolean getUseClientMode() { - return client; - } - - @Override - public void setNeedClientAuth(boolean b) { - // NOOP - } - - @Override - public boolean getNeedClientAuth() { - return false; - } - - @Override - public void setWantClientAuth(boolean b) { - // NOOP - } - - @Override - public boolean getWantClientAuth() { - return false; - } - - @Override - public void setEnableSessionCreation(boolean b) { - // NOOP - } - - @Override - public boolean getEnableSessionCreation() { - return false; - } - } } diff --git a/handler/src/main/java/io/netty/handler/ssl/OpenSslX509Certificate.java b/handler/src/main/java/io/netty/handler/ssl/OpenSslX509Certificate.java index b94a195..d3188eb 100644 --- a/handler/src/main/java/io/netty/handler/ssl/OpenSslX509Certificate.java +++ b/handler/src/main/java/io/netty/handler/ssl/OpenSslX509Certificate.java @@ -15,6 +15,8 @@ */ package io.netty.handler.ssl; +import io.netty.util.internal.SuppressJava6Requirement; + import javax.security.auth.x500.X500Principal; import java.io.ByteArrayInputStream; import java.math.BigInteger; @@ -41,7 +43,7 @@ final class OpenSslX509Certificate extends X509Certificate { private final byte[] bytes; private X509Certificate wrapped; - public OpenSslX509Certificate(byte[] bytes) { + OpenSslX509Certificate(byte[] bytes) { this.bytes = bytes; } @@ -81,6 +83,7 @@ final class OpenSslX509Certificate extends X509Certificate { } // No @Override annotation as it was only introduced in Java8. + @SuppressJava6Requirement(reason = "Can only be called from Java8 as class is package-private") public void verify(PublicKey key, Provider sigProvider) throws CertificateException, NoSuchAlgorithmException, InvalidKeyException, SignatureException { unwrap().verify(key, sigProvider); diff --git a/handler/src/main/java/io/netty/handler/ssl/OpenSslX509KeyManagerFactory.java b/handler/src/main/java/io/netty/handler/ssl/OpenSslX509KeyManagerFactory.java index 90d94cb..2a86332 100644 --- a/handler/src/main/java/io/netty/handler/ssl/OpenSslX509KeyManagerFactory.java +++ b/handler/src/main/java/io/netty/handler/ssl/OpenSslX509KeyManagerFactory.java @@ -52,6 +52,8 @@ import java.util.Map; * Special {@link KeyManagerFactory} that pre-compute the keymaterial used when {@link SslProvider#OPENSSL} or * {@link SslProvider#OPENSSL_REFCNT} is used and so will improve handshake times and its performance. * + * + * * Because the keymaterial is pre-computed any modification to the {@link KeyStore} is ignored after * {@link #init(KeyStore, char[])} is called. * @@ -252,15 +254,47 @@ public final class OpenSslX509KeyManagerFactory extends KeyManagerFactory { public static OpenSslX509KeyManagerFactory newEngineBased(X509Certificate[] certificateChain, String password) throws CertificateException, IOException, KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException { - KeyStore store = new OpenSslEngineKeyStore(certificateChain.clone()); + KeyStore store = new OpenSslKeyStore(certificateChain.clone(), false); store.load(null, null); OpenSslX509KeyManagerFactory factory = new OpenSslX509KeyManagerFactory(); factory.init(store, password == null ? null : password.toCharArray()); return factory; } - private static final class OpenSslEngineKeyStore extends KeyStore { - private OpenSslEngineKeyStore(final X509Certificate[] certificateChain) { + /** + * See {@link OpenSslX509KeyManagerFactory#newEngineBased(X509Certificate[], String)}. + */ + public static OpenSslX509KeyManagerFactory newKeyless(File chain) + throws CertificateException, IOException, + KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException { + return newKeyless(SslContext.toX509Certificates(chain)); + } + + /** + * See {@link OpenSslX509KeyManagerFactory#newEngineBased(X509Certificate[], String)}. + */ + public static OpenSslX509KeyManagerFactory newKeyless(InputStream chain) + throws CertificateException, IOException, + KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException { + return newKeyless(SslContext.toX509Certificates(chain)); + } + + /** + * Returns a new initialized {@link OpenSslX509KeyManagerFactory} which will provide its private key by using the + * {@link OpenSslPrivateKeyMethod}. + */ + public static OpenSslX509KeyManagerFactory newKeyless(X509Certificate... certificateChain) + throws CertificateException, IOException, + KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException { + KeyStore store = new OpenSslKeyStore(certificateChain.clone(), true); + store.load(null, null); + OpenSslX509KeyManagerFactory factory = new OpenSslX509KeyManagerFactory(); + factory.init(store, null); + return factory; + } + + private static final class OpenSslKeyStore extends KeyStore { + private OpenSslKeyStore(final X509Certificate[] certificateChain, final boolean keyless) { super(new KeyStoreSpi() { private final Date creationDate = new Date(); @@ -268,15 +302,21 @@ public final class OpenSslX509KeyManagerFactory extends KeyManagerFactory { @Override public Key engineGetKey(String alias, char[] password) throws UnrecoverableKeyException { if (engineContainsAlias(alias)) { - try { - return new OpenSslPrivateKey(SSL.loadPrivateKeyFromEngine( - alias, password == null ? null : new String(password))); - } catch (Exception e) { - UnrecoverableKeyException keyException = - new UnrecoverableKeyException("Unable to load key from engine"); - keyException.initCause(e); - throw keyException; + final long privateKeyAddress; + if (keyless) { + privateKeyAddress = 0; + } else { + try { + privateKeyAddress = SSL.loadPrivateKeyFromEngine( + alias, password == null ? null : new String(password)); + } catch (Exception e) { + UnrecoverableKeyException keyException = + new UnrecoverableKeyException("Unable to load key from engine"); + keyException.initCause(e); + throw keyException; + } } + return new OpenSslPrivateKey(privateKeyAddress); } return null; } diff --git a/handler/src/main/java/io/netty/handler/ssl/OpenSslX509TrustManagerWrapper.java b/handler/src/main/java/io/netty/handler/ssl/OpenSslX509TrustManagerWrapper.java index 0a0db0b..b3ddcc1 100644 --- a/handler/src/main/java/io/netty/handler/ssl/OpenSslX509TrustManagerWrapper.java +++ b/handler/src/main/java/io/netty/handler/ssl/OpenSslX509TrustManagerWrapper.java @@ -17,6 +17,7 @@ package io.netty.handler.ssl; import io.netty.util.internal.EmptyArrays; import io.netty.util.internal.PlatformDependent; +import io.netty.util.internal.SuppressJava6Requirement; import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; @@ -39,6 +40,7 @@ import java.security.cert.X509Certificate; * This is really a "hack" until there is an official API as requested on the in * <a href="https://bugs.openjdk.java.net/projects/JDK/issues/JDK-8210843">JDK-8210843</a>. */ +@SuppressJava6Requirement(reason = "Usage guarded by java version check") final class OpenSslX509TrustManagerWrapper { private static final InternalLogger LOGGER = InternalLoggerFactory .getInstance(OpenSslX509TrustManagerWrapper.class); @@ -71,11 +73,13 @@ final class OpenSslX509TrustManagerWrapper { @Override public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException { + throw new CertificateException(); } @Override public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException { + throw new CertificateException(); } @Override @@ -161,6 +165,7 @@ final class OpenSslX509TrustManagerWrapper { this.tmOffset = tmOffset; } + @SuppressJava6Requirement(reason = "Usage guarded by java version check") @Override public X509TrustManager wrapIfNeeded(X509TrustManager manager) { if (!(manager instanceof X509ExtendedTrustManager)) { diff --git a/handler/src/main/java/io/netty/handler/ssl/PemPrivateKey.java b/handler/src/main/java/io/netty/handler/ssl/PemPrivateKey.java index 46145a0..cdb6b64 100644 --- a/handler/src/main/java/io/netty/handler/ssl/PemPrivateKey.java +++ b/handler/src/main/java/io/netty/handler/ssl/PemPrivateKey.java @@ -65,6 +65,10 @@ public final class PemPrivateKey extends AbstractReferenceCounted implements Pri throw new IllegalArgumentException(key.getClass().getName() + " does not support encoding"); } + return toPEM(allocator, useDirect, bytes); + } + + static PemEncoded toPEM(ByteBufAllocator allocator, boolean useDirect, byte[] bytes) { ByteBuf encoded = Unpooled.wrappedBuffer(bytes); try { ByteBuf base64 = SslUtils.toBase64(allocator, encoded); @@ -207,6 +211,7 @@ public final class PemPrivateKey extends AbstractReferenceCounted implements Pri * * @see Destroyable#destroy() */ + @Override public void destroy() { release(refCnt()); } @@ -218,6 +223,7 @@ public final class PemPrivateKey extends AbstractReferenceCounted implements Pri * * @see Destroyable#isDestroyed() */ + @Override public boolean isDestroyed() { return refCnt() == 0; } diff --git a/handler/src/main/java/io/netty/handler/ssl/PemReader.java b/handler/src/main/java/io/netty/handler/ssl/PemReader.java index 4cddad9..a3a351b 100644 --- a/handler/src/main/java/io/netty/handler/ssl/PemReader.java +++ b/handler/src/main/java/io/netty/handler/ssl/PemReader.java @@ -126,7 +126,7 @@ final class PemReader { Matcher m = KEY_PATTERN.matcher(content); if (!m.find()) { throw new KeyException("could not find a PKCS #8 private key in input stream" + - " (see http://netty.io/wiki/sslcontextbuilder-and-private-key.html for more information)"); + " (see https://netty.io/wiki/sslcontextbuilder-and-private-key.html for more information)"); } ByteBuf base64 = Unpooled.copiedBuffer(m.group(1), CharsetUtil.US_ASCII); diff --git a/handler/src/main/java/io/netty/handler/ssl/PemValue.java b/handler/src/main/java/io/netty/handler/ssl/PemValue.java index becb5b8..ada5e4d 100644 --- a/handler/src/main/java/io/netty/handler/ssl/PemValue.java +++ b/handler/src/main/java/io/netty/handler/ssl/PemValue.java @@ -34,7 +34,7 @@ class PemValue extends AbstractReferenceCounted implements PemEncoded { private final boolean sensitive; - public PemValue(ByteBuf content, boolean sensitive) { + PemValue(ByteBuf content, boolean sensitive) { this.content = ObjectUtil.checkNotNull(content, "content"); this.sensitive = sensitive; } diff --git a/handler/src/main/java/io/netty/handler/ssl/PseudoRandomFunction.java b/handler/src/main/java/io/netty/handler/ssl/PseudoRandomFunction.java new file mode 100644 index 0000000..77eea0b --- /dev/null +++ b/handler/src/main/java/io/netty/handler/ssl/PseudoRandomFunction.java @@ -0,0 +1,94 @@ +/* + * Copyright 2019 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.util.internal.EmptyArrays; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.security.GeneralSecurityException; +import java.util.Arrays; + +/** + * This pseudorandom function (PRF) takes as input a secret, a seed, and + * an identifying label and produces an output of arbitrary length. + * + * This is used by the TLS RFC to construct/deconstruct an array of bytes into + * composite secrets. + * + * {@link <a href="https://tools.ietf.org/html/rfc5246">rfc5246</a>} + */ +final class PseudoRandomFunction { + + /** + * Constructor never to be called. + */ + private PseudoRandomFunction() { + } + + /** + * Use a single hash function to expand a secret and seed into an + * arbitrary quantity of output. + * + * P_hash(secret, seed) = HMAC_hash(secret, A(1) + seed) + + * HMAC_hash(secret, A(2) + seed) + + * HMAC_hash(secret, A(3) + seed) + ... + * where + indicates concatenation. + * A() is defined as: + * A(0) = seed + * A(i) = HMAC_hash(secret, A(i-1)) + * @param secret The starting secret to use for expansion + * @param label An ascii string without a length byte or trailing null character. + * @param seed The seed of the hash + * @param length The number of bytes to return + * @param algo the hmac algorithm to use + * @return The expanded secrets + * @throws IllegalArgumentException if the algo could not be found. + */ + static byte[] hash(byte[] secret, byte[] label, byte[] seed, int length, String algo) { + if (length < 0) { + throw new IllegalArgumentException("You must provide a length greater than zero."); + } + try { + Mac hmac = Mac.getInstance(algo); + hmac.init(new SecretKeySpec(secret, algo)); + /* + * P_hash(secret, seed) = HMAC_hash(secret, A(1) + seed) + + * HMAC_hash(secret, A(2) + seed) + HMAC_hash(secret, A(3) + seed) + ... + * where + indicates concatenation. A() is defined as: A(0) = seed, A(i) + * = HMAC_hash(secret, A(i-1)) + */ + + int iterations = (int) Math.ceil(length / (double) hmac.getMacLength()); + byte[] expansion = EmptyArrays.EMPTY_BYTES; + byte[] data = concat(label, seed); + byte[] A = data; + for (int i = 0; i < iterations; i++) { + A = hmac.doFinal(A); + expansion = concat(expansion, hmac.doFinal(concat(A, data))); + } + return Arrays.copyOf(expansion, length); + } catch (GeneralSecurityException e) { + throw new IllegalArgumentException("Could not find algo: " + algo, e); + } + } + + private static byte[] concat(byte[] first, byte[] second) { + byte[] result = Arrays.copyOf(first, first.length + second.length); + System.arraycopy(second, 0, result, first.length, second.length); + return result; + } +} diff --git a/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslClientContext.java b/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslClientContext.java index f508971..56893b3 100644 --- a/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslClientContext.java +++ b/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslClientContext.java @@ -16,6 +16,7 @@ package io.netty.handler.ssl; import io.netty.internal.tcnative.CertificateCallback; +import io.netty.util.internal.SuppressJava6Requirement; import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; import io.netty.internal.tcnative.SSL; @@ -33,7 +34,6 @@ import java.util.Set; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLException; -import javax.net.ssl.SSLHandshakeException; import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509ExtendedTrustManager; import javax.net.ssl.X509TrustManager; @@ -63,13 +63,13 @@ public final class ReferenceCountedOpenSslClientContext extends ReferenceCounted KeyManagerFactory keyManagerFactory, Iterable<String> ciphers, CipherSuiteFilter cipherFilter, ApplicationProtocolConfig apn, String[] protocols, long sessionCacheSize, long sessionTimeout, - boolean enableOcsp) throws SSLException { + boolean enableOcsp, String keyStore) throws SSLException { super(ciphers, cipherFilter, apn, sessionCacheSize, sessionTimeout, SSL.SSL_MODE_CLIENT, keyCertChain, ClientAuth.NONE, protocols, false, enableOcsp, true); boolean success = false; try { sessionContext = newSessionContext(this, ctx, engineMap, trustCertCollection, trustManagerFactory, - keyCertChain, key, keyPassword, keyManagerFactory); + keyCertChain, key, keyPassword, keyManagerFactory, keyStore); success = true; } finally { if (!success) { @@ -87,8 +87,9 @@ public final class ReferenceCountedOpenSslClientContext extends ReferenceCounted OpenSslEngineMap engineMap, X509Certificate[] trustCertCollection, TrustManagerFactory trustManagerFactory, - X509Certificate[] keyCertChain, PrivateKey key, String keyPassword, - KeyManagerFactory keyManagerFactory) throws SSLException { + X509Certificate[] keyCertChain, PrivateKey key, + String keyPassword, KeyManagerFactory keyManagerFactory, + String keyStore) throws SSLException { if (key == null && keyCertChain != null || key != null && keyCertChain == null) { throw new IllegalArgumentException( "Either both keyCertChain and key needs to be null or none of them"); @@ -108,7 +109,7 @@ public final class ReferenceCountedOpenSslClientContext extends ReferenceCounted // javadocs state that keyManagerFactory has precedent over keyCertChain if (keyManagerFactory == null && keyCertChain != null) { char[] keyPasswordChars = keyStorePassword(keyPassword); - KeyStore ks = buildKeyStore(keyCertChain, key, keyPasswordChars); + KeyStore ks = buildKeyStore(keyCertChain, key, keyPasswordChars, keyStore); if (ks.aliases().hasMoreElements()) { keyManagerFactory = new OpenSslX509KeyManagerFactory(); } else { @@ -131,11 +132,17 @@ public final class ReferenceCountedOpenSslClientContext extends ReferenceCounted throw new SSLException("failed to set certificate and key", e); } - SSLContext.setVerify(ctx, SSL.SSL_CVERIFY_NONE, VERIFY_DEPTH); + // On the client side we always need to use SSL_CVERIFY_OPTIONAL (which will translate to SSL_VERIFY_PEER) + // to ensure that when the TrustManager throws we will produce the correct alert back to the server. + // + // See: + // - https://www.openssl.org/docs/man1.0.2/man3/SSL_CTX_set_verify.html + // - https://github.com/netty/netty/issues/8942 + SSLContext.setVerify(ctx, SSL.SSL_CVERIFY_OPTIONAL, VERIFY_DEPTH); try { if (trustCertCollection != null) { - trustManagerFactory = buildTrustManagerFactory(trustCertCollection, trustManagerFactory); + trustManagerFactory = buildTrustManagerFactory(trustCertCollection, trustManagerFactory, keyStore); } else if (trustManagerFactory == null) { trustManagerFactory = TrustManagerFactory.getInstance( TrustManagerFactory.getDefaultAlgorithm()); @@ -149,13 +156,7 @@ public final class ReferenceCountedOpenSslClientContext extends ReferenceCounted // // See https://github.com/netty/netty/issues/5372 - // Use this to prevent an error when running on java < 7 - if (useExtendedTrustManager(manager)) { - SSLContext.setCertVerifyCallback(ctx, - new ExtendedTrustManagerVerifyCallback(engineMap, (X509ExtendedTrustManager) manager)); - } else { - SSLContext.setCertVerifyCallback(ctx, new TrustManagerVerifyCallback(engineMap, manager)); - } + setVerifyCallback(ctx, engineMap, manager); } catch (Exception e) { if (keyMaterialProvider != null) { keyMaterialProvider.destroy(); @@ -172,6 +173,17 @@ public final class ReferenceCountedOpenSslClientContext extends ReferenceCounted } } + @SuppressJava6Requirement(reason = "Guarded by java version check") + private static void setVerifyCallback(long ctx, OpenSslEngineMap engineMap, X509TrustManager manager) { + // Use this to prevent an error when running on java < 7 + if (useExtendedTrustManager(manager)) { + SSLContext.setCertVerifyCallback(ctx, + new ExtendedTrustManagerVerifyCallback(engineMap, (X509ExtendedTrustManager) manager)); + } else { + SSLContext.setCertVerifyCallback(ctx, new TrustManagerVerifyCallback(engineMap, manager)); + } + } + // No cache is currently supported for client side mode. static final class OpenSslClientSessionContext extends OpenSslSessionContext { OpenSslClientSessionContext(ReferenceCountedOpenSslContext context, OpenSslKeyMaterialProvider provider) { @@ -228,12 +240,13 @@ public final class ReferenceCountedOpenSslClientContext extends ReferenceCounted } } + @SuppressJava6Requirement(reason = "Usage guarded by java version check") private static final class ExtendedTrustManagerVerifyCallback extends AbstractCertificateVerifier { private final X509ExtendedTrustManager manager; ExtendedTrustManagerVerifyCallback(OpenSslEngineMap engineMap, X509ExtendedTrustManager manager) { super(engineMap); - this.manager = OpenSslTlsv13X509ExtendedTrustManager.wrap(manager, true); + this.manager = OpenSslTlsv13X509ExtendedTrustManager.wrap(manager); } @Override @@ -255,6 +268,10 @@ public final class ReferenceCountedOpenSslClientContext extends ReferenceCounted @Override public void handle(long ssl, byte[] keyTypeBytes, byte[][] asn1DerEncodedPrincipals) throws Exception { final ReferenceCountedOpenSslEngine engine = engineMap.get(ssl); + // May be null if it was destroyed in the meantime. + if (engine == null) { + return; + } try { final Set<String> keyTypesSet = supportedClientKeyTypes(keyTypeBytes); final String[] keyTypes = keyTypesSet.toArray(new String[0]); @@ -270,9 +287,7 @@ public final class ReferenceCountedOpenSslClientContext extends ReferenceCounted keyManagerHolder.setKeyMaterialClientSide(engine, keyTypes, issuers); } catch (Throwable cause) { logger.debug("request of key failed", cause); - SSLHandshakeException e = new SSLHandshakeException("General OpenSslEngine problem"); - e.initCause(cause); - engine.handshakeException = e; + engine.initHandshakeException(cause); } } diff --git a/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslContext.java b/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslContext.java index da1fdb1..bc72db4 100644 --- a/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslContext.java +++ b/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslContext.java @@ -20,20 +20,23 @@ import io.netty.buffer.ByteBufAllocator; import io.netty.internal.tcnative.CertificateVerifier; import io.netty.internal.tcnative.SSL; import io.netty.internal.tcnative.SSLContext; +import io.netty.internal.tcnative.SSLPrivateKeyMethod; import io.netty.util.AbstractReferenceCounted; import io.netty.util.ReferenceCounted; import io.netty.util.ResourceLeakDetector; import io.netty.util.ResourceLeakDetectorFactory; import io.netty.util.ResourceLeakTracker; +import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.PlatformDependent; import io.netty.util.internal.StringUtil; +import io.netty.util.internal.SuppressJava6Requirement; import io.netty.util.internal.SystemPropertyUtil; +import io.netty.util.internal.UnstableApi; import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; -import java.security.AccessController; import java.security.PrivateKey; -import java.security.PrivilegedAction; +import java.security.SignatureException; import java.security.cert.CertPathValidatorException; import java.security.cert.Certificate; import java.security.cert.CertificateExpiredException; @@ -44,6 +47,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.concurrent.Executor; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; @@ -76,16 +80,11 @@ public abstract class ReferenceCountedOpenSslContext extends SslContext implemen private static final InternalLogger logger = InternalLoggerFactory.getInstance(ReferenceCountedOpenSslContext.class); - private static final int DEFAULT_BIO_NON_APPLICATION_BUFFER_SIZE = - AccessController.doPrivileged(new PrivilegedAction<Integer>() { - @Override - public Integer run() { - return Math.max(1, - SystemPropertyUtil.getInt("io.netty.handler.ssl.openssl.bioNonApplicationBufferSize", - 2048)); - } - }); - + private static final int DEFAULT_BIO_NON_APPLICATION_BUFFER_SIZE = Math.max(1, + SystemPropertyUtil.getInt("io.netty.handler.ssl.openssl.bioNonApplicationBufferSize", + 2048)); + static final boolean USE_TASKS = + SystemPropertyUtil.getBoolean("io.netty.handler.ssl.openssl.useTasks", false); private static final Integer DH_KEY_LENGTH; private static final ResourceLeakDetector<ReferenceCountedOpenSslContext> leakDetector = ResourceLeakDetectorFactory.instance().newResourceLeakDetector(ReferenceCountedOpenSslContext.class); @@ -164,12 +163,7 @@ public abstract class ReferenceCountedOpenSslContext extends SslContext implemen Integer dhLen = null; try { - String dhKeySize = AccessController.doPrivileged(new PrivilegedAction<String>() { - @Override - public String run() { - return SystemPropertyUtil.get("jdk.tls.ephemeralDHKeySize"); - } - }); + String dhKeySize = SystemPropertyUtil.get("jdk.tls.ephemeralDHKeySize"); if (dhKeySize != null) { try { dhLen = Integer.valueOf(dhKeySize); @@ -341,6 +335,8 @@ public abstract class ReferenceCountedOpenSslContext extends SslContext implemen if (enableOcsp) { SSLContext.enableOcsp(ctx, isClient()); } + + SSLContext.setUseTasks(ctx, USE_TASKS); success = true; } finally { if (!success) { @@ -400,6 +396,17 @@ public abstract class ReferenceCountedOpenSslContext extends SslContext implemen return new SslHandler(newEngine0(alloc, peerHost, peerPort, false), startTls); } + @Override + protected SslHandler newHandler(ByteBufAllocator alloc, boolean startTls, Executor executor) { + return new SslHandler(newEngine0(alloc, null, -1, false), startTls, executor); + } + + @Override + protected SslHandler newHandler(ByteBufAllocator alloc, String peerHost, int peerPort, + boolean startTls, Executor executor) { + return new SslHandler(newEngine0(alloc, peerHost, peerPort, false), executor); + } + SSLEngine newEngine0(ByteBufAllocator alloc, String peerHost, int peerPort, boolean jdkCompatibilityMode) { return new ReferenceCountedOpenSslEngine(this, alloc, peerHost, peerPort, jdkCompatibilityMode, true); } @@ -502,6 +509,37 @@ public abstract class ReferenceCountedOpenSslContext extends SslContext implemen } } + /** + * Set the {@link OpenSslPrivateKeyMethod} to use. This allows to offload private-key operations + * if needed. + * + * This method is currently only supported when {@code BoringSSL} is used. + * + * @param method method to use. + */ + @UnstableApi + public final void setPrivateKeyMethod(OpenSslPrivateKeyMethod method) { + ObjectUtil.checkNotNull(method, "method"); + Lock writerLock = ctxLock.writeLock(); + writerLock.lock(); + try { + SSLContext.setPrivateKeyMethod(ctx, new PrivateKeyMethod(engineMap, method)); + } finally { + writerLock.unlock(); + } + } + + // Exposed for testing only + final void setUseTasks(boolean useTasks) { + Lock writerLock = ctxLock.writeLock(); + writerLock.lock(); + try { + SSLContext.setUseTasks(ctx, useTasks); + } finally { + writerLock.unlock(); + } + } + // IMPORTANT: This method must only be called from either the constructor or the finalizer as a user MUST never // get access to an OpenSslSessionContext after this method was called to prevent the user from // producing a segfault. @@ -538,7 +576,10 @@ public abstract class ReferenceCountedOpenSslContext extends SslContext implemen protected static X509TrustManager chooseTrustManager(TrustManager[] managers) { for (TrustManager m : managers) { if (m instanceof X509TrustManager) { - return OpenSslX509TrustManagerWrapper.wrapIfNeeded((X509TrustManager) m); + if (PlatformDependent.javaVersion() >= 7) { + return OpenSslX509TrustManagerWrapper.wrapIfNeeded((X509TrustManager) m); + } + return (X509TrustManager) m; } } throw new IllegalStateException("no X509TrustManager found"); @@ -597,6 +638,7 @@ public abstract class ReferenceCountedOpenSslContext extends SslContext implemen } } + @SuppressJava6Requirement(reason = "Guarded by java version check") static boolean useExtendedTrustManager(X509TrustManager trustManager) { return PlatformDependent.javaVersion() >= 7 && trustManager instanceof X509ExtendedTrustManager; } @@ -649,16 +691,18 @@ public abstract class ReferenceCountedOpenSslContext extends SslContext implemen @Override public final int verify(long ssl, byte[][] chain, String auth) { - X509Certificate[] peerCerts = certificates(chain); final ReferenceCountedOpenSslEngine engine = engineMap.get(ssl); + if (engine == null) { + // May be null if it was destroyed in the meantime. + return CertificateVerifier.X509_V_ERR_UNSPECIFIED; + } + X509Certificate[] peerCerts = certificates(chain); try { verify(engine, peerCerts, auth); return CertificateVerifier.X509_V_OK; } catch (Throwable cause) { logger.debug("verification of certificate failed", cause); - SSLHandshakeException e = new SSLHandshakeException("General OpenSslEngine problem"); - e.initCause(cause); - engine.handshakeException = e; + engine.initHandshakeException(cause); // Try to extract the correct error code that should be used. if (cause instanceof OpenSslCertificateException) { @@ -673,30 +717,7 @@ public abstract class ReferenceCountedOpenSslContext extends SslContext implemen return CertificateVerifier.X509_V_ERR_CERT_NOT_YET_VALID; } if (PlatformDependent.javaVersion() >= 7) { - if (cause instanceof CertificateRevokedException) { - return CertificateVerifier.X509_V_ERR_CERT_REVOKED; - } - - // The X509TrustManagerImpl uses a Validator which wraps a CertPathValidatorException into - // an CertificateException. So we need to handle the wrapped CertPathValidatorException to be - // able to send the correct alert. - Throwable wrapped = cause.getCause(); - while (wrapped != null) { - if (wrapped instanceof CertPathValidatorException) { - CertPathValidatorException ex = (CertPathValidatorException) wrapped; - CertPathValidatorException.Reason reason = ex.getReason(); - if (reason == CertPathValidatorException.BasicReason.EXPIRED) { - return CertificateVerifier.X509_V_ERR_CERT_HAS_EXPIRED; - } - if (reason == CertPathValidatorException.BasicReason.NOT_YET_VALID) { - return CertificateVerifier.X509_V_ERR_CERT_NOT_YET_VALID; - } - if (reason == CertPathValidatorException.BasicReason.REVOKED) { - return CertificateVerifier.X509_V_ERR_CERT_REVOKED; - } - } - wrapped = wrapped.getCause(); - } + return translateToError(cause); } // Could not detect a specific error code to use, so fallback to a default code. @@ -704,6 +725,35 @@ public abstract class ReferenceCountedOpenSslContext extends SslContext implemen } } + @SuppressJava6Requirement(reason = "Usage guarded by java version check") + private static int translateToError(Throwable cause) { + if (cause instanceof CertificateRevokedException) { + return CertificateVerifier.X509_V_ERR_CERT_REVOKED; + } + + // The X509TrustManagerImpl uses a Validator which wraps a CertPathValidatorException into + // an CertificateException. So we need to handle the wrapped CertPathValidatorException to be + // able to send the correct alert. + Throwable wrapped = cause.getCause(); + while (wrapped != null) { + if (wrapped instanceof CertPathValidatorException) { + CertPathValidatorException ex = (CertPathValidatorException) wrapped; + CertPathValidatorException.Reason reason = ex.getReason(); + if (reason == CertPathValidatorException.BasicReason.EXPIRED) { + return CertificateVerifier.X509_V_ERR_CERT_HAS_EXPIRED; + } + if (reason == CertPathValidatorException.BasicReason.NOT_YET_VALID) { + return CertificateVerifier.X509_V_ERR_CERT_NOT_YET_VALID; + } + if (reason == CertPathValidatorException.BasicReason.REVOKED) { + return CertificateVerifier.X509_V_ERR_CERT_REVOKED; + } + } + wrapped = wrapped.getCause(); + } + return CertificateVerifier.X509_V_ERR_UNSPECIFIED; + } + abstract void verify(ReferenceCountedOpenSslEngine engine, X509Certificate[] peerCerts, String auth) throws Exception; } @@ -861,12 +911,59 @@ public abstract class ReferenceCountedOpenSslContext extends SslContext implemen return ((OpenSslX509KeyManagerFactory) factory).newProvider(); } - X509KeyManager keyManager = chooseX509KeyManager(factory.getKeyManagers()); if (factory instanceof OpenSslCachingX509KeyManagerFactory) { // The user explicit used OpenSslCachingX509KeyManagerFactory which signals us that its fine to cache. - return new OpenSslCachingKeyMaterialProvider(keyManager, password); + return ((OpenSslCachingX509KeyManagerFactory) factory).newProvider(password); } // We can not be sure if the material may change at runtime so we will not cache it. - return new OpenSslKeyMaterialProvider(keyManager, password); + return new OpenSslKeyMaterialProvider(chooseX509KeyManager(factory.getKeyManagers()), password); + } + + private static final class PrivateKeyMethod implements SSLPrivateKeyMethod { + + private final OpenSslEngineMap engineMap; + private final OpenSslPrivateKeyMethod keyMethod; + PrivateKeyMethod(OpenSslEngineMap engineMap, OpenSslPrivateKeyMethod keyMethod) { + this.engineMap = engineMap; + this.keyMethod = keyMethod; + } + + private ReferenceCountedOpenSslEngine retrieveEngine(long ssl) throws SSLException { + ReferenceCountedOpenSslEngine engine = engineMap.get(ssl); + if (engine == null) { + throw new SSLException("Could not find a " + + StringUtil.simpleClassName(ReferenceCountedOpenSslEngine.class) + " for sslPointer " + ssl); + } + return engine; + } + + @Override + public byte[] sign(long ssl, int signatureAlgorithm, byte[] digest) throws Exception { + ReferenceCountedOpenSslEngine engine = retrieveEngine(ssl); + try { + return verifyResult(keyMethod.sign(engine, signatureAlgorithm, digest)); + } catch (Exception e) { + engine.initHandshakeException(e); + throw e; + } + } + + @Override + public byte[] decrypt(long ssl, byte[] input) throws Exception { + ReferenceCountedOpenSslEngine engine = retrieveEngine(ssl); + try { + return verifyResult(keyMethod.decrypt(engine, input)); + } catch (Exception e) { + engine.initHandshakeException(e); + throw e; + } + } + + private static byte[] verifyResult(byte[] result) throws SignatureException { + if (result == null) { + throw new SignatureException(); + } + return result; + } } } diff --git a/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslEngine.java b/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslEngine.java index f93d283..163ad17 100644 --- a/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslEngine.java +++ b/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslEngine.java @@ -26,9 +26,10 @@ import io.netty.util.ResourceLeakDetector; import io.netty.util.ResourceLeakDetectorFactory; import io.netty.util.ResourceLeakTracker; import io.netty.util.internal.EmptyArrays; +import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.PlatformDependent; import io.netty.util.internal.StringUtil; -import io.netty.util.internal.ThrowableUtil; +import io.netty.util.internal.SuppressJava6Requirement; import io.netty.util.internal.UnstableApi; import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; @@ -46,9 +47,9 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; import java.util.concurrent.locks.Lock; +import javax.crypto.spec.SecretKeySpec; import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLEngineResult; import javax.net.ssl.SSLException; @@ -76,6 +77,7 @@ import static io.netty.util.internal.ObjectUtil.checkNotNull; import static java.lang.Integer.MAX_VALUE; import static java.lang.Math.min; import static javax.net.ssl.SSLEngineResult.HandshakeStatus.FINISHED; +import static javax.net.ssl.SSLEngineResult.HandshakeStatus.NEED_TASK; import static javax.net.ssl.SSLEngineResult.HandshakeStatus.NEED_UNWRAP; import static javax.net.ssl.SSLEngineResult.HandshakeStatus.NEED_WRAP; import static javax.net.ssl.SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING; @@ -97,12 +99,6 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc private static final InternalLogger logger = InternalLoggerFactory.getInstance(ReferenceCountedOpenSslEngine.class); - private static final SSLException BEGIN_HANDSHAKE_ENGINE_CLOSED = ThrowableUtil.unknownStackTrace( - new SSLException("engine closed"), ReferenceCountedOpenSslEngine.class, "beginHandshake()"); - private static final SSLException HANDSHAKE_ENGINE_CLOSED = ThrowableUtil.unknownStackTrace( - new SSLException("engine closed"), ReferenceCountedOpenSslEngine.class, "handshake()"); - private static final SSLException RENEGOTIATION_UNSUPPORTED = ThrowableUtil.unknownStackTrace( - new SSLException("renegotiation unsupported"), ReferenceCountedOpenSslEngine.class, "beginHandshake()"); private static final ResourceLeakDetector<ReferenceCountedOpenSslEngine> leakDetector = ResourceLeakDetectorFactory.instance().newResourceLeakDetector(ReferenceCountedOpenSslEngine.class); private static final int OPENSSL_OP_NO_PROTOCOL_INDEX_SSLV2 = 0; @@ -119,10 +115,6 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc SSL.SSL_OP_NO_TLSv1_2, SSL.SSL_OP_NO_TLSv1_3 }; - /** - * <a href="https://www.openssl.org/docs/man1.0.2/crypto/X509_check_host.html">The flags argument is usually 0</a>. - */ - private static final int DEFAULT_HOSTNAME_VALIDATION_FLAGS = 0; /** * Depends upon tcnative ... only use if tcnative is available! @@ -133,9 +125,6 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc */ private static final int MAX_RECORD_SIZE = SSL.SSL_MAX_RECORD_LENGTH; - private static final AtomicIntegerFieldUpdater<ReferenceCountedOpenSslEngine> DESTROYED_UPDATER = - AtomicIntegerFieldUpdater.newUpdater(ReferenceCountedOpenSslEngine.class, "destroyed"); - private static final SSLEngineResult NEED_UNWRAP_OK = new SSLEngineResult(OK, NEED_UNWRAP, 0, 0); private static final SSLEngineResult NEED_UNWRAP_CLOSED = new SSLEngineResult(CLOSED, NEED_UNWRAP, 0, 0); private static final SSLEngineResult NEED_WRAP_OK = new SSLEngineResult(OK, NEED_WRAP, 0, 0); @@ -167,8 +156,9 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc private HandshakeState handshakeState = HandshakeState.NOT_STARTED; private boolean receivedShutdown; - private volatile int destroyed; + private volatile boolean destroyed; private volatile String applicationProtocol; + private volatile boolean needTask; // Reference Counting private final ResourceLeakTracker<ReferenceCountedOpenSslEngine> leak; @@ -189,6 +179,7 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc boolean closed = leak.close(ReferenceCountedOpenSslEngine.this); assert closed; } + parentContext.release(); } }; @@ -216,16 +207,14 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc final ByteBufAllocator alloc; private final OpenSslEngineMap engineMap; private final OpenSslApplicationProtocolNegotiator apn; + private final ReferenceCountedOpenSslContext parentContext; private final OpenSslSession session; private final ByteBuffer[] singleSrcBuffer = new ByteBuffer[1]; private final ByteBuffer[] singleDstBuffer = new ByteBuffer[1]; private final boolean enableOcsp; private int maxWrapOverhead; private int maxWrapBufferSize; - - // This is package-private as we set it from OpenSslContext if an exception is thrown during - // the verification step. - SSLHandshakeException handshakeException; + private Throwable handshakeException; /** * Create a new instance. @@ -369,22 +358,46 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc calculateMaxWrapOverhead(); } catch (Throwable cause) { // Call shutdown so we are sure we correctly release all native memory and also guard against the - // case when shutdown() will be called by the finalizer again. If we would call SSL.free(...) directly - // the finalizer may end up calling it again as we would miss to update the DESTROYED_UPDATER. + // case when shutdown() will be called by the finalizer again. shutdown(); PlatformDependent.throwException(cause); } } + // Now that everything looks good and we're going to successfully return the + // object so we need to retain a reference to the parent context. + parentContext = context; + parentContext.retain(); + // Only create the leak after everything else was executed and so ensure we don't produce a false-positive for // the ResourceLeakDetector. leak = leakDetection ? leakDetector.track(this) : null; } - final void setKeyMaterial(OpenSslKeyMaterial keyMaterial) throws Exception { - SSL.setKeyMaterial(ssl, keyMaterial.certificateChainAddress(), keyMaterial.privateKeyAddress()); + final synchronized String[] authMethods() { + if (isDestroyed()) { + return EmptyArrays.EMPTY_STRINGS; + } + return SSL.authenticationMethods(ssl); + } + + final boolean setKeyMaterial(OpenSslKeyMaterial keyMaterial) throws Exception { + synchronized (this) { + if (isDestroyed()) { + return false; + } + SSL.setKeyMaterial(ssl, keyMaterial.certificateChainAddress(), keyMaterial.privateKeyAddress()); + } localCertificateChain = keyMaterial.certificateChain(); + return true; + } + + final synchronized SecretKeySpec masterKey() { + if (isDestroyed()) { + return null; + } + return new SecretKeySpec(SSL.getMasterKey(ssl), "AES"); } /** @@ -401,7 +414,9 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc } synchronized (this) { - SSL.setOcspResponse(ssl, response); + if (!isDestroyed()) { + SSL.setOcspResponse(ssl, response); + } } } @@ -419,6 +434,9 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc } synchronized (this) { + if (isDestroyed()) { + return EmptyArrays.EMPTY_BYTES; + } return SSL.getOcspResponse(ssl); } } @@ -490,7 +508,8 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc * Destroys this engine. */ public final synchronized void shutdown() { - if (DESTROYED_UPDATER.compareAndSet(this, 0, 1)) { + if (!destroyed) { + destroyed = true; engineMap.remove(ssl); SSL.freeSSL(ssl); ssl = networkBIO = 0; @@ -735,7 +754,7 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc // Flush any data that may have been written implicitly during the handshake by OpenSSL. bytesProduced = SSL.bioFlushByteBuffer(networkBIO); - if (bytesProduced > 0 && handshakeException != null) { + if (handshakeException != null) { // TODO(scott): It is possible that when the handshake failed there was not enough room in the // non-application buffers to hold the alert. We should get all the data before progressing on. // However I'm not aware of a way to do this with the OpenSSL APIs. @@ -744,7 +763,16 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc // We produced / consumed some data during the handshake, signal back to the caller. // If there is a handshake exception and we have produced data, we should send the data before // we allow handshake() to throw the handshake exception. - return newResult(NEED_WRAP, 0, bytesProduced); + // + // When the user calls wrap() again we will propagate the handshake error back to the user as + // soon as there is no more data to was produced (as part of an alert etc). + if (bytesProduced > 0) { + return newResult(NEED_WRAP, 0, bytesProduced); + } + // Nothing was produced see if there is a handshakeException that needs to be propagated + // to the caller by calling handshakeException() which will return the right HandshakeStatus + // if it can "recover" from the exception for now. + return newResult(handshakeException(), 0, 0); } status = handshake(); @@ -753,6 +781,10 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc // we may have freed up space by flushing above. bytesProduced = bioLengthBefore - SSL.bioLengthByteBuffer(networkBIO); + if (status == NEED_TASK) { + return newResult(status, 0, bytesProduced); + } + if (bytesProduced > 0) { // If we have filled up the dst buffer and we have not finished the handshake we should try to // wrap again. Otherwise we should only try to wrap again if there is still data pending in @@ -833,14 +865,18 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc bytesWritten = writePlaintextData(src, min(remaining, availableCapacityForWrap)); } + // Determine how much encrypted data was generated. + // + // Even if SSL_write doesn't consume any application data it is possible that OpenSSL will + // produce non-application data into the BIO. For example session tickets.... + // See https://github.com/netty/netty/issues/10041 + final int pendingNow = SSL.bioLengthByteBuffer(networkBIO); + bytesProduced += bioLengthBefore - pendingNow; + bioLengthBefore = pendingNow; + if (bytesWritten > 0) { bytesConsumed += bytesWritten; - // Determine how much encrypted data was generated: - final int pendingNow = SSL.bioLengthByteBuffer(networkBIO); - bytesProduced += bioLengthBefore - pendingNow; - bioLengthBefore = pendingNow; - if (jdkCompatibilityMode || bytesProduced == dst.remaining()) { return newResultMayFinishHandshake(status, bytesConsumed, bytesProduced); } @@ -883,6 +919,11 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc // to write encrypted data to. This is an OVERFLOW condition. // [1] https://www.openssl.org/docs/manmaster/ssl/SSL_write.html return newResult(BUFFER_OVERFLOW, status, bytesConsumed, bytesProduced); + } else if (sslError == SSL.SSL_ERROR_WANT_X509_LOOKUP || + sslError == SSL.SSL_ERROR_WANT_CERTIFICATE_VERIFY || + sslError == SSL.SSL_ERROR_WANT_PRIVATE_KEY_OPERATION) { + + return newResult(NEED_TASK, bytesConsumed, bytesProduced); } else { // Everything else is considered as error throw shutdownWithError("SSL_write", sslError); @@ -923,6 +964,10 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc } return new SSLEngineResult(CLOSED, hs, bytesConsumed, bytesProduced); } + if (hs == NEED_TASK) { + // Set needTask to true so getHandshakeStatus() will return the correct value. + needTask = true; + } return new SSLEngineResult(status, hs, bytesConsumed, bytesProduced); } @@ -958,7 +1003,14 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc if (handshakeState == HandshakeState.FINISHED) { return new SSLException(errorString); } - return new SSLHandshakeException(errorString); + + SSLHandshakeException exception = new SSLHandshakeException(errorString); + // If we have a handshakeException stored already we should include it as well to help the user debug things. + if (handshakeException != null) { + exception.initCause(handshakeException); + handshakeException = null; + } + return exception; } public final SSLEngineResult unwrap( @@ -966,9 +1018,7 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc final ByteBuffer[] dsts, int dstsOffset, final int dstsLength) throws SSLException { // Throw required runtime exceptions - if (srcs == null) { - throw new NullPointerException("srcs"); - } + ObjectUtil.checkNotNull(srcs, "srcs"); if (srcsOffset >= srcs.length || srcsOffset + srcsLength > srcs.length) { throw new IndexOutOfBoundsException( @@ -1020,6 +1070,11 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc } status = handshake(); + + if (status == NEED_TASK) { + return newResult(status, 0, 0); + } + if (status == NEED_WRAP) { return NEED_WRAP_OK; } @@ -1160,7 +1215,12 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc closeAll(); } return newResultMayFinishHandshake(isInboundDone() ? CLOSED : OK, status, - bytesConsumed, bytesProduced); + bytesConsumed, bytesProduced); + } else if (sslError == SSL.SSL_ERROR_WANT_X509_LOOKUP || + sslError == SSL.SSL_ERROR_WANT_CERTIFICATE_VERIFY || + sslError == SSL.SSL_ERROR_WANT_PRIVATE_KEY_OPERATION) { + return newResult(isInboundDone() ? CLOSED : OK, + NEED_TASK, bytesConsumed, bytesProduced); } else { return sslReadErrorResult(sslError, SSL.getLastErrorNumber(), bytesConsumed, bytesProduced); @@ -1293,10 +1353,29 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc } @Override - public final Runnable getDelegatedTask() { - // Currently, we do not delegate SSL computation tasks - // TODO: in the future, possibly create tasks to do encrypt / decrypt async - return null; + public final synchronized Runnable getDelegatedTask() { + if (isDestroyed()) { + return null; + } + final Runnable task = SSL.getTask(ssl); + if (task == null) { + return null; + } + return new Runnable() { + @Override + public void run() { + if (isDestroyed()) { + // The engine was destroyed in the meantime, just return. + return; + } + try { + task.run(); + } finally { + // The task was run, reset needTask to false so getHandshakeStatus() returns the correct value. + needTask = false; + } + } + }; } @Override @@ -1591,7 +1670,7 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc public final synchronized void beginHandshake() throws SSLException { switch (handshakeState) { case STARTED_IMPLICITLY: - checkEngineClosed(BEGIN_HANDSHAKE_ENGINE_CLOSED); + checkEngineClosed(); // A user did not start handshake by calling this method by him/herself, // but handshake has been started already by wrap() or unwrap() implicitly. @@ -1607,10 +1686,13 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc // Nothing to do as the handshake is not done yet. break; case FINISHED: - throw RENEGOTIATION_UNSUPPORTED; + throw new SSLException("renegotiation unsupported"); case NOT_STARTED: handshakeState = HandshakeState.STARTED_EXPLICITLY; - handshake(); + if (handshake() == NEED_TASK) { + // Set needTask to true so getHandshakeStatus() will return the correct value. + needTask = true; + } calculateMaxWrapOverhead(); break; default: @@ -1618,9 +1700,9 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc } } - private void checkEngineClosed(SSLException cause) throws SSLException { + private void checkEngineClosed() throws SSLException { if (isDestroyed()) { - throw cause; + throw new SSLException("engine closed"); } } @@ -1637,27 +1719,51 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc return cert == null || cert.length == 0; } + private SSLEngineResult.HandshakeStatus handshakeException() throws SSLException { + if (SSL.bioLengthNonApplication(networkBIO) > 0) { + // There is something pending, we need to consume it first via a WRAP so we don't loose anything. + return NEED_WRAP; + } + + Throwable exception = handshakeException; + assert exception != null; + handshakeException = null; + shutdown(); + if (exception instanceof SSLHandshakeException) { + throw (SSLHandshakeException) exception; + } + SSLHandshakeException e = new SSLHandshakeException("General OpenSslEngine problem"); + e.initCause(exception); + throw e; + } + + /** + * Should be called if the handshake will be failed due a callback that throws an exception. + * This cause will then be used to give more details as part of the {@link SSLHandshakeException}. + */ + final void initHandshakeException(Throwable cause) { + assert handshakeException == null; + handshakeException = cause; + } + private SSLEngineResult.HandshakeStatus handshake() throws SSLException { + if (needTask) { + return NEED_TASK; + } if (handshakeState == HandshakeState.FINISHED) { return FINISHED; } - checkEngineClosed(HANDSHAKE_ENGINE_CLOSED); - // Check if we have a pending handshakeException and if so see if we need to consume all pending data from the - // BIO first or can just shutdown and throw it now. - // This is needed so we ensure close_notify etc is correctly send to the remote peer. - // See https://github.com/netty/netty/issues/3900 - SSLHandshakeException exception = handshakeException; - if (exception != null) { - if (SSL.bioLengthNonApplication(networkBIO) > 0) { - // There is something pending, we need to consume it first via a WRAP so we don't loose anything. - return NEED_WRAP; - } - // No more data left to send to the remote peer, so null out the exception field, shutdown and throw - // the exception. - handshakeException = null; - shutdown(); - throw exception; + checkEngineClosed(); + + if (handshakeException != null) { + // Let's call SSL.doHandshake(...) again in case there is some async operation pending that would fill the + // outbound buffer. + if (SSL.doHandshake(ssl) <= 0) { + // Clear any error that was put on the stack by the handshake + SSL.clearError(); + } + return handshakeException(); } // Adding the OpenSslEngine to the OpenSslEngineMap so it can be used in the AbstractCertificateVerifier. @@ -1668,26 +1774,32 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc int code = SSL.doHandshake(ssl); if (code <= 0) { + int sslError = SSL.getError(ssl, code); + if (sslError == SSL.SSL_ERROR_WANT_READ || sslError == SSL.SSL_ERROR_WANT_WRITE) { + return pendingStatus(SSL.bioLengthNonApplication(networkBIO)); + } + + if (sslError == SSL.SSL_ERROR_WANT_X509_LOOKUP || + sslError == SSL.SSL_ERROR_WANT_CERTIFICATE_VERIFY || + sslError == SSL.SSL_ERROR_WANT_PRIVATE_KEY_OPERATION) { + return NEED_TASK; + } + // Check if we have a pending exception that was created during the handshake and if so throw it after // shutdown the connection. if (handshakeException != null) { - exception = handshakeException; - handshakeException = null; - shutdown(); - throw exception; + return handshakeException(); } - int sslError = SSL.getError(ssl, code); - if (sslError == SSL.SSL_ERROR_WANT_READ || sslError == SSL.SSL_ERROR_WANT_WRITE) { - return pendingStatus(SSL.bioLengthNonApplication(networkBIO)); - } else { - // Everything else is considered as error - throw shutdownWithError("SSL_do_handshake", sslError); - } + // Everything else is considered as error + throw shutdownWithError("SSL_do_handshake", sslError); + } + // We have produced more data as part of the handshake if this is the case the user should call wrap(...) + if (SSL.bioLengthNonApplication(networkBIO) > 0) { + return NEED_WRAP; } // if SSL_do_handshake returns > 0 or sslError == SSL.SSL_ERROR_NAME it means the handshake was finished. session.handshakeFinished(); - engineMap.remove(ssl); return FINISHED; } @@ -1704,12 +1816,26 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc @Override public final synchronized SSLEngineResult.HandshakeStatus getHandshakeStatus() { // Check if we are in the initial handshake phase or shutdown phase - return needPendingStatus() ? pendingStatus(SSL.bioLengthNonApplication(networkBIO)) : NOT_HANDSHAKING; + if (needPendingStatus()) { + if (needTask) { + // There is a task outstanding + return NEED_TASK; + } + return pendingStatus(SSL.bioLengthNonApplication(networkBIO)); + } + return NOT_HANDSHAKING; } private SSLEngineResult.HandshakeStatus getHandshakeStatus(int pending) { // Check if we are in the initial handshake phase or shutdown phase - return needPendingStatus() ? pendingStatus(pending) : NOT_HANDSHAKING; + if (needPendingStatus()) { + if (needTask) { + // There is a task outstanding + return NEED_TASK; + } + return pendingStatus(pending); + } + return NOT_HANDSHAKING; } private boolean needPendingStatus() { @@ -1830,6 +1956,7 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc return false; } + @SuppressJava6Requirement(reason = "Usage guarded by java version check") @Override public final synchronized SSLParameters getSSLParameters() { SSLParameters sslParameters = super.getSSLParameters(); @@ -1853,6 +1980,7 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc return sslParameters; } + @SuppressJava6Requirement(reason = "Usage guarded by java version check") @Override public final synchronized void setSSLParameters(SSLParameters sslParameters) { int version = PlatformDependent.javaVersion(); @@ -1882,18 +2010,6 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc final String endPointIdentificationAlgorithm = sslParameters.getEndpointIdentificationAlgorithm(); final boolean endPointVerificationEnabled = isEndPointVerificationEnabled(endPointIdentificationAlgorithm); - final boolean wasEndPointVerificationEnabled = - isEndPointVerificationEnabled(this.endPointIdentificationAlgorithm); - - if (wasEndPointVerificationEnabled && !endPointVerificationEnabled) { - // Passing in null will disable hostname verification again so only do so if it was enabled before. - SSL.setHostNameValidation(ssl, DEFAULT_HOSTNAME_VALIDATION_FLAGS, null); - } else { - String host = endPointVerificationEnabled ? getPeerHost() : null; - if (host != null && !host.isEmpty()) { - SSL.setHostNameValidation(ssl, DEFAULT_HOSTNAME_VALIDATION_FLAGS, host); - } - } // If the user asks for hostname verification we must ensure we verify the peer. // If the user disables hostname verification we leave it up to the user to change the mode manually. if (clientMode && endPointVerificationEnabled) { @@ -1911,7 +2027,7 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc } private boolean isDestroyed() { - return destroyed != 0; + return destroyed; } final boolean checkSniHostnameMatch(byte[] hostname) { @@ -1938,7 +2054,6 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc // thread. private X509Certificate[] x509PeerCerts; private Certificate[] peerCerts; - private Certificate[] localCerts; private String protocol; private String cipher; @@ -2010,12 +2125,9 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc @Override public void putValue(String name, Object value) { - if (name == null) { - throw new NullPointerException("name"); - } - if (value == null) { - throw new NullPointerException("value"); - } + ObjectUtil.checkNotNull(name, "name"); + ObjectUtil.checkNotNull(value, "value"); + final Object old; synchronized (this) { Map<String, Object> values = this.values; @@ -2035,9 +2147,7 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc @Override public Object getValue(String name) { - if (name == null) { - throw new NullPointerException("name"); - } + ObjectUtil.checkNotNull(name, "name"); synchronized (this) { if (values == null) { return null; @@ -2048,9 +2158,7 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc @Override public void removeValue(String name) { - if (name == null) { - throw new NullPointerException("name"); - } + ObjectUtil.checkNotNull(name, "name"); final Object old; synchronized (this) { @@ -2093,7 +2201,6 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc id = SSL.getSessionId(ssl); cipher = toJavaCipherSuite(SSL.getCipherForSSL(ssl)); protocol = SSL.getVersion(ssl); - localCerts = localCertificateChain; initPeerCerts(); selectApplicationProtocol(); @@ -2228,6 +2335,7 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc @Override public Certificate[] getLocalCertificates() { + Certificate[] localCerts = ReferenceCountedOpenSslEngine.this.localCertificateChain; if (localCerts == null) { return null; } @@ -2254,7 +2362,7 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc @Override public Principal getLocalPrincipal() { - Certificate[] local = localCerts; + Certificate[] local = ReferenceCountedOpenSslEngine.this.localCertificateChain; if (local == null || local.length == 0) { return null; } diff --git a/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslServerContext.java b/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslServerContext.java index e901aeb..6d78e6d 100644 --- a/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslServerContext.java +++ b/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslServerContext.java @@ -22,6 +22,7 @@ import io.netty.internal.tcnative.SSLContext; import io.netty.internal.tcnative.SniHostNameMatcher; import io.netty.util.CharsetUtil; import io.netty.util.internal.PlatformDependent; +import io.netty.util.internal.SuppressJava6Requirement; import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; @@ -30,7 +31,6 @@ import java.security.PrivateKey; import java.security.cert.X509Certificate; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLException; -import javax.net.ssl.SSLHandshakeException; import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509ExtendedTrustManager; import javax.net.ssl.X509TrustManager; @@ -56,25 +56,25 @@ public final class ReferenceCountedOpenSslServerContext extends ReferenceCounted X509Certificate[] keyCertChain, PrivateKey key, String keyPassword, KeyManagerFactory keyManagerFactory, Iterable<String> ciphers, CipherSuiteFilter cipherFilter, ApplicationProtocolConfig apn, long sessionCacheSize, long sessionTimeout, ClientAuth clientAuth, String[] protocols, boolean startTls, - boolean enableOcsp) throws SSLException { + boolean enableOcsp, String keyStore) throws SSLException { this(trustCertCollection, trustManagerFactory, keyCertChain, key, keyPassword, keyManagerFactory, ciphers, cipherFilter, toNegotiator(apn), sessionCacheSize, sessionTimeout, clientAuth, protocols, startTls, - enableOcsp); + enableOcsp, keyStore); } - private ReferenceCountedOpenSslServerContext( + ReferenceCountedOpenSslServerContext( X509Certificate[] trustCertCollection, TrustManagerFactory trustManagerFactory, X509Certificate[] keyCertChain, PrivateKey key, String keyPassword, KeyManagerFactory keyManagerFactory, Iterable<String> ciphers, CipherSuiteFilter cipherFilter, OpenSslApplicationProtocolNegotiator apn, long sessionCacheSize, long sessionTimeout, ClientAuth clientAuth, String[] protocols, boolean startTls, - boolean enableOcsp) throws SSLException { + boolean enableOcsp, String keyStore) throws SSLException { super(ciphers, cipherFilter, apn, sessionCacheSize, sessionTimeout, SSL.SSL_MODE_SERVER, keyCertChain, clientAuth, protocols, startTls, enableOcsp, true); // Create a new SSL_CTX and configure it. boolean success = false; try { sessionContext = newSessionContext(this, ctx, engineMap, trustCertCollection, trustManagerFactory, - keyCertChain, key, keyPassword, keyManagerFactory); + keyCertChain, key, keyPassword, keyManagerFactory, keyStore); success = true; } finally { if (!success) { @@ -93,7 +93,8 @@ public final class ReferenceCountedOpenSslServerContext extends ReferenceCounted X509Certificate[] trustCertCollection, TrustManagerFactory trustManagerFactory, X509Certificate[] keyCertChain, PrivateKey key, - String keyPassword, KeyManagerFactory keyManagerFactory) + String keyPassword, KeyManagerFactory keyManagerFactory, + String keyStore) throws SSLException { OpenSslKeyMaterialProvider keyMaterialProvider = null; try { @@ -112,7 +113,7 @@ public final class ReferenceCountedOpenSslServerContext extends ReferenceCounted // keyManagerFactory for the server so build one if it is not specified. if (keyManagerFactory == null) { char[] keyPasswordChars = keyStorePassword(keyPassword); - KeyStore ks = buildKeyStore(keyCertChain, key, keyPasswordChars); + KeyStore ks = buildKeyStore(keyCertChain, key, keyPasswordChars, keyStore); if (ks.aliases().hasMoreElements()) { keyManagerFactory = new OpenSslX509KeyManagerFactory(); } else { @@ -131,7 +132,7 @@ public final class ReferenceCountedOpenSslServerContext extends ReferenceCounted } try { if (trustCertCollection != null) { - trustManagerFactory = buildTrustManagerFactory(trustCertCollection, trustManagerFactory); + trustManagerFactory = buildTrustManagerFactory(trustCertCollection, trustManagerFactory, keyStore); } else if (trustManagerFactory == null) { // Mimic the way SSLContext.getInstance(KeyManager[], null, null) works trustManagerFactory = TrustManagerFactory.getInstance( @@ -147,13 +148,7 @@ public final class ReferenceCountedOpenSslServerContext extends ReferenceCounted // // See https://github.com/netty/netty/issues/5372 - // Use this to prevent an error when running on java < 7 - if (useExtendedTrustManager(manager)) { - SSLContext.setCertVerifyCallback(ctx, new ExtendedTrustManagerVerifyCallback( - engineMap, (X509ExtendedTrustManager) manager)); - } else { - SSLContext.setCertVerifyCallback(ctx, new TrustManagerVerifyCallback(engineMap, manager)); - } + setVerifyCallback(ctx, engineMap, manager); X509Certificate[] issuers = manager.getAcceptedIssuers(); if (issuers != null && issuers.length > 0) { @@ -194,6 +189,17 @@ public final class ReferenceCountedOpenSslServerContext extends ReferenceCounted } } + @SuppressJava6Requirement(reason = "Guarded by java version check") + private static void setVerifyCallback(long ctx, OpenSslEngineMap engineMap, X509TrustManager manager) { + // Use this to prevent an error when running on java < 7 + if (useExtendedTrustManager(manager)) { + SSLContext.setCertVerifyCallback(ctx, new ExtendedTrustManagerVerifyCallback( + engineMap, (X509ExtendedTrustManager) manager)); + } else { + SSLContext.setCertVerifyCallback(ctx, new TrustManagerVerifyCallback(engineMap, manager)); + } + } + private static final class OpenSslServerCertificateCallback implements CertificateCallback { private final OpenSslEngineMap engineMap; private final OpenSslKeyMaterialManager keyManagerHolder; @@ -206,15 +212,17 @@ public final class ReferenceCountedOpenSslServerContext extends ReferenceCounted @Override public void handle(long ssl, byte[] keyTypeBytes, byte[][] asn1DerEncodedPrincipals) throws Exception { final ReferenceCountedOpenSslEngine engine = engineMap.get(ssl); + if (engine == null) { + // Maybe null if destroyed in the meantime. + return; + } try { // For now we just ignore the asn1DerEncodedPrincipals as this is kind of inline with what the // OpenJDK SSLEngineImpl does. keyManagerHolder.setKeyMaterialServerSide(engine); } catch (Throwable cause) { logger.debug("Failed to set the server-side key material", cause); - SSLHandshakeException e = new SSLHandshakeException("General OpenSslEngine problem"); - e.initCause(cause); - engine.handshakeException = e; + engine.initHandshakeException(cause); } } } @@ -234,12 +242,13 @@ public final class ReferenceCountedOpenSslServerContext extends ReferenceCounted } } + @SuppressJava6Requirement(reason = "Usage guarded by java version check") private static final class ExtendedTrustManagerVerifyCallback extends AbstractCertificateVerifier { private final X509ExtendedTrustManager manager; ExtendedTrustManagerVerifyCallback(OpenSslEngineMap engineMap, X509ExtendedTrustManager manager) { super(engineMap); - this.manager = OpenSslTlsv13X509ExtendedTrustManager.wrap(manager, false); + this.manager = OpenSslTlsv13X509ExtendedTrustManager.wrap(manager); } @Override diff --git a/handler/src/main/java/io/netty/handler/ssl/SniHandler.java b/handler/src/main/java/io/netty/handler/ssl/SniHandler.java index cda2bbd..c6a8227 100644 --- a/handler/src/main/java/io/netty/handler/ssl/SniHandler.java +++ b/handler/src/main/java/io/netty/handler/ssl/SniHandler.java @@ -15,6 +15,7 @@ */ package io.netty.handler.ssl; +import io.netty.buffer.ByteBufAllocator; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.DecoderException; import io.netty.util.AsyncMapping; @@ -129,7 +130,7 @@ public class SniHandler extends AbstractSniHandler<SslContext> { protected void replaceHandler(ChannelHandlerContext ctx, String hostname, SslContext sslContext) throws Exception { SslHandler sslHandler = null; try { - sslHandler = sslContext.newHandler(ctx.alloc()); + sslHandler = newSslHandler(sslContext, ctx.alloc()); ctx.pipeline().replace(this, SslHandler.class.getName(), sslHandler); sslHandler = null; } finally { @@ -142,6 +143,14 @@ public class SniHandler extends AbstractSniHandler<SslContext> { } } + /** + * Returns a new {@link SslHandler} using the given {@link SslContext} and {@link ByteBufAllocator}. + * Users may override this method to implement custom behavior. + */ + protected SslHandler newSslHandler(SslContext context, ByteBufAllocator allocator) { + return context.newHandler(allocator); + } + private static final class AsyncMappingAdapter implements AsyncMapping<String, SslContext> { private final Mapping<? super String, ? extends SslContext> mapping; diff --git a/handler/src/main/java/io/netty/handler/ssl/SslClientHelloHandler.java b/handler/src/main/java/io/netty/handler/ssl/SslClientHelloHandler.java new file mode 100644 index 0000000..4bcf349 --- /dev/null +++ b/handler/src/main/java/io/netty/handler/ssl/SslClientHelloHandler.java @@ -0,0 +1,312 @@ +/* + * Copyright 2017 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.ByteBufUtil; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelOutboundHandler; +import io.netty.channel.ChannelPromise; +import io.netty.handler.codec.ByteToMessageDecoder; +import io.netty.handler.codec.DecoderException; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.FutureListener; +import io.netty.util.internal.PlatformDependent; +import io.netty.util.internal.logging.InternalLogger; +import io.netty.util.internal.logging.InternalLoggerFactory; + +import java.net.SocketAddress; +import java.util.List; + +/** + * {@link ByteToMessageDecoder} which allows to be notified once a full {@code ClientHello} was received. + */ +public abstract class SslClientHelloHandler<T> extends ByteToMessageDecoder implements ChannelOutboundHandler { + + private static final InternalLogger logger = + InternalLoggerFactory.getInstance(SslClientHelloHandler.class); + + private boolean handshakeFailed; + private boolean suppressRead; + private boolean readPending; + private ByteBuf handshakeBuffer; + + @Override + protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception { + if (!suppressRead && !handshakeFailed) { + try { + int readerIndex = in.readerIndex(); + int readableBytes = in.readableBytes(); + int handshakeLength = -1; + + // Check if we have enough data to determine the record type and length. + while (readableBytes >= SslUtils.SSL_RECORD_HEADER_LENGTH) { + final int contentType = in.getUnsignedByte(readerIndex); + switch (contentType) { + case SslUtils.SSL_CONTENT_TYPE_CHANGE_CIPHER_SPEC: + // fall-through + case SslUtils.SSL_CONTENT_TYPE_ALERT: + final int len = SslUtils.getEncryptedPacketLength(in, readerIndex); + + // Not an SSL/TLS packet + if (len == SslUtils.NOT_ENCRYPTED) { + handshakeFailed = true; + NotSslRecordException e = new NotSslRecordException( + "not an SSL/TLS record: " + ByteBufUtil.hexDump(in)); + in.skipBytes(in.readableBytes()); + ctx.fireUserEventTriggered(new SniCompletionEvent(e)); + SslUtils.handleHandshakeFailure(ctx, e, true); + throw e; + } + if (len == SslUtils.NOT_ENOUGH_DATA) { + // Not enough data + return; + } + // No ClientHello + select(ctx, null); + return; + case SslUtils.SSL_CONTENT_TYPE_HANDSHAKE: + final int majorVersion = in.getUnsignedByte(readerIndex + 1); + // SSLv3 or TLS + if (majorVersion == 3) { + int packetLength = in.getUnsignedShort(readerIndex + 3) + + SslUtils.SSL_RECORD_HEADER_LENGTH; + + if (readableBytes < packetLength) { + // client hello incomplete; try again to decode once more data is ready. + return; + } else if (packetLength == SslUtils.SSL_RECORD_HEADER_LENGTH) { + select(ctx, null); + return; + } + + final int endOffset = readerIndex + packetLength; + + // Let's check if we already parsed the handshake length or not. + if (handshakeLength == -1) { + if (readerIndex + 4 > endOffset) { + // Need more data to read HandshakeType and handshakeLength (4 bytes) + return; + } + + final int handshakeType = in.getUnsignedByte(readerIndex + + SslUtils.SSL_RECORD_HEADER_LENGTH); + + // Check if this is a clientHello(1) + // See https://tools.ietf.org/html/rfc5246#section-7.4 + if (handshakeType != 1) { + select(ctx, null); + return; + } + + // Read the length of the handshake as it may arrive in fragments + // See https://tools.ietf.org/html/rfc5246#section-7.4 + handshakeLength = in.getUnsignedMedium(readerIndex + + SslUtils.SSL_RECORD_HEADER_LENGTH + 1); + + // Consume handshakeType and handshakeLength (this sums up as 4 bytes) + readerIndex += 4; + packetLength -= 4; + + if (handshakeLength + 4 + SslUtils.SSL_RECORD_HEADER_LENGTH <= packetLength) { + // We have everything we need in one packet. + // Skip the record header + readerIndex += SslUtils.SSL_RECORD_HEADER_LENGTH; + select(ctx, in.retainedSlice(readerIndex, handshakeLength)); + return; + } else { + if (handshakeBuffer == null) { + handshakeBuffer = ctx.alloc().buffer(handshakeLength); + } else { + // Clear the buffer so we can aggregate into it again. + handshakeBuffer.clear(); + } + } + } + + // Combine the encapsulated data in one buffer but not include the SSL_RECORD_HEADER + handshakeBuffer.writeBytes(in, readerIndex + SslUtils.SSL_RECORD_HEADER_LENGTH, + packetLength - SslUtils.SSL_RECORD_HEADER_LENGTH); + readerIndex += packetLength; + readableBytes -= packetLength; + if (handshakeLength <= handshakeBuffer.readableBytes()) { + ByteBuf clientHello = handshakeBuffer.setIndex(0, handshakeLength); + handshakeBuffer = null; + + select(ctx, clientHello); + return; + } + break; + } + // fall-through + default: + // not tls, ssl or application data + select(ctx, null); + return; + } + } + } catch (NotSslRecordException e) { + // Just rethrow as in this case we also closed the channel and this is consistent with SslHandler. + throw e; + } catch (Exception e) { + // unexpected encoding, ignore sni and use default + if (logger.isDebugEnabled()) { + logger.debug("Unexpected client hello packet: " + ByteBufUtil.hexDump(in), e); + } + select(ctx, null); + } + } + } + + private void releaseHandshakeBuffer() { + releaseIfNotNull(handshakeBuffer); + handshakeBuffer = null; + } + + private static void releaseIfNotNull(ByteBuf buffer) { + if (buffer != null) { + buffer.release(); + } + } + + private void select(final ChannelHandlerContext ctx, ByteBuf clientHello) throws Exception { + final Future<T> future; + try { + future = lookup(ctx, clientHello); + if (future.isDone()) { + onLookupComplete(ctx, future); + } else { + suppressRead = true; + final ByteBuf finalClientHello = clientHello; + future.addListener(new FutureListener<T>() { + @Override + public void operationComplete(Future<T> future) { + releaseIfNotNull(finalClientHello); + try { + suppressRead = false; + try { + onLookupComplete(ctx, future); + } catch (DecoderException err) { + ctx.fireExceptionCaught(err); + } catch (Exception cause) { + ctx.fireExceptionCaught(new DecoderException(cause)); + } catch (Throwable cause) { + ctx.fireExceptionCaught(cause); + } + } finally { + if (readPending) { + readPending = false; + ctx.read(); + } + } + } + }); + + // Ownership was transferred to the FutureListener. + clientHello = null; + } + } catch (Throwable cause) { + PlatformDependent.throwException(cause); + } finally { + releaseIfNotNull(clientHello); + } + } + + @Override + protected void handlerRemoved0(ChannelHandlerContext ctx) throws Exception { + releaseHandshakeBuffer(); + + super.handlerRemoved0(ctx); + } + + /** + * Kicks off a lookup for the given {@code ClientHello} and returns a {@link Future} which in turn will + * notify the {@link #onLookupComplete(ChannelHandlerContext, Future)} on completion. + * + * See https://tools.ietf.org/html/rfc5246#section-7.4.1.2 + * + * <pre> + * struct { + * ProtocolVersion client_version; + * Random random; + * SessionID session_id; + * CipherSuite cipher_suites<2..2^16-2>; + * CompressionMethod compression_methods<1..2^8-1>; + * select (extensions_present) { + * case false: + * struct {}; + * case true: + * Extension extensions<0..2^16-1>; + * }; + * } ClientHello; + * </pre> + * + * @see #onLookupComplete(ChannelHandlerContext, Future) + */ + protected abstract Future<T> lookup(ChannelHandlerContext ctx, ByteBuf clientHello) throws Exception; + + /** + * Called upon completion of the {@link #lookup(ChannelHandlerContext, ByteBuf)} {@link Future}. + * + * @see #lookup(ChannelHandlerContext, ByteBuf) + */ + protected abstract void onLookupComplete(ChannelHandlerContext ctx, Future<T> future) throws Exception; + + @Override + public void read(ChannelHandlerContext ctx) throws Exception { + if (suppressRead) { + readPending = true; + } else { + ctx.read(); + } + } + + @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 disconnect(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception { + ctx.disconnect(promise); + } + + @Override + public void close(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception { + ctx.close(promise); + } + + @Override + public void deregister(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception { + ctx.deregister(promise); + } + + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { + ctx.write(msg, promise); + } + + @Override + public void flush(ChannelHandlerContext ctx) throws Exception { + ctx.flush(); + } +} diff --git a/handler/src/main/java/io/netty/handler/ssl/SslContext.java b/handler/src/main/java/io/netty/handler/ssl/SslContext.java index 6c5a6c6..fef2702 100644 --- a/handler/src/main/java/io/netty/handler/ssl/SslContext.java +++ b/handler/src/main/java/io/netty/handler/ssl/SslContext.java @@ -24,6 +24,8 @@ import io.netty.channel.ChannelPipeline; import io.netty.handler.ssl.ApplicationProtocolConfig.Protocol; import io.netty.handler.ssl.ApplicationProtocolConfig.SelectedListenerFailureBehavior; import io.netty.handler.ssl.ApplicationProtocolConfig.SelectorFailureBehavior; +import io.netty.util.AttributeMap; +import io.netty.util.DefaultAttributeMap; import io.netty.util.internal.EmptyArrays; import java.security.Provider; @@ -60,6 +62,7 @@ import java.security.cert.X509Certificate; import java.security.spec.InvalidKeySpecException; import java.security.spec.PKCS8EncodedKeySpec; import java.util.List; +import java.util.concurrent.Executor; /** * A secure socket protocol implementation which acts as a factory for {@link SSLEngine} and {@link SslHandler}. @@ -96,6 +99,7 @@ public abstract class SslContext { } private final boolean startTls; + private final AttributeMap attributes = new DefaultAttributeMap(); /** * Returns the default server-side implementation provider currently in use. @@ -338,7 +342,7 @@ public abstract class SslContext { Iterable<String> ciphers, CipherSuiteFilter cipherFilter, ApplicationProtocolConfig apn, long sessionCacheSize, long sessionTimeout) throws SSLException { return newServerContext(provider, null, null, certChainFile, keyFile, keyPassword, null, - ciphers, cipherFilter, apn, sessionCacheSize, sessionTimeout); + ciphers, cipherFilter, apn, sessionCacheSize, sessionTimeout, KeyStore.getDefaultType()); } /** @@ -381,12 +385,57 @@ public abstract class SslContext { File keyCertChainFile, File keyFile, String keyPassword, KeyManagerFactory keyManagerFactory, Iterable<String> ciphers, CipherSuiteFilter cipherFilter, ApplicationProtocolConfig apn, long sessionCacheSize, long sessionTimeout) throws SSLException { + return newServerContext(provider, trustCertCollectionFile, trustManagerFactory, keyCertChainFile, + keyFile, keyPassword, keyManagerFactory, ciphers, cipherFilter, apn, + sessionCacheSize, sessionTimeout, KeyStore.getDefaultType()); + } + + /** + * Creates a new server-side {@link SslContext}. + * @param provider the {@link SslContext} implementation to use. + * {@code null} to use the current default one. + * @param trustCertCollectionFile an X.509 certificate collection file in PEM format. + * This provides the certificate collection used for mutual authentication. + * {@code null} to use the system default + * @param trustManagerFactory the {@link TrustManagerFactory} that provides the {@link TrustManager}s + * that verifies the certificates sent from clients. + * {@code null} to use the default or the results of parsing + * {@code trustCertCollectionFile}. + * This parameter is ignored if {@code provider} is not {@link SslProvider#JDK}. + * @param keyCertChainFile an X.509 certificate chain file in PEM format + * @param keyFile a PKCS#8 private key file in PEM format + * @param keyPassword the password of the {@code keyFile}. + * {@code null} if it's not password-protected. + * @param keyManagerFactory the {@link KeyManagerFactory} that provides the {@link KeyManager}s + * that is used to encrypt data being sent to clients. + * {@code null} to use the default or the results of parsing + * {@code keyCertChainFile} and {@code keyFile}. + * This parameter is ignored if {@code provider} is not {@link SslProvider#JDK}. + * @param ciphers the cipher suites to enable, in the order of preference. + * {@code null} to use the default cipher suites. + * @param cipherFilter a filter to apply over the supplied list of ciphers + * Only required if {@code provider} is {@link SslProvider#JDK} + * @param apn Provides a means to configure parameters related to application protocol negotiation. + * @param sessionCacheSize the size of the cache used for storing SSL session objects. + * {@code 0} to use the default value. + * @param sessionTimeout the timeout for the cached SSL session objects, in seconds. + * {@code 0} to use the default value. + * @param keyStore the keystore type that should be used + * @return a new server-side {@link SslContext} + */ + static SslContext newServerContext( + SslProvider provider, + File trustCertCollectionFile, TrustManagerFactory trustManagerFactory, + File keyCertChainFile, File keyFile, String keyPassword, KeyManagerFactory keyManagerFactory, + Iterable<String> ciphers, CipherSuiteFilter cipherFilter, ApplicationProtocolConfig apn, + long sessionCacheSize, long sessionTimeout, String keyStore) throws SSLException { try { return newServerContextInternal(provider, null, toX509Certificates(trustCertCollectionFile), trustManagerFactory, toX509Certificates(keyCertChainFile), toPrivateKey(keyFile, keyPassword), keyPassword, keyManagerFactory, ciphers, cipherFilter, apn, - sessionCacheSize, sessionTimeout, ClientAuth.NONE, null, false, false); + sessionCacheSize, sessionTimeout, ClientAuth.NONE, null, + false, false, keyStore); } catch (Exception e) { if (e instanceof SSLException) { throw (SSLException) e; @@ -402,7 +451,7 @@ public abstract class SslContext { X509Certificate[] keyCertChain, PrivateKey key, String keyPassword, KeyManagerFactory keyManagerFactory, Iterable<String> ciphers, CipherSuiteFilter cipherFilter, ApplicationProtocolConfig apn, long sessionCacheSize, long sessionTimeout, ClientAuth clientAuth, String[] protocols, boolean startTls, - boolean enableOcsp) throws SSLException { + boolean enableOcsp, String keyStoreType) throws SSLException { if (provider == null) { provider = defaultServerProvider(); @@ -416,19 +465,19 @@ public abstract class SslContext { return new JdkSslServerContext(sslContextProvider, trustCertCollection, trustManagerFactory, keyCertChain, key, keyPassword, keyManagerFactory, ciphers, cipherFilter, apn, sessionCacheSize, sessionTimeout, - clientAuth, protocols, startTls); + clientAuth, protocols, startTls, keyStoreType); case OPENSSL: verifyNullSslContextProvider(provider, sslContextProvider); return new OpenSslServerContext( trustCertCollection, trustManagerFactory, keyCertChain, key, keyPassword, keyManagerFactory, ciphers, cipherFilter, apn, sessionCacheSize, sessionTimeout, - clientAuth, protocols, startTls, enableOcsp); + clientAuth, protocols, startTls, enableOcsp, keyStoreType); case OPENSSL_REFCNT: verifyNullSslContextProvider(provider, sslContextProvider); return new ReferenceCountedOpenSslServerContext( trustCertCollection, trustManagerFactory, keyCertChain, key, keyPassword, keyManagerFactory, ciphers, cipherFilter, apn, sessionCacheSize, sessionTimeout, - clientAuth, protocols, startTls, enableOcsp); + clientAuth, protocols, startTls, enableOcsp, keyStoreType); default: throw new Error(provider.toString()); } @@ -744,7 +793,8 @@ public abstract class SslContext { toX509Certificates(trustCertCollectionFile), trustManagerFactory, toX509Certificates(keyCertChainFile), toPrivateKey(keyFile, keyPassword), keyPassword, keyManagerFactory, ciphers, cipherFilter, - apn, null, sessionCacheSize, sessionTimeout, false); + apn, null, sessionCacheSize, sessionTimeout, false, + KeyStore.getDefaultType()); } catch (Exception e) { if (e instanceof SSLException) { throw (SSLException) e; @@ -759,7 +809,7 @@ public abstract class SslContext { X509Certificate[] trustCert, TrustManagerFactory trustManagerFactory, X509Certificate[] keyCertChain, PrivateKey key, String keyPassword, KeyManagerFactory keyManagerFactory, Iterable<String> ciphers, CipherSuiteFilter cipherFilter, ApplicationProtocolConfig apn, String[] protocols, - long sessionCacheSize, long sessionTimeout, boolean enableOcsp) throws SSLException { + long sessionCacheSize, long sessionTimeout, boolean enableOcsp, String keyStoreType) throws SSLException { if (provider == null) { provider = defaultClientProvider(); } @@ -770,19 +820,20 @@ public abstract class SslContext { } return new JdkSslClientContext(sslContextProvider, trustCert, trustManagerFactory, keyCertChain, key, keyPassword, - keyManagerFactory, ciphers, cipherFilter, apn, protocols, sessionCacheSize, sessionTimeout); + keyManagerFactory, ciphers, cipherFilter, apn, protocols, sessionCacheSize, + sessionTimeout, keyStoreType); case OPENSSL: verifyNullSslContextProvider(provider, sslContextProvider); return new OpenSslClientContext( trustCert, trustManagerFactory, keyCertChain, key, keyPassword, keyManagerFactory, ciphers, cipherFilter, apn, protocols, sessionCacheSize, sessionTimeout, - enableOcsp); + enableOcsp, keyStoreType); case OPENSSL_REFCNT: verifyNullSslContextProvider(provider, sslContextProvider); return new ReferenceCountedOpenSslClientContext( trustCert, trustManagerFactory, keyCertChain, key, keyPassword, keyManagerFactory, ciphers, cipherFilter, apn, protocols, sessionCacheSize, sessionTimeout, - enableOcsp); + enableOcsp, keyStoreType); default: throw new Error(provider.toString()); } @@ -814,6 +865,13 @@ public abstract class SslContext { this.startTls = startTls; } + /** + * Returns the {@link AttributeMap} that belongs to this {@link SslContext} . + */ + public final AttributeMap attributes() { + return attributes; + } + /** * Returns {@code true} if and only if this context is for server-side. */ @@ -879,6 +937,22 @@ public abstract class SslContext { */ public abstract SSLSessionContext sessionContext(); + /** + * Create a new SslHandler. + * @see #newHandler(ByteBufAllocator, Executor) + */ + public final SslHandler newHandler(ByteBufAllocator alloc) { + return newHandler(alloc, startTls); + } + + /** + * Create a new SslHandler. + * @see #newHandler(ByteBufAllocator) + */ + protected SslHandler newHandler(ByteBufAllocator alloc, boolean startTls) { + return new SslHandler(newEngine(alloc), startTls); + } + /** * Creates a new {@link SslHandler}. * <p>If {@link SslProvider#OPENSSL_REFCNT} is used then the returned {@link SslHandler} will release the engine @@ -900,18 +974,37 @@ public abstract class SslContext { * <a href="https://docs.oracle.com/javase/7/docs/api/javax/net/ssl/SSLEngine.html">SSLEngine javadocs</a> which * limits wrap/unwrap to operate on a single SSL/TLS packet. * @param alloc If supported by the SSLEngine then the SSLEngine will use this to allocate ByteBuf objects. + * @param delegatedTaskExecutor the {@link Executor} that will be used to execute tasks that are returned by + * {@link SSLEngine#getDelegatedTask()}. * @return a new {@link SslHandler} */ - public final SslHandler newHandler(ByteBufAllocator alloc) { - return newHandler(alloc, startTls); + public SslHandler newHandler(ByteBufAllocator alloc, Executor delegatedTaskExecutor) { + return newHandler(alloc, startTls, delegatedTaskExecutor); } /** * Create a new SslHandler. - * @see #newHandler(ByteBufAllocator) + * @see #newHandler(ByteBufAllocator, String, int, boolean, Executor) */ - protected SslHandler newHandler(ByteBufAllocator alloc, boolean startTls) { - return new SslHandler(newEngine(alloc), startTls); + protected SslHandler newHandler(ByteBufAllocator alloc, boolean startTls, Executor executor) { + return new SslHandler(newEngine(alloc), startTls, executor); + } + + /** + * Creates a new {@link SslHandler} + * + * @see #newHandler(ByteBufAllocator, String, int, Executor) + */ + public final SslHandler newHandler(ByteBufAllocator alloc, String peerHost, int peerPort) { + return newHandler(alloc, peerHost, peerPort, startTls); + } + + /** + * Create a new SslHandler. + * @see #newHandler(ByteBufAllocator, String, int, boolean, Executor) + */ + protected SslHandler newHandler(ByteBufAllocator alloc, String peerHost, int peerPort, boolean startTls) { + return new SslHandler(newEngine(alloc, peerHost, peerPort), startTls); } /** @@ -937,19 +1030,19 @@ public abstract class SslContext { * @param alloc If supported by the SSLEngine then the SSLEngine will use this to allocate ByteBuf objects. * @param peerHost the non-authoritative name of the host * @param peerPort the non-authoritative port + * @param delegatedTaskExecutor the {@link Executor} that will be used to execute tasks that are returned by + * {@link SSLEngine#getDelegatedTask()}. * * @return a new {@link SslHandler} */ - public final SslHandler newHandler(ByteBufAllocator alloc, String peerHost, int peerPort) { - return newHandler(alloc, peerHost, peerPort, startTls); + public SslHandler newHandler(ByteBufAllocator alloc, String peerHost, int peerPort, + Executor delegatedTaskExecutor) { + return newHandler(alloc, peerHost, peerPort, startTls, delegatedTaskExecutor); } - /** - * Create a new SslHandler. - * @see #newHandler(ByteBufAllocator, String, int, boolean) - */ - protected SslHandler newHandler(ByteBufAllocator alloc, String peerHost, int peerPort, boolean startTls) { - return new SslHandler(newEngine(alloc, peerHost, peerPort), startTls); + protected SslHandler newHandler(ByteBufAllocator alloc, String peerHost, int peerPort, boolean startTls, + Executor delegatedTaskExecutor) { + return new SslHandler(newEngine(alloc, peerHost, peerPort), startTls, delegatedTaskExecutor); } /** @@ -990,16 +1083,21 @@ public abstract class SslContext { /** * Generates a new {@link KeyStore}. * - * @param certChain a X.509 certificate chain + * @param certChain an X.509 certificate chain * @param key a PKCS#8 private key * @param keyPasswordChars the password of the {@code keyFile}. * {@code null} if it's not password-protected. + * @param keyStoreType The KeyStore Type you want to use * @return generated {@link KeyStore}. */ - static KeyStore buildKeyStore(X509Certificate[] certChain, PrivateKey key, char[] keyPasswordChars) + static KeyStore buildKeyStore(X509Certificate[] certChain, PrivateKey key, + char[] keyPasswordChars, String keyStoreType) throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException { - KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType()); + if (keyStoreType == null) { + keyStoreType = KeyStore.getDefaultType(); + } + KeyStore ks = KeyStore.getInstance(keyStoreType); ks.load(null, null); ks.setKeyEntry(ALIAS, key, keyPasswordChars, certChain); return ks; @@ -1059,9 +1157,22 @@ public abstract class SslContext { protected static TrustManagerFactory buildTrustManagerFactory( File certChainFile, TrustManagerFactory trustManagerFactory) throws NoSuchAlgorithmException, CertificateException, KeyStoreException, IOException { + return buildTrustManagerFactory(certChainFile, trustManagerFactory, KeyStore.getDefaultType()); + } + + /** + * Build a {@link TrustManagerFactory} from a certificate chain file. + * @param certChainFile The certificate file to build from. + * @param trustManagerFactory The existing {@link TrustManagerFactory} that will be used if not {@code null}. + * @param keyType The KeyStore Type you want to use + * @return A {@link TrustManagerFactory} which contains the certificates in {@code certChainFile} + */ + static TrustManagerFactory buildTrustManagerFactory( + File certChainFile, TrustManagerFactory trustManagerFactory, String keyType) + throws NoSuchAlgorithmException, CertificateException, KeyStoreException, IOException { X509Certificate[] x509Certs = toX509Certificates(certChainFile); - return buildTrustManagerFactory(x509Certs, trustManagerFactory); + return buildTrustManagerFactory(x509Certs, trustManagerFactory, keyType); } static X509Certificate[] toX509Certificates(File file) throws CertificateException { @@ -1105,9 +1216,12 @@ public abstract class SslContext { } static TrustManagerFactory buildTrustManagerFactory( - X509Certificate[] certCollection, TrustManagerFactory trustManagerFactory) + X509Certificate[] certCollection, TrustManagerFactory trustManagerFactory, String keyStoreType) throws NoSuchAlgorithmException, CertificateException, KeyStoreException, IOException { - final KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType()); + if (keyStoreType == null) { + keyStoreType = KeyStore.getDefaultType(); + } + final KeyStore ks = KeyStore.getInstance(keyStoreType); ks.load(null, null); int i = 1; @@ -1143,10 +1257,22 @@ public abstract class SslContext { } static KeyManagerFactory buildKeyManagerFactory(X509Certificate[] certChain, PrivateKey key, String keyPassword, - KeyManagerFactory kmf) + KeyManagerFactory kmf, String keyStoreType) throws UnrecoverableKeyException, KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException { - return buildKeyManagerFactory(certChain, KeyManagerFactory.getDefaultAlgorithm(), key, keyPassword, kmf); + return buildKeyManagerFactory(certChain, KeyManagerFactory.getDefaultAlgorithm(), key, + keyPassword, kmf, keyStoreType); + } + + static KeyManagerFactory buildKeyManagerFactory(X509Certificate[] certChainFile, + String keyAlgorithm, PrivateKey key, + String keyPassword, KeyManagerFactory kmf, + String keyStore) + throws KeyStoreException, NoSuchAlgorithmException, IOException, + CertificateException, UnrecoverableKeyException { + char[] keyPasswordChars = keyStorePassword(keyPassword); + KeyStore ks = buildKeyStore(certChainFile, key, keyPasswordChars, keyStore); + return buildKeyManagerFactory(ks, keyAlgorithm, keyPasswordChars, kmf); } static KeyManagerFactory buildKeyManagerFactory(X509Certificate[] certChainFile, @@ -1155,7 +1281,7 @@ public abstract class SslContext { throws KeyStoreException, NoSuchAlgorithmException, IOException, CertificateException, UnrecoverableKeyException { char[] keyPasswordChars = keyStorePassword(keyPassword); - KeyStore ks = buildKeyStore(certChainFile, key, keyPasswordChars); + KeyStore ks = buildKeyStore(certChainFile, key, keyPasswordChars, KeyStore.getDefaultType()); return buildKeyManagerFactory(ks, keyAlgorithm, keyPasswordChars, kmf); } diff --git a/handler/src/main/java/io/netty/handler/ssl/SslContextBuilder.java b/handler/src/main/java/io/netty/handler/ssl/SslContextBuilder.java index ae21440..f7794a9 100644 --- a/handler/src/main/java/io/netty/handler/ssl/SslContextBuilder.java +++ b/handler/src/main/java/io/netty/handler/ssl/SslContextBuilder.java @@ -16,20 +16,26 @@ package io.netty.handler.ssl; -import static io.netty.util.internal.ObjectUtil.checkNotNull; - import io.netty.util.internal.UnstableApi; -import java.security.Provider; +import javax.net.ssl.KeyManager; import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLException; +import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; - import java.io.File; import java.io.InputStream; +import java.security.KeyStore; import java.security.PrivateKey; +import java.security.Provider; import java.security.cert.X509Certificate; -import javax.net.ssl.SSLEngine; +import java.util.ArrayList; +import java.util.List; + +import static io.netty.util.internal.EmptyArrays.EMPTY_STRINGS; +import static io.netty.util.internal.EmptyArrays.EMPTY_X509_CERTIFICATES; +import static io.netty.util.internal.ObjectUtil.checkNotNull; /** * Builder for configuring a new SslContext for creation. @@ -76,6 +82,17 @@ public final class SslContextBuilder { return new SslContextBuilder(true).keyManager(key, keyCertChain); } + /** + * Creates a builder for new server-side {@link SslContext}. + * + * @param key a PKCS#8 private key + * @param keyCertChain the X.509 certificate chain + * @see #keyManager(PrivateKey, X509Certificate[]) + */ + public static SslContextBuilder forServer(PrivateKey key, Iterable<? extends X509Certificate> keyCertChain) { + return forServer(key, toArray(keyCertChain, EMPTY_X509_CERTIFICATES)); + } + /** * Creates a builder for new server-side {@link SslContext}. * @@ -118,6 +135,20 @@ public final class SslContextBuilder { return new SslContextBuilder(true).keyManager(key, keyPassword, keyCertChain); } + /** + * Creates a builder for new server-side {@link SslContext}. + * + * @param key a PKCS#8 private key + * @param keyCertChain the X.509 certificate chain + * @param keyPassword the password of the {@code keyFile}, or {@code null} if it's not + * password-protected + * @see #keyManager(File, File, String) + */ + public static SslContextBuilder forServer( + PrivateKey key, String keyPassword, Iterable<? extends X509Certificate> keyCertChain) { + return forServer(key, keyPassword, toArray(keyCertChain, EMPTY_X509_CERTIFICATES)); + } + /** * Creates a builder for new server-side {@link SslContext}. * @@ -131,6 +162,15 @@ public final class SslContextBuilder { return new SslContextBuilder(true).keyManager(keyManagerFactory); } + /** + * Creates a builder for new server-side {@link SslContext} with {@link KeyManager}. + * + * @param KeyManager non-{@code null} KeyManager for server's private key + */ + public static SslContextBuilder forServer(KeyManager keyManager) { + return new SslContextBuilder(true).keyManager(keyManager); + } + private final boolean forServer; private SslProvider provider; private Provider sslContextProvider; @@ -149,6 +189,7 @@ public final class SslContextBuilder { private String[] protocols; private boolean startTls; private boolean enableOcsp; + private String keyStoreType = KeyStore.getDefaultType(); private SslContextBuilder(boolean forServer) { this.forServer = forServer; @@ -162,6 +203,14 @@ public final class SslContextBuilder { return this; } + /** + * Sets the {@link KeyStore} type that should be used. {@code null} uses the default one. + */ + public SslContextBuilder keyStoreType(String keyStoreType) { + this.keyStoreType = keyStoreType; + return this; + } + /** * The SSLContext {@link Provider} to use. {@code null} uses the default one. This is only * used with {@link SslProvider#JDK}. @@ -205,6 +254,13 @@ public final class SslContextBuilder { return this; } + /** + * Trusted certificates for verifying the remote endpoint's certificate, {@code null} uses the system default. + */ + public SslContextBuilder trustManager(Iterable<? extends X509Certificate> trustCertCollection) { + return trustManager(toArray(trustCertCollection, EMPTY_X509_CERTIFICATES)); + } + /** * Trusted manager for verifying the remote endpoint's certificate. {@code null} uses the system default. */ @@ -214,6 +270,19 @@ public final class SslContextBuilder { return this; } + /** + * A single trusted manager for verifying the remote endpoint's certificate. + * This is helpful when custom implementation of {@link TrustManager} is needed. + * Internally, a simple wrapper of {@link TrustManagerFactory} that only produces this + * specified {@link TrustManager} will be created, thus all the requirements specified in + * {@link #trustManager(TrustManagerFactory trustManagerFactory)} also apply here. + */ + public SslContextBuilder trustManager(TrustManager trustManager) { + this.trustManagerFactory = new TrustManagerFactoryWrapper(trustManager); + trustCertCollection = null; + return this; + } + /** * Identifying certificate for this host. {@code keyCertChainFile} and {@code keyFile} may * be {@code null} for client contexts, which disables mutual authentication. @@ -247,6 +316,17 @@ public final class SslContextBuilder { return keyManager(key, null, keyCertChain); } + /** + * Identifying certificate for this host. {@code keyCertChain} and {@code key} may + * be {@code null} for client contexts, which disables mutual authentication. + * + * @param key a PKCS#8 private key + * @param keyCertChain an X.509 certificate chain + */ + public SslContextBuilder keyManager(PrivateKey key, Iterable<? extends X509Certificate> keyCertChain) { + return keyManager(key, toArray(keyCertChain, EMPTY_X509_CERTIFICATES)); + } + /** * Identifying certificate for this host. {@code keyCertChainFile} and {@code keyFile} may * be {@code null} for client contexts, which disables mutual authentication. @@ -331,6 +411,20 @@ public final class SslContextBuilder { return this; } + /** + * Identifying certificate for this host. {@code keyCertChain} and {@code key} may + * be {@code null} for client contexts, which disables mutual authentication. + * + * @param key a PKCS#8 private key file + * @param keyPassword the password of the {@code key}, or {@code null} if it's not + * password-protected + * @param keyCertChain an X.509 certificate chain + */ + public SslContextBuilder keyManager(PrivateKey key, String keyPassword, + Iterable<? extends X509Certificate> keyCertChain) { + return keyManager(key, keyPassword, toArray(keyCertChain, EMPTY_X509_CERTIFICATES)); + } + /** * Identifying manager for this host. {@code keyManagerFactory} may be {@code null} for * client contexts, which disables mutual authentication. Using a {@link KeyManagerFactory} @@ -353,6 +447,28 @@ public final class SslContextBuilder { return this; } + /** + * A single key manager managing the identity information of this host. + * This is helpful when custom implementation of {@link KeyManager} is needed. + * Internally, a wrapper of {@link KeyManagerFactory} that only produces this specified + * {@link KeyManager} will be created, thus all the requirements specified in + * {@link #keyManager(KeyManagerFactory keyManagerFactory)} also apply here. + */ + public SslContextBuilder keyManager(KeyManager keyManager) { + if (forServer) { + checkNotNull(keyManager, "keyManager required for servers"); + } + if (keyManager != null) { + this.keyManagerFactory = new KeyManagerFactoryWrapper(keyManager); + } else { + this.keyManagerFactory = null; + } + keyCertChain = null; + key = null; + keyPassword = null; + return this; + } + /** * The cipher suites to enable, in the order of preference. {@code null} to use default * cipher suites. @@ -367,9 +483,8 @@ public final class SslContextBuilder { * cipher suites will be used. */ public SslContextBuilder ciphers(Iterable<String> ciphers, CipherSuiteFilter cipherFilter) { - checkNotNull(cipherFilter, "cipherFilter"); + this.cipherFilter = checkNotNull(cipherFilter, "cipherFilter"); this.ciphers = ciphers; - this.cipherFilter = cipherFilter; return this; } @@ -417,6 +532,15 @@ public final class SslContextBuilder { return this; } + /** + * The TLS protocol versions to enable. + * @param protocols The protocols to enable, or {@code null} to enable the default protocols. + * @see SSLEngine#setEnabledCipherSuites(String[]) + */ + public SslContextBuilder protocols(Iterable<String> protocols) { + return protocols(toArray(protocols, EMPTY_STRINGS)); + } + /** * {@code true} if the first write request shouldn't be encrypted. */ @@ -447,11 +571,22 @@ public final class SslContextBuilder { return SslContext.newServerContextInternal(provider, sslContextProvider, trustCertCollection, trustManagerFactory, keyCertChain, key, keyPassword, keyManagerFactory, ciphers, cipherFilter, apn, sessionCacheSize, sessionTimeout, clientAuth, protocols, startTls, - enableOcsp); + enableOcsp, keyStoreType); } else { return SslContext.newClientContextInternal(provider, sslContextProvider, trustCertCollection, trustManagerFactory, keyCertChain, key, keyPassword, keyManagerFactory, - ciphers, cipherFilter, apn, protocols, sessionCacheSize, sessionTimeout, enableOcsp); + ciphers, cipherFilter, apn, protocols, sessionCacheSize, sessionTimeout, enableOcsp, keyStoreType); + } + } + + private static <T> T[] toArray(Iterable<? extends T> iterable, T[] prototype) { + if (iterable == null) { + return null; + } + final List<T> list = new ArrayList<T>(); + for (T element : iterable) { + list.add(element); } + return list.toArray(prototype); } } 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 fe53373..b279abf 100644 --- a/handler/src/main/java/io/netty/handler/ssl/SslHandler.java +++ b/handler/src/main/java/io/netty/handler/ssl/SslHandler.java @@ -33,6 +33,7 @@ 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.DecoderException; import io.netty.handler.codec.UnsupportedMessageTypeException; import io.netty.util.ReferenceCountUtil; import io.netty.util.ReferenceCounted; @@ -43,8 +44,8 @@ import io.netty.util.concurrent.FutureListener; import io.netty.util.concurrent.ImmediateExecutor; import io.netty.util.concurrent.Promise; import io.netty.util.concurrent.PromiseNotifier; +import io.netty.util.internal.ObjectUtil; 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; @@ -55,10 +56,9 @@ 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.RejectedExecutionException; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.regex.Pattern; @@ -173,18 +173,6 @@ public class SslHandler extends ByteToMessageDecoder implements ChannelOutboundH 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(...)"); - /** * <a href="https://tools.ietf.org/html/rfc5246#section-6.2">2^14</a> which is the maximum sized plaintext chunk * allowed by the TLS RFC. @@ -222,14 +210,10 @@ public class SslHandler extends ByteToMessageDecoder implements ChannelOutboundH } @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); + ByteBuf allocateWrapBuffer(SslHandler handler, ByteBufAllocator allocator, + int pendingBytes, int numComponents) { + return allocator.directBuffer(((ReferenceCountedOpenSslEngine) handler.engine) + .calculateMaxLengthForWrap(pendingBytes, numComponents)); } @Override @@ -271,8 +255,10 @@ public class SslHandler extends ByteToMessageDecoder implements ChannelOutboundH } @Override - int calculateWrapBufferCapacity(SslHandler handler, int pendingBytes, int numComponents) { - return ((ConscryptAlpnSslEngine) handler.engine).calculateOutNetBufSize(pendingBytes, numComponents); + ByteBuf allocateWrapBuffer(SslHandler handler, ByteBufAllocator allocator, + int pendingBytes, int numComponents) { + return allocator.directBuffer( + ((ConscryptAlpnSslEngine) handler.engine).calculateOutNetBufSize(pendingBytes, numComponents)); } @Override @@ -314,8 +300,15 @@ public class SslHandler extends ByteToMessageDecoder implements ChannelOutboundH } @Override - int calculateWrapBufferCapacity(SslHandler handler, int pendingBytes, int numComponents) { - return handler.engine.getSession().getPacketBufferSize(); + ByteBuf allocateWrapBuffer(SslHandler handler, ByteBufAllocator allocator, + int pendingBytes, int numComponents) { + // As for the JDK SSLEngine we always need to allocate buffers of the size required by the SSLEngine + // (normally ~16KB). This is required even if the amount of data to encrypt is very small. Use heap + // buffers to reduce the native memory usage. + // + // Beside this the JDK SSLEngine also (as of today) will do an extra heap to direct buffer copy + // if a direct buffer is used as its internals operate on byte[]. + return allocator.heapBuffer(handler.engine.getSession().getPacketBufferSize()); } @Override @@ -339,23 +332,21 @@ public class SslHandler extends ByteToMessageDecoder implements ChannelOutboundH 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); + abstract ByteBuf allocateWrapBuffer(SslHandler handler, ByteBufAllocator allocator, + int pendingBytes, int numComponents); + // BEGIN Platform-dependent flags /** - * {@code true} if and only if {@link SSLEngine} expects a direct buffer. + * {@code true} if and only if {@link SSLEngine} expects a direct buffer and so if a heap buffer + * is given will make an extra memory copy. */ final boolean wantsDirectBuffer; @@ -390,6 +381,7 @@ public class SslHandler extends ByteToMessageDecoder implements ChannelOutboundH private boolean flushedBeforeHandshake; private boolean readDuringHandshake; private boolean handshakeStarted; + private SslHandlerCoalescingBufferQueue pendingUnencryptedWrites; private Promise<Channel> handshakePromise = new LazyChannelPromise(); private final LazyChannelPromise sslClosePromise = new LazyChannelPromise(); @@ -402,6 +394,7 @@ public class SslHandler extends ByteToMessageDecoder implements ChannelOutboundH private boolean outboundClosed; private boolean closeNotify; + private boolean processTask; private int packetLength; @@ -417,7 +410,7 @@ public class SslHandler extends ByteToMessageDecoder implements ChannelOutboundH volatile int wrapDataSize = MAX_PLAINTEXT_LENGTH; /** - * Creates a new instance. + * Creates a new instance which runs all delegated tasks directly on the {@link EventExecutor}. * * @param engine the {@link SSLEngine} this handler will use */ @@ -426,39 +419,40 @@ public class SslHandler extends ByteToMessageDecoder implements ChannelOutboundH } /** - * Creates a new instance. + * Creates a new instance which runs all delegated tasks directly on the {@link EventExecutor}. * * @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. + * Creates a new instance. + * + * @param engine the {@link SSLEngine} this handler will use + * @param delegatedTaskExecutor the {@link Executor} that will be used to execute tasks that are returned by + * {@link SSLEngine#getDelegatedTask()}. */ - @Deprecated public SslHandler(SSLEngine engine, Executor delegatedTaskExecutor) { this(engine, false, delegatedTaskExecutor); } /** - * @deprecated Use {@link #SslHandler(SSLEngine, boolean)} instead. + * 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} + * @param delegatedTaskExecutor the {@link Executor} that will be used to execute tasks that are returned by + * {@link SSLEngine#getDelegatedTask()}. */ - @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; + this.engine = ObjectUtil.checkNotNull(engine, "engine"); + this.delegatedTaskExecutor = ObjectUtil.checkNotNull(delegatedTaskExecutor, "delegatedTaskExecutor"); engineType = SslEngineType.forEngine(engine); - this.delegatedTaskExecutor = delegatedTaskExecutor; this.startTls = startTls; this.jdkCompatibilityMode = engineType.jdkCompatibilityMode(engine); setCumulator(engineType.cumulator); @@ -469,10 +463,7 @@ public class SslHandler extends ByteToMessageDecoder implements ChannelOutboundH } public void setHandshakeTimeout(long handshakeTimeout, TimeUnit unit) { - if (unit == null) { - throw new NullPointerException("unit"); - } - + ObjectUtil.checkNotNull(unit, "unit"); setHandshakeTimeoutMillis(unit.toMillis(handshakeTimeout)); } @@ -774,6 +765,10 @@ public class SslHandler extends ByteToMessageDecoder implements ChannelOutboundH return; } + if (processTask) { + return; + } + try { wrapAndFlush(ctx); } catch (Throwable cause) { @@ -813,7 +808,7 @@ public class SslHandler extends ByteToMessageDecoder implements ChannelOutboundH 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()) { + outer: while (!ctx.isRemoved()) { promise = ctx.newPromise(); buf = wrapDataSize > 0 ? pendingUnencryptedWrites.remove(alloc, wrapDataSize, promise) : @@ -831,11 +826,12 @@ public class SslHandler extends ByteToMessageDecoder implements ChannelOutboundH if (result.getStatus() == Status.CLOSED) { buf.release(); buf = null; - promise.tryFailure(SSLENGINE_CLOSED); + SSLException exception = new SSLException("SSLEngine closed already"); + promise.tryFailure(exception); promise = null; // SSLEngine has been closed already. // Any further write attempts should be denied. - pendingUnencryptedWrites.releaseAndFailAll(ctx, SSLENGINE_CLOSED); + pendingUnencryptedWrites.releaseAndFailAll(ctx, exception); return; } else { if (buf.isReadable()) { @@ -850,7 +846,11 @@ public class SslHandler extends ByteToMessageDecoder implements ChannelOutboundH switch (result.getHandshakeStatus()) { case NEED_TASK: - runDelegatedTasks(); + if (!runDelegatedTasks(inUnwrap)) { + // We scheduled a task on the delegatingTaskExecutor, so stop processing as we will + // resume once the task completes. + break outer; + } break; case FINISHED: setHandshakeSuccess(); @@ -858,11 +858,25 @@ public class SslHandler extends ByteToMessageDecoder implements ChannelOutboundH case NOT_HANDSHAKING: setHandshakeSuccessIfStillHandshaking(); // deliberate fall-through - case NEED_WRAP: - finishWrap(ctx, out, promise, inUnwrap, false); + case NEED_WRAP: { + ChannelPromise p = promise; + + // Null out the promise so it is not reused in the finally block in the cause of + // finishWrap(...) throwing. promise = null; - out = null; + final ByteBuf b; + + if (out.isReadable()) { + // There is something in the out buffer. Ensure we null it out so it is not re-used. + b = out; + out = null; + } else { + // If out is not readable we can re-use it and so save an extra allocation + b = null; + } + finishWrap(ctx, b, p, inUnwrap, false); break; + } case NEED_UNWRAP: needUnwrap = true; return; @@ -913,13 +927,13 @@ public class SslHandler extends ByteToMessageDecoder implements ChannelOutboundH * {@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 { + private boolean wrapNonAppData(final 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()) { + outer: 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. @@ -929,19 +943,32 @@ public class SslHandler extends ByteToMessageDecoder implements ChannelOutboundH SSLEngineResult result = wrap(alloc, engine, Unpooled.EMPTY_BUFFER, out); if (result.bytesProduced() > 0) { - ctx.write(out); + ctx.write(out).addListener(new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture future) { + Throwable cause = future.cause(); + if (cause != null) { + setHandshakeFailureTransportFailure(ctx, cause); + } + } + }); if (inUnwrap) { needsFlush = true; } out = null; } - switch (result.getHandshakeStatus()) { + HandshakeStatus status = result.getHandshakeStatus(); + switch (status) { case FINISHED: setHandshakeSuccess(); return false; case NEED_TASK: - runDelegatedTasks(); + if (!runDelegatedTasks(inUnwrap)) { + // We scheduled a task on the delegatingTaskExecutor, so stop processing as we will + // resume once the task completes. + break outer; + } break; case NEED_UNWRAP: if (inUnwrap) { @@ -967,7 +994,9 @@ public class SslHandler extends ByteToMessageDecoder implements ChannelOutboundH throw new IllegalStateException("Unknown handshake status: " + result.getHandshakeStatus()); } - if (result.bytesProduced() == 0) { + // Check if did not produce any bytes and if so break out of the loop, but only if we did not process + // a task as last action. It's fine to not produce any data as part of executing a task. + if (result.bytesProduced() == 0 && status != HandshakeStatus.NEED_TASK) { break; } @@ -1044,12 +1073,13 @@ public class SslHandler extends ByteToMessageDecoder implements ChannelOutboundH @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { + ClosedChannelException exception = new ClosedChannelException(); // Make sure to release SSLEngine, // and notify the handshake future if the connection has been closed during handshake. - setHandshakeFailure(ctx, CHANNEL_CLOSED, !outboundClosed, handshakeStarted, false); + setHandshakeFailure(ctx, exception, !outboundClosed, handshakeStarted, false); // Ensure we always notify the sslClosePromise as well - notifyClosePromise(CHANNEL_CLOSED); + notifyClosePromise(exception); super.channelInactive(ctx); } @@ -1133,8 +1163,10 @@ public class SslHandler extends ByteToMessageDecoder implements ChannelOutboundH return true; } } catch (Throwable cause) { - logger.debug("Unexpected exception while loading class {} classname {}", - getClass(), classname, cause); + if (logger.isDebugEnabled()) { + logger.debug("Unexpected exception while loading class {} classname {}", + getClass(), classname, cause); + } } } } @@ -1243,6 +1275,9 @@ public class SslHandler extends ByteToMessageDecoder implements ChannelOutboundH @Override protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws SSLException { + if (processTask) { + return; + } if (jdkCompatibilityMode) { decodeJdkCompatible(ctx, in); } else { @@ -1252,6 +1287,10 @@ public class SslHandler extends ByteToMessageDecoder implements ChannelOutboundH @Override public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { + channelReadComplete0(ctx); + } + + private void channelReadComplete0(ChannelHandlerContext ctx) { // Discard bytes of the cumulation buffer if needed. discardSomeReadBytes(); @@ -1366,7 +1405,16 @@ public class SslHandler extends ByteToMessageDecoder implements ChannelOutboundH } break; case NEED_TASK: - runDelegatedTasks(); + if (!runDelegatedTasks(true)) { + // We scheduled a task on the delegatingTaskExecutor, so stop processing as we will + // resume once the task completes. + // + // We break out of the loop only and do NOT return here as we still may need to notify + // about the closure of the SSLEngine. + // + wrapLater = false; + break unwrapLoop; + } break; case FINISHED: setHandshakeSuccess(); @@ -1401,7 +1449,9 @@ public class SslHandler extends ByteToMessageDecoder implements ChannelOutboundH throw new IllegalStateException("unknown handshake status: " + handshakeStatus); } - if (status == Status.BUFFER_UNDERFLOW || consumed == 0 && produced == 0) { + if (status == Status.BUFFER_UNDERFLOW || + // If we processed NEED_TASK we should try again even we did not consume or produce anything. + handshakeStatus != HandshakeStatus.NEED_TASK && consumed == 0 && produced == 0) { if (handshakeStatus == HandshakeStatus.NEED_UNWRAP) { // The underlying engine is starving so we need to feed it with more data. // See https://github.com/netty/netty/pull/5039 @@ -1447,65 +1497,239 @@ public class SslHandler extends ByteToMessageDecoder implements ChannelOutboundH out.nioBuffer(index, len); } - /** - * Fetches all delegated tasks from the {@link SSLEngine} and runs them via the {@link #delegatedTaskExecutor}. - * If the {@link #delegatedTaskExecutor} is {@link ImmediateExecutor}, just call {@link Runnable#run()} directly - * instead of using {@link Executor#execute(Runnable)}. Otherwise, run the tasks via - * the {@link #delegatedTaskExecutor} and wait until the tasks are finished. - */ - private void runDelegatedTasks() { - if (delegatedTaskExecutor == ImmediateExecutor.INSTANCE) { - for (;;) { - Runnable task = engine.getDelegatedTask(); - if (task == null) { - break; - } + private static boolean inEventLoop(Executor executor) { + return executor instanceof EventExecutor && ((EventExecutor) executor).inEventLoop(); + } - task.run(); + private static void runAllDelegatedTasks(SSLEngine engine) { + for (;;) { + Runnable task = engine.getDelegatedTask(); + if (task == null) { + return; } + task.run(); + } + } + + /** + * Will either run the delegated task directly calling {@link Runnable#run()} and return {@code true} or will + * offload the delegated task using {@link Executor#execute(Runnable)} and return {@code false}. + * + * If the task is offloaded it will take care to resume its work on the {@link EventExecutor} once there are no + * more tasks to process. + */ + private boolean runDelegatedTasks(boolean inUnwrap) { + if (delegatedTaskExecutor == ImmediateExecutor.INSTANCE || inEventLoop(delegatedTaskExecutor)) { + // We should run the task directly in the EventExecutor thread and not offload at all. + runAllDelegatedTasks(engine); + return true; } else { - final List<Runnable> tasks = new ArrayList<Runnable>(2); - for (;;) { - final Runnable task = engine.getDelegatedTask(); - if (task == null) { - break; + executeDelegatedTasks(inUnwrap); + return false; + } + } + + private void executeDelegatedTasks(boolean inUnwrap) { + processTask = true; + try { + delegatedTaskExecutor.execute(new SslTasksRunner(inUnwrap)); + } catch (RejectedExecutionException e) { + processTask = false; + throw e; + } + } + + /** + * {@link Runnable} that will be scheduled on the {@code delegatedTaskExecutor} and will take care + * of resume work on the {@link EventExecutor} once the task was executed. + */ + private final class SslTasksRunner implements Runnable { + private final boolean inUnwrap; + + SslTasksRunner(boolean inUnwrap) { + this.inUnwrap = inUnwrap; + } + + // Handle errors which happened during task processing. + private void taskError(Throwable e) { + if (inUnwrap) { + // As the error happened while the task was scheduled as part of unwrap(...) we also need to ensure + // we fire it through the pipeline as inbound error to be consistent with what we do in decode(...). + // + // This will also ensure we fail the handshake future and flush all produced data. + try { + handleUnwrapThrowable(ctx, e); + } catch (Throwable cause) { + safeExceptionCaught(cause); } + } else { + setHandshakeFailure(ctx, e); + forceFlush(ctx); + } + } - tasks.add(task); + // Try to call exceptionCaught(...) + private void safeExceptionCaught(Throwable cause) { + try { + exceptionCaught(ctx, wrapIfNeeded(cause)); + } catch (Throwable error) { + ctx.fireExceptionCaught(error); } + } - if (tasks.isEmpty()) { - return; + private Throwable wrapIfNeeded(Throwable cause) { + if (!inUnwrap) { + // If we are not in unwrap(...) we can just rethrow without wrapping at all. + return cause; + } + // As the exception would have been triggered by an inbound operation we will need to wrap it in a + // DecoderException to mimic what a decoder would do when decode(...) throws. + return cause instanceof DecoderException ? cause : new DecoderException(cause); + } + + private void tryDecodeAgain() { + try { + channelRead(ctx, Unpooled.EMPTY_BUFFER); + } catch (Throwable cause) { + safeExceptionCaught(cause); + } finally { + // As we called channelRead(...) we also need to call channelReadComplete(...) which + // will ensure we either call ctx.fireChannelReadComplete() or will trigger a ctx.read() if + // more data is needed. + channelReadComplete0(ctx); } + } - final CountDownLatch latch = new CountDownLatch(1); - delegatedTaskExecutor.execute(new Runnable() { - @Override - public void run() { - try { - for (Runnable task: tasks) { - task.run(); + /** + * Executed after the wrapped {@code task} was executed via {@code delegatedTaskExecutor} to resume work + * on the {@link EventExecutor}. + */ + private void resumeOnEventExecutor() { + assert ctx.executor().inEventLoop(); + + processTask = false; + + try { + HandshakeStatus status = engine.getHandshakeStatus(); + switch (status) { + // There is another task that needs to be executed and offloaded to the delegatingTaskExecutor. + case NEED_TASK: + executeDelegatedTasks(inUnwrap); + + break; + + // The handshake finished, lets notify about the completion of it and resume processing. + case FINISHED: + setHandshakeSuccess(); + + // deliberate fall-through + + // Not handshaking anymore, lets notify about the completion if not done yet and resume processing. + case NOT_HANDSHAKING: + setHandshakeSuccessIfStillHandshaking(); + try { + // Lets call wrap to ensure we produce the alert if there is any pending and also to + // ensure we flush any queued data.. + wrap(ctx, inUnwrap); + } catch (Throwable e) { + taskError(e); + return; + } + if (inUnwrap) { + // If we were in the unwrap call when the task was processed we should also try to unwrap + // non app data first as there may not anything left in the inbound buffer to process. + unwrapNonAppData(ctx); } - } catch (Exception e) { - ctx.fireExceptionCaught(e); - } finally { - latch.countDown(); - } - } - }); - boolean interrupted = false; - while (latch.getCount() != 0) { - try { - latch.await(); - } catch (InterruptedException e) { - // Interrupt later. - interrupted = true; + // Flush now as we may have written some data as part of the wrap call. + forceFlush(ctx); + + tryDecodeAgain(); + break; + + // We need more data so lets try to unwrap first and then call decode again which will feed us + // with buffered data (if there is any). + case NEED_UNWRAP: + try { + unwrapNonAppData(ctx); + } catch (SSLException e) { + handleUnwrapThrowable(ctx, e); + return; + } + tryDecodeAgain(); + break; + + // To make progress we need to call SSLEngine.wrap(...) which may produce more output data + // that will be written to the Channel. + case NEED_WRAP: + try { + if (!wrapNonAppData(ctx, false) && inUnwrap) { + // The handshake finished in wrapNonAppData(...), we need to try call + // unwrapNonAppData(...) as we may have some alert that we should read. + // + // This mimics what we would do when we are calling this method while in unwrap(...). + unwrapNonAppData(ctx); + } + + // Flush now as we may have written some data as part of the wrap call. + forceFlush(ctx); + } catch (Throwable e) { + taskError(e); + return; + } + + // Now try to feed in more data that we have buffered. + tryDecodeAgain(); + break; + + default: + // Should never reach here as we handle all cases. + throw new AssertionError(); } + } catch (Throwable cause) { + safeExceptionCaught(cause); } + } + + @Override + public void run() { + try { + runAllDelegatedTasks(engine); + + // All tasks were processed. + assert engine.getHandshakeStatus() != HandshakeStatus.NEED_TASK; - if (interrupted) { - Thread.currentThread().interrupt(); + // Jump back on the EventExecutor. + ctx.executor().execute(new Runnable() { + @Override + public void run() { + resumeOnEventExecutor(); + } + }); + } catch (final Throwable cause) { + handleException(cause); + } + } + + private void handleException(final Throwable cause) { + if (ctx.executor().inEventLoop()) { + processTask = false; + safeExceptionCaught(cause); + } else { + try { + ctx.executor().execute(new Runnable() { + @Override + public void run() { + processTask = false; + safeExceptionCaught(cause); + } + }); + } catch (RejectedExecutionException ignore) { + processTask = false; + // the context itself will handle the rejected exception when try to schedule the operation so + // ignore the RejectedExecutionException + ctx.fireExceptionCaught(cause); + } } } } @@ -1582,11 +1806,26 @@ public class SslHandler extends ByteToMessageDecoder implements ChannelOutboundH } } finally { // Ensure we remove and fail all pending writes in all cases and so release memory quickly. - releaseAndFailAll(cause); + releaseAndFailAll(ctx, cause); } } - private void releaseAndFailAll(Throwable cause) { + private void setHandshakeFailureTransportFailure(ChannelHandlerContext ctx, Throwable cause) { + // If TLS control frames fail to write we are in an unknown state and may become out of + // sync with our peer. We give up and close the channel. This will also take care of + // cleaning up any outstanding state (e.g. handshake promise, queued unencrypted data). + try { + SSLException transportFailure = new SSLException("failure when writing TLS control frames", cause); + releaseAndFailAll(ctx, transportFailure); + if (handshakePromise.tryFailure(transportFailure)) { + ctx.fireUserEventTriggered(new SslHandshakeCompletionEvent(transportFailure)); + } + } finally { + ctx.close(); + } + } + + private void releaseAndFailAll(ChannelHandlerContext ctx, Throwable cause) { if (pendingUnencryptedWrites != null) { pendingUnencryptedWrites.releaseAndFailAll(ctx, cause); } @@ -1694,9 +1933,7 @@ public class SslHandler extends ByteToMessageDecoder implements ChannelOutboundH * Performs TLS renegotiation. */ public Future<Channel> renegotiate(final Promise<Channel> promise) { - if (promise == null) { - throw new NullPointerException("promise"); - } + ObjectUtil.checkNotNull(promise, "promise"); ChannelHandlerContext ctx = this.ctx; if (ctx == null) { @@ -1777,12 +2014,14 @@ public class SslHandler extends ByteToMessageDecoder implements ChannelOutboundH if (localHandshakePromise.isDone()) { return; } + SSLException exception = + new SslHandshakeTimeoutException("handshake timed out after " + handshakeTimeoutMillis + "ms"); try { - if (localHandshakePromise.tryFailure(HANDSHAKE_TIMED_OUT)) { - SslUtils.handleHandshakeFailure(ctx, HANDSHAKE_TIMED_OUT, true); + if (localHandshakePromise.tryFailure(exception)) { + SslUtils.handleHandshakeFailure(ctx, exception, true); } } finally { - releaseAndFailAll(HANDSHAKE_TIMED_OUT); + releaseAndFailAll(ctx, exception); } } }, handshakeTimeoutMillis, TimeUnit.MILLISECONDS); @@ -1920,7 +2159,7 @@ public class SslHandler extends ByteToMessageDecoder implements ChannelOutboundH * the specified amount of pending bytes. */ private ByteBuf allocateOutNetBuf(ChannelHandlerContext ctx, int pendingBytes, int numComponents) { - return allocate(ctx, engineType.calculateWrapBufferCapacity(this, pendingBytes, numComponents)); + return engineType.allocateWrapBuffer(this, ctx.alloc(), pendingBytes, numComponents); } /** @@ -1954,7 +2193,11 @@ public class SslHandler extends ByteToMessageDecoder implements ChannelOutboundH protected ByteBuf composeFirst(ByteBufAllocator allocator, ByteBuf first) { if (first instanceof CompositeByteBuf) { CompositeByteBuf composite = (CompositeByteBuf) first; - first = allocator.directBuffer(composite.readableBytes()); + if (engineType.wantsDirectBuffer) { + first = allocator.directBuffer(composite.readableBytes()); + } else { + first = allocator.heapBuffer(composite.readableBytes()); + } try { first.writeBytes(composite); } catch (Throwable cause) { diff --git a/handler/src/main/java/io/netty/handler/ssl/SslHandshakeTimeoutException.java b/handler/src/main/java/io/netty/handler/ssl/SslHandshakeTimeoutException.java new file mode 100644 index 0000000..fbddbcc --- /dev/null +++ b/handler/src/main/java/io/netty/handler/ssl/SslHandshakeTimeoutException.java @@ -0,0 +1,28 @@ +/* + * Copyright 2020 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 javax.net.ssl.SSLHandshakeException; + +/** + * {@link SSLHandshakeException} that is used when a handshake failed due a configured timeout. + */ +public final class SslHandshakeTimeoutException extends SSLHandshakeException { + + SslHandshakeTimeoutException(String reason) { + super(reason); + } +} diff --git a/handler/src/main/java/io/netty/handler/ssl/SslMasterKeyHandler.java b/handler/src/main/java/io/netty/handler/ssl/SslMasterKeyHandler.java new file mode 100644 index 0000000..b1c710c --- /dev/null +++ b/handler/src/main/java/io/netty/handler/ssl/SslMasterKeyHandler.java @@ -0,0 +1,188 @@ +/* + * Copyright 2019 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.ByteBufUtil; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.util.internal.ReflectionUtil; +import io.netty.util.internal.SystemPropertyUtil; +import io.netty.util.internal.logging.InternalLogger; +import io.netty.util.internal.logging.InternalLoggerFactory; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLSession; +import java.lang.reflect.Field; + +/** + * The {@link SslMasterKeyHandler} is a channel-handler you can include in your pipeline to consume the master key + * & session identifier for a TLS session. + * This can be very useful, for instance the {@link WiresharkSslMasterKeyHandler} implementation will + * log the secret & identifier in a format that is consumable by Wireshark -- allowing easy decryption of pcap/tcpdumps. + */ +public abstract class SslMasterKeyHandler extends ChannelInboundHandlerAdapter { + + private static final InternalLogger logger = InternalLoggerFactory.getInstance(SslMasterKeyHandler.class); + + /** + * The JRE SSLSessionImpl cannot be imported + */ + private static final Class<?> SSL_SESSIONIMPL_CLASS; + + /** + * The master key field in the SSLSessionImpl + */ + private static final Field SSL_SESSIONIMPL_MASTER_SECRET_FIELD; + + /** + * A system property that can be used to turn on/off the {@link SslMasterKeyHandler} dynamically without having + * to edit your pipeline. + * <code>-Dio.netty.ssl.masterKeyHandler=true</code> + */ + public static final String SYSTEM_PROP_KEY = "io.netty.ssl.masterKeyHandler"; + + /** + * The unavailability cause of whether the private Sun implementation of SSLSessionImpl is available. + */ + private static final Throwable UNAVAILABILITY_CAUSE; + + static { + Throwable cause = null; + Class<?> clazz = null; + Field field = null; + try { + clazz = Class.forName("sun.security.ssl.SSLSessionImpl"); + field = clazz.getDeclaredField("masterSecret"); + cause = ReflectionUtil.trySetAccessible(field, true); + } catch (Throwable e) { + cause = e; + logger.debug("sun.security.ssl.SSLSessionImpl is unavailable.", e); + } + UNAVAILABILITY_CAUSE = cause; + SSL_SESSIONIMPL_CLASS = clazz; + SSL_SESSIONIMPL_MASTER_SECRET_FIELD = field; + } + + /** + * Constructor. + */ + protected SslMasterKeyHandler() { + } + + /** + * Ensure that SSLSessionImpl is available. + * @throws UnsatisfiedLinkError if unavailable + */ + public static void ensureSunSslEngineAvailability() { + if (UNAVAILABILITY_CAUSE != null) { + throw new IllegalStateException( + "Failed to find SSLSessionImpl on classpath", UNAVAILABILITY_CAUSE); + } + } + + /** + * Returns the cause of unavailability. + * + * @return the cause if unavailable. {@code null} if available. + */ + public static Throwable sunSslEngineUnavailabilityCause() { + return UNAVAILABILITY_CAUSE; + } + + /* Returns {@code true} if and only if sun.security.ssl.SSLSessionImpl exists in the runtime. + */ + public static boolean isSunSslEngineAvailable() { + return UNAVAILABILITY_CAUSE == null; + } + + /** + * Consume the master key for the session and the sessionId + * @param masterKey A 48-byte secret shared between the client and server. + * @param session The current TLS session + */ + protected abstract void accept(SecretKey masterKey, SSLSession session); + + @Override + public final void userEventTriggered(ChannelHandlerContext ctx, Object evt) { + //only try to log the session info if the ssl handshake has successfully completed. + if (evt == SslHandshakeCompletionEvent.SUCCESS) { + boolean shouldHandle = SystemPropertyUtil.getBoolean(SYSTEM_PROP_KEY, false); + + if (shouldHandle) { + final SslHandler handler = ctx.pipeline().get(SslHandler.class); + final SSLEngine engine = handler.engine(); + final SSLSession sslSession = engine.getSession(); + + //the OpenJDK does not expose a way to get the master secret, so try to use reflection to get it. + if (isSunSslEngineAvailable() && sslSession.getClass().equals(SSL_SESSIONIMPL_CLASS)) { + final SecretKey secretKey; + try { + secretKey = (SecretKey) SSL_SESSIONIMPL_MASTER_SECRET_FIELD.get(sslSession); + } catch (IllegalAccessException e) { + throw new IllegalArgumentException("Failed to access the field 'masterSecret' " + + "via reflection.", e); + } + accept(secretKey, sslSession); + } else if (OpenSsl.isAvailable() && engine instanceof ReferenceCountedOpenSslEngine) { + SecretKeySpec secretKey = ((ReferenceCountedOpenSslEngine) engine).masterKey(); + accept(secretKey, sslSession); + } + } + } + + ctx.fireUserEventTriggered(evt); + } + + /** + * Create a {@link WiresharkSslMasterKeyHandler} instance. + * This TLS master key handler logs the master key and session-id in a format + * understood by Wireshark -- this can be especially useful if you need to ever + * decrypt a TLS session and are using perfect forward secrecy (i.e. Diffie-Hellman) + * The key and session identifier are forwarded to the log named 'io.netty.wireshark'. + */ + public static SslMasterKeyHandler newWireSharkSslMasterKeyHandler() { + return new WiresharkSslMasterKeyHandler(); + } + + /** + * Record the session identifier and master key to the {@link InternalLogger} named <code>io.netty.wireshark</code>. + * ex. <code>RSA Session-ID:XXX Master-Key:YYY</code> + * This format is understood by Wireshark 1.6.0. + * https://code.wireshark.org/review/gitweb?p=wireshark.git;a=commit;h=686d4cabb41185591c361f9ec6b709034317144b + * The key and session identifier are forwarded to the log named 'io.netty.wireshark'. + */ + private static final class WiresharkSslMasterKeyHandler extends SslMasterKeyHandler { + + private static final InternalLogger wireshark_logger = + InternalLoggerFactory.getInstance("io.netty.wireshark"); + + private static final char[] hexCode = "0123456789ABCDEF".toCharArray(); + + @Override + protected void accept(SecretKey masterKey, SSLSession session) { + if (masterKey.getEncoded().length != 48) { + throw new IllegalArgumentException("An invalid length master key was provided."); + } + final byte[] sessionId = session.getId(); + wireshark_logger.warn("RSA Session-ID:{} Master-Key:{}", + ByteBufUtil.hexDump(sessionId).toLowerCase(), + ByteBufUtil.hexDump(masterKey.getEncoded()).toLowerCase()); + } + } + +} diff --git a/handler/src/main/java/io/netty/handler/ssl/SslProvider.java b/handler/src/main/java/io/netty/handler/ssl/SslProvider.java index 00fc2aa..e72cfed 100644 --- a/handler/src/main/java/io/netty/handler/ssl/SslProvider.java +++ b/handler/src/main/java/io/netty/handler/ssl/SslProvider.java @@ -35,5 +35,21 @@ public enum SslProvider { * OpenSSL-based implementation which does not have finalizers and instead implements {@link ReferenceCounted}. */ @UnstableApi - OPENSSL_REFCNT + OPENSSL_REFCNT; + + /** + * Returns {@code true} if the specified {@link SslProvider} supports + * <a href="https://tools.ietf.org/html/rfc7301#section-6">TLS ALPN Extension</a>, {@code false} otherwise. + */ + public static boolean isAlpnSupported(final SslProvider provider) { + switch (provider) { + case JDK: + return JdkAlpnApplicationProtocolNegotiator.isAlpnSupported(); + case OPENSSL: + case OPENSSL_REFCNT: + return OpenSsl.isAlpnSupported(); + default: + throw new Error("Unknown SslProvider: " + provider); + } + } } diff --git a/handler/src/main/java/io/netty/handler/ssl/SslUtils.java b/handler/src/main/java/io/netty/handler/ssl/SslUtils.java index e764036..661c846 100644 --- a/handler/src/main/java/io/netty/handler/ssl/SslUtils.java +++ b/handler/src/main/java/io/netty/handler/ssl/SslUtils.java @@ -113,6 +113,7 @@ final class SslUtils { defaultCiphers.add("TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384"); defaultCiphers.add("TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256"); defaultCiphers.add("TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"); + defaultCiphers.add("TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"); defaultCiphers.add("TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA"); // AES256 requires JCE unlimited strength jurisdiction policy files. defaultCiphers.add("TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA"); @@ -122,9 +123,7 @@ final class SslUtils { // AES256 requires JCE unlimited strength jurisdiction policy files. defaultCiphers.add("TLS_RSA_WITH_AES_256_CBC_SHA"); - for (String tlsv13Cipher: DEFAULT_TLSV13_CIPHER_SUITES) { - defaultCiphers.add(tlsv13Cipher); - } + Collections.addAll(defaultCiphers, DEFAULT_TLSV13_CIPHER_SUITES); DEFAULT_CIPHER_SUITES = defaultCiphers.toArray(new String[0]); } diff --git a/handler/src/main/java/io/netty/handler/ssl/SupportedCipherSuiteFilter.java b/handler/src/main/java/io/netty/handler/ssl/SupportedCipherSuiteFilter.java index 2656723..d263300 100644 --- a/handler/src/main/java/io/netty/handler/ssl/SupportedCipherSuiteFilter.java +++ b/handler/src/main/java/io/netty/handler/ssl/SupportedCipherSuiteFilter.java @@ -15,6 +15,8 @@ */ package io.netty.handler.ssl; +import io.netty.util.internal.ObjectUtil; + import javax.net.ssl.SSLEngine; import java.util.ArrayList; import java.util.List; @@ -31,12 +33,8 @@ public final class SupportedCipherSuiteFilter implements CipherSuiteFilter { @Override public String[] filterCipherSuites(Iterable<String> ciphers, List<String> defaultCiphers, Set<String> supportedCiphers) { - if (defaultCiphers == null) { - throw new NullPointerException("defaultCiphers"); - } - if (supportedCiphers == null) { - throw new NullPointerException("supportedCiphers"); - } + ObjectUtil.checkNotNull(defaultCiphers, "defaultCiphers"); + ObjectUtil.checkNotNull(supportedCiphers, "supportedCiphers"); final List<String> newCiphers; if (ciphers == null) { diff --git a/handler/src/main/java/io/netty/handler/ssl/TrustManagerFactoryWrapper.java b/handler/src/main/java/io/netty/handler/ssl/TrustManagerFactoryWrapper.java new file mode 100644 index 0000000..5abf8aa --- /dev/null +++ b/handler/src/main/java/io/netty/handler/ssl/TrustManagerFactoryWrapper.java @@ -0,0 +1,44 @@ +/* + * Copyright 2019 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.handler.ssl.util.SimpleTrustManagerFactory; +import io.netty.util.internal.ObjectUtil; + +import java.security.KeyStore; +import javax.net.ssl.ManagerFactoryParameters; +import javax.net.ssl.TrustManager; + +final class TrustManagerFactoryWrapper extends SimpleTrustManagerFactory { + private final TrustManager tm; + + TrustManagerFactoryWrapper(TrustManager tm) { + this.tm = ObjectUtil.checkNotNull(tm, "tm"); + } + + @Override + protected void engineInit(KeyStore keyStore) throws Exception { } + + @Override + protected void engineInit(ManagerFactoryParameters managerFactoryParameters) + throws Exception { } + + @Override + protected TrustManager[] engineGetTrustManagers() { + return new TrustManager[] {tm}; + } +} diff --git a/handler/src/main/java/io/netty/handler/ssl/ocsp/OcspClientHandler.java b/handler/src/main/java/io/netty/handler/ssl/ocsp/OcspClientHandler.java index aff0949..c45c50e 100644 --- a/handler/src/main/java/io/netty/handler/ssl/ocsp/OcspClientHandler.java +++ b/handler/src/main/java/io/netty/handler/ssl/ocsp/OcspClientHandler.java @@ -21,7 +21,6 @@ import io.netty.handler.ssl.ReferenceCountedOpenSslContext; import io.netty.handler.ssl.ReferenceCountedOpenSslEngine; import io.netty.handler.ssl.SslHandshakeCompletionEvent; import io.netty.util.internal.ObjectUtil; -import io.netty.util.internal.ThrowableUtil; import io.netty.util.internal.UnstableApi; import javax.net.ssl.SSLHandshakeException; @@ -35,9 +34,6 @@ import javax.net.ssl.SSLHandshakeException; @UnstableApi public abstract class OcspClientHandler extends ChannelInboundHandlerAdapter { - private static final SSLHandshakeException OCSP_VERIFICATION_EXCEPTION = ThrowableUtil.unknownStackTrace( - new SSLHandshakeException("Bad OCSP response"), OcspClientHandler.class, "verify(...)"); - private final ReferenceCountedOpenSslEngine engine; protected OcspClientHandler(ReferenceCountedOpenSslEngine engine) { @@ -56,7 +52,7 @@ public abstract class OcspClientHandler extends ChannelInboundHandlerAdapter { SslHandshakeCompletionEvent event = (SslHandshakeCompletionEvent) evt; if (event.isSuccess() && !verify(ctx, engine)) { - throw OCSP_VERIFICATION_EXCEPTION; + throw new SSLHandshakeException("Bad OCSP response"); } } diff --git a/handler/src/main/java/io/netty/handler/ssl/util/FingerprintTrustManagerFactory.java b/handler/src/main/java/io/netty/handler/ssl/util/FingerprintTrustManagerFactory.java index 4543345..8a023d4 100644 --- a/handler/src/main/java/io/netty/handler/ssl/util/FingerprintTrustManagerFactory.java +++ b/handler/src/main/java/io/netty/handler/ssl/util/FingerprintTrustManagerFactory.java @@ -18,8 +18,9 @@ package io.netty.handler.ssl.util; import io.netty.buffer.ByteBufUtil; import io.netty.buffer.Unpooled; -import io.netty.util.internal.EmptyArrays; import io.netty.util.concurrent.FastThreadLocal; +import io.netty.util.internal.EmptyArrays; +import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.StringUtil; import javax.net.ssl.ManagerFactoryParameters; @@ -156,9 +157,7 @@ public final class FingerprintTrustManagerFactory extends SimpleTrustManagerFact * @param fingerprints a list of SHA1 fingerprints */ public FingerprintTrustManagerFactory(byte[]... fingerprints) { - if (fingerprints == null) { - throw new NullPointerException("fingerprints"); - } + ObjectUtil.checkNotNull(fingerprints, "fingerprints"); List<byte[]> list = new ArrayList<byte[]>(fingerprints.length); for (byte[] f: fingerprints) { @@ -176,9 +175,7 @@ public final class FingerprintTrustManagerFactory extends SimpleTrustManagerFact } private static byte[][] toFingerprintArray(Iterable<String> fingerprints) { - if (fingerprints == null) { - throw new NullPointerException("fingerprints"); - } + ObjectUtil.checkNotNull(fingerprints, "fingerprints"); List<byte[]> list = new ArrayList<byte[]>(); for (String f: fingerprints) { diff --git a/handler/src/main/java/io/netty/handler/ssl/util/OpenJdkSelfSignedCertGenerator.java b/handler/src/main/java/io/netty/handler/ssl/util/OpenJdkSelfSignedCertGenerator.java index 30d74e2..dbab743 100644 --- a/handler/src/main/java/io/netty/handler/ssl/util/OpenJdkSelfSignedCertGenerator.java +++ b/handler/src/main/java/io/netty/handler/ssl/util/OpenJdkSelfSignedCertGenerator.java @@ -16,6 +16,7 @@ package io.netty.handler.ssl.util; +import io.netty.util.internal.SuppressJava6Requirement; import sun.security.x509.AlgorithmId; import sun.security.x509.CertificateAlgorithmId; import sun.security.x509.CertificateIssuerName; @@ -42,6 +43,7 @@ import static io.netty.handler.ssl.util.SelfSignedCertificate.*; */ final class OpenJdkSelfSignedCertGenerator { + @SuppressJava6Requirement(reason = "Usage guarded by dependency check") static String[] generate(String fqdn, KeyPair keypair, SecureRandom random, Date notBefore, Date notAfter) throws Exception { PrivateKey key = keypair.getPrivate(); diff --git a/handler/src/main/java/io/netty/handler/ssl/util/SelfSignedCertificate.java b/handler/src/main/java/io/netty/handler/ssl/util/SelfSignedCertificate.java index 9f010ce..c0b8467 100644 --- a/handler/src/main/java/io/netty/handler/ssl/util/SelfSignedCertificate.java +++ b/handler/src/main/java/io/netty/handler/ssl/util/SelfSignedCertificate.java @@ -21,6 +21,7 @@ import io.netty.buffer.Unpooled; import io.netty.handler.codec.base64.Base64; import io.netty.util.CharsetUtil; import io.netty.util.internal.SystemPropertyUtil; +import io.netty.util.internal.ThrowableUtil; import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; @@ -48,7 +49,7 @@ import java.util.Date; * It is purely for testing purposes, and thus it is very insecure. * It even uses an insecure pseudo-random generator for faster generation internally. * </p><p> - * A X.509 certificate file and a RSA private key file are generated in a system's temporary directory using + * An X.509 certificate file and a RSA private key file are generated in a system's temporary directory using * {@link java.io.File#createTempFile(String, String)}, and they are deleted when the JVM exits using * {@link java.io.File#deleteOnExit()}. * </p><p> @@ -67,6 +68,14 @@ public final class SelfSignedCertificate { private static final Date DEFAULT_NOT_AFTER = new Date(SystemPropertyUtil.getLong( "io.netty.selfSignedCertificate.defaultNotAfter", 253402300799000L)); + /** + * FIPS 140-2 encryption requires the key length to be 2048 bits or greater. + * Let's use that as a sane default but allow the default to be set dynamically + * for those that need more stringent security requirements. + */ + private static final int DEFAULT_KEY_LENGTH_BITS = + SystemPropertyUtil.getInt("io.netty.handler.ssl.util.selfSignedKeyStrength", 2048); + private final File certificate; private final File privateKey; private final X509Certificate cert; @@ -107,7 +116,7 @@ public final class SelfSignedCertificate { public SelfSignedCertificate(String fqdn, Date notBefore, Date notAfter) throws CertificateException { // Bypass entropy collection by using insecure random generator. // We just want to generate it without any delay because it's for testing purposes only. - this(fqdn, ThreadLocalInsecureRandom.current(), 1024, notBefore, notAfter); + this(fqdn, ThreadLocalInsecureRandom.current(), DEFAULT_KEY_LENGTH_BITS, notBefore, notAfter); } /** @@ -154,10 +163,11 @@ public final class SelfSignedCertificate { paths = BouncyCastleSelfSignedCertGenerator.generate(fqdn, keypair, random, notBefore, notAfter); } catch (Throwable t2) { logger.debug("Failed to generate a self-signed X.509 certificate using Bouncy Castle:", t2); - throw new CertificateException( + final CertificateException certificateException = new CertificateException( "No provider succeeded to generate a self-signed certificate. " + "See debug log for the root cause.", t2); - // TODO: consider using Java 7 addSuppressed to append t + ThrowableUtil.addSuppressed(certificateException, t); + throw certificateException; } } diff --git a/handler/src/main/java/io/netty/handler/ssl/util/SimpleKeyManagerFactory.java b/handler/src/main/java/io/netty/handler/ssl/util/SimpleKeyManagerFactory.java new file mode 100644 index 0000000..31b28f8 --- /dev/null +++ b/handler/src/main/java/io/netty/handler/ssl/util/SimpleKeyManagerFactory.java @@ -0,0 +1,154 @@ +/* + * Copyright 2019 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.util; + +import io.netty.util.concurrent.FastThreadLocal; +import io.netty.util.internal.ObjectUtil; +import io.netty.util.internal.PlatformDependent; +import io.netty.util.internal.StringUtil; +import io.netty.util.internal.SuppressJava6Requirement; +import java.security.InvalidAlgorithmParameterException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.Provider; +import javax.net.ssl.ManagerFactoryParameters; +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.KeyManagerFactorySpi; +import javax.net.ssl.X509ExtendedKeyManager; +import javax.net.ssl.X509KeyManager; + +/** + * Helps to implement a custom {@link KeyManagerFactory}. + */ +public abstract class SimpleKeyManagerFactory extends KeyManagerFactory { + + private static final Provider PROVIDER = new Provider("", 0.0, "") { + private static final long serialVersionUID = -2680540247105807895L; + }; + + /** + * {@link SimpleKeyManagerFactorySpi} must have a reference to {@link SimpleKeyManagerFactory} + * to delegate its callbacks back to {@link SimpleKeyManagerFactory}. However, it is impossible to do so, + * because {@link KeyManagerFactory} requires {@link KeyManagerFactorySpi} at construction time and + * does not provide a way to access it later. + * + * To work around this issue, we use an ugly hack which uses a {@link FastThreadLocal }. + */ + private static final FastThreadLocal<SimpleKeyManagerFactorySpi> CURRENT_SPI = + new FastThreadLocal<SimpleKeyManagerFactorySpi>() { + @Override + protected SimpleKeyManagerFactorySpi initialValue() { + return new SimpleKeyManagerFactorySpi(); + } + }; + + /** + * Creates a new instance. + */ + protected SimpleKeyManagerFactory() { + this(StringUtil.EMPTY_STRING); + } + + /** + * Creates a new instance. + * + * @param name the name of this {@link KeyManagerFactory} + */ + protected SimpleKeyManagerFactory(String name) { + super(CURRENT_SPI.get(), PROVIDER, ObjectUtil.checkNotNull(name, "name")); + CURRENT_SPI.get().init(this); + CURRENT_SPI.remove(); + } + + /** + * Initializes this factory with a source of certificate authorities and related key material. + * + * @see KeyManagerFactorySpi#engineInit(KeyStore, char[]) + */ + protected abstract void engineInit(KeyStore keyStore, char[] var2) throws Exception; + + /** + * Initializes this factory with a source of provider-specific key material. + * + * @see KeyManagerFactorySpi#engineInit(ManagerFactoryParameters) + */ + protected abstract void engineInit(ManagerFactoryParameters managerFactoryParameters) throws Exception; + + /** + * Returns one key manager for each type of key material. + * + * @see KeyManagerFactorySpi#engineGetKeyManagers() + */ + protected abstract KeyManager[] engineGetKeyManagers(); + + private static final class SimpleKeyManagerFactorySpi extends KeyManagerFactorySpi { + + private SimpleKeyManagerFactory parent; + private volatile KeyManager[] keyManagers; + + void init(SimpleKeyManagerFactory parent) { + this.parent = parent; + } + + @Override + protected void engineInit(KeyStore keyStore, char[] pwd) throws KeyStoreException { + try { + parent.engineInit(keyStore, pwd); + } catch (KeyStoreException e) { + throw e; + } catch (Exception e) { + throw new KeyStoreException(e); + } + } + + @Override + protected void engineInit( + ManagerFactoryParameters managerFactoryParameters) throws InvalidAlgorithmParameterException { + try { + parent.engineInit(managerFactoryParameters); + } catch (InvalidAlgorithmParameterException e) { + throw e; + } catch (Exception e) { + throw new InvalidAlgorithmParameterException(e); + } + } + + @Override + protected KeyManager[] engineGetKeyManagers() { + KeyManager[] keyManagers = this.keyManagers; + if (keyManagers == null) { + keyManagers = parent.engineGetKeyManagers(); + if (PlatformDependent.javaVersion() >= 7) { + wrapIfNeeded(keyManagers); + } + this.keyManagers = keyManagers; + } + return keyManagers.clone(); + } + + @SuppressJava6Requirement(reason = "Usage guarded by java version check") + private static void wrapIfNeeded(KeyManager[] keyManagers) { + for (int i = 0; i < keyManagers.length; i++) { + final KeyManager tm = keyManagers[i]; + if (tm instanceof X509KeyManager && !(tm instanceof X509ExtendedKeyManager)) { + keyManagers[i] = new X509KeyManagerWrapper((X509KeyManager) tm); + } + } + } + } +} diff --git a/handler/src/main/java/io/netty/handler/ssl/util/SimpleTrustManagerFactory.java b/handler/src/main/java/io/netty/handler/ssl/util/SimpleTrustManagerFactory.java index a11cede..d2fa903 100644 --- a/handler/src/main/java/io/netty/handler/ssl/util/SimpleTrustManagerFactory.java +++ b/handler/src/main/java/io/netty/handler/ssl/util/SimpleTrustManagerFactory.java @@ -17,7 +17,9 @@ package io.netty.handler.ssl.util; import io.netty.util.concurrent.FastThreadLocal; +import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.PlatformDependent; +import io.netty.util.internal.SuppressJava6Requirement; import javax.net.ssl.ManagerFactoryParameters; import javax.net.ssl.TrustManager; @@ -72,9 +74,7 @@ public abstract class SimpleTrustManagerFactory extends TrustManagerFactory { CURRENT_SPI.get().init(this); CURRENT_SPI.remove(); - if (name == null) { - throw new NullPointerException("name"); - } + ObjectUtil.checkNotNull(name, "name"); } /** @@ -136,16 +136,21 @@ public abstract class SimpleTrustManagerFactory extends TrustManagerFactory { if (trustManagers == null) { trustManagers = parent.engineGetTrustManagers(); if (PlatformDependent.javaVersion() >= 7) { - for (int i = 0; i < trustManagers.length; i++) { - final TrustManager tm = trustManagers[i]; - if (tm instanceof X509TrustManager && !(tm instanceof X509ExtendedTrustManager)) { - trustManagers[i] = new X509TrustManagerWrapper((X509TrustManager) tm); - } - } + wrapIfNeeded(trustManagers); } this.trustManagers = trustManagers; } return trustManagers.clone(); } + + @SuppressJava6Requirement(reason = "Usage guarded by java version check") + private static void wrapIfNeeded(TrustManager[] trustManagers) { + for (int i = 0; i < trustManagers.length; i++) { + final TrustManager tm = trustManagers[i]; + if (tm instanceof X509TrustManager && !(tm instanceof X509ExtendedTrustManager)) { + trustManagers[i] = new X509TrustManagerWrapper((X509TrustManager) tm); + } + } + } } } diff --git a/handler/src/main/java/io/netty/handler/ssl/util/X509KeyManagerWrapper.java b/handler/src/main/java/io/netty/handler/ssl/util/X509KeyManagerWrapper.java new file mode 100644 index 0000000..20c1f26 --- /dev/null +++ b/handler/src/main/java/io/netty/handler/ssl/util/X509KeyManagerWrapper.java @@ -0,0 +1,78 @@ +/* + * Copyright 2019 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.util; + +import static io.netty.util.internal.ObjectUtil.checkNotNull; + +import io.netty.util.internal.SuppressJava6Requirement; +import java.net.Socket; +import java.security.Principal; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.X509ExtendedKeyManager; +import javax.net.ssl.X509KeyManager; + +@SuppressJava6Requirement(reason = "Usage guarded by java version check") +final class X509KeyManagerWrapper extends X509ExtendedKeyManager { + + private final X509KeyManager delegate; + + X509KeyManagerWrapper(X509KeyManager delegate) { + this.delegate = checkNotNull(delegate, "delegate"); + } + + @Override + public String[] getClientAliases(String var1, Principal[] var2) { + return delegate.getClientAliases(var1, var2); + } + + @Override + public String chooseClientAlias(String[] var1, Principal[] var2, Socket var3) { + return delegate.chooseClientAlias(var1, var2, var3); + } + + @Override + public String[] getServerAliases(String var1, Principal[] var2) { + return delegate.getServerAliases(var1, var2); + } + + @Override + public String chooseServerAlias(String var1, Principal[] var2, Socket var3) { + return delegate.chooseServerAlias(var1, var2, var3); + } + + @Override + public X509Certificate[] getCertificateChain(String var1) { + return delegate.getCertificateChain(var1); + } + + @Override + public PrivateKey getPrivateKey(String var1) { + return delegate.getPrivateKey(var1); + } + + @Override + public String chooseEngineClientAlias(String[] keyType, Principal[] issuers, SSLEngine engine) { + return delegate.chooseClientAlias(keyType, issuers, null); + } + + @Override + public String chooseEngineServerAlias(String keyType, Principal[] issuers, SSLEngine engine) { + return delegate.chooseServerAlias(keyType, issuers, null); + } +} diff --git a/handler/src/main/java/io/netty/handler/ssl/util/X509TrustManagerWrapper.java b/handler/src/main/java/io/netty/handler/ssl/util/X509TrustManagerWrapper.java index 1b70b97..4955ef9 100644 --- a/handler/src/main/java/io/netty/handler/ssl/util/X509TrustManagerWrapper.java +++ b/handler/src/main/java/io/netty/handler/ssl/util/X509TrustManagerWrapper.java @@ -15,6 +15,8 @@ */ package io.netty.handler.ssl.util; +import io.netty.util.internal.SuppressJava6Requirement; + import javax.net.ssl.SSLEngine; import javax.net.ssl.X509ExtendedTrustManager; import javax.net.ssl.X509TrustManager; @@ -24,6 +26,7 @@ import java.security.cert.X509Certificate; import static io.netty.util.internal.ObjectUtil.*; +@SuppressJava6Requirement(reason = "Usage guarded by java version check") final class X509TrustManagerWrapper extends X509ExtendedTrustManager { private final X509TrustManager delegate; diff --git a/handler/src/main/java/io/netty/handler/stream/ChunkedFile.java b/handler/src/main/java/io/netty/handler/stream/ChunkedFile.java index 3e12e4a..17d1c42 100644 --- a/handler/src/main/java/io/netty/handler/stream/ChunkedFile.java +++ b/handler/src/main/java/io/netty/handler/stream/ChunkedFile.java @@ -19,6 +19,7 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.FileRegion; +import io.netty.util.internal.ObjectUtil; import java.io.File; import java.io.IOException; @@ -82,26 +83,14 @@ public class ChunkedFile implements ChunkedInput<ByteBuf> { * {@link #readChunk(ChannelHandlerContext)} call */ public ChunkedFile(RandomAccessFile file, long offset, long length, int chunkSize) throws IOException { - if (file == null) { - throw new NullPointerException("file"); - } - if (offset < 0) { - throw new IllegalArgumentException( - "offset: " + offset + " (expected: 0 or greater)"); - } - if (length < 0) { - throw new IllegalArgumentException( - "length: " + length + " (expected: 0 or greater)"); - } - if (chunkSize <= 0) { - throw new IllegalArgumentException( - "chunkSize: " + chunkSize + - " (expected: a positive integer)"); - } + ObjectUtil.checkNotNull(file, "file"); + ObjectUtil.checkPositiveOrZero(offset, "offset"); + ObjectUtil.checkPositiveOrZero(length, "length"); + ObjectUtil.checkPositive(chunkSize, "chunkSize"); this.file = file; this.offset = startOffset = offset; - endOffset = offset + length; + this.endOffset = offset + length; this.chunkSize = chunkSize; file.seek(offset); diff --git a/handler/src/main/java/io/netty/handler/stream/ChunkedNioFile.java b/handler/src/main/java/io/netty/handler/stream/ChunkedNioFile.java index 339a3e5..f46db09 100644 --- a/handler/src/main/java/io/netty/handler/stream/ChunkedNioFile.java +++ b/handler/src/main/java/io/netty/handler/stream/ChunkedNioFile.java @@ -19,10 +19,12 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.FileRegion; +import io.netty.util.internal.ObjectUtil; import java.io.File; -import java.io.FileInputStream; import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.channels.ClosedChannelException; import java.nio.channels.FileChannel; /** @@ -45,7 +47,7 @@ public class ChunkedNioFile implements ChunkedInput<ByteBuf> { * Creates a new instance that fetches data from the specified file. */ public ChunkedNioFile(File in) throws IOException { - this(new FileInputStream(in).getChannel()); + this(new RandomAccessFile(in, "r").getChannel()); } /** @@ -55,7 +57,7 @@ public class ChunkedNioFile implements ChunkedInput<ByteBuf> { * {@link #readChunk(ChannelHandlerContext)} call */ public ChunkedNioFile(File in, int chunkSize) throws IOException { - this(new FileInputStream(in).getChannel(), chunkSize); + this(new RandomAccessFile(in, "r").getChannel(), chunkSize); } /** @@ -85,25 +87,12 @@ public class ChunkedNioFile implements ChunkedInput<ByteBuf> { */ public ChunkedNioFile(FileChannel in, long offset, long length, int chunkSize) throws IOException { - if (in == null) { - throw new NullPointerException("in"); - } - if (offset < 0) { - throw new IllegalArgumentException( - "offset: " + offset + " (expected: 0 or greater)"); - } - if (length < 0) { - throw new IllegalArgumentException( - "length: " + length + " (expected: 0 or greater)"); - } - if (chunkSize <= 0) { - throw new IllegalArgumentException( - "chunkSize: " + chunkSize + - " (expected: a positive integer)"); - } - - if (offset != 0) { - in.position(offset); + ObjectUtil.checkNotNull(in, "in"); + ObjectUtil.checkPositiveOrZero(offset, "offset"); + ObjectUtil.checkPositiveOrZero(length, "length"); + ObjectUtil.checkPositive(chunkSize, "chunkSize"); + if (!in.isOpen()) { + throw new ClosedChannelException(); } this.in = in; this.chunkSize = chunkSize; @@ -161,7 +150,7 @@ public class ChunkedNioFile implements ChunkedInput<ByteBuf> { try { int readBytes = 0; for (;;) { - int localReadBytes = buffer.writeBytes(in, chunkSize - readBytes); + int localReadBytes = buffer.writeBytes(in, offset + readBytes, chunkSize - readBytes); if (localReadBytes < 0) { break; } diff --git a/handler/src/main/java/io/netty/handler/stream/ChunkedNioStream.java b/handler/src/main/java/io/netty/handler/stream/ChunkedNioStream.java index 22feb63..ebdddce 100644 --- a/handler/src/main/java/io/netty/handler/stream/ChunkedNioStream.java +++ b/handler/src/main/java/io/netty/handler/stream/ChunkedNioStream.java @@ -18,6 +18,7 @@ package io.netty.handler.stream; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.channel.ChannelHandlerContext; +import io.netty.util.internal.ObjectUtil; import java.nio.ByteBuffer; import java.nio.channels.ReadableByteChannel; @@ -53,9 +54,7 @@ public class ChunkedNioStream implements ChunkedInput<ByteBuf> { * {@link #readChunk(ChannelHandlerContext)} call */ public ChunkedNioStream(ReadableByteChannel in, int chunkSize) { - if (in == null) { - throw new NullPointerException("in"); - } + ObjectUtil.checkNotNull(in, "in"); if (chunkSize <= 0) { throw new IllegalArgumentException("chunkSize: " + chunkSize + " (expected: a positive integer)"); diff --git a/handler/src/main/java/io/netty/handler/stream/ChunkedStream.java b/handler/src/main/java/io/netty/handler/stream/ChunkedStream.java index f476c03..ee2d038 100644 --- a/handler/src/main/java/io/netty/handler/stream/ChunkedStream.java +++ b/handler/src/main/java/io/netty/handler/stream/ChunkedStream.java @@ -18,6 +18,7 @@ package io.netty.handler.stream; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.channel.ChannelHandlerContext; +import io.netty.util.internal.ObjectUtil; import java.io.InputStream; import java.io.PushbackInputStream; @@ -55,14 +56,8 @@ public class ChunkedStream implements ChunkedInput<ByteBuf> { * {@link #readChunk(ChannelHandlerContext)} call */ public ChunkedStream(InputStream in, int chunkSize) { - if (in == null) { - throw new NullPointerException("in"); - } - if (chunkSize <= 0) { - throw new IllegalArgumentException( - "chunkSize: " + chunkSize + - " (expected: a positive integer)"); - } + ObjectUtil.checkNotNull(in, "in"); + ObjectUtil.checkPositive(chunkSize, "chunkSize"); if (in instanceof PushbackInputStream) { this.in = (PushbackInputStream) in; diff --git a/handler/src/main/java/io/netty/handler/stream/ChunkedWriteHandler.java b/handler/src/main/java/io/netty/handler/stream/ChunkedWriteHandler.java index f39328d..a222595 100644 --- a/handler/src/main/java/io/netty/handler/stream/ChunkedWriteHandler.java +++ b/handler/src/main/java/io/netty/handler/stream/ChunkedWriteHandler.java @@ -72,7 +72,6 @@ public class ChunkedWriteHandler extends ChannelDuplexHandler { private final Queue<PendingWrite> queue = new ArrayDeque<PendingWrite>(); private volatile ChannelHandlerContext ctx; - private PendingWrite currentWrite; public ChunkedWriteHandler() { } @@ -119,9 +118,7 @@ public class ChunkedWriteHandler extends ChannelDuplexHandler { try { doFlush(ctx); } catch (Exception e) { - if (logger.isWarnEnabled()) { - logger.warn("Unexpected exception while sending chunks.", e); - } + logger.warn("Unexpected exception while sending chunks.", e); } } @@ -152,13 +149,7 @@ public class ChunkedWriteHandler extends ChannelDuplexHandler { private void discard(Throwable cause) { for (;;) { - PendingWrite currentWrite = this.currentWrite; - - if (this.currentWrite == null) { - currentWrite = queue.poll(); - } else { - this.currentWrite = null; - } + PendingWrite currentWrite = queue.poll(); if (currentWrite == null) { break; @@ -166,22 +157,28 @@ public class ChunkedWriteHandler extends ChannelDuplexHandler { Object message = currentWrite.msg; if (message instanceof ChunkedInput) { ChunkedInput<?> in = (ChunkedInput<?>) message; + boolean endOfInput; + long inputLength; try { - if (!in.isEndOfInput()) { - if (cause == null) { - cause = new ClosedChannelException(); - } - currentWrite.fail(cause); - } else { - currentWrite.success(in.length()); - } + endOfInput = in.isEndOfInput(); + inputLength = in.length(); closeInput(in); } catch (Exception e) { + closeInput(in); currentWrite.fail(e); if (logger.isWarnEnabled()) { - logger.warn(ChunkedInput.class.getSimpleName() + ".isEndOfInput() failed", e); + logger.warn(ChunkedInput.class.getSimpleName() + " failed", e); } - closeInput(in); + continue; + } + + if (!endOfInput) { + if (cause == null) { + cause = new ClosedChannelException(); + } + currentWrite.fail(cause); + } else { + currentWrite.success(inputLength); } } else { if (cause == null) { @@ -202,9 +199,7 @@ public class ChunkedWriteHandler extends ChannelDuplexHandler { boolean requiresFlush = true; ByteBufAllocator allocator = ctx.alloc(); while (channel.isWritable()) { - if (currentWrite == null) { - currentWrite = queue.poll(); - } + final PendingWrite currentWrite = queue.peek(); if (currentWrite == null) { break; @@ -220,11 +215,10 @@ public class ChunkedWriteHandler extends ChannelDuplexHandler { // as this had to be done already by someone who resolved the // promise (using ChunkedInput.close method). // See https://github.com/netty/netty/issues/8700. - this.currentWrite = null; + queue.remove(); continue; } - final PendingWrite currentWrite = this.currentWrite; final Object pendingMessage = currentWrite.msg; if (pendingMessage instanceof ChunkedInput) { @@ -243,14 +237,14 @@ public class ChunkedWriteHandler extends ChannelDuplexHandler { suspend = false; } } catch (final Throwable t) { - this.currentWrite = null; + queue.remove(); if (message != null) { ReferenceCountUtil.release(message); } - currentWrite.fail(t); closeInput(chunks); + currentWrite.fail(t); break; } @@ -267,60 +261,42 @@ public class ChunkedWriteHandler extends ChannelDuplexHandler { message = Unpooled.EMPTY_BUFFER; } - ChannelFuture f = ctx.write(message); + // Flush each chunk to conserve memory + ChannelFuture f = ctx.writeAndFlush(message); if (endOfInput) { - this.currentWrite = null; - - // Register a listener which will close the input once the write is complete. - // This is needed because the Chunk may have some resource bound that can not - // be closed before its not written. - // - // See https://github.com/netty/netty/issues/303 - f.addListener(new ChannelFutureListener() { - @Override - public void operationComplete(ChannelFuture future) throws Exception { - if (!future.isSuccess()) { - closeInput(chunks); - currentWrite.fail(future.cause()); - } else { - currentWrite.progress(chunks.progress(), chunks.length()); - currentWrite.success(chunks.length()); - } - } - }); - } else if (channel.isWritable()) { - f.addListener(new ChannelFutureListener() { - @Override - public void operationComplete(ChannelFuture future) throws Exception { - if (!future.isSuccess()) { - closeInput((ChunkedInput<?>) pendingMessage); - currentWrite.fail(future.cause()); - } else { - currentWrite.progress(chunks.progress(), chunks.length()); + queue.remove(); + + if (f.isDone()) { + handleEndOfInputFuture(f, currentWrite); + } else { + // Register a listener which will close the input once the write is complete. + // This is needed because the Chunk may have some resource bound that can not + // be closed before its not written. + // + // See https://github.com/netty/netty/issues/303 + f.addListener(new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture future) { + handleEndOfInputFuture(future, currentWrite); } - } - }); + }); + } } else { - f.addListener(new ChannelFutureListener() { - @Override - public void operationComplete(ChannelFuture future) throws Exception { - if (!future.isSuccess()) { - closeInput((ChunkedInput<?>) pendingMessage); - currentWrite.fail(future.cause()); - } else { - currentWrite.progress(chunks.progress(), chunks.length()); - if (channel.isWritable()) { - resumeTransfer(); - } + final boolean resume = !channel.isWritable(); + if (f.isDone()) { + handleFuture(f, currentWrite, resume); + } else { + f.addListener(new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture future) { + handleFuture(future, currentWrite, resume); } - } - }); + }); + } } - // Flush each chunk to conserve memory - ctx.flush(); requiresFlush = false; } else { - this.currentWrite = null; + queue.remove(); ctx.write(pendingMessage, currentWrite.promise); requiresFlush = true; } @@ -336,6 +312,34 @@ public class ChunkedWriteHandler extends ChannelDuplexHandler { } } + private static void handleEndOfInputFuture(ChannelFuture future, PendingWrite currentWrite) { + ChunkedInput<?> input = (ChunkedInput<?>) currentWrite.msg; + if (!future.isSuccess()) { + closeInput(input); + currentWrite.fail(future.cause()); + } else { + // read state of the input in local variables before closing it + long inputProgress = input.progress(); + long inputLength = input.length(); + closeInput(input); + currentWrite.progress(inputProgress, inputLength); + currentWrite.success(inputLength); + } + } + + private void handleFuture(ChannelFuture future, PendingWrite currentWrite, boolean resume) { + ChunkedInput<?> input = (ChunkedInput<?>) currentWrite.msg; + if (!future.isSuccess()) { + closeInput(input); + currentWrite.fail(future.cause()); + } else { + currentWrite.progress(input.progress(), input.length()); + if (resume && future.channel().isWritable()) { + resumeTransfer(); + } + } + } + private static void closeInput(ChunkedInput<?> chunks) { try { chunks.close(); diff --git a/handler/src/main/java/io/netty/handler/timeout/IdleStateEvent.java b/handler/src/main/java/io/netty/handler/timeout/IdleStateEvent.java index 7ec9e63..0251d06 100644 --- a/handler/src/main/java/io/netty/handler/timeout/IdleStateEvent.java +++ b/handler/src/main/java/io/netty/handler/timeout/IdleStateEvent.java @@ -17,17 +17,24 @@ package io.netty.handler.timeout; import io.netty.channel.Channel; import io.netty.util.internal.ObjectUtil; +import io.netty.util.internal.StringUtil; /** * A user event triggered by {@link IdleStateHandler} when a {@link Channel} is idle. */ public class IdleStateEvent { - public static final IdleStateEvent FIRST_READER_IDLE_STATE_EVENT = new IdleStateEvent(IdleState.READER_IDLE, true); - public static final IdleStateEvent READER_IDLE_STATE_EVENT = new IdleStateEvent(IdleState.READER_IDLE, false); - public static final IdleStateEvent FIRST_WRITER_IDLE_STATE_EVENT = new IdleStateEvent(IdleState.WRITER_IDLE, true); - public static final IdleStateEvent WRITER_IDLE_STATE_EVENT = new IdleStateEvent(IdleState.WRITER_IDLE, false); - public static final IdleStateEvent FIRST_ALL_IDLE_STATE_EVENT = new IdleStateEvent(IdleState.ALL_IDLE, true); - public static final IdleStateEvent ALL_IDLE_STATE_EVENT = new IdleStateEvent(IdleState.ALL_IDLE, false); + public static final IdleStateEvent FIRST_READER_IDLE_STATE_EVENT = + new DefaultIdleStateEvent(IdleState.READER_IDLE, true); + public static final IdleStateEvent READER_IDLE_STATE_EVENT = + new DefaultIdleStateEvent(IdleState.READER_IDLE, false); + public static final IdleStateEvent FIRST_WRITER_IDLE_STATE_EVENT = + new DefaultIdleStateEvent(IdleState.WRITER_IDLE, true); + public static final IdleStateEvent WRITER_IDLE_STATE_EVENT = + new DefaultIdleStateEvent(IdleState.WRITER_IDLE, false); + public static final IdleStateEvent FIRST_ALL_IDLE_STATE_EVENT = + new DefaultIdleStateEvent(IdleState.ALL_IDLE, true); + public static final IdleStateEvent ALL_IDLE_STATE_EVENT = + new DefaultIdleStateEvent(IdleState.ALL_IDLE, false); private final IdleState state; private final boolean first; @@ -56,4 +63,23 @@ public class IdleStateEvent { public boolean isFirst() { return first; } + + @Override + public String toString() { + return StringUtil.simpleClassName(this) + '(' + state + (first ? ", first" : "") + ')'; + } + + private static final class DefaultIdleStateEvent extends IdleStateEvent { + private final String representation; + + DefaultIdleStateEvent(IdleState state, boolean first) { + super(state, first); + this.representation = "IdleStateEvent(" + state + (first ? ", first" : "") + ')'; + } + + @Override + public String toString() { + return representation; + } + } } diff --git a/handler/src/main/java/io/netty/handler/timeout/IdleStateHandler.java b/handler/src/main/java/io/netty/handler/timeout/IdleStateHandler.java index 299e4c7..c36ef42 100644 --- a/handler/src/main/java/io/netty/handler/timeout/IdleStateHandler.java +++ b/handler/src/main/java/io/netty/handler/timeout/IdleStateHandler.java @@ -25,6 +25,7 @@ import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelOutboundBuffer; import io.netty.channel.ChannelPromise; +import io.netty.util.internal.ObjectUtil; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; @@ -129,6 +130,7 @@ public class IdleStateHandler extends ChannelDuplexHandler { private long lastChangeCheckTimeStamp; private int lastMessageHashCode; private long lastPendingWriteBytes; + private long lastFlushProgress; /** * Creates a new instance firing {@link IdleStateEvent}s. @@ -189,9 +191,7 @@ public class IdleStateHandler extends ChannelDuplexHandler { public IdleStateHandler(boolean observeOutput, long readerIdleTime, long writerIdleTime, long allIdleTime, TimeUnit unit) { - if (unit == null) { - throw new NullPointerException("unit"); - } + ObjectUtil.checkNotNull(unit, "unit"); this.observeOutput = observeOutput; @@ -399,6 +399,7 @@ public class IdleStateHandler extends ChannelDuplexHandler { if (buf != null) { lastMessageHashCode = System.identityHashCode(buf.current()); lastPendingWriteBytes = buf.totalPendingWriteBytes(); + lastFlushProgress = buf.currentProgress(); } } } @@ -443,6 +444,15 @@ public class IdleStateHandler extends ChannelDuplexHandler { return true; } } + + long flushProgress = buf.currentProgress(); + if (flushProgress != lastFlushProgress) { + lastFlushProgress = flushProgress; + + if (!first) { + return true; + } + } } } diff --git a/handler/src/main/java/io/netty/handler/timeout/ReadTimeoutException.java b/handler/src/main/java/io/netty/handler/timeout/ReadTimeoutException.java index b0aaa95..032ed57 100644 --- a/handler/src/main/java/io/netty/handler/timeout/ReadTimeoutException.java +++ b/handler/src/main/java/io/netty/handler/timeout/ReadTimeoutException.java @@ -15,6 +15,8 @@ */ package io.netty.handler.timeout; +import io.netty.util.internal.PlatformDependent; + /** * A {@link TimeoutException} raised by {@link ReadTimeoutHandler} when no data * was read within a certain period of time. @@ -23,7 +25,12 @@ public final class ReadTimeoutException extends TimeoutException { private static final long serialVersionUID = 169287984113283421L; - public static final ReadTimeoutException INSTANCE = new ReadTimeoutException(); + public static final ReadTimeoutException INSTANCE = PlatformDependent.javaVersion() >= 7 ? + new ReadTimeoutException(true) : new ReadTimeoutException(); + + ReadTimeoutException() { } - private ReadTimeoutException() { } + private ReadTimeoutException(boolean shared) { + super(shared); + } } diff --git a/handler/src/main/java/io/netty/handler/timeout/TimeoutException.java b/handler/src/main/java/io/netty/handler/timeout/TimeoutException.java index 072220b..256da86 100644 --- a/handler/src/main/java/io/netty/handler/timeout/TimeoutException.java +++ b/handler/src/main/java/io/netty/handler/timeout/TimeoutException.java @@ -25,7 +25,12 @@ public class TimeoutException extends ChannelException { private static final long serialVersionUID = 4673641882869672533L; - TimeoutException() { } + TimeoutException() { + } + + TimeoutException(boolean shared) { + super(null, null, shared); + } @Override public Throwable fillInStackTrace() { diff --git a/handler/src/main/java/io/netty/handler/timeout/WriteTimeoutException.java b/handler/src/main/java/io/netty/handler/timeout/WriteTimeoutException.java index f728608..6e7cc97 100644 --- a/handler/src/main/java/io/netty/handler/timeout/WriteTimeoutException.java +++ b/handler/src/main/java/io/netty/handler/timeout/WriteTimeoutException.java @@ -15,15 +15,22 @@ */ package io.netty.handler.timeout; +import io.netty.util.internal.PlatformDependent; + /** - * A {@link TimeoutException} raised by {@link WriteTimeoutHandler} when no data - * was written within a certain period of time. + * A {@link TimeoutException} raised by {@link WriteTimeoutHandler} when a write operation + * cannot finish in a certain period of time. */ public final class WriteTimeoutException extends TimeoutException { private static final long serialVersionUID = -144786655770296065L; - public static final WriteTimeoutException INSTANCE = new WriteTimeoutException(); + public static final WriteTimeoutException INSTANCE = PlatformDependent.javaVersion() >= 7 ? + new WriteTimeoutException(true) : new WriteTimeoutException(); private WriteTimeoutException() { } + + private WriteTimeoutException(boolean shared) { + super(shared); + } } diff --git a/handler/src/main/java/io/netty/handler/timeout/WriteTimeoutHandler.java b/handler/src/main/java/io/netty/handler/timeout/WriteTimeoutHandler.java index 70c5881..71ed283 100644 --- a/handler/src/main/java/io/netty/handler/timeout/WriteTimeoutHandler.java +++ b/handler/src/main/java/io/netty/handler/timeout/WriteTimeoutHandler.java @@ -24,6 +24,7 @@ import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelOutboundHandlerAdapter; import io.netty.channel.ChannelPromise; +import io.netty.util.internal.ObjectUtil; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; @@ -93,9 +94,7 @@ public class WriteTimeoutHandler extends ChannelOutboundHandlerAdapter { * the {@link TimeUnit} of {@code timeout} */ public WriteTimeoutHandler(long timeout, TimeUnit unit) { - if (unit == null) { - throw new NullPointerException("unit"); - } + ObjectUtil.checkNotNull(unit, "unit"); if (timeout <= 0) { timeoutNanos = 0; diff --git a/handler/src/main/java/io/netty/handler/traffic/AbstractTrafficShapingHandler.java b/handler/src/main/java/io/netty/handler/traffic/AbstractTrafficShapingHandler.java index 09e0f38..ae7f89c 100644 --- a/handler/src/main/java/io/netty/handler/traffic/AbstractTrafficShapingHandler.java +++ b/handler/src/main/java/io/netty/handler/traffic/AbstractTrafficShapingHandler.java @@ -67,7 +67,7 @@ public abstract class AbstractTrafficShapingHandler extends ChannelDuplexHandler static final long DEFAULT_MAX_SIZE = 4 * 1024 * 1024L; /** - * Default minimal time to wait + * Default minimal time to wait: 10ms */ static final long MINIMAL_WAIT = 10; diff --git a/handler/src/main/java/io/netty/handler/traffic/GlobalChannelTrafficCounter.java b/handler/src/main/java/io/netty/handler/traffic/GlobalChannelTrafficCounter.java index 6af7de8..9741584 100644 --- a/handler/src/main/java/io/netty/handler/traffic/GlobalChannelTrafficCounter.java +++ b/handler/src/main/java/io/netty/handler/traffic/GlobalChannelTrafficCounter.java @@ -78,8 +78,6 @@ public class GlobalChannelTrafficCounter extends TrafficCounter { perChannel.channelTrafficCounter.resetAccounting(newLastTime); } trafficShapingHandler1.doAccounting(counter); - counter.scheduledFuture = counter.executor.schedule(this, counter.checkInterval.get(), - TimeUnit.MILLISECONDS); } } @@ -97,7 +95,7 @@ public class GlobalChannelTrafficCounter extends TrafficCounter { monitorActive = true; monitor = new MixedTrafficMonitoringTask((GlobalChannelTrafficShapingHandler) trafficShapingHandler, this); scheduledFuture = - executor.schedule(monitor, localCheckInterval, TimeUnit.MILLISECONDS); + executor.scheduleAtFixedRate(monitor, 0, localCheckInterval, TimeUnit.MILLISECONDS); } } diff --git a/handler/src/main/java/io/netty/handler/traffic/GlobalTrafficShapingHandler.java b/handler/src/main/java/io/netty/handler/traffic/GlobalTrafficShapingHandler.java index ad624f8..d6e59cc 100644 --- a/handler/src/main/java/io/netty/handler/traffic/GlobalTrafficShapingHandler.java +++ b/handler/src/main/java/io/netty/handler/traffic/GlobalTrafficShapingHandler.java @@ -21,6 +21,7 @@ import io.netty.channel.Channel; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelPromise; import io.netty.util.concurrent.EventExecutor; +import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.PlatformDependent; import java.util.ArrayDeque; @@ -103,10 +104,11 @@ public class GlobalTrafficShapingHandler extends AbstractTrafficShapingHandler { * Create the global TrafficCounter. */ void createGlobalTrafficCounter(ScheduledExecutorService executor) { - if (executor == null) { - throw new NullPointerException("executor"); - } - TrafficCounter tc = new TrafficCounter(this, executor, "GlobalTC", checkInterval); + TrafficCounter tc = new TrafficCounter(this, + ObjectUtil.checkNotNull(executor, "executor"), + "GlobalTC", + checkInterval); + setTrafficCounter(tc); tc.start(); } diff --git a/handler/src/main/java/io/netty/handler/traffic/TrafficCounter.java b/handler/src/main/java/io/netty/handler/traffic/TrafficCounter.java index 65fc77e..0ca40a5 100644 --- a/handler/src/main/java/io/netty/handler/traffic/TrafficCounter.java +++ b/handler/src/main/java/io/netty/handler/traffic/TrafficCounter.java @@ -15,6 +15,7 @@ */ package io.netty.handler.traffic; +import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; @@ -174,7 +175,6 @@ public class TrafficCounter { if (trafficShapingHandler != null) { trafficShapingHandler.doAccounting(TrafficCounter.this); } - scheduledFuture = executor.schedule(this, checkInterval.get(), TimeUnit.MILLISECONDS); } } @@ -192,7 +192,7 @@ public class TrafficCounter { monitorActive = true; monitor = new TrafficMonitoringTask(); scheduledFuture = - executor.schedule(monitor, localCheckInterval, TimeUnit.MILLISECONDS); + executor.scheduleAtFixedRate(monitor, 0, localCheckInterval, TimeUnit.MILLISECONDS); } } @@ -251,13 +251,10 @@ public class TrafficCounter { * the checkInterval in millisecond between two computations. */ public TrafficCounter(ScheduledExecutorService executor, String name, long checkInterval) { - if (name == null) { - throw new NullPointerException("name"); - } + this.name = ObjectUtil.checkNotNull(name, "name"); trafficShapingHandler = null; this.executor = executor; - this.name = name; init(checkInterval); } @@ -283,13 +280,10 @@ public class TrafficCounter { if (trafficShapingHandler == null) { throw new IllegalArgumentException("trafficShapingHandler"); } - if (name == null) { - throw new NullPointerException("name"); - } + this.name = ObjectUtil.checkNotNull(name, "name"); this.trafficShapingHandler = trafficShapingHandler; this.executor = executor; - this.name = name; init(checkInterval); } @@ -317,7 +311,8 @@ public class TrafficCounter { // No more active monitoring lastTime.set(milliSecondFromNano()); } else { - // Start if necessary + // Restart + stop(); start(); } } diff --git a/handler/src/main/resources/META-INF/native-image/io.netty/handler/native-image.properties b/handler/src/main/resources/META-INF/native-image/io.netty/handler/native-image.properties new file mode 100644 index 0000000..edbc7f1 --- /dev/null +++ b/handler/src/main/resources/META-INF/native-image/io.netty/handler/native-image.properties @@ -0,0 +1,15 @@ +# Copyright 2019 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. + +Args = --initialize-at-run-time=io.netty.handler.ssl.util.ThreadLocalInsecureRandom diff --git a/handler/src/test/java/io/netty/handler/address/DynamicAddressConnectHandlerTest.java b/handler/src/test/java/io/netty/handler/address/DynamicAddressConnectHandlerTest.java new file mode 100644 index 0000000..574a959 --- /dev/null +++ b/handler/src/test/java/io/netty/handler/address/DynamicAddressConnectHandlerTest.java @@ -0,0 +1,107 @@ +/* + * Copyright 2019 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.address; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelOutboundHandlerAdapter; +import io.netty.channel.ChannelPromise; +import io.netty.channel.embedded.EmbeddedChannel; +import org.junit.Test; + +import java.net.SocketAddress; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; + +public class DynamicAddressConnectHandlerTest { + private static final SocketAddress LOCAL = new SocketAddress() { }; + private static final SocketAddress LOCAL_NEW = new SocketAddress() { }; + private static final SocketAddress REMOTE = new SocketAddress() { }; + private static final SocketAddress REMOTE_NEW = new SocketAddress() { }; + @Test + public void testReplaceAddresses() { + + EmbeddedChannel channel = new EmbeddedChannel(new ChannelOutboundHandlerAdapter() { + @Override + public void connect(ChannelHandlerContext ctx, SocketAddress remoteAddress, + SocketAddress localAddress, ChannelPromise promise) { + try { + assertSame(REMOTE_NEW, remoteAddress); + assertSame(LOCAL_NEW, localAddress); + promise.setSuccess(); + } catch (Throwable cause) { + promise.setFailure(cause); + } + } + }, new DynamicAddressConnectHandler() { + @Override + protected SocketAddress localAddress(SocketAddress remoteAddress, SocketAddress localAddress) { + assertSame(REMOTE, remoteAddress); + assertSame(LOCAL, localAddress); + return LOCAL_NEW; + } + + @Override + protected SocketAddress remoteAddress(SocketAddress remoteAddress, SocketAddress localAddress) { + assertSame(REMOTE, remoteAddress); + assertSame(LOCAL, localAddress); + return REMOTE_NEW; + } + }); + channel.connect(REMOTE, LOCAL).syncUninterruptibly(); + assertNull(channel.pipeline().get(DynamicAddressConnectHandler.class)); + assertFalse(channel.finish()); + } + + @Test + public void testLocalAddressThrows() { + testThrows0(true); + } + + @Test + public void testRemoteAddressThrows() { + testThrows0(false); + } + + private static void testThrows0(final boolean localThrows) { + final IllegalStateException exception = new IllegalStateException(); + + EmbeddedChannel channel = new EmbeddedChannel(new DynamicAddressConnectHandler() { + @Override + protected SocketAddress localAddress( + SocketAddress remoteAddress, SocketAddress localAddress) throws Exception { + if (localThrows) { + throw exception; + } + return super.localAddress(remoteAddress, localAddress); + } + + @Override + protected SocketAddress remoteAddress(SocketAddress remoteAddress, SocketAddress localAddress) + throws Exception { + if (!localThrows) { + throw exception; + } + return super.remoteAddress(remoteAddress, localAddress); + } + }); + assertSame(exception, channel.connect(REMOTE, LOCAL).cause()); + assertNotNull(channel.pipeline().get(DynamicAddressConnectHandler.class)); + assertFalse(channel.finish()); + } +} diff --git a/handler/src/test/java/io/netty/handler/address/ResolveAddressHandlerTest.java b/handler/src/test/java/io/netty/handler/address/ResolveAddressHandlerTest.java new file mode 100644 index 0000000..4685869 --- /dev/null +++ b/handler/src/test/java/io/netty/handler/address/ResolveAddressHandlerTest.java @@ -0,0 +1,139 @@ +/* + * Copyright 2020 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.address; + +import io.netty.bootstrap.Bootstrap; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.DefaultEventLoopGroup; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.local.LocalAddress; +import io.netty.channel.local.LocalChannel; +import io.netty.channel.local.LocalServerChannel; +import io.netty.resolver.AbstractAddressResolver; +import io.netty.resolver.AddressResolver; +import io.netty.resolver.AddressResolverGroup; +import io.netty.util.concurrent.EventExecutor; +import io.netty.util.concurrent.Promise; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.net.SocketAddress; +import java.net.UnknownHostException; +import java.util.List; +import java.util.UUID; + +import static org.junit.Assert.*; + +public class ResolveAddressHandlerTest { + + private static final LocalAddress UNRESOLVED = new LocalAddress("unresolved-" + UUID.randomUUID().toString()); + private static final LocalAddress RESOLVED = new LocalAddress("resolved-" + UUID.randomUUID().toString()); + private static final Exception ERROR = new UnknownHostException(); + + private static EventLoopGroup group; + + @BeforeClass + public static void createEventLoop() { + group = new DefaultEventLoopGroup(); + } + + @AfterClass + public static void destroyEventLoop() { + if (group != null) { + group.shutdownGracefully(); + } + } + + @Test + public void testResolveSuccessful() { + testResolve(false); + } + + @Test + public void testResolveFails() { + testResolve(true); + } + + private static void testResolve(boolean fail) { + AddressResolverGroup<SocketAddress> resolverGroup = new TestResolverGroup(fail); + Bootstrap cb = new Bootstrap(); + cb.group(group).channel(LocalChannel.class).handler(new ResolveAddressHandler(resolverGroup)); + + ServerBootstrap sb = new ServerBootstrap(); + sb.group(group) + .channel(LocalServerChannel.class) + .childHandler(new ChannelInboundHandlerAdapter() { + @Override + public void channelActive(ChannelHandlerContext ctx) { + ctx.close(); + } + }); + + // Start server + Channel sc = sb.bind(RESOLVED).syncUninterruptibly().channel(); + ChannelFuture future = cb.connect(UNRESOLVED).awaitUninterruptibly(); + try { + if (fail) { + assertSame(ERROR, future.cause()); + } else { + assertTrue(future.isSuccess()); + } + future.channel().close().syncUninterruptibly(); + } finally { + future.channel().close().syncUninterruptibly(); + sc.close().syncUninterruptibly(); + resolverGroup.close(); + } + } + + private static final class TestResolverGroup extends AddressResolverGroup<SocketAddress> { + private final boolean fail; + + TestResolverGroup(boolean fail) { + this.fail = fail; + } + + @Override + protected AddressResolver<SocketAddress> newResolver(EventExecutor executor) { + return new AbstractAddressResolver<SocketAddress>(executor) { + @Override + protected boolean doIsResolved(SocketAddress address) { + return address == RESOLVED; + } + + @Override + protected void doResolve(SocketAddress unresolvedAddress, Promise<SocketAddress> promise) { + assertSame(UNRESOLVED, unresolvedAddress); + if (fail) { + promise.setFailure(ERROR); + } else { + promise.setSuccess(RESOLVED); + } + } + + @Override + protected void doResolveAll(SocketAddress unresolvedAddress, Promise<List<SocketAddress>> promise) { + fail(); + } + }; + } + }; +} diff --git a/handler/src/test/java/io/netty/handler/flow/FlowControlHandlerTest.java b/handler/src/test/java/io/netty/handler/flow/FlowControlHandlerTest.java index a4effb1..5876137 100644 --- a/handler/src/test/java/io/netty/handler/flow/FlowControlHandlerTest.java +++ b/handler/src/test/java/io/netty/handler/flow/FlowControlHandlerTest.java @@ -29,10 +29,13 @@ import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelOption; import io.netty.channel.ChannelPipeline; import io.netty.channel.EventLoopGroup; +import io.netty.channel.embedded.EmbeddedChannel; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.handler.codec.ByteToMessageDecoder; +import io.netty.handler.timeout.IdleStateEvent; +import io.netty.handler.timeout.IdleStateHandler; import io.netty.util.ReferenceCountUtil; import org.junit.AfterClass; import org.junit.BeforeClass; @@ -40,12 +43,14 @@ import org.junit.Test; import java.net.SocketAddress; import java.util.List; +import java.util.Queue; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Exchanger; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.atomic.AtomicReference; import static java.util.concurrent.TimeUnit.*; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; +import static org.junit.Assert.*; public class FlowControlHandlerTest { private static EventLoopGroup GROUP; @@ -76,7 +81,7 @@ public class FlowControlHandlerTest { .childOption(ChannelOption.AUTO_READ, autoRead) .childHandler(new ChannelInitializer<Channel>() { @Override - protected void initChannel(Channel ch) throws Exception { + protected void initChannel(Channel ch) { ChannelPipeline pipeline = ch.pipeline(); pipeline.addLast(new OneByteToThreeStringsDecoder()); pipeline.addLast(handlers); @@ -368,13 +373,119 @@ public class FlowControlHandlerTest { } } + @Test + public void testReentranceNotCausesNPE() throws Throwable { + final Exchanger<Channel> peerRef = new Exchanger<Channel>(); + final CountDownLatch latch = new CountDownLatch(3); + final AtomicReference<Throwable> causeRef = new AtomicReference<Throwable>(); + ChannelInboundHandlerAdapter handler = new ChannelDuplexHandler() { + @Override + public void channelActive(ChannelHandlerContext ctx) throws Exception { + ctx.fireChannelActive(); + peerRef.exchange(ctx.channel(), 1L, SECONDS); + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + latch.countDown(); + ctx.read(); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + causeRef.set(cause); + } + }; + + FlowControlHandler flow = new FlowControlHandler(); + Channel server = newServer(false, flow, handler); + Channel client = newClient(server.localAddress()); + try { + // The client connection on the server side + Channel peer = peerRef.exchange(null, 1L, SECONDS); + + // Write the message + client.writeAndFlush(newOneMessage()) + .syncUninterruptibly(); + + // channelRead(1) + peer.read(); + assertTrue(latch.await(1L, SECONDS)); + assertTrue(flow.isQueueEmpty()); + + Throwable cause = causeRef.get(); + if (cause != null) { + throw cause; + } + } finally { + client.close(); + server.close(); + } + } + + @Test + public void testSwallowedReadComplete() throws Exception { + final long delayMillis = 100; + final Queue<IdleStateEvent> userEvents = new LinkedBlockingQueue<IdleStateEvent>(); + final EmbeddedChannel channel = new EmbeddedChannel(false, false, + new FlowControlHandler(), + new IdleStateHandler(delayMillis, 0, 0, MILLISECONDS), + new ChannelInboundHandlerAdapter() { + @Override + public void channelActive(ChannelHandlerContext ctx) { + ctx.fireChannelActive(); + ctx.read(); + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + ctx.fireChannelRead(msg); + ctx.read(); + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) { + ctx.fireChannelReadComplete(); + ctx.read(); + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { + if (evt instanceof IdleStateEvent) { + userEvents.add((IdleStateEvent) evt); + } + ctx.fireUserEventTriggered(evt); + } + } + ); + + channel.config().setAutoRead(false); + assertFalse(channel.config().isAutoRead()); + + channel.register(); + + // Reset read timeout by some message + assertTrue(channel.writeInbound(Unpooled.EMPTY_BUFFER)); + channel.flushInbound(); + assertEquals(Unpooled.EMPTY_BUFFER, channel.readInbound()); + + // Emulate 'no more messages in NIO channel' on the next read attempt. + channel.flushInbound(); + assertNull(channel.readInbound()); + + Thread.sleep(delayMillis); + channel.runPendingTasks(); + assertEquals(IdleStateEvent.FIRST_READER_IDLE_STATE_EVENT, userEvents.poll()); + assertFalse(channel.finish()); + } + /** * This is a fictional message decoder. It decodes each {@code byte} * into three strings. */ private static final class OneByteToThreeStringsDecoder extends ByteToMessageDecoder { @Override - protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception { + protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) { for (int i = 0; i < in.readableBytes(); i++) { out.add("1"); out.add("2"); diff --git a/handler/src/test/java/io/netty/handler/flush/FlushConsolidationHandlerTest.java b/handler/src/test/java/io/netty/handler/flush/FlushConsolidationHandlerTest.java index 05eb80c..1a3af22 100644 --- a/handler/src/test/java/io/netty/handler/flush/FlushConsolidationHandlerTest.java +++ b/handler/src/test/java/io/netty/handler/flush/FlushConsolidationHandlerTest.java @@ -19,6 +19,8 @@ import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.channel.ChannelOutboundHandlerAdapter; import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.GenericFutureListener; import org.junit.Test; import java.util.concurrent.atomic.AtomicInteger; @@ -152,6 +154,26 @@ public class FlushConsolidationHandlerTest { assertFalse(channel.finish()); } + /** + * See https://github.com/netty/netty/issues/9923 + */ + @Test + public void testResend() throws Exception { + final AtomicInteger flushCount = new AtomicInteger(); + final EmbeddedChannel channel = newChannel(flushCount, true); + channel.writeAndFlush(1L).addListener(new GenericFutureListener<Future<? super Void>>() { + @Override + public void operationComplete(Future<? super Void> future) throws Exception { + channel.writeAndFlush(1L); + } + }); + channel.flushOutbound(); + assertEquals(1L, channel.readOutbound()); + assertEquals(1L, channel.readOutbound()); + assertNull(channel.readOutbound()); + assertFalse(channel.finish()); + } + private static EmbeddedChannel newChannel(final AtomicInteger flushCount, boolean consolidateWhenNoReadInProgress) { return new EmbeddedChannel( new ChannelOutboundHandlerAdapter() { diff --git a/handler/src/test/java/io/netty/handler/logging/LoggingHandlerTest.java b/handler/src/test/java/io/netty/handler/logging/LoggingHandlerTest.java index 2841688..622c89c 100644 --- a/handler/src/test/java/io/netty/handler/logging/LoggingHandlerTest.java +++ b/handler/src/test/java/io/netty/handler/logging/LoggingHandlerTest.java @@ -39,6 +39,7 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import static io.netty.util.internal.StringUtil.NEWLINE; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.nullValue; import static org.hamcrest.CoreMatchers.sameInstance; @@ -217,7 +218,20 @@ public class LoggingHandlerTest { ByteBuf msg = Unpooled.copiedBuffer("hello", CharsetUtil.UTF_8); EmbeddedChannel channel = new EmbeddedChannel(new LoggingHandler()); channel.writeInbound(msg); - verify(appender).doAppend(argThat(new RegexLogMatcher(".+READ: " + msg.readableBytes() + "B$"))); + verify(appender).doAppend(argThat(new RegexLogMatcher(".+READ: " + msg.readableBytes() + "B$", true))); + + ByteBuf handledMsg = channel.readInbound(); + assertThat(msg, is(sameInstance(handledMsg))); + handledMsg.release(); + assertThat(channel.readInbound(), is(nullValue())); + } + + @Test + public void shouldLogByteBufDataReadWithSimpleFormat() throws Exception { + ByteBuf msg = Unpooled.copiedBuffer("hello", CharsetUtil.UTF_8); + EmbeddedChannel channel = new EmbeddedChannel(new LoggingHandler(LogLevel.DEBUG, ByteBufFormat.SIMPLE)); + channel.writeInbound(msg); + verify(appender).doAppend(argThat(new RegexLogMatcher(".+READ: " + msg.readableBytes() + "B$", false))); ByteBuf handledMsg = channel.readInbound(); assertThat(msg, is(sameInstance(handledMsg))); @@ -230,7 +244,7 @@ public class LoggingHandlerTest { ByteBuf msg = Unpooled.EMPTY_BUFFER; EmbeddedChannel channel = new EmbeddedChannel(new LoggingHandler()); channel.writeInbound(msg); - verify(appender).doAppend(argThat(new RegexLogMatcher(".+READ: 0B$"))); + verify(appender).doAppend(argThat(new RegexLogMatcher(".+READ: 0B$", false))); ByteBuf handledMsg = channel.readInbound(); assertThat(msg, is(sameInstance(handledMsg))); @@ -248,7 +262,7 @@ public class LoggingHandlerTest { EmbeddedChannel channel = new EmbeddedChannel(new LoggingHandler()); channel.writeInbound(msg); - verify(appender).doAppend(argThat(new RegexLogMatcher(".+READ: foobar, 5B$"))); + verify(appender).doAppend(argThat(new RegexLogMatcher(".+READ: foobar, 5B$", true))); ByteBufHolder handledMsg = channel.readInbound(); assertThat(msg, is(sameInstance(handledMsg))); @@ -270,10 +284,16 @@ public class LoggingHandlerTest { private static final class RegexLogMatcher implements ArgumentMatcher<ILoggingEvent> { private final String expected; + private final boolean shouldContainNewline; private String actualMsg; RegexLogMatcher(String expected) { + this(expected, false); + } + + RegexLogMatcher(String expected, boolean shouldContainNewline) { this.expected = expected; + this.shouldContainNewline = shouldContainNewline; } @Override @@ -281,7 +301,11 @@ public class LoggingHandlerTest { public boolean matches(ILoggingEvent actual) { // Match only the first line to skip the validation of hex-dump format. actualMsg = actual.getMessage().split("(?s)[\\r\\n]+")[0]; - return actualMsg.matches(expected); + if (actualMsg.matches(expected)) { + // The presence of a newline implies a hex-dump was logged + return actual.getMessage().contains(NEWLINE) == shouldContainNewline; + } + return false; } } diff --git a/handler/src/test/java/io/netty/handler/ssl/AmazonCorrettoSslEngineTest.java b/handler/src/test/java/io/netty/handler/ssl/AmazonCorrettoSslEngineTest.java new file mode 100644 index 0000000..6c7f4c1 --- /dev/null +++ b/handler/src/test/java/io/netty/handler/ssl/AmazonCorrettoSslEngineTest.java @@ -0,0 +1,118 @@ +/* + * Copyright 2019 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 com.amazon.corretto.crypto.provider.AmazonCorrettoCryptoProvider; +import com.amazon.corretto.crypto.provider.SelfTestStatus; +import io.netty.util.internal.PlatformDependent; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Ignore; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import javax.crypto.Cipher; +import java.security.Security; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import static org.junit.Assume.assumeTrue; + +@RunWith(Parameterized.class) +public class AmazonCorrettoSslEngineTest extends SSLEngineTest { + + @Parameterized.Parameters(name = "{index}: bufferType = {0}, combo = {1}, delegate = {2}") + public static Collection<Object[]> data() { + List<Object[]> params = new ArrayList<Object[]>(); + for (BufferType type: BufferType.values()) { + params.add(new Object[] { type, ProtocolCipherCombo.tlsv12(), false }); + params.add(new Object[] { type, ProtocolCipherCombo.tlsv12(), true }); + + if (PlatformDependent.javaVersion() >= 11) { + params.add(new Object[] { type, ProtocolCipherCombo.tlsv13(), true }); + params.add(new Object[] { type, ProtocolCipherCombo.tlsv13(), false }); + } + } + return params; + } + + public AmazonCorrettoSslEngineTest(BufferType type, ProtocolCipherCombo combo, boolean delegate) { + super(type, combo, delegate); + } + + @BeforeClass + public static void checkAccp() { + assumeTrue(AmazonCorrettoCryptoProvider.INSTANCE.getLoadingError() == null && + AmazonCorrettoCryptoProvider.INSTANCE.runSelfTests().equals(SelfTestStatus.PASSED)); + } + + @Override + protected SslProvider sslClientProvider() { + return SslProvider.JDK; + } + + @Override + protected SslProvider sslServerProvider() { + return SslProvider.JDK; + } + + @Before + @Override + public void setup() { + // See https://github.com/corretto/amazon-corretto-crypto-provider/blob/develop/README.md#code + Security.insertProviderAt(AmazonCorrettoCryptoProvider.INSTANCE, 1); + + // See https://github.com/corretto/amazon-corretto-crypto-provider/blob/develop/README.md#verification-optional + try { + AmazonCorrettoCryptoProvider.INSTANCE.assertHealthy(); + String providerName = Cipher.getInstance("AES/GCM/NoPadding").getProvider().getName(); + Assert.assertEquals(AmazonCorrettoCryptoProvider.PROVIDER_NAME, providerName); + } catch (Throwable e) { + Security.removeProvider(AmazonCorrettoCryptoProvider.PROVIDER_NAME); + throw new AssertionError(e); + } + super.setup(); + } + + @After + @Override + public void tearDown() throws InterruptedException { + super.tearDown(); + + // Remove the provider again and verify that it was removed + Security.removeProvider(AmazonCorrettoCryptoProvider.PROVIDER_NAME); + Assert.assertNull(Security.getProvider(AmazonCorrettoCryptoProvider.PROVIDER_NAME)); + } + + @Ignore /* Does the JDK support a "max certificate chain length"? */ + @Override + public void testMutualAuthValidClientCertChainTooLongFailOptionalClientAuth() { + } + + @Ignore /* Does the JDK support a "max certificate chain length"? */ + @Override + public void testMutualAuthValidClientCertChainTooLongFailRequireClientAuth() { + } + + @Override + protected boolean mySetupMutualAuthServerIsValidException(Throwable cause) { + // TODO(scott): work around for a JDK issue. The exception should be SSLHandshakeException. + return super.mySetupMutualAuthServerIsValidException(cause) || causedBySSLException(cause); + } +} diff --git a/handler/src/test/java/io/netty/handler/ssl/CipherSuiteCanaryTest.java b/handler/src/test/java/io/netty/handler/ssl/CipherSuiteCanaryTest.java index 9c394cc..855cb84 100644 --- a/handler/src/test/java/io/netty/handler/ssl/CipherSuiteCanaryTest.java +++ b/handler/src/test/java/io/netty/handler/ssl/CipherSuiteCanaryTest.java @@ -18,6 +18,7 @@ package io.netty.handler.ssl; import io.netty.bootstrap.Bootstrap; import io.netty.bootstrap.ServerBootstrap; +import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.Unpooled; import io.netty.channel.Channel; import io.netty.channel.ChannelHandler; @@ -41,6 +42,9 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import org.junit.AfterClass; @@ -66,7 +70,7 @@ public class CipherSuiteCanaryTest { private static SelfSignedCertificate CERT; - @Parameters(name = "{index}: serverSslProvider = {0}, clientSslProvider = {1}, rfcCipherName = {2}") + @Parameters(name = "{index}: serverSslProvider = {0}, clientSslProvider = {1}, rfcCipherName = {2}, delegate = {3}") public static Collection<Object[]> parameters() { List<Object[]> dst = new ArrayList<Object[]>(); dst.addAll(expand("TLS_DHE_RSA_WITH_AES_128_GCM_SHA256")); // DHE-RSA-AES128-GCM-SHA256 @@ -80,7 +84,7 @@ public class CipherSuiteCanaryTest { } @AfterClass - public static void destory() { + public static void destroy() { GROUP.shutdownGracefully(); CERT.delete(); } @@ -90,11 +94,14 @@ public class CipherSuiteCanaryTest { private final SslProvider clientSslProvider; private final String rfcCipherName; + private final boolean delegate; - public CipherSuiteCanaryTest(SslProvider serverSslProvider, SslProvider clientSslProvider, String rfcCipherName) { + public CipherSuiteCanaryTest(SslProvider serverSslProvider, SslProvider clientSslProvider, + String rfcCipherName, boolean delegate) { this.serverSslProvider = serverSslProvider; this.clientSslProvider = clientSslProvider; this.rfcCipherName = rfcCipherName; + this.delegate = delegate; } private static void assumeCipherAvailable(SslProvider provider, String cipher) throws NoSuchAlgorithmException { @@ -113,6 +120,14 @@ public class CipherSuiteCanaryTest { Assume.assumeTrue("Unsupported cipher: " + cipher, cipherSupported); } + private static SslHandler newSslHandler(SslContext sslCtx, ByteBufAllocator allocator, Executor executor) { + if (executor == null) { + return sslCtx.newHandler(allocator); + } else { + return sslCtx.newHandler(allocator, executor); + } + } + @Test public void testHandshake() throws Exception { // Check if the cipher is supported at all which may not be the case for various JDK versions and OpenSSL API @@ -129,6 +144,8 @@ public class CipherSuiteCanaryTest { .protocols(SslUtils.PROTOCOL_TLS_V1_2) .build(); + final ExecutorService executorService = delegate ? Executors.newCachedThreadPool() : null; + try { final SslContext sslClientContext = SslContextBuilder.forClient() .sslProvider(clientSslProvider) @@ -146,7 +163,7 @@ public class CipherSuiteCanaryTest { @Override protected void initChannel(Channel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); - pipeline.addLast(sslServerContext.newHandler(ch.alloc())); + pipeline.addLast(newSslHandler(sslServerContext, ch.alloc(), executorService)); pipeline.addLast(new SimpleChannelInboundHandler<Object>() { @Override @@ -182,7 +199,7 @@ public class CipherSuiteCanaryTest { @Override protected void initChannel(Channel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); - pipeline.addLast(sslClientContext.newHandler(ch.alloc())); + pipeline.addLast(newSslHandler(sslClientContext, ch.alloc(), executorService)); pipeline.addLast(new SimpleChannelInboundHandler<Object>() { @Override @@ -229,6 +246,10 @@ public class CipherSuiteCanaryTest { } } finally { ReferenceCountUtil.release(sslServerContext); + + if (executorService != null) { + executorService.shutdown(); + } } } @@ -267,7 +288,8 @@ public class CipherSuiteCanaryTest { continue; } - dst.add(new Object[]{serverSslProvider, clientSslProvider, rfcCipherName}); + dst.add(new Object[]{serverSslProvider, clientSslProvider, rfcCipherName, true}); + dst.add(new Object[]{serverSslProvider, clientSslProvider, rfcCipherName, false}); } } diff --git a/handler/src/test/java/io/netty/handler/ssl/ConscryptJdkSslEngineInteropTest.java b/handler/src/test/java/io/netty/handler/ssl/ConscryptJdkSslEngineInteropTest.java index 0976264..da2d767 100644 --- a/handler/src/test/java/io/netty/handler/ssl/ConscryptJdkSslEngineInteropTest.java +++ b/handler/src/test/java/io/netty/handler/ssl/ConscryptJdkSslEngineInteropTest.java @@ -31,17 +31,18 @@ import static org.junit.Assume.assumeTrue; @RunWith(Parameterized.class) public class ConscryptJdkSslEngineInteropTest extends SSLEngineTest { - @Parameterized.Parameters(name = "{index}: bufferType = {0}, combo = {1}") + @Parameterized.Parameters(name = "{index}: bufferType = {0}, combo = {1}, delegate = {2}") public static Collection<Object[]> data() { List<Object[]> params = new ArrayList<Object[]>(); for (BufferType type: BufferType.values()) { - params.add(new Object[] { type, ProtocolCipherCombo.tlsv12()}); + params.add(new Object[] { type, ProtocolCipherCombo.tlsv12(), false }); + params.add(new Object[] { type, ProtocolCipherCombo.tlsv12(), true }); } return params; } - public ConscryptJdkSslEngineInteropTest(BufferType type, ProtocolCipherCombo combo) { - super(type, combo); + public ConscryptJdkSslEngineInteropTest(BufferType type, ProtocolCipherCombo combo, boolean delegate) { + super(type, combo, delegate); } @BeforeClass @@ -86,4 +87,11 @@ public class ConscryptJdkSslEngineInteropTest extends SSLEngineTest { // Ignore due bug in Conscrypt where the incorrect SSLSession object is used in the SSLSessionBindingEvent. // See https://github.com/google/conscrypt/issues/593 } + + @Ignore("Ignore due bug in Conscrypt") + @Override + public void testHandshakeSession() throws Exception { + // Ignore as Conscrypt does not correctly return the local certificates while the TrustManager is invoked. + // See https://github.com/google/conscrypt/issues/634 + } } diff --git a/handler/src/test/java/io/netty/handler/ssl/ConscryptOpenSslEngineInteropTest.java b/handler/src/test/java/io/netty/handler/ssl/ConscryptOpenSslEngineInteropTest.java new file mode 100644 index 0000000..2744ac5 --- /dev/null +++ b/handler/src/test/java/io/netty/handler/ssl/ConscryptOpenSslEngineInteropTest.java @@ -0,0 +1,158 @@ +/* + * Copyright 2019 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 org.junit.BeforeClass; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import javax.net.ssl.SSLEngine; + +import java.security.Provider; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import static io.netty.handler.ssl.OpenSslTestUtils.checkShouldUseKeyManagerFactory; +import static org.junit.Assume.assumeTrue; + +@RunWith(Parameterized.class) +public class ConscryptOpenSslEngineInteropTest extends ConscryptSslEngineTest { + + @Parameterized.Parameters(name = "{index}: bufferType = {0}, combo = {1}, delegate = {2}, useTasks = {3}") + public static Collection<Object[]> data() { + List<Object[]> params = new ArrayList<Object[]>(); + for (BufferType type: BufferType.values()) { + params.add(new Object[] { type, ProtocolCipherCombo.tlsv12(), false, false }); + params.add(new Object[] { type, ProtocolCipherCombo.tlsv12(), false, true }); + + params.add(new Object[] { type, ProtocolCipherCombo.tlsv12(), true, false }); + params.add(new Object[] { type, ProtocolCipherCombo.tlsv12(), true, true }); + } + return params; + } + + private final boolean useTasks; + + public ConscryptOpenSslEngineInteropTest(BufferType type, ProtocolCipherCombo combo, + boolean delegate, boolean useTasks) { + super(type, combo, delegate); + this.useTasks = useTasks; + } + + @BeforeClass + public static void checkOpenssl() { + assumeTrue(OpenSsl.isAvailable()); + } + + @Override + protected SslProvider sslClientProvider() { + return SslProvider.JDK; + } + + @Override + protected SslProvider sslServerProvider() { + return SslProvider.OPENSSL; + } + + @Override + protected Provider serverSslContextProvider() { + return null; + } + + @Override + @Test + @Ignore("TODO: Make this work with Conscrypt") + public void testMutualAuthValidClientCertChainTooLongFailOptionalClientAuth() { + super.testMutualAuthValidClientCertChainTooLongFailOptionalClientAuth(); + } + + @Override + @Test + @Ignore("TODO: Make this work with Conscrypt") + public void testMutualAuthValidClientCertChainTooLongFailRequireClientAuth() { + super.testMutualAuthValidClientCertChainTooLongFailRequireClientAuth(); + } + + @Override + protected boolean mySetupMutualAuthServerIsValidClientException(Throwable cause) { + // TODO(scott): work around for a JDK issue. The exception should be SSLHandshakeException. + return super.mySetupMutualAuthServerIsValidClientException(cause) || causedBySSLException(cause); + } + + @Override + @Test + public void testMutualAuthInvalidIntermediateCASucceedWithOptionalClientAuth() throws Exception { + checkShouldUseKeyManagerFactory(); + super.testMutualAuthInvalidIntermediateCASucceedWithOptionalClientAuth(); + } + + @Override + @Test + public void testMutualAuthInvalidIntermediateCAFailWithOptionalClientAuth() throws Exception { + checkShouldUseKeyManagerFactory(); + super.testMutualAuthInvalidIntermediateCAFailWithOptionalClientAuth(); + } + + @Override + @Test + public void testMutualAuthInvalidIntermediateCAFailWithRequiredClientAuth() throws Exception { + checkShouldUseKeyManagerFactory(); + super.testMutualAuthInvalidIntermediateCAFailWithRequiredClientAuth(); + } + + @Override + @Test + public void testSessionAfterHandshakeKeyManagerFactory() throws Exception { + checkShouldUseKeyManagerFactory(); + super.testSessionAfterHandshakeKeyManagerFactory(); + } + + @Override + @Test + public void testSessionAfterHandshakeKeyManagerFactoryMutualAuth() throws Exception { + checkShouldUseKeyManagerFactory(); + super.testSessionAfterHandshakeKeyManagerFactoryMutualAuth(); + } + + @Override + @Test + public void testSupportedSignatureAlgorithms() throws Exception { + checkShouldUseKeyManagerFactory(); + super.testSupportedSignatureAlgorithms(); + } + + @Override + protected boolean mySetupMutualAuthServerIsValidServerException(Throwable cause) { + // TODO(scott): work around for a JDK issue. The exception should be SSLHandshakeException. + return super.mySetupMutualAuthServerIsValidServerException(cause) || causedBySSLException(cause); + } + + @Override + protected SSLEngine wrapEngine(SSLEngine engine) { + return Java8SslTestUtils.wrapSSLEngineForTesting(engine); + } + + @Override + protected SslContext wrapContext(SslContext context) { + if (context instanceof OpenSslContext) { + ((OpenSslContext) context).setUseTasks(useTasks); + } + return context; + } +} diff --git a/handler/src/test/java/io/netty/handler/ssl/ConscryptSslEngineTest.java b/handler/src/test/java/io/netty/handler/ssl/ConscryptSslEngineTest.java index 7d06840..114552f 100644 --- a/handler/src/test/java/io/netty/handler/ssl/ConscryptSslEngineTest.java +++ b/handler/src/test/java/io/netty/handler/ssl/ConscryptSslEngineTest.java @@ -30,17 +30,18 @@ import static org.junit.Assume.assumeTrue; @RunWith(Parameterized.class) public class ConscryptSslEngineTest extends SSLEngineTest { - @Parameterized.Parameters(name = "{index}: bufferType = {0}, combo = {1}") + @Parameterized.Parameters(name = "{index}: bufferType = {0}, combo = {1}, delegate = {2}") public static Collection<Object[]> data() { List<Object[]> params = new ArrayList<Object[]>(); for (BufferType type: BufferType.values()) { - params.add(new Object[] { type, ProtocolCipherCombo.tlsv12()}); + params.add(new Object[] { type, ProtocolCipherCombo.tlsv12(), false }); + params.add(new Object[] { type, ProtocolCipherCombo.tlsv12(), true }); } return params; } - public ConscryptSslEngineTest(BufferType type, ProtocolCipherCombo combo) { - super(type, combo); + public ConscryptSslEngineTest(BufferType type, ProtocolCipherCombo combo, boolean delegate) { + super(type, combo, delegate); } @BeforeClass @@ -84,4 +85,11 @@ public class ConscryptSslEngineTest extends SSLEngineTest { // Ignore due bug in Conscrypt where the incorrect SSLSession object is used in the SSLSessionBindingEvent. // See https://github.com/google/conscrypt/issues/593 } + + @Ignore("Ignore due bug in Conscrypt") + @Override + public void testHandshakeSession() throws Exception { + // Ignore as Conscrypt does not correctly return the local certificates while the TrustManager is invoked. + // See https://github.com/google/conscrypt/issues/634 + } } diff --git a/handler/src/test/java/io/netty/handler/ssl/JdkConscryptSslEngineInteropTest.java b/handler/src/test/java/io/netty/handler/ssl/JdkConscryptSslEngineInteropTest.java index 309490a..d7aa08f 100644 --- a/handler/src/test/java/io/netty/handler/ssl/JdkConscryptSslEngineInteropTest.java +++ b/handler/src/test/java/io/netty/handler/ssl/JdkConscryptSslEngineInteropTest.java @@ -32,17 +32,18 @@ import static org.junit.Assume.assumeTrue; @RunWith(Parameterized.class) public class JdkConscryptSslEngineInteropTest extends SSLEngineTest { - @Parameterized.Parameters(name = "{index}: bufferType = {0}, combo = {1}") + @Parameterized.Parameters(name = "{index}: bufferType = {0}, combo = {1}, delegate = {2}") public static Collection<Object[]> data() { List<Object[]> params = new ArrayList<Object[]>(); for (BufferType type: BufferType.values()) { - params.add(new Object[] { type, ProtocolCipherCombo.tlsv12()}); + params.add(new Object[] { type, ProtocolCipherCombo.tlsv12(), false }); + params.add(new Object[] { type, ProtocolCipherCombo.tlsv12(), true }); } return params; } - public JdkConscryptSslEngineInteropTest(BufferType type, ProtocolCipherCombo combo) { - super(type, combo); + public JdkConscryptSslEngineInteropTest(BufferType type, ProtocolCipherCombo combo, boolean delegate) { + super(type, combo, delegate); } @BeforeClass @@ -84,4 +85,11 @@ public class JdkConscryptSslEngineInteropTest extends SSLEngineTest { // TODO(scott): work around for a JDK issue. The exception should be SSLHandshakeException. return super.mySetupMutualAuthServerIsValidClientException(cause) || causedBySSLException(cause); } + + @Ignore("Ignore due bug in Conscrypt") + @Override + public void testHandshakeSession() throws Exception { + // Ignore as Conscrypt does not correctly return the local certificates while the TrustManager is invoked. + // See https://github.com/google/conscrypt/issues/634 + } } diff --git a/handler/src/test/java/io/netty/handler/ssl/JdkOpenSslEngineInteroptTest.java b/handler/src/test/java/io/netty/handler/ssl/JdkOpenSslEngineInteroptTest.java index 45b3c7e..90e8d98 100644 --- a/handler/src/test/java/io/netty/handler/ssl/JdkOpenSslEngineInteroptTest.java +++ b/handler/src/test/java/io/netty/handler/ssl/JdkOpenSslEngineInteroptTest.java @@ -33,21 +33,33 @@ import static org.junit.Assume.assumeTrue; @RunWith(Parameterized.class) public class JdkOpenSslEngineInteroptTest extends SSLEngineTest { - @Parameterized.Parameters(name = "{index}: bufferType = {0}, combo = {1}") + @Parameterized.Parameters(name = "{index}: bufferType = {0}, combo = {1}, delegate = {2}, useTasks = {3}") public static Collection<Object[]> data() { List<Object[]> params = new ArrayList<Object[]>(); for (BufferType type: BufferType.values()) { - params.add(new Object[] { type, ProtocolCipherCombo.tlsv12()}); + params.add(new Object[] { type, ProtocolCipherCombo.tlsv12(), false, false }); + params.add(new Object[] { type, ProtocolCipherCombo.tlsv12(), false, true }); + + params.add(new Object[] { type, ProtocolCipherCombo.tlsv12(), true, false }); + params.add(new Object[] { type, ProtocolCipherCombo.tlsv12(), true, true }); if (PlatformDependent.javaVersion() >= 11 && OpenSsl.isTlsv13Supported()) { - params.add(new Object[] { type, ProtocolCipherCombo.tlsv13() }); + params.add(new Object[] { type, ProtocolCipherCombo.tlsv13(), false, false }); + params.add(new Object[] { type, ProtocolCipherCombo.tlsv13(), false, true }); + + params.add(new Object[] { type, ProtocolCipherCombo.tlsv13(), true, false }); + params.add(new Object[] { type, ProtocolCipherCombo.tlsv13(), true, true }); } } return params; } - public JdkOpenSslEngineInteroptTest(BufferType type, ProtocolCipherCombo protocolCipherCombo) { - super(type, protocolCipherCombo); + private final boolean useTasks; + + public JdkOpenSslEngineInteroptTest(BufferType type, ProtocolCipherCombo protocolCipherCombo, + boolean delegate, boolean useTasks) { + super(type, protocolCipherCombo, delegate); + this.useTasks = useTasks; } @BeforeClass @@ -126,8 +138,29 @@ public class JdkOpenSslEngineInteroptTest extends SSLEngineTest { return super.mySetupMutualAuthServerIsValidClientException(cause) || causedBySSLException(cause); } + @Override + public void testHandshakeSession() throws Exception { + checkShouldUseKeyManagerFactory(); + super.testHandshakeSession(); + } + + @Override + @Test + public void testSupportedSignatureAlgorithms() throws Exception { + checkShouldUseKeyManagerFactory(); + super.testSupportedSignatureAlgorithms(); + } + @Override protected SSLEngine wrapEngine(SSLEngine engine) { return Java8SslTestUtils.wrapSSLEngineForTesting(engine); } + + @Override + protected SslContext wrapContext(SslContext context) { + if (context instanceof OpenSslContext) { + ((OpenSslContext) context).setUseTasks(useTasks); + } + return context; + } } diff --git a/handler/src/test/java/io/netty/handler/ssl/JdkSslEngineTest.java b/handler/src/test/java/io/netty/handler/ssl/JdkSslEngineTest.java index 74f000f..b9fd045 100644 --- a/handler/src/test/java/io/netty/handler/ssl/JdkSslEngineTest.java +++ b/handler/src/test/java/io/netty/handler/ssl/JdkSslEngineTest.java @@ -142,14 +142,17 @@ public class JdkSslEngineTest extends SSLEngineTest { private static final String FALLBACK_APPLICATION_LEVEL_PROTOCOL = "my-protocol-http1_1"; private static final String APPLICATION_LEVEL_PROTOCOL_NOT_COMPATIBLE = "my-protocol-FOO"; - @Parameterized.Parameters(name = "{index}: providerType = {0}, bufferType = {1}, combo = {2}") + @Parameterized.Parameters(name = "{index}: providerType = {0}, bufferType = {1}, combo = {2}, delegate = {3}") public static Collection<Object[]> data() { List<Object[]> params = new ArrayList<Object[]>(); for (ProviderType providerType : ProviderType.values()) { for (BufferType bufferType : BufferType.values()) { - params.add(new Object[]{ providerType, bufferType, ProtocolCipherCombo.tlsv12()}); + params.add(new Object[]{ providerType, bufferType, ProtocolCipherCombo.tlsv12(), true }); + params.add(new Object[]{ providerType, bufferType, ProtocolCipherCombo.tlsv12(), false }); + if (PlatformDependent.javaVersion() >= 11) { - params.add(new Object[] { providerType, bufferType, ProtocolCipherCombo.tlsv13() }); + params.add(new Object[] { providerType, bufferType, ProtocolCipherCombo.tlsv13(), true }); + params.add(new Object[] { providerType, bufferType, ProtocolCipherCombo.tlsv13(), false }); } } } @@ -160,8 +163,9 @@ public class JdkSslEngineTest extends SSLEngineTest { private Provider provider; - public JdkSslEngineTest(ProviderType providerType, BufferType bufferType, ProtocolCipherCombo protocolCipherCombo) { - super(bufferType, protocolCipherCombo); + public JdkSslEngineTest(ProviderType providerType, BufferType bufferType, + ProtocolCipherCombo protocolCipherCombo, boolean delegate) { + super(bufferType, protocolCipherCombo, delegate); this.providerType = providerType; } @@ -234,7 +238,7 @@ public class JdkSslEngineTest extends SSLEngineTest { SslContext serverSslCtx = new JdkSslServerContext(providerType.provider(), ssc.certificate(), ssc.privateKey(), null, null, - IdentityCipherSuiteFilter.INSTANCE, serverApn, 0, 0); + IdentityCipherSuiteFilter.INSTANCE, serverApn, 0, 0, null); SslContext clientSslCtx = new JdkSslClientContext(providerType.provider(), null, InsecureTrustManagerFactory.INSTANCE, null, IdentityCipherSuiteFilter.INSTANCE, clientApn, 0, 0); diff --git a/handler/src/test/java/io/netty/handler/ssl/OpenSslCachingKeyMaterialProviderTest.java b/handler/src/test/java/io/netty/handler/ssl/OpenSslCachingKeyMaterialProviderTest.java index cfb4557..41a89ae 100644 --- a/handler/src/test/java/io/netty/handler/ssl/OpenSslCachingKeyMaterialProviderTest.java +++ b/handler/src/test/java/io/netty/handler/ssl/OpenSslCachingKeyMaterialProviderTest.java @@ -16,6 +16,7 @@ package io.netty.handler.ssl; import io.netty.buffer.UnpooledByteBufAllocator; +import org.hamcrest.CoreMatchers; import org.junit.Assert; import org.junit.Test; @@ -33,7 +34,7 @@ public class OpenSslCachingKeyMaterialProviderTest extends OpenSslKeyMaterialPro @Override protected OpenSslKeyMaterialProvider newMaterialProvider(KeyManagerFactory factory, String password) { return new OpenSslCachingKeyMaterialProvider(ReferenceCountedOpenSslContext.chooseX509KeyManager( - factory.getKeyManagers()), password); + factory.getKeyManagers()), password, Integer.MAX_VALUE); } @Override @@ -67,4 +68,22 @@ public class OpenSslCachingKeyMaterialProviderTest extends OpenSslKeyMaterialPro assertEquals(0, material.refCnt()); assertEquals(0, material2.refCnt()); } + + @Test + public void testCacheForSunX509() throws Exception { + OpenSslCachingX509KeyManagerFactory factory = new OpenSslCachingX509KeyManagerFactory( + super.newKeyManagerFactory("SunX509")); + OpenSslKeyMaterialProvider provider = factory.newProvider(PASSWORD); + assertThat(provider, + CoreMatchers.<OpenSslKeyMaterialProvider>instanceOf(OpenSslCachingKeyMaterialProvider.class)); + } + + @Test + public void testNotCacheForX509() throws Exception { + OpenSslCachingX509KeyManagerFactory factory = new OpenSslCachingX509KeyManagerFactory( + super.newKeyManagerFactory("PKIX")); + OpenSslKeyMaterialProvider provider = factory.newProvider(PASSWORD); + assertThat(provider, CoreMatchers.not( + CoreMatchers.<OpenSslKeyMaterialProvider>instanceOf(OpenSslCachingKeyMaterialProvider.class))); + } } diff --git a/handler/src/test/java/io/netty/handler/ssl/OpenSslConscryptSslEngineInteropTest.java b/handler/src/test/java/io/netty/handler/ssl/OpenSslConscryptSslEngineInteropTest.java new file mode 100644 index 0000000..16f224c --- /dev/null +++ b/handler/src/test/java/io/netty/handler/ssl/OpenSslConscryptSslEngineInteropTest.java @@ -0,0 +1,150 @@ +/* + * Copyright 2019 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 org.junit.BeforeClass; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import javax.net.ssl.SSLEngine; +import java.security.Provider; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import static io.netty.handler.ssl.OpenSslTestUtils.checkShouldUseKeyManagerFactory; +import static org.junit.Assume.assumeTrue; + +@RunWith(Parameterized.class) +public class OpenSslConscryptSslEngineInteropTest extends ConscryptSslEngineTest { + + @Parameterized.Parameters(name = "{index}: bufferType = {0}, combo = {1}, delegate = {2}, useTasks = {3}") + public static Collection<Object[]> data() { + List<Object[]> params = new ArrayList<Object[]>(); + for (BufferType type: BufferType.values()) { + params.add(new Object[] { type, ProtocolCipherCombo.tlsv12(), false, false }); + params.add(new Object[] { type, ProtocolCipherCombo.tlsv12(), false, true }); + + params.add(new Object[] { type, ProtocolCipherCombo.tlsv12(), true, false }); + params.add(new Object[] { type, ProtocolCipherCombo.tlsv12(), true, true }); + } + return params; + } + + private final boolean useTasks; + + public OpenSslConscryptSslEngineInteropTest(BufferType type, ProtocolCipherCombo combo, + boolean delegate, boolean useTasks) { + super(type, combo, delegate); + this.useTasks = useTasks; + } + + @BeforeClass + public static void checkOpenssl() { + assumeTrue(OpenSsl.isAvailable()); + } + + @Override + protected SslProvider sslClientProvider() { + return SslProvider.OPENSSL; + } + + @Override + protected SslProvider sslServerProvider() { + return SslProvider.JDK; + } + + @Override + protected Provider clientSslContextProvider() { + return null; + } + + @Override + @Test + @Ignore("TODO: Make this work with Conscrypt") + public void testMutualAuthValidClientCertChainTooLongFailOptionalClientAuth() { + super.testMutualAuthValidClientCertChainTooLongFailOptionalClientAuth(); + } + + @Override + @Test + @Ignore("TODO: Make this work with Conscrypt") + public void testMutualAuthValidClientCertChainTooLongFailRequireClientAuth() { + super.testMutualAuthValidClientCertChainTooLongFailRequireClientAuth(); + } + + @Override + protected boolean mySetupMutualAuthServerIsValidClientException(Throwable cause) { + // TODO(scott): work around for a JDK issue. The exception should be SSLHandshakeException. + return super.mySetupMutualAuthServerIsValidClientException(cause) || causedBySSLException(cause); + } + + @Override + @Test + public void testMutualAuthInvalidIntermediateCASucceedWithOptionalClientAuth() throws Exception { + checkShouldUseKeyManagerFactory(); + super.testMutualAuthInvalidIntermediateCASucceedWithOptionalClientAuth(); + } + + @Override + @Test + public void testMutualAuthInvalidIntermediateCAFailWithOptionalClientAuth() throws Exception { + checkShouldUseKeyManagerFactory(); + super.testMutualAuthInvalidIntermediateCAFailWithOptionalClientAuth(); + } + + @Override + @Test + public void testMutualAuthInvalidIntermediateCAFailWithRequiredClientAuth() throws Exception { + checkShouldUseKeyManagerFactory(); + super.testMutualAuthInvalidIntermediateCAFailWithRequiredClientAuth(); + } + + @Override + @Test + public void testSessionAfterHandshakeKeyManagerFactoryMutualAuth() throws Exception { + checkShouldUseKeyManagerFactory(); + super.testSessionAfterHandshakeKeyManagerFactoryMutualAuth(); + } + + @Override + @Test + public void testSupportedSignatureAlgorithms() throws Exception { + checkShouldUseKeyManagerFactory(); + super.testSupportedSignatureAlgorithms(); + } + + @Override + protected boolean mySetupMutualAuthServerIsValidServerException(Throwable cause) { + // TODO(scott): work around for a JDK issue. The exception should be SSLHandshakeException. + return super.mySetupMutualAuthServerIsValidServerException(cause) || causedBySSLException(cause); + } + + @Override + protected SSLEngine wrapEngine(SSLEngine engine) { + return Java8SslTestUtils.wrapSSLEngineForTesting(engine); + } + + @Override + protected SslContext wrapContext(SslContext context) { + if (context instanceof OpenSslContext) { + ((OpenSslContext) context).setUseTasks(useTasks); + } + return context; + } +} diff --git a/handler/src/test/java/io/netty/handler/ssl/OpenSslEngineTest.java b/handler/src/test/java/io/netty/handler/ssl/OpenSslEngineTest.java index 32be767..2dc947d 100644 --- a/handler/src/test/java/io/netty/handler/ssl/OpenSslEngineTest.java +++ b/handler/src/test/java/io/netty/handler/ssl/OpenSslEngineTest.java @@ -31,19 +31,24 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; +import javax.crypto.Cipher; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; +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.SSLParameters; import java.nio.ByteBuffer; import java.security.AlgorithmConstraints; import java.security.AlgorithmParameters; import java.security.CryptoPrimitive; import java.security.Key; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Set; -import javax.net.ssl.SSLEngine; -import javax.net.ssl.SSLEngineResult; -import javax.net.ssl.SSLException; -import javax.net.ssl.SSLParameters; import static io.netty.handler.ssl.OpenSslTestUtils.checkShouldUseKeyManagerFactory; import static io.netty.handler.ssl.ReferenceCountedOpenSslEngine.MAX_PLAINTEXT_LENGTH; @@ -54,34 +59,46 @@ import static io.netty.handler.ssl.SslUtils.PROTOCOL_TLS_V1_1; import static io.netty.handler.ssl.SslUtils.PROTOCOL_TLS_V1_2; import static io.netty.internal.tcnative.SSL.SSL_CVERIFY_IGNORED; import static java.lang.Integer.MAX_VALUE; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.junit.Assume.assumeTrue; - @RunWith(Parameterized.class) public class OpenSslEngineTest extends SSLEngineTest { private static final String PREFERRED_APPLICATION_LEVEL_PROTOCOL = "my-protocol-http2"; private static final String FALLBACK_APPLICATION_LEVEL_PROTOCOL = "my-protocol-http1_1"; - @Parameterized.Parameters(name = "{index}: bufferType = {0}, combo = {1}") + @Parameterized.Parameters(name = "{index}: bufferType = {0}, combo = {1}, delegate = {2}, useTasks = {3}") public static Collection<Object[]> data() { List<Object[]> params = new ArrayList<Object[]>(); for (BufferType type: BufferType.values()) { - params.add(new Object[] { type, ProtocolCipherCombo.tlsv12()}); + params.add(new Object[] { type, ProtocolCipherCombo.tlsv12(), false, false }); + params.add(new Object[] { type, ProtocolCipherCombo.tlsv12(), false, true }); + + params.add(new Object[] { type, ProtocolCipherCombo.tlsv12(), true, false }); + params.add(new Object[] { type, ProtocolCipherCombo.tlsv12(), true, true }); if (OpenSsl.isTlsv13Supported()) { - params.add(new Object[] { type, ProtocolCipherCombo.tlsv13() }); + params.add(new Object[] { type, ProtocolCipherCombo.tlsv13(), false, false }); + params.add(new Object[] { type, ProtocolCipherCombo.tlsv13(), false, true }); + + params.add(new Object[] { type, ProtocolCipherCombo.tlsv13(), true, false }); + params.add(new Object[] { type, ProtocolCipherCombo.tlsv13(), true, true }); } } return params; } - public OpenSslEngineTest(BufferType type, ProtocolCipherCombo cipherCombo) { - super(type, cipherCombo); + protected final boolean useTasks; + + public OpenSslEngineTest(BufferType type, ProtocolCipherCombo cipherCombo, boolean delegate, boolean useTasks) { + super(type, cipherCombo, delegate); + this.useTasks = useTasks; } @BeforeClass @@ -145,17 +162,16 @@ public class OpenSslEngineTest extends SSLEngineTest { } @Override - @Test - public void testClientHostnameValidationSuccess() throws InterruptedException, SSLException { - assumeTrue(OpenSsl.supportsHostnameValidation()); - super.testClientHostnameValidationSuccess(); + public void testHandshakeSession() throws Exception { + checkShouldUseKeyManagerFactory(); + super.testHandshakeSession(); } @Override @Test - public void testClientHostnameValidationFail() throws InterruptedException, SSLException { - assumeTrue(OpenSsl.supportsHostnameValidation()); - super.testClientHostnameValidationFail(); + public void testSupportedSignatureAlgorithms() throws Exception { + checkShouldUseKeyManagerFactory(); + super.testSupportedSignatureAlgorithms(); } private static boolean isNpnSupported(String versionString) { @@ -223,18 +239,18 @@ public class OpenSslEngineTest extends SSLEngineTest { } @Test public void testWrapBuffersNoWritePendingError() throws Exception { - clientSslCtx = SslContextBuilder.forClient() + clientSslCtx = wrapContext(SslContextBuilder.forClient() .trustManager(InsecureTrustManagerFactory.INSTANCE) .sslProvider(sslClientProvider()) .protocols(protocols()) .ciphers(ciphers()) - .build(); + .build()); SelfSignedCertificate ssc = new SelfSignedCertificate(); - serverSslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()) + serverSslCtx = wrapContext(SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()) .sslProvider(sslServerProvider()) .protocols(protocols()) .ciphers(ciphers()) - .build(); + .build()); SSLEngine clientEngine = null; SSLEngine serverEngine = null; try { @@ -261,18 +277,18 @@ public class OpenSslEngineTest extends SSLEngineTest { @Test public void testOnlySmallBufferNeededForWrap() throws Exception { - clientSslCtx = SslContextBuilder.forClient() + clientSslCtx = wrapContext(SslContextBuilder.forClient() .trustManager(InsecureTrustManagerFactory.INSTANCE) .sslProvider(sslClientProvider()) .protocols(protocols()) .ciphers(ciphers()) - .build(); + .build()); SelfSignedCertificate ssc = new SelfSignedCertificate(); - serverSslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()) + serverSslCtx = wrapContext(SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()) .sslProvider(sslServerProvider()) .protocols(protocols()) .ciphers(ciphers()) - .build(); + .build()); SSLEngine clientEngine = null; SSLEngine serverEngine = null; try { @@ -316,18 +332,18 @@ public class OpenSslEngineTest extends SSLEngineTest { @Test public void testNeededDstCapacityIsCorrectlyCalculated() throws Exception { - clientSslCtx = SslContextBuilder.forClient() + clientSslCtx = wrapContext(SslContextBuilder.forClient() .trustManager(InsecureTrustManagerFactory.INSTANCE) .sslProvider(sslClientProvider()) .protocols(protocols()) .ciphers(ciphers()) - .build(); + .build()); SelfSignedCertificate ssc = new SelfSignedCertificate(); - serverSslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()) + serverSslCtx = wrapContext(SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()) .sslProvider(sslServerProvider()) .protocols(protocols()) .ciphers(ciphers()) - .build(); + .build()); SSLEngine clientEngine = null; SSLEngine serverEngine = null; try { @@ -356,18 +372,18 @@ public class OpenSslEngineTest extends SSLEngineTest { @Test public void testSrcsLenOverFlowCorrectlyHandled() throws Exception { - clientSslCtx = SslContextBuilder.forClient() + clientSslCtx = wrapContext(SslContextBuilder.forClient() .trustManager(InsecureTrustManagerFactory.INSTANCE) .sslProvider(sslClientProvider()) .protocols(protocols()) .ciphers(ciphers()) - .build(); + .build()); SelfSignedCertificate ssc = new SelfSignedCertificate(); - serverSslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()) + serverSslCtx = wrapContext(SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()) .sslProvider(sslServerProvider()) .protocols(protocols()) .ciphers(ciphers()) - .build(); + .build()); SSLEngine clientEngine = null; SSLEngine serverEngine = null; try { @@ -407,12 +423,12 @@ public class OpenSslEngineTest extends SSLEngineTest { @Test public void testCalculateOutNetBufSizeOverflow() throws SSLException { - clientSslCtx = SslContextBuilder.forClient() + clientSslCtx = wrapContext(SslContextBuilder.forClient() .trustManager(InsecureTrustManagerFactory.INSTANCE) .sslProvider(sslClientProvider()) .protocols(protocols()) .ciphers(ciphers()) - .build(); + .build()); SSLEngine clientEngine = null; try { clientEngine = clientSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT); @@ -425,12 +441,12 @@ public class OpenSslEngineTest extends SSLEngineTest { @Test public void testCalculateOutNetBufSize0() throws SSLException { - clientSslCtx = SslContextBuilder.forClient() + clientSslCtx = wrapContext(SslContextBuilder.forClient() .trustManager(InsecureTrustManagerFactory.INSTANCE) .sslProvider(sslClientProvider()) .protocols(protocols()) .ciphers(ciphers()) - .build(); + .build()); SSLEngine clientEngine = null; try { clientEngine = clientSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT); @@ -452,18 +468,18 @@ public class OpenSslEngineTest extends SSLEngineTest { private void testCorrectlyCalculateSpaceForAlert(boolean jdkCompatabilityMode) throws Exception { SelfSignedCertificate ssc = new SelfSignedCertificate(); - serverSslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()) + serverSslCtx = wrapContext(SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()) .sslProvider(sslServerProvider()) .protocols(protocols()) .ciphers(ciphers()) - .build(); + .build()); - clientSslCtx = SslContextBuilder.forClient() + clientSslCtx = wrapContext(SslContextBuilder.forClient() .trustManager(InsecureTrustManagerFactory.INSTANCE) .sslProvider(sslClientProvider()) .protocols(protocols()) .ciphers(ciphers()) - .build(); + .build()); SSLEngine clientEngine = null; SSLEngine serverEngine = null; try { @@ -481,7 +497,7 @@ public class OpenSslEngineTest extends SSLEngineTest { ByteBuffer empty = allocateBuffer(0); ByteBuffer dst = allocateBuffer(clientEngine.getSession().getPacketBufferSize()); - // Limit to something that is guaranteed to be too small to hold a SSL Record. + // Limit to something that is guaranteed to be too small to hold an SSL Record. dst.limit(1); // As we called closeOutbound() before this should produce a BUFFER_OVERFLOW. @@ -514,14 +530,14 @@ public class OpenSslEngineTest extends SSLEngineTest { @Test public void testWrapWithDifferentSizesTLSv1() throws Exception { - clientSslCtx = SslContextBuilder.forClient() + clientSslCtx = wrapContext(SslContextBuilder.forClient() .trustManager(InsecureTrustManagerFactory.INSTANCE) .sslProvider(sslClientProvider()) - .build(); + .build()); SelfSignedCertificate ssc = new SelfSignedCertificate(); - serverSslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()) + serverSslCtx = wrapContext(SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()) .sslProvider(sslServerProvider()) - .build(); + .build()); testWrapWithDifferentSizes(PROTOCOL_TLS_V1, "AES128-SHA"); testWrapWithDifferentSizes(PROTOCOL_TLS_V1, "ECDHE-RSA-AES128-SHA"); @@ -545,14 +561,14 @@ public class OpenSslEngineTest extends SSLEngineTest { @Test public void testWrapWithDifferentSizesTLSv1_1() throws Exception { - clientSslCtx = SslContextBuilder.forClient() + clientSslCtx = wrapContext(SslContextBuilder.forClient() .trustManager(InsecureTrustManagerFactory.INSTANCE) .sslProvider(sslClientProvider()) - .build(); + .build()); SelfSignedCertificate ssc = new SelfSignedCertificate(); - serverSslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()) + serverSslCtx = wrapContext(SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()) .sslProvider(sslServerProvider()) - .build(); + .build()); testWrapWithDifferentSizes(PROTOCOL_TLS_V1_1, "ECDHE-RSA-AES256-SHA"); testWrapWithDifferentSizes(PROTOCOL_TLS_V1_1, "AES256-SHA"); @@ -573,14 +589,14 @@ public class OpenSslEngineTest extends SSLEngineTest { @Test public void testWrapWithDifferentSizesTLSv1_2() throws Exception { - clientSslCtx = SslContextBuilder.forClient() + clientSslCtx = wrapContext(SslContextBuilder.forClient() .trustManager(InsecureTrustManagerFactory.INSTANCE) .sslProvider(sslClientProvider()) - .build(); + .build()); SelfSignedCertificate ssc = new SelfSignedCertificate(); - serverSslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()) + serverSslCtx = wrapContext(SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()) .sslProvider(sslServerProvider()) - .build(); + .build()); testWrapWithDifferentSizes(PROTOCOL_TLS_V1_2, "AES128-SHA"); testWrapWithDifferentSizes(PROTOCOL_TLS_V1_2, "ECDHE-RSA-AES128-SHA"); @@ -611,14 +627,14 @@ public class OpenSslEngineTest extends SSLEngineTest { @Test public void testWrapWithDifferentSizesSSLv3() throws Exception { - clientSslCtx = SslContextBuilder.forClient() + clientSslCtx = wrapContext(SslContextBuilder.forClient() .trustManager(InsecureTrustManagerFactory.INSTANCE) .sslProvider(sslClientProvider()) - .build(); + .build()); SelfSignedCertificate ssc = new SelfSignedCertificate(); - serverSslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()) + serverSslCtx = wrapContext(SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()) .sslProvider(sslServerProvider()) - .build(); + .build()); testWrapWithDifferentSizes(PROTOCOL_SSL_V3, "ADH-AES128-SHA"); testWrapWithDifferentSizes(PROTOCOL_SSL_V3, "ADH-CAMELLIA128-SHA"); @@ -651,21 +667,21 @@ public class OpenSslEngineTest extends SSLEngineTest { public void testMultipleRecordsInOneBufferWithNonZeroPositionJDKCompatabilityModeOff() throws Exception { SelfSignedCertificate cert = new SelfSignedCertificate(); - clientSslCtx = SslContextBuilder + clientSslCtx = wrapContext(SslContextBuilder .forClient() .trustManager(cert.cert()) .sslProvider(sslClientProvider()) .protocols(protocols()) .ciphers(ciphers()) - .build(); + .build()); SSLEngine client = wrapEngine(clientSslCtx.newHandler(UnpooledByteBufAllocator.DEFAULT).engine()); - serverSslCtx = SslContextBuilder + serverSslCtx = wrapContext(SslContextBuilder .forServer(cert.certificate(), cert.privateKey()) .sslProvider(sslServerProvider()) .protocols(protocols()) .ciphers(ciphers()) - .build(); + .build()); SSLEngine server = wrapEngine(serverSslCtx.newHandler(UnpooledByteBufAllocator.DEFAULT).engine()); try { @@ -732,21 +748,21 @@ public class OpenSslEngineTest extends SSLEngineTest { public void testInputTooBigAndFillsUpBuffersJDKCompatabilityModeOff() throws Exception { SelfSignedCertificate cert = new SelfSignedCertificate(); - clientSslCtx = SslContextBuilder + clientSslCtx = wrapContext(SslContextBuilder .forClient() .trustManager(cert.cert()) .sslProvider(sslClientProvider()) .protocols(protocols()) .ciphers(ciphers()) - .build(); + .build()); SSLEngine client = wrapEngine(clientSslCtx.newHandler(UnpooledByteBufAllocator.DEFAULT).engine()); - serverSslCtx = SslContextBuilder + serverSslCtx = wrapContext(SslContextBuilder .forServer(cert.certificate(), cert.privateKey()) .sslProvider(sslServerProvider()) .protocols(protocols()) .ciphers(ciphers()) - .build(); + .build()); SSLEngine server = wrapEngine(serverSslCtx.newHandler(UnpooledByteBufAllocator.DEFAULT).engine()); try { @@ -820,21 +836,21 @@ public class OpenSslEngineTest extends SSLEngineTest { public void testPartialPacketUnwrapJDKCompatabilityModeOff() throws Exception { SelfSignedCertificate cert = new SelfSignedCertificate(); - clientSslCtx = SslContextBuilder + clientSslCtx = wrapContext(SslContextBuilder .forClient() .trustManager(cert.cert()) .sslProvider(sslClientProvider()) .protocols(protocols()) .ciphers(ciphers()) - .build(); + .build()); SSLEngine client = wrapEngine(clientSslCtx.newHandler(UnpooledByteBufAllocator.DEFAULT).engine()); - serverSslCtx = SslContextBuilder + serverSslCtx = wrapContext(SslContextBuilder .forServer(cert.certificate(), cert.privateKey()) .sslProvider(sslServerProvider()) .protocols(protocols()) .ciphers(ciphers()) - .build(); + .build()); SSLEngine server = wrapEngine(serverSslCtx.newHandler(UnpooledByteBufAllocator.DEFAULT).engine()); try { @@ -899,21 +915,21 @@ public class OpenSslEngineTest extends SSLEngineTest { public void testBufferUnderFlowAvoidedIfJDKCompatabilityModeOff() throws Exception { SelfSignedCertificate cert = new SelfSignedCertificate(); - clientSslCtx = SslContextBuilder + clientSslCtx = wrapContext(SslContextBuilder .forClient() .trustManager(cert.cert()) .sslProvider(sslClientProvider()) .protocols(protocols()) .ciphers(ciphers()) - .build(); + .build()); SSLEngine client = wrapEngine(clientSslCtx.newHandler(UnpooledByteBufAllocator.DEFAULT).engine()); - serverSslCtx = SslContextBuilder + serverSslCtx = wrapContext(SslContextBuilder .forServer(cert.certificate(), cert.privateKey()) .sslProvider(sslServerProvider()) .protocols(protocols()) .ciphers(ciphers()) - .build(); + .build()); SSLEngine server = wrapEngine(serverSslCtx.newHandler(UnpooledByteBufAllocator.DEFAULT).engine()); try { @@ -1039,11 +1055,11 @@ public class OpenSslEngineTest extends SSLEngineTest { public void testSNIMatchersDoesNotThrow() throws Exception { assumeTrue(PlatformDependent.javaVersion() >= 8); SelfSignedCertificate ssc = new SelfSignedCertificate(); - serverSslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()) + serverSslCtx = wrapContext(SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()) .sslProvider(sslServerProvider()) .protocols(protocols()) .ciphers(ciphers()) - .build(); + .build()); SSLEngine engine = wrapEngine(serverSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT)); try { @@ -1061,11 +1077,11 @@ public class OpenSslEngineTest extends SSLEngineTest { assumeTrue(PlatformDependent.javaVersion() >= 8); byte[] name = "rb8hx3pww30y3tvw0mwy.v1_1".getBytes(CharsetUtil.UTF_8); SelfSignedCertificate ssc = new SelfSignedCertificate(); - serverSslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()) + serverSslCtx = wrapContext(SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()) .sslProvider(sslServerProvider()) .protocols(protocols()) .ciphers(ciphers()) - .build(); + .build()); SSLEngine engine = wrapEngine(serverSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT)); try { @@ -1083,11 +1099,11 @@ public class OpenSslEngineTest extends SSLEngineTest { @Test(expected = IllegalArgumentException.class) public void testAlgorithmConstraintsThrows() throws Exception { SelfSignedCertificate ssc = new SelfSignedCertificate(); - serverSslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()) + serverSslCtx = wrapContext(SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()) .sslProvider(sslServerProvider()) .protocols(protocols()) .ciphers(ciphers()) - .build(); + .build()); SSLEngine engine = wrapEngine(serverSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT)); try { @@ -1117,6 +1133,177 @@ public class OpenSslEngineTest extends SSLEngineTest { } } + private static void runTasksIfNeeded(SSLEngine engine) { + if (engine.getHandshakeStatus() == HandshakeStatus.NEED_TASK) { + for (;;) { + Runnable task = engine.getDelegatedTask(); + if (task == null) { + assertNotEquals(HandshakeStatus.NEED_TASK, engine.getHandshakeStatus()); + break; + } + task.run(); + } + } + } + + @Test + public void testExtractMasterkeyWorksCorrectly() throws Exception { + SelfSignedCertificate cert = new SelfSignedCertificate(); + serverSslCtx = wrapContext(SslContextBuilder.forServer(cert.key(), cert.cert()) + .sslProvider(SslProvider.OPENSSL).build()); + final SSLEngine serverEngine = + wrapEngine(serverSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT)); + clientSslCtx = wrapContext(SslContextBuilder.forClient() + .trustManager(cert.certificate()) + .sslProvider(SslProvider.OPENSSL).build()); + final SSLEngine clientEngine = + wrapEngine(clientSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT)); + + try { + //lets set the cipher suite to a specific one with DHE + assumeTrue("The diffie hellman cipher is not supported on your runtime.", + Arrays.asList(clientEngine.getSupportedCipherSuites()) + .contains("TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256")); + + //https://www.ietf.org/rfc/rfc5289.txt + //For cipher suites ending with _SHA256, the PRF is the TLS PRF + //[RFC5246] with SHA-256 as the hash function. The MAC is HMAC + //[RFC2104] with SHA-256 as the hash function. + clientEngine.setEnabledCipherSuites(new String[] { "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256" }); + serverEngine.setEnabledCipherSuites(new String[] { "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256" }); + + int appBufferMax = clientEngine.getSession().getApplicationBufferSize(); + int netBufferMax = clientEngine.getSession().getPacketBufferSize(); + + /* + * We'll make the input buffers a bit bigger than the max needed + * size, so that unwrap()s following a successful data transfer + * won't generate BUFFER_OVERFLOWS. + */ + ByteBuffer clientIn = ByteBuffer.allocate(appBufferMax + 50); + ByteBuffer serverIn = ByteBuffer.allocate(appBufferMax + 50); + + ByteBuffer cTOs = ByteBuffer.allocate(netBufferMax); + ByteBuffer sTOc = ByteBuffer.allocate(netBufferMax); + + ByteBuffer clientOut = ByteBuffer.wrap("Hi Server, I'm Client".getBytes()); + ByteBuffer serverOut = ByteBuffer.wrap("Hello Client, I'm Server".getBytes()); + + // This implementation is largely imitated from + // https://docs.oracle.com/javase/8/docs/technotes/ + // guides/security/jsse/samples/sslengine/SSLEngineSimpleDemo.java + // It has been simplified however without the need for running delegation tasks + + // Do handshake for SSL + // A typical handshake will usually contain the following steps: + // 1. wrap: ClientHello + // 2. unwrap: ServerHello/Cert/ServerHelloDone + // 3. wrap: ClientKeyExchange + // 4. wrap: ChangeCipherSpec + // 5. wrap: Finished + // 6. unwrap: ChangeCipherSpec + // 7. unwrap: Finished + + //set a for loop; instead of a while loop to guarantee we quit out eventually + boolean asserted = false; + for (int i = 0; i < 1000; i++) { + + clientEngine.wrap(clientOut, cTOs); + serverEngine.wrap(serverOut, sTOc); + + cTOs.flip(); + sTOc.flip(); + + runTasksIfNeeded(clientEngine); + runTasksIfNeeded(serverEngine); + + clientEngine.unwrap(sTOc, clientIn); + serverEngine.unwrap(cTOs, serverIn); + + runTasksIfNeeded(clientEngine); + runTasksIfNeeded(serverEngine); + + // check when the application data has fully been consumed and sent + // for both the client and server + if ((clientOut.limit() == serverIn.position()) && + (serverOut.limit() == clientIn.position())) { + byte[] serverRandom = SSL.getServerRandom(unwrapEngine(serverEngine).sslPointer()); + byte[] clientRandom = SSL.getClientRandom(unwrapEngine(clientEngine).sslPointer()); + byte[] serverMasterKey = SSL.getMasterKey(unwrapEngine(serverEngine).sslPointer()); + byte[] clientMasterKey = SSL.getMasterKey(unwrapEngine(clientEngine).sslPointer()); + + asserted = true; + assertArrayEquals(serverMasterKey, clientMasterKey); + + // let us re-read the encrypted data and decrypt it ourselves! + cTOs.flip(); + sTOc.flip(); + + // See http://tools.ietf.org/html/rfc5246#section-6.3: + // key_block = PRF(SecurityParameters.master_secret, "key expansion", + // SecurityParameters.server_random + SecurityParameters.client_random); + // + // partitioned: + // client_write_MAC_secret[SecurityParameters.hash_size] + // server_write_MAC_secret[SecurityParameters.hash_size] + // client_write_key[SecurityParameters.key_material_length] + // server_write_key[SecurityParameters.key_material_length] + + int keySize = 16; // AES is 16 bytes or 128 bits + int macSize = 32; // SHA256 is 32 bytes or 256 bits + int keyBlockSize = (2 * keySize) + (2 * macSize); + + byte[] seed = new byte[serverRandom.length + clientRandom.length]; + System.arraycopy(serverRandom, 0, seed, 0, serverRandom.length); + System.arraycopy(clientRandom, 0, seed, serverRandom.length, clientRandom.length); + byte[] keyBlock = PseudoRandomFunction.hash(serverMasterKey, + "key expansion".getBytes(CharsetUtil.US_ASCII), seed, keyBlockSize, "HmacSha256"); + + int offset = 0; + byte[] clientWriteMac = Arrays.copyOfRange(keyBlock, offset, offset + macSize); + offset += macSize; + + byte[] serverWriteMac = Arrays.copyOfRange(keyBlock, offset, offset + macSize); + offset += macSize; + + byte[] clientWriteKey = Arrays.copyOfRange(keyBlock, offset, offset + keySize); + offset += keySize; + + byte[] serverWriteKey = Arrays.copyOfRange(keyBlock, offset, offset + keySize); + offset += keySize; + + //advance the cipher text by 5 + //to take into account the TLS Record Header + cTOs.position(cTOs.position() + 5); + + byte[] ciphertext = new byte[cTOs.remaining()]; + cTOs.get(ciphertext); + + //the initialization vector is the first 16 bytes (128 bits) of the payload + byte[] clientWriteIV = Arrays.copyOfRange(ciphertext, 0, 16); + ciphertext = Arrays.copyOfRange(ciphertext, 16, ciphertext.length); + + SecretKeySpec secretKey = new SecretKeySpec(clientWriteKey, "AES"); + final IvParameterSpec ivForCBC = new IvParameterSpec(clientWriteIV); + Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding"); + cipher.init(Cipher.DECRYPT_MODE, secretKey, ivForCBC); + byte[] plaintext = cipher.doFinal(ciphertext); + assertTrue(new String(plaintext).startsWith("Hi Server, I'm Client")); + break; + } else { + cTOs.compact(); + sTOc.compact(); + } + } + + assertTrue("The assertions were never executed.", asserted); + } finally { + cleanupClientSslEngine(clientEngine); + cleanupServerSslEngine(serverEngine); + cert.delete(); + } + } + @Override protected SslProvider sslClientProvider() { return SslProvider.OPENSSL; @@ -1149,4 +1336,12 @@ public class OpenSslEngineTest extends SSLEngineTest { } return (ReferenceCountedOpenSslEngine) engine; } + + @Override + protected SslContext wrapContext(SslContext context) { + if (context instanceof OpenSslContext) { + ((OpenSslContext) context).setUseTasks(useTasks); + } + return context; + } } diff --git a/handler/src/test/java/io/netty/handler/ssl/OpenSslJdkSslEngineInteroptTest.java b/handler/src/test/java/io/netty/handler/ssl/OpenSslJdkSslEngineInteroptTest.java index df4e757..3bd4e31 100644 --- a/handler/src/test/java/io/netty/handler/ssl/OpenSslJdkSslEngineInteroptTest.java +++ b/handler/src/test/java/io/netty/handler/ssl/OpenSslJdkSslEngineInteroptTest.java @@ -21,7 +21,7 @@ import org.junit.Ignore; import org.junit.Test; import javax.net.ssl.SSLEngine; -import javax.net.ssl.SSLException; + import org.junit.runner.RunWith; import org.junit.runners.Parameterized; @@ -35,21 +35,33 @@ import static org.junit.Assume.assumeTrue; @RunWith(Parameterized.class) public class OpenSslJdkSslEngineInteroptTest extends SSLEngineTest { - @Parameterized.Parameters(name = "{index}: bufferType = {0}, combo = {1}") + @Parameterized.Parameters(name = "{index}: bufferType = {0}, combo = {1}, delegate = {2}, useTasks = {3}") public static Collection<Object[]> data() { List<Object[]> params = new ArrayList<Object[]>(); for (BufferType type: BufferType.values()) { - params.add(new Object[] { type, ProtocolCipherCombo.tlsv12()}); + params.add(new Object[] { type, ProtocolCipherCombo.tlsv12(), false, false }); + params.add(new Object[] { type, ProtocolCipherCombo.tlsv12(), false, true }); + + params.add(new Object[] { type, ProtocolCipherCombo.tlsv12(), true, false}); + params.add(new Object[] { type, ProtocolCipherCombo.tlsv12(), true, true }); if (PlatformDependent.javaVersion() >= 11 && OpenSsl.isTlsv13Supported()) { - params.add(new Object[] { type, ProtocolCipherCombo.tlsv13() }); + params.add(new Object[] { type, ProtocolCipherCombo.tlsv13(), false, false }); + params.add(new Object[] { type, ProtocolCipherCombo.tlsv13(), false, true }); + + params.add(new Object[] { type, ProtocolCipherCombo.tlsv13(), true, false }); + params.add(new Object[] { type, ProtocolCipherCombo.tlsv13(), true, true }); } } return params; } - public OpenSslJdkSslEngineInteroptTest(BufferType type, ProtocolCipherCombo combo) { - super(type, combo); + private final boolean useTasks; + + public OpenSslJdkSslEngineInteroptTest(BufferType type, ProtocolCipherCombo combo, + boolean delegate, boolean useTasks) { + super(type, combo, delegate); + this.useTasks = useTasks; } @BeforeClass @@ -100,33 +112,40 @@ public class OpenSslJdkSslEngineInteroptTest extends SSLEngineTest { @Override @Test - public void testClientHostnameValidationSuccess() throws InterruptedException, SSLException { - assumeTrue(OpenSsl.supportsHostnameValidation()); - super.testClientHostnameValidationSuccess(); + public void testSessionAfterHandshakeKeyManagerFactoryMutualAuth() throws Exception { + checkShouldUseKeyManagerFactory(); + super.testSessionAfterHandshakeKeyManagerFactoryMutualAuth(); } @Override - @Test - public void testClientHostnameValidationFail() throws InterruptedException, SSLException { - assumeTrue(OpenSsl.supportsHostnameValidation()); - super.testClientHostnameValidationFail(); + protected boolean mySetupMutualAuthServerIsValidServerException(Throwable cause) { + // TODO(scott): work around for a JDK issue. The exception should be SSLHandshakeException. + return super.mySetupMutualAuthServerIsValidServerException(cause) || causedBySSLException(cause); } @Override - @Test - public void testSessionAfterHandshakeKeyManagerFactoryMutualAuth() throws Exception { + public void testHandshakeSession() throws Exception { checkShouldUseKeyManagerFactory(); - super.testSessionAfterHandshakeKeyManagerFactoryMutualAuth(); + super.testHandshakeSession(); } @Override - protected boolean mySetupMutualAuthServerIsValidServerException(Throwable cause) { - // TODO(scott): work around for a JDK issue. The exception should be SSLHandshakeException. - return super.mySetupMutualAuthServerIsValidServerException(cause) || causedBySSLException(cause); + @Test + public void testSupportedSignatureAlgorithms() throws Exception { + checkShouldUseKeyManagerFactory(); + super.testSupportedSignatureAlgorithms(); } @Override protected SSLEngine wrapEngine(SSLEngine engine) { return Java8SslTestUtils.wrapSSLEngineForTesting(engine); } + + @Override + protected SslContext wrapContext(SslContext context) { + if (context instanceof OpenSslContext) { + ((OpenSslContext) context).setUseTasks(useTasks); + } + return context; + } } diff --git a/handler/src/test/java/io/netty/handler/ssl/OpenSslKeyMaterialProviderTest.java b/handler/src/test/java/io/netty/handler/ssl/OpenSslKeyMaterialProviderTest.java index 5b793fe..9b41196 100644 --- a/handler/src/test/java/io/netty/handler/ssl/OpenSslKeyMaterialProviderTest.java +++ b/handler/src/test/java/io/netty/handler/ssl/OpenSslKeyMaterialProviderTest.java @@ -15,13 +15,21 @@ */ package io.netty.handler.ssl; +import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.UnpooledByteBufAllocator; +import io.netty.internal.tcnative.SSL; +import io.netty.util.ReferenceCountUtil; import org.junit.BeforeClass; import org.junit.Test; import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.X509KeyManager; +import java.net.Socket; import java.security.KeyStore; +import java.security.Principal; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; import static org.junit.Assert.*; import static org.junit.Assume.assumeTrue; @@ -38,12 +46,16 @@ public class OpenSslKeyMaterialProviderTest { } protected KeyManagerFactory newKeyManagerFactory() throws Exception { + return newKeyManagerFactory(KeyManagerFactory.getDefaultAlgorithm()); + } + + protected KeyManagerFactory newKeyManagerFactory(String algorithm) throws Exception { char[] password = PASSWORD.toCharArray(); final KeyStore keystore = KeyStore.getInstance("PKCS12"); keystore.load(getClass().getResourceAsStream("mutual_auth_server.p12"), password); KeyManagerFactory kmf = - KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + KeyManagerFactory.getInstance(algorithm); kmf.init(keystore, password); return kmf; } @@ -72,4 +84,94 @@ public class OpenSslKeyMaterialProviderTest { provider.destroy(); } + + /** + * Test class used by testChooseOpenSslPrivateKeyMaterial(). + */ + private static final class SingleKeyManager implements X509KeyManager { + private final String keyAlias; + private final PrivateKey pk; + private final X509Certificate[] certChain; + + SingleKeyManager(String keyAlias, PrivateKey pk, X509Certificate[] certChain) { + this.keyAlias = keyAlias; + this.pk = pk; + this.certChain = certChain; + } + + @Override + public String[] getClientAliases(String keyType, Principal[] issuers) { + return new String[]{keyAlias}; + } + + @Override + public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) { + return keyAlias; + } + + @Override + public String[] getServerAliases(String keyType, Principal[] issuers) { + return new String[]{keyAlias}; + } + + @Override + public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) { + return keyAlias; + } + + @Override + public X509Certificate[] getCertificateChain(String alias) { + return certChain; + } + + @Override + public PrivateKey getPrivateKey(String alias) { + return pk; + } + } + + @Test + public void testChooseOpenSslPrivateKeyMaterial() throws Exception { + PrivateKey privateKey = SslContext.toPrivateKey( + getClass().getResourceAsStream("localhost_server.key"), + null); + assertNotNull(privateKey); + assertEquals("PKCS#8", privateKey.getFormat()); + final X509Certificate[] certChain = SslContext.toX509Certificates( + getClass().getResourceAsStream("localhost_server.pem")); + assertNotNull(certChain); + PemEncoded pemKey = null; + long pkeyBio = 0L; + OpenSslPrivateKey sslPrivateKey; + try { + pemKey = PemPrivateKey.toPEM(ByteBufAllocator.DEFAULT, true, privateKey); + pkeyBio = ReferenceCountedOpenSslContext.toBIO(ByteBufAllocator.DEFAULT, pemKey.retain()); + sslPrivateKey = new OpenSslPrivateKey(SSL.parsePrivateKey(pkeyBio, null)); + } finally { + ReferenceCountUtil.safeRelease(pemKey); + if (pkeyBio != 0L) { + SSL.freeBIO(pkeyBio); + } + } + final String keyAlias = "key"; + + OpenSslKeyMaterialProvider provider = new OpenSslKeyMaterialProvider( + new SingleKeyManager(keyAlias, sslPrivateKey, certChain), + null); + OpenSslKeyMaterial material = provider.chooseKeyMaterial(ByteBufAllocator.DEFAULT, keyAlias); + assertNotNull(material); + assertEquals(2, sslPrivateKey.refCnt()); + assertEquals(1, material.refCnt()); + assertTrue(material.release()); + assertEquals(1, sslPrivateKey.refCnt()); + // Can get material multiple times from the same key + material = provider.chooseKeyMaterial(ByteBufAllocator.DEFAULT, keyAlias); + assertNotNull(material); + assertEquals(2, sslPrivateKey.refCnt()); + assertTrue(material.release()); + assertTrue(sslPrivateKey.release()); + assertEquals(0, sslPrivateKey.refCnt()); + assertEquals(0, material.refCnt()); + assertEquals(0, ((OpenSslPrivateKey.OpenSslPrivateKeyMaterial) material).certificateChain); + } } diff --git a/handler/src/test/java/io/netty/handler/ssl/OpenSslPrivateKeyMethodTest.java b/handler/src/test/java/io/netty/handler/ssl/OpenSslPrivateKeyMethodTest.java new file mode 100644 index 0000000..ae830f6 --- /dev/null +++ b/handler/src/test/java/io/netty/handler/ssl/OpenSslPrivateKeyMethodTest.java @@ -0,0 +1,402 @@ +/* + * Copyright 2019 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.bootstrap.Bootstrap; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.Unpooled; +import io.netty.buffer.UnpooledByteBufAllocator; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.DefaultEventLoopGroup; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.channel.local.LocalAddress; +import io.netty.channel.local.LocalChannel; +import io.netty.channel.local.LocalServerChannel; +import io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import io.netty.handler.ssl.util.SelfSignedCertificate; +import io.netty.util.ReferenceCountUtil; +import io.netty.util.concurrent.Promise; +import org.hamcrest.Matchers; +import org.junit.AfterClass; +import org.junit.Assume; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLHandshakeException; +import java.net.SocketAddress; +import java.security.NoSuchAlgorithmException; +import java.security.Signature; +import java.security.SignatureException; +import java.security.spec.MGF1ParameterSpec; +import java.security.spec.PSSParameterSpec; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import static io.netty.handler.ssl.OpenSslTestUtils.checkShouldUseKeyManagerFactory; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +@RunWith(Parameterized.class) +public class OpenSslPrivateKeyMethodTest { + private static final String RFC_CIPHER_NAME = "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"; + private static EventLoopGroup GROUP; + private static SelfSignedCertificate CERT; + private static ExecutorService EXECUTOR; + + @Parameters(name = "{index}: delegate = {0}") + public static Collection<Object[]> parameters() { + List<Object[]> dst = new ArrayList<Object[]>(); + dst.add(new Object[] { true }); + dst.add(new Object[] { false }); + return dst; + } + + @BeforeClass + public static void init() throws Exception { + checkShouldUseKeyManagerFactory(); + + Assume.assumeTrue(OpenSsl.isBoringSSL()); + // Check if the cipher is supported at all which may not be the case for various JDK versions and OpenSSL API + // implementations. + assumeCipherAvailable(SslProvider.OPENSSL); + assumeCipherAvailable(SslProvider.JDK); + + GROUP = new DefaultEventLoopGroup(); + CERT = new SelfSignedCertificate(); + EXECUTOR = Executors.newCachedThreadPool(new ThreadFactory() { + @Override + public Thread newThread(Runnable r) { + return new DelegateThread(r); + } + }); + } + + @AfterClass + public static void destroy() { + if (OpenSsl.isBoringSSL()) { + GROUP.shutdownGracefully(); + CERT.delete(); + EXECUTOR.shutdown(); + } + } + + private final boolean delegate; + + public OpenSslPrivateKeyMethodTest(boolean delegate) { + this.delegate = delegate; + } + + private static void assumeCipherAvailable(SslProvider provider) throws NoSuchAlgorithmException { + boolean cipherSupported = false; + if (provider == SslProvider.JDK) { + SSLEngine engine = SSLContext.getDefault().createSSLEngine(); + for (String c: engine.getSupportedCipherSuites()) { + if (RFC_CIPHER_NAME.equals(c)) { + cipherSupported = true; + break; + } + } + } else { + cipherSupported = OpenSsl.isCipherSuiteAvailable(RFC_CIPHER_NAME); + } + Assume.assumeTrue("Unsupported cipher: " + RFC_CIPHER_NAME, cipherSupported); + } + + private static SslHandler newSslHandler(SslContext sslCtx, ByteBufAllocator allocator, Executor executor) { + if (executor == null) { + return sslCtx.newHandler(allocator); + } else { + return sslCtx.newHandler(allocator, executor); + } + } + + private SslContext buildServerContext(OpenSslPrivateKeyMethod method) throws Exception { + List<String> ciphers = Collections.singletonList(RFC_CIPHER_NAME); + + final KeyManagerFactory kmf = OpenSslX509KeyManagerFactory.newKeyless(CERT.cert()); + + final SslContext sslServerContext = SslContextBuilder.forServer(kmf) + .sslProvider(SslProvider.OPENSSL) + .ciphers(ciphers) + // As this is not a TLSv1.3 cipher we should ensure we talk something else. + .protocols(SslUtils.PROTOCOL_TLS_V1_2) + .build(); + + ((OpenSslContext) sslServerContext).setPrivateKeyMethod(method); + return sslServerContext; + } + + private SslContext buildClientContext() throws Exception { + return SslContextBuilder.forClient() + .sslProvider(SslProvider.JDK) + .ciphers(Collections.singletonList(RFC_CIPHER_NAME)) + // As this is not a TLSv1.3 cipher we should ensure we talk something else. + .protocols(SslUtils.PROTOCOL_TLS_V1_2) + .trustManager(InsecureTrustManagerFactory.INSTANCE) + .build(); + } + + private Executor delegateExecutor() { + return delegate ? EXECUTOR : null; + } + + private void assertThread() { + if (delegate && OpenSslContext.USE_TASKS) { + assertEquals(DelegateThread.class, Thread.currentThread().getClass()); + } else { + assertNotEquals(DelegateThread.class, Thread.currentThread().getClass()); + } + } + + @Test + public void testPrivateKeyMethod() throws Exception { + final AtomicBoolean signCalled = new AtomicBoolean(); + final SslContext sslServerContext = buildServerContext(new OpenSslPrivateKeyMethod() { + @Override + public byte[] sign(SSLEngine engine, int signatureAlgorithm, byte[] input) throws Exception { + signCalled.set(true); + assertThread(); + + assertEquals(CERT.cert().getPublicKey(), + engine.getSession().getLocalCertificates()[0].getPublicKey()); + + // Delegate signing to Java implementation. + final Signature signature; + // Depending on the Java version it will pick one or the other. + if (signatureAlgorithm == OpenSslPrivateKeyMethod.SSL_SIGN_RSA_PKCS1_SHA256) { + signature = Signature.getInstance("SHA256withRSA"); + } else if (signatureAlgorithm == OpenSslPrivateKeyMethod.SSL_SIGN_RSA_PSS_RSAE_SHA256) { + signature = Signature.getInstance("RSASSA-PSS"); + signature.setParameter(new PSSParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA256, + 32, 1)); + } else { + throw new AssertionError("Unexpected signature algorithm " + signatureAlgorithm); + } + signature.initSign(CERT.key()); + signature.update(input); + return signature.sign(); + } + + @Override + public byte[] decrypt(SSLEngine engine, byte[] input) { + throw new UnsupportedOperationException(); + } + }); + + final SslContext sslClientContext = buildClientContext(); + try { + try { + final Promise<Object> serverPromise = GROUP.next().newPromise(); + final Promise<Object> clientPromise = GROUP.next().newPromise(); + + ChannelHandler serverHandler = new ChannelInitializer<Channel>() { + @Override + protected void initChannel(Channel ch) { + ChannelPipeline pipeline = ch.pipeline(); + pipeline.addLast(newSslHandler(sslServerContext, ch.alloc(), delegateExecutor())); + + pipeline.addLast(new SimpleChannelInboundHandler<Object>() { + @Override + public void channelInactive(ChannelHandlerContext ctx) { + serverPromise.cancel(true); + ctx.fireChannelInactive(); + } + + @Override + public void channelRead0(ChannelHandlerContext ctx, Object msg) { + if (serverPromise.trySuccess(null)) { + ctx.writeAndFlush(Unpooled.wrappedBuffer(new byte[] {'P', 'O', 'N', 'G'})); + } + ctx.close(); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + if (!serverPromise.tryFailure(cause)) { + ctx.fireExceptionCaught(cause); + } + } + }); + } + }; + + LocalAddress address = new LocalAddress("test-" + SslProvider.OPENSSL + + '-' + SslProvider.JDK + '-' + RFC_CIPHER_NAME + '-' + delegate); + + Channel server = server(address, serverHandler); + try { + ChannelHandler clientHandler = new ChannelInitializer<Channel>() { + @Override + protected void initChannel(Channel ch) { + ChannelPipeline pipeline = ch.pipeline(); + pipeline.addLast(newSslHandler(sslClientContext, ch.alloc(), delegateExecutor())); + + pipeline.addLast(new SimpleChannelInboundHandler<Object>() { + @Override + public void channelInactive(ChannelHandlerContext ctx) { + clientPromise.cancel(true); + ctx.fireChannelInactive(); + } + + @Override + public void channelRead0(ChannelHandlerContext ctx, Object msg) { + clientPromise.trySuccess(null); + ctx.close(); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + if (!clientPromise.tryFailure(cause)) { + ctx.fireExceptionCaught(cause); + } + } + }); + } + }; + + Channel client = client(server, clientHandler); + try { + client.writeAndFlush(Unpooled.wrappedBuffer(new byte[] {'P', 'I', 'N', 'G'})) + .syncUninterruptibly(); + + assertTrue("client timeout", clientPromise.await(5L, TimeUnit.SECONDS)); + assertTrue("server timeout", serverPromise.await(5L, TimeUnit.SECONDS)); + + clientPromise.sync(); + serverPromise.sync(); + assertTrue(signCalled.get()); + } finally { + client.close().sync(); + } + } finally { + server.close().sync(); + } + } finally { + ReferenceCountUtil.release(sslClientContext); + } + } finally { + ReferenceCountUtil.release(sslServerContext); + } + } + + @Test + public void testPrivateKeyMethodFailsBecauseOfException() throws Exception { + testPrivateKeyMethodFails(false); + } + + @Test + public void testPrivateKeyMethodFailsBecauseOfNull() throws Exception { + testPrivateKeyMethodFails(true); + } + private void testPrivateKeyMethodFails(final boolean returnNull) throws Exception { + final SslContext sslServerContext = buildServerContext(new OpenSslPrivateKeyMethod() { + @Override + public byte[] sign(SSLEngine engine, int signatureAlgorithm, byte[] input) throws Exception { + assertThread(); + if (returnNull) { + return null; + } + throw new SignatureException(); + } + + @Override + public byte[] decrypt(SSLEngine engine, byte[] input) { + throw new UnsupportedOperationException(); + } + }); + final SslContext sslClientContext = buildClientContext(); + + SslHandler serverSslHandler = newSslHandler( + sslServerContext, UnpooledByteBufAllocator.DEFAULT, delegateExecutor()); + SslHandler clientSslHandler = newSslHandler( + sslClientContext, UnpooledByteBufAllocator.DEFAULT, delegateExecutor()); + + try { + try { + LocalAddress address = new LocalAddress("test-" + SslProvider.OPENSSL + + '-' + SslProvider.JDK + '-' + RFC_CIPHER_NAME + '-' + delegate); + + Channel server = server(address, serverSslHandler); + try { + Channel client = client(server, clientSslHandler); + try { + Throwable clientCause = clientSslHandler.handshakeFuture().await().cause(); + Throwable serverCause = serverSslHandler.handshakeFuture().await().cause(); + assertNotNull(clientCause); + assertThat(serverCause, Matchers.instanceOf(SSLHandshakeException.class)); + } finally { + client.close().sync(); + } + } finally { + server.close().sync(); + } + } finally { + ReferenceCountUtil.release(sslClientContext); + } + } finally { + ReferenceCountUtil.release(sslServerContext); + } + } + + private static Channel server(LocalAddress address, ChannelHandler handler) throws Exception { + ServerBootstrap bootstrap = new ServerBootstrap() + .channel(LocalServerChannel.class) + .group(GROUP) + .childHandler(handler); + + return bootstrap.bind(address).sync().channel(); + } + + private static Channel client(Channel server, ChannelHandler handler) throws Exception { + SocketAddress remoteAddress = server.localAddress(); + + Bootstrap bootstrap = new Bootstrap() + .channel(LocalChannel.class) + .group(GROUP) + .handler(handler); + + return bootstrap.connect(remoteAddress).sync().channel(); + } + + private static final class DelegateThread extends Thread { + DelegateThread(Runnable target) { + super(target); + } + } +} diff --git a/handler/src/test/java/io/netty/handler/ssl/PemEncodedTest.java b/handler/src/test/java/io/netty/handler/ssl/PemEncodedTest.java index b2531eb..e6b36aa 100644 --- a/handler/src/test/java/io/netty/handler/ssl/PemEncodedTest.java +++ b/handler/src/test/java/io/netty/handler/ssl/PemEncodedTest.java @@ -26,7 +26,6 @@ import java.io.File; import java.io.FileInputStream; import java.security.PrivateKey; -import io.netty.buffer.Unpooled; import io.netty.buffer.UnpooledByteBufAllocator; import org.junit.Test; diff --git a/handler/src/test/java/io/netty/handler/ssl/PseudoRandomFunctionTest.java b/handler/src/test/java/io/netty/handler/ssl/PseudoRandomFunctionTest.java new file mode 100644 index 0000000..30fc373 --- /dev/null +++ b/handler/src/test/java/io/netty/handler/ssl/PseudoRandomFunctionTest.java @@ -0,0 +1,53 @@ +/* + * Copyright 2019 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.util.CharsetUtil; +import org.bouncycastle.util.encoders.Hex; +import org.junit.Test; + +import static org.junit.Assert.assertArrayEquals; + +/** + * The test vectors here were provided via: + * https://www.ietf.org/mail-archive/web/tls/current/msg03416.html + */ +public class PseudoRandomFunctionTest { + + @Test + public void testPrfSha256() { + byte[] secret = Hex.decode("9b be 43 6b a9 40 f0 17 b1 76 52 84 9a 71 db 35"); + byte[] seed = Hex.decode("a0 ba 9f 93 6c da 31 18 27 a6 f7 96 ff d5 19 8c"); + byte[] label = "test label".getBytes(CharsetUtil.US_ASCII); + byte[] expected = Hex.decode( + "e3 f2 29 ba 72 7b e1 7b" + + "8d 12 26 20 55 7c d4 53" + + "c2 aa b2 1d 07 c3 d4 95" + + "32 9b 52 d4 e6 1e db 5a" + + "6b 30 17 91 e9 0d 35 c9" + + "c9 a4 6b 4e 14 ba f9 af" + + "0f a0 22 f7 07 7d ef 17" + + "ab fd 37 97 c0 56 4b ab" + + "4f bc 91 66 6e 9d ef 9b" + + "97 fc e3 4f 79 67 89 ba" + + "a4 80 82 d1 22 ee 42 c5" + + "a7 2e 5a 51 10 ff f7 01" + + "87 34 7b 66"); + byte[] actual = PseudoRandomFunction.hash(secret, label, seed, expected.length, "HmacSha256"); + assertArrayEquals(expected, actual); + } +} diff --git a/handler/src/test/java/io/netty/handler/ssl/ReferenceCountedOpenSslEngineTest.java b/handler/src/test/java/io/netty/handler/ssl/ReferenceCountedOpenSslEngineTest.java index 588619d..2e7c260 100644 --- a/handler/src/test/java/io/netty/handler/ssl/ReferenceCountedOpenSslEngineTest.java +++ b/handler/src/test/java/io/netty/handler/ssl/ReferenceCountedOpenSslEngineTest.java @@ -15,16 +15,20 @@ */ package io.netty.handler.ssl; +import io.netty.buffer.UnpooledByteBufAllocator; import io.netty.handler.ssl.util.InsecureTrustManagerFactory; import io.netty.util.ReferenceCountUtil; import org.junit.Test; import javax.net.ssl.SSLEngine; +import static junit.framework.TestCase.*; + public class ReferenceCountedOpenSslEngineTest extends OpenSslEngineTest { - public ReferenceCountedOpenSslEngineTest(BufferType type, ProtocolCipherCombo combo) { - super(type, combo); + public ReferenceCountedOpenSslEngineTest(BufferType type, ProtocolCipherCombo combo, boolean delegate, + boolean useTasks) { + super(type, combo, delegate, useTasks); } @Override @@ -59,13 +63,40 @@ public class ReferenceCountedOpenSslEngineTest extends OpenSslEngineTest { @Test(expected = NullPointerException.class) public void testNotLeakOnException() throws Exception { - clientSslCtx = SslContextBuilder.forClient() + clientSslCtx = wrapContext(SslContextBuilder.forClient() .trustManager(InsecureTrustManagerFactory.INSTANCE) .sslProvider(sslClientProvider()) .protocols(protocols()) .ciphers(ciphers()) - .build(); + .build()); clientSslCtx.newEngine(null); } + + @Override + protected SslContext wrapContext(SslContext context) { + if (context instanceof ReferenceCountedOpenSslContext) { + ((ReferenceCountedOpenSslContext) context).setUseTasks(useTasks); + } + return context; + } + + @Test + public void parentContextIsRetainedByChildEngines() throws Exception { + SslContext clientSslCtx = SslContextBuilder.forClient() + .trustManager(InsecureTrustManagerFactory.INSTANCE) + .sslProvider(sslClientProvider()) + .protocols(protocols()) + .ciphers(ciphers()) + .build(); + + SSLEngine engine = clientSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT); + assertEquals(ReferenceCountUtil.refCnt(clientSslCtx), 2); + + cleanupClientSslContext(clientSslCtx); + assertEquals(ReferenceCountUtil.refCnt(clientSslCtx), 1); + + cleanupClientSslEngine(engine); + assertEquals(ReferenceCountUtil.refCnt(clientSslCtx), 0); + } } diff --git a/handler/src/test/java/io/netty/handler/ssl/SSLEngineTest.java b/handler/src/test/java/io/netty/handler/ssl/SSLEngineTest.java index c1a4e12..a607498 100644 --- a/handler/src/test/java/io/netty/handler/ssl/SSLEngineTest.java +++ b/handler/src/test/java/io/netty/handler/ssl/SSLEngineTest.java @@ -36,6 +36,7 @@ import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.handler.ssl.ApplicationProtocolConfig.Protocol; import io.netty.handler.ssl.util.InsecureTrustManagerFactory; import io.netty.handler.ssl.util.SelfSignedCertificate; +import io.netty.handler.ssl.util.SimpleTrustManagerFactory; import io.netty.util.CharsetUtil; import io.netty.util.NetUtil; import io.netty.util.ReferenceCountUtil; @@ -45,6 +46,8 @@ import io.netty.util.concurrent.Promise; import io.netty.util.internal.EmptyArrays; import io.netty.util.internal.PlatformDependent; import io.netty.util.internal.StringUtil; +import io.netty.util.internal.SystemPropertyUtil; +import org.conscrypt.OpenSSLProvider; import org.junit.After; import org.junit.Assume; import org.junit.Before; @@ -54,15 +57,23 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import java.io.ByteArrayInputStream; +import java.io.Closeable; import java.io.File; import java.io.FileInputStream; +import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; import java.net.InetSocketAddress; +import java.net.Socket; import java.nio.ByteBuffer; import java.nio.channels.ClosedChannelException; import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; import java.security.Principal; +import java.security.PrivateKey; import java.security.Provider; +import java.security.UnrecoverableKeyException; import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.util.ArrayList; @@ -73,11 +84,18 @@ import java.util.List; import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; +import javax.crypto.SecretKey; +import javax.net.ssl.ExtendedSSLSession; +import javax.net.ssl.KeyManager; import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.KeyManagerFactorySpi; import javax.net.ssl.ManagerFactoryParameters; import javax.net.ssl.SNIHostName; +import javax.net.ssl.SSLContext; import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLEngineResult; import javax.net.ssl.SSLEngineResult.Status; @@ -92,6 +110,8 @@ import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.TrustManagerFactorySpi; +import javax.net.ssl.X509ExtendedKeyManager; +import javax.net.ssl.X509ExtendedTrustManager; import javax.net.ssl.X509TrustManager; import javax.security.cert.X509Certificate; @@ -252,10 +272,13 @@ public abstract class SSLEngineTest { private final BufferType type; private final ProtocolCipherCombo protocolCipherCombo; + private final boolean delegate; + private ExecutorService delegatingExecutor; - protected SSLEngineTest(BufferType type, ProtocolCipherCombo protocolCipherCombo) { + protected SSLEngineTest(BufferType type, ProtocolCipherCombo protocolCipherCombo, boolean delegate) { this.type = type; this.protocolCipherCombo = protocolCipherCombo; + this.delegate = delegate; } protected ByteBuffer allocateBuffer(int len) { @@ -441,6 +464,9 @@ public abstract class SSLEngineTest { MockitoAnnotations.initMocks(this); serverLatch = new CountDownLatch(1); clientLatch = new CountDownLatch(1); + if (delegate) { + delegatingExecutor = Executors.newCachedThreadPool(); + } } @After @@ -500,6 +526,10 @@ public abstract class SSLEngineTest { clientGroupShutdownFuture.sync(); } serverException = null; + + if (delegatingExecutor != null) { + delegatingExecutor.shutdown(); + } } @Test @@ -668,7 +698,7 @@ public abstract class SSLEngineTest { final boolean serverInitEngine) throws SSLException, InterruptedException { serverSslCtx = - SslContextBuilder.forServer(serverKMF) + wrapContext(SslContextBuilder.forServer(serverKMF) .protocols(protocols()) .ciphers(ciphers()) .sslProvider(sslServerProvider()) @@ -677,10 +707,10 @@ public abstract class SSLEngineTest { .clientAuth(clientAuth) .ciphers(null, IdentityCipherSuiteFilter.INSTANCE) .sessionCacheSize(0) - .sessionTimeout(0).build(); + .sessionTimeout(0).build()); clientSslCtx = - SslContextBuilder.forClient() + wrapContext(SslContextBuilder.forClient() .protocols(protocols()) .ciphers(ciphers()) .sslProvider(sslClientProvider()) @@ -689,7 +719,7 @@ public abstract class SSLEngineTest { .keyManager(clientKMF) .ciphers(null, IdentityCipherSuiteFilter.INSTANCE) .sessionCacheSize(0) - .sessionTimeout(0).build(); + .sessionTimeout(0).build()); serverConnectedChannel = null; sb = new ServerBootstrap(); @@ -703,7 +733,8 @@ public abstract class SSLEngineTest { ch.config().setAllocator(new TestByteBufAllocator(ch.config().getAllocator(), type)); ChannelPipeline p = ch.pipeline(); - SslHandler handler = serverSslCtx.newHandler(ch.alloc()); + SslHandler handler = delegatingExecutor == null ? serverSslCtx.newHandler(ch.alloc()) : + serverSslCtx.newHandler(ch.alloc(), delegatingExecutor); if (serverInitEngine) { mySetupMutualAuthServerInitSslHandler(handler); } @@ -746,7 +777,10 @@ public abstract class SSLEngineTest { protected void initChannel(Channel ch) throws Exception { ch.config().setAllocator(new TestByteBufAllocator(ch.config().getAllocator(), type)); ChannelPipeline p = ch.pipeline(); - p.addLast(clientSslCtx.newHandler(ch.alloc())); + + SslHandler handler = delegatingExecutor == null ? clientSslCtx.newHandler(ch.alloc()) : + clientSslCtx.newHandler(ch.alloc(), delegatingExecutor); + p.addLast(handler); p.addLast(new MessageDelegatorChannelHandler(clientReceiver, clientLatch)); p.addLast(new ChannelInboundHandlerAdapter() { @Override @@ -816,7 +850,7 @@ public abstract class SSLEngineTest { final boolean failureExpected) throws SSLException, InterruptedException { final String expectedHost = "localhost"; - serverSslCtx = SslContextBuilder.forServer(serverCrtFile, serverKeyFile, null) + serverSslCtx = wrapContext(SslContextBuilder.forServer(serverCrtFile, serverKeyFile, null) .sslProvider(sslServerProvider()) .protocols(protocols()) .ciphers(ciphers()) @@ -825,9 +859,9 @@ public abstract class SSLEngineTest { .ciphers(null, IdentityCipherSuiteFilter.INSTANCE) .sessionCacheSize(0) .sessionTimeout(0) - .build(); + .build()); - clientSslCtx = SslContextBuilder.forClient() + clientSslCtx = wrapContext(SslContextBuilder.forClient() .sslProvider(sslClientProvider()) .protocols(protocols()) .ciphers(ciphers()) @@ -836,7 +870,7 @@ public abstract class SSLEngineTest { .ciphers(null, IdentityCipherSuiteFilter.INSTANCE) .sessionCacheSize(0) .sessionTimeout(0) - .build(); + .build()); serverConnectedChannel = null; sb = new ServerBootstrap(); @@ -849,7 +883,10 @@ public abstract class SSLEngineTest { protected void initChannel(Channel ch) throws Exception { ch.config().setAllocator(new TestByteBufAllocator(ch.config().getAllocator(), type)); ChannelPipeline p = ch.pipeline(); - p.addLast(serverSslCtx.newHandler(ch.alloc())); + + SslHandler handler = delegatingExecutor == null ? serverSslCtx.newHandler(ch.alloc()) : + serverSslCtx.newHandler(ch.alloc(), delegatingExecutor); + p.addLast(handler); p.addLast(new MessageDelegatorChannelHandler(serverReceiver, serverLatch)); p.addLast(new ChannelInboundHandlerAdapter() { @Override @@ -889,7 +926,11 @@ public abstract class SSLEngineTest { ch.config().setAllocator(new TestByteBufAllocator(ch.config().getAllocator(), type)); ChannelPipeline p = ch.pipeline(); InetSocketAddress remoteAddress = (InetSocketAddress) serverChannel.localAddress(); - SslHandler sslHandler = clientSslCtx.newHandler(ch.alloc(), expectedHost, 0); + + SslHandler sslHandler = delegatingExecutor == null ? + clientSslCtx.newHandler(ch.alloc(), expectedHost, 0) : + clientSslCtx.newHandler(ch.alloc(), expectedHost, 0, delegatingExecutor); + SSLParameters parameters = sslHandler.engine().getSSLParameters(); if (SslUtils.isValidHostNameForSNI(expectedHost)) { assertEquals(1, parameters.getServerNames().size()); @@ -979,7 +1020,7 @@ public abstract class SSLEngineTest { File clientTrustCrtFile, File clientKeyFile, final File clientCrtFile, String clientKeyPassword) throws InterruptedException, SSLException { serverSslCtx = - SslContextBuilder.forServer(serverCrtFile, serverKeyFile, serverKeyPassword) + wrapContext(SslContextBuilder.forServer(serverCrtFile, serverKeyFile, serverKeyPassword) .sslProvider(sslServerProvider()) .sslContextProvider(serverSslContextProvider()) .protocols(protocols()) @@ -987,9 +1028,9 @@ public abstract class SSLEngineTest { .trustManager(servertTrustCrtFile) .ciphers(null, IdentityCipherSuiteFilter.INSTANCE) .sessionCacheSize(0) - .sessionTimeout(0).build(); + .sessionTimeout(0).build()); clientSslCtx = - SslContextBuilder.forClient() + wrapContext(SslContextBuilder.forClient() .sslProvider(sslClientProvider()) .sslContextProvider(clientSslContextProvider()) .protocols(protocols()) @@ -998,7 +1039,7 @@ public abstract class SSLEngineTest { .keyManager(clientCrtFile, clientKeyFile, clientKeyPassword) .ciphers(null, IdentityCipherSuiteFilter.INSTANCE) .sessionCacheSize(0) - .sessionTimeout(0).build(); + .sessionTimeout(0).build()); serverConnectedChannel = null; sb = new ServerBootstrap(); @@ -1053,7 +1094,10 @@ public abstract class SSLEngineTest { protected void initChannel(Channel ch) throws Exception { ch.config().setAllocator(new TestByteBufAllocator(ch.config().getAllocator(), type)); - final SslHandler handler = clientSslCtx.newHandler(ch.alloc()); + final SslHandler handler = delegatingExecutor == null ? + clientSslCtx.newHandler(ch.alloc()) : + clientSslCtx.newHandler(ch.alloc(), delegatingExecutor); + handler.engine().setNeedClientAuth(true); ChannelPipeline p = ch.pipeline(); p.addLast(handler); @@ -1143,9 +1187,9 @@ public abstract class SSLEngineTest { @Test public void testGetCreationTime() throws Exception { - clientSslCtx = SslContextBuilder.forClient() + clientSslCtx = wrapContext(SslContextBuilder.forClient() .sslProvider(sslClientProvider()) - .sslContextProvider(clientSslContextProvider()).build(); + .sslContextProvider(clientSslContextProvider()).build()); SSLEngine engine = null; try { engine = wrapEngine(clientSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT)); @@ -1157,20 +1201,20 @@ public abstract class SSLEngineTest { @Test public void testSessionInvalidate() throws Exception { - clientSslCtx = SslContextBuilder.forClient() + clientSslCtx = wrapContext(SslContextBuilder.forClient() .trustManager(InsecureTrustManagerFactory.INSTANCE) .sslProvider(sslClientProvider()) .sslContextProvider(clientSslContextProvider()) .protocols(protocols()) .ciphers(ciphers()) - .build(); + .build()); SelfSignedCertificate ssc = new SelfSignedCertificate(); - serverSslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()) + serverSslCtx = wrapContext(SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()) .sslProvider(sslServerProvider()) .sslContextProvider(serverSslContextProvider()) .protocols(protocols()) .ciphers(ciphers()) - .build(); + .build()); SSLEngine clientEngine = null; SSLEngine serverEngine = null; try { @@ -1191,20 +1235,20 @@ public abstract class SSLEngineTest { @Test public void testSSLSessionId() throws Exception { - clientSslCtx = SslContextBuilder.forClient() + clientSslCtx = wrapContext(SslContextBuilder.forClient() .trustManager(InsecureTrustManagerFactory.INSTANCE) .sslProvider(sslClientProvider()) // This test only works for non TLSv1.3 for now .protocols(PROTOCOL_TLS_V1_2) .sslContextProvider(clientSslContextProvider()) - .build(); + .build()); SelfSignedCertificate ssc = new SelfSignedCertificate(); - serverSslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()) + serverSslCtx = wrapContext(SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()) .sslProvider(sslServerProvider()) // This test only works for non TLSv1.3 for now .protocols(PROTOCOL_TLS_V1_2) .sslContextProvider(serverSslContextProvider()) - .build(); + .build()); SSLEngine clientEngine = null; SSLEngine serverEngine = null; try { @@ -1233,12 +1277,12 @@ public abstract class SSLEngineTest { throws CertificateException, SSLException, InterruptedException, ExecutionException { Assume.assumeTrue(PlatformDependent.javaVersion() >= 11); final SelfSignedCertificate ssc = new SelfSignedCertificate(); - serverSslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()) + serverSslCtx = wrapContext(SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()) .sslProvider(sslServerProvider()) .sslContextProvider(serverSslContextProvider()) .protocols(protocols()) .ciphers(ciphers()) - .build(); + .build()); sb = new ServerBootstrap() .group(new NioEventLoopGroup(1)) .channel(NioServerSocketChannel.class) @@ -1248,7 +1292,12 @@ public abstract class SSLEngineTest { ch.config().setAllocator(new TestByteBufAllocator(ch.config().getAllocator(), type)); ChannelPipeline p = ch.pipeline(); - p.addLast(serverSslCtx.newHandler(ch.alloc())); + + SslHandler handler = delegatingExecutor == null ? + serverSslCtx.newHandler(ch.alloc()) : + serverSslCtx.newHandler(ch.alloc(), delegatingExecutor); + + p.addLast(handler); p.addLast(new ChannelInboundHandlerAdapter() { @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { @@ -1287,13 +1336,13 @@ public abstract class SSLEngineTest { serverChannel = sb.bind(new InetSocketAddress(0)).syncUninterruptibly().channel(); - clientSslCtx = SslContextBuilder.forClient() + clientSslCtx = wrapContext(SslContextBuilder.forClient() // OpenSslEngine doesn't support renegotiation on client side .sslProvider(SslProvider.JDK) .trustManager(InsecureTrustManagerFactory.INSTANCE) .protocols(protocols()) .ciphers(ciphers()) - .build(); + .build()); cb = new Bootstrap(); cb.group(new NioEventLoopGroup(1)) @@ -1304,7 +1353,11 @@ public abstract class SSLEngineTest { ch.config().setAllocator(new TestByteBufAllocator(ch.config().getAllocator(), type)); ChannelPipeline p = ch.pipeline(); - SslHandler sslHandler = clientSslCtx.newHandler(ch.alloc()); + + SslHandler sslHandler = delegatingExecutor == null ? + clientSslCtx.newHandler(ch.alloc()) : + clientSslCtx.newHandler(ch.alloc(), delegatingExecutor); + // The renegotiate is not expected to succeed, so we should stop trying in a timely manner so // the unit test can terminate relativley quicly. sslHandler.setHandshakeTimeout(1, TimeUnit.SECONDS); @@ -1352,12 +1405,12 @@ public abstract class SSLEngineTest { try { File serverKeyFile = ResourcesUtil.getFile(getClass(), "test_unencrypted.pem"); File serverCrtFile = ResourcesUtil.getFile(getClass(), "test.crt"); - serverSslCtx = SslContextBuilder.forServer(serverCrtFile, serverKeyFile) + serverSslCtx = wrapContext(SslContextBuilder.forServer(serverCrtFile, serverKeyFile) .sslProvider(sslServerProvider()) .sslContextProvider(serverSslContextProvider()) .protocols(protocols()) .ciphers(ciphers()) - .build(); + .build()); sslEngine = wrapEngine(serverSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT)); @@ -1384,7 +1437,7 @@ public abstract class SSLEngineTest { } } - protected void handshake(SSLEngine clientEngine, SSLEngine serverEngine) throws SSLException { + protected void handshake(SSLEngine clientEngine, SSLEngine serverEngine) throws Exception { ByteBuffer cTOs = allocateBuffer(clientEngine.getSession().getPacketBufferSize()); ByteBuffer sTOc = allocateBuffer(serverEngine.getSession().getPacketBufferSize()); @@ -1477,14 +1530,18 @@ public abstract class SSLEngineTest { return result.getHandshakeStatus() == SSLEngineResult.HandshakeStatus.FINISHED; } - private static void runDelegatedTasks(SSLEngineResult result, SSLEngine engine) { + private void runDelegatedTasks(SSLEngineResult result, SSLEngine engine) throws Exception { if (result.getHandshakeStatus() == SSLEngineResult.HandshakeStatus.NEED_TASK) { for (;;) { Runnable task = engine.getDelegatedTask(); if (task == null) { break; } - task.run(); + if (delegatingExecutor == null) { + task.run(); + } else { + delegatingExecutor.submit(task).get(); + } } } } @@ -1562,7 +1619,7 @@ public abstract class SSLEngineTest { clientCtxBuilder.protocols(PROTOCOL_TLS_V1_2); } - setupHandlers(serverCtxBuilder.build(), clientCtxBuilder.build()); + setupHandlers(wrapContext(serverCtxBuilder.build()), wrapContext(clientCtxBuilder.build())); } finally { ssc.delete(); } @@ -1586,7 +1643,12 @@ public abstract class SSLEngineTest { ch.config().setAllocator(new TestByteBufAllocator(ch.config().getAllocator(), type)); ChannelPipeline p = ch.pipeline(); - p.addLast(serverSslCtx.newHandler(ch.alloc())); + + SslHandler sslHandler = delegatingExecutor == null ? + serverSslCtx.newHandler(ch.alloc()) : + serverSslCtx.newHandler(ch.alloc(), delegatingExecutor); + + p.addLast(sslHandler); p.addLast(new MessageDelegatorChannelHandler(serverReceiver, serverLatch)); p.addLast(new ChannelInboundHandlerAdapter() { @Override @@ -1611,7 +1673,12 @@ public abstract class SSLEngineTest { ch.config().setAllocator(new TestByteBufAllocator(ch.config().getAllocator(), type)); ChannelPipeline p = ch.pipeline(); - p.addLast(clientSslCtx.newHandler(ch.alloc())); + + SslHandler sslHandler = delegatingExecutor == null ? + clientSslCtx.newHandler(ch.alloc()) : + clientSslCtx.newHandler(ch.alloc(), delegatingExecutor); + + p.addLast(sslHandler); p.addLast(new MessageDelegatorChannelHandler(clientReceiver, clientLatch)); p.addLast(new ChannelInboundHandlerAdapter() { @Override @@ -1642,14 +1709,14 @@ public abstract class SSLEngineTest { @Test(timeout = 30000) public void testMutualAuthSameCertChain() throws Exception { serverSslCtx = - SslContextBuilder.forServer( + wrapContext(SslContextBuilder.forServer( new ByteArrayInputStream(X509_CERT_PEM.getBytes(CharsetUtil.UTF_8)), new ByteArrayInputStream(PRIVATE_KEY_PEM.getBytes(CharsetUtil.UTF_8))) .trustManager(new ByteArrayInputStream(X509_CERT_PEM.getBytes(CharsetUtil.UTF_8))) .clientAuth(ClientAuth.REQUIRE).sslProvider(sslServerProvider()) .sslContextProvider(serverSslContextProvider()) .protocols(protocols()) - .ciphers(ciphers()).build(); + .ciphers(ciphers()).build()); sb = new ServerBootstrap(); sb.group(new NioEventLoopGroup(), new NioEventLoopGroup()); @@ -1661,7 +1728,11 @@ public abstract class SSLEngineTest { protected void initChannel(Channel ch) throws Exception { ch.config().setAllocator(new TestByteBufAllocator(ch.config().getAllocator(), type)); - ch.pipeline().addFirst(serverSslCtx.newHandler(ch.alloc())); + SslHandler sslHandler = delegatingExecutor == null ? + serverSslCtx.newHandler(ch.alloc()) : + serverSslCtx.newHandler(ch.alloc(), delegatingExecutor); + + ch.pipeline().addFirst(sslHandler); ch.pipeline().addLast(new ChannelInboundHandlerAdapter() { @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { @@ -1701,13 +1772,13 @@ public abstract class SSLEngineTest { }).bind(new InetSocketAddress(0)).syncUninterruptibly().channel(); clientSslCtx = - SslContextBuilder.forClient().keyManager( + wrapContext(SslContextBuilder.forClient().keyManager( new ByteArrayInputStream(CLIENT_X509_CERT_CHAIN_PEM.getBytes(CharsetUtil.UTF_8)), new ByteArrayInputStream(CLIENT_PRIVATE_KEY_PEM.getBytes(CharsetUtil.UTF_8))) .trustManager(new ByteArrayInputStream(X509_CERT_PEM.getBytes(CharsetUtil.UTF_8))) .sslProvider(sslClientProvider()) .sslContextProvider(clientSslContextProvider()) - .protocols(protocols()).ciphers(ciphers()).build(); + .protocols(protocols()).ciphers(ciphers()).build()); cb = new Bootstrap(); cb.group(new NioEventLoopGroup()); cb.channel(NioSocketChannel.class); @@ -1727,21 +1798,21 @@ public abstract class SSLEngineTest { public void testUnwrapBehavior() throws Exception { SelfSignedCertificate cert = new SelfSignedCertificate(); - clientSslCtx = SslContextBuilder + clientSslCtx = wrapContext(SslContextBuilder .forClient() .trustManager(cert.cert()) .sslProvider(sslClientProvider()) .protocols(protocols()) .ciphers(ciphers()) - .build(); + .build()); SSLEngine client = wrapEngine(clientSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT)); - serverSslCtx = SslContextBuilder + serverSslCtx = wrapContext(SslContextBuilder .forServer(cert.certificate(), cert.privateKey()) .sslProvider(sslServerProvider()) .protocols(protocols()) .ciphers(ciphers()) - .build(); + .build()); SSLEngine server = wrapEngine(serverSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT)); byte[] bytes = "Hello World".getBytes(CharsetUtil.US_ASCII); @@ -1821,19 +1892,19 @@ public abstract class SSLEngineTest { private void testProtocol(String[] clientProtocols, String[] serverProtocols) throws Exception { SelfSignedCertificate cert = new SelfSignedCertificate(); - clientSslCtx = SslContextBuilder + clientSslCtx = wrapContext(SslContextBuilder .forClient() .trustManager(cert.cert()) .sslProvider(sslClientProvider()) .protocols(clientProtocols) - .build(); + .build()); SSLEngine client = wrapEngine(clientSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT)); - serverSslCtx = SslContextBuilder + serverSslCtx = wrapContext(SslContextBuilder .forServer(cert.certificate(), cert.privateKey()) .sslProvider(sslServerProvider()) .protocols(serverProtocols) - .build(); + .build()); SSLEngine server = wrapEngine(serverSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT)); try { @@ -1851,18 +1922,18 @@ public abstract class SSLEngineTest { // Select a mandatory cipher from the TLSv1.2 RFC https://www.ietf.org/rfc/rfc5246.txt so handshakes won't fail // due to no shared/supported cipher. final String sharedCipher = "TLS_RSA_WITH_AES_128_CBC_SHA"; - clientSslCtx = SslContextBuilder.forClient() + clientSslCtx = wrapContext(SslContextBuilder.forClient() .trustManager(InsecureTrustManagerFactory.INSTANCE) - .ciphers(Arrays.asList(sharedCipher)) + .ciphers(Collections.singletonList(sharedCipher)) .protocols(PROTOCOL_TLS_V1_2, PROTOCOL_TLS_V1) .sslProvider(sslClientProvider()) - .build(); + .build()); - serverSslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()) - .ciphers(Arrays.asList(sharedCipher)) + serverSslCtx = wrapContext(SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()) + .ciphers(Collections.singletonList(sharedCipher)) .protocols(PROTOCOL_TLS_V1_2, PROTOCOL_TLS_V1) .sslProvider(sslServerProvider()) - .build(); + .build()); SSLEngine clientEngine = null; SSLEngine serverEngine = null; try { @@ -1882,18 +1953,18 @@ public abstract class SSLEngineTest { // Select a mandatory cipher from the TLSv1.2 RFC https://www.ietf.org/rfc/rfc5246.txt so handshakes won't fail // due to no shared/supported cipher. final String sharedCipher = "TLS_RSA_WITH_AES_128_CBC_SHA"; - clientSslCtx = SslContextBuilder.forClient() + clientSslCtx = wrapContext(SslContextBuilder.forClient() .trustManager(InsecureTrustManagerFactory.INSTANCE) - .ciphers(Arrays.asList(sharedCipher), SupportedCipherSuiteFilter.INSTANCE) + .ciphers(Collections.singletonList(sharedCipher), SupportedCipherSuiteFilter.INSTANCE) .protocols(PROTOCOL_TLS_V1_2, PROTOCOL_TLS_V1) .sslProvider(sslClientProvider()) - .build(); + .build()); - serverSslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()) - .ciphers(Arrays.asList(sharedCipher), SupportedCipherSuiteFilter.INSTANCE) + serverSslCtx = wrapContext(SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()) + .ciphers(Collections.singletonList(sharedCipher), SupportedCipherSuiteFilter.INSTANCE) .protocols(PROTOCOL_TLS_V1_2, PROTOCOL_TLS_V1) .sslProvider(sslServerProvider()) - .build(); + .build()); SSLEngine clientEngine = null; SSLEngine serverEngine = null; try { @@ -1911,21 +1982,21 @@ public abstract class SSLEngineTest { public void testPacketBufferSizeLimit() throws Exception { SelfSignedCertificate cert = new SelfSignedCertificate(); - clientSslCtx = SslContextBuilder + clientSslCtx = wrapContext(SslContextBuilder .forClient() .trustManager(cert.cert()) .sslProvider(sslClientProvider()) .protocols(protocols()) .ciphers(ciphers()) - .build(); + .build()); SSLEngine client = wrapEngine(clientSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT)); - serverSslCtx = SslContextBuilder + serverSslCtx = wrapContext(SslContextBuilder .forServer(cert.certificate(), cert.privateKey()) .sslProvider(sslServerProvider()) .protocols(protocols()) .ciphers(ciphers()) - .build(); + .build()); SSLEngine server = wrapEngine(serverSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT)); try { @@ -1955,12 +2026,12 @@ public abstract class SSLEngineTest { @Test public void testSSLEngineUnwrapNoSslRecord() throws Exception { - clientSslCtx = SslContextBuilder + clientSslCtx = wrapContext(SslContextBuilder .forClient() .sslProvider(sslClientProvider()) .protocols(protocols()) .ciphers(ciphers()) - .build(); + .build()); SSLEngine client = wrapEngine(clientSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT)); try { @@ -1985,12 +2056,12 @@ public abstract class SSLEngineTest { @Test public void testBeginHandshakeAfterEngineClosed() throws SSLException { - clientSslCtx = SslContextBuilder + clientSslCtx = wrapContext(SslContextBuilder .forClient() .sslProvider(sslClientProvider()) .protocols(protocols()) .ciphers(ciphers()) - .build(); + .build()); SSLEngine client = wrapEngine(clientSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT)); try { @@ -2011,20 +2082,20 @@ public abstract class SSLEngineTest { public void testBeginHandshakeCloseOutbound() throws Exception { SelfSignedCertificate cert = new SelfSignedCertificate(); - clientSslCtx = SslContextBuilder + clientSslCtx = wrapContext(SslContextBuilder .forClient() .sslProvider(sslClientProvider()) .protocols(protocols()) .ciphers(ciphers()) - .build(); + .build()); SSLEngine client = wrapEngine(clientSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT)); - serverSslCtx = SslContextBuilder + serverSslCtx = wrapContext(SslContextBuilder .forServer(cert.certificate(), cert.privateKey()) .sslProvider(sslServerProvider()) .protocols(protocols()) .ciphers(ciphers()) - .build(); + .build()); SSLEngine server = wrapEngine(serverSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT)); try { @@ -2062,20 +2133,20 @@ public abstract class SSLEngineTest { public void testCloseInboundAfterBeginHandshake() throws Exception { SelfSignedCertificate cert = new SelfSignedCertificate(); - clientSslCtx = SslContextBuilder + clientSslCtx = wrapContext(SslContextBuilder .forClient() .sslProvider(sslClientProvider()) .protocols(protocols()) .ciphers(ciphers()) - .build(); + .build()); SSLEngine client = wrapEngine(clientSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT)); - serverSslCtx = SslContextBuilder + serverSslCtx = wrapContext(SslContextBuilder .forServer(cert.certificate(), cert.privateKey()) .sslProvider(sslServerProvider()) .protocols(protocols()) .ciphers(ciphers()) - .build(); + .build()); SSLEngine server = wrapEngine(serverSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT)); try { @@ -2102,21 +2173,21 @@ public abstract class SSLEngineTest { public void testCloseNotifySequence() throws Exception { SelfSignedCertificate cert = new SelfSignedCertificate(); - clientSslCtx = SslContextBuilder + clientSslCtx = wrapContext(SslContextBuilder .forClient() .trustManager(cert.cert()) .sslProvider(sslClientProvider()) // This test only works for non TLSv1.3 for now .protocols(PROTOCOL_TLS_V1_2) - .build(); + .build()); SSLEngine client = wrapEngine(clientSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT)); - serverSslCtx = SslContextBuilder + serverSslCtx = wrapContext(SslContextBuilder .forServer(cert.certificate(), cert.privateKey()) .sslProvider(sslServerProvider()) // This test only works for non TLSv1.3 for now .protocols(PROTOCOL_TLS_V1_2) - .build(); + .build()); SSLEngine server = wrapEngine(serverSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT)); try { @@ -2142,7 +2213,14 @@ public abstract class SSLEngineTest { assertEquals(SSLEngineResult.Status.CLOSED, result.getStatus()); // Need an UNWRAP to read the response of the close_notify - assertEquals(SSLEngineResult.HandshakeStatus.NEED_UNWRAP, result.getHandshakeStatus()); + if (PlatformDependent.javaVersion() >= 12 && sslClientProvider() == SslProvider.JDK) { + // This is a workaround for a possible JDK12+ bug. + // + // See http://mail.openjdk.java.net/pipermail/security-dev/2019-February/019406.html. + assertEquals(SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING, result.getHandshakeStatus()); + } else { + assertEquals(SSLEngineResult.HandshakeStatus.NEED_UNWRAP, result.getHandshakeStatus()); + } int produced = result.bytesProduced(); int consumed = result.bytesConsumed(); @@ -2207,7 +2285,7 @@ public abstract class SSLEngineTest { assertTrue(client.isOutboundDone()); assertTrue(client.isInboundDone()); - // Ensure that calling wrap or unwrap again will not produce a SSLException + // Ensure that calling wrap or unwrap again will not produce an SSLException encryptedServerToClient.clear(); plainServerOut.clear(); @@ -2249,21 +2327,21 @@ public abstract class SSLEngineTest { public void testWrapAfterCloseOutbound() throws Exception { SelfSignedCertificate cert = new SelfSignedCertificate(); - clientSslCtx = SslContextBuilder + clientSslCtx = wrapContext(SslContextBuilder .forClient() .trustManager(cert.cert()) .sslProvider(sslClientProvider()) .protocols(protocols()) .ciphers(ciphers()) - .build(); + .build()); SSLEngine client = wrapEngine(clientSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT)); - serverSslCtx = SslContextBuilder + serverSslCtx = wrapContext(SslContextBuilder .forServer(cert.certificate(), cert.privateKey()) .sslProvider(sslServerProvider()) .protocols(protocols()) .ciphers(ciphers()) - .build(); + .build()); SSLEngine server = wrapEngine(serverSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT)); try { @@ -2292,21 +2370,21 @@ public abstract class SSLEngineTest { public void testMultipleRecordsInOneBufferWithNonZeroPosition() throws Exception { SelfSignedCertificate cert = new SelfSignedCertificate(); - clientSslCtx = SslContextBuilder + clientSslCtx = wrapContext(SslContextBuilder .forClient() .trustManager(cert.cert()) .sslProvider(sslClientProvider()) .protocols(protocols()) .ciphers(ciphers()) - .build(); + .build()); SSLEngine client = wrapEngine(clientSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT)); - serverSslCtx = SslContextBuilder + serverSslCtx = wrapContext(SslContextBuilder .forServer(cert.certificate(), cert.privateKey()) .sslProvider(sslServerProvider()) .protocols(protocols()) .ciphers(ciphers()) - .build(); + .build()); SSLEngine server = wrapEngine(serverSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT)); try { @@ -2371,21 +2449,21 @@ public abstract class SSLEngineTest { public void testMultipleRecordsInOneBufferBiggerThenPacketBufferSize() throws Exception { SelfSignedCertificate cert = new SelfSignedCertificate(); - clientSslCtx = SslContextBuilder + clientSslCtx = wrapContext(SslContextBuilder .forClient() .trustManager(cert.cert()) .sslProvider(sslClientProvider()) .protocols(protocols()) .ciphers(ciphers()) - .build(); + .build()); SSLEngine client = wrapEngine(clientSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT)); - serverSslCtx = SslContextBuilder + serverSslCtx = wrapContext(SslContextBuilder .forServer(cert.certificate(), cert.privateKey()) .sslProvider(sslServerProvider()) .protocols(protocols()) .ciphers(ciphers()) - .build(); + .build()); SSLEngine server = wrapEngine(serverSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT)); try { @@ -2439,21 +2517,21 @@ public abstract class SSLEngineTest { public void testBufferUnderFlow() throws Exception { SelfSignedCertificate cert = new SelfSignedCertificate(); - clientSslCtx = SslContextBuilder + clientSslCtx = wrapContext(SslContextBuilder .forClient() .trustManager(cert.cert()) .sslProvider(sslClientProvider()) .protocols(protocols()) .ciphers(ciphers()) - .build(); + .build()); SSLEngine client = wrapEngine(clientSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT)); - serverSslCtx = SslContextBuilder + serverSslCtx = wrapContext(SslContextBuilder .forServer(cert.certificate(), cert.privateKey()) .sslProvider(sslServerProvider()) .protocols(protocols()) .ciphers(ciphers()) - .build(); + .build()); SSLEngine server = wrapEngine(serverSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT)); try { @@ -2514,21 +2592,21 @@ public abstract class SSLEngineTest { public void testWrapDoesNotZeroOutSrc() throws Exception { SelfSignedCertificate cert = new SelfSignedCertificate(); - clientSslCtx = SslContextBuilder + clientSslCtx = wrapContext(SslContextBuilder .forClient() .trustManager(cert.cert()) .sslProvider(sslClientProvider()) .protocols(protocols()) .ciphers(ciphers()) - .build(); + .build()); SSLEngine client = wrapEngine(clientSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT)); - serverSslCtx = SslContextBuilder + serverSslCtx = wrapContext(SslContextBuilder .forServer(cert.certificate(), cert.privateKey()) .sslProvider(sslServerProvider()) .protocols(protocols()) .ciphers(ciphers()) - .build(); + .build()); SSLEngine server = wrapEngine(serverSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT)); try { @@ -2571,12 +2649,12 @@ public abstract class SSLEngineTest { private void testDisableProtocols(String protocol, String... disabledProtocols) throws Exception { SelfSignedCertificate cert = new SelfSignedCertificate(); - SslContext ctx = SslContextBuilder + SslContext ctx = wrapContext(SslContextBuilder .forServer(cert.certificate(), cert.privateKey()) .sslProvider(sslServerProvider()) .protocols(protocols()) .ciphers(ciphers()) - .build(); + .build()); SSLEngine server = wrapEngine(ctx.newEngine(UnpooledByteBufAllocator.DEFAULT)); try { @@ -2606,13 +2684,8 @@ public abstract class SSLEngineTest { @Test public void testUsingX509TrustManagerVerifiesHostname() throws Exception { SslProvider clientProvider = sslClientProvider(); - if (clientProvider == SslProvider.OPENSSL || clientProvider == SslProvider.OPENSSL_REFCNT) { - // Need to check if we support hostname validation in the current used OpenSSL version before running - // the test. - Assume.assumeTrue(OpenSsl.supportsHostnameValidation()); - } SelfSignedCertificate cert = new SelfSignedCertificate(); - clientSslCtx = SslContextBuilder + clientSslCtx = wrapContext(SslContextBuilder .forClient() .trustManager(new TrustManagerFactory(new TrustManagerFactorySpi() { @Override @@ -2650,17 +2723,17 @@ public abstract class SSLEngineTest { }, null, TrustManagerFactory.getDefaultAlgorithm()) { }) .sslProvider(sslClientProvider()) - .build(); + .build()); SSLEngine client = wrapEngine(clientSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT, "netty.io", 1234)); SSLParameters sslParameters = client.getSSLParameters(); sslParameters.setEndpointIdentificationAlgorithm("HTTPS"); client.setSSLParameters(sslParameters); - serverSslCtx = SslContextBuilder + serverSslCtx = wrapContext(SslContextBuilder .forServer(cert.certificate(), cert.privateKey()) .sslProvider(sslServerProvider()) - .build(); + .build()); SSLEngine server = wrapEngine(serverSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT)); try { @@ -2683,8 +2756,9 @@ public abstract class SSLEngineTest { cipherList.add("InvalidCipher"); SSLEngine server = null; try { - serverSslCtx = SslContextBuilder.forServer(cert.key(), cert.cert()).sslProvider(sslClientProvider()) - .ciphers(cipherList).build(); + serverSslCtx = wrapContext(SslContextBuilder.forServer(cert.key(), cert.cert()) + .sslProvider(sslClientProvider()) + .ciphers(cipherList).build()); server = wrapEngine(serverSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT)); fail(); } catch (IllegalArgumentException expected) { @@ -2699,20 +2773,20 @@ public abstract class SSLEngineTest { @Test public void testGetCiphersuite() throws Exception { - clientSslCtx = SslContextBuilder.forClient() + clientSslCtx = wrapContext(SslContextBuilder.forClient() .trustManager(InsecureTrustManagerFactory.INSTANCE) .sslProvider(sslClientProvider()) .sslContextProvider(clientSslContextProvider()) .protocols(protocols()) .ciphers(ciphers()) - .build(); + .build()); SelfSignedCertificate ssc = new SelfSignedCertificate(); - serverSslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()) + serverSslCtx = wrapContext(SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()) .sslProvider(sslServerProvider()) .sslContextProvider(serverSslContextProvider()) .protocols(protocols()) .ciphers(ciphers()) - .build(); + .build()); SSLEngine clientEngine = null; SSLEngine serverEngine = null; try { @@ -2734,20 +2808,20 @@ public abstract class SSLEngineTest { @Test public void testSessionBindingEvent() throws Exception { - clientSslCtx = SslContextBuilder.forClient() + clientSslCtx = wrapContext(SslContextBuilder.forClient() .trustManager(InsecureTrustManagerFactory.INSTANCE) .sslProvider(sslClientProvider()) .sslContextProvider(clientSslContextProvider()) .protocols(protocols()) .ciphers(ciphers()) - .build(); + .build()); SelfSignedCertificate ssc = new SelfSignedCertificate(); - serverSslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()) + serverSslCtx = wrapContext(SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()) .sslProvider(sslServerProvider()) .sslContextProvider(serverSslContextProvider()) .protocols(protocols()) .ciphers(ciphers()) - .build(); + .build()); SSLEngine clientEngine = null; SSLEngine serverEngine = null; try { @@ -2836,7 +2910,7 @@ public abstract class SSLEngineTest { SelfSignedCertificate ssc = new SelfSignedCertificate(); KeyManagerFactory kmf = useKeyManagerFactory ? SslContext.buildKeyManagerFactory( - new java.security.cert.X509Certificate[] { ssc.cert()}, ssc.key(), null, null) : null; + new java.security.cert.X509Certificate[] { ssc.cert()}, ssc.key(), null, null, null) : null; SslContextBuilder clientContextBuilder = SslContextBuilder.forClient(); if (mutualAuth) { @@ -2846,13 +2920,13 @@ public abstract class SSLEngineTest { clientContextBuilder.keyManager(ssc.key(), ssc.cert()); } } - clientSslCtx = clientContextBuilder + clientSslCtx = wrapContext(clientContextBuilder .trustManager(InsecureTrustManagerFactory.INSTANCE) .sslProvider(sslClientProvider()) .sslContextProvider(clientSslContextProvider()) .protocols(protocols()) .ciphers(ciphers()) - .build(); + .build()); SslContextBuilder serverContextBuilder = kmf != null ? SslContextBuilder.forServer(kmf) : @@ -2860,12 +2934,12 @@ public abstract class SSLEngineTest { if (mutualAuth) { serverContextBuilder.clientAuth(ClientAuth.REQUIRE); } - serverSslCtx = serverContextBuilder.trustManager(InsecureTrustManagerFactory.INSTANCE) + serverSslCtx = wrapContext(serverContextBuilder.trustManager(InsecureTrustManagerFactory.INSTANCE) .sslProvider(sslServerProvider()) .sslContextProvider(serverSslContextProvider()) .protocols(protocols()) .ciphers(ciphers()) - .build(); + .build()); SSLEngine clientEngine = null; SSLEngine serverEngine = null; try { @@ -2989,10 +3063,361 @@ public abstract class SSLEngineTest { } } + @Test + public void testSupportedSignatureAlgorithms() throws Exception { + final SelfSignedCertificate ssc = new SelfSignedCertificate(); + + final class TestKeyManagerFactory extends KeyManagerFactory { + TestKeyManagerFactory(final KeyManagerFactory factory) { + super(new KeyManagerFactorySpi() { + + private final KeyManager[] managers = factory.getKeyManagers(); + + @Override + protected void engineInit(KeyStore keyStore, char[] chars) { + throw new UnsupportedOperationException(); + } + + @Override + protected void engineInit(ManagerFactoryParameters managerFactoryParameters) { + throw new UnsupportedOperationException(); + } + + @Override + protected KeyManager[] engineGetKeyManagers() { + KeyManager[] array = new KeyManager[managers.length]; + + for (int i = 0 ; i < array.length; i++) { + final X509ExtendedKeyManager x509ExtendedKeyManager = (X509ExtendedKeyManager) managers[i]; + + array[i] = new X509ExtendedKeyManager() { + @Override + public String[] getClientAliases(String s, Principal[] principals) { + fail(); + return null; + } + + @Override + public String chooseClientAlias( + String[] strings, Principal[] principals, Socket socket) { + fail(); + return null; + } + + @Override + public String[] getServerAliases(String s, Principal[] principals) { + fail(); + return null; + } + + @Override + public String chooseServerAlias(String s, Principal[] principals, Socket socket) { + fail(); + return null; + } + + @Override + public String chooseEngineClientAlias( + String[] strings, Principal[] principals, SSLEngine sslEngine) { + assertNotEquals(0, ((ExtendedSSLSession) sslEngine.getHandshakeSession()) + .getPeerSupportedSignatureAlgorithms().length); + assertNotEquals(0, ((ExtendedSSLSession) sslEngine.getHandshakeSession()) + .getLocalSupportedSignatureAlgorithms().length); + return x509ExtendedKeyManager.chooseEngineClientAlias( + strings, principals, sslEngine); + } + + @Override + public String chooseEngineServerAlias( + String s, Principal[] principals, SSLEngine sslEngine) { + assertNotEquals(0, ((ExtendedSSLSession) sslEngine.getHandshakeSession()) + .getPeerSupportedSignatureAlgorithms().length); + assertNotEquals(0, ((ExtendedSSLSession) sslEngine.getHandshakeSession()) + .getLocalSupportedSignatureAlgorithms().length); + return x509ExtendedKeyManager.chooseEngineServerAlias(s, principals, sslEngine); + } + + @Override + public java.security.cert.X509Certificate[] getCertificateChain(String s) { + return x509ExtendedKeyManager.getCertificateChain(s); + } + + @Override + public PrivateKey getPrivateKey(String s) { + return x509ExtendedKeyManager.getPrivateKey(s); + } + }; + } + return array; + } + }, factory.getProvider(), factory.getAlgorithm()); + } + } + + clientSslCtx = wrapContext(SslContextBuilder.forClient() + .keyManager(new TestKeyManagerFactory(newKeyManagerFactory(ssc))) + .trustManager(InsecureTrustManagerFactory.INSTANCE) + .sslProvider(sslClientProvider()) + .sslContextProvider(clientSslContextProvider()) + .protocols(protocols()) + .ciphers(ciphers()) + .build()); + + serverSslCtx = wrapContext(SslContextBuilder.forServer(new TestKeyManagerFactory(newKeyManagerFactory(ssc))) + .trustManager(InsecureTrustManagerFactory.INSTANCE) + .sslContextProvider(serverSslContextProvider()) + .sslProvider(sslServerProvider()) + .protocols(protocols()) + .ciphers(ciphers()) + .clientAuth(ClientAuth.REQUIRE) + .build()); + SSLEngine clientEngine = null; + SSLEngine serverEngine = null; + try { + clientEngine = wrapEngine(clientSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT)); + serverEngine = wrapEngine(serverSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT)); + handshake(clientEngine, serverEngine); + } finally { + cleanupClientSslEngine(clientEngine); + cleanupServerSslEngine(serverEngine); + ssc.delete(); + } + } + + @Test + public void testHandshakeSession() throws Exception { + final SelfSignedCertificate ssc = new SelfSignedCertificate(); + + final TestTrustManagerFactory clientTmf = new TestTrustManagerFactory(ssc.cert()); + final TestTrustManagerFactory serverTmf = new TestTrustManagerFactory(ssc.cert()); + + clientSslCtx = wrapContext(SslContextBuilder.forClient() + .trustManager(new SimpleTrustManagerFactory() { + @Override + protected void engineInit(KeyStore keyStore) { + // NOOP + } + + @Override + protected void engineInit(ManagerFactoryParameters managerFactoryParameters) { + // NOOP + } + + @Override + protected TrustManager[] engineGetTrustManagers() { + return new TrustManager[] { clientTmf }; + } + }) + .keyManager(newKeyManagerFactory(ssc)) + .sslProvider(sslClientProvider()) + .sslContextProvider(clientSslContextProvider()) + .protocols(protocols()) + .ciphers(ciphers()) + .build()); + serverSslCtx = wrapContext(SslContextBuilder.forServer(newKeyManagerFactory(ssc)) + .trustManager(new SimpleTrustManagerFactory() { + @Override + protected void engineInit(KeyStore keyStore) { + // NOOP + } + + @Override + protected void engineInit(ManagerFactoryParameters managerFactoryParameters) { + // NOOP + } + + @Override + protected TrustManager[] engineGetTrustManagers() { + return new TrustManager[] { serverTmf }; + } + }) + .sslProvider(sslServerProvider()) + .sslContextProvider(serverSslContextProvider()) + .protocols(protocols()) + .ciphers(ciphers()) + .clientAuth(ClientAuth.REQUIRE) + .build()); + SSLEngine clientEngine = null; + SSLEngine serverEngine = null; + try { + clientEngine = wrapEngine(clientSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT)); + serverEngine = wrapEngine(serverSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT)); + handshake(clientEngine, serverEngine); + + assertTrue(clientTmf.isVerified()); + assertTrue(serverTmf.isVerified()); + } finally { + cleanupClientSslEngine(clientEngine); + cleanupServerSslEngine(serverEngine); + ssc.delete(); + } + } + + @Test + public void testMasterKeyLogging() throws Exception { + + /* + * At the moment master key logging is not supported for conscrypt + */ + Assume.assumeFalse(serverSslContextProvider() instanceof OpenSSLProvider); + + /* + * The JDK SSL engine master key retrieval relies on being able to set field access to true. + * That is not available in JDK9+ + */ + Assume.assumeFalse(sslServerProvider() == SslProvider.JDK && PlatformDependent.javaVersion() > 8); + + String originalSystemPropertyValue = SystemPropertyUtil.get(SslMasterKeyHandler.SYSTEM_PROP_KEY); + System.setProperty(SslMasterKeyHandler.SYSTEM_PROP_KEY, Boolean.TRUE.toString()); + + SelfSignedCertificate ssc = new SelfSignedCertificate(); + serverSslCtx = wrapContext(SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()) + .sslProvider(sslServerProvider()) + .sslContextProvider(serverSslContextProvider()) + .build()); + Socket socket = null; + + try { + sb = new ServerBootstrap(); + sb.group(new NioEventLoopGroup(), new NioEventLoopGroup()); + sb.channel(NioServerSocketChannel.class); + + final Promise<SecretKey> promise = sb.config().group().next().newPromise(); + serverChannel = sb.childHandler(new ChannelInitializer<Channel>() { + @Override + protected void initChannel(Channel ch) throws Exception { + ch.config().setAllocator(new TestByteBufAllocator(ch.config().getAllocator(), type)); + + SslHandler sslHandler = delegatingExecutor == null ? + serverSslCtx.newHandler(ch.alloc()) : + serverSslCtx.newHandler(ch.alloc(), delegatingExecutor); + + ch.pipeline().addLast(sslHandler); + ch.pipeline().addLast(new SslMasterKeyHandler() { + @Override + protected void accept(SecretKey masterKey, SSLSession session) { + promise.setSuccess(masterKey); + } + }); + serverConnectedChannel = ch; + } + }).bind(new InetSocketAddress(0)).sync().channel(); + + int port = ((InetSocketAddress) serverChannel.localAddress()).getPort(); + + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, InsecureTrustManagerFactory.INSTANCE.getTrustManagers(), null); + socket = sslContext.getSocketFactory().createSocket(NetUtil.LOCALHOST, port); + OutputStream out = socket.getOutputStream(); + out.write(1); + out.flush(); + + assertTrue(promise.await(10, TimeUnit.SECONDS)); + SecretKey key = promise.get(); + assertEquals("AES secret key must be 48 bytes", 48, key.getEncoded().length); + } finally { + closeQuietly(socket); + if (originalSystemPropertyValue != null) { + System.setProperty(SslMasterKeyHandler.SYSTEM_PROP_KEY, originalSystemPropertyValue); + } else { + System.clearProperty(SslMasterKeyHandler.SYSTEM_PROP_KEY); + } + ssc.delete(); + } + } + + private static void closeQuietly(Closeable c) { + if (c != null) { + try { + c.close(); + } catch (IOException ignore) { + // ignore + } + } + } + + private KeyManagerFactory newKeyManagerFactory(SelfSignedCertificate ssc) + throws UnrecoverableKeyException, KeyStoreException, NoSuchAlgorithmException, + CertificateException, IOException { + return SslContext.buildKeyManagerFactory( + new java.security.cert.X509Certificate[] { ssc.cert() }, ssc.key(), null, null, null); + } + + private final class TestTrustManagerFactory extends X509ExtendedTrustManager { + private final Certificate localCert; + private volatile boolean verified; + + TestTrustManagerFactory(Certificate localCert) { + this.localCert = localCert; + } + + boolean isVerified() { + return verified; + } + + @Override + public void checkClientTrusted( + java.security.cert.X509Certificate[] x509Certificates, String s, Socket socket) { + fail(); + } + + @Override + public void checkServerTrusted( + java.security.cert.X509Certificate[] x509Certificates, String s, Socket socket) { + fail(); + } + + @Override + public void checkClientTrusted( + java.security.cert.X509Certificate[] x509Certificates, String s, SSLEngine sslEngine) { + verified = true; + assertFalse(sslEngine.getUseClientMode()); + SSLSession session = sslEngine.getHandshakeSession(); + assertNotNull(session); + Certificate[] localCertificates = session.getLocalCertificates(); + assertNotNull(localCertificates); + assertEquals(1, localCertificates.length); + assertEquals(localCert, localCertificates[0]); + assertNotNull(session.getLocalPrincipal()); + } + + @Override + public void checkServerTrusted( + java.security.cert.X509Certificate[] x509Certificates, String s, SSLEngine sslEngine) { + verified = true; + assertTrue(sslEngine.getUseClientMode()); + SSLSession session = sslEngine.getHandshakeSession(); + assertNotNull(session); + assertNull(session.getLocalCertificates()); + assertNull(session.getLocalPrincipal()); + } + + @Override + public void checkClientTrusted( + java.security.cert.X509Certificate[] x509Certificates, String s) { + fail(); + } + + @Override + public void checkServerTrusted( + java.security.cert.X509Certificate[] x509Certificates, String s) { + fail(); + } + + @Override + public java.security.cert.X509Certificate[] getAcceptedIssuers() { + return EmptyArrays.EMPTY_X509_CERTIFICATES; + } + } + protected SSLEngine wrapEngine(SSLEngine engine) { return engine; } + protected SslContext wrapContext(SslContext context) { + return context; + } + protected List<String> ciphers() { return Collections.singletonList(protocolCipherCombo.cipher); } diff --git a/handler/src/test/java/io/netty/handler/ssl/SniClientJava8TestUtil.java b/handler/src/test/java/io/netty/handler/ssl/SniClientJava8TestUtil.java index 4db7c7b..d4bfe25 100644 --- a/handler/src/test/java/io/netty/handler/ssl/SniClientJava8TestUtil.java +++ b/handler/src/test/java/io/netty/handler/ssl/SniClientJava8TestUtil.java @@ -260,7 +260,7 @@ final class SniClientJava8TestUtil { IOException, CertificateException { return new SniX509KeyManagerFactory( new SNIHostName(hostname), SslContext.buildKeyManagerFactory( - new X509Certificate[] { cert.cert() }, cert.key(), null, null)); + new X509Certificate[] { cert.cert() }, cert.key(), null, null, null)); } private static final class SniX509KeyManagerFactory extends KeyManagerFactory { diff --git a/handler/src/test/java/io/netty/handler/ssl/SniClientTest.java b/handler/src/test/java/io/netty/handler/ssl/SniClientTest.java index 56ea815..d959e87 100644 --- a/handler/src/test/java/io/netty/handler/ssl/SniClientTest.java +++ b/handler/src/test/java/io/netty/handler/ssl/SniClientTest.java @@ -29,7 +29,6 @@ import io.netty.handler.ssl.util.InsecureTrustManagerFactory; import io.netty.handler.ssl.util.SelfSignedCertificate; import io.netty.util.Mapping; import io.netty.util.ReferenceCountUtil; -import io.netty.util.ReferenceCounted; import io.netty.util.concurrent.Promise; import io.netty.util.internal.PlatformDependent; import org.junit.Assert; @@ -114,7 +113,7 @@ public class SniClientTest { KeyManagerFactory kmf = PlatformDependent.javaVersion() >= 8 ? SniClientJava8TestUtil.newSniX509KeyManagerFactory(cert, sniHostName) : SslContext.buildKeyManagerFactory( - new X509Certificate[] { cert.cert() }, cert.key(), null, null); + new X509Certificate[] { cert.cert() }, cert.key(), null, null, null); sslServerContext = SslContextBuilder.forServer(kmf) .sslProvider(sslServerProvider) diff --git a/handler/src/test/java/io/netty/handler/ssl/SniHandlerTest.java b/handler/src/test/java/io/netty/handler/ssl/SniHandlerTest.java index d3bf1f2..02f4ac3 100644 --- a/handler/src/test/java/io/netty/handler/ssl/SniHandlerTest.java +++ b/handler/src/test/java/io/netty/handler/ssl/SniHandlerTest.java @@ -16,6 +16,34 @@ package io.netty.handler.ssl; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.junit.Assume.assumeTrue; + +import java.io.File; +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLException; + +import io.netty.util.concurrent.Future; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + import io.netty.bootstrap.Bootstrap; import io.netty.bootstrap.ServerBootstrap; import io.netty.buffer.ByteBuf; @@ -44,28 +72,11 @@ import io.netty.util.DomainNameMappingBuilder; import io.netty.util.Mapping; import io.netty.util.ReferenceCountUtil; import io.netty.util.ReferenceCounted; -import io.netty.util.internal.ResourcesUtil; import io.netty.util.concurrent.Promise; import io.netty.util.internal.ObjectUtil; +import io.netty.util.internal.ResourcesUtil; import io.netty.util.internal.StringUtil; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; - -import java.io.File; -import java.net.InetSocketAddress; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicReference; - -import javax.net.ssl.SSLEngine; - -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.nullValue; -import static org.junit.Assert.*; -import static org.junit.Assume.assumeTrue; +import org.mockito.Mockito; @RunWith(Parameterized.class) public class SniHandlerTest { @@ -307,7 +318,7 @@ public class SniHandlerTest { try { // Push the handshake message. ch.writeInbound(Unpooled.wrappedBuffer(message)); - // TODO(scott): This should fail becasue the engine should reject zero length records during handshake. + // TODO(scott): This should fail because the engine should reject zero length records during handshake. // See https://github.com/netty/netty/issues/6348. // fail(); } catch (Exception e) { @@ -333,12 +344,55 @@ public class SniHandlerTest { } } + @Test(timeout = 10000) + public void testMajorVersionNot3() throws Exception { + SslContext nettyContext = makeSslContext(provider, false); + + try { + DomainNameMapping<SslContext> mapping = new DomainNameMappingBuilder<SslContext>(nettyContext).build(); + + SniHandler handler = new SniHandler(mapping); + EmbeddedChannel ch = new EmbeddedChannel(handler); + + // invalid + byte[] message = {22, 2, 0, 0, 0}; + try { + // Push the handshake message. + ch.writeInbound(Unpooled.wrappedBuffer(message)); + // TODO(scott): This should fail because the engine should reject zero length records during handshake. + // See https://github.com/netty/netty/issues/6348. + // fail(); + } catch (Exception e) { + // expected + } + + ch.close(); + + // Consume all the outbound data that may be produced by the SSLEngine. + for (;;) { + ByteBuf buf = ch.readOutbound(); + if (buf == null) { + break; + } + buf.release(); + } + + assertThat(ch.finish(), is(false)); + assertThat(handler.hostname(), nullValue()); + assertThat(handler.sslContext(), is(nettyContext)); + } finally { + releaseAll(nettyContext); + } + } + @Test public void testSniWithApnHandler() throws Exception { SslContext nettyContext = makeSslContext(provider, true); SslContext sniContext = makeSslContext(provider, true); final SslContext clientContext = makeSslClientContext(provider, true); try { + final AtomicBoolean serverApnCtx = new AtomicBoolean(false); + final AtomicBoolean clientApnCtx = new AtomicBoolean(false); final CountDownLatch serverApnDoneLatch = new CountDownLatch(1); final CountDownLatch clientApnDoneLatch = new CountDownLatch(1); @@ -363,6 +417,8 @@ public class SniHandlerTest { p.addLast(new ApplicationProtocolNegotiationHandler("foo") { @Override protected void configurePipeline(ChannelHandlerContext ctx, String protocol) { + // addresses issue #9131 + serverApnCtx.set(ctx.pipeline().context(this) != null); serverApnDoneLatch.countDown(); } }); @@ -381,6 +437,8 @@ public class SniHandlerTest { ch.pipeline().addLast(new ApplicationProtocolNegotiationHandler("foo") { @Override protected void configurePipeline(ChannelHandlerContext ctx, String protocol) { + // addresses issue #9131 + clientApnCtx.set(ctx.pipeline().context(this) != null); clientApnDoneLatch.countDown(); } }); @@ -395,6 +453,8 @@ public class SniHandlerTest { assertTrue(serverApnDoneLatch.await(5, TimeUnit.SECONDS)); assertTrue(clientApnDoneLatch.await(5, TimeUnit.SECONDS)); + assertTrue(serverApnCtx.get()); + assertTrue(clientApnCtx.get()); assertThat(handler.hostname(), is("sni.fake.site")); assertThat(handler.sslContext(), is(sniContext)); } finally { @@ -448,6 +508,7 @@ public class SniHandlerTest { boolean success = false; try { + assertEquals(1, ((ReferenceCountedOpenSslContext) sslContext).refCnt()); // The SniHandler's replaceHandler() method allows us to implement custom behavior. // As an example, we want to release() the SslContext upon channelInactive() or rather // when the SslHandler closes it's SslEngine. If you take a close look at SslHandler @@ -455,6 +516,7 @@ public class SniHandlerTest { SSLEngine sslEngine = sslContext.newEngine(ctx.alloc()); try { + assertEquals(2, ((ReferenceCountedOpenSslContext) sslContext).refCnt()); SslHandler customSslHandler = new CustomSslHandler(sslContext, sslEngine) { @Override public void handlerRemoved0(ChannelHandlerContext ctx) throws Exception { @@ -501,8 +563,9 @@ public class SniHandlerTest { cc.writeAndFlush(Unpooled.wrappedBuffer("Hello, World!".getBytes())) .syncUninterruptibly(); - // Notice how the server's SslContext refCnt is 1 - assertEquals(1, ((ReferenceCounted) sslServerContext).refCnt()); + // Notice how the server's SslContext refCnt is 2 as it is incremented when the SSLEngine is created + // and only decremented once it is destroyed. + assertEquals(2, ((ReferenceCounted) sslServerContext).refCnt()); // The client disconnects cc.close().syncUninterruptibly(); @@ -542,7 +605,7 @@ public class SniHandlerTest { private static class CustomSslHandler extends SslHandler { private final SslContext sslContext; - public CustomSslHandler(SslContext sslContext, SSLEngine sslEngine) { + CustomSslHandler(SslContext sslContext, SSLEngine sslEngine) { super(sslEngine); this.sslContext = ObjectUtil.checkNotNull(sslContext, "sslContext"); } @@ -559,4 +622,79 @@ public class SniHandlerTest { ReferenceCountUtil.release(ctx); } } + + @Test + public void testNonFragmented() throws Exception { + testWithFragmentSize(Integer.MAX_VALUE); + } + @Test + public void testFragmented() throws Exception { + testWithFragmentSize(50); + } + + private void testWithFragmentSize(final int maxFragmentSize) throws Exception { + final String sni = "netty.io"; + SelfSignedCertificate cert = new SelfSignedCertificate(); + final SslContext context = SslContextBuilder.forServer(cert.key(), cert.cert()) + .sslProvider(provider) + .build(); + try { + @SuppressWarnings("unchecked") final EmbeddedChannel server = new EmbeddedChannel( + new SniHandler(Mockito.mock(DomainNameMapping.class)) { + @Override + protected Future<SslContext> lookup(final ChannelHandlerContext ctx, final String hostname) { + assertEquals(sni, hostname); + return ctx.executor().newSucceededFuture(context); + } + }); + + final List<ByteBuf> buffers = clientHelloInMultipleFragments(provider, sni, maxFragmentSize); + for (ByteBuf buffer : buffers) { + server.writeInbound(buffer); + } + assertTrue(server.finishAndReleaseAll()); + } finally { + releaseAll(context); + cert.delete(); + } + } + + private static List<ByteBuf> clientHelloInMultipleFragments( + SslProvider provider, String hostname, int maxTlsPlaintextSize) throws SSLException { + final EmbeddedChannel client = new EmbeddedChannel(); + final SslContext ctx = SslContextBuilder.forClient() + .sslProvider(provider) + .trustManager(InsecureTrustManagerFactory.INSTANCE) + .build(); + try { + final SslHandler sslHandler = ctx.newHandler(client.alloc(), hostname, -1); + client.pipeline().addLast(sslHandler); + final ByteBuf clientHello = client.readOutbound(); + List<ByteBuf> buffers = split(clientHello, maxTlsPlaintextSize); + assertTrue(client.finishAndReleaseAll()); + return buffers; + } finally { + releaseAll(ctx); + } + } + + private static List<ByteBuf> split(ByteBuf clientHello, int maxSize) { + final int type = clientHello.readUnsignedByte(); + final int version = clientHello.readUnsignedShort(); + final int length = clientHello.readUnsignedShort(); + assertEquals(length, clientHello.readableBytes()); + + final List<ByteBuf> result = new ArrayList<ByteBuf>(); + while (clientHello.readableBytes() > 0) { + final int toRead = Math.min(maxSize, clientHello.readableBytes()); + final ByteBuf bb = clientHello.alloc().buffer(SslUtils.SSL_RECORD_HEADER_LENGTH + toRead); + bb.writeByte(type); + bb.writeShort(version); + bb.writeShort(toRead); + bb.writeBytes(clientHello, toRead); + result.add(bb); + } + clientHello.release(); + return result; + } } diff --git a/handler/src/test/java/io/netty/handler/ssl/SslContextBuilderTest.java b/handler/src/test/java/io/netty/handler/ssl/SslContextBuilderTest.java index 20f2ccb..e3bc1f4 100644 --- a/handler/src/test/java/io/netty/handler/ssl/SslContextBuilderTest.java +++ b/handler/src/test/java/io/netty/handler/ssl/SslContextBuilderTest.java @@ -17,13 +17,22 @@ package io.netty.handler.ssl; import io.netty.buffer.UnpooledByteBufAllocator; import io.netty.handler.ssl.util.SelfSignedCertificate; +import io.netty.util.CharsetUtil; import org.junit.Assume; -import org.junit.Ignore; -import org.junit.Rule; import org.junit.Test; +import javax.net.ssl.KeyManager; import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLException; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509ExtendedKeyManager; +import javax.net.ssl.X509ExtendedTrustManager; +import java.io.ByteArrayInputStream; +import java.net.Socket; +import java.security.Principal; +import java.security.PrivateKey; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; import java.util.Collections; import static org.junit.Assert.*; @@ -52,6 +61,17 @@ public class SslContextBuilderTest { testClientContext(SslProvider.OPENSSL); } + @Test + public void testKeyStoreTypeJdk() throws Exception { + testKeyStoreType(SslProvider.JDK); + } + + @Test + public void testKeyStoreTypeOpenssl() throws Exception { + Assume.assumeTrue(OpenSsl.isAvailable()); + testKeyStoreType(SslProvider.OPENSSL); + } + @Test public void testServerContextFromFileJdk() throws Exception { testServerContextFromFile(SslProvider.JDK); @@ -74,6 +94,63 @@ public class SslContextBuilderTest { testServerContext(SslProvider.OPENSSL); } + @Test + public void testContextFromManagersJdk() throws Exception { + testContextFromManagers(SslProvider.JDK); + } + + @Test + public void testContextFromManagersOpenssl() throws Exception { + Assume.assumeTrue(OpenSsl.isAvailable()); + testContextFromManagers(SslProvider.OPENSSL); + } + + @Test(expected = SSLException.class) + public void testUnsupportedPrivateKeyFailsFastForServer() throws Exception { + Assume.assumeTrue(OpenSsl.isBoringSSL()); + testUnsupportedPrivateKeyFailsFast(true); + } + + @Test(expected = SSLException.class) + public void testUnsupportedPrivateKeyFailsFastForClient() throws Exception { + Assume.assumeTrue(OpenSsl.isBoringSSL()); + testUnsupportedPrivateKeyFailsFast(false); + } + private static void testUnsupportedPrivateKeyFailsFast(boolean server) throws Exception { + Assume.assumeTrue(OpenSsl.isBoringSSL()); + String cert = "-----BEGIN CERTIFICATE-----\n" + + "MIICODCCAY2gAwIBAgIEXKTrajAKBggqhkjOPQQDBDBUMQswCQYDVQQGEwJVUzEM\n" + + "MAoGA1UECAwDTi9hMQwwCgYDVQQHDANOL2ExDDAKBgNVBAoMA04vYTEMMAoGA1UE\n" + + "CwwDTi9hMQ0wCwYDVQQDDARUZXN0MB4XDTE5MDQwMzE3MjA0MloXDTIwMDQwMjE3\n" + + "MjA0MlowVDELMAkGA1UEBhMCVVMxDDAKBgNVBAgMA04vYTEMMAoGA1UEBwwDTi9h\n" + + "MQwwCgYDVQQKDANOL2ExDDAKBgNVBAsMA04vYTENMAsGA1UEAwwEVGVzdDCBpzAQ\n" + + "BgcqhkjOPQIBBgUrgQQAJwOBkgAEBPYWoTjlS2pCMGEM2P8qZnmURWA5e7XxPfIh\n" + + "HA876sjmgjJluPgT0OkweuxI4Y/XjzcPnnEBONgzAV1X93UmXdtRiIau/zvsAeFb\n" + + "j/q+6sfj1jdnUk6QsMx22kAwplXHmdz1z5ShXQ7mDZPxDbhCPEAUXzIzOqvWIZyA\n" + + "HgFxZXmQKEhExA8nxgSIvzQ3ucMwMAoGCCqGSM49BAMEA4GYADCBlAJIAdPD6jaN\n" + + "vGxkxcsIbcHn2gSfP1F1G8iNJYrXIN91KbQm8OEp4wxqnBwX8gb/3rmSoEhIU/te\n" + + "CcHuFs0guBjfgRWtJ/eDnKB/AkgDbkqrB5wqJFBmVd/rJ5QdwUVNuGP/vDjFVlb6\n" + + "Esny6//gTL7jYubLUKHOPIMftCZ2Jn4b+5l0kAs62HD5XkZLPDTwRbf7VCE=\n" + + "-----END CERTIFICATE-----"; + String key = "-----BEGIN PRIVATE KEY-----\n" + + "MIIBCQIBADAQBgcqhkjOPQIBBgUrgQQAJwSB8TCB7gIBAQRIALNClTXqQWWlYDHw\n" + + "LjNxXpLk17iPepkmablhbxmYX/8CNzoz1o2gcUidoIO2DM9hm7adI/W31EOmSiUJ\n" + + "+UsC/ZH3i2qr0wn+oAcGBSuBBAAnoYGVA4GSAAQE9hahOOVLakIwYQzY/ypmeZRF\n" + + "YDl7tfE98iEcDzvqyOaCMmW4+BPQ6TB67Ejhj9ePNw+ecQE42DMBXVf3dSZd21GI\n" + + "hq7/O+wB4VuP+r7qx+PWN2dSTpCwzHbaQDCmVceZ3PXPlKFdDuYNk/ENuEI8QBRf\n" + + "MjM6q9YhnIAeAXFleZAoSETEDyfGBIi/NDe5wzA=\n" + + "-----END PRIVATE KEY-----"; + if (server) { + SslContextBuilder.forServer(new ByteArrayInputStream(cert.getBytes(CharsetUtil.US_ASCII)), + new ByteArrayInputStream(key.getBytes(CharsetUtil.US_ASCII)), null) + .sslProvider(SslProvider.OPENSSL).build(); + } else { + SslContextBuilder.forClient().keyManager(new ByteArrayInputStream(cert.getBytes(CharsetUtil.US_ASCII)), + new ByteArrayInputStream(key.getBytes(CharsetUtil.US_ASCII)), null) + .sslProvider(SslProvider.OPENSSL).build(); + } + } + @Test(expected = IllegalArgumentException.class) public void testInvalidCipherJdk() throws Exception { Assume.assumeTrue(OpenSsl.isAvailable()); @@ -95,6 +172,17 @@ public class SslContextBuilderTest { } } + private static void testKeyStoreType(SslProvider provider) throws Exception { + SelfSignedCertificate cert = new SelfSignedCertificate(); + SslContextBuilder builder = SslContextBuilder.forServer(cert.certificate(), cert.privateKey()) + .sslProvider(provider) + .keyStoreType("PKCS12"); + SslContext context = builder.build(); + SSLEngine engine = context.newEngine(UnpooledByteBufAllocator.DEFAULT); + engine.closeInbound(); + engine.closeOutbound(); + } + private static void testInvalidCipher(SslProvider provider) throws Exception { SelfSignedCertificate cert = new SelfSignedCertificate(); SslContextBuilder builder = SslContextBuilder.forClient() @@ -165,4 +253,104 @@ public class SslContextBuilderTest { engine.closeInbound(); engine.closeOutbound(); } + + private static void testContextFromManagers(SslProvider provider) throws Exception { + final SelfSignedCertificate cert = new SelfSignedCertificate(); + KeyManager customKeyManager = new X509ExtendedKeyManager() { + @Override + public String[] getClientAliases(String s, + Principal[] principals) { + return new String[0]; + } + + @Override + public String chooseClientAlias(String[] strings, + Principal[] principals, + Socket socket) { + return "cert_sent_to_server"; + } + + @Override + public String[] getServerAliases(String s, + Principal[] principals) { + return new String[0]; + } + + @Override + public String chooseServerAlias(String s, + Principal[] principals, + Socket socket) { + return null; + } + + @Override + public X509Certificate[] getCertificateChain(String s) { + X509Certificate[] certificates = new X509Certificate[1]; + certificates[0] = cert.cert(); + return new X509Certificate[0]; + } + + @Override + public PrivateKey getPrivateKey(String s) { + return cert.key(); + } + }; + TrustManager customTrustManager = new X509ExtendedTrustManager() { + @Override + public void checkClientTrusted( + X509Certificate[] x509Certificates, String s, + Socket socket) throws CertificateException { } + + @Override + public void checkServerTrusted( + X509Certificate[] x509Certificates, String s, + Socket socket) throws CertificateException { } + + @Override + public void checkClientTrusted( + X509Certificate[] x509Certificates, String s, + SSLEngine sslEngine) throws CertificateException { } + + @Override + public void checkServerTrusted( + X509Certificate[] x509Certificates, String s, + SSLEngine sslEngine) throws CertificateException { } + + @Override + public void checkClientTrusted( + X509Certificate[] x509Certificates, String s) + throws CertificateException { } + + @Override + public void checkServerTrusted( + X509Certificate[] x509Certificates, String s) + throws CertificateException { } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + }; + SslContextBuilder client_builder = SslContextBuilder.forClient() + .sslProvider(provider) + .keyManager(customKeyManager) + .trustManager(customTrustManager) + .clientAuth(ClientAuth.OPTIONAL); + SslContext client_context = client_builder.build(); + SSLEngine client_engine = client_context.newEngine(UnpooledByteBufAllocator.DEFAULT); + assertFalse(client_engine.getWantClientAuth()); + assertFalse(client_engine.getNeedClientAuth()); + client_engine.closeInbound(); + client_engine.closeOutbound(); + SslContextBuilder server_builder = SslContextBuilder.forServer(customKeyManager) + .sslProvider(provider) + .trustManager(customTrustManager) + .clientAuth(ClientAuth.REQUIRE); + SslContext server_context = server_builder.build(); + SSLEngine server_engine = server_context.newEngine(UnpooledByteBufAllocator.DEFAULT); + assertFalse(server_engine.getWantClientAuth()); + assertTrue(server_engine.getNeedClientAuth()); + server_engine.closeInbound(); + server_engine.closeOutbound(); + } } diff --git a/handler/src/test/java/io/netty/handler/ssl/SslContextTrustManagerTest.java b/handler/src/test/java/io/netty/handler/ssl/SslContextTrustManagerTest.java index 97b90f2..04b6a8e 100644 --- a/handler/src/test/java/io/netty/handler/ssl/SslContextTrustManagerTest.java +++ b/handler/src/test/java/io/netty/handler/ssl/SslContextTrustManagerTest.java @@ -110,7 +110,7 @@ public class SslContextTrustManagerTest { throws Exception { X509Certificate[] certCollection = loadCertCollection(resourceNames); TrustManagerFactory tmf = SslContext.buildTrustManagerFactory( - certCollection, null); + certCollection, null, null); for (TrustManager tm : tmf.getTrustManagers()) { if (tm instanceof X509TrustManager) { diff --git a/handler/src/test/java/io/netty/handler/ssl/SslErrorTest.java b/handler/src/test/java/io/netty/handler/ssl/SslErrorTest.java index 9f20582..6e24b56 100644 --- a/handler/src/test/java/io/netty/handler/ssl/SslErrorTest.java +++ b/handler/src/test/java/io/netty/handler/ssl/SslErrorTest.java @@ -64,7 +64,8 @@ import java.util.Locale; @RunWith(Parameterized.class) public class SslErrorTest { - @Parameterized.Parameters(name = "{index}: serverProvider = {0}, clientProvider = {1}, exception = {2}") + @Parameterized.Parameters( + name = "{index}: serverProvider = {0}, clientProvider = {1}, exception = {2}, serverProduceError = {3}") public static Collection<Object[]> data() { List<SslProvider> serverProviders = new ArrayList<SslProvider>(2); List<SslProvider> clientProviders = new ArrayList<SslProvider>(3); @@ -95,7 +96,8 @@ public class SslErrorTest { for (SslProvider serverProvider: serverProviders) { for (SslProvider clientProvider: clientProviders) { for (CertificateException exception: exceptions) { - params.add(new Object[] { serverProvider, clientProvider, exception}); + params.add(new Object[] { serverProvider, clientProvider, exception, true }); + params.add(new Object[] { serverProvider, clientProvider, exception, false }); } } } @@ -110,11 +112,14 @@ public class SslErrorTest { private final SslProvider serverProvider; private final SslProvider clientProvider; private final CertificateException exception; + private final boolean serverProduceError; - public SslErrorTest(SslProvider serverProvider, SslProvider clientProvider, CertificateException exception) { + public SslErrorTest(SslProvider serverProvider, SslProvider clientProvider, + CertificateException exception, boolean serverProduceError) { this.serverProvider = serverProvider; this.clientProvider = clientProvider; this.exception = exception; + this.serverProduceError = serverProduceError; } @Test(timeout = 30000) @@ -124,56 +129,41 @@ public class SslErrorTest { Assume.assumeTrue(OpenSsl.isAvailable()); SelfSignedCertificate ssc = new SelfSignedCertificate(); - final SslContext sslServerCtx = - SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()) - .sslProvider(serverProvider) - .trustManager(new SimpleTrustManagerFactory() { - @Override - protected void engineInit(KeyStore keyStore) { } - @Override - protected void engineInit(ManagerFactoryParameters managerFactoryParameters) { } - - @Override - protected TrustManager[] engineGetTrustManagers() { - return new TrustManager[] { new X509TrustManager() { - - @Override - public void checkClientTrusted(X509Certificate[] x509Certificates, String s) - throws CertificateException { - throw exception; - } - - @Override - public void checkServerTrusted(X509Certificate[] x509Certificates, String s) - throws CertificateException { - // NOOP - } - @Override - public X509Certificate[] getAcceptedIssuers() { - return EmptyArrays.EMPTY_X509_CERTIFICATES; - } - } }; - } - }).clientAuth(ClientAuth.REQUIRE).build(); - - final SslContext sslClientCtx = SslContextBuilder.forClient() - .trustManager(InsecureTrustManagerFactory.INSTANCE) + SslContextBuilder sslServerCtxBuilder = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()) + .sslProvider(serverProvider) + .clientAuth(ClientAuth.REQUIRE); + SslContextBuilder sslClientCtxBuilder = SslContextBuilder.forClient() .keyManager(new File(getClass().getResource("test.crt").getFile()), new File(getClass().getResource("test_unencrypted.pem").getFile())) - .sslProvider(clientProvider).build(); + .sslProvider(clientProvider); + + if (serverProduceError) { + sslServerCtxBuilder.trustManager(new ExceptionTrustManagerFactory()); + sslClientCtxBuilder.trustManager(InsecureTrustManagerFactory.INSTANCE); + } else { + sslServerCtxBuilder.trustManager(InsecureTrustManagerFactory.INSTANCE); + sslClientCtxBuilder.trustManager(new ExceptionTrustManagerFactory()); + } + + final SslContext sslServerCtx = sslServerCtxBuilder.build(); + final SslContext sslClientCtx = sslClientCtxBuilder.build(); Channel serverChannel = null; Channel clientChannel = null; EventLoopGroup group = new NioEventLoopGroup(); + final Promise<Void> promise = group.next().newPromise(); try { serverChannel = new ServerBootstrap().group(group) .channel(NioServerSocketChannel.class) .handler(new LoggingHandler(LogLevel.INFO)) .childHandler(new ChannelInitializer<Channel>() { @Override - protected void initChannel(Channel ch) throws Exception { + protected void initChannel(Channel ch) { ch.pipeline().addLast(sslServerCtx.newHandler(ch.alloc())); + if (!serverProduceError) { + ch.pipeline().addLast(new AlertValidationHandler(promise)); + } ch.pipeline().addLast(new ChannelInboundHandlerAdapter() { @Override @@ -184,48 +174,20 @@ public class SslErrorTest { } }).bind(0).sync().channel(); - final Promise<Void> promise = group.next().newPromise(); - clientChannel = new Bootstrap().group(group) .channel(NioSocketChannel.class) .handler(new ChannelInitializer<Channel>() { @Override - protected void initChannel(Channel ch) throws Exception { + protected void initChannel(Channel ch) { ch.pipeline().addLast(sslClientCtx.newHandler(ch.alloc())); + if (serverProduceError) { + ch.pipeline().addLast(new AlertValidationHandler(promise)); + } ch.pipeline().addLast(new ChannelInboundHandlerAdapter() { + @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { - // Unwrap as its wrapped by a DecoderException - Throwable unwrappedCause = cause.getCause(); - if (unwrappedCause instanceof SSLException) { - if (exception instanceof TestCertificateException) { - CertPathValidatorException.Reason reason = - ((CertPathValidatorException) exception.getCause()).getReason(); - if (reason == CertPathValidatorException.BasicReason.EXPIRED) { - verifyException(unwrappedCause, "expired", promise); - } else if (reason == CertPathValidatorException.BasicReason.NOT_YET_VALID) { - // BoringSSL uses "expired" in this case while others use "bad" - if (OpenSsl.isBoringSSL()) { - verifyException(unwrappedCause, "expired", promise); - } else { - verifyException(unwrappedCause, "bad", promise); - } - } else if (reason == CertPathValidatorException.BasicReason.REVOKED) { - verifyException(unwrappedCause, "revoked", promise); - } - } else if (exception instanceof CertificateExpiredException) { - verifyException(unwrappedCause, "expired", promise); - } else if (exception instanceof CertificateNotYetValidException) { - // BoringSSL uses "expired" in this case while others use "bad" - if (OpenSsl.isBoringSSL()) { - verifyException(unwrappedCause, "expired", promise); - } else { - verifyException(unwrappedCause, "bad", promise); - } - } else if (exception instanceof CertificateRevokedException) { - verifyException(unwrappedCause, "revoked", promise); - } - } + ctx.close(); } }); } @@ -246,11 +208,88 @@ public class SslErrorTest { } } + private final class ExceptionTrustManagerFactory extends SimpleTrustManagerFactory { + @Override + protected void engineInit(KeyStore keyStore) { } + @Override + protected void engineInit(ManagerFactoryParameters managerFactoryParameters) { } + + @Override + protected TrustManager[] engineGetTrustManagers() { + return new TrustManager[] { new X509TrustManager() { + + @Override + public void checkClientTrusted(X509Certificate[] x509Certificates, String s) + throws CertificateException { + throw exception; + } + + @Override + public void checkServerTrusted(X509Certificate[] x509Certificates, String s) + throws CertificateException { + throw exception; + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return EmptyArrays.EMPTY_X509_CERTIFICATES; + } + } }; + } + } + + private final class AlertValidationHandler extends ChannelInboundHandlerAdapter { + private final Promise<Void> promise; + + AlertValidationHandler(Promise<Void> promise) { + this.promise = promise; + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + // Unwrap as its wrapped by a DecoderException + Throwable unwrappedCause = cause.getCause(); + if (unwrappedCause instanceof SSLException) { + if (exception instanceof TestCertificateException) { + CertPathValidatorException.Reason reason = + ((CertPathValidatorException) exception.getCause()).getReason(); + if (reason == CertPathValidatorException.BasicReason.EXPIRED) { + verifyException(unwrappedCause, "expired", promise); + } else if (reason == CertPathValidatorException.BasicReason.NOT_YET_VALID) { + // BoringSSL uses "expired" in this case while others use "bad" + if (OpenSsl.isBoringSSL()) { + verifyException(unwrappedCause, "expired", promise); + } else { + verifyException(unwrappedCause, "bad", promise); + } + } else if (reason == CertPathValidatorException.BasicReason.REVOKED) { + verifyException(unwrappedCause, "revoked", promise); + } + } else if (exception instanceof CertificateExpiredException) { + verifyException(unwrappedCause, "expired", promise); + } else if (exception instanceof CertificateNotYetValidException) { + // BoringSSL uses "expired" in this case while others use "bad" + if (OpenSsl.isBoringSSL()) { + verifyException(unwrappedCause, "expired", promise); + } else { + verifyException(unwrappedCause, "bad", promise); + } + } else if (exception instanceof CertificateRevokedException) { + verifyException(unwrappedCause, "revoked", promise); + } + } + } + } + // Its a bit hacky to verify against the message that is part of the exception but there is no other way // at the moment as there are no different exceptions for the different alerts. - private static void verifyException(Throwable cause, String messagePart, Promise<Void> promise) { + private void verifyException(Throwable cause, String messagePart, Promise<Void> promise) { String message = cause.getMessage(); - if (message.toLowerCase(Locale.UK).contains(messagePart.toLowerCase(Locale.UK))) { + if (message.toLowerCase(Locale.UK).contains(messagePart.toLowerCase(Locale.UK)) || + // When the error is produced on the client side and the client side uses JDK as provider it will always + // use "certificate unknown". + !serverProduceError && clientProvider == SslProvider.JDK && + message.toLowerCase(Locale.UK).contains("unknown")) { promise.setSuccess(null); } else { Throwable error = new AssertionError("message not contains '" + messagePart + "': " + message); @@ -262,7 +301,7 @@ public class SslErrorTest { private static final class TestCertificateException extends CertificateException { private static final long serialVersionUID = -5816338303868751410L; - public TestCertificateException(Throwable cause) { + TestCertificateException(Throwable cause) { super(cause); } } diff --git a/handler/src/test/java/io/netty/handler/ssl/SslHandlerTest.java b/handler/src/test/java/io/netty/handler/ssl/SslHandlerTest.java index 772a15f..11347c4 100644 --- a/handler/src/test/java/io/netty/handler/ssl/SslHandlerTest.java +++ b/handler/src/test/java/io/netty/handler/ssl/SslHandlerTest.java @@ -23,6 +23,7 @@ import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.Unpooled; import io.netty.buffer.UnpooledByteBufAllocator; import io.netty.channel.Channel; +import io.netty.channel.ChannelDuplexHandler; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandler; @@ -42,6 +43,7 @@ import io.netty.channel.local.LocalServerChannel; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.codec.ByteToMessageDecoder; import io.netty.handler.codec.CodecException; import io.netty.handler.codec.DecoderException; import io.netty.handler.codec.UnsupportedMessageTypeException; @@ -53,16 +55,25 @@ import io.netty.util.ReferenceCountUtil; import io.netty.util.ReferenceCounted; import io.netty.util.concurrent.Future; import io.netty.util.concurrent.FutureListener; +import io.netty.util.concurrent.ImmediateEventExecutor; +import io.netty.util.concurrent.ImmediateExecutor; import io.netty.util.concurrent.Promise; +import io.netty.util.internal.PlatformDependent; import org.hamcrest.CoreMatchers; import org.junit.Test; import java.net.InetSocketAddress; import java.nio.channels.ClosedChannelException; import java.security.NoSuchAlgorithmException; +import java.util.List; +import java.util.Queue; import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; @@ -74,9 +85,13 @@ import javax.net.ssl.SSLException; import javax.net.ssl.SSLProtocolException; import static io.netty.buffer.Unpooled.wrappedBuffer; -import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.nullValue; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -84,6 +99,62 @@ import static org.junit.Assume.assumeTrue; public class SslHandlerTest { + @Test(timeout = 5000) + public void testNonApplicationDataFailureFailsQueuedWrites() throws NoSuchAlgorithmException, InterruptedException { + final CountDownLatch writeLatch = new CountDownLatch(1); + final Queue<ChannelPromise> writesToFail = new ConcurrentLinkedQueue<ChannelPromise>(); + SSLEngine engine = newClientModeSSLEngine(); + SslHandler handler = new SslHandler(engine) { + @Override + public void write(final ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { + super.write(ctx, msg, promise); + writeLatch.countDown(); + } + }; + EmbeddedChannel ch = new EmbeddedChannel(new ChannelDuplexHandler() { + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) { + if (msg instanceof ByteBuf) { + if (((ByteBuf) msg).isReadable()) { + writesToFail.add(promise); + } else { + promise.setSuccess(); + } + } + ReferenceCountUtil.release(msg); + } + }, handler); + + try { + final CountDownLatch writeCauseLatch = new CountDownLatch(1); + final AtomicReference<Throwable> failureRef = new AtomicReference<Throwable>(); + ch.write(Unpooled.wrappedBuffer(new byte[]{1})).addListener(new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture future) { + failureRef.compareAndSet(null, future.cause()); + writeCauseLatch.countDown(); + } + }); + writeLatch.await(); + + // Simulate failing the SslHandler non-application writes after there are applications writes queued. + ChannelPromise promiseToFail; + while ((promiseToFail = writesToFail.poll()) != null) { + promiseToFail.setFailure(new RuntimeException("fake exception")); + } + + writeCauseLatch.await(); + Throwable writeCause = failureRef.get(); + assertNotNull(writeCause); + assertThat(writeCause, is(CoreMatchers.<Throwable>instanceOf(SSLException.class))); + Throwable cause = handler.handshakeFuture().cause(); + assertNotNull(cause); + assertThat(cause, is(CoreMatchers.<Throwable>instanceOf(SSLException.class))); + } finally { + assertFalse(ch.finishAndReleaseAll()); + } + } + @Test public void testNoSslHandshakeEventWhenNoHandshake() throws Exception { final AtomicBoolean inActive = new AtomicBoolean(false); @@ -123,12 +194,12 @@ public class SslHandlerTest { assertFalse(ch.finishAndReleaseAll()); } - @Test(expected = SSLException.class, timeout = 3000) + @Test(expected = SslHandshakeTimeoutException.class, timeout = 3000) public void testClientHandshakeTimeout() throws Exception { testHandshakeTimeout(true); } - @Test(expected = SSLException.class, timeout = 3000) + @Test(expected = SslHandshakeTimeoutException.class, timeout = 3000) public void testServerHandshakeTimeout() throws Exception { testHandshakeTimeout(false); } @@ -143,6 +214,16 @@ public class SslHandlerTest { return engine; } + private static SSLEngine newClientModeSSLEngine() throws NoSuchAlgorithmException { + SSLEngine engine = SSLContext.getDefault().createSSLEngine(); + // Set the mode before we try to do the handshake as otherwise it may throw an IllegalStateException. + // See: + // - https://docs.oracle.com/javase/10/docs/api/javax/net/ssl/SSLEngine.html#beginHandshake() + // - http://mail.openjdk.java.net/pipermail/security-dev/2018-July/017715.html + engine.setUseClientMode(true); + return engine; + } + private static void testHandshakeTimeout(boolean client) throws Exception { SSLEngine engine = SSLContext.getDefault().createSSLEngine(); engine.setUseClientMode(client); @@ -250,10 +331,11 @@ public class SslHandlerTest { .sslProvider(SslProvider.OPENSSL) .build(); try { + assertEquals(1, ((ReferenceCounted) sslContext).refCnt()); SSLEngine sslEngine = sslContext.newEngine(ByteBufAllocator.DEFAULT); EmbeddedChannel ch = new EmbeddedChannel(new SslHandler(sslEngine)); - assertEquals(1, ((ReferenceCounted) sslContext).refCnt()); + assertEquals(2, ((ReferenceCounted) sslContext).refCnt()); assertEquals(1, ((ReferenceCounted) sslEngine).refCnt()); assertTrue(ch.finishAndReleaseAll()); @@ -824,4 +906,278 @@ public class SslHandlerTest { ReferenceCountUtil.release(sslClientCtx); } } + + @Test + public void testHandshakeWithExecutorThatExecuteDirecty() throws Exception { + testHandshakeWithExecutor(new Executor() { + @Override + public void execute(Runnable command) { + command.run(); + } + }); + } + + @Test + public void testHandshakeWithImmediateExecutor() throws Exception { + testHandshakeWithExecutor(ImmediateExecutor.INSTANCE); + } + + @Test + public void testHandshakeWithImmediateEventExecutor() throws Exception { + testHandshakeWithExecutor(ImmediateEventExecutor.INSTANCE); + } + + @Test + public void testHandshakeWithExecutor() throws Exception { + ExecutorService executorService = Executors.newCachedThreadPool(); + try { + testHandshakeWithExecutor(executorService); + } finally { + executorService.shutdown(); + } + } + + private static void testHandshakeWithExecutor(Executor executor) throws Exception { + final SslContext sslClientCtx = SslContextBuilder.forClient() + .trustManager(InsecureTrustManagerFactory.INSTANCE) + .sslProvider(SslProvider.JDK).build(); + + final SelfSignedCertificate cert = new SelfSignedCertificate(); + final SslContext sslServerCtx = SslContextBuilder.forServer(cert.key(), cert.cert()) + .sslProvider(SslProvider.JDK).build(); + + EventLoopGroup group = new NioEventLoopGroup(); + Channel sc = null; + Channel cc = null; + final SslHandler clientSslHandler = sslClientCtx.newHandler(UnpooledByteBufAllocator.DEFAULT, executor); + final SslHandler serverSslHandler = sslServerCtx.newHandler(UnpooledByteBufAllocator.DEFAULT, executor); + + try { + sc = new ServerBootstrap() + .group(group) + .channel(NioServerSocketChannel.class) + .childHandler(serverSslHandler) + .bind(new InetSocketAddress(0)).syncUninterruptibly().channel(); + + ChannelFuture future = new Bootstrap() + .group(group) + .channel(NioSocketChannel.class) + .handler(new ChannelInitializer<Channel>() { + @Override + protected void initChannel(Channel ch) { + ch.pipeline().addLast(clientSslHandler); + } + }).connect(sc.localAddress()); + cc = future.syncUninterruptibly().channel(); + + assertTrue(clientSslHandler.handshakeFuture().await().isSuccess()); + assertTrue(serverSslHandler.handshakeFuture().await().isSuccess()); + } finally { + if (cc != null) { + cc.close().syncUninterruptibly(); + } + if (sc != null) { + sc.close().syncUninterruptibly(); + } + group.shutdownGracefully(); + ReferenceCountUtil.release(sslClientCtx); + } + } + + @Test + public void testClientHandshakeTimeoutBecauseExecutorNotExecute() throws Exception { + testHandshakeTimeoutBecauseExecutorNotExecute(true); + } + + @Test + public void testServerHandshakeTimeoutBecauseExecutorNotExecute() throws Exception { + testHandshakeTimeoutBecauseExecutorNotExecute(false); + } + + private static void testHandshakeTimeoutBecauseExecutorNotExecute(final boolean client) throws Exception { + final SslContext sslClientCtx = SslContextBuilder.forClient() + .trustManager(InsecureTrustManagerFactory.INSTANCE) + .sslProvider(SslProvider.JDK).build(); + + final SelfSignedCertificate cert = new SelfSignedCertificate(); + final SslContext sslServerCtx = SslContextBuilder.forServer(cert.key(), cert.cert()) + .sslProvider(SslProvider.JDK).build(); + + EventLoopGroup group = new NioEventLoopGroup(); + Channel sc = null; + Channel cc = null; + final SslHandler clientSslHandler = sslClientCtx.newHandler(UnpooledByteBufAllocator.DEFAULT, new Executor() { + @Override + public void execute(Runnable command) { + if (!client) { + command.run(); + } + // Do nothing to simulate slow execution. + } + }); + if (client) { + clientSslHandler.setHandshakeTimeout(100, TimeUnit.MILLISECONDS); + } + final SslHandler serverSslHandler = sslServerCtx.newHandler(UnpooledByteBufAllocator.DEFAULT, new Executor() { + @Override + public void execute(Runnable command) { + if (client) { + command.run(); + } + // Do nothing to simulate slow execution. + } + }); + if (!client) { + serverSslHandler.setHandshakeTimeout(100, TimeUnit.MILLISECONDS); + } + try { + sc = new ServerBootstrap() + .group(group) + .channel(NioServerSocketChannel.class) + .childHandler(serverSslHandler) + .bind(new InetSocketAddress(0)).syncUninterruptibly().channel(); + + ChannelFuture future = new Bootstrap() + .group(group) + .channel(NioSocketChannel.class) + .handler(new ChannelInitializer<Channel>() { + @Override + protected void initChannel(Channel ch) { + ch.pipeline().addLast(clientSslHandler); + } + }).connect(sc.localAddress()); + cc = future.syncUninterruptibly().channel(); + + if (client) { + Throwable cause = clientSslHandler.handshakeFuture().await().cause(); + assertThat(cause, CoreMatchers.<Throwable>instanceOf(SSLException.class)); + assertThat(cause.getMessage(), containsString("timed out")); + assertFalse(serverSslHandler.handshakeFuture().await().isSuccess()); + } else { + Throwable cause = serverSslHandler.handshakeFuture().await().cause(); + assertThat(cause, CoreMatchers.<Throwable>instanceOf(SSLException.class)); + assertThat(cause.getMessage(), containsString("timed out")); + assertFalse(clientSslHandler.handshakeFuture().await().isSuccess()); + } + } finally { + if (cc != null) { + cc.close().syncUninterruptibly(); + } + if (sc != null) { + sc.close().syncUninterruptibly(); + } + group.shutdownGracefully(); + ReferenceCountUtil.release(sslClientCtx); + } + } + + @Test(timeout = 5000L) + public void testSessionTicketsWithTLSv12() throws Throwable { + testSessionTickets(SslUtils.PROTOCOL_TLS_V1_2); + } + + @Test(timeout = 5000L) + public void testSessionTicketsWithTLSv13() throws Throwable { + assumeTrue(OpenSsl.isTlsv13Supported()); + testSessionTickets(SslUtils.PROTOCOL_TLS_V1_3); + } + + private static void testSessionTickets(String protocol) throws Throwable { + assumeTrue(OpenSsl.isAvailable()); + final SslContext sslClientCtx = SslContextBuilder.forClient() + .trustManager(InsecureTrustManagerFactory.INSTANCE) + .sslProvider(SslProvider.OPENSSL) + .protocols(protocol) + .build(); + + final SelfSignedCertificate cert = new SelfSignedCertificate(); + final SslContext sslServerCtx = SslContextBuilder.forServer(cert.key(), cert.cert()) + .sslProvider(SslProvider.OPENSSL) + .protocols(protocol) + .build(); + + OpenSslSessionTicketKey key = new OpenSslSessionTicketKey(new byte[OpenSslSessionTicketKey.NAME_SIZE], + new byte[OpenSslSessionTicketKey.HMAC_KEY_SIZE], new byte[OpenSslSessionTicketKey.AES_KEY_SIZE]); + ((OpenSslSessionContext) sslClientCtx.sessionContext()).setTicketKeys(key); + ((OpenSslSessionContext) sslServerCtx.sessionContext()).setTicketKeys(key); + + EventLoopGroup group = new NioEventLoopGroup(); + Channel sc = null; + Channel cc = null; + final SslHandler clientSslHandler = sslClientCtx.newHandler(UnpooledByteBufAllocator.DEFAULT); + final SslHandler serverSslHandler = sslServerCtx.newHandler(UnpooledByteBufAllocator.DEFAULT); + + final BlockingQueue<Object> queue = new LinkedBlockingQueue<Object>(); + final byte[] bytes = new byte[96]; + PlatformDependent.threadLocalRandom().nextBytes(bytes); + try { + sc = new ServerBootstrap() + .group(group) + .channel(NioServerSocketChannel.class) + .childHandler(new ChannelInitializer<Channel>() { + @Override + protected void initChannel(Channel ch) { + ch.pipeline().addLast(serverSslHandler); + ch.pipeline().addLast(new ChannelInboundHandlerAdapter() { + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { + if (evt instanceof SslHandshakeCompletionEvent) { + ctx.writeAndFlush(Unpooled.wrappedBuffer(bytes)); + } + } + }); + } + }) + .bind(new InetSocketAddress(0)).syncUninterruptibly().channel(); + + ChannelFuture future = new Bootstrap() + .group(group) + .channel(NioSocketChannel.class) + .handler(new ChannelInitializer<Channel>() { + @Override + protected void initChannel(Channel ch) { + ch.pipeline().addLast(clientSslHandler); + ch.pipeline().addLast(new ByteToMessageDecoder() { + + @Override + protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) { + if (in.readableBytes() == bytes.length) { + queue.add(in.readBytes(bytes.length)); + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + queue.add(cause); + } + }); + } + }).connect(sc.localAddress()); + cc = future.syncUninterruptibly().channel(); + + assertTrue(clientSslHandler.handshakeFuture().await().isSuccess()); + assertTrue(serverSslHandler.handshakeFuture().await().isSuccess()); + Object obj = queue.take(); + if (obj instanceof ByteBuf) { + ByteBuf buffer = (ByteBuf) obj; + ByteBuf expected = Unpooled.wrappedBuffer(bytes); + try { + assertEquals(expected, buffer); + } finally { + expected.release(); + } + } else { + throw (Throwable) obj; + } + } finally { + if (cc != null) { + cc.close().syncUninterruptibly(); + } + if (sc != null) { + sc.close().syncUninterruptibly(); + } + group.shutdownGracefully(); + ReferenceCountUtil.release(sslClientCtx); + } + } } diff --git a/handler/src/test/java/io/netty/handler/ssl/ocsp/OcspTest.java b/handler/src/test/java/io/netty/handler/ssl/ocsp/OcspTest.java index 161f52a..cfed824 100644 --- a/handler/src/test/java/io/netty/handler/ssl/ocsp/OcspTest.java +++ b/handler/src/test/java/io/netty/handler/ssl/ocsp/OcspTest.java @@ -459,7 +459,7 @@ public class OcspTest { private volatile byte[] response; - public TestClientOcspContext(boolean valid) { + TestClientOcspContext(boolean valid) { this.valid = valid; } @@ -481,7 +481,7 @@ public class OcspTest { private final OcspClientCallback callback; - public OcspClientCallbackHandler(ReferenceCountedOpenSslEngine engine, OcspClientCallback callback) { + OcspClientCallbackHandler(ReferenceCountedOpenSslEngine engine, OcspClientCallback callback) { super(engine); this.callback = callback; } @@ -496,7 +496,7 @@ public class OcspTest { private static final class OcspTestException extends IllegalStateException { private static final long serialVersionUID = 4516426833250228159L; - public OcspTestException(String message) { + OcspTestException(String message) { super(message); } } diff --git a/handler/src/test/java/io/netty/handler/stream/ChunkedWriteHandlerTest.java b/handler/src/test/java/io/netty/handler/stream/ChunkedWriteHandlerTest.java index 5b03048..7d08feb 100644 --- a/handler/src/test/java/io/netty/handler/stream/ChunkedWriteHandlerTest.java +++ b/handler/src/test/java/io/netty/handler/stream/ChunkedWriteHandlerTest.java @@ -21,21 +21,27 @@ import io.netty.buffer.Unpooled; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelPromise; import io.netty.channel.ChannelOutboundHandlerAdapter; +import io.netty.channel.ChannelPromise; import io.netty.channel.embedded.EmbeddedChannel; import io.netty.util.CharsetUtil; import io.netty.util.ReferenceCountUtil; +import org.junit.Assert; import org.junit.Test; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; +import java.io.RandomAccessFile; import java.nio.channels.Channels; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.FileChannel; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import static java.util.concurrent.TimeUnit.*; import static org.junit.Assert.*; public class ChunkedWriteHandlerTest { @@ -100,6 +106,41 @@ public class ChunkedWriteHandlerTest { check(new ChunkedNioFile(TMP), new ChunkedNioFile(TMP), new ChunkedNioFile(TMP)); } + @Test + public void testChunkedNioFileLeftPositionUnchanged() throws IOException { + FileChannel in = null; + final long expectedPosition = 10; + try { + in = new RandomAccessFile(TMP, "r").getChannel(); + in.position(expectedPosition); + check(new ChunkedNioFile(in) { + @Override + public void close() throws Exception { + //no op + } + }); + Assert.assertTrue(in.isOpen()); + Assert.assertEquals(expectedPosition, in.position()); + } finally { + if (in != null) { + in.close(); + } + } + } + + @Test(expected = ClosedChannelException.class) + public void testChunkedNioFileFailOnClosedFileChannel() throws IOException { + final FileChannel in = new RandomAccessFile(TMP, "r").getChannel(); + in.close(); + check(new ChunkedNioFile(in) { + @Override + public void close() throws Exception { + //no op + } + }); + Assert.fail(); + } + @Test public void testUnchunkedData() throws IOException { check(Unpooled.wrappedBuffer(BYTES)); @@ -433,6 +474,142 @@ public class ChunkedWriteHandlerTest { assertEquals(1, chunks.get()); } + @Test + public void testCloseSuccessfulChunkedInput() { + int chunks = 10; + TestChunkedInput input = new TestChunkedInput(chunks); + EmbeddedChannel ch = new EmbeddedChannel(new ChunkedWriteHandler()); + + assertTrue(ch.writeOutbound(input)); + + for (int i = 0; i < chunks; i++) { + ByteBuf buf = ch.readOutbound(); + assertEquals(i, buf.readInt()); + buf.release(); + } + + assertTrue(input.isClosed()); + assertFalse(ch.finish()); + } + + @Test + public void testCloseFailedChunkedInput() { + Exception error = new Exception("Unable to produce a chunk"); + ThrowingChunkedInput input = new ThrowingChunkedInput(error); + + EmbeddedChannel ch = new EmbeddedChannel(new ChunkedWriteHandler()); + + try { + ch.writeOutbound(input); + fail("Exception expected"); + } catch (Exception e) { + assertEquals(error, e); + } + + assertTrue(input.isClosed()); + assertFalse(ch.finish()); + } + + @Test + public void testWriteListenerInvokedAfterSuccessfulChunkedInputClosed() throws Exception { + final TestChunkedInput input = new TestChunkedInput(2); + EmbeddedChannel ch = new EmbeddedChannel(new ChunkedWriteHandler()); + + final AtomicBoolean inputClosedWhenListenerInvoked = new AtomicBoolean(); + final CountDownLatch listenerInvoked = new CountDownLatch(1); + + ChannelFuture writeFuture = ch.write(input); + writeFuture.addListener(new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture future) { + inputClosedWhenListenerInvoked.set(input.isClosed()); + listenerInvoked.countDown(); + } + }); + ch.flush(); + + assertTrue(listenerInvoked.await(10, SECONDS)); + assertTrue(writeFuture.isSuccess()); + assertTrue(inputClosedWhenListenerInvoked.get()); + assertTrue(ch.finishAndReleaseAll()); + } + + @Test + public void testWriteListenerInvokedAfterFailedChunkedInputClosed() throws Exception { + final ThrowingChunkedInput input = new ThrowingChunkedInput(new RuntimeException()); + EmbeddedChannel ch = new EmbeddedChannel(new ChunkedWriteHandler()); + + final AtomicBoolean inputClosedWhenListenerInvoked = new AtomicBoolean(); + final CountDownLatch listenerInvoked = new CountDownLatch(1); + + ChannelFuture writeFuture = ch.write(input); + writeFuture.addListener(new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture future) { + inputClosedWhenListenerInvoked.set(input.isClosed()); + listenerInvoked.countDown(); + } + }); + ch.flush(); + + assertTrue(listenerInvoked.await(10, SECONDS)); + assertFalse(writeFuture.isSuccess()); + assertTrue(inputClosedWhenListenerInvoked.get()); + assertFalse(ch.finish()); + } + + @Test + public void testWriteListenerInvokedAfterChannelClosedAndInputFullyConsumed() throws Exception { + // use empty input which has endOfInput = true + final TestChunkedInput input = new TestChunkedInput(0); + EmbeddedChannel ch = new EmbeddedChannel(new ChunkedWriteHandler()); + + final AtomicBoolean inputClosedWhenListenerInvoked = new AtomicBoolean(); + final CountDownLatch listenerInvoked = new CountDownLatch(1); + + ChannelFuture writeFuture = ch.write(input); + writeFuture.addListener(new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture future) { + inputClosedWhenListenerInvoked.set(input.isClosed()); + listenerInvoked.countDown(); + } + }); + ch.close(); // close channel to make handler discard the input on subsequent flush + ch.flush(); + + assertTrue(listenerInvoked.await(10, SECONDS)); + assertTrue(writeFuture.isSuccess()); + assertTrue(inputClosedWhenListenerInvoked.get()); + assertFalse(ch.finish()); + } + + @Test + public void testWriteListenerInvokedAfterChannelClosedAndInputNotFullyConsumed() throws Exception { + // use non-empty input which has endOfInput = false + final TestChunkedInput input = new TestChunkedInput(42); + EmbeddedChannel ch = new EmbeddedChannel(new ChunkedWriteHandler()); + + final AtomicBoolean inputClosedWhenListenerInvoked = new AtomicBoolean(); + final CountDownLatch listenerInvoked = new CountDownLatch(1); + + ChannelFuture writeFuture = ch.write(input); + writeFuture.addListener(new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture future) { + inputClosedWhenListenerInvoked.set(input.isClosed()); + listenerInvoked.countDown(); + } + }); + ch.close(); // close channel to make handler discard the input on subsequent flush + ch.flush(); + + assertTrue(listenerInvoked.await(10, SECONDS)); + assertFalse(writeFuture.isSuccess()); + assertTrue(inputClosedWhenListenerInvoked.get()); + assertFalse(ch.finish()); + } + private static void check(Object... inputs) { EmbeddedChannel ch = new EmbeddedChannel(new ChunkedWriteHandler()); @@ -524,4 +701,96 @@ public class ChunkedWriteHandlerTest { assertEquals(BYTES.length, read); } + + private static final class TestChunkedInput implements ChunkedInput<ByteBuf> { + private final int chunksToProduce; + + private int chunksProduced; + private volatile boolean closed; + + TestChunkedInput(int chunksToProduce) { + this.chunksToProduce = chunksToProduce; + } + + @Override + public boolean isEndOfInput() { + return chunksProduced >= chunksToProduce; + } + + @Override + public void close() { + closed = true; + } + + @Override + public ByteBuf readChunk(ChannelHandlerContext ctx) { + return readChunk(ctx.alloc()); + } + + @Override + public ByteBuf readChunk(ByteBufAllocator allocator) { + ByteBuf buf = allocator.buffer(); + buf.writeInt(chunksProduced); + chunksProduced++; + return buf; + } + + @Override + public long length() { + return chunksToProduce; + } + + @Override + public long progress() { + return chunksProduced; + } + + boolean isClosed() { + return closed; + } + } + + private static final class ThrowingChunkedInput implements ChunkedInput<ByteBuf> { + private final Exception error; + + private volatile boolean closed; + + ThrowingChunkedInput(Exception error) { + this.error = error; + } + + @Override + public boolean isEndOfInput() { + return false; + } + + @Override + public void close() { + closed = true; + } + + @Override + public ByteBuf readChunk(ChannelHandlerContext ctx) throws Exception { + return readChunk(ctx.alloc()); + } + + @Override + public ByteBuf readChunk(ByteBufAllocator allocator) throws Exception { + throw error; + } + + @Override + public long length() { + return -1; + } + + @Override + public long progress() { + return -1; + } + + boolean isClosed() { + return closed; + } + } } diff --git a/handler/src/test/java/io/netty/handler/timeout/IdleStateEventTest.java b/handler/src/test/java/io/netty/handler/timeout/IdleStateEventTest.java new file mode 100644 index 0000000..5e2d25e --- /dev/null +++ b/handler/src/test/java/io/netty/handler/timeout/IdleStateEventTest.java @@ -0,0 +1,34 @@ +/* + * Copyright 2019 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.timeout; + +import org.junit.Test; + +import static io.netty.handler.timeout.IdleStateEvent.*; +import static org.hamcrest.Matchers.hasToString; +import static org.junit.Assert.*; + +public class IdleStateEventTest { + @Test + public void testHumanReadableToString() { + assertThat(FIRST_READER_IDLE_STATE_EVENT, hasToString("IdleStateEvent(READER_IDLE, first)")); + assertThat(READER_IDLE_STATE_EVENT, hasToString("IdleStateEvent(READER_IDLE)")); + assertThat(FIRST_WRITER_IDLE_STATE_EVENT, hasToString("IdleStateEvent(WRITER_IDLE, first)")); + assertThat(WRITER_IDLE_STATE_EVENT, hasToString("IdleStateEvent(WRITER_IDLE)")); + assertThat(FIRST_ALL_IDLE_STATE_EVENT, hasToString("IdleStateEvent(ALL_IDLE, first)")); + assertThat(ALL_IDLE_STATE_EVENT, hasToString("IdleStateEvent(ALL_IDLE)")); + } +} diff --git a/handler/src/test/java/io/netty/handler/timeout/IdleStateHandlerTest.java b/handler/src/test/java/io/netty/handler/timeout/IdleStateHandlerTest.java index 0a5b627..668f3f1 100644 --- a/handler/src/test/java/io/netty/handler/timeout/IdleStateHandlerTest.java +++ b/handler/src/test/java/io/netty/handler/timeout/IdleStateHandlerTest.java @@ -236,6 +236,7 @@ public class IdleStateHandlerTest { channel.writeAndFlush(Unpooled.wrappedBuffer(new byte[] { 1 })); channel.writeAndFlush(Unpooled.wrappedBuffer(new byte[] { 2 })); channel.writeAndFlush(Unpooled.wrappedBuffer(new byte[] { 3 })); + channel.writeAndFlush(Unpooled.wrappedBuffer(new byte[5 * 1024])); // Establish a baseline. We're not consuming anything and let it idle once. idleStateHandler.tickRun(); @@ -283,6 +284,30 @@ public class IdleStateHandlerTest { assertEquals(0, events.size()); assertEquals(26L, idleStateHandler.tick(TimeUnit.SECONDS)); // 23s + 2s + 1s + // Consume part of the message every 2 seconds, then be idle for 1 seconds, + // then run the task and we should get an IdleStateEvent because the first trigger + idleStateHandler.tick(2L, TimeUnit.SECONDS); + assertNotNullAndRelease(channel.consumePart(1024)); + idleStateHandler.tick(2L, TimeUnit.SECONDS); + assertNotNullAndRelease(channel.consumePart(1024)); + idleStateHandler.tickRun(1L, TimeUnit.SECONDS); + assertEquals(1, events.size()); + assertEquals(31L, idleStateHandler.tick(TimeUnit.SECONDS)); // 26s + 2s + 2s + 1s + events.clear(); + + // Consume part of the message every 2 seconds, then be idle for 1 seconds, + // then consume all the rest of the message, then run the task and we shouldn't + // get an IdleStateEvent because the data is flowing and we haven't been idle for long enough! + idleStateHandler.tick(2L, TimeUnit.SECONDS); + assertNotNullAndRelease(channel.consumePart(1024)); + idleStateHandler.tick(2L, TimeUnit.SECONDS); + assertNotNullAndRelease(channel.consumePart(1024)); + idleStateHandler.tickRun(1L, TimeUnit.SECONDS); + assertEquals(0, events.size()); + assertEquals(36L, idleStateHandler.tick(TimeUnit.SECONDS)); // 31s + 2s + 2s + 1s + idleStateHandler.tick(2L, TimeUnit.SECONDS); + assertNotNullAndRelease(channel.consumePart(1024)); + // There are no messages left! Advance the ticker by 3 seconds, // attempt a consume() but it will be null, then advance the // ticker by an another 2 seconds and we should get an IdleStateEvent @@ -292,7 +317,7 @@ public class IdleStateHandlerTest { idleStateHandler.tickRun(2L, TimeUnit.SECONDS); assertEquals(1, events.size()); - assertEquals(31L, idleStateHandler.tick(TimeUnit.SECONDS)); // 26s + 3s + 2s + assertEquals(43L, idleStateHandler.tick(TimeUnit.SECONDS)); // 36s + 2s + 3s + 2s // q.e.d. } finally { @@ -317,7 +342,7 @@ public class IdleStateHandlerTest { private long ticksInNanos; - public TestableIdleStateHandler(boolean observeOutput, + TestableIdleStateHandler(boolean observeOutput, long readerIdleTime, long writerIdleTime, long allIdleTime, TimeUnit unit) { super(observeOutput, readerIdleTime, writerIdleTime, allIdleTime, unit); @@ -369,7 +394,7 @@ public class IdleStateHandlerTest { private static class ObservableChannel extends EmbeddedChannel { - public ObservableChannel(ChannelHandler... handlers) { + ObservableChannel(ChannelHandler... handlers) { super(handlers); } @@ -379,7 +404,7 @@ public class IdleStateHandlerTest { // the messages in the ChannelOutboundBuffer. } - public Object consume() { + private Object consume() { ChannelOutboundBuffer buf = unsafe().outboundBuffer(); if (buf != null) { Object msg = buf.current(); @@ -391,5 +416,24 @@ public class IdleStateHandlerTest { } return null; } + + /** + * Consume the part of a message. + * + * @param byteCount count of byte to be consumed + * @return the message currently being consumed + */ + private Object consumePart(int byteCount) { + ChannelOutboundBuffer buf = unsafe().outboundBuffer(); + if (buf != null) { + Object msg = buf.current(); + if (msg != null) { + ReferenceCountUtil.retain(msg); + buf.removeBytes(byteCount); + return msg; + } + } + return null; + } } } diff --git a/license/LICENSE.dnsinfo.txt b/license/LICENSE.dnsinfo.txt new file mode 100644 index 0000000..7554838 --- /dev/null +++ b/license/LICENSE.dnsinfo.txt @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2004-2006, 2008, 2009, 2011 Apple Inc. All rights reserved. + * + * @APPLE_LICENSE_HEADER_START@ + * + * This file contains Original Code and/or Modifications of Original Code + * as defined in and that are subject to the Apple Public Source License + * Version 2.0 (the 'License'). You may not use this file except in + * compliance with the License. Please obtain a copy of the License at + * http://www.opensource.apple.com/apsl/ and read it before using this + * file. + * + * The Original Code and all software distributed under the License are + * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER + * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, + * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. + * Please see the License for the specific language governing rights and + * limitations under the License. + * + * @APPLE_LICENSE_HEADER_END@ + */ diff --git a/license/LICENSE.hyper-hpack.txt b/license/LICENSE.hyper-hpack.txt new file mode 100644 index 0000000..d24c351 --- /dev/null +++ b/license/LICENSE.hyper-hpack.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Cory Benfield + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/license/LICENSE.jboss-marshalling.txt b/license/LICENSE.jboss-marshalling.txt index d80fdbe..e454a52 100644 --- a/license/LICENSE.jboss-marshalling.txt +++ b/license/LICENSE.jboss-marshalling.txt @@ -1,504 +1,178 @@ - GNU LESSER GENERAL PUBLIC LICENSE - Version 2.1, February 1999 - - Copyright (C) 1991, 1999 Free Software Foundation, Inc. - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - -[This is the first released version of the Lesser GPL. It also counts - as the successor of the GNU Library Public License, version 2, hence - the version number 2.1.] - - Preamble - - The licenses for most software are designed to take away your -freedom to share and change it. By contrast, the GNU General Public -Licenses are intended to guarantee your freedom to share and change -free software--to make sure the software is free for all its users. - - This license, the Lesser General Public License, applies to some -specially designated software packages--typically libraries--of the -Free Software Foundation and other authors who decide to use it. You -can use it too, but we suggest you first think carefully about whether -this license or the ordinary General Public License is the better -strategy to use in any particular case, based on the explanations below. - - When we speak of free software, we are referring to freedom of use, -not price. Our General Public Licenses are designed to make sure that -you have the freedom to distribute copies of free software (and charge -for this service if you wish); that you receive source code or can get -it if you want it; that you can change the software and use pieces of -it in new free programs; and that you are informed that you can do -these things. - - To protect your rights, we need to make restrictions that forbid -distributors to deny you these rights or to ask you to surrender these -rights. These restrictions translate to certain responsibilities for -you if you distribute copies of the library or if you modify it. - - For example, if you distribute copies of the library, whether gratis -or for a fee, you must give the recipients all the rights that we gave -you. You must make sure that they, too, receive or can get the source -code. If you link other code with the library, you must provide -complete object files to the recipients, so that they can relink them -with the library after making changes to the library and recompiling -it. And you must show them these terms so they know their rights. - - We protect your rights with a two-step method: (1) we copyright the -library, and (2) we offer you this license, which gives you legal -permission to copy, distribute and/or modify the library. - - To protect each distributor, we want to make it very clear that -there is no warranty for the free library. Also, if the library is -modified by someone else and passed on, the recipients should know -that what they have is not the original version, so that the original -author's reputation will not be affected by problems that might be -introduced by others. - - Finally, software patents pose a constant threat to the existence of -any free program. We wish to make sure that a company cannot -effectively restrict the users of a free program by obtaining a -restrictive license from a patent holder. Therefore, we insist that -any patent license obtained for a version of the library must be -consistent with the full freedom of use specified in this license. - - Most GNU software, including some libraries, is covered by the -ordinary GNU General Public License. This license, the GNU Lesser -General Public License, applies to certain designated libraries, and -is quite different from the ordinary General Public License. We use -this license for certain libraries in order to permit linking those -libraries into non-free programs. - - When a program is linked with a library, whether statically or using -a shared library, the combination of the two is legally speaking a -combined work, a derivative of the original library. The ordinary -General Public License therefore permits such linking only if the -entire combination fits its criteria of freedom. The Lesser General -Public License permits more lax criteria for linking other code with -the library. - - We call this license the "Lesser" General Public License because it -does Less to protect the user's freedom than the ordinary General -Public License. It also provides other free software developers Less -of an advantage over competing non-free programs. These disadvantages -are the reason we use the ordinary General Public License for many -libraries. However, the Lesser license provides advantages in certain -special circumstances. - - For example, on rare occasions, there may be a special need to -encourage the widest possible use of a certain library, so that it becomes -a de-facto standard. To achieve this, non-free programs must be -allowed to use the library. A more frequent case is that a free -library does the same job as widely used non-free libraries. In this -case, there is little to gain by limiting the free library to free -software only, so we use the Lesser General Public License. - - In other cases, permission to use a particular library in non-free -programs enables a greater number of people to use a large body of -free software. For example, permission to use the GNU C Library in -non-free programs enables many more people to use the whole GNU -operating system, as well as its variant, the GNU/Linux operating -system. - - Although the Lesser General Public License is Less protective of the -users' freedom, it does ensure that the user of a program that is -linked with the Library has the freedom and the wherewithal to run -that program using a modified version of the Library. - - The precise terms and conditions for copying, distribution and -modification follow. Pay close attention to the difference between a -"work based on the library" and a "work that uses the library". The -former contains code derived from the library, whereas the latter must -be combined with the library in order to run. - - GNU LESSER GENERAL PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - - 0. This License Agreement applies to any software library or other -program which contains a notice placed by the copyright holder or -other authorized party saying it may be distributed under the terms of -this Lesser General Public License (also called "this License"). -Each licensee is addressed as "you". - - A "library" means a collection of software functions and/or data -prepared so as to be conveniently linked with application programs -(which use some of those functions and data) to form executables. - - The "Library", below, refers to any such software library or work -which has been distributed under these terms. A "work based on the -Library" means either the Library or any derivative work under -copyright law: that is to say, a work containing the Library or a -portion of it, either verbatim or with modifications and/or translated -straightforwardly into another language. (Hereinafter, translation is -included without limitation in the term "modification".) - - "Source code" for a work means the preferred form of the work for -making modifications to it. For a library, complete source code means -all the source code for all modules it contains, plus any associated -interface definition files, plus the scripts used to control compilation -and installation of the library. - - Activities other than copying, distribution and modification are not -covered by this License; they are outside its scope. The act of -running a program using the Library is not restricted, and output from -such a program is covered only if its contents constitute a work based -on the Library (independent of the use of the Library in a tool for -writing it). Whether that is true depends on what the Library does -and what the program that uses the Library does. - - 1. You may copy and distribute verbatim copies of the Library's -complete source code as you receive it, in any medium, provided that -you conspicuously and appropriately publish on each copy an -appropriate copyright notice and disclaimer of warranty; keep intact -all the notices that refer to this License and to the absence of any -warranty; and distribute a copy of this License along with the -Library. - - You may charge a fee for the physical act of transferring a copy, -and you may at your option offer warranty protection in exchange for a -fee. - - 2. You may modify your copy or copies of the Library or any portion -of it, thus forming a work based on the Library, and copy and -distribute such modifications or work under the terms of Section 1 -above, provided that you also meet all of these conditions: - - a) The modified work must itself be a software library. - - b) You must cause the files modified to carry prominent notices - stating that you changed the files and the date of any change. - - c) You must cause the whole of the work to be licensed at no - charge to all third parties under the terms of this License. - - d) If a facility in the modified Library refers to a function or a - table of data to be supplied by an application program that uses - the facility, other than as an argument passed when the facility - is invoked, then you must make a good faith effort to ensure that, - in the event an application does not supply such function or - table, the facility still operates, and performs whatever part of - its purpose remains meaningful. - - (For example, a function in a library to compute square roots has - a purpose that is entirely well-defined independent of the - application. Therefore, Subsection 2d requires that any - application-supplied function or table used by this function must - be optional: if the application does not supply it, the square - root function must still compute square roots.) - -These requirements apply to the modified work as a whole. If -identifiable sections of that work are not derived from the Library, -and can be reasonably considered independent and separate works in -themselves, then this License, and its terms, do not apply to those -sections when you distribute them as separate works. But when you -distribute the same sections as part of a whole which is a work based -on the Library, the distribution of the whole must be on the terms of -this License, whose permissions for other licensees extend to the -entire whole, and thus to each and every part regardless of who wrote -it. - -Thus, it is not the intent of this section to claim rights or contest -your rights to work written entirely by you; rather, the intent is to -exercise the right to control the distribution of derivative or -collective works based on the Library. - -In addition, mere aggregation of another work not based on the Library -with the Library (or with a work based on the Library) on a volume of -a storage or distribution medium does not bring the other work under -the scope of this License. - - 3. You may opt to apply the terms of the ordinary GNU General Public -License instead of this License to a given copy of the Library. To do -this, you must alter all the notices that refer to this License, so -that they refer to the ordinary GNU General Public License, version 2, -instead of to this License. (If a newer version than version 2 of the -ordinary GNU General Public License has appeared, then you can specify -that version instead if you wish.) Do not make any other change in -these notices. - - Once this change is made in a given copy, it is irreversible for -that copy, so the ordinary GNU General Public License applies to all -subsequent copies and derivative works made from that copy. - - This option is useful when you wish to copy part of the code of -the Library into a program that is not a library. - - 4. You may copy and distribute the Library (or a portion or -derivative of it, under Section 2) in object code or executable form -under the terms of Sections 1 and 2 above provided that you accompany -it with the complete corresponding machine-readable source code, which -must be distributed under the terms of Sections 1 and 2 above on a -medium customarily used for software interchange. - - If distribution of object code is made by offering access to copy -from a designated place, then offering equivalent access to copy the -source code from the same place satisfies the requirement to -distribute the source code, even though third parties are not -compelled to copy the source along with the object code. - - 5. A program that contains no derivative of any portion of the -Library, but is designed to work with the Library by being compiled or -linked with it, is called a "work that uses the Library". Such a -work, in isolation, is not a derivative work of the Library, and -therefore falls outside the scope of this License. - - However, linking a "work that uses the Library" with the Library -creates an executable that is a derivative of the Library (because it -contains portions of the Library), rather than a "work that uses the -library". The executable is therefore covered by this License. -Section 6 states terms for distribution of such executables. - - When a "work that uses the Library" uses material from a header file -that is part of the Library, the object code for the work may be a -derivative work of the Library even though the source code is not. -Whether this is true is especially significant if the work can be -linked without the Library, or if the work is itself a library. The -threshold for this to be true is not precisely defined by law. - - If such an object file uses only numerical parameters, data -structure layouts and accessors, and small macros and small inline -functions (ten lines or less in length), then the use of the object -file is unrestricted, regardless of whether it is legally a derivative -work. (Executables containing this object code plus portions of the -Library will still fall under Section 6.) - - Otherwise, if the work is a derivative of the Library, you may -distribute the object code for the work under the terms of Section 6. -Any executables containing that work also fall under Section 6, -whether or not they are linked directly with the Library itself. - - 6. As an exception to the Sections above, you may also combine or -link a "work that uses the Library" with the Library to produce a -work containing portions of the Library, and distribute that work -under terms of your choice, provided that the terms permit -modification of the work for the customer's own use and reverse -engineering for debugging such modifications. - - You must give prominent notice with each copy of the work that the -Library is used in it and that the Library and its use are covered by -this License. You must supply a copy of this License. If the work -during execution displays copyright notices, you must include the -copyright notice for the Library among them, as well as a reference -directing the user to the copy of this License. Also, you must do one -of these things: - - a) Accompany the work with the complete corresponding - machine-readable source code for the Library including whatever - changes were used in the work (which must be distributed under - Sections 1 and 2 above); and, if the work is an executable linked - with the Library, with the complete machine-readable "work that - uses the Library", as object code and/or source code, so that the - user can modify the Library and then relink to produce a modified - executable containing the modified Library. (It is understood - that the user who changes the contents of definitions files in the - Library will not necessarily be able to recompile the application - to use the modified definitions.) - - b) Use a suitable shared library mechanism for linking with the - Library. A suitable mechanism is one that (1) uses at run time a - copy of the library already present on the user's computer system, - rather than copying library functions into the executable, and (2) - will operate properly with a modified version of the library, if - the user installs one, as long as the modified version is - interface-compatible with the version that the work was made with. - - c) Accompany the work with a written offer, valid for at - least three years, to give the same user the materials - specified in Subsection 6a, above, for a charge no more - than the cost of performing this distribution. - - d) If distribution of the work is made by offering access to copy - from a designated place, offer equivalent access to copy the above - specified materials from the same place. - - e) Verify that the user has already received a copy of these - materials or that you have already sent this user a copy. - - For an executable, the required form of the "work that uses the -Library" must include any data and utility programs needed for -reproducing the executable from it. However, as a special exception, -the materials to be distributed need not include anything that is -normally distributed (in either source or binary form) with the major -components (compiler, kernel, and so on) of the operating system on -which the executable runs, unless that component itself accompanies -the executable. - - It may happen that this requirement contradicts the license -restrictions of other proprietary libraries that do not normally -accompany the operating system. Such a contradiction means you cannot -use both them and the Library together in an executable that you -distribute. - - 7. You may place library facilities that are a work based on the -Library side-by-side in a single library together with other library -facilities not covered by this License, and distribute such a combined -library, provided that the separate distribution of the work based on -the Library and of the other library facilities is otherwise -permitted, and provided that you do these two things: - - a) Accompany the combined library with a copy of the same work - based on the Library, uncombined with any other library - facilities. This must be distributed under the terms of the - Sections above. - - b) Give prominent notice with the combined library of the fact - that part of it is a work based on the Library, and explaining - where to find the accompanying uncombined form of the same work. - - 8. You may not copy, modify, sublicense, link with, or distribute -the Library except as expressly provided under this License. Any -attempt otherwise to copy, modify, sublicense, link with, or -distribute the Library is void, and will automatically terminate your -rights under this License. However, parties who have received copies, -or rights, from you under this License will not have their licenses -terminated so long as such parties remain in full compliance. - - 9. You are not required to accept this License, since you have not -signed it. However, nothing else grants you permission to modify or -distribute the Library or its derivative works. These actions are -prohibited by law if you do not accept this License. Therefore, by -modifying or distributing the Library (or any work based on the -Library), you indicate your acceptance of this License to do so, and -all its terms and conditions for copying, distributing or modifying -the Library or works based on it. - - 10. Each time you redistribute the Library (or any work based on the -Library), the recipient automatically receives a license from the -original licensor to copy, distribute, link with or modify the Library -subject to these terms and conditions. You may not impose any further -restrictions on the recipients' exercise of the rights granted herein. -You are not responsible for enforcing compliance by third parties with -this License. - - 11. If, as a consequence of a court judgment or allegation of patent -infringement or for any other reason (not limited to patent issues), -conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot -distribute so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you -may not distribute the Library at all. For example, if a patent -license would not permit royalty-free redistribution of the Library by -all those who receive copies directly or indirectly through you, then -the only way you could satisfy both it and this License would be to -refrain entirely from distribution of the Library. - -If any portion of this section is held invalid or unenforceable under any -particular circumstance, the balance of the section is intended to apply, -and the section as a whole is intended to apply in other circumstances. - -It is not the purpose of this section to induce you to infringe any -patents or other property right claims or to contest validity of any -such claims; this section has the sole purpose of protecting the -integrity of the free software distribution system which is -implemented by public license practices. Many people have made -generous contributions to the wide range of software distributed -through that system in reliance on consistent application of that -system; it is up to the author/donor to decide if he or she is willing -to distribute software through any other system and a licensee cannot -impose that choice. - -This section is intended to make thoroughly clear what is believed to -be a consequence of the rest of this License. - - 12. If the distribution and/or use of the Library is restricted in -certain countries either by patents or by copyrighted interfaces, the -original copyright holder who places the Library under this License may add -an explicit geographical distribution limitation excluding those countries, -so that distribution is permitted only in or among countries not thus -excluded. In such case, this License incorporates the limitation as if -written in the body of this License. - - 13. The Free Software Foundation may publish revised and/or new -versions of the Lesser General Public License from time to time. -Such new versions will be similar in spirit to the present version, -but may differ in detail to address new problems or concerns. - -Each version is given a distinguishing version number. If the Library -specifies a version number of this License which applies to it and -"any later version", you have the option of following the terms and -conditions either of that version or of any later version published by -the Free Software Foundation. If the Library does not specify a -license version number, you may choose any version ever published by -the Free Software Foundation. - - 14. If you wish to incorporate parts of the Library into other free -programs whose distribution conditions are incompatible with these, -write to the author to ask for permission. For software which is -copyrighted by the Free Software Foundation, write to the Free -Software Foundation; we sometimes make exceptions for this. Our -decision will be guided by the two goals of preserving the free status -of all derivatives of our free software and of promoting the sharing -and reuse of software generally. - - NO WARRANTY - - 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO -WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. -EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR -OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY -KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE -LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME -THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN -WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY -AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU -FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR -CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE -LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING -RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A -FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF -SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH -DAMAGES. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Libraries - - If you develop a new library, and you want it to be of the greatest -possible use to the public, we recommend making it free software that -everyone can redistribute and change. You can do so by permitting -redistribution under these terms (or, alternatively, under the terms of the -ordinary General Public License). - - To apply these terms, attach the following notices to the library. It is -safest to attach them to the start of each source file to most effectively -convey the exclusion of warranty; and each file should have at least the -"copyright" line and a pointer to where the full notice is found. - - <one line to give the library's name and a brief idea of what it does.> - Copyright (C) <year> <name of author> - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Lesser General Public - License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public - License along with this library; if not, write to the Free Software - Foundation, Inc., 51 Franklin Street, 5th Floor, Boston, MA 02110-1301 USA - -Also add information on how to contact you by electronic and paper mail. - -You should also get your employer (if you work as a programmer) or your -school, if any, to sign a "copyright disclaimer" for the library, if -necessary. Here is a sample; alter the names: - - Yoyodyne, Inc., hereby disclaims all copyright interest in the - library `Frob' (a library for tweaking knobs) written by James Random Hacker. - - <signature of Ty Coon>, 1 April 1990 - Ty Coon, President of Vice - -That's all there is to it! + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/license/LICENSE.nghttp2-hpack.txt b/license/LICENSE.nghttp2-hpack.txt new file mode 100644 index 0000000..8020179 --- /dev/null +++ b/license/LICENSE.nghttp2-hpack.txt @@ -0,0 +1,23 @@ +The MIT License + +Copyright (c) 2012, 2014, 2015, 2016 Tatsuhiro Tsujikawa +Copyright (c) 2012, 2014, 2015, 2016 nghttp2 contributors + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/microbench/README.md b/microbench/README.md index b109a14..4505f1d 100644 --- a/microbench/README.md +++ b/microbench/README.md @@ -1,4 +1,4 @@ ## Microbenchmark tests -See [our wiki page](http://netty.io/wiki/microbenchmarks.html). +See [our wiki page](https://netty.io/wiki/microbenchmarks.html). diff --git a/microbench/pom.xml b/microbench/pom.xml index f2ce65a..acc7472 100644 --- a/microbench/pom.xml +++ b/microbench/pom.xml @@ -20,7 +20,7 @@ <parent> <groupId>io.netty</groupId> <artifactId>netty-parent</artifactId> - <version>4.1.33.Final</version> + <version>4.1.48.Final</version> </parent> <artifactId>netty-microbench</artifactId> @@ -31,15 +31,15 @@ <properties> <!-- Skip tests by default; run only if -DskipTests=false is specified --> <skipTests>true</skipTests> - <jmh.version>1.21</jmh.version> + <jmh.version>1.22</jmh.version> <!-- This only be set when run on linux as on other platforms we just want to include the jar without native code --> <epoll.classifier /> <!-- This only be set when run on mac as on other platforms we just want to include the jar without native code --> <kqueue.classifier /> + <skipJapicmp>true</skipJapicmp> </properties> - <profiles> <profile> <id>linux</id> diff --git a/microbench/src/main/java/io/netty/buffer/AbstractReferenceCountedByteBufBenchmark.java b/microbench/src/main/java/io/netty/buffer/AbstractReferenceCountedByteBufBenchmark.java index 985eb70..a9a2677 100644 --- a/microbench/src/main/java/io/netty/buffer/AbstractReferenceCountedByteBufBenchmark.java +++ b/microbench/src/main/java/io/netty/buffer/AbstractReferenceCountedByteBufBenchmark.java @@ -49,7 +49,7 @@ public class AbstractReferenceCountedByteBufBenchmark extends AbstractMicrobench } @Benchmark - @BenchmarkMode(Mode.SampleTime) + @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.NANOSECONDS) public boolean retainReleaseUncontended() { buf.retain(); @@ -58,9 +58,9 @@ public class AbstractReferenceCountedByteBufBenchmark extends AbstractMicrobench } @Benchmark - @BenchmarkMode(Mode.SampleTime) + @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.NANOSECONDS) - @GroupThreads(6) + @GroupThreads(4) public boolean retainReleaseContended() { buf.retain(); Blackhole.consumeCPU(delay); diff --git a/microbench/src/main/java/io/netty/buffer/ByteBufAccessBenchmark.java b/microbench/src/main/java/io/netty/buffer/ByteBufAccessBenchmark.java new file mode 100644 index 0000000..1271895 --- /dev/null +++ b/microbench/src/main/java/io/netty/buffer/ByteBufAccessBenchmark.java @@ -0,0 +1,165 @@ +/* +* Copyright 2019 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.buffer; + +import java.nio.ByteBuffer; +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.TearDown; +import org.openjdk.jmh.annotations.Warmup; + +import io.netty.microbench.util.AbstractMicrobenchmark; +import io.netty.util.internal.PlatformDependent; + +@Warmup(iterations = 5, time = 1500, timeUnit = TimeUnit.MILLISECONDS) +@Measurement(iterations = 10, time = 1500, timeUnit = TimeUnit.MILLISECONDS) +@Fork(3) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +public class ByteBufAccessBenchmark extends AbstractMicrobenchmark { + + static final class NioFacade extends WrappedByteBuf { + private final ByteBuffer byteBuffer; + NioFacade(ByteBuffer byteBuffer) { + super(Unpooled.EMPTY_BUFFER); + this.byteBuffer = byteBuffer; + } + @Override + public ByteBuf setLong(int index, long value) { + byteBuffer.putLong(index, value); + return this; + } + @Override + public long getLong(int index) { + return byteBuffer.getLong(index); + } + @Override + public byte readByte() { + return byteBuffer.get(); + } + @Override + public ByteBuf touch() { + // hack since WrappedByteBuf.readerIndex(int) is final + byteBuffer.position(0); + return this; + } + @Override + public boolean release() { + PlatformDependent.freeDirectBuffer(byteBuffer); + return true; + } + } + + public enum ByteBufType { + UNSAFE { + @Override + ByteBuf newBuffer() { + return new UnpooledUnsafeDirectByteBuf( + UnpooledByteBufAllocator.DEFAULT, 64, 64).setIndex(0, 64); + } + }, + UNSAFE_SLICE { + @Override + ByteBuf newBuffer() { + return UNSAFE.newBuffer().slice(16, 48); + } + }, + HEAP { + @Override + ByteBuf newBuffer() { + return new UnpooledUnsafeHeapByteBuf( + UnpooledByteBufAllocator.DEFAULT, 64, 64).setIndex(0, 64); + } + }, + COMPOSITE { + @Override + ByteBuf newBuffer() { + return Unpooled.wrappedBuffer(UNSAFE.newBuffer(), HEAP.newBuffer()); + } + }, + NIO { + @Override + ByteBuf newBuffer() { + return new NioFacade(ByteBuffer.allocateDirect(64)); + } + }; + abstract ByteBuf newBuffer(); + } + + @Param + public ByteBufType bufferType; + + @Param({ "true", "false" }) + public String checkAccessible; + + @Param({ "true", "false" }) + public String checkBounds; + + @Param({ "8" }) + public int batchSize; // applies only to readBatch benchmark + + @Setup + public void setup() { + System.setProperty("io.netty.buffer.checkAccessible", checkAccessible); + System.setProperty("io.netty.buffer.checkBounds", checkBounds); + buffer = bufferType.newBuffer(); + } + + private ByteBuf buffer; + + @TearDown + public void tearDown() { + buffer.release(); + System.clearProperty("io.netty.buffer.checkAccessible"); + System.clearProperty("io.netty.buffer.checkBounds"); + } + + @Benchmark + public long setGetLong() { + return buffer.setLong(0, 1).getLong(0); + } + + @Benchmark + public ByteBuf setLong() { + return buffer.setLong(0, 1); + } + + @Benchmark + public int readBatch() { + buffer.readerIndex(0).touch(); + int result = 0; + // WARNING! + // Please do not replace this sum loop with a BlackHole::consume loop: + // BlackHole::consume could prevent the JVM to perform certain optimizations + // forcing ByteBuf::readByte to be executed in order. + // The purpose of the benchmark is to mimic accesses on ByteBuf + // as in a real (single-threaded) case ie without (compiler) memory barriers that would + // disable certain optimizations or would make bounds checks (if enabled) + // to happen on each access. + for (int i = 0, size = batchSize; i < size; i++) { + result += buffer.readByte(); + } + return result; + } +} diff --git a/microbench/src/main/java/io/netty/handler/codec/DateFormatter2Benchmark.java b/microbench/src/main/java/io/netty/handler/codec/DateFormatter2Benchmark.java new file mode 100644 index 0000000..dd44ed6 --- /dev/null +++ b/microbench/src/main/java/io/netty/handler/codec/DateFormatter2Benchmark.java @@ -0,0 +1,95 @@ +/* + * Copyright 2019 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.codec; + +import io.netty.microbench.util.AbstractMicrobenchmark; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; + +import java.util.Date; + +@Threads(1) +@Warmup(iterations = 5) +@Measurement(iterations = 5) +public class DateFormatter2Benchmark extends AbstractMicrobenchmark { + + @Param({"Sun, 27 Jan 2016 19:18:46 GMT", "Sun, 27 Dec 2016 19:18:46 GMT"}) + String DATE_STRING; + + @Benchmark + public Date parseHttpHeaderDateFormatterNew() { + return DateFormatter.parseHttpDate(DATE_STRING); + } + + /* + @Benchmark + public Date parseHttpHeaderDateFormatter() { + return DateFormatterOld.parseHttpDate(DATE_STRING); + } + */ + + /* + * Benchmark (DATE_STRING) Mode Cnt Score Error Units + * parseHttpHeaderDateFormatter Sun, 27 Jan 2016 19:18:46 GMT thrpt 6 4142781.221 ± 82155.002 ops/s + * parseHttpHeaderDateFormatter Sun, 27 Dec 2016 19:18:46 GMT thrpt 6 3781810.558 ± 38679.061 ops/s + * parseHttpHeaderDateFormatterNew Sun, 27 Jan 2016 19:18:46 GMT thrpt 6 4372569.705 ± 30257.537 ops/s + * parseHttpHeaderDateFormatterNew Sun, 27 Dec 2016 19:18:46 GMT thrpt 6 4339785.100 ± 57542.660 ops/s + */ + + /*Old DateFormatter.tryParseMonth method: + private boolean tryParseMonth(CharSequence txt, int tokenStart, int tokenEnd) { + int len = tokenEnd - tokenStart; + + if (len != 3) { + return false; + } + + if (matchMonth("Jan", txt, tokenStart)) { + month = Calendar.JANUARY; + } else if (matchMonth("Feb", txt, tokenStart)) { + month = Calendar.FEBRUARY; + } else if (matchMonth("Mar", txt, tokenStart)) { + month = Calendar.MARCH; + } else if (matchMonth("Apr", txt, tokenStart)) { + month = Calendar.APRIL; + } else if (matchMonth("May", txt, tokenStart)) { + month = Calendar.MAY; + } else if (matchMonth("Jun", txt, tokenStart)) { + month = Calendar.JUNE; + } else if (matchMonth("Jul", txt, tokenStart)) { + month = Calendar.JULY; + } else if (matchMonth("Aug", txt, tokenStart)) { + month = Calendar.AUGUST; + } else if (matchMonth("Sep", txt, tokenStart)) { + month = Calendar.SEPTEMBER; + } else if (matchMonth("Oct", txt, tokenStart)) { + month = Calendar.OCTOBER; + } else if (matchMonth("Nov", txt, tokenStart)) { + month = Calendar.NOVEMBER; + } else if (matchMonth("Dec", txt, tokenStart)) { + month = Calendar.DECEMBER; + } else { + return false; + } + + return true; + } + */ + +} diff --git a/microbench/src/main/java/io/netty/handler/codec/http/DecodeHexBenchmark.java b/microbench/src/main/java/io/netty/handler/codec/http/DecodeHexBenchmark.java new file mode 100644 index 0000000..61afbac --- /dev/null +++ b/microbench/src/main/java/io/netty/handler/codec/http/DecodeHexBenchmark.java @@ -0,0 +1,136 @@ +/* + * Copyright 2019 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.codec.http; + +import io.netty.microbench.util.AbstractMicrobenchmark; +import io.netty.util.internal.PlatformDependent; +import io.netty.util.internal.StringUtil; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; + +import java.util.Arrays; +import java.util.concurrent.TimeUnit; + +@State(Scope.Benchmark) +@Warmup(iterations = 5, time = 1) +@Measurement(iterations = 5, time = 1) +@OutputTimeUnit(TimeUnit.MICROSECONDS) +public class DecodeHexBenchmark extends AbstractMicrobenchmark { + + @Param({ + //with HEX chars + "135aBa9BBCEA030b947d79fCcaf48Bde", + //with HEX chars + 'g' + "4DDeA5gDD1C6fE567E1b6gf0C40FEcDg", + }) + private String hex; + private char[] hexDigits; + + @Setup + public void init() { + hexDigits = hex.toCharArray(); + } + + @Benchmark + public long hexDigits() { + long v = 0; + final char[] hexDigits = this.hexDigits; + for (int i = 0, size = hexDigits.length; i < size; i++) { + v += StringUtil.decodeHexNibble(hexDigits[i]); + } + return v; + } + + @Benchmark + public long hexDigitsWithChecks() { + long v = 0; + final char[] hexDigits = this.hexDigits; + for (int i = 0, size = hexDigits.length; i < size; i++) { + v += decodeHexNibbleWithCheck(hexDigits[i]); + } + return v; + } + + @Benchmark + public long hexDigitsOriginal() { + long v = 0; + final char[] hexDigits = this.hexDigits; + for (int i = 0, size = hexDigits.length; i < size; i++) { + v += decodeHexNibble(hexDigits[i]); + } + return v; + } + + private static int decodeHexNibble(final char c) { + if (c >= '0' && c <= '9') { + return c - '0'; + } + if (c >= 'A' && c <= 'F') { + return c - ('A' - 0xA); + } + if (c >= 'a' && c <= 'f') { + return c - ('a' - 0xA); + } + return -1; + } + + private static final byte[] HEX2B; + + static { + HEX2B = new byte['f' + 1]; + Arrays.fill(HEX2B, (byte) -1); + HEX2B['0'] = (byte) 0; + HEX2B['1'] = (byte) 1; + HEX2B['2'] = (byte) 2; + HEX2B['3'] = (byte) 3; + HEX2B['4'] = (byte) 4; + HEX2B['5'] = (byte) 5; + HEX2B['6'] = (byte) 6; + HEX2B['7'] = (byte) 7; + HEX2B['8'] = (byte) 8; + HEX2B['9'] = (byte) 9; + HEX2B['A'] = (byte) 10; + HEX2B['B'] = (byte) 11; + HEX2B['C'] = (byte) 12; + HEX2B['D'] = (byte) 13; + HEX2B['E'] = (byte) 14; + HEX2B['F'] = (byte) 15; + HEX2B['a'] = (byte) 10; + HEX2B['b'] = (byte) 11; + HEX2B['c'] = (byte) 12; + HEX2B['d'] = (byte) 13; + HEX2B['e'] = (byte) 14; + HEX2B['f'] = (byte) 15; + } + + private static int decodeHexNibbleWithCheck(final char c) { + final int index = c; + if (index >= HEX2B.length) { + return -1; + } + if (PlatformDependent.hasUnsafe()) { + return PlatformDependent.getByte(HEX2B, index); + } + return HEX2B[index]; + } + +} diff --git a/microbench/src/main/java/io/netty/handler/codec/http/HttpMethodMapBenchmark.java b/microbench/src/main/java/io/netty/handler/codec/http/HttpMethodMapBenchmark.java index 69dae0f..08bd84e 100644 --- a/microbench/src/main/java/io/netty/handler/codec/http/HttpMethodMapBenchmark.java +++ b/microbench/src/main/java/io/netty/handler/codec/http/HttpMethodMapBenchmark.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 The Netty Project + * Copyright 2019 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 @@ -22,6 +22,7 @@ import org.openjdk.jmh.annotations.OutputTimeUnit; import org.openjdk.jmh.annotations.Scope; import org.openjdk.jmh.annotations.State; import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; import java.util.HashMap; import java.util.Map; @@ -151,68 +152,56 @@ public class HttpMethodMapBenchmark extends AbstractMicrobenchmark { } @Benchmark - public int oldMapKnownMethods() throws Exception { - int x = 0; + public void oldMapKnownMethods(Blackhole bh) throws Exception { for (int i = 0; i < KNOWN_METHODS.length; ++i) { - x += OLD_MAP.get(KNOWN_METHODS[i]).toString().length(); + bh.consume(OLD_MAP.get(KNOWN_METHODS[i])); } - return x; } @Benchmark - public int newMapKnownMethods() throws Exception { - int x = 0; + public void newMapKnownMethods(Blackhole bh) throws Exception { for (int i = 0; i < KNOWN_METHODS.length; ++i) { - x += NEW_MAP.get(KNOWN_METHODS[i]).toString().length(); + bh.consume(NEW_MAP.get(KNOWN_METHODS[i])); } - return x; } @Benchmark - public int oldMapMixMethods() throws Exception { - int x = 0; + public void oldMapMixMethods(Blackhole bh) throws Exception { for (int i = 0; i < MIXED_METHODS.length; ++i) { HttpMethod method = OLD_MAP.get(MIXED_METHODS[i]); if (method != null) { - x += method.toString().length(); + bh.consume(method); } } - return x; } @Benchmark - public int newMapMixMethods() throws Exception { - int x = 0; + public void newMapMixMethods(Blackhole bh) throws Exception { for (int i = 0; i < MIXED_METHODS.length; ++i) { HttpMethod method = NEW_MAP.get(MIXED_METHODS[i]); if (method != null) { - x += method.toString().length(); + bh.consume(method); } } - return x; } @Benchmark - public int oldMapUnknownMethods() throws Exception { - int x = 0; + public void oldMapUnknownMethods(Blackhole bh) throws Exception { for (int i = 0; i < UNKNOWN_METHODS.length; ++i) { HttpMethod method = OLD_MAP.get(UNKNOWN_METHODS[i]); if (method != null) { - x += method.toString().length(); + bh.consume(method); } } - return x; } @Benchmark - public int newMapUnknownMethods() throws Exception { - int x = 0; + public void newMapUnknownMethods(Blackhole bh) throws Exception { for (int i = 0; i < UNKNOWN_METHODS.length; ++i) { HttpMethod method = NEW_MAP.get(UNKNOWN_METHODS[i]); if (method != null) { - x += method.toString().length(); + bh.consume(method); } } - return x; } } diff --git a/microbench/src/main/java/io/netty/handler/codec/http/QueryStringDecoderBenchmark.java b/microbench/src/main/java/io/netty/handler/codec/http/QueryStringDecoderBenchmark.java new file mode 100644 index 0000000..efd1dae --- /dev/null +++ b/microbench/src/main/java/io/netty/handler/codec/http/QueryStringDecoderBenchmark.java @@ -0,0 +1,66 @@ +/* + * Copyright 2019 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.codec.http; + +import io.netty.microbench.util.AbstractMicrobenchmark; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; + +import java.nio.charset.Charset; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +@Threads(1) +@Warmup(iterations = 3) +@Measurement(iterations = 3) +@OutputTimeUnit(TimeUnit.MICROSECONDS) +public class QueryStringDecoderBenchmark extends AbstractMicrobenchmark { + + private static final Charset SHIFT_JIS = Charset.forName("Shift-JIS"); + + @Benchmark + public Map<String, List<String>> noDecoding() { + return new QueryStringDecoder("foo=bar&cat=dog", false).parameters(); + } + + @Benchmark + public Map<String, List<String>> onlyDecoding() { + // ã»ã’=ã¼ã‘&ãã“=ã„㬠+ return new QueryStringDecoder("%E3%81%BB%E3%81%92=%E3%81%BC%E3%81%91&%E3%81%AD%E3%81%93=%E3%81%84%E3%81%AC", + false) + .parameters(); + } + + @Benchmark + public Map<String, List<String>> mixedDecoding() { + // foo=bar&ã»ã’=ã¼ã‘&cat=dog&ãã“=ã„㬠+ return new QueryStringDecoder("foo=bar%E3%81%BB%E3%81%92=%E3%81%BC%E3%81%91&cat=dog&" + + "&%E3%81%AD%E3%81%93=%E3%81%84%E3%81%AC", false) + .parameters(); + } + + @Benchmark + public Map<String, List<String>> nonStandardDecoding() { + // ã»ã’=ã¼ã‘&ãã“=ã„㬠in Shift-JIS + return new QueryStringDecoder("%82%D9%82%B0=%82%DA%82%AF&%82%CB%82%B1=%82%A2%82%CA", + SHIFT_JIS, false) + .parameters(); + } +} diff --git a/microbench/src/main/java/io/netty/handler/codec/http/QueryStringEncoderBenchmark.java b/microbench/src/main/java/io/netty/handler/codec/http/QueryStringEncoderBenchmark.java new file mode 100644 index 0000000..88013eb --- /dev/null +++ b/microbench/src/main/java/io/netty/handler/codec/http/QueryStringEncoderBenchmark.java @@ -0,0 +1,95 @@ +/* + * Copyright 2019 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.codec.http; + +import io.netty.microbench.util.AbstractMicrobenchmark; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; + +import java.util.concurrent.TimeUnit; + +@Threads(1) +@Warmup(iterations = 3) +@Measurement(iterations = 3) +@OutputTimeUnit(TimeUnit.MICROSECONDS) +public class QueryStringEncoderBenchmark extends AbstractMicrobenchmark { + private String shortAscii; + private String shortUtf8; + private String shortAsciiFirst; + + private String longAscii; + private String longUtf8; + private String longAsciiFirst; + + @Setup + public void setUp() { + // Avoid constant pool for strings since it's common for at least values to not be constant. + shortAscii = new String("foo".toCharArray()); + shortUtf8 = new String("ã»ã’ã»ã’".toCharArray()); + shortAsciiFirst = shortAscii + shortUtf8; + longAscii = repeat(shortAscii, 100); + longUtf8 = repeat(shortUtf8, 100); + longAsciiFirst = longAscii + longUtf8; + } + + @Benchmark + public String shortAscii() { + return encode(shortAscii); + } + + @Benchmark + public String shortUtf8() { + return encode(shortUtf8); + } + + @Benchmark + public String shortAsciiFirst() { + return encode(shortAsciiFirst); + } + + @Benchmark + public String longAscii() { + return encode(longAscii); + } + + @Benchmark + public String longUtf8() { + return encode(longUtf8); + } + + @Benchmark + public String longAsciiFirst() { + return encode(longAsciiFirst); + } + + private static String encode(String s) { + QueryStringEncoder encoder = new QueryStringEncoder(""); + encoder.addParam(s, s); + return encoder.toString(); + } + + private static String repeat(String s, int num) { + StringBuilder sb = new StringBuilder(num * s.length()); + for (int i = 0; i < num; i++) { + sb.append(s); + } + return sb.toString(); + } +} diff --git a/microbench/src/main/java/io/netty/handler/codec/http2/HpackBenchmarkUtil.java b/microbench/src/main/java/io/netty/handler/codec/http2/HpackBenchmarkUtil.java index c94bc59..e8a3a56 100644 --- a/microbench/src/main/java/io/netty/handler/codec/http2/HpackBenchmarkUtil.java +++ b/microbench/src/main/java/io/netty/handler/codec/http2/HpackBenchmarkUtil.java @@ -49,7 +49,7 @@ public final class HpackBenchmarkUtil { final HpackHeadersSize size; final boolean limitToAscii; - public HeadersKey(HpackHeadersSize size, boolean limitToAscii) { + HeadersKey(HpackHeadersSize size, boolean limitToAscii) { this.size = size; this.limitToAscii = limitToAscii; } diff --git a/microbench/src/main/java/io/netty/handler/codec/http2/HpackDecoderBenchmark.java b/microbench/src/main/java/io/netty/handler/codec/http2/HpackDecoderBenchmark.java index 3ecf1bc..8a82414 100644 --- a/microbench/src/main/java/io/netty/handler/codec/http2/HpackDecoderBenchmark.java +++ b/microbench/src/main/java/io/netty/handler/codec/http2/HpackDecoderBenchmark.java @@ -72,7 +72,7 @@ public class HpackDecoderBenchmark extends AbstractMicrobenchmark { @Benchmark @BenchmarkMode(Mode.Throughput) public void decode(final Blackhole bh) throws Http2Exception { - HpackDecoder hpackDecoder = new HpackDecoder(DEFAULT_HEADER_LIST_SIZE, 32); + HpackDecoder hpackDecoder = new HpackDecoder(Integer.MAX_VALUE); @SuppressWarnings("unchecked") Http2Headers headers = new DefaultHttp2Headers() { diff --git a/microbench/src/main/java/io/netty/handler/codec/http2/HpackHeader.java b/microbench/src/main/java/io/netty/handler/codec/http2/HpackHeader.java index c0b6359..482db41 100644 --- a/microbench/src/main/java/io/netty/handler/codec/http2/HpackHeader.java +++ b/microbench/src/main/java/io/netty/handler/codec/http2/HpackHeader.java @@ -40,14 +40,14 @@ import java.util.Random; /** * Helper class representing a single header entry. Used by the benchmarks. */ -class HpackHeader { +final class HpackHeader { private static final String ALPHABET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"; final CharSequence name; final CharSequence value; - HpackHeader(byte[] name, byte[] value) { + private HpackHeader(byte[] name, byte[] value) { this.name = new AsciiString(name, false); this.value = new AsciiString(value, false); } @@ -59,7 +59,8 @@ class HpackHeader { boolean limitToAscii) { List<HpackHeader> hpackHeaders = new ArrayList<HpackHeader>(numHeaders); for (int i = 0; i < numHeaders; ++i) { - byte[] name = randomBytes(new byte[nameLength], limitToAscii); + // Force always ascii for header names + byte[] name = randomBytes(new byte[nameLength], true); byte[] value = randomBytes(new byte[valueLength], limitToAscii); hpackHeaders.add(new HpackHeader(name, value)); } diff --git a/microbench/src/main/java/io/netty/microbench/channel/DefaultChannelPipelineBenchmark.java b/microbench/src/main/java/io/netty/microbench/channel/DefaultChannelPipelineBenchmark.java new file mode 100644 index 0000000..c8a2409 --- /dev/null +++ b/microbench/src/main/java/io/netty/microbench/channel/DefaultChannelPipelineBenchmark.java @@ -0,0 +1,84 @@ +/* + * Copyright 2019 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.microbench.channel; + +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.microbench.util.AbstractMicrobenchmark; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; + +@Warmup(iterations = 5) +@Measurement(iterations = 5) +@State(Scope.Benchmark) +public class DefaultChannelPipelineBenchmark extends AbstractMicrobenchmark { + + private static final ChannelHandler NOOP_HANDLER = new ChannelInboundHandlerAdapter() { + @Override + public boolean isSharable() { + return true; + } + }; + + private static final ChannelHandler CONSUMING_HANDLER = new ChannelInboundHandlerAdapter() { + @Override + public void channelReadComplete(ChannelHandlerContext ctx) { + // NOOP + } + + @Override + public boolean isSharable() { + return true; + } + }; + + @Param({ "4" }) + public int extraHandlers; + + private ChannelPipeline pipeline; + + @Setup(Level.Iteration) + public void setup() { + pipeline = new EmbeddedChannel().pipeline(); + for (int i = 0; i < extraHandlers; i++) { + pipeline.addLast(NOOP_HANDLER); + } + pipeline.addLast(CONSUMING_HANDLER); + } + + @TearDown + public void tearDown() { + pipeline.channel().close(); + } + + @Benchmark + public void propagateEvent(Blackhole hole) { + for (int i = 0; i < 100; i++) { + hole.consume(pipeline.fireChannelReadComplete()); + } + } +} diff --git a/microbench/src/main/java/io/netty/microbench/channel/EmbeddedChannelHandlerContext.java b/microbench/src/main/java/io/netty/microbench/channel/EmbeddedChannelHandlerContext.java index 059458a..c724ca3 100644 --- a/microbench/src/main/java/io/netty/microbench/channel/EmbeddedChannelHandlerContext.java +++ b/microbench/src/main/java/io/netty/microbench/channel/EmbeddedChannelHandlerContext.java @@ -45,7 +45,7 @@ public abstract class EmbeddedChannelHandlerContext implements ChannelHandlerCon this.alloc = checkNotNull(alloc, "alloc"); this.channel = checkNotNull(channel, "channel"); this.handler = checkNotNull(handler, "handler"); - eventLoop = checkNotNull(channel.eventLoop(), "eventLoop"); + this.eventLoop = checkNotNull(channel.eventLoop(), "eventLoop"); } protected abstract void handleException(Throwable t); diff --git a/microbench/src/main/java/io/netty/microbench/channel/EmbeddedChannelWriteReleaseHandlerContext.java b/microbench/src/main/java/io/netty/microbench/channel/EmbeddedChannelWriteReleaseHandlerContext.java index b50c53c..6884bc9 100644 --- a/microbench/src/main/java/io/netty/microbench/channel/EmbeddedChannelWriteReleaseHandlerContext.java +++ b/microbench/src/main/java/io/netty/microbench/channel/EmbeddedChannelWriteReleaseHandlerContext.java @@ -31,6 +31,7 @@ public abstract class EmbeddedChannelWriteReleaseHandlerContext extends Embedded super(alloc, handler, channel); } + @Override protected abstract void handleException(Throwable t); @Override diff --git a/microbench/src/main/java/io/netty/microbench/channel/epoll/EpollSocketChannelBenchmark.java b/microbench/src/main/java/io/netty/microbench/channel/epoll/EpollSocketChannelBenchmark.java index 5ecd186..f4708bd 100644 --- a/microbench/src/main/java/io/netty/microbench/channel/epoll/EpollSocketChannelBenchmark.java +++ b/microbench/src/main/java/io/netty/microbench/channel/epoll/EpollSocketChannelBenchmark.java @@ -30,10 +30,15 @@ import io.netty.microbench.util.AbstractMicrobenchmark; import io.netty.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.GroupThreads; import org.openjdk.jmh.annotations.Setup; import org.openjdk.jmh.annotations.TearDown; public class EpollSocketChannelBenchmark extends AbstractMicrobenchmark { + private static final Runnable runnable = new Runnable() { + @Override + public void run() { } + }; private EpollEventLoopGroup group; private Channel serverChan; @@ -136,4 +141,15 @@ public class EpollSocketChannelBenchmark extends AbstractMicrobenchmark { public Object pingPong() throws Exception { return chan.pipeline().writeAndFlush(abyte.retainedSlice()).sync(); } + + @Benchmark + public Object executeSingle() throws Exception { + return chan.eventLoop().submit(runnable).get(); + } + + @Benchmark + @GroupThreads(3) + public Object executeMulti() throws Exception { + return chan.eventLoop().submit(runnable).get(); + } } diff --git a/microbench/src/main/java/io/netty/microbench/concurrent/BurstCostExecutorsBenchmark.java b/microbench/src/main/java/io/netty/microbench/concurrent/BurstCostExecutorsBenchmark.java index 11164d2..acab0f0 100644 --- a/microbench/src/main/java/io/netty/microbench/concurrent/BurstCostExecutorsBenchmark.java +++ b/microbench/src/main/java/io/netty/microbench/concurrent/BurstCostExecutorsBenchmark.java @@ -67,7 +67,7 @@ public class BurstCostExecutorsBenchmark extends AbstractMicrobenchmark { private final AtomicBoolean poisoned = new AtomicBoolean(); private final Thread executorThread; - public SpinExecutorService(int maxTasks) { + SpinExecutorService(int maxTasks) { tasks = PlatformDependent.newFixedMpscQueue(maxTasks); executorThread = new Thread(new Runnable() { @Override diff --git a/microbench/src/main/java/io/netty/microbench/concurrent/FastThreadLocalFastPathBenchmark.java b/microbench/src/main/java/io/netty/microbench/concurrent/FastThreadLocalFastPathBenchmark.java index c0073fb..c371ad1 100644 --- a/microbench/src/main/java/io/netty/microbench/concurrent/FastThreadLocalFastPathBenchmark.java +++ b/microbench/src/main/java/io/netty/microbench/concurrent/FastThreadLocalFastPathBenchmark.java @@ -20,6 +20,7 @@ import io.netty.util.concurrent.FastThreadLocal; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.Measurement; import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.infra.Blackhole; import java.util.Random; @@ -56,20 +57,16 @@ public class FastThreadLocalFastPathBenchmark extends AbstractMicrobenchmark { } @Benchmark - public int jdkThreadLocalGet() { - int result = 0; + public void jdkThreadLocalGet(Blackhole bh) { for (ThreadLocal<Integer> i: jdkThreadLocals) { - result += i.get(); + bh.consume(i.get()); } - return result; } @Benchmark - public int fastThreadLocal() { - int result = 0; + public void fastThreadLocal(Blackhole bh) { for (FastThreadLocal<Integer> i: fastThreadLocals) { - result += i.get(); + bh.consume(i.get()); } - return result; } } diff --git a/microbench/src/main/java/io/netty/microbench/concurrent/FastThreadLocalSlowPathBenchmark.java b/microbench/src/main/java/io/netty/microbench/concurrent/FastThreadLocalSlowPathBenchmark.java index 20d414a..70a19a0 100644 --- a/microbench/src/main/java/io/netty/microbench/concurrent/FastThreadLocalSlowPathBenchmark.java +++ b/microbench/src/main/java/io/netty/microbench/concurrent/FastThreadLocalSlowPathBenchmark.java @@ -20,6 +20,7 @@ import io.netty.util.concurrent.FastThreadLocal; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.Measurement; import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.infra.Blackhole; import java.util.Random; @@ -60,20 +61,16 @@ public class FastThreadLocalSlowPathBenchmark extends AbstractMicrobenchmark { } @Benchmark - public int jdkThreadLocalGet() { - int result = 0; + public void jdkThreadLocalGet(Blackhole bh) { for (ThreadLocal<Integer> i: jdkThreadLocals) { - result += i.get(); + bh.consume(i.get()); } - return result; } @Benchmark - public int fastThreadLocal() { - int result = 0; + public void fastThreadLocal(Blackhole bh) { for (FastThreadLocal<Integer> i: fastThreadLocals) { - result += i.get(); + bh.consume(i.get()); } - return result; } } diff --git a/microbench/src/main/java/io/netty/microbench/headers/ReadOnlyHttp2HeadersBenchmark.java b/microbench/src/main/java/io/netty/microbench/headers/ReadOnlyHttp2HeadersBenchmark.java index 9efc2f3..920da6f 100644 --- a/microbench/src/main/java/io/netty/microbench/headers/ReadOnlyHttp2HeadersBenchmark.java +++ b/microbench/src/main/java/io/netty/microbench/headers/ReadOnlyHttp2HeadersBenchmark.java @@ -35,6 +35,7 @@ import org.openjdk.jmh.annotations.Setup; import org.openjdk.jmh.annotations.State; import org.openjdk.jmh.annotations.Threads; import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; import java.util.Map; import java.util.UUID; @@ -68,23 +69,23 @@ public class ReadOnlyHttp2HeadersBenchmark extends AbstractMicrobenchmark { @Benchmark @BenchmarkMode(Mode.AverageTime) - public int defaultTrailers() { + public void defaultTrailers(Blackhole bh) { Http2Headers headers = new DefaultHttp2Headers(false); for (int i = 0; i < headerCount; ++i) { headers.add(headerNames[i], headerValues[i]); } - return iterate(headers); + iterate(headers, bh); } @Benchmark @BenchmarkMode(Mode.AverageTime) - public int readOnlyTrailers() { - return iterate(ReadOnlyHttp2Headers.trailers(false, buildPairs())); + public void readOnlyTrailers(Blackhole bh) { + iterate(ReadOnlyHttp2Headers.trailers(false, buildPairs()), bh); } @Benchmark @BenchmarkMode(Mode.AverageTime) - public int defaultClientHeaders() { + public void defaultClientHeaders(Blackhole bh) { Http2Headers headers = new DefaultHttp2Headers(false); for (int i = 0; i < headerCount; ++i) { headers.add(headerNames[i], headerValues[i]); @@ -93,39 +94,37 @@ public class ReadOnlyHttp2HeadersBenchmark extends AbstractMicrobenchmark { headers.scheme(HttpScheme.HTTPS.name()); headers.path(path); headers.authority(authority); - return iterate(headers); + iterate(headers, bh); } @Benchmark @BenchmarkMode(Mode.AverageTime) - public int readOnlyClientHeaders() { - return iterate(ReadOnlyHttp2Headers.clientHeaders(false, HttpMethod.POST.asciiName(), path, - HttpScheme.HTTPS.name(), authority, buildPairs())); + public void readOnlyClientHeaders(Blackhole bh) { + iterate(ReadOnlyHttp2Headers.clientHeaders(false, HttpMethod.POST.asciiName(), path, + HttpScheme.HTTPS.name(), authority, buildPairs()), bh); } @Benchmark @BenchmarkMode(Mode.AverageTime) - public int defaultServerHeaders() { + public void defaultServerHeaders(Blackhole bh) { Http2Headers headers = new DefaultHttp2Headers(false); for (int i = 0; i < headerCount; ++i) { headers.add(headerNames[i], headerValues[i]); } headers.status(HttpResponseStatus.OK.codeAsText()); - return iterate(headers); + iterate(headers, bh); } @Benchmark @BenchmarkMode(Mode.AverageTime) - public int readOnlyServerHeaders() { - return iterate(ReadOnlyHttp2Headers.serverHeaders(false, HttpResponseStatus.OK.codeAsText(), buildPairs())); + public void readOnlyServerHeaders(Blackhole bh) { + iterate(ReadOnlyHttp2Headers.serverHeaders(false, HttpResponseStatus.OK.codeAsText(), buildPairs()), bh); } - private static int iterate(Http2Headers headers) { - int length = 0; + private static void iterate(Http2Headers headers, Blackhole bh) { for (Map.Entry<CharSequence, CharSequence> entry : headers) { - length += entry.getKey().length() + entry.getValue().length(); + bh.consume(entry); } - return length; } private AsciiString[] buildPairs() { diff --git a/microbench/src/main/java/io/netty/microbench/http/HttpRequestDecoderBenchmark.java b/microbench/src/main/java/io/netty/microbench/http/HttpRequestDecoderBenchmark.java index 191b992..fa0ba5c 100644 --- a/microbench/src/main/java/io/netty/microbench/http/HttpRequestDecoderBenchmark.java +++ b/microbench/src/main/java/io/netty/microbench/http/HttpRequestDecoderBenchmark.java @@ -98,14 +98,14 @@ public class HttpRequestDecoderBenchmark extends AbstractMicrobenchmark { amount = headerLength - a; } - // if header is done it should produce a HttpRequest - channel.writeInbound(Unpooled.wrappedBuffer(content, a, amount)); + // if header is done it should produce an HttpRequest + channel.writeInbound(Unpooled.wrappedBuffer(content, a, amount).asReadOnly()); a += amount; } for (int i = CONTENT_LENGTH; i > 0; i --) { // Should produce HttpContent - channel.writeInbound(Unpooled.wrappedBuffer(content, content.length - i, 1)); + channel.writeInbound(Unpooled.wrappedBuffer(content, content.length - i, 1).asReadOnly()); } } } diff --git a/microbench/src/main/java/io/netty/microbench/internal/RecyclableArrayListBenchmark.java b/microbench/src/main/java/io/netty/microbench/internal/RecyclableArrayListBenchmark.java index dfb95a6..32fe2bb 100644 --- a/microbench/src/main/java/io/netty/microbench/internal/RecyclableArrayListBenchmark.java +++ b/microbench/src/main/java/io/netty/microbench/internal/RecyclableArrayListBenchmark.java @@ -36,8 +36,8 @@ public class RecyclableArrayListBenchmark extends AbstractMicrobenchmark { public int size; @Benchmark - public void recycleSameThread() { + public boolean recycleSameThread() { RecyclableArrayList list = RecyclableArrayList.newInstance(size); - list.recycle(); + return list.recycle(); } } diff --git a/microbench/src/main/java/io/netty/util/concurrent/ScheduleFutureTaskBenchmark.java b/microbench/src/main/java/io/netty/util/concurrent/ScheduleFutureTaskBenchmark.java new file mode 100644 index 0000000..d5fdd5b --- /dev/null +++ b/microbench/src/main/java/io/netty/util/concurrent/ScheduleFutureTaskBenchmark.java @@ -0,0 +1,109 @@ +/* + * Copyright 2019 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.util.concurrent; + +import java.util.concurrent.Callable; +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; + +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.microbench.util.AbstractMicrobenchmark; + +@Warmup(iterations = 5, time = 3, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 10, time = 3, timeUnit = TimeUnit.SECONDS) +@State(Scope.Benchmark) +public class ScheduleFutureTaskBenchmark extends AbstractMicrobenchmark { + + static final Callable<Void> NO_OP = new Callable<Void>() { + @Override + public Void call() throws Exception { + return null; + } + }; + + @State(Scope.Thread) + public static class ThreadState { + + @Param({ "100000" }) + int num; + + AbstractScheduledEventExecutor eventLoop; + + @Setup(Level.Trial) + public void reset() { + eventLoop = (AbstractScheduledEventExecutor) new NioEventLoopGroup(1).next(); + } + + @Setup(Level.Invocation) + public void clear() { + eventLoop.submit(new Runnable() { + @Override + public void run() { + eventLoop.cancelScheduledTasks(); + } + }).awaitUninterruptibly(); + } + + @TearDown(Level.Trial) + public void shutdown() { + clear(); + eventLoop.parent().shutdownGracefully().awaitUninterruptibly(); + } + } + + @Benchmark + @Threads(3) + public Future<?> scheduleLots(final ThreadState threadState) { + return threadState.eventLoop.submit(new Runnable() { + @Override + public void run() { + for (int i = 1; i <= threadState.num; i++) { + threadState.eventLoop.schedule(NO_OP, i, TimeUnit.HOURS); + } + } + }).syncUninterruptibly(); + } + + @Benchmark + @Threads(1) + public Future<?> scheduleLotsOutsideLoop(final ThreadState threadState) { + final AbstractScheduledEventExecutor eventLoop = threadState.eventLoop; + for (int i = 1; i <= threadState.num; i++) { + eventLoop.schedule(NO_OP, i, TimeUnit.HOURS); + } + return null; + } + + @Benchmark + @Threads(1) + public Future<?> scheduleCancelLotsOutsideLoop(final ThreadState threadState) { + final AbstractScheduledEventExecutor eventLoop = threadState.eventLoop; + for (int i = 1; i <= threadState.num; i++) { + eventLoop.schedule(NO_OP, i, TimeUnit.HOURS).cancel(false); + } + return null; + } +} diff --git a/microbench/src/main/java/io/netty/util/concurrent/package-info.java b/microbench/src/main/java/io/netty/util/concurrent/package-info.java new file mode 100644 index 0000000..5059d12 --- /dev/null +++ b/microbench/src/main/java/io/netty/util/concurrent/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2019 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. + */ +/** + * Benchmarks for {@link io.netty.util.concurrent}. + */ +package io.netty.util.concurrent; diff --git a/pom.xml b/pom.xml index 60aa19d..96016e1 100644 --- a/pom.xml +++ b/pom.xml @@ -26,10 +26,10 @@ <groupId>io.netty</groupId> <artifactId>netty-parent</artifactId> <packaging>pom</packaging> - <version>4.1.33.Final</version> + <version>4.1.48.Final</version> <name>Netty</name> - <url>http://netty.io/</url> + <url>https://netty.io/</url> <description> Netty is an asynchronous event-driven network application framework for rapid development of maintainable high performance protocol servers and @@ -38,7 +38,7 @@ <organization> <name>The Netty Project</name> - <url>http://netty.io/</url> + <url>https://netty.io/</url> </organization> <licenses> @@ -53,7 +53,7 @@ <url>https://github.com/netty/netty</url> <connection>scm:git:git://github.com/netty/netty.git</connection> <developerConnection>scm:git:ssh://git@github.com/netty/netty.git</developerConnection> - <tag>netty-4.1.33.Final</tag> + <tag>netty-4.1.48.Final</tag> </scm> <developers> @@ -61,13 +61,42 @@ <id>netty.io</id> <name>The Netty Project Contributors</name> <email>netty@googlegroups.com</email> - <url>http://netty.io/</url> + <url>https://netty.io/</url> <organization>The Netty Project</organization> - <organizationUrl>http://netty.io/</organizationUrl> + <organizationUrl>https://netty.io/</organizationUrl> </developer> </developers> <profiles> + <profile> + <id>not_x86_64</id> + <activation> + <os> + <arch>!x86_64</arch> + </os> + </activation> + <properties> + <!-- Use no classifier as we only support x86_64 atm--> + <tcnative.classifier /> + <skipShadingTestsuite>true</skipShadingTestsuite> + </properties> + </profile> + + <!-- Detect if we use GraalVM and if so enable the native image testsuite --> + <profile> + <id>graal</id> + <activation> + <file> + <!-- GraalVM Component Updater should exists when using GraalVM--> + <exists>${java.home}/bin/gu</exists> + </file> + </activation> + <properties> + <skipNativeImageTestsuite>false</skipNativeImageTestsuite> + <forbiddenapis.skip>true</forbiddenapis.skip> + <testJvm /> + </properties> + </profile> <!-- JDK13 --> <profile> <id>java13</id> @@ -97,6 +126,9 @@ <jdk>12</jdk> </activation> <properties> + <argLine.java9.extras /> + <!-- Export some stuff which is used during our tests --> + <argLine.java9>--illegal-access=deny ${argLine.java9.extras}</argLine.java9> <!-- Not use alpn agent as Java11 supports alpn out of the box --> <argLine.alpnAgent /> <forbiddenapis.skip>true</forbiddenapis.skip> @@ -119,6 +151,9 @@ <jdk>11</jdk> </activation> <properties> + <argLine.java9.extras /> + <!-- Export some stuff which is used during our tests --> + <argLine.java9>--illegal-access=deny ${argLine.java9.extras}</argLine.java9> <!-- Not use alpn agent as Java11 supports alpn out of the box --> <argLine.alpnAgent /> <forbiddenapis.skip>true</forbiddenapis.skip> @@ -138,6 +173,9 @@ <jdk>10</jdk> </activation> <properties> + <argLine.java9.extras /> + <!-- Export some stuff which is used during our tests --> + <argLine.java9>--illegal-access=deny --add-modules java.xml.bind ${argLine.java9.extras}</argLine.java9> <!-- Not use alpn agent as Java10 supports alpn out of the box --> <argLine.alpnAgent /> <forbiddenapis.skip>true</forbiddenapis.skip> @@ -154,10 +192,10 @@ <properties> <argLine.java9.extras /> <!-- Export some stuff which is used during our tests --> - <argLine.java9>--add-modules java.xml.bind ${argLine.java9.extras}</argLine.java9> + <argLine.java9>--illegal-access=deny --add-modules java.xml.bind ${argLine.java9.extras}</argLine.java9> <!-- Not use alpn agent as Java9 supports alpn out of the box --> <argLine.alpnAgent /> - <!-- Skip as maven plugin not works with Java9 yet --> + <!-- Skip as maven plugin not works with Java9 yet --> <forbiddenapis.skip>true</forbiddenapis.skip> <!-- Needed because of https://issues.apache.org/jira/browse/MENFORCER-275 --> <enforcer.plugin.version>3.0.0-M1</enforcer.plugin.version> @@ -177,7 +215,7 @@ <profile> <id>leak</id> <properties> - <argLine.leak>-Dio.netty.leakDetectionLevel=paranoid -Dio.netty.leakDetection.maxRecords=32</argLine.leak> + <argLine.leak>-Dio.netty.leakDetectionLevel=paranoid -Dio.netty.leakDetection.targetRecords=32</argLine.leak> </properties> </profile> <profile> @@ -253,7 +291,7 @@ <netty.dev.tools.directory>${project.build.directory}/dev-tools</netty.dev.tools.directory> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> - <netty.build.version>22</netty.build.version> + <netty.build.version>26</netty.build.version> <jboss.marshalling.version>1.4.11.Final</jboss.marshalling.version> <jetty.alpnAgent.version>2.0.8</jetty.alpnAgent.version> <jetty.alpnAgent.path>"${settings.localRepository}"/org/mortbay/jetty/alpn/jetty-alpn-agent/${jetty.alpnAgent.version}/jetty-alpn-agent-${jetty.alpnAgent.version}.jar</jetty.alpnAgent.path> @@ -272,9 +310,11 @@ <argLine.javaProperties>-D_</argLine.javaProperties> <!-- Configure the os-maven-plugin extension to expand the classifier on --> <!-- Fedora-"like" systems. This is currently only used for the netty-tcnative dependency --> - <os.detection.classifierWithLikes>fedora</os.detection.classifierWithLikes> + <osmaven.version>1.6.2</osmaven.version> + <!-- keep in sync with PlatformDependent#ALLOWED_LINUX_OS_CLASSIFIERS --> + <os.detection.classifierWithLikes>fedora,suse,arch</os.detection.classifierWithLikes> <tcnative.artifactId>netty-tcnative</tcnative.artifactId> - <tcnative.version>2.0.20.Final</tcnative.version> + <tcnative.version>2.0.29.Final</tcnative.version> <tcnative.classifier>${os.detected.classifier}</tcnative.classifier> <conscrypt.groupId>org.conscrypt</conscrypt.groupId> <conscrypt.artifactId>conscrypt-openjdk-uber</conscrypt.artifactId> @@ -285,10 +325,16 @@ <logging.logLevel>debug</logging.logLevel> <log4j2.version>2.6.2</log4j2.version> <enforcer.plugin.version>1.4.1</enforcer.plugin.version> - <testJavaHome>${env.JAVA_HOME}</testJavaHome> + <testJavaHome>${java.home}</testJavaHome> + <testJvm>${testJavaHome}/bin/java</testJvm> <skipOsgiTestsuite>false</skipOsgiTestsuite> <skipAutobahnTestsuite>false</skipAutobahnTestsuite> <skipHttp2Testsuite>false</skipHttp2Testsuite> + <skipJapicmp>false</skipJapicmp> + <graalvm.version>19.0.0</graalvm.version> + <!-- By default skip native testsuite as it requires a custom environment with graalvm installed --> + <skipNativeImageTestsuite>true</skipNativeImageTestsuite> + <skipShadingTestsuite>false</skipShadingTestsuite> </properties> <modules> @@ -310,6 +356,7 @@ <module>codec-xml</module> <module>resolver</module> <module>resolver-dns</module> + <module>resolver-dns-native-macos</module> <module>tarball</module> <module>transport</module> <module>transport-native-unix-common-tests</module> @@ -327,6 +374,8 @@ <module>testsuite-http2</module> <module>testsuite-osgi</module> <module>testsuite-shading</module> + <module>testsuite-native-image</module> + <module>transport-blockhound-tests</module> <module>microbench</module> <module>bom</module> </modules> @@ -423,7 +472,7 @@ <optional>true</optional> </dependency> - <!-- + <!-- Completely optional and only needed for OCSP stapling to construct and parse OCSP requests and responses. --> @@ -466,7 +515,7 @@ <dependency> <groupId>org.jctools</groupId> <artifactId>jctools-core</artifactId> - <version>2.1.1</version> + <version>3.0.0</version> </dependency> <dependency> @@ -592,7 +641,7 @@ <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-compress</artifactId> - <version>1.18</version> + <version>1.19</version> <scope>test</scope> </dependency> @@ -626,6 +675,13 @@ <version>${log4j2.version}</version> <scope>test</scope> </dependency> + + <!-- BlockHound integration --> + <dependency> + <groupId>io.projectreactor.tools</groupId> + <artifactId>blockhound</artifactId> + <version>1.0.2.RELEASE</version> + </dependency> </dependencies> </dependencyManagement> @@ -665,11 +721,42 @@ <extension> <groupId>kr.motd.maven</groupId> <artifactId>os-maven-plugin</artifactId> - <version>1.6.0</version> + <version>${osmaven.version}</version> </extension> </extensions> <plugins> + <plugin> + <groupId>com.github.siom79.japicmp</groupId> + <artifactId>japicmp-maven-plugin</artifactId> + <version>0.14.3</version> + <configuration> + <parameter> + <ignoreMissingOldVersion>true</ignoreMissingOldVersion> + <breakBuildOnBinaryIncompatibleModifications>true</breakBuildOnBinaryIncompatibleModifications> + <breakBuildOnSourceIncompatibleModifications>true</breakBuildOnSourceIncompatibleModifications> + <oldVersionPattern>\d+\.\d+\.\d+\.Final</oldVersionPattern> + <ignoreMissingClassesByRegularExpressions> + <!-- ignore everything which is not part of netty itself as the plugin can not handle optional dependencies --> + <ignoreMissingClassesByRegularExpression>^(?!io\.netty\.).*</ignoreMissingClassesByRegularExpression> + <ignoreMissingClassesByRegularExpression>^io\.netty\.internal\.tcnative\..*</ignoreMissingClassesByRegularExpression> + </ignoreMissingClassesByRegularExpressions> + <excludes> + <exclude>@io.netty.util.internal.UnstableApi</exclude> + <exclude>io.netty.util.internal.shaded</exclude> + </excludes> + </parameter> + <skip>${skipJapicmp}</skip> + </configuration> + <executions> + <execution> + <phase>verify</phase> + <goals> + <goal>cmp</goal> + </goals> + </execution> + </executions> + </plugin> <plugin> <artifactId>maven-enforcer-plugin</artifactId> <version>${enforcer.plugin.version}</version> @@ -691,10 +778,10 @@ </requireMavenVersion> <requireProperty> <regexMessage> - x86_64/AARCH64 JDK must be used. + x86_64/AARCH64/PPCLE64/s390x_64 JDK must be used. </regexMessage> <property>os.detected.arch</property> - <regex>^(x86_64|aarch_64)$</regex> + <regex>^(x86_64|aarch_64|ppcle_64|s390_64)$</regex> </requireProperty> </rules> </configuration> @@ -742,78 +829,7 @@ <version>1.1</version> </signature> <ignores> - <ignore>sun.misc.Unsafe</ignore> - <ignore>sun.misc.Cleaner</ignore> - <ignore>sun.nio.ch.DirectBuffer</ignore> - - <ignore>java.util.zip.Deflater</ignore> - - <!-- Used for NIO --> - <ignore>java.nio.channels.DatagramChannel</ignore> - <ignore>java.nio.channels.MembershipKey</ignore> - <ignore>java.nio.channels.ServerSocketChannel</ignore> - <ignore>java.nio.channels.SocketChannel</ignore> - <ignore>java.net.StandardProtocolFamily</ignore> - <ignore>java.nio.channels.spi.SelectorProvider</ignore> - <ignore>java.net.SocketOption</ignore> - <ignore>java.net.StandardSocketOptions</ignore> - <ignore>java.nio.channels.NetworkChannel</ignore> - - <!-- Self-signed certificate generation --> - <ignore>sun.security.x509.AlgorithmId</ignore> - <ignore>sun.security.x509.CertificateAlgorithmId</ignore> - <ignore>sun.security.x509.CertificateIssuerName</ignore> - <ignore>sun.security.x509.CertificateSerialNumber</ignore> - <ignore>sun.security.x509.CertificateSubjectName</ignore> - <ignore>sun.security.x509.CertificateValidity</ignore> - <ignore>sun.security.x509.CertificateVersion</ignore> - <ignore>sun.security.x509.CertificateX509Key</ignore> - <ignore>sun.security.x509.X500Name</ignore> - <ignore>sun.security.x509.X509CertInfo</ignore> - <ignore>sun.security.x509.X509CertImpl</ignore> - - <!-- SSLSession implementation --> - <ignore>javax.net.ssl.SSLEngine</ignore> - <ignore>javax.net.ssl.ExtendedSSLSession</ignore> - <ignore>javax.net.ssl.X509ExtendedTrustManager</ignore> - <ignore>javax.net.ssl.SSLParameters</ignore> - <ignore>javax.net.ssl.SNIServerName</ignore> - <ignore>javax.net.ssl.SNIHostName</ignore> - <ignore>javax.net.ssl.SNIMatcher</ignore> - <ignore>java.security.AlgorithmConstraints</ignore> - <ignore>java.security.cert.CertificateRevokedException</ignore> - <ignore>java.security.cert.CertPathValidatorException</ignore> - <ignore>java.security.cert.CertPathValidatorException$Reason</ignore> - <ignore>java.security.cert.CertPathValidatorException$BasicReason</ignore> - - <ignore>java.util.concurrent.ConcurrentLinkedDeque</ignore> - <ignore>java.util.concurrent.ThreadLocalRandom</ignore> - - <!-- Compression --> - <ignore>java.util.zip.CRC32</ignore> - <ignore>java.util.zip.Adler32</ignore> - - <!-- NioDatagramChannel implementation --> - <ignore>java.net.ProtocolFamily</ignore> - - <!-- JDK 9 --> <ignore>java.nio.ByteBuffer</ignore> - <ignore>java.nio.CharBuffer</ignore> - - <!-- JDK 8 --> - <ignore>java.util.concurrent.atomic.LongAdder</ignore> - <ignore>java.util.function.BiFunction</ignore> - <ignore>java.security.cert.X509Certificate</ignore> - - <!-- Resolver --> - <ignore>java.net.InetAddress</ignore> - - <!-- NoexecVolumeDetector --> - <ignore>java.nio.file.attribute.PosixFilePermission</ignore> - <ignore>java.nio.file.Files</ignore> - <ignore>java.nio.file.LinkOption</ignore> - <ignore>java.nio.file.Path</ignore> - <ignore>java.io.File</ignore> </ignores> <annotations> <annotation>io.netty.util.internal.SuppressJava6Requirement</annotation> @@ -830,7 +846,7 @@ </plugin> <plugin> <artifactId>maven-checkstyle-plugin</artifactId> - <version>2.12.1</version> + <version>3.1.0</version> <executions> <execution> <id>check-style</id> @@ -844,11 +860,19 @@ <failsOnError>true</failsOnError> <failOnViolation>true</failOnViolation> <configLocation>io/netty/checkstyle.xml</configLocation> - <includeTestSourceDirectory>true</includeTestSourceDirectory> + <sourceDirectories> + <sourceDirectory>${project.build.sourceDirectory}</sourceDirectory> + <sourceDirectory>${project.build.testSourceDirectory}</sourceDirectory> + </sourceDirectories> </configuration> </execution> </executions> <dependencies> + <dependency> + <groupId>com.puppycrawl.tools</groupId> + <artifactId>checkstyle</artifactId> + <version>8.29</version> + </dependency> <dependency> <groupId>${project.groupId}</groupId> <artifactId>netty-build</artifactId> @@ -922,8 +946,8 @@ <value>io.netty.build.junit.TimedOutTestsListener</value> </property> </properties> - <jvm>${testJavaHome}/bin/java</jvm> - <!-- Ensure the whole stacktrace is preserved when an exception is thrown. See https://issues.apache.org/jira/browse/SUREFIRE-1457 --> + <jvm>${testJvm}</jvm> + <!-- Ensure the whole stacktrace is preserved when an exception is thrown. See https://issues.apache.org/jira/browse/SUREFIRE-1457 --> <trimStackTrace>false</trimStackTrace> </configuration> </plugin> @@ -972,7 +996,7 @@ <plugin> <artifactId>maven-source-plugin</artifactId> - <version>3.0.1</version> + <version>3.2.0</version> <!-- Eclipse-related OSGi manifests See https://github.com/netty/netty/issues/3886 More information: http://rajakannappan.blogspot.ie/2010/03/automating-eclipse-source-bundle.html --> @@ -990,23 +1014,18 @@ </configuration> <executions> - <!-- - ~ This workaround prevents Maven from executing the 'generate-sources' phase twice. - ~ See http://jira.codehaus.org/browse/MSOURCES-13 - ~ and http://blog.peterlynch.ca/2010/05/maven-how-to-prevent-generate-sources.html - --> <execution> <id>attach-sources</id> - <phase>invalid</phase> + <phase>prepare-package</phase> <goals> - <goal>jar</goal> + <goal>jar-no-fork</goal> </goals> </execution> <execution> - <id>attach-sources-no-fork</id> - <phase>package</phase> + <id>attach-test-sources</id> + <phase>prepare-package</phase> <goals> - <goal>jar-no-fork</goal> + <goal>test-jar-no-fork</goal> </goals> </execution> </executions> @@ -1211,6 +1230,11 @@ </archive> </configuration> </execution> + <execution> + <goals> + <goal>test-jar</goal> + </goals> + </execution> </executions> </plugin> <plugin> @@ -1261,7 +1285,7 @@ <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> - <version>3.1.0</version> + <version>3.2.1</version> </plugin> <!-- Workaround for the 'M2E plugin execution not covered' problem. diff --git a/resolver-dns-native-macos/pom.xml b/resolver-dns-native-macos/pom.xml new file mode 100644 index 0000000..639b04a --- /dev/null +++ b/resolver-dns-native-macos/pom.xml @@ -0,0 +1,185 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + ~ Copyright 2019 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. + --> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>io.netty</groupId> + <artifactId>netty-parent</artifactId> + <version>4.1.48.Final</version> + </parent> + <artifactId>netty-resolver-dns-native-macos</artifactId> + + <name>Netty/Resolver/DNS/MacOS</name> + <packaging>jar</packaging> + + <profiles> + <profile> + <id>mac</id> + <activation> + <os> + <family>mac</family> + </os> + </activation> + <properties> + <jni.compiler.args.ldflags>LDFLAGS=-Wl,-weak_library,${unix.common.lib.unpacked.dir}/lib${unix.common.lib.name}.a</jni.compiler.args.ldflags> + <skipTests>false</skipTests> + </properties> + <build> + <plugins> + <plugin> + <artifactId>maven-dependency-plugin</artifactId> + <executions> + <!-- unpack the unix-common static library and include files --> + <execution> + <id>unpack</id> + <phase>generate-sources</phase> + <goals> + <goal>unpack-dependencies</goal> + </goals> + <configuration> + <includeGroupIds>${project.groupId}</includeGroupIds> + <includeArtifactIds>netty-transport-native-unix-common</includeArtifactIds> + <classifier>${jni.classifier}</classifier> + <outputDirectory>${unix.common.lib.dir}</outputDirectory> + <includes>META-INF/native/**</includes> + <overWriteReleases>false</overWriteReleases> + <overWriteSnapshots>true</overWriteSnapshots> + </configuration> + </execution> + </executions> + </plugin> + + <plugin> + <groupId>org.fusesource.hawtjni</groupId> + <artifactId>maven-hawtjni-plugin</artifactId> + <executions> + <execution> + <id>build-native-lib</id> + <configuration> + <name>netty_resolver_dns_native_macos_${os.detected.arch}</name> + <nativeSourceDirectory>${project.basedir}/src/main/c</nativeSourceDirectory> + <libDirectory>${project.build.outputDirectory}</libDirectory> + <!-- We use Maven's artifact classifier instead. + This hack will make the hawtjni plugin to put the native library + under 'META-INF/native' rather than 'META-INF/native/${platform}'. --> + <platform>.</platform> + <configureArgs> + <arg>${jni.compiler.args.ldflags}</arg> + <arg>${jni.compiler.args.cflags}</arg> + </configureArgs> + </configuration> + <goals> + <goal>generate</goal> + <goal>build</goal> + </goals> + </execution> + </executions> + </plugin> + + <plugin> + <artifactId>maven-jar-plugin</artifactId> + <executions> + <!-- Generate the JAR that contains the native library in it. --> + <execution> + <id>native-jar</id> + <goals> + <goal>jar</goal> + </goals> + <configuration> + <archive> + <manifest> + <addDefaultImplementationEntries>true</addDefaultImplementationEntries> + </manifest> + <manifestEntries> + <Bundle-NativeCode>META-INF/native/libnetty_resolver_dns_native_macos_${os.detected.arch}.jnilib; osname=MacOSX, processor=${os.detected.arch}"</Bundle-NativeCode> + <Automatic-Module-Name>${javaModuleName}</Automatic-Module-Name> + </manifestEntries> + <index>true</index> + <manifestFile>${project.build.outputDirectory}/META-INF/MANIFEST.MF</manifestFile> + </archive> + <classifier>${jni.classifier}</classifier> + </configuration> + </execution> + </executions> + </plugin> + </plugins> + </build> + <dependencies> + <dependency> + <groupId>io.netty</groupId> + <artifactId>netty-transport-native-unix-common</artifactId> + <version>${project.version}</version> + <classifier>${jni.classifier}</classifier> + <!-- + The unix-common with classifier dependency is optional because it is not a runtime dependency, but a build time + dependency to get the static library which is built directly into the shared library generated by this project. + --> + <optional>true</optional> + </dependency> + </dependencies> + </profile> + </profiles> + + <properties> + <javaModuleName>io.netty.resolver.dns.macos</javaModuleName> + <!-- Needed as we use SelfSignedCertificate in our tests --> + <unix.common.lib.name>netty-unix-common</unix.common.lib.name> + <unix.common.lib.dir>${project.build.directory}/unix-common-lib</unix.common.lib.dir> + <unix.common.lib.unpacked.dir>${unix.common.lib.dir}/META-INF/native/lib</unix.common.lib.unpacked.dir> + <unix.common.include.unpacked.dir>${unix.common.lib.dir}/META-INF/native/include</unix.common.include.unpacked.dir> + <jni.compiler.args.cflags>CFLAGS=-O3 -Werror -fno-omit-frame-pointer -Wunused-variable -fvisibility=hidden -I${unix.common.include.unpacked.dir}</jni.compiler.args.cflags> + <jni.compiler.args.ldflags>LDFLAGS=-z now -L${unix.common.lib.unpacked.dir} -Wl,--whole-archive -l${unix.common.lib.name} -Wl,--no-whole-archive</jni.compiler.args.ldflags> + </properties> + + <dependencies> + <dependency> + <groupId>io.netty</groupId> + <artifactId>netty-common</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>io.netty</groupId> + <artifactId>netty-resolver-dns</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>io.netty</groupId> + <artifactId>netty-transport-native-unix-common</artifactId> + <version>${project.version}</version> + </dependency> + </dependencies> + + <build> + <plugins> + <plugin> + <artifactId>maven-jar-plugin</artifactId> + <executions> + <!-- Generate the fallback JAR that does not contain the native library. --> + <execution> + <id>default-jar</id> + <configuration> + <excludes> + <exclude>META-INF/native/**</exclude> + </excludes> + </configuration> + </execution> + </executions> + </plugin> + </plugins> + </build> +</project> + diff --git a/resolver-dns-native-macos/src/main/c/dnsinfo.h b/resolver-dns-native-macos/src/main/c/dnsinfo.h new file mode 100644 index 0000000..8b10e8d --- /dev/null +++ b/resolver-dns-native-macos/src/main/c/dnsinfo.h @@ -0,0 +1,106 @@ +/* + * Copyright 2019 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. + */ +/* + * Copyright (c) 2004-2006, 2008, 2009, 2011 Apple Inc. All rights reserved. + * + * @APPLE_LICENSE_HEADER_START@ + * + * This file contains Original Code and/or Modifications of Original Code + * as defined in and that are subject to the Apple Public Source License + * Version 2.0 (the 'License'). You may not use this file except in + * compliance with the License. Please obtain a copy of the License at + * http://www.opensource.apple.com/apsl/ and read it before using this + * file. + * + * The Original Code and all software distributed under the License are + * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER + * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, + * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. + * Please see the License for the specific language governing rights and + * limitations under the License. + * + * @APPLE_LICENSE_HEADER_END@ + */ +#ifndef __DNSINFO_H__ +#define __DNSINFO_H__ +/* + * These routines provide access to the systems DNS configuration + */ +#include <sys/cdefs.h> +#include <stdint.h> +#include <sys/types.h> +#include <sys/socket.h> +#include <netinet/in.h> +#define DNSINFO_VERSION 20111104 +#define DEFAULT_SEARCH_ORDER 200000 /* search order for the "default" resolver domain name */ +#define DNS_PTR(type, name) \ + union { \ + type name; \ + uint64_t _ ## name ## _p; \ + } +#define DNS_VAR(type, name) \ + type name +#pragma pack(4) +typedef struct { + struct in_addr address; + struct in_addr mask; +} dns_sortaddr_t; +#pragma pack() +#pragma pack(4) +typedef struct { + DNS_PTR(char *, domain); /* domain */ + DNS_VAR(int32_t, n_nameserver); /* # nameserver */ + DNS_PTR(struct sockaddr **, nameserver); + DNS_VAR(uint16_t, port); /* port (in host byte order) */ + DNS_VAR(int32_t, n_search); /* # search */ + DNS_PTR(char **, search); + DNS_VAR(int32_t, n_sortaddr); /* # sortaddr */ + DNS_PTR(dns_sortaddr_t **, sortaddr); + DNS_PTR(char *, options); /* options */ + DNS_VAR(uint32_t, timeout); /* timeout */ + DNS_VAR(uint32_t, search_order); /* search_order */ + DNS_VAR(uint32_t, if_index); + DNS_VAR(uint32_t, flags); + DNS_VAR(uint32_t, reach_flags); /* SCNetworkReachabilityFlags */ + DNS_VAR(uint32_t, reserved[5]); +} dns_resolver_t; +#pragma pack() +#define DNS_RESOLVER_FLAGS_SCOPED 1 /* configuration is for scoped questions */ +#pragma pack(4) +typedef struct { + DNS_VAR(int32_t, n_resolver); /* resolver configurations */ + DNS_PTR(dns_resolver_t **, resolver); + DNS_VAR(int32_t, n_scoped_resolver); /* "scoped" resolver configurations */ + DNS_PTR(dns_resolver_t **, scoped_resolver); + DNS_VAR(uint32_t, reserved[5]); +} dns_config_t; +#pragma pack() +__BEGIN_DECLS +/* + * DNS configuration access APIs + */ +const char * +dns_configuration_notify_key (); +dns_config_t * +dns_configuration_copy (); +void +dns_configuration_free (dns_config_t *config); +void +_dns_configuration_ack (dns_config_t *config, + const char *bundle_id); +__END_DECLS +#endif /* __DNSINFO_H__ */ \ No newline at end of file diff --git a/resolver-dns-native-macos/src/main/c/netty_resolver_dns_macos.c b/resolver-dns-native-macos/src/main/c/netty_resolver_dns_macos.c new file mode 100644 index 0000000..68df75c --- /dev/null +++ b/resolver-dns-native-macos/src/main/c/netty_resolver_dns_macos.c @@ -0,0 +1,263 @@ +/* + * Copyright 2019 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. + */ +#include <errno.h> +#include <string.h> +#include <unistd.h> +#include <dlfcn.h> +#include <sys/socket.h> +#include <stdlib.h> +#include "dnsinfo.h" +#include "netty_unix_jni.h" +#include "netty_unix_util.h" +#include "netty_unix_socket.h" +#include "netty_unix_errors.h" + +static jclass dnsResolverClass = NULL; +static jclass byteArrayClass = NULL; +static jclass stringClass = NULL; +static jmethodID dnsResolverMethodId = NULL; + +// JNI Registered Methods Begin + +// We use the same API as mDNSResponder and Chromium to retrieve the current nameserver configuration for the system: +// See: +// https://src.chromium.org/viewvc/chrome?revision=218617&view=revision +// https://opensource.apple.com/tarballs/mDNSResponder/ +static jobjectArray netty_resolver_dns_macos_resolvers(JNIEnv* env, jclass clazz) { + dns_config_t* config = dns_configuration_copy(); + + jobjectArray array = (*env)->NewObjectArray(env, config->n_resolver, dnsResolverClass, NULL); + if (array == NULL) { + goto error; + } + + for (int i = 0; i < config->n_resolver; i++) { + dns_resolver_t* resolver = config->resolver[i]; + jstring domain = NULL; + + if (resolver->domain != NULL) { + domain = (*env)->NewStringUTF(env, resolver->domain); + if (domain == NULL) { + goto error; + } + } + + jobjectArray addressArray = (*env)->NewObjectArray(env, resolver->n_nameserver, byteArrayClass, NULL); + if (addressArray == NULL) { + goto error; + } + + for (int a = 0; a < resolver->n_nameserver; a++) { + jbyteArray address = netty_unix_socket_createInetSocketAddressArray(env, (const struct sockaddr_storage *) resolver->nameserver[a]); + if (address == NULL) { + netty_unix_errors_throwOutOfMemoryError(env); + goto error; + } + (*env)->SetObjectArrayElement(env, addressArray, a, address); + } + + jint port = resolver->port; + + jobjectArray searchArray = (*env)->NewObjectArray(env, resolver->n_search, stringClass, NULL); + if (searchArray == NULL) { + goto error; + } + + for (int a = 0; a < resolver->n_search; a++) { + jstring search = (*env)->NewStringUTF(env, resolver->search[a]); + if (search == NULL) { + goto error; + } + + (*env)->SetObjectArrayElement(env, searchArray, a, search); + } + + jstring options = NULL; + if (resolver->options != NULL) { + options = (*env)->NewStringUTF(env, resolver->options); + if (options == NULL) { + goto error; + } + } + + jint timeout = resolver->timeout; + jint searchOrder = resolver->search_order; + + jobject java_resolver = (*env)->NewObject(env, dnsResolverClass, dnsResolverMethodId, domain, + addressArray, port, searchArray, options, timeout, searchOrder); + if (java_resolver == NULL) { + goto error; + } + (*env)->SetObjectArrayElement(env, array, i, java_resolver); + } + + dns_configuration_free(config); + return array; +error: + dns_configuration_free(config); + return NULL; +} + + +// JNI Method Registration Table Begin + +static JNINativeMethod* createDynamicMethodsTable(const char* packagePrefix) { + JNINativeMethod* dynamicMethods = malloc(sizeof(JNINativeMethod) * 1); + + char* dynamicTypeName = netty_unix_util_prepend(packagePrefix, "io/netty/resolver/dns/macos/DnsResolver;"); + JNINativeMethod* dynamicMethod = &dynamicMethods[0]; + dynamicMethod->name = "resolvers"; + dynamicMethod->signature = netty_unix_util_prepend("()[L", dynamicTypeName); + dynamicMethod->fnPtr = (void *) netty_resolver_dns_macos_resolvers; + free(dynamicTypeName); + return dynamicMethods; +} + +static void freeDynamicMethodsTable(JNINativeMethod* dynamicMethods) { + free(dynamicMethods[0].signature); + free(dynamicMethods); +} + +// JNI Method Registration Table End + + +static void JNI_OnUnload_netty_resolver_dns_native_macos0(JavaVM* vm, void* reserved) { + JNIEnv* env; + if ((*vm)->GetEnv(vm, (void**) &env, NETTY_JNI_VERSION) != JNI_OK) { + // Something is wrong but nothing we can do about this :( + return; + } + + if (byteArrayClass != NULL) { + (*env)->DeleteGlobalRef(env, byteArrayClass); + byteArrayClass = NULL; + } + + if (stringClass != NULL) { + (*env)->DeleteGlobalRef(env, stringClass); + stringClass = NULL; + } +} + +static void netty_resolver_dns_native_macos0_OnUnLoad(JNIEnv* env) { + +} + +static jint JNI_OnLoad_netty_resolver_dns_native_macos0(JavaVM* vm, void* reserved) { + JNIEnv* env; + if ((*vm)->GetEnv(vm, (void**) &env, NETTY_JNI_VERSION) != JNI_OK) { + return JNI_ERR; + } + +#ifndef NETTY_BUILD_STATIC + Dl_info dlinfo; + jint status = 0; + // We need to use an address of a function that is uniquely part of this library, so choose a static + // function. See https://github.com/netty/netty/issues/4840. + if (!dladdr((void*) netty_resolver_dns_native_macos0_OnUnLoad, &dlinfo)) { + fprintf(stderr, "FATAL: resolver-dns-native-macos JNI call to dladdr failed!\n"); + return JNI_ERR; + } + char* packagePrefix = netty_unix_util_parse_package_prefix(dlinfo.dli_fname, "netty_resolver_dns_native_macos", &status); + if (status == JNI_ERR) { + fprintf(stderr, "FATAL: resolver-dns-native-macos JNI encountered unexpected dlinfo.dli_fname: %s\n", dlinfo.dli_fname); + return JNI_ERR; + } +#endif /* NETTY_BUILD_STATIC */ + + // Register the methods which are not referenced by static member variables + JNINativeMethod* dynamicMethods = createDynamicMethodsTable(packagePrefix); + if (netty_unix_util_register_natives(env, + packagePrefix, + "io/netty/resolver/dns/macos/MacOSDnsServerAddressStreamProvider", + dynamicMethods, 1) != 0) { + freeDynamicMethodsTable(dynamicMethods); + fprintf(stderr, "FATAL: Couldnt register natives"); + + return JNI_ERR; + } + freeDynamicMethodsTable(dynamicMethods); + dynamicMethods = NULL; + + + char* nettyClassName = netty_unix_util_prepend(packagePrefix, "io/netty/resolver/dns/macos/DnsResolver"); + jclass localDnsResolverClass = (*env)->FindClass(env, nettyClassName); + free(nettyClassName); + nettyClassName = NULL; + if (localDnsResolverClass == NULL) { + // pending exception... + return JNI_ERR; + } + dnsResolverClass = (jclass) (*env)->NewGlobalRef(env, localDnsResolverClass); + if (dnsResolverClass == NULL) { + return JNI_ERR; + } + dnsResolverMethodId = (*env)->GetMethodID(env, dnsResolverClass, "<init>", "(Ljava/lang/String;[[BI[Ljava/lang/String;Ljava/lang/String;II)V"); + if (dnsResolverMethodId == NULL) { + netty_unix_errors_throwRuntimeException(env, "failed to get method ID: DnsResolver.<init>(String, byte[][], String[], String, int, int)"); + return JNI_ERR; + } + + if (packagePrefix != NULL) { + free(packagePrefix); + packagePrefix = NULL; + } + + jclass byteArrayCls = (*env)->FindClass(env, "[B"); + if (byteArrayCls == NULL) { + // pending exception... + return JNI_ERR; + } + byteArrayClass = (jclass) (*env)->NewGlobalRef(env, byteArrayCls); + if (byteArrayClass == NULL) { + return JNI_ERR; + } + + jclass stringCls = (*env)->FindClass(env, "java/lang/String"); + if (stringCls == NULL) { + // pending exception... + return JNI_ERR; + } + stringClass = (jclass) (*env)->NewGlobalRef(env, stringCls); + if (stringClass == NULL) { + return JNI_ERR; + } + + return NETTY_JNI_VERSION; +} + +// We build with -fvisibility=hidden so ensure we mark everything that needs to be visible with JNIEXPORT +// http://mail.openjdk.java.net/pipermail/core-libs-dev/2013-February/014549.html + +// Invoked by the JVM when statically linked +JNIEXPORT jint JNI_OnLoad_netty_resolver_dns_native_macos(JavaVM* vm, void* reserved) { + return JNI_OnLoad_netty_resolver_dns_native_macos0(vm, reserved); +} + +// Invoked by the JVM when statically linked +JNIEXPORT void JNI_OnUnload_netty_resolver_dns_native_macos(JavaVM* vm, void* reserved) { + JNI_OnUnload_netty_resolver_dns_native_macos0(vm, reserved); +} + +#ifndef NETTY_BUILD_STATIC +JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) { + return JNI_OnLoad_netty_resolver_dns_native_macos0(vm, reserved); +} + +JNIEXPORT void JNI_OnUnload(JavaVM* vm, void* reserved) { + return JNI_OnUnload_netty_resolver_dns_native_macos0(vm, reserved); +} +#endif /* NETTY_BUILD_STATIC */ diff --git a/resolver-dns-native-macos/src/main/java/io/netty/resolver/dns/macos/DnsResolver.java b/resolver-dns-native-macos/src/main/java/io/netty/resolver/dns/macos/DnsResolver.java new file mode 100644 index 0000000..9744c65 --- /dev/null +++ b/resolver-dns-native-macos/src/main/java/io/netty/resolver/dns/macos/DnsResolver.java @@ -0,0 +1,81 @@ +/* + * Copyright 2019 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.resolver.dns.macos; + +import io.netty.channel.unix.NativeInetAddress; + +import java.net.InetSocketAddress; + +/** + * Represent the {@code dns_resolver_t} struct. + */ +final class DnsResolver { + + private final String domain; + private final InetSocketAddress[] nameservers; + private final int port; + private final String[] searches; + private final String options; + private final int timeout; + private final int searchOrder; + + DnsResolver(String domain, byte[][] nameservers, int port, + String[] searches, String options, int timeout, int searchOrder) { + this.domain = domain; + if (nameservers == null) { + this.nameservers = new InetSocketAddress[0]; + } else { + this.nameservers = new InetSocketAddress[nameservers.length]; + for (int i = 0; i < nameservers.length; i++) { + byte[] addr = nameservers[i]; + this.nameservers[i] = NativeInetAddress.address(addr, 0, addr.length); + } + } + this.port = port; + this.searches = searches; + this.options = options; + this.timeout = timeout; + this.searchOrder = searchOrder; + } + + String domain() { + return domain; + } + + InetSocketAddress[] nameservers() { + return nameservers; + } + + int port() { + return port; + } + + String[] searches() { + return searches; + } + + String options() { + return options; + } + + int timeout() { + return timeout; + } + + int searchOrder() { + return searchOrder; + } +} diff --git a/resolver-dns-native-macos/src/main/java/io/netty/resolver/dns/macos/MacOSDnsServerAddressStreamProvider.java b/resolver-dns-native-macos/src/main/java/io/netty/resolver/dns/macos/MacOSDnsServerAddressStreamProvider.java new file mode 100644 index 0000000..6cdcff6 --- /dev/null +++ b/resolver-dns-native-macos/src/main/java/io/netty/resolver/dns/macos/MacOSDnsServerAddressStreamProvider.java @@ -0,0 +1,179 @@ +/* + * Copyright 2019 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.resolver.dns.macos; + +import io.netty.resolver.dns.DnsServerAddressStream; +import io.netty.resolver.dns.DnsServerAddressStreamProvider; +import io.netty.resolver.dns.DnsServerAddressStreamProviders; +import io.netty.resolver.dns.DnsServerAddresses; +import io.netty.util.internal.NativeLibraryLoader; +import io.netty.util.internal.PlatformDependent; +import io.netty.util.internal.StringUtil; +import io.netty.util.internal.SystemPropertyUtil; +import io.netty.util.internal.ThrowableUtil; +import io.netty.util.internal.logging.InternalLogger; +import io.netty.util.internal.logging.InternalLoggerFactory; + +import java.net.InetSocketAddress; +import java.util.Collections; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +/** + * {@link DnsServerAddressStreamProvider} implementation which makes use of the same mechanism as + * <a href="https://opensource.apple.com/tarballs/mDNSResponder/">Apple's open source mDNSResponder</a> to retrieve the + * current nameserver configuration of the system. + */ +public final class MacOSDnsServerAddressStreamProvider implements DnsServerAddressStreamProvider { + + private static final Throwable UNAVAILABILITY_CAUSE; + + private static final InternalLogger logger = + InternalLoggerFactory.getInstance(MacOSDnsServerAddressStreamProvider.class); + + // Let's refresh every 10 seconds. + private static final long REFRESH_INTERVAL = TimeUnit.SECONDS.toNanos(10); + + static { + Throwable cause = null; + try { + loadNativeLibrary(); + } catch (Throwable error) { + cause = error; + } + UNAVAILABILITY_CAUSE = cause; + } + + private static void loadNativeLibrary() { + String name = SystemPropertyUtil.get("os.name").toLowerCase(Locale.UK).trim(); + if (!name.startsWith("mac")) { + throw new IllegalStateException("Only supported on MacOS"); + } + String staticLibName = "netty_resolver_dns_native_macos"; + String sharedLibName = staticLibName + '_' + PlatformDependent.normalizedArch(); + ClassLoader cl = PlatformDependent.getClassLoader(MacOSDnsServerAddressStreamProvider.class); + try { + NativeLibraryLoader.load(sharedLibName, cl); + } catch (UnsatisfiedLinkError e1) { + try { + NativeLibraryLoader.load(staticLibName, cl); + logger.debug("Failed to load {}", sharedLibName, e1); + } catch (UnsatisfiedLinkError e2) { + ThrowableUtil.addSuppressed(e1, e2); + throw e1; + } + } + } + + public static boolean isAvailable() { + return UNAVAILABILITY_CAUSE == null; + } + + public static void ensureAvailability() { + if (UNAVAILABILITY_CAUSE != null) { + throw (Error) new UnsatisfiedLinkError( + "failed to load the required native library").initCause(UNAVAILABILITY_CAUSE); + } + } + + public static Throwable unavailabilityCause() { + return UNAVAILABILITY_CAUSE; + } + + public MacOSDnsServerAddressStreamProvider() { + ensureAvailability(); + } + + private volatile Map<String, DnsServerAddresses> currentMappings = retrieveCurrentMappings(); + private final AtomicLong lastRefresh = new AtomicLong(System.nanoTime()); + + private static Map<String, DnsServerAddresses> retrieveCurrentMappings() { + DnsResolver[] resolvers = resolvers(); + + if (resolvers == null || resolvers.length == 0) { + return Collections.emptyMap(); + } + Map<String, DnsServerAddresses> resolverMap = new HashMap<String, DnsServerAddresses>(resolvers.length); + for (DnsResolver resolver: resolvers) { + // Skip mdns + if ("mdns".equalsIgnoreCase(resolver.options())) { + continue; + } + InetSocketAddress[] nameservers = resolver.nameservers(); + if (nameservers == null || nameservers.length == 0) { + continue; + } + String domain = resolver.domain(); + if (domain == null) { + // Default mapping. + domain = StringUtil.EMPTY_STRING; + } + InetSocketAddress[] servers = resolver.nameservers(); + for (int a = 0; a < servers.length; a++) { + InetSocketAddress address = servers[a]; + // Check if the default port should be used + if (address.getPort() == 0) { + int port = resolver.port(); + if (port == 0) { + port = 53; + } + servers[a] = new InetSocketAddress(address.getAddress(), port); + } + } + + resolverMap.put(domain, DnsServerAddresses.sequential(servers)); + } + return resolverMap; + } + + @Override + public DnsServerAddressStream nameServerAddressStream(String hostname) { + long last = lastRefresh.get(); + Map<String, DnsServerAddresses> resolverMap = currentMappings; + if (System.nanoTime() - last > REFRESH_INTERVAL) { + // This is slightly racy which means it will be possible still use the old configuration for a small + // amount of time, but that's ok. + if (lastRefresh.compareAndSet(last, System.nanoTime())) { + resolverMap = currentMappings = retrieveCurrentMappings(); + } + } + + final String originalHostname = hostname; + for (;;) { + int i = hostname.indexOf('.', 1); + if (i < 0 || i == hostname.length() - 1) { + // Try access default mapping. + DnsServerAddresses addresses = resolverMap.get(StringUtil.EMPTY_STRING); + if (addresses != null) { + return addresses.stream(); + } + return DnsServerAddressStreamProviders.unixDefault().nameServerAddressStream(originalHostname); + } + + DnsServerAddresses addresses = resolverMap.get(hostname); + if (addresses != null) { + return addresses.stream(); + } + + hostname = hostname.substring(i + 1); + } + } + + private static native DnsResolver[] resolvers(); +} diff --git a/resolver-dns-native-macos/src/main/java/io/netty/resolver/dns/macos/package-info.java b/resolver-dns-native-macos/src/main/java/io/netty/resolver/dns/macos/package-info.java new file mode 100644 index 0000000..56a806f --- /dev/null +++ b/resolver-dns-native-macos/src/main/java/io/netty/resolver/dns/macos/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2019 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. + */ + +/** + * MacOS specific nameserver resolution. + */ +package io.netty.resolver.dns.macos; diff --git a/resolver-dns-native-macos/src/test/java/io/netty/resolver/dns/macos/MacOSDnsServerAddressStreamProviderTest.java b/resolver-dns-native-macos/src/test/java/io/netty/resolver/dns/macos/MacOSDnsServerAddressStreamProviderTest.java new file mode 100644 index 0000000..424716f --- /dev/null +++ b/resolver-dns-native-macos/src/test/java/io/netty/resolver/dns/macos/MacOSDnsServerAddressStreamProviderTest.java @@ -0,0 +1,52 @@ +/* + * Copyright 2019 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.resolver.dns.macos; + +import io.netty.resolver.dns.DnsServerAddressStream; +import io.netty.resolver.dns.DnsServerAddressStreamProvider; +import io.netty.resolver.dns.DnsServerAddressStreamProviders; +import org.hamcrest.Matchers; +import org.junit.Assert; +import org.junit.Assume; +import org.junit.BeforeClass; +import org.junit.Test; + +public class MacOSDnsServerAddressStreamProviderTest { + + @BeforeClass + public static void assume() { + Assume.assumeTrue(MacOSDnsServerAddressStreamProvider.isAvailable()); + } + + @Test + public void testStream() { + DnsServerAddressStreamProvider provider = new MacOSDnsServerAddressStreamProvider(); + DnsServerAddressStream stream = provider.nameServerAddressStream("netty.io"); + Assert.assertNotNull(stream); + Assert.assertNotEquals(0, stream.size()); + + for (int i = 0; i < stream.size(); i++) { + Assert.assertNotEquals(0, stream.next().getPort()); + } + } + + @Test + public void testDefaultUseCorrectInstance() { + Assert.assertThat(DnsServerAddressStreamProviders.platformDefault(), + Matchers.instanceOf(MacOSDnsServerAddressStreamProvider.class)); + } + +} diff --git a/resolver-dns/pom.xml b/resolver-dns/pom.xml index 1f81cca..d18cc6a 100644 --- a/resolver-dns/pom.xml +++ b/resolver-dns/pom.xml @@ -20,7 +20,7 @@ <parent> <groupId>io.netty</groupId> <artifactId>netty-parent</artifactId> - <version>4.1.33.Final</version> + <version>4.1.48.Final</version> </parent> <artifactId>netty-resolver-dns</artifactId> diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/AuthoritativeDnsServerCache.java b/resolver-dns/src/main/java/io/netty/resolver/dns/AuthoritativeDnsServerCache.java index b011066..f4a443e 100644 --- a/resolver-dns/src/main/java/io/netty/resolver/dns/AuthoritativeDnsServerCache.java +++ b/resolver-dns/src/main/java/io/netty/resolver/dns/AuthoritativeDnsServerCache.java @@ -16,14 +16,12 @@ package io.netty.resolver.dns; import io.netty.channel.EventLoop; -import io.netty.util.internal.UnstableApi; import java.net.InetSocketAddress; /** * Cache which stores the nameservers that should be used to resolve a specific hostname. */ -@UnstableApi public interface AuthoritativeDnsServerCache { /** diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/AuthoritativeDnsServerCacheAdapter.java b/resolver-dns/src/main/java/io/netty/resolver/dns/AuthoritativeDnsServerCacheAdapter.java index 470106e..5fd7840 100644 --- a/resolver-dns/src/main/java/io/netty/resolver/dns/AuthoritativeDnsServerCacheAdapter.java +++ b/resolver-dns/src/main/java/io/netty/resolver/dns/AuthoritativeDnsServerCacheAdapter.java @@ -17,7 +17,6 @@ package io.netty.resolver.dns; import io.netty.channel.EventLoop; import io.netty.handler.codec.dns.DnsRecord; -import io.netty.util.internal.UnstableApi; import java.net.InetAddress; import java.net.InetSocketAddress; @@ -30,7 +29,6 @@ import static io.netty.util.internal.ObjectUtil.checkNotNull; * {@link AuthoritativeDnsServerCache} implementation which delegates all operations to a wrapped {@link DnsCache}. * This implementation is only present to preserve a upgrade story. */ -@UnstableApi final class AuthoritativeDnsServerCacheAdapter implements AuthoritativeDnsServerCache { private static final DnsRecord[] EMPTY = new DnsRecord[0]; diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/BiDnsQueryLifecycleObserver.java b/resolver-dns/src/main/java/io/netty/resolver/dns/BiDnsQueryLifecycleObserver.java index e4b4c35..cd596c1 100644 --- a/resolver-dns/src/main/java/io/netty/resolver/dns/BiDnsQueryLifecycleObserver.java +++ b/resolver-dns/src/main/java/io/netty/resolver/dns/BiDnsQueryLifecycleObserver.java @@ -18,7 +18,6 @@ package io.netty.resolver.dns; import io.netty.channel.ChannelFuture; import io.netty.handler.codec.dns.DnsQuestion; import io.netty.handler.codec.dns.DnsResponseCode; -import io.netty.util.internal.UnstableApi; import java.net.InetSocketAddress; import java.util.List; @@ -28,7 +27,6 @@ import static io.netty.util.internal.ObjectUtil.checkNotNull; /** * Combines two {@link DnsQueryLifecycleObserver} into a single {@link DnsQueryLifecycleObserver}. */ -@UnstableApi public final class BiDnsQueryLifecycleObserver implements DnsQueryLifecycleObserver { private final DnsQueryLifecycleObserver a; private final DnsQueryLifecycleObserver b; diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/BiDnsQueryLifecycleObserverFactory.java b/resolver-dns/src/main/java/io/netty/resolver/dns/BiDnsQueryLifecycleObserverFactory.java index 6745a36..b7125bc 100644 --- a/resolver-dns/src/main/java/io/netty/resolver/dns/BiDnsQueryLifecycleObserverFactory.java +++ b/resolver-dns/src/main/java/io/netty/resolver/dns/BiDnsQueryLifecycleObserverFactory.java @@ -16,14 +16,12 @@ package io.netty.resolver.dns; import io.netty.handler.codec.dns.DnsQuestion; -import io.netty.util.internal.UnstableApi; import static io.netty.util.internal.ObjectUtil.checkNotNull; /** * Combines two {@link DnsQueryLifecycleObserverFactory} into a single {@link DnsQueryLifecycleObserverFactory}. */ -@UnstableApi public final class BiDnsQueryLifecycleObserverFactory implements DnsQueryLifecycleObserverFactory { private final DnsQueryLifecycleObserverFactory a; private final DnsQueryLifecycleObserverFactory b; diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/DatagramDnsQueryContext.java b/resolver-dns/src/main/java/io/netty/resolver/dns/DatagramDnsQueryContext.java new file mode 100644 index 0000000..0ac80b9 --- /dev/null +++ b/resolver-dns/src/main/java/io/netty/resolver/dns/DatagramDnsQueryContext.java @@ -0,0 +1,51 @@ +/* + * Copyright 2019 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.resolver.dns; + +import io.netty.channel.AddressedEnvelope; +import io.netty.channel.Channel; +import io.netty.handler.codec.dns.DatagramDnsQuery; +import io.netty.handler.codec.dns.DnsQuery; +import io.netty.handler.codec.dns.DnsQuestion; +import io.netty.handler.codec.dns.DnsRecord; +import io.netty.handler.codec.dns.DnsResponse; +import io.netty.util.concurrent.Promise; + +import java.net.InetSocketAddress; + +final class DatagramDnsQueryContext extends DnsQueryContext { + + DatagramDnsQueryContext(DnsNameResolver parent, InetSocketAddress nameServerAddr, DnsQuestion question, + DnsRecord[] additionals, + Promise<AddressedEnvelope<DnsResponse, InetSocketAddress>> promise) { + super(parent, nameServerAddr, question, additionals, promise); + } + + @Override + protected DnsQuery newQuery(int id) { + return new DatagramDnsQuery(null, nameServerAddr(), id); + } + + @Override + protected Channel channel() { + return parent().ch; + } + + @Override + protected String protocol() { + return "UDP"; + } +} diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/DefaultAuthoritativeDnsServerCache.java b/resolver-dns/src/main/java/io/netty/resolver/dns/DefaultAuthoritativeDnsServerCache.java index 6bc29a6..7f8a9b1 100644 --- a/resolver-dns/src/main/java/io/netty/resolver/dns/DefaultAuthoritativeDnsServerCache.java +++ b/resolver-dns/src/main/java/io/netty/resolver/dns/DefaultAuthoritativeDnsServerCache.java @@ -17,7 +17,6 @@ package io.netty.resolver.dns; import io.netty.channel.EventLoop; import io.netty.util.internal.PlatformDependent; -import io.netty.util.internal.UnstableApi; import java.net.InetSocketAddress; import java.util.Collections; @@ -30,7 +29,6 @@ import static io.netty.util.internal.ObjectUtil.*; /** * Default implementation of {@link AuthoritativeDnsServerCache}, backed by a {@link ConcurrentMap}. */ -@UnstableApi public class DefaultAuthoritativeDnsServerCache implements AuthoritativeDnsServerCache { private final int minTtl; @@ -117,9 +115,7 @@ public class DefaultAuthoritativeDnsServerCache implements AuthoritativeDnsServe @Override public boolean clear(String hostname) { - checkNotNull(hostname, "hostname"); - - return resolveCache.clear(hostname); + return resolveCache.clear(checkNotNull(hostname, "hostname")); } @Override diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/DefaultDnsCache.java b/resolver-dns/src/main/java/io/netty/resolver/dns/DefaultDnsCache.java index c197839..9d85768 100644 --- a/resolver-dns/src/main/java/io/netty/resolver/dns/DefaultDnsCache.java +++ b/resolver-dns/src/main/java/io/netty/resolver/dns/DefaultDnsCache.java @@ -18,7 +18,6 @@ package io.netty.resolver.dns; import io.netty.channel.EventLoop; import io.netty.handler.codec.dns.DnsRecord; import io.netty.util.internal.StringUtil; -import io.netty.util.internal.UnstableApi; import java.net.InetAddress; import java.util.Collections; @@ -32,7 +31,6 @@ import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero; * Default implementation of {@link DnsCache}, backed by a {@link ConcurrentMap}. * If any additional {@link DnsRecord} is used, no caching takes place. */ -@UnstableApi public class DefaultDnsCache implements DnsCache { private final Cache<DefaultDnsCacheEntry> resolveCache = new Cache<DefaultDnsCacheEntry>() { diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/DefaultDnsCnameCache.java b/resolver-dns/src/main/java/io/netty/resolver/dns/DefaultDnsCnameCache.java index 8638808..88429b9 100644 --- a/resolver-dns/src/main/java/io/netty/resolver/dns/DefaultDnsCnameCache.java +++ b/resolver-dns/src/main/java/io/netty/resolver/dns/DefaultDnsCnameCache.java @@ -17,7 +17,6 @@ package io.netty.resolver.dns; import io.netty.channel.EventLoop; import io.netty.util.AsciiString; -import io.netty.util.internal.UnstableApi; import java.util.List; @@ -26,7 +25,6 @@ import static io.netty.util.internal.ObjectUtil.*; /** * Default implementation of a {@link DnsCnameCache}. */ -@UnstableApi public final class DefaultDnsCnameCache implements DnsCnameCache { private final int minTtl; private final int maxTtl; @@ -69,8 +67,7 @@ public final class DefaultDnsCnameCache implements DnsCnameCache { @SuppressWarnings("unchecked") @Override public String get(String hostname) { - checkNotNull(hostname, "hostname"); - List<? extends String> cached = cache.get(hostname); + List<? extends String> cached = cache.get(checkNotNull(hostname, "hostname")); if (cached == null || cached.isEmpty()) { return null; } @@ -93,7 +90,6 @@ public final class DefaultDnsCnameCache implements DnsCnameCache { @Override public boolean clear(String hostname) { - checkNotNull(hostname, "hostname"); - return cache.clear(hostname); + return cache.clear(checkNotNull(hostname, "hostname")); } } diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/DefaultDnsServerAddressStreamProvider.java b/resolver-dns/src/main/java/io/netty/resolver/dns/DefaultDnsServerAddressStreamProvider.java index 00be072..6bc491c 100644 --- a/resolver-dns/src/main/java/io/netty/resolver/dns/DefaultDnsServerAddressStreamProvider.java +++ b/resolver-dns/src/main/java/io/netty/resolver/dns/DefaultDnsServerAddressStreamProvider.java @@ -18,7 +18,6 @@ package io.netty.resolver.dns; import io.netty.util.NetUtil; import io.netty.util.internal.PlatformDependent; import io.netty.util.internal.SocketUtils; -import io.netty.util.internal.UnstableApi; import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; @@ -37,7 +36,6 @@ import static io.netty.resolver.dns.DnsServerAddresses.sequential; * <p> * This may use the JDK's blocking DNS resolution to bootstrap the default DNS server addresses. */ -@UnstableApi public final class DefaultDnsServerAddressStreamProvider implements DnsServerAddressStreamProvider { private static final InternalLogger logger = InternalLoggerFactory.getInstance(DefaultDnsServerAddressStreamProvider.class); @@ -55,7 +53,9 @@ public final class DefaultDnsServerAddressStreamProvider implements DnsServerAdd DirContextUtils.addNameServers(defaultNameServers, DNS_PORT); } - if (defaultNameServers.isEmpty()) { + // Only try when using Java8 and lower as otherwise it will produce: + // WARNING: Illegal reflective access by io.netty.resolver.dns.DefaultDnsServerAddressStreamProvider + if (PlatformDependent.javaVersion() < 9 && defaultNameServers.isEmpty()) { try { Class<?> configClass = Class.forName("sun.net.dns.ResolverConfiguration"); Method open = configClass.getMethod("open"); diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsAddressResolveContext.java b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsAddressResolveContext.java index 85078c6..944e4d7 100644 --- a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsAddressResolveContext.java +++ b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsAddressResolveContext.java @@ -19,7 +19,7 @@ import static io.netty.resolver.dns.DnsAddressDecoder.decodeAddress; import java.net.InetAddress; import java.net.UnknownHostException; -import java.util.ArrayList; +import java.util.Collections; import java.util.List; import io.netty.channel.EventLoop; @@ -31,13 +31,16 @@ final class DnsAddressResolveContext extends DnsResolveContext<InetAddress> { private final DnsCache resolveCache; private final AuthoritativeDnsServerCache authoritativeDnsServerCache; + private final boolean completeEarlyIfPossible; DnsAddressResolveContext(DnsNameResolver parent, String hostname, DnsRecord[] additionals, DnsServerAddressStream nameServerAddrs, DnsCache resolveCache, - AuthoritativeDnsServerCache authoritativeDnsServerCache) { + AuthoritativeDnsServerCache authoritativeDnsServerCache, + boolean completeEarlyIfPossible) { super(parent, hostname, DnsRecord.CLASS_IN, parent.resolveRecordTypes(), additionals, nameServerAddrs); this.resolveCache = resolveCache; this.authoritativeDnsServerCache = authoritativeDnsServerCache; + this.completeEarlyIfPossible = completeEarlyIfPossible; } @Override @@ -46,7 +49,7 @@ final class DnsAddressResolveContext extends DnsResolveContext<InetAddress> { DnsRecord[] additionals, DnsServerAddressStream nameServerAddrs) { return new DnsAddressResolveContext(parent, hostname, additionals, nameServerAddrs, resolveCache, - authoritativeDnsServerCache); + authoritativeDnsServerCache, completeEarlyIfPossible); } @Override @@ -56,27 +59,19 @@ final class DnsAddressResolveContext extends DnsResolveContext<InetAddress> { @Override List<InetAddress> filterResults(List<InetAddress> unfiltered) { - final Class<? extends InetAddress> inetAddressType = parent.preferredAddressType().addressType(); - final int size = unfiltered.size(); - int numExpected = 0; - for (int i = 0; i < size; i++) { - InetAddress address = unfiltered.get(i); - if (inetAddressType.isInstance(address)) { - numExpected++; - } - } - if (numExpected == size || numExpected == 0) { - // If all the results are the preferred type, or none of them are, then we don't need to do any filtering. - return unfiltered; - } - List<InetAddress> filtered = new ArrayList<InetAddress>(numExpected); - for (int i = 0; i < size; i++) { - InetAddress address = unfiltered.get(i); - if (inetAddressType.isInstance(address)) { - filtered.add(address); - } - } - return filtered; + Collections.sort(unfiltered, PreferredAddressTypeComparator.comparator(parent.preferredAddressType())); + return unfiltered; + } + + @Override + boolean isCompleteEarly(InetAddress resolved) { + return completeEarlyIfPossible && parent.preferredAddressType().addressType() == resolved.getClass(); + } + + @Override + boolean isDuplicateAllowed() { + // We don't want include duplicates to mimic JDK behaviour. + return false; } @Override diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsAddressResolverGroup.java b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsAddressResolverGroup.java index 303c42a..69ece07 100644 --- a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsAddressResolverGroup.java +++ b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsAddressResolverGroup.java @@ -18,7 +18,6 @@ package io.netty.resolver.dns; import io.netty.channel.ChannelFactory; import io.netty.channel.EventLoop; -import io.netty.channel.ReflectiveChannelFactory; import io.netty.channel.socket.DatagramChannel; import io.netty.resolver.AddressResolver; import io.netty.resolver.AddressResolverGroup; @@ -27,7 +26,6 @@ import io.netty.resolver.NameResolver; import io.netty.util.concurrent.EventExecutor; import io.netty.util.concurrent.Promise; import io.netty.util.internal.StringUtil; -import io.netty.util.internal.UnstableApi; import java.net.InetAddress; import java.net.InetSocketAddress; @@ -39,7 +37,6 @@ import static io.netty.util.internal.PlatformDependent.newConcurrentHashMap; /** * A {@link AddressResolverGroup} of {@link DnsNameResolver}s. */ -@UnstableApi public class DnsAddressResolverGroup extends AddressResolverGroup<InetSocketAddress> { private final DnsNameResolverBuilder dnsResolverBuilder; diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsCache.java b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsCache.java index 28e8989..1ddaa53 100644 --- a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsCache.java +++ b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsCache.java @@ -17,7 +17,6 @@ package io.netty.resolver.dns; import io.netty.channel.EventLoop; import io.netty.handler.codec.dns.DnsRecord; -import io.netty.util.internal.UnstableApi; import java.net.InetAddress; import java.util.List; @@ -25,7 +24,6 @@ import java.util.List; /** * A cache for DNS resolution entries. */ -@UnstableApi public interface DnsCache { /** diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsCacheEntry.java b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsCacheEntry.java index 129402b..25eaec0 100644 --- a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsCacheEntry.java +++ b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsCacheEntry.java @@ -15,14 +15,11 @@ */ package io.netty.resolver.dns; -import io.netty.util.internal.UnstableApi; - import java.net.InetAddress; /** * Represents the results from a previous DNS query which can be cached. */ -@UnstableApi public interface DnsCacheEntry { /** * Get the resolved address. diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsCnameCache.java b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsCnameCache.java index 820ef67..0c1b370 100644 --- a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsCnameCache.java +++ b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsCnameCache.java @@ -16,12 +16,10 @@ package io.netty.resolver.dns; import io.netty.channel.EventLoop; -import io.netty.util.internal.UnstableApi; /** * A cache for {@code CNAME}s. */ -@UnstableApi public interface DnsCnameCache { /** diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolver.java b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolver.java index c8384ff..625320d 100644 --- a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolver.java +++ b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolver.java @@ -31,7 +31,9 @@ import io.netty.channel.ChannelPromise; import io.netty.channel.EventLoop; import io.netty.channel.FixedRecvByteBufAllocator; import io.netty.channel.socket.DatagramChannel; +import io.netty.channel.socket.DatagramPacket; import io.netty.channel.socket.InternetProtocolFamily; +import io.netty.channel.socket.SocketChannel; import io.netty.handler.codec.dns.DatagramDnsQueryEncoder; import io.netty.handler.codec.dns.DatagramDnsResponse; import io.netty.handler.codec.dns.DatagramDnsResponseDecoder; @@ -41,12 +43,13 @@ import io.netty.handler.codec.dns.DnsRawRecord; import io.netty.handler.codec.dns.DnsRecord; import io.netty.handler.codec.dns.DnsRecordType; import io.netty.handler.codec.dns.DnsResponse; +import io.netty.handler.codec.dns.TcpDnsQueryEncoder; +import io.netty.handler.codec.dns.TcpDnsResponseDecoder; import io.netty.resolver.HostsFileEntries; import io.netty.resolver.HostsFileEntriesResolver; import io.netty.resolver.InetNameResolver; import io.netty.resolver.ResolvedAddressTypes; import io.netty.util.NetUtil; -import io.netty.util.ReferenceCountUtil; import io.netty.util.concurrent.EventExecutor; import io.netty.util.concurrent.FastThreadLocal; import io.netty.util.concurrent.Future; @@ -55,7 +58,6 @@ import io.netty.util.concurrent.Promise; import io.netty.util.internal.EmptyArrays; import io.netty.util.internal.PlatformDependent; import io.netty.util.internal.StringUtil; -import io.netty.util.internal.UnstableApi; import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; @@ -65,10 +67,14 @@ import java.net.Inet4Address; import java.net.Inet6Address; import java.net.InetAddress; import java.net.InetSocketAddress; +import java.net.NetworkInterface; +import java.net.SocketAddress; +import java.net.SocketException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; +import java.util.Enumeration; import java.util.Iterator; import java.util.List; @@ -80,7 +86,6 @@ import static io.netty.util.internal.ObjectUtil.checkPositive; /** * A DNS-based {@link InetNameResolver}. */ -@UnstableApi public class DnsNameResolver extends InetNameResolver { private static final InternalLogger logger = InternalLoggerFactory.getInstance(DnsNameResolver.class); @@ -109,7 +114,7 @@ public class DnsNameResolver extends InetNameResolver { private static final int DEFAULT_NDOTS; static { - if (NetUtil.isIpV4StackPreferred()) { + if (NetUtil.isIpV4StackPreferred() || !anyInterfaceSupportsIpV6()) { DEFAULT_RESOLVE_ADDRESS_TYPES = ResolvedAddressTypes.IPV4_ONLY; LOCALHOST_ADDRESS = NetUtil.LOCALHOST4; } else { @@ -145,20 +150,66 @@ public class DnsNameResolver extends InetNameResolver { DEFAULT_NDOTS = ndots; } + /** + * Returns {@code true} if any {@link NetworkInterface} supports {@code IPv6}, {@code false} otherwise. + */ + private static boolean anyInterfaceSupportsIpV6() { + try { + Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces(); + while (interfaces.hasMoreElements()) { + NetworkInterface iface = interfaces.nextElement(); + Enumeration<InetAddress> addresses = iface.getInetAddresses(); + while (addresses.hasMoreElements()) { + if (addresses.nextElement() instanceof Inet6Address) { + return true; + } + } + } + } catch (SocketException e) { + logger.debug("Unable to detect if any interface supports IPv6, assuming IPv4-only", e); + // ignore + } + return false; + } + @SuppressWarnings("unchecked") private static List<String> getSearchDomainsHack() throws Exception { - // This code on Java 9+ yields a warning about illegal reflective access that will be denied in - // a future release. There doesn't seem to be a better way to get search domains for Windows yet. - Class<?> configClass = Class.forName("sun.net.dns.ResolverConfiguration"); - Method open = configClass.getMethod("open"); - Method nameservers = configClass.getMethod("searchlist"); - Object instance = open.invoke(null); - - return (List<String>) nameservers.invoke(instance); + // Only try if not using Java9 and later + // See https://github.com/netty/netty/issues/9500 + if (PlatformDependent.javaVersion() < 9) { + // This code on Java 9+ yields a warning about illegal reflective access that will be denied in + // a future release. There doesn't seem to be a better way to get search domains for Windows yet. + Class<?> configClass = Class.forName("sun.net.dns.ResolverConfiguration"); + Method open = configClass.getMethod("open"); + Method nameservers = configClass.getMethod("searchlist"); + Object instance = open.invoke(null); + + return (List<String>) nameservers.invoke(instance); + } + return Collections.emptyList(); } - private static final DatagramDnsResponseDecoder DECODER = new DatagramDnsResponseDecoder(); - private static final DatagramDnsQueryEncoder ENCODER = new DatagramDnsQueryEncoder(); + private static final DatagramDnsResponseDecoder DATAGRAM_DECODER = new DatagramDnsResponseDecoder() { + @Override + protected DnsResponse decodeResponse(ChannelHandlerContext ctx, DatagramPacket packet) throws Exception { + DnsResponse response = super.decodeResponse(ctx, packet); + if (packet.content().isReadable()) { + // If there is still something to read we did stop parsing because of a truncated message. + // This can happen if we enabled EDNS0 but our MTU is not big enough to handle all the + // data. + response.setTruncated(true); + + if (logger.isDebugEnabled()) { + logger.debug( + "{} RECEIVED: UDP truncated packet received, consider adjusting maxPayloadSize for the {}.", + ctx.channel(), StringUtil.simpleClassName(DnsNameResolver.class)); + } + } + return response; + } + }; + private static final DatagramDnsQueryEncoder DATAGRAM_ENCODER = new DatagramDnsQueryEncoder(); + private static final TcpDnsQueryEncoder TCP_ENCODER = new TcpDnsQueryEncoder(); final Future<Channel> channelFuture; final Channel ch; @@ -202,6 +253,8 @@ public class DnsNameResolver extends InetNameResolver { private final DnsRecordType[] resolveRecordTypes; private final boolean decodeIdn; private final DnsQueryLifecycleObserverFactory dnsQueryLifecycleObserverFactory; + private final boolean completeOncePreferredResolved; + private final ChannelFactory<? extends SocketChannel> socketChannelFactory; /** * Creates a new DNS-based name resolver that communicates with the specified list of DNS servers. @@ -300,15 +353,16 @@ public class DnsNameResolver extends InetNameResolver { String[] searchDomains, int ndots, boolean decodeIdn) { - this(eventLoop, channelFactory, resolveCache, NoopDnsCnameCache.INSTANCE, authoritativeDnsServerCache, + this(eventLoop, channelFactory, null, resolveCache, NoopDnsCnameCache.INSTANCE, authoritativeDnsServerCache, dnsQueryLifecycleObserverFactory, queryTimeoutMillis, resolvedAddressTypes, recursionDesired, maxQueriesPerResolve, traceEnabled, maxPayloadSize, optResourceEnabled, hostsFileEntriesResolver, - dnsServerAddressStreamProvider, searchDomains, ndots, decodeIdn); + dnsServerAddressStreamProvider, searchDomains, ndots, decodeIdn, false); } DnsNameResolver( EventLoop eventLoop, ChannelFactory<? extends DatagramChannel> channelFactory, + ChannelFactory<? extends SocketChannel> socketChannelFactory, final DnsCache resolveCache, final DnsCnameCache cnameCache, final AuthoritativeDnsServerCache authoritativeDnsServerCache, @@ -324,7 +378,8 @@ public class DnsNameResolver extends InetNameResolver { DnsServerAddressStreamProvider dnsServerAddressStreamProvider, String[] searchDomains, int ndots, - boolean decodeIdn) { + boolean decodeIdn, + boolean completeOncePreferredResolved) { super(eventLoop); this.queryTimeoutMillis = checkPositive(queryTimeoutMillis, "queryTimeoutMillis"); this.resolvedAddressTypes = resolvedAddressTypes != null ? resolvedAddressTypes : DEFAULT_RESOLVE_ADDRESS_TYPES; @@ -346,7 +401,8 @@ public class DnsNameResolver extends InetNameResolver { this.searchDomains = searchDomains != null ? searchDomains.clone() : DEFAULT_SEARCH_DOMAINS; this.ndots = ndots >= 0 ? ndots : DEFAULT_NDOTS; this.decodeIdn = decodeIdn; - + this.completeOncePreferredResolved = completeOncePreferredResolved; + this.socketChannelFactory = socketChannelFactory; switch (this.resolvedAddressTypes) { case IPV4_ONLY: supportsAAAARecords = false; @@ -386,8 +442,8 @@ public class DnsNameResolver extends InetNameResolver { final DnsResponseHandler responseHandler = new DnsResponseHandler(executor().<Channel>newPromise()); b.handler(new ChannelInitializer<DatagramChannel>() { @Override - protected void initChannel(DatagramChannel ch) throws Exception { - ch.pipeline().addLast(DECODER, ENCODER, responseHandler); + protected void initChannel(DatagramChannel ch) { + ch.pipeline().addLast(DATAGRAM_ENCODER, DATAGRAM_DECODER, responseHandler); } }); @@ -826,7 +882,7 @@ public class DnsNameResolver extends InetNameResolver { } if (!doResolveCached(hostname, additionals, promise, resolveCache)) { - doResolveUncached(hostname, additionals, promise, resolveCache); + doResolveUncached(hostname, additionals, promise, resolveCache, true); } } @@ -861,22 +917,28 @@ public class DnsNameResolver extends InetNameResolver { static <T> void trySuccess(Promise<T> promise, T result) { if (!promise.trySuccess(result)) { - logger.warn("Failed to notify success ({}) to a promise: {}", result, promise); + // There is nothing really wrong with not be able to notify the promise as we may have raced here because + // of multiple queries that have been executed. Log it with trace level anyway just in case the user + // wants to better understand what happened. + logger.trace("Failed to notify success ({}) to a promise: {}", result, promise); } } private static void tryFailure(Promise<?> promise, Throwable cause) { if (!promise.tryFailure(cause)) { - logger.warn("Failed to notify failure to a promise: {}", promise, cause); + // There is nothing really wrong with not be able to notify the promise as we may have raced here because + // of multiple queries that have been executed. Log it with trace level anyway just in case the user + // wants to better understand what happened. + logger.trace("Failed to notify failure to a promise: {}", promise, cause); } } private void doResolveUncached(String hostname, DnsRecord[] additionals, final Promise<InetAddress> promise, - DnsCache resolveCache) { + DnsCache resolveCache, boolean completeEarlyIfPossible) { final Promise<List<InetAddress>> allPromise = executor().newPromise(); - doResolveAllUncached(hostname, additionals, allPromise, resolveCache); + doResolveAllUncached(hostname, additionals, allPromise, resolveCache, true); allPromise.addListener(new FutureListener<List<InetAddress>>() { @Override public void operationComplete(Future<List<InetAddress>> future) { @@ -923,7 +985,7 @@ public class DnsNameResolver extends InetNameResolver { } if (!doResolveAllCached(hostname, additionals, promise, resolveCache, resolvedInternetProtocolFamilies)) { - doResolveAllUncached(hostname, additionals, promise, resolveCache); + doResolveAllUncached(hostname, additionals, promise, resolveCache, completeOncePreferredResolved); } } @@ -966,33 +1028,35 @@ public class DnsNameResolver extends InetNameResolver { private void doResolveAllUncached(final String hostname, final DnsRecord[] additionals, final Promise<List<InetAddress>> promise, - final DnsCache resolveCache) { + final DnsCache resolveCache, + final boolean completeEarlyIfPossible) { // Call doResolveUncached0(...) in the EventLoop as we may need to submit multiple queries which would need // to submit multiple Runnable at the end if we are not already on the EventLoop. EventExecutor executor = executor(); if (executor.inEventLoop()) { - doResolveAllUncached0(hostname, additionals, promise, resolveCache); + doResolveAllUncached0(hostname, additionals, promise, resolveCache, completeEarlyIfPossible); } else { executor.execute(new Runnable() { @Override public void run() { - doResolveAllUncached0(hostname, additionals, promise, resolveCache); + doResolveAllUncached0(hostname, additionals, promise, resolveCache, completeEarlyIfPossible); } }); } } private void doResolveAllUncached0(String hostname, - DnsRecord[] additionals, - Promise<List<InetAddress>> promise, - DnsCache resolveCache) { + DnsRecord[] additionals, + Promise<List<InetAddress>> promise, + DnsCache resolveCache, + boolean completeEarlyIfPossible) { assert executor().inEventLoop(); final DnsServerAddressStream nameServerAddrs = dnsServerAddressStreamProvider.nameServerAddressStream(hostname); - new DnsAddressResolveContext(this, hostname, additionals, nameServerAddrs, - resolveCache, authoritativeDnsServerCache).resolve(promise); + new DnsAddressResolveContext(this, hostname, additionals, nameServerAddrs, resolveCache, + authoritativeDnsServerCache, completeEarlyIfPossible).resolve(promise); } private static String hostname(String inetHost) { @@ -1105,7 +1169,7 @@ public class DnsNameResolver extends InetNameResolver { final Promise<AddressedEnvelope<DnsResponse, InetSocketAddress>> castPromise = cast( checkNotNull(promise, "promise")); try { - new DnsQueryContext(this, nameServerAddr, question, additionals, castPromise) + new DatagramDnsQueryContext(this, nameServerAddr, question, additionals, castPromise) .query(flush, writePromise); return castPromise; } catch (Exception e) { @@ -1132,24 +1196,107 @@ public class DnsNameResolver extends InetNameResolver { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { - try { - final DatagramDnsResponse res = (DatagramDnsResponse) msg; - final int queryId = res.id(); + final DatagramDnsResponse res = (DatagramDnsResponse) msg; + final int queryId = res.id(); - if (logger.isDebugEnabled()) { - logger.debug("{} RECEIVED: [{}: {}], {}", ch, queryId, res.sender(), res); - } + if (logger.isDebugEnabled()) { + logger.debug("{} RECEIVED: UDP [{}: {}], {}", ch, queryId, res.sender(), res); + } - final DnsQueryContext qCtx = queryContextManager.get(res.sender(), queryId); - if (qCtx == null) { - logger.warn("{} Received a DNS response with an unknown ID: {}", ch, queryId); - return; - } + final DnsQueryContext qCtx = queryContextManager.get(res.sender(), queryId); + if (qCtx == null) { + logger.warn("{} Received a DNS response with an unknown ID: {}", ch, queryId); + res.release(); + return; + } + // Check if the response was truncated and if we can fallback to TCP to retry. + if (!res.isTruncated() || socketChannelFactory == null) { qCtx.finish(res); - } finally { - ReferenceCountUtil.safeRelease(msg); + return; } + + Bootstrap bs = new Bootstrap(); + bs.option(ChannelOption.SO_REUSEADDR, true) + .group(executor()) + .channelFactory(socketChannelFactory) + .handler(TCP_ENCODER); + bs.connect(res.sender()).addListener(new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture future) { + if (!future.isSuccess()) { + if (logger.isDebugEnabled()) { + logger.debug("{} Unable to fallback to TCP [{}]", queryId, future.cause()); + } + + // TCP fallback failed, just use the truncated response. + qCtx.finish(res); + return; + } + final Channel channel = future.channel(); + + Promise<AddressedEnvelope<DnsResponse, InetSocketAddress>> promise = + channel.eventLoop().newPromise(); + final TcpDnsQueryContext tcpCtx = new TcpDnsQueryContext(DnsNameResolver.this, channel, + (InetSocketAddress) channel.remoteAddress(), qCtx.question(), + EMPTY_ADDITIONALS, promise); + + channel.pipeline().addLast(new TcpDnsResponseDecoder()); + channel.pipeline().addLast(new ChannelInboundHandlerAdapter() { + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + Channel channel = ctx.channel(); + DnsResponse response = (DnsResponse) msg; + int queryId = response.id(); + + if (logger.isDebugEnabled()) { + logger.debug("{} RECEIVED: TCP [{}: {}], {}", channel, queryId, + channel.remoteAddress(), response); + } + + DnsQueryContext foundCtx = queryContextManager.get(res.sender(), queryId); + if (foundCtx == tcpCtx) { + tcpCtx.finish(new AddressedEnvelopeAdapter( + (InetSocketAddress) ctx.channel().remoteAddress(), + (InetSocketAddress) ctx.channel().localAddress(), + response)); + } else { + response.release(); + tcpCtx.tryFailure("Received TCP response with unexpected ID", null, false); + logger.warn("{} Received a DNS response with an unexpected ID: {}", + channel, queryId); + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + if (tcpCtx.tryFailure("TCP fallback error", cause, false) && logger.isDebugEnabled()) { + logger.debug("{} Error during processing response: TCP [{}: {}]", + ctx.channel(), queryId, + ctx.channel().remoteAddress(), cause); + } + } + }); + + promise.addListener( + new FutureListener<AddressedEnvelope<DnsResponse, InetSocketAddress>>() { + @Override + public void operationComplete( + Future<AddressedEnvelope<DnsResponse, InetSocketAddress>> future) { + channel.close(); + + if (future.isSuccess()) { + qCtx.finish(future.getNow()); + res.release(); + } else { + // TCP fallback failed, just use the truncated response. + qCtx.finish(res); + } + } + }); + tcpCtx.query(true, future.channel().newPromise()); + } + }); } @Override @@ -1160,7 +1307,116 @@ public class DnsNameResolver extends InetNameResolver { @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { - logger.warn("{} Unexpected exception: ", ch, cause); + logger.warn("{} Unexpected exception: ", ctx.channel(), cause); + } + } + + private static final class AddressedEnvelopeAdapter implements AddressedEnvelope<DnsResponse, InetSocketAddress> { + private final InetSocketAddress sender; + private final InetSocketAddress recipient; + private final DnsResponse response; + + AddressedEnvelopeAdapter(InetSocketAddress sender, InetSocketAddress recipient, DnsResponse response) { + this.sender = sender; + this.recipient = recipient; + this.response = response; + } + + @Override + public DnsResponse content() { + return response; + } + + @Override + public InetSocketAddress sender() { + return sender; + } + + @Override + public InetSocketAddress recipient() { + return recipient; + } + + @Override + public AddressedEnvelope<DnsResponse, InetSocketAddress> retain() { + response.retain(); + return this; + } + + @Override + public AddressedEnvelope<DnsResponse, InetSocketAddress> retain(int increment) { + response.retain(increment); + return this; + } + + @Override + public AddressedEnvelope<DnsResponse, InetSocketAddress> touch() { + response.touch(); + return this; + } + + @Override + public AddressedEnvelope<DnsResponse, InetSocketAddress> touch(Object hint) { + response.touch(hint); + return this; + } + + @Override + public int refCnt() { + return response.refCnt(); + } + + @Override + public boolean release() { + return response.release(); + } + + @Override + public boolean release(int decrement) { + return response.release(decrement); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + if (!(obj instanceof AddressedEnvelope)) { + return false; + } + + @SuppressWarnings("unchecked") + final AddressedEnvelope<?, SocketAddress> that = (AddressedEnvelope<?, SocketAddress>) obj; + if (sender() == null) { + if (that.sender() != null) { + return false; + } + } else if (!sender().equals(that.sender())) { + return false; + } + + if (recipient() == null) { + if (that.recipient() != null) { + return false; + } + } else if (!recipient().equals(that.recipient())) { + return false; + } + + return response.equals(obj); + } + + @Override + public int hashCode() { + int hashCode = response.hashCode(); + if (sender() != null) { + hashCode = hashCode * 31 + sender().hashCode(); + } + if (recipient() != null) { + hashCode = hashCode * 31 + recipient().hashCode(); + } + return hashCode; } } } diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverBuilder.java b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverBuilder.java index 06266cc..4d39612 100644 --- a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverBuilder.java +++ b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverBuilder.java @@ -20,9 +20,10 @@ import io.netty.channel.EventLoop; import io.netty.channel.ReflectiveChannelFactory; import io.netty.channel.socket.DatagramChannel; import io.netty.channel.socket.InternetProtocolFamily; +import io.netty.channel.socket.SocketChannel; import io.netty.resolver.HostsFileEntriesResolver; import io.netty.resolver.ResolvedAddressTypes; -import io.netty.util.internal.UnstableApi; +import io.netty.util.concurrent.Future; import java.util.ArrayList; import java.util.Arrays; @@ -35,10 +36,10 @@ import static io.netty.util.internal.ObjectUtil.intValue; /** * A {@link DnsNameResolver} builder. */ -@UnstableApi public final class DnsNameResolverBuilder { private EventLoop eventLoop; private ChannelFactory<? extends DatagramChannel> channelFactory; + private ChannelFactory<? extends SocketChannel> socketChannelFactory; private DnsCache resolveCache; private DnsCnameCache cnameCache; private AuthoritativeDnsServerCache authoritativeDnsServerCache; @@ -47,6 +48,7 @@ public final class DnsNameResolverBuilder { private Integer negativeTtl; private long queryTimeoutMillis = 5000; private ResolvedAddressTypes resolvedAddressTypes = DnsNameResolver.DEFAULT_RESOLVE_ADDRESS_TYPES; + private boolean completeOncePreferredResolved; private boolean recursionDesired = true; private int maxQueriesPerResolve = 16; private boolean traceEnabled; @@ -113,6 +115,35 @@ public final class DnsNameResolverBuilder { return channelFactory(new ReflectiveChannelFactory<DatagramChannel>(channelType)); } + /** + * Sets the {@link ChannelFactory} that will create a {@link SocketChannel} for + * <a href="https://tools.ietf.org/html/rfc7766">TCP fallback</a> if needed. + * + * @param channelFactory the {@link ChannelFactory} or {@code null} + * if <a href="https://tools.ietf.org/html/rfc7766">TCP fallback</a> should not be supported. + * @return {@code this} + */ + public DnsNameResolverBuilder socketChannelFactory(ChannelFactory<? extends SocketChannel> channelFactory) { + this.socketChannelFactory = channelFactory; + return this; + } + + /** + * Sets the {@link ChannelFactory} as a {@link ReflectiveChannelFactory} of this type for + * <a href="https://tools.ietf.org/html/rfc7766">TCP fallback</a> if needed. + * Use as an alternative to {@link #socketChannelFactory(ChannelFactory)}. + * + * @param channelType the type or {@code null} if <a href="https://tools.ietf.org/html/rfc7766">TCP fallback</a> + * should not be supported. + * @return {@code this} + */ + public DnsNameResolverBuilder socketChannelType(Class<? extends SocketChannel> channelType) { + if (channelType == null) { + return socketChannelFactory(null); + } + return socketChannelFactory(new ReflectiveChannelFactory<SocketChannel>(channelType)); + } + /** * Sets the cache for resolution results. * @@ -253,6 +284,18 @@ public final class DnsNameResolverBuilder { return this; } + /** + * If {@code true} {@link DnsNameResolver#resolveAll(String)} will notify the returned {@link Future} as + * soon as all queries for the preferred address-type are complete. + * + * @param completeOncePreferredResolved {@code true} to enable, {@code false} to disable. + * @return {@code this} + */ + public DnsNameResolverBuilder completeOncePreferredResolved(boolean completeOncePreferredResolved) { + this.completeOncePreferredResolved = completeOncePreferredResolved; + return this; + } + /** * Sets if this resolver has to send a DNS query with the RD (recursion desired) flag set. * @@ -430,6 +473,7 @@ public final class DnsNameResolverBuilder { return new DnsNameResolver( eventLoop, channelFactory, + socketChannelFactory, resolveCache, cnameCache, authoritativeDnsServerCache, @@ -445,7 +489,8 @@ public final class DnsNameResolverBuilder { dnsServerAddressStreamProvider, searchDomains, ndots, - decodeIdn); + decodeIdn, + completeOncePreferredResolved); } /** @@ -464,6 +509,10 @@ public final class DnsNameResolverBuilder { copiedBuilder.channelFactory(channelFactory); } + if (socketChannelFactory != null) { + copiedBuilder.socketChannelFactory(socketChannelFactory); + } + if (resolveCache != null) { copiedBuilder.resolveCache(resolveCache); } @@ -506,6 +555,7 @@ public final class DnsNameResolverBuilder { copiedBuilder.ndots(ndots); copiedBuilder.decodeIdn(decodeIdn); + copiedBuilder.completeOncePreferredResolved(completeOncePreferredResolved); return copiedBuilder; } diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverException.java b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverException.java index 77e3474..5cbe283 100644 --- a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverException.java +++ b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverException.java @@ -18,14 +18,12 @@ package io.netty.resolver.dns; import io.netty.handler.codec.dns.DnsQuestion; import io.netty.util.internal.EmptyArrays; import io.netty.util.internal.ObjectUtil; -import io.netty.util.internal.UnstableApi; import java.net.InetSocketAddress; /** * A {@link RuntimeException} raised when {@link DnsNameResolver} failed to perform a successful query. */ -@UnstableApi public class DnsNameResolverException extends RuntimeException { private static final long serialVersionUID = -8826717909627131850L; diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverTimeoutException.java b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverTimeoutException.java index e1ad13d..7d3e721 100644 --- a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverTimeoutException.java +++ b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverTimeoutException.java @@ -16,7 +16,6 @@ package io.netty.resolver.dns; import io.netty.handler.codec.dns.DnsQuestion; -import io.netty.util.internal.UnstableApi; import java.net.InetSocketAddress; @@ -24,7 +23,6 @@ import java.net.InetSocketAddress; * A {@link DnsNameResolverException} raised when {@link DnsNameResolver} failed to perform a successful query because * of an timeout. In this case you may want to retry the operation. */ -@UnstableApi public final class DnsNameResolverTimeoutException extends DnsNameResolverException { private static final long serialVersionUID = -8826717969627131854L; diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsQueryContext.java b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsQueryContext.java index 08bbcb6..d476c1f 100644 --- a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsQueryContext.java +++ b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsQueryContext.java @@ -20,7 +20,6 @@ import io.netty.channel.Channel; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelPromise; -import io.netty.handler.codec.dns.DatagramDnsQuery; import io.netty.handler.codec.dns.AbstractDnsOptPseudoRrRecord; import io.netty.handler.codec.dns.DnsQuery; import io.netty.handler.codec.dns.DnsQuestion; @@ -40,7 +39,7 @@ import java.util.concurrent.TimeUnit; import static io.netty.util.internal.ObjectUtil.checkNotNull; -final class DnsQueryContext implements FutureListener<AddressedEnvelope<DnsResponse, InetSocketAddress>> { +abstract class DnsQueryContext implements FutureListener<AddressedEnvelope<DnsResponse, InetSocketAddress>> { private static final InternalLogger logger = InternalLoggerFactory.getInstance(DnsQueryContext.class); @@ -89,10 +88,18 @@ final class DnsQueryContext implements FutureListener<AddressedEnvelope<DnsRespo return question; } + DnsNameResolver parent() { + return parent; + } + + protected abstract DnsQuery newQuery(int id); + protected abstract Channel channel(); + protected abstract String protocol(); + void query(boolean flush, ChannelPromise writePromise) { final DnsQuestion question = question(); final InetSocketAddress nameServerAddr = nameServerAddr(); - final DatagramDnsQuery query = new DatagramDnsQuery(null, nameServerAddr, id); + final DnsQuery query = newQuery(id); query.setRecursionDesired(recursionDesired); @@ -107,7 +114,7 @@ final class DnsQueryContext implements FutureListener<AddressedEnvelope<DnsRespo } if (logger.isDebugEnabled()) { - logger.debug("{} WRITE: [{}: {}], {}", parent.ch, id, nameServerAddr, question); + logger.debug("{} WRITE: {}, [{}: {}], {}", channel(), protocol(), id, nameServerAddr, question); } sendQuery(query, flush, writePromise); @@ -136,8 +143,8 @@ final class DnsQueryContext implements FutureListener<AddressedEnvelope<DnsRespo } private void writeQuery(final DnsQuery query, final boolean flush, final ChannelPromise writePromise) { - final ChannelFuture writeFuture = flush ? parent.ch.writeAndFlush(query, writePromise) : - parent.ch.write(query, writePromise); + final ChannelFuture writeFuture = flush ? channel().writeAndFlush(query, writePromise) : + channel().write(query, writePromise); if (writeFuture.isDone()) { onQueryWriteCompletion(writeFuture); } else { @@ -152,7 +159,7 @@ final class DnsQueryContext implements FutureListener<AddressedEnvelope<DnsRespo private void onQueryWriteCompletion(ChannelFuture writeFuture) { if (!writeFuture.isSuccess()) { - setFailure("failed to send a query", writeFuture.cause()); + tryFailure("failed to send a query via " + protocol(), writeFuture.cause(), false); return; } @@ -167,39 +174,37 @@ final class DnsQueryContext implements FutureListener<AddressedEnvelope<DnsRespo return; } - setFailure("query timed out after " + queryTimeoutMillis + " milliseconds", null); + tryFailure("query via " + protocol() + " timed out after " + + queryTimeoutMillis + " milliseconds", null, true); } }, queryTimeoutMillis, TimeUnit.MILLISECONDS); } } + /** + * Takes ownership of passed envelope + */ void finish(AddressedEnvelope<? extends DnsResponse, InetSocketAddress> envelope) { final DnsResponse res = envelope.content(); if (res.count(DnsSection.QUESTION) != 1) { logger.warn("Received a DNS response with invalid number of questions: {}", envelope); - return; - } - - if (!question().equals(res.recordAt(DnsSection.QUESTION))) { + } else if (!question().equals(res.recordAt(DnsSection.QUESTION))) { logger.warn("Received a mismatching DNS response: {}", envelope); - return; + } else if (trySuccess(envelope)) { + return; // Ownership transferred, don't release } - - setSuccess(envelope); + envelope.release(); } - private void setSuccess(AddressedEnvelope<? extends DnsResponse, InetSocketAddress> envelope) { - Promise<AddressedEnvelope<DnsResponse, InetSocketAddress>> promise = this.promise; - @SuppressWarnings("unchecked") - AddressedEnvelope<DnsResponse, InetSocketAddress> castResponse = - (AddressedEnvelope<DnsResponse, InetSocketAddress>) envelope.retain(); - if (!promise.trySuccess(castResponse)) { - // We failed to notify the promise as it was failed before, thus we need to release the envelope - envelope.release(); - } + @SuppressWarnings("unchecked") + private boolean trySuccess(AddressedEnvelope<? extends DnsResponse, InetSocketAddress> envelope) { + return promise.trySuccess((AddressedEnvelope<DnsResponse, InetSocketAddress>) envelope); } - private void setFailure(String message, Throwable cause) { + boolean tryFailure(String message, Throwable cause, boolean timeout) { + if (promise.isDone()) { + return false; + } final InetSocketAddress nameServerAddr = nameServerAddr(); final StringBuilder buf = new StringBuilder(message.length() + 64); @@ -210,14 +215,14 @@ final class DnsQueryContext implements FutureListener<AddressedEnvelope<DnsRespo .append(" (no stack trace available)"); final DnsNameResolverException e; - if (cause == null) { + if (timeout) { // This was caused by an timeout so use DnsNameResolverTimeoutException to allow the user to // handle it special (like retry the query). e = new DnsNameResolverTimeoutException(nameServerAddr, question(), buf.toString()); } else { e = new DnsNameResolverException(nameServerAddr, question(), buf.toString(), cause); } - promise.tryFailure(e); + return promise.tryFailure(e); } @Override diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsQueryLifecycleObserver.java b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsQueryLifecycleObserver.java index 39ce19d..3ad2ebc 100644 --- a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsQueryLifecycleObserver.java +++ b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsQueryLifecycleObserver.java @@ -19,7 +19,6 @@ import io.netty.channel.ChannelFuture; import io.netty.handler.codec.dns.DnsQuestion; import io.netty.handler.codec.dns.DnsRecordType; import io.netty.handler.codec.dns.DnsResponseCode; -import io.netty.util.internal.UnstableApi; import java.net.InetSocketAddress; import java.util.List; @@ -43,7 +42,6 @@ import java.util.List; * return an object of type {@link DnsQueryLifecycleObserver}. Implementations may use this to build a query tree to * understand the "sub queries" generated by a single query. */ -@UnstableApi public interface DnsQueryLifecycleObserver { /** * The query has been written. diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsQueryLifecycleObserverFactory.java b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsQueryLifecycleObserverFactory.java index 6b5d822..afe36f9 100644 --- a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsQueryLifecycleObserverFactory.java +++ b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsQueryLifecycleObserverFactory.java @@ -16,12 +16,10 @@ package io.netty.resolver.dns; import io.netty.handler.codec.dns.DnsQuestion; -import io.netty.util.internal.UnstableApi; /** * Used to generate new instances of {@link DnsQueryLifecycleObserver}. */ -@UnstableApi public interface DnsQueryLifecycleObserverFactory { /** * Create a new instance of a {@link DnsQueryLifecycleObserver}. This will be called at the start of a new query. diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsRecordResolveContext.java b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsRecordResolveContext.java index e633eb8..93814e2 100644 --- a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsRecordResolveContext.java +++ b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsRecordResolveContext.java @@ -58,6 +58,16 @@ final class DnsRecordResolveContext extends DnsResolveContext<DnsRecord> { return unfiltered; } + @Override + boolean isCompleteEarly(DnsRecord resolved) { + return false; + } + + @Override + boolean isDuplicateAllowed() { + return true; + } + @Override void cache(String hostname, DnsRecord[] additionals, DnsRecord result, DnsRecord convertedResult) { // Do not cache. diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsResolveContext.java b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsResolveContext.java index f625be9..38fbafc 100644 --- a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsResolveContext.java +++ b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsResolveContext.java @@ -39,6 +39,7 @@ import io.netty.util.concurrent.Promise; import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.PlatformDependent; import io.netty.util.internal.StringUtil; +import io.netty.util.internal.SuppressJava6Requirement; import io.netty.util.internal.ThrowableUtil; import java.net.InetAddress; @@ -62,25 +63,16 @@ import static java.lang.Math.min; abstract class DnsResolveContext<T> { - private static final FutureListener<AddressedEnvelope<DnsResponse, InetSocketAddress>> RELEASE_RESPONSE = - new FutureListener<AddressedEnvelope<DnsResponse, InetSocketAddress>>() { - @Override - public void operationComplete(Future<AddressedEnvelope<DnsResponse, InetSocketAddress>> future) { - if (future.isSuccess()) { - future.getNow().release(); - } - } - }; private static final RuntimeException NXDOMAIN_QUERY_FAILED_EXCEPTION = ThrowableUtil.unknownStackTrace( - new RuntimeException("No answer found and NXDOMAIN response code returned"), + DnsResolveContextException.newStatic("No answer found and NXDOMAIN response code returned"), DnsResolveContext.class, "onResponse(..)"); private static final RuntimeException CNAME_NOT_FOUND_QUERY_FAILED_EXCEPTION = ThrowableUtil.unknownStackTrace( - new RuntimeException("No matching CNAME record found"), + DnsResolveContextException.newStatic("No matching CNAME record found"), DnsResolveContext.class, "onResponseCNAME(..)"); private static final RuntimeException NO_MATCHING_RECORD_QUERY_FAILED_EXCEPTION = ThrowableUtil.unknownStackTrace( - new RuntimeException("No matching record type found"), + DnsResolveContextException.newStatic("No matching record type found"), DnsResolveContext.class, "onResponseAorAAAA(..)"); private static final RuntimeException UNRECOGNIZED_TYPE_QUERY_FAILED_EXCEPTION = ThrowableUtil.unknownStackTrace( @@ -88,7 +80,7 @@ abstract class DnsResolveContext<T> { DnsResolveContext.class, "onResponse(..)"); private static final RuntimeException NAME_SERVERS_EXHAUSTED_EXCEPTION = ThrowableUtil.unknownStackTrace( - new RuntimeException("No name servers returned an answer"), + DnsResolveContextException.newStatic("No name servers returned an answer"), DnsResolveContext.class, "tryToFinishResolve(..)"); @@ -107,6 +99,7 @@ abstract class DnsResolveContext<T> { private List<T> finalResult; private int allowedQueries; private boolean triedCNAME; + private boolean completeEarly; DnsResolveContext(DnsNameResolver parent, String hostname, int dnsClass, DnsRecordType[] expectedTypes, @@ -125,6 +118,27 @@ abstract class DnsResolveContext<T> { allowedQueries = maxAllowedQueries; } + static final class DnsResolveContextException extends RuntimeException { + + private DnsResolveContextException(String message) { + super(message); + } + + @SuppressJava6Requirement(reason = "uses Java 7+ Exception.<init>(String, Throwable, boolean, boolean)" + + " but is guarded by version checks") + private DnsResolveContextException(String message, boolean shared) { + super(message, null, false, true); + assert shared; + } + + static DnsResolveContextException newStatic(String message) { + if (PlatformDependent.javaVersion() >= 7) { + return new DnsResolveContextException(message, true); + } + return new DnsResolveContextException(message); + } + } + /** * The {@link DnsCache} to use while resolving. */ @@ -165,6 +179,14 @@ abstract class DnsResolveContext<T> { */ abstract List<T> filterResults(List<T> unfiltered); + abstract boolean isCompleteEarly(T resolved); + + /** + * Returns {@code true} if we should allow duplicates in the result or {@code false} if no duplicates should + * be included. + */ + abstract boolean isDuplicateAllowed(); + /** * Caches a successful resolution. */ @@ -329,7 +351,8 @@ abstract class DnsResolveContext<T> { final boolean flush, final Promise<List<T>> promise, final Throwable cause) { - if (nameServerAddrStreamIndex >= nameServerAddrStream.size() || allowedQueries == 0 || promise.isCancelled()) { + if (completeEarly || nameServerAddrStreamIndex >= nameServerAddrStream.size() || + allowedQueries == 0 || promise.isCancelled()) { tryToFinishResolve(nameServerAddrStream, nameServerAddrStreamIndex, question, queryLifecycleObserver, promise, cause); return; @@ -456,7 +479,7 @@ abstract class DnsResolveContext<T> { public boolean clear(String hostname) { return authoritativeDnsServerCache.clear(hostname); } - }).resolve(resolverPromise); + }, false).resolve(resolverPromise); } } @@ -655,6 +678,7 @@ abstract class DnsResolveContext<T> { final int answerCount = response.count(DnsSection.ANSWER); boolean found = false; + boolean completeEarly = this.completeEarly; for (int i = 0; i < answerCount; i ++) { final DnsRecord r = response.recordAt(DnsSection.ANSWER, i); final DnsRecordType type = r.type(); @@ -695,19 +719,41 @@ abstract class DnsResolveContext<T> { continue; } + boolean shouldRelease = false; + // Check if we did determine we wanted to complete early before. If this is the case we want to not + // include the result + if (!completeEarly) { + completeEarly = isCompleteEarly(converted); + } + + // We want to ensure we do not have duplicates in finalResult as this may be unexpected. + // + // While using a LinkedHashSet or HashSet may sound like the perfect fit for this we will use an + // ArrayList here as duplicates should be found quite unfrequently in the wild and we dont want to pay + // for the extra memory copy and allocations in this cases later on. if (finalResult == null) { finalResult = new ArrayList<T>(8); + finalResult.add(converted); + } else if (isDuplicateAllowed() || !finalResult.contains(converted)) { + finalResult.add(converted); + } else { + shouldRelease = true; } - finalResult.add(converted); cache(hostname, additionals, r, converted); found = true; + if (shouldRelease) { + ReferenceCountUtil.release(converted); + } // Note that we do not break from the loop here, so we decode/cache all A/AAAA records. } if (cnames.isEmpty()) { if (found) { + if (completeEarly) { + this.completeEarly = true; + } queryLifecycleObserver.querySucceed(); return; } @@ -793,7 +839,7 @@ abstract class DnsResolveContext<T> { final Throwable cause) { // There are no queries left to try. - if (!queriesInProgress.isEmpty()) { + if (!completeEarly && !queriesInProgress.isEmpty()) { queryLifecycleObserver.queryCancelled(allowedQueries); // There are still some queries in process, we will try to notify once the next one finishes until @@ -839,22 +885,24 @@ abstract class DnsResolveContext<T> { } private void finishResolve(Promise<List<T>> promise, Throwable cause) { - if (!queriesInProgress.isEmpty()) { + // If completeEarly was true we still want to continue processing the queries to ensure we still put everything + // in the cache eventually. + if (!completeEarly && !queriesInProgress.isEmpty()) { // If there are queries in progress, we should cancel it because we already finished the resolution. for (Iterator<Future<AddressedEnvelope<DnsResponse, InetSocketAddress>>> i = queriesInProgress.iterator(); i.hasNext();) { Future<AddressedEnvelope<DnsResponse, InetSocketAddress>> f = i.next(); i.remove(); - if (!f.cancel(false)) { - f.addListener(RELEASE_RESPONSE); - } + f.cancel(false); } } if (finalResult != null) { - // Found at least one resolved record. - DnsNameResolver.trySuccess(promise, filterResults(finalResult)); + if (!promise.isDone()) { + // Found at least one resolved record. + DnsNameResolver.trySuccess(promise, filterResults(finalResult)); + } return; } @@ -1225,7 +1273,7 @@ abstract class DnsResolveContext<T> { void update(InetSocketAddress address, long ttl) { assert this.address == null || this.address.isUnresolved(); this.address = address; - this.ttl = min(ttl, ttl); + this.ttl = min(this.ttl, ttl); } void update(InetSocketAddress address) { diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsServerAddressStream.java b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsServerAddressStream.java index af014f9..c5d0453 100644 --- a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsServerAddressStream.java +++ b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsServerAddressStream.java @@ -16,14 +16,11 @@ package io.netty.resolver.dns; -import io.netty.util.internal.UnstableApi; - import java.net.InetSocketAddress; /** * An infinite stream of DNS server addresses. */ -@UnstableApi public interface DnsServerAddressStream { /** * Retrieves the next DNS server address from the stream. diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsServerAddressStreamProvider.java b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsServerAddressStreamProvider.java index 05285e9..e0384ea 100644 --- a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsServerAddressStreamProvider.java +++ b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsServerAddressStreamProvider.java @@ -15,8 +15,6 @@ */ package io.netty.resolver.dns; -import io.netty.util.internal.UnstableApi; - /** * Provides an opportunity to override which {@link DnsServerAddressStream} is used to resolve a specific hostname. * <p> @@ -24,7 +22,6 @@ import io.netty.util.internal.UnstableApi; * <a href="https://developer.apple.com/legacy/library/documentation/Darwin/Reference/ManPages/man5/resolver.5.html"> * /etc/resolver</a>. */ -@UnstableApi public interface DnsServerAddressStreamProvider { /** * Ask this provider for the name servers to query for {@code hostname}. diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsServerAddressStreamProviders.java b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsServerAddressStreamProviders.java index fbcd8cc..8df2492 100644 --- a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsServerAddressStreamProviders.java +++ b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsServerAddressStreamProviders.java @@ -16,47 +16,64 @@ package io.netty.resolver.dns; import io.netty.util.internal.PlatformDependent; -import io.netty.util.internal.UnstableApi; +import io.netty.util.internal.logging.InternalLogger; +import io.netty.util.internal.logging.InternalLoggerFactory; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.security.AccessController; +import java.security.PrivilegedAction; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; /** * Utility methods related to {@link DnsServerAddressStreamProvider}. */ -@UnstableApi public final class DnsServerAddressStreamProviders { - // We use 5 minutes which is the same as what OpenJDK is using in sun.net.dns.ResolverConfigurationImpl. - private static final long REFRESH_INTERVAL = TimeUnit.MINUTES.toNanos(5); - // TODO(scott): how is this done on Windows? This may require a JNI call to GetNetworkParams - // https://msdn.microsoft.com/en-us/library/aa365968(VS.85).aspx. - private static final DnsServerAddressStreamProvider DEFAULT_DNS_SERVER_ADDRESS_STREAM_PROVIDER = - new DnsServerAddressStreamProvider() { - private volatile DnsServerAddressStreamProvider currentProvider = provider(); - private final AtomicLong lastRefresh = new AtomicLong(System.nanoTime()); + private static final InternalLogger LOGGER = + InternalLoggerFactory.getInstance(DnsServerAddressStreamProviders.class); + private static final Constructor<? extends DnsServerAddressStreamProvider> STREAM_PROVIDER_CONSTRUCTOR; - @Override - public DnsServerAddressStream nameServerAddressStream(String hostname) { - long last = lastRefresh.get(); - DnsServerAddressStreamProvider current = currentProvider; - if (System.nanoTime() - last > REFRESH_INTERVAL) { - // This is slightly racy which means it will be possible still use the old configuration for a small - // amount of time, but that's ok. - if (lastRefresh.compareAndSet(last, System.nanoTime())) { - current = currentProvider = provider(); + static { + Constructor<? extends DnsServerAddressStreamProvider> constructor = null; + if (PlatformDependent.isOsx()) { + try { + // As MacOSDnsServerAddressStreamProvider is contained in another jar which depends on this jar + // we use reflection to use it if its on the classpath. + Object maybeProvider = AccessController.doPrivileged(new PrivilegedAction<Object>() { + @Override + public Object run() { + try { + return Class.forName( + "io.netty.resolver.dns.macos.MacOSDnsServerAddressStreamProvider", + true, + DnsServerAddressStreamProviders.class.getClassLoader()); + } catch (Throwable cause) { + return cause; + } + } + }); + if (maybeProvider instanceof Class) { + @SuppressWarnings("unchecked") + Class<? extends DnsServerAddressStreamProvider> providerClass = + (Class<? extends DnsServerAddressStreamProvider>) maybeProvider; + Method method = providerClass.getMethod("ensureAvailability"); + method.invoke(null); + constructor = providerClass.getConstructor(); + constructor.newInstance(); + } else if (!(maybeProvider instanceof ClassNotFoundException)) { + throw (Throwable) maybeProvider; } + } catch (Throwable cause) { + LOGGER.debug( + "Unable to use MacOSDnsServerAddressStreamProvider, fallback to system defaults", cause); + constructor = null; } - return current.nameServerAddressStream(hostname); } - - private DnsServerAddressStreamProvider provider() { - // If on windows just use the DefaultDnsServerAddressStreamProvider.INSTANCE as otherwise - // we will log some error which may be confusing. - return PlatformDependent.isWindows() ? DefaultDnsServerAddressStreamProvider.INSTANCE : - UnixResolverDnsServerAddressStreamProvider.parseSilently(); - } - }; + STREAM_PROVIDER_CONSTRUCTOR = constructor; + } private DnsServerAddressStreamProviders() { } @@ -69,6 +86,57 @@ public final class DnsServerAddressStreamProviders { * configuration. */ public static DnsServerAddressStreamProvider platformDefault() { - return DEFAULT_DNS_SERVER_ADDRESS_STREAM_PROVIDER; + if (STREAM_PROVIDER_CONSTRUCTOR != null) { + try { + return STREAM_PROVIDER_CONSTRUCTOR.newInstance(); + } catch (IllegalAccessException e) { + // ignore + } catch (InstantiationException e) { + // ignore + } catch (InvocationTargetException e) { + // ignore + } + } + return unixDefault(); + } + + public static DnsServerAddressStreamProvider unixDefault() { + return DefaultProviderHolder.DEFAULT_DNS_SERVER_ADDRESS_STREAM_PROVIDER; + } + + // We use a Holder class to only initialize DEFAULT_DNS_SERVER_ADDRESS_STREAM_PROVIDER if we really + // need it. + private static final class DefaultProviderHolder { + // We use 5 minutes which is the same as what OpenJDK is using in sun.net.dns.ResolverConfigurationImpl. + private static final long REFRESH_INTERVAL = TimeUnit.MINUTES.toNanos(5); + + // TODO(scott): how is this done on Windows? This may require a JNI call to GetNetworkParams + // https://msdn.microsoft.com/en-us/library/aa365968(VS.85).aspx. + static final DnsServerAddressStreamProvider DEFAULT_DNS_SERVER_ADDRESS_STREAM_PROVIDER = + new DnsServerAddressStreamProvider() { + private volatile DnsServerAddressStreamProvider currentProvider = provider(); + private final AtomicLong lastRefresh = new AtomicLong(System.nanoTime()); + + @Override + public DnsServerAddressStream nameServerAddressStream(String hostname) { + long last = lastRefresh.get(); + DnsServerAddressStreamProvider current = currentProvider; + if (System.nanoTime() - last > REFRESH_INTERVAL) { + // This is slightly racy which means it will be possible still use the old configuration + // for a small amount of time, but that's ok. + if (lastRefresh.compareAndSet(last, System.nanoTime())) { + current = currentProvider = provider(); + } + } + return current.nameServerAddressStream(hostname); + } + + private DnsServerAddressStreamProvider provider() { + // If on windows just use the DefaultDnsServerAddressStreamProvider.INSTANCE as otherwise + // we will log some error which may be confusing. + return PlatformDependent.isWindows() ? DefaultDnsServerAddressStreamProvider.INSTANCE : + UnixResolverDnsServerAddressStreamProvider.parseSilently(); + } + }; } } diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsServerAddresses.java b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsServerAddresses.java index 72c0acd..12c34ce 100644 --- a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsServerAddresses.java +++ b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsServerAddresses.java @@ -16,7 +16,7 @@ package io.netty.resolver.dns; -import io.netty.util.internal.UnstableApi; +import io.netty.util.internal.ObjectUtil; import java.net.InetSocketAddress; import java.util.ArrayList; @@ -26,7 +26,6 @@ import java.util.List; /** * Provides an infinite sequence of DNS server addresses to {@link DnsNameResolver}. */ -@UnstableApi @SuppressWarnings("IteratorNextCanNotThrowNoSuchElementException") public abstract class DnsServerAddresses { /** @@ -149,9 +148,7 @@ public abstract class DnsServerAddresses { * Returns the {@link DnsServerAddresses} that yields only a single {@code address}. */ public static DnsServerAddresses singleton(final InetSocketAddress address) { - if (address == null) { - throw new NullPointerException("address"); - } + ObjectUtil.checkNotNull(address, "address"); if (address.isUnresolved()) { throw new IllegalArgumentException("cannot use an unresolved DNS server address: " + address); } @@ -160,9 +157,7 @@ public abstract class DnsServerAddresses { } private static List<InetSocketAddress> sanitize(Iterable<? extends InetSocketAddress> addresses) { - if (addresses == null) { - throw new NullPointerException("addresses"); - } + ObjectUtil.checkNotNull(addresses, "addresses"); final List<InetSocketAddress> list; if (addresses instanceof Collection) { @@ -189,9 +184,7 @@ public abstract class DnsServerAddresses { } private static List<InetSocketAddress> sanitize(InetSocketAddress[] addresses) { - if (addresses == null) { - throw new NullPointerException("addresses"); - } + ObjectUtil.checkNotNull(addresses, "addresses"); List<InetSocketAddress> list = new ArrayList<InetSocketAddress>(addresses.length); for (InetSocketAddress a: addresses) { diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/MultiDnsServerAddressStreamProvider.java b/resolver-dns/src/main/java/io/netty/resolver/dns/MultiDnsServerAddressStreamProvider.java index 87e1aaa..915e70e 100644 --- a/resolver-dns/src/main/java/io/netty/resolver/dns/MultiDnsServerAddressStreamProvider.java +++ b/resolver-dns/src/main/java/io/netty/resolver/dns/MultiDnsServerAddressStreamProvider.java @@ -15,15 +15,12 @@ */ package io.netty.resolver.dns; -import io.netty.util.internal.UnstableApi; - import java.util.List; /** * A {@link DnsServerAddressStreamProvider} which iterates through a collection of * {@link DnsServerAddressStreamProvider} until the first non-{@code null} result is found. */ -@UnstableApi public final class MultiDnsServerAddressStreamProvider implements DnsServerAddressStreamProvider { private final DnsServerAddressStreamProvider[] providers; diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/NoopAuthoritativeDnsServerCache.java b/resolver-dns/src/main/java/io/netty/resolver/dns/NoopAuthoritativeDnsServerCache.java index 0cdce77..dcc1172 100644 --- a/resolver-dns/src/main/java/io/netty/resolver/dns/NoopAuthoritativeDnsServerCache.java +++ b/resolver-dns/src/main/java/io/netty/resolver/dns/NoopAuthoritativeDnsServerCache.java @@ -16,16 +16,12 @@ package io.netty.resolver.dns; import io.netty.channel.EventLoop; -import io.netty.util.internal.UnstableApi; import java.net.InetSocketAddress; -import java.util.Collections; -import java.util.List; /** * A noop {@link AuthoritativeDnsServerCache} that actually never caches anything. */ -@UnstableApi public final class NoopAuthoritativeDnsServerCache implements AuthoritativeDnsServerCache { public static final NoopAuthoritativeDnsServerCache INSTANCE = new NoopAuthoritativeDnsServerCache(); diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/NoopDnsCache.java b/resolver-dns/src/main/java/io/netty/resolver/dns/NoopDnsCache.java index ec36c84..9d008a2 100644 --- a/resolver-dns/src/main/java/io/netty/resolver/dns/NoopDnsCache.java +++ b/resolver-dns/src/main/java/io/netty/resolver/dns/NoopDnsCache.java @@ -17,7 +17,6 @@ package io.netty.resolver.dns; import io.netty.channel.EventLoop; import io.netty.handler.codec.dns.DnsRecord; -import io.netty.util.internal.UnstableApi; import java.net.InetAddress; import java.util.Collections; @@ -26,7 +25,6 @@ import java.util.List; /** * A noop DNS cache that actually never caches anything. */ -@UnstableApi public final class NoopDnsCache implements DnsCache { public static final NoopDnsCache INSTANCE = new NoopDnsCache(); diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/NoopDnsCnameCache.java b/resolver-dns/src/main/java/io/netty/resolver/dns/NoopDnsCnameCache.java index 54113c4..a869228 100644 --- a/resolver-dns/src/main/java/io/netty/resolver/dns/NoopDnsCnameCache.java +++ b/resolver-dns/src/main/java/io/netty/resolver/dns/NoopDnsCnameCache.java @@ -16,9 +16,7 @@ package io.netty.resolver.dns; import io.netty.channel.EventLoop; -import io.netty.util.internal.UnstableApi; -@UnstableApi public final class NoopDnsCnameCache implements DnsCnameCache { public static final NoopDnsCnameCache INSTANCE = new NoopDnsCnameCache(); diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/NoopDnsQueryLifecycleObserverFactory.java b/resolver-dns/src/main/java/io/netty/resolver/dns/NoopDnsQueryLifecycleObserverFactory.java index d53e9cd..2dbcfa5 100644 --- a/resolver-dns/src/main/java/io/netty/resolver/dns/NoopDnsQueryLifecycleObserverFactory.java +++ b/resolver-dns/src/main/java/io/netty/resolver/dns/NoopDnsQueryLifecycleObserverFactory.java @@ -16,9 +16,7 @@ package io.netty.resolver.dns; import io.netty.handler.codec.dns.DnsQuestion; -import io.netty.util.internal.UnstableApi; -@UnstableApi public final class NoopDnsQueryLifecycleObserverFactory implements DnsQueryLifecycleObserverFactory { public static final NoopDnsQueryLifecycleObserverFactory INSTANCE = new NoopDnsQueryLifecycleObserverFactory(); diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/PreferredAddressTypeComparator.java b/resolver-dns/src/main/java/io/netty/resolver/dns/PreferredAddressTypeComparator.java new file mode 100644 index 0000000..bcf4dc7 --- /dev/null +++ b/resolver-dns/src/main/java/io/netty/resolver/dns/PreferredAddressTypeComparator.java @@ -0,0 +1,54 @@ +/* + * Copyright 2019 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.resolver.dns; + +import io.netty.channel.socket.InternetProtocolFamily; + +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.util.Comparator; + +final class PreferredAddressTypeComparator implements Comparator<InetAddress> { + + private static final PreferredAddressTypeComparator IPv4 = new PreferredAddressTypeComparator(Inet4Address.class); + private static final PreferredAddressTypeComparator IPv6 = new PreferredAddressTypeComparator(Inet6Address.class); + + static PreferredAddressTypeComparator comparator(InternetProtocolFamily family) { + switch (family) { + case IPv4: + return IPv4; + case IPv6: + return IPv6; + default: + throw new IllegalArgumentException(); + } + } + + private final Class<? extends InetAddress> preferredAddressType; + + private PreferredAddressTypeComparator(Class<? extends InetAddress> preferredAddressType) { + this.preferredAddressType = preferredAddressType; + } + + @Override + public int compare(InetAddress o1, InetAddress o2) { + if (o1.getClass() == o2.getClass()) { + return 0; + } + return preferredAddressType.isAssignableFrom(o1.getClass()) ? -1 : 1; + } +} diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/RoundRobinDnsAddressResolverGroup.java b/resolver-dns/src/main/java/io/netty/resolver/dns/RoundRobinDnsAddressResolverGroup.java index 68c14fe..e725fc8 100644 --- a/resolver-dns/src/main/java/io/netty/resolver/dns/RoundRobinDnsAddressResolverGroup.java +++ b/resolver-dns/src/main/java/io/netty/resolver/dns/RoundRobinDnsAddressResolverGroup.java @@ -23,7 +23,6 @@ import io.netty.resolver.AddressResolver; import io.netty.resolver.AddressResolverGroup; import io.netty.resolver.NameResolver; import io.netty.resolver.RoundRobinInetAddressResolver; -import io.netty.util.internal.UnstableApi; import java.net.InetAddress; import java.net.InetSocketAddress; @@ -33,7 +32,6 @@ import java.net.InetSocketAddress; * multiple are provided by the nameserver. This is ideal for use in applications that use a pool of connections, for * which connecting to a single resolved address would be inefficient. */ -@UnstableApi public class RoundRobinDnsAddressResolverGroup extends DnsAddressResolverGroup { public RoundRobinDnsAddressResolverGroup(DnsNameResolverBuilder dnsResolverBuilder) { diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/SequentialDnsServerAddressStreamProvider.java b/resolver-dns/src/main/java/io/netty/resolver/dns/SequentialDnsServerAddressStreamProvider.java index a23aeba..24f4ebb 100644 --- a/resolver-dns/src/main/java/io/netty/resolver/dns/SequentialDnsServerAddressStreamProvider.java +++ b/resolver-dns/src/main/java/io/netty/resolver/dns/SequentialDnsServerAddressStreamProvider.java @@ -15,8 +15,6 @@ */ package io.netty.resolver.dns; -import io.netty.util.internal.UnstableApi; - import java.net.InetSocketAddress; import static io.netty.resolver.dns.DnsServerAddresses.sequential; @@ -24,7 +22,6 @@ import static io.netty.resolver.dns.DnsServerAddresses.sequential; /** * A {@link DnsServerAddressStreamProvider} which is backed by a sequential list of DNS servers. */ -@UnstableApi public final class SequentialDnsServerAddressStreamProvider extends UniSequentialDnsServerAddressStreamProvider { /** * Create a new instance. diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/ShuffledDnsServerAddressStream.java b/resolver-dns/src/main/java/io/netty/resolver/dns/ShuffledDnsServerAddressStream.java index 9b56f23..9f9b9cf 100644 --- a/resolver-dns/src/main/java/io/netty/resolver/dns/ShuffledDnsServerAddressStream.java +++ b/resolver-dns/src/main/java/io/netty/resolver/dns/ShuffledDnsServerAddressStream.java @@ -21,7 +21,6 @@ import io.netty.util.internal.PlatformDependent; import java.net.InetSocketAddress; import java.util.Collections; import java.util.List; -import java.util.Random; final class ShuffledDnsServerAddressStream implements DnsServerAddressStream { diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/SingletonDnsServerAddressStreamProvider.java b/resolver-dns/src/main/java/io/netty/resolver/dns/SingletonDnsServerAddressStreamProvider.java index cfd5f8b..1b7ccff 100644 --- a/resolver-dns/src/main/java/io/netty/resolver/dns/SingletonDnsServerAddressStreamProvider.java +++ b/resolver-dns/src/main/java/io/netty/resolver/dns/SingletonDnsServerAddressStreamProvider.java @@ -15,14 +15,11 @@ */ package io.netty.resolver.dns; -import io.netty.util.internal.UnstableApi; - import java.net.InetSocketAddress; /** * A {@link DnsServerAddressStreamProvider} which always uses a single DNS server for resolution. */ -@UnstableApi public final class SingletonDnsServerAddressStreamProvider extends UniSequentialDnsServerAddressStreamProvider { /** * Create a new instance. diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/TcpDnsQueryContext.java b/resolver-dns/src/main/java/io/netty/resolver/dns/TcpDnsQueryContext.java new file mode 100644 index 0000000..65a1d05 --- /dev/null +++ b/resolver-dns/src/main/java/io/netty/resolver/dns/TcpDnsQueryContext.java @@ -0,0 +1,53 @@ +/* + * Copyright 2019 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.resolver.dns; + +import io.netty.channel.AddressedEnvelope; +import io.netty.channel.Channel; +import io.netty.handler.codec.dns.DefaultDnsQuery; +import io.netty.handler.codec.dns.DnsQuery; +import io.netty.handler.codec.dns.DnsQuestion; +import io.netty.handler.codec.dns.DnsRecord; +import io.netty.handler.codec.dns.DnsResponse; +import io.netty.util.concurrent.Promise; + +import java.net.InetSocketAddress; + +final class TcpDnsQueryContext extends DnsQueryContext { + + private final Channel channel; + + TcpDnsQueryContext(DnsNameResolver parent, Channel channel, InetSocketAddress nameServerAddr, DnsQuestion question, + DnsRecord[] additionals, Promise<AddressedEnvelope<DnsResponse, InetSocketAddress>> promise) { + super(parent, nameServerAddr, question, additionals, promise); + this.channel = channel; + } + + @Override + protected DnsQuery newQuery(int id) { + return new DefaultDnsQuery(id); + } + + @Override + protected Channel channel() { + return channel; + } + + @Override + protected String protocol() { + return "TCP"; + } +} diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/UnixResolverDnsServerAddressStreamProvider.java b/resolver-dns/src/main/java/io/netty/resolver/dns/UnixResolverDnsServerAddressStreamProvider.java index afe2931..feac4b9 100644 --- a/resolver-dns/src/main/java/io/netty/resolver/dns/UnixResolverDnsServerAddressStreamProvider.java +++ b/resolver-dns/src/main/java/io/netty/resolver/dns/UnixResolverDnsServerAddressStreamProvider.java @@ -17,7 +17,6 @@ package io.netty.resolver.dns; import io.netty.util.NetUtil; import io.netty.util.internal.SocketUtils; -import io.netty.util.internal.UnstableApi; import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; @@ -37,13 +36,13 @@ import java.util.regex.Pattern; import static io.netty.resolver.dns.DefaultDnsServerAddressStreamProvider.DNS_PORT; import static io.netty.util.internal.ObjectUtil.checkNotNull; import static io.netty.util.internal.StringUtil.indexOfNonWhiteSpace; +import static io.netty.util.internal.StringUtil.indexOfWhiteSpace; /** * Able to parse files such as <a href="https://linux.die.net/man/5/resolver">/etc/resolv.conf</a> and * <a href="https://developer.apple.com/legacy/library/documentation/Darwin/Reference/ManPages/man5/resolver.5.html"> * /etc/resolver</a> to respect the system default domain servers. */ -@UnstableApi public final class UnixResolverDnsServerAddressStreamProvider implements DnsServerAddressStreamProvider { private static final InternalLogger logger = InternalLoggerFactory.getInstance(UnixResolverDnsServerAddressStreamProvider.class); @@ -72,7 +71,9 @@ public final class UnixResolverDnsServerAddressStreamProvider implements DnsServ return nameServerCache.mayOverrideNameServers() ? nameServerCache : DefaultDnsServerAddressStreamProvider.INSTANCE; } catch (Exception e) { - logger.debug("failed to parse {} and/or {}", ETC_RESOLV_CONF_FILE, ETC_RESOLVER_DIR, e); + if (logger.isDebugEnabled()) { + logger.debug("failed to parse {} and/or {}", ETC_RESOLV_CONF_FILE, ETC_RESOLVER_DIR, e); + } return DefaultDnsServerAddressStreamProvider.INSTANCE; } } @@ -167,48 +168,65 @@ public final class UnixResolverDnsServerAddressStreamProvider implements DnsServ String line; while ((line = br.readLine()) != null) { line = line.trim(); - char c; - if (line.isEmpty() || (c = line.charAt(0)) == '#' || c == ';') { - continue; - } - if (line.startsWith(NAMESERVER_ROW_LABEL)) { - int i = indexOfNonWhiteSpace(line, NAMESERVER_ROW_LABEL.length()); - if (i < 0) { - throw new IllegalArgumentException("error parsing label " + NAMESERVER_ROW_LABEL + - " in file " + etcResolverFile + ". value: " + line); + try { + char c; + if (line.isEmpty() || (c = line.charAt(0)) == '#' || c == ';') { + continue; } - String maybeIP = line.substring(i); - // There may be a port appended onto the IP address so we attempt to extract it. - if (!NetUtil.isValidIpV4Address(maybeIP) && !NetUtil.isValidIpV6Address(maybeIP)) { - i = maybeIP.lastIndexOf('.'); - if (i + 1 >= maybeIP.length()) { + if (line.startsWith(NAMESERVER_ROW_LABEL)) { + int i = indexOfNonWhiteSpace(line, NAMESERVER_ROW_LABEL.length()); + if (i < 0) { throw new IllegalArgumentException("error parsing label " + NAMESERVER_ROW_LABEL + - " in file " + etcResolverFile + ". invalid IP value: " + line); + " in file " + etcResolverFile + ". value: " + line); } - port = Integer.parseInt(maybeIP.substring(i + 1)); - maybeIP = maybeIP.substring(0, i); - } - addresses.add(SocketUtils.socketAddress(maybeIP, port)); - } else if (line.startsWith(DOMAIN_ROW_LABEL)) { - int i = indexOfNonWhiteSpace(line, DOMAIN_ROW_LABEL.length()); - if (i < 0) { - throw new IllegalArgumentException("error parsing label " + DOMAIN_ROW_LABEL + - " in file " + etcResolverFile + " value: " + line); - } - domainName = line.substring(i); - if (!addresses.isEmpty()) { - putIfAbsent(domainToNameServerStreamMap, domainName, addresses); - } - addresses = new ArrayList<InetSocketAddress>(2); - } else if (line.startsWith(PORT_ROW_LABEL)) { - int i = indexOfNonWhiteSpace(line, PORT_ROW_LABEL.length()); - if (i < 0) { - throw new IllegalArgumentException("error parsing label " + PORT_ROW_LABEL + - " in file " + etcResolverFile + " value: " + line); + String maybeIP; + int x = indexOfWhiteSpace(line, i); + if (x == -1) { + maybeIP = line.substring(i); + } else { + // ignore comments + int idx = indexOfNonWhiteSpace(line, x); + if (idx == -1 || line.charAt(idx) != '#') { + throw new IllegalArgumentException("error parsing label " + NAMESERVER_ROW_LABEL + + " in file " + etcResolverFile + ". value: " + line); + } + maybeIP = line.substring(i, x); + } + + // There may be a port appended onto the IP address so we attempt to extract it. + if (!NetUtil.isValidIpV4Address(maybeIP) && !NetUtil.isValidIpV6Address(maybeIP)) { + i = maybeIP.lastIndexOf('.'); + if (i + 1 >= maybeIP.length()) { + throw new IllegalArgumentException("error parsing label " + NAMESERVER_ROW_LABEL + + " in file " + etcResolverFile + ". invalid IP value: " + line); + } + port = Integer.parseInt(maybeIP.substring(i + 1)); + maybeIP = maybeIP.substring(0, i); + } + addresses.add(SocketUtils.socketAddress(maybeIP, port)); + } else if (line.startsWith(DOMAIN_ROW_LABEL)) { + int i = indexOfNonWhiteSpace(line, DOMAIN_ROW_LABEL.length()); + if (i < 0) { + throw new IllegalArgumentException("error parsing label " + DOMAIN_ROW_LABEL + + " in file " + etcResolverFile + " value: " + line); + } + domainName = line.substring(i); + if (!addresses.isEmpty()) { + putIfAbsent(domainToNameServerStreamMap, domainName, addresses); + } + addresses = new ArrayList<InetSocketAddress>(2); + } else if (line.startsWith(PORT_ROW_LABEL)) { + int i = indexOfNonWhiteSpace(line, PORT_ROW_LABEL.length()); + if (i < 0) { + throw new IllegalArgumentException("error parsing label " + PORT_ROW_LABEL + + " in file " + etcResolverFile + " value: " + line); + } + port = Integer.parseInt(line.substring(i)); + } else if (line.startsWith(SORTLIST_ROW_LABEL)) { + logger.info("row type {} not supported. Ignoring line: {}", SORTLIST_ROW_LABEL, line); } - port = Integer.parseInt(line.substring(i)); - } else if (line.startsWith(SORTLIST_ROW_LABEL)) { - logger.info("row type {} not supported. ignoring line: {}", SORTLIST_ROW_LABEL, line); + } catch (IllegalArgumentException e) { + logger.warn("Could not parse entry. Ignoring line: {}", line, e); } } if (!addresses.isEmpty()) { @@ -238,8 +256,10 @@ public final class UnixResolverDnsServerAddressStreamProvider implements DnsServ DnsServerAddresses existingAddresses = domainToNameServerStreamMap.put(domainName, addresses); if (existingAddresses != null) { domainToNameServerStreamMap.put(domainName, existingAddresses); - logger.debug("Domain name {} already maps to addresses {} so new addresses {} will be discarded", - domainName, existingAddresses, addresses); + if (logger.isDebugEnabled()) { + logger.debug("Domain name {} already maps to addresses {} so new addresses {} will be discarded", + domainName, existingAddresses, addresses); + } } } diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/package-info.java b/resolver-dns/src/main/java/io/netty/resolver/dns/package-info.java index 3edc178..63825ba 100644 --- a/resolver-dns/src/main/java/io/netty/resolver/dns/package-info.java +++ b/resolver-dns/src/main/java/io/netty/resolver/dns/package-info.java @@ -18,7 +18,4 @@ * An alternative to Java's built-in domain name lookup mechanism that resolves a domain name asynchronously, * which supports the queries of an arbitrary DNS record type as well. */ -@UnstableApi package io.netty.resolver.dns; - -import io.netty.util.internal.UnstableApi; diff --git a/resolver-dns/src/test/java/io/netty/resolver/dns/DefaultAuthoritativeDnsServerCacheTest.java b/resolver-dns/src/test/java/io/netty/resolver/dns/DefaultAuthoritativeDnsServerCacheTest.java index ada0eae..4b4d23c 100644 --- a/resolver-dns/src/test/java/io/netty/resolver/dns/DefaultAuthoritativeDnsServerCacheTest.java +++ b/resolver-dns/src/test/java/io/netty/resolver/dns/DefaultAuthoritativeDnsServerCacheTest.java @@ -25,7 +25,6 @@ import org.junit.Test; import java.net.InetAddress; import java.net.InetSocketAddress; import java.util.Comparator; -import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; diff --git a/resolver-dns/src/test/java/io/netty/resolver/dns/DnsNameResolverTest.java b/resolver-dns/src/test/java/io/netty/resolver/dns/DnsNameResolverTest.java index d03ee87..3e35e28 100644 --- a/resolver-dns/src/test/java/io/netty/resolver/dns/DnsNameResolverTest.java +++ b/resolver-dns/src/test/java/io/netty/resolver/dns/DnsNameResolverTest.java @@ -20,13 +20,17 @@ import io.netty.buffer.ByteBufHolder; import io.netty.channel.AddressedEnvelope; import io.netty.channel.ChannelFactory; import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.channel.EventLoop; import io.netty.channel.EventLoopGroup; import io.netty.channel.ReflectiveChannelFactory; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.DatagramChannel; +import io.netty.channel.socket.DatagramPacket; import io.netty.channel.socket.InternetProtocolFamily; import io.netty.channel.socket.nio.NioDatagramChannel; +import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.handler.codec.dns.DefaultDnsQuestion; import io.netty.handler.codec.dns.DnsQuestion; import io.netty.handler.codec.dns.DnsRawRecord; @@ -37,6 +41,7 @@ import io.netty.handler.codec.dns.DnsResponseCode; import io.netty.handler.codec.dns.DnsSection; import io.netty.resolver.HostsFileEntriesResolver; import io.netty.resolver.ResolvedAddressTypes; +import io.netty.util.CharsetUtil; import io.netty.util.NetUtil; import io.netty.util.ReferenceCountUtil; import io.netty.util.concurrent.Future; @@ -46,7 +51,9 @@ import io.netty.util.internal.StringUtil; import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; import org.apache.directory.server.dns.DnsException; +import org.apache.directory.server.dns.io.encoder.DnsMessageEncoder; import org.apache.directory.server.dns.messages.DnsMessage; +import org.apache.directory.server.dns.messages.DnsMessageModifier; import org.apache.directory.server.dns.messages.QuestionRecord; import org.apache.directory.server.dns.messages.RecordClass; import org.apache.directory.server.dns.messages.RecordType; @@ -55,6 +62,7 @@ import org.apache.directory.server.dns.messages.ResourceRecordModifier; import org.apache.directory.server.dns.messages.ResponseCode; import org.apache.directory.server.dns.store.DnsAttribute; import org.apache.directory.server.dns.store.RecordStore; +import org.apache.mina.core.buffer.IoBuffer; import org.hamcrest.Matchers; import org.junit.AfterClass; import org.junit.BeforeClass; @@ -63,10 +71,15 @@ import org.junit.Test; import org.junit.rules.ExpectedException; import java.io.IOException; +import java.io.InputStream; import java.net.DatagramSocket; +import java.net.Inet4Address; import java.net.InetAddress; import java.net.InetSocketAddress; +import java.net.ServerSocket; +import java.net.Socket; import java.net.UnknownHostException; +import java.nio.ByteBuffer; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Arrays; @@ -96,7 +109,6 @@ import static io.netty.handler.codec.dns.DnsRecordType.AAAA; import static io.netty.handler.codec.dns.DnsRecordType.CNAME; import static io.netty.resolver.dns.DnsServerAddresses.sequential; import static java.util.Collections.singletonList; -import static java.util.Collections.singletonMap; import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.instanceOf; @@ -1232,6 +1244,15 @@ public class DnsNameResolverTest { } else { assertEquals(ipv6Address, resolved.getHostAddress()); } + InetAddress ipv4InetAddress = InetAddress.getByAddress("netty.com", + InetAddress.getByName(ipv4Address).getAddress()); + InetAddress ipv6InetAddress = InetAddress.getByAddress("netty.com", + InetAddress.getByName(ipv6Address).getAddress()); + + List<InetAddress> resolvedAll = resolver.resolveAll("netty.com").syncUninterruptibly().getNow(); + List<InetAddress> expected = types == ResolvedAddressTypes.IPV4_PREFERRED ? + Arrays.asList(ipv4InetAddress, ipv6InetAddress) : Arrays.asList(ipv6InetAddress, ipv4InetAddress); + assertEquals(expected, resolvedAll); } finally { nonCompliantDnsServer.stop(); } @@ -2467,4 +2488,422 @@ public class DnsNameResolverTest { true // decodeIdn ).close(); } + + @Test + public void testQueryTxt() throws Exception { + final String hostname = "txt.netty.io"; + final String txt1 = "some text"; + final String txt2 = "some more text"; + + TestDnsServer server = new TestDnsServer(new RecordStore() { + + @Override + public Set<ResourceRecord> getRecords(QuestionRecord question) { + if (question.getDomainName().equals(hostname)) { + Map<String, Object> map1 = new HashMap<String, Object>(); + map1.put(DnsAttribute.CHARACTER_STRING.toLowerCase(), txt1); + + Map<String, Object> map2 = new HashMap<String, Object>(); + map2.put(DnsAttribute.CHARACTER_STRING.toLowerCase(), txt2); + + Set<ResourceRecord> records = new HashSet<ResourceRecord>(); + records.add(new TestDnsServer.TestResourceRecord(question.getDomainName(), RecordType.TXT, map1)); + records.add(new TestDnsServer.TestResourceRecord(question.getDomainName(), RecordType.TXT, map2)); + return records; + } + return Collections.emptySet(); + } + }); + server.start(); + DnsNameResolver resolver = newResolver(ResolvedAddressTypes.IPV4_ONLY) + .nameServerProvider(new SingletonDnsServerAddressStreamProvider(server.localAddress())) + .build(); + try { + AddressedEnvelope<DnsResponse, InetSocketAddress> envelope = resolver.query( + new DefaultDnsQuestion(hostname, DnsRecordType.TXT)).syncUninterruptibly().getNow(); + assertNotNull(envelope.sender()); + + DnsResponse response = envelope.content(); + assertNotNull(response); + + assertEquals(DnsResponseCode.NOERROR, response.code()); + int count = response.count(DnsSection.ANSWER); + + assertEquals(2, count); + List<String> txts = new ArrayList<String>(); + + for (int i = 0; i < 2; i++) { + txts.addAll(decodeTxt(response.recordAt(DnsSection.ANSWER, i))); + } + assertTrue(txts.contains(txt1)); + assertTrue(txts.contains(txt2)); + envelope.release(); + } finally { + resolver.close(); + server.stop(); + } + } + + private static List<String> decodeTxt(DnsRecord record) { + if (!(record instanceof DnsRawRecord)) { + return Collections.emptyList(); + } + List<String> list = new ArrayList<String>(); + ByteBuf data = ((DnsRawRecord) record).content(); + int idx = data.readerIndex(); + int wIdx = data.writerIndex(); + while (idx < wIdx) { + int len = data.getUnsignedByte(idx++); + list.add(data.toString(idx, len, CharsetUtil.UTF_8)); + idx += len; + } + return list; + } + + @Test + public void testNotIncludeDuplicates() throws IOException { + final String name = "netty.io"; + final String ipv4Addr = "1.2.3.4"; + TestDnsServer dnsServer2 = new TestDnsServer(new RecordStore() { + @Override + public Set<ResourceRecord> getRecords(QuestionRecord question) { + Set<ResourceRecord> records = new LinkedHashSet<ResourceRecord>(4); + String qName = question.getDomainName().toLowerCase(); + if (qName.equals(name)) { + records.add(new TestDnsServer.TestResourceRecord( + qName, RecordType.CNAME, + Collections.<String, Object>singletonMap( + DnsAttribute.DOMAIN_NAME.toLowerCase(), "cname.netty.io"))); + records.add(new TestDnsServer.TestResourceRecord(qName, + RecordType.A, Collections.<String, Object>singletonMap( + DnsAttribute.IP_ADDRESS.toLowerCase(), ipv4Addr))); + } else { + records.add(new TestDnsServer.TestResourceRecord(qName, + RecordType.A, Collections.<String, Object>singletonMap( + DnsAttribute.IP_ADDRESS.toLowerCase(), ipv4Addr))); + } + return records; + } + }); + dnsServer2.start(); + DnsNameResolver resolver = null; + try { + DnsNameResolverBuilder builder = newResolver() + .recursionDesired(true) + .maxQueriesPerResolve(16) + .nameServerProvider(new SingletonDnsServerAddressStreamProvider(dnsServer2.localAddress())); + builder.resolvedAddressTypes(ResolvedAddressTypes.IPV4_ONLY); + + resolver = builder.build(); + List<InetAddress> resolvedAddresses = resolver.resolveAll(name).syncUninterruptibly().getNow(); + assertEquals(Collections.singletonList(InetAddress.getByAddress(name, new byte[] { 1, 2, 3, 4 })), + resolvedAddresses); + } finally { + dnsServer2.stop(); + if (resolver != null) { + resolver.close(); + } + } + } + + @Test + public void testIncludeDuplicates() throws IOException { + final String name = "netty.io"; + final String ipv4Addr = "1.2.3.4"; + TestDnsServer dnsServer2 = new TestDnsServer(new RecordStore() { + @Override + public Set<ResourceRecord> getRecords(QuestionRecord question) { + Set<ResourceRecord> records = new LinkedHashSet<ResourceRecord>(2); + String qName = question.getDomainName().toLowerCase(); + records.add(new TestDnsServer.TestResourceRecord(qName, + RecordType.A, Collections.<String, Object>singletonMap( + DnsAttribute.IP_ADDRESS.toLowerCase(), ipv4Addr))); + records.add(new TestDnsServer.TestResourceRecord(qName, + RecordType.A, Collections.<String, Object>singletonMap( + DnsAttribute.IP_ADDRESS.toLowerCase(), ipv4Addr))); + return records; + } + }); + dnsServer2.start(); + DnsNameResolver resolver = null; + try { + DnsNameResolverBuilder builder = newResolver() + .recursionDesired(true) + .maxQueriesPerResolve(16) + .nameServerProvider(new SingletonDnsServerAddressStreamProvider(dnsServer2.localAddress())); + builder.resolvedAddressTypes(ResolvedAddressTypes.IPV4_ONLY); + + resolver = builder.build(); + List<DnsRecord> resolvedAddresses = resolver.resolveAll(new DefaultDnsQuestion(name, A)) + .syncUninterruptibly().getNow(); + assertEquals(2, resolvedAddresses.size()); + for (DnsRecord record: resolvedAddresses) { + ReferenceCountUtil.release(record); + } + } finally { + dnsServer2.stop(); + if (resolver != null) { + resolver.close(); + } + } + } + + @Test + public void testDropAAAA() throws IOException { + String host = "somehost.netty.io"; + TestDnsServer dnsServer2 = new TestDnsServer(Collections.singleton(host)); + dnsServer2.start(true); + DnsNameResolver resolver = null; + try { + DnsNameResolverBuilder builder = newResolver() + .recursionDesired(false) + .queryTimeoutMillis(500) + .resolvedAddressTypes(ResolvedAddressTypes.IPV4_PREFERRED) + .maxQueriesPerResolve(16) + .nameServerProvider(new SingletonDnsServerAddressStreamProvider(dnsServer2.localAddress())); + + resolver = builder.build(); + List<InetAddress> addressList = resolver.resolveAll(host).syncUninterruptibly().getNow(); + assertEquals(1, addressList.size()); + assertEquals(host, addressList.get(0).getHostName()); + } finally { + dnsServer2.stop(); + if (resolver != null) { + resolver.close(); + } + } + } + + @Test(timeout = 2000) + public void testDropAAAAResolveFast() throws IOException { + String host = "somehost.netty.io"; + TestDnsServer dnsServer2 = new TestDnsServer(Collections.singleton(host)); + dnsServer2.start(true); + DnsNameResolver resolver = null; + try { + DnsNameResolverBuilder builder = newResolver() + .recursionDesired(false) + .queryTimeoutMillis(10000) + .resolvedAddressTypes(ResolvedAddressTypes.IPV4_PREFERRED) + .maxQueriesPerResolve(16) + .nameServerProvider(new SingletonDnsServerAddressStreamProvider(dnsServer2.localAddress())); + + resolver = builder.build(); + InetAddress address = resolver.resolve(host).syncUninterruptibly().getNow(); + assertEquals(host, address.getHostName()); + } finally { + dnsServer2.stop(); + if (resolver != null) { + resolver.close(); + } + } + } + + @Test(timeout = 2000) + public void testDropAAAAResolveAllFast() throws IOException { + final String host = "somehost.netty.io"; + TestDnsServer dnsServer2 = new TestDnsServer(new RecordStore() { + @Override + public Set<ResourceRecord> getRecords(QuestionRecord question) throws DnsException { + String name = question.getDomainName(); + if (name.equals(host)) { + Set<ResourceRecord> records = new HashSet<ResourceRecord>(2); + records.add(new TestDnsServer.TestResourceRecord(name, RecordType.A, + Collections.<String, Object>singletonMap(DnsAttribute.IP_ADDRESS.toLowerCase(), + "10.0.0.1"))); + records.add(new TestDnsServer.TestResourceRecord(name, RecordType.A, + Collections.<String, Object>singletonMap(DnsAttribute.IP_ADDRESS.toLowerCase(), + "10.0.0.2"))); + return records; + } + return null; + } + }); + dnsServer2.start(true); + DnsNameResolver resolver = null; + try { + DnsNameResolverBuilder builder = newResolver() + .recursionDesired(false) + .queryTimeoutMillis(10000) + .resolvedAddressTypes(ResolvedAddressTypes.IPV4_PREFERRED) + .completeOncePreferredResolved(true) + .maxQueriesPerResolve(16) + .nameServerProvider(new SingletonDnsServerAddressStreamProvider(dnsServer2.localAddress())); + + resolver = builder.build(); + List<InetAddress> addresses = resolver.resolveAll(host).syncUninterruptibly().getNow(); + assertEquals(2, addresses.size()); + for (InetAddress address: addresses) { + assertThat(address, instanceOf(Inet4Address.class)); + assertEquals(host, address.getHostName()); + } + } finally { + dnsServer2.stop(); + if (resolver != null) { + resolver.close(); + } + } + } + + @Test(timeout = 5000) + public void testTruncatedWithoutTcpFallback() throws IOException { + testTruncated0(false, false); + } + + @Test(timeout = 5000) + public void testTruncatedWithTcpFallback() throws IOException { + testTruncated0(true, false); + } + + @Test(timeout = 5000) + public void testTruncatedWithTcpFallbackBecauseOfMtu() throws IOException { + testTruncated0(true, true); + } + + private static DnsMessageModifier modifierFrom(DnsMessage message) { + DnsMessageModifier modifier = new DnsMessageModifier(); + modifier.setAcceptNonAuthenticatedData(message.isAcceptNonAuthenticatedData()); + modifier.setAdditionalRecords(message.getAdditionalRecords()); + modifier.setAnswerRecords(message.getAnswerRecords()); + modifier.setAuthoritativeAnswer(message.isAuthoritativeAnswer()); + modifier.setAuthorityRecords(message.getAuthorityRecords()); + modifier.setMessageType(message.getMessageType()); + modifier.setOpCode(message.getOpCode()); + modifier.setQuestionRecords(message.getQuestionRecords()); + modifier.setRecursionAvailable(message.isRecursionAvailable()); + modifier.setRecursionDesired(message.isRecursionDesired()); + modifier.setReserved(message.isReserved()); + modifier.setResponseCode(message.getResponseCode()); + modifier.setTransactionId(message.getTransactionId()); + modifier.setTruncated(message.isTruncated()); + return modifier; + } + + private static void testTruncated0(boolean tcpFallback, final boolean truncatedBecauseOfMtu) throws IOException { + final String host = "somehost.netty.io"; + final String txt = "this is a txt record"; + final AtomicReference<DnsMessage> messageRef = new AtomicReference<DnsMessage>(); + + TestDnsServer dnsServer2 = new TestDnsServer(new RecordStore() { + @Override + public Set<ResourceRecord> getRecords(QuestionRecord question) { + String name = question.getDomainName(); + if (name.equals(host)) { + return Collections.<ResourceRecord>singleton( + new TestDnsServer.TestResourceRecord(name, RecordType.TXT, + Collections.<String, Object>singletonMap( + DnsAttribute.CHARACTER_STRING.toLowerCase(), txt))); + } + return null; + } + }) { + @Override + protected DnsMessage filterMessage(DnsMessage message) { + // Store a original message so we can replay it later on. + messageRef.set(message); + + if (!truncatedBecauseOfMtu) { + // Create a copy of the message but set the truncated flag. + DnsMessageModifier modifier = modifierFrom(message); + modifier.setTruncated(true); + return modifier.getDnsMessage(); + } + return message; + } + }; + dnsServer2.start(); + DnsNameResolver resolver = null; + ServerSocket serverSocket = null; + try { + DnsNameResolverBuilder builder = newResolver() + .queryTimeoutMillis(10000) + .resolvedAddressTypes(ResolvedAddressTypes.IPV4_PREFERRED) + .maxQueriesPerResolve(16) + .nameServerProvider(new SingletonDnsServerAddressStreamProvider(dnsServer2.localAddress())); + + if (tcpFallback) { + // If we are configured to use TCP as a fallback also bind a TCP socket + serverSocket = new ServerSocket(dnsServer2.localAddress().getPort()); + serverSocket.setReuseAddress(true); + + builder.socketChannelType(NioSocketChannel.class); + } + resolver = builder.build(); + if (truncatedBecauseOfMtu) { + resolver.ch.pipeline().addFirst(new ChannelInboundHandlerAdapter() { + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + if (msg instanceof DatagramPacket) { + // Truncate the packet by 1 byte. + DatagramPacket packet = (DatagramPacket) msg; + packet.content().writerIndex(packet.content().writerIndex() - 1); + } + ctx.fireChannelRead(msg); + } + }); + } + Future<AddressedEnvelope<DnsResponse, InetSocketAddress>> envelopeFuture = resolver.query( + new DefaultDnsQuestion(host, DnsRecordType.TXT)); + + if (tcpFallback) { + // If we are configured to use TCP as a fallback lets replay the dns message over TCP + Socket socket = serverSocket.accept(); + + InputStream in = socket.getInputStream(); + assertTrue((in.read() << 8 | (in.read() & 0xff)) > 2); // skip length field + int txnId = in.read() << 8 | (in.read() & 0xff); + + IoBuffer ioBuffer = IoBuffer.allocate(1024); + // Must replace the transactionId with the one from the TCP request + DnsMessageModifier modifier = modifierFrom(messageRef.get()); + modifier.setTransactionId(txnId); + new DnsMessageEncoder().encode(ioBuffer, modifier.getDnsMessage()); + ioBuffer.flip(); + + ByteBuffer lenBuffer = ByteBuffer.allocate(2); + lenBuffer.putShort((short) ioBuffer.remaining()); + lenBuffer.flip(); + + while (lenBuffer.hasRemaining()) { + socket.getOutputStream().write(lenBuffer.get()); + } + + while (ioBuffer.hasRemaining()) { + socket.getOutputStream().write(ioBuffer.get()); + } + socket.getOutputStream().flush(); + // Let's wait until we received the envelope before closing the socket. + envelopeFuture.syncUninterruptibly(); + + socket.close(); + serverSocket.close(); + } + + AddressedEnvelope<DnsResponse, InetSocketAddress> envelope = envelopeFuture.syncUninterruptibly().getNow(); + assertNotNull(envelope.sender()); + + DnsResponse response = envelope.content(); + assertNotNull(response); + + assertEquals(DnsResponseCode.NOERROR, response.code()); + int count = response.count(DnsSection.ANSWER); + + assertEquals(1, count); + List<String> texts = decodeTxt(response.recordAt(DnsSection.ANSWER, 0)); + assertEquals(1, texts.size()); + assertEquals(txt, texts.get(0)); + + if (tcpFallback) { + assertFalse(envelope.content().isTruncated()); + } else { + assertTrue(envelope.content().isTruncated()); + } + assertTrue(envelope.release()); + } finally { + dnsServer2.stop(); + if (resolver != null) { + resolver.close(); + } + } + } } diff --git a/resolver-dns/src/test/java/io/netty/resolver/dns/DnsServerAddressStreamProvidersTest.java b/resolver-dns/src/test/java/io/netty/resolver/dns/DnsServerAddressStreamProvidersTest.java new file mode 100644 index 0000000..57be3ad --- /dev/null +++ b/resolver-dns/src/test/java/io/netty/resolver/dns/DnsServerAddressStreamProvidersTest.java @@ -0,0 +1,27 @@ +/* + * Copyright 2020 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.resolver.dns; + +import org.junit.Assert; +import org.junit.Test; + +public class DnsServerAddressStreamProvidersTest { + + @Test + public void testUseCorrectProvider() { + Assert.assertSame(DnsServerAddressStreamProviders.unixDefault(), + DnsServerAddressStreamProviders.platformDefault()); + } +} diff --git a/resolver-dns/src/test/java/io/netty/resolver/dns/PreferredAddressTypeComparatorTest.java b/resolver-dns/src/test/java/io/netty/resolver/dns/PreferredAddressTypeComparatorTest.java new file mode 100644 index 0000000..7dcaba8 --- /dev/null +++ b/resolver-dns/src/test/java/io/netty/resolver/dns/PreferredAddressTypeComparatorTest.java @@ -0,0 +1,70 @@ +/* + * Copyright 2019 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.resolver.dns; + +import io.netty.channel.socket.InternetProtocolFamily; +import org.junit.Assert; +import org.junit.Test; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public class PreferredAddressTypeComparatorTest { + + @Test + public void testIpv4() throws UnknownHostException { + InetAddress ipv4Address1 = InetAddress.getByName("10.0.0.1"); + InetAddress ipv4Address2 = InetAddress.getByName("10.0.0.2"); + InetAddress ipv4Address3 = InetAddress.getByName("10.0.0.3"); + InetAddress ipv6Address1 = InetAddress.getByName("::1"); + InetAddress ipv6Address2 = InetAddress.getByName("::2"); + InetAddress ipv6Address3 = InetAddress.getByName("::3"); + + PreferredAddressTypeComparator ipv4 = PreferredAddressTypeComparator.comparator(InternetProtocolFamily.IPv4); + + List<InetAddress> addressList = new ArrayList<InetAddress>(); + Collections.addAll(addressList, ipv4Address1, ipv4Address2, ipv6Address1, + ipv6Address2, ipv4Address3, ipv6Address3); + Collections.sort(addressList, ipv4); + + Assert.assertEquals(Arrays.asList(ipv4Address1, ipv4Address2, ipv4Address3, ipv6Address1, + ipv6Address2, ipv6Address3), addressList); + } + + @Test + public void testIpv6() throws UnknownHostException { + InetAddress ipv4Address1 = InetAddress.getByName("10.0.0.1"); + InetAddress ipv4Address2 = InetAddress.getByName("10.0.0.2"); + InetAddress ipv4Address3 = InetAddress.getByName("10.0.0.3"); + InetAddress ipv6Address1 = InetAddress.getByName("::1"); + InetAddress ipv6Address2 = InetAddress.getByName("::2"); + InetAddress ipv6Address3 = InetAddress.getByName("::3"); + + PreferredAddressTypeComparator ipv4 = PreferredAddressTypeComparator.comparator(InternetProtocolFamily.IPv6); + + List<InetAddress> addressList = new ArrayList<InetAddress>(); + Collections.addAll(addressList, ipv4Address1, ipv4Address2, ipv6Address1, + ipv6Address2, ipv4Address3, ipv6Address3); + Collections.sort(addressList, ipv4); + + Assert.assertEquals(Arrays.asList(ipv6Address1, + ipv6Address2, ipv6Address3, ipv4Address1, ipv4Address2, ipv4Address3), addressList); + } +} diff --git a/resolver-dns/src/test/java/io/netty/resolver/dns/TestDnsServer.java b/resolver-dns/src/test/java/io/netty/resolver/dns/TestDnsServer.java index 229ea98..2bca6bd 100644 --- a/resolver-dns/src/test/java/io/netty/resolver/dns/TestDnsServer.java +++ b/resolver-dns/src/test/java/io/netty/resolver/dns/TestDnsServer.java @@ -18,6 +18,7 @@ package io.netty.resolver.dns; import io.netty.util.NetUtil; import io.netty.util.internal.PlatformDependent; import org.apache.directory.server.dns.DnsServer; +import org.apache.directory.server.dns.io.decoder.DnsMessageDecoder; import org.apache.directory.server.dns.io.encoder.DnsMessageEncoder; import org.apache.directory.server.dns.io.encoder.ResourceRecordEncoder; import org.apache.directory.server.dns.messages.DnsMessage; @@ -28,7 +29,6 @@ import org.apache.directory.server.dns.messages.ResourceRecord; import org.apache.directory.server.dns.messages.ResourceRecordImpl; import org.apache.directory.server.dns.messages.ResourceRecordModifier; import org.apache.directory.server.dns.protocol.DnsProtocolHandler; -import org.apache.directory.server.dns.protocol.DnsUdpDecoder; import org.apache.directory.server.dns.protocol.DnsUdpEncoder; import org.apache.directory.server.dns.store.DnsAttribute; import org.apache.directory.server.dns.store.RecordStore; @@ -38,6 +38,8 @@ import org.apache.mina.core.session.IoSession; import org.apache.mina.filter.codec.ProtocolCodecFactory; import org.apache.mina.filter.codec.ProtocolCodecFilter; import org.apache.mina.filter.codec.ProtocolDecoder; +import org.apache.mina.filter.codec.ProtocolDecoderAdapter; +import org.apache.mina.filter.codec.ProtocolDecoderOutput; import org.apache.mina.filter.codec.ProtocolEncoder; import org.apache.mina.filter.codec.ProtocolEncoderOutput; import org.apache.mina.transport.socket.DatagramAcceptor; @@ -83,6 +85,13 @@ class TestDnsServer extends DnsServer { @Override public void start() throws IOException { + start(false); + } + + /** + * Start the {@link TestDnsServer} but drop all {@code AAAA} queries and not send any response to these at all. + */ + public void start(final boolean dropAAAAQueries) throws IOException { InetSocketAddress address = new InetSocketAddress(NetUtil.LOCALHOST4, 0); UdpTransport transport = new UdpTransport(address.getHostName(), address.getPort()); setTransports(transport); @@ -94,7 +103,8 @@ class TestDnsServer extends DnsServer { public void sessionCreated(IoSession session) { // USe our own codec to support AAAA testing session.getFilterChain() - .addFirst("codec", new ProtocolCodecFilter(new TestDnsProtocolUdpCodecFactory())); + .addFirst("codec", new ProtocolCodecFilter( + new TestDnsProtocolUdpCodecFactory(dropAAAAQueries))); } }); @@ -142,6 +152,11 @@ class TestDnsServer extends DnsServer { private final class TestDnsProtocolUdpCodecFactory implements ProtocolCodecFactory { private final DnsMessageEncoder encoder = new DnsMessageEncoder(); private final TestAAAARecordEncoder recordEncoder = new TestAAAARecordEncoder(); + private final boolean dropAAAArecords; + + TestDnsProtocolUdpCodecFactory(boolean dropAAAArecords) { + this.dropAAAArecords = dropAAAArecords; + } @Override public ProtocolEncoder getEncoder(IoSession session) { @@ -175,7 +190,22 @@ class TestDnsServer extends DnsServer { @Override public ProtocolDecoder getDecoder(IoSession session) { - return new DnsUdpDecoder(); + return new ProtocolDecoderAdapter() { + private DnsMessageDecoder decoder = new DnsMessageDecoder(); + + @Override + public void decode(IoSession session, IoBuffer in, ProtocolDecoderOutput out) throws IOException { + DnsMessage message = decoder.decode(in); + if (dropAAAArecords) { + for (QuestionRecord record: message.getQuestionRecords()) { + if (record.getRecordType() == RecordType.AAAA) { + return; + } + } + } + out.write(message); + } + }; } private final class TestAAAARecordEncoder extends ResourceRecordEncoder { diff --git a/resolver-dns/src/test/java/io/netty/resolver/dns/UnixResolverDnsServerAddressStreamProviderTest.java b/resolver-dns/src/test/java/io/netty/resolver/dns/UnixResolverDnsServerAddressStreamProviderTest.java index 3399667..6bb133a 100644 --- a/resolver-dns/src/test/java/io/netty/resolver/dns/UnixResolverDnsServerAddressStreamProviderTest.java +++ b/resolver-dns/src/test/java/io/netty/resolver/dns/UnixResolverDnsServerAddressStreamProviderTest.java @@ -164,6 +164,19 @@ public class UnixResolverDnsServerAddressStreamProviderTest { assertEquals(Collections.singletonList("squarecorp.local"), domains); } + @Test + public void ignoreInvalidEntries() throws Exception { + File f = buildFile("domain netty.local\n" + + "nameserver nil\n" + + "nameserver 127.0.0.3\n"); + UnixResolverDnsServerAddressStreamProvider p = + new UnixResolverDnsServerAddressStreamProvider(f, null); + + DnsServerAddressStream stream = p.nameServerAddressStream("somehost"); + assertEquals(1, stream.size()); + assertHostNameEquals("127.0.0.3", stream.next()); + } + private File buildFile(String contents) throws IOException { File f = folder.newFile(); OutputStream out = new FileOutputStream(f); @@ -175,6 +188,17 @@ public class UnixResolverDnsServerAddressStreamProviderTest { return f; } + @Test + public void ignoreComments() throws Exception { + File f = buildFile("domain linecorp.local\n" + + "nameserver 127.0.0.2 #somecomment\n"); + UnixResolverDnsServerAddressStreamProvider p = + new UnixResolverDnsServerAddressStreamProvider(f, null); + + DnsServerAddressStream stream = p.nameServerAddressStream("somehost"); + assertHostNameEquals("127.0.0.2", stream.next()); + } + private static void assertHostNameEquals(String expectedHostname, InetSocketAddress next) { assertEquals("unexpected hostname: " + next, expectedHostname, next.getHostString()); } diff --git a/resolver/pom.xml b/resolver/pom.xml index aaa891a..18d4e6b 100644 --- a/resolver/pom.xml +++ b/resolver/pom.xml @@ -20,7 +20,7 @@ <parent> <groupId>io.netty</groupId> <artifactId>netty-parent</artifactId> - <version>4.1.33.Final</version> + <version>4.1.48.Final</version> </parent> <artifactId>netty-resolver</artifactId> diff --git a/resolver/src/main/java/io/netty/resolver/AbstractAddressResolver.java b/resolver/src/main/java/io/netty/resolver/AbstractAddressResolver.java index fa034d0..b9fea2e 100644 --- a/resolver/src/main/java/io/netty/resolver/AbstractAddressResolver.java +++ b/resolver/src/main/java/io/netty/resolver/AbstractAddressResolver.java @@ -20,7 +20,6 @@ import io.netty.util.concurrent.EventExecutor; import io.netty.util.concurrent.Future; import io.netty.util.concurrent.Promise; import io.netty.util.internal.TypeParameterMatcher; -import io.netty.util.internal.UnstableApi; import java.net.SocketAddress; import java.nio.channels.UnsupportedAddressTypeException; @@ -32,7 +31,6 @@ import static io.netty.util.internal.ObjectUtil.checkNotNull; /** * A skeletal {@link AddressResolver} implementation. */ -@UnstableApi public abstract class AbstractAddressResolver<T extends SocketAddress> implements AddressResolver<T> { private final EventExecutor executor; @@ -44,7 +42,7 @@ public abstract class AbstractAddressResolver<T extends SocketAddress> implement */ protected AbstractAddressResolver(EventExecutor executor) { this.executor = checkNotNull(executor, "executor"); - matcher = TypeParameterMatcher.find(this, AbstractAddressResolver.class, "T"); + this.matcher = TypeParameterMatcher.find(this, AbstractAddressResolver.class, "T"); } /** @@ -54,7 +52,7 @@ public abstract class AbstractAddressResolver<T extends SocketAddress> implement */ protected AbstractAddressResolver(EventExecutor executor, Class<? extends T> addressType) { this.executor = checkNotNull(executor, "executor"); - matcher = TypeParameterMatcher.get(addressType); + this.matcher = TypeParameterMatcher.get(addressType); } /** diff --git a/resolver/src/main/java/io/netty/resolver/AddressResolver.java b/resolver/src/main/java/io/netty/resolver/AddressResolver.java index 042affd..adb613a 100644 --- a/resolver/src/main/java/io/netty/resolver/AddressResolver.java +++ b/resolver/src/main/java/io/netty/resolver/AddressResolver.java @@ -17,7 +17,6 @@ package io.netty.resolver; import io.netty.util.concurrent.Future; import io.netty.util.concurrent.Promise; -import io.netty.util.internal.UnstableApi; import java.io.Closeable; import java.net.SocketAddress; @@ -27,7 +26,6 @@ import java.util.List; /** * Resolves a possibility unresolved {@link SocketAddress}. */ -@UnstableApi public interface AddressResolver<T extends SocketAddress> extends Closeable { /** diff --git a/resolver/src/main/java/io/netty/resolver/AddressResolverGroup.java b/resolver/src/main/java/io/netty/resolver/AddressResolverGroup.java index e9bb9df..595f53e 100644 --- a/resolver/src/main/java/io/netty/resolver/AddressResolverGroup.java +++ b/resolver/src/main/java/io/netty/resolver/AddressResolverGroup.java @@ -19,7 +19,8 @@ package io.netty.resolver; import io.netty.util.concurrent.EventExecutor; import io.netty.util.concurrent.Future; import io.netty.util.concurrent.FutureListener; -import io.netty.util.internal.UnstableApi; +import io.netty.util.concurrent.GenericFutureListener; +import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; @@ -32,7 +33,6 @@ import java.util.concurrent.ConcurrentMap; /** * Creates and manages {@link NameResolver}s so that each {@link EventExecutor} has its own resolver instance. */ -@UnstableApi public abstract class AddressResolverGroup<T extends SocketAddress> implements Closeable { private static final InternalLogger logger = InternalLoggerFactory.getInstance(AddressResolverGroup.class); @@ -43,18 +43,19 @@ public abstract class AddressResolverGroup<T extends SocketAddress> implements C private final Map<EventExecutor, AddressResolver<T>> resolvers = new IdentityHashMap<EventExecutor, AddressResolver<T>>(); + private final Map<EventExecutor, GenericFutureListener<Future<Object>>> executorTerminationListeners = + new IdentityHashMap<EventExecutor, GenericFutureListener<Future<Object>>>(); + protected AddressResolverGroup() { } /** * Returns the {@link AddressResolver} associated with the specified {@link EventExecutor}. If there's no associated - * resolved found, this method creates and returns a new resolver instance created by + * resolver found, this method creates and returns a new resolver instance created by * {@link #newResolver(EventExecutor)} so that the new resolver is reused on another - * {@link #getResolver(EventExecutor)} call with the same {@link EventExecutor}. + * {@code #getResolver(EventExecutor)} call with the same {@link EventExecutor}. */ public AddressResolver<T> getResolver(final EventExecutor executor) { - if (executor == null) { - throw new NullPointerException("executor"); - } + ObjectUtil.checkNotNull(executor, "executor"); if (executor.isShuttingDown()) { throw new IllegalStateException("executor not accepting a task"); @@ -72,15 +73,20 @@ public abstract class AddressResolverGroup<T extends SocketAddress> implements C } resolvers.put(executor, newResolver); - executor.terminationFuture().addListener(new FutureListener<Object>() { + + final FutureListener<Object> terminationListener = new FutureListener<Object>() { @Override - public void operationComplete(Future<Object> future) throws Exception { + public void operationComplete(Future<Object> future) { synchronized (resolvers) { resolvers.remove(executor); + executorTerminationListeners.remove(executor); } newResolver.close(); } - }); + }; + + executorTerminationListeners.put(executor, terminationListener); + executor.terminationFuture().addListener(terminationListener); r = newResolver; } @@ -101,12 +107,20 @@ public abstract class AddressResolverGroup<T extends SocketAddress> implements C @SuppressWarnings({ "unchecked", "SuspiciousToArrayCall" }) public void close() { final AddressResolver<T>[] rArray; + final Map.Entry<EventExecutor, GenericFutureListener<Future<Object>>>[] listeners; + synchronized (resolvers) { rArray = (AddressResolver<T>[]) resolvers.values().toArray(new AddressResolver[0]); resolvers.clear(); + listeners = executorTerminationListeners.entrySet().toArray(new Map.Entry[0]); + executorTerminationListeners.clear(); + } + + for (final Map.Entry<EventExecutor, GenericFutureListener<Future<Object>>> entry : listeners) { + entry.getKey().terminationFuture().removeListener(entry.getValue()); } - for (AddressResolver<T> r: rArray) { + for (final AddressResolver<T> r: rArray) { try { r.close(); } catch (Throwable t) { diff --git a/resolver/src/main/java/io/netty/resolver/CompositeNameResolver.java b/resolver/src/main/java/io/netty/resolver/CompositeNameResolver.java index 8074c3d..e93547e 100644 --- a/resolver/src/main/java/io/netty/resolver/CompositeNameResolver.java +++ b/resolver/src/main/java/io/netty/resolver/CompositeNameResolver.java @@ -19,19 +19,18 @@ import io.netty.util.concurrent.EventExecutor; import io.netty.util.concurrent.Future; import io.netty.util.concurrent.FutureListener; import io.netty.util.concurrent.Promise; -import io.netty.util.internal.UnstableApi; +import io.netty.util.internal.ObjectUtil; import java.util.Arrays; import java.util.List; -import static io.netty.util.internal.ObjectUtil.*; +import static io.netty.util.internal.ObjectUtil.checkNotNull; /** * A composite {@link SimpleNameResolver} that resolves a host name against a sequence of {@link NameResolver}s. * * In case of a failure, only the last one will be reported. */ -@UnstableApi public final class CompositeNameResolver<T> extends SimpleNameResolver<T> { private final NameResolver<T>[] resolvers; @@ -45,9 +44,7 @@ public final class CompositeNameResolver<T> extends SimpleNameResolver<T> { super(executor); checkNotNull(resolvers, "resolvers"); for (int i = 0; i < resolvers.length; i++) { - if (resolvers[i] == null) { - throw new NullPointerException("resolvers[" + i + ']'); - } + ObjectUtil.checkNotNull(resolvers[i], "resolvers[" + i + ']'); } if (resolvers.length < 2) { throw new IllegalArgumentException("resolvers: " + Arrays.asList(resolvers) + diff --git a/resolver/src/main/java/io/netty/resolver/DefaultAddressResolverGroup.java b/resolver/src/main/java/io/netty/resolver/DefaultAddressResolverGroup.java index 9e22b8f..1b6fb53 100644 --- a/resolver/src/main/java/io/netty/resolver/DefaultAddressResolverGroup.java +++ b/resolver/src/main/java/io/netty/resolver/DefaultAddressResolverGroup.java @@ -17,14 +17,12 @@ package io.netty.resolver; import io.netty.util.concurrent.EventExecutor; -import io.netty.util.internal.UnstableApi; import java.net.InetSocketAddress; /** * A {@link AddressResolverGroup} of {@link DefaultNameResolver}s. */ -@UnstableApi public final class DefaultAddressResolverGroup extends AddressResolverGroup<InetSocketAddress> { public static final DefaultAddressResolverGroup INSTANCE = new DefaultAddressResolverGroup(); diff --git a/resolver/src/main/java/io/netty/resolver/DefaultHostsFileEntriesResolver.java b/resolver/src/main/java/io/netty/resolver/DefaultHostsFileEntriesResolver.java index 9051262..97ca29b 100644 --- a/resolver/src/main/java/io/netty/resolver/DefaultHostsFileEntriesResolver.java +++ b/resolver/src/main/java/io/netty/resolver/DefaultHostsFileEntriesResolver.java @@ -17,7 +17,6 @@ package io.netty.resolver; import io.netty.util.CharsetUtil; import io.netty.util.internal.PlatformDependent; -import io.netty.util.internal.UnstableApi; import java.net.Inet4Address; import java.net.Inet6Address; @@ -29,7 +28,6 @@ import java.util.Map; /** * Default {@link HostsFileEntriesResolver} that resolves hosts file entries only once. */ -@UnstableApi public final class DefaultHostsFileEntriesResolver implements HostsFileEntriesResolver { private final Map<String, Inet4Address> inet4Entries; diff --git a/resolver/src/main/java/io/netty/resolver/DefaultNameResolver.java b/resolver/src/main/java/io/netty/resolver/DefaultNameResolver.java index d6d3aea..38ae0eb 100644 --- a/resolver/src/main/java/io/netty/resolver/DefaultNameResolver.java +++ b/resolver/src/main/java/io/netty/resolver/DefaultNameResolver.java @@ -19,7 +19,6 @@ package io.netty.resolver; import io.netty.util.internal.SocketUtils; import io.netty.util.concurrent.EventExecutor; import io.netty.util.concurrent.Promise; -import io.netty.util.internal.UnstableApi; import java.net.InetAddress; import java.net.UnknownHostException; @@ -30,7 +29,6 @@ import java.util.List; * A {@link InetNameResolver} that resolves using JDK's built-in domain name lookup mechanism. * Note that this resolver performs a blocking name lookup from the caller thread. */ -@UnstableApi public class DefaultNameResolver extends InetNameResolver { public DefaultNameResolver(EventExecutor executor) { diff --git a/resolver/src/main/java/io/netty/resolver/HostsFileEntries.java b/resolver/src/main/java/io/netty/resolver/HostsFileEntries.java index 03f555c..7ea76a4 100644 --- a/resolver/src/main/java/io/netty/resolver/HostsFileEntries.java +++ b/resolver/src/main/java/io/netty/resolver/HostsFileEntries.java @@ -15,8 +15,6 @@ */ package io.netty.resolver; -import io.netty.util.internal.UnstableApi; - import java.net.Inet4Address; import java.net.Inet6Address; import java.util.Collections; @@ -26,7 +24,6 @@ import java.util.Map; /** * A container of hosts file entries */ -@UnstableApi public final class HostsFileEntries { /** diff --git a/resolver/src/main/java/io/netty/resolver/HostsFileEntriesResolver.java b/resolver/src/main/java/io/netty/resolver/HostsFileEntriesResolver.java index 753467d..feedbd9 100644 --- a/resolver/src/main/java/io/netty/resolver/HostsFileEntriesResolver.java +++ b/resolver/src/main/java/io/netty/resolver/HostsFileEntriesResolver.java @@ -15,14 +15,11 @@ */ package io.netty.resolver; -import io.netty.util.internal.UnstableApi; - import java.net.InetAddress; /** * Resolves a hostname against the hosts file entries. */ -@UnstableApi public interface HostsFileEntriesResolver { /** diff --git a/resolver/src/main/java/io/netty/resolver/HostsFileParser.java b/resolver/src/main/java/io/netty/resolver/HostsFileParser.java index 16eaba6..113572d 100644 --- a/resolver/src/main/java/io/netty/resolver/HostsFileParser.java +++ b/resolver/src/main/java/io/netty/resolver/HostsFileParser.java @@ -17,7 +17,6 @@ package io.netty.resolver; import io.netty.util.NetUtil; import io.netty.util.internal.PlatformDependent; -import io.netty.util.internal.UnstableApi; import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; @@ -43,7 +42,6 @@ import static io.netty.util.internal.ObjectUtil.*; /** * A parser for hosts files. */ -@UnstableApi public final class HostsFileParser { private static final String WINDOWS_DEFAULT_SYSTEM_ROOT = "C:\\Windows"; diff --git a/resolver/src/main/java/io/netty/resolver/InetNameResolver.java b/resolver/src/main/java/io/netty/resolver/InetNameResolver.java index eb5cfe0..f90cd10 100644 --- a/resolver/src/main/java/io/netty/resolver/InetNameResolver.java +++ b/resolver/src/main/java/io/netty/resolver/InetNameResolver.java @@ -17,7 +17,6 @@ package io.netty.resolver; import io.netty.util.concurrent.EventExecutor; import io.netty.util.concurrent.Future; -import io.netty.util.internal.UnstableApi; import java.net.InetAddress; import java.net.InetSocketAddress; @@ -25,7 +24,6 @@ import java.net.InetSocketAddress; /** * A skeletal {@link NameResolver} implementation that resolves {@link InetAddress}. */ -@UnstableApi public abstract class InetNameResolver extends SimpleNameResolver<InetAddress> { private volatile AddressResolver<InetSocketAddress> addressResolver; diff --git a/resolver/src/main/java/io/netty/resolver/InetSocketAddressResolver.java b/resolver/src/main/java/io/netty/resolver/InetSocketAddressResolver.java index c7db25d..7f5f5b2 100644 --- a/resolver/src/main/java/io/netty/resolver/InetSocketAddressResolver.java +++ b/resolver/src/main/java/io/netty/resolver/InetSocketAddressResolver.java @@ -19,7 +19,6 @@ import io.netty.util.concurrent.EventExecutor; import io.netty.util.concurrent.Future; import io.netty.util.concurrent.FutureListener; import io.netty.util.concurrent.Promise; -import io.netty.util.internal.UnstableApi; import java.net.InetAddress; import java.net.InetSocketAddress; @@ -29,7 +28,6 @@ import java.util.List; /** * A {@link AbstractAddressResolver} that resolves {@link InetSocketAddress}. */ -@UnstableApi public class InetSocketAddressResolver extends AbstractAddressResolver<InetSocketAddress> { final NameResolver<InetAddress> nameResolver; diff --git a/resolver/src/main/java/io/netty/resolver/NameResolver.java b/resolver/src/main/java/io/netty/resolver/NameResolver.java index 53a56b4..818122c 100644 --- a/resolver/src/main/java/io/netty/resolver/NameResolver.java +++ b/resolver/src/main/java/io/netty/resolver/NameResolver.java @@ -18,7 +18,6 @@ package io.netty.resolver; import io.netty.util.concurrent.Future; import io.netty.util.concurrent.Promise; -import io.netty.util.internal.UnstableApi; import java.io.Closeable; import java.util.List; @@ -26,7 +25,6 @@ import java.util.List; /** * Resolves an arbitrary string that represents the name of an endpoint into an address. */ -@UnstableApi public interface NameResolver<T> extends Closeable { /** diff --git a/resolver/src/main/java/io/netty/resolver/NoopAddressResolver.java b/resolver/src/main/java/io/netty/resolver/NoopAddressResolver.java index 9551469..c34bb57 100644 --- a/resolver/src/main/java/io/netty/resolver/NoopAddressResolver.java +++ b/resolver/src/main/java/io/netty/resolver/NoopAddressResolver.java @@ -18,7 +18,6 @@ package io.netty.resolver; import io.netty.util.concurrent.EventExecutor; import io.netty.util.concurrent.Promise; -import io.netty.util.internal.UnstableApi; import java.net.SocketAddress; import java.util.Collections; @@ -28,7 +27,6 @@ import java.util.List; * A {@link AddressResolver} that does not perform any resolution but always reports successful resolution. * This resolver is useful when name resolution is performed by a handler in a pipeline, such as a proxy handler. */ -@UnstableApi public class NoopAddressResolver extends AbstractAddressResolver<SocketAddress> { public NoopAddressResolver(EventExecutor executor) { diff --git a/resolver/src/main/java/io/netty/resolver/NoopAddressResolverGroup.java b/resolver/src/main/java/io/netty/resolver/NoopAddressResolverGroup.java index 7727652..980cc50 100644 --- a/resolver/src/main/java/io/netty/resolver/NoopAddressResolverGroup.java +++ b/resolver/src/main/java/io/netty/resolver/NoopAddressResolverGroup.java @@ -17,14 +17,12 @@ package io.netty.resolver; import io.netty.util.concurrent.EventExecutor; -import io.netty.util.internal.UnstableApi; import java.net.SocketAddress; /** * A {@link AddressResolverGroup} of {@link NoopAddressResolver}s. */ -@UnstableApi public final class NoopAddressResolverGroup extends AddressResolverGroup<SocketAddress> { public static final NoopAddressResolverGroup INSTANCE = new NoopAddressResolverGroup(); diff --git a/resolver/src/main/java/io/netty/resolver/ResolvedAddressTypes.java b/resolver/src/main/java/io/netty/resolver/ResolvedAddressTypes.java index ae3e55d..ea849bf 100644 --- a/resolver/src/main/java/io/netty/resolver/ResolvedAddressTypes.java +++ b/resolver/src/main/java/io/netty/resolver/ResolvedAddressTypes.java @@ -15,12 +15,9 @@ */ package io.netty.resolver; -import io.netty.util.internal.UnstableApi; - /** * Defined resolved address types. */ -@UnstableApi public enum ResolvedAddressTypes { /** * Only resolve IPv4 addresses diff --git a/resolver/src/main/java/io/netty/resolver/RoundRobinInetAddressResolver.java b/resolver/src/main/java/io/netty/resolver/RoundRobinInetAddressResolver.java index f505328..9c26f38 100644 --- a/resolver/src/main/java/io/netty/resolver/RoundRobinInetAddressResolver.java +++ b/resolver/src/main/java/io/netty/resolver/RoundRobinInetAddressResolver.java @@ -20,7 +20,6 @@ import io.netty.util.concurrent.Future; import io.netty.util.concurrent.FutureListener; import io.netty.util.concurrent.Promise; import io.netty.util.internal.PlatformDependent; -import io.netty.util.internal.UnstableApi; import java.net.InetAddress; import java.net.InetSocketAddress; @@ -35,7 +34,6 @@ import java.util.List; * if multiple are returned by the {@link NameResolver}. * Use {@link #asAddressResolver()} to create a {@link InetSocketAddress} resolver */ -@UnstableApi public class RoundRobinInetAddressResolver extends InetNameResolver { private final NameResolver<InetAddress> nameResolver; @@ -100,4 +98,9 @@ public class RoundRobinInetAddressResolver extends InetNameResolver { private static int randomIndex(int numAddresses) { return numAddresses == 1 ? 0 : PlatformDependent.threadLocalRandom().nextInt(numAddresses); } + + @Override + public void close() { + nameResolver.close(); + } } diff --git a/resolver/src/main/java/io/netty/resolver/SimpleNameResolver.java b/resolver/src/main/java/io/netty/resolver/SimpleNameResolver.java index 923530f..c8af37a 100644 --- a/resolver/src/main/java/io/netty/resolver/SimpleNameResolver.java +++ b/resolver/src/main/java/io/netty/resolver/SimpleNameResolver.java @@ -19,7 +19,6 @@ package io.netty.resolver; import io.netty.util.concurrent.EventExecutor; import io.netty.util.concurrent.Future; import io.netty.util.concurrent.Promise; -import io.netty.util.internal.UnstableApi; import java.util.List; @@ -28,7 +27,6 @@ import static io.netty.util.internal.ObjectUtil.*; /** * A skeletal {@link NameResolver} implementation. */ -@UnstableApi public abstract class SimpleNameResolver<T> implements NameResolver<T> { private final EventExecutor executor; diff --git a/resolver/src/main/java/io/netty/resolver/package-info.java b/resolver/src/main/java/io/netty/resolver/package-info.java index 0dd9ab2..a924598 100644 --- a/resolver/src/main/java/io/netty/resolver/package-info.java +++ b/resolver/src/main/java/io/netty/resolver/package-info.java @@ -17,7 +17,4 @@ /** * Resolves an arbitrary string that represents the name of an endpoint into an address. */ -@UnstableApi package io.netty.resolver; - -import io.netty.util.internal.UnstableApi; diff --git a/tarball/pom.xml b/tarball/pom.xml index d55b673..401aca9 100644 --- a/tarball/pom.xml +++ b/tarball/pom.xml @@ -20,7 +20,7 @@ <parent> <groupId>io.netty</groupId> <artifactId>netty-parent</artifactId> - <version>4.1.33.Final</version> + <version>4.1.48.Final</version> </parent> <artifactId>netty-tarball</artifactId> @@ -123,6 +123,14 @@ <scope>compile</scope> <optional>true</optional> </dependency> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>netty-resolver-dns-native-macos</artifactId> + <version>${project.version}</version> + <classifier>osx-x86_64</classifier> + <scope>compile</scope> + <optional>true</optional> + </dependency> </dependencies> </profile> <profile> @@ -145,6 +153,14 @@ <scope>compile</scope> <optional>true</optional> </dependency> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>netty-resolver-dns-native-macos</artifactId> + <version>${project.version}</version> + <classifier>osx-x86_64</classifier> + <scope>compile</scope> + <optional>true</optional> + </dependency> </dependencies> </profile> diff --git a/testsuite-autobahn/pom.xml b/testsuite-autobahn/pom.xml index c85f7e1..c4c7edb 100644 --- a/testsuite-autobahn/pom.xml +++ b/testsuite-autobahn/pom.xml @@ -20,7 +20,7 @@ <parent> <groupId>io.netty</groupId> <artifactId>netty-parent</artifactId> - <version>4.1.33.Final</version> + <version>4.1.48.Final</version> </parent> <artifactId>netty-testsuite-autobahn</artifactId> @@ -28,6 +28,10 @@ <name>Netty/Testsuite/Autobahn</name> + <properties> + <skipJapicmp>true</skipJapicmp> + </properties> + <dependencies> <dependency> <groupId>${project.groupId}</groupId> diff --git a/testsuite-autobahn/src/main/java/io/netty/testsuite/autobahn/AutobahnServerHandler.java b/testsuite-autobahn/src/main/java/io/netty/testsuite/autobahn/AutobahnServerHandler.java index 75f4062..2febe9e 100644 --- a/testsuite-autobahn/src/main/java/io/netty/testsuite/autobahn/AutobahnServerHandler.java +++ b/testsuite-autobahn/src/main/java/io/netty/testsuite/autobahn/AutobahnServerHandler.java @@ -73,13 +73,13 @@ public class AutobahnServerHandler extends ChannelInboundHandlerAdapter { throws Exception { // Handle a bad request. if (!req.decoderResult().isSuccess()) { - sendHttpResponse(ctx, req, new DefaultFullHttpResponse(HTTP_1_1, BAD_REQUEST)); + sendHttpResponse(ctx, req, new DefaultFullHttpResponse(HTTP_1_1, BAD_REQUEST, ctx.alloc().buffer(0))); return; } // Allow only GET methods. - if (req.method() != GET) { - sendHttpResponse(ctx, req, new DefaultFullHttpResponse(HTTP_1_1, FORBIDDEN)); + if (!GET.equals(req.method())) { + sendHttpResponse(ctx, req, new DefaultFullHttpResponse(HTTP_1_1, FORBIDDEN, ctx.alloc().buffer(0))); return; } diff --git a/testsuite-http2/pom.xml b/testsuite-http2/pom.xml index 061cd94..802b278 100644 --- a/testsuite-http2/pom.xml +++ b/testsuite-http2/pom.xml @@ -20,7 +20,7 @@ <parent> <groupId>io.netty</groupId> <artifactId>netty-parent</artifactId> - <version>4.1.33.Final</version> + <version>4.1.48.Final</version> </parent> <artifactId>netty-testsuite-http2</artifactId> @@ -28,6 +28,10 @@ <name>Netty/Testsuite/Http2</name> + <properties> + <skipJapicmp>true</skipJapicmp> + </properties> + <dependencies> <dependency> <groupId>${project.groupId}</groupId> diff --git a/testsuite-http2/src/main/java/io/netty/testsuite/http2/HelloWorldHttp1Handler.java b/testsuite-http2/src/main/java/io/netty/testsuite/http2/HelloWorldHttp1Handler.java index 493e31b..cf15818 100644 --- a/testsuite-http2/src/main/java/io/netty/testsuite/http2/HelloWorldHttp1Handler.java +++ b/testsuite-http2/src/main/java/io/netty/testsuite/http2/HelloWorldHttp1Handler.java @@ -47,7 +47,7 @@ public class HelloWorldHttp1Handler extends SimpleChannelInboundHandler<FullHttp @Override public void channelRead0(ChannelHandlerContext ctx, FullHttpRequest req) throws Exception { if (HttpUtil.is100ContinueExpected(req)) { - ctx.write(new DefaultFullHttpResponse(HTTP_1_1, CONTINUE)); + ctx.write(new DefaultFullHttpResponse(HTTP_1_1, CONTINUE, ctx.alloc().buffer(0))); } boolean keepAlive = HttpUtil.isKeepAlive(req); diff --git a/testsuite-http2/src/main/java/io/netty/testsuite/http2/Http2Server.java b/testsuite-http2/src/main/java/io/netty/testsuite/http2/Http2Server.java index 360cd24..e562b00 100644 --- a/testsuite-http2/src/main/java/io/netty/testsuite/http2/Http2Server.java +++ b/testsuite-http2/src/main/java/io/netty/testsuite/http2/Http2Server.java @@ -26,7 +26,7 @@ import io.netty.handler.logging.LogLevel; import io.netty.handler.logging.LoggingHandler; /** - * A HTTP/2 Server that responds to requests with a Hello World. Once started, you can test the + * An HTTP/2 Server that responds to requests with a Hello World. Once started, you can test the * server with the example client. */ public final class Http2Server { diff --git a/testsuite-http2/src/main/java/io/netty/testsuite/http2/Http2ServerInitializer.java b/testsuite-http2/src/main/java/io/netty/testsuite/http2/Http2ServerInitializer.java index 7b67fc9..582de34 100644 --- a/testsuite-http2/src/main/java/io/netty/testsuite/http2/Http2ServerInitializer.java +++ b/testsuite-http2/src/main/java/io/netty/testsuite/http2/Http2ServerInitializer.java @@ -16,6 +16,8 @@ package io.netty.testsuite.http2; +import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero; + import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.channel.ChannelInitializer; @@ -31,7 +33,6 @@ import io.netty.handler.codec.http.HttpServerUpgradeHandler.UpgradeCodecFactory; import io.netty.handler.codec.http2.CleartextHttp2ServerUpgradeHandler; import io.netty.handler.codec.http2.Http2CodecUtil; import io.netty.handler.codec.http2.Http2ServerUpgradeCodec; -import io.netty.handler.ssl.SslContext; import io.netty.util.AsciiString; import io.netty.util.ReferenceCountUtil; @@ -59,9 +60,7 @@ public class Http2ServerInitializer extends ChannelInitializer<SocketChannel> { } Http2ServerInitializer(int maxHttpContentLength) { - if (maxHttpContentLength < 0) { - throw new IllegalArgumentException("maxHttpContentLength (expected >= 0): " + maxHttpContentLength); - } + checkPositiveOrZero(maxHttpContentLength, "maxHttpContentLength"); this.maxHttpContentLength = maxHttpContentLength; } diff --git a/testsuite-native-image/pom.xml b/testsuite-native-image/pom.xml new file mode 100644 index 0000000..f0fbde8 --- /dev/null +++ b/testsuite-native-image/pom.xml @@ -0,0 +1,121 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + ~ Copyright 2017 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. + --> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> + + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>io.netty</groupId> + <artifactId>netty-parent</artifactId> + <version>4.1.48.Final</version> + </parent> + + <artifactId>netty-testsuite-native-image</artifactId> + <packaging>jar</packaging> + + <name>Netty/Testsuite/NativeImage</name> + + <properties> + <skipJapicmp>true</skipJapicmp> + </properties> + + <dependencies> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>netty-common</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>netty-buffer</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>netty-transport</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>netty-handler</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>netty-codec-http</artifactId> + <version>${project.version}</version> + </dependency> + + </dependencies> + + <profiles> + <profile> + <id>skipTests</id> + <activation> + <property> + <name>skipTests</name> + </property> + </activation> + <properties> + <skipNativeImageTestsuite>true</skipNativeImageTestsuite> + </properties> + </profile> + </profiles> + + <build> + <plugins> + <plugin> + <groupId>com.oracle.substratevm</groupId> + <artifactId>native-image-maven-plugin</artifactId> + <version>${graalvm.version}</version> + <executions> + <execution> + <goals> + <goal>native-image</goal> + </goals> + <phase>package</phase> + </execution> + </executions> + <configuration> + <skip>${skipNativeImageTestsuite}</skip> + <imageName>${project.artifactId}</imageName> + <mainClass>io.netty.testsuite.svm.HttpNativeServer</mainClass> + <buildArgs>--report-unsupported-elements-at-runtime --allow-incomplete-classpath</buildArgs> + </configuration> + </plugin> + <plugin> + <groupId>org.codehaus.mojo</groupId> + <artifactId>exec-maven-plugin</artifactId> + <version>1.6.0</version> + <executions> + <!-- This will do a whitesmoke test: if the substitutions are missing the binary will fail to run --> + <!-- If the metadata is missing the build above will fail --> + <execution> + <id>verify-native-image</id> + <phase>verify</phase> + <goals> + <goal>exec</goal> + </goals> + </execution> + </executions> + <configuration> + <skip>${skipNativeImageTestsuite}</skip> + <executable>${project.build.directory}/${project.artifactId}</executable> + </configuration> + </plugin> + </plugins> + </build> +</project> diff --git a/testsuite-native-image/src/main/java/io/netty/testsuite/svm/HttpNativeServer.java b/testsuite-native-image/src/main/java/io/netty/testsuite/svm/HttpNativeServer.java new file mode 100644 index 0000000..3676cd0 --- /dev/null +++ b/testsuite-native-image/src/main/java/io/netty/testsuite/svm/HttpNativeServer.java @@ -0,0 +1,64 @@ +/* + * Copyright 2019 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.testsuite.svm; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelOption; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; + +/** + * An HTTP server that sends back the content of the received HTTP request + * in a pretty plaintext form. + */ +public final class HttpNativeServer { + + /** + * Main entry point (not instantiable) + */ + private HttpNativeServer() { + } + + public static void main(String[] args) throws Exception { + // Configure the server. + EventLoopGroup bossGroup = new NioEventLoopGroup(1); + EventLoopGroup workerGroup = new NioEventLoopGroup(); + // Control status. + boolean serverStartSucess = false; + try { + ServerBootstrap b = new ServerBootstrap(); + b.option(ChannelOption.SO_BACKLOG, 1024); + b.group(bossGroup, workerGroup) + .channel(NioServerSocketChannel.class) + .handler(new LoggingHandler(LogLevel.INFO)) + .childHandler(new HttpNativeServerInitializer()); + + Channel channel = b.bind(0).sync().channel(); + System.err.println("Server started, will shutdown now."); + channel.close().sync(); + serverStartSucess = true; + } finally { + bossGroup.shutdownGracefully(); + workerGroup.shutdownGracefully(); + } + // return the right system exit code to signal success + System.exit(serverStartSucess ? 0 : 1); + } +} diff --git a/testsuite-native-image/src/main/java/io/netty/testsuite/svm/HttpNativeServerHandler.java b/testsuite-native-image/src/main/java/io/netty/testsuite/svm/HttpNativeServerHandler.java new file mode 100644 index 0000000..33531dc --- /dev/null +++ b/testsuite-native-image/src/main/java/io/netty/testsuite/svm/HttpNativeServerHandler.java @@ -0,0 +1,67 @@ +/* + * Copyright 2013 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.testsuite.svm; + +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.handler.codec.http.DefaultFullHttpResponse; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.HttpObject; +import io.netty.handler.codec.http.HttpUtil; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.util.AsciiString; + +import static io.netty.handler.codec.http.HttpHeaderNames.*; +import static io.netty.handler.codec.http.HttpResponseStatus.*; +import static io.netty.handler.codec.http.HttpVersion.*; + +public class HttpNativeServerHandler extends SimpleChannelInboundHandler<HttpObject> { + private static final byte[] CONTENT = { 'H', 'e', 'l', 'l', 'o', ' ', 'N', 'a', 't', 'i', 'v', 'e' }; + + private static final AsciiString KEEP_ALIVE = AsciiString.cached("keep-alive"); + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) { + ctx.flush(); + } + + @Override + public void channelRead0(ChannelHandlerContext ctx, HttpObject msg) { + if (msg instanceof HttpRequest) { + HttpRequest req = (HttpRequest) msg; + + boolean keepAlive = HttpUtil.isKeepAlive(req); + FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, OK, Unpooled.wrappedBuffer(CONTENT)); + response.headers().set(CONTENT_TYPE, "text/plain"); + response.headers().setInt(CONTENT_LENGTH, response.content().readableBytes()); + + if (!keepAlive) { + ctx.write(response).addListener(ChannelFutureListener.CLOSE); + } else { + response.headers().set(CONNECTION, KEEP_ALIVE); + ctx.write(response); + } + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + cause.printStackTrace(); + ctx.close(); + } +} diff --git a/testsuite-native-image/src/main/java/io/netty/testsuite/svm/HttpNativeServerInitializer.java b/testsuite-native-image/src/main/java/io/netty/testsuite/svm/HttpNativeServerInitializer.java new file mode 100644 index 0000000..80a53a0 --- /dev/null +++ b/testsuite-native-image/src/main/java/io/netty/testsuite/svm/HttpNativeServerInitializer.java @@ -0,0 +1,33 @@ +/* + * Copyright 2019 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.testsuite.svm; + +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.http.HttpServerCodec; +import io.netty.handler.codec.http.HttpServerExpectContinueHandler; + +public class HttpNativeServerInitializer extends ChannelInitializer<SocketChannel> { + + @Override + public void initChannel(SocketChannel ch) { + ChannelPipeline p = ch.pipeline(); + p.addLast(new HttpServerCodec()); + p.addLast(new HttpServerExpectContinueHandler()); + p.addLast(new HttpNativeServerHandler()); + } +} diff --git a/testsuite-native-image/src/main/java/io/netty/testsuite/svm/package-info.java b/testsuite-native-image/src/main/java/io/netty/testsuite/svm/package-info.java new file mode 100644 index 0000000..89c639a --- /dev/null +++ b/testsuite-native-image/src/main/java/io/netty/testsuite/svm/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2019 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. + */ + +/** + * A hello world server that should be compiled to native. + */ +package io.netty.testsuite.svm; diff --git a/testsuite-osgi/pom.xml b/testsuite-osgi/pom.xml index 767d801..153da53 100644 --- a/testsuite-osgi/pom.xml +++ b/testsuite-osgi/pom.xml @@ -20,7 +20,7 @@ <parent> <groupId>io.netty</groupId> <artifactId>netty-parent</artifactId> - <version>4.1.33.Final</version> + <version>4.1.48.Final</version> </parent> <artifactId>netty-testsuite-osgi</artifactId> @@ -31,6 +31,7 @@ <properties> <exam.version>4.13.0</exam.version> <argLine.java9.extras>--add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/jdk.internal.loader=ALL-UNNAMED --add-opens=java.base/java.net=ALL-UNNAMED --add-opens=java.base/java.security=ALL-UNNAMED</argLine.java9.extras> + <skipJapicmp>true</skipJapicmp> </properties> <profiles> @@ -45,6 +46,40 @@ <skipOsgiTestsuite>true</skipOsgiTestsuite> </properties> </profile> + + <profile> + <id>linux</id> + <activation> + <os> + <family>linux</family> + </os> + </activation> + <dependencies> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>netty-transport-native-epoll</artifactId> + <version>${project.version}</version> + <scope>test</scope> + </dependency> + </dependencies> + </profile> + + <profile> + <id>mac</id> + <activation> + <os> + <family>mac</family> + </os> + </activation> + <dependencies> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>netty-transport-native-kqueue</artifactId> + <version>${project.version}</version> + <scope>test</scope> + </dependency> + </dependencies> + </profile> </profiles> <dependencies> @@ -145,12 +180,6 @@ <version>${project.version}</version> <scope>test</scope> </dependency> - <dependency> - <groupId>${project.groupId}</groupId> - <artifactId>netty-transport-rxtx</artifactId> - <version>${project.version}</version> - <scope>test</scope> - </dependency> <dependency> <groupId>${project.groupId}</groupId> <artifactId>netty-transport-sctp</artifactId> @@ -165,67 +194,54 @@ </dependency> <dependency> - <groupId>org.ops4j.pax.exam</groupId> - <artifactId>pax-exam-container-native</artifactId> - <version>${exam.version}</version> - <scope>test</scope> + <groupId>org.apache.felix</groupId> + <artifactId>org.apache.felix.configadmin</artifactId> + <version>1.9.14</version> </dependency> <dependency> - <groupId>org.ops4j.pax.exam</groupId> - <artifactId>pax-exam-junit4</artifactId> - <version>${exam.version}</version> + <groupId>org.apache.felix</groupId> + <artifactId>org.apache.felix.framework</artifactId> + <version>6.0.2</version> <scope>test</scope> </dependency> <dependency> <groupId>org.ops4j.pax.exam</groupId> - <artifactId>pax-exam</artifactId> + <artifactId>pax-exam-junit4</artifactId> <version>${exam.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>org.ops4j.pax.exam</groupId> - <artifactId>pax-exam-spi</artifactId> + <artifactId>pax-exam-container-native</artifactId> <version>${exam.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>org.ops4j.pax.exam</groupId> - <artifactId>pax-exam-link-mvn</artifactId> + <artifactId>pax-exam-link-assembly</artifactId> <version>${exam.version}</version> <scope>test</scope> </dependency> - <dependency> - <groupId>org.ops4j.pax.url</groupId> - <artifactId>pax-url-wrap</artifactId> - <version>2.4.7</version> - </dependency> - - <dependency> - <groupId>org.osgi</groupId> - <artifactId>org.osgi.core</artifactId> - <version>6.0.0</version> - <scope>test</scope> - </dependency> - <dependency> - <groupId>org.apache.felix</groupId> - <artifactId>org.apache.felix.framework</artifactId> - <version>5.6.10</version> - <scope>test</scope> - </dependency> - </dependencies> <build> <plugins> <plugin> - <groupId>org.ops4j.pax.exam</groupId> - <artifactId>maven-paxexam-plugin</artifactId> + <groupId>com.github.veithen.alta</groupId> + <artifactId>alta-maven-plugin</artifactId> + <version>0.6.2</version> <executions> <execution> - <id>generate-config</id> <goals> - <goal>generate-depends-file</goal> + <goal>generate-test-resources</goal> </goals> + <configuration> + <name>%bundle.symbolicName%.link</name> + <value>%url%</value> + <dependencySet> + <scope>test</scope> + </dependencySet> + </configuration> </execution> </executions> </plugin> @@ -233,6 +249,9 @@ <artifactId>maven-surefire-plugin</artifactId> <configuration> <skip>${skipOsgiTestsuite}</skip> + <additionalClasspathElements> + <additionalClasspathElement>${project.build.directory}/generated-test-resources/alta</additionalClasspathElement> + </additionalClasspathElements> </configuration> </plugin> </plugins> diff --git a/testsuite-osgi/src/test/java/io/netty/osgitests/OsgiBundleTest.java b/testsuite-osgi/src/test/java/io/netty/osgitests/OsgiBundleTest.java index b56cd6c..aa86ffc 100644 --- a/testsuite-osgi/src/test/java/io/netty/osgitests/OsgiBundleTest.java +++ b/testsuite-osgi/src/test/java/io/netty/osgitests/OsgiBundleTest.java @@ -19,21 +19,16 @@ package io.netty.osgitests; import static org.junit.Assert.assertFalse; import static org.ops4j.pax.exam.CoreOptions.frameworkProperty; import static org.ops4j.pax.exam.CoreOptions.junitBundles; -import static org.ops4j.pax.exam.CoreOptions.mavenBundle; import static org.ops4j.pax.exam.CoreOptions.systemProperty; -import static org.ops4j.pax.exam.CoreOptions.wrappedBundle; +import static org.ops4j.pax.exam.CoreOptions.url; import static org.osgi.framework.Constants.FRAMEWORK_BOOTDELEGATION; -import java.io.BufferedReader; import java.io.File; -import java.io.FileReader; -import java.io.IOException; +import java.io.FilenameFilter; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.HashSet; import java.util.Set; -import java.util.regex.Pattern; import org.junit.Test; import org.junit.runner.RunWith; @@ -44,45 +39,25 @@ import io.netty.util.internal.PlatformDependent; @RunWith(PaxExam.class) public class OsgiBundleTest { - private static final Pattern SLASH = Pattern.compile("/", Pattern.LITERAL); - private static final String DEPENCIES_LINE = "# dependencies"; - private static final String GROUP = "io.netty"; - private static final Collection<String> BUNDLES; + private static final Collection<String> LINKS; static { - final Set<String> artifacts = new HashSet<String>(); - final File f = new File("target/classes/META-INF/maven/dependencies.properties"); - try { - final BufferedReader r = new BufferedReader(new FileReader(f)); - try { - boolean haveDeps = false; + final Set<String> links = new HashSet<String>(); - while (true) { - final String line = r.readLine(); - if (line == null) { - // End-of-file - break; - } - - // We need to ignore any lines up to the dependencies - // line, otherwise we would include ourselves. - if (DEPENCIES_LINE.equals(line)) { - haveDeps = true; - } else if (haveDeps && line.startsWith(GROUP)) { - final String[] split = SLASH.split(line); - if (split.length > 1) { - artifacts.add(split[1]); - } - } - } - } finally { - r.close(); + final File directory = new File("target/generated-test-resources/alta/"); + File[] files = directory.listFiles(new FilenameFilter() { + @Override + public boolean accept(File dir, String name) { + return (name.startsWith("io.netty") || name.startsWith("com.barchart.udt")) && name.endsWith(".link"); } - } catch (IOException e) { - throw new ExceptionInInitializerError(e); + }); + if (files == null) { + throw new IllegalStateException(directory + " is not found or is not a directory"); } - - BUNDLES = artifacts; + for (File f: files) { + links.add(f.getName()); + } + LINKS = links; } @Configuration @@ -92,13 +67,10 @@ public class OsgiBundleTest { // Avoid boot delegating sun.misc which would fail testCanLoadPlatformDependent() options.add(frameworkProperty(FRAMEWORK_BOOTDELEGATION).value("com.sun.*")); options.add(systemProperty("pax.exam.osgi.unresolved.fail").value("true")); - options.addAll(Arrays.asList(junitBundles())); - - options.add(mavenBundle("com.barchart.udt", "barchart-udt-bundle").versionAsInProject()); - options.add(wrappedBundle(mavenBundle("org.rxtx", "rxtx").versionAsInProject())); + options.add(junitBundles()); - for (String name : BUNDLES) { - options.add(mavenBundle(GROUP, name).versionAsInProject()); + for (String link : LINKS) { + options.add(url("link:classpath:" + link)); } return options.toArray(new Option[0]); @@ -107,11 +79,11 @@ public class OsgiBundleTest { @Test public void testResolvedBundles() { // No-op, as we just want the bundles to be resolved. Just check if we tested something - assertFalse("At least one bundle needs to be tested", BUNDLES.isEmpty()); + assertFalse("At least one bundle needs to be tested", LINKS.isEmpty()); } @Test public void testCanLoadPlatformDependent() { - assertFalse(PlatformDependent.hasUnsafe()); + assertFalse(PlatformDependent.addressSize() == 0); } } diff --git a/testsuite-shading/pom.xml b/testsuite-shading/pom.xml index 890105e..3ea27d9 100644 --- a/testsuite-shading/pom.xml +++ b/testsuite-shading/pom.xml @@ -20,7 +20,7 @@ <parent> <groupId>io.netty</groupId> <artifactId>netty-parent</artifactId> - <version>4.1.33.Final</version> + <version>4.1.48.Final</version> </parent> <artifactId>netty-testsuite-shading</artifactId> @@ -38,6 +38,7 @@ <jarName>${project.artifactId}-${project.version}.jar</jarName> <shadedPackagePrefix>io.netty.</shadedPackagePrefix> + <skipJapicmp>true</skipJapicmp> </properties> <build> @@ -65,6 +66,17 @@ </dependency> </dependencies> <profiles> + <profile> + <id>skipTests</id> + <activation> + <property> + <name>skipTests</name> + </property> + </activation> + <properties> + <skipShadingTestsuite>true</skipShadingTestsuite> + </properties> + </profile> <profile> <id>windows</id> <activation> @@ -192,6 +204,7 @@ <goal>run</goal> </goals> <configuration> + <skip>${skipShadingTestsuite}</skip> <target> <unzip dest="${classesShadedDir}/"> <fileset dir="${project.build.directory}/"> @@ -221,6 +234,7 @@ <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-failsafe-plugin</artifactId> <configuration> + <skip>${skipShadingTestsuite}</skip> <systemPropertyVariables> <shadingPrefix>${shadingPrefix}</shadingPrefix> <shadingPrefix2>${shadingPrefix2}</shadingPrefix2> @@ -336,6 +350,7 @@ <goal>run</goal> </goals> <configuration> + <skip>${skipShadingTestsuite}</skip> <target> <unzip dest="${classesShadedDir}/"> <fileset dir="${project.build.directory}/"> @@ -365,6 +380,7 @@ <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-failsafe-plugin</artifactId> <configuration> + <skip>${skipShadingTestsuite}</skip> <systemPropertyVariables> <shadingPrefix>${shadingPrefix}</shadingPrefix> <shadingPrefix2>${shadingPrefix2}</shadingPrefix2> diff --git a/testsuite/pom.xml b/testsuite/pom.xml index 366f620..fd5ba93 100644 --- a/testsuite/pom.xml +++ b/testsuite/pom.xml @@ -20,7 +20,7 @@ <parent> <groupId>io.netty</groupId> <artifactId>netty-parent</artifactId> - <version>4.1.33.Final</version> + <version>4.1.48.Final</version> </parent> <artifactId>netty-testsuite</artifactId> @@ -104,6 +104,7 @@ <properties> <!-- Needed for SSL tests as these use the SelfSignedCertificate --> <argLine.java9.extras>--add-exports java.base/sun.security.x509=ALL-UNNAMED</argLine.java9.extras> + <skipJapicmp>true</skipJapicmp> </properties> <build> diff --git a/testsuite/src/main/java/io/netty/testsuite/transport/AbstractSingleThreadEventLoopTest.java b/testsuite/src/main/java/io/netty/testsuite/transport/AbstractSingleThreadEventLoopTest.java new file mode 100644 index 0000000..80442c9 --- /dev/null +++ b/testsuite/src/main/java/io/netty/testsuite/transport/AbstractSingleThreadEventLoopTest.java @@ -0,0 +1,168 @@ +/* + * Copyright 2019 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.testsuite.transport; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.util.concurrent.Callable; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.TimeUnit; + +import org.junit.Test; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.EventLoop; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.ServerChannel; +import io.netty.channel.SingleThreadEventLoop; +import io.netty.channel.local.LocalAddress; +import io.netty.channel.local.LocalServerChannel; +import io.netty.util.concurrent.EventExecutor; +import io.netty.util.concurrent.Future; + +public abstract class AbstractSingleThreadEventLoopTest { + + @Test + public void testChannelsRegistered() throws Exception { + EventLoopGroup group = newEventLoopGroup(); + final SingleThreadEventLoop loop = (SingleThreadEventLoop) group.next(); + + try { + final Channel ch1 = newChannel(); + final Channel ch2 = newChannel(); + + int rc = registeredChannels(loop); + boolean channelCountSupported = rc != -1; + + if (channelCountSupported) { + assertEquals(0, registeredChannels(loop)); + } + + assertTrue(loop.register(ch1).syncUninterruptibly().isSuccess()); + assertTrue(loop.register(ch2).syncUninterruptibly().isSuccess()); + if (channelCountSupported) { + assertEquals(2, registeredChannels(loop)); + } + + assertTrue(ch1.deregister().syncUninterruptibly().isSuccess()); + if (channelCountSupported) { + assertEquals(1, registeredChannels(loop)); + } + } finally { + group.shutdownGracefully(); + } + } + + // Only reliable if run from event loop + private static int registeredChannels(final SingleThreadEventLoop loop) throws Exception { + return loop.submit(new Callable<Integer>() { + @Override + public Integer call() { + return loop.registeredChannels(); + } + }).get(1, TimeUnit.SECONDS); + } + + @Test + @SuppressWarnings("deprecation") + public void shutdownBeforeStart() throws Exception { + EventLoopGroup group = newEventLoopGroup(); + assertFalse(group.awaitTermination(2, TimeUnit.MILLISECONDS)); + group.shutdown(); + assertTrue(group.awaitTermination(200, TimeUnit.MILLISECONDS)); + } + + @Test + public void shutdownGracefullyZeroQuietBeforeStart() throws Exception { + EventLoopGroup group = newEventLoopGroup(); + assertTrue(group.shutdownGracefully(0L, 2L, TimeUnit.SECONDS).await(200L)); + } + + // Copied from AbstractEventLoopTest + @Test(timeout = 5000) + public void testShutdownGracefullyNoQuietPeriod() throws Exception { + EventLoopGroup loop = newEventLoopGroup(); + ServerBootstrap b = new ServerBootstrap(); + b.group(loop) + .channel(serverChannelClass()) + .childHandler(new ChannelInboundHandlerAdapter()); + + // Not close the Channel to ensure the EventLoop is still shutdown in time. + ChannelFuture cf = serverChannelClass() == LocalServerChannel.class + ? b.bind(new LocalAddress("local")) : b.bind(0); + cf.sync().channel(); + + Future<?> f = loop.shutdownGracefully(0, 1, TimeUnit.MINUTES); + assertTrue(loop.awaitTermination(600, TimeUnit.MILLISECONDS)); + assertTrue(f.syncUninterruptibly().isSuccess()); + assertTrue(loop.isShutdown()); + assertTrue(loop.isTerminated()); + } + + @Test + public void shutdownGracefullyBeforeStart() throws Exception { + EventLoopGroup group = newEventLoopGroup(); + assertTrue(group.shutdownGracefully(200L, 1000L, TimeUnit.MILLISECONDS).await(500L)); + } + + @Test + public void gracefulShutdownAfterStart() throws Exception { + EventLoop loop = newEventLoopGroup().next(); + final CountDownLatch latch = new CountDownLatch(1); + loop.execute(new Runnable() { + @Override + public void run() { + latch.countDown(); + } + }); + + // Wait for the event loop thread to start. + latch.await(); + + // Request the event loop thread to stop. + loop.shutdownGracefully(200L, 3000L, TimeUnit.MILLISECONDS); + + // Wait until the event loop is terminated. + assertTrue(loop.awaitTermination(500L, TimeUnit.MILLISECONDS)); + + assertRejection(loop); + } + + private static final Runnable NOOP = new Runnable() { + @Override + public void run() { } + }; + + private static void assertRejection(EventExecutor loop) { + try { + loop.execute(NOOP); + fail("A task must be rejected after shutdown() is called."); + } catch (RejectedExecutionException e) { + // Expected + } + } + + protected abstract EventLoopGroup newEventLoopGroup(); + protected abstract Channel newChannel(); + protected abstract Class<? extends ServerChannel> serverChannelClass(); +} diff --git a/testsuite/src/main/java/io/netty/testsuite/transport/DefaultEventLoopTest.java b/testsuite/src/main/java/io/netty/testsuite/transport/DefaultEventLoopTest.java new file mode 100644 index 0000000..cb13b80 --- /dev/null +++ b/testsuite/src/main/java/io/netty/testsuite/transport/DefaultEventLoopTest.java @@ -0,0 +1,41 @@ +/* + * Copyright 2019 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.testsuite.transport; + +import io.netty.channel.Channel; +import io.netty.channel.DefaultEventLoopGroup; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.ServerChannel; +import io.netty.channel.local.LocalChannel; +import io.netty.channel.local.LocalServerChannel; + +public class DefaultEventLoopTest extends AbstractSingleThreadEventLoopTest { + + @Override + protected EventLoopGroup newEventLoopGroup() { + return new DefaultEventLoopGroup(); + } + + @Override + protected Channel newChannel() { + return new LocalChannel(); + } + + @Override + protected Class<? extends ServerChannel> serverChannelClass() { + return LocalServerChannel.class; + } +} diff --git a/testsuite/src/main/java/io/netty/testsuite/transport/NioEventLoopTest.java b/testsuite/src/main/java/io/netty/testsuite/transport/NioEventLoopTest.java new file mode 100644 index 0000000..e4bc928 --- /dev/null +++ b/testsuite/src/main/java/io/netty/testsuite/transport/NioEventLoopTest.java @@ -0,0 +1,41 @@ +/* + * Copyright 2019 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.testsuite.transport; + +import io.netty.channel.Channel; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.ServerChannel; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.channel.socket.nio.NioSocketChannel; + +public class NioEventLoopTest extends AbstractSingleThreadEventLoopTest { + + @Override + protected EventLoopGroup newEventLoopGroup() { + return new NioEventLoopGroup(); + } + + @Override + protected Channel newChannel() { + return new NioSocketChannel(); + } + + @Override + protected Class<? extends ServerChannel> serverChannelClass() { + return NioServerSocketChannel.class; + } +} diff --git a/testsuite/src/main/java/io/netty/testsuite/transport/socket/AbstractDatagramTest.java b/testsuite/src/main/java/io/netty/testsuite/transport/socket/AbstractDatagramTest.java index 2d19f3d..ebf597b 100644 --- a/testsuite/src/main/java/io/netty/testsuite/transport/socket/AbstractDatagramTest.java +++ b/testsuite/src/main/java/io/netty/testsuite/transport/socket/AbstractDatagramTest.java @@ -18,6 +18,7 @@ package io.netty.testsuite.transport.socket; import io.netty.bootstrap.Bootstrap; import io.netty.buffer.ByteBufAllocator; import io.netty.channel.ChannelOption; +import io.netty.channel.socket.InternetProtocolFamily; import io.netty.testsuite.transport.AbstractComboTestsuiteTest; import io.netty.testsuite.transport.TestsuitePermutation; import io.netty.util.NetUtil; @@ -34,7 +35,7 @@ public abstract class AbstractDatagramTest extends AbstractComboTestsuiteTest<Bo @Override protected List<TestsuitePermutation.BootstrapComboFactory<Bootstrap, Bootstrap>> newFactories() { - return SocketTestPermutation.INSTANCE.datagram(); + return SocketTestPermutation.INSTANCE.datagram(internetProtocolFamily()); } @Override @@ -44,11 +45,17 @@ public abstract class AbstractDatagramTest extends AbstractComboTestsuiteTest<Bo } protected SocketAddress newSocketAddress() { - // We use LOCALHOST4 as we use InternetProtocolFamily.IPv4 when creating the DatagramChannel and its - // not supported to bind to and IPV6 address in this case. - // - // See also http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/e74259b3eadc/ - // src/share/classes/sun/nio/ch/DatagramChannelImpl.java#l684 - return new InetSocketAddress(NetUtil.LOCALHOST4, 0); + switch (internetProtocolFamily()) { + case IPv4: + return new InetSocketAddress(NetUtil.LOCALHOST4, 0); + case IPv6: + return new InetSocketAddress(NetUtil.LOCALHOST6, 0); + default: + throw new AssertionError(); + } + } + + protected InternetProtocolFamily internetProtocolFamily() { + return InternetProtocolFamily.IPv4; } } diff --git a/testsuite/src/main/java/io/netty/testsuite/transport/socket/AbstractSocketReuseFdTest.java b/testsuite/src/main/java/io/netty/testsuite/transport/socket/AbstractSocketReuseFdTest.java new file mode 100644 index 0000000..6e8ae87 --- /dev/null +++ b/testsuite/src/main/java/io/netty/testsuite/transport/socket/AbstractSocketReuseFdTest.java @@ -0,0 +1,180 @@ +/* + * Copyright 2019 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.testsuite.transport.socket; + +import io.netty.bootstrap.Bootstrap; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelOption; +import io.netty.testsuite.transport.TestsuitePermutation; +import io.netty.util.CharsetUtil; +import io.netty.util.concurrent.ImmediateEventExecutor; +import io.netty.util.concurrent.Promise; +import org.junit.Test; + +import java.io.IOException; +import java.net.SocketAddress; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +public abstract class AbstractSocketReuseFdTest extends AbstractSocketTest { + @Override + protected abstract SocketAddress newSocketAddress(); + + @Override + protected abstract List<TestsuitePermutation.BootstrapComboFactory<ServerBootstrap, Bootstrap>> newFactories(); + + @Test(timeout = 60000) + public void testReuseFd() throws Throwable { + run(); + } + + public void testReuseFd(ServerBootstrap sb, Bootstrap cb) throws Throwable { + sb.childOption(ChannelOption.AUTO_READ, true); + cb.option(ChannelOption.AUTO_READ, true); + + // Use a number which will typically not exceed /proc/sys/net/core/somaxconn (which is 128 on linux by default + // often). + int numChannels = 100; + final AtomicReference<Throwable> globalException = new AtomicReference<Throwable>(); + final AtomicInteger serverRemaining = new AtomicInteger(numChannels); + final AtomicInteger clientRemaining = new AtomicInteger(numChannels); + final Promise<Void> serverDonePromise = ImmediateEventExecutor.INSTANCE.newPromise(); + final Promise<Void> clientDonePromise = ImmediateEventExecutor.INSTANCE.newPromise(); + + sb.childHandler(new ChannelInitializer<Channel>() { + @Override + public void initChannel(Channel sch) { + ReuseFdHandler sh = new ReuseFdHandler( + false, + globalException, + serverRemaining, + serverDonePromise); + sch.pipeline().addLast("handler", sh); + } + }); + + cb.handler(new ChannelInitializer<Channel>() { + @Override + public void initChannel(Channel sch) { + ReuseFdHandler ch = new ReuseFdHandler( + true, + globalException, + clientRemaining, + clientDonePromise); + sch.pipeline().addLast("handler", ch); + } + }); + + ChannelFutureListener listener = new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture future) { + if (!future.isSuccess()) { + clientDonePromise.tryFailure(future.cause()); + } + } + }; + + Channel sc = sb.bind().sync().channel(); + for (int i = 0; i < numChannels; i++) { + cb.connect(sc.localAddress()).addListener(listener); + } + + clientDonePromise.sync(); + serverDonePromise.sync(); + sc.close().sync(); + + if (globalException.get() != null && !(globalException.get() instanceof IOException)) { + throw globalException.get(); + } + } + + static class ReuseFdHandler extends ChannelInboundHandlerAdapter { + private static final String EXPECTED_PAYLOAD = "payload"; + + private final Promise<Void> donePromise; + private final AtomicInteger remaining; + private final boolean client; + volatile Channel channel; + final AtomicReference<Throwable> globalException; + final AtomicReference<Throwable> exception = new AtomicReference<Throwable>(); + final StringBuilder received = new StringBuilder(); + + ReuseFdHandler( + boolean client, + AtomicReference<Throwable> globalException, + AtomicInteger remaining, + Promise<Void> donePromise) { + this.client = client; + this.globalException = globalException; + this.remaining = remaining; + this.donePromise = donePromise; + } + + @Override + public void channelActive(ChannelHandlerContext ctx) { + channel = ctx.channel(); + if (client) { + ctx.writeAndFlush(Unpooled.copiedBuffer(EXPECTED_PAYLOAD, CharsetUtil.US_ASCII)); + } + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + if (msg instanceof ByteBuf) { + ByteBuf buf = (ByteBuf) msg; + received.append(buf.toString(CharsetUtil.US_ASCII)); + buf.release(); + + if (received.toString().equals(EXPECTED_PAYLOAD)) { + if (client) { + ctx.close(); + } else { + ctx.writeAndFlush(Unpooled.copiedBuffer(EXPECTED_PAYLOAD, CharsetUtil.US_ASCII)); + } + } + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + if (exception.compareAndSet(null, cause)) { + donePromise.tryFailure(new IllegalStateException("exceptionCaught: " + ctx.channel(), cause)); + ctx.close(); + } + globalException.compareAndSet(null, cause); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) { + if (remaining.decrementAndGet() == 0) { + if (received.toString().equals(EXPECTED_PAYLOAD)) { + donePromise.setSuccess(null); + } else { + donePromise.tryFailure(new Exception("Unexpected payload:" + received)); + } + } + } + } +} diff --git a/testsuite/src/main/java/io/netty/testsuite/transport/socket/AbstractSocketShutdownOutputByPeerTest.java b/testsuite/src/main/java/io/netty/testsuite/transport/socket/AbstractSocketShutdownOutputByPeerTest.java new file mode 100644 index 0000000..6e895a4 --- /dev/null +++ b/testsuite/src/main/java/io/netty/testsuite/transport/socket/AbstractSocketShutdownOutputByPeerTest.java @@ -0,0 +1,163 @@ +/* + * Copyright 2019 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.testsuite.transport.socket; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.buffer.ByteBuf; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelOption; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.channel.socket.ChannelInputShutdownEvent; +import io.netty.channel.socket.DuplexChannel; +import org.junit.Test; + +import java.io.IOException; +import java.net.SocketAddress; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.Assert.*; + +public abstract class AbstractSocketShutdownOutputByPeerTest<Socket> extends AbstractServerSocketTest { + + @Test(timeout = 30000) + public void testShutdownOutput() throws Throwable { + run(); + } + + public void testShutdownOutput(ServerBootstrap sb) throws Throwable { + TestHandler h = new TestHandler(); + Socket s = newSocket(); + Channel sc = null; + try { + sc = sb.childHandler(h).childOption(ChannelOption.ALLOW_HALF_CLOSURE, true).bind().sync().channel(); + + connect(s, sc.localAddress()); + write(s, 1); + + assertEquals(1, (int) h.queue.take()); + + assertTrue(h.ch.isOpen()); + assertTrue(h.ch.isActive()); + assertFalse(h.ch.isInputShutdown()); + assertFalse(h.ch.isOutputShutdown()); + + shutdownOutput(s); + + h.halfClosure.await(); + + assertTrue(h.ch.isOpen()); + assertTrue(h.ch.isActive()); + assertTrue(h.ch.isInputShutdown()); + assertFalse(h.ch.isOutputShutdown()); + + while (h.closure.getCount() != 1 && h.halfClosureCount.intValue() != 1) { + Thread.sleep(100); + } + } finally { + if (sc != null) { + sc.close(); + } + close(s); + } + } + + @Test(timeout = 30000) + public void testShutdownOutputWithoutOption() throws Throwable { + run(); + } + + public void testShutdownOutputWithoutOption(ServerBootstrap sb) throws Throwable { + TestHandler h = new TestHandler(); + Socket s = newSocket(); + Channel sc = null; + try { + sc = sb.childHandler(h).bind().sync().channel(); + + connect(s, sc.localAddress()); + write(s, 1); + + assertEquals(1, (int) h.queue.take()); + + assertTrue(h.ch.isOpen()); + assertTrue(h.ch.isActive()); + assertFalse(h.ch.isInputShutdown()); + assertFalse(h.ch.isOutputShutdown()); + + shutdownOutput(s); + + h.closure.await(); + + assertFalse(h.ch.isOpen()); + assertFalse(h.ch.isActive()); + assertTrue(h.ch.isInputShutdown()); + assertTrue(h.ch.isOutputShutdown()); + + while (h.halfClosure.getCount() != 1 && h.halfClosureCount.intValue() != 0) { + Thread.sleep(100); + } + } finally { + if (sc != null) { + sc.close(); + } + close(s); + } + } + + protected abstract void shutdownOutput(Socket s) throws IOException; + + protected abstract void connect(Socket s, SocketAddress address) throws IOException; + + protected abstract void close(Socket s) throws IOException; + + protected abstract void write(Socket s, int data) throws IOException; + + protected abstract Socket newSocket(); + + private static class TestHandler extends SimpleChannelInboundHandler<ByteBuf> { + volatile DuplexChannel ch; + final BlockingQueue<Byte> queue = new LinkedBlockingQueue<Byte>(); + final CountDownLatch halfClosure = new CountDownLatch(1); + final CountDownLatch closure = new CountDownLatch(1); + final AtomicInteger halfClosureCount = new AtomicInteger(); + + @Override + public void channelActive(ChannelHandlerContext ctx) throws Exception { + ch = (DuplexChannel) ctx.channel(); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) throws Exception { + closure.countDown(); + } + + @Override + public void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception { + queue.offer(msg.readByte()); + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + if (evt instanceof ChannelInputShutdownEvent) { + halfClosureCount.incrementAndGet(); + halfClosure.countDown(); + } + } + } +} diff --git a/testsuite/src/main/java/io/netty/testsuite/transport/socket/CompositeBufferGatheringWriteTest.java b/testsuite/src/main/java/io/netty/testsuite/transport/socket/CompositeBufferGatheringWriteTest.java index 3538d8b..c8c6132 100644 --- a/testsuite/src/main/java/io/netty/testsuite/transport/socket/CompositeBufferGatheringWriteTest.java +++ b/testsuite/src/main/java/io/netty/testsuite/transport/socket/CompositeBufferGatheringWriteTest.java @@ -27,7 +27,6 @@ import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelOption; -import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.util.ReferenceCountUtil; import org.junit.Test; diff --git a/testsuite/src/main/java/io/netty/testsuite/transport/socket/DatagramMulticastIPv6Test.java b/testsuite/src/main/java/io/netty/testsuite/transport/socket/DatagramMulticastIPv6Test.java new file mode 100644 index 0000000..aa85e65 --- /dev/null +++ b/testsuite/src/main/java/io/netty/testsuite/transport/socket/DatagramMulticastIPv6Test.java @@ -0,0 +1,26 @@ +/* + * Copyright 2019 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.testsuite.transport.socket; + +import io.netty.channel.socket.InternetProtocolFamily; + +public class DatagramMulticastIPv6Test extends DatagramMulticastTest { + + @Override + protected InternetProtocolFamily internetProtocolFamily() { + return InternetProtocolFamily.IPv6; + } +} diff --git a/testsuite/src/main/java/io/netty/testsuite/transport/socket/DatagramMulticastTest.java b/testsuite/src/main/java/io/netty/testsuite/transport/socket/DatagramMulticastTest.java index 881e9a5..6ea3209 100644 --- a/testsuite/src/main/java/io/netty/testsuite/transport/socket/DatagramMulticastTest.java +++ b/testsuite/src/main/java/io/netty/testsuite/transport/socket/DatagramMulticastTest.java @@ -17,18 +17,26 @@ package io.netty.testsuite.transport.socket; import io.netty.bootstrap.Bootstrap; import io.netty.buffer.Unpooled; -import io.netty.channel.Channel; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelOption; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.channel.socket.DatagramChannel; import io.netty.channel.socket.DatagramPacket; +import io.netty.channel.socket.InternetProtocolFamily; import io.netty.channel.socket.oio.OioDatagramChannel; -import io.netty.util.NetUtil; +import io.netty.testsuite.transport.TestsuitePermutation; import io.netty.util.internal.SocketUtils; +import org.junit.Assume; import org.junit.Test; +import java.io.IOException; +import java.net.InetAddress; import java.net.InetSocketAddress; +import java.net.MulticastSocket; +import java.net.NetworkInterface; +import java.net.UnknownHostException; +import java.util.Enumeration; +import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -42,6 +50,10 @@ public class DatagramMulticastTest extends AbstractDatagramTest { } public void testMulticast(Bootstrap sb, Bootstrap cb) throws Throwable { + NetworkInterface iface = multicastNetworkInterface(); + Assume.assumeNotNull("No NetworkInterface found that supports multicast and " + + internetProtocolFamily(), iface); + MulticastTestHandler mhandler = new MulticastTestHandler(); sb.handler(new SimpleChannelInboundHandler<Object>() { @@ -53,14 +65,17 @@ public class DatagramMulticastTest extends AbstractDatagramTest { cb.handler(mhandler); - sb.option(ChannelOption.IP_MULTICAST_IF, NetUtil.LOOPBACK_IF); + sb.option(ChannelOption.IP_MULTICAST_IF, iface); sb.option(ChannelOption.SO_REUSEADDR, true); - cb.option(ChannelOption.IP_MULTICAST_IF, NetUtil.LOOPBACK_IF); + + cb.option(ChannelOption.IP_MULTICAST_IF, iface); cb.option(ChannelOption.SO_REUSEADDR, true); - Channel sc = sb.bind(newSocketAddress()).sync().channel(); + DatagramChannel sc = (DatagramChannel) sb.bind(newSocketAddress(iface)).sync().channel(); + assertEquals(iface, sc.config().getNetworkInterface()); + assertInterfaceAddress(iface, sc.config().getInterface()); - InetSocketAddress addr = (InetSocketAddress) sc.localAddress(); + InetSocketAddress addr = sc.localAddress(); cb.localAddress(addr.getPort()); if (sc instanceof OioDatagramChannel) { @@ -71,17 +86,18 @@ public class DatagramMulticastTest extends AbstractDatagramTest { return; } DatagramChannel cc = (DatagramChannel) cb.bind().sync().channel(); + assertEquals(iface, cc.config().getNetworkInterface()); + assertInterfaceAddress(iface, cc.config().getInterface()); - String group = "230.0.0.1"; - InetSocketAddress groupAddress = SocketUtils.socketAddress(group, addr.getPort()); + InetSocketAddress groupAddress = SocketUtils.socketAddress(groupAddress(), addr.getPort()); - cc.joinGroup(groupAddress, NetUtil.LOOPBACK_IF).sync(); + cc.joinGroup(groupAddress, iface).sync(); sc.writeAndFlush(new DatagramPacket(Unpooled.copyInt(1), groupAddress)).sync(); assertTrue(mhandler.await()); // leave the group - cc.leaveGroup(groupAddress, NetUtil.LOOPBACK_IF).sync(); + cc.leaveGroup(groupAddress, iface).sync(); // sleep a second to make sure we left the group Thread.sleep(1000); @@ -90,10 +106,32 @@ public class DatagramMulticastTest extends AbstractDatagramTest { sc.writeAndFlush(new DatagramPacket(Unpooled.copyInt(1), groupAddress)).sync(); mhandler.await(); + cc.config().setLoopbackModeDisabled(false); + sc.config().setLoopbackModeDisabled(false); + + assertFalse(cc.config().isLoopbackModeDisabled()); + assertFalse(sc.config().isLoopbackModeDisabled()); + + cc.config().setLoopbackModeDisabled(true); + sc.config().setLoopbackModeDisabled(true); + + assertTrue(cc.config().isLoopbackModeDisabled()); + assertTrue(sc.config().isLoopbackModeDisabled()); + sc.close().awaitUninterruptibly(); cc.close().awaitUninterruptibly(); } + private static void assertInterfaceAddress(NetworkInterface networkInterface, InetAddress expected) { + Enumeration<InetAddress> addresses = networkInterface.getInetAddresses(); + while (addresses.hasMoreElements()) { + if (expected.equals(addresses.nextElement())) { + return; + } + } + fail(); + } + private static final class MulticastTestHandler extends SimpleChannelInboundHandler<DatagramPacket> { private final CountDownLatch latch = new CountDownLatch(1); @@ -123,4 +161,64 @@ public class DatagramMulticastTest extends AbstractDatagramTest { return success; } } + + @Override + protected List<TestsuitePermutation.BootstrapComboFactory<Bootstrap, Bootstrap>> newFactories() { + return SocketTestPermutation.INSTANCE.datagram(internetProtocolFamily()); + } + + private InetSocketAddress newAnySocketAddress() throws UnknownHostException { + switch (internetProtocolFamily()) { + case IPv4: + return new InetSocketAddress(InetAddress.getByName("0.0.0.0"), 0); + case IPv6: + return new InetSocketAddress(InetAddress.getByName("::"), 0); + default: + throw new AssertionError(); + } + } + + private InetSocketAddress newSocketAddress(NetworkInterface iface) { + Enumeration<InetAddress> addresses = iface.getInetAddresses(); + while (addresses.hasMoreElements()) { + InetAddress address = addresses.nextElement(); + if (internetProtocolFamily().addressType().isAssignableFrom(address.getClass())) { + return new InetSocketAddress(address, 0); + } + } + throw new AssertionError(); + } + + private NetworkInterface multicastNetworkInterface() throws IOException { + Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces(); + while (interfaces.hasMoreElements()) { + NetworkInterface iface = interfaces.nextElement(); + if (iface.isUp() && iface.supportsMulticast()) { + Enumeration<InetAddress> addresses = iface.getInetAddresses(); + while (addresses.hasMoreElements()) { + InetAddress address = addresses.nextElement(); + if (internetProtocolFamily().addressType().isAssignableFrom(address.getClass())) { + MulticastSocket socket = new MulticastSocket(newAnySocketAddress()); + socket.setReuseAddress(true); + socket.setNetworkInterface(iface); + try { + socket.send(new java.net.DatagramPacket(new byte[] { 1, 2, 3, 4 }, 4, + new InetSocketAddress(groupAddress(), 12345))); + return iface; + } catch (IOException ignore) { + // Try the next interface + } finally { + socket.close(); + } + } + } + } + } + return null; + } + + private String groupAddress() { + return internetProtocolFamily() == InternetProtocolFamily.IPv4 ? + "230.0.0.1" : "FF01:0:0:0:0:0:0:101"; + } } diff --git a/testsuite/src/main/java/io/netty/testsuite/transport/socket/DatagramUnicastIPv6MappedTest.java b/testsuite/src/main/java/io/netty/testsuite/transport/socket/DatagramUnicastIPv6MappedTest.java new file mode 100644 index 0000000..8abc9ca --- /dev/null +++ b/testsuite/src/main/java/io/netty/testsuite/transport/socket/DatagramUnicastIPv6MappedTest.java @@ -0,0 +1,39 @@ +/* + * Copyright 2019 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.testsuite.transport.socket; + +import io.netty.util.NetUtil; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.SocketAddress; + +public class DatagramUnicastIPv6MappedTest extends DatagramUnicastIPv6Test { + + @Override + protected SocketAddress newSocketAddress() { + return new InetSocketAddress(0); + } + + @Override + protected InetSocketAddress sendToAddress(InetSocketAddress serverAddress) { + InetAddress addr = serverAddress.getAddress(); + if (addr.isAnyLocalAddress()) { + return new InetSocketAddress(NetUtil.LOCALHOST4, serverAddress.getPort()); + } + return serverAddress; + } +} diff --git a/testsuite/src/main/java/io/netty/testsuite/transport/socket/DatagramUnicastIPv6Test.java b/testsuite/src/main/java/io/netty/testsuite/transport/socket/DatagramUnicastIPv6Test.java new file mode 100644 index 0000000..533384f --- /dev/null +++ b/testsuite/src/main/java/io/netty/testsuite/transport/socket/DatagramUnicastIPv6Test.java @@ -0,0 +1,50 @@ +/* + * Copyright 2019 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.testsuite.transport.socket; + +import io.netty.channel.socket.InternetProtocolFamily; +import io.netty.util.internal.PlatformDependent; +import io.netty.util.internal.SuppressJava6Requirement; +import org.junit.Assume; +import org.junit.BeforeClass; + +import java.io.IOException; +import java.net.StandardProtocolFamily; +import java.nio.channels.Channel; +import java.nio.channels.spi.SelectorProvider; + +public class DatagramUnicastIPv6Test extends DatagramUnicastTest { + + @SuppressJava6Requirement(reason = "Guarded by java version check") + @BeforeClass + public static void assumeIpv6Supported() { + try { + if (PlatformDependent.javaVersion() < 7) { + throw new UnsupportedOperationException(); + } + Channel channel = SelectorProvider.provider().openDatagramChannel(StandardProtocolFamily.INET6); + channel.close(); + } catch (UnsupportedOperationException e) { + Assume.assumeNoException("IPv6 not supported", e); + } catch (IOException ignore) { + // Ignore + } + } + @Override + protected InternetProtocolFamily internetProtocolFamily() { + return InternetProtocolFamily.IPv6; + } +} diff --git a/testsuite/src/main/java/io/netty/testsuite/transport/socket/DatagramUnicastTest.java b/testsuite/src/main/java/io/netty/testsuite/transport/socket/DatagramUnicastTest.java index b49f678..aca8b60 100644 --- a/testsuite/src/main/java/io/netty/testsuite/transport/socket/DatagramUnicastTest.java +++ b/testsuite/src/main/java/io/netty/testsuite/transport/socket/DatagramUnicastTest.java @@ -27,12 +27,19 @@ import io.netty.channel.ChannelOption; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.channel.socket.DatagramChannel; import io.netty.channel.socket.DatagramPacket; +import io.netty.util.NetUtil; import org.junit.Test; +import java.net.Inet6Address; +import java.net.InetAddress; import java.net.InetSocketAddress; +import java.net.SocketAddress; import java.nio.channels.NotYetConnectedException; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; import static org.junit.Assert.*; @@ -157,36 +164,38 @@ public class DatagramUnicastTest extends AbstractDatagramTest { } }); - final CountDownLatch latch = new CountDownLatch(count); - sc = setupServerChannel(sb, bytes, latch); + final SocketAddress sender; if (bindClient) { cc = cb.bind(newSocketAddress()).sync().channel(); + sender = cc.localAddress(); } else { cb.option(ChannelOption.DATAGRAM_CHANNEL_ACTIVE_ON_REGISTRATION, true); cc = cb.register().sync().channel(); + sender = null; } - InetSocketAddress addr = (InetSocketAddress) sc.localAddress(); + + final CountDownLatch latch = new CountDownLatch(count); + AtomicReference<Throwable> errorRef = new AtomicReference<Throwable>(); + sc = setupServerChannel(sb, bytes, sender, latch, errorRef, false); + + InetSocketAddress addr = sendToAddress((InetSocketAddress) sc.localAddress()); + List<ChannelFuture> futures = new ArrayList<ChannelFuture>(count); for (int i = 0; i < count; i++) { - switch (wrapType) { - case DUP: - cc.write(new DatagramPacket(buf.retainedDuplicate(), addr)); - break; - case SLICE: - cc.write(new DatagramPacket(buf.retainedSlice(), addr)); - break; - case READ_ONLY: - cc.write(new DatagramPacket(buf.retain().asReadOnly(), addr)); - break; - case NONE: - cc.write(new DatagramPacket(buf.retain(), addr)); - break; - default: - throw new Error("unknown wrap type: " + wrapType); - } + futures.add(write(cc, buf, addr, wrapType)); } // release as we used buf.retain() before cc.flush(); - assertTrue(latch.await(10, TimeUnit.SECONDS)); + + for (ChannelFuture future: futures) { + future.sync(); + } + if (!latch.await(10, TimeUnit.SECONDS)) { + Throwable error = errorRef.get(); + if (error != null) { + throw error; + } + fail(); + } } finally { // release as we used buf.retain() before buf.release(); @@ -196,6 +205,20 @@ public class DatagramUnicastTest extends AbstractDatagramTest { } } + private static ChannelFuture write(Channel cc, ByteBuf buf, InetSocketAddress remote, WrapType wrapType) { + switch (wrapType) { + case DUP: + return cc.write(new DatagramPacket(buf.retainedDuplicate(), remote)); + case SLICE: + return cc.write(new DatagramPacket(buf.retainedSlice(), remote)); + case READ_ONLY: + return cc.write(new DatagramPacket(buf.retain().asReadOnly(), remote)); + case NONE: + return cc.write(new DatagramPacket(buf.retain(), remote)); + default: + throw new Error("unknown wrap type: " + wrapType); + } + } private void testSimpleSendWithConnect(Bootstrap sb, Bootstrap cb, ByteBuf buf, final byte[] bytes, int count) throws Throwable { try { @@ -209,10 +232,33 @@ public class DatagramUnicastTest extends AbstractDatagramTest { private void testSimpleSendWithConnect0(Bootstrap sb, Bootstrap cb, ByteBuf buf, final byte[] bytes, int count, WrapType wrapType) throws Throwable { - cb.handler(new SimpleChannelInboundHandler<Object>() { + final CountDownLatch clientLatch = new CountDownLatch(count); + final AtomicReference<Throwable> clientErrorRef = new AtomicReference<Throwable>(); + cb.handler(new SimpleChannelInboundHandler<DatagramPacket>() { + @Override + public void channelRead0(ChannelHandlerContext ctx, DatagramPacket msg) throws Exception { + try { + ByteBuf buf = msg.content(); + assertEquals(bytes.length, buf.readableBytes()); + for (int i = 0; i < bytes.length; i++) { + assertEquals(bytes[i], buf.getByte(buf.readerIndex() + i)); + } + + InetSocketAddress localAddress = (InetSocketAddress) ctx.channel().localAddress(); + if (localAddress.getAddress().isAnyLocalAddress()) { + assertEquals(localAddress.getPort(), msg.recipient().getPort()); + } else { + // Test that the channel's localAddress is equal to the message's recipient + assertEquals(localAddress, msg.recipient()); + } + } finally { + clientLatch.countDown(); + } + } + @Override - public void channelRead0(ChannelHandlerContext ctx, Object msgs) throws Exception { - // Nothing will be sent. + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + clientErrorRef.compareAndSet(null, cause); } }); @@ -220,35 +266,46 @@ public class DatagramUnicastTest extends AbstractDatagramTest { DatagramChannel cc = null; try { final CountDownLatch latch = new CountDownLatch(count); - sc = setupServerChannel(sb, bytes, latch); - cc = (DatagramChannel) cb.connect(sc.localAddress()).sync().channel(); + final AtomicReference<Throwable> errorRef = new AtomicReference<Throwable>(); + cc = (DatagramChannel) cb.bind(newSocketAddress()).sync().channel(); + sc = setupServerChannel(sb, bytes, cc.localAddress(), latch, errorRef, true); + + cc.connect(sendToAddress((InetSocketAddress) sc.localAddress())).syncUninterruptibly(); + List<ChannelFuture> futures = new ArrayList<ChannelFuture>(); for (int i = 0; i < count; i++) { - switch (wrapType) { - case DUP: - cc.write(buf.retainedDuplicate()); - break; - case SLICE: - cc.write(buf.retainedSlice()); - break; - case READ_ONLY: - cc.write(buf.retain().asReadOnly()); - break; - case NONE: - cc.write(buf.retain()); - break; - default: - throw new Error("unknown wrap type: " + wrapType); - } + futures.add(write(cc, buf, wrapType)); } cc.flush(); - assertTrue(latch.await(10, TimeUnit.SECONDS)); + for (ChannelFuture future: futures) { + future.sync(); + } + + if (!latch.await(10, TimeUnit.SECONDS)) { + Throwable cause = errorRef.get(); + if (cause != null) { + throw cause; + } + fail(); + } + if (!clientLatch.await(10, TimeUnit.SECONDS)) { + Throwable cause = clientErrorRef.get(); + if (cause != null) { + throw cause; + } + fail(); + } assertTrue(cc.isConnected()); + assertNotNull(cc.localAddress()); + assertNotNull(cc.remoteAddress()); + // Test what happens when we call disconnect() cc.disconnect().syncUninterruptibly(); assertFalse(cc.isConnected()); + assertNotNull(cc.localAddress()); + assertNull(cc.remoteAddress()); ChannelFuture future = cc.writeAndFlush( buf.retain().duplicate()).awaitUninterruptibly(); @@ -263,8 +320,25 @@ public class DatagramUnicastTest extends AbstractDatagramTest { } } + private static ChannelFuture write(Channel cc, ByteBuf buf, WrapType wrapType) { + switch (wrapType) { + case DUP: + return cc.write(buf.retainedDuplicate()); + case SLICE: + return cc.write(buf.retainedSlice()); + case READ_ONLY: + return cc.write(buf.retain().asReadOnly()); + case NONE: + return cc.write(buf.retain()); + default: + throw new Error("unknown wrap type: " + wrapType); + } + } + @SuppressWarnings("deprecation") - private Channel setupServerChannel(Bootstrap sb, final byte[] bytes, final CountDownLatch latch) + private Channel setupServerChannel(Bootstrap sb, final byte[] bytes, final SocketAddress sender, + final CountDownLatch latch, final AtomicReference<Throwable> errorRef, + final boolean echo) throws Throwable { sb.handler(new ChannelInitializer<Channel>() { @Override @@ -272,16 +346,38 @@ public class DatagramUnicastTest extends AbstractDatagramTest { ch.pipeline().addLast(new SimpleChannelInboundHandler<DatagramPacket>() { @Override public void channelRead0(ChannelHandlerContext ctx, DatagramPacket msg) throws Exception { - ByteBuf buf = msg.content(); - assertEquals(bytes.length, buf.readableBytes()); - for (byte b : bytes) { - assertEquals(b, buf.readByte()); + try { + if (sender == null) { + assertNotNull(msg.sender()); + } else { + InetSocketAddress senderAddress = (InetSocketAddress) sender; + if (senderAddress.getAddress().isAnyLocalAddress()) { + assertEquals(senderAddress.getPort(), msg.sender().getPort()); + } else { + assertEquals(sender, msg.sender()); + } + } + + ByteBuf buf = msg.content(); + assertEquals(bytes.length, buf.readableBytes()); + for (int i = 0; i < bytes.length; i++) { + assertEquals(bytes[i], buf.getByte(buf.readerIndex() + i)); + } + + // Test that the channel's localAddress is equal to the message's recipient + assertEquals(ctx.channel().localAddress(), msg.recipient()); + + if (echo) { + ctx.writeAndFlush(new DatagramPacket(buf.retainedDuplicate(), msg.sender())); + } + } finally { + latch.countDown(); } + } - // Test that the channel's localAddress is equal to the message's recipient - assertEquals(ctx.channel().localAddress(), msg.recipient()); - - latch.countDown(); + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + errorRef.compareAndSet(null, cause); } }); } @@ -294,4 +390,15 @@ public class DatagramUnicastTest extends AbstractDatagramTest { channel.close().sync(); } } + + protected InetSocketAddress sendToAddress(InetSocketAddress serverAddress) { + InetAddress addr = serverAddress.getAddress(); + if (addr.isAnyLocalAddress()) { + if (addr instanceof Inet6Address) { + return new InetSocketAddress(NetUtil.LOCALHOST6, serverAddress.getPort()); + } + return new InetSocketAddress(NetUtil.LOCALHOST4, serverAddress.getPort()); + } + return serverAddress; + } } diff --git a/testsuite/src/main/java/io/netty/testsuite/transport/socket/SocketConnectTest.java b/testsuite/src/main/java/io/netty/testsuite/transport/socket/SocketConnectTest.java index 0f96d04..f26bc05 100644 --- a/testsuite/src/main/java/io/netty/testsuite/transport/socket/SocketConnectTest.java +++ b/testsuite/src/main/java/io/netty/testsuite/transport/socket/SocketConnectTest.java @@ -21,8 +21,6 @@ import io.netty.channel.Channel; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; -import io.netty.testsuite.util.TestUtils; -import io.netty.util.NetUtil; import io.netty.util.concurrent.ImmediateEventExecutor; import io.netty.util.concurrent.Promise; import org.junit.Test; diff --git a/testsuite/src/main/java/io/netty/testsuite/transport/socket/SocketFileRegionTest.java b/testsuite/src/main/java/io/netty/testsuite/transport/socket/SocketFileRegionTest.java index 53deb6c..ae85825 100644 --- a/testsuite/src/main/java/io/netty/testsuite/transport/socket/SocketFileRegionTest.java +++ b/testsuite/src/main/java/io/netty/testsuite/transport/socket/SocketFileRegionTest.java @@ -22,17 +22,19 @@ import io.netty.buffer.Unpooled; import io.netty.channel.Channel; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandler; +import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.channel.ChannelOption; import io.netty.channel.DefaultFileRegion; import io.netty.channel.FileRegion; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.util.internal.PlatformDependent; +import org.hamcrest.CoreMatchers; import org.junit.Test; import java.io.File; -import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; +import java.io.RandomAccessFile; import java.nio.channels.WritableByteChannel; import java.util.Random; import java.util.concurrent.atomic.AtomicReference; @@ -73,6 +75,11 @@ public class SocketFileRegionTest extends AbstractSocketTest { run(); } + @Test + public void testFileRegionCountLargerThenFile() throws Throwable { + run(); + } + public void testFileRegion(ServerBootstrap sb, Bootstrap cb) throws Throwable { testFileRegion0(sb, cb, false, true, true); } @@ -93,6 +100,34 @@ public class SocketFileRegionTest extends AbstractSocketTest { testFileRegion0(sb, cb, true, false, true); } + public void testFileRegionCountLargerThenFile(ServerBootstrap sb, Bootstrap cb) throws Throwable { + File file = File.createTempFile("netty-", ".tmp"); + file.deleteOnExit(); + + final FileOutputStream out = new FileOutputStream(file); + out.write(data); + out.close(); + + sb.childHandler(new SimpleChannelInboundHandler<ByteBuf>() { + @Override + protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) { + // Just drop the message. + } + }); + cb.handler(new ChannelInboundHandlerAdapter()); + + Channel sc = sb.bind().sync().channel(); + Channel cc = cb.connect(sc.localAddress()).sync().channel(); + + // Request file region which is bigger then the underlying file. + FileRegion region = new DefaultFileRegion( + new RandomAccessFile(file, "r").getChannel(), 0, data.length + 1024); + + assertThat(cc.writeAndFlush(region).await().cause(), CoreMatchers.<Throwable>instanceOf(IOException.class)); + cc.close().sync(); + sc.close().sync(); + } + private static void testFileRegion0( ServerBootstrap sb, Bootstrap cb, boolean voidPromise, final boolean autoRead, boolean defaultFileRegion) throws Throwable { @@ -148,8 +183,8 @@ public class SocketFileRegionTest extends AbstractSocketTest { Channel cc = cb.connect(sc.localAddress()).sync().channel(); FileRegion region = new DefaultFileRegion( - new FileInputStream(file).getChannel(), startOffset, data.length - bufferSize); - FileRegion emptyRegion = new DefaultFileRegion(new FileInputStream(file).getChannel(), 0, 0); + new RandomAccessFile(file, "r").getChannel(), startOffset, data.length - bufferSize); + FileRegion emptyRegion = new DefaultFileRegion(new RandomAccessFile(file, "r").getChannel(), 0, 0); if (!defaultFileRegion) { region = new FileRegionWrapper(region); diff --git a/testsuite/src/main/java/io/netty/testsuite/transport/socket/SocketShutdownOutputByPeerTest.java b/testsuite/src/main/java/io/netty/testsuite/transport/socket/SocketShutdownOutputByPeerTest.java index f18109e..d42a3c0 100644 --- a/testsuite/src/main/java/io/netty/testsuite/transport/socket/SocketShutdownOutputByPeerTest.java +++ b/testsuite/src/main/java/io/netty/testsuite/transport/socket/SocketShutdownOutputByPeerTest.java @@ -15,138 +15,37 @@ */ package io.netty.testsuite.transport.socket; -import io.netty.bootstrap.ServerBootstrap; -import io.netty.buffer.ByteBuf; -import io.netty.channel.Channel; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelOption; -import io.netty.channel.SimpleChannelInboundHandler; import io.netty.util.internal.SocketUtils; -import io.netty.channel.socket.ChannelInputShutdownEvent; -import io.netty.channel.socket.SocketChannel; -import org.junit.Test; +import java.io.IOException; import java.net.Socket; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.atomic.AtomicInteger; +import java.net.SocketAddress; -import static org.junit.Assert.*; +public class SocketShutdownOutputByPeerTest extends AbstractSocketShutdownOutputByPeerTest<Socket> { -public class SocketShutdownOutputByPeerTest extends AbstractServerSocketTest { - - @Test(timeout = 30000) - public void testShutdownOutput() throws Throwable { - run(); + @Override + protected void shutdownOutput(Socket s) throws IOException { + s.shutdownOutput(); } - public void testShutdownOutput(ServerBootstrap sb) throws Throwable { - TestHandler h = new TestHandler(); - Socket s = new Socket(); - Channel sc = null; - try { - sc = sb.childHandler(h).childOption(ChannelOption.ALLOW_HALF_CLOSURE, true).bind().sync().channel(); - - SocketUtils.connect(s, sc.localAddress(), 10000); - s.getOutputStream().write(1); - - assertEquals(1, (int) h.queue.take()); - - assertTrue(h.ch.isOpen()); - assertTrue(h.ch.isActive()); - assertFalse(h.ch.isInputShutdown()); - assertFalse(h.ch.isOutputShutdown()); - - s.shutdownOutput(); - - h.halfClosure.await(); - - assertTrue(h.ch.isOpen()); - assertTrue(h.ch.isActive()); - assertTrue(h.ch.isInputShutdown()); - assertFalse(h.ch.isOutputShutdown()); - assertEquals(1, h.closure.getCount()); - Thread.sleep(100); - assertEquals(1, h.halfClosureCount.intValue()); - } finally { - if (sc != null) { - sc.close(); - } - s.close(); - } + @Override + protected void connect(Socket s, SocketAddress address) throws IOException { + SocketUtils.connect(s, address, 10000); } - @Test(timeout = 30000) - public void testShutdownOutputWithoutOption() throws Throwable { - run(); + @Override + protected void close(Socket s) throws IOException { + s.close(); } - public void testShutdownOutputWithoutOption(ServerBootstrap sb) throws Throwable { - TestHandler h = new TestHandler(); - Socket s = new Socket(); - Channel sc = null; - try { - sc = sb.childHandler(h).bind().sync().channel(); - - SocketUtils.connect(s, sc.localAddress(), 10000); - s.getOutputStream().write(1); - - assertEquals(1, (int) h.queue.take()); - - assertTrue(h.ch.isOpen()); - assertTrue(h.ch.isActive()); - assertFalse(h.ch.isInputShutdown()); - assertFalse(h.ch.isOutputShutdown()); - - s.shutdownOutput(); - - h.closure.await(); - - assertFalse(h.ch.isOpen()); - assertFalse(h.ch.isActive()); - assertTrue(h.ch.isInputShutdown()); - assertTrue(h.ch.isOutputShutdown()); - - assertEquals(1, h.halfClosure.getCount()); - Thread.sleep(100); - assertEquals(0, h.halfClosureCount.intValue()); - } finally { - if (sc != null) { - sc.close(); - } - s.close(); - } + @Override + protected void write(Socket s, int data) throws IOException { + s.getOutputStream().write(data); } - private static class TestHandler extends SimpleChannelInboundHandler<ByteBuf> { - volatile SocketChannel ch; - final BlockingQueue<Byte> queue = new LinkedBlockingQueue<Byte>(); - final CountDownLatch halfClosure = new CountDownLatch(1); - final CountDownLatch closure = new CountDownLatch(1); - final AtomicInteger halfClosureCount = new AtomicInteger(); - - @Override - public void channelActive(ChannelHandlerContext ctx) throws Exception { - ch = (SocketChannel) ctx.channel(); - } - - @Override - public void channelInactive(ChannelHandlerContext ctx) throws Exception { - closure.countDown(); - } - - @Override - public void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception { - queue.offer(msg.readByte()); - } - - @Override - public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { - if (evt instanceof ChannelInputShutdownEvent) { - halfClosureCount.incrementAndGet(); - halfClosure.countDown(); - } - } + @Override + protected Socket newSocket() { + return new Socket(); } + } diff --git a/testsuite/src/main/java/io/netty/testsuite/transport/socket/SocketSslClientRenegotiateTest.java b/testsuite/src/main/java/io/netty/testsuite/transport/socket/SocketSslClientRenegotiateTest.java index 8036f08..4574ca7 100644 --- a/testsuite/src/main/java/io/netty/testsuite/transport/socket/SocketSslClientRenegotiateTest.java +++ b/testsuite/src/main/java/io/netty/testsuite/transport/socket/SocketSslClientRenegotiateTest.java @@ -18,6 +18,7 @@ package io.netty.testsuite.transport.socket; import io.netty.bootstrap.Bootstrap; import io.netty.bootstrap.ServerBootstrap; import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; import io.netty.channel.Channel; import io.netty.channel.ChannelHandler.Sharable; import io.netty.channel.ChannelHandlerContext; @@ -46,6 +47,9 @@ import java.security.cert.CertificateException; import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicReference; import javax.net.ssl.SSLHandshakeException; @@ -73,7 +77,7 @@ public class SocketSslClientRenegotiateTest extends AbstractSocketTest { KEY_FILE = ssc.privateKey(); } - @Parameters(name = "{index}: serverEngine = {0}, clientEngine = {1}") + @Parameters(name = "{index}: serverEngine = {0}, clientEngine = {1}, delegate = {2}") public static Collection<Object[]> data() throws Exception { List<SslContext> serverContexts = new ArrayList<SslContext>(); List<SslContext> clientContexts = new ArrayList<SslContext>(); @@ -91,7 +95,8 @@ public class SocketSslClientRenegotiateTest extends AbstractSocketTest { for (SslContext sc: serverContexts) { for (SslContext cc: clientContexts) { for (int i = 0; i < 32; i++) { - params.add(new Object[] { sc, cc}); + params.add(new Object[] { sc, cc, true}); + params.add(new Object[] { sc, cc, false}); } } } @@ -101,6 +106,7 @@ public class SocketSslClientRenegotiateTest extends AbstractSocketTest { private final SslContext serverCtx; private final SslContext clientCtx; + private final boolean delegate; private final AtomicReference<Throwable> clientException = new AtomicReference<Throwable>(); private final AtomicReference<Throwable> serverException = new AtomicReference<Throwable>(); @@ -116,9 +122,10 @@ public class SocketSslClientRenegotiateTest extends AbstractSocketTest { private final TestHandler serverHandler = new TestHandler(serverException); public SocketSslClientRenegotiateTest( - SslContext serverCtx, SslContext clientCtx) { + SslContext serverCtx, SslContext clientCtx, boolean delegate) { this.serverCtx = serverCtx; this.clientCtx = clientCtx; + this.delegate = delegate; } @Test(timeout = 30000) @@ -129,58 +136,74 @@ public class SocketSslClientRenegotiateTest extends AbstractSocketTest { run(); } + private static SslHandler newSslHandler(SslContext sslCtx, ByteBufAllocator allocator, Executor executor) { + if (executor == null) { + return sslCtx.newHandler(allocator); + } else { + return sslCtx.newHandler(allocator, executor); + } + } + public void testSslRenegotiationRejected(ServerBootstrap sb, Bootstrap cb) throws Throwable { reset(); - sb.childHandler(new ChannelInitializer<Channel>() { - @Override - @SuppressWarnings("deprecation") - public void initChannel(Channel sch) throws Exception { - serverChannel = sch; - serverSslHandler = serverCtx.newHandler(sch.alloc()); - // As we test renegotiation we should use a protocol that support it. - serverSslHandler.engine().setEnabledProtocols(new String[] { "TLSv1.2" }); - sch.pipeline().addLast("ssl", serverSslHandler); - sch.pipeline().addLast("handler", serverHandler); - } - }); - - cb.handler(new ChannelInitializer<Channel>() { - @Override - @SuppressWarnings("deprecation") - public void initChannel(Channel sch) throws Exception { - clientChannel = sch; - clientSslHandler = clientCtx.newHandler(sch.alloc()); - // As we test renegotiation we should use a protocol that support it. - clientSslHandler.engine().setEnabledProtocols(new String[] { "TLSv1.2" }); - sch.pipeline().addLast("ssl", clientSslHandler); - sch.pipeline().addLast("handler", clientHandler); - } - }); - - Channel sc = sb.bind().sync().channel(); - cb.connect(sc.localAddress()).sync(); + final ExecutorService executorService = delegate ? Executors.newCachedThreadPool() : null; - Future<Channel> clientHandshakeFuture = clientSslHandler.handshakeFuture(); - clientHandshakeFuture.sync(); - - String renegotiation = clientSslHandler.engine().getEnabledCipherSuites()[0]; - // Use the first previous enabled ciphersuite and try to renegotiate. - clientSslHandler.engine().setEnabledCipherSuites(new String[] { renegotiation }); - clientSslHandler.renegotiate().await(); - serverChannel.close().awaitUninterruptibly(); - clientChannel.close().awaitUninterruptibly(); - sc.close().awaitUninterruptibly(); try { - if (serverException.get() != null) { - throw serverException.get(); + sb.childHandler(new ChannelInitializer<Channel>() { + @Override + @SuppressWarnings("deprecation") + public void initChannel(Channel sch) throws Exception { + serverChannel = sch; + serverSslHandler = newSslHandler(serverCtx, sch.alloc(), executorService); + // As we test renegotiation we should use a protocol that support it. + serverSslHandler.engine().setEnabledProtocols(new String[]{"TLSv1.2"}); + sch.pipeline().addLast("ssl", serverSslHandler); + sch.pipeline().addLast("handler", serverHandler); + } + }); + + cb.handler(new ChannelInitializer<Channel>() { + @Override + @SuppressWarnings("deprecation") + public void initChannel(Channel sch) throws Exception { + clientChannel = sch; + clientSslHandler = newSslHandler(clientCtx, sch.alloc(), executorService); + // As we test renegotiation we should use a protocol that support it. + clientSslHandler.engine().setEnabledProtocols(new String[]{"TLSv1.2"}); + sch.pipeline().addLast("ssl", clientSslHandler); + sch.pipeline().addLast("handler", clientHandler); + } + }); + + Channel sc = sb.bind().sync().channel(); + cb.connect(sc.localAddress()).sync(); + + Future<Channel> clientHandshakeFuture = clientSslHandler.handshakeFuture(); + clientHandshakeFuture.sync(); + + String renegotiation = clientSslHandler.engine().getEnabledCipherSuites()[0]; + // Use the first previous enabled ciphersuite and try to renegotiate. + clientSslHandler.engine().setEnabledCipherSuites(new String[]{renegotiation}); + clientSslHandler.renegotiate().await(); + serverChannel.close().awaitUninterruptibly(); + clientChannel.close().awaitUninterruptibly(); + sc.close().awaitUninterruptibly(); + try { + if (serverException.get() != null) { + throw serverException.get(); + } + fail(); + } catch (DecoderException e) { + assertTrue(e.getCause() instanceof SSLHandshakeException); + } + if (clientException.get() != null) { + throw clientException.get(); + } + } finally { + if (executorService != null) { + executorService.shutdown(); } - fail(); - } catch (DecoderException e) { - assertTrue(e.getCause() instanceof SSLHandshakeException); - } - if (clientException.get() != null) { - throw clientException.get(); } } diff --git a/testsuite/src/main/java/io/netty/testsuite/transport/socket/SocketSslGreetingTest.java b/testsuite/src/main/java/io/netty/testsuite/transport/socket/SocketSslGreetingTest.java index d7db90b..b3f0e46 100644 --- a/testsuite/src/main/java/io/netty/testsuite/transport/socket/SocketSslGreetingTest.java +++ b/testsuite/src/main/java/io/netty/testsuite/transport/socket/SocketSslGreetingTest.java @@ -18,6 +18,7 @@ package io.netty.testsuite.transport.socket; import io.netty.bootstrap.Bootstrap; import io.netty.bootstrap.ServerBootstrap; import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; import io.netty.channel.Channel; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInitializer; @@ -48,6 +49,9 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicReference; import static org.junit.Assert.assertEquals; @@ -74,7 +78,7 @@ public class SocketSslGreetingTest extends AbstractSocketTest { KEY_FILE = ssc.privateKey(); } - @Parameters(name = "{index}: serverEngine = {0}, clientEngine = {1}") + @Parameters(name = "{index}: serverEngine = {0}, clientEngine = {1}, delegate = {2}") public static Collection<Object[]> data() throws Exception { List<SslContext> serverContexts = new ArrayList<SslContext>(); serverContexts.add(SslContextBuilder.forServer(CERT_FILE, KEY_FILE).sslProvider(SslProvider.JDK).build()); @@ -95,7 +99,8 @@ public class SocketSslGreetingTest extends AbstractSocketTest { List<Object[]> params = new ArrayList<Object[]>(); for (SslContext sc: serverContexts) { for (SslContext cc: clientContexts) { - params.add(new Object[] { sc, cc }); + params.add(new Object[] { sc, cc, true }); + params.add(new Object[] { sc, cc, false }); } } return params; @@ -103,10 +108,20 @@ public class SocketSslGreetingTest extends AbstractSocketTest { private final SslContext serverCtx; private final SslContext clientCtx; + private final boolean delegate; - public SocketSslGreetingTest(SslContext serverCtx, SslContext clientCtx) { + public SocketSslGreetingTest(SslContext serverCtx, SslContext clientCtx, boolean delegate) { this.serverCtx = serverCtx; this.clientCtx = clientCtx; + this.delegate = delegate; + } + + private static SslHandler newSslHandler(SslContext sslCtx, ByteBufAllocator allocator, Executor executor) { + if (executor == null) { + return sslCtx.newHandler(allocator); + } else { + return sslCtx.newHandler(allocator, executor); + } } // Test for https://github.com/netty/netty/pull/2437 @@ -119,46 +134,53 @@ public class SocketSslGreetingTest extends AbstractSocketTest { final ServerHandler sh = new ServerHandler(); final ClientHandler ch = new ClientHandler(); - sb.childHandler(new ChannelInitializer<Channel>() { - @Override - public void initChannel(Channel sch) throws Exception { - ChannelPipeline p = sch.pipeline(); - p.addLast(serverCtx.newHandler(sch.alloc())); - p.addLast(new LoggingHandler(LOG_LEVEL)); - p.addLast(sh); - } - }); - - cb.handler(new ChannelInitializer<Channel>() { - @Override - public void initChannel(Channel sch) throws Exception { - ChannelPipeline p = sch.pipeline(); - p.addLast(clientCtx.newHandler(sch.alloc())); - p.addLast(new LoggingHandler(LOG_LEVEL)); - p.addLast(ch); - } - }); + final ExecutorService executorService = delegate ? Executors.newCachedThreadPool() : null; + try { + sb.childHandler(new ChannelInitializer<Channel>() { + @Override + public void initChannel(Channel sch) throws Exception { + ChannelPipeline p = sch.pipeline(); + p.addLast(newSslHandler(serverCtx, sch.alloc(), executorService)); + p.addLast(new LoggingHandler(LOG_LEVEL)); + p.addLast(sh); + } + }); + + cb.handler(new ChannelInitializer<Channel>() { + @Override + public void initChannel(Channel sch) throws Exception { + ChannelPipeline p = sch.pipeline(); + p.addLast(newSslHandler(clientCtx, sch.alloc(), executorService)); + p.addLast(new LoggingHandler(LOG_LEVEL)); + p.addLast(ch); + } + }); - Channel sc = sb.bind().sync().channel(); - Channel cc = cb.connect(sc.localAddress()).sync().channel(); + Channel sc = sb.bind().sync().channel(); + Channel cc = cb.connect(sc.localAddress()).sync().channel(); - ch.latch.await(); + ch.latch.await(); - sh.channel.close().awaitUninterruptibly(); - cc.close().awaitUninterruptibly(); - sc.close().awaitUninterruptibly(); + sh.channel.close().awaitUninterruptibly(); + cc.close().awaitUninterruptibly(); + sc.close().awaitUninterruptibly(); - if (sh.exception.get() != null && !(sh.exception.get() instanceof IOException)) { - throw sh.exception.get(); - } - if (ch.exception.get() != null && !(ch.exception.get() instanceof IOException)) { - throw ch.exception.get(); - } - if (sh.exception.get() != null) { - throw sh.exception.get(); - } - if (ch.exception.get() != null) { - throw ch.exception.get(); + if (sh.exception.get() != null && !(sh.exception.get() instanceof IOException)) { + throw sh.exception.get(); + } + if (ch.exception.get() != null && !(ch.exception.get() instanceof IOException)) { + throw ch.exception.get(); + } + if (sh.exception.get() != null) { + throw sh.exception.get(); + } + if (ch.exception.get() != null) { + throw ch.exception.get(); + } + } finally { + if (executorService != null) { + executorService.shutdown(); + } } } diff --git a/testsuite/src/main/java/io/netty/testsuite/transport/socket/SocketTestPermutation.java b/testsuite/src/main/java/io/netty/testsuite/transport/socket/SocketTestPermutation.java index 3bf9883..8c41f37 100644 --- a/testsuite/src/main/java/io/netty/testsuite/transport/socket/SocketTestPermutation.java +++ b/testsuite/src/main/java/io/netty/testsuite/transport/socket/SocketTestPermutation.java @@ -70,6 +70,7 @@ public class SocketTestPermutation { new OioEventLoopGroup(Integer.MAX_VALUE, new DefaultThreadFactory("testsuite-oio-worker", true)); protected <A extends AbstractBootstrap<?, ?>, B extends AbstractBootstrap<?, ?>> + List<BootstrapComboFactory<A, B>> combo(List<BootstrapFactory<A>> sbfs, List<BootstrapFactory<B>> cbfs) { List<BootstrapComboFactory<A, B>> list = new ArrayList<BootstrapComboFactory<A, B>>(); @@ -112,7 +113,7 @@ public class SocketTestPermutation { return list; } - public List<BootstrapComboFactory<Bootstrap, Bootstrap>> datagram() { + public List<BootstrapComboFactory<Bootstrap, Bootstrap>> datagram(final InternetProtocolFamily family) { // Make the list of Bootstrap factories. List<BootstrapFactory<Bootstrap>> bfs = Arrays.asList( new BootstrapFactory<Bootstrap>() { @@ -121,7 +122,7 @@ public class SocketTestPermutation { return new Bootstrap().group(nioWorkerGroup).channelFactory(new ChannelFactory<Channel>() { @Override public Channel newChannel() { - return new NioDatagramChannel(InternetProtocolFamily.IPv4); + return new NioDatagramChannel(family); } @Override diff --git a/testsuite/src/main/java/io/netty/testsuite/transport/udt/UDTClientServerConnectionTest.java b/testsuite/src/main/java/io/netty/testsuite/transport/udt/UDTClientServerConnectionTest.java index c878643..67aae95 100644 --- a/testsuite/src/main/java/io/netty/testsuite/transport/udt/UDTClientServerConnectionTest.java +++ b/testsuite/src/main/java/io/netty/testsuite/transport/udt/UDTClientServerConnectionTest.java @@ -35,6 +35,9 @@ import io.netty.util.CharsetUtil; import io.netty.util.NetUtil; import io.netty.util.concurrent.DefaultThreadFactory; import io.netty.util.concurrent.GlobalEventExecutor; +import io.netty.util.internal.PlatformDependent; +import org.junit.Assume; +import org.junit.BeforeClass; import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -333,6 +336,22 @@ public class UDTClientServerConnectionTest { static final int WAIT_COUNT = 50; static final int WAIT_SLEEP = 100; + @BeforeClass + public static void assumeUdt() { + Assume.assumeTrue("com.barchart.udt.SocketUDT can not be loaded and initialized", canLoadAndInit()); + Assume.assumeFalse("Not supported on J9 JVM", PlatformDependent.isJ9Jvm()); + } + + private static boolean canLoadAndInit() { + try { + Class.forName("com.barchart.udt.SocketUDT", true, + UDTClientServerConnectionTest.class.getClassLoader()); + return true; + } catch (Throwable e) { + return false; + } + } + /** * Verify UDT client/server connect and disconnect. */ diff --git a/testsuite/src/main/java/io/netty/testsuite/util/TestUtils.java b/testsuite/src/main/java/io/netty/testsuite/util/TestUtils.java index 98cb333..c75236b 100644 --- a/testsuite/src/main/java/io/netty/testsuite/util/TestUtils.java +++ b/testsuite/src/main/java/io/netty/testsuite/util/TestUtils.java @@ -16,6 +16,7 @@ package io.netty.testsuite.util; import io.netty.util.CharsetUtil; +import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; import org.junit.rules.TestName; @@ -112,9 +113,7 @@ public final class TestUtils { public static void dump(String filenamePrefix) throws IOException { - if (filenamePrefix == null) { - throw new NullPointerException("filenamePrefix"); - } + ObjectUtil.checkNotNull(filenamePrefix, "filenamePrefix"); final String timestamp = timestamp(); final File heapDumpFile = new File(filenamePrefix + '.' + timestamp + ".hprof"); @@ -142,6 +141,10 @@ public final class TestUtils { return name.endsWith(".hprof"); } }); + if (files == null) { + logger.warn("failed to find heap dump due to I/O error!"); + return; + } final byte[] buf = new byte[65536]; final LZMA2Options options = new LZMA2Options(LZMA2Options.PRESET_DEFAULT); diff --git a/transport-blockhound-tests/pom.xml b/transport-blockhound-tests/pom.xml new file mode 100644 index 0000000..9bbc2a9 --- /dev/null +++ b/transport-blockhound-tests/pom.xml @@ -0,0 +1,77 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + ~ Copyright 2019 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. + --> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> + + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>io.netty</groupId> + <artifactId>netty-parent</artifactId> + <version>4.1.48.Final</version> + </parent> + + <artifactId>netty-transport-blockhound-tests</artifactId> + <packaging>jar</packaging> + <description> + Tests for the BlockHound integration. + </description> + + <name>Netty/Transport/BlockHound/Tests</name> + + <profiles> + <profile> + <id>java13</id> + <activation> + <jdk>13</jdk> + </activation> + <properties> + <argLine.common>-XX:+AllowRedefinitionToAddDeleteMethods</argLine.common> + </properties> + </profile> + </profiles> + + <properties> + <maven.compiler.source>1.8</maven.compiler.source> + <maven.compiler.target>1.8</maven.compiler.target> + <!-- Needed for SelfSignedCertificate --> + <argLine.java9.extras>--add-exports java.base/sun.security.x509=ALL-UNNAMED</argLine.java9.extras> + <skipJapicmp>true</skipJapicmp> + </properties> + + <dependencies> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>netty-transport</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>netty-handler</artifactId> + <version>${project.version}</version> + </dependency> + + <dependency> + <groupId>org.bouncycastle</groupId> + <artifactId>bcpkix-jdk15on</artifactId> + <optional>true</optional> + </dependency> + <dependency> + <groupId>io.projectreactor.tools</groupId> + <artifactId>blockhound</artifactId> + <scope>test</scope> + </dependency> + </dependencies> +</project> diff --git a/transport-blockhound-tests/src/test/java/io/netty/util/internal/NettyBlockHoundIntegrationTest.java b/transport-blockhound-tests/src/test/java/io/netty/util/internal/NettyBlockHoundIntegrationTest.java new file mode 100644 index 0000000..817257b --- /dev/null +++ b/transport-blockhound-tests/src/test/java/io/netty/util/internal/NettyBlockHoundIntegrationTest.java @@ -0,0 +1,215 @@ +/* + * Copyright 2019 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.util.internal; + +import io.netty.bootstrap.Bootstrap; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.buffer.UnpooledByteBufAllocator; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslContextBuilder; +import io.netty.handler.ssl.SslHandler; +import io.netty.handler.ssl.SslHandshakeCompletionEvent; +import io.netty.handler.ssl.SslProvider; +import io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import io.netty.handler.ssl.util.SelfSignedCertificate; +import io.netty.util.ReferenceCountUtil; +import io.netty.util.concurrent.DefaultThreadFactory; +import io.netty.util.concurrent.EventExecutor; +import io.netty.util.concurrent.GlobalEventExecutor; +import io.netty.util.concurrent.ImmediateEventExecutor; +import io.netty.util.concurrent.ImmediateExecutor; +import io.netty.util.concurrent.ScheduledFuture; +import io.netty.util.concurrent.SingleThreadEventExecutor; +import io.netty.util.internal.Hidden.NettyBlockHoundIntegration; +import org.hamcrest.Matchers; +import org.junit.BeforeClass; +import org.junit.Test; +import reactor.blockhound.BlockHound; +import reactor.blockhound.BlockingOperationError; +import reactor.blockhound.integration.BlockHoundIntegration; + +import java.net.InetSocketAddress; +import java.util.ServiceLoader; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.FutureTask; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +public class NettyBlockHoundIntegrationTest { + + @BeforeClass + public static void setUpClass() { + BlockHound.install(); + } + + @Test + public void testServiceLoader() { + for (BlockHoundIntegration integration : ServiceLoader.load(BlockHoundIntegration.class)) { + if (integration instanceof NettyBlockHoundIntegration) { + return; + } + } + + fail("NettyBlockHoundIntegration cannot be loaded with ServiceLoader"); + } + + @Test + public void testBlockingCallsInNettyThreads() throws Exception { + final FutureTask<Void> future = new FutureTask<>(() -> { + Thread.sleep(0); + return null; + }); + GlobalEventExecutor.INSTANCE.execute(future); + + try { + future.get(5, TimeUnit.SECONDS); + fail("Expected an exception due to a blocking call but none was thrown"); + } catch (ExecutionException e) { + assertThat(e.getCause(), Matchers.instanceOf(BlockingOperationError.class)); + } + } + + @Test(timeout = 5000L) + public void testGlobalEventExecutorTakeTask() throws InterruptedException { + testEventExecutorTakeTask(GlobalEventExecutor.INSTANCE); + } + + @Test(timeout = 5000L) + public void testSingleThreadEventExecutorTakeTask() throws InterruptedException { + SingleThreadEventExecutor executor = + new SingleThreadEventExecutor(null, new DefaultThreadFactory("test"), true) { + @Override + protected void run() { + while (!confirmShutdown()) { + Runnable task = takeTask(); + if (task != null) { + task.run(); + } + } + } + }; + testEventExecutorTakeTask(executor); + } + + private static void testEventExecutorTakeTask(EventExecutor eventExecutor) throws InterruptedException { + CountDownLatch latch = new CountDownLatch(1); + ScheduledFuture<?> f = eventExecutor.schedule(latch::countDown, 10, TimeUnit.MILLISECONDS); + f.sync(); + latch.await(); + } + + // Tests copied from io.netty.handler.ssl.SslHandlerTest + @Test + public void testHandshakeWithExecutorThatExecuteDirectory() throws Exception { + testHandshakeWithExecutor(Runnable::run); + } + + @Test + public void testHandshakeWithImmediateExecutor() throws Exception { + testHandshakeWithExecutor(ImmediateExecutor.INSTANCE); + } + + @Test + public void testHandshakeWithImmediateEventExecutor() throws Exception { + testHandshakeWithExecutor(ImmediateEventExecutor.INSTANCE); + } + + @Test + public void testHandshakeWithExecutor() throws Exception { + ExecutorService executorService = Executors.newCachedThreadPool(); + try { + testHandshakeWithExecutor(executorService); + } finally { + executorService.shutdown(); + } + } + + private static void testHandshakeWithExecutor(Executor executor) throws Exception { + final SslContext sslClientCtx = SslContextBuilder.forClient() + .trustManager(InsecureTrustManagerFactory.INSTANCE) + .sslProvider(SslProvider.JDK).build(); + + final SelfSignedCertificate cert = new SelfSignedCertificate(); + final SslContext sslServerCtx = SslContextBuilder.forServer(cert.key(), cert.cert()) + .sslProvider(SslProvider.JDK).build(); + + EventLoopGroup group = new NioEventLoopGroup(); + Channel sc = null; + Channel cc = null; + final SslHandler clientSslHandler = sslClientCtx.newHandler(UnpooledByteBufAllocator.DEFAULT, executor); + final SslHandler serverSslHandler = sslServerCtx.newHandler(UnpooledByteBufAllocator.DEFAULT, executor); + + try { + sc = new ServerBootstrap() + .group(group) + .channel(NioServerSocketChannel.class) + .childHandler(serverSslHandler) + .bind(new InetSocketAddress(0)).syncUninterruptibly().channel(); + + ChannelFuture future = new Bootstrap() + .group(group) + .channel(NioSocketChannel.class) + .handler(new ChannelInitializer<Channel>() { + @Override + protected void initChannel(Channel ch) { + ch.pipeline() + .addLast(clientSslHandler) + .addLast(new ChannelInboundHandlerAdapter() { + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { + if (evt instanceof SslHandshakeCompletionEvent && + ((SslHandshakeCompletionEvent) evt).cause() != null) { + ((SslHandshakeCompletionEvent) evt).cause().printStackTrace(); + } + ctx.fireUserEventTriggered(evt); + } + }); + } + }).connect(sc.localAddress()); + cc = future.syncUninterruptibly().channel(); + + assertTrue(clientSslHandler.handshakeFuture().await().isSuccess()); + assertTrue(serverSslHandler.handshakeFuture().await().isSuccess()); + } finally { + if (cc != null) { + cc.close().syncUninterruptibly(); + } + if (sc != null) { + sc.close().syncUninterruptibly(); + } + group.shutdownGracefully(); + ReferenceCountUtil.release(sslClientCtx); + } + } +} diff --git a/transport-native-epoll/README.md b/transport-native-epoll/README.md index 43f97ee..ebc7960 100644 --- a/transport-native-epoll/README.md +++ b/transport-native-epoll/README.md @@ -1,3 +1,3 @@ # Native transport for Linux -See [our wiki page](http://netty.io/wiki/native-transports.html). +See [our wiki page](https://netty.io/wiki/native-transports.html). diff --git a/transport-native-epoll/pom.xml b/transport-native-epoll/pom.xml index 4f1f3b8..424c359 100644 --- a/transport-native-epoll/pom.xml +++ b/transport-native-epoll/pom.xml @@ -19,7 +19,7 @@ <parent> <groupId>io.netty</groupId> <artifactId>netty-parent</artifactId> - <version>4.1.33.Final</version> + <version>4.1.48.Final</version> </parent> <artifactId>netty-transport-native-epoll</artifactId> @@ -28,13 +28,15 @@ <properties> <javaModuleName>io.netty.transport.epoll</javaModuleName> - <!-- Needed by the native transport as we need the memoryAddress of the ByteBuffer --> - <argLine.java9.extras>--add-exports java.base/sun.security.x509=ALL-UNNAMED --add-opens=java.base/java.nio=ALL-UNNAMED</argLine.java9.extras> + <!-- Needed as we use SelfSignedCertificate in our tests --> + <argLine.java9.extras>--add-exports java.base/sun.security.x509=ALL-UNNAMED</argLine.java9.extras> <unix.common.lib.name>netty-unix-common</unix.common.lib.name> <unix.common.lib.dir>${project.build.directory}/unix-common-lib</unix.common.lib.dir> <unix.common.lib.unpacked.dir>${unix.common.lib.dir}/META-INF/native/lib</unix.common.lib.unpacked.dir> <unix.common.include.unpacked.dir>${unix.common.lib.dir}/META-INF/native/include</unix.common.include.unpacked.dir> + <jni.compiler.args.cflags>CFLAGS=-O3 -Werror -fno-omit-frame-pointer -Wunused-variable -fvisibility=hidden -I${unix.common.include.unpacked.dir}</jni.compiler.args.cflags> <jni.compiler.args.ldflags>LDFLAGS=-L${unix.common.lib.unpacked.dir} -Wl,--no-as-needed -lrt -Wl,--whole-archive -l${unix.common.lib.name} -Wl,--no-whole-archive</jni.compiler.args.ldflags> + <nativeSourceDirectory>${project.basedir}/src/main/c</nativeSourceDirectory> <skipTests>true</skipTests> </properties> @@ -146,7 +148,7 @@ <id>build-native-lib</id> <configuration> <name>netty_transport_native_epoll_${os.detected.arch}</name> - <nativeSourceDirectory>${project.basedir}/src/main/c</nativeSourceDirectory> + <nativeSourceDirectory>${nativeSourceDirectory}</nativeSourceDirectory> <libDirectory>${project.build.outputDirectory}</libDirectory> <!-- We use Maven's artifact classifier instead. This hack will make the hawtjni plugin to put the native library @@ -155,13 +157,13 @@ <configureArgs> <arg>${jni.compiler.args.ldflags}</arg> <arg>${jni.compiler.args.cflags}</arg> + <configureArg>--libdir=${project.build.directory}/native-build/target/lib</configureArg> </configureArgs> </configuration> <goals> <goal>generate</goal> <goal>build</goal> </goals> - <phase>compile</phase> </execution> </executions> </plugin> @@ -191,102 +193,6 @@ </execution> </executions> </plugin> - - <plugin> - <artifactId>maven-antrun-plugin</artifactId> - <executions> - <execution> - <!-- Phase must be before regex-glibc-sendmmsg and regex-linux-sendmmsg --> - <phase>validate</phase> - <goals> - <goal>run</goal> - </goals> - <id>ant-get-systeminfo</id> - <configuration> - <exportAntProperties>true</exportAntProperties> - <tasks> - <exec executable="sh" outputproperty="ldd_version"> - <arg value="-c" /> - <arg value="ldd --version | head -1" /> - </exec> - <exec executable="uname" outputproperty="uname_os_version"> - <arg value="-r" /> - </exec> - </tasks> - </configuration> - </execution> - </executions> - </plugin> - <plugin> - <groupId>org.codehaus.mojo</groupId> - <artifactId>build-helper-maven-plugin</artifactId> - <executions> - <execution> - <!-- Phase must be before regex-combined-sendmmsg --> - <phase>initialize</phase> - <id>regex-glibc-sendmmsg</id> - <goals> - <goal>regex-property</goal> - </goals> - <configuration> - <name>glibc.sendmmsg.support</name> - <value>${ldd_version}</value> - <!-- Version must be >= 2.14 - set to IO_NETTY_SENDMSSG_NOT_FOUND if this version is not satisfied --> - <regex>^((?!^[^)]+\)\s+(0*2\.1[4-9]|0*2\.[2-9][0-9]+|0*[3-9][0-9]*|0*[1-9]+[0-9]+).*).)*$</regex> - <replacement>IO_NETTY_SENDMSSG_NOT_FOUND</replacement> - <failIfNoMatch>false</failIfNoMatch> - </configuration> - </execution> - <execution> - <!-- Phase must be before regex-combined-sendmmsg --> - <phase>initialize</phase> - <id>regex-linux-sendmmsg</id> - <goals> - <goal>regex-property</goal> - </goals> - <configuration> - <name>linux.sendmmsg.support</name> - <value>${uname_os_version}</value> - <!-- Version must be >= 3 - set to IO_NETTY_SENDMSSG_NOT_FOUND if this version is not satisfied --> - <regex>^((?!^[0-9]*[3-9]\.?.*).)*$</regex> - <replacement>IO_NETTY_SENDMSSG_NOT_FOUND</replacement> - <failIfNoMatch>false</failIfNoMatch> - </configuration> - </execution> - <execution> - <!-- Phase must be before regex-unset-if-needed-sendmmsg --> - <phase>generate-sources</phase> - <id>regex-combined-sendmmsg</id> - <goals> - <goal>regex-property</goal> - </goals> - <configuration> - <name>jni.compiler.args.cflags</name> - <value>${linux.sendmmsg.support}${glibc.sendmmsg.support}</value> - <!-- If glibc and linux kernel are both not sufficient...then define the CFLAGS --> - <regex>.*IO_NETTY_SENDMSSG_NOT_FOUND.*</regex> - <replacement>CFLAGS=-O3 -DIO_NETTY_SENDMMSG_NOT_FOUND -Werror -fno-omit-frame-pointer -Wunused-variable -fvisibility=hidden -I${unix.common.include.unpacked.dir}</replacement> - <failIfNoMatch>false</failIfNoMatch> - </configuration> - </execution> - <execution> - <!-- Phase must be before build-native-lib --> - <phase>generate-sources</phase> - <id>regex-unset-if-needed-sendmmsg</id> - <goals> - <goal>regex-property</goal> - </goals> - <configuration> - <name>jni.compiler.args.cflags</name> - <value>${jni.compiler.args.cflags}</value> - <!-- If glibc and linux kernel are both not sufficient...then define the CFLAGS --> - <regex>^((?!CFLAGS=).)*$</regex> - <replacement>CFLAGS=-O3 -Werror -fno-omit-frame-pointer -Wunused-variable -fvisibility=hidden -I${unix.common.include.unpacked.dir}</replacement> - <failIfNoMatch>false</failIfNoMatch> - </configuration> - </execution> - </executions> - </plugin> </plugins> </build> @@ -350,6 +256,24 @@ <build> <plugins> + <!-- Also include c files in source jar --> + <plugin> + <groupId>org.codehaus.mojo</groupId> + <artifactId>build-helper-maven-plugin</artifactId> + <executions> + <execution> + <phase>generate-sources</phase> + <goals> + <goal>add-source</goal> + </goals> + <configuration> + <sources> + <source>${nativeSourceDirectory}</source> + </sources> + </configuration> + </execution> + </executions> + </plugin> <plugin> <artifactId>maven-jar-plugin</artifactId> <executions> diff --git a/transport-native-epoll/src/main/c/netty_epoll_linuxsocket.c b/transport-native-epoll/src/main/c/netty_epoll_linuxsocket.c index 05d889f..3bb7b20 100644 --- a/transport-native-epoll/src/main/c/netty_epoll_linuxsocket.c +++ b/transport-native-epoll/src/main/c/netty_epoll_linuxsocket.c @@ -64,6 +64,44 @@ static jfieldID fdFieldId = NULL; static jfieldID fileDescriptorFieldId = NULL; // JNI Registered Methods Begin +static void netty_epoll_linuxsocket_setTimeToLive(JNIEnv* env, jclass clazz, jint fd, jint optval) { + netty_unix_socket_setOption(env, fd, IPPROTO_IP, IP_TTL, &optval, sizeof(optval)); +} + +static void netty_epoll_linuxsocket_setIpMulticastLoop(JNIEnv* env, jclass clazz, jint fd, jboolean ipv6, jint optval) { + if (ipv6 == JNI_TRUE) { + u_int val = (u_int) optval; + netty_unix_socket_setOption(env, fd, IPPROTO_IPV6, IPV6_MULTICAST_LOOP, &val, sizeof(val)); + } else { + u_char val = (u_char) optval; + netty_unix_socket_setOption(env, fd, IPPROTO_IP, IP_MULTICAST_LOOP, &val, sizeof(val)); + } +} + +static void netty_epoll_linuxsocket_setInterface(JNIEnv* env, jclass clazz, jint fd, jboolean ipv6, jbyteArray interfaceAddress, jint scopeId, jint interfaceIndex) { + struct sockaddr_storage interfaceAddr; + socklen_t interfaceAddrSize; + struct sockaddr_in* interfaceIpAddr; + + memset(&interfaceAddr, 0, sizeof(interfaceAddr)); + + if (ipv6 == JNI_TRUE) { + if (interfaceIndex == -1) { + netty_unix_errors_throwIOException(env, "Unable to find network index"); + return; + } + netty_unix_socket_setOption(env, fd, IPPROTO_IPV6, IPV6_MULTICAST_IF, &interfaceIndex, sizeof(interfaceIndex)); + } else { + if (netty_unix_socket_initSockaddr(env, ipv6, interfaceAddress, scopeId, 0, &interfaceAddr, &interfaceAddrSize) == -1) { + netty_unix_errors_throwIOException(env, "Could not init sockaddr"); + return; + } + + interfaceIpAddr = (struct sockaddr_in*) &interfaceAddr; + netty_unix_socket_setOption(env, fd, IPPROTO_IP, IP_MULTICAST_IF, &interfaceIpAddr->sin_addr, sizeof(interfaceIpAddr->sin_addr)); + } +} + static void netty_epoll_linuxsocket_setTcpCork(JNIEnv* env, jclass clazz, jint fd, jint optval) { netty_unix_socket_setOption(env, fd, IPPROTO_TCP, TCP_CORK, &optval, sizeof(optval)); } @@ -120,10 +158,235 @@ static void netty_epoll_linuxsocket_setSoBusyPoll(JNIEnv* env, jclass clazz, jin netty_unix_socket_setOption(env, fd, SOL_SOCKET, SO_BUSY_POLL, &optval, sizeof(optval)); } -static void netty_epoll_linuxsocket_setTcpMd5Sig(JNIEnv* env, jclass clazz, jint fd, jbyteArray address, jint scopeId, jbyteArray key) { +static void netty_epoll_linuxsocket_joinGroup(JNIEnv* env, jclass clazz, jint fd, jboolean ipv6, jbyteArray groupAddress, jbyteArray interfaceAddress, jint scopeId, jint interfaceIndex) { + struct sockaddr_storage groupAddr; + socklen_t groupAddrSize; + struct sockaddr_storage interfaceAddr; + socklen_t interfaceAddrSize; + struct sockaddr_in* groupIpAddr; + struct sockaddr_in* interfaceIpAddr; + struct ip_mreq mreq; + + struct sockaddr_in6* groupIp6Addr; + struct ipv6_mreq mreq6; + + memset(&groupAddr, 0, sizeof(groupAddr)); + memset(&interfaceAddr, 0, sizeof(interfaceAddr)); + + if (netty_unix_socket_initSockaddr(env, ipv6, groupAddress, scopeId, 0, &groupAddr, &groupAddrSize) == -1) { + netty_unix_errors_throwIOException(env, "Could not init sockaddr for groupAddress"); + return; + } + + switch (groupAddr.ss_family) { + case AF_INET: + if (netty_unix_socket_initSockaddr(env, ipv6, interfaceAddress, scopeId, 0, &interfaceAddr, &interfaceAddrSize) == -1) { + netty_unix_errors_throwIOException(env, "Could not init sockaddr for interfaceAddr"); + return; + } + + interfaceIpAddr = (struct sockaddr_in*) &interfaceAddr; + groupIpAddr = (struct sockaddr_in*) &groupAddr; + + memcpy(&mreq.imr_multiaddr, &groupIpAddr->sin_addr, sizeof(groupIpAddr->sin_addr)); + memcpy(&mreq.imr_interface, &interfaceIpAddr->sin_addr, sizeof(interfaceIpAddr->sin_addr)); + netty_unix_socket_setOption(env, fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)); + break; + case AF_INET6: + if (interfaceIndex == -1) { + netty_unix_errors_throwIOException(env, "Unable to find network index"); + return; + } + mreq6.ipv6mr_interface = interfaceIndex; + + groupIp6Addr = (struct sockaddr_in6*) &groupAddr; + memcpy(&mreq6.ipv6mr_multiaddr, &groupIp6Addr->sin6_addr, sizeof(groupIp6Addr->sin6_addr)); + netty_unix_socket_setOption(env, fd, IPPROTO_IPV6, IPV6_JOIN_GROUP, &mreq6, sizeof(mreq6)); + break; + default: + netty_unix_errors_throwIOException(env, "Address family not supported"); + break; + } +} + +static void netty_epoll_linuxsocket_joinSsmGroup(JNIEnv* env, jclass clazz, jint fd, jboolean ipv6, jbyteArray groupAddress, jbyteArray interfaceAddress, jint scopeId, jint interfaceIndex, jbyteArray sourceAddress) { + struct sockaddr_storage groupAddr; + socklen_t groupAddrSize; + struct sockaddr_storage interfaceAddr; + socklen_t interfaceAddrSize; + struct sockaddr_storage sourceAddr; + socklen_t sourceAddrSize; + struct sockaddr_in* groupIpAddr; + struct sockaddr_in* interfaceIpAddr; + struct sockaddr_in* sourceIpAddr; + struct ip_mreq_source mreq; + + struct group_source_req mreq6; + + memset(&groupAddr, 0, sizeof(groupAddr)); + memset(&sourceAddr, 0, sizeof(sourceAddr)); + memset(&interfaceAddr, 0, sizeof(interfaceAddr)); + + if (netty_unix_socket_initSockaddr(env, ipv6, groupAddress, scopeId, 0, &groupAddr, &groupAddrSize) == -1) { + netty_unix_errors_throwIOException(env, "Could not init sockaddr for groupAddress"); + return; + } + + if (netty_unix_socket_initSockaddr(env, ipv6, sourceAddress, scopeId, 0, &sourceAddr, &sourceAddrSize) == -1) { + netty_unix_errors_throwIOException(env, "Could not init sockaddr for sourceAddress"); + return; + } + + switch (groupAddr.ss_family) { + case AF_INET: + if (netty_unix_socket_initSockaddr(env, ipv6, interfaceAddress, scopeId, 0, &interfaceAddr, &interfaceAddrSize) == -1) { + netty_unix_errors_throwIOException(env, "Could not init sockaddr for interfaceAddress"); + return; + } + interfaceIpAddr = (struct sockaddr_in*) &interfaceAddr; + groupIpAddr = (struct sockaddr_in*) &groupAddr; + sourceIpAddr = (struct sockaddr_in*) &sourceAddr; + memcpy(&mreq.imr_multiaddr, &groupIpAddr->sin_addr, sizeof(groupIpAddr->sin_addr)); + memcpy(&mreq.imr_interface, &interfaceIpAddr->sin_addr, sizeof(interfaceIpAddr->sin_addr)); + memcpy(&mreq.imr_sourceaddr, &sourceIpAddr->sin_addr, sizeof(sourceIpAddr->sin_addr)); + netty_unix_socket_setOption(env, fd, IPPROTO_IP, IP_ADD_SOURCE_MEMBERSHIP, &mreq, sizeof(mreq)); + break; + case AF_INET6: + if (interfaceIndex == -1) { + netty_unix_errors_throwIOException(env, "Unable to find network index"); + return; + } + mreq6.gsr_group = groupAddr; + mreq6.gsr_interface = interfaceIndex; + mreq6.gsr_source = sourceAddr; + netty_unix_socket_setOption(env, fd, IPPROTO_IPV6, MCAST_JOIN_SOURCE_GROUP, &mreq6, sizeof(mreq6)); + break; + default: + netty_unix_errors_throwIOException(env, "Address family not supported"); + break; + } +} + +static void netty_epoll_linuxsocket_leaveGroup(JNIEnv* env, jclass clazz, jint fd, jboolean ipv6, jbyteArray groupAddress, jbyteArray interfaceAddress, jint scopeId, jint interfaceIndex) { + struct sockaddr_storage groupAddr; + socklen_t groupAddrSize; + + struct sockaddr_storage interfaceAddr; + socklen_t interfaceAddrSize; + struct sockaddr_in* groupIpAddr; + struct sockaddr_in* interfaceIpAddr; + struct ip_mreq mreq; + + struct sockaddr_in6* groupIp6Addr; + struct ipv6_mreq mreq6; + + memset(&groupAddr, 0, sizeof(groupAddr)); + memset(&interfaceAddr, 0, sizeof(interfaceAddr)); + + if (netty_unix_socket_initSockaddr(env, ipv6, groupAddress, scopeId, 0, &groupAddr, &groupAddrSize) == -1) { + netty_unix_errors_throwIOException(env, "Could not init sockaddr for groupAddress"); + return; + } + + switch (groupAddr.ss_family) { + case AF_INET: + if (netty_unix_socket_initSockaddr(env, ipv6, interfaceAddress, scopeId, 0, &interfaceAddr, &interfaceAddrSize) == -1) { + netty_unix_errors_throwIOException(env, "Could not init sockaddr for interfaceAddress"); + return; + } + interfaceIpAddr = (struct sockaddr_in*) &interfaceAddr; + groupIpAddr = (struct sockaddr_in*) &groupAddr; + + memcpy(&mreq.imr_multiaddr, &groupIpAddr->sin_addr, sizeof(groupIpAddr->sin_addr)); + memcpy(&mreq.imr_interface, &interfaceIpAddr->sin_addr, sizeof(interfaceIpAddr->sin_addr)); + netty_unix_socket_setOption(env, fd, IPPROTO_IP, IP_DROP_MEMBERSHIP, &mreq, sizeof(mreq)); + break; + case AF_INET6: + if (interfaceIndex == -1) { + netty_unix_errors_throwIOException(env, "Unable to find network index"); + return; + } + mreq6.ipv6mr_interface = interfaceIndex; + + groupIp6Addr = (struct sockaddr_in6*) &groupAddr; + memcpy(&mreq6.ipv6mr_multiaddr, &groupIp6Addr->sin6_addr, sizeof(groupIp6Addr->sin6_addr)); + netty_unix_socket_setOption(env, fd, IPPROTO_IPV6, IPV6_LEAVE_GROUP, &mreq6, sizeof(mreq6)); + break; + default: + netty_unix_errors_throwIOException(env, "Address family not supported"); + break; + } +} + +static void netty_epoll_linuxsocket_leaveSsmGroup(JNIEnv* env, jclass clazz, jint fd, jboolean ipv6, jbyteArray groupAddress, jbyteArray interfaceAddress, jint scopeId, jint interfaceIndex, jbyteArray sourceAddress) { + struct sockaddr_storage groupAddr; + socklen_t groupAddrSize; + struct sockaddr_storage interfaceAddr; + socklen_t interfaceAddrSize; + struct sockaddr_storage sourceAddr; + socklen_t sourceAddrSize; + struct sockaddr_in* groupIpAddr; + struct sockaddr_in* interfaceIpAddr; + struct sockaddr_in* sourceIpAddr; + + struct ip_mreq_source mreq; + struct group_source_req mreq6; + + memset(&groupAddr, 0, sizeof(groupAddr)); + memset(&sourceAddr, 0, sizeof(sourceAddr)); + memset(&interfaceAddr, 0, sizeof(interfaceAddr)); + + + if (netty_unix_socket_initSockaddr(env, ipv6, groupAddress, scopeId, 0, &groupAddr, &groupAddrSize) == -1) { + netty_unix_errors_throwIOException(env, "Could not init sockaddr for groupAddress"); + return; + } + + if (netty_unix_socket_initSockaddr(env, ipv6, sourceAddress, scopeId, 0, &sourceAddr, &sourceAddrSize) == -1) { + netty_unix_errors_throwIOException(env, "Could not init sockaddr for sourceAddress"); + return; + } + + switch (groupAddr.ss_family) { + case AF_INET: + if (netty_unix_socket_initSockaddr(env, ipv6, interfaceAddress, scopeId, 0, &interfaceAddr, &interfaceAddrSize) == -1) { + netty_unix_errors_throwIOException(env, "Could not init sockaddr for interfaceAddress"); + return; + } + interfaceIpAddr = (struct sockaddr_in*) &interfaceAddr; + + groupIpAddr = (struct sockaddr_in*) &groupAddr; + sourceIpAddr = (struct sockaddr_in*) &sourceAddr; + memcpy(&mreq.imr_multiaddr, &groupIpAddr->sin_addr, sizeof(groupIpAddr->sin_addr)); + memcpy(&mreq.imr_interface, &interfaceIpAddr->sin_addr, sizeof(interfaceIpAddr->sin_addr)); + memcpy(&mreq.imr_sourceaddr, &sourceIpAddr->sin_addr, sizeof(sourceIpAddr->sin_addr)); + netty_unix_socket_setOption(env, fd, IPPROTO_IP, IP_DROP_SOURCE_MEMBERSHIP, &mreq, sizeof(mreq)); + break; + case AF_INET6: + if (interfaceIndex == -1) { + netty_unix_errors_throwIOException(env, "Unable to find network index"); + return; + } + + mreq6.gsr_group = groupAddr; + mreq6.gsr_interface = interfaceIndex; + mreq6.gsr_source = sourceAddr; + netty_unix_socket_setOption(env, fd, IPPROTO_IPV6, MCAST_LEAVE_SOURCE_GROUP, &mreq6, sizeof(mreq6)); + break; + default: + netty_unix_errors_throwIOException(env, "Address family not supported"); + break; + } +} + +static void netty_epoll_linuxsocket_setTcpMd5Sig(JNIEnv* env, jclass clazz, jint fd, jboolean ipv6, jbyteArray address, jint scopeId, jbyteArray key) { struct sockaddr_storage addr; socklen_t addrSize; - if (netty_unix_socket_initSockaddr(env, address, scopeId, 0, &addr, &addrSize) == -1) { + + memset(&addr, 0, sizeof(addr)); + + if (netty_unix_socket_initSockaddr(env, ipv6, address, scopeId, 0, &addr, &addrSize) == -1) { + netty_unix_errors_throwIOException(env, "Could not init sockaddr"); return; } @@ -154,7 +417,49 @@ static void netty_epoll_linuxsocket_setTcpMd5Sig(JNIEnv* env, jclass clazz, jint } if (setsockopt(fd, IPPROTO_TCP, TCP_MD5SIG, &md5sig, sizeof(md5sig)) < 0) { - netty_unix_errors_throwChannelExceptionErrorNo(env, "setsockopt() failed: ", errno); + netty_unix_errors_throwIOExceptionErrorNo(env, "setsockopt() failed: ", errno); + } +} + +static int netty_epoll_linuxsocket_getInterface(JNIEnv* env, jclass clazz, jint fd, jboolean ipv6) { + if (ipv6 == JNI_TRUE) { + int optval; + if (netty_unix_socket_getOption(env, fd, IPPROTO_IPV6, IPV6_MULTICAST_IF, &optval, sizeof(optval)) == -1) { + return -1; + } + return optval; + } else { + struct in_addr optval; + if (netty_unix_socket_getOption(env, fd, IPPROTO_IP, IP_MULTICAST_IF, &optval, sizeof(optval)) == -1) { + return -1; + } + + return ntohl(optval.s_addr); + } +} + +static jint netty_epoll_linuxsocket_getTimeToLive(JNIEnv* env, jclass clazz, jint fd) { + int optval; + if (netty_unix_socket_getOption(env, fd, IPPROTO_IP, IP_TTL, &optval, sizeof(optval)) == -1) { + return -1; + } + return optval; +} + + +static jint netty_epoll_linuxsocket_getIpMulticastLoop(JNIEnv* env, jclass clazz, jint fd, jboolean ipv6) { + if (ipv6 == JNI_TRUE) { + u_int optval; + if (netty_unix_socket_getOption(env, fd, IPPROTO_IPV6, IPV6_MULTICAST_LOOP, &optval, sizeof(optval)) == -1) { + return -1; + } + return (jint) optval; + } else { + u_char optval; + if (netty_unix_socket_getOption(env, fd, IPPROTO_IP, IP_MULTICAST_LOOP, &optval, sizeof(optval)) == -1) { + return -1; + } + return (jint) optval; } } @@ -357,6 +662,12 @@ static jlong netty_epoll_linuxsocket_sendFile(JNIEnv* env, jclass clazz, jint fd // JNI Method Registration Table Begin static const JNINativeMethod fixed_method_table[] = { + { "setTimeToLive", "(II)V", (void *) netty_epoll_linuxsocket_setTimeToLive }, + { "getTimeToLive", "(I)I", (void *) netty_epoll_linuxsocket_getTimeToLive }, + { "setInterface", "(IZ[BII)V", (void *) netty_epoll_linuxsocket_setInterface }, + { "getInterface", "(IZ)I", (void *) netty_epoll_linuxsocket_getInterface }, + { "setIpMulticastLoop", "(IZI)V", (void * ) netty_epoll_linuxsocket_setIpMulticastLoop }, + { "getIpMulticastLoop", "(IZ)I", (void * ) netty_epoll_linuxsocket_getIpMulticastLoop }, { "setTcpCork", "(II)V", (void *) netty_epoll_linuxsocket_setTcpCork }, { "setSoBusyPoll", "(II)V", (void *) netty_epoll_linuxsocket_setSoBusyPoll }, { "setTcpQuickAck", "(II)V", (void *) netty_epoll_linuxsocket_setTcpQuickAck }, @@ -385,7 +696,11 @@ static const JNINativeMethod fixed_method_table[] = { { "isIpTransparent", "(I)I", (void *) netty_epoll_linuxsocket_isIpTransparent }, { "isIpRecvOrigDestAddr", "(I)I", (void *) netty_epoll_linuxsocket_isIpRecvOrigDestAddr }, { "getTcpInfo", "(I[J)V", (void *) netty_epoll_linuxsocket_getTcpInfo }, - { "setTcpMd5Sig", "(I[BI[B)V", (void *) netty_epoll_linuxsocket_setTcpMd5Sig } + { "setTcpMd5Sig", "(IZ[BI[B)V", (void *) netty_epoll_linuxsocket_setTcpMd5Sig }, + { "joinGroup", "(IZ[B[BII)V", (void *) netty_epoll_linuxsocket_joinGroup }, + { "joinSsmGroup", "(IZ[B[BII[B)V", (void *) netty_epoll_linuxsocket_joinSsmGroup }, + { "leaveGroup", "(IZ[B[BII)V", (void *) netty_epoll_linuxsocket_leaveGroup }, + { "leaveSsmGroup", "(IZ[B[BII[B)V", (void *) netty_epoll_linuxsocket_leaveSsmGroup } // "sendFile" has a dynamic signature }; @@ -396,113 +711,83 @@ static jint dynamicMethodsTableSize() { } static JNINativeMethod* createDynamicMethodsTable(const char* packagePrefix) { - JNINativeMethod* dynamicMethods = malloc(sizeof(JNINativeMethod) * dynamicMethodsTableSize()); + char* dynamicTypeName = NULL; + size_t size = sizeof(JNINativeMethod) * dynamicMethodsTableSize(); + JNINativeMethod* dynamicMethods = malloc(size); + if (dynamicMethods == NULL) { + return NULL; + } + memset(dynamicMethods, 0, size); memcpy(dynamicMethods, fixed_method_table, sizeof(fixed_method_table)); + JNINativeMethod* dynamicMethod = &dynamicMethods[fixed_method_table_size]; - char* dynamicTypeName = netty_unix_util_prepend(packagePrefix, "io/netty/channel/unix/PeerCredentials;"); + NETTY_PREPEND(packagePrefix, "io/netty/channel/unix/PeerCredentials;", dynamicTypeName, error); + NETTY_PREPEND("(I)L", dynamicTypeName, dynamicMethod->signature, error); dynamicMethod->name = "getPeerCredentials"; - dynamicMethod->signature = netty_unix_util_prepend("(I)L", dynamicTypeName); dynamicMethod->fnPtr = (void *) netty_epoll_linuxsocket_getPeerCredentials; - free(dynamicTypeName); + netty_unix_util_free_dynamic_name(&dynamicTypeName); ++dynamicMethod; - dynamicTypeName = netty_unix_util_prepend(packagePrefix, "io/netty/channel/DefaultFileRegion;JJJ)J"); + NETTY_PREPEND(packagePrefix, "io/netty/channel/DefaultFileRegion;JJJ)J", dynamicTypeName, error); + NETTY_PREPEND("(IL", dynamicTypeName, dynamicMethod->signature, error); dynamicMethod->name = "sendFile"; - dynamicMethod->signature = netty_unix_util_prepend("(IL", dynamicTypeName); dynamicMethod->fnPtr = (void *) netty_epoll_linuxsocket_sendFile; - free(dynamicTypeName); + netty_unix_util_free_dynamic_name(&dynamicTypeName); return dynamicMethods; +error: + free(dynamicTypeName); + netty_unix_util_free_dynamic_methods_table(dynamicMethods, fixed_method_table_size, dynamicMethodsTableSize()); + return NULL; } -static void freeDynamicMethodsTable(JNINativeMethod* dynamicMethods) { - jint fullMethodTableSize = dynamicMethodsTableSize(); - jint i = fixed_method_table_size; - for (; i < fullMethodTableSize; ++i) { - free(dynamicMethods[i].signature); - } - free(dynamicMethods); -} // JNI Method Registration Table End jint netty_epoll_linuxsocket_JNI_OnLoad(JNIEnv* env, const char* packagePrefix) { + int ret = JNI_ERR; + char* nettyClassName = NULL; + jclass fileRegionCls = NULL; + jclass fileChannelCls = NULL; + jclass fileDescriptorCls = NULL; + // Register the methods which are not referenced by static member variables JNINativeMethod* dynamicMethods = createDynamicMethodsTable(packagePrefix); + if (dynamicMethods == NULL) { + goto done; + } if (netty_unix_util_register_natives(env, packagePrefix, "io/netty/channel/epoll/LinuxSocket", dynamicMethods, dynamicMethodsTableSize()) != 0) { - freeDynamicMethodsTable(dynamicMethods); - return JNI_ERR; + goto done; } - freeDynamicMethodsTable(dynamicMethods); - dynamicMethods = NULL; - char* nettyClassName = netty_unix_util_prepend(packagePrefix, "io/netty/channel/unix/PeerCredentials"); - jclass localPeerCredsClass = (*env)->FindClass(env, nettyClassName); - free(nettyClassName); - nettyClassName = NULL; - if (localPeerCredsClass == NULL) { - // pending exception... - return JNI_ERR; - } - peerCredentialsClass = (jclass) (*env)->NewGlobalRef(env, localPeerCredsClass); - if (peerCredentialsClass == NULL) { - // out-of-memory! - netty_unix_errors_throwOutOfMemoryError(env); - return JNI_ERR; - } - peerCredentialsMethodId = (*env)->GetMethodID(env, peerCredentialsClass, "<init>", "(II[I)V"); - if (peerCredentialsMethodId == NULL) { - netty_unix_errors_throwRuntimeException(env, "failed to get method ID: PeerCredentials.<init>(int, int, int[])"); - return JNI_ERR; - } + NETTY_PREPEND(packagePrefix, "io/netty/channel/unix/PeerCredentials", nettyClassName, done); + NETTY_LOAD_CLASS(env, peerCredentialsClass, nettyClassName, done); + netty_unix_util_free_dynamic_name(&nettyClassName); - nettyClassName = netty_unix_util_prepend(packagePrefix, "io/netty/channel/DefaultFileRegion"); - jclass fileRegionCls = (*env)->FindClass(env, nettyClassName); - free(nettyClassName); - nettyClassName = NULL; - if (fileRegionCls == NULL) { - return JNI_ERR; - } - fileChannelFieldId = (*env)->GetFieldID(env, fileRegionCls, "file", "Ljava/nio/channels/FileChannel;"); - if (fileChannelFieldId == NULL) { - netty_unix_errors_throwRuntimeException(env, "failed to get field ID: DefaultFileRegion.file"); - return JNI_ERR; - } - transferredFieldId = (*env)->GetFieldID(env, fileRegionCls, "transferred", "J"); - if (transferredFieldId == NULL) { - netty_unix_errors_throwRuntimeException(env, "failed to get field ID: DefaultFileRegion.transferred"); - return JNI_ERR; - } + NETTY_GET_METHOD(env, peerCredentialsClass, peerCredentialsMethodId, "<init>", "(II[I)V", done); - jclass fileChannelCls = (*env)->FindClass(env, "sun/nio/ch/FileChannelImpl"); - if (fileChannelCls == NULL) { - // pending exception... - return JNI_ERR; - } - fileDescriptorFieldId = (*env)->GetFieldID(env, fileChannelCls, "fd", "Ljava/io/FileDescriptor;"); - if (fileDescriptorFieldId == NULL) { - netty_unix_errors_throwRuntimeException(env, "failed to get field ID: FileChannelImpl.fd"); - return JNI_ERR; - } + NETTY_PREPEND(packagePrefix, "io/netty/channel/DefaultFileRegion", nettyClassName, done); + NETTY_FIND_CLASS(env, fileRegionCls, nettyClassName, done); + netty_unix_util_free_dynamic_name(&nettyClassName); - jclass fileDescriptorCls = (*env)->FindClass(env, "java/io/FileDescriptor"); - if (fileDescriptorCls == NULL) { - // pending exception... - return JNI_ERR; - } - fdFieldId = (*env)->GetFieldID(env, fileDescriptorCls, "fd", "I"); - if (fdFieldId == NULL) { - netty_unix_errors_throwRuntimeException(env, "failed to get field ID: FileDescriptor.fd"); - return JNI_ERR; - } + NETTY_GET_FIELD(env, fileRegionCls, fileChannelFieldId, "file", "Ljava/nio/channels/FileChannel;", done); + NETTY_GET_FIELD(env, fileRegionCls, transferredFieldId, "transferred", "J", done); + + NETTY_FIND_CLASS(env, fileChannelCls, "sun/nio/ch/FileChannelImpl", done); + NETTY_GET_FIELD(env, fileChannelCls, fileDescriptorFieldId, "fd", "Ljava/io/FileDescriptor;", done); - return NETTY_JNI_VERSION; + NETTY_FIND_CLASS(env, fileDescriptorCls, "java/io/FileDescriptor", done); + NETTY_GET_FIELD(env, fileDescriptorCls, fdFieldId, "fd", "I", done); + + ret = NETTY_JNI_VERSION; +done: + netty_unix_util_free_dynamic_methods_table(dynamicMethods, fixed_method_table_size, dynamicMethodsTableSize()); + free(nettyClassName); + + return ret; } void netty_epoll_linuxsocket_JNI_OnUnLoad(JNIEnv* env) { - if (peerCredentialsClass != NULL) { - (*env)->DeleteGlobalRef(env, peerCredentialsClass); - peerCredentialsClass = NULL; - } + NETTY_UNLOAD_CLASS(env, peerCredentialsClass); } diff --git a/transport-native-epoll/src/main/c/netty_epoll_native.c b/transport-native-epoll/src/main/c/netty_epoll_native.c index 96e55e1..42c9446 100644 --- a/transport-native-epoll/src/main/c/netty_epoll_native.c +++ b/transport-native-epoll/src/main/c/netty_epoll_native.c @@ -36,6 +36,9 @@ #include <inttypes.h> #include <link.h> #include <time.h> +// Needed to be able to use syscalls directly and so not depend on newer GLIBC versions +#include <linux/net.h> +#include <sys/syscall.h> #include "netty_epoll_linuxsocket.h" #include "netty_unix_buffer.h" @@ -54,19 +57,43 @@ // optional extern int epoll_create1(int flags) __attribute__((weak)); -#ifdef IO_NETTY_SENDMMSG_NOT_FOUND -extern int sendmmsg(int sockfd, struct mmsghdr* msgvec, unsigned int vlen, unsigned int flags) __attribute__((weak)); - #ifndef __USE_GNU struct mmsghdr { struct msghdr msg_hdr; /* Message header */ unsigned int msg_len; /* Number of bytes transmitted */ }; #endif + +// All linux syscall numbers are stable so this is safe. +#ifndef SYS_recvmmsg +// Only support SYS_recvmmsg for __x86_64__ / __i386__ for now +#if defined(__x86_64__) +// See https://github.com/torvalds/linux/blob/v5.4/arch/x86/entry/syscalls/syscall_64.tbl +#define SYS_recvmmsg 299 +#elif defined(__i386__) +// See https://github.com/torvalds/linux/blob/v5.4/arch/x86/entry/syscalls/syscall_32.tbl +#define SYS_recvmmsg 337 +#else +#define SYS_recvmmsg -1 #endif +#endif // SYS_recvmmsg + +#ifndef SYS_sendmmsg +// Only support SYS_sendmmsg for __x86_64__ / __i386__ for now +#if defined(__x86_64__) +// See https://github.com/torvalds/linux/blob/v5.4/arch/x86/entry/syscalls/syscall_64.tbl +#define SYS_sendmmsg 307 +#elif defined(__i386__) +// See https://github.com/torvalds/linux/blob/v5.4/arch/x86/entry/syscalls/syscall_32.tbl +#define SYS_sendmmsg 345 +#else +#define SYS_sendmmsg -1 +#endif +#endif // SYS_sendmmsg // Those are initialized in the init(...) method and cached for performance reasons static jfieldID packetAddrFieldId = NULL; +static jfieldID packetAddrLenFieldId = NULL; static jfieldID packetScopeIdFieldId = NULL; static jfieldID packetPortFieldId = NULL; static jfieldID packetMemoryAddressFieldId = NULL; @@ -116,10 +143,27 @@ static jint netty_epoll_native_timerFd(JNIEnv* env, jclass clazz) { } static void netty_epoll_native_eventFdWrite(JNIEnv* env, jclass clazz, jint fd, jlong value) { - jint eventFD = eventfd_write(fd, (eventfd_t) value); - - if (eventFD < 0) { - netty_unix_errors_throwChannelExceptionErrorNo(env, "eventfd_write() failed: ", errno); + uint64_t val; + + for (;;) { + jint ret = eventfd_write(fd, (eventfd_t) value); + + if (ret < 0) { + // We need to read before we can write again, let's try to read and then write again and if this + // fails we will bail out. + // + // See http://man7.org/linux/man-pages/man2/eventfd.2.html. + if (errno == EAGAIN) { + if (eventfd_read(fd, &val) == 0 || errno == EAGAIN) { + // Try again + continue; + } + netty_unix_errors_throwChannelExceptionErrorNo(env, "eventfd_read(...) failed: ", errno); + } else { + netty_unix_errors_throwChannelExceptionErrorNo(env, "eventfd_write(...) failed: ", errno); + } + } + break; } } @@ -169,50 +213,46 @@ static jint netty_epoll_native_epollCreate(JNIEnv* env, jclass clazz) { return efd; } -static jint netty_epoll_native_epollWait0(JNIEnv* env, jclass clazz, jint efd, jlong address, jint len, jint timerFd, jint tvSec, jint tvNsec) { +static void netty_epoll_native_timerFdSetTime(JNIEnv* env, jclass clazz, jint timerFd, jint tvSec, jint tvNsec) { + struct itimerspec ts; + memset(&ts.it_interval, 0, sizeof(struct timespec)); + ts.it_value.tv_sec = tvSec; + ts.it_value.tv_nsec = tvNsec; + if (timerfd_settime(timerFd, 0, &ts, NULL) < 0) { + netty_unix_errors_throwIOExceptionErrorNo(env, "timerfd_settime() failed: ", errno); + } +} + +static jint netty_epoll_native_epollWait(JNIEnv* env, jclass clazz, jint efd, jlong address, jint len, jint timeout) { struct epoll_event *ev = (struct epoll_event*) (intptr_t) address; int result, err; - if (tvSec == 0 && tvNsec == 0) { - // Zeros = poll (aka return immediately). - do { - result = epoll_wait(efd, ev, len, 0); - if (result >= 0) { - return result; - } - } while((err = errno) == EINTR); - } else { - // only reschedule the timer if there is a newer event. - // -1 is a special value used by EpollEventLoop. - if (tvSec != ((jint) -1) && tvNsec != ((jint) -1)) { - struct itimerspec ts; - memset(&ts.it_interval, 0, sizeof(struct timespec)); - ts.it_value.tv_sec = tvSec; - ts.it_value.tv_nsec = tvNsec; - if (timerfd_settime(timerFd, 0, &ts, NULL) < 0) { - netty_unix_errors_throwChannelExceptionErrorNo(env, "timerfd_settime() failed: ", errno); - return -1; - } + do { + result = epoll_wait(efd, ev, len, timeout); + if (result >= 0) { + return result; } - do { - result = epoll_wait(efd, ev, len, -1); - if (result > 0) { - // Detect timeout, and preserve the epoll_wait API. - if (result == 1 && ev[0].data.fd == timerFd) { - // We assume that timerFD is in ET mode. So we must consume this event to ensure we are notified - // of future timer events because ET mode only notifies a single time until the event is consumed. - uint64_t timerFireCount; - // We don't care what the result is. We just want to consume the wakeup event and reset ET. - result = read(timerFd, &timerFireCount, sizeof(uint64_t)); - return 0; - } - return result; - } - } while((err = errno) == EINTR); - } + } while((err = errno) == EINTR); return -err; } +// This method is deprecated! +static jint netty_epoll_native_epollWait0(JNIEnv* env, jclass clazz, jint efd, jlong address, jint len, jint timerFd, jint tvSec, jint tvNsec) { + // only reschedule the timer if there is a newer event. + // -1 is a special value used by EpollEventLoop. + if (tvSec != ((jint) -1) && tvNsec != ((jint) -1)) { + struct itimerspec ts; + memset(&ts.it_interval, 0, sizeof(struct timespec)); + ts.it_value.tv_sec = tvSec; + ts.it_value.tv_nsec = tvNsec; + if (timerfd_settime(timerFd, 0, &ts, NULL) < 0) { + netty_unix_errors_throwChannelExceptionErrorNo(env, "timerfd_settime() failed: ", errno); + return -1; + } + } + return netty_epoll_native_epollWait(env, clazz, efd, address, len, -1); +} + static inline void cpu_relax() { #if defined(__x86_64__) asm volatile("pause\n": : :"memory"); @@ -265,7 +305,7 @@ static jint netty_epoll_native_epollCtlDel0(JNIEnv* env, jclass clazz, jint efd, return res; } -static jint netty_epoll_native_sendmmsg0(JNIEnv* env, jclass clazz, jint fd, jobjectArray packets, jint offset, jint len) { +static jint netty_epoll_native_sendmmsg0(JNIEnv* env, jclass clazz, jint fd, jboolean ipv6, jobjectArray packets, jint offset, jint len) { struct mmsghdr msg[len]; struct sockaddr_storage addr[len]; socklen_t addrSize; @@ -277,24 +317,28 @@ static jint netty_epoll_native_sendmmsg0(JNIEnv* env, jclass clazz, jint fd, job jobject packet = (*env)->GetObjectArrayElement(env, packets, i + offset); jbyteArray address = (jbyteArray) (*env)->GetObjectField(env, packet, packetAddrFieldId); - jint scopeId = (*env)->GetIntField(env, packet, packetScopeIdFieldId); - jint port = (*env)->GetIntField(env, packet, packetPortFieldId); + jint addrLen = (*env)->GetIntField(env, packet, packetAddrLenFieldId); - if (netty_unix_socket_initSockaddr(env, address, scopeId, port, &addr[i], &addrSize) == -1) { - return -1; - } + if (addrLen != 0) { + jint scopeId = (*env)->GetIntField(env, packet, packetScopeIdFieldId); + jint port = (*env)->GetIntField(env, packet, packetPortFieldId); - msg[i].msg_hdr.msg_name = &addr[i]; - msg[i].msg_hdr.msg_namelen = addrSize; + if (netty_unix_socket_initSockaddr(env, ipv6, address, scopeId, port, &addr[i], &addrSize) == -1) { + return -1; + } + msg[i].msg_hdr.msg_name = &addr[i]; + msg[i].msg_hdr.msg_namelen = addrSize; + } msg[i].msg_hdr.msg_iov = (struct iovec*) (intptr_t) (*env)->GetLongField(env, packet, packetMemoryAddressFieldId); - msg[i].msg_hdr.msg_iovlen = (*env)->GetIntField(env, packet, packetCountFieldId);; + msg[i].msg_hdr.msg_iovlen = (*env)->GetIntField(env, packet, packetCountFieldId); } ssize_t res; int err; do { - res = sendmmsg(fd, msg, len, 0); + // We directly use the syscall to prevent depending on GLIBC 2.14. + res = syscall(SYS_sendmmsg, fd, msg, len, 0); // keep on writing if it was interrupted } while (res == -1 && ((err = errno) == EINTR)); @@ -304,6 +348,72 @@ static jint netty_epoll_native_sendmmsg0(JNIEnv* env, jclass clazz, jint fd, job return (jint) res; } +static jint netty_epoll_native_recvmmsg0(JNIEnv* env, jclass clazz, jint fd, jboolean ipv6, jobjectArray packets, jint offset, jint len) { + struct mmsghdr msg[len]; + memset(msg, 0, sizeof(msg)); + struct sockaddr_storage addr[len]; + int addrSize = sizeof(addr); + memset(addr, 0, addrSize); + + int i; + + for (i = 0; i < len; i++) { + jobject packet = (*env)->GetObjectArrayElement(env, packets, i + offset); + msg[i].msg_hdr.msg_iov = (struct iovec*) (intptr_t) (*env)->GetLongField(env, packet, packetMemoryAddressFieldId); + msg[i].msg_hdr.msg_iovlen = (*env)->GetIntField(env, packet, packetCountFieldId); + + msg[i].msg_hdr.msg_name = addr + i; + msg[i].msg_hdr.msg_namelen = (socklen_t) addrSize; + } + + ssize_t res; + int err; + do { + // We directly use the syscall to prevent depending on GLIBC 2.12. + res = syscall(SYS_recvmmsg, fd, &msg, len, 0, NULL); + + // keep on reading if it was interrupted + } while (res == -1 && ((err = errno) == EINTR)); + + if (res < 0) { + return -err; + } + + for (i = 0; i < res; i++) { + jobject packet = (*env)->GetObjectArrayElement(env, packets, i + offset); + jbyteArray address = (jbyteArray) (*env)->GetObjectField(env, packet, packetAddrFieldId); + + (*env)->SetIntField(env, packet, packetCountFieldId, msg[i].msg_len); + + struct sockaddr_storage* addr = (struct sockaddr_storage*) msg[i].msg_hdr.msg_name; + + if (addr->ss_family == AF_INET) { + struct sockaddr_in* ipaddr = (struct sockaddr_in*) addr; + + (*env)->SetByteArrayRegion(env, address, 0, 4, (jbyte*) &ipaddr->sin_addr.s_addr); + (*env)->SetIntField(env, packet, packetAddrLenFieldId, 4); + (*env)->SetIntField(env, packet, packetScopeIdFieldId, 0); + (*env)->SetIntField(env, packet, packetPortFieldId, ntohs(ipaddr->sin_port)); + } else { + int addrLen = netty_unix_socket_ipAddressLength(addr); + struct sockaddr_in6* ip6addr = (struct sockaddr_in6*) addr; + + if (addrLen == 4) { + // IPV4 mapped IPV6 address + jbyte* addr = (jbyte*) &ip6addr->sin6_addr.s6_addr; + (*env)->SetByteArrayRegion(env, address, 0, 4, addr + 12); + } else { + (*env)->SetByteArrayRegion(env, address, 0, 16, (jbyte*) &ip6addr->sin6_addr.s6_addr); + } + (*env)->SetIntField(env, packet, packetAddrLenFieldId, addrLen); + (*env)->SetIntField(env, packet, packetScopeIdFieldId, ip6addr->sin6_scope_id); + (*env)->SetIntField(env, packet, packetPortFieldId, ntohs(ip6addr->sin6_port)); + } + } + + return (jint) res; +} + static jstring netty_epoll_native_kernelVersion(JNIEnv* env, jclass clazz) { struct utsname name; @@ -314,14 +424,28 @@ static jstring netty_epoll_native_kernelVersion(JNIEnv* env, jclass clazz) { netty_unix_errors_throwRuntimeExceptionErrorNo(env, "uname() failed: ", errno); return NULL; } - static jboolean netty_epoll_native_isSupportingSendmmsg(JNIEnv* env, jclass clazz) { - // Use & to avoid warnings with -Wtautological-pointer-compare when sendmmsg is - // not weakly defined. - if (&sendmmsg != NULL) { - return JNI_TRUE; + if (SYS_sendmmsg == -1) { + return JNI_FALSE; } - return JNI_FALSE; + if (syscall(SYS_sendmmsg, -1, NULL, 0, 0) == -1) { + if (errno == ENOSYS) { + return JNI_FALSE; + } + } + return JNI_TRUE; +} + +static jboolean netty_epoll_native_isSupportingRecvmmsg(JNIEnv* env, jclass clazz) { + if (SYS_recvmmsg == -1) { + return JNI_FALSE; + } + if (syscall(SYS_recvmmsg, -1, NULL, 0, 0, NULL) == -1) { + if (errno == ENOSYS) { + return JNI_FALSE; + } + } + return JNI_TRUE; } static jboolean netty_epoll_native_isSupportingTcpFastopen(JNIEnv* env, jclass clazz) { @@ -368,7 +492,7 @@ static jint netty_epoll_native_splice0(JNIEnv* env, jclass clazz, jint fd, jlong loff_t off_out = (loff_t) offOut; loff_t* p_off_in = off_in >= 0 ? &off_in : NULL; - loff_t* p_off_out = off_in >= 0 ? &off_out : NULL; + loff_t* p_off_out = off_out >= 0 ? &off_out : NULL; do { res = splice(fd, p_off_in, fdOut, p_off_out, (size_t) len, SPLICE_F_NONBLOCK | SPLICE_F_MOVE); @@ -402,6 +526,7 @@ static const JNINativeMethod statically_referenced_fixed_method_table[] = { { "epollerr", "()I", (void *) netty_epoll_native_epollerr }, { "tcpMd5SigMaxKeyLen", "()I", (void *) netty_epoll_native_tcpMd5SigMaxKeyLen }, { "isSupportingSendmmsg", "()Z", (void *) netty_epoll_native_isSupportingSendmmsg }, + { "isSupportingRecvmmsg", "()Z", (void *) netty_epoll_native_isSupportingRecvmmsg }, { "isSupportingTcpFastopen", "()Z", (void *) netty_epoll_native_isSupportingTcpFastopen }, { "kernelVersion", "()Ljava/lang/String;", (void *) netty_epoll_native_kernelVersion } }; @@ -412,8 +537,10 @@ static const JNINativeMethod fixed_method_table[] = { { "eventFdWrite", "(IJ)V", (void *) netty_epoll_native_eventFdWrite }, { "eventFdRead", "(I)V", (void *) netty_epoll_native_eventFdRead }, { "timerFdRead", "(I)V", (void *) netty_epoll_native_timerFdRead }, + { "timerFdSetTime", "(III)V", (void *) netty_epoll_native_timerFdSetTime }, { "epollCreate", "()I", (void *) netty_epoll_native_epollCreate }, - { "epollWait0", "(IJIIII)I", (void *) netty_epoll_native_epollWait0 }, + { "epollWait0", "(IJIIII)I", (void *) netty_epoll_native_epollWait0 }, // This method is deprecated! + { "epollWait", "(IJII)I", (void *) netty_epoll_native_epollWait }, { "epollBusyWait0", "(IJI)I", (void *) netty_epoll_native_epollBusyWait0 }, { "epollCtlAdd0", "(III)I", (void *) netty_epoll_native_epollCtlAdd0 }, { "epollCtlMod0", "(III)I", (void *) netty_epoll_native_epollCtlMod0 }, @@ -426,38 +553,53 @@ static const JNINativeMethod fixed_method_table[] = { static const jint fixed_method_table_size = sizeof(fixed_method_table) / sizeof(fixed_method_table[0]); static jint dynamicMethodsTableSize() { - return fixed_method_table_size + 1; // 1 is for the dynamic method signatures. + return fixed_method_table_size + 2; // 2 is for the dynamic method signatures. } static JNINativeMethod* createDynamicMethodsTable(const char* packagePrefix) { - JNINativeMethod* dynamicMethods = malloc(sizeof(JNINativeMethod) * dynamicMethodsTableSize()); + char* dynamicTypeName = NULL; + size_t size = sizeof(JNINativeMethod) * dynamicMethodsTableSize(); + JNINativeMethod* dynamicMethods = malloc(size); + if (dynamicMethods == NULL) { + return NULL; + } + memset(dynamicMethods, 0, size); memcpy(dynamicMethods, fixed_method_table, sizeof(fixed_method_table)); - char* dynamicTypeName = netty_unix_util_prepend(packagePrefix, "io/netty/channel/epoll/NativeDatagramPacketArray$NativeDatagramPacket;II)I"); + JNINativeMethod* dynamicMethod = &dynamicMethods[fixed_method_table_size]; + NETTY_PREPEND(packagePrefix, "io/netty/channel/epoll/NativeDatagramPacketArray$NativeDatagramPacket;II)I", dynamicTypeName, error); + NETTY_PREPEND("(IZ[L", dynamicTypeName, dynamicMethod->signature, error); dynamicMethod->name = "sendmmsg0"; - dynamicMethod->signature = netty_unix_util_prepend("(I[L", dynamicTypeName); dynamicMethod->fnPtr = (void *) netty_epoll_native_sendmmsg0; - free(dynamicTypeName); + netty_unix_util_free_dynamic_name(&dynamicTypeName); + + ++dynamicMethod; + NETTY_PREPEND(packagePrefix, "io/netty/channel/epoll/NativeDatagramPacketArray$NativeDatagramPacket;II)I", dynamicTypeName, error); + NETTY_PREPEND("(IZ[L", dynamicTypeName, dynamicMethod->signature, error); + dynamicMethod->name = "recvmmsg0"; + dynamicMethod->fnPtr = (void *) netty_epoll_native_recvmmsg0; + netty_unix_util_free_dynamic_name(&dynamicTypeName); + return dynamicMethods; +error: + free(dynamicTypeName); + netty_unix_util_free_dynamic_methods_table(dynamicMethods, fixed_method_table_size, dynamicMethodsTableSize()); + return NULL; } -static void freeDynamicMethodsTable(JNINativeMethod* dynamicMethods) { - jint fullMethodTableSize = dynamicMethodsTableSize(); - jint i = fixed_method_table_size; - for (; i < fullMethodTableSize; ++i) { - free(dynamicMethods[i].signature); - } - free(dynamicMethods); -} // JNI Method Registration Table End static jint netty_epoll_native_JNI_OnLoad(JNIEnv* env, const char* packagePrefix) { + int ret = JNI_ERR; int limitsOnLoadCalled = 0; int errorsOnLoadCalled = 0; int filedescriptorOnLoadCalled = 0; int socketOnLoadCalled = 0; int bufferOnLoadCalled = 0; int linuxsocketOnLoadCalled = 0; + char* nettyClassName = NULL; + jclass nativeDatagramPacketCls = NULL; + JNINativeMethod* dynamicMethods = NULL; // We must register the statically referenced methods first! if (netty_unix_util_register_natives(env, @@ -465,116 +607,97 @@ static jint netty_epoll_native_JNI_OnLoad(JNIEnv* env, const char* packagePrefix "io/netty/channel/epoll/NativeStaticallyReferencedJniMethods", statically_referenced_fixed_method_table, statically_referenced_fixed_method_table_size) != 0) { - goto error; + goto done; } // Register the methods which are not referenced by static member variables - JNINativeMethod* dynamicMethods = createDynamicMethodsTable(packagePrefix); + dynamicMethods = createDynamicMethodsTable(packagePrefix); + if (dynamicMethods == NULL) { + goto done; + } + if (netty_unix_util_register_natives(env, packagePrefix, "io/netty/channel/epoll/Native", dynamicMethods, dynamicMethodsTableSize()) != 0) { - freeDynamicMethodsTable(dynamicMethods); - goto error; + goto done; } - freeDynamicMethodsTable(dynamicMethods); - dynamicMethods = NULL; // Load all c modules that we depend upon if (netty_unix_limits_JNI_OnLoad(env, packagePrefix) == JNI_ERR) { - goto error; + goto done; } limitsOnLoadCalled = 1; if (netty_unix_errors_JNI_OnLoad(env, packagePrefix) == JNI_ERR) { - goto error; + goto done; } errorsOnLoadCalled = 1; if (netty_unix_filedescriptor_JNI_OnLoad(env, packagePrefix) == JNI_ERR) { - goto error; + goto done; } filedescriptorOnLoadCalled = 1; if (netty_unix_socket_JNI_OnLoad(env, packagePrefix) == JNI_ERR) { - goto error; + goto done; } socketOnLoadCalled = 1; if (netty_unix_buffer_JNI_OnLoad(env, packagePrefix) == JNI_ERR) { - goto error; + goto done; } bufferOnLoadCalled = 1; if (netty_epoll_linuxsocket_JNI_OnLoad(env, packagePrefix) == JNI_ERR) { - goto error; + goto done; } linuxsocketOnLoadCalled = 1; // Initialize this module - char* nettyClassName = netty_unix_util_prepend(packagePrefix, "io/netty/channel/epoll/NativeDatagramPacketArray$NativeDatagramPacket"); - jclass nativeDatagramPacketCls = (*env)->FindClass(env, nettyClassName); - free(nettyClassName); - nettyClassName = NULL; - if (nativeDatagramPacketCls == NULL) { - // pending exception... - goto error; - } + NETTY_PREPEND(packagePrefix, "io/netty/channel/epoll/NativeDatagramPacketArray$NativeDatagramPacket", nettyClassName, done); + NETTY_FIND_CLASS(env, nativeDatagramPacketCls, nettyClassName, done); + netty_unix_util_free_dynamic_name(&nettyClassName); - packetAddrFieldId = (*env)->GetFieldID(env, nativeDatagramPacketCls, "addr", "[B"); - if (packetAddrFieldId == NULL) { - netty_unix_errors_throwRuntimeException(env, "failed to get field ID: NativeDatagramPacket.addr"); - goto error; - } - packetScopeIdFieldId = (*env)->GetFieldID(env, nativeDatagramPacketCls, "scopeId", "I"); - if (packetScopeIdFieldId == NULL) { - netty_unix_errors_throwRuntimeException(env, "failed to get field ID: NativeDatagramPacket.scopeId"); - goto error; - } - packetPortFieldId = (*env)->GetFieldID(env, nativeDatagramPacketCls, "port", "I"); - if (packetPortFieldId == NULL) { - netty_unix_errors_throwRuntimeException(env, "failed to get field ID: NativeDatagramPacket.port"); - goto error; - } - packetMemoryAddressFieldId = (*env)->GetFieldID(env, nativeDatagramPacketCls, "memoryAddress", "J"); - if (packetMemoryAddressFieldId == NULL) { - netty_unix_errors_throwRuntimeException(env, "failed to get field ID: NativeDatagramPacket.memoryAddress"); - goto error; - } + NETTY_GET_FIELD(env, nativeDatagramPacketCls, packetAddrFieldId, "addr", "[B", done); + NETTY_GET_FIELD(env, nativeDatagramPacketCls, packetAddrLenFieldId, "addrLen", "I", done); + NETTY_GET_FIELD(env, nativeDatagramPacketCls, packetScopeIdFieldId, "scopeId", "I", done); + NETTY_GET_FIELD(env, nativeDatagramPacketCls, packetPortFieldId, "port", "I", done); + NETTY_GET_FIELD(env, nativeDatagramPacketCls, packetMemoryAddressFieldId, "memoryAddress", "J", done); + NETTY_GET_FIELD(env, nativeDatagramPacketCls, packetCountFieldId, "count", "I", done); - packetCountFieldId = (*env)->GetFieldID(env, nativeDatagramPacketCls, "count", "I"); - if (packetCountFieldId == NULL) { - netty_unix_errors_throwRuntimeException(env, "failed to get field ID: NativeDatagramPacket.count"); - goto error; - } + ret = NETTY_JNI_VERSION; +done: - return NETTY_JNI_VERSION; + netty_unix_util_free_dynamic_methods_table(dynamicMethods, fixed_method_table_size, dynamicMethodsTableSize()); + free(nettyClassName); -error: - if (limitsOnLoadCalled == 1) { - netty_unix_limits_JNI_OnUnLoad(env); - } - if (errorsOnLoadCalled == 1) { - netty_unix_errors_JNI_OnUnLoad(env); - } - if (filedescriptorOnLoadCalled == 1) { - netty_unix_filedescriptor_JNI_OnUnLoad(env); - } - if (socketOnLoadCalled == 1) { - netty_unix_socket_JNI_OnUnLoad(env); - } - if (bufferOnLoadCalled == 1) { - netty_unix_buffer_JNI_OnUnLoad(env); - } - if (linuxsocketOnLoadCalled == 1) { - netty_epoll_linuxsocket_JNI_OnUnLoad(env); - } - packetAddrFieldId = NULL; - packetScopeIdFieldId = NULL; - packetPortFieldId = NULL; - packetMemoryAddressFieldId = NULL; - packetCountFieldId = NULL; - - return JNI_ERR; + if (ret == JNI_ERR) { + if (limitsOnLoadCalled == 1) { + netty_unix_limits_JNI_OnUnLoad(env); + } + if (errorsOnLoadCalled == 1) { + netty_unix_errors_JNI_OnUnLoad(env); + } + if (filedescriptorOnLoadCalled == 1) { + netty_unix_filedescriptor_JNI_OnUnLoad(env); + } + if (socketOnLoadCalled == 1) { + netty_unix_socket_JNI_OnUnLoad(env); + } + if (bufferOnLoadCalled == 1) { + netty_unix_buffer_JNI_OnUnLoad(env); + } + if (linuxsocketOnLoadCalled == 1) { + netty_epoll_linuxsocket_JNI_OnUnLoad(env); + } + packetAddrFieldId = NULL; + packetAddrLenFieldId = NULL; + packetScopeIdFieldId = NULL; + packetPortFieldId = NULL; + packetMemoryAddressFieldId = NULL; + packetCountFieldId = NULL; + } + return ret; } static void netty_epoll_native_JNI_OnUnLoad(JNIEnv* env) { @@ -586,6 +709,7 @@ static void netty_epoll_native_JNI_OnUnLoad(JNIEnv* env) { netty_epoll_linuxsocket_JNI_OnUnLoad(env); packetAddrFieldId = NULL; + packetAddrLenFieldId = NULL; packetScopeIdFieldId = NULL; packetPortFieldId = NULL; packetMemoryAddressFieldId = NULL; @@ -616,11 +740,7 @@ static jint JNI_OnLoad_netty_transport_native_epoll0(JavaVM* vm, void* reserved) #endif /* NETTY_BUILD_STATIC */ jint ret = netty_epoll_native_JNI_OnLoad(env, packagePrefix); - if (packagePrefix != NULL) { - free(packagePrefix); - packagePrefix = NULL; - } - + free(packagePrefix); return ret; } diff --git a/transport-native-epoll/src/main/java/io/netty/channel/epoll/AbstractEpollChannel.java b/transport-native-epoll/src/main/java/io/netty/channel/epoll/AbstractEpollChannel.java index 25ae95b..8e05153 100644 --- a/transport-native-epoll/src/main/java/io/netty/channel/epoll/AbstractEpollChannel.java +++ b/transport-native-epoll/src/main/java/io/netty/channel/epoll/AbstractEpollChannel.java @@ -38,7 +38,6 @@ import io.netty.channel.unix.FileDescriptor; import io.netty.channel.unix.Socket; import io.netty.channel.unix.UnixChannel; import io.netty.util.ReferenceCountUtil; -import io.netty.util.internal.ThrowableUtil; import java.io.IOException; import java.net.InetSocketAddress; @@ -57,8 +56,6 @@ import static io.netty.channel.unix.UnixChannelUtil.computeRemoteAddr; import static io.netty.util.internal.ObjectUtil.checkNotNull; abstract class AbstractEpollChannel extends AbstractChannel implements UnixChannel { - private static final ClosedChannelException DO_CLOSE_CLOSED_CHANNEL_EXCEPTION = ThrowableUtil.unknownStackTrace( - new ClosedChannelException(), AbstractEpollChannel.class, "doClose()"); private static final ChannelMetadata METADATA = new ChannelMetadata(false); final LinuxSocket socket; /** @@ -84,24 +81,24 @@ abstract class AbstractEpollChannel extends AbstractChannel implements UnixChann AbstractEpollChannel(Channel parent, LinuxSocket fd, boolean active) { super(parent); - socket = checkNotNull(fd, "fd"); + this.socket = checkNotNull(fd, "fd"); this.active = active; if (active) { // Directly cache the remote and local addresses // See https://github.com/netty/netty/issues/2359 - local = fd.localAddress(); - remote = fd.remoteAddress(); + this.local = fd.localAddress(); + this.remote = fd.remoteAddress(); } } AbstractEpollChannel(Channel parent, LinuxSocket fd, SocketAddress remote) { super(parent); - socket = checkNotNull(fd, "fd"); - active = true; + this.socket = checkNotNull(fd, "fd"); + this.active = true; // Directly cache the remote and local addresses // See https://github.com/netty/netty/issues/2359 this.remote = remote; - local = fd.localAddress(); + this.local = fd.localAddress(); } static boolean isSoErrorZero(Socket fd) { @@ -158,7 +155,7 @@ abstract class AbstractEpollChannel extends AbstractChannel implements UnixChann ChannelPromise promise = connectPromise; if (promise != null) { // Use tryFailure() instead of setFailure() to avoid the race against cancel(). - promise.tryFailure(DO_CLOSE_CLOSED_CHANNEL_EXCEPTION); + promise.tryFailure(new ClosedChannelException()); connectPromise = null; } @@ -194,6 +191,11 @@ abstract class AbstractEpollChannel extends AbstractChannel implements UnixChann } } + void resetCachedAddresses() { + local = socket.localAddress(); + remote = socket.remoteAddress(); + } + @Override protected void doDisconnect() throws Exception { doClose(); @@ -237,6 +239,9 @@ abstract class AbstractEpollChannel extends AbstractChannel implements UnixChann } private static boolean isAllowHalfClosure(ChannelConfig config) { + if (config instanceof EpollDomainSocketChannelConfig) { + return ((EpollDomainSocketChannelConfig) config).isAllowHalfClosure(); + } return config instanceof SocketChannelConfig && ((SocketChannelConfig) config).isAllowHalfClosure(); } @@ -388,7 +393,9 @@ abstract class AbstractEpollChannel extends AbstractChannel implements UnixChann */ abstract void epollInReady(); - final void epollInBefore() { maybeMoreDataToRead = false; } + final void epollInBefore() { + maybeMoreDataToRead = false; + } final void epollInFinally(ChannelConfig config) { maybeMoreDataToRead = allocHandle.maybeMoreDataToRead(); diff --git a/transport-native-epoll/src/main/java/io/netty/channel/epoll/AbstractEpollStreamChannel.java b/transport-native-epoll/src/main/java/io/netty/channel/epoll/AbstractEpollStreamChannel.java index 70208d2..95e31b3 100644 --- a/transport-native-epoll/src/main/java/io/netty/channel/epoll/AbstractEpollStreamChannel.java +++ b/transport-native-epoll/src/main/java/io/netty/channel/epoll/AbstractEpollStreamChannel.java @@ -37,7 +37,6 @@ import io.netty.channel.unix.SocketWritableByteChannel; import io.netty.channel.unix.UnixChannelUtil; import io.netty.util.internal.PlatformDependent; import io.netty.util.internal.StringUtil; -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; @@ -54,6 +53,7 @@ import static io.netty.channel.internal.ChannelUtils.MAX_BYTES_PER_GATHERING_WRI import static io.netty.channel.internal.ChannelUtils.WRITE_STATUS_SNDBUF_FULL; import static io.netty.channel.unix.FileDescriptor.pipe; import static io.netty.util.internal.ObjectUtil.checkNotNull; +import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero; public abstract class AbstractEpollStreamChannel extends AbstractEpollChannel implements DuplexChannel { private static final ChannelMetadata METADATA = new ChannelMetadata(false, 16); @@ -61,15 +61,7 @@ public abstract class AbstractEpollStreamChannel extends AbstractEpollChannel im " (expected: " + StringUtil.simpleClassName(ByteBuf.class) + ", " + StringUtil.simpleClassName(DefaultFileRegion.class) + ')'; private static final InternalLogger logger = InternalLoggerFactory.getInstance(AbstractEpollStreamChannel.class); - private static final ClosedChannelException CLEAR_SPLICE_QUEUE_CLOSED_CHANNEL_EXCEPTION = - ThrowableUtil.unknownStackTrace(new ClosedChannelException(), - AbstractEpollStreamChannel.class, "clearSpliceQueue()"); - private static final ClosedChannelException SPLICE_TO_CLOSED_CHANNEL_EXCEPTION = ThrowableUtil.unknownStackTrace( - new ClosedChannelException(), - AbstractEpollStreamChannel.class, "spliceTo(...)"); - private static final ClosedChannelException FAIL_SPLICE_IF_CLOSED_CLOSED_CHANNEL_EXCEPTION = - ThrowableUtil.unknownStackTrace(new ClosedChannelException(), - AbstractEpollStreamChannel.class, "failSpliceIfClosed(...)"); + private final Runnable flushTask = new Runnable() { @Override public void run() { @@ -78,9 +70,9 @@ public abstract class AbstractEpollStreamChannel extends AbstractEpollChannel im ((AbstractEpollUnsafe) unsafe()).flush0(); } }; - private Queue<SpliceInTask> spliceQueue; // Lazy init these if we need to splice(...) + private volatile Queue<SpliceInTask> spliceQueue; private FileDescriptor pipeIn; private FileDescriptor pipeOut; @@ -163,16 +155,14 @@ public abstract class AbstractEpollStreamChannel extends AbstractEpollChannel im if (ch.eventLoop() != eventLoop()) { throw new IllegalArgumentException("EventLoops are not the same."); } - if (len < 0) { - throw new IllegalArgumentException("len: " + len + " (expected: >= 0)"); - } + checkPositiveOrZero(len, "len"); if (ch.config().getEpollMode() != EpollMode.LEVEL_TRIGGERED || config().getEpollMode() != EpollMode.LEVEL_TRIGGERED) { throw new IllegalStateException("spliceTo() supported only when using " + EpollMode.LEVEL_TRIGGERED); } checkNotNull(promise, "promise"); if (!isOpen()) { - promise.tryFailure(SPLICE_TO_CLOSED_CHANNEL_EXCEPTION); + promise.tryFailure(new ClosedChannelException()); } else { addToSpliceQueue(new SpliceInChannelTask(ch, len, promise)); failSpliceIfClosed(promise); @@ -214,18 +204,14 @@ public abstract class AbstractEpollStreamChannel extends AbstractEpollChannel im */ public final ChannelFuture spliceTo(final FileDescriptor ch, final int offset, final int len, final ChannelPromise promise) { - if (len < 0) { - throw new IllegalArgumentException("len: " + len + " (expected: >= 0)"); - } - if (offset < 0) { - throw new IllegalArgumentException("offset must be >= 0 but was " + offset); - } + checkPositiveOrZero(len, "len"); + checkPositiveOrZero(offset, "offset"); if (config().getEpollMode() != EpollMode.LEVEL_TRIGGERED) { throw new IllegalStateException("spliceTo() supported only when using " + EpollMode.LEVEL_TRIGGERED); } checkNotNull(promise, "promise"); if (!isOpen()) { - promise.tryFailure(SPLICE_TO_CLOSED_CHANNEL_EXCEPTION); + promise.tryFailure(new ClosedChannelException()); } else { addToSpliceQueue(new SpliceFdTask(ch, offset, len, promise)); failSpliceIfClosed(promise); @@ -237,7 +223,7 @@ public abstract class AbstractEpollStreamChannel extends AbstractEpollChannel im if (!isOpen()) { // Seems like the Channel was closed in the meantime try to fail the promise to prevent any // cases where a future may not be notified otherwise. - if (promise.tryFailure(FAIL_SPLICE_IF_CLOSED_CLOSED_CHANNEL_EXCEPTION)) { + if (promise.tryFailure(new ClosedChannelException())) { eventLoop().execute(new Runnable() { @Override public void run() { @@ -372,13 +358,13 @@ public abstract class AbstractEpollStreamChannel extends AbstractEpollChannel im * </ul> */ private int writeDefaultFileRegion(ChannelOutboundBuffer in, DefaultFileRegion region) throws Exception { + final long offset = region.transferred(); final long regionCount = region.count(); - if (region.transferred() >= regionCount) { + if (offset >= regionCount) { in.remove(); return 0; } - final long offset = region.transferred(); final long flushedAmount = socket.sendFile(region, region.position(), offset, regionCount - offset); if (flushedAmount > 0) { in.progress(flushedAmount); @@ -386,6 +372,8 @@ public abstract class AbstractEpollStreamChannel extends AbstractEpollChannel im in.remove(); } return 1; + } else if (flushedAmount == 0) { + validateFileRegion(region, offset); } return WRITE_STATUS_SNDBUF_FULL; } @@ -690,15 +678,21 @@ public abstract class AbstractEpollStreamChannel extends AbstractEpollChannel im } private void clearSpliceQueue() { - if (spliceQueue == null) { + Queue<SpliceInTask> sQueue = spliceQueue; + if (sQueue == null) { return; } + ClosedChannelException exception = null; + for (;;) { - SpliceInTask task = spliceQueue.poll(); + SpliceInTask task = sQueue.poll(); if (task == null) { break; } - task.promise.tryFailure(CLEAR_SPLICE_QUEUE_CLOSED_CHANNEL_EXCEPTION); + if (exception == null) { + exception = new ClosedChannelException(); + } + task.promise.tryFailure(exception); } } @@ -707,9 +701,7 @@ public abstract class AbstractEpollStreamChannel extends AbstractEpollChannel im try { fd.close(); } catch (IOException e) { - if (logger.isWarnEnabled()) { - logger.warn("Error while closing a pipe", e); - } + logger.warn("Error while closing a pipe", e); } } } @@ -762,15 +754,16 @@ public abstract class AbstractEpollStreamChannel extends AbstractEpollChannel im ByteBuf byteBuf = null; boolean close = false; try { + Queue<SpliceInTask> sQueue = null; do { - if (spliceQueue != null) { - SpliceInTask spliceTask = spliceQueue.peek(); + if (sQueue != null || (sQueue = spliceQueue) != null) { + SpliceInTask spliceTask = sQueue.peek(); if (spliceTask != null) { if (spliceTask.spliceIn(allocHandle)) { // We need to check if it is still active as if not we removed all SpliceTasks in // doClose(...) if (isActive()) { - spliceQueue.remove(); + sQueue.remove(); } continue; } else { @@ -830,24 +823,16 @@ public abstract class AbstractEpollStreamChannel extends AbstractEpollChannel im } private void addToSpliceQueue(final SpliceInTask task) { - EventLoop eventLoop = eventLoop(); - if (eventLoop.inEventLoop()) { - addToSpliceQueue0(task); - } else { - eventLoop.execute(new Runnable() { - @Override - public void run() { - addToSpliceQueue0(task); + Queue<SpliceInTask> sQueue = spliceQueue; + if (sQueue == null) { + synchronized (this) { + sQueue = spliceQueue; + if (sQueue == null) { + spliceQueue = sQueue = PlatformDependent.newMpscQueue(); } - }); - } - } - - private void addToSpliceQueue0(SpliceInTask task) { - if (spliceQueue == null) { - spliceQueue = PlatformDependent.newMpscQueue(); + } } - spliceQueue.add(task); + sQueue.add(task); } protected abstract class SpliceInTask { @@ -990,7 +975,7 @@ public abstract class AbstractEpollStreamChannel extends AbstractEpollChannel im private final class SpliceFdTask extends SpliceInTask { private final FileDescriptor fd; private final ChannelPromise promise; - private final int offset; + private int offset; SpliceFdTask(FileDescriptor fd, int offset, int len, ChannelPromise promise) { super(len, promise); @@ -1020,6 +1005,7 @@ public abstract class AbstractEpollStreamChannel extends AbstractEpollChannel im } do { int splicedOut = Native.splice(pipeIn.intValue(), -1, fd.intValue(), offset, splicedIn); + offset += splicedOut; splicedIn -= splicedOut; } while (splicedIn > 0); if (len == 0) { diff --git a/transport-native-epoll/src/main/java/io/netty/channel/epoll/Epoll.java b/transport-native-epoll/src/main/java/io/netty/channel/epoll/Epoll.java index e4ecf42..f2b0231 100644 --- a/transport-native-epoll/src/main/java/io/netty/channel/epoll/Epoll.java +++ b/transport-native-epoll/src/main/java/io/netty/channel/epoll/Epoll.java @@ -19,13 +19,14 @@ import io.netty.channel.unix.FileDescriptor; import io.netty.util.internal.SystemPropertyUtil; /** - * Tells if <a href="http://netty.io/wiki/native-transports.html">{@code netty-transport-native-epoll}</a> is supported. + * Tells if <a href="https://netty.io/wiki/native-transports.html">{@code netty-transport-native-epoll}</a> is + * supported. */ public final class Epoll { private static final Throwable UNAVAILABILITY_CAUSE; - static { + static { Throwable cause = null; if (SystemPropertyUtil.getBoolean("io.netty.transport.noNative", false)) { @@ -61,15 +62,15 @@ public final class Epoll { } /** - * Returns {@code true} if and only if the - * <a href="http://netty.io/wiki/native-transports.html">{@code netty-transport-native-epoll}</a> is available. + * Returns {@code true} if and only if the <a href="https://netty.io/wiki/native-transports.html">{@code + * netty-transport-native-epoll}</a> is available. */ public static boolean isAvailable() { return UNAVAILABILITY_CAUSE == null; } /** - * Ensure that <a href="http://netty.io/wiki/native-transports.html">{@code netty-transport-native-epoll}</a> is + * Ensure that <a href="https://netty.io/wiki/native-transports.html">{@code netty-transport-native-epoll}</a> is * available. * * @throws UnsatisfiedLinkError if unavailable @@ -82,8 +83,8 @@ public final class Epoll { } /** - * Returns the cause of unavailability of - * <a href="http://netty.io/wiki/native-transports.html">{@code netty-transport-native-epoll}</a>. + * Returns the cause of unavailability of <a href="https://netty.io/wiki/native-transports.html"> + * {@code netty-transport-native-epoll}</a>. * * @return the cause if unavailable. {@code null} if available. */ @@ -91,5 +92,6 @@ public final class Epoll { return UNAVAILABILITY_CAUSE; } - private Epoll() { } + private Epoll() { + } } diff --git a/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollChannelConfig.java b/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollChannelConfig.java index 2d2610c..c3c4dfd 100644 --- a/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollChannelConfig.java +++ b/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollChannelConfig.java @@ -22,6 +22,7 @@ import io.netty.channel.DefaultChannelConfig; import io.netty.channel.MessageSizeEstimator; import io.netty.channel.RecvByteBufAllocator; import io.netty.channel.WriteBufferWaterMark; +import io.netty.util.internal.ObjectUtil; import java.io.IOException; import java.util.Map; @@ -147,9 +148,8 @@ public class EpollChannelConfig extends DefaultChannelConfig { * <strong>Be aware this config setting can only be adjusted before the channel was registered.</strong> */ public EpollChannelConfig setEpollMode(EpollMode mode) { - if (mode == null) { - throw new NullPointerException("mode"); - } + ObjectUtil.checkNotNull(mode, "mode"); + try { switch (mode) { case EDGE_TRIGGERED: diff --git a/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollChannelOption.java b/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollChannelOption.java index 1f5127c..765eea2 100644 --- a/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollChannelOption.java +++ b/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollChannelOption.java @@ -45,6 +45,8 @@ public final class EpollChannelOption<T> extends UnixChannelOption<T> { public static final ChannelOption<Map<InetAddress, byte[]>> TCP_MD5SIG = valueOf("TCP_MD5SIG"); + public static final ChannelOption<Integer> MAX_DATAGRAM_PAYLOAD_SIZE = valueOf("MAX_DATAGRAM_PAYLOAD_SIZE"); + @SuppressWarnings({ "unused", "deprecation" }) private EpollChannelOption() { } diff --git a/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollDatagramChannel.java b/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollDatagramChannel.java index 714f612..8e28501 100644 --- a/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollDatagramChannel.java +++ b/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollDatagramChannel.java @@ -17,6 +17,7 @@ package io.netty.channel.epoll; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.Unpooled; import io.netty.channel.AddressedEnvelope; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelMetadata; @@ -27,17 +28,25 @@ import io.netty.channel.DefaultAddressedEnvelope; import io.netty.channel.socket.DatagramChannel; import io.netty.channel.socket.DatagramChannelConfig; import io.netty.channel.socket.DatagramPacket; +import io.netty.channel.socket.InternetProtocolFamily; import io.netty.channel.unix.DatagramSocketAddress; +import io.netty.channel.unix.Errors; +import io.netty.channel.unix.Errors.NativeIoException; import io.netty.channel.unix.IovArray; +import io.netty.channel.unix.Socket; import io.netty.channel.unix.UnixChannelUtil; +import io.netty.util.ReferenceCountUtil; +import io.netty.util.internal.ObjectUtil; +import io.netty.util.internal.RecyclableArrayList; import io.netty.util.internal.StringUtil; import java.io.IOException; +import java.net.Inet4Address; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.NetworkInterface; +import java.net.PortUnreachableException; import java.net.SocketAddress; -import java.net.SocketException; import java.nio.ByteBuffer; import static io.netty.channel.epoll.LinuxSocket.newSocketDgram; @@ -58,17 +67,34 @@ public final class EpollDatagramChannel extends AbstractEpollChannel implements private final EpollDatagramChannelConfig config; private volatile boolean connected; + /** + * Create a new instance which selects the {@link InternetProtocolFamily} to use depending + * on the Operation Systems default which will be chosen. + */ public EpollDatagramChannel() { - super(newSocketDgram()); - config = new EpollDatagramChannelConfig(this); + this(null); + } + + /** + * Create a new instance using the given {@link InternetProtocolFamily}. If {@code null} is used it will depend + * on the Operation Systems default which will be chosen. + */ + public EpollDatagramChannel(InternetProtocolFamily family) { + this(family == null ? + newSocketDgram(Socket.isIPv6Preferred()) : newSocketDgram(family == InternetProtocolFamily.IPv6), + false); } + /** + * Create a new instance which selects the {@link InternetProtocolFamily} to use depending + * on the Operation Systems default which will be chosen. + */ public EpollDatagramChannel(int fd) { - this(new LinuxSocket(fd)); + this(new LinuxSocket(fd), true); } - EpollDatagramChannel(LinuxSocket fd) { - super(null, fd, true); + private EpollDatagramChannel(LinuxSocket fd, boolean active) { + super(null, fd, active); config = new EpollDatagramChannelConfig(this); } @@ -109,7 +135,7 @@ public final class EpollDatagramChannel extends AbstractEpollChannel implements return joinGroup( multicastAddress, NetworkInterface.getByInetAddress(localAddress().getAddress()), null, promise); - } catch (SocketException e) { + } catch (IOException e) { promise.setFailure(e); } return promise; @@ -139,15 +165,15 @@ public final class EpollDatagramChannel extends AbstractEpollChannel implements final InetAddress multicastAddress, final NetworkInterface networkInterface, final InetAddress source, final ChannelPromise promise) { - if (multicastAddress == null) { - throw new NullPointerException("multicastAddress"); - } + ObjectUtil.checkNotNull(multicastAddress, "multicastAddress"); + ObjectUtil.checkNotNull(networkInterface, "networkInterface"); - if (networkInterface == null) { - throw new NullPointerException("networkInterface"); + try { + socket.joinGroup(multicastAddress, networkInterface, source); + promise.setSuccess(); + } catch (IOException e) { + promise.setFailure(e); } - - promise.setFailure(new UnsupportedOperationException("Multicast not supported")); return promise; } @@ -161,7 +187,7 @@ public final class EpollDatagramChannel extends AbstractEpollChannel implements try { return leaveGroup( multicastAddress, NetworkInterface.getByInetAddress(localAddress().getAddress()), null, promise); - } catch (SocketException e) { + } catch (IOException e) { promise.setFailure(e); } return promise; @@ -190,15 +216,15 @@ public final class EpollDatagramChannel extends AbstractEpollChannel implements public ChannelFuture leaveGroup( final InetAddress multicastAddress, final NetworkInterface networkInterface, final InetAddress source, final ChannelPromise promise) { - if (multicastAddress == null) { - throw new NullPointerException("multicastAddress"); - } - if (networkInterface == null) { - throw new NullPointerException("networkInterface"); - } - - promise.setFailure(new UnsupportedOperationException("Multicast not supported")); + ObjectUtil.checkNotNull(multicastAddress, "multicastAddress"); + ObjectUtil.checkNotNull(networkInterface, "networkInterface"); + try { + socket.leaveGroup(multicastAddress, networkInterface, source); + promise.setSuccess(); + } catch (IOException e) { + promise.setFailure(e); + } return promise; } @@ -213,16 +239,10 @@ public final class EpollDatagramChannel extends AbstractEpollChannel implements public ChannelFuture block( final InetAddress multicastAddress, final NetworkInterface networkInterface, final InetAddress sourceToBlock, final ChannelPromise promise) { - if (multicastAddress == null) { - throw new NullPointerException("multicastAddress"); - } - if (sourceToBlock == null) { - throw new NullPointerException("sourceToBlock"); - } + ObjectUtil.checkNotNull(multicastAddress, "multicastAddress"); + ObjectUtil.checkNotNull(sourceToBlock, "sourceToBlock"); + ObjectUtil.checkNotNull(networkInterface, "networkInterface"); - if (networkInterface == null) { - throw new NullPointerException("networkInterface"); - } promise.setFailure(new UnsupportedOperationException("Multicast not supported")); return promise; } @@ -253,6 +273,13 @@ public final class EpollDatagramChannel extends AbstractEpollChannel implements @Override protected void doBind(SocketAddress localAddress) throws Exception { + if (localAddress instanceof InetSocketAddress) { + InetSocketAddress socketAddress = (InetSocketAddress) localAddress; + if (socketAddress.getAddress().isAnyLocalAddress() && + socketAddress.getAddress() instanceof Inet4Address && Socket.isIPv6Preferred()) { + localAddress = new InetSocketAddress(LinuxSocket.INET6_ANY, socketAddress.getPort()); + } + } super.doBind(localAddress); active = true; } @@ -270,8 +297,8 @@ public final class EpollDatagramChannel extends AbstractEpollChannel implements try { // Check if sendmmsg(...) is supported which is only the case for GLIBC 2.14+ if (Native.IS_SUPPORTING_SENDMMSG && in.size() > 1) { - NativeDatagramPacketArray array = ((EpollEventLoop) eventLoop()).cleanDatagramPacketArray(); - in.forEachFlushedMessage(array); + NativeDatagramPacketArray array = cleanDatagramPacketArray(); + array.add(in, isConnected()); int cnt = array.count(); if (cnt >= 1) { @@ -280,7 +307,7 @@ public final class EpollDatagramChannel extends AbstractEpollChannel implements NativeDatagramPacketArray.NativeDatagramPacket[] packets = array.packets(); while (cnt > 0) { - int send = Native.sendmmsg(socket.intValue(), packets, offset, cnt); + int send = socket.sendmmsg(packets, offset, cnt); if (send == 0) { // Did not write all messages. setFlag(Native.EPOLLOUT); @@ -349,7 +376,7 @@ public final class EpollDatagramChannel extends AbstractEpollChannel implements } } else if (data.nioBufferCount() > 1) { IovArray array = ((EpollEventLoop) eventLoop()).cleanIovArray(); - array.add(data); + array.add(data, data.readerIndex(), data.readableBytes()); int cnt = array.count(); assert cnt != 0; @@ -412,6 +439,7 @@ public final class EpollDatagramChannel extends AbstractEpollChannel implements protected void doDisconnect() throws Exception { socket.disconnect(); connected = active = false; + resetCachedAddresses(); } @Override @@ -449,47 +477,43 @@ public final class EpollDatagramChannel extends AbstractEpollChannel implements Throwable exception = null; try { - ByteBuf data = null; try { + boolean connected = isConnected(); do { - data = allocHandle.allocate(allocator); - allocHandle.attemptedBytesRead(data.writableBytes()); - final DatagramSocketAddress remoteAddress; - if (data.hasMemoryAddress()) { - // has a memory address so use optimized call - remoteAddress = socket.recvFromAddress(data.memoryAddress(), data.writerIndex(), - data.capacity()); - } else { - ByteBuffer nioData = data.internalNioBuffer(data.writerIndex(), data.writableBytes()); - remoteAddress = socket.recvFrom(nioData, nioData.position(), nioData.limit()); + ByteBuf byteBuf = allocHandle.allocate(allocator); + final boolean read; + int datagramSize = config().getMaxDatagramPayloadSize(); + + // Only try to use recvmmsg if its really supported by the running system. + int numDatagram = Native.IS_SUPPORTING_RECVMMSG ? + datagramSize == 0 ? 1 : byteBuf.writableBytes() / datagramSize : + 0; + + try { + if (numDatagram <= 1) { + if (connected) { + read = connectedRead(allocHandle, byteBuf, datagramSize); + } else { + read = read(allocHandle, byteBuf, datagramSize); + } + } else { + // Try to use scattering reads via recvmmsg(...) syscall. + read = scatteringRead(allocHandle, byteBuf, datagramSize, numDatagram); + } + } catch (NativeIoException e) { + if (connected) { + throw translateForConnected(e); + } + throw e; } - if (remoteAddress == null) { - allocHandle.lastBytesRead(-1); - data.release(); - data = null; + if (read) { + readPending = false; + } else { break; } - - InetSocketAddress localAddress = remoteAddress.localAddress(); - if (localAddress == null) { - localAddress = (InetSocketAddress) localAddress(); - } - - allocHandle.incMessagesRead(1); - allocHandle.lastBytesRead(remoteAddress.receivedAmount()); - data.writerIndex(data.writerIndex() + allocHandle.lastBytesRead()); - - readPending = false; - pipeline.fireChannelRead( - new DatagramPacket(data, localAddress, remoteAddress)); - - data = null; } while (allocHandle.continueReading()); } catch (Throwable t) { - if (data != null) { - data.release(); - } exception = t; } @@ -504,4 +528,165 @@ public final class EpollDatagramChannel extends AbstractEpollChannel implements } } } + + private boolean connectedRead(EpollRecvByteAllocatorHandle allocHandle, ByteBuf byteBuf, int maxDatagramPacketSize) + throws Exception { + try { + int writable = maxDatagramPacketSize != 0 ? Math.min(byteBuf.writableBytes(), maxDatagramPacketSize) + : byteBuf.writableBytes(); + allocHandle.attemptedBytesRead(writable); + + int writerIndex = byteBuf.writerIndex(); + int localReadAmount; + if (byteBuf.hasMemoryAddress()) { + localReadAmount = socket.readAddress(byteBuf.memoryAddress(), writerIndex, writerIndex + writable); + } else { + ByteBuffer buf = byteBuf.internalNioBuffer(writerIndex, writable); + localReadAmount = socket.read(buf, buf.position(), buf.limit()); + } + + if (localReadAmount <= 0) { + allocHandle.lastBytesRead(localReadAmount); + + // nothing was read, release the buffer. + return false; + } + byteBuf.writerIndex(writerIndex + localReadAmount); + + allocHandle.lastBytesRead(maxDatagramPacketSize <= 0 ? + localReadAmount : writable); + + DatagramPacket packet = new DatagramPacket(byteBuf, localAddress(), remoteAddress()); + allocHandle.incMessagesRead(1); + + pipeline().fireChannelRead(packet); + byteBuf = null; + return true; + } finally { + if (byteBuf != null) { + byteBuf.release(); + } + } + } + + private IOException translateForConnected(NativeIoException e) { + // We need to correctly translate connect errors to match NIO behaviour. + if (e.expectedErr() == Errors.ERROR_ECONNREFUSED_NEGATIVE) { + PortUnreachableException error = new PortUnreachableException(e.getMessage()); + error.initCause(e); + return error; + } + return e; + } + + private boolean scatteringRead(EpollRecvByteAllocatorHandle allocHandle, + ByteBuf byteBuf, int datagramSize, int numDatagram) throws IOException { + RecyclableArrayList bufferPackets = null; + try { + int offset = byteBuf.writerIndex(); + NativeDatagramPacketArray array = cleanDatagramPacketArray(); + + for (int i = 0; i < numDatagram; i++, offset += datagramSize) { + if (!array.addWritable(byteBuf, offset, datagramSize)) { + break; + } + } + + allocHandle.attemptedBytesRead(offset - byteBuf.writerIndex()); + + NativeDatagramPacketArray.NativeDatagramPacket[] packets = array.packets(); + + int received = socket.recvmmsg(packets, 0, array.count()); + if (received == 0) { + allocHandle.lastBytesRead(-1); + return false; + } + int bytesReceived = received * datagramSize; + byteBuf.writerIndex(bytesReceived); + InetSocketAddress local = localAddress(); + if (received == 1) { + // Single packet fast-path + DatagramPacket packet = packets[0].newDatagramPacket(byteBuf, local); + allocHandle.lastBytesRead(datagramSize); + allocHandle.incMessagesRead(1); + pipeline().fireChannelRead(packet); + byteBuf = null; + return true; + } + + // Its important that we process all received data out of the NativeDatagramPacketArray + // before we call fireChannelRead(...). This is because the user may call flush() + // in a channelRead(...) method and so may re-use the NativeDatagramPacketArray again. + bufferPackets = RecyclableArrayList.newInstance(); + for (int i = 0; i < received; i++) { + DatagramPacket packet = packets[i].newDatagramPacket(byteBuf.readRetainedSlice(datagramSize), local); + bufferPackets.add(packet); + } + + allocHandle.lastBytesRead(bytesReceived); + allocHandle.incMessagesRead(received); + + for (int i = 0; i < received; i++) { + pipeline().fireChannelRead(bufferPackets.set(i, Unpooled.EMPTY_BUFFER)); + } + bufferPackets.recycle(); + bufferPackets = null; + return true; + } finally { + if (byteBuf != null) { + byteBuf.release(); + } + if (bufferPackets != null) { + for (int i = 0; i < bufferPackets.size(); i++) { + ReferenceCountUtil.release(bufferPackets.get(i)); + } + bufferPackets.recycle(); + } + } + } + + private boolean read(EpollRecvByteAllocatorHandle allocHandle, ByteBuf byteBuf, int maxDatagramPacketSize) + throws IOException { + try { + int writable = maxDatagramPacketSize != 0 ? Math.min(byteBuf.writableBytes(), maxDatagramPacketSize) + : byteBuf.writableBytes(); + allocHandle.attemptedBytesRead(writable); + int writerIndex = byteBuf.writerIndex(); + final DatagramSocketAddress remoteAddress; + if (byteBuf.hasMemoryAddress()) { + // has a memory address so use optimized call + remoteAddress = socket.recvFromAddress( + byteBuf.memoryAddress(), writerIndex, writerIndex + writable); + } else { + ByteBuffer nioData = byteBuf.internalNioBuffer(writerIndex, writable); + remoteAddress = socket.recvFrom(nioData, nioData.position(), nioData.limit()); + } + + if (remoteAddress == null) { + allocHandle.lastBytesRead(-1); + return false; + } + InetSocketAddress localAddress = remoteAddress.localAddress(); + if (localAddress == null) { + localAddress = localAddress(); + } + int received = remoteAddress.receivedAmount(); + allocHandle.lastBytesRead(maxDatagramPacketSize <= 0 ? + received : writable); + byteBuf.writerIndex(writerIndex + received); + allocHandle.incMessagesRead(1); + + pipeline().fireChannelRead(new DatagramPacket(byteBuf, localAddress, remoteAddress)); + byteBuf = null; + return true; + } finally { + if (byteBuf != null) { + byteBuf.release(); + } + } + } + + private NativeDatagramPacketArray cleanDatagramPacketArray() { + return ((EpollEventLoop) eventLoop()).cleanDatagramPacketArray(); + } } diff --git a/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollDatagramChannelConfig.java b/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollDatagramChannelConfig.java index 778b555..e97d2c5 100644 --- a/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollDatagramChannelConfig.java +++ b/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollDatagramChannelConfig.java @@ -15,6 +15,7 @@ */ package io.netty.channel.epoll; +import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.channel.ChannelException; import io.netty.channel.ChannelOption; @@ -23,6 +24,7 @@ import io.netty.channel.MessageSizeEstimator; import io.netty.channel.RecvByteBufAllocator; import io.netty.channel.WriteBufferWaterMark; import io.netty.channel.socket.DatagramChannelConfig; +import io.netty.util.internal.ObjectUtil; import java.io.IOException; import java.net.InetAddress; @@ -32,6 +34,7 @@ import java.util.Map; public final class EpollDatagramChannelConfig extends EpollChannelConfig implements DatagramChannelConfig { private static final RecvByteBufAllocator DEFAULT_RCVBUF_ALLOCATOR = new FixedRecvByteBufAllocator(2048); private boolean activeOnOpen; + private volatile int maxDatagramSize; EpollDatagramChannelConfig(EpollDatagramChannel channel) { super(channel); @@ -48,7 +51,7 @@ public final class EpollDatagramChannelConfig extends EpollChannelConfig impleme ChannelOption.IP_MULTICAST_ADDR, ChannelOption.IP_MULTICAST_IF, ChannelOption.IP_MULTICAST_TTL, ChannelOption.IP_TOS, ChannelOption.DATAGRAM_CHANNEL_ACTIVE_ON_REGISTRATION, EpollChannelOption.SO_REUSEPORT, EpollChannelOption.IP_FREEBIND, EpollChannelOption.IP_TRANSPARENT, - EpollChannelOption.IP_RECVORIGDSTADDR); + EpollChannelOption.IP_RECVORIGDSTADDR, EpollChannelOption.MAX_DATAGRAM_PAYLOAD_SIZE); } @SuppressWarnings({ "unchecked", "deprecation" }) @@ -96,6 +99,9 @@ public final class EpollDatagramChannelConfig extends EpollChannelConfig impleme if (option == EpollChannelOption.IP_RECVORIGDSTADDR) { return (T) Boolean.valueOf(isIpRecvOrigDestAddr()); } + if (option == EpollChannelOption.MAX_DATAGRAM_PAYLOAD_SIZE) { + return (T) Integer.valueOf(getMaxDatagramPayloadSize()); + } return super.getOption(option); } @@ -132,6 +138,8 @@ public final class EpollDatagramChannelConfig extends EpollChannelConfig impleme setIpTransparent((Boolean) value); } else if (option == EpollChannelOption.IP_RECVORIGDSTADDR) { setIpRecvOrigDestAddr((Boolean) value); + } else if (option == EpollChannelOption.MAX_DATAGRAM_PAYLOAD_SIZE) { + setMaxDatagramPayloadSize((Integer) value); } else { return super.setOption(option, value); } @@ -316,42 +324,79 @@ public final class EpollDatagramChannelConfig extends EpollChannelConfig impleme @Override public boolean isLoopbackModeDisabled() { - return false; + try { + return ((EpollDatagramChannel) channel).socket.isLoopbackModeDisabled(); + } catch (IOException e) { + throw new ChannelException(e); + } } @Override public DatagramChannelConfig setLoopbackModeDisabled(boolean loopbackModeDisabled) { - throw new UnsupportedOperationException("Multicast not supported"); + try { + ((EpollDatagramChannel) channel).socket.setLoopbackModeDisabled(loopbackModeDisabled); + return this; + } catch (IOException e) { + throw new ChannelException(e); + } } @Override public int getTimeToLive() { - return -1; + try { + return ((EpollDatagramChannel) channel).socket.getTimeToLive(); + } catch (IOException e) { + throw new ChannelException(e); + } } @Override public EpollDatagramChannelConfig setTimeToLive(int ttl) { - throw new UnsupportedOperationException("Multicast not supported"); + try { + ((EpollDatagramChannel) channel).socket.setTimeToLive(ttl); + return this; + } catch (IOException e) { + throw new ChannelException(e); + } } @Override public InetAddress getInterface() { - return null; + try { + return ((EpollDatagramChannel) channel).socket.getInterface(); + } catch (IOException e) { + throw new ChannelException(e); + } } @Override public EpollDatagramChannelConfig setInterface(InetAddress interfaceAddress) { - throw new UnsupportedOperationException("Multicast not supported"); + try { + ((EpollDatagramChannel) channel).socket.setInterface(interfaceAddress); + return this; + } catch (IOException e) { + throw new ChannelException(e); + } } @Override public NetworkInterface getNetworkInterface() { - return null; + try { + return ((EpollDatagramChannel) channel).socket.getNetworkInterface(); + } catch (IOException e) { + throw new ChannelException(e); + } } @Override public EpollDatagramChannelConfig setNetworkInterface(NetworkInterface networkInterface) { - throw new UnsupportedOperationException("Multicast not supported"); + try { + EpollDatagramChannel datagramChannel = (EpollDatagramChannel) channel; + datagramChannel.socket.setNetworkInterface(networkInterface); + return this; + } catch (IOException e) { + throw new ChannelException(e); + } } @Override @@ -462,4 +507,23 @@ public final class EpollDatagramChannelConfig extends EpollChannelConfig impleme } } + /** + * Set the maximum {@link io.netty.channel.socket.DatagramPacket} size. This will be used to determine if + * {@code recvmmsg} should be used when reading from the underlying socket. When {@code recvmmsg} is used + * we may be able to read multiple {@link io.netty.channel.socket.DatagramPacket}s with one syscall and so + * greatly improve the performance. This number will be used to slice {@link ByteBuf}s returned by the used + * {@link RecvByteBufAllocator}. You can use {@code 0} to disable the usage of recvmmsg, any other bigger value + * will enable it. + */ + public EpollDatagramChannelConfig setMaxDatagramPayloadSize(int maxDatagramSize) { + this.maxDatagramSize = ObjectUtil.checkPositiveOrZero(maxDatagramSize, "maxDatagramSize"); + return this; + } + + /** + * Get the maximum {@link io.netty.channel.socket.DatagramPacket} size. + */ + public int getMaxDatagramPayloadSize() { + return maxDatagramSize; + } } diff --git a/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollDomainSocketChannelConfig.java b/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollDomainSocketChannelConfig.java index ea6c532..22321a4 100644 --- a/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollDomainSocketChannelConfig.java +++ b/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollDomainSocketChannelConfig.java @@ -20,14 +20,23 @@ import io.netty.channel.ChannelOption; import io.netty.channel.MessageSizeEstimator; import io.netty.channel.RecvByteBufAllocator; import io.netty.channel.WriteBufferWaterMark; +import io.netty.channel.socket.SocketChannelConfig; import io.netty.channel.unix.DomainSocketChannelConfig; import io.netty.channel.unix.DomainSocketReadMode; +import io.netty.util.internal.ObjectUtil; +import java.io.IOException; import java.util.Map; +import static io.netty.channel.ChannelOption.ALLOW_HALF_CLOSURE; +import static io.netty.channel.ChannelOption.SO_RCVBUF; +import static io.netty.channel.ChannelOption.SO_SNDBUF; +import static io.netty.channel.unix.UnixChannelOption.DOMAIN_SOCKET_READ_MODE; + public final class EpollDomainSocketChannelConfig extends EpollChannelConfig implements DomainSocketChannelConfig { private volatile DomainSocketReadMode mode = DomainSocketReadMode.BYTES; + private volatile boolean allowHalfClosure; EpollDomainSocketChannelConfig(AbstractEpollChannel channel) { super(channel); @@ -35,15 +44,24 @@ public final class EpollDomainSocketChannelConfig extends EpollChannelConfig @Override public Map<ChannelOption<?>, Object> getOptions() { - return getOptions(super.getOptions(), EpollChannelOption.DOMAIN_SOCKET_READ_MODE); + return getOptions(super.getOptions(), DOMAIN_SOCKET_READ_MODE, ALLOW_HALF_CLOSURE, SO_SNDBUF, SO_RCVBUF); } @SuppressWarnings("unchecked") @Override public <T> T getOption(ChannelOption<T> option) { - if (option == EpollChannelOption.DOMAIN_SOCKET_READ_MODE) { + if (option == DOMAIN_SOCKET_READ_MODE) { return (T) getReadMode(); } + if (option == ALLOW_HALF_CLOSURE) { + return (T) Boolean.valueOf(isAllowHalfClosure()); + } + if (option == SO_SNDBUF) { + return (T) Integer.valueOf(getSendBufferSize()); + } + if (option == SO_RCVBUF) { + return (T) Integer.valueOf(getReceiveBufferSize()); + } return super.getOption(option); } @@ -51,8 +69,14 @@ public final class EpollDomainSocketChannelConfig extends EpollChannelConfig public <T> boolean setOption(ChannelOption<T> option, T value) { validate(option, value); - if (option == EpollChannelOption.DOMAIN_SOCKET_READ_MODE) { + if (option == DOMAIN_SOCKET_READ_MODE) { setReadMode((DomainSocketReadMode) value); + } else if (option == ALLOW_HALF_CLOSURE) { + setAllowHalfClosure((Boolean) value); + } else if (option == SO_SNDBUF) { + setSendBufferSize((Integer) value); + } else if (option == SO_RCVBUF) { + setReceiveBufferSize((Integer) value); } else { return super.setOption(option, value); } @@ -137,10 +161,7 @@ public final class EpollDomainSocketChannelConfig extends EpollChannelConfig @Override public EpollDomainSocketChannelConfig setReadMode(DomainSocketReadMode mode) { - if (mode == null) { - throw new NullPointerException("mode"); - } - this.mode = mode; + this.mode = ObjectUtil.checkNotNull(mode, "mode"); return this; } @@ -148,4 +169,53 @@ public final class EpollDomainSocketChannelConfig extends EpollChannelConfig public DomainSocketReadMode getReadMode() { return mode; } + + /** + * @see SocketChannelConfig#isAllowHalfClosure() + */ + public boolean isAllowHalfClosure() { + return allowHalfClosure; + } + + /** + * @see SocketChannelConfig#setAllowHalfClosure(boolean) + */ + public EpollDomainSocketChannelConfig setAllowHalfClosure(boolean allowHalfClosure) { + this.allowHalfClosure = allowHalfClosure; + return this; + } + + public int getSendBufferSize() { + try { + return ((EpollDomainSocketChannel) channel).socket.getSendBufferSize(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public EpollDomainSocketChannelConfig setSendBufferSize(int sendBufferSize) { + try { + ((EpollDomainSocketChannel) channel).socket.setSendBufferSize(sendBufferSize); + return this; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public int getReceiveBufferSize() { + try { + return ((EpollDomainSocketChannel) channel).socket.getReceiveBufferSize(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public EpollDomainSocketChannelConfig setReceiveBufferSize(int receiveBufferSize) { + try { + ((EpollDomainSocketChannel) channel).socket.setReceiveBufferSize(receiveBufferSize); + return this; + } catch (IOException e) { + throw new RuntimeException(e); + } + } } diff --git a/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollEventLoop.java b/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollEventLoop.java index 3420d67..526276c 100644 --- a/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollEventLoop.java +++ b/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollEventLoop.java @@ -17,6 +17,7 @@ package io.netty.channel.epoll; import io.netty.channel.EventLoop; import io.netty.channel.EventLoopGroup; +import io.netty.channel.EventLoopTaskQueueFactory; import io.netty.channel.SelectStrategy; import io.netty.channel.SingleThreadEventLoop; import io.netty.channel.epoll.AbstractEpollChannel.AbstractEpollUnsafe; @@ -34,7 +35,7 @@ import io.netty.util.internal.logging.InternalLoggerFactory; import java.io.IOException; import java.util.Queue; import java.util.concurrent.Executor; -import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; +import java.util.concurrent.atomic.AtomicLong; import static java.lang.Math.min; @@ -43,8 +44,6 @@ import static java.lang.Math.min; */ class EpollEventLoop extends SingleThreadEventLoop { private static final InternalLogger logger = InternalLoggerFactory.getInstance(EpollEventLoop.class); - private static final AtomicIntegerFieldUpdater<EpollEventLoop> WAKEN_UP_UPDATER = - AtomicIntegerFieldUpdater.newUpdater(EpollEventLoop.class, "wakenUp"); static { // Ensure JNI is initialized by the time this class is loaded by this time! @@ -52,8 +51,6 @@ class EpollEventLoop extends SingleThreadEventLoop { Epoll.ensureAvailability(); } - // Pick a number that no task could have previously used. - private long prevDeadlineNanos = nanoTime() - 1; private final FileDescriptor epollFd; private final FileDescriptor eventFd; private final FileDescriptor timerFd; @@ -72,16 +69,26 @@ class EpollEventLoop extends SingleThreadEventLoop { return epollWaitNow(); } }; - @SuppressWarnings("unused") // AtomicIntegerFieldUpdater - private volatile int wakenUp; + + private static final long AWAKE = -1L; + private static final long NONE = Long.MAX_VALUE; + + // nextWakeupNanos is: + // AWAKE when EL is awake + // NONE when EL is waiting with no wakeup scheduled + // other value T when EL is waiting with wakeup scheduled at time T + private final AtomicLong nextWakeupNanos = new AtomicLong(AWAKE); + private boolean pendingWakeup; private volatile int ioRatio = 50; // See http://man7.org/linux/man-pages/man2/timerfd_create.2.html. private static final long MAX_SCHEDULED_TIMERFD_NS = 999999999; EpollEventLoop(EventLoopGroup parent, Executor executor, int maxEvents, - SelectStrategy strategy, RejectedExecutionHandler rejectedExecutionHandler) { - super(parent, executor, false, DEFAULT_MAX_PENDING_TASKS, rejectedExecutionHandler); + SelectStrategy strategy, RejectedExecutionHandler rejectedExecutionHandler, + EventLoopTaskQueueFactory queueFactory) { + super(parent, executor, false, newTaskQueue(queueFactory), newTaskQueue(queueFactory), + rejectedExecutionHandler); selectStrategy = ObjectUtil.checkNotNull(strategy, "strategy"); if (maxEvents == 0) { allowGrowing = true; @@ -98,12 +105,16 @@ class EpollEventLoop extends SingleThreadEventLoop { this.epollFd = epollFd = Native.newEpollCreate(); this.eventFd = eventFd = Native.newEventFd(); try { - Native.epollCtlAdd(epollFd.intValue(), eventFd.intValue(), Native.EPOLLIN); + // It is important to use EPOLLET here as we only want to get the notification once per + // wakeup and don't call eventfd_read(...). + Native.epollCtlAdd(epollFd.intValue(), eventFd.intValue(), Native.EPOLLIN | Native.EPOLLET); } catch (IOException e) { throw new IllegalStateException("Unable to add eventFd filedescriptor to epoll", e); } this.timerFd = timerFd = Native.newTimerFd(); try { + // It is important to use EPOLLET here as we only want to get the notification once per + // wakeup and don't call read(...). Native.epollCtlAdd(epollFd.intValue(), timerFd.intValue(), Native.EPOLLIN | Native.EPOLLET); } catch (IOException e) { throw new IllegalStateException("Unable to add timerFd filedescriptor to epoll", e); @@ -136,6 +147,14 @@ class EpollEventLoop extends SingleThreadEventLoop { } } + private static Queue<Runnable> newTaskQueue( + EventLoopTaskQueueFactory queueFactory) { + if (queueFactory == null) { + return newTaskQueue0(DEFAULT_MAX_PENDING_TASKS); + } + return queueFactory.newTaskQueue(DEFAULT_MAX_PENDING_TASKS); + } + /** * Return a cleared {@link IovArray} that can be used for writes in this {@link EventLoop}. */ @@ -162,12 +181,24 @@ class EpollEventLoop extends SingleThreadEventLoop { @Override protected void wakeup(boolean inEventLoop) { - if (!inEventLoop && WAKEN_UP_UPDATER.compareAndSet(this, 0, 1)) { + if (!inEventLoop && nextWakeupNanos.getAndSet(AWAKE) != AWAKE) { // write to the evfd which will then wake-up epoll_wait(...) Native.eventFdWrite(eventFd.intValue(), 1L); } } + @Override + protected boolean beforeScheduledTaskSubmitted(long deadlineNanos) { + // Note this is also correct for the nextWakeupNanos == -1 (AWAKE) case + return deadlineNanos < nextWakeupNanos.get(); + } + + @Override + protected boolean afterScheduledTaskSubmitted(long deadlineNanos) { + // Note this is also correct for the nextWakeupNanos == -1 (AWAKE) case + return deadlineNanos < nextWakeupNanos.get(); + } + /** * Register the given epoll with this {@link EventLoop}. */ @@ -175,7 +206,11 @@ class EpollEventLoop extends SingleThreadEventLoop { assert inEventLoop(); int fd = ch.socket.intValue(); Native.epollCtlAdd(epollFd.intValue(), fd, ch.flags); - channels.put(fd, ch); + AbstractEpollChannel old = channels.put(fd, ch); + + // We either expect to have no Channel in the map with the same FD or that the FD of the old Channel is already + // closed. + assert old == null || !old.isOpen(); } /** @@ -191,22 +226,31 @@ class EpollEventLoop extends SingleThreadEventLoop { */ void remove(AbstractEpollChannel ch) throws IOException { assert inEventLoop(); + int fd = ch.socket.intValue(); - if (ch.isOpen()) { - int fd = ch.socket.intValue(); - if (channels.remove(fd) != null) { - // Remove the epoll. This is only needed if it's still open as otherwise it will be automatically - // removed once the file-descriptor is closed. - Native.epollCtlDel(epollFd.intValue(), ch.fd().intValue()); - } + AbstractEpollChannel old = channels.remove(fd); + if (old != null && old != ch) { + // The Channel mapping was already replaced due FD reuse, put back the stored Channel. + channels.put(fd, old); + + // If we found another Channel in the map that is mapped to the same FD the given Channel MUST be closed. + assert !ch.isOpen(); + } else if (ch.isOpen()) { + // Remove the epoll. This is only needed if it's still open as otherwise it will be automatically + // removed once the file-descriptor is closed. + Native.epollCtlDel(epollFd.intValue(), fd); } } @Override protected Queue<Runnable> newTaskQueue(int maxPendingTasks) { + return newTaskQueue0(maxPendingTasks); + } + + private static Queue<Runnable> newTaskQueue0(int maxPendingTasks) { // This event loop never calls takeTask() return maxPendingTasks == Integer.MAX_VALUE ? PlatformDependent.<Runnable>newMpscQueue() - : PlatformDependent.<Runnable>newMpscQueue(maxPendingTasks); + : PlatformDependent.<Runnable>newMpscQueue(maxPendingTasks); } /** @@ -227,40 +271,41 @@ class EpollEventLoop extends SingleThreadEventLoop { this.ioRatio = ioRatio; } - private int epollWait(boolean oldWakeup) throws IOException { - // If a task was submitted when wakenUp value was 1, the task didn't get a chance to produce wakeup event. - // So we need to check task queue again before calling epoll_wait. If we don't, the task might be pended - // until epoll_wait was timed out. It might be pended until idle timeout if IdleStateHandler existed - // in pipeline. - if (oldWakeup && hasTasks()) { - return epollWaitNow(); - } + @Override + public int registeredChannels() { + return channels.size(); + } - int delaySeconds; - int delayNanos; - long curDeadlineNanos = deadlineNanos(); - if (curDeadlineNanos == prevDeadlineNanos) { - delaySeconds = -1; - delayNanos = -1; - } else { - long totalDelay = delayNanos(System.nanoTime()); - prevDeadlineNanos = curDeadlineNanos; - delaySeconds = (int) min(totalDelay / 1000000000L, Integer.MAX_VALUE); - delayNanos = (int) min(totalDelay - delaySeconds * 1000000000L, MAX_SCHEDULED_TIMERFD_NS); + private int epollWait(long deadlineNanos) throws IOException { + if (deadlineNanos == NONE) { + return Native.epollWait(epollFd, events, timerFd, Integer.MAX_VALUE, 0); // disarm timer } + long totalDelay = deadlineToDelayNanos(deadlineNanos); + int delaySeconds = (int) min(totalDelay / 1000000000L, Integer.MAX_VALUE); + int delayNanos = (int) min(totalDelay - delaySeconds * 1000000000L, MAX_SCHEDULED_TIMERFD_NS); return Native.epollWait(epollFd, events, timerFd, delaySeconds, delayNanos); } + private int epollWaitNoTimerChange() throws IOException { + return Native.epollWait(epollFd, events, false); + } + private int epollWaitNow() throws IOException { - return Native.epollWait(epollFd, events, timerFd, 0, 0); + return Native.epollWait(epollFd, events, true); } private int epollBusyWait() throws IOException { return Native.epollBusyWait(epollFd, events); } + private int epollWaitTimeboxed() throws IOException { + // Wait with 1 second "safeguard" timeout + return Native.epollWait(epollFd, events, 1000); + } + @Override protected void run() { + long prevDeadlineNanos = NONE; for (;;) { try { int strategy = selectStrategy.calculateStrategy(selectNowSupplier, hasTasks()); @@ -273,38 +318,45 @@ class EpollEventLoop extends SingleThreadEventLoop { break; case SelectStrategy.SELECT: - strategy = epollWait(WAKEN_UP_UPDATER.getAndSet(this, 0) == 1); - - // 'wakenUp.compareAndSet(false, true)' is always evaluated - // before calling 'selector.wakeup()' to reduce the wake-up - // overhead. (Selector.wakeup() is an expensive operation.) - // - // However, there is a race condition in this approach. - // The race condition is triggered when 'wakenUp' is set to - // true too early. - // - // 'wakenUp' is set to true too early if: - // 1) Selector is waken up between 'wakenUp.set(false)' and - // 'selector.select(...)'. (BAD) - // 2) Selector is waken up between 'selector.select(...)' and - // 'if (wakenUp.get()) { ... }'. (OK) - // - // In the first case, 'wakenUp' is set to true and the - // following 'selector.select(...)' will wake up immediately. - // Until 'wakenUp' is set to false again in the next round, - // 'wakenUp.compareAndSet(false, true)' will fail, and therefore - // any attempt to wake up the Selector will fail, too, causing - // the following 'selector.select(...)' call to block - // unnecessarily. - // - // To fix this problem, we wake up the selector again if wakenUp - // is true immediately after selector.select(...). - // It is inefficient in that it wakes up the selector for both - // the first case (BAD - wake-up required) and the second case - // (OK - no wake-up required). - - if (wakenUp == 1) { - Native.eventFdWrite(eventFd.intValue(), 1L); + if (pendingWakeup) { + // We are going to be immediately woken so no need to reset wakenUp + // or check for timerfd adjustment. + strategy = epollWaitTimeboxed(); + if (strategy != 0) { + break; + } + // We timed out so assume that we missed the write event due to an + // abnormally failed syscall (the write itself or a prior epoll_wait) + logger.warn("Missed eventfd write (not seen after > 1 second)"); + pendingWakeup = false; + if (hasTasks()) { + break; + } + // fall-through + } + + long curDeadlineNanos = nextScheduledTaskDeadlineNanos(); + if (curDeadlineNanos == -1L) { + curDeadlineNanos = NONE; // nothing on the calendar + } + nextWakeupNanos.set(curDeadlineNanos); + try { + if (!hasTasks()) { + if (curDeadlineNanos == prevDeadlineNanos) { + // No timer activity needed + strategy = epollWaitNoTimerChange(); + } else { + // Timerfd needs to be re-armed or disarmed + prevDeadlineNanos = curDeadlineNanos; + strategy = epollWait(curDeadlineNanos); + } + } + } finally { + // Try get() first to avoid much more expensive CAS in the case we + // were woken via the wakeup() method (submitted task) + if (nextWakeupNanos.get() == AWAKE || nextWakeupNanos.getAndSet(AWAKE) == AWAKE) { + pendingWakeup = true; + } } // fallthrough default: @@ -313,25 +365,26 @@ class EpollEventLoop extends SingleThreadEventLoop { final int ioRatio = this.ioRatio; if (ioRatio == 100) { try { - if (strategy > 0) { - processReady(events, strategy); + if (strategy > 0 && processReady(events, strategy)) { + prevDeadlineNanos = NONE; } } finally { // Ensure we always run tasks. runAllTasks(); } - } else { + } else if (strategy > 0) { final long ioStartTime = System.nanoTime(); - try { - if (strategy > 0) { - processReady(events, strategy); + if (processReady(events, strategy)) { + prevDeadlineNanos = NONE; } } finally { // Ensure we always run tasks. final long ioTime = System.nanoTime() - ioStartTime; runAllTasks(ioTime * (100 - ioRatio) / ioRatio); } + } else { + runAllTasks(0); // This will run the minimum number of tasks } if (allowGrowing && strategy == events.length()) { //increase the size of the array as we needed the whole space for the events @@ -370,29 +423,24 @@ class EpollEventLoop extends SingleThreadEventLoop { } private void closeAll() { - try { - epollWaitNow(); - } catch (IOException ignore) { - // ignore on close - } // Using the intermediate collection to prevent ConcurrentModificationException. // In the `close()` method, the channel is deleted from `channels` map. AbstractEpollChannel[] localChannels = channels.values().toArray(new AbstractEpollChannel[0]); - for (AbstractEpollChannel ch : localChannels) { + for (AbstractEpollChannel ch: localChannels) { ch.unsafe().close(ch.unsafe().voidPromise()); } } - private void processReady(EpollEventArray events, int ready) { + // Returns true if a timerFd event was encountered + private boolean processReady(EpollEventArray events, int ready) { + boolean timerFired = false; for (int i = 0; i < ready; i ++) { final int fd = events.fd(i); if (fd == eventFd.intValue()) { - // consume wakeup event. - Native.eventFdRead(fd); + pendingWakeup = false; } else if (fd == timerFd.intValue()) { - // consume wakeup event, necessary because the timer is added with ET mode. - Native.timerFdRead(fd); + timerFired = true; } else { final long ev = events.events(i); @@ -446,15 +494,29 @@ class EpollEventLoop extends SingleThreadEventLoop { } } } + return timerFired; } @Override protected void cleanup() { try { - try { - epollFd.close(); - } catch (IOException e) { - logger.warn("Failed to close the epoll fd.", e); + // Ensure any in-flight wakeup writes have been performed prior to closing eventFd. + while (pendingWakeup) { + try { + int count = epollWaitTimeboxed(); + if (count == 0) { + // We timed-out so assume that the write we're expecting isn't coming + break; + } + for (int i = 0; i < count; i++) { + if (events.fd(i) == eventFd.intValue()) { + pendingWakeup = false; + break; + } + } + } catch (IOException ignore) { + // ignore + } } try { eventFd.close(); @@ -466,6 +528,12 @@ class EpollEventLoop extends SingleThreadEventLoop { } catch (IOException e) { logger.warn("Failed to close the timer fd.", e); } + + try { + epollFd.close(); + } catch (IOException e) { + logger.warn("Failed to close the epoll fd.", e); + } } finally { // release native memory if (iovArray != null) { diff --git a/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollEventLoopGroup.java b/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollEventLoopGroup.java index acb4212..756093f 100644 --- a/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollEventLoopGroup.java +++ b/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollEventLoopGroup.java @@ -18,9 +18,9 @@ package io.netty.channel.epoll; import io.netty.channel.DefaultSelectStrategyFactory; import io.netty.channel.EventLoop; import io.netty.channel.EventLoopGroup; +import io.netty.channel.EventLoopTaskQueueFactory; import io.netty.channel.MultithreadEventLoopGroup; import io.netty.channel.SelectStrategyFactory; -import io.netty.util.concurrent.EventExecutor; import io.netty.util.concurrent.EventExecutorChooserFactory; import io.netty.util.concurrent.RejectedExecutionHandler; import io.netty.util.concurrent.RejectedExecutionHandlers; @@ -52,6 +52,14 @@ public final class EpollEventLoopGroup extends MultithreadEventLoopGroup { this(nThreads, (ThreadFactory) null); } + /** + * Create a new instance using the default number of threads and the given {@link ThreadFactory}. + */ + @SuppressWarnings("deprecation") + public EpollEventLoopGroup(ThreadFactory threadFactory) { + this(0, threadFactory, 0); + } + /** * Create a new instance using the specified number of threads and the default {@link ThreadFactory}. */ @@ -119,19 +127,28 @@ public final class EpollEventLoopGroup extends MultithreadEventLoopGroup { super(nThreads, executor, chooserFactory, 0, selectStrategyFactory, rejectedExecutionHandler); } + public EpollEventLoopGroup(int nThreads, Executor executor, EventExecutorChooserFactory chooserFactory, + SelectStrategyFactory selectStrategyFactory, + RejectedExecutionHandler rejectedExecutionHandler, + EventLoopTaskQueueFactory queueFactory) { + super(nThreads, executor, chooserFactory, 0, selectStrategyFactory, rejectedExecutionHandler, queueFactory); + } + /** - * Sets the percentage of the desired amount of time spent for I/O in the child event loops. The default value is - * {@code 50}, which means the event loop will try to spend the same amount of time for I/O as for non-I/O tasks. + * @deprecated This method will be removed in future releases, and is not guaranteed to have any impacts. */ + @Deprecated public void setIoRatio(int ioRatio) { - for (EventExecutor e: this) { - ((EpollEventLoop) e).setIoRatio(ioRatio); + if (ioRatio <= 0 || ioRatio > 100) { + throw new IllegalArgumentException("ioRatio: " + ioRatio + " (expected: 0 < ioRatio <= 100)"); } } @Override protected EventLoop newChild(Executor executor, Object... args) throws Exception { + EventLoopTaskQueueFactory queueFactory = args.length == 4 ? (EventLoopTaskQueueFactory) args[3] : null; return new EpollEventLoop(this, executor, (Integer) args[0], - ((SelectStrategyFactory) args[1]).newSelectStrategy(), (RejectedExecutionHandler) args[2]); + ((SelectStrategyFactory) args[1]).newSelectStrategy(), + (RejectedExecutionHandler) args[2], queueFactory); } } diff --git a/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollRecvByteAllocatorHandle.java b/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollRecvByteAllocatorHandle.java index a500a89..2685369 100644 --- a/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollRecvByteAllocatorHandle.java +++ b/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollRecvByteAllocatorHandle.java @@ -17,16 +17,14 @@ package io.netty.channel.epoll; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; -import io.netty.channel.ChannelConfig; -import io.netty.channel.RecvByteBufAllocator; +import io.netty.channel.RecvByteBufAllocator.DelegatingHandle; +import io.netty.channel.RecvByteBufAllocator.ExtendedHandle; import io.netty.channel.unix.PreferredDirectByteBufAllocator; import io.netty.util.UncheckedBooleanSupplier; -import io.netty.util.internal.ObjectUtil; -class EpollRecvByteAllocatorHandle implements RecvByteBufAllocator.ExtendedHandle { +class EpollRecvByteAllocatorHandle extends DelegatingHandle implements ExtendedHandle { private final PreferredDirectByteBufAllocator preferredDirectByteBufAllocator = new PreferredDirectByteBufAllocator(); - private final RecvByteBufAllocator.ExtendedHandle delegate; private final UncheckedBooleanSupplier defaultMaybeMoreDataSupplier = new UncheckedBooleanSupplier() { @Override public boolean get() { @@ -36,8 +34,8 @@ class EpollRecvByteAllocatorHandle implements RecvByteBufAllocator.ExtendedHandl private boolean isEdgeTriggered; private boolean receivedRdHup; - EpollRecvByteAllocatorHandle(RecvByteBufAllocator.ExtendedHandle handle) { - delegate = ObjectUtil.checkNotNull(handle, "handle"); + EpollRecvByteAllocatorHandle(ExtendedHandle handle) { + super(handle); } final void receivedRdHup() { @@ -74,57 +72,17 @@ class EpollRecvByteAllocatorHandle implements RecvByteBufAllocator.ExtendedHandl public final ByteBuf allocate(ByteBufAllocator alloc) { // We need to ensure we always allocate a direct ByteBuf as we can only use a direct buffer to read via JNI. preferredDirectByteBufAllocator.updateAllocator(alloc); - return delegate.allocate(preferredDirectByteBufAllocator); - } - - @Override - public final int guess() { - return delegate.guess(); - } - - @Override - public final void reset(ChannelConfig config) { - delegate.reset(config); - } - - @Override - public final void incMessagesRead(int numMessages) { - delegate.incMessagesRead(numMessages); - } - - @Override - public final void lastBytesRead(int bytes) { - delegate.lastBytesRead(bytes); - } - - @Override - public final int lastBytesRead() { - return delegate.lastBytesRead(); - } - - @Override - public final int attemptedBytesRead() { - return delegate.attemptedBytesRead(); - } - - @Override - public final void attemptedBytesRead(int bytes) { - delegate.attemptedBytesRead(bytes); - } - - @Override - public final void readComplete() { - delegate.readComplete(); + return delegate().allocate(preferredDirectByteBufAllocator); } @Override public final boolean continueReading(UncheckedBooleanSupplier maybeMoreDataSupplier) { - return delegate.continueReading(maybeMoreDataSupplier); + return ((ExtendedHandle) delegate()).continueReading(maybeMoreDataSupplier); } @Override public final boolean continueReading() { // We must override the supplier which determines if there maybe more data to read. - return delegate.continueReading(defaultMaybeMoreDataSupplier); + return continueReading(defaultMaybeMoreDataSupplier); } } diff --git a/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollRecvByteAllocatorStreamingHandle.java b/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollRecvByteAllocatorStreamingHandle.java index f6ba5f5..071acd5 100644 --- a/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollRecvByteAllocatorStreamingHandle.java +++ b/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollRecvByteAllocatorStreamingHandle.java @@ -18,7 +18,7 @@ package io.netty.channel.epoll; import io.netty.channel.RecvByteBufAllocator; final class EpollRecvByteAllocatorStreamingHandle extends EpollRecvByteAllocatorHandle { - public EpollRecvByteAllocatorStreamingHandle(RecvByteBufAllocator.ExtendedHandle handle) { + EpollRecvByteAllocatorStreamingHandle(RecvByteBufAllocator.ExtendedHandle handle) { super(handle); } diff --git a/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollServerChannelConfig.java b/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollServerChannelConfig.java index 514b6c3..6f85573 100644 --- a/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollServerChannelConfig.java +++ b/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollServerChannelConfig.java @@ -30,6 +30,7 @@ import java.util.Map; import static io.netty.channel.ChannelOption.SO_BACKLOG; import static io.netty.channel.ChannelOption.SO_RCVBUF; import static io.netty.channel.ChannelOption.SO_REUSEADDR; +import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero; public class EpollServerChannelConfig extends EpollChannelConfig implements ServerSocketChannelConfig { private volatile int backlog = NetUtil.SOMAXCONN; @@ -81,6 +82,7 @@ public class EpollServerChannelConfig extends EpollChannelConfig implements Serv return true; } + @Override public boolean isReuseAddress() { try { return ((AbstractEpollChannel) channel).socket.isReuseAddress(); @@ -89,6 +91,7 @@ public class EpollServerChannelConfig extends EpollChannelConfig implements Serv } } + @Override public EpollServerChannelConfig setReuseAddress(boolean reuseAddress) { try { ((AbstractEpollChannel) channel).socket.setReuseAddress(reuseAddress); @@ -98,6 +101,7 @@ public class EpollServerChannelConfig extends EpollChannelConfig implements Serv } } + @Override public int getReceiveBufferSize() { try { return ((AbstractEpollChannel) channel).socket.getReceiveBufferSize(); @@ -106,6 +110,7 @@ public class EpollServerChannelConfig extends EpollChannelConfig implements Serv } } + @Override public EpollServerChannelConfig setReceiveBufferSize(int receiveBufferSize) { try { ((AbstractEpollChannel) channel).socket.setReceiveBufferSize(receiveBufferSize); @@ -115,14 +120,14 @@ public class EpollServerChannelConfig extends EpollChannelConfig implements Serv } } + @Override public int getBacklog() { return backlog; } + @Override public EpollServerChannelConfig setBacklog(int backlog) { - if (backlog < 0) { - throw new IllegalArgumentException("backlog: " + backlog); - } + checkPositiveOrZero(backlog, "backlog"); this.backlog = backlog; return this; } @@ -146,9 +151,7 @@ public class EpollServerChannelConfig extends EpollChannelConfig implements Serv * @see <a href="https://tools.ietf.org/html/rfc7413">RFC 7413 TCP FastOpen</a> */ public EpollServerChannelConfig setTcpFastopen(int pendingFastOpenRequestsThreshold) { - if (this.pendingFastOpenRequestsThreshold < 0) { - throw new IllegalArgumentException("pendingFastOpenRequestsThreshold: " + pendingFastOpenRequestsThreshold); - } + checkPositiveOrZero(this.pendingFastOpenRequestsThreshold, "pendingFastOpenRequestsThreshold"); this.pendingFastOpenRequestsThreshold = pendingFastOpenRequestsThreshold; return this; } diff --git a/transport-native-epoll/src/main/java/io/netty/channel/epoll/LinuxSocket.java b/transport-native-epoll/src/main/java/io/netty/channel/epoll/LinuxSocket.java index d3578b4..db2d246 100644 --- a/transport-native-epoll/src/main/java/io/netty/channel/epoll/LinuxSocket.java +++ b/transport-native-epoll/src/main/java/io/netty/channel/epoll/LinuxSocket.java @@ -15,35 +15,143 @@ */ package io.netty.channel.epoll; +import io.netty.channel.ChannelException; import io.netty.channel.DefaultFileRegion; -import io.netty.channel.unix.Errors.NativeIoException; import io.netty.channel.unix.NativeInetAddress; import io.netty.channel.unix.PeerCredentials; import io.netty.channel.unix.Socket; -import io.netty.util.internal.ThrowableUtil; +import io.netty.channel.socket.InternetProtocolFamily; +import io.netty.util.internal.PlatformDependent; +import io.netty.util.internal.SocketUtils; import java.io.IOException; import java.net.InetAddress; -import java.nio.channels.ClosedChannelException; +import java.net.Inet6Address; +import java.net.NetworkInterface; +import java.net.UnknownHostException; +import java.util.Enumeration; -import static io.netty.channel.unix.Errors.ERRNO_EPIPE_NEGATIVE; import static io.netty.channel.unix.Errors.ioResult; -import static io.netty.channel.unix.Errors.newConnectionResetException; /** * A socket which provides access Linux native methods. */ final class LinuxSocket extends Socket { + static final InetAddress INET6_ANY = unsafeInetAddrByName("::"); + private static final InetAddress INET_ANY = unsafeInetAddrByName("0.0.0.0"); private static final long MAX_UINT32_T = 0xFFFFFFFFL; - private static final NativeIoException SENDFILE_CONNECTION_RESET_EXCEPTION = - newConnectionResetException("syscall:sendfile(...)", ERRNO_EPIPE_NEGATIVE); - private static final ClosedChannelException SENDFILE_CLOSED_CHANNEL_EXCEPTION = ThrowableUtil.unknownStackTrace( - new ClosedChannelException(), Native.class, "sendfile(...)"); - public LinuxSocket(int fd) { + LinuxSocket(int fd) { super(fd); } + private InternetProtocolFamily family() { + return ipv6 ? InternetProtocolFamily.IPv6 : InternetProtocolFamily.IPv4; + } + + int sendmmsg(NativeDatagramPacketArray.NativeDatagramPacket[] msgs, + int offset, int len) throws IOException { + return Native.sendmmsg(intValue(), ipv6, msgs, offset, len); + } + + int recvmmsg(NativeDatagramPacketArray.NativeDatagramPacket[] msgs, + int offset, int len) throws IOException { + return Native.recvmmsg(intValue(), ipv6, msgs, offset, len); + } + + void setTimeToLive(int ttl) throws IOException { + setTimeToLive(intValue(), ttl); + } + + void setInterface(InetAddress address) throws IOException { + final NativeInetAddress a = NativeInetAddress.newInstance(address); + setInterface(intValue(), ipv6, a.address(), a.scopeId(), interfaceIndex(address)); + } + + void setNetworkInterface(NetworkInterface netInterface) throws IOException { + InetAddress address = deriveInetAddress(netInterface, family() == InternetProtocolFamily.IPv6); + if (address.equals(family() == InternetProtocolFamily.IPv4 ? INET_ANY : INET6_ANY)) { + throw new IOException("NetworkInterface does not support " + family()); + } + final NativeInetAddress nativeAddress = NativeInetAddress.newInstance(address); + setInterface(intValue(), ipv6, nativeAddress.address(), nativeAddress.scopeId(), interfaceIndex(netInterface)); + } + + InetAddress getInterface() throws IOException { + NetworkInterface inf = getNetworkInterface(); + if (inf != null) { + Enumeration<InetAddress> addresses = SocketUtils.addressesFromNetworkInterface(inf); + if (addresses.hasMoreElements()) { + return addresses.nextElement(); + } + } + return null; + } + + NetworkInterface getNetworkInterface() throws IOException { + int ret = getInterface(intValue(), ipv6); + if (ipv6) { + return PlatformDependent.javaVersion() >= 7 ? NetworkInterface.getByIndex(ret) : null; + } + InetAddress address = inetAddress(ret); + return address != null ? NetworkInterface.getByInetAddress(address) : null; + } + + private static InetAddress inetAddress(int value) { + byte[] var1 = { + (byte) (value >>> 24 & 255), + (byte) (value >>> 16 & 255), + (byte) (value >>> 8 & 255), + (byte) (value & 255) + }; + + try { + return InetAddress.getByAddress(var1); + } catch (UnknownHostException ignore) { + return null; + } + } + + void joinGroup(InetAddress group, NetworkInterface netInterface, InetAddress source) throws IOException { + final NativeInetAddress g = NativeInetAddress.newInstance(group); + final boolean isIpv6 = group instanceof Inet6Address; + final NativeInetAddress i = NativeInetAddress.newInstance(deriveInetAddress(netInterface, isIpv6)); + if (source != null) { + final NativeInetAddress s = NativeInetAddress.newInstance(source); + joinSsmGroup(intValue(), ipv6, g.address(), i.address(), + g.scopeId(), interfaceIndex(netInterface), s.address()); + } else { + joinGroup(intValue(), ipv6, g.address(), i.address(), g.scopeId(), interfaceIndex(netInterface)); + } + } + + void leaveGroup(InetAddress group, NetworkInterface netInterface, InetAddress source) throws IOException { + final NativeInetAddress g = NativeInetAddress.newInstance(group); + final boolean isIpv6 = group instanceof Inet6Address; + final NativeInetAddress i = NativeInetAddress.newInstance(deriveInetAddress(netInterface, isIpv6)); + if (source != null) { + final NativeInetAddress s = NativeInetAddress.newInstance(source); + leaveSsmGroup(intValue(), ipv6, g.address(), i.address(), + g.scopeId(), interfaceIndex(netInterface), s.address()); + } else { + leaveGroup(intValue(), ipv6, g.address(), i.address(), g.scopeId(), interfaceIndex(netInterface)); + } + } + + private static int interfaceIndex(NetworkInterface networkInterface) { + return PlatformDependent.javaVersion() >= 7 ? networkInterface.getIndex() : -1; + } + + private static int interfaceIndex(InetAddress address) throws IOException { + if (PlatformDependent.javaVersion() >= 7) { + NetworkInterface iface = NetworkInterface.getByInetAddress(address); + if (iface != null) { + return iface.getIndex(); + } + } + return -1; + } + void setTcpDeferAccept(int deferAccept) throws IOException { setTcpDeferAccept(intValue(), deferAccept); } @@ -107,13 +215,17 @@ final class LinuxSocket extends Socket { setIpRecvOrigDestAddr(intValue(), enabled ? 1 : 0); } + int getTimeToLive() throws IOException { + return getTimeToLive(intValue()); + } + void getTcpInfo(EpollTcpInfo info) throws IOException { getTcpInfo(intValue(), info.info); } void setTcpMd5Sig(InetAddress address, byte[] key) throws IOException { final NativeInetAddress a = NativeInetAddress.newInstance(address); - setTcpMd5Sig(intValue(), a.address(), a.scopeId(), key); + setTcpMd5Sig(intValue(), ipv6, a.address(), a.scopeId(), key); } boolean isTcpCork() throws IOException { @@ -168,6 +280,14 @@ final class LinuxSocket extends Socket { return getPeerCredentials(intValue()); } + boolean isLoopbackModeDisabled() throws IOException { + return getIpMulticastLoop(intValue(), ipv6) == 0; + } + + void setLoopbackModeDisabled(boolean loopbackModeDisabled) throws IOException { + setIpMulticastLoop(intValue(), ipv6, loopbackModeDisabled ? 0 : 1); + } + long sendFile(DefaultFileRegion src, long baseOffset, long offset, long length) throws IOException { // Open the file-region as it may be created via the lazy constructor. This is needed as we directly access // the FileChannel field via JNI. @@ -177,21 +297,60 @@ final class LinuxSocket extends Socket { if (res >= 0) { return res; } - return ioResult("sendfile", (int) res, SENDFILE_CONNECTION_RESET_EXCEPTION, SENDFILE_CLOSED_CHANNEL_EXCEPTION); + return ioResult("sendfile", (int) res); + } + + private static InetAddress deriveInetAddress(NetworkInterface netInterface, boolean ipv6) { + final InetAddress ipAny = ipv6 ? INET6_ANY : INET_ANY; + if (netInterface != null) { + final Enumeration<InetAddress> ias = netInterface.getInetAddresses(); + while (ias.hasMoreElements()) { + final InetAddress ia = ias.nextElement(); + final boolean isV6 = ia instanceof Inet6Address; + if (isV6 == ipv6) { + return ia; + } + } + } + return ipAny; + } + + public static LinuxSocket newSocketStream(boolean ipv6) { + return new LinuxSocket(newSocketStream0(ipv6)); } public static LinuxSocket newSocketStream() { - return new LinuxSocket(newSocketStream0()); + return newSocketStream(isIPv6Preferred()); + } + + public static LinuxSocket newSocketDgram(boolean ipv6) { + return new LinuxSocket(newSocketDgram0(ipv6)); } public static LinuxSocket newSocketDgram() { - return new LinuxSocket(newSocketDgram0()); + return newSocketDgram(isIPv6Preferred()); } public static LinuxSocket newSocketDomain() { return new LinuxSocket(newSocketDomain0()); } + private static InetAddress unsafeInetAddrByName(String inetName) { + try { + return InetAddress.getByName(inetName); + } catch (UnknownHostException uhe) { + throw new ChannelException(uhe); + } + } + + private static native void joinGroup(int fd, boolean ipv6, byte[] group, byte[] interfaceAddress, + int scopeId, int interfaceIndex) throws IOException; + private static native void joinSsmGroup(int fd, boolean ipv6, byte[] group, byte[] interfaceAddress, + int scopeId, int interfaceIndex, byte[] source) throws IOException; + private static native void leaveGroup(int fd, boolean ipv6, byte[] group, byte[] interfaceAddress, + int scopeId, int interfaceIndex) throws IOException; + private static native void leaveSsmGroup(int fd, boolean ipv6, byte[] group, byte[] interfaceAddress, + int scopeId, int interfaceIndex, byte[] source) throws IOException; private static native long sendFile(int socketFd, DefaultFileRegion src, long baseOffset, long offset, long length) throws IOException; @@ -204,6 +363,7 @@ final class LinuxSocket extends Socket { private static native int getTcpKeepIntvl(int fd) throws IOException; private static native int getTcpKeepCnt(int fd) throws IOException; private static native int getTcpUserTimeout(int fd) throws IOException; + private static native int getTimeToLive(int fd) throws IOException; private static native int isIpFreeBind(int fd) throws IOException; private static native int isIpTransparent(int fd) throws IOException; private static native int isIpRecvOrigDestAddr(int fd) throws IOException; @@ -225,5 +385,12 @@ final class LinuxSocket extends Socket { private static native void setIpFreeBind(int fd, int freeBind) throws IOException; private static native void setIpTransparent(int fd, int transparent) throws IOException; private static native void setIpRecvOrigDestAddr(int fd, int transparent) throws IOException; - private static native void setTcpMd5Sig(int fd, byte[] address, int scopeId, byte[] key) throws IOException; + private static native void setTcpMd5Sig( + int fd, boolean ipv6, byte[] address, int scopeId, byte[] key) throws IOException; + private static native void setInterface( + int fd, boolean ipv6, byte[] interfaceAddress, int scopeId, int networkInterfaceIndex) throws IOException; + private static native int getInterface(int fd, boolean ipv6); + private static native int getIpMulticastLoop(int fd, boolean ipv6) throws IOException; + private static native void setIpMulticastLoop(int fd, boolean ipv6, int enabled) throws IOException; + private static native void setTimeToLive(int fd, int ttl) throws IOException; } diff --git a/transport-native-epoll/src/main/java/io/netty/channel/epoll/Native.java b/transport-native-epoll/src/main/java/io/netty/channel/epoll/Native.java index 1eb2e5d..437e0ef 100644 --- a/transport-native-epoll/src/main/java/io/netty/channel/epoll/Native.java +++ b/transport-native-epoll/src/main/java/io/netty/channel/epoll/Native.java @@ -15,7 +15,6 @@ */ package io.netty.channel.epoll; -import io.netty.channel.unix.Errors.NativeIoException; import io.netty.channel.unix.FileDescriptor; import io.netty.channel.unix.Socket; import io.netty.util.internal.NativeLibraryLoader; @@ -26,7 +25,6 @@ import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; import java.io.IOException; -import java.nio.channels.ClosedChannelException; import java.util.Locale; import static io.netty.channel.epoll.NativeStaticallyReferencedJniMethods.epollerr; @@ -34,13 +32,12 @@ import static io.netty.channel.epoll.NativeStaticallyReferencedJniMethods.epolle import static io.netty.channel.epoll.NativeStaticallyReferencedJniMethods.epollin; import static io.netty.channel.epoll.NativeStaticallyReferencedJniMethods.epollout; import static io.netty.channel.epoll.NativeStaticallyReferencedJniMethods.epollrdhup; +import static io.netty.channel.epoll.NativeStaticallyReferencedJniMethods.isSupportingRecvmmsg; import static io.netty.channel.epoll.NativeStaticallyReferencedJniMethods.isSupportingSendmmsg; import static io.netty.channel.epoll.NativeStaticallyReferencedJniMethods.isSupportingTcpFastopen; import static io.netty.channel.epoll.NativeStaticallyReferencedJniMethods.kernelVersion; import static io.netty.channel.epoll.NativeStaticallyReferencedJniMethods.tcpMd5SigMaxKeyLen; -import static io.netty.channel.unix.Errors.ERRNO_EPIPE_NEGATIVE; import static io.netty.channel.unix.Errors.ioResult; -import static io.netty.channel.unix.Errors.newConnectionResetException; import static io.netty.channel.unix.Errors.newIOException; /** @@ -71,24 +68,12 @@ public final class Native { public static final int EPOLLERR = epollerr(); public static final boolean IS_SUPPORTING_SENDMMSG = isSupportingSendmmsg(); + static final boolean IS_SUPPORTING_RECVMMSG = isSupportingRecvmmsg(); + public static final boolean IS_SUPPORTING_TCP_FASTOPEN = isSupportingTcpFastopen(); public static final int TCP_MD5SIG_MAXKEYLEN = tcpMd5SigMaxKeyLen(); public static final String KERNEL_VERSION = kernelVersion(); - private static final NativeIoException SENDMMSG_CONNECTION_RESET_EXCEPTION; - private static final NativeIoException SPLICE_CONNECTION_RESET_EXCEPTION; - private static final ClosedChannelException SENDMMSG_CLOSED_CHANNEL_EXCEPTION = ThrowableUtil.unknownStackTrace( - new ClosedChannelException(), Native.class, "sendmmsg(...)"); - private static final ClosedChannelException SPLICE_CLOSED_CHANNEL_EXCEPTION = ThrowableUtil.unknownStackTrace( - new ClosedChannelException(), Native.class, "splice(...)"); - - static { - SENDMMSG_CONNECTION_RESET_EXCEPTION = newConnectionResetException("syscall:sendmmsg(...)", - ERRNO_EPIPE_NEGATIVE); - SPLICE_CONNECTION_RESET_EXCEPTION = newConnectionResetException("syscall:splice(...)", - ERRNO_EPIPE_NEGATIVE); - } - public static FileDescriptor newEventFd() { return new FileDescriptor(eventFd()); } @@ -102,6 +87,7 @@ public final class Native { public static native void eventFdWrite(int fd, long value); public static native void eventFdRead(int fd); static native void timerFdRead(int fd); + static native void timerFdSetTime(int fd, int sec, int nsec) throws IOException; public static FileDescriptor newEpollCreate() { return new FileDescriptor(epollCreate()); @@ -109,8 +95,21 @@ public final class Native { private static native int epollCreate(); + /** + * @deprecated this method is no longer supported. This functionality is internal to this package. + */ + @Deprecated public static int epollWait(FileDescriptor epollFd, EpollEventArray events, FileDescriptor timerFd, int timeoutSec, int timeoutNs) throws IOException { + if (timeoutSec == 0 && timeoutNs == 0) { + // Zero timeout => poll (aka return immediately) + return epollWait(epollFd, events, 0); + } + if (timeoutSec == Integer.MAX_VALUE) { + // Max timeout => wait indefinitely: disarm timerfd first + timeoutSec = 0; + timeoutNs = 0; + } int ready = epollWait0(epollFd.intValue(), events.memoryAddress(), events.length(), timerFd.intValue(), timeoutSec, timeoutNs); if (ready < 0) { @@ -118,7 +117,21 @@ public final class Native { } return ready; } - private static native int epollWait0(int efd, long address, int len, int timerFd, int timeoutSec, int timeoutNs); + + static int epollWait(FileDescriptor epollFd, EpollEventArray events, boolean immediatePoll) throws IOException { + return epollWait(epollFd, events, immediatePoll ? 0 : -1); + } + + /** + * This uses epoll's own timeout and does not reset/re-arm any timerfd + */ + static int epollWait(FileDescriptor epollFd, EpollEventArray events, int timeoutMillis) throws IOException { + int ready = epollWait(epollFd.intValue(), events.memoryAddress(), events.length(), timeoutMillis); + if (ready < 0) { + throw newIOException("epoll_wait", ready); + } + return ready; + } /** * Non-blocking variant of @@ -133,6 +146,8 @@ public final class Native { return ready; } + private static native int epollWait0(int efd, long address, int len, int timerFd, int timeoutSec, int timeoutNs); + private static native int epollWait(int efd, long address, int len, int timeout); private static native int epollBusyWait0(int efd, long address, int len); public static void epollCtlAdd(int efd, final int fd, final int flags) throws IOException { @@ -141,7 +156,7 @@ public final class Native { throw newIOException("epoll_ctl", res); } } - private static native int epollCtlAdd0(int efd, final int fd, final int flags); + private static native int epollCtlAdd0(int efd, int fd, int flags); public static void epollCtlMod(int efd, final int fd, final int flags) throws IOException { int res = epollCtlMod0(efd, fd, flags); @@ -149,7 +164,7 @@ public final class Native { throw newIOException("epoll_ctl", res); } } - private static native int epollCtlMod0(int efd, final int fd, final int flags); + private static native int epollCtlMod0(int efd, int fd, int flags); public static void epollCtlDel(int efd, final int fd) throws IOException { int res = epollCtlDel0(efd, fd); @@ -157,7 +172,7 @@ public final class Native { throw newIOException("epoll_ctl", res); } } - private static native int epollCtlDel0(int efd, final int fd); + private static native int epollCtlDel0(int efd, int fd); // File-descriptor operations public static int splice(int fd, long offIn, int fdOut, long offOut, long len) throws IOException { @@ -165,22 +180,40 @@ public final class Native { if (res >= 0) { return res; } - return ioResult("splice", res, SPLICE_CONNECTION_RESET_EXCEPTION, SPLICE_CLOSED_CHANNEL_EXCEPTION); + return ioResult("splice", res); } private static native int splice0(int fd, long offIn, int fdOut, long offOut, long len); - public static int sendmmsg( - int fd, NativeDatagramPacketArray.NativeDatagramPacket[] msgs, int offset, int len) throws IOException { - int res = sendmmsg0(fd, msgs, offset, len); + @Deprecated + public static int sendmmsg(int fd, NativeDatagramPacketArray.NativeDatagramPacket[] msgs, + int offset, int len) throws IOException { + return sendmmsg(fd, Socket.isIPv6Preferred(), msgs, offset, len); + } + + static int sendmmsg(int fd, boolean ipv6, NativeDatagramPacketArray.NativeDatagramPacket[] msgs, + int offset, int len) throws IOException { + int res = sendmmsg0(fd, ipv6, msgs, offset, len); if (res >= 0) { return res; } - return ioResult("sendmmsg", res, SENDMMSG_CONNECTION_RESET_EXCEPTION, SENDMMSG_CLOSED_CHANNEL_EXCEPTION); + return ioResult("sendmmsg", res); } private static native int sendmmsg0( - int fd, NativeDatagramPacketArray.NativeDatagramPacket[] msgs, int offset, int len); + int fd, boolean ipv6, NativeDatagramPacketArray.NativeDatagramPacket[] msgs, int offset, int len); + + static int recvmmsg(int fd, boolean ipv6, NativeDatagramPacketArray.NativeDatagramPacket[] msgs, + int offset, int len) throws IOException { + int res = recvmmsg0(fd, ipv6, msgs, offset, len); + if (res >= 0) { + return res; + } + return ioResult("recvmmsg", res); + } + + private static native int recvmmsg0( + int fd, boolean ipv6, NativeDatagramPacketArray.NativeDatagramPacket[] msgs, int offset, int len); // epoll_event related public static native int sizeofEpollEvent(); diff --git a/transport-native-epoll/src/main/java/io/netty/channel/epoll/NativeDatagramPacketArray.java b/transport-native-epoll/src/main/java/io/netty/channel/epoll/NativeDatagramPacketArray.java index 0bdb9eb..4b0e675 100644 --- a/transport-native-epoll/src/main/java/io/netty/channel/epoll/NativeDatagramPacketArray.java +++ b/transport-native-epoll/src/main/java/io/netty/channel/epoll/NativeDatagramPacketArray.java @@ -17,22 +17,35 @@ package io.netty.channel.epoll; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelOutboundBuffer; +import io.netty.channel.ChannelOutboundBuffer.MessageProcessor; import io.netty.channel.socket.DatagramPacket; import io.netty.channel.unix.IovArray; +import io.netty.channel.unix.Limits; + import java.net.Inet6Address; import java.net.InetAddress; import java.net.InetSocketAddress; +import java.net.UnknownHostException; import static io.netty.channel.unix.Limits.UIO_MAX_IOV; -import static io.netty.channel.unix.NativeInetAddress.ipv4MappedIpv6Address; +import static io.netty.channel.unix.NativeInetAddress.copyIpv4MappedIpv6Address; /** * Support <a href="http://linux.die.net/man/2/sendmmsg">sendmmsg(...)</a> on linux with GLIBC 2.14+ */ -final class NativeDatagramPacketArray implements ChannelOutboundBuffer.MessageProcessor { +final class NativeDatagramPacketArray { // Use UIO_MAX_IOV as this is the maximum number we can write with one sendmmsg(...) call. private final NativeDatagramPacket[] packets = new NativeDatagramPacket[UIO_MAX_IOV]; + + // We share one IovArray for all NativeDatagramPackets to reduce memory overhead. This will allow us to write + // up to IOV_MAX iovec across all messages in one sendmmsg(...) call. + private final IovArray iovArray = new IovArray(); + + // temporary array to copy the ipv4 part of ipv6-mapped-ipv4 addresses and then create a Inet4Address out of it. + private final byte[] ipv4Bytes = new byte[4]; + private final MyMessageProcessor processor = new MyMessageProcessor(); + private int count; NativeDatagramPacketArray() { @@ -41,32 +54,34 @@ final class NativeDatagramPacketArray implements ChannelOutboundBuffer.MessagePr } } - /** - * Try to add the given {@link DatagramPacket}. Returns {@code true} on success, - * {@code false} otherwise. - */ - boolean add(DatagramPacket packet) { + boolean addWritable(ByteBuf buf, int index, int len) { + return add0(buf, index, len, null); + } + + private boolean add0(ByteBuf buf, int index, int len, InetSocketAddress recipient) { if (count == packets.length) { + // We already filled up to UIO_MAX_IOV messages. This is the max allowed per + // recvmmsg(...) / sendmmsg(...) call, we will try again later. return false; } - ByteBuf content = packet.content(); - int len = content.readableBytes(); if (len == 0) { return true; } - NativeDatagramPacket p = packets[count]; - InetSocketAddress recipient = packet.recipient(); - if (!p.init(content, recipient)) { + int offset = iovArray.count(); + if (offset == Limits.IOV_MAX || !iovArray.add(buf, index, len)) { + // Not enough space to hold the whole content, we will try again later. return false; } + NativeDatagramPacket p = packets[count]; + p.init(iovArray.memoryAddress(offset), iovArray.count() - offset, recipient); count++; return true; } - @Override - public boolean processMessage(Object msg) { - return msg instanceof DatagramPacket && add((DatagramPacket) msg); + void add(ChannelOutboundBuffer buffer, boolean connected) throws Exception { + processor.connected = connected; + buffer.forEachFlushedMessage(processor); } /** @@ -85,12 +100,28 @@ final class NativeDatagramPacketArray implements ChannelOutboundBuffer.MessagePr void clear() { this.count = 0; + this.iovArray.clear(); } void release() { - // Release all packets - for (NativeDatagramPacket datagramPacket : packets) { - datagramPacket.release(); + iovArray.release(); + } + + private final class MyMessageProcessor implements MessageProcessor { + private boolean connected; + + @Override + public boolean processMessage(Object msg) { + if (msg instanceof DatagramPacket) { + DatagramPacket packet = (DatagramPacket) msg; + ByteBuf buf = packet.content(); + return add0(buf, buf.readerIndex(), buf.readableBytes(), packet.recipient()); + } + if (msg instanceof ByteBuf && connected) { + ByteBuf buf = (ByteBuf) msg; + return add0(buf, buf.readerIndex(), buf.readableBytes(), null); + } + return false; } } @@ -98,46 +129,50 @@ final class NativeDatagramPacketArray implements ChannelOutboundBuffer.MessagePr * Used to pass needed data to JNI. */ @SuppressWarnings("unused") - static final class NativeDatagramPacket { - // Each NativeDatagramPackets holds a IovArray which is used for gathering writes. - // This is ok as NativeDatagramPacketArray is always obtained from an EpollEventLoop - // field so the memory needed is quite small anyway. - private final IovArray array = new IovArray(); + final class NativeDatagramPacket { // This is the actual struct iovec* private long memoryAddress; private int count; - private byte[] addr; + private final byte[] addr = new byte[16]; + + private int addrLen; private int scopeId; private int port; - private void release() { - array.release(); - } + private void init(long memoryAddress, int count, InetSocketAddress recipient) { + this.memoryAddress = memoryAddress; + this.count = count; - /** - * Init this instance and return {@code true} if the init was successful. - */ - private boolean init(ByteBuf buf, InetSocketAddress recipient) { - array.clear(); - if (!array.add(buf)) { - return false; + if (recipient == null) { + this.scopeId = 0; + this.port = 0; + this.addrLen = 0; + } else { + InetAddress address = recipient.getAddress(); + if (address instanceof Inet6Address) { + System.arraycopy(address.getAddress(), 0, addr, 0, addr.length); + scopeId = ((Inet6Address) address).getScopeId(); + } else { + copyIpv4MappedIpv6Address(address.getAddress(), addr); + scopeId = 0; + } + addrLen = addr.length; + port = recipient.getPort(); } - // always start from offset 0 - memoryAddress = array.memoryAddress(0); - count = array.count(); - - InetAddress address = recipient.getAddress(); - if (address instanceof Inet6Address) { - addr = address.getAddress(); - scopeId = ((Inet6Address) address).getScopeId(); + } + + DatagramPacket newDatagramPacket(ByteBuf buffer, InetSocketAddress localAddress) throws UnknownHostException { + final InetAddress address; + if (addrLen == ipv4Bytes.length) { + System.arraycopy(addr, 0, ipv4Bytes, 0, addrLen); + address = InetAddress.getByAddress(ipv4Bytes); } else { - addr = ipv4MappedIpv6Address(address.getAddress()); - scopeId = 0; + address = Inet6Address.getByAddress(null, addr, scopeId); } - port = recipient.getPort(); - return true; + return new DatagramPacket(buffer.writerIndex(count), + localAddress, new InetSocketAddress(address, port)); } } } diff --git a/transport-native-epoll/src/main/java/io/netty/channel/epoll/NativeStaticallyReferencedJniMethods.java b/transport-native-epoll/src/main/java/io/netty/channel/epoll/NativeStaticallyReferencedJniMethods.java index 4f6f99c..9b20d68 100644 --- a/transport-native-epoll/src/main/java/io/netty/channel/epoll/NativeStaticallyReferencedJniMethods.java +++ b/transport-native-epoll/src/main/java/io/netty/channel/epoll/NativeStaticallyReferencedJniMethods.java @@ -40,6 +40,7 @@ final class NativeStaticallyReferencedJniMethods { static native int iovMax(); static native int uioMaxIov(); static native boolean isSupportingSendmmsg(); + static native boolean isSupportingRecvmmsg(); static native boolean isSupportingTcpFastopen(); static native String kernelVersion(); } diff --git a/transport-native-epoll/src/main/java/io/netty/channel/epoll/TcpMd5Util.java b/transport-native-epoll/src/main/java/io/netty/channel/epoll/TcpMd5Util.java index 080c835..41509d7 100644 --- a/transport-native-epoll/src/main/java/io/netty/channel/epoll/TcpMd5Util.java +++ b/transport-native-epoll/src/main/java/io/netty/channel/epoll/TcpMd5Util.java @@ -39,9 +39,7 @@ final class TcpMd5Util { if (e.getKey() == null) { throw new IllegalArgumentException("newKeys contains an entry with null address: " + newKeys); } - if (key == null) { - throw new NullPointerException("newKeys[" + e.getKey() + ']'); - } + ObjectUtil.checkNotNull(key, "newKeys[" + e.getKey() + ']'); if (key.length == 0) { throw new IllegalArgumentException("newKeys[" + e.getKey() + "] has an empty key."); } diff --git a/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDatagramChannelTest.java b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDatagramChannelTest.java new file mode 100644 index 0000000..ce7646a --- /dev/null +++ b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDatagramChannelTest.java @@ -0,0 +1,108 @@ +/* + * Copyright 2019 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.channel.epoll; + +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.socket.InternetProtocolFamily; +import io.netty.channel.unix.Socket; +import org.junit.Before; +import org.junit.Test; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.SocketAddress; + +import static io.netty.util.NetUtil.LOCALHOST; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +public class EpollDatagramChannelTest { + + @Before + public void setUp() { + Epoll.ensureAvailability(); + } + + @Test + public void testNotActiveNoLocalRemoteAddress() throws IOException { + checkNotActiveNoLocalRemoteAddress(new EpollDatagramChannel()); + checkNotActiveNoLocalRemoteAddress(new EpollDatagramChannel(InternetProtocolFamily.IPv4)); + checkNotActiveNoLocalRemoteAddress(new EpollDatagramChannel(InternetProtocolFamily.IPv6)); + } + + @Test + public void testActiveHasLocalAddress() throws IOException { + Socket socket = Socket.newSocketDgram(); + EpollDatagramChannel channel = new EpollDatagramChannel(socket.intValue()); + InetSocketAddress localAddress = channel.localAddress(); + assertTrue(channel.active); + assertNotNull(localAddress); + assertEquals(socket.localAddress(), localAddress); + channel.fd().close(); + } + + @Test + public void testLocalAddressBeforeAndAfterBind() { + EventLoopGroup group = new EpollEventLoopGroup(1); + try { + TestHandler handler = new TestHandler(); + InetSocketAddress localAddressBeforeBind = new InetSocketAddress(LOCALHOST, 0); + + Bootstrap bootstrap = new Bootstrap(); + bootstrap.group(group) + .channel(EpollDatagramChannel.class) + .localAddress(localAddressBeforeBind) + .handler(handler); + + ChannelFuture future = bootstrap.bind().syncUninterruptibly(); + + assertNull(handler.localAddress); + + SocketAddress localAddressAfterBind = future.channel().localAddress(); + assertNotNull(localAddressAfterBind); + assertTrue(localAddressAfterBind instanceof InetSocketAddress); + assertTrue(((InetSocketAddress) localAddressAfterBind).getPort() != 0); + + future.channel().close().syncUninterruptibly(); + } finally { + group.shutdownGracefully(); + } + } + + private static void checkNotActiveNoLocalRemoteAddress(EpollDatagramChannel channel) throws IOException { + assertFalse(channel.active); + assertNull(channel.localAddress()); + assertNull(channel.remoteAddress()); + channel.fd().close(); + } + + private static final class TestHandler extends ChannelInboundHandlerAdapter { + private volatile SocketAddress localAddress; + + @Override + public void channelRegistered(ChannelHandlerContext ctx) throws Exception { + this.localAddress = ctx.channel().localAddress(); + super.channelRegistered(ctx); + } + } +} diff --git a/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDatagramMulticastIPv6Test.java b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDatagramMulticastIPv6Test.java new file mode 100644 index 0000000..9497078 --- /dev/null +++ b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDatagramMulticastIPv6Test.java @@ -0,0 +1,30 @@ +/* + * Copyright 2019 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.channel.epoll; + +import io.netty.bootstrap.Bootstrap; +import io.netty.testsuite.transport.TestsuitePermutation; +import io.netty.testsuite.transport.socket.DatagramMulticastIPv6Test; + +import java.util.List; + +public class EpollDatagramMulticastIPv6Test extends DatagramMulticastIPv6Test { + + @Override + protected List<TestsuitePermutation.BootstrapComboFactory<Bootstrap, Bootstrap>> newFactories() { + return EpollSocketTestPermutation.INSTANCE.datagram(internetProtocolFamily()); + } +} diff --git a/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDatagramMulticastTest.java b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDatagramMulticastTest.java new file mode 100644 index 0000000..e3ae768 --- /dev/null +++ b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDatagramMulticastTest.java @@ -0,0 +1,29 @@ +/* + * Copyright 2019 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.channel.epoll; + +import io.netty.bootstrap.Bootstrap; +import io.netty.testsuite.transport.TestsuitePermutation; +import io.netty.testsuite.transport.socket.DatagramMulticastTest; + +import java.util.List; + +public class EpollDatagramMulticastTest extends DatagramMulticastTest { + @Override + protected List<TestsuitePermutation.BootstrapComboFactory<Bootstrap, Bootstrap>> newFactories() { + return EpollSocketTestPermutation.INSTANCE.datagram(internetProtocolFamily()); + } +} diff --git a/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDatagramScatteringReadTest.java b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDatagramScatteringReadTest.java new file mode 100644 index 0000000..0c082bd --- /dev/null +++ b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDatagramScatteringReadTest.java @@ -0,0 +1,276 @@ +/* + * Copyright 2019 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.channel.epoll; + +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.AdaptiveRecvByteBufAllocator; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelOption; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.channel.socket.DatagramPacket; +import io.netty.testsuite.transport.TestsuitePermutation; +import io.netty.testsuite.transport.socket.AbstractDatagramTest; +import io.netty.util.internal.PlatformDependent; +import org.junit.Assume; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +public class EpollDatagramScatteringReadTest extends AbstractDatagramTest { + + @BeforeClass + public static void assumeRecvmmsgSupported() { + Assume.assumeTrue(Native.IS_SUPPORTING_RECVMMSG); + } + + @Override + protected List<TestsuitePermutation.BootstrapComboFactory<Bootstrap, Bootstrap>> newFactories() { + return EpollSocketTestPermutation.INSTANCE.epollOnlyDatagram(internetProtocolFamily()); + } + + @Test + public void testScatteringReadPartial() throws Throwable { + run(); + } + + public void testScatteringReadPartial(Bootstrap sb, Bootstrap cb) throws Throwable { + testScatteringRead(sb, cb, false, true); + } + + @Test + public void testScatteringRead() throws Throwable { + run(); + } + + public void testScatteringRead(Bootstrap sb, Bootstrap cb) throws Throwable { + testScatteringRead(sb, cb, false, false); + } + + @Test + public void testScatteringReadConnectedPartial() throws Throwable { + run(); + } + + public void testScatteringReadConnectedPartial(Bootstrap sb, Bootstrap cb) throws Throwable { + testScatteringRead(sb, cb, true, true); + } + + @Test + public void testScatteringConnectedRead() throws Throwable { + run(); + } + + public void testScatteringConnectedRead(Bootstrap sb, Bootstrap cb) throws Throwable { + testScatteringRead(sb, cb, true, false); + } + + private void testScatteringRead(Bootstrap sb, Bootstrap cb, boolean connected, boolean partial) throws Throwable { + int packetSize = 512; + int numPackets = 4; + + sb.option(ChannelOption.RCVBUF_ALLOCATOR, new AdaptiveRecvByteBufAllocator( + packetSize, packetSize * (partial ? numPackets / 2 : numPackets), 64 * 1024)); + sb.option(EpollChannelOption.MAX_DATAGRAM_PAYLOAD_SIZE, packetSize); + + Channel sc = null; + Channel cc = null; + + try { + cb.handler(new SimpleChannelInboundHandler<Object>() { + @Override + public void channelRead0(ChannelHandlerContext ctx, Object msgs) throws Exception { + // Nothing will be sent. + } + }); + cc = cb.bind(newSocketAddress()).sync().channel(); + final SocketAddress ccAddress = cc.localAddress(); + + final AtomicReference<Throwable> errorRef = new AtomicReference<Throwable>(); + final byte[] bytes = new byte[packetSize]; + PlatformDependent.threadLocalRandom().nextBytes(bytes); + + final CountDownLatch latch = new CountDownLatch(numPackets); + sb.handler(new SimpleChannelInboundHandler<DatagramPacket>() { + private int counter; + @Override + public void channelReadComplete(ChannelHandlerContext ctx) { + assertTrue(counter > 1); + counter = 0; + ctx.read(); + } + + @Override + protected void channelRead0(ChannelHandlerContext ctx, DatagramPacket msg) { + assertEquals(ccAddress, msg.sender()); + + assertEquals(bytes.length, msg.content().readableBytes()); + byte[] receivedBytes = new byte[bytes.length]; + msg.content().readBytes(receivedBytes); + assertArrayEquals(bytes, receivedBytes); + + counter++; + latch.countDown(); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + errorRef.compareAndSet(null, cause); + } + }); + + sb.option(ChannelOption.AUTO_READ, false); + sc = sb.bind(newSocketAddress()).sync().channel(); + + if (connected) { + sc.connect(cc.localAddress()).syncUninterruptibly(); + } + + InetSocketAddress addr = (InetSocketAddress) sc.localAddress(); + + List<ChannelFuture> futures = new ArrayList<ChannelFuture>(numPackets); + for (int i = 0; i < numPackets; i++) { + futures.add(cc.write(new DatagramPacket(cc.alloc().directBuffer().writeBytes(bytes), addr))); + } + + cc.flush(); + + for (ChannelFuture f: futures) { + f.sync(); + } + + // Enable autoread now which also triggers a read, this should cause scattering reads (recvmmsg) to happen. + sc.config().setAutoRead(true); + + if (!latch.await(10, TimeUnit.SECONDS)) { + Throwable error = errorRef.get(); + if (error != null) { + throw error; + } + fail("Timeout while waiting for packets"); + } + } finally { + if (cc != null) { + cc.close().syncUninterruptibly(); + } + if (sc != null) { + sc.close().syncUninterruptibly(); + } + } + } + + @Test + public void testScatteringReadWithSmallBuffer() throws Throwable { + run(); + } + + public void testScatteringReadWithSmallBuffer(Bootstrap sb, Bootstrap cb) throws Throwable { + testScatteringReadWithSmallBuffer0(sb, cb, false); + } + + @Test + public void testScatteringConnectedReadWithSmallBuffer() throws Throwable { + run(); + } + + public void testScatteringConnectedReadWithSmallBuffer(Bootstrap sb, Bootstrap cb) throws Throwable { + testScatteringReadWithSmallBuffer0(sb, cb, true); + } + + private void testScatteringReadWithSmallBuffer0(Bootstrap sb, Bootstrap cb, boolean connected) throws Throwable { + int packetSize = 16; + + sb.option(ChannelOption.RCVBUF_ALLOCATOR, new AdaptiveRecvByteBufAllocator(1400, 1400, 64 * 1024)); + sb.option(EpollChannelOption.MAX_DATAGRAM_PAYLOAD_SIZE, 1400); + + Channel sc = null; + Channel cc = null; + + try { + cb.handler(new SimpleChannelInboundHandler<Object>() { + @Override + public void channelRead0(ChannelHandlerContext ctx, Object msgs) { + // Nothing will be sent. + } + }); + cc = cb.bind(newSocketAddress()).sync().channel(); + final SocketAddress ccAddress = cc.localAddress(); + + final AtomicReference<Throwable> errorRef = new AtomicReference<Throwable>(); + final byte[] bytes = new byte[packetSize]; + PlatformDependent.threadLocalRandom().nextBytes(bytes); + + final CountDownLatch latch = new CountDownLatch(1); + sb.handler(new SimpleChannelInboundHandler<DatagramPacket>() { + + @Override + protected void channelRead0(ChannelHandlerContext ctx, DatagramPacket msg) { + assertEquals(ccAddress, msg.sender()); + + assertEquals(bytes.length, msg.content().readableBytes()); + byte[] receivedBytes = new byte[bytes.length]; + msg.content().readBytes(receivedBytes); + assertArrayEquals(bytes, receivedBytes); + + latch.countDown(); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + errorRef.compareAndSet(null, cause); + } + }); + + sc = sb.bind(newSocketAddress()).sync().channel(); + + if (connected) { + sc.connect(cc.localAddress()).syncUninterruptibly(); + } + + InetSocketAddress addr = (InetSocketAddress) sc.localAddress(); + + cc.writeAndFlush(new DatagramPacket(cc.alloc().directBuffer().writeBytes(bytes), addr)).sync(); + + if (!latch.await(10, TimeUnit.SECONDS)) { + Throwable error = errorRef.get(); + if (error != null) { + throw error; + } + fail("Timeout while waiting for packets"); + } + } finally { + if (cc != null) { + cc.close().syncUninterruptibly(); + } + if (sc != null) { + sc.close().syncUninterruptibly(); + } + } + } +} diff --git a/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDatagramUnicastIPv6MappedTest.java b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDatagramUnicastIPv6MappedTest.java new file mode 100644 index 0000000..fd2a8f3 --- /dev/null +++ b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDatagramUnicastIPv6MappedTest.java @@ -0,0 +1,29 @@ +/* + * Copyright 2019 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.channel.epoll; + +import io.netty.bootstrap.Bootstrap; +import io.netty.testsuite.transport.TestsuitePermutation.BootstrapComboFactory; +import io.netty.testsuite.transport.socket.DatagramUnicastIPv6MappedTest; + +import java.util.List; + +public class EpollDatagramUnicastIPv6MappedTest extends DatagramUnicastIPv6MappedTest { + @Override + protected List<BootstrapComboFactory<Bootstrap, Bootstrap>> newFactories() { + return EpollSocketTestPermutation.INSTANCE.datagram(internetProtocolFamily()); + } +} diff --git a/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDatagramUnicastIPv6Test.java b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDatagramUnicastIPv6Test.java new file mode 100644 index 0000000..29e0a51 --- /dev/null +++ b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDatagramUnicastIPv6Test.java @@ -0,0 +1,29 @@ +/* + * Copyright 2019 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.channel.epoll; + +import io.netty.bootstrap.Bootstrap; +import io.netty.testsuite.transport.TestsuitePermutation; +import io.netty.testsuite.transport.socket.DatagramUnicastIPv6Test; + +import java.util.List; + +public class EpollDatagramUnicastIPv6Test extends DatagramUnicastIPv6Test { + @Override + protected List<TestsuitePermutation.BootstrapComboFactory<Bootstrap, Bootstrap>> newFactories() { + return EpollSocketTestPermutation.INSTANCE.datagram(internetProtocolFamily()); + } +} diff --git a/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDatagramUnicastTest.java b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDatagramUnicastTest.java index a8ed58e..9322a7d 100644 --- a/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDatagramUnicastTest.java +++ b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDatagramUnicastTest.java @@ -16,6 +16,7 @@ package io.netty.channel.epoll; import io.netty.bootstrap.Bootstrap; +import io.netty.channel.socket.InternetProtocolFamily; import io.netty.testsuite.transport.TestsuitePermutation; import io.netty.testsuite.transport.socket.DatagramUnicastTest; @@ -24,7 +25,7 @@ import java.util.List; public class EpollDatagramUnicastTest extends DatagramUnicastTest { @Override protected List<TestsuitePermutation.BootstrapComboFactory<Bootstrap, Bootstrap>> newFactories() { - return EpollSocketTestPermutation.INSTANCE.datagram(); + return EpollSocketTestPermutation.INSTANCE.datagram(InternetProtocolFamily.IPv4); } public void testSimpleSendWithConnect(Bootstrap sb, Bootstrap cb) throws Throwable { diff --git a/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDomainSocketReuseFdTest.java b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDomainSocketReuseFdTest.java new file mode 100644 index 0000000..487ea64 --- /dev/null +++ b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDomainSocketReuseFdTest.java @@ -0,0 +1,36 @@ +/* + * Copyright 2019 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.channel.epoll; + +import io.netty.bootstrap.Bootstrap; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.testsuite.transport.TestsuitePermutation; +import io.netty.testsuite.transport.socket.AbstractSocketReuseFdTest; + +import java.net.SocketAddress; +import java.util.List; + +public class EpollDomainSocketReuseFdTest extends AbstractSocketReuseFdTest { + @Override + protected SocketAddress newSocketAddress() { + return EpollSocketTestPermutation.newSocketAddress(); + } + + @Override + protected List<TestsuitePermutation.BootstrapComboFactory<ServerBootstrap, Bootstrap>> newFactories() { + return EpollSocketTestPermutation.INSTANCE.domainSocket(); + } +} diff --git a/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDomainSocketShutdownOutputByPeerTest.java b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDomainSocketShutdownOutputByPeerTest.java new file mode 100644 index 0000000..7d0d483 --- /dev/null +++ b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDomainSocketShutdownOutputByPeerTest.java @@ -0,0 +1,69 @@ +/* + * Copyright 2019 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.channel.epoll; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.unix.Buffer; +import io.netty.testsuite.transport.TestsuitePermutation.BootstrapFactory; +import io.netty.testsuite.transport.socket.AbstractSocketShutdownOutputByPeerTest; + +import java.io.IOException; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.util.List; + +public class EpollDomainSocketShutdownOutputByPeerTest extends AbstractSocketShutdownOutputByPeerTest<LinuxSocket> { + + @Override + protected List<BootstrapFactory<ServerBootstrap>> newFactories() { + return EpollSocketTestPermutation.INSTANCE.serverDomainSocket(); + } + + @Override + protected SocketAddress newSocketAddress() { + return EpollSocketTestPermutation.newSocketAddress(); + } + + @Override + protected void shutdownOutput(LinuxSocket s) throws IOException { + s.shutdown(false, true); + } + + @Override + protected void connect(LinuxSocket s, SocketAddress address) throws IOException { + s.connect(address); + } + + @Override + protected void close(LinuxSocket s) throws IOException { + s.close(); + } + + @Override + protected void write(LinuxSocket s, int data) throws IOException { + final ByteBuffer buf = Buffer.allocateDirectWithNativeOrder(4); + buf.putInt(data); + buf.flip(); + s.write(buf, buf.position(), buf.limit()); + Buffer.free(buf); + } + + @Override + protected LinuxSocket newSocket() { + return LinuxSocket.newSocketDomain(); + } +} diff --git a/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDomainSocketSslClientRenegotiateTest.java b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDomainSocketSslClientRenegotiateTest.java new file mode 100644 index 0000000..3f493d0 --- /dev/null +++ b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDomainSocketSslClientRenegotiateTest.java @@ -0,0 +1,42 @@ +/* + * Copyright 2019 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.channel.epoll; + +import io.netty.bootstrap.Bootstrap; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.handler.ssl.SslContext; +import io.netty.testsuite.transport.TestsuitePermutation; +import io.netty.testsuite.transport.socket.SocketSslClientRenegotiateTest; + +import java.net.SocketAddress; +import java.util.List; + +public class EpollDomainSocketSslClientRenegotiateTest extends SocketSslClientRenegotiateTest { + + public EpollDomainSocketSslClientRenegotiateTest(SslContext serverCtx, SslContext clientCtx, boolean delegate) { + super(serverCtx, clientCtx, delegate); + } + + @Override + protected List<TestsuitePermutation.BootstrapComboFactory<ServerBootstrap, Bootstrap>> newFactories() { + return EpollSocketTestPermutation.INSTANCE.domainSocket(); + } + + @Override + protected SocketAddress newSocketAddress() { + return EpollSocketTestPermutation.newSocketAddress(); + } +} diff --git a/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDomainSocketSslGreetingTest.java b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDomainSocketSslGreetingTest.java index a1ed0c5..4683a70 100644 --- a/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDomainSocketSslGreetingTest.java +++ b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDomainSocketSslGreetingTest.java @@ -26,8 +26,8 @@ import java.util.List; public class EpollDomainSocketSslGreetingTest extends SocketSslGreetingTest { - public EpollDomainSocketSslGreetingTest(SslContext serverCtx, SslContext clientCtx) { - super(serverCtx, clientCtx); + public EpollDomainSocketSslGreetingTest(SslContext serverCtx, SslContext clientCtx, boolean delegate) { + super(serverCtx, clientCtx, delegate); } @Override diff --git a/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollEventLoopTest.java b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollEventLoopTest.java index 4e51114..c6bc431 100644 --- a/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollEventLoopTest.java +++ b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollEventLoopTest.java @@ -18,20 +18,42 @@ package io.netty.channel.epoll; import io.netty.channel.DefaultSelectStrategyFactory; import io.netty.channel.EventLoop; import io.netty.channel.EventLoopGroup; +import io.netty.channel.ServerChannel; +import io.netty.channel.socket.ServerSocketChannel; +import io.netty.channel.unix.FileDescriptor; +import io.netty.testsuite.transport.AbstractSingleThreadEventLoopTest; import io.netty.util.concurrent.DefaultThreadFactory; import io.netty.util.concurrent.Future; import io.netty.util.concurrent.RejectedExecutionHandlers; import io.netty.util.concurrent.ThreadPerTaskExecutor; import org.junit.Test; +import java.io.IOException; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; -public class EpollEventLoopTest { +public class EpollEventLoopTest extends AbstractSingleThreadEventLoopTest { + + @Override + protected EventLoopGroup newEventLoopGroup() { + return new EpollEventLoopGroup(); + } + + @Override + protected ServerSocketChannel newChannel() { + return new EpollServerSocketChannel(); + } + + @Override + protected Class<? extends ServerChannel> serverChannelClass() { + return EpollServerSocketChannel.class; + } @Test public void testScheduleBigDelayNotOverflow() { @@ -39,7 +61,7 @@ public class EpollEventLoopTest { final EventLoopGroup group = new EpollEventLoop(null, new ThreadPerTaskExecutor(new DefaultThreadFactory(getClass())), 0, - DefaultSelectStrategyFactory.INSTANCE.newSelectStrategy(), RejectedExecutionHandlers.reject()) { + DefaultSelectStrategyFactory.INSTANCE.newSelectStrategy(), RejectedExecutionHandlers.reject(), null) { @Override void handleLoopException(Throwable t) { capture.set(t); @@ -63,4 +85,54 @@ public class EpollEventLoopTest { group.shutdownGracefully(); } } + + @Test + public void testEventFDETSemantics() throws Throwable { + final FileDescriptor epoll = Native.newEpollCreate(); + final FileDescriptor eventFd = Native.newEventFd(); + final FileDescriptor timerFd = Native.newTimerFd(); + final EpollEventArray array = new EpollEventArray(1024); + try { + Native.epollCtlAdd(epoll.intValue(), eventFd.intValue(), Native.EPOLLIN | Native.EPOLLET); + final AtomicReference<Throwable> causeRef = new AtomicReference<Throwable>(); + final AtomicInteger integer = new AtomicInteger(); + final Thread t = new Thread(new Runnable() { + @Override + public void run() { + try { + for (int i = 0; i < 2; i++) { + int ready = Native.epollWait(epoll, array, timerFd, -1, -1); + assertEquals(1, ready); + assertEquals(eventFd.intValue(), array.fd(0)); + integer.incrementAndGet(); + } + } catch (IOException e) { + causeRef.set(e); + } + } + }); + t.start(); + Native.eventFdWrite(eventFd.intValue(), 1); + + // Spin until we was the wakeup. + while (integer.get() != 1) { + Thread.sleep(10); + } + // Sleep for a short moment to ensure there is not other wakeup. + Thread.sleep(1000); + assertEquals(1, integer.get()); + Native.eventFdWrite(eventFd.intValue(), 1); + t.join(); + Throwable cause = causeRef.get(); + if (cause != null) { + throw cause; + } + assertEquals(2, integer.get()); + } finally { + array.free(); + epoll.close(); + eventFd.close(); + timerFd.close(); + } + } } diff --git a/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollReuseAddrTest.java b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollReuseAddrTest.java index c6f47cd..1abad07 100644 --- a/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollReuseAddrTest.java +++ b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollReuseAddrTest.java @@ -23,6 +23,8 @@ import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerAdapter; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; import io.netty.util.NetUtil; import io.netty.util.ReferenceCountUtil; import io.netty.util.ResourceLeakDetector; @@ -78,7 +80,7 @@ public class EpollReuseAddrTest { } private static void testMultipleBindDatagramChannelWithoutReusePortFails0(AbstractBootstrap<?, ?> bootstrap) { - bootstrap.handler(new DummyHandler()); + bootstrap.handler(new LoggingHandler(LogLevel.ERROR)); ChannelFuture future = bootstrap.bind().syncUninterruptibly(); try { bootstrap.bind(future.channel().localAddress()).syncUninterruptibly(); diff --git a/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollSocketSslClientRenegotiateTest.java b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollSocketSslClientRenegotiateTest.java new file mode 100644 index 0000000..3f69196 --- /dev/null +++ b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollSocketSslClientRenegotiateTest.java @@ -0,0 +1,36 @@ +/* + * Copyright 2019 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.channel.epoll; + +import io.netty.bootstrap.Bootstrap; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.handler.ssl.SslContext; +import io.netty.testsuite.transport.TestsuitePermutation; +import io.netty.testsuite.transport.socket.SocketSslClientRenegotiateTest; + +import java.util.List; + +public class EpollSocketSslClientRenegotiateTest extends SocketSslClientRenegotiateTest { + + public EpollSocketSslClientRenegotiateTest(SslContext serverCtx, SslContext clientCtx, boolean delegate) { + super(serverCtx, clientCtx, delegate); + } + + @Override + protected List<TestsuitePermutation.BootstrapComboFactory<ServerBootstrap, Bootstrap>> newFactories() { + return EpollSocketTestPermutation.INSTANCE.socket(); + } +} diff --git a/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollSocketSslGreetingTest.java b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollSocketSslGreetingTest.java index 21d86b4..34bf98a 100644 --- a/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollSocketSslGreetingTest.java +++ b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollSocketSslGreetingTest.java @@ -25,8 +25,8 @@ import java.util.List; public class EpollSocketSslGreetingTest extends SocketSslGreetingTest { - public EpollSocketSslGreetingTest(SslContext serverCtx, SslContext clientCtx) { - super(serverCtx, clientCtx); + public EpollSocketSslGreetingTest(SslContext serverCtx, SslContext clientCtx, boolean delegate) { + super(serverCtx, clientCtx, delegate); } @Override diff --git a/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollSocketSslSessionReuseTest.java b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollSocketSslSessionReuseTest.java new file mode 100644 index 0000000..2b78224 --- /dev/null +++ b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollSocketSslSessionReuseTest.java @@ -0,0 +1,36 @@ +/* + * Copyright 2019 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.channel.epoll; + +import io.netty.bootstrap.Bootstrap; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.handler.ssl.SslContext; +import io.netty.testsuite.transport.TestsuitePermutation; +import io.netty.testsuite.transport.socket.SocketSslSessionReuseTest; + +import java.util.List; + +public class EpollSocketSslSessionReuseTest extends SocketSslSessionReuseTest { + + public EpollSocketSslSessionReuseTest(SslContext serverCtx, SslContext clientCtx) { + super(serverCtx, clientCtx); + } + + @Override + protected List<TestsuitePermutation.BootstrapComboFactory<ServerBootstrap, Bootstrap>> newFactories() { + return EpollSocketTestPermutation.INSTANCE.socket(); + } +} diff --git a/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollSocketTestPermutation.java b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollSocketTestPermutation.java index 225c1d2..e5c5ed4 100644 --- a/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollSocketTestPermutation.java +++ b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollSocketTestPermutation.java @@ -36,7 +36,6 @@ import io.netty.util.internal.logging.InternalLoggerFactory; import java.io.BufferedReader; import java.io.File; import java.io.FileReader; -import java.io.IOException; import java.security.AccessController; import java.security.PrivilegedAction; import java.util.ArrayList; @@ -119,7 +118,8 @@ class EpollSocketTestPermutation extends SocketTestPermutation { } @Override - public List<TestsuitePermutation.BootstrapComboFactory<Bootstrap, Bootstrap>> datagram() { + public List<TestsuitePermutation.BootstrapComboFactory<Bootstrap, Bootstrap>> datagram( + final InternetProtocolFamily family) { // Make the list of Bootstrap factories. @SuppressWarnings("unchecked") List<BootstrapFactory<Bootstrap>> bfs = Arrays.asList( @@ -129,7 +129,7 @@ class EpollSocketTestPermutation extends SocketTestPermutation { return new Bootstrap().group(nioWorkerGroup).channelFactory(new ChannelFactory<Channel>() { @Override public Channel newChannel() { - return new NioDatagramChannel(InternetProtocolFamily.IPv4); + return new NioDatagramChannel(family); } @Override @@ -142,13 +142,48 @@ class EpollSocketTestPermutation extends SocketTestPermutation { new BootstrapFactory<Bootstrap>() { @Override public Bootstrap newInstance() { - return new Bootstrap().group(EPOLL_WORKER_GROUP).channel(EpollDatagramChannel.class); + return new Bootstrap().group(EPOLL_WORKER_GROUP).channelFactory(new ChannelFactory<Channel>() { + @Override + public Channel newChannel() { + return new EpollDatagramChannel(family); + } + + @Override + public String toString() { + return InternetProtocolFamily.class.getSimpleName() + ".class"; + } + }); } } ); return combo(bfs, bfs); } + List<TestsuitePermutation.BootstrapComboFactory<Bootstrap, Bootstrap>> epollOnlyDatagram( + final InternetProtocolFamily family) { + return combo(Collections.singletonList(datagramBootstrapFactory(family)), + Collections.singletonList(datagramBootstrapFactory(family))); + } + + private BootstrapFactory<Bootstrap> datagramBootstrapFactory(final InternetProtocolFamily family) { + return new BootstrapFactory<Bootstrap>() { + @Override + public Bootstrap newInstance() { + return new Bootstrap().group(EPOLL_WORKER_GROUP).channelFactory(new ChannelFactory<Channel>() { + @Override + public Channel newChannel() { + return new EpollDatagramChannel(family); + } + + @Override + public String toString() { + return InternetProtocolFamily.class.getSimpleName() + ".class"; + } + }); + } + }; + } + public List<TestsuitePermutation.BootstrapComboFactory<ServerBootstrap, Bootstrap>> domainSocket() { List<TestsuitePermutation.BootstrapComboFactory<ServerBootstrap, Bootstrap>> list = diff --git a/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollSpliceTest.java b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollSpliceTest.java index c53ff1e..a8d9d72 100644 --- a/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollSpliceTest.java +++ b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollSpliceTest.java @@ -27,7 +27,6 @@ import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.channel.EventLoopGroup; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.channel.unix.FileDescriptor; -import io.netty.testsuite.util.TestUtils; import io.netty.util.NetUtil; import org.junit.Assert; import org.junit.Test; @@ -190,7 +189,7 @@ public class EpollSpliceTest { } } - @Test + @Test(timeout = 10000) public void spliceToFile() throws Throwable { EventLoopGroup group = new EpollEventLoopGroup(1); File file = File.createTempFile("netty-splice", null); @@ -216,7 +215,7 @@ public class EpollSpliceTest { i += length; } - while (sh.future == null || !sh.future.isDone()) { + while (sh.future2 == null || !sh.future2.isDone() || !sh.future.isDone()) { if (sh.exception.get() != null) { break; } @@ -292,22 +291,22 @@ public class EpollSpliceTest { private static class SpliceHandler extends ChannelInboundHandlerAdapter { private final File file; - volatile Channel channel; volatile ChannelFuture future; + volatile ChannelFuture future2; final AtomicReference<Throwable> exception = new AtomicReference<Throwable>(); - public SpliceHandler(File file) { + SpliceHandler(File file) { this.file = file; } @Override - public void channelActive(ChannelHandlerContext ctx) - throws Exception { - channel = ctx.channel(); + public void channelActive(ChannelHandlerContext ctx) throws Exception { final EpollSocketChannel ch = (EpollSocketChannel) ctx.channel(); final FileDescriptor fd = FileDescriptor.from(file); - future = ch.spliceTo(fd, 0, data.length); + // splice two halves separately to test starting offset + future = ch.spliceTo(fd, 0, data.length / 2); + future2 = ch.spliceTo(fd, data.length / 2, data.length / 2); } @Override diff --git a/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollTest.java b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollTest.java index 5a9cb19..f66a55e 100644 --- a/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollTest.java +++ b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 The Netty Project + * Copyright 2019 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 @@ -47,7 +47,7 @@ public class EpollTest { @Override public void run() { try { - assertEquals(1, Native.epollWait(epoll, eventArray, timerFd, -1, -1)); + assertEquals(1, Native.epollWait(epoll, eventArray, false)); // This should have been woken up because of eventfd_write. assertEquals(eventfd.intValue(), eventArray.fd(0)); } catch (Throwable cause) { diff --git a/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollWriteBeforeRegisteredTest.java b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollWriteBeforeRegisteredTest.java new file mode 100644 index 0000000..b1943fd --- /dev/null +++ b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollWriteBeforeRegisteredTest.java @@ -0,0 +1,30 @@ +/* + * Copyright 2019 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.channel.epoll; + +import io.netty.bootstrap.Bootstrap; +import io.netty.testsuite.transport.TestsuitePermutation; +import io.netty.testsuite.transport.socket.WriteBeforeRegisteredTest; + +import java.util.List; + +public class EpollWriteBeforeRegisteredTest extends WriteBeforeRegisteredTest { + + @Override + protected List<TestsuitePermutation.BootstrapFactory<Bootstrap>> newFactories() { + return EpollSocketTestPermutation.INSTANCE.clientSocket(); + } +} diff --git a/transport-native-kqueue/pom.xml b/transport-native-kqueue/pom.xml index 3efef19..af537c1 100644 --- a/transport-native-kqueue/pom.xml +++ b/transport-native-kqueue/pom.xml @@ -19,7 +19,7 @@ <parent> <groupId>io.netty</groupId> <artifactId>netty-parent</artifactId> - <version>4.1.33.Final</version> + <version>4.1.48.Final</version> </parent> <artifactId>netty-transport-native-kqueue</artifactId> @@ -71,7 +71,7 @@ <id>build-native-lib</id> <configuration> <name>netty_transport_native_kqueue_${os.detected.arch}</name> - <nativeSourceDirectory>${project.basedir}/src/main/c</nativeSourceDirectory> + <nativeSourceDirectory>${nativeSourceDirectory}</nativeSourceDirectory> <libDirectory>${project.build.outputDirectory}</libDirectory> <!-- We use Maven's artifact classifier instead. This hack will make the hawtjni plugin to put the native library @@ -84,13 +84,13 @@ explicitly set the target platform. Otherwise we may get fatal link errors due to weakly linked methods which are not expected to be present on MacOS (e.g. accept4). --> <arg>MACOSX_DEPLOYMENT_TARGET=10.6</arg> + <configureArg>--libdir=${project.build.directory}/native-build/target/lib</configureArg> </configureArgs> </configuration> <goals> <goal>generate</goal> <goal>build</goal> </goals> - <phase>compile</phase> </execution> </executions> </plugin> @@ -181,7 +181,7 @@ <id>build-native-lib</id> <configuration> <name>netty_transport_native_kqueue_${os.detected.arch}</name> - <nativeSourceDirectory>${project.basedir}/src/main/c</nativeSourceDirectory> + <nativeSourceDirectory>${nativeSourceDirectory}</nativeSourceDirectory> <libDirectory>${project.build.outputDirectory}</libDirectory> <!-- We use Maven's artifact classifier instead. This hack will make the hawtjni plugin to put the native library @@ -198,7 +198,6 @@ <goal>generate</goal> <goal>build</goal> </goals> - <phase>compile</phase> </execution> </executions> </plugin> @@ -288,7 +287,7 @@ <id>build-native-lib</id> <configuration> <name>netty_transport_native_kqueue_${os.detected.arch}</name> - <nativeSourceDirectory>${project.basedir}/src/main/c</nativeSourceDirectory> + <nativeSourceDirectory>${nativeSourceDirectory}</nativeSourceDirectory> <libDirectory>${project.build.outputDirectory}</libDirectory> <!-- We use Maven's artifact classifier instead. This hack will make the hawtjni plugin to put the native library @@ -305,7 +304,6 @@ <goal>generate</goal> <goal>build</goal> </goals> - <phase>compile</phase> </execution> </executions> </plugin> @@ -355,14 +353,15 @@ <properties> <javaModuleName>io.netty.transport.kqueue</javaModuleName> - <!-- Needed by the native transport as we need the memoryAddress of the ByteBuffer --> - <argLine.java9.extras>--add-exports java.base/sun.security.x509=ALL-UNNAMED --add-opens=java.base/java.nio=ALL-UNNAMED</argLine.java9.extras> + <!-- Needed as we use SelfSignedCertificate in our tests --> + <argLine.java9.extras>--add-exports java.base/sun.security.x509=ALL-UNNAMED</argLine.java9.extras> <unix.common.lib.name>netty-unix-common</unix.common.lib.name> <unix.common.lib.dir>${project.build.directory}/unix-common-lib</unix.common.lib.dir> <unix.common.lib.unpacked.dir>${unix.common.lib.dir}/META-INF/native/lib</unix.common.lib.unpacked.dir> <unix.common.include.unpacked.dir>${unix.common.lib.dir}/META-INF/native/include</unix.common.include.unpacked.dir> <jni.compiler.args.cflags>CFLAGS=-O3 -Werror -fno-omit-frame-pointer -Wunused-variable -fvisibility=hidden -I${unix.common.include.unpacked.dir}</jni.compiler.args.cflags> <jni.compiler.args.ldflags>LDFLAGS=-z now -L${unix.common.lib.unpacked.dir} -Wl,--whole-archive -l${unix.common.lib.name} -Wl,--no-whole-archive</jni.compiler.args.ldflags> + <nativeSourceDirectory>${project.basedir}/src/main/c</nativeSourceDirectory> <skipTests>true</skipTests> </properties> @@ -409,6 +408,24 @@ <build> <plugins> + <!-- Also include c files in source jar --> + <plugin> + <groupId>org.codehaus.mojo</groupId> + <artifactId>build-helper-maven-plugin</artifactId> + <executions> + <execution> + <phase>generate-sources</phase> + <goals> + <goal>add-source</goal> + </goals> + <configuration> + <sources> + <source>${nativeSourceDirectory}</source> + </sources> + </configuration> + </execution> + </executions> + </plugin> <plugin> <artifactId>maven-jar-plugin</artifactId> <executions> diff --git a/transport-native-kqueue/src/main/c/netty_kqueue_bsdsocket.c b/transport-native-kqueue/src/main/c/netty_kqueue_bsdsocket.c index e522896..53a3769 100644 --- a/transport-native-kqueue/src/main/c/netty_kqueue_bsdsocket.c +++ b/transport-native-kqueue/src/main/c/netty_kqueue_bsdsocket.c @@ -33,7 +33,7 @@ #include "netty_unix_util.h" // Those are initialized in the init(...) method and cached for performance reasons -static jclass stringCls = NULL; +static jclass stringClass = NULL; static jclass peerCredentialsClass = NULL; static jfieldID fileChannelFieldId = NULL; static jfieldID transferredFieldId = NULL; @@ -108,9 +108,20 @@ static jobjectArray netty_kqueue_bsdsocket_getAcceptFilter(JNIEnv* env, jclass c netty_unix_errors_throwChannelExceptionErrorNo(env, "getsockopt() failed: ", errno); return NULL; } - jobjectArray resultArray = (*env)->NewObjectArray(env, 2, stringCls, NULL); - (*env)->SetObjectArrayElement(env, resultArray, 0, (*env)->NewStringUTF(env, &af.af_name[0])); - (*env)->SetObjectArrayElement(env, resultArray, 1, (*env)->NewStringUTF(env, &af.af_arg[0])); + jobjectArray resultArray = (*env)->NewObjectArray(env, 2, stringClass, NULL); + if (resultArray == NULL) { + return NULL; + } + jstring name = (*env)->NewStringUTF(env, &af.af_name[0]); + if (name == NULL) { + return NULL; + } + jstring arg = (*env)->NewStringUTF(env, &af.af_arg[0]); + if (arg == NULL) { + return NULL; + } + (*env)->SetObjectArrayElement(env, resultArray, 0, name); + (*env)->SetObjectArrayElement(env, resultArray, 1, arg); return resultArray; #else // No know replacement on MacOS // Don't throw here because this is used when getting a list of all options. @@ -151,16 +162,28 @@ static jobject netty_kqueue_bsdsocket_getPeerCredentials(JNIEnv *env, jclass cla } jintArray gids = NULL; if (credentials.cr_ngroups > 1) { - gids = (*env)->NewIntArray(env, credentials.cr_ngroups); + if ((gids = (*env)->NewIntArray(env, credentials.cr_ngroups)) == NULL) { + return NULL; + } (*env)->SetIntArrayRegion(env, gids, 0, credentials.cr_ngroups, (jint*) credentials.cr_groups); } else { // It has been observed on MacOS that cr_ngroups may not be set, but the cr_gid field is set. - gids = (*env)->NewIntArray(env, 1); + if ((gids = (*env)->NewIntArray(env, 1)) == NULL) { + return NULL; + } (*env)->SetIntArrayRegion(env, gids, 0, 1, (jint*) &credentials.cr_gid); } - // TODO: getting the PID may require reading/sending "ancillary data" via SCM_CREDENTIALS which is not desirable. - return (*env)->NewObject(env, peerCredentialsClass, peerCredentialsMethodId, 0, credentials.cr_uid, gids); + pid_t pid = 0; +#ifdef LOCAL_PEERPID + socklen_t len = sizeof(pid); + // Getting the LOCAL_PEERPID is expected to return error in some cases (e.g. server socket FDs) - just return 0. + if (netty_unix_socket_getOption0(fd, SOCK_STREAM, LOCAL_PEERPID, &pid, len) < 0) { + pid = 0; + } +#endif + + return (*env)->NewObject(env, peerCredentialsClass, peerCredentialsMethodId, pid, credentials.cr_uid, gids); } // JNI Registered Methods End @@ -181,121 +204,87 @@ static jint dynamicMethodsTableSize() { } static JNINativeMethod* createDynamicMethodsTable(const char* packagePrefix) { - JNINativeMethod* dynamicMethods = malloc(sizeof(JNINativeMethod) * dynamicMethodsTableSize()); + char* dynamicTypeName = NULL; + size_t size = sizeof(JNINativeMethod) * dynamicMethodsTableSize(); + JNINativeMethod* dynamicMethods = malloc(size); + if (dynamicMethods == NULL) { + return NULL; + } + memset(dynamicMethods, 0, size); memcpy(dynamicMethods, fixed_method_table, sizeof(fixed_method_table)); - char* dynamicTypeName = netty_unix_util_prepend(packagePrefix, "io/netty/channel/DefaultFileRegion;JJJ)J"); + JNINativeMethod* dynamicMethod = &dynamicMethods[fixed_method_table_size]; + NETTY_PREPEND(packagePrefix, "io/netty/channel/DefaultFileRegion;JJJ)J", dynamicTypeName, error); + NETTY_PREPEND("(IL", dynamicTypeName, dynamicMethod->signature, error); dynamicMethod->name = "sendFile"; - dynamicMethod->signature = netty_unix_util_prepend("(IL", dynamicTypeName); dynamicMethod->fnPtr = (void *) netty_kqueue_bsdsocket_sendFile; - free(dynamicTypeName); + netty_unix_util_free_dynamic_name(&dynamicTypeName); ++dynamicMethod; - dynamicTypeName = netty_unix_util_prepend(packagePrefix, "io/netty/channel/unix/PeerCredentials;"); + NETTY_PREPEND(packagePrefix, "io/netty/channel/unix/PeerCredentials;", dynamicTypeName, error); + NETTY_PREPEND("(I)L", dynamicTypeName, dynamicMethod->signature, error); dynamicMethod->name = "getPeerCredentials"; - dynamicMethod->signature = netty_unix_util_prepend("(I)L", dynamicTypeName); dynamicMethod->fnPtr = (void *) netty_kqueue_bsdsocket_getPeerCredentials; - free(dynamicTypeName); + netty_unix_util_free_dynamic_name(&dynamicTypeName); return dynamicMethods; +error: + free(dynamicTypeName); + netty_unix_util_free_dynamic_methods_table(dynamicMethods, fixed_method_table_size, dynamicMethodsTableSize()); + return NULL; } -static void freeDynamicMethodsTable(JNINativeMethod* dynamicMethods) { - jint fullMethodTableSize = dynamicMethodsTableSize(); - jint i = fixed_method_table_size; - for (; i < fullMethodTableSize; ++i) { - free(dynamicMethods[i].signature); - } - free(dynamicMethods); -} // JNI Method Registration Table End jint netty_kqueue_bsdsocket_JNI_OnLoad(JNIEnv* env, const char* packagePrefix) { + int ret = JNI_ERR; + char* nettyClassName = NULL; + jclass fileRegionCls = NULL; + jclass fileChannelCls = NULL; + jclass fileDescriptorCls = NULL; // Register the methods which are not referenced by static member variables JNINativeMethod* dynamicMethods = createDynamicMethodsTable(packagePrefix); + if (dynamicMethods == NULL) { + goto done; + } if (netty_unix_util_register_natives(env, packagePrefix, "io/netty/channel/kqueue/BsdSocket", dynamicMethods, dynamicMethodsTableSize()) != 0) { - freeDynamicMethodsTable(dynamicMethods); - return JNI_ERR; + goto done; } - freeDynamicMethodsTable(dynamicMethods); - dynamicMethods = NULL; // Initialize this module - char* nettyClassName = netty_unix_util_prepend(packagePrefix, "io/netty/channel/DefaultFileRegion"); - jclass fileRegionCls = (*env)->FindClass(env, nettyClassName); - free(nettyClassName); - nettyClassName = NULL; - if (fileRegionCls == NULL) { - return JNI_ERR; - } - fileChannelFieldId = (*env)->GetFieldID(env, fileRegionCls, "file", "Ljava/nio/channels/FileChannel;"); - if (fileChannelFieldId == NULL) { - netty_unix_errors_throwRuntimeException(env, "failed to get field ID: DefaultFileRegion.file"); - return JNI_ERR; - } - transferredFieldId = (*env)->GetFieldID(env, fileRegionCls, "transferred", "J"); - if (transferredFieldId == NULL) { - netty_unix_errors_throwRuntimeException(env, "failed to get field ID: DefaultFileRegion.transferred"); - return JNI_ERR; - } + NETTY_PREPEND(packagePrefix, "io/netty/channel/DefaultFileRegion", nettyClassName, done); + NETTY_FIND_CLASS(env, fileRegionCls, nettyClassName, done); + netty_unix_util_free_dynamic_name(&nettyClassName); - jclass fileChannelCls = (*env)->FindClass(env, "sun/nio/ch/FileChannelImpl"); - if (fileChannelCls == NULL) { - // pending exception... - return JNI_ERR; - } - fileDescriptorFieldId = (*env)->GetFieldID(env, fileChannelCls, "fd", "Ljava/io/FileDescriptor;"); - if (fileDescriptorFieldId == NULL) { - netty_unix_errors_throwRuntimeException(env, "failed to get field ID: FileChannelImpl.fd"); - return JNI_ERR; - } + NETTY_GET_FIELD(env, fileRegionCls, fileChannelFieldId, "file", "Ljava/nio/channels/FileChannel;", done); + NETTY_GET_FIELD(env, fileRegionCls, transferredFieldId, "transferred", "J", done); - jclass fileDescriptorCls = (*env)->FindClass(env, "java/io/FileDescriptor"); - if (fileDescriptorCls == NULL) { - // pending exception... - return JNI_ERR; - } - fdFieldId = (*env)->GetFieldID(env, fileDescriptorCls, "fd", "I"); - if (fdFieldId == NULL) { - netty_unix_errors_throwRuntimeException(env, "failed to get field ID: FileDescriptor.fd"); - return JNI_ERR; - } - stringCls = (*env)->FindClass(env, "java/lang/String"); - if (stringCls == NULL) { - // pending exception... - return JNI_ERR; - } + NETTY_FIND_CLASS(env, fileChannelCls, "sun/nio/ch/FileChannelImpl", done); + NETTY_GET_FIELD(env, fileChannelCls, fileDescriptorFieldId, "fd", "Ljava/io/FileDescriptor;", done); + + NETTY_FIND_CLASS(env, fileDescriptorCls, "java/io/FileDescriptor", done); + NETTY_GET_FIELD(env, fileDescriptorCls, fdFieldId, "fd", "I", done); + + NETTY_LOAD_CLASS(env, stringClass, "java/lang/String", done); - nettyClassName = netty_unix_util_prepend(packagePrefix, "io/netty/channel/unix/PeerCredentials"); - jclass localPeerCredsClass = (*env)->FindClass(env, nettyClassName); + NETTY_PREPEND(packagePrefix, "io/netty/channel/unix/PeerCredentials", nettyClassName, done); + NETTY_LOAD_CLASS(env, peerCredentialsClass, nettyClassName, done); + netty_unix_util_free_dynamic_name(&nettyClassName); + + NETTY_GET_METHOD(env, peerCredentialsClass, peerCredentialsMethodId, "<init>", "(II[I)V", done); + ret = NETTY_JNI_VERSION; +done: + netty_unix_util_free_dynamic_methods_table(dynamicMethods, fixed_method_table_size, dynamicMethodsTableSize()); free(nettyClassName); - nettyClassName = NULL; - if (localPeerCredsClass == NULL) { - // pending exception... - return JNI_ERR; - } - peerCredentialsClass = (jclass) (*env)->NewGlobalRef(env, localPeerCredsClass); - if (peerCredentialsClass == NULL) { - // out-of-memory! - netty_unix_errors_throwOutOfMemoryError(env); - return JNI_ERR; - } - peerCredentialsMethodId = (*env)->GetMethodID(env, peerCredentialsClass, "<init>", "(II[I)V"); - if (peerCredentialsMethodId == NULL) { - netty_unix_errors_throwRuntimeException(env, "failed to get method ID: PeerCredentials.<init>(int, int, int[])"); - return JNI_ERR; - } - return NETTY_JNI_VERSION; + return ret; } void netty_kqueue_bsdsocket_JNI_OnUnLoad(JNIEnv* env) { - if (peerCredentialsClass != NULL) { - (*env)->DeleteGlobalRef(env, peerCredentialsClass); - peerCredentialsClass = NULL; - } + NETTY_UNLOAD_CLASS(env, peerCredentialsClass); + NETTY_UNLOAD_CLASS(env, stringClass); } diff --git a/transport-native-kqueue/src/main/c/netty_kqueue_native.c b/transport-native-kqueue/src/main/c/netty_kqueue_native.c index a2a5a87..9e995ec 100644 --- a/transport-native-kqueue/src/main/c/netty_kqueue_native.c +++ b/transport-native-kqueue/src/main/c/netty_kqueue_native.c @@ -139,15 +139,11 @@ static jint netty_kqueue_native_keventWait(JNIEnv* env, jclass clazz, jint kqueu } netty_unix_util_clock_gettime(waitClockId, &nowTs); - // beforeTs will store the time difference to check for overflow - beforeTs.tv_sec = nowTs.tv_sec - beforeTs.tv_sec; - beforeTs.tv_nsec = nowTs.tv_nsec - beforeTs.tv_nsec; - // Now subtract the time difference - timeoutTs.tv_sec -= beforeTs.tv_sec; - timeoutTs.tv_nsec -= beforeTs.tv_nsec; - if (beforeTs.tv_sec < 0 || beforeTs.tv_nsec < 0 || (timeoutTs.tv_sec <= 0 && timeoutTs.tv_nsec <= 0)) { + if (netty_unix_util_timespec_subtract_ns(&timeoutTs, + netty_unix_util_timespec_elapsed_ns(&beforeTs, &nowTs))) { return 0; } + beforeTs = nowTs; // https://www.freebsd.org/cgi/man.cgi?query=kqueue&sektion=2 // When kevent() call fails with EINTR error, all changes in the changelist have been applied. @@ -392,10 +388,8 @@ static jint JNI_OnLoad_netty_transport_native_kqueue0(JavaVM* vm, void* reserved #endif /* NETTY_BUILD_STATIC */ jint ret = netty_kqueue_native_JNI_OnLoad(env, packagePrefix); - if (packagePrefix != NULL) { - free(packagePrefix); - packagePrefix = NULL; - } + // It's safe to call free(...) with a NULL argument as well. + free(packagePrefix); return ret; } diff --git a/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/AbstractKQueueChannel.java b/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/AbstractKQueueChannel.java index 559999c..02d2a1c 100644 --- a/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/AbstractKQueueChannel.java +++ b/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/AbstractKQueueChannel.java @@ -124,35 +124,7 @@ abstract class AbstractKQueueChannel extends AbstractChannel implements UnixChan // Even if we allow half closed sockets we should give up on reading. Otherwise we may allow a read attempt on a // socket which has not even been connected yet. This has been observed to block during unit tests. inputClosedSeenErrorOnRead = true; - try { - if (isRegistered()) { - // The FD will be closed, which should take care of deleting any associated events from kqueue, but - // since we rely upon jniSelfRef to be consistent we make sure that we clear this reference out for - // all events which are pending in kqueue to avoid referencing a deleted pointer at a later time. - - // Need to check if we are on the EventLoop as doClose() may be triggered by the GlobalEventExecutor - // if SO_LINGER is used. - // - // See https://github.com/netty/netty/issues/7159 - EventLoop loop = eventLoop(); - if (loop.inEventLoop()) { - doDeregister(); - } else { - loop.execute(new Runnable() { - @Override - public void run() { - try { - doDeregister(); - } catch (Throwable cause) { - pipeline().fireExceptionCaught(cause); - } - } - }); - } - } - } finally { - socket.close(); - } + socket.close(); } @Override @@ -160,6 +132,11 @@ abstract class AbstractKQueueChannel extends AbstractChannel implements UnixChan doClose(); } + void resetCachedAddresses() { + local = socket.localAddress(); + remote = socket.remoteAddress(); + } + @Override protected boolean isCompatible(EventLoop loop) { return loop instanceof KQueueEventLoop; @@ -172,12 +149,19 @@ abstract class AbstractKQueueChannel extends AbstractChannel implements UnixChan @Override protected void doDeregister() throws Exception { + ((KQueueEventLoop) eventLoop()).remove(this); + + // As unregisteredFilters() may have not been called because isOpen() returned false we just set both filters + // to false to ensure a consistent state in all cases. + readFilterEnabled = false; + writeFilterEnabled = false; + } + + void unregisterFilters() throws Exception { // Make sure we unregister our filters from kqueue! readFilter(false); writeFilter(false); evSet0(Native.EVFILT_SOCK, Native.EV_DELETE, 0); - - ((KQueueEventLoop) eventLoop()).remove(this); } @Override @@ -297,7 +281,7 @@ abstract class AbstractKQueueChannel extends AbstractChannel implements UnixChan return 1; } } else { - final ByteBuffer nioBuf = buf.nioBufferCount() == 1 ? + final ByteBuffer nioBuf = buf.nioBufferCount() == 1? buf.internalNioBuffer(buf.readerIndex(), buf.readableBytes()) : buf.nioBuffer(); int localFlushedAmount = socket.write(nioBuf, nioBuf.position(), nioBuf.limit()); if (localFlushedAmount > 0) { @@ -314,6 +298,10 @@ abstract class AbstractKQueueChannel extends AbstractChannel implements UnixChan } private static boolean isAllowHalfClosure(ChannelConfig config) { + if (config instanceof KQueueDomainSocketChannelConfig) { + return ((KQueueDomainSocketChannelConfig) config).isAllowHalfClosure(); + } + return config instanceof SocketChannelConfig && ((SocketChannelConfig) config).isAllowHalfClosure(); } @@ -359,7 +347,7 @@ abstract class AbstractKQueueChannel extends AbstractChannel implements UnixChan } private void evSet(short filter, short flags) { - if (isOpen() && isRegistered()) { + if (isRegistered()) { evSet0(filter, flags); } } @@ -369,7 +357,10 @@ abstract class AbstractKQueueChannel extends AbstractChannel implements UnixChan } private void evSet0(short filter, short flags, int fflags) { - ((KQueueEventLoop) eventLoop()).evSet(this, filter, flags, fflags); + // Only try to add to changeList if the FD is still open, if not we already closed it in the meantime. + if (isOpen()) { + ((KQueueEventLoop) eventLoop()).evSet(this, filter, flags, fflags); + } } abstract class AbstractKQueueUnsafe extends AbstractUnsafe { @@ -392,7 +383,9 @@ abstract class AbstractKQueueChannel extends AbstractChannel implements UnixChan abstract void readReady(KQueueRecvByteAllocatorHandle allocHandle); - final void readReadyBefore() { maybeMoreDataToRead = false; } + final void readReadyBefore() { + maybeMoreDataToRead = false; + } final void readReadyFinally(ChannelConfig config) { maybeMoreDataToRead = allocHandle.maybeMoreDataToRead(); @@ -708,7 +701,7 @@ abstract class AbstractKQueueChannel extends AbstractChannel implements UnixChan boolean connected = doConnect0(remoteAddress); if (connected) { - remote = remoteSocketAddr == null ? + remote = remoteSocketAddr == null? remoteAddress : computeRemoteAddr(remoteSocketAddr, socket.remoteAddress()); } // We always need to set the localAddress even if not connected yet as the bind already took place. diff --git a/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/AbstractKQueueStreamChannel.java b/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/AbstractKQueueStreamChannel.java index 7d604f1..4b7f4e0 100644 --- a/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/AbstractKQueueStreamChannel.java +++ b/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/AbstractKQueueStreamChannel.java @@ -210,12 +210,13 @@ public abstract class AbstractKQueueStreamChannel extends AbstractKQueueChannel */ private int writeDefaultFileRegion(ChannelOutboundBuffer in, DefaultFileRegion region) throws Exception { final long regionCount = region.count(); - if (region.transferred() >= regionCount) { + final long offset = region.transferred(); + + if (offset >= regionCount) { in.remove(); return 0; } - final long offset = region.transferred(); final long flushedAmount = socket.sendFile(region, region.position(), offset, regionCount - offset); if (flushedAmount > 0) { in.progress(flushedAmount); @@ -223,6 +224,8 @@ public abstract class AbstractKQueueStreamChannel extends AbstractKQueueChannel in.remove(); } return 1; + } else if (flushedAmount == 0) { + validateFileRegion(region, offset); } return WRITE_STATUS_SNDBUF_FULL; } diff --git a/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/BsdSocket.java b/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/BsdSocket.java index 3de1cea..b712be9 100644 --- a/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/BsdSocket.java +++ b/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/BsdSocket.java @@ -16,26 +16,18 @@ package io.netty.channel.kqueue; import io.netty.channel.DefaultFileRegion; -import io.netty.channel.unix.Errors; import io.netty.channel.unix.PeerCredentials; import io.netty.channel.unix.Socket; -import io.netty.util.internal.ThrowableUtil; import java.io.IOException; -import java.nio.channels.ClosedChannelException; import static io.netty.channel.kqueue.AcceptFilter.PLATFORM_UNSUPPORTED; -import static io.netty.channel.unix.Errors.ERRNO_EPIPE_NEGATIVE; import static io.netty.channel.unix.Errors.ioResult; -import static io.netty.channel.unix.Errors.newConnectionResetException; /** * A socket which provides access BSD native methods. */ final class BsdSocket extends Socket { - private static final Errors.NativeIoException SENDFILE_CONNECTION_RESET_EXCEPTION; - private static final ClosedChannelException SENDFILE_CLOSED_CHANNEL_EXCEPTION = ThrowableUtil.unknownStackTrace( - new ClosedChannelException(), Native.class, "sendfile(..)"); // These limits are just based on observations. I couldn't find anything in header files which formally // define these limits. @@ -43,11 +35,6 @@ final class BsdSocket extends Socket { private static final int FREEBSD_SND_LOW_AT_MAX = 1 << 15; static final int BSD_SND_LOW_AT_MAX = Math.min(APPLE_SND_LOW_AT_MAX, FREEBSD_SND_LOW_AT_MAX); - static { - SENDFILE_CONNECTION_RESET_EXCEPTION = newConnectionResetException("syscall:sendfile", - ERRNO_EPIPE_NEGATIVE); - } - BsdSocket(int fd) { super(fd); } @@ -90,7 +77,7 @@ final class BsdSocket extends Socket { if (res >= 0) { return res; } - return ioResult("sendfile", (int) res, SENDFILE_CONNECTION_RESET_EXCEPTION, SENDFILE_CLOSED_CHANNEL_EXCEPTION); + return ioResult("sendfile", (int) res); } public static BsdSocket newSocketStream() { diff --git a/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/KQueue.java b/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/KQueue.java index b5a772f..37ad6ea 100644 --- a/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/KQueue.java +++ b/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/KQueue.java @@ -25,7 +25,8 @@ import io.netty.util.internal.UnstableApi; @UnstableApi public final class KQueue { private static final Throwable UNAVAILABILITY_CAUSE; - static { + + static { Throwable cause = null; if (SystemPropertyUtil.getBoolean("io.netty.transport.noNative", false)) { cause = new UnsupportedOperationException( @@ -51,15 +52,15 @@ public final class KQueue { } /** - * Returns {@code true} if and only if the - * <a href="http://netty.io/wiki/native-transports.html">{@code netty-transport-native-kqueue}</a> is available. + * Returns {@code true} if and only if the <a href="https://netty.io/wiki/native-transports.html">{@code + * netty-transport-native-kqueue}</a> is available. */ public static boolean isAvailable() { return UNAVAILABILITY_CAUSE == null; } /** - * Ensure that <a href="http://netty.io/wiki/native-transports.html">{@code netty-transport-native-kqueue}</a> is + * Ensure that <a href="https://netty.io/wiki/native-transports.html">{@code netty-transport-native-kqueue}</a> is * available. * * @throws UnsatisfiedLinkError if unavailable @@ -72,8 +73,8 @@ public final class KQueue { } /** - * Returns the cause of unavailability of - * <a href="http://netty.io/wiki/native-transports.html">{@code netty-transport-native-kqueue}</a>. + * Returns the cause of unavailability of <a href="https://netty.io/wiki/native-transports.html">{@code + * netty-transport-native-kqueue}</a>. * * @return the cause if unavailable. {@code null} if available. */ @@ -81,5 +82,6 @@ public final class KQueue { return UNAVAILABILITY_CAUSE; } - private KQueue() { } + private KQueue() { + } } diff --git a/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/KQueueDatagramChannel.java b/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/KQueueDatagramChannel.java index 887951a..db29ebe 100644 --- a/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/KQueueDatagramChannel.java +++ b/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/KQueueDatagramChannel.java @@ -28,8 +28,10 @@ import io.netty.channel.socket.DatagramChannel; import io.netty.channel.socket.DatagramChannelConfig; import io.netty.channel.socket.DatagramPacket; import io.netty.channel.unix.DatagramSocketAddress; +import io.netty.channel.unix.Errors; import io.netty.channel.unix.IovArray; import io.netty.channel.unix.UnixChannelUtil; +import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.StringUtil; import io.netty.util.internal.UnstableApi; @@ -37,11 +39,10 @@ import java.io.IOException; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.NetworkInterface; +import java.net.PortUnreachableException; import java.net.SocketAddress; import java.net.SocketException; import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.List; import static io.netty.channel.kqueue.BsdSocket.newSocketDgram; @@ -140,13 +141,8 @@ public final class KQueueDatagramChannel extends AbstractKQueueChannel implement final InetAddress multicastAddress, final NetworkInterface networkInterface, final InetAddress source, final ChannelPromise promise) { - if (multicastAddress == null) { - throw new NullPointerException("multicastAddress"); - } - - if (networkInterface == null) { - throw new NullPointerException("networkInterface"); - } + ObjectUtil.checkNotNull(multicastAddress, "multicastAddress"); + ObjectUtil.checkNotNull(networkInterface, "networkInterface"); promise.setFailure(new UnsupportedOperationException("Multicast not supported")); return promise; @@ -191,12 +187,8 @@ public final class KQueueDatagramChannel extends AbstractKQueueChannel implement public ChannelFuture leaveGroup( final InetAddress multicastAddress, final NetworkInterface networkInterface, final InetAddress source, final ChannelPromise promise) { - if (multicastAddress == null) { - throw new NullPointerException("multicastAddress"); - } - if (networkInterface == null) { - throw new NullPointerException("networkInterface"); - } + ObjectUtil.checkNotNull(multicastAddress, "multicastAddress"); + ObjectUtil.checkNotNull(networkInterface, "networkInterface"); promise.setFailure(new UnsupportedOperationException("Multicast not supported")); @@ -214,16 +206,9 @@ public final class KQueueDatagramChannel extends AbstractKQueueChannel implement public ChannelFuture block( final InetAddress multicastAddress, final NetworkInterface networkInterface, final InetAddress sourceToBlock, final ChannelPromise promise) { - if (multicastAddress == null) { - throw new NullPointerException("multicastAddress"); - } - if (sourceToBlock == null) { - throw new NullPointerException("sourceToBlock"); - } - - if (networkInterface == null) { - throw new NullPointerException("networkInterface"); - } + ObjectUtil.checkNotNull(multicastAddress, "multicastAddress"); + ObjectUtil.checkNotNull(sourceToBlock, "sourceToBlock"); + ObjectUtil.checkNotNull(networkInterface, "networkInterface"); promise.setFailure(new UnsupportedOperationException("Multicast not supported")); return promise; } @@ -323,7 +308,7 @@ public final class KQueueDatagramChannel extends AbstractKQueueChannel implement } } else if (data.nioBufferCount() > 1) { IovArray array = ((KQueueEventLoop) eventLoop()).cleanArray(); - array.add(data); + array.add(data, data.readerIndex(), data.readableBytes()); int cnt = array.count(); assert cnt != 0; @@ -333,7 +318,7 @@ public final class KQueueDatagramChannel extends AbstractKQueueChannel implement writtenBytes = socket.sendToAddresses(array.memoryAddress(0), cnt, remoteAddress.getAddress(), remoteAddress.getPort()); } - } else { + } else { ByteBuffer nioData = data.internalNioBuffer(data.readerIndex(), data.readableBytes()); if (remoteAddress == null) { writtenBytes = socket.write(nioData, nioData.position(), nioData.limit()); @@ -351,13 +336,13 @@ public final class KQueueDatagramChannel extends AbstractKQueueChannel implement if (msg instanceof DatagramPacket) { DatagramPacket packet = (DatagramPacket) msg; ByteBuf content = packet.content(); - return UnixChannelUtil.isBufferCopyNeededForWrite(content)? + return UnixChannelUtil.isBufferCopyNeededForWrite(content) ? new DatagramPacket(newDirectBuffer(packet, content), packet.recipient()) : msg; } if (msg instanceof ByteBuf) { ByteBuf buf = (ByteBuf) msg; - return UnixChannelUtil.isBufferCopyNeededForWrite(buf)? newDirectBuffer(buf) : buf; + return UnixChannelUtil.isBufferCopyNeededForWrite(buf) ? newDirectBuffer(buf) : buf; } if (msg instanceof AddressedEnvelope) { @@ -367,7 +352,7 @@ public final class KQueueDatagramChannel extends AbstractKQueueChannel implement (e.recipient() == null || e.recipient() instanceof InetSocketAddress)) { ByteBuf content = (ByteBuf) e.content(); - return UnixChannelUtil.isBufferCopyNeededForWrite(content)? + return UnixChannelUtil.isBufferCopyNeededForWrite(content) ? new DefaultAddressedEnvelope<ByteBuf, InetSocketAddress>( newDirectBuffer(e, content), (InetSocketAddress) e.recipient()) : e; } @@ -386,6 +371,7 @@ public final class KQueueDatagramChannel extends AbstractKQueueChannel implement protected void doDisconnect() throws Exception { socket.disconnect(); connected = active = false; + resetCachedAddresses(); } @Override @@ -420,41 +406,72 @@ public final class KQueueDatagramChannel extends AbstractKQueueChannel implement Throwable exception = null; try { - ByteBuf data = null; + ByteBuf byteBuf = null; try { + boolean connected = isConnected(); do { - data = allocHandle.allocate(allocator); - allocHandle.attemptedBytesRead(data.writableBytes()); - final DatagramSocketAddress remoteAddress; - if (data.hasMemoryAddress()) { - // has a memory address so use optimized call - remoteAddress = socket.recvFromAddress(data.memoryAddress(), data.writerIndex(), - data.capacity()); + byteBuf = allocHandle.allocate(allocator); + allocHandle.attemptedBytesRead(byteBuf.writableBytes()); + + final DatagramPacket packet; + if (connected) { + try { + allocHandle.lastBytesRead(doReadBytes(byteBuf)); + } catch (Errors.NativeIoException e) { + // We need to correctly translate connect errors to match NIO behaviour. + if (e.expectedErr() == Errors.ERROR_ECONNREFUSED_NEGATIVE) { + PortUnreachableException error = new PortUnreachableException(e.getMessage()); + error.initCause(e); + throw error; + } + throw e; + } + if (allocHandle.lastBytesRead() <= 0) { + // nothing was read, release the buffer. + byteBuf.release(); + byteBuf = null; + break; + } + packet = new DatagramPacket(byteBuf, + (InetSocketAddress) localAddress(), (InetSocketAddress) remoteAddress()); } else { - ByteBuffer nioData = data.internalNioBuffer(data.writerIndex(), data.writableBytes()); - remoteAddress = socket.recvFrom(nioData, nioData.position(), nioData.limit()); - } - - if (remoteAddress == null) { - allocHandle.lastBytesRead(-1); - data.release(); - data = null; - break; + final DatagramSocketAddress remoteAddress; + if (byteBuf.hasMemoryAddress()) { + // has a memory address so use optimized call + remoteAddress = socket.recvFromAddress(byteBuf.memoryAddress(), byteBuf.writerIndex(), + byteBuf.capacity()); + } else { + ByteBuffer nioData = byteBuf.internalNioBuffer( + byteBuf.writerIndex(), byteBuf.writableBytes()); + remoteAddress = socket.recvFrom(nioData, nioData.position(), nioData.limit()); + } + + if (remoteAddress == null) { + allocHandle.lastBytesRead(-1); + byteBuf.release(); + byteBuf = null; + break; + } + InetSocketAddress localAddress = remoteAddress.localAddress(); + if (localAddress == null) { + localAddress = (InetSocketAddress) localAddress(); + } + allocHandle.lastBytesRead(remoteAddress.receivedAmount()); + byteBuf.writerIndex(byteBuf.writerIndex() + allocHandle.lastBytesRead()); + + packet = new DatagramPacket(byteBuf, localAddress, remoteAddress); } allocHandle.incMessagesRead(1); - allocHandle.lastBytesRead(remoteAddress.receivedAmount()); - data.writerIndex(data.writerIndex() + allocHandle.lastBytesRead()); readPending = false; - pipeline.fireChannelRead( - new DatagramPacket(data, (InetSocketAddress) localAddress(), remoteAddress)); + pipeline.fireChannelRead(packet); - data = null; + byteBuf = null; } while (allocHandle.continueReading()); } catch (Throwable t) { - if (data != null) { - data.release(); + if (byteBuf != null) { + byteBuf.release(); } exception = t; } diff --git a/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/KQueueDomainSocketChannelConfig.java b/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/KQueueDomainSocketChannelConfig.java index eefd4c0..9d343a3 100644 --- a/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/KQueueDomainSocketChannelConfig.java +++ b/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/KQueueDomainSocketChannelConfig.java @@ -20,17 +20,24 @@ import io.netty.channel.ChannelOption; import io.netty.channel.MessageSizeEstimator; import io.netty.channel.RecvByteBufAllocator; import io.netty.channel.WriteBufferWaterMark; +import io.netty.channel.socket.SocketChannelConfig; import io.netty.channel.unix.DomainSocketChannelConfig; import io.netty.channel.unix.DomainSocketReadMode; +import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.UnstableApi; +import java.io.IOException; import java.util.Map; +import static io.netty.channel.ChannelOption.ALLOW_HALF_CLOSURE; +import static io.netty.channel.ChannelOption.SO_RCVBUF; +import static io.netty.channel.ChannelOption.SO_SNDBUF; import static io.netty.channel.unix.UnixChannelOption.DOMAIN_SOCKET_READ_MODE; @UnstableApi public final class KQueueDomainSocketChannelConfig extends KQueueChannelConfig implements DomainSocketChannelConfig { private volatile DomainSocketReadMode mode = DomainSocketReadMode.BYTES; + private volatile boolean allowHalfClosure; KQueueDomainSocketChannelConfig(AbstractKQueueChannel channel) { super(channel); @@ -38,7 +45,7 @@ public final class KQueueDomainSocketChannelConfig extends KQueueChannelConfig i @Override public Map<ChannelOption<?>, Object> getOptions() { - return getOptions(super.getOptions(), DOMAIN_SOCKET_READ_MODE); + return getOptions(super.getOptions(), DOMAIN_SOCKET_READ_MODE, ALLOW_HALF_CLOSURE, SO_SNDBUF, SO_RCVBUF); } @SuppressWarnings("unchecked") @@ -47,6 +54,15 @@ public final class KQueueDomainSocketChannelConfig extends KQueueChannelConfig i if (option == DOMAIN_SOCKET_READ_MODE) { return (T) getReadMode(); } + if (option == ALLOW_HALF_CLOSURE) { + return (T) Boolean.valueOf(isAllowHalfClosure()); + } + if (option == SO_SNDBUF) { + return (T) Integer.valueOf(getSendBufferSize()); + } + if (option == SO_RCVBUF) { + return (T) Integer.valueOf(getReceiveBufferSize()); + } return super.getOption(option); } @@ -56,6 +72,12 @@ public final class KQueueDomainSocketChannelConfig extends KQueueChannelConfig i if (option == DOMAIN_SOCKET_READ_MODE) { setReadMode((DomainSocketReadMode) value); + } else if (option == ALLOW_HALF_CLOSURE) { + setAllowHalfClosure((Boolean) value); + } else if (option == SO_SNDBUF) { + setSendBufferSize((Integer) value); + } else if (option == SO_RCVBUF) { + setReceiveBufferSize((Integer) value); } else { return super.setOption(option, value); } @@ -140,10 +162,7 @@ public final class KQueueDomainSocketChannelConfig extends KQueueChannelConfig i @Override public KQueueDomainSocketChannelConfig setReadMode(DomainSocketReadMode mode) { - if (mode == null) { - throw new NullPointerException("mode"); - } - this.mode = mode; + this.mode = ObjectUtil.checkNotNull(mode, "mode"); return this; } @@ -151,4 +170,53 @@ public final class KQueueDomainSocketChannelConfig extends KQueueChannelConfig i public DomainSocketReadMode getReadMode() { return mode; } + + public int getSendBufferSize() { + try { + return ((KQueueDomainSocketChannel) channel).socket.getSendBufferSize(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public KQueueDomainSocketChannelConfig setSendBufferSize(int sendBufferSize) { + try { + ((KQueueDomainSocketChannel) channel).socket.setSendBufferSize(sendBufferSize); + return this; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public int getReceiveBufferSize() { + try { + return ((KQueueDomainSocketChannel) channel).socket.getReceiveBufferSize(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public KQueueDomainSocketChannelConfig setReceiveBufferSize(int receiveBufferSize) { + try { + ((KQueueDomainSocketChannel) channel).socket.setReceiveBufferSize(receiveBufferSize); + return this; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * @see SocketChannelConfig#isAllowHalfClosure() + */ + public boolean isAllowHalfClosure() { + return allowHalfClosure; + } + + /** + * @see SocketChannelConfig#setAllowHalfClosure(boolean) + */ + public KQueueDomainSocketChannelConfig setAllowHalfClosure(boolean allowHalfClosure) { + this.allowHalfClosure = allowHalfClosure; + return this; + } } diff --git a/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/KQueueEventLoop.java b/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/KQueueEventLoop.java index 25c50a4..90ef189 100644 --- a/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/KQueueEventLoop.java +++ b/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/KQueueEventLoop.java @@ -17,6 +17,7 @@ package io.netty.channel.kqueue; import io.netty.channel.EventLoop; import io.netty.channel.EventLoopGroup; +import io.netty.channel.EventLoopTaskQueueFactory; import io.netty.channel.SelectStrategy; import io.netty.channel.SingleThreadEventLoop; import io.netty.channel.kqueue.AbstractKQueueChannel.AbstractKQueueUnsafe; @@ -71,9 +72,11 @@ final class KQueueEventLoop extends SingleThreadEventLoop { private volatile int ioRatio = 50; KQueueEventLoop(EventLoopGroup parent, Executor executor, int maxEvents, - SelectStrategy strategy, RejectedExecutionHandler rejectedExecutionHandler) { - super(parent, executor, false, DEFAULT_MAX_PENDING_TASKS, rejectedExecutionHandler); - selectStrategy = ObjectUtil.checkNotNull(strategy, "strategy"); + SelectStrategy strategy, RejectedExecutionHandler rejectedExecutionHandler, + EventLoopTaskQueueFactory queueFactory) { + super(parent, executor, false, newTaskQueue(queueFactory), newTaskQueue(queueFactory), + rejectedExecutionHandler); + this.selectStrategy = ObjectUtil.checkNotNull(strategy, "strategy"); this.kqueueFd = Native.newKQueue(); if (maxEvents == 0) { allowGrowing = true; @@ -81,8 +84,8 @@ final class KQueueEventLoop extends SingleThreadEventLoop { } else { allowGrowing = false; } - changeList = new KQueueEventArray(maxEvents); - eventList = new KQueueEventArray(maxEvents); + this.changeList = new KQueueEventArray(maxEvents); + this.eventList = new KQueueEventArray(maxEvents); int result = Native.keventAddUserEvent(kqueueFd.intValue(), KQUEUE_WAKE_UP_IDENT); if (result < 0) { cleanup(); @@ -90,18 +93,45 @@ final class KQueueEventLoop extends SingleThreadEventLoop { } } + private static Queue<Runnable> newTaskQueue( + EventLoopTaskQueueFactory queueFactory) { + if (queueFactory == null) { + return newTaskQueue0(DEFAULT_MAX_PENDING_TASKS); + } + return queueFactory.newTaskQueue(DEFAULT_MAX_PENDING_TASKS); + } + void add(AbstractKQueueChannel ch) { assert inEventLoop(); - channels.put(ch.fd().intValue(), ch); + AbstractKQueueChannel old = channels.put(ch.fd().intValue(), ch); + // We either expect to have no Channel in the map with the same FD or that the FD of the old Channel is already + // closed. + assert old == null || !old.isOpen(); } void evSet(AbstractKQueueChannel ch, short filter, short flags, int fflags) { + assert inEventLoop(); changeList.evSet(ch, filter, flags, fflags); } - void remove(AbstractKQueueChannel ch) { + void remove(AbstractKQueueChannel ch) throws Exception { assert inEventLoop(); - channels.remove(ch.fd().intValue()); + int fd = ch.fd().intValue(); + + AbstractKQueueChannel old = channels.remove(fd); + if (old != null && old != ch) { + // The Channel mapping was already replaced due FD reuse, put back the stored Channel. + channels.put(fd, old); + + // If we found another Channel in the map that is mapped to the same FD the given Channel MUST be closed. + assert !ch.isOpen(); + } else if (ch.isOpen()) { + // Remove the filters. This is only needed if it's still open as otherwise it will be automatically + // removed once the file-descriptor is closed. + // + // See also https://www.freebsd.org/cgi/man.cgi?query=kqueue&sektion=2 + ch.unregisterFilters(); + } } /** @@ -286,9 +316,13 @@ final class KQueueEventLoop extends SingleThreadEventLoop { @Override protected Queue<Runnable> newTaskQueue(int maxPendingTasks) { + return newTaskQueue0(maxPendingTasks); + } + + private static Queue<Runnable> newTaskQueue0(int maxPendingTasks) { // This event loop never calls takeTask() return maxPendingTasks == Integer.MAX_VALUE ? PlatformDependent.<Runnable>newMpscQueue() - : PlatformDependent.<Runnable>newMpscQueue(maxPendingTasks); + : PlatformDependent.<Runnable>newMpscQueue(maxPendingTasks); } /** @@ -309,6 +343,11 @@ final class KQueueEventLoop extends SingleThreadEventLoop { this.ioRatio = ioRatio; } + @Override + public int registeredChannels() { + return channels.size(); + } + @Override protected void cleanup() { try { @@ -330,6 +369,14 @@ final class KQueueEventLoop extends SingleThreadEventLoop { } catch (IOException e) { // ignore on close } + + // Using the intermediate collection to prevent ConcurrentModificationException. + // In the `close()` method, the channel is deleted from `channels` map. + AbstractKQueueChannel[] localChannels = channels.values().toArray(new AbstractKQueueChannel[0]); + + for (AbstractKQueueChannel ch: localChannels) { + ch.unsafe().close(ch.unsafe().voidPromise()); + } } private static void handleLoopException(Throwable t) { diff --git a/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/KQueueEventLoopGroup.java b/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/KQueueEventLoopGroup.java index fe32aa5..34e82a1 100644 --- a/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/KQueueEventLoopGroup.java +++ b/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/KQueueEventLoopGroup.java @@ -17,6 +17,7 @@ package io.netty.channel.kqueue; import io.netty.channel.DefaultSelectStrategyFactory; import io.netty.channel.EventLoop; +import io.netty.channel.EventLoopTaskQueueFactory; import io.netty.channel.MultithreadEventLoopGroup; import io.netty.channel.SelectStrategyFactory; import io.netty.util.concurrent.EventExecutor; @@ -48,6 +49,14 @@ public final class KQueueEventLoopGroup extends MultithreadEventLoopGroup { this(nThreads, (ThreadFactory) null); } + /** + * Create a new instance using the default number of threads and the given {@link ThreadFactory}. + */ + @SuppressWarnings("deprecation") + public KQueueEventLoopGroup(ThreadFactory threadFactory) { + this(0, threadFactory, 0); + } + /** * Create a new instance using the specified number of threads and the default {@link ThreadFactory}. */ @@ -116,6 +125,14 @@ public final class KQueueEventLoopGroup extends MultithreadEventLoopGroup { super(nThreads, executor, chooserFactory, 0, selectStrategyFactory, rejectedExecutionHandler); } + public KQueueEventLoopGroup(int nThreads, Executor executor, EventExecutorChooserFactory chooserFactory, + SelectStrategyFactory selectStrategyFactory, + RejectedExecutionHandler rejectedExecutionHandler, + EventLoopTaskQueueFactory queueFactory) { + super(nThreads, executor, chooserFactory, 0, selectStrategyFactory, + rejectedExecutionHandler, queueFactory); + } + /** * Sets the percentage of the desired amount of time spent for I/O in the child event loops. The default value is * {@code 50}, which means the event loop will try to spend the same amount of time for I/O as for non-I/O tasks. @@ -128,7 +145,10 @@ public final class KQueueEventLoopGroup extends MultithreadEventLoopGroup { @Override protected EventLoop newChild(Executor executor, Object... args) throws Exception { + EventLoopTaskQueueFactory queueFactory = args.length == 4 ? (EventLoopTaskQueueFactory) args[3] : null; + return new KQueueEventLoop(this, executor, (Integer) args[0], - ((SelectStrategyFactory) args[1]).newSelectStrategy(), (RejectedExecutionHandler) args[2]); + ((SelectStrategyFactory) args[1]).newSelectStrategy(), + (RejectedExecutionHandler) args[2], queueFactory); } } diff --git a/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/KQueueRecvByteAllocatorHandle.java b/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/KQueueRecvByteAllocatorHandle.java index 9eed7d4..e220858 100644 --- a/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/KQueueRecvByteAllocatorHandle.java +++ b/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/KQueueRecvByteAllocatorHandle.java @@ -18,18 +18,17 @@ package io.netty.channel.kqueue; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.channel.ChannelConfig; -import io.netty.channel.RecvByteBufAllocator; +import io.netty.channel.RecvByteBufAllocator.DelegatingHandle; +import io.netty.channel.RecvByteBufAllocator.ExtendedHandle; import io.netty.channel.unix.PreferredDirectByteBufAllocator; import io.netty.util.UncheckedBooleanSupplier; -import io.netty.util.internal.ObjectUtil; import static java.lang.Math.max; import static java.lang.Math.min; -final class KQueueRecvByteAllocatorHandle implements RecvByteBufAllocator.ExtendedHandle { +final class KQueueRecvByteAllocatorHandle extends DelegatingHandle implements ExtendedHandle { private final PreferredDirectByteBufAllocator preferredDirectByteBufAllocator = new PreferredDirectByteBufAllocator(); - private final RecvByteBufAllocator.ExtendedHandle delegate; private final UncheckedBooleanSupplier defaultMaybeMoreDataSupplier = new UncheckedBooleanSupplier() { @Override @@ -41,24 +40,19 @@ final class KQueueRecvByteAllocatorHandle implements RecvByteBufAllocator.Extend private boolean readEOF; private long numberBytesPending; - KQueueRecvByteAllocatorHandle(RecvByteBufAllocator.ExtendedHandle handle) { - delegate = ObjectUtil.checkNotNull(handle, "handle"); + KQueueRecvByteAllocatorHandle(ExtendedHandle handle) { + super(handle); } @Override public int guess() { - return overrideGuess ? guess0() : delegate.guess(); + return overrideGuess ? guess0() : delegate().guess(); } @Override public void reset(ChannelConfig config) { overrideGuess = ((KQueueChannelConfig) config).getRcvAllocTransportProvidesGuess(); - delegate.reset(config); - } - - @Override - public void incMessagesRead(int numMessages) { - delegate.incMessagesRead(numMessages); + delegate().reset(config); } @Override @@ -66,44 +60,24 @@ final class KQueueRecvByteAllocatorHandle implements RecvByteBufAllocator.Extend // We need to ensure we always allocate a direct ByteBuf as we can only use a direct buffer to read via JNI. preferredDirectByteBufAllocator.updateAllocator(alloc); return overrideGuess ? preferredDirectByteBufAllocator.ioBuffer(guess0()) : - delegate.allocate(preferredDirectByteBufAllocator); + delegate().allocate(preferredDirectByteBufAllocator); } @Override public void lastBytesRead(int bytes) { numberBytesPending = bytes < 0 ? 0 : max(0, numberBytesPending - bytes); - delegate.lastBytesRead(bytes); - } - - @Override - public int lastBytesRead() { - return delegate.lastBytesRead(); - } - - @Override - public void attemptedBytesRead(int bytes) { - delegate.attemptedBytesRead(bytes); - } - - @Override - public int attemptedBytesRead() { - return delegate.attemptedBytesRead(); - } - - @Override - public void readComplete() { - delegate.readComplete(); + delegate().lastBytesRead(bytes); } @Override public boolean continueReading(UncheckedBooleanSupplier maybeMoreDataSupplier) { - return delegate.continueReading(maybeMoreDataSupplier); + return ((ExtendedHandle) delegate()).continueReading(maybeMoreDataSupplier); } @Override public boolean continueReading() { // We must override the supplier which determines if there maybe more data to read. - return delegate.continueReading(defaultMaybeMoreDataSupplier); + return continueReading(defaultMaybeMoreDataSupplier); } void readEOF() { diff --git a/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/KQueueServerChannelConfig.java b/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/KQueueServerChannelConfig.java index 09291f5..afd19e4 100644 --- a/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/KQueueServerChannelConfig.java +++ b/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/KQueueServerChannelConfig.java @@ -31,6 +31,7 @@ import java.util.Map; import static io.netty.channel.ChannelOption.SO_BACKLOG; import static io.netty.channel.ChannelOption.SO_RCVBUF; import static io.netty.channel.ChannelOption.SO_REUSEADDR; +import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero; @UnstableApi public class KQueueServerChannelConfig extends KQueueChannelConfig implements ServerSocketChannelConfig { @@ -77,6 +78,7 @@ public class KQueueServerChannelConfig extends KQueueChannelConfig implements Se return true; } + @Override public boolean isReuseAddress() { try { return ((AbstractKQueueChannel) channel).socket.isReuseAddress(); @@ -85,6 +87,7 @@ public class KQueueServerChannelConfig extends KQueueChannelConfig implements Se } } + @Override public KQueueServerChannelConfig setReuseAddress(boolean reuseAddress) { try { ((AbstractKQueueChannel) channel).socket.setReuseAddress(reuseAddress); @@ -94,6 +97,7 @@ public class KQueueServerChannelConfig extends KQueueChannelConfig implements Se } } + @Override public int getReceiveBufferSize() { try { return ((AbstractKQueueChannel) channel).socket.getReceiveBufferSize(); @@ -102,6 +106,7 @@ public class KQueueServerChannelConfig extends KQueueChannelConfig implements Se } } + @Override public KQueueServerChannelConfig setReceiveBufferSize(int receiveBufferSize) { try { ((AbstractKQueueChannel) channel).socket.setReceiveBufferSize(receiveBufferSize); @@ -111,14 +116,14 @@ public class KQueueServerChannelConfig extends KQueueChannelConfig implements Se } } + @Override public int getBacklog() { return backlog; } + @Override public KQueueServerChannelConfig setBacklog(int backlog) { - if (backlog < 0) { - throw new IllegalArgumentException("backlog: " + backlog); - } + checkPositiveOrZero(backlog, "backlog"); this.backlog = backlog; return this; } diff --git a/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/NativeLongArray.java b/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/NativeLongArray.java index 7f83738..edcadf5 100644 --- a/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/NativeLongArray.java +++ b/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/NativeLongArray.java @@ -63,6 +63,10 @@ final class NativeLongArray { return size == 0; } + int size() { + return size; + } + void free() { Buffer.free(memory); memoryAddress = 0; diff --git a/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueDatagramUnicastIPv6MappedTest.java b/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueDatagramUnicastIPv6MappedTest.java new file mode 100644 index 0000000..a216d4e --- /dev/null +++ b/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueDatagramUnicastIPv6MappedTest.java @@ -0,0 +1,29 @@ +/* + * Copyright 2019 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.channel.kqueue; + +import io.netty.bootstrap.Bootstrap; +import io.netty.testsuite.transport.TestsuitePermutation.BootstrapComboFactory; +import io.netty.testsuite.transport.socket.DatagramUnicastIPv6MappedTest; + +import java.util.List; + +public class KQueueDatagramUnicastIPv6MappedTest extends DatagramUnicastIPv6MappedTest { + @Override + protected List<BootstrapComboFactory<Bootstrap, Bootstrap>> newFactories() { + return KQueueSocketTestPermutation.INSTANCE.datagram(internetProtocolFamily()); + } +} diff --git a/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueDatagramUnicastIPv6Test.java b/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueDatagramUnicastIPv6Test.java new file mode 100644 index 0000000..226fc3b --- /dev/null +++ b/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueDatagramUnicastIPv6Test.java @@ -0,0 +1,31 @@ +/* + * Copyright 2019 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.channel.kqueue; + +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.socket.InternetProtocolFamily; +import io.netty.testsuite.transport.TestsuitePermutation; +import io.netty.testsuite.transport.socket.DatagramUnicastIPv6Test; +import io.netty.testsuite.transport.socket.DatagramUnicastTest; + +import java.util.List; + +public class KQueueDatagramUnicastIPv6Test extends DatagramUnicastIPv6Test { + @Override + protected List<TestsuitePermutation.BootstrapComboFactory<Bootstrap, Bootstrap>> newFactories() { + return KQueueSocketTestPermutation.INSTANCE.datagram(internetProtocolFamily()); + } +} diff --git a/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueDatagramUnicastTest.java b/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueDatagramUnicastTest.java index c805e90..282c4ab 100644 --- a/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueDatagramUnicastTest.java +++ b/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueDatagramUnicastTest.java @@ -16,6 +16,7 @@ package io.netty.channel.kqueue; import io.netty.bootstrap.Bootstrap; +import io.netty.channel.socket.InternetProtocolFamily; import io.netty.testsuite.transport.TestsuitePermutation; import io.netty.testsuite.transport.socket.DatagramUnicastTest; @@ -24,6 +25,6 @@ import java.util.List; public class KQueueDatagramUnicastTest extends DatagramUnicastTest { @Override protected List<TestsuitePermutation.BootstrapComboFactory<Bootstrap, Bootstrap>> newFactories() { - return KQueueSocketTestPermutation.INSTANCE.datagram(); + return KQueueSocketTestPermutation.INSTANCE.datagram(InternetProtocolFamily.IPv4); } } diff --git a/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueDomainSocketReuseFdTest.java b/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueDomainSocketReuseFdTest.java new file mode 100644 index 0000000..2e239c2 --- /dev/null +++ b/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueDomainSocketReuseFdTest.java @@ -0,0 +1,36 @@ +/* + * Copyright 2019 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.channel.kqueue; + +import io.netty.bootstrap.Bootstrap; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.testsuite.transport.TestsuitePermutation; +import io.netty.testsuite.transport.socket.AbstractSocketReuseFdTest; + +import java.net.SocketAddress; +import java.util.List; + +public class KQueueDomainSocketReuseFdTest extends AbstractSocketReuseFdTest { + @Override + protected SocketAddress newSocketAddress() { + return KQueueSocketTestPermutation.newSocketAddress(); + } + + @Override + protected List<TestsuitePermutation.BootstrapComboFactory<ServerBootstrap, Bootstrap>> newFactories() { + return KQueueSocketTestPermutation.INSTANCE.domainSocket(); + } +} diff --git a/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueDomainSocketShutdownOutputByPeerTest.java b/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueDomainSocketShutdownOutputByPeerTest.java new file mode 100644 index 0000000..76532ac --- /dev/null +++ b/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueDomainSocketShutdownOutputByPeerTest.java @@ -0,0 +1,68 @@ +/* + * Copyright 2019 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.channel.kqueue; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.unix.Buffer; +import io.netty.testsuite.transport.TestsuitePermutation.BootstrapFactory; +import io.netty.testsuite.transport.socket.AbstractSocketShutdownOutputByPeerTest; + +import java.io.IOException; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.util.List; + +public class KQueueDomainSocketShutdownOutputByPeerTest extends AbstractSocketShutdownOutputByPeerTest<BsdSocket> { + + @Override + protected List<BootstrapFactory<ServerBootstrap>> newFactories() { + return KQueueSocketTestPermutation.INSTANCE.serverDomainSocket(); + } + + @Override + protected SocketAddress newSocketAddress() { + return KQueueSocketTestPermutation.newSocketAddress(); + } + + @Override + protected void shutdownOutput(BsdSocket s) throws IOException { + s.shutdown(false, true); + } + + @Override + protected void connect(BsdSocket s, SocketAddress address) throws IOException { + s.connect(address); + } + + @Override + protected void close(BsdSocket s) throws IOException { + s.close(); + } + + @Override + protected void write(BsdSocket s, int data) throws IOException { + final ByteBuffer buf = Buffer.allocateDirectWithNativeOrder(4); + buf.putInt(data); + buf.flip(); + s.write(buf, buf.position(), buf.limit()); + Buffer.free(buf); + } + + @Override + protected BsdSocket newSocket() { + return BsdSocket.newSocketDomain(); + } +} diff --git a/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueDomainSocketSslClientRenegotiateTest.java b/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueDomainSocketSslClientRenegotiateTest.java new file mode 100644 index 0000000..a719b7c --- /dev/null +++ b/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueDomainSocketSslClientRenegotiateTest.java @@ -0,0 +1,42 @@ +/* + * Copyright 2019 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.channel.kqueue; + +import io.netty.bootstrap.Bootstrap; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.handler.ssl.SslContext; +import io.netty.testsuite.transport.TestsuitePermutation; +import io.netty.testsuite.transport.socket.SocketSslClientRenegotiateTest; + +import java.net.SocketAddress; +import java.util.List; + +public class KQueueDomainSocketSslClientRenegotiateTest extends SocketSslClientRenegotiateTest { + + public KQueueDomainSocketSslClientRenegotiateTest(SslContext serverCtx, SslContext clientCtx, boolean delegate) { + super(serverCtx, clientCtx, delegate); + } + + @Override + protected List<TestsuitePermutation.BootstrapComboFactory<ServerBootstrap, Bootstrap>> newFactories() { + return KQueueSocketTestPermutation.INSTANCE.domainSocket(); + } + + @Override + protected SocketAddress newSocketAddress() { + return KQueueSocketTestPermutation.newSocketAddress(); + } +} diff --git a/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueDomainSocketSslGreetingTest.java b/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueDomainSocketSslGreetingTest.java index 492021a..8cbaa00 100644 --- a/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueDomainSocketSslGreetingTest.java +++ b/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueDomainSocketSslGreetingTest.java @@ -26,8 +26,8 @@ import java.util.List; public class KQueueDomainSocketSslGreetingTest extends SocketSslGreetingTest { - public KQueueDomainSocketSslGreetingTest(SslContext serverCtx, SslContext clientCtx) { - super(serverCtx, clientCtx); + public KQueueDomainSocketSslGreetingTest(SslContext serverCtx, SslContext clientCtx, boolean delegate) { + super(serverCtx, clientCtx, delegate); } @Override diff --git a/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueETSocketAutoReadTest.java b/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueETSocketAutoReadTest.java index 27dd392..a8cd58d 100644 --- a/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueETSocketAutoReadTest.java +++ b/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueETSocketAutoReadTest.java @@ -17,7 +17,6 @@ package io.netty.channel.kqueue; import io.netty.bootstrap.Bootstrap; import io.netty.bootstrap.ServerBootstrap; -import io.netty.buffer.ByteBufAllocator; import io.netty.testsuite.transport.TestsuitePermutation; import io.netty.testsuite.transport.socket.SocketAutoReadTest; diff --git a/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueETSocketExceptionHandlingTest.java b/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueETSocketExceptionHandlingTest.java index e65bcdd..3a7f151 100644 --- a/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueETSocketExceptionHandlingTest.java +++ b/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueETSocketExceptionHandlingTest.java @@ -17,7 +17,6 @@ package io.netty.channel.kqueue; import io.netty.bootstrap.Bootstrap; import io.netty.bootstrap.ServerBootstrap; -import io.netty.buffer.ByteBufAllocator; import io.netty.testsuite.transport.TestsuitePermutation; import io.netty.testsuite.transport.socket.SocketExceptionHandlingTest; diff --git a/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueETSocketReadPendingTest.java b/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueETSocketReadPendingTest.java index 628084c..2197b04 100644 --- a/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueETSocketReadPendingTest.java +++ b/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueETSocketReadPendingTest.java @@ -17,7 +17,6 @@ package io.netty.channel.kqueue; import io.netty.bootstrap.Bootstrap; import io.netty.bootstrap.ServerBootstrap; -import io.netty.buffer.ByteBufAllocator; import io.netty.testsuite.transport.TestsuitePermutation; import io.netty.testsuite.transport.socket.SocketReadPendingTest; diff --git a/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueEventLoopTest.java b/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueEventLoopTest.java index 0d44155..55d2e16 100644 --- a/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueEventLoopTest.java +++ b/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueEventLoopTest.java @@ -17,6 +17,9 @@ package io.netty.channel.kqueue; import io.netty.channel.EventLoop; import io.netty.channel.EventLoopGroup; +import io.netty.channel.ServerChannel; +import io.netty.channel.socket.ServerSocketChannel; +import io.netty.testsuite.transport.AbstractSingleThreadEventLoopTest; import io.netty.util.concurrent.Future; import org.junit.Test; @@ -25,7 +28,22 @@ import java.util.concurrent.TimeUnit; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; -public class KQueueEventLoopTest { +public class KQueueEventLoopTest extends AbstractSingleThreadEventLoopTest { + + @Override + protected EventLoopGroup newEventLoopGroup() { + return new KQueueEventLoopGroup(); + } + + @Override + protected ServerSocketChannel newChannel() { + return new KQueueServerSocketChannel(); + } + + @Override + protected Class<? extends ServerChannel> serverChannelClass() { + return KQueueServerSocketChannel.class; + } @Test public void testScheduleBigDelayNotOverflow() { diff --git a/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueSocketConnectionAttemptTest.java b/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueSocketConnectionAttemptTest.java index 824c47c..8fa23e9 100644 --- a/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueSocketConnectionAttemptTest.java +++ b/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueSocketConnectionAttemptTest.java @@ -16,13 +16,9 @@ package io.netty.channel.kqueue; import io.netty.bootstrap.Bootstrap; -import io.netty.channel.Channel; -import io.netty.channel.ChannelInitializer; import io.netty.testsuite.transport.TestsuitePermutation; import io.netty.testsuite.transport.socket.SocketConnectionAttemptTest; -import org.junit.Test; -import java.net.InetSocketAddress; import java.util.List; public class KQueueSocketConnectionAttemptTest extends SocketConnectionAttemptTest { diff --git a/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueSocketSslClientRenegotiateTest.java b/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueSocketSslClientRenegotiateTest.java new file mode 100644 index 0000000..a3ba238 --- /dev/null +++ b/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueSocketSslClientRenegotiateTest.java @@ -0,0 +1,36 @@ +/* + * Copyright 2019 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.channel.kqueue; + +import io.netty.bootstrap.Bootstrap; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.handler.ssl.SslContext; +import io.netty.testsuite.transport.TestsuitePermutation; +import io.netty.testsuite.transport.socket.SocketSslClientRenegotiateTest; + +import java.util.List; + +public class KQueueSocketSslClientRenegotiateTest extends SocketSslClientRenegotiateTest { + + public KQueueSocketSslClientRenegotiateTest(SslContext serverCtx, SslContext clientCtx, boolean delegate) { + super(serverCtx, clientCtx, delegate); + } + + @Override + protected List<TestsuitePermutation.BootstrapComboFactory<ServerBootstrap, Bootstrap>> newFactories() { + return KQueueSocketTestPermutation.INSTANCE.socket(); + } +} diff --git a/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueSocketSslGreetingTest.java b/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueSocketSslGreetingTest.java index 9242fc3..6eecc35 100644 --- a/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueSocketSslGreetingTest.java +++ b/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueSocketSslGreetingTest.java @@ -25,8 +25,8 @@ import java.util.List; public class KQueueSocketSslGreetingTest extends SocketSslGreetingTest { - public KQueueSocketSslGreetingTest(SslContext serverCtx, SslContext clientCtx) { - super(serverCtx, clientCtx); + public KQueueSocketSslGreetingTest(SslContext serverCtx, SslContext clientCtx, boolean delegate) { + super(serverCtx, clientCtx, delegate); } @Override diff --git a/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueSocketSslSessionReuseTest.java b/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueSocketSslSessionReuseTest.java new file mode 100644 index 0000000..5508dcb --- /dev/null +++ b/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueSocketSslSessionReuseTest.java @@ -0,0 +1,36 @@ +/* + * Copyright 2019 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.channel.kqueue; + +import io.netty.bootstrap.Bootstrap; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.handler.ssl.SslContext; +import io.netty.testsuite.transport.TestsuitePermutation; +import io.netty.testsuite.transport.socket.SocketSslSessionReuseTest; + +import java.util.List; + +public class KQueueSocketSslSessionReuseTest extends SocketSslSessionReuseTest { + + public KQueueSocketSslSessionReuseTest(SslContext serverCtx, SslContext clientCtx) { + super(serverCtx, clientCtx); + } + + @Override + protected List<TestsuitePermutation.BootstrapComboFactory<ServerBootstrap, Bootstrap>> newFactories() { + return KQueueSocketTestPermutation.INSTANCE.socket(); + } +} diff --git a/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueSocketTest.java b/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueSocketTest.java index 38f09c0..201fa9e 100644 --- a/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueSocketTest.java +++ b/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueSocketTest.java @@ -24,8 +24,7 @@ import org.junit.Test; import java.io.IOException; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.*; import static org.junit.Assume.assumeTrue; public class KQueueSocketTest extends SocketTest<BsdSocket> { @@ -55,6 +54,33 @@ public class KQueueSocketTest extends SocketTest<BsdSocket> { } } + @Test + public void testPeerPID() throws IOException { + BsdSocket s1 = BsdSocket.newSocketDomain(); + BsdSocket s2 = BsdSocket.newSocketDomain(); + + try { + DomainSocketAddress dsa = UnixTestUtils.newSocketAddress(); + s1.bind(dsa); + s1.listen(1); + + // PID of client socket is expected to be 0 before connection + assertEquals(0, s2.getPeerCredentials().pid()); + assertTrue(s2.connect(dsa)); + byte [] addr = new byte[64]; + int clientFd = s1.accept(addr); + assertNotEquals(-1, clientFd); + PeerCredentials pc = new BsdSocket(clientFd).getPeerCredentials(); + assertNotEquals(0, pc.pid()); + assertNotEquals(0, s2.getPeerCredentials().pid()); + // Server socket FDs should not have pid field set: + assertEquals(0, s1.getPeerCredentials().pid()); + } finally { + s1.close(); + s2.close(); + } + } + @Override protected BsdSocket newSocket() { return BsdSocket.newSocketStream(); diff --git a/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueSocketTestPermutation.java b/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueSocketTestPermutation.java index ab2bef9..9abbdec 100644 --- a/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueSocketTestPermutation.java +++ b/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueSocketTestPermutation.java @@ -103,7 +103,8 @@ class KQueueSocketTestPermutation extends SocketTestPermutation { } @Override - public List<TestsuitePermutation.BootstrapComboFactory<Bootstrap, Bootstrap>> datagram() { + public List<TestsuitePermutation.BootstrapComboFactory<Bootstrap, Bootstrap>> datagram( + final InternetProtocolFamily family) { // Make the list of Bootstrap factories. @SuppressWarnings("unchecked") List<BootstrapFactory<Bootstrap>> bfs = Arrays.asList( @@ -113,7 +114,7 @@ class KQueueSocketTestPermutation extends SocketTestPermutation { return new Bootstrap().group(nioWorkerGroup).channelFactory(new ChannelFactory<Channel>() { @Override public Channel newChannel() { - return new NioDatagramChannel(InternetProtocolFamily.IPv4); + return new NioDatagramChannel(family); } @Override diff --git a/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KqueueWriteBeforeRegisteredTest.java b/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KqueueWriteBeforeRegisteredTest.java new file mode 100644 index 0000000..b8d4475 --- /dev/null +++ b/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KqueueWriteBeforeRegisteredTest.java @@ -0,0 +1,30 @@ +/* + * Copyright 2019 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.channel.kqueue; + +import io.netty.bootstrap.Bootstrap; +import io.netty.testsuite.transport.TestsuitePermutation; +import io.netty.testsuite.transport.socket.WriteBeforeRegisteredTest; + +import java.util.List; + +public class KqueueWriteBeforeRegisteredTest extends WriteBeforeRegisteredTest { + + @Override + protected List<TestsuitePermutation.BootstrapFactory<Bootstrap>> newFactories() { + return KQueueSocketTestPermutation.INSTANCE.clientSocket(); + } +} diff --git a/transport-native-unix-common-tests/pom.xml b/transport-native-unix-common-tests/pom.xml index 79a2317..032cf74 100644 --- a/transport-native-unix-common-tests/pom.xml +++ b/transport-native-unix-common-tests/pom.xml @@ -19,7 +19,7 @@ <parent> <groupId>io.netty</groupId> <artifactId>netty-parent</artifactId> - <version>4.1.33.Final</version> + <version>4.1.48.Final</version> </parent> <artifactId>netty-transport-native-unix-common-tests</artifactId> diff --git a/transport-native-unix-common/pom.xml b/transport-native-unix-common/pom.xml index 586a004..f92050f 100644 --- a/transport-native-unix-common/pom.xml +++ b/transport-native-unix-common/pom.xml @@ -19,7 +19,7 @@ <parent> <groupId>io.netty</groupId> <artifactId>netty-parent</artifactId> - <version>4.1.33.Final</version> + <version>4.1.48.Final</version> </parent> <artifactId>netty-transport-native-unix-common</artifactId> @@ -44,6 +44,29 @@ <nativeJarFile>${project.build.directory}/${project.build.finalName}-${jni.classifier}.jar</nativeJarFile> </properties> + <build> + <plugins> + <!-- Also include c files in source jar --> + <plugin> + <groupId>org.codehaus.mojo</groupId> + <artifactId>build-helper-maven-plugin</artifactId> + <executions> + <execution> + <phase>generate-sources</phase> + <goals> + <goal>add-source</goal> + </goals> + <configuration> + <sources> + <source>${nativeIncludeDir}</source> + </sources> + </configuration> + </execution> + </executions> + </plugin> + </plugins> + </build> + <profiles> <profile> <id>mac</id> diff --git a/transport-native-unix-common/src/main/c/netty_unix_errors.c b/transport-native-unix-common/src/main/c/netty_unix_errors.c index 8298f91..634c99b 100644 --- a/transport-native-unix-common/src/main/c/netty_unix_errors.c +++ b/transport-native-unix-common/src/main/c/netty_unix_errors.c @@ -21,6 +21,7 @@ #include "netty_unix_jni.h" #include "netty_unix_util.h" +static jclass oomErrorClass = NULL; static jclass runtimeExceptionClass = NULL; static jclass channelExceptionClass = NULL; static jclass ioExceptionClass = NULL; @@ -43,12 +44,18 @@ void netty_unix_errors_throwRuntimeException(JNIEnv* env, char* message) { void netty_unix_errors_throwRuntimeExceptionErrorNo(JNIEnv* env, char* message, int errorNumber) { char* allocatedMessage = exceptionMessage(message, errorNumber); + if (allocatedMessage == NULL) { + return; + } (*env)->ThrowNew(env, runtimeExceptionClass, allocatedMessage); free(allocatedMessage); } void netty_unix_errors_throwChannelExceptionErrorNo(JNIEnv* env, char* message, int errorNumber) { char* allocatedMessage = exceptionMessage(message, errorNumber); + if (allocatedMessage == NULL) { + return; + } (*env)->ThrowNew(env, channelExceptionClass, allocatedMessage); free(allocatedMessage); } @@ -63,18 +70,23 @@ void netty_unix_errors_throwPortUnreachableException(JNIEnv* env, char* message) void netty_unix_errors_throwIOExceptionErrorNo(JNIEnv* env, char* message, int errorNumber) { char* allocatedMessage = exceptionMessage(message, errorNumber); + if (allocatedMessage == NULL) { + return; + } (*env)->ThrowNew(env, ioExceptionClass, allocatedMessage); free(allocatedMessage); } void netty_unix_errors_throwClosedChannelException(JNIEnv* env) { jobject exception = (*env)->NewObject(env, closedChannelExceptionClass, closedChannelExceptionMethodId); + if (exception == NULL) { + return; + } (*env)->Throw(env, exception); } void netty_unix_errors_throwOutOfMemoryError(JNIEnv* env) { - jclass exceptionClass = (*env)->FindClass(env, "java/lang/OutOfMemoryError"); - (*env)->ThrowNew(env, exceptionClass, ""); + (*env)->ThrowNew(env, oomErrorClass, ""); } // JNI Registered Methods Begin @@ -151,6 +163,7 @@ static const jint statically_referenced_fixed_method_table_size = sizeof(statica // JNI Method Registration Table End jint netty_unix_errors_JNI_OnLoad(JNIEnv* env, const char* packagePrefix) { + char* nettyClassName = NULL; // We must register the statically referenced methods first! if (netty_unix_util_register_natives(env, packagePrefix, @@ -160,98 +173,33 @@ jint netty_unix_errors_JNI_OnLoad(JNIEnv* env, const char* packagePrefix) { return JNI_ERR; } - jclass localRuntimeExceptionClass = (*env)->FindClass(env, "java/lang/RuntimeException"); - if (localRuntimeExceptionClass == NULL) { - // pending exception... - return JNI_ERR; - } - runtimeExceptionClass = (jclass) (*env)->NewGlobalRef(env, localRuntimeExceptionClass); - if (runtimeExceptionClass == NULL) { - // out-of-memory! - netty_unix_errors_throwOutOfMemoryError(env); - return JNI_ERR; - } + NETTY_LOAD_CLASS(env, oomErrorClass, "java/lang/OutOfMemoryError", error); - char* nettyClassName = netty_unix_util_prepend(packagePrefix, "io/netty/channel/ChannelException"); - jclass localChannelExceptionClass = (*env)->FindClass(env, nettyClassName); - free(nettyClassName); - nettyClassName = NULL; - if (localChannelExceptionClass == NULL) { - // pending exception... - return JNI_ERR; - } - channelExceptionClass = (jclass) (*env)->NewGlobalRef(env, localChannelExceptionClass); - if (channelExceptionClass == NULL) { - // out-of-memory! - netty_unix_errors_throwOutOfMemoryError(env); - return JNI_ERR; - } + NETTY_LOAD_CLASS(env, runtimeExceptionClass, "java/lang/RuntimeException", error); - // cache classes that are used within other jni methods for performance reasons - jclass localClosedChannelExceptionClass = (*env)->FindClass(env, "java/nio/channels/ClosedChannelException"); - if (localClosedChannelExceptionClass == NULL) { - // pending exception... - return JNI_ERR; - } - closedChannelExceptionClass = (jclass) (*env)->NewGlobalRef(env, localClosedChannelExceptionClass); - if (closedChannelExceptionClass == NULL) { - // out-of-memory! - netty_unix_errors_throwOutOfMemoryError(env); - return JNI_ERR; - } - closedChannelExceptionMethodId = (*env)->GetMethodID(env, closedChannelExceptionClass, "<init>", "()V"); - if (closedChannelExceptionMethodId == NULL) { - netty_unix_errors_throwRuntimeException(env, "failed to get method ID: ClosedChannelException.<init>()"); - return JNI_ERR; - } + NETTY_PREPEND(packagePrefix, "io/netty/channel/ChannelException", nettyClassName, error); + NETTY_LOAD_CLASS(env, channelExceptionClass, nettyClassName, error); + netty_unix_util_free_dynamic_name(&nettyClassName); - jclass localIoExceptionClass = (*env)->FindClass(env, "java/io/IOException"); - if (localIoExceptionClass == NULL) { - // pending exception... - return JNI_ERR; - } - ioExceptionClass = (jclass) (*env)->NewGlobalRef(env, localIoExceptionClass); - if (ioExceptionClass == NULL) { - // out-of-memory! - netty_unix_errors_throwOutOfMemoryError(env); - return JNI_ERR; - } + NETTY_LOAD_CLASS(env, closedChannelExceptionClass, "java/nio/channels/ClosedChannelException", error); + NETTY_GET_METHOD(env, closedChannelExceptionClass, closedChannelExceptionMethodId, "<init>", "()V", error); - jclass localPortUnreachableExceptionClass = (*env)->FindClass(env, "java/net/PortUnreachableException"); - if (localPortUnreachableExceptionClass == NULL) { - // pending exception... - return JNI_ERR; - } - portUnreachableExceptionClass = (jclass) (*env)->NewGlobalRef(env, localPortUnreachableExceptionClass); - if (portUnreachableExceptionClass == NULL) { - // out-of-memory! - netty_unix_errors_throwOutOfMemoryError(env); - return JNI_ERR; - } + NETTY_LOAD_CLASS(env, ioExceptionClass, "java/io/IOException", error); + + NETTY_LOAD_CLASS(env, portUnreachableExceptionClass, "java/net/PortUnreachableException", error); return NETTY_JNI_VERSION; +error: + free(nettyClassName); + return JNI_ERR; } void netty_unix_errors_JNI_OnUnLoad(JNIEnv* env) { // delete global references so the GC can collect them - if (runtimeExceptionClass != NULL) { - (*env)->DeleteGlobalRef(env, runtimeExceptionClass); - runtimeExceptionClass = NULL; - } - if (channelExceptionClass != NULL) { - (*env)->DeleteGlobalRef(env, channelExceptionClass); - channelExceptionClass = NULL; - } - if (ioExceptionClass != NULL) { - (*env)->DeleteGlobalRef(env, ioExceptionClass); - ioExceptionClass = NULL; - } - if (portUnreachableExceptionClass != NULL) { - (*env)->DeleteGlobalRef(env, portUnreachableExceptionClass); - portUnreachableExceptionClass = NULL; - } - if (closedChannelExceptionClass != NULL) { - (*env)->DeleteGlobalRef(env, closedChannelExceptionClass); - closedChannelExceptionClass = NULL; - } + NETTY_UNLOAD_CLASS(env, oomErrorClass); + NETTY_UNLOAD_CLASS(env, runtimeExceptionClass); + NETTY_UNLOAD_CLASS(env, channelExceptionClass); + NETTY_UNLOAD_CLASS(env, ioExceptionClass); + NETTY_UNLOAD_CLASS(env, portUnreachableExceptionClass); + NETTY_UNLOAD_CLASS(env, closedChannelExceptionClass); } diff --git a/transport-native-unix-common/src/main/c/netty_unix_filedescriptor.c b/transport-native-unix-common/src/main/c/netty_unix_filedescriptor.c index d13ffb0..7e08bb5 100644 --- a/transport-native-unix-common/src/main/c/netty_unix_filedescriptor.c +++ b/transport-native-unix-common/src/main/c/netty_unix_filedescriptor.c @@ -276,65 +276,41 @@ static const jint method_table_size = sizeof(method_table) / sizeof(method_table // JNI Method Registration Table End jint netty_unix_filedescriptor_JNI_OnLoad(JNIEnv* env, const char* packagePrefix) { + int ret = JNI_ERR; + void* mem = NULL; if (netty_unix_util_register_natives(env, packagePrefix, "io/netty/channel/unix/FileDescriptor", method_table, method_table_size) != 0) { - return JNI_ERR; + goto done; } - void* mem = malloc(1); - if (mem == NULL) { - netty_unix_errors_throwOutOfMemoryError(env); - return JNI_ERR; + if ((mem = malloc(1)) == NULL) { + goto done; } jobject directBuffer = (*env)->NewDirectByteBuffer(env, mem, 1); if (directBuffer == NULL) { - free(mem); - - netty_unix_errors_throwOutOfMemoryError(env); - return JNI_ERR; + goto done; } if ((*env)->GetDirectBufferAddress(env, directBuffer) == NULL) { - free(mem); - - netty_unix_errors_throwRuntimeException(env, "failed to get direct buffer address"); - return JNI_ERR; + goto done; } - jclass cls = (*env)->GetObjectClass(env, directBuffer); - + if (cls == NULL) { + goto done; + } + // Get the method id for Buffer.position() and Buffer.limit(). These are used as fallback if // it is not possible to obtain the position and limit using the fields directly. - posId = (*env)->GetMethodID(env, cls, "position", "()I"); - if (posId == NULL) { - free(mem); + NETTY_GET_METHOD(env, cls, posId, "position", "()I", done); + NETTY_GET_METHOD(env, cls, limitId, "limit", "()I", done); - // position method was not found.. something is wrong so bail out - netty_unix_errors_throwRuntimeException(env, "failed to get method ID: ByteBuffer.position()"); - return JNI_ERR; - } - - limitId = (*env)->GetMethodID(env, cls, "limit", "()I"); - if (limitId == NULL) { - free(mem); - - // limit method was not found.. something is wrong so bail out - netty_unix_errors_throwRuntimeException(env, "failed to get method ID: ByteBuffer.limit()"); - return JNI_ERR; - } // Try to get the ids of the position and limit fields. We later then check if we was able // to find them and if so use them get the position and limit of the buffer. This is // much faster then call back into java via (*env)->CallIntMethod(...). - posFieldId = (*env)->GetFieldID(env, cls, "position", "I"); - if (posFieldId == NULL) { - // this is ok as we can still use the method so just clear the exception - (*env)->ExceptionClear(env); - } - limitFieldId = (*env)->GetFieldID(env, cls, "limit", "I"); - if (limitFieldId == NULL) { - // this is ok as we can still use the method so just clear the exception - (*env)->ExceptionClear(env); - } + NETTY_TRY_GET_FIELD(env, cls, posFieldId, "position", "I"); + NETTY_TRY_GET_FIELD(env, cls, limitFieldId, "limit", "I"); + ret = NETTY_JNI_VERSION; +done: free(mem); - return NETTY_JNI_VERSION; + return ret; } void netty_unix_filedescriptor_JNI_OnUnLoad(JNIEnv* env) { } diff --git a/transport-native-unix-common/src/main/c/netty_unix_socket.c b/transport-native-unix-common/src/main/c/netty_unix_socket.c index 3dd0920..3f04f4b 100644 --- a/transport-native-unix-common/src/main/c/netty_unix_socket.c +++ b/transport-native-unix-common/src/main/c/netty_unix_socket.c @@ -16,6 +16,7 @@ #include <fcntl.h> #include <errno.h> #include <unistd.h> +#include <stddef.h> #include <stdint.h> #include <stdlib.h> #include <string.h> @@ -41,16 +42,16 @@ static jmethodID datagramSocketAddrMethodId = NULL; static jmethodID inetSocketAddrMethodId = NULL; static jclass inetSocketAddressClass = NULL; static int socketType = AF_INET; -static const char* ip4prefix = "::ffff:"; static const unsigned char wildcardAddress[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; static const unsigned char ipv4MappedWildcardAddress[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00 }; +static const unsigned char ipv4MappedAddress[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff }; // Optional external methods extern int accept4(int sockFd, struct sockaddr* addr, socklen_t* addrlen, int flags) __attribute__((weak)) __attribute__((weak_import)); // macro to calculate the length of a sockaddr_un struct for a given path length. // see sys/un.h#SUN_LEN, this is modified to allow nul bytes -#define _UNIX_ADDR_LENGTH(path_len) (uintptr_t) (((struct sockaddr_un *) 0)->sun_path) + path_len +#define _UNIX_ADDR_LENGTH(path_len) ((uintptr_t) offsetof(struct sockaddr_un, sun_path) + (uintptr_t) path_len) static int nettyNonBlockingSocket(int domain, int type, int protocol) { #ifdef SOCK_NONBLOCK @@ -68,47 +69,61 @@ static int nettyNonBlockingSocket(int domain, int type, int protocol) { #endif } +int netty_unix_socket_ipAddressLength(const struct sockaddr_storage* addr) { + if (addr->ss_family == AF_INET) { + return 4; + } + struct sockaddr_in6* s = (struct sockaddr_in6*) addr; + if (memcmp(s->sin6_addr.s6_addr, ipv4MappedAddress, 12) == 0) { + // IPv4-mapped-on-IPv6 + return 4; + } + return 16; +} + static jobject createDatagramSocketAddress(JNIEnv* env, const struct sockaddr_storage* addr, int len, jobject local) { - char ipstr[INET6_ADDRSTRLEN]; int port; - jstring ipString; + int scopeId; + int ipLength = netty_unix_socket_ipAddressLength(addr); + jbyteArray addressBytes = (*env)->NewByteArray(env, ipLength); + if (addressBytes == NULL) { + return NULL; + } if (addr->ss_family == AF_INET) { struct sockaddr_in* s = (struct sockaddr_in*) addr; port = ntohs(s->sin_port); - inet_ntop(AF_INET, &s->sin_addr, ipstr, sizeof ipstr); - ipString = (*env)->NewStringUTF(env, ipstr); + scopeId = 0; + (*env)->SetByteArrayRegion(env, addressBytes, 0, ipLength, (jbyte*) &s->sin_addr.s_addr); } else { struct sockaddr_in6* s = (struct sockaddr_in6*) addr; port = ntohs(s->sin6_port); - inet_ntop(AF_INET6, &s->sin6_addr, ipstr, sizeof ipstr); + scopeId = s->sin6_scope_id; - if (strncasecmp(ipstr, ip4prefix, 7) == 0) { + int offset; + if (ipLength == 4) { // IPv4-mapped-on-IPv6. - // Cut of ::ffff: prefix to workaround performance issues when parsing these - // addresses in InetAddress.getByName(...). - // - // See https://github.com/netty/netty/issues/2867 - ipString = (*env)->NewStringUTF(env, &ipstr[7]); + offset = 12; } else { - ipString = (*env)->NewStringUTF(env, ipstr); + offset = 0; } + jbyte* addr = (jbyte*) &s->sin6_addr.s6_addr; + (*env)->SetByteArrayRegion(env, addressBytes, 0, ipLength, addr + offset); + } + jobject obj = (*env)->NewObject(env, datagramSocketAddressClass, datagramSocketAddrMethodId, addressBytes, scopeId, port, len, local); + if ((*env)->ExceptionCheck(env) == JNI_TRUE) { + return NULL; } - jobject socketAddr = (*env)->NewObject(env, datagramSocketAddressClass, datagramSocketAddrMethodId, ipString, port, len, local); - return socketAddr; + return obj; } static jsize addressLength(const struct sockaddr_storage* addr) { - if (addr->ss_family == AF_INET) { - return 8; - } - struct sockaddr_in6* s = (struct sockaddr_in6*) addr; - if (s->sin6_addr.s6_addr[11] == 0xff && s->sin6_addr.s6_addr[10] == 0xff && - s->sin6_addr.s6_addr[9] == 0x00 && s->sin6_addr.s6_addr[8] == 0x00 && s->sin6_addr.s6_addr[7] == 0x00 && s->sin6_addr.s6_addr[6] == 0x00 && s->sin6_addr.s6_addr[5] == 0x00 && - s->sin6_addr.s6_addr[4] == 0x00 && s->sin6_addr.s6_addr[3] == 0x00 && s->sin6_addr.s6_addr[2] == 0x00 && s->sin6_addr.s6_addr[1] == 0x00 && s->sin6_addr.s6_addr[0] == 0x00) { - // IPv4-mapped-on-IPv6 - return 8; + int len = netty_unix_socket_ipAddressLength(addr); + if (len == 4) { + // Only encode port into it + return len + 4; } - return 24; + // we encode port + scope into it + return len + 8; } static void initInetSocketAddressArray(JNIEnv* env, const struct sockaddr_storage* addr, jbyteArray bArray, int offset, jsize len) { @@ -159,10 +174,12 @@ static void initInetSocketAddressArray(JNIEnv* env, const struct sockaddr_storag } } -static jbyteArray createInetSocketAddressArray(JNIEnv* env, const struct sockaddr_storage* addr) { +jbyteArray netty_unix_socket_createInetSocketAddressArray(JNIEnv* env, const struct sockaddr_storage* addr) { jsize len = addressLength(addr); jbyteArray bArray = (*env)->NewByteArray(env, len); - + if (bArray == NULL) { + return NULL; + } initInetSocketAddressArray(env, addr, bArray, 0, len); return bArray; } @@ -190,6 +207,22 @@ static void netty_unix_socket_initialize(JNIEnv* env, jclass clazz, jboolean ipv } } +static jboolean netty_unix_socket_isIPv6Preferred(JNIEnv* env, jclass clazz) { + return socketType == AF_INET6; +} + + +static jboolean netty_unix_socket_isIPv6(JNIEnv* env, jclass clazz, jint fd) { + struct sockaddr_storage addr; + socklen_t addrlen = sizeof(addr); + if (getsockname(fd, (struct sockaddr*) &addr, &addrlen) == 0) { + return ((struct sockaddr*) &addr)->sa_family == AF_INET6; + } + + netty_unix_errors_throwChannelExceptionErrorNo(env, "getsockname(...) failed: ", errno); + return JNI_FALSE; +} + static void netty_unix_socket_optionHandleError(JNIEnv* env, int err, char* method) { if (err == EBADF) { netty_unix_errors_throwClosedChannelException(env); @@ -206,11 +239,11 @@ static int netty_unix_socket_setOption0(jint fd, int level, int optname, const v return setsockopt(fd, level, optname, optval, len); } -static jint _socket(JNIEnv* env, jclass clazz, int type) { - int fd = nettyNonBlockingSocket(socketType, type, 0); +static jint _socket(JNIEnv* env, jclass clazz, int domain, int type) { + int fd = nettyNonBlockingSocket(domain, type, 0); if (fd == -1) { return -errno; - } else if (socketType == AF_INET6) { + } else if (domain == AF_INET6) { // Try to allow listen /connect ipv4 and ipv6 int optval = 0; if (netty_unix_socket_setOption0(fd, IPPROTO_IPV6, IPV6_V6ONLY, &optval, sizeof(optval)) < 0) { @@ -229,20 +262,29 @@ static jint _socket(JNIEnv* env, jclass clazz, int type) { return fd; } -int netty_unix_socket_initSockaddr(JNIEnv* env, jbyteArray address, jint scopeId, jint jport, +int netty_unix_socket_initSockaddr(JNIEnv* env, jboolean ipv6, jbyteArray address, jint scopeId, jint jport, const struct sockaddr_storage* addr, socklen_t* addrSize) { uint16_t port = htons((uint16_t) jport); + // We use 16 bytes as this allows us to fit ipv6, ipv4 and ipv4 mapped ipv6 addresses in the array. + jbyte addressBytes[16]; - // Use GetPrimitiveArrayCritical and ReleasePrimitiveArrayCritical to signal the VM that we really would like - // to not do a memory copy here. This is ok as we not do any blocking action here anyway. - // This is important as the VM may suspend GC for the time! - jbyte* addressBytes = (*env)->GetPrimitiveArrayCritical(env, address, 0); - if (addressBytes == NULL) { - // No memory left ?!?!? - netty_unix_errors_throwOutOfMemoryError(env); + int len = (*env)->GetArrayLength(env, address); + + if (len > 16) { + // This should never happen but let's guard against it anyway. return -1; } - if (socketType == AF_INET6) { + + // We use GetByteArrayRegion(...) and copy into a small stack allocated buffer and NOT GetPrimitiveArrayCritical(...) + // as there are still multiple GCLocker related bugs which are not fixed yet. + // + // For example: + // https://bugs.openjdk.java.net/browse/JDK-8048556 + // https://bugs.openjdk.java.net/browse/JDK-8057573 + // https://bugs.openjdk.java.net/browse/JDK-8057586 + (*env)->GetByteArrayRegion(env, address, 0, len, addressBytes); + + if (ipv6 == JNI_TRUE) { struct sockaddr_in6* ip6addr = (struct sockaddr_in6*) addr; *addrSize = sizeof(struct sockaddr_in6); ip6addr->sin6_family = AF_INET6; @@ -262,15 +304,13 @@ int netty_unix_socket_initSockaddr(JNIEnv* env, jbyteArray address, jint scopeId ipaddr->sin_port = port; memcpy(&(ipaddr->sin_addr.s_addr), addressBytes + 12, 4); } - - (*env)->ReleasePrimitiveArrayCritical(env, address, addressBytes, JNI_ABORT); return 0; } -static jint _sendTo(JNIEnv* env, jint fd, void* buffer, jint pos, jint limit, jbyteArray address, jint scopeId, jint port) { +static jint _sendTo(JNIEnv* env, jint fd, jboolean ipv6, void* buffer, jint pos, jint limit, jbyteArray address, jint scopeId, jint port) { struct sockaddr_storage addr; socklen_t addrSize; - if (netty_unix_socket_initSockaddr(env, address, scopeId, port, &addr, &addrSize) == -1) { + if (netty_unix_socket_initSockaddr(env, ipv6, address, scopeId, port, &addr, &addrSize) == -1) { return -1; } @@ -350,11 +390,17 @@ static jobject _recvFrom(JNIEnv* env, jint fd, void* buffer, jint pos, jint limi } #ifdef IP_RECVORIGDSTADDR +#if !defined(SOL_IP) && defined(IPPROTO_IP) +#define SOL_IP IPPROTO_IP +#endif /* !SOL_IP && IPPROTO_IP */ if (readLocalAddr) { for (cmsg = CMSG_FIRSTHDR(&msg); cmsg != NULL; cmsg = CMSG_NXTHDR(&msg, cmsg)) { if (cmsg->cmsg_level == SOL_IP && cmsg->cmsg_type == IP_RECVORIGDSTADDR) { memcpy (&daddr, CMSG_DATA(cmsg), sizeof (struct sockaddr_storage)); local = createDatagramSocketAddress(env, &daddr, res, NULL); + if (local == NULL) { + return NULL; + } break; } } @@ -405,10 +451,10 @@ static jint netty_unix_socket_shutdown(JNIEnv* env, jclass clazz, jint fd, jbool return 0; } -static jint netty_unix_socket_bind(JNIEnv* env, jclass clazz, jint fd, jbyteArray address, jint scopeId, jint port) { +static jint netty_unix_socket_bind(JNIEnv* env, jclass clazz, jint fd, jboolean ipv6, jbyteArray address, jint scopeId, jint port) { struct sockaddr_storage addr; socklen_t addrSize; - if (netty_unix_socket_initSockaddr(env, address, scopeId, port, &addr, &addrSize) == -1) { + if (netty_unix_socket_initSockaddr(env, ipv6, address, scopeId, port, &addr, &addrSize) == -1) { return -1; } @@ -425,10 +471,10 @@ static jint netty_unix_socket_listen(JNIEnv* env, jclass clazz, jint fd, jint ba return 0; } -static jint netty_unix_socket_connect(JNIEnv* env, jclass clazz, jint fd, jbyteArray address, jint scopeId, jint port) { +static jint netty_unix_socket_connect(JNIEnv* env, jclass clazz, jint fd, jboolean ipv6, jbyteArray address, jint scopeId, jint port) { struct sockaddr_storage addr; socklen_t addrSize; - if (netty_unix_socket_initSockaddr(env, address, scopeId, port, &addr, &addrSize) == -1) { + if (netty_unix_socket_initSockaddr(env, ipv6, address, scopeId, port, &addr, &addrSize) == -1) { // A runtime exception was thrown return -1; } @@ -463,7 +509,7 @@ static jint netty_unix_socket_finishConnect(JNIEnv* env, jclass clazz, jint fd) return -optval; } -static jint netty_unix_socket_disconnect(JNIEnv* env, jclass clazz, jint fd) { +static jint netty_unix_socket_disconnect(JNIEnv* env, jclass clazz, jint fd, jboolean ipv6) { struct sockaddr_storage addr; int len; @@ -471,7 +517,7 @@ static jint netty_unix_socket_disconnect(JNIEnv* env, jclass clazz, jint fd) { // You can disconnect connection-less sockets by using AF_UNSPEC. // See man 2 connect. - if (socketType == AF_INET6) { + if (ipv6 == JNI_TRUE) { struct sockaddr_in6* ip6addr = (struct sockaddr_in6*) &addr; ip6addr->sin6_family = AF_UNSPEC; len = sizeof(struct sockaddr_in6); @@ -498,6 +544,7 @@ static jint netty_unix_socket_disconnect(JNIEnv* env, jclass clazz, jint fd) { static jint netty_unix_socket_accept(JNIEnv* env, jclass clazz, jint fd, jbyteArray acceptedAddress) { jint socketFd; jsize len; + jbyte len_b; int err; struct sockaddr_storage addr; socklen_t address_len = sizeof(addr); @@ -522,9 +569,10 @@ static jint netty_unix_socket_accept(JNIEnv* env, jclass clazz, jint fd, jbyteAr } len = addressLength(&addr); + len_b = (jbyte) len; // Fill in remote address details - (*env)->SetByteArrayRegion(env, acceptedAddress, 0, 4, (jbyte*) &len); + (*env)->SetByteArrayRegion(env, acceptedAddress, 0, 1, (jbyte*) &len_b); initInetSocketAddressArray(env, &addr, acceptedAddress, 1, len); if (accept4) { @@ -543,7 +591,7 @@ static jbyteArray netty_unix_socket_remoteAddress(JNIEnv* env, jclass clazz, jin if (getpeername(fd, (struct sockaddr*) &addr, &len) == -1) { return NULL; } - return createInetSocketAddressArray(env, &addr); + return netty_unix_socket_createInetSocketAddressArray(env, &addr); } static jbyteArray netty_unix_socket_localAddress(JNIEnv* env, jclass clazz, jint fd) { @@ -552,15 +600,17 @@ static jbyteArray netty_unix_socket_localAddress(JNIEnv* env, jclass clazz, jint if (getsockname(fd, (struct sockaddr*) &addr, &len) == -1) { return NULL; } - return createInetSocketAddressArray(env, &addr); + return netty_unix_socket_createInetSocketAddressArray(env, &addr); } -static jint netty_unix_socket_newSocketDgramFd(JNIEnv* env, jclass clazz) { - return _socket(env, clazz, SOCK_DGRAM); +static jint netty_unix_socket_newSocketDgramFd(JNIEnv* env, jclass clazz, jboolean ipv6) { + int domain = ipv6 == JNI_TRUE ? AF_INET6 : AF_INET; + return _socket(env, clazz, domain, SOCK_DGRAM); } -static jint netty_unix_socket_newSocketStreamFd(JNIEnv* env, jclass clazz) { - return _socket(env, clazz, SOCK_STREAM); +static jint netty_unix_socket_newSocketStreamFd(JNIEnv* env, jclass clazz, jboolean ipv6) { + int domain = ipv6 == JNI_TRUE ? AF_INET6 : AF_INET; + return _socket(env, clazz, domain, SOCK_STREAM); } static jint netty_unix_socket_newSocketDomainFd(JNIEnv* env, jclass clazz) { @@ -571,19 +621,19 @@ static jint netty_unix_socket_newSocketDomainFd(JNIEnv* env, jclass clazz) { return fd; } -static jint netty_unix_socket_sendTo(JNIEnv* env, jclass clazz, jint fd, jobject jbuffer, jint pos, jint limit, jbyteArray address, jint scopeId, jint port) { +static jint netty_unix_socket_sendTo(JNIEnv* env, jclass clazz, jint fd, jboolean ipv6, jobject jbuffer, jint pos, jint limit, jbyteArray address, jint scopeId, jint port) { // We check that GetDirectBufferAddress will not return NULL in OnLoad - return _sendTo(env, fd, (*env)->GetDirectBufferAddress(env, jbuffer), pos, limit, address, scopeId, port); + return _sendTo(env, fd, ipv6, (*env)->GetDirectBufferAddress(env, jbuffer), pos, limit, address, scopeId, port); } -static jint netty_unix_socket_sendToAddress(JNIEnv* env, jclass clazz, jint fd, jlong memoryAddress, jint pos, jint limit, jbyteArray address, jint scopeId, jint port) { - return _sendTo(env, fd, (void *) (intptr_t) memoryAddress, pos, limit, address, scopeId, port); +static jint netty_unix_socket_sendToAddress(JNIEnv* env, jclass clazz, jint fd, jboolean ipv6, jlong memoryAddress, jint pos, jint limit, jbyteArray address, jint scopeId, jint port) { + return _sendTo(env, fd, ipv6, (void *) (intptr_t) memoryAddress, pos, limit, address, scopeId, port); } -static jint netty_unix_socket_sendToAddresses(JNIEnv* env, jclass clazz, jint fd, jlong memoryAddress, jint length, jbyteArray address, jint scopeId, jint port) { +static jint netty_unix_socket_sendToAddresses(JNIEnv* env, jclass clazz, jint fd, jboolean ipv6, jlong memoryAddress, jint length, jbyteArray address, jint scopeId, jint port) { struct sockaddr_storage addr; socklen_t addrSize; - if (netty_unix_socket_initSockaddr(env, address, scopeId, port, &addr, &addrSize) == -1) { + if (netty_unix_socket_initSockaddr(env, ipv6, address, scopeId, port, &addr, &addrSize) == -1) { return -1; } @@ -793,8 +843,8 @@ static void netty_unix_socket_setSoLinger(JNIEnv* env, jclass clazz, jint fd, ji netty_unix_socket_setOption(env, fd, SOL_SOCKET, SO_LINGER, &solinger, sizeof(solinger)); } -static void netty_unix_socket_setTrafficClass(JNIEnv* env, jclass clazz, jint fd, jint optval) { - if (socketType == AF_INET6) { +static void netty_unix_socket_setTrafficClass(JNIEnv* env, jclass clazz, jint fd, jboolean ipv6, jint optval) { + if (ipv6 == JNI_TRUE) { // This call will put an exception on the stack to be processed once the JNI calls completes if // setsockopt failed and return a negative value. int rc = netty_unix_socket_setOption(env, fd, IPPROTO_IPV6, IPV6_TCLASS, &optval, sizeof(optval)); @@ -860,9 +910,9 @@ static jint netty_unix_socket_getSoLinger(JNIEnv* env, jclass clazz, jint fd) { } } -static jint netty_unix_socket_getTrafficClass(JNIEnv* env, jclass clazz, jint fd) { +static jint netty_unix_socket_getTrafficClass(JNIEnv* env, jclass clazz, jint fd, jboolean ipv6) { int optval; - if (socketType == AF_INET6) { + if (ipv6 == JNI_TRUE) { if (netty_unix_socket_getOption0(fd, IPPROTO_IPV6, IPV6_TCLASS, &optval, sizeof(optval)) == -1) { if (errno == ENOPROTOOPT) { if (netty_unix_socket_getOption(env, fd, IPPROTO_IP, IP_TOS, &optval, sizeof(optval)) == -1) { @@ -919,20 +969,20 @@ static jint netty_unix_socket_isBroadcast(JNIEnv* env, jclass clazz, jint fd) { // JNI Method Registration Table Begin static const JNINativeMethod fixed_method_table[] = { { "shutdown", "(IZZ)I", (void *) netty_unix_socket_shutdown }, - { "bind", "(I[BII)I", (void *) netty_unix_socket_bind }, + { "bind", "(IZ[BII)I", (void *) netty_unix_socket_bind }, { "listen", "(II)I", (void *) netty_unix_socket_listen }, - { "connect", "(I[BII)I", (void *) netty_unix_socket_connect }, + { "connect", "(IZ[BII)I", (void *) netty_unix_socket_connect }, { "finishConnect", "(I)I", (void *) netty_unix_socket_finishConnect }, - { "disconnect", "(I)I", (void *) netty_unix_socket_disconnect}, + { "disconnect", "(IZ)I", (void *) netty_unix_socket_disconnect}, { "accept", "(I[B)I", (void *) netty_unix_socket_accept }, { "remoteAddress", "(I)[B", (void *) netty_unix_socket_remoteAddress }, { "localAddress", "(I)[B", (void *) netty_unix_socket_localAddress }, - { "newSocketDgramFd", "()I", (void *) netty_unix_socket_newSocketDgramFd }, - { "newSocketStreamFd", "()I", (void *) netty_unix_socket_newSocketStreamFd }, + { "newSocketDgramFd", "(Z)I", (void *) netty_unix_socket_newSocketDgramFd }, + { "newSocketStreamFd", "(Z)I", (void *) netty_unix_socket_newSocketStreamFd }, { "newSocketDomainFd", "()I", (void *) netty_unix_socket_newSocketDomainFd }, - { "sendTo", "(ILjava/nio/ByteBuffer;II[BII)I", (void *) netty_unix_socket_sendTo }, - { "sendToAddress", "(IJII[BII)I", (void *) netty_unix_socket_sendToAddress }, - { "sendToAddresses", "(IJI[BII)I", (void *) netty_unix_socket_sendToAddresses }, + { "sendTo", "(IZLjava/nio/ByteBuffer;II[BII)I", (void *) netty_unix_socket_sendTo }, + { "sendToAddress", "(IZJII[BII)I", (void *) netty_unix_socket_sendToAddress }, + { "sendToAddresses", "(IZJI[BII)I", (void *) netty_unix_socket_sendToAddresses }, // "recvFrom" has a dynamic signature // "recvFromAddress" has a dynamic signature { "recvFd", "(I)I", (void *) netty_unix_socket_recvFd }, @@ -947,7 +997,7 @@ static const JNINativeMethod fixed_method_table[] = { { "setSendBufferSize", "(II)V", (void *) netty_unix_socket_setSendBufferSize }, { "setKeepAlive", "(II)V", (void *) netty_unix_socket_setKeepAlive }, { "setSoLinger", "(II)V", (void *) netty_unix_socket_setSoLinger }, - { "setTrafficClass", "(II)V", (void *) netty_unix_socket_setTrafficClass }, + { "setTrafficClass", "(IZI)V", (void *) netty_unix_socket_setTrafficClass }, { "isKeepAlive", "(I)I", (void *) netty_unix_socket_isKeepAlive }, { "isTcpNoDelay", "(I)I", (void *) netty_unix_socket_isTcpNoDelay }, { "isBroadcast", "(I)I", (void *) netty_unix_socket_isBroadcast }, @@ -956,9 +1006,11 @@ static const JNINativeMethod fixed_method_table[] = { { "getReceiveBufferSize", "(I)I", (void *) netty_unix_socket_getReceiveBufferSize }, { "getSendBufferSize", "(I)I", (void *) netty_unix_socket_getSendBufferSize }, { "getSoLinger", "(I)I", (void *) netty_unix_socket_getSoLinger }, - { "getTrafficClass", "(I)I", (void *) netty_unix_socket_getTrafficClass }, + { "getTrafficClass", "(IZ)I", (void *) netty_unix_socket_getTrafficClass }, { "getSoError", "(I)I", (void *) netty_unix_socket_getSoError }, - { "initialize", "(Z)V", (void *) netty_unix_socket_initialize } + { "initialize", "(Z)V", (void *) netty_unix_socket_initialize }, + { "isIPv6Preferred", "()Z", (void *) netty_unix_socket_isIPv6Preferred }, + { "isIPv6", "(I)Z", (void *) netty_unix_socket_isIPv6 } }; static const jint fixed_method_table_size = sizeof(fixed_method_table) / sizeof(fixed_method_table[0]); @@ -967,128 +1019,87 @@ static jint dynamicMethodsTableSize() { } static JNINativeMethod* createDynamicMethodsTable(const char* packagePrefix) { - JNINativeMethod* dynamicMethods = malloc(sizeof(JNINativeMethod) * dynamicMethodsTableSize()); + char* dynamicTypeName = NULL; + size_t size = sizeof(JNINativeMethod) * dynamicMethodsTableSize(); + JNINativeMethod* dynamicMethods = malloc(size); + if (dynamicMethods == NULL) { + return NULL; + } + memset(dynamicMethods, 0, size); memcpy(dynamicMethods, fixed_method_table, sizeof(fixed_method_table)); - char* dynamicTypeName = netty_unix_util_prepend(packagePrefix, "io/netty/channel/unix/DatagramSocketAddress;"); + JNINativeMethod* dynamicMethod = &dynamicMethods[fixed_method_table_size]; + NETTY_PREPEND(packagePrefix, "io/netty/channel/unix/DatagramSocketAddress;", dynamicTypeName, error); + NETTY_PREPEND("(ILjava/nio/ByteBuffer;II)L", dynamicTypeName, dynamicMethod->signature, error); dynamicMethod->name = "recvFrom"; - dynamicMethod->signature = netty_unix_util_prepend("(ILjava/nio/ByteBuffer;II)L", dynamicTypeName); dynamicMethod->fnPtr = (void *) netty_unix_socket_recvFrom; - free(dynamicTypeName); + netty_unix_util_free_dynamic_name(&dynamicTypeName); ++dynamicMethod; - dynamicTypeName = netty_unix_util_prepend(packagePrefix, "io/netty/channel/unix/DatagramSocketAddress;"); + NETTY_PREPEND(packagePrefix, "io/netty/channel/unix/DatagramSocketAddress;", dynamicTypeName, error); + NETTY_PREPEND("(IJII)L", dynamicTypeName, dynamicMethod->signature, error); dynamicMethod->name = "recvFromAddress"; - dynamicMethod->signature = netty_unix_util_prepend("(IJII)L", dynamicTypeName); dynamicMethod->fnPtr = (void *) netty_unix_socket_recvFromAddress; - free(dynamicTypeName); + netty_unix_util_free_dynamic_name(&dynamicTypeName); return dynamicMethods; +error: + free(dynamicTypeName); + netty_unix_util_free_dynamic_methods_table(dynamicMethods, fixed_method_table_size, dynamicMethodsTableSize()); + return NULL; } -static void freeDynamicMethodsTable(JNINativeMethod* dynamicMethods) { - jint fullMethodTableSize = dynamicMethodsTableSize(); - jint i = fixed_method_table_size; - for (; i < fullMethodTableSize; ++i) { - free(dynamicMethods[i].signature); - } - free(dynamicMethods); -} // JNI Method Registration Table End jint netty_unix_socket_JNI_OnLoad(JNIEnv* env, const char* packagePrefix) { + int ret = JNI_ERR; + char* nettyClassName = NULL; + void* mem = NULL; JNINativeMethod* dynamicMethods = createDynamicMethodsTable(packagePrefix); + if (dynamicMethods == NULL) { + goto done; + } if (netty_unix_util_register_natives(env, packagePrefix, "io/netty/channel/unix/Socket", dynamicMethods, dynamicMethodsTableSize()) != 0) { - freeDynamicMethodsTable(dynamicMethods); - return JNI_ERR; - } - freeDynamicMethodsTable(dynamicMethods); - dynamicMethods = NULL; - char* nettyClassName = netty_unix_util_prepend(packagePrefix, "io/netty/channel/unix/DatagramSocketAddress"); - jclass localDatagramSocketAddressClass = (*env)->FindClass(env, nettyClassName); - if (localDatagramSocketAddressClass == NULL) { - free(nettyClassName); - nettyClassName = NULL; - // pending exception... - return JNI_ERR; - } - datagramSocketAddressClass = (jclass) (*env)->NewGlobalRef(env, localDatagramSocketAddressClass); - if (datagramSocketAddressClass == NULL) { - free(nettyClassName); - nettyClassName = NULL; - // out-of-memory! - netty_unix_errors_throwOutOfMemoryError(env); - return JNI_ERR; + goto done; } + + NETTY_PREPEND(packagePrefix, "io/netty/channel/unix/DatagramSocketAddress", nettyClassName, done); + NETTY_LOAD_CLASS(env, datagramSocketAddressClass, nettyClassName, done); // Respect shading... char parameters[1024] = {0}; - snprintf(parameters, sizeof(parameters), "(Ljava/lang/String;IIL%s;)V", nettyClassName); + snprintf(parameters, sizeof(parameters), "([BIIIL%s;)V", nettyClassName); + netty_unix_util_free_dynamic_name(&nettyClassName); + NETTY_GET_METHOD(env, datagramSocketAddressClass, datagramSocketAddrMethodId, "<init>", parameters, done); - datagramSocketAddrMethodId = (*env)->GetMethodID(env, datagramSocketAddressClass, "<init>", parameters); - if (datagramSocketAddrMethodId == NULL) { - char msg[1024] = {0}; - snprintf(msg, sizeof(msg), "failed to get method ID: %s.<init>(String, int, int, %s)", nettyClassName, nettyClassName); - free(nettyClassName); - nettyClassName = NULL; - netty_unix_errors_throwRuntimeException(env, msg); - return JNI_ERR; - } - - free(nettyClassName); - nettyClassName = NULL; + NETTY_LOAD_CLASS(env, inetSocketAddressClass, "java/net/InetSocketAddress", done); + NETTY_GET_METHOD(env, inetSocketAddressClass, inetSocketAddrMethodId, "<init>", "(Ljava/lang/String;I)V", done); - jclass localInetSocketAddressClass = (*env)->FindClass(env, "java/net/InetSocketAddress"); - if (localInetSocketAddressClass == NULL) { - // pending exception... - return JNI_ERR; - } - inetSocketAddressClass = (jclass) (*env)->NewGlobalRef(env, localInetSocketAddressClass); - if (inetSocketAddressClass == NULL) { - // out-of-memory! - netty_unix_errors_throwOutOfMemoryError(env); - return JNI_ERR; - } - inetSocketAddrMethodId = (*env)->GetMethodID(env, inetSocketAddressClass, "<init>", "(Ljava/lang/String;I)V"); - if (inetSocketAddrMethodId == NULL) { - netty_unix_errors_throwRuntimeException(env, "failed to get method ID: InetSocketAddress.<init>(String, int)"); - return JNI_ERR; + if ((mem = malloc(1)) == NULL) { + goto done; } - void* mem = malloc(1); - if (mem == NULL) { - netty_unix_errors_throwOutOfMemoryError(env); - return JNI_ERR; - } jobject directBuffer = (*env)->NewDirectByteBuffer(env, mem, 1); if (directBuffer == NULL) { - free(mem); - - netty_unix_errors_throwOutOfMemoryError(env); - return JNI_ERR; + goto done; } if ((*env)->GetDirectBufferAddress(env, directBuffer) == NULL) { - free(mem); - - netty_unix_errors_throwRuntimeException(env, "failed to get direct buffer address"); - return JNI_ERR; + goto done; } - free(mem); - return NETTY_JNI_VERSION; + ret = NETTY_JNI_VERSION; +done: + netty_unix_util_free_dynamic_methods_table(dynamicMethods, fixed_method_table_size, dynamicMethodsTableSize()); + free(nettyClassName); + free(mem); + return ret; } void netty_unix_socket_JNI_OnUnLoad(JNIEnv* env) { - if (datagramSocketAddressClass != NULL) { - (*env)->DeleteGlobalRef(env, datagramSocketAddressClass); - datagramSocketAddressClass = NULL; - } - if (inetSocketAddressClass != NULL) { - (*env)->DeleteGlobalRef(env, inetSocketAddressClass); - inetSocketAddressClass = NULL; - } + NETTY_UNLOAD_CLASS(env, datagramSocketAddressClass); + NETTY_UNLOAD_CLASS(env, inetSocketAddressClass); } diff --git a/transport-native-unix-common/src/main/c/netty_unix_socket.h b/transport-native-unix-common/src/main/c/netty_unix_socket.h index 4c60f6d..96d8794 100644 --- a/transport-native-unix-common/src/main/c/netty_unix_socket.h +++ b/transport-native-unix-common/src/main/c/netty_unix_socket.h @@ -20,9 +20,12 @@ #include <jni.h> // External C methods -int netty_unix_socket_initSockaddr(JNIEnv* env, jbyteArray address, jint scopeId, jint jport, const struct sockaddr_storage* addr, socklen_t* addrSize); +int netty_unix_socket_initSockaddr(JNIEnv* env, jboolean ipv6, jbyteArray address, jint scopeId, jint jport, const struct sockaddr_storage* addr, socklen_t* addrSize); +jbyteArray netty_unix_socket_createInetSocketAddressArray(JNIEnv* env, const struct sockaddr_storage* addr); + int netty_unix_socket_getOption(JNIEnv* env, jint fd, int level, int optname, void* optval, socklen_t optlen); int netty_unix_socket_setOption(JNIEnv* env, jint fd, int level, int optname, const void* optval, socklen_t len); +int netty_unix_socket_ipAddressLength(const struct sockaddr_storage* addr); // These method is sometimes needed if you want to special handle some errno value before throwing an exception. int netty_unix_socket_getOption0(jint fd, int level, int optname, void* optval, socklen_t optlen); diff --git a/transport-native-unix-common/src/main/c/netty_unix_util.c b/transport-native-unix-common/src/main/c/netty_unix_util.c index f21d4c6..3d7bda8 100644 --- a/transport-native-unix-common/src/main/c/netty_unix_util.c +++ b/transport-native-unix-common/src/main/c/netty_unix_util.c @@ -19,21 +19,27 @@ #include <errno.h> #include "netty_unix_util.h" +static const uint64_t NETTY_BILLION = 1000000000L; + #ifdef NETTY_USE_MACH_INSTEAD_OF_CLOCK #include <mach/mach.h> #include <mach/mach_time.h> -static const uint64_t NETTY_BILLION = 1000000000L; #endif /* NETTY_USE_MACH_INSTEAD_OF_CLOCK */ char* netty_unix_util_prepend(const char* prefix, const char* str) { + char* result = NULL; if (prefix == NULL) { - char* result = (char*) malloc(sizeof(char) * (strlen(str) + 1)); + if ((result = (char*) malloc(sizeof(char) * (strlen(str) + 1))) == NULL) { + return NULL; + } strcpy(result, str); return result; } - char* result = (char*) malloc(sizeof(char) * (strlen(prefix) + strlen(str) + 1)); + if ((result = (char*) malloc(sizeof(char) * (strlen(prefix) + strlen(str) + 1))) == NULL) { + return NULL; + } strcpy(result, prefix); strcat(result, str); return result; @@ -81,7 +87,10 @@ char* netty_unix_util_parse_package_prefix(const char* libraryPathName, const ch // packagePrefix length is > 0 // Make a copy so we can modify the value without impacting libraryPathName. size_t packagePrefixLen = packageNameEnd - packagePrefix; - packagePrefix = strndup(packagePrefix, packagePrefixLen); + if ((packagePrefix = strndup(packagePrefix, packagePrefixLen)) == NULL) { + *status = JNI_ERR; + return NULL; + } // Make sure the packagePrefix is in the correct format for the JNI functions it will be used with. char* temp = packagePrefix; packageNameEnd = packagePrefix + packagePrefixLen; @@ -94,13 +103,34 @@ char* netty_unix_util_parse_package_prefix(const char* libraryPathName, const ch // Make sure packagePrefix is terminated with the '/' JNI package separator. if(*(--temp) != '/') { temp = packagePrefix; - packagePrefix = netty_unix_util_prepend(packagePrefix, "/"); + if ((packagePrefix = netty_unix_util_prepend(packagePrefix, "/")) == NULL) { + *status = JNI_ERR; + } free(temp); } return packagePrefix; } // util methods +uint64_t netty_unix_util_timespec_elapsed_ns(const struct timespec* begin, const struct timespec* end) { + return NETTY_BILLION * (end->tv_sec - begin->tv_sec) + (end->tv_nsec - begin->tv_nsec); +} + +jboolean netty_unix_util_timespec_subtract_ns(struct timespec* ts, uint64_t nanos) { + const uint64_t seconds = nanos / NETTY_BILLION; + nanos -= seconds * NETTY_BILLION; + // If there are too many nanos we steal from seconds to avoid underflow on nanos. This way we + // only have to worry about underflow on tv_sec. + if (nanos > ts->tv_nsec) { + --(ts->tv_sec); + ts->tv_nsec += NETTY_BILLION; + } + const jboolean underflow = ts->tv_sec < seconds; + ts->tv_sec -= seconds; + ts->tv_nsec -= nanos; + return underflow; +} + int netty_unix_util_clock_gettime(clockid_t clockId, struct timespec* tp) { #ifdef NETTY_USE_MACH_INSTEAD_OF_CLOCK uint64_t timeNs; @@ -165,13 +195,34 @@ jboolean netty_unix_util_initialize_wait_clock(clockid_t* clockId) { } jint netty_unix_util_register_natives(JNIEnv* env, const char* packagePrefix, const char* className, const JNINativeMethod* methods, jint numMethods) { - char* nettyClassName = netty_unix_util_prepend(packagePrefix, className); + char* nettyClassName = NULL; + int ret = JNI_ERR; + NETTY_PREPEND(packagePrefix, className, nettyClassName, done); + jclass nativeCls = (*env)->FindClass(env, nettyClassName); - free(nettyClassName); - nettyClassName = NULL; if (nativeCls == NULL) { - return JNI_ERR; + goto done; + } + + ret = (*env)->RegisterNatives(env, nativeCls, methods, numMethods); +done: + free(nettyClassName); + return ret; +} + +void netty_unix_util_free_dynamic_methods_table(JNINativeMethod* dynamicMethods, jint fixedMethodTableSize, jint fullMethodTableSize) { + if (dynamicMethods != NULL) { + jint i = fixedMethodTableSize; + for (; i < fullMethodTableSize; ++i) { + free(dynamicMethods[i].signature); + } + free(dynamicMethods); } +} - return (*env)->RegisterNatives(env, nativeCls, methods, numMethods); +void netty_unix_util_free_dynamic_name(char** dynamicName) { + if (dynamicName != NULL && *dynamicName != NULL) { + free(*dynamicName); + *dynamicName = NULL; + } } diff --git a/transport-native-unix-common/src/main/c/netty_unix_util.h b/transport-native-unix-common/src/main/c/netty_unix_util.h index 9f75e32..5aa9e7f 100644 --- a/transport-native-unix-common/src/main/c/netty_unix_util.h +++ b/transport-native-unix-common/src/main/c/netty_unix_util.h @@ -18,6 +18,7 @@ #define NETTY_UNIX_UTIL_H_ #include <jni.h> +#include <stdint.h> #include <time.h> #if defined(__MACH__) && !defined(CLOCK_REALTIME) @@ -35,6 +36,72 @@ typedef int clockid_t; #endif /* __MACH__ */ + +#define NETTY_BEGIN_MACRO if (1) { +#define NETTY_END_MACRO } else (void)(0) + +#define NETTY_FIND_CLASS(E, C, N, R) \ + NETTY_BEGIN_MACRO \ + C = (*(E))->FindClass((E), N); \ + if (C == NULL) { \ + (*(E))->ExceptionClear((E)); \ + goto R; \ + } \ + NETTY_END_MACRO + +#define NETTY_LOAD_CLASS(E, C, N, R) \ + NETTY_BEGIN_MACRO \ + jclass _##C = (*(E))->FindClass((E), N); \ + if (_##C == NULL) { \ + (*(E))->ExceptionClear((E)); \ + goto R; \ + } \ + C = (*(E))->NewGlobalRef((E), _##C); \ + (*(E))->DeleteLocalRef((E), _##C); \ + if (C == NULL) { \ + goto R; \ + } \ + NETTY_END_MACRO + +#define NETTY_UNLOAD_CLASS(E, C) \ + NETTY_BEGIN_MACRO \ + if (C != NULL) { \ + (*(E))->DeleteGlobalRef((E), (C)); \ + C = NULL; \ + } \ + NETTY_END_MACRO + + +#define NETTY_GET_METHOD(E, C, M, N, S, R) \ + NETTY_BEGIN_MACRO \ + M = (*(E))->GetMethodID((E), C, N, S); \ + if (M == NULL) { \ + goto R; \ + } \ + NETTY_END_MACRO + +#define NETTY_GET_FIELD(E, C, F, N, S, R) \ + NETTY_BEGIN_MACRO \ + F = (*(E))->GetFieldID((E), C, N, S); \ + if (F == NULL) { \ + goto R; \ + } \ + NETTY_END_MACRO + +#define NETTY_TRY_GET_FIELD(E, C, F, N, S) \ + NETTY_BEGIN_MACRO \ + F = (*(E))->GetFieldID((E), C, N, S); \ + if (F == NULL) { \ + (*(E))->ExceptionClear((E)); \ + } \ + NETTY_END_MACRO + +#define NETTY_PREPEND(P, S, N, R) \ + NETTY_BEGIN_MACRO \ + if ((N = netty_unix_util_prepend(P, S)) == NULL) { \ + goto R; \ + } \ + NETTY_END_MACRO /** * Return a new string (caller must free this string) which is equivalent to <pre>prefix + str</pre>. * @@ -64,9 +131,26 @@ jboolean netty_unix_util_initialize_wait_clock(clockid_t* clockId); */ int netty_unix_util_clock_gettime(clockid_t clockId, struct timespec* tp); +/** + * Calculate the number of nano seconds elapsed between begin and end. + * + * Returns the number of nano seconds. + */ +uint64_t netty_unix_util_timespec_elapsed_ns(const struct timespec* begin, const struct timespec* end); + +/** + * Subtract <pre>nanos</pre> nano seconds from a <pre>timespec</pre>. + * + * Returns true if there is underflow. + */ +jboolean netty_unix_util_timespec_subtract_ns(struct timespec* ts, uint64_t nanos); + /** * Return type is as defined in http://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/functions.html#wp5833. */ jint netty_unix_util_register_natives(JNIEnv* env, const char* packagePrefix, const char* className, const JNINativeMethod* methods, jint numMethods); +void netty_unix_util_free_dynamic_methods_table(JNINativeMethod* dynamicMethods, jint fixedMethodTableSize, jint fullMethodTableSize); +void netty_unix_util_free_dynamic_name(char** dynamicName); + #endif /* NETTY_UNIX_UTIL_H_ */ diff --git a/transport-native-unix-common/src/main/java/io/netty/channel/unix/DatagramSocketAddress.java b/transport-native-unix-common/src/main/java/io/netty/channel/unix/DatagramSocketAddress.java index 874d705..131557c 100644 --- a/transport-native-unix-common/src/main/java/io/netty/channel/unix/DatagramSocketAddress.java +++ b/transport-native-unix-common/src/main/java/io/netty/channel/unix/DatagramSocketAddress.java @@ -15,7 +15,10 @@ */ package io.netty.channel.unix; +import java.net.Inet6Address; +import java.net.InetAddress; import java.net.InetSocketAddress; +import java.net.UnknownHostException; /** * Act as special {@link InetSocketAddress} to be able to easily pass all needed data from JNI without the need @@ -30,8 +33,9 @@ public final class DatagramSocketAddress extends InetSocketAddress { private final int receivedAmount; private final DatagramSocketAddress localAddress; - DatagramSocketAddress(String addr, int port, int receivedAmount, DatagramSocketAddress local) { - super(addr, port); + DatagramSocketAddress(byte[] addr, int scopeId, int port, int receivedAmount, DatagramSocketAddress local) + throws UnknownHostException { + super(newAddress(addr, scopeId), port); this.receivedAmount = receivedAmount; localAddress = local; } @@ -43,4 +47,11 @@ public final class DatagramSocketAddress extends InetSocketAddress { public int receivedAmount() { return receivedAmount; } + + private static InetAddress newAddress(byte[] bytes, int scopeId) throws UnknownHostException { + if (bytes.length == 4) { + return InetAddress.getByAddress(bytes); + } + return Inet6Address.getByAddress(null, bytes, scopeId); + } } diff --git a/transport-native-unix-common/src/main/java/io/netty/channel/unix/DomainSocketAddress.java b/transport-native-unix-common/src/main/java/io/netty/channel/unix/DomainSocketAddress.java index 496c6a3..b58306c 100644 --- a/transport-native-unix-common/src/main/java/io/netty/channel/unix/DomainSocketAddress.java +++ b/transport-native-unix-common/src/main/java/io/netty/channel/unix/DomainSocketAddress.java @@ -15,6 +15,8 @@ */ package io.netty.channel.unix; +import io.netty.util.internal.ObjectUtil; + import java.io.File; import java.net.SocketAddress; @@ -27,10 +29,7 @@ public final class DomainSocketAddress extends SocketAddress { private final String socketPath; public DomainSocketAddress(String socketPath) { - if (socketPath == null) { - throw new NullPointerException("socketPath"); - } - this.socketPath = socketPath; + this.socketPath = ObjectUtil.checkNotNull(socketPath, "socketPath"); } public DomainSocketAddress(File file) { diff --git a/transport-native-unix-common/src/main/java/io/netty/channel/unix/Errors.java b/transport-native-unix-common/src/main/java/io/netty/channel/unix/Errors.java index 53c5ff2..e75b580 100644 --- a/transport-native-unix-common/src/main/java/io/netty/channel/unix/Errors.java +++ b/transport-native-unix-common/src/main/java/io/netty/channel/unix/Errors.java @@ -62,14 +62,29 @@ public final class Errors { public static final class NativeIoException extends IOException { private static final long serialVersionUID = 8222160204268655526L; private final int expectedErr; + private final boolean fillInStackTrace; + public NativeIoException(String method, int expectedErr) { + this(method, expectedErr, true); + } + + public NativeIoException(String method, int expectedErr, boolean fillInStackTrace) { super(method + "(..) failed: " + ERRORS[-expectedErr]); this.expectedErr = expectedErr; + this.fillInStackTrace = fillInStackTrace; } public int expectedErr() { return expectedErr; } + + @Override + public synchronized Throwable fillInStackTrace() { + if (fillInStackTrace) { + return super.fillInStackTrace(); + } + return this; + } } static final class NativeConnectException extends ConnectException { @@ -92,11 +107,8 @@ public final class Errors { } } - static void throwConnectException(String method, NativeConnectException refusedCause, int err) + static void throwConnectException(String method, int err) throws IOException { - if (err == refusedCause.expectedErr()) { - throw refusedCause; - } if (err == ERROR_EALREADY_NEGATIVE) { throw new ConnectionPendingException(); } @@ -113,7 +125,7 @@ public final class Errors { } public static NativeIoException newConnectionResetException(String method, int errnoNegative) { - NativeIoException exception = newIOException(method, errnoNegative); + NativeIoException exception = new NativeIoException(method, errnoNegative, false); exception.setStackTrace(EmptyArrays.EMPTY_STACK_TRACE); return exception; } @@ -122,6 +134,7 @@ public final class Errors { return new NativeIoException(method, err); } + @Deprecated public static int ioResult(String method, int err, NativeIoException resetCause, ClosedChannelException closedCause) throws IOException { // network stack saturated... try again later @@ -146,5 +159,23 @@ public final class Errors { throw newIOException(method, err); } + public static int ioResult(String method, int err) throws IOException { + // network stack saturated... try again later + if (err == ERRNO_EAGAIN_NEGATIVE || err == ERRNO_EWOULDBLOCK_NEGATIVE) { + return 0; + } + if (err == ERRNO_EBADF_NEGATIVE) { + throw new ClosedChannelException(); + } + if (err == ERRNO_ENOTCONN_NEGATIVE) { + throw new NotYetConnectedException(); + } + if (err == ERRNO_ENOENT_NEGATIVE) { + throw new FileNotFoundException(); + } + + throw new NativeIoException(method, err, false); + } + private Errors() { } } diff --git a/transport-native-unix-common/src/main/java/io/netty/channel/unix/FileDescriptor.java b/transport-native-unix-common/src/main/java/io/netty/channel/unix/FileDescriptor.java index cf26a42..0b0a55e 100644 --- a/transport-native-unix-common/src/main/java/io/netty/channel/unix/FileDescriptor.java +++ b/transport-native-unix-common/src/main/java/io/netty/channel/unix/FileDescriptor.java @@ -15,18 +15,17 @@ */ package io.netty.channel.unix; -import io.netty.util.internal.ThrowableUtil; import java.io.File; import java.io.IOException; import java.nio.ByteBuffer; -import java.nio.channels.ClosedChannelException; import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; import static io.netty.channel.unix.Errors.ioResult; import static io.netty.channel.unix.Errors.newIOException; import static io.netty.channel.unix.Limits.IOV_MAX; import static io.netty.util.internal.ObjectUtil.checkNotNull; +import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero; import static java.lang.Math.min; /** @@ -34,36 +33,6 @@ import static java.lang.Math.min; * {@link FileDescriptor} for it. */ public class FileDescriptor { - private static final ClosedChannelException WRITE_CLOSED_CHANNEL_EXCEPTION = ThrowableUtil.unknownStackTrace( - new ClosedChannelException(), FileDescriptor.class, "write(..)"); - private static final ClosedChannelException WRITE_ADDRESS_CLOSED_CHANNEL_EXCEPTION = - ThrowableUtil.unknownStackTrace(new ClosedChannelException(), FileDescriptor.class, "writeAddress(..)"); - private static final ClosedChannelException WRITEV_CLOSED_CHANNEL_EXCEPTION = ThrowableUtil.unknownStackTrace( - new ClosedChannelException(), FileDescriptor.class, "writev(..)"); - private static final ClosedChannelException WRITEV_ADDRESSES_CLOSED_CHANNEL_EXCEPTION = - ThrowableUtil.unknownStackTrace(new ClosedChannelException(), FileDescriptor.class, "writevAddresses(..)"); - private static final ClosedChannelException READ_CLOSED_CHANNEL_EXCEPTION = ThrowableUtil.unknownStackTrace( - new ClosedChannelException(), FileDescriptor.class, "read(..)"); - private static final ClosedChannelException READ_ADDRESS_CLOSED_CHANNEL_EXCEPTION = ThrowableUtil.unknownStackTrace( - new ClosedChannelException(), FileDescriptor.class, "readAddress(..)"); - private static final Errors.NativeIoException WRITE_CONNECTION_RESET_EXCEPTION = ThrowableUtil.unknownStackTrace( - Errors.newConnectionResetException("syscall:write", Errors.ERRNO_EPIPE_NEGATIVE), - FileDescriptor.class, "write(..)"); - private static final Errors.NativeIoException WRITE_ADDRESS_CONNECTION_RESET_EXCEPTION = - ThrowableUtil.unknownStackTrace(Errors.newConnectionResetException("syscall:write", - Errors.ERRNO_EPIPE_NEGATIVE), FileDescriptor.class, "writeAddress(..)"); - private static final Errors.NativeIoException WRITEV_CONNECTION_RESET_EXCEPTION = ThrowableUtil.unknownStackTrace( - Errors.newConnectionResetException("syscall:writev", Errors.ERRNO_EPIPE_NEGATIVE), - FileDescriptor.class, "writev(..)"); - private static final Errors.NativeIoException WRITEV_ADDRESSES_CONNECTION_RESET_EXCEPTION = - ThrowableUtil.unknownStackTrace(Errors.newConnectionResetException("syscall:writev", - Errors.ERRNO_EPIPE_NEGATIVE), FileDescriptor.class, "writeAddresses(..)"); - private static final Errors.NativeIoException READ_CONNECTION_RESET_EXCEPTION = ThrowableUtil.unknownStackTrace( - Errors.newConnectionResetException("syscall:read", Errors.ERRNO_ECONNRESET_NEGATIVE), - FileDescriptor.class, "read(..)"); - private static final Errors.NativeIoException READ_ADDRESS_CONNECTION_RESET_EXCEPTION = - ThrowableUtil.unknownStackTrace(Errors.newConnectionResetException("syscall:read", - Errors.ERRNO_ECONNRESET_NEGATIVE), FileDescriptor.class, "readAddress(..)"); private static final AtomicIntegerFieldUpdater<FileDescriptor> stateUpdater = AtomicIntegerFieldUpdater.newUpdater(FileDescriptor.class, "state"); @@ -82,9 +51,7 @@ public class FileDescriptor { final int fd; public FileDescriptor(int fd) { - if (fd < 0) { - throw new IllegalArgumentException("fd must be >= 0"); - } + checkPositiveOrZero(fd, "fd"); this.fd = fd; } @@ -127,7 +94,7 @@ public class FileDescriptor { if (res >= 0) { return res; } - return ioResult("write", res, WRITE_CONNECTION_RESET_EXCEPTION, WRITE_CLOSED_CHANNEL_EXCEPTION); + return ioResult("write", res); } public final int writeAddress(long address, int pos, int limit) throws IOException { @@ -135,8 +102,7 @@ public class FileDescriptor { if (res >= 0) { return res; } - return ioResult("writeAddress", res, - WRITE_ADDRESS_CONNECTION_RESET_EXCEPTION, WRITE_ADDRESS_CLOSED_CHANNEL_EXCEPTION); + return ioResult("writeAddress", res); } public final long writev(ByteBuffer[] buffers, int offset, int length, long maxBytesToWrite) throws IOException { @@ -144,7 +110,7 @@ public class FileDescriptor { if (res >= 0) { return res; } - return ioResult("writev", (int) res, WRITEV_CONNECTION_RESET_EXCEPTION, WRITEV_CLOSED_CHANNEL_EXCEPTION); + return ioResult("writev", (int) res); } public final long writevAddresses(long memoryAddress, int length) throws IOException { @@ -152,8 +118,7 @@ public class FileDescriptor { if (res >= 0) { return res; } - return ioResult("writevAddresses", (int) res, - WRITEV_ADDRESSES_CONNECTION_RESET_EXCEPTION, WRITEV_ADDRESSES_CLOSED_CHANNEL_EXCEPTION); + return ioResult("writevAddresses", (int) res); } public final int read(ByteBuffer buf, int pos, int limit) throws IOException { @@ -164,7 +129,7 @@ public class FileDescriptor { if (res == 0) { return -1; } - return ioResult("read", res, READ_CONNECTION_RESET_EXCEPTION, READ_CLOSED_CHANNEL_EXCEPTION); + return ioResult("read", res); } public final int readAddress(long address, int pos, int limit) throws IOException { @@ -175,8 +140,7 @@ public class FileDescriptor { if (res == 0) { return -1; } - return ioResult("readAddress", res, - READ_ADDRESS_CONNECTION_RESET_EXCEPTION, READ_ADDRESS_CLOSED_CHANNEL_EXCEPTION); + return ioResult("readAddress", res); } @Override @@ -207,8 +171,7 @@ public class FileDescriptor { * Open a new {@link FileDescriptor} for the given path. */ public static FileDescriptor from(String path) throws IOException { - checkNotNull(path, "path"); - int res = open(path); + int res = open(checkNotNull(path, "path")); if (res < 0) { throw newIOException("open", res); } diff --git a/transport-native-unix-common/src/main/java/io/netty/channel/unix/IovArray.java b/transport-native-unix-common/src/main/java/io/netty/channel/unix/IovArray.java index e8b4067..77ec3a7 100644 --- a/transport-native-unix-common/src/main/java/io/netty/channel/unix/IovArray.java +++ b/transport-native-unix-common/src/main/java/io/netty/channel/unix/IovArray.java @@ -16,7 +16,6 @@ package io.netty.channel.unix; import io.netty.buffer.ByteBuf; -import io.netty.buffer.CompositeByteBuf; import io.netty.channel.ChannelOutboundBuffer.MessageProcessor; import io.netty.util.internal.PlatformDependent; @@ -78,33 +77,33 @@ public final class IovArray implements MessageProcessor { } /** - * Add a {@link ByteBuf} to this {@link IovArray}. - * @param buf The {@link ByteBuf} to add. - * @return {@code true} if the entire {@link ByteBuf} has been added to this {@link IovArray}. Note in the event - * that {@link ByteBuf} is a {@link CompositeByteBuf} {@code false} may be returned even if some of the components - * have been added. + * @deprecated Use {@link #add(ByteBuf, int, int)} */ + @Deprecated public boolean add(ByteBuf buf) { + return add(buf, buf.readerIndex(), buf.readableBytes()); + } + + public boolean add(ByteBuf buf, int offset, int len) { if (count == IOV_MAX) { // No more room! return false; } else if (buf.nioBufferCount() == 1) { - final int len = buf.readableBytes(); if (len == 0) { return true; } if (buf.hasMemoryAddress()) { - return add(buf.memoryAddress(), buf.readerIndex(), len); + return add(buf.memoryAddress() + offset, len); } else { - ByteBuffer nioBuffer = buf.internalNioBuffer(buf.readerIndex(), len); - return add(Buffer.memoryAddress(nioBuffer), nioBuffer.position(), len); + ByteBuffer nioBuffer = buf.internalNioBuffer(offset, len); + return add(Buffer.memoryAddress(nioBuffer) + nioBuffer.position(), len); } } else { - ByteBuffer[] buffers = buf.nioBuffers(); + ByteBuffer[] buffers = buf.nioBuffers(offset, len); for (ByteBuffer nioBuffer : buffers) { - final int len = nioBuffer.remaining(); - if (len != 0 && - (!add(Buffer.memoryAddress(nioBuffer), nioBuffer.position(), len) || count == IOV_MAX)) { + final int remaining = nioBuffer.remaining(); + if (remaining != 0 && + (!add(Buffer.memoryAddress(nioBuffer) + nioBuffer.position(), remaining) || count == IOV_MAX)) { return false; } } @@ -112,7 +111,7 @@ public final class IovArray implements MessageProcessor { } } - private boolean add(long addr, int offset, int len) { + private boolean add(long addr, int len) { assert addr != 0; // If there is at least 1 entry then we enforce the maximum bytes. We want to accept at least one entry so we @@ -135,19 +134,19 @@ public final class IovArray implements MessageProcessor { if (ADDRESS_SIZE == 8) { // 64bit if (PlatformDependent.hasUnsafe()) { - PlatformDependent.putLong(baseOffset + memoryAddress, addr + offset); + PlatformDependent.putLong(baseOffset + memoryAddress, addr); PlatformDependent.putLong(lengthOffset + memoryAddress, len); } else { - memory.putLong(baseOffset, addr + offset); + memory.putLong(baseOffset, addr); memory.putLong(lengthOffset, len); } } else { assert ADDRESS_SIZE == 4; if (PlatformDependent.hasUnsafe()) { - PlatformDependent.putInt(baseOffset + memoryAddress, (int) addr + offset); + PlatformDependent.putInt(baseOffset + memoryAddress, (int) addr); PlatformDependent.putInt(lengthOffset + memoryAddress, len); } else { - memory.putInt(baseOffset, (int) addr + offset); + memory.putInt(baseOffset, (int) addr); memory.putInt(lengthOffset, len); } } @@ -169,22 +168,22 @@ public final class IovArray implements MessageProcessor { } /** - * Set the maximum amount of bytes that can be added to this {@link IovArray} via {@link #add(ByteBuf)}. + * Set the maximum amount of bytes that can be added to this {@link IovArray} via {@link #add(ByteBuf, int, int)} * <p> * This will not impact the existing state of the {@link IovArray}, and only applies to subsequent calls to * {@link #add(ByteBuf)}. * <p> * In order to ensure some progress is made at least one {@link ByteBuf} will be accepted even if it's size exceeds * this value. - * @param maxBytes the maximum amount of bytes that can be added to this {@link IovArray} via {@link #add(ByteBuf)}. + * @param maxBytes the maximum amount of bytes that can be added to this {@link IovArray}. */ public void maxBytes(long maxBytes) { this.maxBytes = min(SSIZE_MAX, checkPositive(maxBytes, "maxBytes")); } /** - * Get the maximum amount of bytes that can be added to this {@link IovArray} via {@link #add(ByteBuf)}. - * @return the maximum amount of bytes that can be added to this {@link IovArray} via {@link #add(ByteBuf)}. + * Get the maximum amount of bytes that can be added to this {@link IovArray}. + * @return the maximum amount of bytes that can be added to this {@link IovArray}. */ public long maxBytes() { return maxBytes; @@ -206,7 +205,11 @@ public final class IovArray implements MessageProcessor { @Override public boolean processMessage(Object msg) throws Exception { - return msg instanceof ByteBuf && add((ByteBuf) msg); + if (msg instanceof ByteBuf) { + ByteBuf buffer = (ByteBuf) msg; + return add(buffer, buffer.readerIndex(), buffer.readableBytes()); + } + return false; } private static int idx(int index) { diff --git a/transport-native-unix-common/src/main/java/io/netty/channel/unix/NativeInetAddress.java b/transport-native-unix-common/src/main/java/io/netty/channel/unix/NativeInetAddress.java index 599f823..3e76ab9 100644 --- a/transport-native-unix-common/src/main/java/io/netty/channel/unix/NativeInetAddress.java +++ b/transport-native-unix-common/src/main/java/io/netty/channel/unix/NativeInetAddress.java @@ -58,11 +58,15 @@ public final class NativeInetAddress { public static byte[] ipv4MappedIpv6Address(byte[] ipv4) { byte[] address = new byte[16]; - System.arraycopy(IPV4_MAPPED_IPV6_PREFIX, 0, address, 0, IPV4_MAPPED_IPV6_PREFIX.length); - System.arraycopy(ipv4, 0, address, 12, ipv4.length); + copyIpv4MappedIpv6Address(ipv4, address); return address; } + public static void copyIpv4MappedIpv6Address(byte[] ipv4, byte[] ipv6) { + System.arraycopy(IPV4_MAPPED_IPV6_PREFIX, 0, ipv6, 0, IPV4_MAPPED_IPV6_PREFIX.length); + System.arraycopy(ipv4, 0, ipv6, 12, ipv4.length); + } + public static InetSocketAddress address(byte[] addr, int offset, int len) { // The last 4 bytes are always the port final int port = decodeInt(addr, offset + len - 4); diff --git a/transport-native-unix-common/src/main/java/io/netty/channel/unix/Socket.java b/transport-native-unix-common/src/main/java/io/netty/channel/unix/Socket.java index 01df6a1..10a68d4 100644 --- a/transport-native-unix-common/src/main/java/io/netty/channel/unix/Socket.java +++ b/transport-native-unix-common/src/main/java/io/netty/channel/unix/Socket.java @@ -39,44 +39,20 @@ import static io.netty.channel.unix.Errors.throwConnectException; import static io.netty.channel.unix.LimitsStaticallyReferencedJniMethods.udsSunPathSize; import static io.netty.channel.unix.NativeInetAddress.address; import static io.netty.channel.unix.NativeInetAddress.ipv4MappedIpv6Address; -import static io.netty.util.internal.ThrowableUtil.unknownStackTrace; /** * Provides a JNI bridge to native socket operations. * <strong>Internal usage only!</strong> */ public class Socket extends FileDescriptor { - private static final ClosedChannelException SHUTDOWN_CLOSED_CHANNEL_EXCEPTION = unknownStackTrace( - new ClosedChannelException(), Socket.class, "shutdown(..)"); - private static final ClosedChannelException SEND_TO_CLOSED_CHANNEL_EXCEPTION = unknownStackTrace( - new ClosedChannelException(), Socket.class, "sendTo(..)"); - private static final ClosedChannelException SEND_TO_ADDRESS_CLOSED_CHANNEL_EXCEPTION = - unknownStackTrace(new ClosedChannelException(), Socket.class, "sendToAddress(..)"); - private static final ClosedChannelException SEND_TO_ADDRESSES_CLOSED_CHANNEL_EXCEPTION = - unknownStackTrace(new ClosedChannelException(), Socket.class, "sendToAddresses(..)"); - private static final Errors.NativeIoException SEND_TO_CONNECTION_RESET_EXCEPTION = unknownStackTrace( - Errors.newConnectionResetException("syscall:sendto", Errors.ERRNO_EPIPE_NEGATIVE), - Socket.class, "sendTo(..)"); - private static final Errors.NativeIoException SEND_TO_ADDRESS_CONNECTION_RESET_EXCEPTION = - unknownStackTrace(Errors.newConnectionResetException("syscall:sendto", - Errors.ERRNO_EPIPE_NEGATIVE), Socket.class, "sendToAddress"); - private static final Errors.NativeIoException CONNECTION_RESET_EXCEPTION_SENDMSG = unknownStackTrace( - Errors.newConnectionResetException("syscall:sendmsg", - Errors.ERRNO_EPIPE_NEGATIVE), Socket.class, "sendToAddresses(..)"); - private static final Errors.NativeIoException CONNECTION_RESET_SHUTDOWN_EXCEPTION = - unknownStackTrace(Errors.newConnectionResetException("syscall:shutdown", - Errors.ERRNO_ECONNRESET_NEGATIVE), Socket.class, "shutdown"); - private static final Errors.NativeConnectException FINISH_CONNECT_REFUSED_EXCEPTION = - unknownStackTrace(new Errors.NativeConnectException("syscall:getsockopt", - Errors.ERROR_ECONNREFUSED_NEGATIVE), Socket.class, "finishConnect(..)"); - private static final Errors.NativeConnectException CONNECT_REFUSED_EXCEPTION = - unknownStackTrace(new Errors.NativeConnectException("syscall:connect", - Errors.ERROR_ECONNREFUSED_NEGATIVE), Socket.class, "connect(..)"); public static final int UDS_SUN_PATH_SIZE = udsSunPathSize(); + protected final boolean ipv6; + public Socket(int fd) { super(fd); + this.ipv6 = isIPv6(fd); } public final void shutdown() throws IOException { @@ -111,7 +87,7 @@ public class Socket extends FileDescriptor { } int res = shutdown(fd, read, write); if (res < 0) { - ioResult("shutdown", res, CONNECTION_RESET_SHUTDOWN_EXCEPTION, SHUTDOWN_CLOSED_CHANNEL_EXCEPTION); + ioResult("shutdown", res); } } @@ -141,14 +117,14 @@ public class Socket extends FileDescriptor { scopeId = 0; address = ipv4MappedIpv6Address(addr.getAddress()); } - int res = sendTo(fd, buf, pos, limit, address, scopeId, port); + int res = sendTo(fd, ipv6, buf, pos, limit, address, scopeId, port); if (res >= 0) { return res; } if (res == ERROR_ECONNREFUSED_NEGATIVE) { throw new PortUnreachableException("sendTo failed"); } - return ioResult("sendTo", res, SEND_TO_CONNECTION_RESET_EXCEPTION, SEND_TO_CLOSED_CHANNEL_EXCEPTION); + return ioResult("sendTo", res); } public final int sendToAddress(long memoryAddress, int pos, int limit, InetAddress addr, int port) @@ -165,15 +141,14 @@ public class Socket extends FileDescriptor { scopeId = 0; address = ipv4MappedIpv6Address(addr.getAddress()); } - int res = sendToAddress(fd, memoryAddress, pos, limit, address, scopeId, port); + int res = sendToAddress(fd, ipv6, memoryAddress, pos, limit, address, scopeId, port); if (res >= 0) { return res; } if (res == ERROR_ECONNREFUSED_NEGATIVE) { throw new PortUnreachableException("sendToAddress failed"); } - return ioResult("sendToAddress", res, - SEND_TO_ADDRESS_CONNECTION_RESET_EXCEPTION, SEND_TO_ADDRESS_CLOSED_CHANNEL_EXCEPTION); + return ioResult("sendToAddress", res); } public final int sendToAddresses(long memoryAddress, int length, InetAddress addr, int port) throws IOException { @@ -189,7 +164,7 @@ public class Socket extends FileDescriptor { scopeId = 0; address = ipv4MappedIpv6Address(addr.getAddress()); } - int res = sendToAddresses(fd, memoryAddress, length, address, scopeId, port); + int res = sendToAddresses(fd, ipv6, memoryAddress, length, address, scopeId, port); if (res >= 0) { return res; } @@ -197,8 +172,7 @@ public class Socket extends FileDescriptor { if (res == ERROR_ECONNREFUSED_NEGATIVE) { throw new PortUnreachableException("sendToAddresses failed"); } - return ioResult("sendToAddresses", res, - CONNECTION_RESET_EXCEPTION_SENDMSG, SEND_TO_ADDRESSES_CLOSED_CHANNEL_EXCEPTION); + return ioResult("sendToAddresses", res); } public final DatagramSocketAddress recvFrom(ByteBuffer buf, int pos, int limit) throws IOException { @@ -242,7 +216,7 @@ public class Socket extends FileDescriptor { if (socketAddress instanceof InetSocketAddress) { InetSocketAddress inetSocketAddress = (InetSocketAddress) socketAddress; NativeInetAddress address = NativeInetAddress.newInstance(inetSocketAddress.getAddress()); - res = connect(fd, address.address, address.scopeId, inetSocketAddress.getPort()); + res = connect(fd, ipv6, address.address, address.scopeId, inetSocketAddress.getPort()); } else if (socketAddress instanceof DomainSocketAddress) { DomainSocketAddress unixDomainSocketAddress = (DomainSocketAddress) socketAddress; res = connectDomainSocket(fd, unixDomainSocketAddress.path().getBytes(CharsetUtil.UTF_8)); @@ -254,7 +228,7 @@ public class Socket extends FileDescriptor { // connect not complete yet need to wait for EPOLLOUT event return false; } - throwConnectException("connect", CONNECT_REFUSED_EXCEPTION, res); + throwConnectException("connect", res); } return true; } @@ -266,15 +240,15 @@ public class Socket extends FileDescriptor { // connect still in progress return false; } - throwConnectException("finishConnect", FINISH_CONNECT_REFUSED_EXCEPTION, res); + throwConnectException("finishConnect", res); } return true; } public final void disconnect() throws IOException { - int res = disconnect(fd); + int res = disconnect(fd, ipv6); if (res < 0) { - throwConnectException("disconnect", FINISH_CONNECT_REFUSED_EXCEPTION, res); + throwConnectException("disconnect", res); } } @@ -282,7 +256,7 @@ public class Socket extends FileDescriptor { if (socketAddress instanceof InetSocketAddress) { InetSocketAddress addr = (InetSocketAddress) socketAddress; NativeInetAddress address = NativeInetAddress.newInstance(addr.getAddress()); - int res = bind(fd, address.address, address.scopeId, addr.getPort()); + int res = bind(fd, ipv6, address.address, address.scopeId, addr.getPort()); if (res < 0) { throw newIOException("bind", res); } @@ -367,7 +341,7 @@ public class Socket extends FileDescriptor { } public final int getTrafficClass() throws IOException { - return getTrafficClass(fd); + return getTrafficClass(fd, ipv6); } public final void setKeepAlive(boolean keepAlive) throws IOException { @@ -403,9 +377,13 @@ public class Socket extends FileDescriptor { } public final void setTrafficClass(int trafficClass) throws IOException { - setTrafficClass(fd, trafficClass); + setTrafficClass(fd, ipv6, trafficClass); } + public static native boolean isIPv6Preferred(); + + private static native boolean isIPv6(int fd); + @Override public String toString() { return "Socket{" + @@ -434,7 +412,11 @@ public class Socket extends FileDescriptor { } protected static int newSocketStream0() { - int res = newSocketStreamFd(); + return newSocketStream0(isIPv6Preferred()); + } + + protected static int newSocketStream0(boolean ipv6) { + int res = newSocketStreamFd(ipv6); if (res < 0) { throw new ChannelException(newIOException("newSocketStream", res)); } @@ -442,7 +424,11 @@ public class Socket extends FileDescriptor { } protected static int newSocketDgram0() { - int res = newSocketDgramFd(); + return newSocketDgram0(isIPv6Preferred()); + } + + protected static int newSocketDgram0(boolean ipv6) { + int res = newSocketDgramFd(ipv6); if (res < 0) { throw new ChannelException(newIOException("newSocketDgram", res)); } @@ -458,11 +444,11 @@ public class Socket extends FileDescriptor { } private static native int shutdown(int fd, boolean read, boolean write); - private static native int connect(int fd, byte[] address, int scopeId, int port); + private static native int connect(int fd, boolean ipv6, byte[] address, int scopeId, int port); private static native int connectDomainSocket(int fd, byte[] path); private static native int finishConnect(int fd); - private static native int disconnect(int fd); - private static native int bind(int fd, byte[] address, int scopeId, int port); + private static native int disconnect(int fd, boolean ipv6); + private static native int bind(int fd, boolean ipv6, byte[] address, int scopeId, int port); private static native int bindDomainSocket(int fd, byte[] path); private static native int listen(int fd, int backlog); private static native int accept(int fd, byte[] addr); @@ -471,11 +457,11 @@ public class Socket extends FileDescriptor { private static native byte[] localAddress(int fd); private static native int sendTo( - int fd, ByteBuffer buf, int pos, int limit, byte[] address, int scopeId, int port); + int fd, boolean ipv6, ByteBuffer buf, int pos, int limit, byte[] address, int scopeId, int port); private static native int sendToAddress( - int fd, long memoryAddress, int pos, int limit, byte[] address, int scopeId, int port); + int fd, boolean ipv6, long memoryAddress, int pos, int limit, byte[] address, int scopeId, int port); private static native int sendToAddresses( - int fd, long memoryAddress, int length, byte[] address, int scopeId, int port); + int fd, boolean ipv6, long memoryAddress, int length, byte[] address, int scopeId, int port); private static native DatagramSocketAddress recvFrom( int fd, ByteBuffer buf, int pos, int limit) throws IOException; @@ -484,8 +470,8 @@ public class Socket extends FileDescriptor { private static native int recvFd(int fd); private static native int sendFd(int socketFd, int fd); - private static native int newSocketStreamFd(); - private static native int newSocketDgramFd(); + private static native int newSocketStreamFd(boolean ipv6); + private static native int newSocketDgramFd(boolean ipv6); private static native int newSocketDomainFd(); private static native int isReuseAddress(int fd) throws IOException; @@ -497,7 +483,7 @@ public class Socket extends FileDescriptor { private static native int isBroadcast(int fd) throws IOException; private static native int getSoLinger(int fd) throws IOException; private static native int getSoError(int fd) throws IOException; - private static native int getTrafficClass(int fd) throws IOException; + private static native int getTrafficClass(int fd, boolean ipv6) throws IOException; private static native void setReuseAddress(int fd, int reuseAddress) throws IOException; private static native void setReusePort(int fd, int reuseAddress) throws IOException; @@ -507,6 +493,6 @@ public class Socket extends FileDescriptor { private static native void setTcpNoDelay(int fd, int tcpNoDelay) throws IOException; private static native void setSoLinger(int fd, int soLinger) throws IOException; private static native void setBroadcast(int fd, int broadcast) throws IOException; - private static native void setTrafficClass(int fd, int trafficClass) throws IOException; + private static native void setTrafficClass(int fd, boolean ipv6, int trafficClass) throws IOException; private static native void initialize(boolean ipv4Preferred); } diff --git a/transport-rxtx/pom.xml b/transport-rxtx/pom.xml index b1cfd0b..2c241c5 100644 --- a/transport-rxtx/pom.xml +++ b/transport-rxtx/pom.xml @@ -21,7 +21,7 @@ <parent> <groupId>io.netty</groupId> <artifactId>netty-parent</artifactId> - <version>4.1.33.Final</version> + <version>4.1.48.Final</version> </parent> <artifactId>netty-transport-rxtx</artifactId> diff --git a/transport-sctp/pom.xml b/transport-sctp/pom.xml index f1e1c4f..23337aa 100644 --- a/transport-sctp/pom.xml +++ b/transport-sctp/pom.xml @@ -20,7 +20,7 @@ <parent> <groupId>io.netty</groupId> <artifactId>netty-parent</artifactId> - <version>4.1.33.Final</version> + <version>4.1.48.Final</version> </parent> <artifactId>netty-transport-sctp</artifactId> diff --git a/transport-sctp/src/main/java/io/netty/channel/sctp/DefaultSctpChannelConfig.java b/transport-sctp/src/main/java/io/netty/channel/sctp/DefaultSctpChannelConfig.java index c551366..8537e48 100644 --- a/transport-sctp/src/main/java/io/netty/channel/sctp/DefaultSctpChannelConfig.java +++ b/transport-sctp/src/main/java/io/netty/channel/sctp/DefaultSctpChannelConfig.java @@ -24,6 +24,7 @@ import io.netty.channel.DefaultChannelConfig; import io.netty.channel.MessageSizeEstimator; import io.netty.channel.RecvByteBufAllocator; import io.netty.channel.WriteBufferWaterMark; +import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.PlatformDependent; import java.io.IOException; @@ -43,10 +44,7 @@ public class DefaultSctpChannelConfig extends DefaultChannelConfig implements Sc public DefaultSctpChannelConfig(io.netty.channel.sctp.SctpChannel channel, SctpChannel javaChannel) { super(channel); - if (javaChannel == null) { - throw new NullPointerException("javaChannel"); - } - this.javaChannel = javaChannel; + this.javaChannel = ObjectUtil.checkNotNull(javaChannel, "javaChannel"); // Enable TCP_NODELAY by default if possible. if (PlatformDependent.canEnableTcpNoDelayByDefault()) { diff --git a/transport-sctp/src/main/java/io/netty/channel/sctp/DefaultSctpServerChannelConfig.java b/transport-sctp/src/main/java/io/netty/channel/sctp/DefaultSctpServerChannelConfig.java index 4980eb8..d0f0fa6 100644 --- a/transport-sctp/src/main/java/io/netty/channel/sctp/DefaultSctpServerChannelConfig.java +++ b/transport-sctp/src/main/java/io/netty/channel/sctp/DefaultSctpServerChannelConfig.java @@ -15,6 +15,8 @@ */ package io.netty.channel.sctp; +import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero; + import com.sun.nio.sctp.SctpServerChannel; import com.sun.nio.sctp.SctpStandardSocketOptions; import io.netty.buffer.ByteBufAllocator; @@ -25,6 +27,7 @@ import io.netty.channel.MessageSizeEstimator; import io.netty.channel.RecvByteBufAllocator; import io.netty.channel.WriteBufferWaterMark; import io.netty.util.NetUtil; +import io.netty.util.internal.ObjectUtil; import java.io.IOException; import java.util.Map; @@ -43,10 +46,7 @@ public class DefaultSctpServerChannelConfig extends DefaultChannelConfig impleme public DefaultSctpServerChannelConfig( io.netty.channel.sctp.SctpServerChannel channel, SctpServerChannel javaChannel) { super(channel); - if (javaChannel == null) { - throw new NullPointerException("javaChannel"); - } - this.javaChannel = javaChannel; + this.javaChannel = ObjectUtil.checkNotNull(javaChannel, "javaChannel"); } @Override @@ -152,9 +152,7 @@ public class DefaultSctpServerChannelConfig extends DefaultChannelConfig impleme @Override public SctpServerChannelConfig setBacklog(int backlog) { - if (backlog < 0) { - throw new IllegalArgumentException("backlog: " + backlog); - } + checkPositiveOrZero(backlog, "backlog"); this.backlog = backlog; return this; } diff --git a/transport-sctp/src/main/java/io/netty/channel/sctp/SctpMessage.java b/transport-sctp/src/main/java/io/netty/channel/sctp/SctpMessage.java index 8d48e67..a51aa47 100644 --- a/transport-sctp/src/main/java/io/netty/channel/sctp/SctpMessage.java +++ b/transport-sctp/src/main/java/io/netty/channel/sctp/SctpMessage.java @@ -18,6 +18,7 @@ package io.netty.channel.sctp; import com.sun.nio.sctp.MessageInfo; import io.netty.buffer.ByteBuf; import io.netty.buffer.DefaultByteBufHolder; +import io.netty.util.internal.ObjectUtil; /** * Representation of SCTP Data Chunk @@ -61,13 +62,10 @@ public final class SctpMessage extends DefaultByteBufHolder { */ public SctpMessage(MessageInfo msgInfo, ByteBuf payloadBuffer) { super(payloadBuffer); - if (msgInfo == null) { - throw new NullPointerException("msgInfo"); - } - this.msgInfo = msgInfo; - streamIdentifier = msgInfo.streamNumber(); - protocolIdentifier = msgInfo.payloadProtocolID(); - unordered = msgInfo.isUnordered(); + this.msgInfo = ObjectUtil.checkNotNull(msgInfo, "msgInfo"); + this.streamIdentifier = msgInfo.streamNumber(); + this.protocolIdentifier = msgInfo.payloadProtocolID(); + this.unordered = msgInfo.isUnordered(); } /** diff --git a/transport-sctp/src/main/java/io/netty/channel/sctp/SctpNotificationHandler.java b/transport-sctp/src/main/java/io/netty/channel/sctp/SctpNotificationHandler.java index 11a5272..6418a67 100644 --- a/transport-sctp/src/main/java/io/netty/channel/sctp/SctpNotificationHandler.java +++ b/transport-sctp/src/main/java/io/netty/channel/sctp/SctpNotificationHandler.java @@ -22,8 +22,8 @@ import com.sun.nio.sctp.Notification; import com.sun.nio.sctp.PeerAddressChangeNotification; import com.sun.nio.sctp.SendFailedNotification; import com.sun.nio.sctp.ShutdownNotification; - import io.netty.channel.ChannelPipeline; +import io.netty.util.internal.ObjectUtil; /** @@ -35,10 +35,7 @@ public final class SctpNotificationHandler extends AbstractNotificationHandler<O private final SctpChannel sctpChannel; public SctpNotificationHandler(SctpChannel sctpChannel) { - if (sctpChannel == null) { - throw new NullPointerException("sctpChannel"); - } - this.sctpChannel = sctpChannel; + this.sctpChannel = ObjectUtil.checkNotNull(sctpChannel, "sctpChannel"); } @Override diff --git a/transport-sctp/src/main/java/io/netty/channel/sctp/oio/OioSctpServerChannel.java b/transport-sctp/src/main/java/io/netty/channel/sctp/oio/OioSctpServerChannel.java index 3c73728..963a1d4 100755 --- a/transport-sctp/src/main/java/io/netty/channel/sctp/oio/OioSctpServerChannel.java +++ b/transport-sctp/src/main/java/io/netty/channel/sctp/oio/OioSctpServerChannel.java @@ -25,6 +25,7 @@ import io.netty.channel.ChannelPromise; import io.netty.channel.oio.AbstractOioMessageChannel; import io.netty.channel.sctp.DefaultSctpServerChannelConfig; import io.netty.channel.sctp.SctpServerChannelConfig; +import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; @@ -84,11 +85,7 @@ public class OioSctpServerChannel extends AbstractOioMessageChannel */ public OioSctpServerChannel(SctpServerChannel sch) { super(null); - if (sch == null) { - throw new NullPointerException("sctp server channel"); - } - - this.sch = sch; + this.sch = ObjectUtil.checkNotNull(sch, "sctp server channel"); boolean success = false; try { sch.configureBlocking(false); diff --git a/transport-sctp/src/main/java/io/netty/handler/codec/sctp/SctpMessageCompletionHandler.java b/transport-sctp/src/main/java/io/netty/handler/codec/sctp/SctpMessageCompletionHandler.java index 0a09a5e..7f6de63 100644 --- a/transport-sctp/src/main/java/io/netty/handler/codec/sctp/SctpMessageCompletionHandler.java +++ b/transport-sctp/src/main/java/io/netty/handler/codec/sctp/SctpMessageCompletionHandler.java @@ -22,10 +22,10 @@ import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandler; import io.netty.channel.sctp.SctpMessage; import io.netty.handler.codec.MessageToMessageDecoder; +import io.netty.util.collection.IntObjectHashMap; +import io.netty.util.collection.IntObjectMap; -import java.util.HashMap; import java.util.List; -import java.util.Map; /** * {@link MessageToMessageDecoder} which will take care of handle fragmented {@link SctpMessage}s, so @@ -33,7 +33,7 @@ import java.util.Map; * {@link ChannelInboundHandler}. */ public class SctpMessageCompletionHandler extends MessageToMessageDecoder<SctpMessage> { - private final Map<Integer, ByteBuf> fragments = new HashMap<Integer, ByteBuf>(); + private final IntObjectMap<ByteBuf> fragments = new IntObjectHashMap<ByteBuf>(); @Override protected void decode(ChannelHandlerContext ctx, SctpMessage msg, List<Object> out) throws Exception { diff --git a/transport-udt/pom.xml b/transport-udt/pom.xml index 628643d..ddf110c 100644 --- a/transport-udt/pom.xml +++ b/transport-udt/pom.xml @@ -21,7 +21,7 @@ <parent> <groupId>io.netty</groupId> <artifactId>netty-parent</artifactId> - <version>4.1.33.Final</version> + <version>4.1.48.Final</version> </parent> <artifactId>netty-transport-udt</artifactId> diff --git a/transport-udt/src/main/java/io/netty/channel/udt/nio/NioUdtMessageConnectorChannel.java b/transport-udt/src/main/java/io/netty/channel/udt/nio/NioUdtMessageConnectorChannel.java index ca0f295..204b0b7 100644 --- a/transport-udt/src/main/java/io/netty/channel/udt/nio/NioUdtMessageConnectorChannel.java +++ b/transport-udt/src/main/java/io/netty/channel/udt/nio/NioUdtMessageConnectorChannel.java @@ -79,9 +79,7 @@ public class NioUdtMessageConnectorChannel extends AbstractNioMessageChannel imp try { channelUDT.close(); } catch (final Exception e2) { - if (logger.isWarnEnabled()) { - logger.warn("Failed to close channel.", e2); - } + logger.warn("Failed to close channel.", e2); } throw new ChannelException("Failed to configure channel.", e); } diff --git a/transport-udt/src/test/java/io/netty/test/udt/util/CaliperBench.java b/transport-udt/src/test/java/io/netty/test/udt/util/CaliperBench.java index 983daa8..ca86eda 100644 --- a/transport-udt/src/test/java/io/netty/test/udt/util/CaliperBench.java +++ b/transport-udt/src/test/java/io/netty/test/udt/util/CaliperBench.java @@ -91,7 +91,6 @@ public abstract class CaliperBench extends SimpleBenchmark { return; } else { System.out.print("-"); - continue; } } } diff --git a/transport/pom.xml b/transport/pom.xml index e2b4f8e..23af92b 100644 --- a/transport/pom.xml +++ b/transport/pom.xml @@ -20,7 +20,7 @@ <parent> <groupId>io.netty</groupId> <artifactId>netty-parent</artifactId> - <version>4.1.33.Final</version> + <version>4.1.48.Final</version> </parent> <artifactId>netty-transport</artifactId> diff --git a/transport/src/main/java/io/netty/bootstrap/AbstractBootstrap.java b/transport/src/main/java/io/netty/bootstrap/AbstractBootstrap.java index 1030c32..cd3e741 100644 --- a/transport/src/main/java/io/netty/bootstrap/AbstractBootstrap.java +++ b/transport/src/main/java/io/netty/bootstrap/AbstractBootstrap.java @@ -26,10 +26,11 @@ import io.netty.channel.DefaultChannelPromise; import io.netty.channel.EventLoop; import io.netty.channel.EventLoopGroup; import io.netty.channel.ReflectiveChannelFactory; -import io.netty.util.internal.SocketUtils; import io.netty.util.AttributeKey; import io.netty.util.concurrent.EventExecutor; import io.netty.util.concurrent.GlobalEventExecutor; +import io.netty.util.internal.ObjectUtil; +import io.netty.util.internal.SocketUtils; import io.netty.util.internal.StringUtil; import io.netty.util.internal.logging.InternalLogger; @@ -37,8 +38,10 @@ import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.util.Collections; +import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; /** * {@link AbstractBootstrap} is a helper class that makes it easy to bootstrap a {@link Channel}. It support @@ -48,13 +51,20 @@ import java.util.Map; * transports such as datagram (UDP).</p> */ public abstract class AbstractBootstrap<B extends AbstractBootstrap<B, C>, C extends Channel> implements Cloneable { + @SuppressWarnings("unchecked") + static final Map.Entry<ChannelOption<?>, Object>[] EMPTY_OPTION_ARRAY = new Map.Entry[0]; + @SuppressWarnings("unchecked") + static final Map.Entry<AttributeKey<?>, Object>[] EMPTY_ATTRIBUTE_ARRAY = new Map.Entry[0]; volatile EventLoopGroup group; @SuppressWarnings("deprecation") private volatile ChannelFactory<? extends C> channelFactory; private volatile SocketAddress localAddress; + + // The order in which ChannelOptions are applied is important they may depend on each other for validation + // purposes. private final Map<ChannelOption<?>, Object> options = new LinkedHashMap<ChannelOption<?>, Object>(); - private final Map<AttributeKey<?>, Object> attrs = new LinkedHashMap<AttributeKey<?>, Object>(); + private final Map<AttributeKey<?>, Object> attrs = new ConcurrentHashMap<AttributeKey<?>, Object>(); private volatile ChannelHandler handler; AbstractBootstrap() { @@ -69,9 +79,7 @@ public abstract class AbstractBootstrap<B extends AbstractBootstrap<B, C>, C ext synchronized (bootstrap.options) { options.putAll(bootstrap.options); } - synchronized (bootstrap.attrs) { - attrs.putAll(bootstrap.attrs); - } + attrs.putAll(bootstrap.attrs); } /** @@ -79,9 +87,7 @@ public abstract class AbstractBootstrap<B extends AbstractBootstrap<B, C>, C ext * {@link Channel} */ public B group(EventLoopGroup group) { - if (group == null) { - throw new NullPointerException("group"); - } + ObjectUtil.checkNotNull(group, "group"); if (this.group != null) { throw new IllegalStateException("group set already"); } @@ -100,10 +106,9 @@ public abstract class AbstractBootstrap<B extends AbstractBootstrap<B, C>, C ext * {@link Channel} implementation has no no-args constructor. */ public B channel(Class<? extends C> channelClass) { - if (channelClass == null) { - throw new NullPointerException("channelClass"); - } - return channelFactory(new ReflectiveChannelFactory<C>(channelClass)); + return channelFactory(new ReflectiveChannelFactory<C>( + ObjectUtil.checkNotNull(channelClass, "channelClass") + )); } /** @@ -111,9 +116,7 @@ public abstract class AbstractBootstrap<B extends AbstractBootstrap<B, C>, C ext */ @Deprecated public B channelFactory(ChannelFactory<? extends C> channelFactory) { - if (channelFactory == null) { - throw new NullPointerException("channelFactory"); - } + ObjectUtil.checkNotNull(channelFactory, "channelFactory"); if (this.channelFactory != null) { throw new IllegalStateException("channelFactory set already"); } @@ -168,15 +171,11 @@ public abstract class AbstractBootstrap<B extends AbstractBootstrap<B, C>, C ext * created. Use a value of {@code null} to remove a previous set {@link ChannelOption}. */ public <T> B option(ChannelOption<T> option, T value) { - if (option == null) { - throw new NullPointerException("option"); - } - if (value == null) { - synchronized (options) { + ObjectUtil.checkNotNull(option, "option"); + synchronized (options) { + if (value == null) { options.remove(option); - } - } else { - synchronized (options) { + } else { options.put(option, value); } } @@ -188,17 +187,11 @@ public abstract class AbstractBootstrap<B extends AbstractBootstrap<B, C>, C ext * {@code null}, the attribute of the specified {@code key} is removed. */ public <T> B attr(AttributeKey<T> key, T value) { - if (key == null) { - throw new NullPointerException("key"); - } + ObjectUtil.checkNotNull(key, "key"); if (value == null) { - synchronized (attrs) { - attrs.remove(key); - } + attrs.remove(key); } else { - synchronized (attrs) { - attrs.put(key, value); - } + attrs.put(key, value); } return self(); } @@ -272,10 +265,7 @@ public abstract class AbstractBootstrap<B extends AbstractBootstrap<B, C>, C ext */ public ChannelFuture bind(SocketAddress localAddress) { validate(); - if (localAddress == null) { - throw new NullPointerException("localAddress"); - } - return doBind(localAddress); + return doBind(ObjectUtil.checkNotNull(localAddress, "localAddress")); } private ChannelFuture doBind(final SocketAddress localAddress) { @@ -375,10 +365,7 @@ public abstract class AbstractBootstrap<B extends AbstractBootstrap<B, C>, C ext * the {@link ChannelHandler} to use for serving the requests. */ public B handler(ChannelHandler handler) { - if (handler == null) { - throw new NullPointerException("handler"); - } - this.handler = handler; + this.handler = ObjectUtil.checkNotNull(handler, "handler"); return self(); } @@ -398,15 +385,10 @@ public abstract class AbstractBootstrap<B extends AbstractBootstrap<B, C>, C ext */ public abstract AbstractBootstrapConfig<B, C> config(); - static <K, V> Map<K, V> copiedMap(Map<K, V> map) { - final Map<K, V> copied; - synchronized (map) { - if (map.isEmpty()) { - return Collections.emptyMap(); - } - copied = new LinkedHashMap<K, V>(map); + final Map.Entry<ChannelOption<?>, Object>[] newOptionsArray() { + synchronized (options) { + return options.entrySet().toArray(EMPTY_OPTION_ARRAY); } - return Collections.unmodifiableMap(copied); } final Map<ChannelOption<?>, Object> options0() { @@ -431,17 +413,27 @@ public abstract class AbstractBootstrap<B extends AbstractBootstrap<B, C>, C ext } final Map<ChannelOption<?>, Object> options() { - return copiedMap(options); + synchronized (options) { + return copiedMap(options); + } } final Map<AttributeKey<?>, Object> attrs() { return copiedMap(attrs); } - static void setChannelOptions( - Channel channel, Map<ChannelOption<?>, Object> options, InternalLogger logger) { - for (Map.Entry<ChannelOption<?>, Object> e: options.entrySet()) { - setChannelOption(channel, e.getKey(), e.getValue(), logger); + static <K, V> Map<K, V> copiedMap(Map<K, V> map) { + if (map.isEmpty()) { + return Collections.emptyMap(); + } + return Collections.unmodifiableMap(new HashMap<K, V>(map)); + } + + static void setAttributes(Channel channel, Map.Entry<AttributeKey<?>, Object>[] attrs) { + for (Map.Entry<AttributeKey<?>, Object> e: attrs) { + @SuppressWarnings("unchecked") + AttributeKey<Object> key = (AttributeKey<Object>) e.getKey(); + channel.attr(key).set(e.getValue()); } } diff --git a/transport/src/main/java/io/netty/bootstrap/Bootstrap.java b/transport/src/main/java/io/netty/bootstrap/Bootstrap.java index 1e2e483..7859042 100644 --- a/transport/src/main/java/io/netty/bootstrap/Bootstrap.java +++ b/transport/src/main/java/io/netty/bootstrap/Bootstrap.java @@ -18,7 +18,6 @@ package io.netty.bootstrap; import io.netty.channel.Channel; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; -import io.netty.channel.ChannelOption; import io.netty.channel.ChannelPipeline; import io.netty.channel.ChannelPromise; import io.netty.channel.EventLoop; @@ -27,17 +26,15 @@ import io.netty.resolver.AddressResolver; import io.netty.resolver.DefaultAddressResolverGroup; import io.netty.resolver.NameResolver; import io.netty.resolver.AddressResolverGroup; -import io.netty.util.AttributeKey; import io.netty.util.concurrent.Future; import io.netty.util.concurrent.FutureListener; +import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.SocketAddress; -import java.util.Map; -import java.util.Map.Entry; /** * A {@link Bootstrap} that makes it easy to bootstrap a {@link Channel} to use @@ -137,10 +134,7 @@ public class Bootstrap extends AbstractBootstrap<Bootstrap, Channel> { * Connect a {@link Channel} to the remote peer. */ public ChannelFuture connect(SocketAddress remoteAddress) { - if (remoteAddress == null) { - throw new NullPointerException("remoteAddress"); - } - + ObjectUtil.checkNotNull(remoteAddress, "remoteAddress"); validate(); return doResolveAndConnect(remoteAddress, config.localAddress()); } @@ -149,9 +143,7 @@ public class Bootstrap extends AbstractBootstrap<Bootstrap, Channel> { * Connect a {@link Channel} to the remote peer. */ public ChannelFuture connect(SocketAddress remoteAddress, SocketAddress localAddress) { - if (remoteAddress == null) { - throw new NullPointerException("remoteAddress"); - } + ObjectUtil.checkNotNull(remoteAddress, "remoteAddress"); validate(); return doResolveAndConnect(remoteAddress, localAddress); } @@ -197,7 +189,13 @@ public class Bootstrap extends AbstractBootstrap<Bootstrap, Channel> { final SocketAddress localAddress, final ChannelPromise promise) { try { final EventLoop eventLoop = channel.eventLoop(); - final AddressResolver<SocketAddress> resolver = this.resolver.getResolver(eventLoop); + AddressResolver<SocketAddress> resolver; + try { + resolver = this.resolver.getResolver(eventLoop); + } catch (Throwable cause) { + channel.close(); + return promise.setFailure(cause); + } if (!resolver.isSupported(remoteAddress) || resolver.isResolved(remoteAddress)) { // Resolver has no idea about what to do with the specified remote address or it's resolved already. @@ -260,21 +258,12 @@ public class Bootstrap extends AbstractBootstrap<Bootstrap, Channel> { @Override @SuppressWarnings("unchecked") - void init(Channel channel) throws Exception { + void init(Channel channel) { ChannelPipeline p = channel.pipeline(); p.addLast(config.handler()); - final Map<ChannelOption<?>, Object> options = options0(); - synchronized (options) { - setChannelOptions(channel, options, logger); - } - - final Map<AttributeKey<?>, Object> attrs = attrs0(); - synchronized (attrs) { - for (Entry<AttributeKey<?>, Object> e: attrs.entrySet()) { - channel.attr((AttributeKey<Object>) e.getKey()).set(e.getValue()); - } - } + setChannelOptions(channel, newOptionsArray(), logger); + setAttributes(channel, attrs0().entrySet().toArray(EMPTY_ATTRIBUTE_ARRAY)); } @Override diff --git a/transport/src/main/java/io/netty/bootstrap/ServerBootstrap.java b/transport/src/main/java/io/netty/bootstrap/ServerBootstrap.java index 310e9fb..37dd350 100644 --- a/transport/src/main/java/io/netty/bootstrap/ServerBootstrap.java +++ b/transport/src/main/java/io/netty/bootstrap/ServerBootstrap.java @@ -28,12 +28,14 @@ import io.netty.channel.ChannelPipeline; import io.netty.channel.EventLoopGroup; import io.netty.channel.ServerChannel; import io.netty.util.AttributeKey; +import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; import java.util.LinkedHashMap; import java.util.Map; import java.util.Map.Entry; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; /** @@ -44,8 +46,10 @@ public class ServerBootstrap extends AbstractBootstrap<ServerBootstrap, ServerCh private static final InternalLogger logger = InternalLoggerFactory.getInstance(ServerBootstrap.class); + // The order in which child ChannelOptions are applied is important they may depend on each other for validation + // purposes. private final Map<ChannelOption<?>, Object> childOptions = new LinkedHashMap<ChannelOption<?>, Object>(); - private final Map<AttributeKey<?>, Object> childAttrs = new LinkedHashMap<AttributeKey<?>, Object>(); + private final Map<AttributeKey<?>, Object> childAttrs = new ConcurrentHashMap<AttributeKey<?>, Object>(); private final ServerBootstrapConfig config = new ServerBootstrapConfig(this); private volatile EventLoopGroup childGroup; private volatile ChannelHandler childHandler; @@ -59,9 +63,7 @@ public class ServerBootstrap extends AbstractBootstrap<ServerBootstrap, ServerCh synchronized (bootstrap.childOptions) { childOptions.putAll(bootstrap.childOptions); } - synchronized (bootstrap.childAttrs) { - childAttrs.putAll(bootstrap.childAttrs); - } + childAttrs.putAll(bootstrap.childAttrs); } /** @@ -79,13 +81,10 @@ public class ServerBootstrap extends AbstractBootstrap<ServerBootstrap, ServerCh */ public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup) { super.group(parentGroup); - if (childGroup == null) { - throw new NullPointerException("childGroup"); - } if (this.childGroup != null) { throw new IllegalStateException("childGroup set already"); } - this.childGroup = childGroup; + this.childGroup = ObjectUtil.checkNotNull(childGroup, "childGroup"); return this; } @@ -95,15 +94,11 @@ public class ServerBootstrap extends AbstractBootstrap<ServerBootstrap, ServerCh * {@link ChannelOption}. */ public <T> ServerBootstrap childOption(ChannelOption<T> childOption, T value) { - if (childOption == null) { - throw new NullPointerException("childOption"); - } - if (value == null) { - synchronized (childOptions) { + ObjectUtil.checkNotNull(childOption, "childOption"); + synchronized (childOptions) { + if (value == null) { childOptions.remove(childOption); - } - } else { - synchronized (childOptions) { + } else { childOptions.put(childOption, value); } } @@ -115,9 +110,7 @@ public class ServerBootstrap extends AbstractBootstrap<ServerBootstrap, ServerCh * {@code null} the {@link AttributeKey} is removed */ public <T> ServerBootstrap childAttr(AttributeKey<T> childKey, T value) { - if (childKey == null) { - throw new NullPointerException("childKey"); - } + ObjectUtil.checkNotNull(childKey, "childKey"); if (value == null) { childAttrs.remove(childKey); } else { @@ -130,45 +123,28 @@ public class ServerBootstrap extends AbstractBootstrap<ServerBootstrap, ServerCh * Set the {@link ChannelHandler} which is used to serve the request for the {@link Channel}'s. */ public ServerBootstrap childHandler(ChannelHandler childHandler) { - if (childHandler == null) { - throw new NullPointerException("childHandler"); - } - this.childHandler = childHandler; + this.childHandler = ObjectUtil.checkNotNull(childHandler, "childHandler"); return this; } @Override - void init(Channel channel) throws Exception { - final Map<ChannelOption<?>, Object> options = options0(); - synchronized (options) { - setChannelOptions(channel, options, logger); - } - - final Map<AttributeKey<?>, Object> attrs = attrs0(); - synchronized (attrs) { - for (Entry<AttributeKey<?>, Object> e: attrs.entrySet()) { - @SuppressWarnings("unchecked") - AttributeKey<Object> key = (AttributeKey<Object>) e.getKey(); - channel.attr(key).set(e.getValue()); - } - } + void init(Channel channel) { + setChannelOptions(channel, newOptionsArray(), logger); + setAttributes(channel, attrs0().entrySet().toArray(EMPTY_ATTRIBUTE_ARRAY)); ChannelPipeline p = channel.pipeline(); final EventLoopGroup currentChildGroup = childGroup; final ChannelHandler currentChildHandler = childHandler; final Entry<ChannelOption<?>, Object>[] currentChildOptions; - final Entry<AttributeKey<?>, Object>[] currentChildAttrs; synchronized (childOptions) { - currentChildOptions = childOptions.entrySet().toArray(newOptionArray(0)); - } - synchronized (childAttrs) { - currentChildAttrs = childAttrs.entrySet().toArray(newAttrArray(0)); + currentChildOptions = childOptions.entrySet().toArray(EMPTY_OPTION_ARRAY); } + final Entry<AttributeKey<?>, Object>[] currentChildAttrs = childAttrs.entrySet().toArray(EMPTY_ATTRIBUTE_ARRAY); p.addLast(new ChannelInitializer<Channel>() { @Override - public void initChannel(final Channel ch) throws Exception { + public void initChannel(final Channel ch) { final ChannelPipeline pipeline = ch.pipeline(); ChannelHandler handler = config.handler(); if (handler != null) { @@ -199,16 +175,6 @@ public class ServerBootstrap extends AbstractBootstrap<ServerBootstrap, ServerCh return this; } - @SuppressWarnings("unchecked") - private static Entry<AttributeKey<?>, Object>[] newAttrArray(int size) { - return new Entry[size]; - } - - @SuppressWarnings("unchecked") - private static Map.Entry<ChannelOption<?>, Object>[] newOptionArray(int size) { - return new Map.Entry[size]; - } - private static class ServerBootstrapAcceptor extends ChannelInboundHandlerAdapter { private final EventLoopGroup childGroup; @@ -246,10 +212,7 @@ public class ServerBootstrap extends AbstractBootstrap<ServerBootstrap, ServerCh child.pipeline().addLast(childHandler); setChannelOptions(child, childOptions, logger); - - for (Entry<AttributeKey<?>, Object> e: childAttrs) { - child.attr((AttributeKey<Object>) e.getKey()).set(e.getValue()); - } + setAttributes(child, childAttrs); try { childGroup.register(child).addListener(new ChannelFutureListener() { @@ -307,7 +270,9 @@ public class ServerBootstrap extends AbstractBootstrap<ServerBootstrap, ServerCh } final Map<ChannelOption<?>, Object> childOptions() { - return copiedMap(childOptions); + synchronized (childOptions) { + return copiedMap(childOptions); + } } final Map<AttributeKey<?>, Object> childAttrs() { diff --git a/transport/src/main/java/io/netty/channel/AbstractChannel.java b/transport/src/main/java/io/netty/channel/AbstractChannel.java index 11c14a9..e701657 100644 --- a/transport/src/main/java/io/netty/channel/AbstractChannel.java +++ b/transport/src/main/java/io/netty/channel/AbstractChannel.java @@ -20,8 +20,8 @@ import io.netty.channel.socket.ChannelOutputShutdownEvent; import io.netty.channel.socket.ChannelOutputShutdownException; import io.netty.util.DefaultAttributeMap; import io.netty.util.ReferenceCountUtil; +import io.netty.util.internal.ObjectUtil; 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; @@ -44,17 +44,6 @@ public abstract class AbstractChannel extends DefaultAttributeMap implements Cha private static final InternalLogger logger = InternalLoggerFactory.getInstance(AbstractChannel.class); - private static final ClosedChannelException FLUSH0_CLOSED_CHANNEL_EXCEPTION = ThrowableUtil.unknownStackTrace( - new ClosedChannelException(), AbstractUnsafe.class, "flush0()"); - private static final ClosedChannelException ENSURE_OPEN_CLOSED_CHANNEL_EXCEPTION = ThrowableUtil.unknownStackTrace( - new ClosedChannelException(), AbstractUnsafe.class, "ensureOpen(...)"); - private static final ClosedChannelException CLOSE_CLOSED_CHANNEL_EXCEPTION = ThrowableUtil.unknownStackTrace( - new ClosedChannelException(), AbstractUnsafe.class, "close(...)"); - private static final ClosedChannelException WRITE_CLOSED_CHANNEL_EXCEPTION = ThrowableUtil.unknownStackTrace( - new ClosedChannelException(), AbstractUnsafe.class, "write(...)"); - private static final NotYetConnectedException FLUSH0_NOT_YET_CONNECTED_EXCEPTION = ThrowableUtil.unknownStackTrace( - new NotYetConnectedException(), AbstractUnsafe.class, "flush0()"); - private final Channel parent; private final ChannelId id; private final Unsafe unsafe; @@ -67,6 +56,7 @@ public abstract class AbstractChannel extends DefaultAttributeMap implements Cha private volatile EventLoop eventLoop; private volatile boolean registered; private boolean closeInitiated; + private Throwable initialCloseCause; /** Cache for the string representation of this channel */ private boolean strValActive; @@ -461,9 +451,7 @@ public abstract class AbstractChannel extends DefaultAttributeMap implements Cha @Override public final void register(EventLoop eventLoop, final ChannelPromise promise) { - if (eventLoop == null) { - throw new NullPointerException("eventLoop"); - } + ObjectUtil.checkNotNull(eventLoop, "eventLoop"); if (isRegistered()) { promise.setFailure(new IllegalStateException("registered to an event loop already")); return; @@ -589,6 +577,9 @@ public abstract class AbstractChannel extends DefaultAttributeMap implements Cha boolean wasActive = isActive(); try { doDisconnect(); + // Reset remoteAddress and localAddress + remoteAddress = null; + localAddress = null; } catch (Throwable t) { safeSetFailure(promise, t); closeIfClosed(); @@ -612,7 +603,8 @@ public abstract class AbstractChannel extends DefaultAttributeMap implements Cha public final void close(final ChannelPromise promise) { assertEventLoop(); - close(promise, CLOSE_CLOSED_CHANNEL_EXCEPTION, CLOSE_CLOSED_CHANNEL_EXCEPTION, false); + ClosedChannelException closedChannelException = new ClosedChannelException(); + close(promise, closedChannelException, closedChannelException, false); } /** @@ -637,7 +629,7 @@ public abstract class AbstractChannel extends DefaultAttributeMap implements Cha final ChannelOutboundBuffer outboundBuffer = this.outboundBuffer; if (outboundBuffer == null) { - promise.setFailure(CLOSE_CLOSED_CHANNEL_EXCEPTION); + promise.setFailure(new ClosedChannelException()); return; } this.outboundBuffer = null; // Disallow adding any messages and flushes to outboundBuffer. @@ -870,7 +862,7 @@ public abstract class AbstractChannel extends DefaultAttributeMap implements Cha // need to fail the future right away. If it is not null the handling of the rest // will be done in flush0() // See https://github.com/netty/netty/issues/2362 - safeSetFailure(promise, WRITE_CLOSED_CHANNEL_EXCEPTION); + safeSetFailure(promise, newClosedChannelException(initialCloseCause)); // release message now to prevent resource-leak ReferenceCountUtil.release(msg); return; @@ -923,10 +915,10 @@ public abstract class AbstractChannel extends DefaultAttributeMap implements Cha if (!isActive()) { try { if (isOpen()) { - outboundBuffer.failFlushed(FLUSH0_NOT_YET_CONNECTED_EXCEPTION, true); + outboundBuffer.failFlushed(new NotYetConnectedException(), true); } else { // Do not trigger channelWritabilityChanged because the channel is closed already. - outboundBuffer.failFlushed(FLUSH0_CLOSED_CHANNEL_EXCEPTION, false); + outboundBuffer.failFlushed(newClosedChannelException(initialCloseCause), false); } } finally { inFlush0 = false; @@ -946,12 +938,14 @@ public abstract class AbstractChannel extends DefaultAttributeMap implements Cha * This is needed as otherwise {@link #isActive()} , {@link #isOpen()} and {@link #isWritable()} * may still return {@code true} even if the channel should be closed as result of the exception. */ - close(voidPromise(), t, FLUSH0_CLOSED_CHANNEL_EXCEPTION, false); + initialCloseCause = t; + close(voidPromise(), t, newClosedChannelException(t), false); } else { try { shutdownOutput(voidPromise(), t); } catch (Throwable t2) { - close(voidPromise(), t2, FLUSH0_CLOSED_CHANNEL_EXCEPTION, false); + initialCloseCause = t; + close(voidPromise(), t2, newClosedChannelException(t), false); } } } finally { @@ -959,6 +953,14 @@ public abstract class AbstractChannel extends DefaultAttributeMap implements Cha } } + private ClosedChannelException newClosedChannelException(Throwable cause) { + ClosedChannelException exception = new ClosedChannelException(); + if (cause != null) { + exception.initCause(cause); + } + return exception; + } + @Override public final ChannelPromise voidPromise() { assertEventLoop(); @@ -971,7 +973,7 @@ public abstract class AbstractChannel extends DefaultAttributeMap implements Cha return true; } - safeSetFailure(promise, ENSURE_OPEN_CLOSED_CHANNEL_EXCEPTION); + safeSetFailure(promise, newClosedChannelException(initialCloseCause)); return false; } @@ -1122,6 +1124,10 @@ public abstract class AbstractChannel extends DefaultAttributeMap implements Cha return msg; } + protected void validateFileRegion(DefaultFileRegion region, long position) throws IOException { + DefaultFileRegion.validate(region, position); + } + static final class CloseFuture extends DefaultChannelPromise { CloseFuture(AbstractChannel ch) { @@ -1160,7 +1166,6 @@ public abstract class AbstractChannel extends DefaultAttributeMap implements Cha AnnotatedConnectException(ConnectException exception, SocketAddress remoteAddress) { super(exception.getMessage() + ": " + remoteAddress); initCause(exception); - setStackTrace(exception.getStackTrace()); } @Override @@ -1176,7 +1181,6 @@ public abstract class AbstractChannel extends DefaultAttributeMap implements Cha AnnotatedNoRouteToHostException(NoRouteToHostException exception, SocketAddress remoteAddress) { super(exception.getMessage() + ": " + remoteAddress); initCause(exception); - setStackTrace(exception.getStackTrace()); } @Override @@ -1192,7 +1196,6 @@ public abstract class AbstractChannel extends DefaultAttributeMap implements Cha AnnotatedSocketException(SocketException exception, SocketAddress remoteAddress) { super(exception.getMessage() + ": " + remoteAddress); initCause(exception); - setStackTrace(exception.getStackTrace()); } @Override diff --git a/transport/src/main/java/io/netty/channel/AbstractChannelHandlerContext.java b/transport/src/main/java/io/netty/channel/AbstractChannelHandlerContext.java index 9e599c3..92c912a 100644 --- a/transport/src/main/java/io/netty/channel/AbstractChannelHandlerContext.java +++ b/transport/src/main/java/io/netty/channel/AbstractChannelHandlerContext.java @@ -18,12 +18,14 @@ package io.netty.channel; import io.netty.buffer.ByteBufAllocator; import io.netty.util.Attribute; import io.netty.util.AttributeKey; -import io.netty.util.DefaultAttributeMap; -import io.netty.util.Recycler; import io.netty.util.ReferenceCountUtil; import io.netty.util.ResourceLeakHint; +import io.netty.util.concurrent.AbstractEventExecutor; import io.netty.util.concurrent.EventExecutor; import io.netty.util.concurrent.OrderedEventExecutor; +import io.netty.util.internal.ObjectPool; +import io.netty.util.internal.ObjectPool.Handle; +import io.netty.util.internal.ObjectPool.ObjectCreator; import io.netty.util.internal.PromiseNotificationUtil; import io.netty.util.internal.ThrowableUtil; import io.netty.util.internal.ObjectUtil; @@ -35,8 +37,28 @@ import io.netty.util.internal.logging.InternalLoggerFactory; import java.net.SocketAddress; import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; -abstract class AbstractChannelHandlerContext extends DefaultAttributeMap - implements ChannelHandlerContext, ResourceLeakHint { +import static io.netty.channel.ChannelHandlerMask.MASK_BIND; +import static io.netty.channel.ChannelHandlerMask.MASK_CHANNEL_ACTIVE; +import static io.netty.channel.ChannelHandlerMask.MASK_CHANNEL_INACTIVE; +import static io.netty.channel.ChannelHandlerMask.MASK_CHANNEL_READ; +import static io.netty.channel.ChannelHandlerMask.MASK_CHANNEL_READ_COMPLETE; +import static io.netty.channel.ChannelHandlerMask.MASK_CHANNEL_REGISTERED; +import static io.netty.channel.ChannelHandlerMask.MASK_CHANNEL_UNREGISTERED; +import static io.netty.channel.ChannelHandlerMask.MASK_CHANNEL_WRITABILITY_CHANGED; +import static io.netty.channel.ChannelHandlerMask.MASK_CLOSE; +import static io.netty.channel.ChannelHandlerMask.MASK_CONNECT; +import static io.netty.channel.ChannelHandlerMask.MASK_DEREGISTER; +import static io.netty.channel.ChannelHandlerMask.MASK_DISCONNECT; +import static io.netty.channel.ChannelHandlerMask.MASK_EXCEPTION_CAUGHT; +import static io.netty.channel.ChannelHandlerMask.MASK_FLUSH; +import static io.netty.channel.ChannelHandlerMask.MASK_ONLY_INBOUND; +import static io.netty.channel.ChannelHandlerMask.MASK_ONLY_OUTBOUND; +import static io.netty.channel.ChannelHandlerMask.MASK_READ; +import static io.netty.channel.ChannelHandlerMask.MASK_USER_EVENT_TRIGGERED; +import static io.netty.channel.ChannelHandlerMask.MASK_WRITE; +import static io.netty.channel.ChannelHandlerMask.mask; + +abstract class AbstractChannelHandlerContext implements ChannelHandlerContext, ResourceLeakHint { private static final InternalLogger logger = InternalLoggerFactory.getInstance(AbstractChannelHandlerContext.class); volatile AbstractChannelHandlerContext next; @@ -63,11 +85,10 @@ abstract class AbstractChannelHandlerContext extends DefaultAttributeMap */ private static final int INIT = 0; - private final boolean inbound; - private final boolean outbound; private final DefaultChannelPipeline pipeline; private final String name; private final boolean ordered; + private final int executionMask; // Will be set to null if no child executor should be used, otherwise it will be set to the // child executor. @@ -76,20 +97,16 @@ abstract class AbstractChannelHandlerContext extends DefaultAttributeMap // Lazily instantiated tasks used to trigger events to a handler with different executor. // There is no need to make this volatile as at worse it will just create a few more instances then needed. - private Runnable invokeChannelReadCompleteTask; - private Runnable invokeReadTask; - private Runnable invokeChannelWritableStateChangedTask; - private Runnable invokeFlushTask; + private Tasks invokeTasks; private volatile int handlerState = INIT; - AbstractChannelHandlerContext(DefaultChannelPipeline pipeline, EventExecutor executor, String name, - boolean inbound, boolean outbound) { + AbstractChannelHandlerContext(DefaultChannelPipeline pipeline, EventExecutor executor, + String name, Class<? extends ChannelHandler> handlerClass) { this.name = ObjectUtil.checkNotNull(name, "name"); this.pipeline = pipeline; this.executor = executor; - this.inbound = inbound; - this.outbound = outbound; + this.executionMask = mask(handlerClass); // Its ordered if its driven by the EventLoop or the given Executor is an instanceof OrderedEventExecutor. ordered = executor == null || executor instanceof OrderedEventExecutor; } @@ -125,7 +142,7 @@ abstract class AbstractChannelHandlerContext extends DefaultAttributeMap @Override public ChannelHandlerContext fireChannelRegistered() { - invokeChannelRegistered(findContextInbound()); + invokeChannelRegistered(findContextInbound(MASK_CHANNEL_REGISTERED)); return this; } @@ -157,7 +174,7 @@ abstract class AbstractChannelHandlerContext extends DefaultAttributeMap @Override public ChannelHandlerContext fireChannelUnregistered() { - invokeChannelUnregistered(findContextInbound()); + invokeChannelUnregistered(findContextInbound(MASK_CHANNEL_UNREGISTERED)); return this; } @@ -189,7 +206,7 @@ abstract class AbstractChannelHandlerContext extends DefaultAttributeMap @Override public ChannelHandlerContext fireChannelActive() { - invokeChannelActive(findContextInbound()); + invokeChannelActive(findContextInbound(MASK_CHANNEL_ACTIVE)); return this; } @@ -221,7 +238,7 @@ abstract class AbstractChannelHandlerContext extends DefaultAttributeMap @Override public ChannelHandlerContext fireChannelInactive() { - invokeChannelInactive(findContextInbound()); + invokeChannelInactive(findContextInbound(MASK_CHANNEL_INACTIVE)); return this; } @@ -253,7 +270,7 @@ abstract class AbstractChannelHandlerContext extends DefaultAttributeMap @Override public ChannelHandlerContext fireExceptionCaught(final Throwable cause) { - invokeExceptionCaught(next, cause); + invokeExceptionCaught(findContextInbound(MASK_EXCEPTION_CAUGHT), cause); return this; } @@ -304,7 +321,7 @@ abstract class AbstractChannelHandlerContext extends DefaultAttributeMap @Override public ChannelHandlerContext fireUserEventTriggered(final Object event) { - invokeUserEventTriggered(findContextInbound(), event); + invokeUserEventTriggered(findContextInbound(MASK_USER_EVENT_TRIGGERED), event); return this; } @@ -337,7 +354,7 @@ abstract class AbstractChannelHandlerContext extends DefaultAttributeMap @Override public ChannelHandlerContext fireChannelRead(final Object msg) { - invokeChannelRead(findContextInbound(), msg); + invokeChannelRead(findContextInbound(MASK_CHANNEL_READ), msg); return this; } @@ -370,7 +387,7 @@ abstract class AbstractChannelHandlerContext extends DefaultAttributeMap @Override public ChannelHandlerContext fireChannelReadComplete() { - invokeChannelReadComplete(findContextInbound()); + invokeChannelReadComplete(findContextInbound(MASK_CHANNEL_READ_COMPLETE)); return this; } @@ -379,16 +396,11 @@ abstract class AbstractChannelHandlerContext extends DefaultAttributeMap if (executor.inEventLoop()) { next.invokeChannelReadComplete(); } else { - Runnable task = next.invokeChannelReadCompleteTask; - if (task == null) { - next.invokeChannelReadCompleteTask = task = new Runnable() { - @Override - public void run() { - next.invokeChannelReadComplete(); - } - }; + Tasks tasks = next.invokeTasks; + if (tasks == null) { + next.invokeTasks = tasks = new Tasks(next); } - executor.execute(task); + executor.execute(tasks.invokeChannelReadCompleteTask); } } @@ -406,7 +418,7 @@ abstract class AbstractChannelHandlerContext extends DefaultAttributeMap @Override public ChannelHandlerContext fireChannelWritabilityChanged() { - invokeChannelWritabilityChanged(findContextInbound()); + invokeChannelWritabilityChanged(findContextInbound(MASK_CHANNEL_WRITABILITY_CHANGED)); return this; } @@ -415,16 +427,11 @@ abstract class AbstractChannelHandlerContext extends DefaultAttributeMap if (executor.inEventLoop()) { next.invokeChannelWritabilityChanged(); } else { - Runnable task = next.invokeChannelWritableStateChangedTask; - if (task == null) { - next.invokeChannelWritableStateChangedTask = task = new Runnable() { - @Override - public void run() { - next.invokeChannelWritabilityChanged(); - } - }; + Tasks tasks = next.invokeTasks; + if (tasks == null) { + next.invokeTasks = tasks = new Tasks(next); } - executor.execute(task); + executor.execute(tasks.invokeChannelWritableStateChangedTask); } } @@ -472,15 +479,13 @@ abstract class AbstractChannelHandlerContext extends DefaultAttributeMap @Override public ChannelFuture bind(final SocketAddress localAddress, final ChannelPromise promise) { - if (localAddress == null) { - throw new NullPointerException("localAddress"); - } + ObjectUtil.checkNotNull(localAddress, "localAddress"); if (isNotValidPromise(promise, false)) { // cancelled return promise; } - final AbstractChannelHandlerContext next = findContextOutbound(); + final AbstractChannelHandlerContext next = findContextOutbound(MASK_BIND); EventExecutor executor = next.executor(); if (executor.inEventLoop()) { next.invokeBind(localAddress, promise); @@ -490,7 +495,7 @@ abstract class AbstractChannelHandlerContext extends DefaultAttributeMap public void run() { next.invokeBind(localAddress, promise); } - }, promise, null); + }, promise, null, false); } return promise; } @@ -515,16 +520,14 @@ abstract class AbstractChannelHandlerContext extends DefaultAttributeMap @Override public ChannelFuture connect( final SocketAddress remoteAddress, final SocketAddress localAddress, final ChannelPromise promise) { + ObjectUtil.checkNotNull(remoteAddress, "remoteAddress"); - if (remoteAddress == null) { - throw new NullPointerException("remoteAddress"); - } if (isNotValidPromise(promise, false)) { // cancelled return promise; } - final AbstractChannelHandlerContext next = findContextOutbound(); + final AbstractChannelHandlerContext next = findContextOutbound(MASK_CONNECT); EventExecutor executor = next.executor(); if (executor.inEventLoop()) { next.invokeConnect(remoteAddress, localAddress, promise); @@ -534,7 +537,7 @@ abstract class AbstractChannelHandlerContext extends DefaultAttributeMap public void run() { next.invokeConnect(remoteAddress, localAddress, promise); } - }, promise, null); + }, promise, null, false); } return promise; } @@ -553,32 +556,27 @@ abstract class AbstractChannelHandlerContext extends DefaultAttributeMap @Override public ChannelFuture disconnect(final ChannelPromise promise) { + if (!channel().metadata().hasDisconnect()) { + // Translate disconnect to close if the channel has no notion of disconnect-reconnect. + // So far, UDP/IP is the only transport that has such behavior. + return close(promise); + } if (isNotValidPromise(promise, false)) { // cancelled return promise; } - final AbstractChannelHandlerContext next = findContextOutbound(); + final AbstractChannelHandlerContext next = findContextOutbound(MASK_DISCONNECT); EventExecutor executor = next.executor(); if (executor.inEventLoop()) { - // Translate disconnect to close if the channel has no notion of disconnect-reconnect. - // So far, UDP/IP is the only transport that has such behavior. - if (!channel().metadata().hasDisconnect()) { - next.invokeClose(promise); - } else { - next.invokeDisconnect(promise); - } + next.invokeDisconnect(promise); } else { safeExecute(executor, new Runnable() { @Override public void run() { - if (!channel().metadata().hasDisconnect()) { - next.invokeClose(promise); - } else { - next.invokeDisconnect(promise); - } + next.invokeDisconnect(promise); } - }, promise, null); + }, promise, null, false); } return promise; } @@ -602,7 +600,7 @@ abstract class AbstractChannelHandlerContext extends DefaultAttributeMap return promise; } - final AbstractChannelHandlerContext next = findContextOutbound(); + final AbstractChannelHandlerContext next = findContextOutbound(MASK_CLOSE); EventExecutor executor = next.executor(); if (executor.inEventLoop()) { next.invokeClose(promise); @@ -612,7 +610,7 @@ abstract class AbstractChannelHandlerContext extends DefaultAttributeMap public void run() { next.invokeClose(promise); } - }, promise, null); + }, promise, null, false); } return promise; @@ -637,7 +635,7 @@ abstract class AbstractChannelHandlerContext extends DefaultAttributeMap return promise; } - final AbstractChannelHandlerContext next = findContextOutbound(); + final AbstractChannelHandlerContext next = findContextOutbound(MASK_DEREGISTER); EventExecutor executor = next.executor(); if (executor.inEventLoop()) { next.invokeDeregister(promise); @@ -647,7 +645,7 @@ abstract class AbstractChannelHandlerContext extends DefaultAttributeMap public void run() { next.invokeDeregister(promise); } - }, promise, null); + }, promise, null, false); } return promise; @@ -667,21 +665,16 @@ abstract class AbstractChannelHandlerContext extends DefaultAttributeMap @Override public ChannelHandlerContext read() { - final AbstractChannelHandlerContext next = findContextOutbound(); + final AbstractChannelHandlerContext next = findContextOutbound(MASK_READ); EventExecutor executor = next.executor(); if (executor.inEventLoop()) { next.invokeRead(); } else { - Runnable task = next.invokeReadTask; - if (task == null) { - next.invokeReadTask = task = new Runnable() { - @Override - public void run() { - next.invokeRead(); - } - }; + Tasks tasks = next.invokeTasks; + if (tasks == null) { + next.invokeTasks = tasks = new Tasks(next); } - executor.execute(task); + executor.execute(tasks.invokeReadTask); } return this; @@ -706,26 +699,12 @@ abstract class AbstractChannelHandlerContext extends DefaultAttributeMap @Override public ChannelFuture write(final Object msg, final ChannelPromise promise) { - if (msg == null) { - throw new NullPointerException("msg"); - } - - try { - if (isNotValidPromise(promise, true)) { - ReferenceCountUtil.release(msg); - // cancelled - return promise; - } - } catch (RuntimeException e) { - ReferenceCountUtil.release(msg); - throw e; - } write(msg, false, promise); return promise; } - private void invokeWrite(Object msg, ChannelPromise promise) { + void invokeWrite(Object msg, ChannelPromise promise) { if (invokeHandler()) { invokeWrite0(msg, promise); } else { @@ -743,21 +722,16 @@ abstract class AbstractChannelHandlerContext extends DefaultAttributeMap @Override public ChannelHandlerContext flush() { - final AbstractChannelHandlerContext next = findContextOutbound(); + final AbstractChannelHandlerContext next = findContextOutbound(MASK_FLUSH); EventExecutor executor = next.executor(); if (executor.inEventLoop()) { next.invokeFlush(); } else { - Runnable task = next.invokeFlushTask; - if (task == null) { - next.invokeFlushTask = task = new Runnable() { - @Override - public void run() { - next.invokeFlush(); - } - }; + Tasks tasks = next.invokeTasks; + if (tasks == null) { + next.invokeTasks = tasks = new Tasks(next); } - safeExecute(executor, task, channel().voidPromise(), null); + safeExecute(executor, tasks.invokeFlushTask, channel().voidPromise(), null, false); } return this; @@ -781,22 +755,11 @@ abstract class AbstractChannelHandlerContext extends DefaultAttributeMap @Override public ChannelFuture writeAndFlush(Object msg, ChannelPromise promise) { - if (msg == null) { - throw new NullPointerException("msg"); - } - - if (isNotValidPromise(promise, true)) { - ReferenceCountUtil.release(msg); - // cancelled - return promise; - } - write(msg, true, promise); - return promise; } - private void invokeWriteAndFlush(Object msg, ChannelPromise promise) { + void invokeWriteAndFlush(Object msg, ChannelPromise promise) { if (invokeHandler()) { invokeWrite0(msg, promise); invokeFlush0(); @@ -806,7 +769,20 @@ abstract class AbstractChannelHandlerContext extends DefaultAttributeMap } private void write(Object msg, boolean flush, ChannelPromise promise) { - AbstractChannelHandlerContext next = findContextOutbound(); + ObjectUtil.checkNotNull(msg, "msg"); + try { + if (isNotValidPromise(promise, true)) { + ReferenceCountUtil.release(msg); + // cancelled + return; + } + } catch (RuntimeException e) { + ReferenceCountUtil.release(msg); + throw e; + } + + final AbstractChannelHandlerContext next = findContextOutbound(flush ? + (MASK_WRITE | MASK_FLUSH) : MASK_WRITE); final Object m = pipeline.touch(msg, next); EventExecutor executor = next.executor(); if (executor.inEventLoop()) { @@ -816,14 +792,9 @@ abstract class AbstractChannelHandlerContext extends DefaultAttributeMap next.invokeWrite(m, promise); } } else { - final AbstractWriteTask task; - if (flush) { - task = WriteAndFlushTask.newInstance(next, m, promise); - } else { - task = WriteTask.newInstance(next, m, promise); - } - if (!safeExecute(executor, task, promise, m)) { - // We failed to submit the AbstractWriteTask. We need to cancel it so we decrement the pending bytes + final WriteTask task = WriteTask.newInstance(next, m, promise, flush); + if (!safeExecute(executor, task, promise, m, !flush)) { + // We failed to submit the WriteTask. We need to cancel it so we decrement the pending bytes // and put it back in the Recycler for re-use later. // // See https://github.com/netty/netty/issues/8343. @@ -901,9 +872,7 @@ abstract class AbstractChannelHandlerContext extends DefaultAttributeMap } private boolean isNotValidPromise(ChannelPromise promise, boolean allowVoidPromise) { - if (promise == null) { - throw new NullPointerException("promise"); - } + ObjectUtil.checkNotNull(promise, "promise"); if (promise.isDone()) { // Check if the promise was cancelled and if so signal that the processing of the operation @@ -937,22 +906,35 @@ abstract class AbstractChannelHandlerContext extends DefaultAttributeMap return false; } - private AbstractChannelHandlerContext findContextInbound() { + private AbstractChannelHandlerContext findContextInbound(int mask) { AbstractChannelHandlerContext ctx = this; + EventExecutor currentExecutor = executor(); do { ctx = ctx.next; - } while (!ctx.inbound); + } while (skipContext(ctx, currentExecutor, mask, MASK_ONLY_INBOUND)); return ctx; } - private AbstractChannelHandlerContext findContextOutbound() { + private AbstractChannelHandlerContext findContextOutbound(int mask) { AbstractChannelHandlerContext ctx = this; + EventExecutor currentExecutor = executor(); do { ctx = ctx.prev; - } while (!ctx.outbound); + } while (skipContext(ctx, currentExecutor, mask, MASK_ONLY_OUTBOUND)); return ctx; } + private static boolean skipContext( + AbstractChannelHandlerContext ctx, EventExecutor currentExecutor, int mask, int onlyMask) { + // Ensure we correctly handle MASK_EXCEPTION_CAUGHT which is not included in the MASK_EXCEPTION_CAUGHT + return (ctx.executionMask & (onlyMask | mask)) == 0 || + // We can only skip if the EventExecutor is the same as otherwise we need to ensure we offload + // everything to preserve ordering. + // + // See https://github.com/netty/netty/issues/10067 + (ctx.executor() == currentExecutor && (ctx.executionMask & mask) == 0); + } + @Override public ChannelPromise voidPromise() { return channel().voidPromise(); @@ -1031,9 +1013,14 @@ abstract class AbstractChannelHandlerContext extends DefaultAttributeMap return channel().hasAttr(key); } - private static boolean safeExecute(EventExecutor executor, Runnable runnable, ChannelPromise promise, Object msg) { + private static boolean safeExecute(EventExecutor executor, Runnable runnable, + ChannelPromise promise, Object msg, boolean lazy) { try { - executor.execute(runnable); + if (lazy && executor instanceof AbstractEventExecutor) { + ((AbstractEventExecutor) executor).lazyExecute(runnable); + } else { + executor.execute(runnable); + } return true; } catch (Throwable cause) { try { @@ -1057,28 +1044,41 @@ abstract class AbstractChannelHandlerContext extends DefaultAttributeMap return StringUtil.simpleClassName(ChannelHandlerContext.class) + '(' + name + ", " + channel() + ')'; } - abstract static class AbstractWriteTask implements Runnable { + static final class WriteTask implements Runnable { + private static final ObjectPool<WriteTask> RECYCLER = ObjectPool.newPool(new ObjectCreator<WriteTask>() { + @Override + public WriteTask newObject(Handle<WriteTask> handle) { + return new WriteTask(handle); + } + }); + + static WriteTask newInstance(AbstractChannelHandlerContext ctx, + Object msg, ChannelPromise promise, boolean flush) { + WriteTask task = RECYCLER.get(); + init(task, ctx, msg, promise, flush); + return task; + } private static final boolean ESTIMATE_TASK_SIZE_ON_SUBMIT = SystemPropertyUtil.getBoolean("io.netty.transport.estimateSizeOnSubmit", true); - // Assuming a 64-bit JVM, 16 bytes object header, 3 reference fields and one int field, plus alignment + // Assuming compressed oops, 12 bytes obj header, 4 ref fields and one int field private static final int WRITE_TASK_OVERHEAD = - SystemPropertyUtil.getInt("io.netty.transport.writeTaskSizeOverhead", 48); + SystemPropertyUtil.getInt("io.netty.transport.writeTaskSizeOverhead", 32); - private final Recycler.Handle<AbstractWriteTask> handle; + private final Handle<WriteTask> handle; private AbstractChannelHandlerContext ctx; private Object msg; private ChannelPromise promise; - private int size; + private int size; // sign bit controls flush @SuppressWarnings("unchecked") - private AbstractWriteTask(Recycler.Handle<? extends AbstractWriteTask> handle) { - this.handle = (Recycler.Handle<AbstractWriteTask>) handle; + private WriteTask(Handle<? extends WriteTask> handle) { + this.handle = (Handle<WriteTask>) handle; } - protected static void init(AbstractWriteTask task, AbstractChannelHandlerContext ctx, - Object msg, ChannelPromise promise) { + protected static void init(WriteTask task, AbstractChannelHandlerContext ctx, + Object msg, ChannelPromise promise, boolean flush) { task.ctx = ctx; task.msg = msg; task.promise = promise; @@ -1089,13 +1089,20 @@ abstract class AbstractChannelHandlerContext extends DefaultAttributeMap } else { task.size = 0; } + if (flush) { + task.size |= Integer.MIN_VALUE; + } } @Override - public final void run() { + public void run() { try { decrementPendingOutboundBytes(); - write(ctx, msg, promise); + if (size >= 0) { + ctx.invokeWrite(msg, promise); + } else { + ctx.invokeWriteAndFlush(msg, promise); + } } finally { recycle(); } @@ -1111,7 +1118,7 @@ abstract class AbstractChannelHandlerContext extends DefaultAttributeMap private void decrementPendingOutboundBytes() { if (ESTIMATE_TASK_SIZE_ON_SUBMIT) { - ctx.pipeline.decrementPendingOutboundBytes(size); + ctx.pipeline.decrementPendingOutboundBytes(size & Integer.MAX_VALUE); } } @@ -1122,57 +1129,37 @@ abstract class AbstractChannelHandlerContext extends DefaultAttributeMap promise = null; handle.recycle(this); } - - protected void write(AbstractChannelHandlerContext ctx, Object msg, ChannelPromise promise) { - ctx.invokeWrite(msg, promise); - } } - static final class WriteTask extends AbstractWriteTask implements SingleThreadEventLoop.NonWakeupRunnable { - - private static final Recycler<WriteTask> RECYCLER = new Recycler<WriteTask>() { + private static final class Tasks { + private final AbstractChannelHandlerContext next; + private final Runnable invokeChannelReadCompleteTask = new Runnable() { @Override - protected WriteTask newObject(Handle<WriteTask> handle) { - return new WriteTask(handle); + public void run() { + next.invokeChannelReadComplete(); } }; - - static WriteTask newInstance( - AbstractChannelHandlerContext ctx, Object msg, ChannelPromise promise) { - WriteTask task = RECYCLER.get(); - init(task, ctx, msg, promise); - return task; - } - - private WriteTask(Recycler.Handle<WriteTask> handle) { - super(handle); - } - } - - static final class WriteAndFlushTask extends AbstractWriteTask { - - private static final Recycler<WriteAndFlushTask> RECYCLER = new Recycler<WriteAndFlushTask>() { + private final Runnable invokeReadTask = new Runnable() { @Override - protected WriteAndFlushTask newObject(Handle<WriteAndFlushTask> handle) { - return new WriteAndFlushTask(handle); + public void run() { + next.invokeRead(); + } + }; + private final Runnable invokeChannelWritableStateChangedTask = new Runnable() { + @Override + public void run() { + next.invokeChannelWritabilityChanged(); + } + }; + private final Runnable invokeFlushTask = new Runnable() { + @Override + public void run() { + next.invokeFlush(); } }; - static WriteAndFlushTask newInstance( - AbstractChannelHandlerContext ctx, Object msg, ChannelPromise promise) { - WriteAndFlushTask task = RECYCLER.get(); - init(task, ctx, msg, promise); - return task; - } - - private WriteAndFlushTask(Recycler.Handle<WriteAndFlushTask> handle) { - super(handle); - } - - @Override - public void write(AbstractChannelHandlerContext ctx, Object msg, ChannelPromise promise) { - super.write(ctx, msg, promise); - ctx.invokeFlush(); + Tasks(AbstractChannelHandlerContext next) { + this.next = next; } } } diff --git a/transport/src/main/java/io/netty/channel/AdaptiveRecvByteBufAllocator.java b/transport/src/main/java/io/netty/channel/AdaptiveRecvByteBufAllocator.java index a2db615..d2456dd 100644 --- a/transport/src/main/java/io/netty/channel/AdaptiveRecvByteBufAllocator.java +++ b/transport/src/main/java/io/netty/channel/AdaptiveRecvByteBufAllocator.java @@ -18,6 +18,7 @@ package io.netty.channel; import java.util.ArrayList; import java.util.List; +import static io.netty.util.internal.ObjectUtil.checkPositive; import static java.lang.Math.max; import static java.lang.Math.min; @@ -95,7 +96,7 @@ public class AdaptiveRecvByteBufAllocator extends DefaultMaxMessagesRecvByteBufA private int nextReceiveBufferSize; private boolean decreaseNow; - public HandleImpl(int minIndex, int maxIndex, int initial) { + HandleImpl(int minIndex, int maxIndex, int initial) { this.minIndex = minIndex; this.maxIndex = maxIndex; @@ -121,7 +122,7 @@ public class AdaptiveRecvByteBufAllocator extends DefaultMaxMessagesRecvByteBufA } private void record(int actualReadBytes) { - if (actualReadBytes <= SIZE_TABLE[max(0, index - INDEX_DECREMENT - 1)]) { + if (actualReadBytes <= SIZE_TABLE[max(0, index - INDEX_DECREMENT)]) { if (decreaseNow) { index = max(index - INDEX_DECREMENT, minIndex); nextReceiveBufferSize = SIZE_TABLE[index]; @@ -163,9 +164,7 @@ public class AdaptiveRecvByteBufAllocator extends DefaultMaxMessagesRecvByteBufA * @param maximum the inclusive upper bound of the expected buffer size */ public AdaptiveRecvByteBufAllocator(int minimum, int initial, int maximum) { - if (minimum <= 0) { - throw new IllegalArgumentException("minimum: " + minimum); - } + checkPositive(minimum, "minimum"); if (initial < minimum) { throw new IllegalArgumentException("initial: " + initial); } diff --git a/transport/src/main/java/io/netty/channel/ChannelDuplexHandler.java b/transport/src/main/java/io/netty/channel/ChannelDuplexHandler.java index 07c6484..eac4645 100644 --- a/transport/src/main/java/io/netty/channel/ChannelDuplexHandler.java +++ b/transport/src/main/java/io/netty/channel/ChannelDuplexHandler.java @@ -15,6 +15,8 @@ */ package io.netty.channel; +import io.netty.channel.ChannelHandlerMask.Skip; + import java.net.SocketAddress; /** @@ -32,6 +34,7 @@ public class ChannelDuplexHandler extends ChannelInboundHandlerAdapter implement * * Sub-classes may override this method to change behavior. */ + @Skip @Override public void bind(ChannelHandlerContext ctx, SocketAddress localAddress, ChannelPromise promise) throws Exception { @@ -44,6 +47,7 @@ public class ChannelDuplexHandler extends ChannelInboundHandlerAdapter implement * * Sub-classes may override this method to change behavior. */ + @Skip @Override public void connect(ChannelHandlerContext ctx, SocketAddress remoteAddress, SocketAddress localAddress, ChannelPromise promise) throws Exception { @@ -56,6 +60,7 @@ public class ChannelDuplexHandler extends ChannelInboundHandlerAdapter implement * * Sub-classes may override this method to change behavior. */ + @Skip @Override public void disconnect(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception { @@ -68,6 +73,7 @@ public class ChannelDuplexHandler extends ChannelInboundHandlerAdapter implement * * Sub-classes may override this method to change behavior. */ + @Skip @Override public void close(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception { ctx.close(promise); @@ -79,6 +85,7 @@ public class ChannelDuplexHandler extends ChannelInboundHandlerAdapter implement * * Sub-classes may override this method to change behavior. */ + @Skip @Override public void deregister(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception { ctx.deregister(promise); @@ -90,6 +97,7 @@ public class ChannelDuplexHandler extends ChannelInboundHandlerAdapter implement * * Sub-classes may override this method to change behavior. */ + @Skip @Override public void read(ChannelHandlerContext ctx) throws Exception { ctx.read(); @@ -101,6 +109,7 @@ public class ChannelDuplexHandler extends ChannelInboundHandlerAdapter implement * * Sub-classes may override this method to change behavior. */ + @Skip @Override public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { ctx.write(msg, promise); @@ -112,6 +121,7 @@ public class ChannelDuplexHandler extends ChannelInboundHandlerAdapter implement * * Sub-classes may override this method to change behavior. */ + @Skip @Override public void flush(ChannelHandlerContext ctx) throws Exception { ctx.flush(); diff --git a/transport/src/main/java/io/netty/channel/ChannelException.java b/transport/src/main/java/io/netty/channel/ChannelException.java index 3aa89ff..32f4642 100644 --- a/transport/src/main/java/io/netty/channel/ChannelException.java +++ b/transport/src/main/java/io/netty/channel/ChannelException.java @@ -15,6 +15,10 @@ */ package io.netty.channel; +import io.netty.util.internal.PlatformDependent; +import io.netty.util.internal.SuppressJava6Requirement; +import io.netty.util.internal.UnstableApi; + /** * A {@link RuntimeException} which is thrown when an I/O operation fails. */ @@ -48,4 +52,19 @@ public class ChannelException extends RuntimeException { public ChannelException(Throwable cause) { super(cause); } + + @UnstableApi + @SuppressJava6Requirement(reason = "uses Java 7+ RuntimeException.<init>(String, Throwable, boolean, boolean)" + + " but is guarded by version checks") + protected ChannelException(String message, Throwable cause, boolean shared) { + super(message, cause, false, true); + assert shared; + } + + static ChannelException newStatic(String message, Throwable cause) { + if (PlatformDependent.javaVersion() >= 7) { + return new ChannelException(message, cause, true); + } + return new ChannelException(message, cause); + } } diff --git a/transport/src/main/java/io/netty/channel/ChannelFlushPromiseNotifier.java b/transport/src/main/java/io/netty/channel/ChannelFlushPromiseNotifier.java index 26594a3..eb8f97c 100644 --- a/transport/src/main/java/io/netty/channel/ChannelFlushPromiseNotifier.java +++ b/transport/src/main/java/io/netty/channel/ChannelFlushPromiseNotifier.java @@ -15,6 +15,10 @@ */ package io.netty.channel; +import io.netty.util.internal.ObjectUtil; + +import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero; + import java.util.ArrayDeque; import java.util.Queue; @@ -61,12 +65,8 @@ public final class ChannelFlushPromiseNotifier { * {@code pendingDataSize} was reached. */ public ChannelFlushPromiseNotifier add(ChannelPromise promise, long pendingDataSize) { - if (promise == null) { - throw new NullPointerException("promise"); - } - if (pendingDataSize < 0) { - throw new IllegalArgumentException("pendingDataSize must be >= 0 but was " + pendingDataSize); - } + ObjectUtil.checkNotNull(promise, "promise"); + checkPositiveOrZero(pendingDataSize, "pendingDataSize"); long checkpoint = writeCounter + pendingDataSize; if (promise instanceof FlushCheckpoint) { FlushCheckpoint cp = (FlushCheckpoint) promise; @@ -81,9 +81,7 @@ public final class ChannelFlushPromiseNotifier { * Increase the current write counter by the given delta */ public ChannelFlushPromiseNotifier increaseWriteCounter(long delta) { - if (delta < 0) { - throw new IllegalArgumentException("delta must be >= 0 but was " + delta); - } + checkPositiveOrZero(delta, "delta"); writeCounter += delta; return this; } diff --git a/transport/src/main/java/io/netty/channel/ChannelHandler.java b/transport/src/main/java/io/netty/channel/ChannelHandler.java index f940108..3879056 100644 --- a/transport/src/main/java/io/netty/channel/ChannelHandler.java +++ b/transport/src/main/java/io/netty/channel/ChannelHandler.java @@ -191,7 +191,8 @@ public interface ChannelHandler { /** * Gets called if a {@link Throwable} was thrown. * - * @deprecated is part of {@link ChannelInboundHandler} + * @deprecated if you want to handle this event you should implement {@link ChannelInboundHandler} and + * implement the method there. */ @Deprecated void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception; diff --git a/transport/src/main/java/io/netty/channel/ChannelHandlerAdapter.java b/transport/src/main/java/io/netty/channel/ChannelHandlerAdapter.java index aadc691..2041ebd 100644 --- a/transport/src/main/java/io/netty/channel/ChannelHandlerAdapter.java +++ b/transport/src/main/java/io/netty/channel/ChannelHandlerAdapter.java @@ -16,6 +16,7 @@ package io.netty.channel; +import io.netty.channel.ChannelHandlerMask.Skip; import io.netty.util.internal.InternalThreadLocalMap; import java.util.Map; @@ -81,8 +82,12 @@ public abstract class ChannelHandlerAdapter implements ChannelHandler { * to the next {@link ChannelHandler} in the {@link ChannelPipeline}. * * Sub-classes may override this method to change behavior. + * + * @deprecated is part of {@link ChannelInboundHandler} */ + @Skip @Override + @Deprecated public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { ctx.fireExceptionCaught(cause); } diff --git a/transport/src/main/java/io/netty/channel/ChannelHandlerMask.java b/transport/src/main/java/io/netty/channel/ChannelHandlerMask.java new file mode 100644 index 0000000..d307508 --- /dev/null +++ b/transport/src/main/java/io/netty/channel/ChannelHandlerMask.java @@ -0,0 +1,205 @@ +/* + * Copyright 2019 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.channel; + +import io.netty.util.concurrent.FastThreadLocal; +import io.netty.util.internal.PlatformDependent; + +import io.netty.util.internal.logging.InternalLogger; +import io.netty.util.internal.logging.InternalLoggerFactory; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.Method; +import java.net.SocketAddress; +import java.security.AccessController; +import java.security.PrivilegedExceptionAction; +import java.util.Map; +import java.util.WeakHashMap; + +final class ChannelHandlerMask { + private static final InternalLogger logger = InternalLoggerFactory.getInstance(ChannelHandlerMask.class); + + // Using to mask which methods must be called for a ChannelHandler. + static final int MASK_EXCEPTION_CAUGHT = 1; + static final int MASK_CHANNEL_REGISTERED = 1 << 1; + static final int MASK_CHANNEL_UNREGISTERED = 1 << 2; + static final int MASK_CHANNEL_ACTIVE = 1 << 3; + static final int MASK_CHANNEL_INACTIVE = 1 << 4; + static final int MASK_CHANNEL_READ = 1 << 5; + static final int MASK_CHANNEL_READ_COMPLETE = 1 << 6; + static final int MASK_USER_EVENT_TRIGGERED = 1 << 7; + static final int MASK_CHANNEL_WRITABILITY_CHANGED = 1 << 8; + static final int MASK_BIND = 1 << 9; + static final int MASK_CONNECT = 1 << 10; + static final int MASK_DISCONNECT = 1 << 11; + static final int MASK_CLOSE = 1 << 12; + static final int MASK_DEREGISTER = 1 << 13; + static final int MASK_READ = 1 << 14; + static final int MASK_WRITE = 1 << 15; + static final int MASK_FLUSH = 1 << 16; + + static final int MASK_ONLY_INBOUND = MASK_CHANNEL_REGISTERED | + MASK_CHANNEL_UNREGISTERED | MASK_CHANNEL_ACTIVE | MASK_CHANNEL_INACTIVE | MASK_CHANNEL_READ | + MASK_CHANNEL_READ_COMPLETE | MASK_USER_EVENT_TRIGGERED | MASK_CHANNEL_WRITABILITY_CHANGED; + private static final int MASK_ALL_INBOUND = MASK_EXCEPTION_CAUGHT | MASK_ONLY_INBOUND; + static final int MASK_ONLY_OUTBOUND = MASK_BIND | MASK_CONNECT | MASK_DISCONNECT | + MASK_CLOSE | MASK_DEREGISTER | MASK_READ | MASK_WRITE | MASK_FLUSH; + private static final int MASK_ALL_OUTBOUND = MASK_EXCEPTION_CAUGHT | MASK_ONLY_OUTBOUND; + + private static final FastThreadLocal<Map<Class<? extends ChannelHandler>, Integer>> MASKS = + new FastThreadLocal<Map<Class<? extends ChannelHandler>, Integer>>() { + @Override + protected Map<Class<? extends ChannelHandler>, Integer> initialValue() { + return new WeakHashMap<Class<? extends ChannelHandler>, Integer>(32); + } + }; + + /** + * Return the {@code executionMask}. + */ + static int mask(Class<? extends ChannelHandler> clazz) { + // Try to obtain the mask from the cache first. If this fails calculate it and put it in the cache for fast + // lookup in the future. + Map<Class<? extends ChannelHandler>, Integer> cache = MASKS.get(); + Integer mask = cache.get(clazz); + if (mask == null) { + mask = mask0(clazz); + cache.put(clazz, mask); + } + return mask; + } + + /** + * Calculate the {@code executionMask}. + */ + private static int mask0(Class<? extends ChannelHandler> handlerType) { + int mask = MASK_EXCEPTION_CAUGHT; + try { + if (ChannelInboundHandler.class.isAssignableFrom(handlerType)) { + mask |= MASK_ALL_INBOUND; + + if (isSkippable(handlerType, "channelRegistered", ChannelHandlerContext.class)) { + mask &= ~MASK_CHANNEL_REGISTERED; + } + if (isSkippable(handlerType, "channelUnregistered", ChannelHandlerContext.class)) { + mask &= ~MASK_CHANNEL_UNREGISTERED; + } + if (isSkippable(handlerType, "channelActive", ChannelHandlerContext.class)) { + mask &= ~MASK_CHANNEL_ACTIVE; + } + if (isSkippable(handlerType, "channelInactive", ChannelHandlerContext.class)) { + mask &= ~MASK_CHANNEL_INACTIVE; + } + if (isSkippable(handlerType, "channelRead", ChannelHandlerContext.class, Object.class)) { + mask &= ~MASK_CHANNEL_READ; + } + if (isSkippable(handlerType, "channelReadComplete", ChannelHandlerContext.class)) { + mask &= ~MASK_CHANNEL_READ_COMPLETE; + } + if (isSkippable(handlerType, "channelWritabilityChanged", ChannelHandlerContext.class)) { + mask &= ~MASK_CHANNEL_WRITABILITY_CHANGED; + } + if (isSkippable(handlerType, "userEventTriggered", ChannelHandlerContext.class, Object.class)) { + mask &= ~MASK_USER_EVENT_TRIGGERED; + } + } + + if (ChannelOutboundHandler.class.isAssignableFrom(handlerType)) { + mask |= MASK_ALL_OUTBOUND; + + if (isSkippable(handlerType, "bind", ChannelHandlerContext.class, + SocketAddress.class, ChannelPromise.class)) { + mask &= ~MASK_BIND; + } + if (isSkippable(handlerType, "connect", ChannelHandlerContext.class, SocketAddress.class, + SocketAddress.class, ChannelPromise.class)) { + mask &= ~MASK_CONNECT; + } + if (isSkippable(handlerType, "disconnect", ChannelHandlerContext.class, ChannelPromise.class)) { + mask &= ~MASK_DISCONNECT; + } + if (isSkippable(handlerType, "close", ChannelHandlerContext.class, ChannelPromise.class)) { + mask &= ~MASK_CLOSE; + } + if (isSkippable(handlerType, "deregister", ChannelHandlerContext.class, ChannelPromise.class)) { + mask &= ~MASK_DEREGISTER; + } + if (isSkippable(handlerType, "read", ChannelHandlerContext.class)) { + mask &= ~MASK_READ; + } + if (isSkippable(handlerType, "write", ChannelHandlerContext.class, + Object.class, ChannelPromise.class)) { + mask &= ~MASK_WRITE; + } + if (isSkippable(handlerType, "flush", ChannelHandlerContext.class)) { + mask &= ~MASK_FLUSH; + } + } + + if (isSkippable(handlerType, "exceptionCaught", ChannelHandlerContext.class, Throwable.class)) { + mask &= ~MASK_EXCEPTION_CAUGHT; + } + } catch (Exception e) { + // Should never reach here. + PlatformDependent.throwException(e); + } + + return mask; + } + + @SuppressWarnings("rawtypes") + private static boolean isSkippable( + final Class<?> handlerType, final String methodName, final Class<?>... paramTypes) throws Exception { + return AccessController.doPrivileged(new PrivilegedExceptionAction<Boolean>() { + @Override + public Boolean run() throws Exception { + Method m; + try { + m = handlerType.getMethod(methodName, paramTypes); + } catch (NoSuchMethodException e) { + if (logger.isDebugEnabled()) { + logger.debug( + "Class {} missing method {}, assume we can not skip execution", handlerType, methodName, e); + } + return false; + } + return m != null && m.isAnnotationPresent(Skip.class); + } + }); + } + + private ChannelHandlerMask() { } + + /** + * Indicates that the annotated event handler method in {@link ChannelHandler} will not be invoked by + * {@link ChannelPipeline} and so <strong>MUST</strong> only be used when the {@link ChannelHandler} + * method does nothing except forward to the next {@link ChannelHandler} in the pipeline. + * <p> + * Note that this annotation is not {@linkplain Inherited inherited}. If a user overrides a method annotated with + * {@link Skip}, it will not be skipped anymore. Similarly, the user can override a method not annotated with + * {@link Skip} and simply pass the event through to the next handler, which reverses the behavior of the + * supertype. + * </p> + */ + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + @interface Skip { + // no value + } +} diff --git a/transport/src/main/java/io/netty/channel/ChannelInboundHandlerAdapter.java b/transport/src/main/java/io/netty/channel/ChannelInboundHandlerAdapter.java index d0a1d2d..f9a8884 100644 --- a/transport/src/main/java/io/netty/channel/ChannelInboundHandlerAdapter.java +++ b/transport/src/main/java/io/netty/channel/ChannelInboundHandlerAdapter.java @@ -15,6 +15,8 @@ */ package io.netty.channel; +import io.netty.channel.ChannelHandlerMask.Skip; + /** * Abstract base class for {@link ChannelInboundHandler} implementations which provide * implementations of all of their methods. @@ -37,6 +39,7 @@ public class ChannelInboundHandlerAdapter extends ChannelHandlerAdapter implemen * * Sub-classes may override this method to change behavior. */ + @Skip @Override public void channelRegistered(ChannelHandlerContext ctx) throws Exception { ctx.fireChannelRegistered(); @@ -48,6 +51,7 @@ public class ChannelInboundHandlerAdapter extends ChannelHandlerAdapter implemen * * Sub-classes may override this method to change behavior. */ + @Skip @Override public void channelUnregistered(ChannelHandlerContext ctx) throws Exception { ctx.fireChannelUnregistered(); @@ -59,6 +63,7 @@ public class ChannelInboundHandlerAdapter extends ChannelHandlerAdapter implemen * * Sub-classes may override this method to change behavior. */ + @Skip @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { ctx.fireChannelActive(); @@ -70,6 +75,7 @@ public class ChannelInboundHandlerAdapter extends ChannelHandlerAdapter implemen * * Sub-classes may override this method to change behavior. */ + @Skip @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { ctx.fireChannelInactive(); @@ -81,6 +87,7 @@ public class ChannelInboundHandlerAdapter extends ChannelHandlerAdapter implemen * * Sub-classes may override this method to change behavior. */ + @Skip @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { ctx.fireChannelRead(msg); @@ -92,6 +99,7 @@ public class ChannelInboundHandlerAdapter extends ChannelHandlerAdapter implemen * * Sub-classes may override this method to change behavior. */ + @Skip @Override public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { ctx.fireChannelReadComplete(); @@ -103,6 +111,7 @@ public class ChannelInboundHandlerAdapter extends ChannelHandlerAdapter implemen * * Sub-classes may override this method to change behavior. */ + @Skip @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { ctx.fireUserEventTriggered(evt); @@ -114,6 +123,7 @@ public class ChannelInboundHandlerAdapter extends ChannelHandlerAdapter implemen * * Sub-classes may override this method to change behavior. */ + @Skip @Override public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception { ctx.fireChannelWritabilityChanged(); @@ -125,7 +135,9 @@ public class ChannelInboundHandlerAdapter extends ChannelHandlerAdapter implemen * * Sub-classes may override this method to change behavior. */ + @Skip @Override + @SuppressWarnings("deprecation") public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { ctx.fireExceptionCaught(cause); diff --git a/transport/src/main/java/io/netty/channel/ChannelMetadata.java b/transport/src/main/java/io/netty/channel/ChannelMetadata.java index c77f530..e3b116b 100644 --- a/transport/src/main/java/io/netty/channel/ChannelMetadata.java +++ b/transport/src/main/java/io/netty/channel/ChannelMetadata.java @@ -15,6 +15,8 @@ */ package io.netty.channel; +import static io.netty.util.internal.ObjectUtil.checkPositive; + import java.net.SocketAddress; /** @@ -46,10 +48,7 @@ public final class ChannelMetadata { * set for {@link MaxMessagesRecvByteBufAllocator#maxMessagesPerRead()}. Must be {@code > 0}. */ public ChannelMetadata(boolean hasDisconnect, int defaultMaxMessagesPerRead) { - if (defaultMaxMessagesPerRead <= 0) { - throw new IllegalArgumentException("defaultMaxMessagesPerRead: " + defaultMaxMessagesPerRead + - " (expected > 0)"); - } + checkPositive(defaultMaxMessagesPerRead, "defaultMaxMessagesPerRead"); this.hasDisconnect = hasDisconnect; this.defaultMaxMessagesPerRead = defaultMaxMessagesPerRead; } diff --git a/transport/src/main/java/io/netty/channel/ChannelOption.java b/transport/src/main/java/io/netty/channel/ChannelOption.java index 97bf315..4da4295 100644 --- a/transport/src/main/java/io/netty/channel/ChannelOption.java +++ b/transport/src/main/java/io/netty/channel/ChannelOption.java @@ -18,6 +18,7 @@ package io.netty.channel; import io.netty.buffer.ByteBufAllocator; import io.netty.util.AbstractConstant; import io.netty.util.ConstantPool; +import io.netty.util.internal.ObjectUtil; import java.net.InetAddress; import java.net.NetworkInterface; @@ -65,7 +66,10 @@ public class ChannelOption<T> extends AbstractConstant<ChannelOption<T>> { /** * Creates a new {@link ChannelOption} for the given {@code name} or fail with an * {@link IllegalArgumentException} if a {@link ChannelOption} for the given {@code name} exists. + * + * @deprecated use {@link #valueOf(String)}. */ + @Deprecated @SuppressWarnings("unchecked") public static <T> ChannelOption<T> newInstance(String name) { return (ChannelOption<T>) pool.newInstance(name); @@ -146,8 +150,6 @@ public class ChannelOption<T> extends AbstractConstant<ChannelOption<T>> { * may override this for special checks. */ public void validate(T value) { - if (value == null) { - throw new NullPointerException("value"); - } + ObjectUtil.checkNotNull(value, "value"); } } diff --git a/transport/src/main/java/io/netty/channel/ChannelOutboundBuffer.java b/transport/src/main/java/io/netty/channel/ChannelOutboundBuffer.java index d3a934a..b3202c6 100644 --- a/transport/src/main/java/io/netty/channel/ChannelOutboundBuffer.java +++ b/transport/src/main/java/io/netty/channel/ChannelOutboundBuffer.java @@ -19,11 +19,13 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufHolder; import io.netty.buffer.Unpooled; import io.netty.channel.socket.nio.NioSocketChannel; -import io.netty.util.Recycler; -import io.netty.util.Recycler.Handle; import io.netty.util.ReferenceCountUtil; import io.netty.util.concurrent.FastThreadLocal; import io.netty.util.internal.InternalThreadLocalMap; +import io.netty.util.internal.ObjectPool; +import io.netty.util.internal.ObjectPool.Handle; +import io.netty.util.internal.ObjectPool.ObjectCreator; +import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.PromiseNotificationUtil; import io.netty.util.internal.SystemPropertyUtil; import io.netty.util.internal.logging.InternalLogger; @@ -52,7 +54,7 @@ import static java.lang.Math.min; public final class ChannelOutboundBuffer { // Assuming a 64-bit JVM: // - 16 bytes object header - // - 8 reference fields + // - 6 reference fields // - 2 long fields // - 2 int fields // - 1 boolean field @@ -220,6 +222,18 @@ public final class ChannelOutboundBuffer { return entry.msg; } + /** + * Return the current message flush progress. + * @return {@code 0} if nothing was flushed before for the current message or there is no current message + */ + public long currentProgress() { + Entry entry = flushedEntry; + if (entry == null) { + return 0; + } + return entry.progress; + } + /** * Notify the {@link ChannelPromise} of the current message about writing progress. */ @@ -227,9 +241,9 @@ public final class ChannelOutboundBuffer { Entry e = flushedEntry; assert e != null; ChannelPromise p = e.promise; + long progress = e.progress + amount; + e.progress = progress; if (p instanceof ChannelProgressivePromise) { - long progress = e.progress + amount; - e.progress = progress; ((ChannelProgressivePromise) p).tryProgress(progress, e.total); } } @@ -754,9 +768,7 @@ public final class ChannelOutboundBuffer { * returns {@code false} or there are no more flushed messages to process. */ public void forEachFlushedMessage(MessageProcessor processor) throws Exception { - if (processor == null) { - throw new NullPointerException("processor"); - } + ObjectUtil.checkNotNull(processor, "processor"); Entry entry = flushedEntry; if (entry == null) { @@ -786,12 +798,12 @@ public final class ChannelOutboundBuffer { } static final class Entry { - private static final Recycler<Entry> RECYCLER = new Recycler<Entry>() { + private static final ObjectPool<Entry> RECYCLER = ObjectPool.newPool(new ObjectCreator<Entry>() { @Override - protected Entry newObject(Handle<Entry> handle) { + public Entry newObject(Handle<Entry> handle) { return new Entry(handle); } - }; + }); private final Handle<Entry> handle; Entry next; diff --git a/transport/src/main/java/io/netty/channel/ChannelOutboundHandlerAdapter.java b/transport/src/main/java/io/netty/channel/ChannelOutboundHandlerAdapter.java index fa96892..c68bfdb 100644 --- a/transport/src/main/java/io/netty/channel/ChannelOutboundHandlerAdapter.java +++ b/transport/src/main/java/io/netty/channel/ChannelOutboundHandlerAdapter.java @@ -15,6 +15,8 @@ */ package io.netty.channel; +import io.netty.channel.ChannelHandlerMask.Skip; + import java.net.SocketAddress; /** @@ -29,6 +31,7 @@ public class ChannelOutboundHandlerAdapter extends ChannelHandlerAdapter impleme * * Sub-classes may override this method to change behavior. */ + @Skip @Override public void bind(ChannelHandlerContext ctx, SocketAddress localAddress, ChannelPromise promise) throws Exception { @@ -41,6 +44,7 @@ public class ChannelOutboundHandlerAdapter extends ChannelHandlerAdapter impleme * * Sub-classes may override this method to change behavior. */ + @Skip @Override public void connect(ChannelHandlerContext ctx, SocketAddress remoteAddress, SocketAddress localAddress, ChannelPromise promise) throws Exception { @@ -53,6 +57,7 @@ public class ChannelOutboundHandlerAdapter extends ChannelHandlerAdapter impleme * * Sub-classes may override this method to change behavior. */ + @Skip @Override public void disconnect(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception { @@ -65,6 +70,7 @@ public class ChannelOutboundHandlerAdapter extends ChannelHandlerAdapter impleme * * Sub-classes may override this method to change behavior. */ + @Skip @Override public void close(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception { @@ -77,6 +83,7 @@ public class ChannelOutboundHandlerAdapter extends ChannelHandlerAdapter impleme * * Sub-classes may override this method to change behavior. */ + @Skip @Override public void deregister(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception { ctx.deregister(promise); @@ -88,6 +95,7 @@ public class ChannelOutboundHandlerAdapter extends ChannelHandlerAdapter impleme * * Sub-classes may override this method to change behavior. */ + @Skip @Override public void read(ChannelHandlerContext ctx) throws Exception { ctx.read(); @@ -99,6 +107,7 @@ public class ChannelOutboundHandlerAdapter extends ChannelHandlerAdapter impleme * * Sub-classes may override this method to change behavior. */ + @Skip @Override public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { ctx.write(msg, promise); @@ -110,6 +119,7 @@ public class ChannelOutboundHandlerAdapter extends ChannelHandlerAdapter impleme * * Sub-classes may override this method to change behavior. */ + @Skip @Override public void flush(ChannelHandlerContext ctx) throws Exception { ctx.flush(); diff --git a/transport/src/main/java/io/netty/channel/CoalescingBufferQueue.java b/transport/src/main/java/io/netty/channel/CoalescingBufferQueue.java index fd58360..346e53f 100644 --- a/transport/src/main/java/io/netty/channel/CoalescingBufferQueue.java +++ b/transport/src/main/java/io/netty/channel/CoalescingBufferQueue.java @@ -19,7 +19,6 @@ import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.CompositeByteBuf; import io.netty.buffer.Unpooled; import io.netty.util.internal.ObjectUtil; -import io.netty.util.internal.PlatformDependent; /** * A FIFO queue of bytes where producers add bytes by repeatedly adding {@link ByteBuf} and consumers take bytes in diff --git a/transport/src/main/java/io/netty/channel/CombinedChannelDuplexHandler.java b/transport/src/main/java/io/netty/channel/CombinedChannelDuplexHandler.java index b24ccab..bd0e569 100644 --- a/transport/src/main/java/io/netty/channel/CombinedChannelDuplexHandler.java +++ b/transport/src/main/java/io/netty/channel/CombinedChannelDuplexHandler.java @@ -19,6 +19,7 @@ import io.netty.buffer.ByteBufAllocator; import io.netty.util.Attribute; import io.netty.util.AttributeKey; import io.netty.util.concurrent.EventExecutor; +import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.ThrowableUtil; import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; @@ -78,12 +79,9 @@ public class CombinedChannelDuplexHandler<I extends ChannelInboundHandler, O ext " was constructed with non-default constructor."); } - if (inboundHandler == null) { - throw new NullPointerException("inboundHandler"); - } - if (outboundHandler == null) { - throw new NullPointerException("outboundHandler"); - } + ObjectUtil.checkNotNull(inboundHandler, "inboundHandler"); + ObjectUtil.checkNotNull(outboundHandler, "outboundHandler"); + if (inboundHandler instanceof ChannelOutboundHandler) { throw new IllegalArgumentException( "inboundHandler must not implement " + diff --git a/transport/src/main/java/io/netty/channel/CompleteChannelFuture.java b/transport/src/main/java/io/netty/channel/CompleteChannelFuture.java index 67a86e5..083029e 100644 --- a/transport/src/main/java/io/netty/channel/CompleteChannelFuture.java +++ b/transport/src/main/java/io/netty/channel/CompleteChannelFuture.java @@ -19,6 +19,7 @@ import io.netty.util.concurrent.CompleteFuture; import io.netty.util.concurrent.EventExecutor; import io.netty.util.concurrent.Future; import io.netty.util.concurrent.GenericFutureListener; +import io.netty.util.internal.ObjectUtil; /** * A skeletal {@link ChannelFuture} implementation which represents a @@ -35,10 +36,7 @@ abstract class CompleteChannelFuture extends CompleteFuture<Void> implements Cha */ protected CompleteChannelFuture(Channel channel, EventExecutor executor) { super(executor); - if (channel == null) { - throw new NullPointerException("channel"); - } - this.channel = channel; + this.channel = ObjectUtil.checkNotNull(channel, "channel"); } @Override diff --git a/transport/src/main/java/io/netty/channel/DefaultAddressedEnvelope.java b/transport/src/main/java/io/netty/channel/DefaultAddressedEnvelope.java index 12cdabb..8fb46da 100644 --- a/transport/src/main/java/io/netty/channel/DefaultAddressedEnvelope.java +++ b/transport/src/main/java/io/netty/channel/DefaultAddressedEnvelope.java @@ -18,6 +18,7 @@ package io.netty.channel; import io.netty.util.ReferenceCountUtil; import io.netty.util.ReferenceCounted; +import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.StringUtil; import java.net.SocketAddress; @@ -39,10 +40,7 @@ public class DefaultAddressedEnvelope<M, A extends SocketAddress> implements Add * {@code sender} address. */ public DefaultAddressedEnvelope(M message, A recipient, A sender) { - if (message == null) { - throw new NullPointerException("message"); - } - + ObjectUtil.checkNotNull(message, "message"); if (recipient == null && sender == null) { throw new NullPointerException("recipient and sender"); } diff --git a/transport/src/main/java/io/netty/channel/DefaultChannelConfig.java b/transport/src/main/java/io/netty/channel/DefaultChannelConfig.java index 4118708..9966b06 100644 --- a/transport/src/main/java/io/netty/channel/DefaultChannelConfig.java +++ b/transport/src/main/java/io/netty/channel/DefaultChannelConfig.java @@ -16,6 +16,7 @@ package io.netty.channel; import io.netty.buffer.ByteBufAllocator; +import io.netty.util.internal.ObjectUtil; import java.util.IdentityHashMap; import java.util.Map; @@ -36,6 +37,8 @@ import static io.netty.channel.ChannelOption.WRITE_BUFFER_LOW_WATER_MARK; import static io.netty.channel.ChannelOption.WRITE_BUFFER_WATER_MARK; import static io.netty.channel.ChannelOption.WRITE_SPIN_COUNT; import static io.netty.util.internal.ObjectUtil.checkNotNull; +import static io.netty.util.internal.ObjectUtil.checkPositive; +import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero; /** * The default {@link ChannelConfig} implementation. @@ -99,9 +102,7 @@ public class DefaultChannelConfig implements ChannelConfig { @SuppressWarnings("unchecked") @Override public boolean setOptions(Map<ChannelOption<?>, ?> options) { - if (options == null) { - throw new NullPointerException("options"); - } + ObjectUtil.checkNotNull(options, "options"); boolean setAllOptions = true; for (Entry<ChannelOption<?>, ?> e: options.entrySet()) { @@ -116,9 +117,7 @@ public class DefaultChannelConfig implements ChannelConfig { @Override @SuppressWarnings({ "unchecked", "deprecation" }) public <T> T getOption(ChannelOption<T> option) { - if (option == null) { - throw new NullPointerException("option"); - } + ObjectUtil.checkNotNull(option, "option"); if (option == CONNECT_TIMEOUT_MILLIS) { return (T) Integer.valueOf(getConnectTimeoutMillis()); @@ -196,10 +195,7 @@ public class DefaultChannelConfig implements ChannelConfig { } protected <T> void validate(ChannelOption<T> option, T value) { - if (option == null) { - throw new NullPointerException("option"); - } - option.validate(value); + ObjectUtil.checkNotNull(option, "option").validate(value); } @Override @@ -209,10 +205,7 @@ public class DefaultChannelConfig implements ChannelConfig { @Override public ChannelConfig setConnectTimeoutMillis(int connectTimeoutMillis) { - if (connectTimeoutMillis < 0) { - throw new IllegalArgumentException(String.format( - "connectTimeoutMillis: %d (expected: >= 0)", connectTimeoutMillis)); - } + checkPositiveOrZero(connectTimeoutMillis, "connectTimeoutMillis"); this.connectTimeoutMillis = connectTimeoutMillis; return this; } @@ -261,10 +254,7 @@ public class DefaultChannelConfig implements ChannelConfig { @Override public ChannelConfig setWriteSpinCount(int writeSpinCount) { - if (writeSpinCount <= 0) { - throw new IllegalArgumentException( - "writeSpinCount must be a positive integer."); - } + checkPositive(writeSpinCount, "writeSpinCount"); // Integer.MAX_VALUE is used as a special value in the channel implementations to indicate the channel cannot // accept any more data, and results in the writeOp being set on the selector (or execute a runnable which tries // to flush later because the writeSpinCount quantum has been exhausted). This strategy prevents additional @@ -283,10 +273,7 @@ public class DefaultChannelConfig implements ChannelConfig { @Override public ChannelConfig setAllocator(ByteBufAllocator allocator) { - if (allocator == null) { - throw new NullPointerException("allocator"); - } - this.allocator = allocator; + this.allocator = ObjectUtil.checkNotNull(allocator, "allocator"); return this; } @@ -357,10 +344,7 @@ public class DefaultChannelConfig implements ChannelConfig { @Override public ChannelConfig setWriteBufferHighWaterMark(int writeBufferHighWaterMark) { - if (writeBufferHighWaterMark < 0) { - throw new IllegalArgumentException( - "writeBufferHighWaterMark must be >= 0"); - } + checkPositiveOrZero(writeBufferHighWaterMark, "writeBufferHighWaterMark"); for (;;) { WriteBufferWaterMark waterMark = writeBufferWaterMark; if (writeBufferHighWaterMark < waterMark.low()) { @@ -383,10 +367,7 @@ public class DefaultChannelConfig implements ChannelConfig { @Override public ChannelConfig setWriteBufferLowWaterMark(int writeBufferLowWaterMark) { - if (writeBufferLowWaterMark < 0) { - throw new IllegalArgumentException( - "writeBufferLowWaterMark must be >= 0"); - } + checkPositiveOrZero(writeBufferLowWaterMark, "writeBufferLowWaterMark"); for (;;) { WriteBufferWaterMark waterMark = writeBufferWaterMark; if (writeBufferLowWaterMark > waterMark.high()) { @@ -420,10 +401,7 @@ public class DefaultChannelConfig implements ChannelConfig { @Override public ChannelConfig setMessageSizeEstimator(MessageSizeEstimator estimator) { - if (estimator == null) { - throw new NullPointerException("estimator"); - } - msgSizeEstimator = estimator; + this.msgSizeEstimator = ObjectUtil.checkNotNull(estimator, "estimator"); return this; } diff --git a/transport/src/main/java/io/netty/channel/DefaultChannelHandlerContext.java b/transport/src/main/java/io/netty/channel/DefaultChannelHandlerContext.java index 58454b8..26f11d5 100644 --- a/transport/src/main/java/io/netty/channel/DefaultChannelHandlerContext.java +++ b/transport/src/main/java/io/netty/channel/DefaultChannelHandlerContext.java @@ -23,10 +23,7 @@ final class DefaultChannelHandlerContext extends AbstractChannelHandlerContext { DefaultChannelHandlerContext( DefaultChannelPipeline pipeline, EventExecutor executor, String name, ChannelHandler handler) { - super(pipeline, executor, name, isInbound(handler), isOutbound(handler)); - if (handler == null) { - throw new NullPointerException("handler"); - } + super(pipeline, executor, name, handler.getClass()); this.handler = handler; } @@ -34,12 +31,4 @@ final class DefaultChannelHandlerContext extends AbstractChannelHandlerContext { public ChannelHandler handler() { return handler; } - - private static boolean isInbound(ChannelHandler handler) { - return handler instanceof ChannelInboundHandler; - } - - private static boolean isOutbound(ChannelHandler handler) { - return handler instanceof ChannelOutboundHandler; - } } diff --git a/transport/src/main/java/io/netty/channel/DefaultChannelPipeline.java b/transport/src/main/java/io/netty/channel/DefaultChannelPipeline.java index 2b307cc..dd8531f 100644 --- a/transport/src/main/java/io/netty/channel/DefaultChannelPipeline.java +++ b/transport/src/main/java/io/netty/channel/DefaultChannelPipeline.java @@ -341,9 +341,7 @@ public class DefaultChannelPipeline implements ChannelPipeline { @Override public final ChannelPipeline addFirst(EventExecutorGroup executor, ChannelHandler... handlers) { - if (handlers == null) { - throw new NullPointerException("handlers"); - } + ObjectUtil.checkNotNull(handlers, "handlers"); if (handlers.length == 0 || handlers[0] == null) { return this; } @@ -374,9 +372,7 @@ public class DefaultChannelPipeline implements ChannelPipeline { @Override public final ChannelPipeline addLast(EventExecutorGroup executor, ChannelHandler... handlers) { - if (handlers == null) { - throw new NullPointerException("handlers"); - } + ObjectUtil.checkNotNull(handlers, "handlers"); for (ChannelHandler h: handlers) { if (h == null) { @@ -457,7 +453,7 @@ public class DefaultChannelPipeline implements ChannelPipeline { assert ctx != head && ctx != tail; synchronized (this) { - remove0(ctx); + atomicRemoveFromHandlerList(ctx); // If the registered is false it means that the channel was not registered on an eventloop yet. // In this case we remove the context from the pipeline and add a task that will call @@ -482,7 +478,10 @@ public class DefaultChannelPipeline implements ChannelPipeline { return ctx; } - private static void remove0(AbstractChannelHandlerContext ctx) { + /** + * Method is synchronized to make the handler removal from the double linked list atomic. + */ + private synchronized void atomicRemoveFromHandlerList(AbstractChannelHandlerContext ctx) { AbstractChannelHandlerContext prev = ctx.prev; AbstractChannelHandlerContext next = ctx.next; prev.next = next; @@ -611,7 +610,7 @@ public class DefaultChannelPipeline implements ChannelPipeline { } catch (Throwable t) { boolean removed = false; try { - remove0(ctx); + atomicRemoveFromHandlerList(ctx); ctx.callHandlerRemoved(); removed = true; } catch (Throwable t2) { @@ -711,18 +710,12 @@ public class DefaultChannelPipeline implements ChannelPipeline { @Override public final ChannelHandlerContext context(String name) { - if (name == null) { - throw new NullPointerException("name"); - } - - return context0(name); + return context0(ObjectUtil.checkNotNull(name, "name")); } @Override public final ChannelHandlerContext context(ChannelHandler handler) { - if (handler == null) { - throw new NullPointerException("handler"); - } + ObjectUtil.checkNotNull(handler, "handler"); AbstractChannelHandlerContext ctx = head.next; for (;;) { @@ -741,9 +734,7 @@ public class DefaultChannelPipeline implements ChannelPipeline { @Override public final ChannelHandlerContext context(Class<? extends ChannelHandler> handlerType) { - if (handlerType == null) { - throw new NullPointerException("handlerType"); - } + ObjectUtil.checkNotNull(handlerType, "handlerType"); AbstractChannelHandlerContext ctx = head.next; for (;;) { @@ -881,9 +872,7 @@ public class DefaultChannelPipeline implements ChannelPipeline { final EventExecutor executor = ctx.executor(); if (inEventLoop || executor.inEventLoop(currentThread)) { - synchronized (this) { - remove0(ctx); - } + atomicRemoveFromHandlerList(ctx); callHandlerRemoved0(ctx); } else { final AbstractChannelHandlerContext finalCtx = ctx; @@ -1198,6 +1187,19 @@ public class DefaultChannelPipeline implements ChannelPipeline { } } + /** + * Called once a message hit the end of the {@link ChannelPipeline} without been handled by the user + * in {@link ChannelInboundHandler#channelRead(ChannelHandlerContext, Object)}. This method is responsible + * to call {@link ReferenceCountUtil#release(Object)} on the given msg at some point. + */ + protected void onUnhandledInboundMessage(ChannelHandlerContext ctx, Object msg) { + onUnhandledInboundMessage(msg); + if (logger.isDebugEnabled()) { + logger.debug("Discarded message pipeline : {}. Channel : {}.", + ctx.pipeline().names(), ctx.channel()); + } + } + /** * Called once the {@link ChannelInboundHandler#channelReadComplete(ChannelHandlerContext)} event hit * the end of the {@link ChannelPipeline}. @@ -1243,7 +1245,7 @@ public class DefaultChannelPipeline implements ChannelPipeline { final class TailContext extends AbstractChannelHandlerContext implements ChannelInboundHandler { TailContext(DefaultChannelPipeline pipeline) { - super(pipeline, null, TAIL_NAME, true, false); + super(pipeline, null, TAIL_NAME, TailContext.class); setAddComplete(); } @@ -1291,7 +1293,7 @@ public class DefaultChannelPipeline implements ChannelPipeline { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { - onUnhandledInboundMessage(msg); + onUnhandledInboundMessage(ctx, msg); } @Override @@ -1306,7 +1308,7 @@ public class DefaultChannelPipeline implements ChannelPipeline { private final Unsafe unsafe; HeadContext(DefaultChannelPipeline pipeline) { - super(pipeline, null, HEAD_NAME, true, true); + super(pipeline, null, HEAD_NAME, HeadContext.class); unsafe = pipeline.channel().unsafe(); setAddComplete(); } @@ -1468,7 +1470,7 @@ public class DefaultChannelPipeline implements ChannelPipeline { "Can't invoke handlerAdded() as the EventExecutor {} rejected it, removing handler {}.", executor, ctx.name(), e); } - remove0(ctx); + atomicRemoveFromHandlerList(ctx); ctx.setRemoved(); } } diff --git a/transport/src/main/java/io/netty/channel/DefaultEventLoopGroup.java b/transport/src/main/java/io/netty/channel/DefaultEventLoopGroup.java index ee5eeea..bbbec3b 100644 --- a/transport/src/main/java/io/netty/channel/DefaultEventLoopGroup.java +++ b/transport/src/main/java/io/netty/channel/DefaultEventLoopGroup.java @@ -39,6 +39,15 @@ public class DefaultEventLoopGroup extends MultithreadEventLoopGroup { this(nThreads, (ThreadFactory) null); } + /** + * Create a new instance with the default number of threads and the given {@link ThreadFactory}. + * + * @param threadFactory the {@link ThreadFactory} or {@code null} to use the default + */ + public DefaultEventLoopGroup(ThreadFactory threadFactory) { + this(0, threadFactory); + } + /** * Create a new instance * diff --git a/transport/src/main/java/io/netty/channel/DefaultFileRegion.java b/transport/src/main/java/io/netty/channel/DefaultFileRegion.java index 0e8d486..867f3f5 100644 --- a/transport/src/main/java/io/netty/channel/DefaultFileRegion.java +++ b/transport/src/main/java/io/netty/channel/DefaultFileRegion.java @@ -17,6 +17,7 @@ package io.netty.channel; import io.netty.util.AbstractReferenceCounted; import io.netty.util.IllegalReferenceCountException; +import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; @@ -26,6 +27,8 @@ import java.io.RandomAccessFile; import java.nio.channels.FileChannel; import java.nio.channels.WritableByteChannel; +import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero; + /** * Default {@link FileRegion} implementation which transfer data from a {@link FileChannel} or {@link File}. * @@ -49,19 +52,10 @@ public class DefaultFileRegion extends AbstractReferenceCounted implements FileR * @param count the number of bytes to transfer */ public DefaultFileRegion(FileChannel file, long position, long count) { - if (file == null) { - throw new NullPointerException("file"); - } - if (position < 0) { - throw new IllegalArgumentException("position must be >= 0 but was " + position); - } - if (count < 0) { - throw new IllegalArgumentException("count must be >= 0 but was " + count); - } - this.file = file; - this.position = position; - this.count = count; - f = null; + this.file = ObjectUtil.checkNotNull(file, "file"); + this.position = checkPositiveOrZero(position, "position"); + this.count = checkPositiveOrZero(count, "count"); + this.f = null; } /** @@ -73,18 +67,9 @@ public class DefaultFileRegion extends AbstractReferenceCounted implements FileR * @param count the number of bytes to transfer */ public DefaultFileRegion(File f, long position, long count) { - if (f == null) { - throw new NullPointerException("f"); - } - if (position < 0) { - throw new IllegalArgumentException("position must be >= 0 but was " + position); - } - if (count < 0) { - throw new IllegalArgumentException("count must be >= 0 but was " + count); - } - this.position = position; - this.count = count; - this.f = f; + this.f = ObjectUtil.checkNotNull(f, "f"); + this.position = checkPositiveOrZero(position, "position"); + this.count = checkPositiveOrZero(count, "count"); } /** @@ -145,6 +130,12 @@ public class DefaultFileRegion extends AbstractReferenceCounted implements FileR long written = file.transferTo(this.position + position, count, target); if (written > 0) { transferred += written; + } else if (written == 0) { + // If the amount of written data is 0 we need to check if the requested count is bigger then the + // actual file itself as it may have been truncated on disk. + // + // See https://github.com/netty/netty/issues/8868 + validate(this, position); } return written; } @@ -161,9 +152,7 @@ public class DefaultFileRegion extends AbstractReferenceCounted implements FileR try { file.close(); } catch (IOException e) { - if (logger.isWarnEnabled()) { - logger.warn("Failed to close a file.", e); - } + logger.warn("Failed to close a file.", e); } } @@ -188,4 +177,16 @@ public class DefaultFileRegion extends AbstractReferenceCounted implements FileR public FileRegion touch(Object hint) { return this; } + + static void validate(DefaultFileRegion region, long position) throws IOException { + // If the amount of written data is 0 we need to check if the requested count is bigger then the + // actual file itself as it may have been truncated on disk. + // + // See https://github.com/netty/netty/issues/8868 + long size = region.file.size(); + long count = region.count - position; + if (region.position + count + position > size) { + throw new IOException("Underlying file size " + size + " smaller then requested count " + region.count); + } + } } diff --git a/transport/src/main/java/io/netty/channel/DefaultMaxBytesRecvByteBufAllocator.java b/transport/src/main/java/io/netty/channel/DefaultMaxBytesRecvByteBufAllocator.java index 9077551..24f40b5 100644 --- a/transport/src/main/java/io/netty/channel/DefaultMaxBytesRecvByteBufAllocator.java +++ b/transport/src/main/java/io/netty/channel/DefaultMaxBytesRecvByteBufAllocator.java @@ -15,6 +15,8 @@ */ package io.netty.channel; +import static io.netty.util.internal.ObjectUtil.checkPositive; + import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.util.UncheckedBooleanSupplier; @@ -124,9 +126,7 @@ public class DefaultMaxBytesRecvByteBufAllocator implements MaxBytesRecvByteBufA @Override public DefaultMaxBytesRecvByteBufAllocator maxBytesPerRead(int maxBytesPerRead) { - if (maxBytesPerRead <= 0) { - throw new IllegalArgumentException("maxBytesPerRead: " + maxBytesPerRead + " (expected: > 0)"); - } + checkPositive(maxBytesPerRead, "maxBytesPerRead"); // There is a dependency between this.maxBytesPerRead and this.maxBytesPerIndividualRead (a < b). // Write operations must be synchronized, but independent read operations can just be volatile. synchronized (this) { @@ -149,10 +149,7 @@ public class DefaultMaxBytesRecvByteBufAllocator implements MaxBytesRecvByteBufA @Override public DefaultMaxBytesRecvByteBufAllocator maxBytesPerIndividualRead(int maxBytesPerIndividualRead) { - if (maxBytesPerIndividualRead <= 0) { - throw new IllegalArgumentException( - "maxBytesPerIndividualRead: " + maxBytesPerIndividualRead + " (expected: > 0)"); - } + checkPositive(maxBytesPerIndividualRead, "maxBytesPerIndividualRead"); // There is a dependency between this.maxBytesPerRead and this.maxBytesPerIndividualRead (a < b). // Write operations must be synchronized, but independent read operations can just be volatile. synchronized (this) { @@ -174,13 +171,8 @@ public class DefaultMaxBytesRecvByteBufAllocator implements MaxBytesRecvByteBufA } private static void checkMaxBytesPerReadPair(int maxBytesPerRead, int maxBytesPerIndividualRead) { - if (maxBytesPerRead <= 0) { - throw new IllegalArgumentException("maxBytesPerRead: " + maxBytesPerRead + " (expected: > 0)"); - } - if (maxBytesPerIndividualRead <= 0) { - throw new IllegalArgumentException( - "maxBytesPerIndividualRead: " + maxBytesPerIndividualRead + " (expected: > 0)"); - } + checkPositive(maxBytesPerRead, "maxBytesPerRead"); + checkPositive(maxBytesPerIndividualRead, "maxBytesPerIndividualRead"); if (maxBytesPerRead < maxBytesPerIndividualRead) { throw new IllegalArgumentException( "maxBytesPerRead cannot be less than " + diff --git a/transport/src/main/java/io/netty/channel/DefaultMaxMessagesRecvByteBufAllocator.java b/transport/src/main/java/io/netty/channel/DefaultMaxMessagesRecvByteBufAllocator.java index 0a44822..2e13606 100644 --- a/transport/src/main/java/io/netty/channel/DefaultMaxMessagesRecvByteBufAllocator.java +++ b/transport/src/main/java/io/netty/channel/DefaultMaxMessagesRecvByteBufAllocator.java @@ -15,6 +15,8 @@ */ package io.netty.channel; +import static io.netty.util.internal.ObjectUtil.checkPositive; + import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.util.UncheckedBooleanSupplier; @@ -42,9 +44,7 @@ public abstract class DefaultMaxMessagesRecvByteBufAllocator implements MaxMessa @Override public MaxMessagesRecvByteBufAllocator maxMessagesPerRead(int maxMessagesPerRead) { - if (maxMessagesPerRead <= 0) { - throw new IllegalArgumentException("maxMessagesPerRead: " + maxMessagesPerRead + " (expected: > 0)"); - } + checkPositive(maxMessagesPerRead, "maxMessagesPerRead"); this.maxMessagesPerRead = maxMessagesPerRead; return this; } diff --git a/transport/src/main/java/io/netty/channel/DefaultMessageSizeEstimator.java b/transport/src/main/java/io/netty/channel/DefaultMessageSizeEstimator.java index 1459743..cedbb2a 100644 --- a/transport/src/main/java/io/netty/channel/DefaultMessageSizeEstimator.java +++ b/transport/src/main/java/io/netty/channel/DefaultMessageSizeEstimator.java @@ -15,6 +15,8 @@ */ package io.netty.channel; +import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero; + import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufHolder; @@ -59,9 +61,7 @@ public final class DefaultMessageSizeEstimator implements MessageSizeEstimator { * @param unknownSize The size which is returned for unknown messages. */ public DefaultMessageSizeEstimator(int unknownSize) { - if (unknownSize < 0) { - throw new IllegalArgumentException("unknownSize: " + unknownSize + " (expected: >= 0)"); - } + checkPositiveOrZero(unknownSize, "unknownSize"); handle = new HandleImpl(unknownSize); } diff --git a/transport/src/main/java/io/netty/channel/EventLoopTaskQueueFactory.java b/transport/src/main/java/io/netty/channel/EventLoopTaskQueueFactory.java new file mode 100644 index 0000000..c2788da --- /dev/null +++ b/transport/src/main/java/io/netty/channel/EventLoopTaskQueueFactory.java @@ -0,0 +1,35 @@ +/* + * Copyright 2019 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.channel; + +import java.util.Queue; + +/** + * Factory used to create {@link Queue} instances that will be used to store tasks for an {@link EventLoop}. + * + * Generally speaking the returned {@link Queue} MUST be thread-safe and depending on the {@link EventLoop} + * implementation must be of type {@link java.util.concurrent.BlockingQueue}. + */ +public interface EventLoopTaskQueueFactory { + + /** + * Returns a new {@link Queue} to use. + * @param maxCapacity the maximum amount of elements that can be stored in the {@link Queue} at a given point + * in time. + * @return the new queue. + */ + Queue<Runnable> newTaskQueue(int maxCapacity); +} diff --git a/transport/src/main/java/io/netty/channel/ExtendedClosedChannelException.java b/transport/src/main/java/io/netty/channel/ExtendedClosedChannelException.java new file mode 100644 index 0000000..3b908cd --- /dev/null +++ b/transport/src/main/java/io/netty/channel/ExtendedClosedChannelException.java @@ -0,0 +1,32 @@ +/* + * Copyright 2019 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.channel; + +import java.nio.channels.ClosedChannelException; + +final class ExtendedClosedChannelException extends ClosedChannelException { + + ExtendedClosedChannelException(Throwable cause) { + if (cause != null) { + initCause(cause); + } + } + + @Override + public Throwable fillInStackTrace() { + return this; + } +} diff --git a/transport/src/main/java/io/netty/channel/FailedChannelFuture.java b/transport/src/main/java/io/netty/channel/FailedChannelFuture.java index 92f83cd..f820905 100644 --- a/transport/src/main/java/io/netty/channel/FailedChannelFuture.java +++ b/transport/src/main/java/io/netty/channel/FailedChannelFuture.java @@ -16,6 +16,7 @@ package io.netty.channel; import io.netty.util.concurrent.EventExecutor; +import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.PlatformDependent; /** @@ -35,10 +36,7 @@ final class FailedChannelFuture extends CompleteChannelFuture { */ FailedChannelFuture(Channel channel, EventExecutor executor, Throwable cause) { super(channel, executor); - if (cause == null) { - throw new NullPointerException("cause"); - } - this.cause = cause; + this.cause = ObjectUtil.checkNotNull(cause, "cause"); } @Override diff --git a/transport/src/main/java/io/netty/channel/FixedRecvByteBufAllocator.java b/transport/src/main/java/io/netty/channel/FixedRecvByteBufAllocator.java index 8dab77d..8bdd43d 100644 --- a/transport/src/main/java/io/netty/channel/FixedRecvByteBufAllocator.java +++ b/transport/src/main/java/io/netty/channel/FixedRecvByteBufAllocator.java @@ -15,6 +15,8 @@ */ package io.netty.channel; +import static io.netty.util.internal.ObjectUtil.checkPositive; + /** * The {@link RecvByteBufAllocator} that always yields the same buffer * size prediction. This predictor ignores the feed back from the I/O thread. @@ -26,7 +28,7 @@ public class FixedRecvByteBufAllocator extends DefaultMaxMessagesRecvByteBufAllo private final class HandleImpl extends MaxMessageHandle { private final int bufferSize; - public HandleImpl(int bufferSize) { + HandleImpl(int bufferSize) { this.bufferSize = bufferSize; } @@ -41,10 +43,7 @@ public class FixedRecvByteBufAllocator extends DefaultMaxMessagesRecvByteBufAllo * the specified buffer size. */ public FixedRecvByteBufAllocator(int bufferSize) { - if (bufferSize <= 0) { - throw new IllegalArgumentException( - "bufferSize must greater than 0: " + bufferSize); - } + checkPositive(bufferSize, "bufferSize"); this.bufferSize = bufferSize; } diff --git a/transport/src/main/java/io/netty/channel/MessageSizeEstimator.java b/transport/src/main/java/io/netty/channel/MessageSizeEstimator.java index 5c84927..92f164d 100644 --- a/transport/src/main/java/io/netty/channel/MessageSizeEstimator.java +++ b/transport/src/main/java/io/netty/channel/MessageSizeEstimator.java @@ -16,8 +16,8 @@ package io.netty.channel; /** - * Responsible to estimate size of a message. The size represent how much memory the message will ca. reserve in - * memory. + * Responsible to estimate the size of a message. The size represents approximately how much memory the message will + * reserve in memory. */ public interface MessageSizeEstimator { diff --git a/transport/src/main/java/io/netty/channel/MultithreadEventLoopGroup.java b/transport/src/main/java/io/netty/channel/MultithreadEventLoopGroup.java index a9bc23d..ba05062 100644 --- a/transport/src/main/java/io/netty/channel/MultithreadEventLoopGroup.java +++ b/transport/src/main/java/io/netty/channel/MultithreadEventLoopGroup.java @@ -96,4 +96,5 @@ public abstract class MultithreadEventLoopGroup extends MultithreadEventExecutor public ChannelFuture register(Channel channel, ChannelPromise promise) { return next().register(channel, promise); } + } diff --git a/transport/src/main/java/io/netty/channel/PendingWriteQueue.java b/transport/src/main/java/io/netty/channel/PendingWriteQueue.java index 16ae47b..b945130 100644 --- a/transport/src/main/java/io/netty/channel/PendingWriteQueue.java +++ b/transport/src/main/java/io/netty/channel/PendingWriteQueue.java @@ -15,9 +15,10 @@ */ package io.netty.channel; -import io.netty.util.Recycler; import io.netty.util.ReferenceCountUtil; import io.netty.util.concurrent.PromiseCombiner; +import io.netty.util.internal.ObjectPool; +import io.netty.util.internal.ObjectPool.ObjectCreator; import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.SystemPropertyUtil; import io.netty.util.internal.logging.InternalLogger; @@ -92,12 +93,8 @@ public final class PendingWriteQueue { */ public void add(Object msg, ChannelPromise promise) { assert ctx.executor().inEventLoop(); - if (msg == null) { - throw new NullPointerException("msg"); - } - if (promise == null) { - throw new NullPointerException("promise"); - } + ObjectUtil.checkNotNull(msg, "msg"); + ObjectUtil.checkNotNull(promise, "promise"); // It is possible for writes to be triggered from removeAndFailAll(). To preserve ordering, // we should add them to the queue and let removeAndFailAll() fail them later. int messageSize = size(msg); @@ -130,7 +127,7 @@ public final class PendingWriteQueue { } ChannelPromise p = ctx.newPromise(); - PromiseCombiner combiner = new PromiseCombiner(); + PromiseCombiner combiner = new PromiseCombiner(ctx.executor()); try { // It is possible for some of the written promises to trigger more writes. The new writes // will "revive" the queue, so we need to write them up until the queue is empty. @@ -165,9 +162,7 @@ public final class PendingWriteQueue { */ public void removeAndFailAll(Throwable cause) { assert ctx.executor().inEventLoop(); - if (cause == null) { - throw new NullPointerException("cause"); - } + ObjectUtil.checkNotNull(cause, "cause"); // It is possible for some of the failed promises to trigger more writes. The new writes // will "revive" the queue, so we need to clean them up until the queue is empty. for (PendingWrite write = head; write != null; write = head) { @@ -192,11 +187,9 @@ public final class PendingWriteQueue { */ public void removeAndFail(Throwable cause) { assert ctx.executor().inEventLoop(); - if (cause == null) { - throw new NullPointerException("cause"); - } - PendingWrite write = head; + ObjectUtil.checkNotNull(cause, "cause"); + PendingWrite write = head; if (write == null) { return; } @@ -292,20 +285,20 @@ public final class PendingWriteQueue { * Holds all meta-data and construct the linked-list structure. */ static final class PendingWrite { - private static final Recycler<PendingWrite> RECYCLER = new Recycler<PendingWrite>() { + private static final ObjectPool<PendingWrite> RECYCLER = ObjectPool.newPool(new ObjectCreator<PendingWrite>() { @Override - protected PendingWrite newObject(Handle<PendingWrite> handle) { + public PendingWrite newObject(ObjectPool.Handle<PendingWrite> handle) { return new PendingWrite(handle); } - }; + }); - private final Recycler.Handle<PendingWrite> handle; + private final ObjectPool.Handle<PendingWrite> handle; private PendingWrite next; private long size; private ChannelPromise promise; private Object msg; - private PendingWrite(Recycler.Handle<PendingWrite> handle) { + private PendingWrite(ObjectPool.Handle<PendingWrite> handle) { this.handle = handle; } diff --git a/transport/src/main/java/io/netty/channel/SimpleChannelInboundHandler.java b/transport/src/main/java/io/netty/channel/SimpleChannelInboundHandler.java index ddc516e..6fbdbde 100644 --- a/transport/src/main/java/io/netty/channel/SimpleChannelInboundHandler.java +++ b/transport/src/main/java/io/netty/channel/SimpleChannelInboundHandler.java @@ -38,12 +38,6 @@ import io.netty.util.internal.TypeParameterMatcher; * Be aware that depending of the constructor parameters it will release all handled messages by passing them to * {@link ReferenceCountUtil#release(Object)}. In this case you may need to use * {@link ReferenceCountUtil#retain(Object)} if you pass the object to the next handler in the {@link ChannelPipeline}. - * - * <h3>Forward compatibility notice</h3> - * <p> - * Please keep in mind that {@link #channelRead0(ChannelHandlerContext, I)} will be renamed to - * {@code messageReceived(ChannelHandlerContext, I)} in 5.0. - * </p> */ public abstract class SimpleChannelInboundHandler<I> extends ChannelInboundHandlerAdapter { @@ -115,9 +109,6 @@ public abstract class SimpleChannelInboundHandler<I> extends ChannelInboundHandl } /** - * <strong>Please keep in mind that this method will be renamed to - * {@code messageReceived(ChannelHandlerContext, I)} in 5.0.</strong> - * * Is called for each message of type {@link I}. * * @param ctx the {@link ChannelHandlerContext} which this {@link SimpleChannelInboundHandler} diff --git a/transport/src/main/java/io/netty/channel/SingleThreadEventLoop.java b/transport/src/main/java/io/netty/channel/SingleThreadEventLoop.java index c547b34..7a08ba8 100644 --- a/transport/src/main/java/io/netty/channel/SingleThreadEventLoop.java +++ b/transport/src/main/java/io/netty/channel/SingleThreadEventLoop.java @@ -59,6 +59,13 @@ public abstract class SingleThreadEventLoop extends SingleThreadEventExecutor im tailTasks = newTaskQueue(maxPendingTasks); } + protected SingleThreadEventLoop(EventLoopGroup parent, Executor executor, + boolean addTaskWakesUp, Queue<Runnable> taskQueue, Queue<Runnable> tailTaskQueue, + RejectedExecutionHandler rejectedExecutionHandler) { + super(parent, executor, addTaskWakesUp, taskQueue, rejectedExecutionHandler); + tailTasks = ObjectUtil.checkNotNull(tailTaskQueue, "tailTaskQueue"); + } + @Override public EventLoopGroup parent() { return (EventLoopGroup) super.parent(); @@ -84,13 +91,8 @@ public abstract class SingleThreadEventLoop extends SingleThreadEventExecutor im @Deprecated @Override public ChannelFuture register(final Channel channel, final ChannelPromise promise) { - if (channel == null) { - throw new NullPointerException("channel"); - } - if (promise == null) { - throw new NullPointerException("promise"); - } - + ObjectUtil.checkNotNull(promise, "promise"); + ObjectUtil.checkNotNull(channel, "channel"); channel.unsafe().register(this, promise); return promise; } @@ -111,7 +113,7 @@ public abstract class SingleThreadEventLoop extends SingleThreadEventExecutor im reject(task); } - if (wakesUpForTask(task)) { + if (!(task instanceof LazyRunnable) && wakesUpForTask(task)) { wakeup(inEventLoop()); } } @@ -128,11 +130,6 @@ public abstract class SingleThreadEventLoop extends SingleThreadEventExecutor im return tailTasks.remove(ObjectUtil.checkNotNull(task, "task")); } - @Override - protected boolean wakesUpForTask(Runnable task) { - return !(task instanceof NonWakeupRunnable); - } - @Override protected void afterRunningAllTasks() { runAllTasksFrom(tailTasks); @@ -149,7 +146,12 @@ public abstract class SingleThreadEventLoop extends SingleThreadEventExecutor im } /** - * Marker interface for {@link Runnable} that will not trigger an {@link #wakeup(boolean)} in all cases. + * Returns the number of {@link Channel}s registered with this {@link EventLoop} or {@code -1} + * if operation is not supported. The returned value is not guaranteed to be exact accurate and + * should be viewed as a best effort. */ - interface NonWakeupRunnable extends Runnable { } + @UnstableApi + public int registeredChannels() { + return -1; + } } diff --git a/transport/src/main/java/io/netty/channel/ThreadPerChannelEventLoop.java b/transport/src/main/java/io/netty/channel/ThreadPerChannelEventLoop.java index 1d4b958..497e796 100644 --- a/transport/src/main/java/io/netty/channel/ThreadPerChannelEventLoop.java +++ b/transport/src/main/java/io/netty/channel/ThreadPerChannelEventLoop.java @@ -95,4 +95,9 @@ public class ThreadPerChannelEventLoop extends SingleThreadEventLoop { parent.activeChildren.remove(this); parent.idleChildren.add(this); } + + @Override + public int registeredChannels() { + return 1; + } } diff --git a/transport/src/main/java/io/netty/channel/ThreadPerChannelEventLoopGroup.java b/transport/src/main/java/io/netty/channel/ThreadPerChannelEventLoopGroup.java index 7ee89d0..a3a95a6 100644 --- a/transport/src/main/java/io/netty/channel/ThreadPerChannelEventLoopGroup.java +++ b/transport/src/main/java/io/netty/channel/ThreadPerChannelEventLoopGroup.java @@ -18,6 +18,7 @@ package io.netty.channel; import io.netty.util.concurrent.AbstractEventExecutorGroup; import io.netty.util.concurrent.DefaultPromise; +import io.netty.util.concurrent.DefaultThreadFactory; import io.netty.util.concurrent.EventExecutor; import io.netty.util.concurrent.Future; import io.netty.util.concurrent.FutureListener; @@ -25,6 +26,7 @@ import io.netty.util.concurrent.GlobalEventExecutor; import io.netty.util.concurrent.Promise; import io.netty.util.concurrent.ThreadPerTaskExecutor; import io.netty.util.internal.EmptyArrays; +import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.PlatformDependent; import io.netty.util.internal.ReadOnlyIterator; import io.netty.util.internal.ThrowableUtil; @@ -35,7 +37,6 @@ import java.util.Queue; import java.util.Set; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.Executor; -import java.util.concurrent.Executors; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; @@ -85,7 +86,7 @@ public class ThreadPerChannelEventLoopGroup extends AbstractEventExecutorGroup i * Use {@code 0} to use no limit */ protected ThreadPerChannelEventLoopGroup(int maxChannels) { - this(maxChannels, Executors.defaultThreadFactory()); + this(maxChannels, (ThreadFactory) null); } /** @@ -101,7 +102,7 @@ public class ThreadPerChannelEventLoopGroup extends AbstractEventExecutorGroup i * @param args arguments which will passed to each {@link #newChild(Object...)} call. */ protected ThreadPerChannelEventLoopGroup(int maxChannels, ThreadFactory threadFactory, Object... args) { - this(maxChannels, new ThreadPerTaskExecutor(threadFactory), args); + this(maxChannels, threadFactory == null ? null : new ThreadPerTaskExecutor(threadFactory), args); } /** @@ -117,12 +118,9 @@ public class ThreadPerChannelEventLoopGroup extends AbstractEventExecutorGroup i * @param args arguments which will passed to each {@link #newChild(Object...)} call. */ protected ThreadPerChannelEventLoopGroup(int maxChannels, Executor executor, Object... args) { - if (maxChannels < 0) { - throw new IllegalArgumentException(String.format( - "maxChannels: %d (expected: >= 0)", maxChannels)); - } + ObjectUtil.checkPositiveOrZero(maxChannels, "maxChannels"); if (executor == null) { - throw new NullPointerException("executor"); + executor = new ThreadPerTaskExecutor(new DefaultThreadFactory(getClass())); } if (args == null) { @@ -135,7 +133,7 @@ public class ThreadPerChannelEventLoopGroup extends AbstractEventExecutorGroup i this.executor = executor; tooManyChannels = ThrowableUtil.unknownStackTrace( - new ChannelException("too many channels (max: " + maxChannels + ')'), + ChannelException.newStatic("too many channels (max: " + maxChannels + ')', null), ThreadPerChannelEventLoopGroup.class, "nextChild()"); } @@ -274,9 +272,7 @@ public class ThreadPerChannelEventLoopGroup extends AbstractEventExecutorGroup i @Override public ChannelFuture register(Channel channel) { - if (channel == null) { - throw new NullPointerException("channel"); - } + ObjectUtil.checkNotNull(channel, "channel"); try { EventLoop l = nextChild(); return l.register(new DefaultChannelPromise(channel, l)); @@ -298,9 +294,7 @@ public class ThreadPerChannelEventLoopGroup extends AbstractEventExecutorGroup i @Deprecated @Override public ChannelFuture register(Channel channel, ChannelPromise promise) { - if (channel == null) { - throw new NullPointerException("channel"); - } + ObjectUtil.checkNotNull(channel, "channel"); try { return nextChild().register(channel, promise); } catch (Throwable t) { diff --git a/transport/src/main/java/io/netty/channel/VoidChannelPromise.java b/transport/src/main/java/io/netty/channel/VoidChannelPromise.java index c684843..eeffb6b 100644 --- a/transport/src/main/java/io/netty/channel/VoidChannelPromise.java +++ b/transport/src/main/java/io/netty/channel/VoidChannelPromise.java @@ -18,6 +18,7 @@ package io.netty.channel; import io.netty.util.concurrent.AbstractFuture; import io.netty.util.concurrent.Future; import io.netty.util.concurrent.GenericFutureListener; +import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.UnstableApi; import java.util.concurrent.TimeUnit; @@ -35,9 +36,7 @@ public final class VoidChannelPromise extends AbstractFuture<Void> implements Ch * @param channel the {@link Channel} associated with this future */ public VoidChannelPromise(final Channel channel, boolean fireException) { - if (channel == null) { - throw new NullPointerException("channel"); - } + ObjectUtil.checkNotNull(channel, "channel"); this.channel = channel; if (fireException) { fireExceptionListener = new ChannelFutureListener() { @@ -162,6 +161,7 @@ public final class VoidChannelPromise extends AbstractFuture<Void> implements Ch fail(); return this; } + @Override public VoidChannelPromise setFailure(Throwable cause) { fireException0(cause); diff --git a/transport/src/main/java/io/netty/channel/WriteBufferWaterMark.java b/transport/src/main/java/io/netty/channel/WriteBufferWaterMark.java index ee3d466..3deb74f 100644 --- a/transport/src/main/java/io/netty/channel/WriteBufferWaterMark.java +++ b/transport/src/main/java/io/netty/channel/WriteBufferWaterMark.java @@ -15,6 +15,8 @@ */ package io.netty.channel; +import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero; + /** * WriteBufferWaterMark is used to set low water mark and high water mark for the write buffer. * <p> @@ -54,9 +56,7 @@ public final class WriteBufferWaterMark { */ WriteBufferWaterMark(int low, int high, boolean validate) { if (validate) { - if (low < 0) { - throw new IllegalArgumentException("write buffer's low water mark must be >= 0"); - } + checkPositiveOrZero(low, "low"); if (high < low) { throw new IllegalArgumentException( "write buffer's high water mark cannot be less than " + diff --git a/transport/src/main/java/io/netty/channel/embedded/EmbeddedChannel.java b/transport/src/main/java/io/netty/channel/embedded/EmbeddedChannel.java index cf4e298..976d0b0 100644 --- a/transport/src/main/java/io/netty/channel/embedded/EmbeddedChannel.java +++ b/transport/src/main/java/io/netty/channel/embedded/EmbeddedChannel.java @@ -26,6 +26,7 @@ import io.netty.channel.ChannelConfig; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelId; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelMetadata; @@ -160,8 +161,25 @@ public class EmbeddedChannel extends AbstractChannel { * @param handlers the {@link ChannelHandler}s which will be add in the {@link ChannelPipeline} */ public EmbeddedChannel(ChannelId channelId, boolean register, boolean hasDisconnect, + ChannelHandler... handlers) { + this(null, channelId, register, hasDisconnect, handlers); + } + + /** + * Create a new instance with the channel ID set to the given ID and the pipeline + * initialized with the specified handlers. + * + * @param parent the parent {@link Channel} of this {@link EmbeddedChannel}. + * @param channelId the {@link ChannelId} that will be used to identify this channel + * @param register {@code true} if this {@link Channel} is registered to the {@link EventLoop} in the + * constructor. If {@code false} the user will need to call {@link #register()}. + * @param hasDisconnect {@code false} if this {@link Channel} will delegate {@link #disconnect()} + * to {@link #close()}, {@link false} otherwise. + * @param handlers the {@link ChannelHandler}s which will be add in the {@link ChannelPipeline} + */ + public EmbeddedChannel(Channel parent, ChannelId channelId, boolean register, boolean hasDisconnect, final ChannelHandler... handlers) { - super(null, channelId); + super(parent, channelId); metadata = metadata(hasDisconnect); config = new DefaultChannelConfig(this); setup(register, handlers); @@ -856,8 +874,8 @@ public class EmbeddedChannel extends AbstractChannel { } @Override - protected void onUnhandledInboundMessage(Object msg) { - handleInboundMessage(msg); + protected void onUnhandledInboundMessage(ChannelHandlerContext ctx, Object msg) { + handleInboundMessage(msg); } } } diff --git a/transport/src/main/java/io/netty/channel/embedded/EmbeddedEventLoop.java b/transport/src/main/java/io/netty/channel/embedded/EmbeddedEventLoop.java index bd95fee..7a43875 100644 --- a/transport/src/main/java/io/netty/channel/embedded/EmbeddedEventLoop.java +++ b/transport/src/main/java/io/netty/channel/embedded/EmbeddedEventLoop.java @@ -45,10 +45,7 @@ final class EmbeddedEventLoop extends AbstractScheduledEventExecutor implements @Override public void execute(Runnable command) { - if (command == null) { - throw new NullPointerException("command"); - } - tasks.add(command); + tasks.add(ObjectUtil.checkNotNull(command, "command")); } void runTasks() { diff --git a/transport/src/main/java/io/netty/channel/group/ChannelGroupException.java b/transport/src/main/java/io/netty/channel/group/ChannelGroupException.java index aeabd64..aeca9bc 100644 --- a/transport/src/main/java/io/netty/channel/group/ChannelGroupException.java +++ b/transport/src/main/java/io/netty/channel/group/ChannelGroupException.java @@ -18,6 +18,7 @@ package io.netty.channel.group; import io.netty.channel.Channel; import io.netty.channel.ChannelException; import io.netty.channel.ChannelFuture; +import io.netty.util.internal.ObjectUtil; import java.util.Collection; import java.util.Collections; @@ -32,12 +33,8 @@ public class ChannelGroupException extends ChannelException implements Iterable< private final Collection<Map.Entry<Channel, Throwable>> failed; public ChannelGroupException(Collection<Map.Entry<Channel, Throwable>> causes) { - if (causes == null) { - throw new NullPointerException("causes"); - } - if (causes.isEmpty()) { - throw new IllegalArgumentException("causes must be non empty"); - } + ObjectUtil.checkNonEmpty(causes, "causes"); + failed = Collections.unmodifiableCollection(causes); } diff --git a/transport/src/main/java/io/netty/channel/group/CombinedIterator.java b/transport/src/main/java/io/netty/channel/group/CombinedIterator.java index 1c42eb0..6684488 100644 --- a/transport/src/main/java/io/netty/channel/group/CombinedIterator.java +++ b/transport/src/main/java/io/netty/channel/group/CombinedIterator.java @@ -15,6 +15,8 @@ */ package io.netty.channel.group; +import io.netty.util.internal.ObjectUtil; + import java.util.Iterator; import java.util.NoSuchElementException; @@ -27,15 +29,9 @@ final class CombinedIterator<E> implements Iterator<E> { private Iterator<E> currentIterator; CombinedIterator(Iterator<E> i1, Iterator<E> i2) { - if (i1 == null) { - throw new NullPointerException("i1"); - } - if (i2 == null) { - throw new NullPointerException("i2"); - } - this.i1 = i1; - this.i2 = i2; - currentIterator = i1; + this.i1 = ObjectUtil.checkNotNull(i1, "i1"); + this.i2 = ObjectUtil.checkNotNull(i2, "i2"); + this.currentIterator = i1; } @Override diff --git a/transport/src/main/java/io/netty/channel/group/DefaultChannelGroup.java b/transport/src/main/java/io/netty/channel/group/DefaultChannelGroup.java index 71f217a..05e0dff 100644 --- a/transport/src/main/java/io/netty/channel/group/DefaultChannelGroup.java +++ b/transport/src/main/java/io/netty/channel/group/DefaultChannelGroup.java @@ -24,6 +24,7 @@ import io.netty.channel.ChannelId; import io.netty.channel.ServerChannel; import io.netty.util.ReferenceCountUtil; import io.netty.util.concurrent.EventExecutor; +import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.PlatformDependent; import io.netty.util.internal.StringUtil; @@ -91,9 +92,7 @@ public class DefaultChannelGroup extends AbstractSet<Channel> implements Channel * the same name, which means no duplicate check is done against group names. */ public DefaultChannelGroup(String name, EventExecutor executor, boolean stayClosed) { - if (name == null) { - throw new NullPointerException("name"); - } + ObjectUtil.checkNotNull(name, "name"); this.name = name; this.executor = executor; this.stayClosed = stayClosed; @@ -256,12 +255,8 @@ public class DefaultChannelGroup extends AbstractSet<Channel> implements Channel @Override public ChannelGroupFuture write(Object message, ChannelMatcher matcher, boolean voidPromise) { - if (message == null) { - throw new NullPointerException("message"); - } - if (matcher == null) { - throw new NullPointerException("matcher"); - } + ObjectUtil.checkNotNull(message, "message"); + ObjectUtil.checkNotNull(matcher, "matcher"); final ChannelGroupFuture future; if (voidPromise) { @@ -272,7 +267,7 @@ public class DefaultChannelGroup extends AbstractSet<Channel> implements Channel } future = voidFuture; } else { - Map<Channel, ChannelFuture> futures = new LinkedHashMap<Channel, ChannelFuture>(size()); + Map<Channel, ChannelFuture> futures = new LinkedHashMap<Channel, ChannelFuture>(nonServerChannels.size()); for (Channel c: nonServerChannels.values()) { if (matcher.matches(c)) { futures.put(c, c.write(safeDuplicate(message))); @@ -301,9 +296,7 @@ public class DefaultChannelGroup extends AbstractSet<Channel> implements Channel @Override public ChannelGroupFuture disconnect(ChannelMatcher matcher) { - if (matcher == null) { - throw new NullPointerException("matcher"); - } + ObjectUtil.checkNotNull(matcher, "matcher"); Map<Channel, ChannelFuture> futures = new LinkedHashMap<Channel, ChannelFuture>(size()); @@ -324,9 +317,7 @@ public class DefaultChannelGroup extends AbstractSet<Channel> implements Channel @Override public ChannelGroupFuture close(ChannelMatcher matcher) { - if (matcher == null) { - throw new NullPointerException("matcher"); - } + ObjectUtil.checkNotNull(matcher, "matcher"); Map<Channel, ChannelFuture> futures = new LinkedHashMap<Channel, ChannelFuture>(size()); @@ -357,9 +348,7 @@ public class DefaultChannelGroup extends AbstractSet<Channel> implements Channel @Override public ChannelGroupFuture deregister(ChannelMatcher matcher) { - if (matcher == null) { - throw new NullPointerException("matcher"); - } + ObjectUtil.checkNotNull(matcher, "matcher"); Map<Channel, ChannelFuture> futures = new LinkedHashMap<Channel, ChannelFuture>(size()); @@ -400,9 +389,7 @@ public class DefaultChannelGroup extends AbstractSet<Channel> implements Channel @Override public ChannelGroupFuture writeAndFlush(Object message, ChannelMatcher matcher, boolean voidPromise) { - if (message == null) { - throw new NullPointerException("message"); - } + ObjectUtil.checkNotNull(message, "message"); final ChannelGroupFuture future; if (voidPromise) { @@ -413,7 +400,7 @@ public class DefaultChannelGroup extends AbstractSet<Channel> implements Channel } future = voidFuture; } else { - Map<Channel, ChannelFuture> futures = new LinkedHashMap<Channel, ChannelFuture>(size()); + Map<Channel, ChannelFuture> futures = new LinkedHashMap<Channel, ChannelFuture>(nonServerChannels.size()); for (Channel c: nonServerChannels.values()) { if (matcher.matches(c)) { futures.put(c, c.writeAndFlush(safeDuplicate(message))); diff --git a/transport/src/main/java/io/netty/channel/group/DefaultChannelGroupFuture.java b/transport/src/main/java/io/netty/channel/group/DefaultChannelGroupFuture.java index e4afe23..350a690 100644 --- a/transport/src/main/java/io/netty/channel/group/DefaultChannelGroupFuture.java +++ b/transport/src/main/java/io/netty/channel/group/DefaultChannelGroupFuture.java @@ -24,6 +24,7 @@ import io.netty.util.concurrent.EventExecutor; import io.netty.util.concurrent.Future; import io.netty.util.concurrent.GenericFutureListener; import io.netty.util.concurrent.ImmediateEventExecutor; +import io.netty.util.internal.ObjectUtil; import java.util.ArrayList; import java.util.Collection; @@ -82,14 +83,8 @@ final class DefaultChannelGroupFuture extends DefaultPromise<Void> implements Ch */ DefaultChannelGroupFuture(ChannelGroup group, Collection<ChannelFuture> futures, EventExecutor executor) { super(executor); - if (group == null) { - throw new NullPointerException("group"); - } - if (futures == null) { - throw new NullPointerException("futures"); - } - - this.group = group; + this.group = ObjectUtil.checkNotNull(group, "group"); + ObjectUtil.checkNotNull(futures, "futures"); Map<Channel, ChannelFuture> futureMap = new LinkedHashMap<Channel, ChannelFuture>(); for (ChannelFuture f: futures) { diff --git a/transport/src/main/java/io/netty/channel/local/LocalAddress.java b/transport/src/main/java/io/netty/channel/local/LocalAddress.java index 6098cca..bba011c 100644 --- a/transport/src/main/java/io/netty/channel/local/LocalAddress.java +++ b/transport/src/main/java/io/netty/channel/local/LocalAddress.java @@ -16,6 +16,7 @@ package io.netty.channel.local; import io.netty.channel.Channel; +import io.netty.util.internal.ObjectUtil; import java.net.SocketAddress; @@ -50,9 +51,7 @@ public final class LocalAddress extends SocketAddress implements Comparable<Loca * Creates a new instance with the specified ID. */ public LocalAddress(String id) { - if (id == null) { - throw new NullPointerException("id"); - } + ObjectUtil.checkNotNull(id, "id"); id = id.trim().toLowerCase(); if (id.isEmpty()) { throw new IllegalArgumentException("empty id"); diff --git a/transport/src/main/java/io/netty/channel/local/LocalChannel.java b/transport/src/main/java/io/netty/channel/local/LocalChannel.java index 62bd4d6..50c8e8d 100644 --- a/transport/src/main/java/io/netty/channel/local/LocalChannel.java +++ b/transport/src/main/java/io/netty/channel/local/LocalChannel.java @@ -32,7 +32,6 @@ import io.netty.util.concurrent.Future; import io.netty.util.concurrent.SingleThreadEventExecutor; import io.netty.util.internal.InternalThreadLocalMap; import io.netty.util.internal.PlatformDependent; -import io.netty.util.internal.ThrowableUtil; import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; @@ -55,10 +54,6 @@ public class LocalChannel extends AbstractChannel { AtomicReferenceFieldUpdater.newUpdater(LocalChannel.class, Future.class, "finishReadFuture"); private static final ChannelMetadata METADATA = new ChannelMetadata(false); private static final int MAX_READER_STACK_DEPTH = 8; - private static final ClosedChannelException DO_WRITE_CLOSED_CHANNEL_EXCEPTION = ThrowableUtil.unknownStackTrace( - new ClosedChannelException(), LocalChannel.class, "doWrite(...)"); - private static final ClosedChannelException DO_CLOSE_CLOSED_CHANNEL_EXCEPTION = ThrowableUtil.unknownStackTrace( - new ClosedChannelException(), LocalChannel.class, "doClose()"); private enum State { OPEN, BOUND, CONNECTED, CLOSED } @@ -234,7 +229,7 @@ public class LocalChannel extends AbstractChannel { ChannelPromise promise = connectPromise; if (promise != null) { // Use tryFailure() instead of setFailure() to avoid the race against cancel(). - promise.tryFailure(DO_CLOSE_CLOSED_CHANNEL_EXCEPTION); + promise.tryFailure(new ClosedChannelException()); connectPromise = null; } } @@ -347,7 +342,7 @@ public class LocalChannel extends AbstractChannel { case BOUND: throw new NotYetConnectedException(); case CLOSED: - throw DO_WRITE_CLOSED_CHANNEL_EXCEPTION; + throw new ClosedChannelException(); case CONNECTED: break; } @@ -356,6 +351,7 @@ public class LocalChannel extends AbstractChannel { writeInProgress = true; try { + ClosedChannelException exception = null; for (;;) { Object msg = in.current(); if (msg == null) { @@ -368,7 +364,10 @@ public class LocalChannel extends AbstractChannel { peer.inboundBuffer.add(ReferenceCountUtil.retain(msg)); in.remove(); } else { - in.remove(DO_WRITE_CLOSED_CHANNEL_EXCEPTION); + if (exception == null) { + exception = new ClosedChannelException(); + } + in.remove(exception); } } catch (Throwable cause) { in.remove(cause); diff --git a/transport/src/main/java/io/netty/channel/local/LocalEventLoopGroup.java b/transport/src/main/java/io/netty/channel/local/LocalEventLoopGroup.java index 2bd3ff6..c2315a8 100644 --- a/transport/src/main/java/io/netty/channel/local/LocalEventLoopGroup.java +++ b/transport/src/main/java/io/netty/channel/local/LocalEventLoopGroup.java @@ -39,6 +39,15 @@ public class LocalEventLoopGroup extends DefaultEventLoopGroup { super(nThreads); } + /** + * Create a new instance with the default number of threads and the given {@link ThreadFactory}. + * + * @param threadFactory the {@link ThreadFactory} or {@code null} to use the default + */ + public LocalEventLoopGroup(ThreadFactory threadFactory) { + super(0, threadFactory); + } + /** * Create a new instance * diff --git a/transport/src/main/java/io/netty/channel/nio/AbstractNioChannel.java b/transport/src/main/java/io/netty/channel/nio/AbstractNioChannel.java index 01467b9..a1c4520 100644 --- a/transport/src/main/java/io/netty/channel/nio/AbstractNioChannel.java +++ b/transport/src/main/java/io/netty/channel/nio/AbstractNioChannel.java @@ -29,7 +29,6 @@ import io.netty.channel.ConnectTimeoutException; import io.netty.channel.EventLoop; import io.netty.util.ReferenceCountUtil; import io.netty.util.ReferenceCounted; -import io.netty.util.internal.ThrowableUtil; import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; @@ -51,9 +50,6 @@ public abstract class AbstractNioChannel extends AbstractChannel { private static final InternalLogger logger = InternalLoggerFactory.getInstance(AbstractNioChannel.class); - private static final ClosedChannelException DO_CLOSE_CLOSED_CHANNEL_EXCEPTION = ThrowableUtil.unknownStackTrace( - new ClosedChannelException(), AbstractNioChannel.class, "doClose()"); - private final SelectableChannel ch; protected final int readInterestOp; volatile SelectionKey selectionKey; @@ -90,10 +86,8 @@ public abstract class AbstractNioChannel extends AbstractChannel { try { ch.close(); } catch (IOException e2) { - if (logger.isWarnEnabled()) { - logger.warn( + logger.warn( "Failed to close a partially initialized socket.", e2); - } } throw new ChannelException("Failed to enter non-blocking mode.", e); @@ -505,7 +499,7 @@ public abstract class AbstractNioChannel extends AbstractChannel { ChannelPromise promise = connectPromise; if (promise != null) { // Use tryFailure() instead of setFailure() to avoid the race against cancel(). - promise.tryFailure(DO_CLOSE_CLOSED_CHANNEL_EXCEPTION); + promise.tryFailure(new ClosedChannelException()); connectPromise = null; } diff --git a/transport/src/main/java/io/netty/channel/nio/NioEventLoop.java b/transport/src/main/java/io/netty/channel/nio/NioEventLoop.java index 100065f..9962778 100644 --- a/transport/src/main/java/io/netty/channel/nio/NioEventLoop.java +++ b/transport/src/main/java/io/netty/channel/nio/NioEventLoop.java @@ -19,10 +19,12 @@ import io.netty.channel.Channel; import io.netty.channel.ChannelException; import io.netty.channel.EventLoop; import io.netty.channel.EventLoopException; +import io.netty.channel.EventLoopTaskQueueFactory; import io.netty.channel.SelectStrategy; import io.netty.channel.SingleThreadEventLoop; import io.netty.util.IntSupplier; import io.netty.util.concurrent.RejectedExecutionHandler; +import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.PlatformDependent; import io.netty.util.internal.ReflectionUtil; import io.netty.util.internal.SystemPropertyUtil; @@ -45,8 +47,7 @@ import java.util.Iterator; import java.util.Queue; import java.util.Set; import java.util.concurrent.Executor; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; /** * {@link SingleThreadEventLoop} implementation which register the {@link Channel}'s to a @@ -116,13 +117,14 @@ public final class NioEventLoop extends SingleThreadEventLoop { private final SelectorProvider provider; - /** - * Boolean that controls determines if a blocked Selector.select should - * break out of its selection process. In our case we use a timeout for - * the select method and the select method will block for that time unless - * waken up. - */ - private final AtomicBoolean wakenUp = new AtomicBoolean(); + private static final long AWAKE = -1L; + private static final long NONE = Long.MAX_VALUE; + + // nextWakeupNanos is: + // AWAKE when EL is awake + // NONE when EL is waiting with no wakeup scheduled + // other value T when EL is waiting with wakeup scheduled at time T + private final AtomicLong nextWakeupNanos = new AtomicLong(AWAKE); private final SelectStrategy selectStrategy; @@ -131,19 +133,23 @@ public final class NioEventLoop extends SingleThreadEventLoop { private boolean needsToSelectAgain; NioEventLoop(NioEventLoopGroup parent, Executor executor, SelectorProvider selectorProvider, - SelectStrategy strategy, RejectedExecutionHandler rejectedExecutionHandler) { - super(parent, executor, false, DEFAULT_MAX_PENDING_TASKS, rejectedExecutionHandler); - if (selectorProvider == null) { - throw new NullPointerException("selectorProvider"); - } - if (strategy == null) { - throw new NullPointerException("selectStrategy"); - } - provider = selectorProvider; + SelectStrategy strategy, RejectedExecutionHandler rejectedExecutionHandler, + EventLoopTaskQueueFactory queueFactory) { + super(parent, executor, false, newTaskQueue(queueFactory), newTaskQueue(queueFactory), + rejectedExecutionHandler); + this.provider = ObjectUtil.checkNotNull(selectorProvider, "selectorProvider"); + this.selectStrategy = ObjectUtil.checkNotNull(strategy, "selectStrategy"); final SelectorTuple selectorTuple = openSelector(); - selector = selectorTuple.selector; - unwrappedSelector = selectorTuple.unwrappedSelector; - selectStrategy = strategy; + this.selector = selectorTuple.selector; + this.unwrappedSelector = selectorTuple.unwrappedSelector; + } + + private static Queue<Runnable> newTaskQueue( + EventLoopTaskQueueFactory queueFactory) { + if (queueFactory == null) { + return newTaskQueue0(DEFAULT_MAX_PENDING_TASKS); + } + return queueFactory.newTaskQueue(DEFAULT_MAX_PENDING_TASKS); } private static final class SelectorTuple { @@ -265,9 +271,13 @@ public final class NioEventLoop extends SingleThreadEventLoop { @Override protected Queue<Runnable> newTaskQueue(int maxPendingTasks) { + return newTaskQueue0(maxPendingTasks); + } + + private static Queue<Runnable> newTaskQueue0(int maxPendingTasks) { // This event loop never calls takeTask() return maxPendingTasks == Integer.MAX_VALUE ? PlatformDependent.<Runnable>newMpscQueue() - : PlatformDependent.<Runnable>newMpscQueue(maxPendingTasks); + : PlatformDependent.<Runnable>newMpscQueue(maxPendingTasks); } /** @@ -276,9 +286,7 @@ public final class NioEventLoop extends SingleThreadEventLoop { * be executed by this event loop when the {@link SelectableChannel} is ready. */ public void register(final SelectableChannel ch, final int interestOps, final NioTask<?> task) { - if (ch == null) { - throw new NullPointerException("ch"); - } + ObjectUtil.checkNotNull(ch, "ch"); if (interestOps == 0) { throw new IllegalArgumentException("interestOps must be non-zero."); } @@ -286,9 +294,7 @@ public final class NioEventLoop extends SingleThreadEventLoop { throw new IllegalArgumentException( "invalid interestOps: " + interestOps + "(validOps: " + ch.validOps() + ')'); } - if (task == null) { - throw new NullPointerException("task"); - } + ObjectUtil.checkNotNull(task, "task"); if (isShutdown()) { throw new IllegalStateException("event loop shut down"); @@ -329,8 +335,10 @@ public final class NioEventLoop extends SingleThreadEventLoop { } /** - * Sets the percentage of the desired amount of time spent for I/O in the event loop. The default value is - * {@code 50}, which means the event loop will try to spend the same amount of time for I/O as for non-I/O tasks. + * Sets the percentage of the desired amount of time spent for I/O in the event loop. Value range from 1-100. + * The default value is {@code 50}, which means the event loop will try to spend the same amount of time for I/O + * as for non-I/O tasks. The lower the number the more time can be spent on non-I/O tasks. If value set to + * {@code 100}, this feature will be disabled and event loop will not attempt to balance I/O and non-I/O tasks. */ public void setIoRatio(int ioRatio) { if (ioRatio <= 0 || ioRatio > 100) { @@ -356,6 +364,11 @@ public final class NioEventLoop extends SingleThreadEventLoop { rebuildSelector0(); } + @Override + public int registeredChannels() { + return selector.keys().size() - cancelledKeys; + } + private void rebuildSelector0() { final Selector oldSelector = selector; final SelectorTuple newSelectorTuple; @@ -420,10 +433,13 @@ public final class NioEventLoop extends SingleThreadEventLoop { @Override protected void run() { + int selectCnt = 0; for (;;) { try { + int strategy; try { - switch (selectStrategy.calculateStrategy(selectNowSupplier, hasTasks())) { + strategy = selectStrategy.calculateStrategy(selectNowSupplier, hasTasks()); + switch (strategy) { case SelectStrategy.CONTINUE: continue; @@ -431,38 +447,19 @@ public final class NioEventLoop extends SingleThreadEventLoop { // fall-through to SELECT since the busy-wait is not supported with NIO case SelectStrategy.SELECT: - select(wakenUp.getAndSet(false)); - - // 'wakenUp.compareAndSet(false, true)' is always evaluated - // before calling 'selector.wakeup()' to reduce the wake-up - // overhead. (Selector.wakeup() is an expensive operation.) - // - // However, there is a race condition in this approach. - // The race condition is triggered when 'wakenUp' is set to - // true too early. - // - // 'wakenUp' is set to true too early if: - // 1) Selector is waken up between 'wakenUp.set(false)' and - // 'selector.select(...)'. (BAD) - // 2) Selector is waken up between 'selector.select(...)' and - // 'if (wakenUp.get()) { ... }'. (OK) - // - // In the first case, 'wakenUp' is set to true and the - // following 'selector.select(...)' will wake up immediately. - // Until 'wakenUp' is set to false again in the next round, - // 'wakenUp.compareAndSet(false, true)' will fail, and therefore - // any attempt to wake up the Selector will fail, too, causing - // the following 'selector.select(...)' call to block - // unnecessarily. - // - // To fix this problem, we wake up the selector again if wakenUp - // is true immediately after selector.select(...). - // It is inefficient in that it wakes up the selector for both - // the first case (BAD - wake-up required) and the second case - // (OK - no wake-up required). - - if (wakenUp.get()) { - selector.wakeup(); + long curDeadlineNanos = nextScheduledTaskDeadlineNanos(); + if (curDeadlineNanos == -1L) { + curDeadlineNanos = NONE; // nothing on the calendar + } + nextWakeupNanos.set(curDeadlineNanos); + try { + if (!hasTasks()) { + strategy = select(curDeadlineNanos); + } + } finally { + // This update is just to help block unnecessary selector wakeups + // so use of lazySet is ok (no race condition) + nextWakeupNanos.lazySet(AWAKE); } // fall through default: @@ -471,29 +468,52 @@ public final class NioEventLoop extends SingleThreadEventLoop { // If we receive an IOException here its because the Selector is messed up. Let's rebuild // the selector and retry. https://github.com/netty/netty/issues/8566 rebuildSelector0(); + selectCnt = 0; handleLoopException(e); continue; } + selectCnt++; cancelledKeys = 0; needsToSelectAgain = false; final int ioRatio = this.ioRatio; + boolean ranTasks; if (ioRatio == 100) { try { - processSelectedKeys(); + if (strategy > 0) { + processSelectedKeys(); + } } finally { // Ensure we always run tasks. - runAllTasks(); + ranTasks = runAllTasks(); } - } else { + } else if (strategy > 0) { final long ioStartTime = System.nanoTime(); try { processSelectedKeys(); } finally { // Ensure we always run tasks. final long ioTime = System.nanoTime() - ioStartTime; - runAllTasks(ioTime * (100 - ioRatio) / ioRatio); + ranTasks = runAllTasks(ioTime * (100 - ioRatio) / ioRatio); } + } else { + ranTasks = runAllTasks(0); // This will run the minimum number of tasks + } + + if (ranTasks || strategy > 0) { + if (selectCnt > MIN_PREMATURE_SELECTOR_RETURNS && logger.isDebugEnabled()) { + logger.debug("Selector.select() returned prematurely {} times in a row for Selector {}.", + selectCnt - 1, selector); + } + selectCnt = 0; + } else if (unexpectedSelectorWakeup(selectCnt)) { // Unexpected wakeup (unusual case) + selectCnt = 0; + } + } catch (CancelledKeyException e) { + // Harmless exception - log anyway + if (logger.isDebugEnabled()) { + logger.debug(CancelledKeyException.class.getSimpleName() + " raised by a Selector {} - JDK bug?", + selector, e); } } catch (Throwable t) { handleLoopException(t); @@ -512,6 +532,33 @@ public final class NioEventLoop extends SingleThreadEventLoop { } } + // returns true if selectCnt should be reset + private boolean unexpectedSelectorWakeup(int selectCnt) { + if (Thread.interrupted()) { + // Thread was interrupted so reset selected keys and break so we not run into a busy loop. + // As this is most likely a bug in the handler of the user or it's client library we will + // also log it. + // + // See https://github.com/netty/netty/issues/2426 + if (logger.isDebugEnabled()) { + logger.debug("Selector.select() returned prematurely because " + + "Thread.currentThread().interrupt() was called. Use " + + "NioEventLoop.shutdownGracefully() to shutdown the NioEventLoop."); + } + return true; + } + if (SELECTOR_AUTO_REBUILD_THRESHOLD > 0 && + selectCnt >= SELECTOR_AUTO_REBUILD_THRESHOLD) { + // The selector returned prematurely many times in a row. + // Rebuild the selector to work around the problem. + logger.warn("Selector.select() returned prematurely {} times in a row; rebuilding Selector {}.", + selectCnt, selector); + rebuildSelector(); + return true; + } + return false; + } + private static void handleLoopException(Throwable t) { logger.warn("Unexpected exception in the selector loop.", t); @@ -550,15 +597,6 @@ public final class NioEventLoop extends SingleThreadEventLoop { } } - @Override - protected Runnable pollTask() { - Runnable task = super.pollTask(); - if (needsToSelectAgain) { - selectAgain(); - } - return task; - } - private void processSelectedKeysPlain(Set<SelectionKey> selectedKeys) { // check if the set is empty and if so just return to not create garbage by // creating a new Iterator every time even if there is nothing to process. @@ -643,11 +681,10 @@ public final class NioEventLoop extends SingleThreadEventLoop { // and thus the SelectionKey could be cancelled as part of the deregistration process, but the channel is // still healthy and should not be closed. // See https://github.com/netty/netty/issues/5125 - if (eventLoop != this || eventLoop == null) { - return; + if (eventLoop == this) { + // close the channel if the key is not valid anymore + unsafe.close(unsafe.voidPromise()); } - // close the channel if the key is not valid anymore - unsafe.close(unsafe.voidPromise()); return; } @@ -736,122 +773,38 @@ public final class NioEventLoop extends SingleThreadEventLoop { @Override protected void wakeup(boolean inEventLoop) { - if (!inEventLoop && wakenUp.compareAndSet(false, true)) { + if (!inEventLoop && nextWakeupNanos.getAndSet(AWAKE) != AWAKE) { selector.wakeup(); } } + @Override + protected boolean beforeScheduledTaskSubmitted(long deadlineNanos) { + // Note this is also correct for the nextWakeupNanos == -1 (AWAKE) case + return deadlineNanos < nextWakeupNanos.get(); + } + + @Override + protected boolean afterScheduledTaskSubmitted(long deadlineNanos) { + // Note this is also correct for the nextWakeupNanos == -1 (AWAKE) case + return deadlineNanos < nextWakeupNanos.get(); + } + Selector unwrappedSelector() { return unwrappedSelector; } int selectNow() throws IOException { - try { - return selector.selectNow(); - } finally { - // restore wakeup state if needed - if (wakenUp.get()) { - selector.wakeup(); - } - } + return selector.selectNow(); } - private void select(boolean oldWakenUp) throws IOException { - Selector selector = this.selector; - try { - int selectCnt = 0; - long currentTimeNanos = System.nanoTime(); - long selectDeadLineNanos = currentTimeNanos + delayNanos(currentTimeNanos); - - for (;;) { - long timeoutMillis = (selectDeadLineNanos - currentTimeNanos + 500000L) / 1000000L; - if (timeoutMillis <= 0) { - if (selectCnt == 0) { - selector.selectNow(); - selectCnt = 1; - } - break; - } - - // If a task was submitted when wakenUp value was true, the task didn't get a chance to call - // Selector#wakeup. So we need to check task queue again before executing select operation. - // If we don't, the task might be pended until select operation was timed out. - // It might be pended until idle timeout if IdleStateHandler existed in pipeline. - if (hasTasks() && wakenUp.compareAndSet(false, true)) { - selector.selectNow(); - selectCnt = 1; - break; - } - - int selectedKeys = selector.select(timeoutMillis); - selectCnt ++; - - if (selectedKeys != 0 || oldWakenUp || wakenUp.get() || hasTasks() || hasScheduledTasks()) { - // - Selected something, - // - waken up by user, or - // - the task queue has a pending task. - // - a scheduled task is ready for processing - break; - } - if (Thread.interrupted()) { - // Thread was interrupted so reset selected keys and break so we not run into a busy loop. - // As this is most likely a bug in the handler of the user or it's client library we will - // also log it. - // - // See https://github.com/netty/netty/issues/2426 - if (logger.isDebugEnabled()) { - logger.debug("Selector.select() returned prematurely because " + - "Thread.currentThread().interrupt() was called. Use " + - "NioEventLoop.shutdownGracefully() to shutdown the NioEventLoop."); - } - selectCnt = 1; - break; - } - - long time = System.nanoTime(); - if (time - TimeUnit.MILLISECONDS.toNanos(timeoutMillis) >= currentTimeNanos) { - // timeoutMillis elapsed without anything selected. - selectCnt = 1; - } else if (SELECTOR_AUTO_REBUILD_THRESHOLD > 0 && - selectCnt >= SELECTOR_AUTO_REBUILD_THRESHOLD) { - // The code exists in an extra method to ensure the method is not too big to inline as this - // branch is not very likely to get hit very frequently. - selector = selectRebuildSelector(selectCnt); - selectCnt = 1; - break; - } - - currentTimeNanos = time; - } - - if (selectCnt > MIN_PREMATURE_SELECTOR_RETURNS) { - if (logger.isDebugEnabled()) { - logger.debug("Selector.select() returned prematurely {} times in a row for Selector {}.", - selectCnt - 1, selector); - } - } - } catch (CancelledKeyException e) { - if (logger.isDebugEnabled()) { - logger.debug(CancelledKeyException.class.getSimpleName() + " raised by a Selector {} - JDK bug?", - selector, e); - } - // Harmless exception - log anyway + private int select(long deadlineNanos) throws IOException { + if (deadlineNanos == NONE) { + return selector.select(); } - } - - private Selector selectRebuildSelector(int selectCnt) throws IOException { - // The selector returned prematurely many times in a row. - // Rebuild the selector to work around the problem. - logger.warn( - "Selector.select() returned prematurely {} times in a row; rebuilding Selector {}.", - selectCnt, selector); - - rebuildSelector(); - Selector selector = this.selector; - - // Select again to populate selectedKeys. - selector.selectNow(); - return selector; + // Timeout will only be 0 if deadline is within 5 microsecs + long timeoutMillis = deadlineToDelayNanos(deadlineNanos + 995000L) / 1000000L; + return timeoutMillis <= 0 ? selector.selectNow() : selector.select(timeoutMillis); } private void selectAgain() { diff --git a/transport/src/main/java/io/netty/channel/nio/NioEventLoopGroup.java b/transport/src/main/java/io/netty/channel/nio/NioEventLoopGroup.java index 833b754..68278cc 100644 --- a/transport/src/main/java/io/netty/channel/nio/NioEventLoopGroup.java +++ b/transport/src/main/java/io/netty/channel/nio/NioEventLoopGroup.java @@ -18,6 +18,7 @@ package io.netty.channel.nio; import io.netty.channel.Channel; import io.netty.channel.EventLoop; import io.netty.channel.DefaultSelectStrategyFactory; +import io.netty.channel.EventLoopTaskQueueFactory; import io.netty.channel.MultithreadEventLoopGroup; import io.netty.channel.SelectStrategyFactory; import io.netty.util.concurrent.EventExecutor; @@ -51,6 +52,14 @@ public class NioEventLoopGroup extends MultithreadEventLoopGroup { this(nThreads, (Executor) null); } + /** + * Create a new instance using the default number of threads, the given {@link ThreadFactory} and the + * {@link SelectorProvider} which is returned by {@link SelectorProvider#provider()}. + */ + public NioEventLoopGroup(ThreadFactory threadFactory) { + this(0, threadFactory, SelectorProvider.provider()); + } + /** * Create a new instance using the specified number of threads, the given {@link ThreadFactory} and the * {@link SelectorProvider} which is returned by {@link SelectorProvider#provider()}. @@ -101,6 +110,15 @@ public class NioEventLoopGroup extends MultithreadEventLoopGroup { super(nThreads, executor, chooserFactory, selectorProvider, selectStrategyFactory, rejectedExecutionHandler); } + public NioEventLoopGroup(int nThreads, Executor executor, EventExecutorChooserFactory chooserFactory, + final SelectorProvider selectorProvider, + final SelectStrategyFactory selectStrategyFactory, + final RejectedExecutionHandler rejectedExecutionHandler, + final EventLoopTaskQueueFactory taskQueueFactory) { + super(nThreads, executor, chooserFactory, selectorProvider, selectStrategyFactory, + rejectedExecutionHandler, taskQueueFactory); + } + /** * Sets the percentage of the desired amount of time spent for I/O in the child event loops. The default value is * {@code 50}, which means the event loop will try to spend the same amount of time for I/O as for non-I/O tasks. @@ -123,7 +141,8 @@ public class NioEventLoopGroup extends MultithreadEventLoopGroup { @Override protected EventLoop newChild(Executor executor, Object... args) throws Exception { + EventLoopTaskQueueFactory queueFactory = args.length == 4 ? (EventLoopTaskQueueFactory) args[3] : null; return new NioEventLoop(this, executor, (SelectorProvider) args[0], - ((SelectStrategyFactory) args[1]).newSelectStrategy(), (RejectedExecutionHandler) args[2]); + ((SelectStrategyFactory) args[1]).newSelectStrategy(), (RejectedExecutionHandler) args[2], queueFactory); } } diff --git a/transport/src/main/java/io/netty/channel/oio/OioByteStreamChannel.java b/transport/src/main/java/io/netty/channel/oio/OioByteStreamChannel.java index d352823..d21a8b5 100644 --- a/transport/src/main/java/io/netty/channel/oio/OioByteStreamChannel.java +++ b/transport/src/main/java/io/netty/channel/oio/OioByteStreamChannel.java @@ -19,6 +19,7 @@ import io.netty.buffer.ByteBuf; import io.netty.channel.Channel; import io.netty.channel.FileRegion; import io.netty.channel.RecvByteBufAllocator; +import io.netty.util.internal.ObjectUtil; import java.io.EOFException; import java.io.IOException; @@ -75,14 +76,8 @@ public abstract class OioByteStreamChannel extends AbstractOioByteChannel { if (this.os != null) { throw new IllegalStateException("output was set already"); } - if (is == null) { - throw new NullPointerException("is"); - } - if (os == null) { - throw new NullPointerException("os"); - } - this.is = is; - this.os = os; + this.is = ObjectUtil.checkNotNull(is, "is"); + this.os = ObjectUtil.checkNotNull(os, "os"); } @Override diff --git a/transport/src/main/java/io/netty/channel/oio/OioEventLoopGroup.java b/transport/src/main/java/io/netty/channel/oio/OioEventLoopGroup.java index 91a2c4b..8c9bff4 100644 --- a/transport/src/main/java/io/netty/channel/oio/OioEventLoopGroup.java +++ b/transport/src/main/java/io/netty/channel/oio/OioEventLoopGroup.java @@ -24,7 +24,6 @@ import io.netty.channel.EventLoopGroup; import io.netty.channel.ThreadPerChannelEventLoopGroup; import java.util.concurrent.Executor; -import java.util.concurrent.Executors; import java.util.concurrent.ThreadFactory; /** @@ -53,7 +52,7 @@ public class OioEventLoopGroup extends ThreadPerChannelEventLoopGroup { * Use {@code 0} to use no limit */ public OioEventLoopGroup(int maxChannels) { - this(maxChannels, Executors.defaultThreadFactory()); + this(maxChannels, (ThreadFactory) null); } /** diff --git a/transport/src/main/java/io/netty/channel/pool/AbstractChannelPoolMap.java b/transport/src/main/java/io/netty/channel/pool/AbstractChannelPoolMap.java index 4b7213e..a58c93f 100644 --- a/transport/src/main/java/io/netty/channel/pool/AbstractChannelPoolMap.java +++ b/transport/src/main/java/io/netty/channel/pool/AbstractChannelPoolMap.java @@ -15,6 +15,10 @@ */ package io.netty.channel.pool; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.GenericFutureListener; +import io.netty.util.concurrent.GlobalEventExecutor; +import io.netty.util.concurrent.Promise; import io.netty.util.internal.PlatformDependent; import io.netty.util.internal.ReadOnlyIterator; @@ -41,7 +45,7 @@ public abstract class AbstractChannelPoolMap<K, P extends ChannelPool> P old = map.putIfAbsent(key, pool); if (old != null) { // We need to destroy the newly created pool as we not use it. - pool.close(); + poolCloseAsyncIfSupported(pool); pool = old; } } @@ -51,17 +55,65 @@ public abstract class AbstractChannelPoolMap<K, P extends ChannelPool> * Remove the {@link ChannelPool} from this {@link AbstractChannelPoolMap}. Returns {@code true} if removed, * {@code false} otherwise. * + * If the removed pool extends {@link SimpleChannelPool} it will be closed asynchronously to avoid blocking in + * this method. + * * Please note that {@code null} keys are not allowed. */ public final boolean remove(K key) { P pool = map.remove(checkNotNull(key, "key")); if (pool != null) { - pool.close(); + poolCloseAsyncIfSupported(pool); return true; } return false; } + /** + * Remove the {@link ChannelPool} from this {@link AbstractChannelPoolMap}. Returns a future that comletes with a + * {@code true} result if the pool has been removed by this call, otherwise the result is {@code false}. + * + * If the removed pool extends {@link SimpleChannelPool} it will be closed asynchronously to avoid blocking in + * this method. The returned future will be completed once this asynchronous pool close operation completes. + */ + private Future<Boolean> removeAsyncIfSupported(K key) { + P pool = map.remove(checkNotNull(key, "key")); + if (pool != null) { + final Promise<Boolean> removePromise = GlobalEventExecutor.INSTANCE.newPromise(); + poolCloseAsyncIfSupported(pool).addListener(new GenericFutureListener<Future<? super Void>>() { + @Override + public void operationComplete(Future<? super Void> future) throws Exception { + if (future.isSuccess()) { + removePromise.setSuccess(Boolean.TRUE); + } else { + removePromise.setFailure(future.cause()); + } + } + }); + return removePromise; + } + return GlobalEventExecutor.INSTANCE.newSucceededFuture(Boolean.FALSE); + } + + /** + * If the pool implementation supports asynchronous close, then use it to avoid a blocking close call in case + * the ChannelPoolMap operations are called from an EventLoop. + * + * @param pool the ChannelPool to be closed + */ + private static Future<Void> poolCloseAsyncIfSupported(ChannelPool pool) { + if (pool instanceof SimpleChannelPool) { + return ((SimpleChannelPool) pool).closeAsync(); + } else { + try { + pool.close(); + return GlobalEventExecutor.INSTANCE.newSucceededFuture(null); + } catch (Exception e) { + return GlobalEventExecutor.INSTANCE.newFailedFuture(e); + } + } + } + @Override public final Iterator<Entry<K, P>> iterator() { return new ReadOnlyIterator<Entry<K, P>>(map.entrySet().iterator()); @@ -94,7 +146,8 @@ public abstract class AbstractChannelPoolMap<K, P extends ChannelPool> @Override public final void close() { for (K key: map.keySet()) { - remove(key); + // Wait for remove to finish to ensure that resources are released before returning from close + removeAsyncIfSupported(key).syncUninterruptibly(); } } } diff --git a/transport/src/main/java/io/netty/channel/pool/FixedChannelPool.java b/transport/src/main/java/io/netty/channel/pool/FixedChannelPool.java index 5ca376f..7b62213 100644 --- a/transport/src/main/java/io/netty/channel/pool/FixedChannelPool.java +++ b/transport/src/main/java/io/netty/channel/pool/FixedChannelPool.java @@ -23,12 +23,12 @@ import io.netty.util.concurrent.FutureListener; import io.netty.util.concurrent.GlobalEventExecutor; import io.netty.util.concurrent.Promise; import io.netty.util.internal.ObjectUtil; -import io.netty.util.internal.ThrowableUtil; import java.nio.channels.ClosedChannelException; import java.util.ArrayDeque; import java.util.Queue; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.Callable; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -38,18 +38,7 @@ import java.util.concurrent.TimeoutException; * number of concurrent connections. */ public class FixedChannelPool extends SimpleChannelPool { - private static final IllegalStateException FULL_EXCEPTION = ThrowableUtil.unknownStackTrace( - new IllegalStateException("Too many outstanding acquire operations"), - FixedChannelPool.class, "acquire0(...)"); - private static final TimeoutException TIMEOUT_EXCEPTION = ThrowableUtil.unknownStackTrace( - new TimeoutException("Acquire operation took longer then configured maximum time"), - FixedChannelPool.class, "<init>(...)"); - static final IllegalStateException POOL_CLOSED_ON_RELEASE_EXCEPTION = ThrowableUtil.unknownStackTrace( - new IllegalStateException("FixedChannelPool was closed"), - FixedChannelPool.class, "release(...)"); - static final IllegalStateException POOL_CLOSED_ON_ACQUIRE_EXCEPTION = ThrowableUtil.unknownStackTrace( - new IllegalStateException("FixedChannelPool was closed"), - FixedChannelPool.class, "acquire0(...)"); + public enum AcquireTimeoutAction { /** * Create a new connection when the timeout is detected. @@ -205,7 +194,13 @@ public class FixedChannelPool extends SimpleChannelPool { @Override public void onTimeout(AcquireTask task) { // Fail the promise as we timed out. - task.promise.setFailure(TIMEOUT_EXCEPTION); + task.promise.setFailure(new TimeoutException( + "Acquire operation took longer then configured maximum time") { + @Override + public Throwable fillInStackTrace() { + return this; + } + }); } }; break; @@ -258,7 +253,7 @@ public class FixedChannelPool extends SimpleChannelPool { assert executor.inEventLoop(); if (closed) { - promise.setFailure(POOL_CLOSED_ON_ACQUIRE_EXCEPTION); + promise.setFailure(new IllegalStateException("FixedChannelPool was closed")); return; } if (acquiredChannelCount.get() < maxConnections) { @@ -273,7 +268,7 @@ public class FixedChannelPool extends SimpleChannelPool { super.acquire(p); } else { if (pendingAcquireCount >= maxPendingAcquires) { - promise.setFailure(FULL_EXCEPTION); + tooManyOutstanding(promise); } else { AcquireTask task = new AcquireTask(promise); if (pendingAcquireQueue.offer(task)) { @@ -283,7 +278,7 @@ public class FixedChannelPool extends SimpleChannelPool { task.timeoutFuture = executor.schedule(timeoutTask, acquireTimeoutNanos, TimeUnit.NANOSECONDS); } } else { - promise.setFailure(FULL_EXCEPTION); + tooManyOutstanding(promise); } } @@ -291,6 +286,10 @@ public class FixedChannelPool extends SimpleChannelPool { } } + private void tooManyOutstanding(Promise<?> promise) { + promise.setFailure(new IllegalStateException("Too many outstanding acquire operations")); + } + @Override public Future<Void> release(final Channel channel, final Promise<Void> promise) { ObjectUtil.checkNotNull(promise, "promise"); @@ -304,7 +303,7 @@ public class FixedChannelPool extends SimpleChannelPool { if (closed) { // Since the pool is closed, we have no choice but to close the channel channel.close(); - promise.setFailure(POOL_CLOSED_ON_RELEASE_EXCEPTION); + promise.setFailure(new IllegalStateException("FixedChannelPool was closed")); return; } @@ -366,7 +365,7 @@ public class FixedChannelPool extends SimpleChannelPool { final long expireNanoTime = System.nanoTime() + acquireTimeoutNanos; ScheduledFuture<?> timeoutFuture; - public AcquireTask(Promise<Channel> promise) { + AcquireTask(Promise<Channel> promise) { super(promise); // We need to create a new promise as we need to ensure the AcquireListener runs in the correct // EventLoop. @@ -415,7 +414,7 @@ public class FixedChannelPool extends SimpleChannelPool { // Since the pool is closed, we have no choice but to close the channel future.getNow().close(); } - originalPromise.setFailure(POOL_CLOSED_ON_ACQUIRE_EXCEPTION); + originalPromise.setFailure(new IllegalStateException("FixedChannelPool was closed")); return; } @@ -443,19 +442,47 @@ public class FixedChannelPool extends SimpleChannelPool { @Override public void close() { + try { + closeAsync().await(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException(e); + } + } + + /** + * Closes the pool in an async manner. + * + * @return Future which represents completion of the close task + */ + @Override + public Future<Void> closeAsync() { if (executor.inEventLoop()) { - close0(); + return close0(); } else { - executor.submit(new Runnable() { + final Promise<Void> closeComplete = executor.newPromise(); + executor.execute(new Runnable() { @Override public void run() { - close0(); + close0().addListener(new FutureListener<Void>() { + @Override + public void operationComplete(Future<Void> f) throws Exception { + if (f.isSuccess()) { + closeComplete.setSuccess(null); + } else { + closeComplete.setFailure(f.cause()); + } + } + }); } - }).awaitUninterruptibly(); + }); + return closeComplete; } } - private void close0() { + private Future<Void> close0() { + assert executor.inEventLoop(); + if (!closed) { closed = true; for (;;) { @@ -474,12 +501,15 @@ public class FixedChannelPool extends SimpleChannelPool { // Ensure we dispatch this on another Thread as close0 will be called from the EventExecutor and we need // to ensure we will not block in a EventExecutor. - GlobalEventExecutor.INSTANCE.execute(new Runnable() { + return GlobalEventExecutor.INSTANCE.submit(new Callable<Void>() { @Override - public void run() { + public Void call() throws Exception { FixedChannelPool.super.close(); + return null; } }); } + + return GlobalEventExecutor.INSTANCE.newSucceededFuture(null); } } diff --git a/transport/src/main/java/io/netty/channel/pool/SimpleChannelPool.java b/transport/src/main/java/io/netty/channel/pool/SimpleChannelPool.java index 6fcfd44..050de39 100644 --- a/transport/src/main/java/io/netty/channel/pool/SimpleChannelPool.java +++ b/transport/src/main/java/io/netty/channel/pool/SimpleChannelPool.java @@ -24,11 +24,12 @@ import io.netty.channel.EventLoop; import io.netty.util.AttributeKey; import io.netty.util.concurrent.Future; import io.netty.util.concurrent.FutureListener; +import io.netty.util.concurrent.GlobalEventExecutor; import io.netty.util.concurrent.Promise; import io.netty.util.internal.PlatformDependent; -import io.netty.util.internal.ThrowableUtil; import java.util.Deque; +import java.util.concurrent.Callable; import static io.netty.util.internal.ObjectUtil.*; @@ -40,10 +41,8 @@ import static io.netty.util.internal.ObjectUtil.*; * */ public class SimpleChannelPool implements ChannelPool { - private static final AttributeKey<SimpleChannelPool> POOL_KEY = AttributeKey.newInstance("channelPool"); - private static final IllegalStateException FULL_EXCEPTION = ThrowableUtil.unknownStackTrace( - new IllegalStateException("ChannelPool full"), SimpleChannelPool.class, "releaseAndOffer(...)"); - + private static final AttributeKey<SimpleChannelPool> POOL_KEY = + AttributeKey.newInstance("io.netty.channel.pool.SimpleChannelPool"); private final Deque<Channel> deque = PlatformDependent.newConcurrentDeque(); private final ChannelPoolHandler handler; private final ChannelHealthChecker healthCheck; @@ -160,8 +159,7 @@ public class SimpleChannelPool implements ChannelPool { @Override public Future<Channel> acquire(final Promise<Channel> promise) { - checkNotNull(promise, "promise"); - return acquireHealthyFromPoolOrNew(promise); + return acquireHealthyFromPoolOrNew(checkNotNull(promise, "promise")); } /** @@ -206,9 +204,10 @@ public class SimpleChannelPool implements ChannelPool { return promise; } - private void notifyConnect(ChannelFuture future, Promise<Channel> promise) { + private void notifyConnect(ChannelFuture future, Promise<Channel> promise) throws Exception { if (future.isSuccess()) { Channel channel = future.channel(); + handler.channelAcquired(channel); if (!promise.trySuccess(channel)) { // Promise was completed in the meantime (like cancelled), just release the channel again release(channel); @@ -351,16 +350,21 @@ public class SimpleChannelPool implements ChannelPool { handler.channelReleased(channel); promise.setSuccess(null); } else { - closeAndFail(channel, FULL_EXCEPTION, promise); + closeAndFail(channel, new IllegalStateException("ChannelPool full") { + @Override + public Throwable fillInStackTrace() { + return this; + } + }, promise); } } - private static void closeChannel(Channel channel) { + private void closeChannel(Channel channel) { channel.attr(POOL_KEY).getAndSet(null); channel.close(); } - private static void closeAndFail(Channel channel, Throwable cause, Promise<?> promise) { + private void closeAndFail(Channel channel, Throwable cause, Promise<?> promise) { closeChannel(channel); promise.tryFailure(cause); } @@ -398,4 +402,20 @@ public class SimpleChannelPool implements ChannelPool { channel.close().awaitUninterruptibly(); } } + + /** + * Closes the pool in an async manner. + * + * @return Future which represents completion of the close task + */ + public Future<Void> closeAsync() { + // Execute close asynchronously in case this is being invoked on an eventloop to avoid blocking + return GlobalEventExecutor.INSTANCE.submit(new Callable<Void>() { + @Override + public Void call() throws Exception { + close(); + return null; + } + }); + } } diff --git a/transport/src/main/java/io/netty/channel/socket/DefaultDatagramChannelConfig.java b/transport/src/main/java/io/netty/channel/socket/DefaultDatagramChannelConfig.java index 5d3418c..515a187 100644 --- a/transport/src/main/java/io/netty/channel/socket/DefaultDatagramChannelConfig.java +++ b/transport/src/main/java/io/netty/channel/socket/DefaultDatagramChannelConfig.java @@ -23,6 +23,7 @@ import io.netty.channel.FixedRecvByteBufAllocator; import io.netty.channel.MessageSizeEstimator; import io.netty.channel.RecvByteBufAllocator; import io.netty.channel.WriteBufferWaterMark; +import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.PlatformDependent; import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; @@ -52,10 +53,7 @@ public class DefaultDatagramChannelConfig extends DefaultChannelConfig implement */ public DefaultDatagramChannelConfig(DatagramChannel channel, DatagramSocket javaSocket) { super(channel, new FixedRecvByteBufAllocator(2048)); - if (javaSocket == null) { - throw new NullPointerException("javaSocket"); - } - this.javaSocket = javaSocket; + this.javaSocket = ObjectUtil.checkNotNull(javaSocket, "javaSocket"); } protected final DatagramSocket javaSocket() { diff --git a/transport/src/main/java/io/netty/channel/socket/DefaultServerSocketChannelConfig.java b/transport/src/main/java/io/netty/channel/socket/DefaultServerSocketChannelConfig.java index 57ac53d..eb89644 100644 --- a/transport/src/main/java/io/netty/channel/socket/DefaultServerSocketChannelConfig.java +++ b/transport/src/main/java/io/netty/channel/socket/DefaultServerSocketChannelConfig.java @@ -23,6 +23,7 @@ import io.netty.channel.MessageSizeEstimator; import io.netty.channel.RecvByteBufAllocator; import io.netty.channel.WriteBufferWaterMark; import io.netty.util.NetUtil; +import io.netty.util.internal.ObjectUtil; import java.net.ServerSocket; import java.net.SocketException; @@ -31,6 +32,7 @@ import java.util.Map; import static io.netty.channel.ChannelOption.SO_BACKLOG; import static io.netty.channel.ChannelOption.SO_RCVBUF; import static io.netty.channel.ChannelOption.SO_REUSEADDR; +import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero; /** * The default {@link ServerSocketChannelConfig} implementation. @@ -46,10 +48,7 @@ public class DefaultServerSocketChannelConfig extends DefaultChannelConfig */ public DefaultServerSocketChannelConfig(ServerSocketChannel channel, ServerSocket javaSocket) { super(channel); - if (javaSocket == null) { - throw new NullPointerException("javaSocket"); - } - this.javaSocket = javaSocket; + this.javaSocket = ObjectUtil.checkNotNull(javaSocket, "javaSocket"); } @Override @@ -141,9 +140,7 @@ public class DefaultServerSocketChannelConfig extends DefaultChannelConfig @Override public ServerSocketChannelConfig setBacklog(int backlog) { - if (backlog < 0) { - throw new IllegalArgumentException("backlog: " + backlog); - } + checkPositiveOrZero(backlog, "backlog"); this.backlog = backlog; return this; } diff --git a/transport/src/main/java/io/netty/channel/socket/DefaultSocketChannelConfig.java b/transport/src/main/java/io/netty/channel/socket/DefaultSocketChannelConfig.java index c154582..8ab4685 100644 --- a/transport/src/main/java/io/netty/channel/socket/DefaultSocketChannelConfig.java +++ b/transport/src/main/java/io/netty/channel/socket/DefaultSocketChannelConfig.java @@ -22,6 +22,7 @@ import io.netty.channel.DefaultChannelConfig; import io.netty.channel.MessageSizeEstimator; import io.netty.channel.RecvByteBufAllocator; import io.netty.channel.WriteBufferWaterMark; +import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.PlatformDependent; import java.net.Socket; @@ -44,10 +45,7 @@ public class DefaultSocketChannelConfig extends DefaultChannelConfig */ public DefaultSocketChannelConfig(SocketChannel channel, Socket javaSocket) { super(channel); - if (javaSocket == null) { - throw new NullPointerException("javaSocket"); - } - this.javaSocket = javaSocket; + this.javaSocket = ObjectUtil.checkNotNull(javaSocket, "javaSocket"); // Enable TCP_NODELAY by default if possible. if (PlatformDependent.canEnableTcpNoDelayByDefault()) { diff --git a/transport/src/main/java/io/netty/channel/socket/nio/NioChannelOption.java b/transport/src/main/java/io/netty/channel/socket/nio/NioChannelOption.java index 3f9550f..e2e7561 100644 --- a/transport/src/main/java/io/netty/channel/socket/nio/NioChannelOption.java +++ b/transport/src/main/java/io/netty/channel/socket/nio/NioChannelOption.java @@ -17,6 +17,7 @@ package io.netty.channel.socket.nio; import io.netty.channel.ChannelException; import io.netty.channel.ChannelOption; +import io.netty.util.internal.SuppressJava6Requirement; import java.io.IOException; import java.nio.channels.Channel; @@ -29,6 +30,7 @@ import java.util.Set; * Provides {@link ChannelOption} over a given {@link java.net.SocketOption} which is then passed through the underlying * {@link java.nio.channels.NetworkChannel}. */ +@SuppressJava6Requirement(reason = "Usage explicit by the user") public final class NioChannelOption<T> extends ChannelOption<T> { private final java.net.SocketOption<T> option; @@ -53,6 +55,7 @@ public final class NioChannelOption<T> extends ChannelOption<T> { // See https://github.com/netty/netty/issues/8166 // Internal helper methods to remove code duplication between Nio*Channel implementations. + @SuppressJava6Requirement(reason = "Usage guarded by java version check") static <T> boolean setOption(Channel jdkChannel, NioChannelOption<T> option, T value) { java.nio.channels.NetworkChannel channel = (java.nio.channels.NetworkChannel) jdkChannel; if (!channel.supportedOptions().contains(option.option)) { @@ -71,6 +74,7 @@ public final class NioChannelOption<T> extends ChannelOption<T> { } } + @SuppressJava6Requirement(reason = "Usage guarded by java version check") static <T> T getOption(Channel jdkChannel, NioChannelOption<T> option) { java.nio.channels.NetworkChannel channel = (java.nio.channels.NetworkChannel) jdkChannel; @@ -89,6 +93,7 @@ public final class NioChannelOption<T> extends ChannelOption<T> { } } + @SuppressJava6Requirement(reason = "Usage guarded by java version check") @SuppressWarnings("unchecked") static ChannelOption[] getOptions(Channel jdkChannel) { java.nio.channels.NetworkChannel channel = (java.nio.channels.NetworkChannel) jdkChannel; diff --git a/transport/src/main/java/io/netty/channel/socket/nio/NioDatagramChannel.java b/transport/src/main/java/io/netty/channel/socket/nio/NioDatagramChannel.java index 2fc314d..f706db3 100644 --- a/transport/src/main/java/io/netty/channel/socket/nio/NioDatagramChannel.java +++ b/transport/src/main/java/io/netty/channel/socket/nio/NioDatagramChannel.java @@ -30,10 +30,11 @@ import io.netty.channel.nio.AbstractNioMessageChannel; import io.netty.channel.socket.DatagramChannelConfig; import io.netty.channel.socket.DatagramPacket; import io.netty.channel.socket.InternetProtocolFamily; +import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.SocketUtils; import io.netty.util.internal.PlatformDependent; import io.netty.util.internal.StringUtil; -import io.netty.util.internal.UnstableApi; +import io.netty.util.internal.SuppressJava6Requirement; import java.io.IOException; import java.net.InetAddress; @@ -89,6 +90,7 @@ public final class NioDatagramChannel } } + @SuppressJava6Requirement(reason = "Usage guarded by java version check") private static DatagramChannel newSocket(SelectorProvider provider, InternetProtocolFamily ipFamily) { if (ipFamily == null) { return newSocket(provider); @@ -395,6 +397,7 @@ public final class NioDatagramChannel return joinGroup(multicastAddress, networkInterface, source, newPromise()); } + @SuppressJava6Requirement(reason = "Usage guarded by java version check") @Override public ChannelFuture joinGroup( InetAddress multicastAddress, NetworkInterface networkInterface, @@ -402,13 +405,8 @@ public final class NioDatagramChannel checkJavaVersion(); - if (multicastAddress == null) { - throw new NullPointerException("multicastAddress"); - } - - if (networkInterface == null) { - throw new NullPointerException("networkInterface"); - } + ObjectUtil.checkNotNull(multicastAddress, "multicastAddress"); + ObjectUtil.checkNotNull(networkInterface, "networkInterface"); try { MembershipKey key; @@ -475,18 +473,15 @@ public final class NioDatagramChannel return leaveGroup(multicastAddress, networkInterface, source, newPromise()); } + @SuppressJava6Requirement(reason = "Usage guarded by java version check") @Override public ChannelFuture leaveGroup( InetAddress multicastAddress, NetworkInterface networkInterface, InetAddress source, ChannelPromise promise) { checkJavaVersion(); - if (multicastAddress == null) { - throw new NullPointerException("multicastAddress"); - } - if (networkInterface == null) { - throw new NullPointerException("networkInterface"); - } + ObjectUtil.checkNotNull(multicastAddress, "multicastAddress"); + ObjectUtil.checkNotNull(networkInterface, "networkInterface"); synchronized (this) { if (memberships != null) { @@ -528,22 +523,17 @@ public final class NioDatagramChannel /** * Block the given sourceToBlock address for the given multicastAddress on the given networkInterface */ + @SuppressJava6Requirement(reason = "Usage guarded by java version check") @Override public ChannelFuture block( InetAddress multicastAddress, NetworkInterface networkInterface, InetAddress sourceToBlock, ChannelPromise promise) { checkJavaVersion(); - if (multicastAddress == null) { - throw new NullPointerException("multicastAddress"); - } - if (sourceToBlock == null) { - throw new NullPointerException("sourceToBlock"); - } + ObjectUtil.checkNotNull(multicastAddress, "multicastAddress"); + ObjectUtil.checkNotNull(sourceToBlock, "sourceToBlock"); + ObjectUtil.checkNotNull(networkInterface, "networkInterface"); - if (networkInterface == null) { - throw new NullPointerException("networkInterface"); - } synchronized (this) { if (memberships != null) { List<MembershipKey> keys = memberships.get(multicastAddress); diff --git a/transport/src/main/java/io/netty/channel/socket/nio/NioServerSocketChannel.java b/transport/src/main/java/io/netty/channel/socket/nio/NioServerSocketChannel.java index 128e531..565cb04 100644 --- a/transport/src/main/java/io/netty/channel/socket/nio/NioServerSocketChannel.java +++ b/transport/src/main/java/io/netty/channel/socket/nio/NioServerSocketChannel.java @@ -24,6 +24,7 @@ import io.netty.channel.nio.AbstractNioMessageChannel; import io.netty.channel.socket.DefaultServerSocketChannelConfig; import io.netty.channel.socket.ServerSocketChannelConfig; import io.netty.util.internal.PlatformDependent; +import io.netty.util.internal.SuppressJava6Requirement; import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; @@ -106,7 +107,9 @@ public class NioServerSocketChannel extends AbstractNioMessageChannel @Override public boolean isActive() { - return javaChannel().socket().isBound(); + // As java.nio.ServerSocketChannel.isBound() will continue to return true even after the channel was closed + // we will also need to check if it is open. + return isOpen() && javaChannel().socket().isBound(); } @Override @@ -124,6 +127,7 @@ public class NioServerSocketChannel extends AbstractNioMessageChannel return SocketUtils.localSocketAddress(javaChannel().socket()); } + @SuppressJava6Requirement(reason = "Usage guarded by java version check") @Override protected void doBind(SocketAddress localAddress) throws Exception { if (PlatformDependent.javaVersion() >= 7) { @@ -218,7 +222,6 @@ public class NioServerSocketChannel extends AbstractNioMessageChannel return super.getOption(option); } - @SuppressWarnings("unchecked") @Override public Map<ChannelOption<?>, Object> getOptions() { if (PlatformDependent.javaVersion() >= 7) { diff --git a/transport/src/main/java/io/netty/channel/socket/nio/NioSocketChannel.java b/transport/src/main/java/io/netty/channel/socket/nio/NioSocketChannel.java index 7443179..6295958 100644 --- a/transport/src/main/java/io/netty/channel/socket/nio/NioSocketChannel.java +++ b/transport/src/main/java/io/netty/channel/socket/nio/NioSocketChannel.java @@ -33,6 +33,7 @@ import io.netty.channel.socket.SocketChannelConfig; import io.netty.util.concurrent.GlobalEventExecutor; import io.netty.util.internal.PlatformDependent; import io.netty.util.internal.SocketUtils; +import io.netty.util.internal.SuppressJava6Requirement; import io.netty.util.internal.UnstableApi; import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; @@ -152,6 +153,7 @@ public class NioSocketChannel extends AbstractNioByteChannel implements io.netty return (InetSocketAddress) super.remoteAddress(); } + @SuppressJava6Requirement(reason = "Usage guarded by java version check") @UnstableApi @Override protected final void doShutdownOutput() throws Exception { @@ -270,6 +272,7 @@ public class NioSocketChannel extends AbstractNioByteChannel implements io.netty } } + @SuppressJava6Requirement(reason = "Usage guarded by java version check") private void shutdownInput0() throws Exception { if (PlatformDependent.javaVersion() >= 7) { javaChannel().shutdownInput(); @@ -496,7 +499,6 @@ public class NioSocketChannel extends AbstractNioByteChannel implements io.netty return super.getOption(option); } - @SuppressWarnings("unchecked") @Override public Map<ChannelOption<?>, Object> getOptions() { if (PlatformDependent.javaVersion() >= 7) { @@ -517,7 +519,7 @@ public class NioSocketChannel extends AbstractNioByteChannel implements io.netty // Multiply by 2 to give some extra space in case the OS can process write data faster than we can provide. int newSendBufferSize = getSendBufferSize() << 1; if (newSendBufferSize > 0) { - setMaxBytesPerGatheringWrite(getSendBufferSize() << 1); + setMaxBytesPerGatheringWrite(newSendBufferSize); } } diff --git a/transport/src/main/java/io/netty/channel/socket/nio/ProtocolFamilyConverter.java b/transport/src/main/java/io/netty/channel/socket/nio/ProtocolFamilyConverter.java index e4f4dc2..7c2f141 100644 --- a/transport/src/main/java/io/netty/channel/socket/nio/ProtocolFamilyConverter.java +++ b/transport/src/main/java/io/netty/channel/socket/nio/ProtocolFamilyConverter.java @@ -16,6 +16,7 @@ package io.netty.channel.socket.nio; import io.netty.channel.socket.InternetProtocolFamily; +import io.netty.util.internal.SuppressJava6Requirement; import java.net.ProtocolFamily; import java.net.StandardProtocolFamily; @@ -32,6 +33,7 @@ final class ProtocolFamilyConverter { /** * Convert the {@link InternetProtocolFamily}. This MUST only be called on jdk version >= 7. */ + @SuppressJava6Requirement(reason = "Usage guarded by java version check") public static ProtocolFamily convert(InternetProtocolFamily family) { switch (family) { case IPv4: diff --git a/transport/src/main/java/io/netty/channel/socket/oio/OioServerSocketChannel.java b/transport/src/main/java/io/netty/channel/socket/oio/OioServerSocketChannel.java index bf91829..dfa6b02 100644 --- a/transport/src/main/java/io/netty/channel/socket/oio/OioServerSocketChannel.java +++ b/transport/src/main/java/io/netty/channel/socket/oio/OioServerSocketChannel.java @@ -20,6 +20,7 @@ import io.netty.channel.ChannelMetadata; import io.netty.channel.ChannelOutboundBuffer; import io.netty.channel.oio.AbstractOioMessageChannel; import io.netty.channel.socket.ServerSocketChannel; +import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.SocketUtils; import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; @@ -32,7 +33,6 @@ import java.net.SocketAddress; import java.net.SocketTimeoutException; import java.util.List; import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; /** * {@link ServerSocketChannel} which accepts new connections and create the {@link OioSocketChannel}'s for them. @@ -59,7 +59,6 @@ public class OioServerSocketChannel extends AbstractOioMessageChannel } final ServerSocket socket; - final Lock shutdownLock = new ReentrantLock(); private final OioServerSocketChannelConfig config; /** @@ -76,9 +75,7 @@ public class OioServerSocketChannel extends AbstractOioMessageChannel */ public OioServerSocketChannel(ServerSocket socket) { super(null); - if (socket == null) { - throw new NullPointerException("socket"); - } + ObjectUtil.checkNotNull(socket, "socket"); boolean success = false; try { diff --git a/transport/src/main/resources/META-INF/native-image/io.netty/transport/native-image.properties b/transport/src/main/resources/META-INF/native-image/io.netty/transport/native-image.properties new file mode 100644 index 0000000..dc33561 --- /dev/null +++ b/transport/src/main/resources/META-INF/native-image/io.netty/transport/native-image.properties @@ -0,0 +1,15 @@ +# Copyright 2019 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. + +Args = -H:ReflectionConfigurationResources=${.}/reflection-config.json diff --git a/transport/src/main/resources/META-INF/native-image/io.netty/transport/reflection-config.json b/transport/src/main/resources/META-INF/native-image/io.netty/transport/reflection-config.json new file mode 100644 index 0000000..1ebbb43 --- /dev/null +++ b/transport/src/main/resources/META-INF/native-image/io.netty/transport/reflection-config.json @@ -0,0 +1,15 @@ +[ + { + "name": "io.netty.channel.socket.nio.NioServerSocketChannel", + "methods": [ + { "name": "<init>", "parameterTypes": [] } + ] + }, + { + "name": "sun.nio.ch.SelectorImpl", + "fields": [ + { "name": "selectedKeys", "allowUnsafeAccess" : true}, + { "name": "publicSelectedKeys", "allowUnsafeAccess" : true} + ] + } +] diff --git a/transport/src/test/java/io/netty/bootstrap/BootstrapTest.java b/transport/src/test/java/io/netty/bootstrap/BootstrapTest.java index cc93a91..03b2f09 100644 --- a/transport/src/test/java/io/netty/bootstrap/BootstrapTest.java +++ b/transport/src/test/java/io/netty/bootstrap/BootstrapTest.java @@ -17,13 +17,17 @@ package io.netty.bootstrap; import io.netty.channel.Channel; +import io.netty.channel.ChannelConfig; import io.netty.channel.ChannelFactory; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandler.Sharable; import io.netty.channel.ChannelInboundHandler; import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelOption; import io.netty.channel.ChannelPromise; +import io.netty.channel.DefaultChannelConfig; import io.netty.channel.DefaultEventLoop; import io.netty.channel.DefaultEventLoopGroup; import io.netty.channel.EventLoopGroup; @@ -48,6 +52,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.LinkedBlockingQueue; import static org.hamcrest.Matchers.*; @@ -271,6 +276,38 @@ public class BootstrapTest { assertThat(connectFuture.channel().isOpen(), is(false)); } + @Test + public void testGetResolverFailed() throws Exception { + class TestException extends RuntimeException { } + + final Bootstrap bootstrapA = new Bootstrap(); + bootstrapA.group(groupA); + bootstrapA.channel(LocalChannel.class); + + bootstrapA.resolver(new AddressResolverGroup<SocketAddress>() { + @Override + protected AddressResolver<SocketAddress> newResolver(EventExecutor executor) { + throw new TestException(); + } + }); + bootstrapA.handler(dummyHandler); + + final ServerBootstrap bootstrapB = new ServerBootstrap(); + bootstrapB.group(groupB); + bootstrapB.channel(LocalServerChannel.class); + bootstrapB.childHandler(dummyHandler); + SocketAddress localAddress = bootstrapB.bind(LocalAddress.ANY).sync().channel().localAddress(); + + // Connect to the server using the asynchronous resolver. + ChannelFuture connectFuture = bootstrapA.connect(localAddress); + + // Should fail with the IllegalStateException. + assertThat(connectFuture.await(10000), is(true)); + assertThat(connectFuture.cause(), instanceOf(IllegalStateException.class)); + assertThat(connectFuture.cause().getCause(), instanceOf(TestException.class)); + assertThat(connectFuture.channel().isOpen(), is(false)); + } + @Test public void testChannelFactoryFailureNotifiesPromise() throws Exception { final RuntimeException exception = new RuntimeException("newChannel crash"); @@ -293,6 +330,56 @@ public class BootstrapTest { assertThat(connectFuture.channel(), is(not(nullValue()))); } + @Test + public void testChannelOptionOrderPreserve() throws InterruptedException { + final BlockingQueue<ChannelOption<?>> options = new LinkedBlockingQueue<ChannelOption<?>>(); + class ChannelConfigValidator extends DefaultChannelConfig { + ChannelConfigValidator(Channel channel) { + super(channel); + } + + @Override + public <T> boolean setOption(ChannelOption<T> option, T value) { + options.add(option); + return super.setOption(option, value); + } + } + final CountDownLatch latch = new CountDownLatch(1); + final Bootstrap bootstrap = new Bootstrap() + .handler(new ChannelInitializer<Channel>() { + @Override + protected void initChannel(Channel ch) { + latch.countDown(); + } + }) + .group(groupA) + .channelFactory(new ChannelFactory<Channel>() { + @Override + public Channel newChannel() { + return new LocalChannel() { + private ChannelConfigValidator config; + @Override + public synchronized ChannelConfig config() { + if (config == null) { + config = new ChannelConfigValidator(this); + } + return config; + } + }; + } + }) + .option(ChannelOption.WRITE_BUFFER_LOW_WATER_MARK, 1) + .option(ChannelOption.WRITE_BUFFER_HIGH_WATER_MARK, 2); + + bootstrap.register().syncUninterruptibly(); + + latch.await(); + + // Check the order is the same as what we defined before. + assertSame(ChannelOption.WRITE_BUFFER_LOW_WATER_MARK, options.take()); + assertSame(ChannelOption.WRITE_BUFFER_HIGH_WATER_MARK, options.take()); + } + private static final class DelayedEventLoopGroup extends DefaultEventLoop { @Override public ChannelFuture register(final Channel channel, final ChannelPromise promise) { diff --git a/transport/src/test/java/io/netty/channel/AbstractChannelTest.java b/transport/src/test/java/io/netty/channel/AbstractChannelTest.java index afbe27c..9d5110e 100644 --- a/transport/src/test/java/io/netty/channel/AbstractChannelTest.java +++ b/transport/src/test/java/io/netty/channel/AbstractChannelTest.java @@ -15,8 +15,12 @@ */ package io.netty.channel; +import java.io.IOException; +import java.net.InetSocketAddress; import java.net.SocketAddress; +import java.nio.channels.ClosedChannelException; +import io.netty.util.NetUtil; import org.junit.Test; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; @@ -82,6 +86,69 @@ public class AbstractChannelTest { assertTrue(channelId instanceof DefaultChannelId); } + @Test + public void testClosedChannelExceptionCarryIOException() throws Exception { + final IOException ioException = new IOException(); + final Channel channel = new TestChannel() { + private boolean open = true; + private boolean active; + + @Override + protected AbstractUnsafe newUnsafe() { + return new AbstractUnsafe() { + @Override + public void connect( + SocketAddress remoteAddress, SocketAddress localAddress, ChannelPromise promise) { + active = true; + promise.setSuccess(); + } + }; + } + + @Override + protected void doClose() { + active = false; + open = false; + } + + @Override + protected void doWrite(ChannelOutboundBuffer in) throws Exception { + throw ioException; + } + + @Override + public boolean isOpen() { + return open; + } + + @Override + public boolean isActive() { + return active; + } + }; + + EventLoop loop = new DefaultEventLoop(); + try { + registerChannel(loop, channel); + channel.connect(new InetSocketAddress(NetUtil.LOCALHOST, 8888)).sync(); + assertSame(ioException, channel.writeAndFlush("").await().cause()); + + assertClosedChannelException(channel.writeAndFlush(""), ioException); + assertClosedChannelException(channel.write(""), ioException); + assertClosedChannelException(channel.bind(new InetSocketAddress(NetUtil.LOCALHOST, 8888)), ioException); + } finally { + channel.close(); + loop.shutdownGracefully(); + } + } + + private static void assertClosedChannelException(ChannelFuture future, IOException expected) + throws InterruptedException { + Throwable cause = future.await().cause(); + assertTrue(cause instanceof ClosedChannelException); + assertSame(expected, cause.getCause()); + } + private static void registerChannel(EventLoop eventLoop, Channel channel) throws Exception { DefaultChannelPromise future = new DefaultChannelPromise(channel); channel.unsafe().register(eventLoop, future); @@ -90,19 +157,16 @@ public class AbstractChannelTest { private static class TestChannel extends AbstractChannel { private static final ChannelMetadata TEST_METADATA = new ChannelMetadata(false); - private class TestUnsafe extends AbstractUnsafe { - @Override - public void connect(SocketAddress remoteAddress, SocketAddress localAddress, ChannelPromise promise) { } - } + private final ChannelConfig config = new DefaultChannelConfig(this); - public TestChannel() { + TestChannel() { super(null); } @Override public ChannelConfig config() { - return new DefaultChannelConfig(this); + return config; } @Override @@ -122,7 +186,12 @@ public class AbstractChannelTest { @Override protected AbstractUnsafe newUnsafe() { - return new TestUnsafe(); + return new AbstractUnsafe() { + @Override + public void connect(SocketAddress remoteAddress, SocketAddress localAddress, ChannelPromise promise) { + promise.setFailure(new UnsupportedOperationException()); + } + }; } @Override @@ -141,16 +210,16 @@ public class AbstractChannelTest { } @Override - protected void doBind(SocketAddress localAddress) throws Exception { } + protected void doBind(SocketAddress localAddress) { } @Override - protected void doDisconnect() throws Exception { } + protected void doDisconnect() { } @Override - protected void doClose() throws Exception { } + protected void doClose() { } @Override - protected void doBeginRead() throws Exception { } + protected void doBeginRead() { } @Override protected void doWrite(ChannelOutboundBuffer in) throws Exception { } diff --git a/transport/src/test/java/io/netty/channel/AbstractEventLoopTest.java b/transport/src/test/java/io/netty/channel/AbstractEventLoopTest.java index b4d29a6..df7fe13 100644 --- a/transport/src/test/java/io/netty/channel/AbstractEventLoopTest.java +++ b/transport/src/test/java/io/netty/channel/AbstractEventLoopTest.java @@ -75,7 +75,7 @@ public abstract class AbstractEventLoopTest { b.bind(0).sync().channel(); Future<?> f = loop.shutdownGracefully(0, 1, TimeUnit.MINUTES); - assertTrue(loop.awaitTermination(2, TimeUnit.SECONDS)); + assertTrue(loop.awaitTermination(600, TimeUnit.MILLISECONDS)); assertTrue(f.syncUninterruptibly().isSuccess()); assertTrue(loop.isShutdown()); assertTrue(loop.isTerminated()); diff --git a/transport/src/test/java/io/netty/channel/AdaptiveRecvByteBufAllocatorTest.java b/transport/src/test/java/io/netty/channel/AdaptiveRecvByteBufAllocatorTest.java index 762db69..8f95b2b 100644 --- a/transport/src/test/java/io/netty/channel/AdaptiveRecvByteBufAllocatorTest.java +++ b/transport/src/test/java/io/netty/channel/AdaptiveRecvByteBufAllocatorTest.java @@ -54,6 +54,26 @@ public class AdaptiveRecvByteBufAllocatorTest { allocReadExpected(handle, alloc, 8388608); } + @Test + public void memoryAllocationIntervalsTest() { + computingNext(512, 512); + computingNext(8192, 1110); + computingNext(8192, 1200); + computingNext(4096, 1300); + computingNext(4096, 1500); + computingNext(2048, 1700); + computingNext(2048, 1550); + computingNext(2048, 2000); + computingNext(2048, 1900); + } + + private void computingNext(long expectedSize, int actualReadBytes) { + assertEquals(expectedSize, handle.guess()); + handle.reset(config); + handle.lastBytesRead(actualReadBytes); + handle.readComplete(); + } + @Test public void lastPartialReadDoesNotRampDown() { allocReadExpected(handle, alloc, 512); diff --git a/transport/src/test/java/io/netty/channel/DefaultChannelPipelineTailTest.java b/transport/src/test/java/io/netty/channel/DefaultChannelPipelineTailTest.java index 697db84..7eb624b 100644 --- a/transport/src/test/java/io/netty/channel/DefaultChannelPipelineTailTest.java +++ b/transport/src/test/java/io/netty/channel/DefaultChannelPipelineTailTest.java @@ -237,7 +237,7 @@ public class DefaultChannelPipelineTailTest { private static class MyChannelFactory implements ChannelFactory<MyChannel> { private final MyChannel channel; - public MyChannelFactory(MyChannel channel) { + MyChannelFactory(MyChannel channel) { this.channel = channel; } @@ -365,7 +365,7 @@ public class DefaultChannelPipelineTailTest { private class MyChannelPipeline extends DefaultChannelPipeline { - public MyChannelPipeline(Channel channel) { + MyChannelPipeline(Channel channel) { super(channel); } diff --git a/transport/src/test/java/io/netty/channel/DefaultChannelPipelineTest.java b/transport/src/test/java/io/netty/channel/DefaultChannelPipelineTest.java index 31de253..4d49fdc 100644 --- a/transport/src/test/java/io/netty/channel/DefaultChannelPipelineTest.java +++ b/transport/src/test/java/io/netty/channel/DefaultChannelPipelineTest.java @@ -21,10 +21,10 @@ import io.netty.bootstrap.ServerBootstrap; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelHandler.Sharable; +import io.netty.channel.ChannelHandlerMask.Skip; import io.netty.channel.embedded.EmbeddedChannel; import io.netty.channel.local.LocalAddress; import io.netty.channel.local.LocalChannel; -import io.netty.channel.local.LocalEventLoopGroup; import io.netty.channel.local.LocalServerChannel; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.oio.OioEventLoopGroup; @@ -43,8 +43,10 @@ import io.netty.util.concurrent.Promise; import io.netty.util.concurrent.UnorderedThreadPoolEventExecutor; import org.junit.After; import org.junit.AfterClass; +import org.junit.BeforeClass; import org.junit.Test; +import java.net.SocketAddress; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collections; @@ -72,11 +74,16 @@ import static org.junit.Assert.fail; public class DefaultChannelPipelineTest { - private static final EventLoopGroup group = new DefaultEventLoopGroup(1); + private static EventLoopGroup group; private Channel self; private Channel peer; + @BeforeClass + public static void beforeClass() throws Exception { + group = new DefaultEventLoopGroup(1); + } + @AfterClass public static void afterClass() throws Exception { group.shutdownGracefully().sync(); @@ -257,6 +264,61 @@ public class DefaultChannelPipelineTest { assertSame(pipeline.get("handler2"), newHandler2); } + @Test(expected = IllegalArgumentException.class) + public void testReplaceHandlerChecksDuplicateNames() { + ChannelPipeline pipeline = new LocalChannel().pipeline(); + + ChannelHandler handler1 = newHandler(); + ChannelHandler handler2 = newHandler(); + pipeline.addLast("handler1", handler1); + pipeline.addLast("handler2", handler2); + + ChannelHandler newHandler1 = newHandler(); + pipeline.replace("handler1", "handler2", newHandler1); + } + + @Test + public void testReplaceNameWithGenerated() { + ChannelPipeline pipeline = new LocalChannel().pipeline(); + + ChannelHandler handler1 = newHandler(); + pipeline.addLast("handler1", handler1); + assertSame(pipeline.get("handler1"), handler1); + + ChannelHandler newHandler1 = newHandler(); + pipeline.replace("handler1", null, newHandler1); + assertSame(pipeline.get("DefaultChannelPipelineTest$TestHandler#0"), newHandler1); + assertNull(pipeline.get("handler1")); + } + + @Test + public void testRenameChannelHandler() { + ChannelPipeline pipeline = new LocalChannel().pipeline(); + + ChannelHandler handler1 = newHandler(); + pipeline.addLast("handler1", handler1); + pipeline.addLast("handler2", handler1); + pipeline.addLast("handler3", handler1); + assertSame(pipeline.get("handler1"), handler1); + assertSame(pipeline.get("handler2"), handler1); + assertSame(pipeline.get("handler3"), handler1); + + ChannelHandler newHandler1 = newHandler(); + pipeline.replace("handler1", "newHandler1", newHandler1); + assertSame(pipeline.get("newHandler1"), newHandler1); + assertNull(pipeline.get("handler1")); + + ChannelHandler newHandler3 = newHandler(); + pipeline.replace("handler3", "newHandler3", newHandler3); + assertSame(pipeline.get("newHandler3"), newHandler3); + assertNull(pipeline.get("handler3")); + + ChannelHandler newHandler2 = newHandler(); + pipeline.replace("handler2", "newHandler2", newHandler2); + assertSame(pipeline.get("newHandler2"), newHandler2); + assertNull(pipeline.get("handler2")); + } + @Test public void testChannelHandlerContextNavigation() { ChannelPipeline pipeline = new LocalChannel().pipeline(); @@ -1223,6 +1285,480 @@ public class DefaultChannelPipelineTest { } } + @Test + public void testSkipHandlerMethodsIfAnnotated() { + EmbeddedChannel channel = new EmbeddedChannel(true); + ChannelPipeline pipeline = channel.pipeline(); + + final class SkipHandler implements ChannelInboundHandler, ChannelOutboundHandler { + private int state = 2; + private Error errorRef; + + private void fail() { + errorRef = new AssertionError("Method should never been called"); + } + + @Skip + @Override + public void bind(ChannelHandlerContext ctx, SocketAddress localAddress, ChannelPromise promise) { + fail(); + ctx.bind(localAddress, promise); + } + + @Skip + @Override + public void connect(ChannelHandlerContext ctx, SocketAddress remoteAddress, + SocketAddress localAddress, ChannelPromise promise) { + fail(); + ctx.connect(remoteAddress, localAddress, promise); + } + + @Skip + @Override + public void disconnect(ChannelHandlerContext ctx, ChannelPromise promise) { + fail(); + ctx.disconnect(promise); + } + + @Skip + @Override + public void close(ChannelHandlerContext ctx, ChannelPromise promise) { + fail(); + ctx.close(promise); + } + + @Skip + @Override + public void deregister(ChannelHandlerContext ctx, ChannelPromise promise) { + fail(); + ctx.deregister(promise); + } + + @Skip + @Override + public void read(ChannelHandlerContext ctx) { + fail(); + ctx.read(); + } + + @Skip + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) { + fail(); + ctx.write(msg, promise); + } + + @Skip + @Override + public void flush(ChannelHandlerContext ctx) { + fail(); + ctx.flush(); + } + + @Skip + @Override + public void channelRegistered(ChannelHandlerContext ctx) { + fail(); + ctx.fireChannelRegistered(); + } + + @Skip + @Override + public void channelUnregistered(ChannelHandlerContext ctx) { + fail(); + ctx.fireChannelUnregistered(); + } + + @Skip + @Override + public void channelActive(ChannelHandlerContext ctx) { + fail(); + ctx.fireChannelActive(); + } + + @Skip + @Override + public void channelInactive(ChannelHandlerContext ctx) { + fail(); + ctx.fireChannelInactive(); + } + + @Skip + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + fail(); + ctx.fireChannelRead(msg); + } + + @Skip + @Override + public void channelReadComplete(ChannelHandlerContext ctx) { + fail(); + ctx.fireChannelReadComplete(); + } + + @Skip + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { + fail(); + ctx.fireUserEventTriggered(evt); + } + + @Skip + @Override + public void channelWritabilityChanged(ChannelHandlerContext ctx) { + fail(); + ctx.fireChannelWritabilityChanged(); + } + + @Skip + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + fail(); + ctx.fireExceptionCaught(cause); + } + + @Override + public void handlerAdded(ChannelHandlerContext ctx) { + state--; + } + + @Override + public void handlerRemoved(ChannelHandlerContext ctx) { + state--; + } + + void assertSkipped() { + assertEquals(0, state); + Error error = errorRef; + if (error != null) { + throw error; + } + } + } + + final class OutboundCalledHandler extends ChannelOutboundHandlerAdapter { + private static final int MASK_BIND = 1; + private static final int MASK_CONNECT = 1 << 1; + private static final int MASK_DISCONNECT = 1 << 2; + private static final int MASK_CLOSE = 1 << 3; + private static final int MASK_DEREGISTER = 1 << 4; + private static final int MASK_READ = 1 << 5; + private static final int MASK_WRITE = 1 << 6; + private static final int MASK_FLUSH = 1 << 7; + private static final int MASK_ADDED = 1 << 8; + private static final int MASK_REMOVED = 1 << 9; + + private int executionMask; + + @Override + public void handlerAdded(ChannelHandlerContext ctx) { + executionMask |= MASK_ADDED; + } + + @Override + public void handlerRemoved(ChannelHandlerContext ctx) { + executionMask |= MASK_REMOVED; + } + + @Override + public void bind(ChannelHandlerContext ctx, SocketAddress localAddress, ChannelPromise promise) { + executionMask |= MASK_BIND; + promise.setSuccess(); + } + + @Override + public void connect(ChannelHandlerContext ctx, SocketAddress remoteAddress, + SocketAddress localAddress, ChannelPromise promise) { + executionMask |= MASK_CONNECT; + promise.setSuccess(); + } + + @Override + public void disconnect(ChannelHandlerContext ctx, ChannelPromise promise) { + executionMask |= MASK_DISCONNECT; + promise.setSuccess(); + } + + @Override + public void close(ChannelHandlerContext ctx, ChannelPromise promise) { + executionMask |= MASK_CLOSE; + promise.setSuccess(); + } + + @Override + public void deregister(ChannelHandlerContext ctx, ChannelPromise promise) { + executionMask |= MASK_DEREGISTER; + promise.setSuccess(); + } + + @Override + public void read(ChannelHandlerContext ctx) { + executionMask |= MASK_READ; + } + + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) { + executionMask |= MASK_WRITE; + promise.setSuccess(); + } + + @Override + public void flush(ChannelHandlerContext ctx) { + executionMask |= MASK_FLUSH; + } + + void assertCalled() { + assertCalled("handlerAdded", MASK_ADDED); + assertCalled("handlerRemoved", MASK_REMOVED); + assertCalled("bind", MASK_BIND); + assertCalled("connect", MASK_CONNECT); + assertCalled("disconnect", MASK_DISCONNECT); + assertCalled("close", MASK_CLOSE); + assertCalled("deregister", MASK_DEREGISTER); + assertCalled("read", MASK_READ); + assertCalled("write", MASK_WRITE); + assertCalled("flush", MASK_FLUSH); + } + + private void assertCalled(String methodName, int mask) { + assertTrue(methodName + " was not called", (executionMask & mask) != 0); + } + } + + final class InboundCalledHandler extends ChannelInboundHandlerAdapter { + + private static final int MASK_CHANNEL_REGISTER = 1; + private static final int MASK_CHANNEL_UNREGISTER = 1 << 1; + private static final int MASK_CHANNEL_ACTIVE = 1 << 2; + private static final int MASK_CHANNEL_INACTIVE = 1 << 3; + private static final int MASK_CHANNEL_READ = 1 << 4; + private static final int MASK_CHANNEL_READ_COMPLETE = 1 << 5; + private static final int MASK_USER_EVENT_TRIGGERED = 1 << 6; + private static final int MASK_CHANNEL_WRITABILITY_CHANGED = 1 << 7; + private static final int MASK_EXCEPTION_CAUGHT = 1 << 8; + private static final int MASK_ADDED = 1 << 9; + private static final int MASK_REMOVED = 1 << 10; + + private int executionMask; + + @Override + public void handlerAdded(ChannelHandlerContext ctx) { + executionMask |= MASK_ADDED; + } + + @Override + public void handlerRemoved(ChannelHandlerContext ctx) { + executionMask |= MASK_REMOVED; + } + + @Override + public void channelRegistered(ChannelHandlerContext ctx) { + executionMask |= MASK_CHANNEL_REGISTER; + } + + @Override + public void channelUnregistered(ChannelHandlerContext ctx) { + executionMask |= MASK_CHANNEL_UNREGISTER; + } + + @Override + public void channelActive(ChannelHandlerContext ctx) { + executionMask |= MASK_CHANNEL_ACTIVE; + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) { + executionMask |= MASK_CHANNEL_INACTIVE; + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + executionMask |= MASK_CHANNEL_READ; + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) { + executionMask |= MASK_CHANNEL_READ_COMPLETE; + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { + executionMask |= MASK_USER_EVENT_TRIGGERED; + } + + @Override + public void channelWritabilityChanged(ChannelHandlerContext ctx) { + executionMask |= MASK_CHANNEL_WRITABILITY_CHANGED; + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + executionMask |= MASK_EXCEPTION_CAUGHT; + } + + void assertCalled() { + assertCalled("handlerAdded", MASK_ADDED); + assertCalled("handlerRemoved", MASK_REMOVED); + assertCalled("channelRegistered", MASK_CHANNEL_REGISTER); + assertCalled("channelUnregistered", MASK_CHANNEL_UNREGISTER); + assertCalled("channelActive", MASK_CHANNEL_ACTIVE); + assertCalled("channelInactive", MASK_CHANNEL_INACTIVE); + assertCalled("channelRead", MASK_CHANNEL_READ); + assertCalled("channelReadComplete", MASK_CHANNEL_READ_COMPLETE); + assertCalled("userEventTriggered", MASK_USER_EVENT_TRIGGERED); + assertCalled("channelWritabilityChanged", MASK_CHANNEL_WRITABILITY_CHANGED); + assertCalled("exceptionCaught", MASK_EXCEPTION_CAUGHT); + } + + private void assertCalled(String methodName, int mask) { + assertTrue(methodName + " was not called", (executionMask & mask) != 0); + } + } + + OutboundCalledHandler outboundCalledHandler = new OutboundCalledHandler(); + SkipHandler skipHandler = new SkipHandler(); + InboundCalledHandler inboundCalledHandler = new InboundCalledHandler(); + pipeline.addLast(outboundCalledHandler, skipHandler, inboundCalledHandler); + + pipeline.fireChannelRegistered(); + pipeline.fireChannelUnregistered(); + pipeline.fireChannelActive(); + pipeline.fireChannelInactive(); + pipeline.fireChannelRead(""); + pipeline.fireChannelReadComplete(); + pipeline.fireChannelWritabilityChanged(); + pipeline.fireUserEventTriggered(""); + pipeline.fireExceptionCaught(new Exception()); + + pipeline.deregister().syncUninterruptibly(); + pipeline.bind(new SocketAddress() { + }).syncUninterruptibly(); + pipeline.connect(new SocketAddress() { + }).syncUninterruptibly(); + pipeline.disconnect().syncUninterruptibly(); + pipeline.close().syncUninterruptibly(); + pipeline.write(""); + pipeline.flush(); + pipeline.read(); + + pipeline.remove(outboundCalledHandler); + pipeline.remove(inboundCalledHandler); + pipeline.remove(skipHandler); + + assertFalse(channel.finish()); + + outboundCalledHandler.assertCalled(); + inboundCalledHandler.assertCalled(); + skipHandler.assertSkipped(); + } + + @Test + public void testWriteThrowsReleaseMessage() { + testWriteThrowsReleaseMessage0(false); + } + + @Test + public void testWriteAndFlushThrowsReleaseMessage() { + testWriteThrowsReleaseMessage0(true); + } + + private void testWriteThrowsReleaseMessage0(boolean flush) { + ReferenceCounted referenceCounted = new AbstractReferenceCounted() { + @Override + protected void deallocate() { + // NOOP + } + + @Override + public ReferenceCounted touch(Object hint) { + return this; + } + }; + assertEquals(1, referenceCounted.refCnt()); + + Channel channel = new LocalChannel(); + Channel channel2 = new LocalChannel(); + group.register(channel).syncUninterruptibly(); + group.register(channel2).syncUninterruptibly(); + + try { + if (flush) { + channel.writeAndFlush(referenceCounted, channel2.newPromise()); + } else { + channel.write(referenceCounted, channel2.newPromise()); + } + fail(); + } catch (IllegalArgumentException expected) { + // expected + } + assertEquals(0, referenceCounted.refCnt()); + + channel.close().syncUninterruptibly(); + channel2.close().syncUninterruptibly(); + } + + @Test(timeout = 5000) + public void testHandlerAddedFailedButHandlerStillRemoved() throws InterruptedException { + testHandlerAddedFailedButHandlerStillRemoved0(false); + } + + @Test(timeout = 5000) + public void testHandlerAddedFailedButHandlerStillRemovedWithLaterRegister() throws InterruptedException { + testHandlerAddedFailedButHandlerStillRemoved0(true); + } + + private static void testHandlerAddedFailedButHandlerStillRemoved0(boolean lateRegister) + throws InterruptedException { + EventExecutorGroup executorGroup = new DefaultEventExecutorGroup(16); + final int numHandlers = 32; + try { + Channel channel = new LocalChannel(); + channel.config().setOption(ChannelOption.SINGLE_EVENTEXECUTOR_PER_GROUP, false); + if (!lateRegister) { + group.register(channel).sync(); + } + channel.pipeline().addFirst(newHandler()); + + List<CountDownLatch> latchList = new ArrayList<CountDownLatch>(numHandlers); + for (int i = 0; i < numHandlers; i++) { + CountDownLatch latch = new CountDownLatch(1); + channel.pipeline().addFirst(executorGroup, "h" + i, new BadChannelHandler(latch)); + latchList.add(latch); + } + if (lateRegister) { + group.register(channel).sync(); + } + + for (int i = 0; i < numHandlers; i++) { + // Wait until the latch was countDown which means handlerRemoved(...) was called. + latchList.get(i).await(); + assertNull(channel.pipeline().get("h" + i)); + } + } finally { + executorGroup.shutdownGracefully(); + } + } + + private static final class BadChannelHandler extends ChannelHandlerAdapter { + private final CountDownLatch latch; + + BadChannelHandler(CountDownLatch latch) { + this.latch = latch; + } + + @Override + public void handlerAdded(ChannelHandlerContext ctx) throws Exception { + TimeUnit.MILLISECONDS.sleep(10); + throw new RuntimeException(); + } + + @Override + public void handlerRemoved(ChannelHandlerContext ctx) { + latch.countDown(); + } + } + @Test(timeout = 5000) public void handlerAddedStateUpdatedBeforeHandlerAddedDoneForceEventLoop() throws InterruptedException { handlerAddedStateUpdatedBeforeHandlerAddedDone(true); diff --git a/transport/src/test/java/io/netty/channel/DefaultFileRegionTest.java b/transport/src/test/java/io/netty/channel/DefaultFileRegionTest.java new file mode 100644 index 0000000..e416bcc --- /dev/null +++ b/transport/src/test/java/io/netty/channel/DefaultFileRegionTest.java @@ -0,0 +1,120 @@ +/* + * Copyright 2019 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.channel; + +import io.netty.util.internal.PlatformDependent; +import org.junit.Test; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.channels.Channels; +import java.nio.channels.WritableByteChannel; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +public class DefaultFileRegionTest { + + private static final byte[] data = new byte[1048576 * 10]; + + static { + PlatformDependent.threadLocalRandom().nextBytes(data); + } + + private static File newFile() throws IOException { + File file = File.createTempFile("netty-", ".tmp"); + file.deleteOnExit(); + + final FileOutputStream out = new FileOutputStream(file); + out.write(data); + out.close(); + return file; + } + + @Test + public void testCreateFromFile() throws IOException { + File file = newFile(); + try { + testFileRegion(new DefaultFileRegion(file, 0, data.length)); + } finally { + file.delete(); + } + } + + @Test + public void testCreateFromFileChannel() throws IOException { + File file = newFile(); + RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r"); + try { + testFileRegion(new DefaultFileRegion(randomAccessFile.getChannel(), 0, data.length)); + } finally { + randomAccessFile.close(); + file.delete(); + } + } + + private static void testFileRegion(FileRegion region) throws IOException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + WritableByteChannel channel = Channels.newChannel(outputStream); + + try { + assertEquals(data.length, region.count()); + assertEquals(0, region.transferred()); + assertEquals(data.length, region.transferTo(channel, 0)); + assertEquals(data.length, region.count()); + assertEquals(data.length, region.transferred()); + assertArrayEquals(data, outputStream.toByteArray()); + } finally { + channel.close(); + } + } + + @Test + public void testTruncated() throws IOException { + File file = newFile(); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + WritableByteChannel channel = Channels.newChannel(outputStream); + RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw"); + + try { + FileRegion region = new DefaultFileRegion(randomAccessFile.getChannel(), 0, data.length); + + randomAccessFile.getChannel().truncate(data.length - 1024); + + assertEquals(data.length, region.count()); + assertEquals(0, region.transferred()); + + assertEquals(data.length - 1024, region.transferTo(channel, 0)); + assertEquals(data.length, region.count()); + assertEquals(data.length - 1024, region.transferred()); + try { + region.transferTo(channel, data.length - 1024); + fail(); + } catch (IOException expected) { + // expected + } + } finally { + channel.close(); + + randomAccessFile.close(); + file.delete(); + } + } +} diff --git a/transport/src/test/java/io/netty/channel/DelegatingChannelPromiseNotifierTest.java b/transport/src/test/java/io/netty/channel/DelegatingChannelPromiseNotifierTest.java index 2f4b0aa..d187773 100644 --- a/transport/src/test/java/io/netty/channel/DelegatingChannelPromiseNotifierTest.java +++ b/transport/src/test/java/io/netty/channel/DelegatingChannelPromiseNotifierTest.java @@ -20,8 +20,6 @@ import io.netty.util.concurrent.GenericFutureListener; import org.junit.Test; import org.mockito.Mockito; -import static org.junit.Assert.*; - public class DelegatingChannelPromiseNotifierTest { @Test public void varargsNotifiersAllowed() { diff --git a/transport/src/test/java/io/netty/channel/embedded/EmbeddedChannelTest.java b/transport/src/test/java/io/netty/channel/embedded/EmbeddedChannelTest.java index a5dae45..ff0f8a7 100644 --- a/transport/src/test/java/io/netty/channel/embedded/EmbeddedChannelTest.java +++ b/transport/src/test/java/io/netty/channel/embedded/EmbeddedChannelTest.java @@ -54,6 +54,17 @@ import io.netty.util.concurrent.ScheduledFuture; public class EmbeddedChannelTest { + @Test + public void testParent() { + EmbeddedChannel parent = new EmbeddedChannel(); + EmbeddedChannel channel = new EmbeddedChannel(parent, EmbeddedChannelId.INSTANCE, true, false); + assertSame(parent, channel.parent()); + assertNull(parent.parent()); + + assertFalse(channel.finish()); + assertFalse(parent.finish()); + } + @Test public void testNotRegistered() throws Exception { EmbeddedChannel channel = new EmbeddedChannel(false, false); @@ -281,6 +292,17 @@ public class EmbeddedChannelTest { assertNull(handler.pollEvent()); } + @Test + public void testHasNoDisconnectSkipDisconnect() throws InterruptedException { + EmbeddedChannel channel = new EmbeddedChannel(false, new ChannelOutboundHandlerAdapter() { + @Override + public void close(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception { + promise.tryFailure(new Throwable()); + } + }); + assertFalse(channel.disconnect().isSuccess()); + } + @Test public void testFinishAndReleaseAll() { ByteBuf in = Unpooled.buffer(); diff --git a/transport/src/test/java/io/netty/channel/nio/NioEventLoopTest.java b/transport/src/test/java/io/netty/channel/nio/NioEventLoopTest.java index 8b176bc..eb73cb6 100644 --- a/transport/src/test/java/io/netty/channel/nio/NioEventLoopTest.java +++ b/transport/src/test/java/io/netty/channel/nio/NioEventLoopTest.java @@ -17,15 +17,21 @@ package io.netty.channel.nio; import io.netty.channel.AbstractEventLoopTest; import io.netty.channel.Channel; +import io.netty.channel.DefaultSelectStrategyFactory; import io.netty.channel.EventLoop; import io.netty.channel.EventLoopGroup; +import io.netty.channel.EventLoopTaskQueueFactory; import io.netty.channel.SelectStrategy; import io.netty.channel.SelectStrategyFactory; +import io.netty.channel.SingleThreadEventLoop; import io.netty.channel.socket.ServerSocketChannel; import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.util.IntSupplier; +import io.netty.util.concurrent.DefaultEventExecutorChooserFactory; import io.netty.util.concurrent.DefaultThreadFactory; import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.RejectedExecutionHandlers; +import io.netty.util.concurrent.ThreadPerTaskExecutor; import org.hamcrest.core.IsInstanceOf; import org.junit.Test; @@ -35,9 +41,13 @@ import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.SocketChannel; import java.nio.channels.spi.SelectorProvider; +import java.util.Queue; +import java.util.concurrent.Callable; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import static org.junit.Assert.*; @@ -258,4 +268,73 @@ public class NioEventLoopTest extends AbstractEventLoopTest { } } + @Test(timeout = 3000L) + public void testChannelsRegistered() throws Exception { + NioEventLoopGroup group = new NioEventLoopGroup(1); + final NioEventLoop loop = (NioEventLoop) group.next(); + + try { + final Channel ch1 = new NioServerSocketChannel(); + final Channel ch2 = new NioServerSocketChannel(); + + assertEquals(0, registeredChannels(loop)); + + assertTrue(loop.register(ch1).syncUninterruptibly().isSuccess()); + assertTrue(loop.register(ch2).syncUninterruptibly().isSuccess()); + assertEquals(2, registeredChannels(loop)); + + assertTrue(ch1.deregister().syncUninterruptibly().isSuccess()); + + int registered; + // As SelectionKeys are removed in a lazy fashion in the JDK implementation we may need to query a few + // times before we see the right number of registered chanels. + while ((registered = registeredChannels(loop)) == 2) { + Thread.sleep(50); + } + assertEquals(1, registered); + } finally { + group.shutdownGracefully(); + } + } + + // Only reliable if run from event loop + private static int registeredChannels(final SingleThreadEventLoop loop) throws Exception { + return loop.submit(new Callable<Integer>() { + @Override + public Integer call() { + return loop.registeredChannels(); + } + }).get(1, TimeUnit.SECONDS); + } + + @Test + public void testCustomQueue() { + final AtomicBoolean called = new AtomicBoolean(); + NioEventLoopGroup group = new NioEventLoopGroup(1, + new ThreadPerTaskExecutor(new DefaultThreadFactory(NioEventLoopGroup.class)), + DefaultEventExecutorChooserFactory.INSTANCE, SelectorProvider.provider(), + DefaultSelectStrategyFactory.INSTANCE, RejectedExecutionHandlers.reject(), + new EventLoopTaskQueueFactory() { + @Override + public Queue<Runnable> newTaskQueue(int maxCapacity) { + called.set(true); + return new LinkedBlockingQueue<Runnable>(maxCapacity); + } + }); + + final NioEventLoop loop = (NioEventLoop) group.next(); + + try { + loop.submit(new Runnable() { + @Override + public void run() { + // NOOP. + } + }).syncUninterruptibly(); + assertTrue(called.get()); + } finally { + group.shutdownGracefully(); + } + } + } diff --git a/transport/src/test/java/io/netty/channel/pool/AbstractChannelPoolMapTest.java b/transport/src/test/java/io/netty/channel/pool/AbstractChannelPoolMapTest.java index 8d03cd7..3946d22 100644 --- a/transport/src/test/java/io/netty/channel/pool/AbstractChannelPoolMapTest.java +++ b/transport/src/test/java/io/netty/channel/pool/AbstractChannelPoolMapTest.java @@ -22,19 +22,26 @@ import io.netty.channel.EventLoopGroup; import io.netty.channel.local.LocalAddress; import io.netty.channel.local.LocalChannel; import io.netty.channel.local.LocalEventLoopGroup; +import io.netty.util.concurrent.EventExecutor; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.GenericFutureListener; +import io.netty.util.concurrent.Promise; import org.junit.Test; import java.net.ConnectException; +import java.util.concurrent.TimeUnit; -import static org.junit.Assert.*; +import static io.netty.channel.pool.ChannelPoolTestUtils.getLocalAddrId; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; public class AbstractChannelPoolMapTest { - private static final String LOCAL_ADDR_ID = "test.id"; - @Test(expected = ConnectException.class) public void testMap() throws Exception { EventLoopGroup group = new LocalEventLoopGroup(); - LocalAddress addr = new LocalAddress(LOCAL_ADDR_ID); + LocalAddress addr = new LocalAddress(getLocalAddrId()); final Bootstrap cb = new Bootstrap(); cb.remoteAddress(addr); cb.group(group) @@ -65,6 +72,62 @@ public class AbstractChannelPoolMapTest { assertEquals(0, poolMap.size()); pool.acquire().syncUninterruptibly(); + poolMap.close(); + } + + @Test + public void testRemoveClosesChannelPool() { + EventLoopGroup group = new LocalEventLoopGroup(); + LocalAddress addr = new LocalAddress(getLocalAddrId()); + final Bootstrap cb = new Bootstrap(); + cb.remoteAddress(addr); + cb.group(group) + .channel(LocalChannel.class); + + AbstractChannelPoolMap<EventLoop, TestPool> poolMap = + new AbstractChannelPoolMap<EventLoop, TestPool>() { + @Override + protected TestPool newPool(EventLoop key) { + return new TestPool(cb.clone(key), new TestChannelPoolHandler()); + } + }; + + EventLoop loop = group.next(); + + TestPool pool = poolMap.get(loop); + assertTrue(poolMap.remove(loop)); + + // the pool should be closed eventually after remove + pool.closeFuture.awaitUninterruptibly(1, TimeUnit.SECONDS); + assertTrue(pool.closeFuture.isDone()); + poolMap.close(); + } + + @Test + public void testCloseClosesPoolsImmediately() { + EventLoopGroup group = new LocalEventLoopGroup(); + LocalAddress addr = new LocalAddress(getLocalAddrId()); + final Bootstrap cb = new Bootstrap(); + cb.remoteAddress(addr); + cb.group(group) + .channel(LocalChannel.class); + + AbstractChannelPoolMap<EventLoop, TestPool> poolMap = + new AbstractChannelPoolMap<EventLoop, TestPool>() { + @Override + protected TestPool newPool(EventLoop key) { + return new TestPool(cb.clone(key), new TestChannelPoolHandler()); + } + }; + + EventLoop loop = group.next(); + + TestPool pool = poolMap.get(loop); + assertFalse(pool.closeFuture.isDone()); + + // the pool should be closed immediately after remove + poolMap.close(); + assertTrue(pool.closeFuture.isDone()); } private static final class TestChannelPoolHandler extends AbstractChannelPoolHandler { @@ -73,4 +136,30 @@ public class AbstractChannelPoolMapTest { // NOOP } } + + private static final class TestPool extends SimpleChannelPool { + private final Promise<Void> closeFuture; + + TestPool(Bootstrap bootstrap, ChannelPoolHandler handler) { + super(bootstrap, handler); + EventExecutor executor = bootstrap.config().group().next(); + closeFuture = executor.newPromise(); + } + + @Override + public Future<Void> closeAsync() { + Future<Void> poolClose = super.closeAsync(); + poolClose.addListener(new GenericFutureListener<Future<? super Void>>() { + @Override + public void operationComplete(Future<? super Void> future) throws Exception { + if (future.isSuccess()) { + closeFuture.setSuccess(null); + } else { + closeFuture.setFailure(future.cause()); + } + } + }); + return poolClose; + } + } } diff --git a/transport/src/test/java/io/netty/channel/pool/ChannelPoolTestUtils.java b/transport/src/test/java/io/netty/channel/pool/ChannelPoolTestUtils.java new file mode 100644 index 0000000..5c7486d --- /dev/null +++ b/transport/src/test/java/io/netty/channel/pool/ChannelPoolTestUtils.java @@ -0,0 +1,29 @@ +/* + * 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.channel.pool; + +import io.netty.util.internal.ThreadLocalRandom; + +final class ChannelPoolTestUtils { + private static final String LOCAL_ADDR_ID = "test.id"; + + private ChannelPoolTestUtils() { + } + + static String getLocalAddrId() { + return LOCAL_ADDR_ID + ThreadLocalRandom.current().nextInt(); + } +} diff --git a/transport/src/test/java/io/netty/channel/pool/FixedChannelPoolMapDeadlockTest.java b/transport/src/test/java/io/netty/channel/pool/FixedChannelPoolMapDeadlockTest.java new file mode 100644 index 0000000..399383a --- /dev/null +++ b/transport/src/test/java/io/netty/channel/pool/FixedChannelPoolMapDeadlockTest.java @@ -0,0 +1,264 @@ +/* + * Copyright 2019 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.channel.pool; + +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.Channel; +import io.netty.channel.DefaultEventLoop; +import io.netty.channel.EventLoop; +import io.netty.channel.local.LocalAddress; +import io.netty.channel.local.LocalChannel; +import io.netty.util.concurrent.Future; +import org.junit.Test; + +import java.util.concurrent.Callable; +import java.util.concurrent.CyclicBarrier; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import static org.junit.Assert.*; + +/** + * This is a test case for the deadlock scenario described in https://github.com/netty/netty/issues/8238. + */ +public class FixedChannelPoolMapDeadlockTest { + + private static final NoopHandler NOOP_HANDLER = new NoopHandler(); + + @Test + public void testDeadlockOnAcquire() throws Exception { + + final EventLoop threadA1 = new DefaultEventLoop(); + final Bootstrap bootstrapA1 = new Bootstrap() + .channel(LocalChannel.class).group(threadA1).localAddress(new LocalAddress("A1")); + final EventLoop threadA2 = new DefaultEventLoop(); + final Bootstrap bootstrapA2 = new Bootstrap() + .channel(LocalChannel.class).group(threadA2).localAddress(new LocalAddress("A2")); + final EventLoop threadB1 = new DefaultEventLoop(); + final Bootstrap bootstrapB1 = new Bootstrap() + .channel(LocalChannel.class).group(threadB1).localAddress(new LocalAddress("B1")); + final EventLoop threadB2 = new DefaultEventLoop(); + final Bootstrap bootstrapB2 = new Bootstrap() + .channel(LocalChannel.class).group(threadB2).localAddress(new LocalAddress("B2")); + + final FixedChannelPool poolA1 = new FixedChannelPool(bootstrapA1, NOOP_HANDLER, 1); + final FixedChannelPool poolA2 = new FixedChannelPool(bootstrapB2, NOOP_HANDLER, 1); + final FixedChannelPool poolB1 = new FixedChannelPool(bootstrapB1, NOOP_HANDLER, 1); + final FixedChannelPool poolB2 = new FixedChannelPool(bootstrapA2, NOOP_HANDLER, 1); + + // Synchronize threads on these barriers to ensure order of execution, first wait until each thread is inside + // the newPool callbak, then hold the two threads that should lose the match until the first two returns, then + // release them to test if they deadlock when trying to release their pools on each other's threads. + final CyclicBarrier arrivalBarrier = new CyclicBarrier(4); + final CyclicBarrier releaseBarrier = new CyclicBarrier(3); + + final AbstractChannelPoolMap<String, FixedChannelPool> channelPoolMap = + new AbstractChannelPoolMap<String, FixedChannelPool>() { + + @Override + protected FixedChannelPool newPool(String key) { + + // Thread A1 gets a new pool on eventexecutor thread A1 (anywhere but A2 or B2) + // Thread B1 gets a new pool on eventexecutor thread B1 (anywhere but A2 or B2) + // Thread A2 gets a new pool on eventexecutor thread B2 + // Thread B2 gets a new pool on eventexecutor thread A2 + + if ("A".equals(key)) { + if (threadA1.inEventLoop()) { + // Thread A1 gets pool A with thread A1 + await(arrivalBarrier); + return poolA1; + } else if (threadA2.inEventLoop()) { + // Thread A2 gets pool A with thread B2, but only after A1 won + await(arrivalBarrier); + await(releaseBarrier); + return poolA2; + } + } else if ("B".equals(key)) { + if (threadB1.inEventLoop()) { + // Thread B1 gets pool with thread B1 + await(arrivalBarrier); + return poolB1; + } else if (threadB2.inEventLoop()) { + // Thread B2 gets pool with thread A2 + await(arrivalBarrier); + await(releaseBarrier); + return poolB2; + } + } + throw new AssertionError("Unexpected key=" + key + " or thread=" + + Thread.currentThread().getName()); + } + }; + + // Thread A1 calls ChannelPoolMap.get(A) + // Thread A2 calls ChannelPoolMap.get(A) + // Thread B1 calls ChannelPoolMap.get(B) + // Thread B2 calls ChannelPoolMap.get(B) + + Future<FixedChannelPool> futureA1 = threadA1.submit(new Callable<FixedChannelPool>() { + @Override + public FixedChannelPool call() throws Exception { + return channelPoolMap.get("A"); + } + }); + + Future<FixedChannelPool> futureA2 = threadA2.submit(new Callable<FixedChannelPool>() { + @Override + public FixedChannelPool call() throws Exception { + return channelPoolMap.get("A"); + } + }); + + Future<FixedChannelPool> futureB1 = threadB1.submit(new Callable<FixedChannelPool>() { + @Override + public FixedChannelPool call() throws Exception { + return channelPoolMap.get("B"); + } + }); + + Future<FixedChannelPool> futureB2 = threadB2.submit(new Callable<FixedChannelPool>() { + @Override + public FixedChannelPool call() throws Exception { + return channelPoolMap.get("B"); + } + }); + + // Thread A1 succeeds on updating the map and moves on + // Thread B1 succeeds on updating the map and moves on + // These should always succeed and return with new pools + try { + assertSame(poolA1, futureA1.get(1, TimeUnit.SECONDS)); + assertSame(poolB1, futureB1.get(1, TimeUnit.SECONDS)); + } catch (Exception e) { + shutdown(threadA1, threadA2, threadB1, threadB2); + throw e; + } + + // Now release the other two threads which at this point lost the race and will try to clean up the acquired + // pools. The expected scenario is that both pools close, in case of a deadlock they will hang. + await(releaseBarrier); + + // Thread A2 fails to update the map and submits close to thread B2 + // Thread B2 fails to update the map and submits close to thread A2 + // If the close is blocking, then these calls will time out as the threads are waiting for each other + // If the close is not blocking, then the previously created pools will be returned + try { + assertSame(poolA1, futureA2.get(1, TimeUnit.SECONDS)); + assertSame(poolB1, futureB2.get(1, TimeUnit.SECONDS)); + } catch (TimeoutException e) { + // Fail the test on timeout to distinguish from other errors + throw new AssertionError(e); + } finally { + poolA1.close(); + poolA2.close(); + poolB1.close(); + poolB2.close(); + channelPoolMap.close(); + shutdown(threadA1, threadA2, threadB1, threadB2); + } + } + + @Test + public void testDeadlockOnRemove() throws Exception { + + final EventLoop thread1 = new DefaultEventLoop(); + final Bootstrap bootstrap1 = new Bootstrap() + .channel(LocalChannel.class).group(thread1).localAddress(new LocalAddress("#1")); + final EventLoop thread2 = new DefaultEventLoop(); + final Bootstrap bootstrap2 = new Bootstrap() + .channel(LocalChannel.class).group(thread2).localAddress(new LocalAddress("#2")); + + // pool1 runs on thread2, pool2 runs on thread1 + final FixedChannelPool pool1 = new FixedChannelPool(bootstrap2, NOOP_HANDLER, 1); + final FixedChannelPool pool2 = new FixedChannelPool(bootstrap1, NOOP_HANDLER, 1); + + final AbstractChannelPoolMap<String, FixedChannelPool> channelPoolMap = + new AbstractChannelPoolMap<String, FixedChannelPool>() { + + @Override + protected FixedChannelPool newPool(String key) { + if ("#1".equals(key)) { + return pool1; + } else if ("#2".equals(key)) { + return pool2; + } else { + throw new AssertionError("Unexpected key=" + key); + } + } + }; + + assertSame(pool1, channelPoolMap.get("#1")); + assertSame(pool2, channelPoolMap.get("#2")); + + // thread1 tries to remove pool1 which is running on thread2 + // thread2 tries to remove pool2 which is running on thread1 + + final CyclicBarrier barrier = new CyclicBarrier(2); + + Future<?> future1 = thread1.submit(new Runnable() { + @Override + public void run() { + await(barrier); + channelPoolMap.remove("#1"); + } + }); + + Future<?> future2 = thread2.submit(new Runnable() { + @Override + public void run() { + await(barrier); + channelPoolMap.remove("#2"); + } + }); + + // A blocking close on remove will cause a deadlock here and the test will time out + try { + future1.get(1, TimeUnit.SECONDS); + future2.get(1, TimeUnit.SECONDS); + } catch (TimeoutException e) { + // Fail the test on timeout to distinguish from other errors + throw new AssertionError(e); + } finally { + pool1.close(); + pool2.close(); + channelPoolMap.close(); + shutdown(thread1, thread2); + } + } + + private static void await(CyclicBarrier barrier) { + try { + barrier.await(1, TimeUnit.SECONDS); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static void shutdown(EventLoop... eventLoops) { + for (EventLoop eventLoop : eventLoops) { + eventLoop.shutdownGracefully(0, 0, TimeUnit.SECONDS); + } + } + + private static class NoopHandler extends AbstractChannelPoolHandler { + @Override + public void channelCreated(Channel ch) throws Exception { + // noop + } + }; +} diff --git a/transport/src/test/java/io/netty/channel/pool/FixedChannelPoolTest.java b/transport/src/test/java/io/netty/channel/pool/FixedChannelPoolTest.java index bbf1deb..0ee08fa 100644 --- a/transport/src/test/java/io/netty/channel/pool/FixedChannelPoolTest.java +++ b/transport/src/test/java/io/netty/channel/pool/FixedChannelPoolTest.java @@ -20,30 +20,38 @@ import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.Channel; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPromise; +import io.netty.channel.DefaultEventLoopGroup; import io.netty.channel.EventLoopGroup; import io.netty.channel.local.LocalAddress; import io.netty.channel.local.LocalChannel; -import io.netty.channel.local.LocalEventLoopGroup; import io.netty.channel.local.LocalServerChannel; import io.netty.channel.pool.FixedChannelPool.AcquireTimeoutAction; import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.GenericFutureListener; import org.junit.AfterClass; +import org.junit.Assert; import org.junit.BeforeClass; import org.junit.Test; +import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -import static org.junit.Assert.*; +import static io.netty.channel.pool.ChannelPoolTestUtils.getLocalAddrId; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; public class FixedChannelPoolTest { - private static final String LOCAL_ADDR_ID = "test.id"; - private static EventLoopGroup group; @BeforeClass public static void createEventLoop() { - group = new LocalEventLoopGroup(); + group = new DefaultEventLoopGroup(); } @AfterClass @@ -55,7 +63,7 @@ public class FixedChannelPoolTest { @Test public void testAcquire() throws Exception { - LocalAddress addr = new LocalAddress(LOCAL_ADDR_ID); + LocalAddress addr = new LocalAddress(getLocalAddrId()); Bootstrap cb = new Bootstrap(); cb.remoteAddress(addr); cb.group(group) @@ -88,11 +96,12 @@ public class FixedChannelPoolTest { assertSame(channel, channel2); assertEquals(1, handler.channelCount()); - assertEquals(1, handler.acquiredCount()); + assertEquals(2, handler.acquiredCount()); assertEquals(1, handler.releasedCount()); sc.close().syncUninterruptibly(); channel2.close().syncUninterruptibly(); + pool.close(); } @Test(expected = TimeoutException.class) @@ -106,7 +115,7 @@ public class FixedChannelPoolTest { } private static void testAcquireTimeout(long timeoutMillis) throws Exception { - LocalAddress addr = new LocalAddress(LOCAL_ADDR_ID); + LocalAddress addr = new LocalAddress(getLocalAddrId()); Bootstrap cb = new Bootstrap(); cb.remoteAddress(addr); cb.group(group) @@ -135,12 +144,13 @@ public class FixedChannelPoolTest { } finally { sc.close().syncUninterruptibly(); channel.close().syncUninterruptibly(); + pool.close(); } } @Test public void testAcquireNewConnection() throws Exception { - LocalAddress addr = new LocalAddress(LOCAL_ADDR_ID); + LocalAddress addr = new LocalAddress(getLocalAddrId()); Bootstrap cb = new Bootstrap(); cb.remoteAddress(addr); cb.group(group) @@ -168,6 +178,7 @@ public class FixedChannelPoolTest { sc.close().syncUninterruptibly(); channel.close().syncUninterruptibly(); channel2.close().syncUninterruptibly(); + pool.close(); } /** @@ -176,7 +187,7 @@ public class FixedChannelPoolTest { */ @Test public void testAcquireNewConnectionWhen() throws Exception { - LocalAddress addr = new LocalAddress(LOCAL_ADDR_ID); + LocalAddress addr = new LocalAddress(getLocalAddrId()); Bootstrap cb = new Bootstrap(); cb.remoteAddress(addr); cb.group(group) @@ -205,11 +216,12 @@ public class FixedChannelPoolTest { assertNotSame(channel1, channel2); sc.close().syncUninterruptibly(); channel2.close().syncUninterruptibly(); + pool.close(); } @Test(expected = IllegalStateException.class) public void testAcquireBoundQueue() throws Exception { - LocalAddress addr = new LocalAddress(LOCAL_ADDR_ID); + LocalAddress addr = new LocalAddress(getLocalAddrId()); Bootstrap cb = new Bootstrap(); cb.remoteAddress(addr); cb.group(group) @@ -239,12 +251,13 @@ public class FixedChannelPoolTest { } finally { sc.close().syncUninterruptibly(); channel.close().syncUninterruptibly(); + pool.close(); } } @Test(expected = IllegalArgumentException.class) public void testReleaseDifferentPool() throws Exception { - LocalAddress addr = new LocalAddress(LOCAL_ADDR_ID); + LocalAddress addr = new LocalAddress(getLocalAddrId()); Bootstrap cb = new Bootstrap(); cb.remoteAddress(addr); cb.group(group) @@ -273,12 +286,14 @@ public class FixedChannelPoolTest { } finally { sc.close().syncUninterruptibly(); channel.close().syncUninterruptibly(); + pool.close(); + pool2.close(); } } @Test public void testReleaseAfterClosePool() throws Exception { - LocalAddress addr = new LocalAddress(LOCAL_ADDR_ID); + LocalAddress addr = new LocalAddress(getLocalAddrId()); Bootstrap cb = new Bootstrap(); cb.remoteAddress(addr); cb.group(group).channel(LocalChannel.class); @@ -310,17 +325,18 @@ public class FixedChannelPoolTest { pool.release(channel).syncUninterruptibly(); fail(); } catch (IllegalStateException e) { - assertSame(FixedChannelPool.POOL_CLOSED_ON_RELEASE_EXCEPTION, e); + // expected } // Since the pool is closed, the Channel should have been closed as well. channel.closeFuture().syncUninterruptibly(); assertFalse("Unexpected open channel", channel.isOpen()); sc.close().syncUninterruptibly(); + pool.close(); } @Test public void testReleaseClosed() { - LocalAddress addr = new LocalAddress(LOCAL_ADDR_ID); + LocalAddress addr = new LocalAddress(getLocalAddrId()); Bootstrap cb = new Bootstrap(); cb.remoteAddress(addr); cb.group(group).channel(LocalChannel.class); @@ -344,6 +360,43 @@ public class FixedChannelPoolTest { pool.release(channel).syncUninterruptibly(); sc.close().syncUninterruptibly(); + pool.close(); + } + + @Test + public void testCloseAsync() throws ExecutionException, InterruptedException { + LocalAddress addr = new LocalAddress(getLocalAddrId()); + Bootstrap cb = new Bootstrap(); + cb.remoteAddress(addr); + cb.group(group).channel(LocalChannel.class); + + ServerBootstrap sb = new ServerBootstrap(); + sb.group(group) + .channel(LocalServerChannel.class) + .childHandler(new ChannelInitializer<LocalChannel>() { + @Override + public void initChannel(LocalChannel ch) throws Exception { + ch.pipeline().addLast(new ChannelInboundHandlerAdapter()); + } + }); + + // Start server + final Channel sc = sb.bind(addr).syncUninterruptibly().channel(); + + final FixedChannelPool pool = new FixedChannelPool(cb, new TestChannelPoolHandler(), 2); + + pool.acquire().get(); + pool.acquire().get(); + + final ChannelPromise closePromise = sc.newPromise(); + pool.closeAsync().addListener(new GenericFutureListener<Future<? super Void>>() { + @Override + public void operationComplete(Future<? super Void> future) throws Exception { + Assert.assertEquals(0, pool.acquiredChannelCount()); + sc.close(closePromise).syncUninterruptibly(); + } + }).awaitUninterruptibly(); + closePromise.awaitUninterruptibly(); } private static final class TestChannelPoolHandler extends AbstractChannelPoolHandler { diff --git a/transport/src/test/java/io/netty/channel/pool/SimpleChannelPoolTest.java b/transport/src/test/java/io/netty/channel/pool/SimpleChannelPoolTest.java index a91790c..debb9df 100644 --- a/transport/src/test/java/io/netty/channel/pool/SimpleChannelPoolTest.java +++ b/transport/src/test/java/io/netty/channel/pool/SimpleChannelPoolTest.java @@ -20,29 +20,34 @@ import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.Channel; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.channel.ChannelInitializer; +import io.netty.channel.DefaultEventLoopGroup; import io.netty.channel.EventLoopGroup; import io.netty.channel.local.LocalAddress; import io.netty.channel.local.LocalChannel; -import io.netty.channel.local.LocalEventLoopGroup; import io.netty.channel.local.LocalServerChannel; import io.netty.util.concurrent.Future; import org.hamcrest.CoreMatchers; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; import java.util.Queue; import java.util.concurrent.LinkedBlockingQueue; - -import static org.junit.Assert.*; +import java.util.concurrent.TimeUnit; + +import static io.netty.channel.pool.ChannelPoolTestUtils.getLocalAddrId; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; public class SimpleChannelPoolTest { - private static final String LOCAL_ADDR_ID = "test.id"; - @Test public void testAcquire() throws Exception { - EventLoopGroup group = new LocalEventLoopGroup(); - LocalAddress addr = new LocalAddress(LOCAL_ADDR_ID); + EventLoopGroup group = new DefaultEventLoopGroup(); + LocalAddress addr = new LocalAddress(getLocalAddrId()); Bootstrap cb = new Bootstrap(); cb.remoteAddress(addr); cb.group(group) @@ -82,17 +87,18 @@ public class SimpleChannelPoolTest { assertFalse(channel.isActive()); } - assertEquals(1, handler.acquiredCount()); + assertEquals(2, handler.acquiredCount()); assertEquals(2, handler.releasedCount()); sc.close().sync(); + pool.close(); group.shutdownGracefully(); } @Test public void testBoundedChannelPoolSegment() throws Exception { - EventLoopGroup group = new LocalEventLoopGroup(); - LocalAddress addr = new LocalAddress(LOCAL_ADDR_ID); + EventLoopGroup group = new DefaultEventLoopGroup(); + LocalAddress addr = new LocalAddress(getLocalAddrId()); Bootstrap cb = new Bootstrap(); cb.remoteAddress(addr); cb.group(group) @@ -139,11 +145,12 @@ public class SimpleChannelPoolTest { channel2.close().sync(); assertEquals(2, handler.channelCount()); - assertEquals(0, handler.acquiredCount()); + assertEquals(2, handler.acquiredCount()); assertEquals(1, handler.releasedCount()); sc.close().sync(); channel.close().sync(); channel2.close().sync(); + pool.close(); group.shutdownGracefully(); } @@ -154,8 +161,8 @@ public class SimpleChannelPoolTest { */ @Test public void testUnhealthyChannelIsNotOffered() throws Exception { - EventLoopGroup group = new LocalEventLoopGroup(); - LocalAddress addr = new LocalAddress(LOCAL_ADDR_ID); + EventLoopGroup group = new DefaultEventLoopGroup(); + LocalAddress addr = new LocalAddress(getLocalAddrId()); Bootstrap cb = new Bootstrap(); cb.remoteAddress(addr); cb.group(group) @@ -189,6 +196,7 @@ public class SimpleChannelPoolTest { assertNotSame(channel1, channel3); sc.close().syncUninterruptibly(); channel3.close().syncUninterruptibly(); + pool.close(); group.shutdownGracefully(); } @@ -200,8 +208,8 @@ public class SimpleChannelPoolTest { */ @Test public void testUnhealthyChannelIsOfferedWhenNoHealthCheckRequested() throws Exception { - EventLoopGroup group = new LocalEventLoopGroup(); - LocalAddress addr = new LocalAddress(LOCAL_ADDR_ID); + EventLoopGroup group = new DefaultEventLoopGroup(); + LocalAddress addr = new LocalAddress(getLocalAddrId()); Bootstrap cb = new Bootstrap(); cb.remoteAddress(addr); cb.group(group) @@ -232,6 +240,7 @@ public class SimpleChannelPoolTest { assertNotSame(channel1, channel2); sc.close().syncUninterruptibly(); channel2.close().syncUninterruptibly(); + pool.close(); group.shutdownGracefully(); } @@ -301,4 +310,46 @@ public class SimpleChannelPoolTest { noHealthCheckOnReleasePool.close(); } } + + @Test + public void testCloseAsync() throws Exception { + final LocalAddress addr = new LocalAddress(getLocalAddrId()); + final EventLoopGroup group = new DefaultEventLoopGroup(); + + // Start server + final ServerBootstrap sb = new ServerBootstrap() + .group(group) + .channel(LocalServerChannel.class) + .childHandler(new ChannelInitializer<LocalChannel>() { + @Override + protected void initChannel(LocalChannel ch) throws Exception { + ch.pipeline().addLast(new ChannelInboundHandlerAdapter()); + } + }); + final Channel sc = sb.bind(addr).syncUninterruptibly().channel(); + + // Create pool, acquire and return channels + final Bootstrap bootstrap = new Bootstrap() + .channel(LocalChannel.class).group(group).remoteAddress(addr); + final SimpleChannelPool pool = new SimpleChannelPool(bootstrap, new CountingChannelPoolHandler()); + Channel ch1 = pool.acquire().syncUninterruptibly().getNow(); + Channel ch2 = pool.acquire().syncUninterruptibly().getNow(); + pool.release(ch1).get(1, TimeUnit.SECONDS); + pool.release(ch2).get(1, TimeUnit.SECONDS); + + // Assert that returned channels are open before close + assertTrue(ch1.isOpen()); + assertTrue(ch2.isOpen()); + + // Close asynchronously with timeout + pool.closeAsync().get(1, TimeUnit.SECONDS); + + // Assert channels were indeed closed + assertFalse(ch1.isOpen()); + assertFalse(ch2.isOpen()); + + sc.close().sync(); + pool.close(); + group.shutdownGracefully(); + } } diff --git a/transport/src/test/java/io/netty/channel/socket/nio/NioServerSocketChannelTest.java b/transport/src/test/java/io/netty/channel/socket/nio/NioServerSocketChannelTest.java index ec50223..f9c68b6 100644 --- a/transport/src/test/java/io/netty/channel/socket/nio/NioServerSocketChannelTest.java +++ b/transport/src/test/java/io/netty/channel/socket/nio/NioServerSocketChannelTest.java @@ -15,6 +15,7 @@ */ package io.netty.channel.socket.nio; +import io.netty.channel.Channel; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import org.junit.Assert; @@ -45,6 +46,23 @@ public class NioServerSocketChannelTest extends AbstractNioChannelTest<NioServer } } + @Test + public void testIsActiveFalseAfterClose() { + NioServerSocketChannel serverSocketChannel = new NioServerSocketChannel(); + EventLoopGroup group = new NioEventLoopGroup(1); + try { + group.register(serverSocketChannel).syncUninterruptibly(); + Channel channel = serverSocketChannel.bind(new InetSocketAddress(0)).syncUninterruptibly().channel(); + Assert.assertTrue(channel.isActive()); + Assert.assertTrue(channel.isOpen()); + channel.close().syncUninterruptibly(); + Assert.assertFalse(channel.isOpen()); + Assert.assertFalse(channel.isActive()); + } finally { + group.shutdownGracefully(); + } + } + @Override protected NioServerSocketChannel newNioChannel() { return new NioServerSocketChannel(); diff --git a/transport/test.log b/transport/test.log deleted file mode 100644 index e69de29..0000000 -- GitLab