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