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 }