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