c4dbbe39c9
Motivation: We should just add `executor()` to the `ChannelOutboundInvoker` interface and override this method in `Channel` to return `EventLoop`. Modifications: - Add `executor()` method to `ChannelOutboundInvoker` - Let `Channel` override this method and return `EventLoop`. - Adjust all usages of `eventLoop()` - Add some default implementations Result: API cleanup
566 lines
27 KiB
Java
566 lines
27 KiB
Java
/*
|
|
* 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:
|
|
*
|
|
* https://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.ChannelHandler;
|
|
import io.netty.channel.ChannelHandlerAdapter;
|
|
import io.netty.channel.ChannelHandlerContext;
|
|
import io.netty.channel.ChannelInitializer;
|
|
import io.netty.channel.EventLoopGroup;
|
|
import io.netty.channel.MultithreadEventLoopGroup;
|
|
import io.netty.channel.nio.NioHandler;
|
|
import io.netty.channel.socket.nio.NioServerSocketChannel;
|
|
import io.netty.channel.socket.nio.NioSocketChannel;
|
|
import io.netty.handler.ssl.ApplicationProtocolConfig;
|
|
import io.netty.handler.ssl.ApplicationProtocolNames;
|
|
import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler;
|
|
import io.netty.handler.ssl.ClientAuth;
|
|
import io.netty.handler.ssl.OpenSsl;
|
|
import io.netty.handler.ssl.SslContext;
|
|
import io.netty.handler.ssl.SslContextBuilder;
|
|
import io.netty.handler.ssl.SslHandshakeCompletionEvent;
|
|
import io.netty.handler.ssl.SslProvider;
|
|
import io.netty.handler.ssl.SupportedCipherSuiteFilter;
|
|
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
|
|
import io.netty.handler.ssl.util.SelfSignedCertificate;
|
|
import io.netty.util.CharsetUtil;
|
|
import io.netty.util.NetUtil;
|
|
import io.netty.util.ReferenceCountUtil;
|
|
import org.junit.jupiter.api.AfterEach;
|
|
import org.junit.jupiter.api.BeforeEach;
|
|
import org.junit.jupiter.api.Disabled;
|
|
import org.junit.jupiter.api.Test;
|
|
import org.junit.jupiter.api.Timeout;
|
|
import org.junit.jupiter.api.condition.DisabledOnOs;
|
|
import org.junit.jupiter.api.condition.OS;
|
|
|
|
import javax.net.ssl.SSLException;
|
|
import javax.net.ssl.X509TrustManager;
|
|
import java.net.InetSocketAddress;
|
|
import java.security.cert.CertificateException;
|
|
import java.security.cert.CertificateExpiredException;
|
|
import java.security.cert.X509Certificate;
|
|
import java.util.concurrent.CountDownLatch;
|
|
import java.util.concurrent.Executors;
|
|
import java.util.concurrent.ScheduledExecutorService;
|
|
import java.util.concurrent.atomic.AtomicReference;
|
|
|
|
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
|
import static org.junit.jupiter.api.Assumptions.assumeTrue;
|
|
|
|
public class Http2MultiplexTransportTest {
|
|
private static final ChannelHandler DISCARD_HANDLER = new ChannelHandlerAdapter() {
|
|
|
|
@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;
|
|
|
|
@BeforeEach
|
|
public void setup() {
|
|
eventLoopGroup = new MultithreadEventLoopGroup(NioHandler.newFactory());
|
|
}
|
|
|
|
@AfterEach
|
|
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(value = 10000, unit = MILLISECONDS)
|
|
public void asyncSettingsAckWithMultiplexCodec() throws Exception {
|
|
asyncSettingsAck0(new Http2MultiplexCodecBuilder(true, DISCARD_HANDLER).build(), null);
|
|
}
|
|
|
|
@Test
|
|
@Timeout(value = 10000, unit = MILLISECONDS)
|
|
public void asyncSettingsAckWithMultiplexHandler() throws Exception {
|
|
asyncSettingsAck0(new Http2FrameCodecBuilder(true).build(),
|
|
new Http2MultiplexHandler(DISCARD_HANDLER));
|
|
}
|
|
|
|
private void asyncSettingsAck0(final Http2FrameCodec codec, final ChannelHandler multiplexer)
|
|
throws Exception {
|
|
// 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 ChannelHandler() {
|
|
@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)).get();
|
|
|
|
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 ChannelHandler() {
|
|
@Override
|
|
public void channelRead(ChannelHandlerContext ctx, Object msg) {
|
|
if (msg instanceof Http2SettingsFrame) {
|
|
clientSettingsLatch.countDown();
|
|
}
|
|
ReferenceCountUtil.release(msg);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
clientChannel = bs.connect(serverChannel.localAddress()).get();
|
|
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(value = 5000L, unit = MILLISECONDS)
|
|
public void testFlushNotDiscarded() throws Exception {
|
|
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 ChannelHandler() {
|
|
@Override
|
|
public void channelRead(final ChannelHandlerContext ctx, Object msg) {
|
|
if (msg instanceof Http2HeadersFrame && ((Http2HeadersFrame) msg).isEndStream()) {
|
|
executorService.schedule(() -> {
|
|
ctx.writeAndFlush(new DefaultHttp2HeadersFrame(
|
|
new DefaultHttp2Headers(), false)).addListener(future -> {
|
|
ctx.write(new DefaultHttp2DataFrame(
|
|
Unpooled.copiedBuffer("Hello World",
|
|
CharsetUtil.US_ASCII), true));
|
|
ctx.channel().executor().execute(ctx::flush);
|
|
});
|
|
}, 500, MILLISECONDS);
|
|
}
|
|
ReferenceCountUtil.release(msg);
|
|
}
|
|
}));
|
|
}
|
|
});
|
|
serverChannel = sb.bind(new InetSocketAddress(NetUtil.LOCALHOST, 0)).get();
|
|
|
|
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()).get();
|
|
Http2StreamChannelBootstrap h2Bootstrap = new Http2StreamChannelBootstrap(clientChannel);
|
|
h2Bootstrap.handler(new ChannelHandler() {
|
|
@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();
|
|
}
|
|
}
|
|
|
|
@Test
|
|
@Timeout(value = 5000L, unit = MILLISECONDS)
|
|
public void testSSLExceptionOpenSslTLSv12() throws Exception {
|
|
testSslException(SslProvider.OPENSSL, false);
|
|
}
|
|
|
|
@Test
|
|
@Timeout(value = 5000L, unit = MILLISECONDS)
|
|
public void testSSLExceptionOpenSslTLSv13() throws Exception {
|
|
testSslException(SslProvider.OPENSSL, true);
|
|
}
|
|
|
|
@Disabled("JDK SSLEngine does not produce an alert")
|
|
@Test
|
|
@Timeout(value = 5000L, unit = MILLISECONDS)
|
|
public void testSSLExceptionJDKTLSv12() throws Exception {
|
|
testSslException(SslProvider.JDK, false);
|
|
}
|
|
|
|
@Disabled("JDK SSLEngine does not produce an alert")
|
|
@Test
|
|
@Timeout(value = 5000L, unit = MILLISECONDS)
|
|
public void testSSLExceptionJDKTLSv13() throws Exception {
|
|
testSslException(SslProvider.JDK, true);
|
|
}
|
|
|
|
private void testSslException(SslProvider provider, final boolean tlsv13) throws Exception {
|
|
assumeTrue(SslProvider.isAlpnSupported(provider));
|
|
if (tlsv13) {
|
|
assumeTrue(SslProvider.isTlsv13Supported(provider));
|
|
}
|
|
final String protocol = tlsv13 ? "TLSv1.3" : "TLSv1.2";
|
|
SelfSignedCertificate ssc = null;
|
|
try {
|
|
ssc = new SelfSignedCertificate();
|
|
final SslContext sslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey())
|
|
.trustManager(new X509TrustManager() {
|
|
@Override
|
|
public void checkClientTrusted(X509Certificate[] chain, String authType)
|
|
throws CertificateException {
|
|
throw new CertificateExpiredException();
|
|
}
|
|
|
|
@Override
|
|
public void checkServerTrusted(X509Certificate[] chain, String authType)
|
|
throws CertificateException {
|
|
throw new CertificateExpiredException();
|
|
}
|
|
|
|
@Override
|
|
public X509Certificate[] getAcceptedIssuers() {
|
|
return new X509Certificate[0];
|
|
}
|
|
}).sslProvider(provider)
|
|
.ciphers(Http2SecurityUtil.CIPHERS, SupportedCipherSuiteFilter.INSTANCE)
|
|
.protocols(protocol)
|
|
.applicationProtocolConfig(new ApplicationProtocolConfig(
|
|
ApplicationProtocolConfig.Protocol.ALPN,
|
|
// NO_ADVERTISE is currently the only mode supported by both OpenSsl and JDK providers.
|
|
ApplicationProtocolConfig.SelectorFailureBehavior.NO_ADVERTISE,
|
|
// ACCEPT is currently the only mode supported by both OpenSsl and JDK providers.
|
|
ApplicationProtocolConfig.SelectedListenerFailureBehavior.ACCEPT,
|
|
ApplicationProtocolNames.HTTP_2,
|
|
ApplicationProtocolNames.HTTP_1_1)).clientAuth(ClientAuth.REQUIRE)
|
|
.build();
|
|
|
|
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(sslCtx.newHandler(ch.alloc()));
|
|
ch.pipeline().addLast(new Http2FrameCodecBuilder(true).build());
|
|
ch.pipeline().addLast(new Http2MultiplexHandler(DISCARD_HANDLER));
|
|
}
|
|
});
|
|
serverChannel = sb.bind(new InetSocketAddress(NetUtil.LOCALHOST, 0)).get();
|
|
|
|
final SslContext clientCtx = SslContextBuilder.forClient()
|
|
.keyManager(ssc.key(), ssc.cert())
|
|
.sslProvider(provider)
|
|
/* NOTE: the cipher filter may not include all ciphers required by the HTTP/2 specification.
|
|
* Please refer to the HTTP/2 specification for cipher requirements. */
|
|
.ciphers(Http2SecurityUtil.CIPHERS, SupportedCipherSuiteFilter.INSTANCE)
|
|
.trustManager(InsecureTrustManagerFactory.INSTANCE)
|
|
.protocols(protocol)
|
|
.applicationProtocolConfig(new ApplicationProtocolConfig(
|
|
ApplicationProtocolConfig.Protocol.ALPN,
|
|
// NO_ADVERTISE is currently the only mode supported by both OpenSsl and JDK providers.
|
|
ApplicationProtocolConfig.SelectorFailureBehavior.NO_ADVERTISE,
|
|
// ACCEPT is currently the only mode supported by both OpenSsl and JDK providers.
|
|
ApplicationProtocolConfig.SelectedListenerFailureBehavior.ACCEPT,
|
|
ApplicationProtocolNames.HTTP_2,
|
|
ApplicationProtocolNames.HTTP_1_1))
|
|
.build();
|
|
|
|
final CountDownLatch latch = new CountDownLatch(2);
|
|
final AtomicReference<AssertionError> errorRef = new AtomicReference<AssertionError>();
|
|
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(clientCtx.newHandler(ch.alloc()));
|
|
ch.pipeline().addLast(new Http2FrameCodecBuilder(false).build());
|
|
ch.pipeline().addLast(new Http2MultiplexHandler(DISCARD_HANDLER));
|
|
ch.pipeline().addLast(new ChannelHandler() {
|
|
@Override
|
|
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
|
|
if (evt instanceof SslHandshakeCompletionEvent) {
|
|
SslHandshakeCompletionEvent handshakeCompletionEvent =
|
|
(SslHandshakeCompletionEvent) evt;
|
|
if (handshakeCompletionEvent.isSuccess()) {
|
|
// In case of TLSv1.3 we should succeed the handshake. The alert for
|
|
// the mTLS failure will be send in the next round-trip.
|
|
if (!tlsv13) {
|
|
errorRef.set(new AssertionError("TLSv1.3 expected"));
|
|
}
|
|
|
|
Http2StreamChannelBootstrap h2Bootstrap =
|
|
new Http2StreamChannelBootstrap(ctx.channel());
|
|
h2Bootstrap.handler(new ChannelHandler() {
|
|
@Override
|
|
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
|
|
if (cause.getCause() instanceof SSLException) {
|
|
latch.countDown();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void channelInactive(ChannelHandlerContext ctx) {
|
|
latch.countDown();
|
|
}
|
|
});
|
|
h2Bootstrap.open().addListener(future -> {
|
|
if (future.isSuccess()) {
|
|
future.getNow().writeAndFlush(new DefaultHttp2HeadersFrame(
|
|
new DefaultHttp2Headers(), false));
|
|
}
|
|
});
|
|
|
|
} else if (handshakeCompletionEvent.cause() instanceof SSLException) {
|
|
// In case of TLSv1.2 we should never see the handshake succeed as the alert for
|
|
// the mTLS failure will be send in the same round-trip.
|
|
if (tlsv13) {
|
|
errorRef.set(new AssertionError("TLSv1.2 expected"));
|
|
}
|
|
latch.countDown();
|
|
latch.countDown();
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
});
|
|
clientChannel = bs.connect(serverChannel.localAddress()).get();
|
|
latch.await();
|
|
AssertionError error = errorRef.get();
|
|
if (error != null) {
|
|
throw error;
|
|
}
|
|
} finally {
|
|
if (ssc != null) {
|
|
ssc.delete();
|
|
}
|
|
}
|
|
}
|
|
|
|
@Test
|
|
@DisabledOnOs(value = OS.WINDOWS, disabledReason = "See: https://github.com/netty/netty/issues/11542")
|
|
@Timeout(value = 5000L, unit = MILLISECONDS)
|
|
public void testFireChannelReadAfterHandshakeSuccess_JDK() throws Exception {
|
|
assumeTrue(SslProvider.isAlpnSupported(SslProvider.JDK));
|
|
testFireChannelReadAfterHandshakeSuccess(SslProvider.JDK);
|
|
}
|
|
|
|
@Disabled("This fails atm... needs investigation")
|
|
@Test
|
|
@DisabledOnOs(value = OS.WINDOWS, disabledReason = "See: https://github.com/netty/netty/issues/11542")
|
|
@Timeout(value = 5000L, unit = MILLISECONDS)
|
|
public void testFireChannelReadAfterHandshakeSuccess_OPENSSL() throws Exception {
|
|
assumeTrue(OpenSsl.isAvailable());
|
|
assumeTrue(SslProvider.isAlpnSupported(SslProvider.OPENSSL));
|
|
testFireChannelReadAfterHandshakeSuccess(SslProvider.OPENSSL);
|
|
}
|
|
|
|
private void testFireChannelReadAfterHandshakeSuccess(SslProvider provider) throws Exception {
|
|
SelfSignedCertificate ssc = null;
|
|
try {
|
|
ssc = new SelfSignedCertificate();
|
|
final SslContext serverCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey())
|
|
.sslProvider(provider)
|
|
.ciphers(Http2SecurityUtil.CIPHERS, SupportedCipherSuiteFilter.INSTANCE)
|
|
.applicationProtocolConfig(new ApplicationProtocolConfig(
|
|
ApplicationProtocolConfig.Protocol.ALPN,
|
|
ApplicationProtocolConfig.SelectorFailureBehavior.NO_ADVERTISE,
|
|
ApplicationProtocolConfig.SelectedListenerFailureBehavior.ACCEPT,
|
|
ApplicationProtocolNames.HTTP_2,
|
|
ApplicationProtocolNames.HTTP_1_1))
|
|
.build();
|
|
|
|
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(serverCtx.newHandler(ch.alloc()));
|
|
ch.pipeline().addLast(new ApplicationProtocolNegotiationHandler(ApplicationProtocolNames.HTTP_1_1) {
|
|
@Override
|
|
protected void configurePipeline(ChannelHandlerContext ctx, String protocol) {
|
|
ctx.pipeline().addLast(new Http2FrameCodecBuilder(true).build());
|
|
ctx.pipeline().addLast(new Http2MultiplexHandler(new ChannelHandler() {
|
|
@Override
|
|
public void channelRead(final ChannelHandlerContext ctx, Object msg) {
|
|
if (msg instanceof Http2HeadersFrame && ((Http2HeadersFrame) msg).isEndStream()) {
|
|
ctx.writeAndFlush(new DefaultHttp2HeadersFrame(
|
|
new DefaultHttp2Headers(), false))
|
|
.addListener(future -> {
|
|
ctx.writeAndFlush(new DefaultHttp2DataFrame(
|
|
Unpooled.copiedBuffer("Hello World", CharsetUtil.US_ASCII),
|
|
true));
|
|
});
|
|
}
|
|
ReferenceCountUtil.release(msg);
|
|
}
|
|
}));
|
|
}
|
|
});
|
|
}
|
|
});
|
|
serverChannel = sb.bind(new InetSocketAddress(NetUtil.LOCALHOST, 0)).get();
|
|
|
|
final SslContext clientCtx = SslContextBuilder.forClient()
|
|
.sslProvider(provider)
|
|
.ciphers(Http2SecurityUtil.CIPHERS, SupportedCipherSuiteFilter.INSTANCE)
|
|
.trustManager(InsecureTrustManagerFactory.INSTANCE)
|
|
.applicationProtocolConfig(new ApplicationProtocolConfig(
|
|
ApplicationProtocolConfig.Protocol.ALPN,
|
|
ApplicationProtocolConfig.SelectorFailureBehavior.NO_ADVERTISE,
|
|
ApplicationProtocolConfig.SelectedListenerFailureBehavior.ACCEPT,
|
|
ApplicationProtocolNames.HTTP_2,
|
|
ApplicationProtocolNames.HTTP_1_1))
|
|
.build();
|
|
|
|
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(clientCtx.newHandler(ch.alloc()));
|
|
ch.pipeline().addLast(new Http2FrameCodecBuilder(false).build());
|
|
ch.pipeline().addLast(new Http2MultiplexHandler(DISCARD_HANDLER));
|
|
ch.pipeline().addLast(new ChannelHandler() {
|
|
@Override
|
|
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
|
|
if (evt instanceof SslHandshakeCompletionEvent) {
|
|
SslHandshakeCompletionEvent handshakeCompletionEvent =
|
|
(SslHandshakeCompletionEvent) evt;
|
|
if (handshakeCompletionEvent.isSuccess()) {
|
|
Http2StreamChannelBootstrap h2Bootstrap =
|
|
new Http2StreamChannelBootstrap(clientChannel);
|
|
h2Bootstrap.handler(new ChannelHandler() {
|
|
@Override
|
|
public void channelRead(ChannelHandlerContext ctx, Object msg) {
|
|
if (msg instanceof Http2DataFrame && ((Http2DataFrame) msg).isEndStream()) {
|
|
latch.countDown();
|
|
}
|
|
ReferenceCountUtil.release(msg);
|
|
}
|
|
});
|
|
h2Bootstrap.open().addListener(future -> {
|
|
if (future.isSuccess()) {
|
|
future.getNow().writeAndFlush(new DefaultHttp2HeadersFrame(
|
|
new DefaultHttp2Headers(), true));
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
});
|
|
clientChannel = bs.connect(serverChannel.localAddress()).get();
|
|
|
|
latch.await();
|
|
} finally {
|
|
if (ssc != null) {
|
|
ssc.delete();
|
|
}
|
|
}
|
|
}
|
|
}
|