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