/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.handler.codec.http;
import static io.netty.util.AsciiString.containsContentEqualsIgnoreCase;
import static io.netty.util.AsciiString.containsAllContentEqualsIgnoreCase;
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.internal.ObjectUtil.checkNotNull;
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.
/**
* 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.
/**
* The source codec that is used in the pipeline initially.
*/
public interface SourceCodec {
Removes this codec (i.e. all associated handlers) from the pipeline.
/**
* Removes this codec (i.e. all associated handlers) from the pipeline.
*/
void upgradeFrom(ChannelHandlerContext ctx);
}
A codec that the source can be upgraded to.
/**
* 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 HttpHeaderNames.CONNECTION
header as well. /**
* 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<CharSequence> requiredUpgradeHeaders();
Prepares the upgradeHeaders
for a protocol update based upon the contents of upgradeRequest
. This method returns a boolean value to proceed or abort the upgrade in progress. If false
is returned, the upgrade is aborted and the upgradeRequest
will be passed through the inbound pipeline as if no upgrade was performed. If true
is returned, the upgrade will proceed to the next step which invokes upgradeTo
. When returning true
, you can add headers to the upgradeHeaders
so that they are added to the 101 Switching protocols response. /**
* 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.
Params: - ctx – the context for the current handler.
- upgradeRequest – the request that triggered the upgrade to this protocol.
/**
* 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 UpgradeCodec
for the requested protocol name. /**
* Creates a new {@link UpgradeCodec} for the requested protocol name.
*/
public interface UpgradeCodecFactory {
Invoked by HttpServerUpgradeHandler
for all the requested protocol names in the order of the client preference. The first non-null
UpgradeCodec
returned by this method will be selected. Returns: a new UpgradeCodec
, or null
if the specified protocol name is not supported
/**
* 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.
/**
* 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.
/**
* The protocol that the channel has been upgraded to.
*/
public CharSequence protocol() {
return protocol;
}
Gets the request that triggered the protocol upgrade.
/**
* 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 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 HttpServerUpgradeHandler(SourceCodec, UpgradeCodecFactory, int)
to specify the maximum length of the content of an upgrade request.
Params: - sourceCodec – the codec that is being used initially
- upgradeCodecFactory – the factory that creates a new upgrade codec
for one of the requested upgrade protocols
/**
* Constructs the upgrader with the supported codecs.
* <p>
* 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.
* </p>
*
* @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);
}
Constructs the upgrader with the supported codecs.
Params: - sourceCodec – the codec that is being used initially
- upgradeCodecFactory – the factory that creates a new upgrade codec
for one of the requested upgrade protocols
- maxContentLength – the maximum length of the content of an upgrade request
/**
* 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);
this.sourceCodec = checkNotNull(sourceCodec, "sourceCodec");
this.upgradeCodecFactory = checkNotNull(upgradeCodecFactory, "upgradeCodecFactory");
}
@Override
protected void decode(ChannelHandlerContext ctx, HttpObject msg, List<Object> out)
throws Exception {
// Determine if we're already handling an upgrade request or just starting a new one.
handlingUpgrade |= isUpgradeRequest(msg);
if (!handlingUpgrade) {
// Not handling an upgrade request, just pass it to the next handler.
ReferenceCountUtil.retain(msg);
out.add(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 or not the message is an HTTP upgrade request.
/**
* Determines whether or not the message is an HTTP upgrade request.
*/
private static boolean isUpgradeRequest(HttpObject msg) {
return msg instanceof HttpRequest && ((HttpRequest) msg).headers().get(HttpHeaderNames.UPGRADE) != null;
}
Attempts to upgrade to the protocol(s) identified by the HttpHeaderNames.UPGRADE
header (if provided in the request). Params: - ctx – the context for this handler.
- request – the HTTP request.
Returns: true
if the upgrade occurred, otherwise false
.
/**
* 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<CharSequence> 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.
CharSequence connectionHeader = request.headers().get(HttpHeaderNames.CONNECTION);
if (connectionHeader == null) {
return false;
}
// Make sure the CONNECTION header contains UPGRADE as well as all protocol-specific headers.
Collection<CharSequence> requiredHeaders = upgradeCodec.requiredUpgradeHeaders();
List<CharSequence> values = splitHeader(connectionHeader);
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.
/**
* Creates the 101 Switching Protocols response message.
*/
private static FullHttpResponse createUpgradeResponse(CharSequence upgradeProtocol) {
DefaultFullHttpResponse res = new DefaultFullHttpResponse(HTTP_1_1, SWITCHING_PROTOCOLS,
Unpooled.EMPTY_BUFFER, false);
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.
/**
* Splits a comma-separated header value. The returned set is case-insensitive and contains each
* part with whitespace removed.
*/
private static List<CharSequence> splitHeader(CharSequence header) {
final StringBuilder builder = new StringBuilder(header.length());
final List<CharSequence> protocols = new ArrayList<CharSequence>(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;
}
}