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