HTTP2 example uses SSL and NPN.

Motivation:

HTTP2 is generally negotiated over SSL, makes more sense to provide an SSL example.

Modifications:

Copy the SDPY example to add SSL and NPN negotiation to the HTTP2 example.

Result:

Http2Server and Http2Client examples use SSL.
This commit is contained in:
Scott Blum 2014-04-15 18:20:07 -04:00 committed by Norman Maurer
parent bb063a9fdd
commit c66aae3539
10 changed files with 459 additions and 23 deletions

View File

@ -0,0 +1,159 @@
/*
* Copyright 2014 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.draft10;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPipeline;
import io.netty.handler.codec.ByteToMessageDecoder;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpRequestDecoder;
import io.netty.handler.codec.http.HttpResponseEncoder;
import io.netty.handler.codec.http2.draft10.connection.Http2ConnectionHandler;
import io.netty.handler.codec.http2.draft10.frame.Http2FrameCodec;
import io.netty.handler.ssl.SslHandler;
import javax.net.ssl.SSLEngine;
import java.util.List;
/**
* {@link io.netty.channel.ChannelHandler} which is responsible to setup the {@link io.netty.channel.ChannelPipeline}
* either for HTTP or HTTP2. This offers an easy way for users to support both at the same time while not care to much
* about the low-level details.
*/
public abstract class Http2OrHttpChooser extends ByteToMessageDecoder {
// TODO: Replace with generic NPN handler
public enum SelectedProtocol {
/** Must be updated to match the HTTP/2 draft number. */
HTTP_2("h2-10"),
HTTP_1_1("http/1.1"),
HTTP_1_0("http/1.0"),
UNKNOWN("Unknown");
private final String name;
SelectedProtocol(String defaultName) {
name = defaultName;
}
public String protocolName() {
return name;
}
/**
* Get an instance of this enum based on the protocol name returned by the NPN server provider
*
* @param name
* the protocol name
* @return the SelectedProtocol instance
*/
public static SelectedProtocol protocol(String name) {
for (SelectedProtocol protocol : SelectedProtocol.values()) {
if (protocol.protocolName().equals(name)) {
return protocol;
}
}
return UNKNOWN;
}
}
private final int maxHttpContentLength;
protected Http2OrHttpChooser(int maxHttpContentLength) {
this.maxHttpContentLength = maxHttpContentLength;
}
/**
* Return the {@link SelectedProtocol} for the {@link javax.net.ssl.SSLEngine}. If its not known yet implementations
* MUST return {@link SelectedProtocol#UNKNOWN}.
*
*/
protected abstract SelectedProtocol getProtocol(SSLEngine engine);
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
if (initPipeline(ctx)) {
// When we reached here we can remove this handler as its now clear
// what protocol we want to use
// from this point on. This will also take care of forward all
// messages.
ctx.pipeline().remove(this);
}
}
private boolean initPipeline(ChannelHandlerContext ctx) {
// Get the SslHandler from the ChannelPipeline so we can obtain the
// SslEngine from it.
SslHandler handler = ctx.pipeline().get(SslHandler.class);
if (handler == null) {
// HTTP2 is negotiated through SSL.
throw new IllegalStateException("SslHandler is needed for HTTP2");
}
SelectedProtocol protocol = getProtocol(handler.engine());
switch (protocol) {
case UNKNOWN:
// Not done with choosing the protocol, so just return here for now,
return false;
case HTTP_2:
addHttp2Handlers(ctx);
break;
case HTTP_1_0:
case HTTP_1_1:
addHttpHandlers(ctx);
break;
default:
throw new IllegalStateException("Unknown SelectedProtocol");
}
return true;
}
/**
* Add all {@link io.netty.channel.ChannelHandler}'s that are needed for HTTP_2.
*/
protected void addHttp2Handlers(ChannelHandlerContext ctx) {
ChannelPipeline pipeline = ctx.pipeline();
pipeline.addLast("http2FrameCodec", new Http2FrameCodec());
pipeline.addLast("http2ConnectionHandler", new Http2ConnectionHandler(true));
pipeline.addLast("http2RequestHandler", createHttp2RequestHandler());
}
/**
* Add all {@link io.netty.channel.ChannelHandler}'s that are needed for HTTP.
*/
protected void addHttpHandlers(ChannelHandlerContext ctx) {
ChannelPipeline pipeline = ctx.pipeline();
pipeline.addLast("httpRequestDecoder", new HttpRequestDecoder());
pipeline.addLast("httpResponseEncoder", new HttpResponseEncoder());
pipeline.addLast("httpChunkAggregator", new HttpObjectAggregator(maxHttpContentLength));
pipeline.addLast("httpRequestHandler", createHttp1RequestHandler());
}
/**
* Create the {@link io.netty.channel.ChannelHandler} that is responsible for handling the http requests when the
* {@link SelectedProtocol} was {@link SelectedProtocol#HTTP_1_0} or {@link SelectedProtocol#HTTP_1_1}
*/
protected abstract ChannelHandler createHttp1RequestHandler();
/**
* Create the {@link io.netty.channel.ChannelHandler} that is responsible for handling the http responses when the
* when the {@link SelectedProtocol} was {@link SelectedProtocol#HTTP_2}.
*/
protected abstract ChannelHandler createHttp2RequestHandler();
}

