1 /*
2 * Copyright 2012 The Netty Project
3 *
4 * The Netty Project licenses this file to you under the Apache License,
5 * version 2.0 (the "License"); you may not use this file except in compliance
6 * with the License. You may obtain a copy of the License at:
7 *
8 * https://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 * License for the specific language governing permissions and limitations
14 * under the License.
15 */
16 package io.netty.handler.codec.http;
17
18 import io.netty.buffer.ByteBuf;
19 import io.netty.channel.ChannelHandlerContext;
20 import io.netty.channel.ChannelInboundHandlerAdapter;
21 import io.netty.channel.embedded.EmbeddedChannel;
22 import io.netty.handler.codec.CodecException;
23 import io.netty.handler.codec.DecoderResult;
24 import io.netty.handler.codec.MessageToMessageDecoder;
25 import io.netty.util.ReferenceCountUtil;
26
27 import java.util.List;
28
29 /**
30 * Decodes the content of the received {@link HttpRequest} and {@link HttpContent}.
31 * The original content is replaced with the new content decoded by the
32 * {@link EmbeddedChannel}, which is created by {@link #newContentDecoder(String)}.
33 * Once decoding is finished, the value of the <tt>'Content-Encoding'</tt>
34 * header is set to the target content encoding, as returned by {@link #getTargetContentEncoding(String)}.
35 * Also, the <tt>'Content-Length'</tt> header is updated to the length of the
36 * decoded content. If the content encoding of the original is not supported
37 * by the decoder, {@link #newContentDecoder(String)} should return {@code null}
38 * so that no decoding occurs (i.e. pass-through).
39 * <p>
40 * Please note that this is an abstract class. You have to extend this class
41 * and implement {@link #newContentDecoder(String)} properly to make this class
42 * functional. For example, refer to the source code of {@link HttpContentDecompressor}.
43 * <p>
44 * This handler must be placed after {@link HttpObjectDecoder} in the pipeline
45 * so that this handler can intercept HTTP requests after {@link HttpObjectDecoder}
46 * converts {@link ByteBuf}s into HTTP requests.
47 */
48 public abstract class HttpContentDecoder extends MessageToMessageDecoder<HttpObject> {
49
50 static final String IDENTITY = HttpHeaderValues.IDENTITY.toString();
51
52 protected ChannelHandlerContext ctx;
53 private EmbeddedChannel decoder;
54 private boolean continueResponse;
55 private boolean needRead = true;
56 private ByteBufForwarder forwarder;
57
58 @Override
59 protected void decode(ChannelHandlerContext ctx, HttpObject msg, List<Object> out) throws Exception {
60 needRead = true;
61 if (msg instanceof HttpResponse && ((HttpResponse) msg).status().code() == 100) {
62
63 if (!(msg instanceof LastHttpContent)) {
64 continueResponse = true;
65 }
66 // 100-continue response must be passed through.
67 needRead = false;
68 ctx.fireChannelRead(ReferenceCountUtil.retain(msg));
69 return;
70 }
71
72 if (continueResponse) {
73 if (msg instanceof LastHttpContent) {
74 continueResponse = false;
75 }
76 needRead = false;
77 ctx.fireChannelRead(ReferenceCountUtil.retain(msg));
78 return;
79 }
80
81 if (msg instanceof HttpMessage) {
82 cleanup();
83 final HttpMessage message = (HttpMessage) msg;
84 final HttpHeaders headers = message.headers();
85
86 // Determine the content encoding.
87 String contentEncoding = headers.get(HttpHeaderNames.CONTENT_ENCODING);
88 if (contentEncoding != null) {
89 contentEncoding = contentEncoding.trim();
90 } else {
91 String transferEncoding = headers.get(HttpHeaderNames.TRANSFER_ENCODING);
92 if (transferEncoding != null) {
93 int idx = transferEncoding.indexOf(",");
94 if (idx != -1) {
95 contentEncoding = transferEncoding.substring(0, idx).trim();
96 } else {
97 contentEncoding = transferEncoding.trim();
98 }
99 } else {
100 contentEncoding = IDENTITY;
101 }
102 }
103 decoder = newContentDecoder(contentEncoding);
104
105 if (decoder == null) {
106 if (message instanceof HttpContent) {
107 ((HttpContent) message).retain();
108 }
109 needRead = false;
110 ctx.fireChannelRead(message);
111 return;
112 }
113 decoder.pipeline().addLast(forwarder);
114
115 // Remove content-length header:
116 // the correct value can be set only after all chunks are processed/decoded.
117 // If buffering is not an issue, add HttpObjectAggregator down the chain, it will set the header.
118 // Otherwise, rely on LastHttpContent message.
119 if (headers.contains(HttpHeaderNames.CONTENT_LENGTH)) {
120 headers.remove(HttpHeaderNames.CONTENT_LENGTH);
121 headers.set(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.CHUNKED);
122 }
123 // Either it is already chunked or EOF terminated.
124 // See https://github.com/netty/netty/issues/5892
125
126 // set new content encoding,
127 CharSequence targetContentEncoding = getTargetContentEncoding(contentEncoding);
128 if (HttpHeaderValues.IDENTITY.contentEquals(targetContentEncoding)) {
129 // Do NOT set the 'Content-Encoding' header if the target encoding is 'identity'
130 // as per: https://tools.ietf.org/html/rfc2616#section-14.11
131 headers.remove(HttpHeaderNames.CONTENT_ENCODING);
132 } else {
133 headers.set(HttpHeaderNames.CONTENT_ENCODING, targetContentEncoding);
134 }
135
136 if (message instanceof HttpContent) {
137 // If message is a full request or response object (headers + data), don't copy data part into out.
138 // Output headers only; data part will be decoded below.
139 // Note: "copy" object must not be an instance of LastHttpContent class,
140 // as this would (erroneously) indicate the end of the HttpMessage to other handlers.
141 HttpMessage copy;
142 if (message instanceof HttpRequest) {
143 HttpRequest r = (HttpRequest) message; // HttpRequest or FullHttpRequest
144 copy = new DefaultHttpRequest(r.protocolVersion(), r.method(), r.uri());
145 } else if (message instanceof HttpResponse) {
146 HttpResponse r = (HttpResponse) message; // HttpResponse or FullHttpResponse
147 copy = new DefaultHttpResponse(r.protocolVersion(), r.status());
148 } else {
149 throw new CodecException("Object of class " + message.getClass().getName() +
150 " is not an HttpRequest or HttpResponse");
151 }
152 copy.headers().set(message.headers());
153 copy.setDecoderResult(message.decoderResult());
154 needRead = false;
155 ctx.fireChannelRead(copy);
156 } else {
157 needRead = false;
158 ctx.fireChannelRead(message);
159 }
160 }
161
162 if (msg instanceof HttpContent) {
163 final HttpContent c = (HttpContent) msg;
164 if (decoder == null) {
165 needRead = false;
166 ctx.fireChannelRead(c.retain());
167 } else {
168 // call retain here as it will call release after its written to the channel
169 decoder.writeInbound(c.content().retain());
170
171 if (c instanceof LastHttpContent) {
172 boolean notEmpty = decoder.finish();
173 decoder = null;
174 assert !notEmpty;
175 LastHttpContent last = (LastHttpContent) c;
176 // Generate an additional chunk if the decoder produced
177 // the last product on closure,
178 HttpHeaders headers = last.trailingHeaders();
179 needRead = false;
180 if (headers.isEmpty()) {
181 ctx.fireChannelRead(LastHttpContent.EMPTY_LAST_CONTENT);
182 } else {
183 ctx.fireChannelRead(new ComposedLastHttpContent(headers, DecoderResult.SUCCESS));
184 }
185 }
186 }
187 }
188 }
189
190 @Override
191 public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
192 boolean needRead = this.needRead;
193 this.needRead = true;
194
195 try {
196 ctx.fireChannelReadComplete();
197 } finally {
198 if (needRead && !ctx.channel().config().isAutoRead()) {
199 ctx.read();
200 }
201 }
202 }
203
204 /**
205 * Returns a new {@link EmbeddedChannel} that decodes the HTTP message
206 * content encoded in the specified <tt>contentEncoding</tt>.
207 *
208 * @param contentEncoding the value of the {@code "Content-Encoding"} header
209 * @return a new {@link EmbeddedChannel} if the specified encoding is supported.
210 * {@code null} otherwise (alternatively, you can throw an exception
211 * to block unknown encoding).
212 */
213 protected abstract EmbeddedChannel newContentDecoder(String contentEncoding) throws Exception;
214
215 /**
216 * Returns the expected content encoding of the decoded content.
217 * This getMethod returns {@code "identity"} by default, which is the case for
218 * most decoders.
219 *
220 * @param contentEncoding the value of the {@code "Content-Encoding"} header
221 * @return the expected content encoding of the new content
222 */
223 protected String getTargetContentEncoding(
224 @SuppressWarnings("UnusedParameters") String contentEncoding) throws Exception {
225 return IDENTITY;
226 }
227
228 @Override
229 public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
230 cleanupSafely(ctx);
231 super.handlerRemoved(ctx);
232 }
233
234 @Override
235 public void channelInactive(ChannelHandlerContext ctx) throws Exception {
236 cleanupSafely(ctx);
237 super.channelInactive(ctx);
238 }
239
240 @Override
241 public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
242 this.ctx = ctx;
243 forwarder = new ByteBufForwarder(ctx);
244 super.handlerAdded(ctx);
245 }
246
247 private void cleanup() {
248 if (decoder != null) {
249 // Clean-up the previous decoder if not cleaned up correctly.
250 decoder.finishAndReleaseAll();
251 decoder = null;
252 }
253 }
254
255 private void cleanupSafely(ChannelHandlerContext ctx) {
256 try {
257 cleanup();
258 } catch (Throwable cause) {
259 // If cleanup throws any error we need to propagate it through the pipeline
260 // so we don't fail to propagate pipeline events.
261 ctx.fireExceptionCaught(cause);
262 }
263 }
264
265 private final class ByteBufForwarder extends ChannelInboundHandlerAdapter {
266
267 private final ChannelHandlerContext targetCtx;
268
269 ByteBufForwarder(ChannelHandlerContext targetCtx) {
270 this.targetCtx = targetCtx;
271 }
272
273 @Override
274 public boolean isSharable() {
275 // We need to mark the handler as sharable as we will add it to every EmbeddedChannel that is
276 // generated.
277 return true;
278 }
279
280 @Override
281 public void channelRead(ChannelHandlerContext ctx, Object msg) {
282 ByteBuf buf = (ByteBuf) msg;
283 if (!buf.isReadable()) {
284 buf.release();
285 return;
286 }
287 needRead = false;
288 targetCtx.fireChannelRead(new DefaultHttpContent(buf));
289 }
290 }
291 }