Improve the API design of Http2OrHttpChooser and SpdyOrHttpChooser

Related: #3641 and #3813

Motivation:

When setting up an HTTP/1 or HTTP/2 (or SPDY) pipeline, a user usually
ends up with adding arbitrary set of handlers.

Http2OrHttpChooser and SpdyOrHttpChooser have two abstract methods
(create*Handler()) that expect a user to return a single handler, and
also have add*Handlers() methods that add the handler returned by
create*Handler() to the pipeline as well as the pre-defined set of
handlers.

The problem is, some users (read: I) don't need all of them or the
user wants to add more than one handler. For example, take a look at
io.netty.example.http2.tiles.Http2OrHttpHandler, which works around
this issue by overriding addHttp2Handlers() and making
createHttp2RequestHandler() a no-op.

Modifications:

- Replace add*Handlers() and create*Handler() with configure*()
- Rename getProtocol() to selectProtocol() to make what it does clear
- Provide the default implementation of selectProtocol()
- Remove SelectedProtocol.UNKNOWN and use null instead, because
  'UNKNOWN' is not a protocol
- Proper exception handling in the *OrHttpChooser so that the
  exception is logged and the connection is closed when failed to
  select a protocol
- Make SpdyClient example always use SSL. It was always using SSL
  anyway.
- Implement SslHandshakeCompletionEvent.toString() for debuggability
- Remove an orphaned class: JettyNpnSslSession
- Add SslHandler.applicationProtocol() to get the name of the
  application protocol
  - SSLSession.getProtocol() now returns transport-layer protocol name
    only, so that it conforms to its contract.

Result:

- *OrHttpChooser have better API.
- *OrHttpChooser handle protocol selection failure properly.
- SSLSession.getProtocol() now conforms to its contract.
- SpdyClient example works with SpdyServer example out of the box
This commit is contained in:
Trustin Lee 2015-05-30 17:53:46 +09:00
parent 9bcfef0f10
commit afb46b926f
15 changed files with 277 additions and 433 deletions

View File

