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