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