@ -16,18 +16,16 @@
package io.netty.handler.codec.spdy; package io.netty.handler.codec.spdy;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandler; import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandler; import io.netty.channel.ChannelInboundHandler;
import io.netty.channel.ChannelPipeline; import io.netty.channel.ChannelPipeline;
import io.netty.handler.codec.ByteToMessageDecoder; import io.netty.handler.codec.ByteToMessageDecoder;
import io.netty.handler.codec.http.HttpObjectAggregator; import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.HttpRequestDecoder;
import io.netty.handler.codec.http.HttpResponseEncoder;
import io.netty.handler.ssl.SslHandler; import io.netty.handler.ssl.SslHandler;
import io.netty.util.internal.StringUtil; import io.netty.util.internal.logging.InternalLogger;
import io.netty.util.internal.logging.InternalLoggerFactory;
import javax.net.ssl.SSLEngine;
import java.util.List; import java.util.List;
/** /**
@ -37,13 +35,14 @@ import java.util.List;
*/ */
public abstract class SpdyOrHttpChooser extends ByteToMessageDecoder { public abstract class SpdyOrHttpChooser extends ByteToMessageDecoder {
private static final InternalLogger logger = InternalLoggerFactory.getInstance(SpdyOrHttpChooser.class);
// TODO: Replace with generic NPN handler // TODO: Replace with generic NPN handler
public enum SelectedProtocol { public enum SelectedProtocol {
SPDY_3_1("spdy/3.1"), SPDY_3_1("spdy/3.1"),
HTTP_1_1("http/1.1"), HTTP_1_1("http/1.1"),
HTTP_1_0("http/1.0"), HTTP_1_0("http/1.0");
UNKNOWN("Unknown");
private final String name; private final String name;
@ -58,9 +57,8 @@ public abstract class SpdyOrHttpChooser extends ByteToMessageDecoder {
/** /**
* Get an instance of this enum based on the protocol name returned by the NPN server provider * Get an instance of this enum based on the protocol name returned by the NPN server provider
* *
* @param name * @param name the protocol name
* the protocol name * @return the selected protocol or {@code null} if there is no match
* @return the SelectedProtocol instance
*/ */
public static SelectedProtocol protocol(String name) { public static SelectedProtocol protocol(String name) {
for (SelectedProtocol protocol : SelectedProtocol.values()) { for (SelectedProtocol protocol : SelectedProtocol.values()) {
@ -68,36 +66,15 @@ public abstract class SpdyOrHttpChooser extends ByteToMessageDecoder {
return protocol; return protocol;
} }
} }
return UNKNOWN; return null;
} }
} }
private final int maxSpdyContentLength; protected SpdyOrHttpChooser() { }
private final int maxHttpContentLength;
protected SpdyOrHttpChooser(int maxSpdyContentLength, int maxHttpContentLength) {
this.maxSpdyContentLength = maxSpdyContentLength;
this.maxHttpContentLength = maxHttpContentLength;
}
/**
* Return the {@link SelectedProtocol} for the {@link SSLEngine}. If its not known yet implementations MUST return
* {@link SelectedProtocol#UNKNOWN}.
*
*/
protected SelectedProtocol getProtocol(SSLEngine engine) {
String[] protocol = StringUtil.split(engine.getSession().getProtocol(), ':');
if (protocol.length < 2) {
// Use HTTP/1.1 as default
return SelectedProtocol.HTTP_1_1;
}
SelectedProtocol selectedProtocol = SelectedProtocol.protocol(protocol[1]);
return selectedProtocol;
}
@Override @Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception { protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
if (initPipeline(ctx)) { if (configurePipeline(ctx)) {
// When we reached here we can remove this handler as its now clear // When we reached here we can remove this handler as its now clear
// what protocol we want to use // what protocol we want to use
// from this point on. This will also take care of forward all // from this point on. This will also take care of forward all
@ -106,72 +83,95 @@ public abstract class SpdyOrHttpChooser extends ByteToMessageDecoder {
} }
} }
private boolean initPipeline(ChannelHandlerContext ctx) { private boolean configurePipeline(ChannelHandlerContext ctx) {
// Get the SslHandler from the ChannelPipeline so we can obtain the // Get the SslHandler from the ChannelPipeline so we can obtain the
// SslEngine from it. // SslEngine from it.
SslHandler handler = ctx.pipeline().get(SslHandler.class); SslHandler handler = ctx.pipeline().get(SslHandler.class);
if (handler == null) { if (handler == null) {
// SslHandler is needed by SPDY by design. // SslHandler is needed by SPDY by design.
throw new IllegalStateException("SslHandler is needed for SPDY"); throw new IllegalStateException("cannot find a SslHandler in the pipeline (required for SPDY)");
} }
SelectedProtocol protocol = getProtocol(handler.engine()); if (!handler.handshakeFuture().isDone()) {
switch (protocol) {
case UNKNOWN:
// Not done with choosing the protocol, so just return here for now,
return false; return false;
}
SelectedProtocol protocol;
try {
protocol = selectProtocol(handler);
} catch (Exception e) {
throw new IllegalStateException("failed to get the selected protocol", e);
}
if (protocol == null) {
throw new IllegalStateException("unknown protocol");
}
switch (protocol) {
case SPDY_3_1: case SPDY_3_1:
addSpdyHandlers(ctx, SpdyVersion.SPDY_3_1); try {
configureSpdy(ctx, SpdyVersion.SPDY_3_1);
} catch (Exception e) {
throw new IllegalStateException("failed to configure a SPDY pipeline", e);
}
break; break;
case HTTP_1_0: case HTTP_1_0:
case HTTP_1_1: case HTTP_1_1:
addHttpHandlers(ctx); try {
configureHttp1(ctx);
} catch (Exception e) {
throw new IllegalStateException("failed to configure a HTTP/1 pipeline", e);
}
break; break;
default:
throw new IllegalStateException("Unknown SelectedProtocol");
} }
return true; return true;
} }
/** /**
* Add all {@link ChannelHandler}'s that are needed for SPDY with the given version. * Returns the {@link SelectedProtocol} for the current SSL session. By default, this method returns the first
*/ * known protocol.
protected void addSpdyHandlers(ChannelHandlerContext ctx, SpdyVersion version) {
ChannelPipeline pipeline = ctx.pipeline();
pipeline.addLast("spdyFrameCodec", new SpdyFrameCodec(version));
pipeline.addLast("spdySessionHandler", new SpdySessionHandler(version, true));
pipeline.addLast("spdyHttpEncoder", new SpdyHttpEncoder(version));
pipeline.addLast("spdyHttpDecoder", new SpdyHttpDecoder(version, maxSpdyContentLength));
pipeline.addLast("spdyStreamIdHandler", new SpdyHttpResponseStreamIdHandler());
pipeline.addLast("httpRequestHandler", createHttpRequestHandlerForSpdy());
}
/**
* Add all {@link 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", createHttpRequestHandlerForHttp());
}
/**
* Create the {@link ChannelInboundHandler} 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 ChannelInboundHandler createHttpRequestHandlerForHttp();
/**
* Create the {@link ChannelInboundHandler} that is responsible for handling the http responses
* when the {@link SelectedProtocol} was {@link SelectedProtocol#SPDY_3_1}.
* *
* By default this getMethod will just delecate to {@link #createHttpRequestHandlerForHttp()}, but sub-classes may * @return the selected application-level protocol, or {@code null} if the application-level protocol name of
* override this to change the behaviour. * the specified {@code sslHandler} is neither {@code "http/1.1"}, {@code "http/1.0"} nor {@code "spdy/3.1"}
*/ */
protected ChannelInboundHandler createHttpRequestHandlerForSpdy() { protected SelectedProtocol selectProtocol(SslHandler sslHandler) throws Exception {
return createHttpRequestHandlerForHttp(); final String appProto = sslHandler.applicationProtocol();
return appProto != null? SelectedProtocol.protocol(appProto) : SelectedProtocol.HTTP_1_1;
}
/**
* Configures the {@link Channel} of the specified {@code ctx} for HTTP/2.
* <p>
* A typical implementation of this method will look like the following:
* <pre>
* {@link ChannelPipeline} p = ctx.pipeline();
* p.addLast(new {@link SpdyFrameCodec}(version));
* p.addLast(new {@link SpdySessionHandler}(version, true));
* p.addLast(new {@link SpdyHttpEncoder}(version));
* p.addLast(new {@link SpdyHttpDecoder}(version, <i>maxSpdyContentLength</i>));
* p.addLast(new {@link SpdyHttpResponseStreamIdHandler}());
* p.addLast(new <i>YourHttpRequestHandler</i>());
* </pre>
* </p>
*/
protected abstract void configureSpdy(ChannelHandlerContext ctx, SpdyVersion version) throws Exception;
/**
* Configures the {@link Channel} of the specified {@code ctx} for HTTP/1.
* <p>
* A typical implementation of this method will look like the following:
* <pre>
* {@link ChannelPipeline} p = ctx.pipeline();
* p.addLast(new {@link HttpServerCodec}());
* p.addLast(new <i>YourHttpRequestHandler</i>());
* </pre>
* </p>
*/
protected abstract void configureHttp1(ChannelHandlerContext ctx) throws Exception;
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
logger.warn("{} Failed to select the application-level protocol:", ctx.channel(), cause);
ctx.close();
} }
} }

