All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.netty.handler.codec.http.HttpServerUpgradeHandler Maven / Gradle / Ivy

There is a newer version: 5.0.0.Alpha2
Show newest version
/*
 * 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:
 *
 * https://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.http;

import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.util.ReferenceCountUtil;
import io.netty.util.ReferenceCounted;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

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.AsciiString.containsAllContentEqualsIgnoreCase;
import static io.netty.util.AsciiString.containsContentEqualsIgnoreCase;
import static io.netty.util.internal.ObjectUtil.checkNotNull;
import static io.netty.util.internal.StringUtil.COMMA;

/**
 * A server-side handler that receives HTTP requests and optionally performs a protocol switch if
 * the requested protocol is supported. Once an upgrade is performed, this handler removes itself
 * from the pipeline.
 */
public class HttpServerUpgradeHandler extends HttpObjectAggregator {

    /**
     * The source codec that is used in the pipeline initially.
     */
    public interface SourceCodec {
        /**
         * Removes this codec (i.e. all associated handlers) from the pipeline.
         */
        void upgradeFrom(ChannelHandlerContext ctx);
    }

    /**
     * A codec that the source can be upgraded to.
     */
    public interface UpgradeCodec {
        /**
         * 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.
         */
        Collection requiredUpgradeHeaders();

        /**
         * Prepares the {@code upgradeHeaders} for a protocol update based upon the contents of {@code upgradeRequest}.
         * This method returns a boolean value to proceed or abort the upgrade in progress. If {@code false} is
         * returned, the upgrade is aborted and the {@code upgradeRequest} will be passed through the inbound pipeline
         * as if no upgrade was performed. If {@code true} is returned, the upgrade will proceed to the next
         * step which invokes {@link #upgradeTo}. When returning {@code true}, you can add headers to
         * the {@code upgradeHeaders} so that they are added to the 101 Switching protocols response.
         */
        boolean prepareUpgradeResponse(ChannelHandlerContext ctx, FullHttpRequest upgradeRequest,
                                    HttpHeaders upgradeHeaders);

        /**
         * Performs an HTTP protocol upgrade from the source codec. This method is responsible for
         * adding all handlers required for the new protocol.
         *
         * @param ctx the context for the current handler.
         * @param upgradeRequest the request that triggered the upgrade to this protocol.
         */
        void upgradeTo(ChannelHandlerContext ctx, FullHttpRequest upgradeRequest);
    }

    /**
     * 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(CharSequence 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
     * (if required) can be sent using the new protocol.
     */
    public static final class UpgradeEvent implements ReferenceCounted {
        private final CharSequence protocol;
        private final FullHttpRequest upgradeRequest;

        UpgradeEvent(CharSequence protocol, FullHttpRequest upgradeRequest) {
            this.protocol = protocol;
            this.upgradeRequest = upgradeRequest;
        }

        /**
         * The protocol that the channel has been upgraded to.
         */
        public CharSequence protocol() {
            return protocol;
        }

        /**
         * Gets the request that triggered the protocol upgrade.
         */
        public FullHttpRequest upgradeRequest() {
            return upgradeRequest;
        }

        @Override
        public int refCnt() {
            return upgradeRequest.refCnt();
        }

        @Override
        public UpgradeEvent retain() {
            upgradeRequest.retain();
            return this;
        }

        @Override
        public UpgradeEvent retain(int increment) {
            upgradeRequest.retain(increment);
            return this;
        }

        @Override
        public UpgradeEvent touch() {
            upgradeRequest.touch();
            return this;
        }

        @Override
        public UpgradeEvent touch(Object hint) {
            upgradeRequest.touch(hint);
            return this;
        }

        @Override
        public boolean release() {
            return upgradeRequest.release();
        }

        @Override
        public boolean release(int decrement) {
            return upgradeRequest.release(decrement);
        }

        @Override
        public String toString() {
            return "UpgradeEvent [protocol=" + protocol + ", upgradeRequest=" + upgradeRequest + ']';
        }
    }

