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