View File

@ -15,34 +15,35 @@
*/ */
package io.netty.handler.codec.http2; package io.netty.handler.codec.http2;
import static io.netty.handler.codec.http2.Http2CodecUtil.TLS_UPGRADE_PROTOCOL_NAME;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPipeline; import io.netty.channel.ChannelPipeline;
import io.netty.handler.codec.ByteToMessageDecoder; import io.netty.handler.codec.ByteToMessageDecoder;
import io.netty.handler.codec.http.HttpObjectAggregator; import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.HttpRequestDecoder;
import io.netty.handler.codec.http.HttpResponseEncoder;
import io.netty.handler.ssl.SslHandler; import io.netty.handler.ssl.SslHandler;
import io.netty.util.internal.logging.InternalLogger;
import io.netty.util.internal.logging.InternalLoggerFactory;
import java.util.List; import java.util.List;
import javax.net.ssl.SSLEngine; import static io.netty.handler.codec.http2.Http2CodecUtil.*;
/** /**
* {@link io.netty.channel.ChannelHandler} which is responsible to setup the * {@link ChannelHandler} which is responsible to setup the
* {@link io.netty.channel.ChannelPipeline} either for HTTP or HTTP2. This offers an easy way for * {@link 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. * users to support both at the same time while not care to much about the low-level details.
*/ */
public abstract class Http2OrHttpChooser extends ByteToMessageDecoder { public abstract class Http2OrHttpChooser extends ByteToMessageDecoder {
private static final InternalLogger logger = InternalLoggerFactory.getInstance(Http2OrHttpChooser.class);
public enum SelectedProtocol { public enum SelectedProtocol {
/** Must be updated to match the HTTP/2 draft number. */ /** Must be updated to match the HTTP/2 draft number. */
HTTP_2(TLS_UPGRADE_PROTOCOL_NAME), HTTP_2(TLS_UPGRADE_PROTOCOL_NAME),
HTTP_1_1("http/1.1"), HTTP_1_1("http/1.1"),
HTTP_1_0("http/1.0"), HTTP_1_0("http/1.0");
UNKNOWN("Unknown");
private final String name; private final String name;
@ -55,11 +56,10 @@ public abstract class Http2OrHttpChooser extends ByteToMessageDecoder {
} }
/** /**
* Get an instance of this enum based on the protocol name returned by the NPN server provider * Get an instance of this enum based on the protocol name returned by the ALPN server provider
* *
* @param name * @param name the protocol name
* the protocol name * @return the selected protocol or {@code null} if there is no match
* @return the SelectedProtocol instance
*/ */
public static SelectedProtocol protocol(String name) { public static SelectedProtocol protocol(String name) {
for (SelectedProtocol protocol : SelectedProtocol.values()) { for (SelectedProtocol protocol : SelectedProtocol.values()) {
@ -67,25 +67,15 @@ public abstract class Http2OrHttpChooser extends ByteToMessageDecoder {
return protocol; return protocol;
} }
} }
return UNKNOWN; return null;
} }
} }
private final int maxHttpContentLength; protected Http2OrHttpChooser() { }
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 @Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception { protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
if (initPipeline(ctx)) { if (configurePipeline(ctx)) {
// When we reached here we can remove this handler as its now clear // When we reached here we can remove this handler as its now clear
// what protocol we want to use // what protocol we want to use
// from this point on. This will also take care of forward all // from this point on. This will also take care of forward all
@ -94,62 +84,87 @@ public abstract class Http2OrHttpChooser extends ByteToMessageDecoder {
} }
} }
private boolean initPipeline(ChannelHandlerContext ctx) { private boolean configurePipeline(ChannelHandlerContext ctx) {
// Get the SslHandler from the ChannelPipeline so we can obtain the // Get the SslHandler from the ChannelPipeline so we can obtain the
// SslEngine from it. // SslEngine from it.
SslHandler handler = ctx.pipeline().get(SslHandler.class); SslHandler handler = ctx.pipeline().get(SslHandler.class);
if (handler == null) { if (handler == null) {
// HTTP2 is negotiated through SSL. // HTTP/2 is negotiated through SSL.
throw new IllegalStateException("SslHandler is needed for HTTP2"); throw new IllegalStateException("cannot find a SslHandler in the pipeline (required for HTTP/2)");
}
if (!handler.handshakeFuture().isDone()) {
return false;
}
SelectedProtocol protocol;
try {
protocol = selectProtocol(handler);
} catch (Exception e) {
throw new IllegalStateException("failed to get the selected protocol", e);
}
if (protocol == null) {
throw new IllegalStateException("unknown protocol");
} }
SelectedProtocol protocol = getProtocol(handler.engine());
switch (protocol) { switch (protocol) {
case UNKNOWN:
// Not done with choosing the protocol, so just return here for now,
return false;
case HTTP_2: case HTTP_2:
addHttp2Handlers(ctx); try {
configureHttp2(ctx);
} catch (Exception e) {
throw new IllegalStateException("failed to configure a HTTP/2 pipeline", e);
}
break; break;
case HTTP_1_0: case HTTP_1_0:
case HTTP_1_1: case HTTP_1_1:
addHttpHandlers(ctx); try {
configureHttp1(ctx);
} catch (Exception e) {
throw new IllegalStateException("failed to configure a HTTP/1 pipeline", e);
}
break; break;
default:
throw new IllegalStateException("Unknown SelectedProtocol");
} }
return true; return true;
} }
/** /**
* Add all {@link io.netty.channel.ChannelHandler}'s that are needed for HTTP_2. * Returns the {@link SelectedProtocol} for the current SSL session. By default, this method returns the first
* known protocol.
*
* @return the selected application-level protocol, or {@code null} if the application-level protocol name of
* the specified {@code sslHandler} is neither {@code "http/1.1"}, {@code "http/1.0"} nor {@code "h2"}
*/ */
protected void addHttp2Handlers(ChannelHandlerContext ctx) { protected SelectedProtocol selectProtocol(SslHandler sslHandler) throws Exception {
ChannelPipeline pipeline = ctx.pipeline(); final String appProto = sslHandler.applicationProtocol();
pipeline.addLast("http2ConnectionHandler", createHttp2RequestHandler()); return appProto != null? SelectedProtocol.protocol(appProto) : SelectedProtocol.HTTP_1_1;
} }
/** /**
* Add all {@link io.netty.channel.ChannelHandler}'s that are needed for HTTP. * Configures the {@link Channel} of the specified {@code ctx} for HTTP/2.
* <p>
* A typical implementation of this method will add a new {@link Http2ConnectionHandler} implementation
* to the pipeline.
* </p>
*/ */
protected void addHttpHandlers(ChannelHandlerContext ctx) { protected abstract void configureHttp2(ChannelHandlerContext ctx) throws Exception;
ChannelPipeline pipeline = ctx.pipeline();
pipeline.addLast("httpRequestDecoder", new HttpRequestDecoder()); /**
pipeline.addLast("httpResponseEncoder", new HttpResponseEncoder()); * Configures the {@link Channel} of the specified {@code ctx} for HTTP/1.
pipeline.addLast("httpChunkAggregator", new HttpObjectAggregator(maxHttpContentLength)); * <p>
pipeline.addLast("httpRequestHandler", createHttp1RequestHandler()); * A typical implementation of this method will look like the following:
* <pre>
* {@link ChannelPipeline} p = ctx.pipeline();
* p.addLast(new {@link HttpServerCodec}());
* p.addLast(new <i>YourHttpRequestHandler</i>());
* </pre>
* </p>
*/
protected abstract void configureHttp1(ChannelHandlerContext ctx) throws Exception;
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
logger.warn("{} Failed to select the application-level protocol:", ctx.channel(), cause);
ctx.close();
} }
/**
* 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 ChannelHandler} that is responsible for handling the http responses
* when the when the {@link SelectedProtocol} was {@link SelectedProtocol#HTTP_2}.
*/
protected abstract Http2ConnectionHandler createHttp2RequestHandler();
} }

