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    *   http://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.buffer.ByteBufHolder;
20  import io.netty.channel.ChannelHandlerContext;
21  import io.netty.channel.embedded.EmbeddedChannel;
22  import io.netty.handler.codec.MessageToMessageCodec;
23  import io.netty.util.ReferenceCountUtil;
24  
25  import java.util.ArrayDeque;
26  import java.util.List;
27  import java.util.Queue;
28  
29  /**
30   * Encodes the content of the outbound {@link HttpResponse} and {@link HttpContent}.
31   * The original content is replaced with the new content encoded by the
32   * {@link EmbeddedChannel}, which is created by {@link #beginEncode(HttpResponse, String)}.
33   * Once encoding is finished, the value of the <tt>'Content-Encoding'</tt> header
34   * is set to the target content encoding, as returned by
35   * {@link #beginEncode(HttpResponse, String)}.
36   * Also, the <tt>'Content-Length'</tt> header is updated to the length of the
37   * encoded content.  If there is no supported or allowed encoding in the
38   * corresponding {@link HttpRequest}'s {@code "Accept-Encoding"} header,
39   * {@link #beginEncode(HttpResponse, String)} should return {@code null} so that
40   * no encoding occurs (i.e. pass-through).
41   * <p>
42   * Please note that this is an abstract class.  You have to extend this class
43   * and implement {@link #beginEncode(HttpResponse, String)} properly to make
44   * this class functional.  For example, refer to the source code of
45   * {@link HttpContentCompressor}.
46   * <p>
47   * This handler must be placed after {@link HttpObjectEncoder} in the pipeline
48   * so that this handler can intercept HTTP responses before {@link HttpObjectEncoder}
49   * converts them into {@link ByteBuf}s.
50   */
51  public abstract class HttpContentEncoder extends MessageToMessageCodec<HttpRequest, HttpObject> {
52  
53      private enum State {
54          PASS_THROUGH,
55          AWAIT_HEADERS,
56          AWAIT_CONTENT
57      }
58  
59      private static final CharSequence ZERO_LENGTH_HEAD = "HEAD";
60      private static final CharSequence ZERO_LENGTH_CONNECT = "CONNECT";
61      private static final int CONTINUE_CODE = HttpResponseStatus.CONTINUE.code();
62  
63      private final Queue<CharSequence> acceptEncodingQueue = new ArrayDeque<CharSequence>();
64      private EmbeddedChannel encoder;
65      private State state = State.AWAIT_HEADERS;
66  
67      @Override
68      public boolean acceptOutboundMessage(Object msg) throws Exception {
69          return msg instanceof HttpContent || msg instanceof HttpResponse;
70      }
71  
72      @Override
73      protected void decode(ChannelHandlerContext ctx, HttpRequest msg, List<Object> out)
74              throws Exception {
75          CharSequence acceptedEncoding = msg.headers().get(HttpHeaderNames.ACCEPT_ENCODING);
76          if (acceptedEncoding == null) {
77              acceptedEncoding = HttpContentDecoder.IDENTITY;
78          }
79  
80          HttpMethod meth = msg.method();
81          if (meth == HttpMethod.HEAD) {
82              acceptedEncoding = ZERO_LENGTH_HEAD;
83          } else if (meth == HttpMethod.CONNECT) {
84              acceptedEncoding = ZERO_LENGTH_CONNECT;
85          }
86  
87          acceptEncodingQueue.add(acceptedEncoding);
88          out.add(ReferenceCountUtil.retain(msg));
89      }
90  
91      @Override
92      protected void encode(ChannelHandlerContext ctx, HttpObject msg, List<Object> out) throws Exception {
93          final boolean isFull = msg instanceof HttpResponse && msg instanceof LastHttpContent;
94          switch (state) {
95              case AWAIT_HEADERS: {
96                  ensureHeaders(msg);
97                  assert encoder == null;
98  
99                  final HttpResponse res = (HttpResponse) msg;
100                 final int code = res.status().code();
101                 final CharSequence acceptEncoding;
102                 if (code == CONTINUE_CODE) {
103                     // We need to not poll the encoding when response with CONTINUE as another response will follow
104                     // for the issued request. See https://github.com/netty/netty/issues/4079
105                     acceptEncoding = null;
106                 } else {
107                     // Get the list of encodings accepted by the peer.
108                     acceptEncoding = acceptEncodingQueue.poll();
109                     if (acceptEncoding == null) {
110                         throw new IllegalStateException("cannot send more responses than requests");
111                     }
112                 }
113 
114                 /*
115                  * per rfc2616 4.3 Message Body
116                  * All 1xx (informational), 204 (no content), and 304 (not modified) responses MUST NOT include a
117                  * message-body. All other responses do include a message-body, although it MAY be of zero length.
118                  *
119                  * 9.4 HEAD
120                  * The HEAD method is identical to GET except that the server MUST NOT return a message-body
121                  * in the response.
122                  *
123                  * Also we should pass through HTTP/1.0 as transfer-encoding: chunked is not supported.
124                  *
125                  * See https://github.com/netty/netty/issues/5382
126                  */
127                 if (isPassthru(res.protocolVersion(), code, acceptEncoding)) {
128                     if (isFull) {
129                         out.add(ReferenceCountUtil.retain(res));
130                     } else {
131                         out.add(res);
132                         // Pass through all following contents.
133                         state = State.PASS_THROUGH;
134                     }
135                     break;
136                 }
137 
138                 if (isFull) {
139                     // Pass through the full response with empty content and continue waiting for the the next resp.
140                     if (!((ByteBufHolder) res).content().isReadable()) {
141                         out.add(ReferenceCountUtil.retain(res));
142                         break;
143                     }
144                 }
145 
146                 // Prepare to encode the content.
147                 final Result result = beginEncode(res, acceptEncoding.toString());
148 
149                 // If unable to encode, pass through.
150                 if (result == null) {
151                     if (isFull) {
152                         out.add(ReferenceCountUtil.retain(res));
153                     } else {
154                         out.add(res);
155                         // Pass through all following contents.
156                         state = State.PASS_THROUGH;
157                     }
158                     break;
159                 }
160 
161                 encoder = result.contentEncoder();
162 
163                 // Encode the content and remove or replace the existing headers
164                 // so that the message looks like a decoded message.
165                 res.headers().set(HttpHeaderNames.CONTENT_ENCODING, result.targetContentEncoding());
166 
167                 // Output the rewritten response.
168                 if (isFull) {
169                     // Convert full message into unfull one.
170                     HttpResponse newRes = new DefaultHttpResponse(res.protocolVersion(), res.status());
171                     newRes.headers().set(res.headers());
172                     out.add(newRes);
173 
174                     ensureContent(res);
175                     encodeFullResponse(newRes, (HttpContent) res, out);
176                     break;
177                 } else {
178                     // Make the response chunked to simplify content transformation.
179                     res.headers().remove(HttpHeaderNames.CONTENT_LENGTH);
180                     res.headers().set(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.CHUNKED);
181 
182                     out.add(res);
183                     state = State.AWAIT_CONTENT;
184                     if (!(msg instanceof HttpContent)) {
185                         // only break out the switch statement if we have not content to process
186                         // See https://github.com/netty/netty/issues/2006
187                         break;
188                     }
189                     // Fall through to encode the content
190                 }
191             }
192             case AWAIT_CONTENT: {
193                 ensureContent(msg);
194                 if (encodeContent((HttpContent) msg, out)) {
195                     state = State.AWAIT_HEADERS;
196                 }
197                 break;
198             }
199             case PASS_THROUGH: {
200                 ensureContent(msg);
201                 out.add(ReferenceCountUtil.retain(msg));
202                 // Passed through all following contents of the current response.
203                 if (msg instanceof LastHttpContent) {
204                     state = State.AWAIT_HEADERS;
205                 }
206                 break;
207             }
208         }
209     }
210 
211     private void encodeFullResponse(HttpResponse newRes, HttpContent content, List<Object> out) {
212         int existingMessages = out.size();
213         encodeContent(content, out);
214 
215         if (HttpUtil.isContentLengthSet(newRes)) {
216             // adjust the content-length header
217             int messageSize = 0;
218             for (int i = existingMessages; i < out.size(); i++) {
219                 Object item = out.get(i);
220                 if (item instanceof HttpContent) {
221                     messageSize += ((HttpContent) item).content().readableBytes();
222                 }
223             }
224             HttpUtil.setContentLength(newRes, messageSize);
225         } else {
226             newRes.headers().set(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.CHUNKED);
227         }
228     }
229 
230     private static boolean isPassthru(HttpVersion version, int code, CharSequence httpMethod) {
231         return code < 200 || code == 204 || code == 304 ||
232                (httpMethod == ZERO_LENGTH_HEAD || (httpMethod == ZERO_LENGTH_CONNECT && code == 200)) ||
233                 version == HttpVersion.HTTP_1_0;
234     }
235 
236     private static void ensureHeaders(HttpObject msg) {
237         if (!(msg instanceof HttpResponse)) {
238             throw new IllegalStateException(
239                     "unexpected message type: " +
240                     msg.getClass().getName() + " (expected: " + HttpResponse.class.getSimpleName() + ')');
241         }
242     }
243 
244     private static void ensureContent(HttpObject msg) {
245         if (!(msg instanceof HttpContent)) {
246             throw new IllegalStateException(
247                     "unexpected message type: " +
248                     msg.getClass().getName() + " (expected: " + HttpContent.class.getSimpleName() + ')');
249         }
250     }
251 
252     private boolean encodeContent(HttpContent c, List<Object> out) {
253         ByteBuf content = c.content();
254 
255         encode(content, out);
256 
257         if (c instanceof LastHttpContent) {
258             finishEncode(out);
259             LastHttpContent last = (LastHttpContent) c;
260 
261             // Generate an additional chunk if the decoder produced
262             // the last product on closure,
263             HttpHeaders headers = last.trailingHeaders();
264             if (headers.isEmpty()) {
265                 out.add(LastHttpContent.EMPTY_LAST_CONTENT);
266             } else {
267                 out.add(new ComposedLastHttpContent(headers));
268             }
269             return true;
270         }
271         return false;
272     }
273 
274     /**
275      * Prepare to encode the HTTP message content.
276      *
277      * @param headers
278      *        the headers
279      * @param acceptEncoding
280      *        the value of the {@code "Accept-Encoding"} header
281      *
282      * @return the result of preparation, which is composed of the determined
283      *         target content encoding and a new {@link EmbeddedChannel} that
284      *         encodes the content into the target content encoding.
285      *         {@code null} if {@code acceptEncoding} is unsupported or rejected
286      *         and thus the content should be handled as-is (i.e. no encoding).
287      */
288     protected abstract Result beginEncode(HttpResponse headers, String acceptEncoding) throws Exception;
289 
290     @Override
291     public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
292         cleanupSafely(ctx);
293         super.handlerRemoved(ctx);
294     }
295 
296     @Override
297     public void channelInactive(ChannelHandlerContext ctx) throws Exception {
298         cleanupSafely(ctx);
299         super.channelInactive(ctx);
300     }
301 
302     private void cleanup() {
303         if (encoder != null) {
304             // Clean-up the previous encoder if not cleaned up correctly.
305             encoder.finishAndReleaseAll();
306             encoder = null;
307         }
308     }
309 
310     private void cleanupSafely(ChannelHandlerContext ctx) {
311         try {
312             cleanup();
313         } catch (Throwable cause) {
314             // If cleanup throws any error we need to propagate it through the pipeline
315             // so we don't fail to propagate pipeline events.
316             ctx.fireExceptionCaught(cause);
317         }
318     }
319 
320     private void encode(ByteBuf in, List<Object> out) {
321         // call retain here as it will call release after its written to the channel
322         encoder.writeOutbound(in.retain());
323         fetchEncoderOutput(out);
324     }
325 
326     private void finishEncode(List<Object> out) {
327         if (encoder.finish()) {
328             fetchEncoderOutput(out);
329         }
330         encoder = null;
331     }
332 
333     private void fetchEncoderOutput(List<Object> out) {
334         for (;;) {
335             ByteBuf buf = encoder.readOutbound();
336             if (buf == null) {
337                 break;
338             }
339             if (!buf.isReadable()) {
340                 buf.release();
341                 continue;
342             }
343             out.add(new DefaultHttpContent(buf));
344         }
345     }
346 
347     public static final class Result {
348         private final String targetContentEncoding;
349         private final EmbeddedChannel contentEncoder;
350 
351         public Result(String targetContentEncoding, EmbeddedChannel contentEncoder) {
352             if (targetContentEncoding == null) {
353                 throw new NullPointerException("targetContentEncoding");
354             }
355             if (contentEncoder == null) {
356                 throw new NullPointerException("contentEncoder");
357             }
358 
359             this.targetContentEncoding = targetContentEncoding;
360             this.contentEncoder = contentEncoder;
361         }
362 
363         public String targetContentEncoding() {
364             return targetContentEncoding;
365         }
366 
367         public EmbeddedChannel contentEncoder() {
368             return contentEncoder;
369         }
370     }
371 }