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