View File

@ -14,45 +14,28 @@
*/ */
package io.netty.example.http2.helloworld.server; package io.netty.example.http2.helloworld.server;
import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http2.Http2ConnectionHandler; import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http2.Http2OrHttpChooser; import io.netty.handler.codec.http2.Http2OrHttpChooser;
import javax.net.ssl.SSLEngine;
/** /**
* Negotiates with the browser if HTTP2 or HTTP is going to be used. Once decided, the Netty * 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. * pipeline is setup with the correct handlers for the selected protocol.
*/ */
public class Http2OrHttpHandler extends Http2OrHttpChooser { public class Http2OrHttpHandler extends Http2OrHttpChooser {
private static final int MAX_CONTENT_LENGTH = 1024 * 100; private static final int MAX_CONTENT_LENGTH = 1024 * 100;
public Http2OrHttpHandler() { @Override
this(MAX_CONTENT_LENGTH); protected void configureHttp2(ChannelHandlerContext ctx) throws Exception {
} ctx.pipeline().addLast(new HelloWorldHttp2Handler());
public Http2OrHttpHandler(int maxHttpContentLength) {
super(maxHttpContentLength);
} }
@Override @Override
protected SelectedProtocol getProtocol(SSLEngine engine) { protected void configureHttp1(ChannelHandlerContext ctx) throws Exception {
String[] protocol = engine.getSession().getProtocol().split(":"); ctx.pipeline().addLast(new HttpServerCodec(),
if (protocol != null && protocol.length > 1) { new HttpObjectAggregator(MAX_CONTENT_LENGTH),
SelectedProtocol selectedProtocol = SelectedProtocol.protocol(protocol[1]); new HelloWorldHttp1Handler("ALPN Negotiation"));
System.err.println("Selected Protocol is " + selectedProtocol);
return selectedProtocol;
}
return SelectedProtocol.UNKNOWN;
}
@Override
protected ChannelHandler createHttp1RequestHandler() {
return new HelloWorldHttp1Handler("ALPN Negotiation");
}
@Override
protected Http2ConnectionHandler createHttp2RequestHandler() {
return new HelloWorldHttp2Handler();
} }
} }

