/* * JBoss, Home of Professional Open Source. * Copyright 2014 Red Hat, Inc., and individual contributors * as indicated by the @author tags. * * Licensed 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.undertow.server.protocol.http2; import io.netty.bootstrap.Bootstrap; import io.netty.buffer.ByteBuf; import io.netty.channel.Channel; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelOption; import io.netty.channel.ChannelPipeline; import io.netty.channel.ChannelPromise; import io.netty.channel.EventLoopGroup; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; 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.HttpClientUpgradeHandler; 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.HttpVersion; import io.netty.handler.codec.http2.DefaultHttp2Connection; import io.netty.handler.codec.http2.DefaultHttp2FrameReader; import io.netty.handler.codec.http2.DefaultHttp2FrameWriter; import io.netty.handler.codec.http2.DelegatingDecompressorFrameListener; import io.netty.handler.codec.http2.Http2ClientUpgradeCodec; import io.netty.handler.codec.http2.Http2Connection; import io.netty.handler.codec.http2.Http2FrameLogger; import io.netty.handler.codec.http2.Http2FrameReader; import io.netty.handler.codec.http2.Http2FrameWriter; import io.netty.handler.codec.http2.Http2InboundFrameLogger; import io.netty.handler.codec.http2.Http2OutboundFrameLogger; import io.netty.handler.codec.http2.Http2Settings; import io.netty.handler.codec.http2.HttpConversionUtil; import io.netty.handler.codec.http2.HttpToHttp2ConnectionHandler; import io.netty.handler.codec.http2.HttpToHttp2ConnectionHandlerBuilder; import io.netty.handler.codec.http2.InboundHttp2ToHttpAdapterBuilder; import io.netty.handler.logging.LogLevel; import io.undertow.Handlers; import io.undertow.Undertow; import io.undertow.UndertowOptions; import io.undertow.server.HttpHandler; import io.undertow.server.HttpServerExchange; import io.undertow.server.session.SessionCookieConfig; import io.undertow.testutils.DefaultServer; import io.undertow.testutils.HttpOneOnly; import io.undertow.util.Headers; import io.undertow.util.HttpString; import org.junit.AfterClass; import org.junit.Assert; import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; import org.xnio.Options; import java.net.URI; import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; import java.util.Iterator; import java.util.Map.Entry; import java.util.SortedMap; import java.util.TreeMap; import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.TimeUnit; /** * Tests the load balancing proxy * * @author Stuart Douglas */ @RunWith(DefaultServer.class) @HttpOneOnly public class HTTP2ViaUpgradeTestCase { static Undertow server; static volatile String message; private static final LinkedBlockingDeque<String> messages = new LinkedBlockingDeque<>(); @BeforeClass public static void setup() throws URISyntaxException { final SessionCookieConfig sessionConfig = new SessionCookieConfig(); int port = DefaultServer.getHostPort("default"); server = Undertow.builder() .addHttpListener(port + 1, DefaultServer.getHostAddress("default")) .setServerOption(UndertowOptions.ENABLE_HTTP2, true) .setSocketOption(Options.REUSE_ADDRESSES, true) .setHandler(Handlers.header(new Http2UpgradeHandler(new HttpHandler() { @Override public void handleRequest(HttpServerExchange exchange) throws Exception { if (!(exchange.getConnection() instanceof Http2ServerConnection)) { throw new RuntimeException("Not HTTP2"); } exchange.getResponseHeaders().add(new HttpString("X-Custom-Header"), "foo"); exchange.getResponseSender().send(message); } }, "h2c", "h2c-17"), Headers.SEC_WEB_SOCKET_ACCEPT_STRING, "fake")) //work around Netty bug, it assumes that every upgrade request that does not have this header is an old style websocket upgrade .build(); server.start(); } @AfterClass public static void stop() { server.stop(); } @Test public void testHttp2WithNettyClient() throws Exception { message = "Hello World"; EventLoopGroup workerGroup = new NioEventLoopGroup(); Http2ClientInitializer initializer = new Http2ClientInitializer(Integer.MAX_VALUE); try { // Configure the client. Bootstrap b = new Bootstrap(); b.group(workerGroup); b.channel(NioSocketChannel.class); b.option(ChannelOption.SO_KEEPALIVE, true); final int port = DefaultServer.getHostPort("default") + 1; final String host = DefaultServer.getHostAddress("default"); b.remoteAddress(host, port); b.handler(initializer); // Start the client. Channel channel = b.connect().syncUninterruptibly().channel(); Http2SettingsHandler http2SettingsHandler = initializer.settingsHandler(); http2SettingsHandler.awaitSettings(5, TimeUnit.SECONDS); HttpResponseHandler responseHandler = initializer.responseHandler(); int streamId = 3; URI hostName = URI.create("http://" + host + ':' + port); System.err.println("Sending request(s)..."); // Create a simple GET request. final ChannelPromise promise = channel.newPromise(); responseHandler.put(streamId, promise); FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, hostName.toString()); request.headers().add(HttpHeaderNames.HOST, hostName); request.headers().add(HttpHeaderNames.ACCEPT_ENCODING, HttpHeaderValues.GZIP); request.headers().add(HttpHeaderNames.ACCEPT_ENCODING, HttpHeaderValues.DEFLATE); channel.writeAndFlush(request); streamId += 2; promise.await(10, TimeUnit.SECONDS); Assert.assertEquals(message, messages.poll()); System.out.println("Finished HTTP/2 request(s)"); // Wait until the connection is closed. channel.close().syncUninterruptibly(); } finally { workerGroup.shutdownGracefully(); } } static class Http2ClientInitializer extends ChannelInitializer<SocketChannel> { private static final Http2FrameLogger logger = new Http2FrameLogger(LogLevel.INFO, Http2ClientInitializer.class); private final int maxContentLength; private HttpToHttp2ConnectionHandler connectionHandler; private HttpResponseHandler responseHandler; private Http2SettingsHandler settingsHandler; Http2ClientInitializer(int maxContentLength) { this.maxContentLength = maxContentLength; } @Override public void initChannel(SocketChannel ch) throws Exception { final Http2Connection connection = new DefaultHttp2Connection(false); connectionHandler = new HttpToHttp2ConnectionHandlerBuilder() .connection(connection) .frameListener(new DelegatingDecompressorFrameListener(connection, new InboundHttp2ToHttpAdapterBuilder(connection) .maxContentLength(maxContentLength) .propagateSettings(true) .build())) .build(); responseHandler = new HttpResponseHandler(); settingsHandler = new Http2SettingsHandler(ch.newPromise()); configureClearText(ch); } public HttpResponseHandler responseHandler() { return responseHandler; } public Http2SettingsHandler settingsHandler() { return settingsHandler; } protected void configureEndOfPipeline(ChannelPipeline pipeline) { pipeline.addLast(settingsHandler, responseHandler); } /** * Configure the pipeline for a cleartext upgrade from HTTP to HTTP/2. */ private void configureClearText(SocketChannel ch) { HttpClientCodec sourceCodec = new HttpClientCodec(); Http2ClientUpgradeCodec upgradeCodec = new Http2ClientUpgradeCodec(connectionHandler); HttpClientUpgradeHandler upgradeHandler = new HttpClientUpgradeHandler(sourceCodec, upgradeCodec, 65536); ch.pipeline().addLast(sourceCodec, upgradeHandler, new UpgradeRequestHandler(), new UserEventLogger()); } /** * 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, "/sdf"); upgradeRequest.headers().add(Headers.HOST_STRING, "default"); ctx.writeAndFlush(upgradeRequest); ctx.fireChannelActive(); // Done with this handler, remove it from the pipeline. ctx.pipeline().remove(this); configureEndOfPipeline(ctx.pipeline()); } } /** * Class that logs any User Events triggered on this channel. */ private static class UserEventLogger extends ChannelInboundHandlerAdapter { @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { System.out.println("User Event Triggered: " + evt); ctx.fireUserEventTriggered(evt); } } private static Http2FrameReader frameReader() { return new Http2InboundFrameLogger(new DefaultHttp2FrameReader(), logger); } private static Http2FrameWriter frameWriter() { return new Http2OutboundFrameLogger(new DefaultHttp2FrameWriter(), logger); } } static class Http2SettingsHandler extends SimpleChannelInboundHandler<Http2Settings> { private ChannelPromise promise; /** * Create new instance * * @param promise Promise object used to notify when first settings are received */ Http2SettingsHandler(ChannelPromise promise) { this.promise = promise; } /** * Wait for this handler to be added after the upgrade to HTTP/2, and for initial preface * handshake to complete. * * @param timeout Time to wait * @param unit {@link java.util.concurrent.TimeUnit} for {@code timeout} * @throws Exception if timeout or other failure occurs */ public void awaitSettings(long timeout, TimeUnit unit) throws Exception { if (!promise.awaitUninterruptibly(timeout, unit)) { throw new IllegalStateException("Timed out waiting for settings"); } if (!promise.isSuccess()) { throw new RuntimeException(promise.cause()); } } @Override protected void channelRead0(ChannelHandlerContext ctx, Http2Settings msg) throws Exception { promise.setSuccess(); // Only care about the first settings message ctx.pipeline().remove(this); } } static class HttpResponseHandler extends SimpleChannelInboundHandler<FullHttpResponse> { private SortedMap<Integer, ChannelPromise> streamidPromiseMap; HttpResponseHandler() { streamidPromiseMap = new TreeMap<Integer, ChannelPromise>(); } /** * Create an association between an anticipated response stream id and a {@link io.netty.channel.ChannelPromise} * * @param streamId The stream for which a response is expected * @param promise The promise object that will be used to wait/notify events * @return The previous object associated with {@code streamId} * @see HttpResponseHandler#awaitResponses(long, java.util.concurrent.TimeUnit) */ public ChannelPromise put(int streamId, ChannelPromise promise) { return streamidPromiseMap.put(streamId, promise); } /** * Wait (sequentially) for a time duration for each anticipated response * * @param timeout Value of time to wait for each response * @param unit Units associated with {@code timeout} * @see HttpResponseHandler#put(int, io.netty.channel.ChannelPromise) */ public void awaitResponses(long timeout, TimeUnit unit) { Iterator<Entry<Integer, ChannelPromise>> itr = streamidPromiseMap.entrySet().iterator(); while (itr.hasNext()) { Entry<Integer, ChannelPromise> entry = itr.next(); ChannelPromise promise = entry.getValue(); if (!promise.awaitUninterruptibly(timeout, unit)) { throw new IllegalStateException("Timed out waiting for response on stream id " + entry.getKey()); } if (!promise.isSuccess()) { throw new RuntimeException(promise.cause()); } System.out.println("---Stream id: " + entry.getKey() + " received---"); itr.remove(); } } @Override protected void channelRead0(ChannelHandlerContext ctx, FullHttpResponse msg) throws Exception { Integer streamId = msg.headers().getInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text()); if (streamId == null) { System.err.println("HttpResponseHandler unexpected message received: " + msg); return; } ChannelPromise promise = streamidPromiseMap.get(streamId); if (promise == null) { System.err.println("Message received for unknown stream id " + streamId); } else { // Do stuff with the message (for now just print it) ByteBuf content = msg.content(); if (content.isReadable()) { int contentLength = content.readableBytes(); byte[] arr = new byte[contentLength]; content.readBytes(arr); messages.add(new String(arr, StandardCharsets.UTF_8)); } promise.setSuccess(); } } } }