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