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 [email protected] HttpResponse} and [email protected] HttpContent}.
33   * The original content is replaced with the new content encoded by the
34   * [email protected] EmbeddedChannel}, which is created by [email protected] #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   * [email protected] #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 [email protected] HttpRequest}'s [email protected] "Accept-Encoding"} header,
41   * [email protected] #beginEncode(HttpResponse, String)} should return [email protected] 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 [email protected] #beginEncode(HttpResponse, String)} properly to make
46   * this class functional.  For example, refer to the source code of
47   * [email protected] HttpContentCompressor}.
48   * <p>
49   * This handler must be placed after [email protected] HttpObjectEncoder} in the pipeline
50   * so that this handler can intercept HTTP responses before [email protected] HttpObjectEncoder}
51   * converts them into [email protected] 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 CharSequence acceptEncoding;
67      private EmbeddedChannel encoder;
68      private State state = State.AWAIT_HEADERS;
69  
70      @Override
71      public boolean acceptOutboundMessage(Object msg) throws Exception {
72          return msg instanceof HttpContent || msg instanceof HttpResponse;
73      }
74  
75      @Override
76      protected void decode(ChannelHandlerContext ctx, HttpRequest msg, List<Object> out)
77              throws Exception {
78          CharSequence acceptedEncoding = msg.headers().get(HttpHeaders.Names.ACCEPT_ENCODING);
79          if (acceptedEncoding == null) {
80              acceptedEncoding = HttpHeaders.Values.IDENTITY;
81          }
82  
83          HttpMethod meth = msg.getMethod();
84          if (meth == HttpMethod.HEAD) {
85              acceptedEncoding = ZERO_LENGTH_HEAD;
86          } else if (meth == HttpMethod.CONNECT) {
87              acceptedEncoding = ZERO_LENGTH_CONNECT;
88          }
89  
90          acceptEncodingQueue.add(acceptedEncoding);
91          out.add(ReferenceCountUtil.retain(msg));
92      }
93  
94      @Override
95      protected void encode(ChannelHandlerContext ctx, HttpObject msg, List<Object> out) throws Exception {
96          final boolean isFull = msg instanceof HttpResponse && msg instanceof LastHttpContent;
97          switch (state) {
98              case AWAIT_HEADERS: {
99                  ensureHeaders(msg);
100                 assert encoder == null;
101 
102                 final HttpResponse res = (HttpResponse) msg;
103                 final int code = res.getStatus().code();
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                  * This code is now inline with HttpClientDecoder.Decoder
126                  */
127                 if (isPassthru(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(Names.CONTENT_ENCODING, result.targetContentEncoding());
166 
167                 // Make the response chunked to simplify content transformation.
168                 res.headers().remove(Names.CONTENT_LENGTH);
169                 res.headers().set(Names.TRANSFER_ENCODING, Values.CHUNKED);
170 
171                 // Output the rewritten response.
172                 if (isFull) {
173                     // Convert full message into unfull one.
174                     HttpResponse newRes = new DefaultHttpResponse(res.getProtocolVersion(), res.getStatus());
175                     newRes.headers().set(res.headers());
176                     out.add(newRes);
177                     // Fall through to encode the content of the full response.
178                 } else {
179                     out.add(res);
180                     state = State.AWAIT_CONTENT;
181                     if (!(msg instanceof HttpContent)) {
182                         // only break out the switch statement if we have not content to process
183                         // See https://github.com/netty/netty/issues/2006
184                         break;
185                     }
186                     // Fall through to encode the content
187                 }
188             }
189             case AWAIT_CONTENT: {
190                 ensureContent(msg);
191                 if (encodeContent((HttpContent) msg, out)) {
192                     state = State.AWAIT_HEADERS;
193                 }
194                 break;
195             }
196             case PASS_THROUGH: {
197                 ensureContent(msg);
198                 out.add(ReferenceCountUtil.retain(msg));
199                 // Passed through all following contents of the current response.
200                 if (msg instanceof LastHttpContent) {
201                     state = State.AWAIT_HEADERS;
202                 }
203                 break;
204             }
205         }
206     }
207 
208     private static boolean isPassthru(int code, CharSequence httpMethod) {
209         return code < 200 || code == 204 || code == 304 ||
210                (httpMethod == ZERO_LENGTH_HEAD || (httpMethod == ZERO_LENGTH_CONNECT && code == 200));
211     }
212 
213     private static void ensureHeaders(HttpObject msg) {
214         if (!(msg instanceof HttpResponse)) {
215             throw new IllegalStateException(
216                     "unexpected message type: " +
217                     msg.getClass().getName() + " (expected: " + HttpResponse.class.getSimpleName() + ')');
218         }
219     }
220 
221     private static void ensureContent(HttpObject msg) {
222         if (!(msg instanceof HttpContent)) {
223             throw new IllegalStateException(
224                     "unexpected message type: " +
225                     msg.getClass().getName() + " (expected: " + HttpContent.class.getSimpleName() + ')');
226         }
227     }
228 
229     private boolean encodeContent(HttpContent c, List<Object> out) {
230         ByteBuf content = c.content();
231 
232         encode(content, out);
233 
234         if (c instanceof LastHttpContent) {
235             finishEncode(out);
236             LastHttpContent last = (LastHttpContent) c;
237 
238             // Generate an additional chunk if the decoder produced
239             // the last product on closure,
240             HttpHeaders headers = last.trailingHeaders();
241             if (headers.isEmpty()) {
242                 out.add(LastHttpContent.EMPTY_LAST_CONTENT);
243             } else {
244                 out.add(new ComposedLastHttpContent(headers));
245             }
246             return true;
247         }
248         return false;
249     }
250 
251     /**
252      * Prepare to encode the HTTP message content.
253      *
254      * @param headers
255      *        the headers
256      * @param acceptEncoding
257      *        the value of the [email protected] "Accept-Encoding"} header
258      *
259      * @return the result of preparation, which is composed of the determined
260      *         target content encoding and a new [email protected] EmbeddedChannel} that
261      *         encodes the content into the target content encoding.
262      *         [email protected] null} if [email protected] acceptEncoding} is unsupported or rejected
263      *         and thus the content should be handled as-is (i.e. no encoding).
264      */
265     protected abstract Result beginEncode(HttpResponse headers, String acceptEncoding) throws Exception;
266 
267     @Override
268     public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
269         cleanup();
270         super.handlerRemoved(ctx);
271     }
272 
273     @Override
274     public void channelInactive(ChannelHandlerContext ctx) throws Exception {
275         cleanup();
276         super.channelInactive(ctx);
277     }
278 
279     private void cleanup() {
280         if (encoder != null) {
281             // Clean-up the previous encoder if not cleaned up correctly.
282             if (encoder.finish()) {
283                 for (;;) {
284                     ByteBuf buf = (ByteBuf) encoder.readOutbound();
285                     if (buf == null) {
286                         break;
287                     }
288                     // Release the buffer
289                     // https://github.com/netty/netty/issues/1524
290                     buf.release();
291                 }
292             }
293             encoder = null;
294         }
295     }
296 
297     private void encode(ByteBuf in, List<Object> out) {
298         // call retain here as it will call release after its written to the channel
299         encoder.writeOutbound(in.retain());
300         fetchEncoderOutput(out);
301     }
302 
303     private void finishEncode(List<Object> out) {
304         if (encoder.finish()) {
305             fetchEncoderOutput(out);
306         }
307         encoder = null;
308     }
309 
310     private void fetchEncoderOutput(List<Object> out) {
311         for (;;) {
312             ByteBuf buf = (ByteBuf) encoder.readOutbound();
313             if (buf == null) {
314                 break;
315             }
316             if (!buf.isReadable()) {
317                 buf.release();
318                 continue;
319             }
320             out.add(new DefaultHttpContent(buf));
321         }
322     }
323 
324     public static final class Result {
325         private final String targetContentEncoding;
326         private final EmbeddedChannel contentEncoder;
327 
328         public Result(String targetContentEncoding, EmbeddedChannel contentEncoder) {
329             if (targetContentEncoding == null) {
330                 throw new NullPointerException("targetContentEncoding");
331             }
332             if (contentEncoder == null) {
333                 throw new NullPointerException("contentEncoder");
334             }
335 
336             this.targetContentEncoding = targetContentEncoding;
337             this.contentEncoder = contentEncoder;
338         }
339 
340         public String targetContentEncoding() {
341             return targetContentEncoding;
342         }
343 
344         public EmbeddedChannel contentEncoder() {
345             return contentEncoder;
346         }
347     }
348 }