io.netty5.handler.codec.http.HttpContentDecoder Maven / Gradle / Ivy
/*
* 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.netty5.handler.codec.http;
import io.netty5.buffer.Buffer;
import io.netty5.channel.ChannelHandlerContext;
import io.netty5.channel.ChannelOption;
import io.netty5.handler.codec.CodecException;
import io.netty5.handler.codec.MessageToMessageDecoder;
import io.netty5.handler.codec.compression.Decompressor;
import io.netty5.handler.codec.http.headers.HttpHeaders;
import io.netty5.util.AsciiString;
import io.netty5.util.Resource;
/**
* Decodes the content of the received {@link HttpRequest} and {@link HttpContent}.
* The original content is replaced with the new content decoded by the
* {@link Decompressor}, which is created by {@link #newContentDecoder(CharSequence)}.
* Once decoding is finished, the value of the 'Content-Encoding'
* header is set to the target content encoding, as returned by {@link #getTargetContentEncoding(String)}.
* Also, the 'Content-Length' header is updated to the length of the
* decoded content. If the content encoding of the original is not supported
* by the decoder, {@link #newContentDecoder(CharSequence)} should return {@code null}
* so that no decoding occurs (i.e. pass-through).
*
* Please note that this is an abstract class. You have to extend this class
* and implement {@link #newContentDecoder(CharSequence)} properly to make this class
* functional. For example, refer to the source code of {@link HttpContentDecompressor}.
*
* This handler must be placed after {@link HttpObjectDecoder} in the pipeline
* so that this handler can intercept HTTP requests after {@link HttpObjectDecoder}
* converts {@link Buffer}s into HTTP requests.
*/
public abstract class HttpContentDecoder extends MessageToMessageDecoder {
static final String IDENTITY = HttpHeaderValues.IDENTITY.toString();
protected ChannelHandlerContext ctx;
private Decompressor decompressor;
private boolean continueResponse;
private boolean needRead = true;
@Override
protected void decodeAndClose(ChannelHandlerContext ctx, HttpObject msg) throws Exception {
boolean dispose = true;
try {
if (msg instanceof HttpResponse && ((HttpResponse) msg).status().code() == 100) {
if (!(msg instanceof LastHttpContent)) {
continueResponse = true;
}
// 100-continue response must be passed through.
fireChannelRead(ctx, msg);
return;
}
if (continueResponse) {
if (msg instanceof LastHttpContent) {
continueResponse = false;
}
// 100-continue response must be passed through.
fireChannelRead(ctx, msg);
return;
}
if (msg instanceof HttpMessage) {
cleanup();
final HttpMessage message = (HttpMessage) msg;
final HttpHeaders headers = message.headers();
// Determine the content encoding.
CharSequence contentEncoding = headers.get(HttpHeaderNames.CONTENT_ENCODING);
if (contentEncoding != null) {
contentEncoding = AsciiString.trim(contentEncoding);
} else {
CharSequence transferEncoding = headers.get(HttpHeaderNames.TRANSFER_ENCODING);
if (transferEncoding != null) {
int idx = AsciiString.indexOf(transferEncoding, ',', 0);
if (idx != -1) {
contentEncoding = AsciiString.trim(AsciiString.substring(transferEncoding, 0, idx));
} else {
contentEncoding = AsciiString.trim(transferEncoding);
}
} else {
contentEncoding = IDENTITY;
}
}
decompressor = newContentDecoder(contentEncoding);
if (decompressor == null) {
dispose = false;
fireChannelRead(ctx, message);
return;
}
// Remove content-length header:
// the correct value can be set only after all chunks are processed/decoded.
// If buffering is not an issue, add HttpObjectAggregator down the chain, it will set the header.
// Otherwise, rely on LastHttpContent message.
if (headers.contains(HttpHeaderNames.CONTENT_LENGTH)) {
headers.remove(HttpHeaderNames.CONTENT_LENGTH);
headers.set(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.CHUNKED);
}
// Either it is already chunked or EOF terminated.
// See https://github.com/netty/netty/issues/5892
// set new content encoding,
CharSequence targetContentEncoding = getTargetContentEncoding(contentEncoding);
if (HttpHeaderValues.IDENTITY.contentEquals(targetContentEncoding)) {
// Do NOT set the 'Content-Encoding' header if the target encoding is 'identity'
// as per: https://tools.ietf.org/html/rfc2616#section-14.11
headers.remove(HttpHeaderNames.CONTENT_ENCODING);
} else {
headers.set(HttpHeaderNames.CONTENT_ENCODING, targetContentEncoding);
}
if (message instanceof HttpContent) {
// If message is a full request or response object (headers + data), don't copy data part into out.
// Output headers only; data part will be decoded below.
// Note: "copy" object must not be an instance of LastHttpContent class,
// as this would (erroneously) indicate the end of the HttpMessage to other handlers.
HttpMessage copy;
if (message instanceof HttpRequest) {
HttpRequest r = (HttpRequest) message; // HttpRequest or FullHttpRequest
copy = new DefaultHttpRequest(r.protocolVersion(), r.method(), r.uri());
} else if (message instanceof HttpResponse) {
HttpResponse r = (HttpResponse) message; // HttpResponse or FullHttpResponse
copy = new DefaultHttpResponse(r.protocolVersion(), r.status());
} else {
throw new CodecException("Object of class " + message.getClass().getName() +
" is not an HttpRequest or HttpResponse");
}
copy.headers().set(message.headers());
copy.setDecoderResult(message.decoderResult());
fireChannelRead(ctx, copy);
} else {
fireChannelRead(ctx, message);
}
}
if (msg instanceof HttpContent) {
final HttpContent> c = (HttpContent>) msg;
if (decompressor == null) {
dispose = false;
fireChannelRead(ctx, c);
} else {
decodeContent(ctx, c);
}
}
} finally {
if (dispose) {
Resource.dispose(msg);
}
}
}
private void decodeContent(ChannelHandlerContext ctx, HttpContent> c) {
final Buffer payload = c.payload();
assert decompressor != null;
while (!decompressor.isFinished()) {
int idx = payload.readerOffset();
Buffer decompressed = decompressor.decompress(payload, ctx.bufferAllocator());
if (decompressed != null) {
if (decompressed.readableBytes() == 0) {
decompressed.close();
return;
}
ctx.fireChannelRead(new DefaultHttpContent(decompressed));
} else if (idx == payload.readerOffset()) {
break;
}
}
if (c instanceof LastHttpContent) {
decompressor.close();
decompressor = null;
LastHttpContent> last = (LastHttpContent>) c;
// Generate an additional chunk if the decoder produced
// the last product on closure,
HttpHeaders headers = last.trailingHeaders();
if (headers.isEmpty()) {
fireChannelRead(ctx, new EmptyLastHttpContent(ctx.bufferAllocator()));
} else {
fireChannelRead(ctx, new DefaultLastHttpContent(ctx.bufferAllocator().allocate(0), headers));
}
}
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
boolean needRead = this.needRead;
this.needRead = true;
try {
ctx.fireChannelReadComplete();
} finally {
if (needRead && !ctx.channel().getOption(ChannelOption.AUTO_READ)) {
ctx.read();
}
}
}
/**
* Returns a new {@link Decompressor} that decodes the HTTP message
* content encoded in the specified contentEncoding.
*
* @param contentEncoding the value of the {@code "Content-Encoding"} header
* @return a new {@link Decompressor} if the specified encoding is supported.
* {@code null} otherwise (alternatively, you can throw an exception
* to block unknown encoding).
*/
protected abstract Decompressor newContentDecoder(CharSequence contentEncoding) throws Exception;
/**
* Returns the expected content encoding of the decoded content.
* This getMethod returns {@code "identity"} by default, which is the case for
* most decoders.
*
* @param contentEncoding the value of the {@code "Content-Encoding"} header
* @return the expected content encoding of the new content
*/
protected String getTargetContentEncoding(
@SuppressWarnings("UnusedParameters") CharSequence contentEncoding) throws Exception {
return IDENTITY;
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
cleanupSafely(ctx);
super.handlerRemoved(ctx);
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
cleanupSafely(ctx);
super.channelInactive(ctx);
}
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
this.ctx = ctx;
super.handlerAdded(ctx);
}
private void cleanup() {
if (decompressor != null) {
// Clean-up the previous decompressor if not cleaned up correctly.
decompressor.close();
decompressor = null;
}
}
private void cleanupSafely(ChannelHandlerContext ctx) {
try {
cleanup();
} catch (Throwable cause) {
// If cleanup throws any error we need to propagate it through the pipeline
// so we don't fail to propagate pipeline events.
ctx.fireChannelExceptionCaught(cause);
}
}
private void fireChannelRead(ChannelHandlerContext ctx, Object msg) {
needRead = false;
ctx.fireChannelRead(msg);
}
}