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