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