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    *   https://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: https://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 }