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 d288e196a6..d49fd92198 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 @@ -363,7 +363,7 @@ public class Http2FrameCodec extends ChannelDuplexHandler { dataFrame.streamId(streamId); ctx.fireChannelRead(dataFrame); - // We return the bytes in bytesConsumed() once the stream channel consumed the bytes. + // We return the bytes in consumeBytes() once the stream channel consumed the bytes. return 0; } } 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 458de4a6c2..2f3d6de67d 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,6 +17,7 @@ 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; @@ -95,6 +96,25 @@ public class Http2ServerUpgradeCodec implements HttpServerUpgradeHandler.Upgrade this(handlerName, http2Codec.frameCodec().connectionHandler(), http2Codec); } + /** + * Creates the codec using a default name for the connection handler when adding to the + * pipeline. + * + * @param http2Codec the HTTP/2 frame handler. + * @param handlers the handlers that will handle the {@link Http2Frame}s. + */ + public Http2ServerUpgradeCodec(final Http2FrameCodec http2Codec, final ChannelHandler... handlers) { + this(null, http2Codec.connectionHandler(), new ChannelHandlerAdapter() { + + @Override + public void handlerAdded(ChannelHandlerContext ctx) throws Exception { + ctx.pipeline().addLast(http2Codec); + ctx.pipeline().addLast(handlers); + ctx.pipeline().remove(this); + } + }); + } + Http2ServerUpgradeCodec(String handlerName, Http2ConnectionHandler connectionHandler, ChannelHandler upgradeToHandler) { this.handlerName = handlerName; diff --git a/example/src/main/java/io/netty/example/http2/helloworld/frame/server/HelloWorldHttp2Handler.java b/example/src/main/java/io/netty/example/http2/helloworld/frame/server/HelloWorldHttp2Handler.java new file mode 100644 index 0000000000..b7d1e383f0 --- /dev/null +++ b/example/src/main/java/io/netty/example/http2/helloworld/frame/server/HelloWorldHttp2Handler.java @@ -0,0 +1,104 @@ +/* + * 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.example.http2.helloworld.frame.server; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelHandler.Sharable; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http2.DefaultHttp2DataFrame; +import io.netty.handler.codec.http2.DefaultHttp2Headers; +import io.netty.handler.codec.http2.DefaultHttp2HeadersFrame; +import io.netty.handler.codec.http2.DefaultHttp2WindowUpdateFrame; +import io.netty.handler.codec.http2.Http2DataFrame; +import io.netty.handler.codec.http2.Http2Headers; +import io.netty.handler.codec.http2.Http2HeadersFrame; +import io.netty.util.CharsetUtil; + +import static io.netty.buffer.Unpooled.copiedBuffer; +import static io.netty.buffer.Unpooled.unreleasableBuffer; +import static io.netty.handler.codec.http.HttpResponseStatus.OK; + +/** + * A simple handler that responds with the message "Hello World!". + * + *

This example is making use of the "frame codec" http2 API. This API is very experimental and incomplete. + */ +@Sharable +public class HelloWorldHttp2Handler extends ChannelDuplexHandler { + + static final ByteBuf RESPONSE_BYTES = unreleasableBuffer(copiedBuffer("Hello World", CharsetUtil.UTF_8)); + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + super.exceptionCaught(ctx, cause); + cause.printStackTrace(); + ctx.close(); + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + if (msg instanceof Http2HeadersFrame) { + onHeadersRead(ctx, (Http2HeadersFrame) msg); + } else if (msg instanceof Http2DataFrame) { + onDataRead(ctx, (Http2DataFrame) msg); + } else { + super.channelRead(ctx, msg); + } + } + + /** + * If receive a frame with end-of-stream set, send a pre-canned response. + */ + public void onDataRead(ChannelHandlerContext ctx, Http2DataFrame data) throws Exception { + int consumed = data.padding() + data.content().readableBytes(); + int streamId = data.streamId(); + + if (data.isEndStream()) { + sendResponse(ctx, streamId, data.content()); + } else { + // We do not send back the response to the remote-peer, so we need to release it. + data.release(); + } + + // Update the flowcontroller + ctx.write(new DefaultHttp2WindowUpdateFrame(consumed).streamId(streamId)); + } + + /** + * If receive a frame with end-of-stream set, send a pre-canned response. + */ + public void onHeadersRead(ChannelHandlerContext ctx, Http2HeadersFrame headers) + throws Exception { + if (headers.isEndStream()) { + ByteBuf content = ctx.alloc().buffer(); + content.writeBytes(RESPONSE_BYTES.duplicate()); + ByteBufUtil.writeAscii(content, " - via HTTP/2"); + sendResponse(ctx, headers.streamId(), content); + } + } + + /** + * Sends a "Hello World" DATA frame to the client. + */ + private static void sendResponse(ChannelHandlerContext ctx, int streamId, ByteBuf payload) { + // Send a frame for the response status + Http2Headers headers = new DefaultHttp2Headers().status(OK.codeAsText()); + ctx.write(new DefaultHttp2HeadersFrame(headers).streamId(streamId)); + ctx.writeAndFlush(new DefaultHttp2DataFrame(payload, true).streamId(streamId)); + } +} 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 new file mode 100644 index 0000000000..3743e5992c --- /dev/null +++ b/example/src/main/java/io/netty/example/http2/helloworld/frame/server/Http2OrHttpHandler.java @@ -0,0 +1,53 @@ +/* + * 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.example.http2.helloworld.frame.server; + +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.ssl.ApplicationProtocolNames; +import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler; + +/** + * Negotiates with the browser if HTTP2 or HTTP is going to be used. Once decided, the Netty + * pipeline is setup with the correct handlers for the selected protocol. + */ +public class Http2OrHttpHandler extends ApplicationProtocolNegotiationHandler { + + private static final int MAX_CONTENT_LENGTH = 1024 * 100; + + protected Http2OrHttpHandler() { + super(ApplicationProtocolNames.HTTP_1_1); + } + + @Override + protected void configurePipeline(ChannelHandlerContext ctx, String protocol) throws Exception { + if (ApplicationProtocolNames.HTTP_2.equals(protocol)) { + ctx.pipeline().addLast(new Http2FrameCodec(true), new HelloWorldHttp2Handler()); + return; + } + + if (ApplicationProtocolNames.HTTP_1_1.equals(protocol)) { + ctx.pipeline().addLast(new HttpServerCodec(), + new HttpObjectAggregator(MAX_CONTENT_LENGTH), + new HelloWorldHttp1Handler("ALPN Negotiation")); + return; + } + + throw new IllegalStateException("unknown protocol: " + protocol); + } +} 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 new file mode 100644 index 0000000000..f363721d68 --- /dev/null +++ b/example/src/main/java/io/netty/example/http2/helloworld/frame/server/Http2Server.java @@ -0,0 +1,96 @@ +/* + * 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.example.http2.helloworld.frame.server; + +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.codec.http2.Http2SecurityUtil; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; +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.OpenSsl; +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.SelfSignedCertificate; + +/** + * A HTTP/2 Server that responds to requests with a Hello World. Once started, you can test the + * server with the example client. + * + *

This example is making use of the "multiplexing" http2 API, where streams are mapped to child + * Channels. This API is very experimental and incomplete. + */ +public final class Http2Server { + + static final boolean SSL = System.getProperty("ssl") != null; + + static final int PORT = Integer.parseInt(System.getProperty("port", SSL? "8443" : "8080")); + + public static void main(String[] args) throws Exception { + // Configure SSL. + final SslContext sslCtx; + if (SSL) { + SslProvider provider = OpenSsl.isAlpnSupported() ? SslProvider.OPENSSL : SslProvider.JDK; + SelfSignedCertificate ssc = new SelfSignedCertificate(); + sslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()) + .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) + .applicationProtocolConfig(new ApplicationProtocolConfig( + Protocol.ALPN, + // NO_ADVERTISE is currently the only mode supported by both OpenSsl and JDK providers. + SelectorFailureBehavior.NO_ADVERTISE, + // ACCEPT is currently the only mode supported by both OpenSsl and JDK providers. + SelectedListenerFailureBehavior.ACCEPT, + ApplicationProtocolNames.HTTP_2, + ApplicationProtocolNames.HTTP_1_1)) + .build(); + } else { + sslCtx = null; + } + // Configure the server. + EventLoopGroup group = new NioEventLoopGroup(); + try { + ServerBootstrap b = new ServerBootstrap(); + b.option(ChannelOption.SO_BACKLOG, 1024); + b.group(group) + .channel(NioServerSocketChannel.class) + .handler(new LoggingHandler(LogLevel.INFO)) + .childHandler(new Http2ServerInitializer(sslCtx)); + + Channel ch = b.bind(PORT).sync().channel(); + + System.err.println("Open your HTTP/2-enabled web browser and navigate to " + + (SSL? "https" : "http") + "://127.0.0.1:" + PORT + '/'); + + ch.closeFuture().sync(); + } finally { + group.shutdownGracefully(); + } + } +} 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 new file mode 100644 index 0000000000..0fb29b9f35 --- /dev/null +++ b/example/src/main/java/io/netty/example/http2/helloworld/frame/server/Http2ServerInitializer.java @@ -0,0 +1,122 @@ +/* + * 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.example.http2.helloworld.frame.server; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.channel.socket.SocketChannel; +import io.netty.example.http2.helloworld.server.HelloWorldHttp1Handler; +import io.netty.handler.codec.http.HttpMessage; +import io.netty.handler.codec.http.HttpObjectAggregator; +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.Http2CodecUtil; +import io.netty.handler.codec.http2.Http2FrameCodec; +import io.netty.handler.codec.http2.Http2ServerUpgradeCodec; +import io.netty.handler.ssl.SslContext; +import io.netty.util.AsciiString; +import io.netty.util.ReferenceCountUtil; + +/** + * Sets up the Netty pipeline for the example server. Depending on the endpoint config, sets up the + * pipeline for NPN or cleartext HTTP upgrade to HTTP/2. + */ +public class Http2ServerInitializer extends ChannelInitializer { + + private static final UpgradeCodecFactory upgradeCodecFactory = new UpgradeCodecFactory() { + @Override + public UpgradeCodec newUpgradeCodec(CharSequence protocol) { + if (AsciiString.contentEquals(Http2CodecUtil.HTTP_UPGRADE_PROTOCOL_NAME, protocol)) { + return new Http2ServerUpgradeCodec(new Http2FrameCodec(true), new HelloWorldHttp2Handler()); + } else { + return null; + } + } + }; + + private final SslContext sslCtx; + private final int maxHttpContentLength; + + public Http2ServerInitializer(SslContext sslCtx) { + this(sslCtx, 16 * 1024); + } + + public Http2ServerInitializer(SslContext sslCtx, int maxHttpContentLength) { + if (maxHttpContentLength < 0) { + throw new IllegalArgumentException("maxHttpContentLength (expected >= 0): " + maxHttpContentLength); + } + this.sslCtx = sslCtx; + this.maxHttpContentLength = maxHttpContentLength; + } + + @Override + public void initChannel(SocketChannel ch) { + if (sslCtx != null) { + configureSsl(ch); + } else { + configureClearText(ch); + } + } + + /** + * Configure the pipeline for TLS NPN negotiation to HTTP/2. + */ + private void configureSsl(SocketChannel ch) { + ch.pipeline().addLast(sslCtx.newHandler(ch.alloc()), new Http2OrHttpHandler()); + } + + /** + * Configure the pipeline for a cleartext upgrade from HTTP to HTTP/2.0 + */ + private void configureClearText(SocketChannel ch) { + final ChannelPipeline p = ch.pipeline(); + final HttpServerCodec sourceCodec = new HttpServerCodec(); + + p.addLast(sourceCodec); + p.addLast(new HttpServerUpgradeHandler(sourceCodec, upgradeCodecFactory)); + p.addLast(new SimpleChannelInboundHandler() { + @Override + protected void channelRead0(ChannelHandlerContext ctx, HttpMessage msg) throws Exception { + // 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.replace(this, null, new HttpObjectAggregator(maxHttpContentLength)); + ctx.fireChannelRead(ReferenceCountUtil.retain(msg)); + } + }); + + p.addLast(new UserEventLogger()); + } + + /** + * Class that logs any User Events triggered on this channel. + */ + private static class UserEventLogger extends ChannelInboundHandlerAdapter { + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { + System.out.println("User Event Triggered: " + evt); + ctx.fireUserEventTriggered(evt); + } + } +}