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, CharSequence)}.
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, CharSequence)}.
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, CharSequence)} 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, CharSequence)} 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 final Queue<CharSequence> acceptEncodingQueue = new ArrayDeque<CharSequence>();
60      private CharSequence acceptEncoding;
61      private EmbeddedChannel encoder;
62      private State state = State.AWAIT_HEADERS;
63  
64      @Override
65      public boolean acceptOutboundMessage(Object msg) throws Exception {
66          return msg instanceof HttpContent || msg instanceof HttpResponse;
67      }
68  
69      @Override
70      protected void decode(ChannelHandlerContext ctx, HttpRequest msg, List<Object> out)
71              throws Exception {
72          CharSequence acceptedEncoding = msg.headers().get(HttpHeaderNames.ACCEPT_ENCODING);
73          if (acceptedEncoding == null) {
74              acceptedEncoding = HttpHeaderValues.IDENTITY;
75          }
76          acceptEncodingQueue.add(acceptedEncoding);
77          out.add(ReferenceCountUtil.retain(msg));
78      }
79  
80      @Override
81      protected void encode(ChannelHandlerContext ctx, HttpObject msg, List<Object> out) throws Exception {
82          final boolean isFull = msg instanceof HttpResponse && msg instanceof LastHttpContent;
83          switch (state) {
84              case AWAIT_HEADERS: {
85                  ensureHeaders(msg);
86                  assert encoder == null;
87  
88                  final HttpResponse res = (HttpResponse) msg;
89  
90                  /*
91                   * per rfc2616 4.3 Message Body
92                   * All 1xx (informational), 204 (no content), and 304 (not modified) responses MUST NOT include a
93                   * message-body. All other responses do include a message-body, although it MAY be of zero length.
94                   */
95                  if (isPassthru(res)) {
96                      if (isFull) {
97                          out.add(ReferenceCountUtil.retain(res));
98                      } else {
99                          out.add(res);
100                         // Pass through all following contents.
101                         state = State.PASS_THROUGH;
102                     }
103                     break;
104                 }
105 
106                 // Get the list of encodings accepted by the peer.
107                 acceptEncoding = acceptEncodingQueue.poll();
108                 if (acceptEncoding == null) {
109                     throw new IllegalStateException("cannot send more responses than requests");
110                 }
111 
112                 if (isFull) {
113                     // Pass through the full response with empty content and continue waiting for the the next resp.
114                     if (!((ByteBufHolder) res).content().isReadable()) {
115                         out.add(ReferenceCountUtil.retain(res));
116                         break;
117                     }
118                 }
119 
120                 // Prepare to encode the content.
121                 final Result result = beginEncode(res, acceptEncoding);
122 
123                 // If unable to encode, pass through.
124                 if (result == null) {
125                     if (isFull) {
126                         out.add(ReferenceCountUtil.retain(res));
127                     } else {
128                         out.add(res);
129                         // Pass through all following contents.
130                         state = State.PASS_THROUGH;
131                     }
132                     break;
133                 }
134 
135                 encoder = result.contentEncoder();
136 
137                 // Encode the content and remove or replace the existing headers
138                 // so that the message looks like a decoded message.
139                 res.headers().set(HttpHeaderNames.CONTENT_ENCODING, result.targetContentEncoding());
140 
141                 // Make the response chunked to simplify content transformation.
142                 res.headers().remove(HttpHeaderNames.CONTENT_LENGTH);
143                 res.headers().set(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.CHUNKED);
144 
145                 // Output the rewritten response.
146                 if (isFull) {
147                     // Convert full message into unfull one.
148                     HttpResponse newRes = new DefaultHttpResponse(res.protocolVersion(), res.status());
149                     newRes.headers().set(res.headers());
150                     out.add(newRes);
151                     // Fall through to encode the content of the full response.
152                 } else {
153                     out.add(res);
154                     state = State.AWAIT_CONTENT;
155                     if (!(msg instanceof HttpContent)) {
156                         // only break out the switch statement if we have not content to process
157                         // See https://github.com/netty/netty/issues/2006
158                         break;
159                     }
160                     // Fall through to encode the content
161                 }
162             }
163             case AWAIT_CONTENT: {
164                 ensureContent(msg);
165                 if (encodeContent((HttpContent) msg, out)) {
166                     state = State.AWAIT_HEADERS;
167                 }
168                 break;
169             }
170             case PASS_THROUGH: {
171                 ensureContent(msg);
172                 out.add(ReferenceCountUtil.retain(msg));
173                 // Passed through all following contents of the current response.
174                 if (msg instanceof LastHttpContent) {
175                     state = State.AWAIT_HEADERS;
176                 }
177                 break;
178             }
179         }
180     }
181 
182     private static boolean isPassthru(HttpResponse res) {
183         final int code = res.status().code();
184         return code < 200 || code == 204 || code == 304;
185     }
186 
187     private static void ensureHeaders(HttpObject msg) {
188         if (!(msg instanceof HttpResponse)) {
189             throw new IllegalStateException(
190                     "unexpected message type: " +
191                     msg.getClass().getName() + " (expected: " + HttpResponse.class.getSimpleName() + ')');
192         }
193     }
194 
195     private static void ensureContent(HttpObject msg) {
196         if (!(msg instanceof HttpContent)) {
197             throw new IllegalStateException(
198                     "unexpected message type: " +
199                     msg.getClass().getName() + " (expected: " + HttpContent.class.getSimpleName() + ')');
200         }
201     }
202 
203     private boolean encodeContent(HttpContent c, List<Object> out) {
204         ByteBuf content = c.content();
205 
206         encode(content, out);
207 
208         if (c instanceof LastHttpContent) {
209             finishEncode(out);
210             LastHttpContent last = (LastHttpContent) c;
211 
212             // Generate an additional chunk if the decoder produced
213             // the last product on closure,
214             HttpHeaders headers = last.trailingHeaders();
215             if (headers.isEmpty()) {
216                 out.add(LastHttpContent.EMPTY_LAST_CONTENT);
217             } else {
218                 out.add(new ComposedLastHttpContent(headers));
219             }
220             return true;
221         }
222         return false;
223     }
224 
225     /**
226      * Prepare to encode the HTTP message content.
227      *
228      * @param headers
229      *        the headers
230      * @param acceptEncoding
231      *        the value of the {@code "Accept-Encoding"} header
232      *
233      * @return the result of preparation, which is composed of the determined
234      *         target content encoding and a new {@link EmbeddedChannel} that
235      *         encodes the content into the target content encoding.
236      *         {@code null} if {@code acceptEncoding} is unsupported or rejected
237      *         and thus the content should be handled as-is (i.e. no encoding).
238      */
239     protected abstract Result beginEncode(HttpResponse headers, CharSequence acceptEncoding) throws Exception;
240 
241     @Override
242     public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
243         cleanup();
244         super.handlerRemoved(ctx);
245     }
246 
247     @Override
248     public void channelInactive(ChannelHandlerContext ctx) throws Exception {
249         cleanup();
250         super.channelInactive(ctx);
251     }
252 
253     private void cleanup() {
254         if (encoder != null) {
255             // Clean-up the previous encoder if not cleaned up correctly.
256             if (encoder.finish()) {
257                 for (;;) {
258                     ByteBuf buf = encoder.readOutbound();
259                     if (buf == null) {
260                         break;
261                     }
262                     // Release the buffer
263                     // https://github.com/netty/netty/issues/1524
264                     buf.release();
265                 }
266             }
267             encoder = null;
268         }
269     }
270 
271     private void encode(ByteBuf in, List<Object> out) {
272         // call retain here as it will call release after its written to the channel
273         encoder.writeOutbound(in.retain());
274         fetchEncoderOutput(out);
275     }
276 
277     private void finishEncode(List<Object> out) {
278         if (encoder.finish()) {
279             fetchEncoderOutput(out);
280         }
281         encoder = null;
282     }
283 
284     private void fetchEncoderOutput(List<Object> out) {
285         for (;;) {
286             ByteBuf buf = encoder.readOutbound();
287             if (buf == null) {
288                 break;
289             }
290             if (!buf.isReadable()) {
291                 buf.release();
292                 continue;
293             }
294             out.add(new DefaultHttpContent(buf));
295         }
296     }
297 
298     public static final class Result {
299         private final String targetContentEncoding;
300         private final EmbeddedChannel contentEncoder;
301 
302         public Result(String targetContentEncoding, EmbeddedChannel contentEncoder) {
303             if (targetContentEncoding == null) {
304                 throw new NullPointerException("targetContentEncoding");
305             }
306             if (contentEncoder == null) {
307                 throw new NullPointerException("contentEncoder");
308             }
309 
310             this.targetContentEncoding = targetContentEncoding;
311             this.contentEncoder = contentEncoder;
312         }
313 
314         public String targetContentEncoding() {
315             return targetContentEncoding;
316         }
317 
318         public EmbeddedChannel contentEncoder() {
319             return contentEncoder;
320         }
321     }
322 }