From 263e6979a6fae38b8faf03f6d17fffe315e370cf Mon Sep 17 00:00:00 2001 From: Trustin Lee Date: Mon, 1 Jun 2015 16:05:00 +0900 Subject: [PATCH] Replace SpdyOrHttpChooser and Http2OrHttpChooser with ApplicationProtocolNegotiationHandler Motivation: SpdyOrHttpChooser and Http2OrHttpChooser duplicate fair amount code with each other. Modification: - Replace SpdyOrHttpChooser and Http2OrHttpChooser with ApplicationProtocolNegotiationHandler - Add ApplicationProtocolNames to define the known application-level protocol names - Deprecate SpdyOrHttpChooser Result: - Less code duplication - A user can perform dynamic pipeline configuration that follows ALPN/NPN for any protocols. --- .../handler/codec/spdy/SpdyOrHttpChooser.java | 9 +- .../netty/example/spdy/client/SpdyClient.java | 6 +- .../spdy/server/SpdyOrHttpHandler.java | 28 +++- .../netty/example/spdy/server/SpdyServer.java | 6 +- .../ssl/ApplicationProtocolAccessor.java | 6 + .../handler/ssl/ApplicationProtocolNames.java | 59 +++++++++ ...ApplicationProtocolNegotiationHandler.java | 125 ++++++++++++++++++ .../handler/ssl/ApplicationProtocolUtil.java | 1 - .../ssl/SslHandshakeCompletionEvent.java | 8 +- 9 files changed, 224 insertions(+), 24 deletions(-) create mode 100644 handler/src/main/java/io/netty/handler/ssl/ApplicationProtocolNames.java create mode 100644 handler/src/main/java/io/netty/handler/ssl/ApplicationProtocolNegotiationHandler.java diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyOrHttpChooser.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyOrHttpChooser.java index e176ef1055..0e9940d231 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyOrHttpChooser.java +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyOrHttpChooser.java @@ -17,11 +17,12 @@ package io.netty.handler.codec.spdy; import io.netty.buffer.ByteBuf; import io.netty.channel.Channel; +import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelInboundHandler; import io.netty.channel.ChannelPipeline; import io.netty.handler.codec.ByteToMessageDecoder; import io.netty.handler.codec.http.HttpServerCodec; +import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler; import io.netty.handler.ssl.SslHandler; import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; @@ -29,16 +30,12 @@ import io.netty.util.internal.logging.InternalLoggerFactory; import java.util.List; /** - * {@link ChannelInboundHandler} which is responsible to setup the {@link ChannelPipeline} either for - * HTTP or SPDY. This offers an easy way for users to support both at the same time while not care to - * much about the low-level details. + * @deprecated Use {@link ApplicationProtocolNegotiationHandler} instead. */ public abstract class SpdyOrHttpChooser extends ByteToMessageDecoder { private static final InternalLogger logger = InternalLoggerFactory.getInstance(SpdyOrHttpChooser.class); - // TODO: Replace with generic NPN handler - public enum SelectedProtocol { SPDY_3_1("spdy/3.1"), HTTP_1_1("http/1.1"), diff --git a/example/src/main/java/io/netty/example/spdy/client/SpdyClient.java b/example/src/main/java/io/netty/example/spdy/client/SpdyClient.java index c6473465af..b552962161 100644 --- a/example/src/main/java/io/netty/example/spdy/client/SpdyClient.java +++ b/example/src/main/java/io/netty/example/spdy/client/SpdyClient.java @@ -26,11 +26,11 @@ import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.HttpMethod; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpVersion; -import io.netty.handler.codec.spdy.SpdyOrHttpChooser.SelectedProtocol; 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.SslContext; import io.netty.handler.ssl.SslContextBuilder; import io.netty.handler.ssl.util.InsecureTrustManagerFactory; @@ -63,8 +63,8 @@ public final class SpdyClient { SelectorFailureBehavior.NO_ADVERTISE, // ACCEPT is currently the only mode supported by both OpenSsl and JDK providers. SelectedListenerFailureBehavior.ACCEPT, - SelectedProtocol.SPDY_3_1.protocolName(), - SelectedProtocol.HTTP_1_1.protocolName())) + ApplicationProtocolNames.SPDY_3_1, + ApplicationProtocolNames.HTTP_1_1)) .build(); HttpResponseClientHandler httpResponseHandler = new HttpResponseClientHandler(); diff --git a/example/src/main/java/io/netty/example/spdy/server/SpdyOrHttpHandler.java b/example/src/main/java/io/netty/example/spdy/server/SpdyOrHttpHandler.java index 19490a2cd6..bed9bd24da 100644 --- a/example/src/main/java/io/netty/example/spdy/server/SpdyOrHttpHandler.java +++ b/example/src/main/java/io/netty/example/spdy/server/SpdyOrHttpHandler.java @@ -23,20 +23,39 @@ 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.SpdySessionHandler; import io.netty.handler.codec.spdy.SpdyVersion; +import io.netty.handler.ssl.ApplicationProtocolNames; +import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler; /** * Negotiates with the browser if SPDY or HTTP is going to be used. Once decided, the Netty pipeline is setup with * the correct handlers for the selected protocol. */ -public class SpdyOrHttpHandler extends SpdyOrHttpChooser { +public class SpdyOrHttpHandler extends ApplicationProtocolNegotiationHandler { private static final int MAX_CONTENT_LENGTH = 1024 * 100; + protected SpdyOrHttpHandler() { + super(ApplicationProtocolNames.HTTP_1_1); + } + @Override - protected void configureSpdy(ChannelHandlerContext ctx, SpdyVersion version) throws Exception { + protected void configurePipeline(ChannelHandlerContext ctx, String protocol) throws Exception { + if (ApplicationProtocolNames.SPDY_3_1.equals(protocol)) { + configureSpdy(ctx, SpdyVersion.SPDY_3_1); + return; + } + + if (ApplicationProtocolNames.HTTP_1_1.equals(protocol)) { + configureHttp1(ctx); + return; + } + + throw new IllegalStateException("unknown protocol: " + protocol); + } + + private static void configureSpdy(ChannelHandlerContext ctx, SpdyVersion version) throws Exception { ChannelPipeline p = ctx.pipeline(); p.addLast(new SpdyFrameCodec(version)); p.addLast(new SpdySessionHandler(version, true)); @@ -46,8 +65,7 @@ public class SpdyOrHttpHandler extends SpdyOrHttpChooser { p.addLast(new SpdyServerHandler()); } - @Override - protected void configureHttp1(ChannelHandlerContext ctx) throws Exception { + private static void configureHttp1(ChannelHandlerContext ctx) throws Exception { ChannelPipeline p = ctx.pipeline(); p.addLast(new HttpServerCodec()); p.addLast(new HttpObjectAggregator(MAX_CONTENT_LENGTH)); diff --git a/example/src/main/java/io/netty/example/spdy/server/SpdyServer.java b/example/src/main/java/io/netty/example/spdy/server/SpdyServer.java index 67d3cf24e8..827988ddec 100644 --- a/example/src/main/java/io/netty/example/spdy/server/SpdyServer.java +++ b/example/src/main/java/io/netty/example/spdy/server/SpdyServer.java @@ -21,13 +21,13 @@ 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.spdy.SpdyOrHttpChooser.SelectedProtocol; 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.SslContext; import io.netty.handler.ssl.SslContextBuilder; import io.netty.handler.ssl.util.SelfSignedCertificate; @@ -64,8 +64,8 @@ public final class SpdyServer { SelectorFailureBehavior.NO_ADVERTISE, // ACCEPT is currently the only mode supported by both OpenSsl and JDK providers. SelectedListenerFailureBehavior.ACCEPT, - SelectedProtocol.SPDY_3_1.protocolName(), - SelectedProtocol.HTTP_1_1.protocolName())) + ApplicationProtocolNames.SPDY_3_1, + ApplicationProtocolNames.HTTP_1_1)) .build(); // Configure the server. diff --git a/handler/src/main/java/io/netty/handler/ssl/ApplicationProtocolAccessor.java b/handler/src/main/java/io/netty/handler/ssl/ApplicationProtocolAccessor.java index 812762db3c..8835bc810a 100644 --- a/handler/src/main/java/io/netty/handler/ssl/ApplicationProtocolAccessor.java +++ b/handler/src/main/java/io/netty/handler/ssl/ApplicationProtocolAccessor.java @@ -20,5 +20,11 @@ package io.netty.handler.ssl; * Provides a way to get the application-level protocol name from ALPN or NPN. */ interface ApplicationProtocolAccessor { + /** + * Returns the name of the negotiated application-level protocol. + * + * @return the application-level protocol name or + * {@code null} if the negotiation failed or the client does not have ALPN/NPN extension + */ String getApplicationProtocol(); } diff --git a/handler/src/main/java/io/netty/handler/ssl/ApplicationProtocolNames.java b/handler/src/main/java/io/netty/handler/ssl/ApplicationProtocolNames.java new file mode 100644 index 0000000000..b17fd69804 --- /dev/null +++ b/handler/src/main/java/io/netty/handler/ssl/ApplicationProtocolNames.java @@ -0,0 +1,59 @@ +/* + * 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 set of protocol names used in ALPN and NPN. + * + * @see RFC7540 (HTTP/2) + * @see RFC7301 (TLS ALPN Extension) + * @see TLS NPN Extension Draft + */ +public final class ApplicationProtocolNames { + + /** + * {@code "h2"}: HTTP version 2 + */ + public static final String HTTP_2 = "h2"; + + /** + * {@code "http/1.1"}: HTTP version 1.1 + */ + public static final String HTTP_1_1 = "http/1.1"; + + /** + * {@code "spdy/3.1"}: SPDY version 3.1 + */ + public static final String SPDY_3_1 = "spdy/3.1"; + + /** + * {@code "spdy/3"}: SPDY version 3 + */ + public static final String SPDY_3 = "spdy/3"; + + /** + * {@code "spdy/2"}: SPDY version 2 + */ + public static final String SPDY_2 = "spdy/2"; + + /** + * {@code "spdy/1"}: SPDY version 1 + */ + public static final String SPDY_1 = "spdy/1"; + + private ApplicationProtocolNames() { } +} diff --git a/handler/src/main/java/io/netty/handler/ssl/ApplicationProtocolNegotiationHandler.java b/handler/src/main/java/io/netty/handler/ssl/ApplicationProtocolNegotiationHandler.java new file mode 100644 index 0000000000..48a632706d --- /dev/null +++ b/handler/src/main/java/io/netty/handler/ssl/ApplicationProtocolNegotiationHandler.java @@ -0,0 +1,125 @@ +/* + * 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; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.util.internal.ObjectUtil; +import io.netty.util.internal.logging.InternalLogger; +import io.netty.util.internal.logging.InternalLoggerFactory; + +/** + * Configures a {@link ChannelPipeline} depending on the application-level protocol negotiation result of + * {@link SslHandler}. For example, you could configure your HTTP pipeline depending on the result of ALPN: + *
+ * public class MyInitializer extends {@link ChannelInitializer}<{@link Channel}> {
+ *     private final {@link SslContext} sslCtx;
+ *
+ *     public MyInitializer({@link SslContext} sslCtx) {
+ *         this.sslCtx = sslCtx;
+ *     }
+ *
+ *     protected void initChannel({@link Channel} ch) {
+ *         {@link ChannelPipeline} p = ch.pipeline();
+ *         p.addLast(sslCtx.newHandler(...)); // Adds {@link SslHandler}
+ *         p.addLast(new MyNegotiationHandler());
+ *     }
+ * }
+ *
+ * public class MyNegotiationHandler extends {@link ApplicationProtocolNegotiationHandler} {
+ *     public MyNegotiationHandler() {
+ *         super({@link ApplicationProtocolNames}.HTTP_1_1);
+ *     }
+ *
+ *     protected void configurePipeline({@link ChannelHandlerContext} ctx, String protocol) {
+ *         if ({@link ApplicationProtocolNames}.HTTP_2.equals(protocol) {
+ *             configureHttp2(ctx);
+ *         } else if ({@link ApplicationProtocolNames}.HTTP_1_1.equals(protocol)) {
+ *             configureHttp1(ctx);
+ *         } else {
+ *             throw new IllegalStateException("unknown protocol: " + protocol);
+ *         }
+ *     }
+ * }
+ * 
+ */ +public abstract class ApplicationProtocolNegotiationHandler extends ChannelInboundHandlerAdapter { + + private static final InternalLogger logger = + InternalLoggerFactory.getInstance(ApplicationProtocolNegotiationHandler.class); + + private final String fallbackProtocol; + private SslHandler sslHandler; + + /** + * Creates a new instance with the specified fallback protocol name. + * + * @param fallbackProtocol the name of the protocol to use when + * ALPN/NPN negotiation fails or the client does not support ALPN/NPN + */ + protected ApplicationProtocolNegotiationHandler(String fallbackProtocol) { + this.fallbackProtocol = ObjectUtil.checkNotNull(fallbackProtocol, "fallbackProtocol"); + } + + @Override + public void handlerAdded(ChannelHandlerContext ctx) throws Exception { + // FIXME: There is no way to tell if the SSL handler is placed before the negotiation handler. + final SslHandler sslHandler = ctx.pipeline().get(SslHandler.class); + if (sslHandler == null) { + throw new IllegalStateException( + "cannot find a SslHandler in the pipeline (required for application-level protocol negotiation)"); + } + + this.sslHandler = sslHandler; + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + if (evt instanceof SslHandshakeCompletionEvent) { + ctx.pipeline().remove(this); + + SslHandshakeCompletionEvent handshakeEvent = (SslHandshakeCompletionEvent) evt; + if (handshakeEvent.isSuccess()) { + String protocol = sslHandler.applicationProtocol(); + configurePipeline(ctx, protocol != null? protocol : fallbackProtocol); + } else { + logger.warn("{} TLS handshake failed:", ctx.channel(), handshakeEvent.cause()); + ctx.close(); + } + } + + ctx.fireUserEventTriggered(evt); + } + + /** + * Invoked on successful initial SSL/TLS handshake. Implement this method to configure your pipeline + * for the negotiated application-level protocol. + * + * @param protocol the name of the negotiated application-level protocol, or + * the fallback protocol name specified in the constructor call if negotiation failed or the client + * isn't aware of ALPN/NPN extension + */ + protected abstract void configurePipeline(ChannelHandlerContext ctx, String protocol) 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(); + } +} diff --git a/handler/src/main/java/io/netty/handler/ssl/ApplicationProtocolUtil.java b/handler/src/main/java/io/netty/handler/ssl/ApplicationProtocolUtil.java index 03a077ec69..0581e9b2c2 100644 --- a/handler/src/main/java/io/netty/handler/ssl/ApplicationProtocolUtil.java +++ b/handler/src/main/java/io/netty/handler/ssl/ApplicationProtocolUtil.java @@ -22,7 +22,6 @@ import java.util.List; * Utility class for application protocol common operations. */ final class ApplicationProtocolUtil { - private static final int DEFAULT_LIST_SIZE = 2; private ApplicationProtocolUtil() { diff --git a/handler/src/main/java/io/netty/handler/ssl/SslHandshakeCompletionEvent.java b/handler/src/main/java/io/netty/handler/ssl/SslHandshakeCompletionEvent.java index b2a1de061f..a81e087e23 100644 --- a/handler/src/main/java/io/netty/handler/ssl/SslHandshakeCompletionEvent.java +++ b/handler/src/main/java/io/netty/handler/ssl/SslHandshakeCompletionEvent.java @@ -61,11 +61,7 @@ public final class SslHandshakeCompletionEvent { @Override public String toString() { - Throwable cause = cause(); - if (cause == null) { - return "SslHandshakeCompletionEvent(SUCCESS)"; - } else { - return "SslHandshakeCompletionEvent(" + cause + ')'; - } + final Throwable cause = cause(); + return cause == null? "SslHandshakeCompletionEvent(SUCCESS)" : "SslHandshakeCompletionEvent(" + cause + ')'; } }