edf4e28afa
Motivation: The expression "not is success" can mean that either the future failed, or it has not yet completed. However, many places where such an expression is used is expecting the future to have completed. Specifically, they are expecting to be able to call `cause()` on the future. It is both more correct, and semantically clearer, to call `isFailed()` instead of `!isSuccess()`. Modification: Change all places that used `!isSuccess()` to mean that the future had failed, to use `isFailed()`. A few places are relying on `isSuccess()` returning `false` for _incomplete_ futures, and these places have been left unchanged. Result: Clearer code, with potentially fewer latent bugs.
582 lines
20 KiB
Java
582 lines
20 KiB
Java
/*
|
|
* Copyright 2012 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.ByteBuf;
|
|
import io.netty.buffer.Unpooled;
|
|
import io.netty.channel.ChannelHandler;
|
|
import io.netty.channel.ChannelHandlerContext;
|
|
import io.netty.channel.ChannelPipeline;
|
|
import io.netty.handler.codec.DecoderResult;
|
|
import io.netty.handler.codec.MessageAggregator;
|
|
import io.netty.handler.codec.TooLongFrameException;
|
|
import io.netty.util.concurrent.Future;
|
|
import io.netty.util.internal.logging.InternalLogger;
|
|
import io.netty.util.internal.logging.InternalLoggerFactory;
|
|
|
|
import static io.netty.handler.codec.http.HttpHeaderNames.CONNECTION;
|
|
import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_LENGTH;
|
|
import static io.netty.handler.codec.http.HttpHeaderNames.EXPECT;
|
|
import static io.netty.handler.codec.http.HttpUtil.getContentLength;
|
|
|
|
/**
|
|
* A {@link ChannelHandler} that aggregates an {@link HttpMessage}
|
|
* and its following {@link HttpContent}s into a single {@link FullHttpRequest}
|
|
* or {@link FullHttpResponse} (depending on if it used to handle requests or responses)
|
|
* with no following {@link HttpContent}s. It is useful when you don't want to take
|
|
* care of HTTP messages whose transfer encoding is 'chunked'. Insert this
|
|
* handler after {@link HttpResponseDecoder} in the {@link ChannelPipeline} if being used to handle
|
|
* responses, or after {@link HttpRequestDecoder} and {@link HttpResponseEncoder} in the
|
|
* {@link ChannelPipeline} if being used to handle requests.
|
|
* <blockquote>
|
|
* <pre>
|
|
* {@link ChannelPipeline} p = ...;
|
|
* ...
|
|
* p.addLast("decoder", <b>new {@link HttpRequestDecoder}()</b>);
|
|
* p.addLast("encoder", <b>new {@link HttpResponseEncoder}()</b>);
|
|
* p.addLast("aggregator", <b>new {@link HttpObjectAggregator}(1048576)</b>);
|
|
* ...
|
|
* p.addLast("handler", new HttpRequestHandler());
|
|
* </pre>
|
|
* </blockquote>
|
|
* <p>
|
|
* For convenience, consider putting a {@link HttpServerCodec} before the {@link HttpObjectAggregator}
|
|
* as it functions as both a {@link HttpRequestDecoder} and a {@link HttpResponseEncoder}.
|
|
* </p>
|
|
* Be aware that {@link HttpObjectAggregator} may end up sending a {@link HttpResponse}:
|
|
* <table border summary="Possible Responses">
|
|
* <tbody>
|
|
* <tr>
|
|
* <th>Response Status</th>
|
|
* <th>Condition When Sent</th>
|
|
* </tr>
|
|
* <tr>
|
|
* <td>100 Continue</td>
|
|
* <td>A '100-continue' expectation is received and the 'content-length' doesn't exceed maxContentLength</td>
|
|
* </tr>
|
|
* <tr>
|
|
* <td>417 Expectation Failed</td>
|
|
* <td>A '100-continue' expectation is received and the 'content-length' exceeds maxContentLength</td>
|
|
* </tr>
|
|
* <tr>
|
|
* <td>413 Request Entity Too Large</td>
|
|
* <td>Either the 'content-length' or the bytes received so far exceed maxContentLength</td>
|
|
* </tr>
|
|
* </tbody>
|
|
* </table>
|
|
*
|
|
* @see FullHttpRequest
|
|
* @see FullHttpResponse
|
|
* @see HttpResponseDecoder
|
|
* @see HttpServerCodec
|
|
*/
|
|
public class HttpObjectAggregator
|
|
extends MessageAggregator<HttpObject, HttpMessage, HttpContent, FullHttpMessage> {
|
|
private static final InternalLogger logger = InternalLoggerFactory.getInstance(HttpObjectAggregator.class);
|
|
private static final FullHttpResponse CONTINUE =
|
|
new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.CONTINUE, Unpooled.EMPTY_BUFFER);
|
|
private static final FullHttpResponse EXPECTATION_FAILED = new DefaultFullHttpResponse(
|
|
HttpVersion.HTTP_1_1, HttpResponseStatus.EXPECTATION_FAILED, Unpooled.EMPTY_BUFFER);
|
|
private static final FullHttpResponse TOO_LARGE_CLOSE = new DefaultFullHttpResponse(
|
|
HttpVersion.HTTP_1_1, HttpResponseStatus.REQUEST_ENTITY_TOO_LARGE, Unpooled.EMPTY_BUFFER);
|
|
private static final FullHttpResponse TOO_LARGE = new DefaultFullHttpResponse(
|
|
HttpVersion.HTTP_1_1, HttpResponseStatus.REQUEST_ENTITY_TOO_LARGE, Unpooled.EMPTY_BUFFER);
|
|
|
|
static {
|
|
EXPECTATION_FAILED.headers().set(CONTENT_LENGTH, 0);
|
|
TOO_LARGE.headers().set(CONTENT_LENGTH, 0);
|
|
|
|
TOO_LARGE_CLOSE.headers().set(CONTENT_LENGTH, 0);
|
|
TOO_LARGE_CLOSE.headers().set(CONNECTION, HttpHeaderValues.CLOSE);
|
|
}
|
|
|
|
private final boolean closeOnExpectationFailed;
|
|
|
|
/**
|
|
* Creates a new instance.
|
|
* @param maxContentLength the maximum length of the aggregated content in bytes.
|
|
* If the length of the aggregated content exceeds this value,
|
|
* {@link #handleOversizedMessage(ChannelHandlerContext, HttpMessage)} will be called.
|
|
*/
|
|
public HttpObjectAggregator(int maxContentLength) {
|
|
this(maxContentLength, false);
|
|
}
|
|
|
|
/**
|
|
* Creates a new instance.
|
|
* @param maxContentLength the maximum length of the aggregated content in bytes.
|
|
* If the length of the aggregated content exceeds this value,
|
|
* {@link #handleOversizedMessage(ChannelHandlerContext, HttpMessage)} will be called.
|
|
* @param closeOnExpectationFailed If a 100-continue response is detected but the content length is too large
|
|
* then {@code true} means close the connection. otherwise the connection will remain open and data will be
|
|
* consumed and discarded until the next request is received.
|
|
*/
|
|
public HttpObjectAggregator(int maxContentLength, boolean closeOnExpectationFailed) {
|
|
super(maxContentLength);
|
|
this.closeOnExpectationFailed = closeOnExpectationFailed;
|
|
}
|
|
|
|
@Override
|
|
protected boolean isStartMessage(HttpObject msg) throws Exception {
|
|
return msg instanceof HttpMessage;
|
|
}
|
|
|
|
@Override
|
|
protected boolean isContentMessage(HttpObject msg) throws Exception {
|
|
return msg instanceof HttpContent;
|
|
}
|
|
|
|
@Override
|
|
protected boolean isLastContentMessage(HttpContent msg) throws Exception {
|
|
return msg instanceof LastHttpContent;
|
|
}
|
|
|
|
@Override
|
|
protected boolean isAggregated(HttpObject msg) throws Exception {
|
|
return msg instanceof FullHttpMessage;
|
|
}
|
|
|
|
@Override
|
|
protected boolean isContentLengthInvalid(HttpMessage start, int maxContentLength) {
|
|
try {
|
|
return getContentLength(start, -1L) > maxContentLength;
|
|
} catch (final NumberFormatException e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private static Object continueResponse(HttpMessage start, int maxContentLength, ChannelPipeline pipeline) {
|
|
if (HttpUtil.isUnsupportedExpectation(start)) {
|
|
// if the request contains an unsupported expectation, we return 417
|
|
pipeline.fireUserEventTriggered(HttpExpectationFailedEvent.INSTANCE);
|
|
return EXPECTATION_FAILED.retainedDuplicate();
|
|
} else if (HttpUtil.is100ContinueExpected(start)) {
|
|
// if the request contains 100-continue but the content-length is too large, we return 413
|
|
if (getContentLength(start, -1L) <= maxContentLength) {
|
|
return CONTINUE.retainedDuplicate();
|
|
}
|
|
pipeline.fireUserEventTriggered(HttpExpectationFailedEvent.INSTANCE);
|
|
return TOO_LARGE.retainedDuplicate();
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
@Override
|
|
protected Object newContinueResponse(HttpMessage start, int maxContentLength, ChannelPipeline pipeline) {
|
|
Object response = continueResponse(start, maxContentLength, pipeline);
|
|
// we're going to respond based on the request expectation so there's no
|
|
// need to propagate the expectation further.
|
|
if (response != null) {
|
|
start.headers().remove(EXPECT);
|
|
}
|
|
return response;
|
|
}
|
|
|
|
@Override
|
|
protected boolean closeAfterContinueResponse(Object msg) {
|
|
return closeOnExpectationFailed && ignoreContentAfterContinueResponse(msg);
|
|
}
|
|
|
|
@Override
|
|
protected boolean ignoreContentAfterContinueResponse(Object msg) {
|
|
if (msg instanceof HttpResponse) {
|
|
final HttpResponse httpResponse = (HttpResponse) msg;
|
|
return httpResponse.status().codeClass().equals(HttpStatusClass.CLIENT_ERROR);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
@Override
|
|
protected FullHttpMessage beginAggregation(HttpMessage start, ByteBuf content) throws Exception {
|
|
assert !(start instanceof FullHttpMessage);
|
|
|
|
HttpUtil.setTransferEncodingChunked(start, false);
|
|
|
|
AggregatedFullHttpMessage ret;
|
|
if (start instanceof HttpRequest) {
|
|
ret = new AggregatedFullHttpRequest((HttpRequest) start, content, null);
|
|
} else if (start instanceof HttpResponse) {
|
|
ret = new AggregatedFullHttpResponse((HttpResponse) start, content, null);
|
|
} else {
|
|
throw new Error();
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
@Override
|
|
protected void aggregate(FullHttpMessage aggregated, HttpContent content) throws Exception {
|
|
if (content instanceof LastHttpContent) {
|
|
// Merge trailing headers into the message.
|
|
((AggregatedFullHttpMessage) aggregated).setTrailingHeaders(((LastHttpContent) content).trailingHeaders());
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void finishAggregation(FullHttpMessage aggregated) throws Exception {
|
|
// Set the 'Content-Length' header. If one isn't already set.
|
|
// This is important as HEAD responses will use a 'Content-Length' header which
|
|
// does not match the actual body, but the number of bytes that would be
|
|
// transmitted if a GET would have been used.
|
|
//
|
|
// See rfc2616 14.13 Content-Length
|
|
if (!HttpUtil.isContentLengthSet(aggregated)) {
|
|
aggregated.headers().set(
|
|
CONTENT_LENGTH,
|
|
String.valueOf(aggregated.content().readableBytes()));
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void handleOversizedMessage(final ChannelHandlerContext ctx, HttpMessage oversized) throws Exception {
|
|
if (oversized instanceof HttpRequest) {
|
|
// send back a 413 and close the connection
|
|
|
|
// If the client started to send data already, close because it's impossible to recover.
|
|
// If keep-alive is off and 'Expect: 100-continue' is missing, no need to leave the connection open.
|
|
if (oversized instanceof FullHttpMessage ||
|
|
!HttpUtil.is100ContinueExpected(oversized) && !HttpUtil.isKeepAlive(oversized)) {
|
|
Future<Void> future = ctx.writeAndFlush(TOO_LARGE_CLOSE.retainedDuplicate());
|
|
future.addListener(future1 -> {
|
|
if (future1.isFailed()) {
|
|
logger.debug("Failed to send a 413 Request Entity Too Large.", future1.cause());
|
|
}
|
|
ctx.close();
|
|
});
|
|
} else {
|
|
ctx.writeAndFlush(TOO_LARGE.retainedDuplicate()).addListener(future -> {
|
|
if (future.isFailed()) {
|
|
logger.debug("Failed to send a 413 Request Entity Too Large.", future.cause());
|
|
ctx.close();
|
|
}
|
|
});
|
|
}
|
|
} else if (oversized instanceof HttpResponse) {
|
|
throw new ResponseTooLargeException("Response entity too large: " + oversized);
|
|
} else {
|
|
throw new IllegalStateException();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
|
|
super.exceptionCaught(ctx, cause);
|
|
if (cause instanceof ResponseTooLargeException) {
|
|
ctx.close();
|
|
}
|
|
}
|
|
|
|
private static final class ResponseTooLargeException extends TooLongFrameException {
|
|
ResponseTooLargeException(String message) {
|
|
super(message);
|
|
}
|
|
}
|
|
|
|
private abstract static class AggregatedFullHttpMessage implements FullHttpMessage {
|
|
protected final HttpMessage message;
|
|
private final ByteBuf content;
|
|
private HttpHeaders trailingHeaders;
|
|
|
|
AggregatedFullHttpMessage(HttpMessage message, ByteBuf content, HttpHeaders trailingHeaders) {
|
|
this.message = message;
|
|
this.content = content;
|
|
this.trailingHeaders = trailingHeaders;
|
|
}
|
|
|
|
@Override
|
|
public HttpHeaders trailingHeaders() {
|
|
HttpHeaders trailingHeaders = this.trailingHeaders;
|
|
if (trailingHeaders == null) {
|
|
return EmptyHttpHeaders.INSTANCE;
|
|
} else {
|
|
return trailingHeaders;
|
|
}
|
|
}
|
|
|
|
void setTrailingHeaders(HttpHeaders trailingHeaders) {
|
|
this.trailingHeaders = trailingHeaders;
|
|
}
|
|
|
|
@Override
|
|
public HttpVersion getProtocolVersion() {
|
|
return message.protocolVersion();
|
|
}
|
|
|
|
@Override
|
|
public HttpVersion protocolVersion() {
|
|
return message.protocolVersion();
|
|
}
|
|
|
|
@Override
|
|
public FullHttpMessage setProtocolVersion(HttpVersion version) {
|
|
message.setProtocolVersion(version);
|
|
return this;
|
|
}
|
|
|
|
@Override
|
|
public HttpHeaders headers() {
|
|
return message.headers();
|
|
}
|
|
|
|
@Override
|
|
public DecoderResult decoderResult() {
|
|
return message.decoderResult();
|
|
}
|
|
|
|
@Override
|
|
public DecoderResult getDecoderResult() {
|
|
return message.decoderResult();
|
|
}
|
|
|
|
@Override
|
|
public void setDecoderResult(DecoderResult result) {
|
|
message.setDecoderResult(result);
|
|
}
|
|
|
|
@Override
|
|
public ByteBuf content() {
|
|
return content;
|
|
}
|
|
|
|
@Override
|
|
public int refCnt() {
|
|
return content.refCnt();
|
|
}
|
|
|
|
@Override
|
|
public FullHttpMessage retain() {
|
|
content.retain();
|
|
return this;
|
|
}
|
|
|
|
@Override
|
|
public FullHttpMessage retain(int increment) {
|
|
content.retain(increment);
|
|
return this;
|
|
}
|
|
|
|
@Override
|
|
public FullHttpMessage touch(Object hint) {
|
|
content.touch(hint);
|
|
return this;
|
|
}
|
|
|
|
@Override
|
|
public FullHttpMessage touch() {
|
|
content.touch();
|
|
return this;
|
|
}
|
|
|
|
@Override
|
|
public boolean release() {
|
|
return content.release();
|
|
}
|
|
|
|
@Override
|
|
public boolean release(int decrement) {
|
|
return content.release(decrement);
|
|
}
|
|
|
|
@Override
|
|
public abstract FullHttpMessage copy();
|
|
|
|
@Override
|
|
public abstract FullHttpMessage duplicate();
|
|
|
|
@Override
|
|
public abstract FullHttpMessage retainedDuplicate();
|
|
}
|
|
|
|
private static final class AggregatedFullHttpRequest extends AggregatedFullHttpMessage implements FullHttpRequest {
|
|
|
|
AggregatedFullHttpRequest(HttpRequest request, ByteBuf content, HttpHeaders trailingHeaders) {
|
|
super(request, content, trailingHeaders);
|
|
}
|
|
|
|
@Override
|
|
public FullHttpRequest copy() {
|
|
return replace(content().copy());
|
|
}
|
|
|
|
@Override
|
|
public FullHttpRequest duplicate() {
|
|
return replace(content().duplicate());
|
|
}
|
|
|
|
@Override
|
|
public FullHttpRequest retainedDuplicate() {
|
|
return replace(content().retainedDuplicate());
|
|
}
|
|
|
|
@Override
|
|
public FullHttpRequest replace(ByteBuf content) {
|
|
DefaultFullHttpRequest dup = new DefaultFullHttpRequest(protocolVersion(), method(), uri(), content,
|
|
headers().copy(), trailingHeaders().copy());
|
|
dup.setDecoderResult(decoderResult());
|
|
return dup;
|
|
}
|
|
|
|
@Override
|
|
public FullHttpRequest retain(int increment) {
|
|
super.retain(increment);
|
|
return this;
|
|
}
|
|
|
|
@Override
|
|
public FullHttpRequest retain() {
|
|
super.retain();
|
|
return this;
|
|
}
|
|
|
|
@Override
|
|
public FullHttpRequest touch() {
|
|
super.touch();
|
|
return this;
|
|
}
|
|
|
|
@Override
|
|
public FullHttpRequest touch(Object hint) {
|
|
super.touch(hint);
|
|
return this;
|
|
}
|
|
|
|
@Override
|
|
public FullHttpRequest setMethod(HttpMethod method) {
|
|
((HttpRequest) message).setMethod(method);
|
|
return this;
|
|
}
|
|
|
|
@Override
|
|
public FullHttpRequest setUri(String uri) {
|
|
((HttpRequest) message).setUri(uri);
|
|
return this;
|
|
}
|
|
|
|
@Override
|
|
public HttpMethod getMethod() {
|
|
return ((HttpRequest) message).method();
|
|
}
|
|
|
|
@Override
|
|
public String getUri() {
|
|
return ((HttpRequest) message).uri();
|
|
}
|
|
|
|
@Override
|
|
public HttpMethod method() {
|
|
return getMethod();
|
|
}
|
|
|
|
@Override
|
|
public String uri() {
|
|
return getUri();
|
|
}
|
|
|
|
@Override
|
|
public FullHttpRequest setProtocolVersion(HttpVersion version) {
|
|
super.setProtocolVersion(version);
|
|
return this;
|
|
}
|
|
|
|
@Override
|
|
public String toString() {
|
|
return HttpMessageUtil.appendFullRequest(new StringBuilder(256), this).toString();
|
|
}
|
|
}
|
|
|
|
private static final class AggregatedFullHttpResponse extends AggregatedFullHttpMessage
|
|
implements FullHttpResponse {
|
|
|
|
AggregatedFullHttpResponse(HttpResponse message, ByteBuf content, HttpHeaders trailingHeaders) {
|
|
super(message, content, trailingHeaders);
|
|
}
|
|
|
|
@Override
|
|
public FullHttpResponse copy() {
|
|
return replace(content().copy());
|
|
}
|
|
|
|
@Override
|
|
public FullHttpResponse duplicate() {
|
|
return replace(content().duplicate());
|
|
}
|
|
|
|
@Override
|
|
public FullHttpResponse retainedDuplicate() {
|
|
return replace(content().retainedDuplicate());
|
|
}
|
|
|
|
@Override
|
|
public FullHttpResponse replace(ByteBuf content) {
|
|
DefaultFullHttpResponse dup = new DefaultFullHttpResponse(getProtocolVersion(), getStatus(), content,
|
|
headers().copy(), trailingHeaders().copy());
|
|
dup.setDecoderResult(decoderResult());
|
|
return dup;
|
|
}
|
|
|
|
@Override
|
|
public FullHttpResponse setStatus(HttpResponseStatus status) {
|
|
((HttpResponse) message).setStatus(status);
|
|
return this;
|
|
}
|
|
|
|
@Override
|
|
public HttpResponseStatus getStatus() {
|
|
return ((HttpResponse) message).status();
|
|
}
|
|
|
|
@Override
|
|
public HttpResponseStatus status() {
|
|
return getStatus();
|
|
}
|
|
|
|
@Override
|
|
public FullHttpResponse setProtocolVersion(HttpVersion version) {
|
|
super.setProtocolVersion(version);
|
|
return this;
|
|
}
|
|
|
|
@Override
|
|
public FullHttpResponse retain(int increment) {
|
|
super.retain(increment);
|
|
return this;
|
|
}
|
|
|
|
@Override
|
|
public FullHttpResponse retain() {
|
|
super.retain();
|
|
return this;
|
|
}
|
|
|
|
@Override
|
|
public FullHttpResponse touch(Object hint) {
|
|
super.touch(hint);
|
|
return this;
|
|
}
|
|
|
|
@Override
|
|
public FullHttpResponse touch() {
|
|
super.touch();
|
|
return this;
|
|
}
|
|
|
|
@Override
|
|
public String toString() {
|
|
return HttpMessageUtil.appendFullResponse(new StringBuilder(256), this).toString();
|
|
}
|
|
}
|
|
}
|