View File

@ -16,46 +16,31 @@
package io.netty.example.http2.tiles; package io.netty.example.http2.tiles;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http2.DefaultHttp2Connection; import io.netty.handler.codec.http2.DefaultHttp2Connection;
import io.netty.handler.codec.http2.DefaultHttp2FrameReader; import io.netty.handler.codec.http2.DefaultHttp2FrameReader;
import io.netty.handler.codec.http2.DefaultHttp2FrameWriter; import io.netty.handler.codec.http2.DefaultHttp2FrameWriter;
import io.netty.handler.codec.http2.Http2ConnectionHandler;
import io.netty.handler.codec.http2.Http2OrHttpChooser; import io.netty.handler.codec.http2.Http2OrHttpChooser;
import io.netty.handler.codec.http2.HttpToHttp2ConnectionHandler; import io.netty.handler.codec.http2.HttpToHttp2ConnectionHandler;
import io.netty.handler.codec.http2.InboundHttp2ToHttpAdapter; import io.netty.handler.codec.http2.InboundHttp2ToHttpAdapter;
import javax.net.ssl.SSLEngine;
/** /**
* Used during protocol negotiation, the main function of this handler is to * Used during protocol negotiation, the main function of this handler is to
* return the HTTP/1.1 or HTTP/2 handler once the protocol has been negotiated. * return the HTTP/1.1 or HTTP/2 handler once the protocol has been negotiated.
*/ */
public class Http2OrHttpHandler extends Http2OrHttpChooser { public class Http2OrHttpHandler extends Http2OrHttpChooser {
public Http2OrHttpHandler(int maxHttpContentLength) { private static final int MAX_CONTENT_LENGTH = 1024 * 100;
super(maxHttpContentLength);
}
@Override @Override
protected SelectedProtocol getProtocol(SSLEngine engine) { protected void configureHttp2(ChannelHandlerContext ctx) {
String[] protocol = engine.getSession().getProtocol().split(":");
if (protocol != null && protocol.length > 1) {
SelectedProtocol selectedProtocol = SelectedProtocol.protocol(protocol[1]);
System.err.println("Selected Protocol is " + selectedProtocol);
return selectedProtocol;
}
return SelectedProtocol.UNKNOWN;
}
@Override
protected void addHttp2Handlers(ChannelHandlerContext ctx) {
DefaultHttp2Connection connection = new DefaultHttp2Connection(true); DefaultHttp2Connection connection = new DefaultHttp2Connection(true);
DefaultHttp2FrameWriter writer = new DefaultHttp2FrameWriter(); DefaultHttp2FrameWriter writer = new DefaultHttp2FrameWriter();
DefaultHttp2FrameReader reader = new DefaultHttp2FrameReader(); DefaultHttp2FrameReader reader = new DefaultHttp2FrameReader();
InboundHttp2ToHttpAdapter listener = new InboundHttp2ToHttpAdapter.Builder(connection).propagateSettings(true) InboundHttp2ToHttpAdapter listener = new InboundHttp2ToHttpAdapter.Builder(connection).propagateSettings(true)
.validateHttpHeaders(false).maxContentLength(1024 * 100).build(); .validateHttpHeaders(false).maxContentLength(MAX_CONTENT_LENGTH).build();
ctx.pipeline().addLast("httpToHttp2", new HttpToHttp2ConnectionHandler(connection, ctx.pipeline().addLast("httpToHttp2", new HttpToHttp2ConnectionHandler(connection,
// Loggers can be activated for debugging purposes // Loggers can be activated for debugging purposes
@ -66,12 +51,9 @@ public class Http2OrHttpHandler extends Http2OrHttpChooser {
} }
@Override @Override
protected ChannelHandler createHttp1RequestHandler() { protected void configureHttp1(ChannelHandlerContext ctx) throws Exception {
return new FallbackRequestHandler(); ctx.pipeline().addLast(new HttpServerCodec(),
} new HttpObjectAggregator(MAX_CONTENT_LENGTH),
new FallbackRequestHandler());
@Override
protected Http2ConnectionHandler createHttp2RequestHandler() {
return null; // NOOP
} }
} }

View File

@ -48,7 +48,6 @@ import javax.net.ssl.SSLException;
public class Http2Server { public class Http2Server {
public static final int PORT = Integer.parseInt(System.getProperty("http2-port", "8443")); public static final int PORT = Integer.parseInt(System.getProperty("http2-port", "8443"));
static final int MAX_CONTENT_LENGTH = 1024 * 100;
private final EventLoopGroup group; private final EventLoopGroup group;
@ -63,7 +62,7 @@ public class Http2Server {
b.group(group).channel(NioServerSocketChannel.class).childHandler(new ChannelInitializer<SocketChannel>() { b.group(group).channel(NioServerSocketChannel.class).childHandler(new ChannelInitializer<SocketChannel>() {
@Override @Override
protected void initChannel(SocketChannel ch) throws Exception { protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(sslCtx.newHandler(ch.alloc()), new Http2OrHttpHandler(MAX_CONTENT_LENGTH)); ch.pipeline().addLast(sslCtx.newHandler(ch.alloc()), new Http2OrHttpHandler());
} }
}); });

View File

@ -51,9 +51,8 @@ import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
*/ */
public final class SpdyClient { public final class SpdyClient {
static final boolean SSL = System.getProperty("ssl") != null;
static final String HOST = System.getProperty("host", "127.0.0.1"); 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 int PORT = Integer.parseInt(System.getProperty("port", "8443"));
public static void main(String[] args) throws Exception { public static void main(String[] args) throws Exception {
// Configure SSL. // Configure SSL.
@ -99,9 +98,7 @@ public final class SpdyClient {
// Wait until the connection is closed. // Wait until the connection is closed.
channel.close().syncUninterruptibly(); channel.close().syncUninterruptibly();
} finally { } finally {
if (workerGroup != null) { workerGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
} }
} }
} }

View File

@ -15,8 +15,17 @@
*/ */
package io.netty.example.spdy.server; package io.netty.example.spdy.server;
import io.netty.channel.ChannelInboundHandler; import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPipeline;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.spdy.SpdyFrameCodec;
import io.netty.handler.codec.spdy.SpdyHttpDecoder;
import io.netty.handler.codec.spdy.SpdyHttpEncoder;
import io.netty.handler.codec.spdy.SpdyHttpResponseStreamIdHandler;
import io.netty.handler.codec.spdy.SpdyOrHttpChooser; import io.netty.handler.codec.spdy.SpdyOrHttpChooser;
import io.netty.handler.codec.spdy.SpdySessionHandler;
import io.netty.handler.codec.spdy.SpdyVersion;
/** /**
* Negotiates with the browser if SPDY or HTTP is going to be used. Once decided, the Netty pipeline is setup with * Negotiates with the browser if SPDY or HTTP is going to be used. Once decided, the Netty pipeline is setup with
@ -26,17 +35,22 @@ public class SpdyOrHttpHandler extends SpdyOrHttpChooser {
private static final int MAX_CONTENT_LENGTH = 1024 * 100; private static final int MAX_CONTENT_LENGTH = 1024 * 100;
public SpdyOrHttpHandler() { @Override
this(MAX_CONTENT_LENGTH, MAX_CONTENT_LENGTH); protected void configureSpdy(ChannelHandlerContext ctx, SpdyVersion version) throws Exception {
} ChannelPipeline p = ctx.pipeline();
p.addLast(new SpdyFrameCodec(version));
public SpdyOrHttpHandler(int maxSpdyContentLength, int maxHttpContentLength) { p.addLast(new SpdySessionHandler(version, true));
super(maxSpdyContentLength, maxHttpContentLength); p.addLast(new SpdyHttpEncoder(version));
p.addLast(new SpdyHttpDecoder(version, MAX_CONTENT_LENGTH));
p.addLast(new SpdyHttpResponseStreamIdHandler());
p.addLast(new SpdyServerHandler());
} }
@Override @Override
protected ChannelInboundHandler createHttpRequestHandlerForHttp() { protected void configureHttp1(ChannelHandlerContext ctx) throws Exception {
return new SpdyServerHandler(); ChannelPipeline p = ctx.pipeline();
p.addLast(new HttpServerCodec());
p.addLast(new HttpObjectAggregator(MAX_CONTENT_LENGTH));
p.addLast(new SpdyServerHandler());
} }
} }

View File

@ -0,0 +1,24 @@
/*
* Copyright 2015 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;
/**
* Provides a way to get the application-level protocol name from ALPN or NPN.
*/
interface ApplicationProtocolAccessor {
String getApplicationProtocol();
}

View File

@ -22,6 +22,7 @@ import java.util.List;
* Utility class for application protocol common operations. * Utility class for application protocol common operations.
*/ */
final class ApplicationProtocolUtil { final class ApplicationProtocolUtil {
private static final int DEFAULT_LIST_SIZE = 2; private static final int DEFAULT_LIST_SIZE = 2;
private ApplicationProtocolUtil() { private ApplicationProtocolUtil() {

View File

@ -24,7 +24,7 @@ import javax.security.cert.X509Certificate;
import java.security.Principal; import java.security.Principal;
import java.security.cert.Certificate; import java.security.cert.Certificate;
final class JdkSslSession implements SSLSession { final class JdkSslSession implements SSLSession, ApplicationProtocolAccessor {
private final SSLEngine engine; private final SSLEngine engine;
private volatile String applicationProtocol; private volatile String applicationProtocol;
@ -32,39 +32,22 @@ final class JdkSslSession implements SSLSession {
this.engine = engine; this.engine = engine;
} }
void setApplicationProtocol(String applicationProtocol) { private SSLSession unwrap() {
if (applicationProtocol != null) { return engine.getSession();
applicationProtocol = applicationProtocol.replace(':', '_');
}
this.applicationProtocol = applicationProtocol;
} }
@Override @Override
public String getProtocol() { public String getProtocol() {
final String protocol = unwrap().getProtocol(); return unwrap().getProtocol();
final String applicationProtocol = this.applicationProtocol;
if (applicationProtocol == null) {
if (protocol != null) {
return protocol.replace(':', '_');
} else {
return null;
}
}
final StringBuilder buf = new StringBuilder(32);
if (protocol != null) {
buf.append(protocol.replace(':', '_'));
buf.append(':');
} else {
buf.append("null:");
}
buf.append(applicationProtocol);
return buf.toString();
} }
private SSLSession unwrap() { @Override
return engine.getSession(); public String getApplicationProtocol() {
return applicationProtocol;
}
void setApplicationProtocol(String applicationProtocol) {
this.applicationProtocol = applicationProtocol;
} }
@Override @Override

View File

@ -1,170 +0,0 @@
/*
* 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.ssl;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSessionContext;
import javax.security.cert.X509Certificate;
import java.security.Principal;
import java.security.cert.Certificate;
final class JettyNpnSslSession implements SSLSession {
private final SSLEngine engine;
private volatile String applicationProtocol;
JettyNpnSslSession(SSLEngine engine) {
this.engine = engine;
}
void setApplicationProtocol(String applicationProtocol) {
if (applicationProtocol != null) {
applicationProtocol = applicationProtocol.replace(':', '_');
}
this.applicationProtocol = applicationProtocol;
}
@Override
public String getProtocol() {
final String protocol = unwrap().getProtocol();
final String applicationProtocol = this.applicationProtocol;
if (applicationProtocol == null) {
if (protocol != null) {
return protocol.replace(':', '_');
} else {
return null;
}
}
final StringBuilder buf = new StringBuilder(32);
if (protocol != null) {
buf.append(protocol.replace(':', '_'));
buf.append(':');
} else {
buf.append("null:");
}
buf.append(applicationProtocol);
return buf.toString();
}
private SSLSession unwrap() {
return engine.getSession();
}
@Override
public byte[] getId() {
return unwrap().getId();
}
@Override
public SSLSessionContext getSessionContext() {
return unwrap().getSessionContext();
}
@Override
public long getCreationTime() {
return unwrap().getCreationTime();
}
@Override
public long getLastAccessedTime() {
return unwrap().getLastAccessedTime();
}
@Override
public void invalidate() {
unwrap().invalidate();
}
@Override
public boolean isValid() {
return unwrap().isValid();
}
@Override
public void putValue(String s, Object o) {
unwrap().putValue(s, o);
}
@Override
public Object getValue(String s) {
return unwrap().getValue(s);
}
@Override
public void removeValue(String s) {
unwrap().removeValue(s);
}
@Override
public String[] getValueNames() {
return unwrap().getValueNames();
}
@Override
public Certificate[] getPeerCertificates() throws SSLPeerUnverifiedException {
return unwrap().getPeerCertificates();
}
@Override
public Certificate[] getLocalCertificates() {
return unwrap().getLocalCertificates();
}
@Override
public X509Certificate[] getPeerCertificateChain() throws SSLPeerUnverifiedException {
return unwrap().getPeerCertificateChain();
}
@Override
public Principal getPeerPrincipal() throws SSLPeerUnverifiedException {
return unwrap().getPeerPrincipal();
}
@Override
public Principal getLocalPrincipal() {
return unwrap().getLocalPrincipal();
}
@Override
public String getCipherSuite() {
return unwrap().getCipherSuite();
}
@Override
public String getPeerHost() {
return unwrap().getPeerHost();
}
@Override
public int getPeerPort() {
return unwrap().getPeerPort();
}
@Override
public int getPacketBufferSize() {
return unwrap().getPacketBufferSize();
}
@Override
public int getApplicationBufferSize() {
return unwrap().getApplicationBufferSize();
}
}

View File

@ -944,7 +944,7 @@ public final class OpenSslEngine extends SSLEngine {
SSL.setOptions(ssl, SSL.SSL_OP_NO_TLSv1_2); SSL.setOptions(ssl, SSL.SSL_OP_NO_TLSv1_2);
} }
} else { } else {
throw new IllegalStateException("failed to enable protocols: " + protocols); throw new IllegalStateException("failed to enable protocols: " + Arrays.asList(protocols));
} }
} }
} }
@ -1062,7 +1062,6 @@ public final class OpenSslEngine extends SSLEngine {
private static String selectApplicationProtocol(List<String> protocols, private static String selectApplicationProtocol(List<String> protocols,
SelectedListenerFailureBehavior behavior, SelectedListenerFailureBehavior behavior,
String applicationProtocol) throws SSLException { String applicationProtocol) throws SSLException {
applicationProtocol = applicationProtocol.replace(':', '_');
if (behavior == SelectedListenerFailureBehavior.ACCEPT) { if (behavior == SelectedListenerFailureBehavior.ACCEPT) {
return applicationProtocol; return applicationProtocol;
} else { } else {
@ -1074,7 +1073,7 @@ public final class OpenSslEngine extends SSLEngine {
if (behavior == SelectedListenerFailureBehavior.CHOOSE_MY_LAST_PROTOCOL) { if (behavior == SelectedListenerFailureBehavior.CHOOSE_MY_LAST_PROTOCOL) {
return protocols.get(size - 1); return protocols.get(size - 1);
} else { } else {
throw new SSLException("Unknown protocol " + applicationProtocol); throw new SSLException("unknown protocol " + applicationProtocol);
} }
} }
} }
@ -1262,7 +1261,7 @@ public final class OpenSslEngine extends SSLEngine {
shutdown(); shutdown();
} }
private final class OpenSslSession implements SSLSession { private final class OpenSslSession implements SSLSession, ApplicationProtocolAccessor {
// SSLSession implementation seems to not need to be thread-safe so no need for volatile etc. // SSLSession implementation seems to not need to be thread-safe so no need for volatile etc.
private X509Certificate[] x509PeerCerts; private X509Certificate[] x509PeerCerts;
@ -1481,20 +1480,18 @@ public final class OpenSslEngine extends SSLEngine {
@Override @Override
public String getProtocol() { public String getProtocol() {
String applicationProtocol = OpenSslEngine.this.applicationProtocol;
final String version;
synchronized (OpenSslEngine.this) { synchronized (OpenSslEngine.this) {
if (destroyed == 0) { if (destroyed == 0) {
version = SSL.getVersion(ssl); return SSL.getVersion(ssl);
} else { } else {
return StringUtil.EMPTY_STRING; return StringUtil.EMPTY_STRING;
} }
} }
if (applicationProtocol == null || applicationProtocol.isEmpty()) { }
return version;
} else { @Override
return version + ':' + applicationProtocol; public String getApplicationProtocol() {
} return applicationProtocol;
} }
@Override @Override

View File

@ -50,6 +50,7 @@ import javax.net.ssl.SSLEngineResult;
import javax.net.ssl.SSLEngineResult.HandshakeStatus; import javax.net.ssl.SSLEngineResult.HandshakeStatus;
import javax.net.ssl.SSLEngineResult.Status; import javax.net.ssl.SSLEngineResult.Status;
import javax.net.ssl.SSLException; import javax.net.ssl.SSLException;
import javax.net.ssl.SSLSession;
import java.io.IOException; import java.io.IOException;
import java.net.SocketAddress; import java.net.SocketAddress;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
@ -352,6 +353,20 @@ public class SslHandler extends ByteToMessageDecoder implements ChannelOutboundH
return engine; return engine;
} }
/**
* Returns the name of the current application-level protocol.
*
* @return the protocol name or {@code null} if application-level protocol has not been negotiated
*/
public String applicationProtocol() {
SSLSession sess = engine().getSession();
if (!(sess instanceof ApplicationProtocolAccessor)) {
return null;
}
return ((ApplicationProtocolAccessor) sess).getApplicationProtocol();
}
/** /**
* Returns a {@link Future} that will get notified once the current TLS handshake completes. * Returns a {@link Future} that will get notified once the current TLS handshake completes.
* *

View File

@ -58,4 +58,14 @@ public final class SslHandshakeCompletionEvent {
public Throwable cause() { public Throwable cause() {
return cause; return cause;
} }
@Override
public String toString() {
Throwable cause = cause();
if (cause == null) {
return "SslHandshakeCompletionEvent(SUCCESS)";
} else {
return "SslHandshakeCompletionEvent(" + cause + ')';
}
}
} }

View File

@ -262,14 +262,8 @@ public abstract class SSLEngineTest {
private static void verifyApplicationLevelProtocol(Channel channel, String expectedApplicationProtocol) { private static void verifyApplicationLevelProtocol(Channel channel, String expectedApplicationProtocol) {
SslHandler handler = channel.pipeline().get(SslHandler.class); SslHandler handler = channel.pipeline().get(SslHandler.class);
assertNotNull(handler); assertNotNull(handler);
String[] protocol = handler.engine().getSession().getProtocol().split(":"); String appProto = handler.applicationProtocol();
assertNotNull(protocol); assertEquals(appProto, expectedApplicationProtocol);
if (expectedApplicationProtocol != null && !expectedApplicationProtocol.isEmpty()) {
assertTrue("protocol.length must be greater than 1 but is " + protocol.length, protocol.length > 1);
assertEquals(expectedApplicationProtocol, protocol[1]);
} else {
assertEquals(1, protocol.length);
}
} }
private static void writeAndVerifyReceived(ByteBuf message, Channel sendChannel, CountDownLatch receiverLatch, private static void writeAndVerifyReceived(ByteBuf message, Channel sendChannel, CountDownLatch receiverLatch,