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      private static final String IDENTITY = HttpHeaderValues.IDENTITY.toString();
49  
50      private EmbeddedChannel decoder;
51      private boolean continueResponse;
52  
53      @Override
54      protected void decode(ChannelHandlerContext ctx, HttpObject msg, List<Object> out) throws Exception {
55          if (msg instanceof HttpResponse && ((HttpResponse) msg).status().code() == 100) {
56  
57              if (!(msg instanceof LastHttpContent)) {
58                  continueResponse = true;
59              }
60              // 100-continue response must be passed through.
61              out.add(ReferenceCountUtil.retain(msg));
62              return;
63          }
64  
65          if (continueResponse) {
66              if (msg instanceof LastHttpContent) {
67                  continueResponse = false;
68              }
69              // 100-continue response must be passed through.
70              out.add(ReferenceCountUtil.retain(msg));
71              return;
72          }
73  
74          if (msg instanceof HttpMessage) {
75              cleanup();
76              final HttpMessage message = (HttpMessage) msg;
77              final HttpHeaders headers = message.headers();
78  
79              // Determine the content encoding.
80              String contentEncoding = headers.getAndConvert(HttpHeaderNames.CONTENT_ENCODING);
81              if (contentEncoding != null) {
82                  contentEncoding = contentEncoding.trim();
83              } else {
84                  contentEncoding = IDENTITY;
85              }
86              decoder = newContentDecoder(contentEncoding);
87  
88              if (decoder == null) {
89                  if (message instanceof HttpContent) {
90                      ((HttpContent) message).retain();
91                  }
92                  out.add(message);
93                  return;
94              }
95  
96              // Remove content-length header:
97              // the correct value can be set only after all chunks are processed/decoded.
98              // If buffering is not an issue, add HttpObjectAggregator down the chain, it will set the header.
99              // Otherwise, rely on LastHttpContent message.
100             headers.remove(HttpHeaderNames.CONTENT_LENGTH);
101 
102             // set new content encoding,
103             CharSequence targetContentEncoding = getTargetContentEncoding(contentEncoding);
104             if (HttpHeaderValues.IDENTITY.equals(targetContentEncoding)) {
105                 // Do NOT set the 'Content-Encoding' header if the target encoding is 'identity'
106                 // as per: http://tools.ietf.org/html/rfc2616#section-14.11
107                 headers.remove(HttpHeaderNames.CONTENT_ENCODING);
108             } else {
109                 headers.set(HttpHeaderNames.CONTENT_ENCODING, targetContentEncoding);
110             }
111 
112             if (message instanceof HttpContent) {
113                 // If message is a full request or response object (headers + data), don't copy data part into out.
114                 // Output headers only; data part will be decoded below.
115                 // Note: "copy" object must not be an instance of LastHttpContent class,
116                 // as this would (erroneously) indicate the end of the HttpMessage to other handlers.
117                 HttpMessage copy;
118                 if (message instanceof HttpRequest) {
119                     HttpRequest r = (HttpRequest) message; // HttpRequest or FullHttpRequest
120                     copy = new DefaultHttpRequest(r.protocolVersion(), r.method(), r.uri());
121                 } else if (message instanceof HttpResponse) {
122                     HttpResponse r = (HttpResponse) message; // HttpResponse or FullHttpResponse
123                     copy = new DefaultHttpResponse(r.protocolVersion(), r.status());
124                 } else {
125                     throw new CodecException("Object of class " + message.getClass().getName() +
126                                              " is not a HttpRequest or HttpResponse");
127                 }
128                 copy.headers().set(message.headers());
129                 copy.setDecoderResult(message.decoderResult());
130                 out.add(copy);
131             } else {
132                 out.add(message);
133             }
134         }
135 
136         if (msg instanceof HttpContent) {
137             final HttpContent c = (HttpContent) msg;
138             if (decoder == null) {
139                 out.add(c.retain());
140             } else {
141                 decodeContent(c, out);
142             }
143         }
144     }
145 
146     private void decodeContent(HttpContent c, List<Object> out) {
147         ByteBuf content = c.content();
148 
149         decode(content, out);
150 
151         if (c instanceof LastHttpContent) {
152             finishDecode(out);
153 
154             LastHttpContent last = (LastHttpContent) c;
155             // Generate an additional chunk if the decoder produced
156             // the last product on closure,
157             HttpHeaders headers = last.trailingHeaders();
158             if (headers.isEmpty()) {
159                 out.add(LastHttpContent.EMPTY_LAST_CONTENT);
160             } else {
161                 out.add(new ComposedLastHttpContent(headers));
162             }
163         }
164     }
165 
166     /**
167      * Returns a new {@link EmbeddedChannel} that decodes the HTTP message
168      * content encoded in the specified <tt>contentEncoding</tt>.
169      *
170      * @param contentEncoding the value of the {@code "Content-Encoding"} header
171      * @return a new {@link EmbeddedChannel} if the specified encoding is supported.
172      *         {@code null} otherwise (alternatively, you can throw an exception
173      *         to block unknown encoding).
174      */
175     protected abstract EmbeddedChannel newContentDecoder(String contentEncoding) throws Exception;
176 
177     /**
178      * Returns the expected content encoding of the decoded content.
179      * This getMethod returns {@code "identity"} by default, which is the case for
180      * most decoders.
181      *
182      * @param contentEncoding the value of the {@code "Content-Encoding"} header
183      * @return the expected content encoding of the new content
184      */
185     protected CharSequence getTargetContentEncoding(
186             @SuppressWarnings("UnusedParameters") String contentEncoding) throws Exception {
187         return HttpHeaderValues.IDENTITY;
188     }
189 
190     @Override
191     public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
192         cleanup();
193         super.handlerRemoved(ctx);
194     }
195 
196     @Override
197     public void channelInactive(ChannelHandlerContext ctx) throws Exception {
198         cleanup();
199         super.channelInactive(ctx);
200     }
201 
202     private void cleanup() {
203         if (decoder != null) {
204             // Clean-up the previous decoder if not cleaned up correctly.
205             if (decoder.finish()) {
206                 for (;;) {
207                     ByteBuf buf = decoder.readInbound();
208                     if (buf == null) {
209                         break;
210                     }
211                     // Release the buffer
212                     buf.release();
213                 }
214             }
215             decoder = null;
216         }
217     }
218 
219     private void decode(ByteBuf in, List<Object> out) {
220         // call retain here as it will call release after its written to the channel
221         decoder.writeInbound(in.retain());
222         fetchDecoderOutput(out);
223     }
224 
225     private void finishDecode(List<Object> out) {
226         if (decoder.finish()) {
227             fetchDecoderOutput(out);
228         }
229         decoder = null;
230     }
231 
232     private void fetchDecoderOutput(List<Object> out) {
233         for (;;) {
234             ByteBuf buf = decoder.readInbound();
235             if (buf == null) {
236                 break;
237             }
238             if (!buf.isReadable()) {
239                 buf.release();
240                 continue;
241             }
242             out.add(new DefaultHttpContent(buf));
243         }
244     }
245 }