diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/HttpServerUpgradeHandler.java b/codec-http/src/main/java/io/netty/handler/codec/http/HttpServerUpgradeHandler.java index 620e7df096..273e4af869 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/HttpServerUpgradeHandler.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/HttpServerUpgradeHandler.java @@ -17,21 +17,16 @@ package io.netty.handler.codec.http; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; -import io.netty.util.AsciiString; import io.netty.util.ReferenceCountUtil; import io.netty.util.ReferenceCounted; +import java.util.ArrayList; import java.util.Collection; -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Set; -import java.util.TreeSet; import static io.netty.handler.codec.http.HttpResponseStatus.SWITCHING_PROTOCOLS; import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1; +import static io.netty.util.internal.ObjectUtil.checkNotNull; /** * A server-side handler that receives HTTP requests and optionally performs a protocol switch if @@ -54,12 +49,6 @@ public class HttpServerUpgradeHandler extends HttpObjectAggregator { * A codec that the source can be upgraded to. */ public interface UpgradeCodec { - /** - * Returns the name of the protocol supported by this codec, as indicated by the - * {@link HttpHeaderNames#UPGRADE} header. - */ - String protocol(); - /** * Gets all protocol-specific headers required by this protocol for a successful upgrade. * Any supplied header will be required to appear in the {@link HttpHeaderNames#CONNECTION} header as well. @@ -87,6 +76,20 @@ public class HttpServerUpgradeHandler extends HttpObjectAggregator { void upgradeTo(ChannelHandlerContext ctx, FullHttpRequest upgradeRequest, FullHttpResponse upgradeResponse); } + /** + * Creates a new {@link UpgradeCodec} for the requested protocol name. + */ + public interface UpgradeCodecFactory { + /** + * Invoked by {@link HttpServerUpgradeHandler} for all the requested protocol names in the order of + * the client preference. The first non-{@code null} {@link UpgradeCodec} returned by this method + * will be selected. + * + * @return a new {@link UpgradeCodec}, or {@code null} if the specified protocol name is not supported + */ + UpgradeCodec newUpgradeCodec(String protocol); + } + /** * User event that is fired to notify about the completion of an HTTP upgrade * to another protocol. Contains the original upgrade request so that the response @@ -160,33 +163,44 @@ public class HttpServerUpgradeHandler extends HttpObjectAggregator { } } - private final Map upgradeCodecMap; + private static final String UPGRADE_STRING = HttpHeaderNames.UPGRADE.toString(); + private final SourceCodec sourceCodec; + private final UpgradeCodecFactory upgradeCodecFactory; private boolean handlingUpgrade; /** * Constructs the upgrader with the supported codecs. + *

+ * The handler instantiated by this constructor will reject an upgrade request with non-empty content. + * It should not be a concern because an upgrade request is most likely a GET request. + * If you have a client that sends a non-GET upgrade request, please consider using + * {@link #HttpServerUpgradeHandler(SourceCodec, UpgradeCodecFactory, int)} to specify the maximum + * length of the content of an upgrade request. + *

* - * @param sourceCodec the codec that is being used initially. - * @param upgradeCodecs the codecs (in order of preference) that this server supports - * upgrading to from the source codec. - * @param maxContentLength the maximum length of the aggregated content. + * @param sourceCodec the codec that is being used initially + * @param upgradeCodecFactory the factory that creates a new upgrade codec + * for one of the requested upgrade protocols */ - public HttpServerUpgradeHandler(SourceCodec sourceCodec, - Collection upgradeCodecs, int maxContentLength) { + public HttpServerUpgradeHandler(SourceCodec sourceCodec, UpgradeCodecFactory upgradeCodecFactory) { + this(sourceCodec, upgradeCodecFactory, 0); + } + + /** + * Constructs the upgrader with the supported codecs. + * + * @param sourceCodec the codec that is being used initially + * @param upgradeCodecFactory the factory that creates a new upgrade codec + * for one of the requested upgrade protocols + * @param maxContentLength the maximum length of the content of an upgrade request + */ + public HttpServerUpgradeHandler( + SourceCodec sourceCodec, UpgradeCodecFactory upgradeCodecFactory, int maxContentLength) { super(maxContentLength); - if (sourceCodec == null) { - throw new NullPointerException("sourceCodec"); - } - if (upgradeCodecs == null) { - throw new NullPointerException("upgradeCodecs"); - } - this.sourceCodec = sourceCodec; - upgradeCodecMap = new LinkedHashMap(upgradeCodecs.size()); - for (UpgradeCodec upgradeCodec : upgradeCodecs) { - String name = upgradeCodec.protocol().toUpperCase(Locale.US); - upgradeCodecMap.put(name, upgradeCodec); - } + + this.sourceCodec = checkNotNull(sourceCodec, "sourceCodec"); + this.upgradeCodecFactory = checkNotNull(upgradeCodecFactory, "upgradeCodecFactory"); } @Override @@ -248,8 +262,20 @@ public class HttpServerUpgradeHandler extends HttpObjectAggregator { */ private boolean upgrade(final ChannelHandlerContext ctx, final FullHttpRequest request) { // Select the best protocol based on those requested in the UPGRADE header. - CharSequence upgradeHeader = request.headers().get(HttpHeaderNames.UPGRADE); - final UpgradeCodec upgradeCodec = selectUpgradeCodec(upgradeHeader); + final ArrayList requestedProtocols = splitHeader(request.headers().get(HttpHeaderNames.UPGRADE)); + final int numRequestedProtocols = requestedProtocols.size(); + UpgradeCodec upgradeCodec = null; + String upgradeProtocol = null; + for (int i = 0; i < numRequestedProtocols; i ++) { + final String p = requestedProtocols.get(i); + final UpgradeCodec c = upgradeCodecFactory.newUpgradeCodec(p); + if (c != null) { + upgradeProtocol = p; + upgradeCodec = c; + break; + } + } + if (upgradeCodec == null) { // None of the requested protocols are supported, don't upgrade. return false; @@ -263,8 +289,8 @@ public class HttpServerUpgradeHandler extends HttpObjectAggregator { // Make sure the CONNECTION header contains UPGRADE as well as all protocol-specific headers. Collection requiredHeaders = upgradeCodec.requiredUpgradeHeaders(); - Set values = splitHeader(connectionHeader); - if (!values.contains(HttpHeaderNames.UPGRADE) || !values.containsAll(requiredHeaders)) { + List values = splitHeader(connectionHeader); + if (!values.contains(UPGRADE_STRING) || !values.containsAll(requiredHeaders)) { return false; } @@ -276,12 +302,14 @@ public class HttpServerUpgradeHandler extends HttpObjectAggregator { } // Create the user event to be fired once the upgrade completes. - final UpgradeEvent event = new UpgradeEvent(upgradeCodec.protocol(), request); + final UpgradeEvent event = new UpgradeEvent(upgradeProtocol, request); // Prepare and send the upgrade response. Wait for this write to complete before upgrading, // since we need the old codec in-place to properly encode the response. - final FullHttpResponse upgradeResponse = createUpgradeResponse(upgradeCodec); + final FullHttpResponse upgradeResponse = createUpgradeResponse(upgradeProtocol); upgradeCodec.prepareUpgradeResponse(ctx, request, upgradeResponse); + + final UpgradeCodec finalUpgradeCodec = upgradeCodec; ctx.writeAndFlush(upgradeResponse).addListener(new ChannelFutureListener() { @Override public void operationComplete(ChannelFuture future) throws Exception { @@ -289,7 +317,7 @@ public class HttpServerUpgradeHandler extends HttpObjectAggregator { if (future.isSuccess()) { // Perform the upgrade to the new protocol. sourceCodec.upgradeFrom(ctx); - upgradeCodec.upgradeTo(ctx, request, upgradeResponse); + finalUpgradeCodec.upgradeTo(ctx, request, upgradeResponse); // Notify that the upgrade has occurred. Retain the event to offset // the release() in the finally block. @@ -309,33 +337,13 @@ public class HttpServerUpgradeHandler extends HttpObjectAggregator { return true; } - /** - * Looks up the most desirable supported upgrade codec from the list of choices in the UPGRADE - * header. If no suitable codec was found, returns {@code null}. - */ - private UpgradeCodec selectUpgradeCodec(CharSequence upgradeHeader) { - Set requestedProtocols = splitHeader(upgradeHeader); - - // Retain only the protocols that are in the protocol map. Maintain the original insertion - // order into the protocolMap, so that the first one in the remaining set is the most - // desirable protocol for the server. - Set supportedProtocols = new LinkedHashSet(upgradeCodecMap.keySet()); - supportedProtocols.retainAll(requestedProtocols); - - if (!supportedProtocols.isEmpty()) { - String protocol = supportedProtocols.iterator().next().toUpperCase(Locale.US); - return upgradeCodecMap.get(protocol); - } - return null; - } - /** * Creates the 101 Switching Protocols response message. */ - private static FullHttpResponse createUpgradeResponse(UpgradeCodec upgradeCodec) { + private static FullHttpResponse createUpgradeResponse(String upgradeProtocol) { DefaultFullHttpResponse res = new DefaultFullHttpResponse(HTTP_1_1, SWITCHING_PROTOCOLS); res.headers().add(HttpHeaderNames.CONNECTION, HttpHeaderValues.UPGRADE); - res.headers().add(HttpHeaderNames.UPGRADE, upgradeCodec.protocol()); + res.headers().add(HttpHeaderNames.UPGRADE, upgradeProtocol); res.headers().add(HttpHeaderNames.CONTENT_LENGTH, "0"); return res; } @@ -344,9 +352,9 @@ public class HttpServerUpgradeHandler extends HttpObjectAggregator { * Splits a comma-separated header value. The returned set is case-insensitive and contains each * part with whitespace removed. */ - private static Set splitHeader(CharSequence header) { - StringBuilder builder = new StringBuilder(header.length()); - Set protocols = new TreeSet(AsciiString.CHARSEQUENCE_CASE_INSENSITIVE_ORDER); + private static ArrayList splitHeader(CharSequence header) { + final StringBuilder builder = new StringBuilder(header.length()); + final ArrayList protocols = new ArrayList(4); for (int i = 0; i < header.length(); ++i) { char c = header.charAt(i); if (Character.isWhitespace(c)) { 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 0c04e9b993..aa1ae40fb4 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 @@ -31,7 +31,6 @@ import java.util.List; import static io.netty.handler.codec.base64.Base64Dialect.URL_SAFE; import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST; import static io.netty.handler.codec.http2.Http2CodecUtil.FRAME_HEADER_LENGTH; -import static io.netty.handler.codec.http2.Http2CodecUtil.HTTP_UPGRADE_PROTOCOL_NAME; import static io.netty.handler.codec.http2.Http2CodecUtil.HTTP_UPGRADE_SETTINGS_HEADER; import static io.netty.handler.codec.http2.Http2CodecUtil.writeFrameHeader; import static io.netty.handler.codec.http2.Http2FrameTypes.SETTINGS; @@ -72,11 +71,6 @@ public class Http2ServerUpgradeCodec implements HttpServerUpgradeHandler.Upgrade frameReader = new DefaultHttp2FrameReader(); } - @Override - public String protocol() { - return HTTP_UPGRADE_PROTOCOL_NAME; - } - @Override public Collection requiredUpgradeHeaders() { return REQUIRED_UPGRADE_HEADERS; diff --git a/example/src/main/java/io/netty/example/http2/helloworld/server/Http2ServerInitializer.java b/example/src/main/java/io/netty/example/http2/helloworld/server/Http2ServerInitializer.java index a29801fb88..ffd115f353 100644 --- a/example/src/main/java/io/netty/example/http2/helloworld/server/Http2ServerInitializer.java +++ b/example/src/main/java/io/netty/example/http2/helloworld/server/Http2ServerInitializer.java @@ -19,21 +19,35 @@ package io.netty.example.http2.helloworld.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.handler.codec.http.HttpMessage; 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.Http2ServerUpgradeCodec; import io.netty.handler.ssl.SslContext; -import java.util.Collections; - /** * 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(String protocol) { + if (Http2CodecUtil.HTTP_UPGRADE_PROTOCOL_NAME.equals(protocol)) { + return new Http2ServerUpgradeCodec(new HelloWorldHttp2Handler()); + } else { + return null; + } + } + }; + private final SslContext sslCtx; public Http2ServerInitializer(SslContext sslCtx) { @@ -60,25 +74,23 @@ public class Http2ServerInitializer extends ChannelInitializer { * Configure the pipeline for a cleartext upgrade from HTTP to HTTP/2. */ private static void configureClearText(SocketChannel ch) { - HttpServerCodec sourceCodec = new HttpServerCodec(); - HttpServerUpgradeHandler.UpgradeCodec upgradeCodec = - new Http2ServerUpgradeCodec(new HelloWorldHttp2Handler()); - HttpServerUpgradeHandler upgradeHandler = - new HttpServerUpgradeHandler(sourceCodec, Collections.singletonList(upgradeCodec), 65536); + final ChannelPipeline p = ch.pipeline(); + final HttpServerCodec sourceCodec = new HttpServerCodec(); - ch.pipeline().addLast(sourceCodec); - ch.pipeline().addLast(upgradeHandler); - ch.pipeline().addLast(new SimpleChannelInboundHandler() { + 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)"); ctx.pipeline().replace(this, "http-hello-world", - new HelloWorldHttp1Handler("Direct. No Upgrade Attempted.")); + new HelloWorldHttp1Handler("Direct. No Upgrade Attempted.")); ctx.fireChannelRead(msg); } }); - ch.pipeline().addLast(new UserEventLogger()); + + p.addLast(new UserEventLogger()); } /**