    private final SourceCodec sourceCodec;
    private final UpgradeCodecFactory upgradeCodecFactory;
    private final HttpHeadersFactory headersFactory;
    private final HttpHeadersFactory trailersFactory;
    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 upgradeCodecFactory the factory that creates a new upgrade codec * for one of the requested upgrade protocols */ public HttpServerUpgradeHandler(SourceCodec sourceCodec, UpgradeCodecFactory upgradeCodecFactory) { this(sourceCodec, upgradeCodecFactory, 0, DefaultHttpHeadersFactory.headersFactory(), DefaultHttpHeadersFactory.trailersFactory()); } /** * 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) { this(sourceCodec, upgradeCodecFactory, maxContentLength, DefaultHttpHeadersFactory.headersFactory(), DefaultHttpHeadersFactory.trailersFactory()); } /** * 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 * @param validateHeaders validate the header names and values of the upgrade response. */ public HttpServerUpgradeHandler(SourceCodec sourceCodec, UpgradeCodecFactory upgradeCodecFactory, int maxContentLength, boolean validateHeaders) { this(sourceCodec, upgradeCodecFactory, maxContentLength, DefaultHttpHeadersFactory.headersFactory().withValidation(validateHeaders), DefaultHttpHeadersFactory.trailersFactory().withValidation(validateHeaders)); } /** * 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 * @param headersFactory The {@link HttpHeadersFactory} to use for headers. * The recommended default factory is {@link DefaultHttpHeadersFactory#headersFactory()}. * @param trailersFactory The {@link HttpHeadersFactory} to use for trailers. * The recommended default factory is {@link DefaultHttpHeadersFactory#trailersFactory()}. */ public HttpServerUpgradeHandler( SourceCodec sourceCodec, UpgradeCodecFactory upgradeCodecFactory, int maxContentLength, HttpHeadersFactory headersFactory, HttpHeadersFactory trailersFactory) { super(maxContentLength); this.sourceCodec = checkNotNull(sourceCodec, "sourceCodec"); this.upgradeCodecFactory = checkNotNull(upgradeCodecFactory, "upgradeCodecFactory"); this.headersFactory = checkNotNull(headersFactory, "headersFactory"); this.trailersFactory = checkNotNull(trailersFactory, "trailersFactory"); } @Override protected void decode(ChannelHandlerContext ctx, HttpObject msg, List out) throws Exception { if (!handlingUpgrade) { // Not handling an upgrade request yet. Check if we received a new upgrade request. if (msg instanceof HttpRequest) { HttpRequest req = (HttpRequest) msg; if (req.headers().contains(HttpHeaderNames.UPGRADE) && shouldHandleUpgradeRequest(req)) { handlingUpgrade = true; } else { ReferenceCountUtil.retain(msg); ctx.fireChannelRead(msg); return; } } else { ReferenceCountUtil.retain(msg); ctx.fireChannelRead(msg); return; } } FullHttpRequest fullRequest; if (msg instanceof FullHttpRequest) { fullRequest = (FullHttpRequest) msg; ReferenceCountUtil.retain(msg); out.add(msg); } else { // Call the base class to handle the aggregation of the full request. super.decode(ctx, msg, out); if (out.isEmpty()) { // The full request hasn't been created yet, still awaiting more data. return; } // Finished aggregating the full request, get it from the output list. assert out.size() == 1; handlingUpgrade = false; fullRequest = (FullHttpRequest) out.get(0); } if (upgrade(ctx, fullRequest)) { // The upgrade was successful, remove the message from the output list // so that it's not propagated to the next handler. This request will // be propagated as a user event instead. out.clear(); } // The upgrade did not succeed, just allow the full request to propagate to the // next handler. } /** * Determines whether the specified upgrade {@link HttpRequest} should be handled by this handler or not. * This method will be invoked only when the request contains an {@code Upgrade} header. * It always returns {@code true} by default, which means any request with an {@code Upgrade} header * will be handled. You can override this method to ignore certain {@code Upgrade} headers, for example: *
{@code
     * @Override
     * protected boolean isUpgradeRequest(HttpRequest req) {
     *   // Do not handle WebSocket upgrades.
     *   return !req.headers().contains(HttpHeaderNames.UPGRADE, "websocket", false);
     * }
     * }
*/ protected boolean shouldHandleUpgradeRequest(HttpRequest req) { return true; } /** * Attempts to upgrade to the protocol(s) identified by the {@link HttpHeaderNames#UPGRADE} header (if provided * in the request). * * @param ctx the context for this handler. * @param request the HTTP request. * @return {@code true} if the upgrade occurred, otherwise {@code false}. */ private boolean upgrade(final ChannelHandlerContext ctx, final FullHttpRequest request) { // Select the best protocol based on those requested in the UPGRADE header. final List requestedProtocols = splitHeader(request.headers().get(HttpHeaderNames.UPGRADE)); final int numRequestedProtocols = requestedProtocols.size(); UpgradeCodec upgradeCodec = null; CharSequence upgradeProtocol = null; for (int i = 0; i < numRequestedProtocols; i ++) { final CharSequence 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; } // Make sure the CONNECTION header is present. List connectionHeaderValues = request.headers().getAll(HttpHeaderNames.CONNECTION); if (connectionHeaderValues == null || connectionHeaderValues.isEmpty()) { return false; } final StringBuilder concatenatedConnectionValue = new StringBuilder(connectionHeaderValues.size() * 10); for (CharSequence connectionHeaderValue : connectionHeaderValues) { concatenatedConnectionValue.append(connectionHeaderValue).append(COMMA); } concatenatedConnectionValue.setLength(concatenatedConnectionValue.length() - 1); // Make sure the CONNECTION header contains UPGRADE as well as all protocol-specific headers. Collection requiredHeaders = upgradeCodec.requiredUpgradeHeaders(); List values = splitHeader(concatenatedConnectionValue); if (!containsContentEqualsIgnoreCase(values, HttpHeaderNames.UPGRADE) || !containsAllContentEqualsIgnoreCase(values, requiredHeaders)) { return false; } // Ensure that all required protocol-specific headers are found in the request. for (CharSequence requiredHeader : requiredHeaders) { if (!request.headers().contains(requiredHeader)) { return false; } } // 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(upgradeProtocol); if (!upgradeCodec.prepareUpgradeResponse(ctx, request, upgradeResponse.headers())) { return false; } // Create the user event to be fired once the upgrade completes. final UpgradeEvent event = new UpgradeEvent(upgradeProtocol, request); // After writing the upgrade response we immediately prepare the // pipeline for the next protocol to avoid a race between completion // of the write future and receiving data before the pipeline is // restructured. try { final ChannelFuture writeComplete = ctx.writeAndFlush(upgradeResponse); // Perform the upgrade to the new protocol. sourceCodec.upgradeFrom(ctx); upgradeCodec.upgradeTo(ctx, request); // Remove this handler from the pipeline. ctx.pipeline().remove(HttpServerUpgradeHandler.this); // Notify that the upgrade has occurred. Retain the event to offset // the release() in the finally block. ctx.fireUserEventTriggered(event.retain()); // Add the listener last to avoid firing upgrade logic after // the channel is already closed since the listener may fire // immediately if the write failed eagerly. writeComplete.addListener(ChannelFutureListener.CLOSE_ON_FAILURE); } finally { // Release the event if the upgrade event wasn't fired. event.release(); } return true; } /** * Creates the 101 Switching Protocols response message. */ private FullHttpResponse createUpgradeResponse(CharSequence upgradeProtocol) { DefaultFullHttpResponse res = new DefaultFullHttpResponse( HTTP_1_1, SWITCHING_PROTOCOLS, Unpooled.EMPTY_BUFFER, headersFactory, trailersFactory); res.headers().add(HttpHeaderNames.CONNECTION, HttpHeaderValues.UPGRADE); res.headers().add(HttpHeaderNames.UPGRADE, upgradeProtocol); return res; } /** * Splits a comma-separated header value. The returned set is case-insensitive and contains each * part with whitespace removed. */ private static List splitHeader(CharSequence header) { final StringBuilder builder = new StringBuilder(header.length()); final List protocols = new ArrayList(4); for (int i = 0; i < header.length(); ++i) { char c = header.charAt(i); if (Character.isWhitespace(c)) { // Don't include any whitespace. continue; } if (c == ',') { // Add the string and reset the builder for the next protocol. protocols.add(builder.toString()); builder.setLength(0); } else { builder.append(c); } } // Add the last protocol if (builder.length() > 0) { protocols.add(builder.toString()); } return protocols; } }