View File

@ -23,6 +23,7 @@ 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.example.http2.server.Http2Server;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http2.draft10.DefaultHttp2Headers;
import io.netty.handler.codec.http2.draft10.Http2Headers;
@ -99,6 +100,7 @@ public class Http2Client {
}
public static void main(String[] args) throws Exception {
Http2Server.checkForNpnSupport();
int port;
if (args.length > 0) {
port = Integer.parseInt(args[0]);

View File

@ -18,25 +18,38 @@ package io.netty.example.http2.client;
import static io.netty.util.internal.logging.InternalLogLevel.INFO;
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.securechat.SecureChatSslContextFactory;
import io.netty.handler.codec.http2.draft10.connection.Http2ConnectionHandler;
import io.netty.handler.codec.http2.draft10.frame.Http2DataFrame;
import io.netty.handler.codec.http2.draft10.frame.Http2FrameCodec;
import io.netty.handler.ssl.SslHandler;
import org.eclipse.jetty.npn.NextProtoNego;
import javax.net.ssl.SSLEngine;
/**
* Configures the client pipeline to support HTTP/2 frames.
*/
public class Http2ClientInitializer extends ChannelInitializer<SocketChannel> {
private final Http2ResponseClientHandler httpResponseHandler;
private final SimpleChannelInboundHandler<Http2DataFrame> httpResponseHandler;
public Http2ClientInitializer(Http2ResponseClientHandler httpResponseHandler) {
public Http2ClientInitializer(SimpleChannelInboundHandler<Http2DataFrame> httpResponseHandler) {
this.httpResponseHandler = httpResponseHandler;
}
@Override
public void initChannel(SocketChannel ch) throws Exception {
SSLEngine engine = SecureChatSslContextFactory.getClientContext().createSSLEngine();
engine.setUseClientMode(true);
NextProtoNego.put(engine, new Http2ClientProvider());
NextProtoNego.debug = true;
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast("ssl", new SslHandler(engine));
pipeline.addLast("http2FrameCodec", new Http2FrameCodec());
pipeline.addLast("http2FrameLogger", new Http2FrameLogger(INFO));
pipeline.addLast("http2ConnectionHandler", new Http2ConnectionHandler(false));

View File

@ -0,0 +1,58 @@
/*
* Copyright 2014 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.client;
import org.eclipse.jetty.npn.NextProtoNego;
import java.util.List;
import static io.netty.handler.codec.http2.draft10.Http2OrHttpChooser.SelectedProtocol.*;
/**
* The Jetty project provides an implementation of the Transport Layer Security (TLS) extension for Next Protocol
* Negotiation (NPN) for OpenJDK 7 or greater. NPN allows the application layer to negotiate which protocol to use
* over the secure connection.
* <p>
* This NPN service provider negotiates using HTTP2.
* <p>
* To enable NPN support, start the JVM with: {@code java -Xbootclasspath/p:<path_to_npn_boot_jar> ...}. The
* "path_to_npn_boot_jar" is the path on the file system for the NPN Boot Jar file which can be downloaded from Maven
* at coordinates org.mortbay.jetty.npn:npn-boot. Different versions applies to different OpenJDK versions.
*
* @see <a href="http://www.eclipse.org/jetty/documentation/current/npn-chapter.html">Jetty documentation</a>
*/
public class Http2ClientProvider implements NextProtoNego.ClientProvider {
private String selectedProtocol;
@Override
public String selectProtocol(List<String> protocols) {
if (protocols.contains(HTTP_2.protocolName())) {
selectedProtocol = HTTP_2.protocolName();
}
return selectedProtocol;
}
@Override
public boolean supports() {
return true;
}
@Override
public void unsupported() {
selectedProtocol = HTTP_1_1.protocolName();
}
}

View File

@ -0,0 +1,62 @@
/*
* Copyright 2014 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.server;
import io.netty.buffer.ByteBuf;
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.HttpRequest;
import static io.netty.handler.codec.http.HttpHeaders.Names.*;
import static io.netty.handler.codec.http.HttpHeaders.*;
import static io.netty.handler.codec.http.HttpResponseStatus.*;
import static io.netty.handler.codec.http.HttpVersion.*;
/**
* HTTP handler that responds with a "Hello World"
*/
public class HelloWorldHttp1Handler extends SimpleChannelInboundHandler<HttpRequest> {
@Override
public void messageReceived(ChannelHandlerContext ctx, HttpRequest req) throws Exception {
if (is100ContinueExpected(req)) {
ctx.write(new DefaultFullHttpResponse(HTTP_1_1, CONTINUE));
}
boolean keepAlive = isKeepAlive(req);
ByteBuf content = ctx.alloc().buffer();
content.writeBytes(HelloWorldHttp2Handler.RESPONSE_BYTES);
FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, OK, content);
response.headers().set(CONTENT_TYPE, "text/plain; charset=UTF-8");
response.headers().set(CONTENT_LENGTH, response.content().readableBytes());
if (!keepAlive) {
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
} else {
response.headers().set(CONNECTION, Values.KEEP_ALIVE);
ctx.writeAndFlush(response);
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
}
}

View File

@ -16,8 +16,8 @@
package io.netty.example.http2.server;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http2.draft10.DefaultHttp2Headers;
import io.netty.handler.codec.http2.draft10.Http2Headers;
import io.netty.handler.codec.http2.draft10.frame.DefaultHttp2DataFrame;
@ -26,24 +26,19 @@ import io.netty.handler.codec.http2.draft10.frame.Http2DataFrame;
import io.netty.handler.codec.http2.draft10.frame.Http2HeadersFrame;
import io.netty.handler.codec.http2.draft10.frame.Http2StreamFrame;
import io.netty.util.CharsetUtil;
import io.netty.util.ReferenceCountUtil;
/**
* A simple handler that responds with the message "Hello World!".
*/
public class HelloWorldHandler extends ChannelHandlerAdapter {
private static final byte[] RESPONSE_BYTES = "Hello World".getBytes(CharsetUtil.UTF_8);
public class HelloWorldHttp2Handler extends SimpleChannelInboundHandler<Http2StreamFrame> {
static final byte[] RESPONSE_BYTES = "Hello World".getBytes(CharsetUtil.UTF_8);
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof Http2StreamFrame) {
Http2StreamFrame frame = (Http2StreamFrame) msg;
if (frame.isEndOfStream()) {
sendResponse(ctx, frame.getStreamId());
}
public void messageReceived(ChannelHandlerContext ctx, Http2StreamFrame frame) throws Exception {
if (frame.isEndOfStream()) {
sendResponse(ctx, frame.getStreamId());
}
ReferenceCountUtil.release(msg);
}
@Override
@ -51,7 +46,7 @@ public class HelloWorldHandler extends ChannelHandlerAdapter {
cause.printStackTrace();
}
private void sendResponse(ChannelHandlerContext ctx, int streamId) {
private static void sendResponse(ChannelHandlerContext ctx, int streamId) {
ByteBuf content = ctx.alloc().buffer();
content.writeBytes(RESPONSE_BYTES);

View File

@ -0,0 +1,60 @@
/*
* Copyright 2014 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.server;
import io.netty.channel.ChannelHandler;
import io.netty.handler.codec.http2.draft10.Http2OrHttpChooser;
import org.eclipse.jetty.npn.NextProtoNego;
import javax.net.ssl.SSLEngine;
import java.util.logging.Logger;
/**
* 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 Http2OrHttpChooser {
private static final Logger logger = Logger.getLogger(
Http2OrHttpHandler.class.getName());
private static final int MAX_CONTENT_LENGTH = 1024 * 100;
public Http2OrHttpHandler() {
this(MAX_CONTENT_LENGTH);
}
public Http2OrHttpHandler(int maxHttpContentLength) {
super(maxHttpContentLength);
}
@Override
protected SelectedProtocol getProtocol(SSLEngine engine) {
Http2ServerProvider provider = (Http2ServerProvider) NextProtoNego.get(engine);
SelectedProtocol selectedProtocol = provider.getSelectedProtocol();
logger.info("Selected Protocol is " + selectedProtocol);
return selectedProtocol;
}
@Override
protected ChannelHandler createHttp1RequestHandler() {
return new HelloWorldHttp1Handler();
}
@Override
protected ChannelHandler createHttp2RequestHandler() {
return new HelloWorldHttp2Handler();
}
}

View File

@ -55,6 +55,7 @@ public class Http2Server {
}
public static void main(String[] args) throws Exception {
checkForNpnSupport();
int port;
if (args.length > 0) {
port = Integer.parseInt(args[0]);
@ -66,4 +67,18 @@ public class Http2Server {
new Http2Server(port).run();
}
public static void checkForNpnSupport() {
try {
Class.forName("sun.security.ssl.NextProtoNegoExtension");
} catch (ClassNotFoundException ignored) {
System.err.println();
System.err.println("Could not locate Next Protocol Negotiation (NPN) implementation.");
System.err.println("The NPN jar should have been made available when building the examples with maven.");
System.err.println("Please check that your JDK is among those supported by Jetty-NPN:");
System.err.println("http://wiki.eclipse.org/Jetty/Feature/NPN#Versions");
System.err.println();
throw new IllegalStateException("Could not locate NPN implementation. See console err for details.");
}
}
}

View File

@ -16,13 +16,14 @@
package io.netty.example.http2.server;
import static io.netty.util.internal.logging.InternalLogLevel.INFO;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.example.http2.client.Http2FrameLogger;
import io.netty.handler.codec.http2.draft10.connection.Http2ConnectionHandler;
import io.netty.handler.codec.http2.draft10.frame.Http2FrameCodec;
import io.netty.example.securechat.SecureChatSslContextFactory;
import io.netty.handler.ssl.SslHandler;
import org.eclipse.jetty.npn.NextProtoNego;
import javax.net.ssl.SSLEngine;
/**
* Sets up the Netty pipeline
@ -32,9 +33,15 @@ public class Http2ServerInitializer extends ChannelInitializer<SocketChannel> {
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
p.addLast("http2FrameCodec", new Http2FrameCodec());
p.addLast("http2FrameLogger", new Http2FrameLogger(INFO));
p.addLast("http2ConnectionHandler", new Http2ConnectionHandler(true));
p.addLast("helloWorldHandler", new HelloWorldHandler());
SSLEngine engine = SecureChatSslContextFactory.getServerContext().createSSLEngine();
engine.setUseClientMode(false);
p.addLast("ssl", new SslHandler(engine));
// Setup NextProtoNego with our server provider
NextProtoNego.put(engine, new Http2ServerProvider());
NextProtoNego.debug = true;
// Negotiates with the browser if HTTP2 or HTTP is going to be used
p.addLast("handler", new Http2OrHttpHandler());
}
}

View File

@ -0,0 +1,65 @@
/*
* Copyright 2014 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.server;
import io.netty.handler.codec.http2.draft10.Http2OrHttpChooser;
import org.eclipse.jetty.npn.NextProtoNego.ServerProvider;
import java.util.Arrays;
import java.util.List;
import static io.netty.handler.codec.http2.draft10.Http2OrHttpChooser.SelectedProtocol.*;
/**
* The Jetty project provides an implementation of the Transport Layer Security (TLS) extension for Next
* Protocol Negotiation (NPN) for OpenJDK 7 or greater. NPN allows the application layer to negotiate which
* protocol to use over the secure connection.
* <p>
* This NPN service provider negotiates using HTTP_2.
* <p>
* To enable NPN support, start the JVM with: {@code java -Xbootclasspath/p:<path_to_npn_boot_jar> ...}. The
* "path_to_npn_boot_jar" is the path on the file system for the NPN Boot Jar file which can be downloaded from
* Maven at coordinates org.mortbay.jetty.npn:npn-boot. Different versions applies to different OpenJDK versions.
*
* @see <a href="http://www.eclipse.org/jetty/documentation/current/npn-chapter.html">Jetty documentation</a>
*/
public class Http2ServerProvider implements ServerProvider {
private String selectedProtocol;
@Override
public void unsupported() {
// if unsupported, default to http/1.1
selectedProtocol = HTTP_1_1.protocolName();
}
@Override
public List<String> protocols() {
return Arrays.asList(HTTP_2.protocolName(), HTTP_1_1.protocolName());
}
@Override
public void protocolSelected(String protocol) {
selectedProtocol = protocol;
}
public Http2OrHttpChooser.SelectedProtocol getSelectedProtocol() {
if (selectedProtocol == null) {
return UNKNOWN;
}
return protocol(selectedProtocol);
}
}