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