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