View Javadoc
1   /*
2    * Copyright 2016 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.netty5.handler.codec.http;
17  
18  import io.netty5.channel.ChannelFutureListeners;
19  import io.netty5.channel.ChannelHandler;
20  import io.netty5.channel.ChannelHandlerContext;
21  import io.netty5.channel.ChannelPipeline;
22  import io.netty5.util.concurrent.Future;
23  
24  import static io.netty5.handler.codec.http.HttpUtil.isContentLengthSet;
25  import static io.netty5.handler.codec.http.HttpUtil.isKeepAlive;
26  import static io.netty5.handler.codec.http.HttpUtil.isTransferEncodingChunked;
27  import static io.netty5.handler.codec.http.HttpUtil.setKeepAlive;
28  
29  /**
30   * HttpServerKeepAliveHandler helps close persistent connections when appropriate.
31   * <p>
32   * The server channel is expected to set the proper 'Connection' header if it can handle persistent connections. {@link
33   * HttpServerKeepAliveHandler} will automatically close the channel for any LastHttpContent that corresponds to a client
34   * request for closing the connection, or if the HttpResponse associated with that LastHttpContent requested closing the
35   * connection or didn't have a self defined message length.
36   * <p>
37   * Since {@link HttpServerKeepAliveHandler} expects {@link HttpObject}s it should be added after {@link HttpServerCodec}
38   * but before any other handlers that might send a {@link HttpResponse}. <blockquote>
39   * <pre>
40   *  {@link ChannelPipeline} p = ...;
41   *  ...
42   *  p.addLast("serverCodec", new {@link HttpServerCodec}());
43   *  p.addLast("httpKeepAlive", <b>new {@link HttpServerKeepAliveHandler}()</b>);
44   *  p.addLast("aggregator", new {@link HttpObjectAggregator}(1048576));
45   *  ...
46   *  p.addLast("handler", new HttpRequestHandler());
47   *  </pre>
48   * </blockquote>
49   */
50  public class HttpServerKeepAliveHandler implements ChannelHandler {
51      private static final String MULTIPART_PREFIX = "multipart";
52  
53      private boolean persistentConnection = true;
54      // Track pending responses to support client pipelining: https://tools.ietf.org/html/rfc7230#section-6.3.2
55      private int pendingResponses;
56  
57      @Override
58      public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
59          // read message and track if it was keepAlive
60          if (msg instanceof HttpRequest) {
61              final HttpRequest request = (HttpRequest) msg;
62              if (persistentConnection) {
63                  pendingResponses += 1;
64                  persistentConnection = isKeepAlive(request);
65              }
66          }
67          ctx.fireChannelRead(msg);
68      }
69  
70      @Override
71      public Future<Void> write(ChannelHandlerContext ctx, Object msg) {
72          // modify message on way out to add headers if needed
73          if (msg instanceof HttpResponse) {
74              final HttpResponse response = (HttpResponse) msg;
75              trackResponse(response);
76              // Assume the response writer knows if they can persist or not and sets isKeepAlive on the response
77              if (!isKeepAlive(response) || !isSelfDefinedMessageLength(response)) {
78                  // No longer keep alive as the client can't tell when the message is done unless we close connection
79                  pendingResponses = 0;
80                  persistentConnection = false;
81              }
82              // Server might think it can keep connection alive, but we should fix response header if we know better
83              if (!shouldKeepAlive()) {
84                  setKeepAlive(response, false);
85              }
86          }
87          boolean shouldClose = msg instanceof LastHttpContent && !shouldKeepAlive();
88          Future<Void> future = ctx.write(msg);
89          if (shouldClose) {
90              future.addListener(ctx, ChannelFutureListeners.CLOSE);
91          }
92          return future;
93      }
94  
95      private void trackResponse(HttpResponse response) {
96          if (!isInformational(response)) {
97              pendingResponses -= 1;
98          }
99      }
100 
101     private boolean shouldKeepAlive() {
102         return pendingResponses != 0 || persistentConnection;
103     }
104 
105     /**
106      * Keep-alive only works if the client can detect when the message has ended without relying on the connection being
107      * closed.
108      * <p>
109      * <ul>
110      *     <li>See <a href="https://tools.ietf.org/html/rfc7230#section-6.3"/></li>
111      *     <li>See <a href="https://tools.ietf.org/html/rfc7230#section-3.3.2"/></li>
112      *     <li>See <a href="https://tools.ietf.org/html/rfc7230#section-3.3.3"/></li>
113      * </ul>
114      *
115      * @param response The HttpResponse to check
116      *
117      * @return true if the response has a self defined message length.
118      */
119     private static boolean isSelfDefinedMessageLength(HttpResponse response) {
120         return isContentLengthSet(response) || isTransferEncodingChunked(response) || isMultipart(response) ||
121                isInformational(response) || response.status().code() == HttpResponseStatus.NO_CONTENT.code();
122     }
123 
124     private static boolean isInformational(HttpResponse response) {
125         return response.status().codeClass() == HttpStatusClass.INFORMATIONAL;
126     }
127 
128     private static boolean isMultipart(HttpResponse response) {
129         String contentType = response.headers().get(HttpHeaderNames.CONTENT_TYPE);
130         return contentType != null &&
131                contentType.regionMatches(true, 0, MULTIPART_PREFIX, 0, MULTIPART_PREFIX.length());
132     }
133 }