1 /*
2 * Copyright 2014 The Netty Project
3 *
4 * The Netty Project licenses this file to you under the Apache License, version 2.0 (the
5 * "License"); you may not use this file except in compliance with the License. You may obtain a
6 * 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 distributed under the License
11 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
12 * or implied. See the License for the specific language governing permissions and limitations under
13 * the License.
14 */
15 package io.netty5.handler.codec.http;
16
17 import io.netty5.buffer.api.BufferAllocator;
18 import io.netty5.channel.internal.DelegatingChannelHandlerContext;
19 import io.netty5.util.Resource;
20 import io.netty5.util.Send;
21 import io.netty5.channel.ChannelFutureListeners;
22 import io.netty5.channel.ChannelHandlerContext;
23 import io.netty5.util.concurrent.Future;
24
25 import java.util.ArrayList;
26 import java.util.Collection;
27 import java.util.List;
28
29 import static io.netty5.handler.codec.http.HttpResponseStatus.SWITCHING_PROTOCOLS;
30 import static io.netty5.handler.codec.http.HttpVersion.HTTP_1_1;
31 import static io.netty5.util.AsciiString.containsAllContentEqualsIgnoreCase;
32 import static io.netty5.util.AsciiString.containsContentEqualsIgnoreCase;
33 import static io.netty5.util.internal.StringUtil.COMMA;
34 import static java.util.Objects.requireNonNull;
35
36 /**
37 * A server-side handler that receives HTTP requests and optionally performs a protocol switch if
38 * the requested protocol is supported. Once an upgrade is performed, this handler removes itself
39 * from the pipeline.
40 */
41 public class HttpServerUpgradeHandler<C extends HttpContent<C>> extends HttpObjectAggregator<C> {
42
43 /**
44 * The source codec that is used in the pipeline initially.
45 */
46 public interface SourceCodec {
47 /**
48 * Removes this codec (i.e. all associated handlers) from the pipeline.
49 */
50 void upgradeFrom(ChannelHandlerContext ctx);
51 }
52
53 /**
54 * A codec that the source can be upgraded to.
55 */
56 public interface UpgradeCodec {
57 /**
58 * Gets all protocol-specific headers required by this protocol for a successful upgrade.
59 * Any supplied header will be required to appear in the {@link HttpHeaderNames#CONNECTION} header as well.
60 */
61 Collection<CharSequence> requiredUpgradeHeaders();
62
63 /**
64 * Prepares the {@code upgradeHeaders} for a protocol update based upon the contents of {@code upgradeRequest}.
65 * This method returns a boolean value to proceed or abort the upgrade in progress. If {@code false} is
66 * returned, the upgrade is aborted and the {@code upgradeRequest} will be passed through the inbound pipeline
67 * as if no upgrade was performed. If {@code true} is returned, the upgrade will proceed to the next
68 * step which invokes {@link #upgradeTo}. When returning {@code true}, you can add headers to
69 * the {@code upgradeHeaders} so that they are added to the 101 Switching protocols response.
70 */
71 boolean prepareUpgradeResponse(ChannelHandlerContext ctx, FullHttpRequest upgradeRequest,
72 HttpHeaders upgradeHeaders);
73
74 /**
75 * Performs an HTTP protocol upgrade from the source codec. This method is responsible for
76 * adding all handlers required for the new protocol.
77 *
78 * @param ctx the context for the current handler.
79 * @param upgradeRequest the request that triggered the upgrade to this protocol.
80 */
81 void upgradeTo(ChannelHandlerContext ctx, FullHttpRequest upgradeRequest);
82 }
83
84 /**
85 * Creates a new {@link UpgradeCodec} for the requested protocol name.
86 */
87 public interface UpgradeCodecFactory {
88 /**
89 * Invoked by {@link HttpServerUpgradeHandler} for all the requested protocol names in the order of
90 * the client preference. The first non-{@code null} {@link UpgradeCodec} returned by this method
91 * will be selected.
92 *
93 * @return a new {@link UpgradeCodec}, or {@code null} if the specified protocol name is not supported
94 */
95 UpgradeCodec newUpgradeCodec(CharSequence protocol);
96 }
97
98 /**
99 * User event that is fired to notify about the completion of an HTTP upgrade
100 * to another protocol. Contains the original upgrade request so that the response
101 * (if required) can be sent using the new protocol.
102 */
103 public static final class UpgradeEvent implements Resource<UpgradeEvent> {
104 private final CharSequence protocol;
105 private final FullHttpRequest upgradeRequest;
106
107 UpgradeEvent(CharSequence protocol, FullHttpRequest upgradeRequest) {
108 this.protocol = protocol;
109 this.upgradeRequest = upgradeRequest;
110 }
111
112 /**
113 * The protocol that the channel has been upgraded to.
114 */
115 public CharSequence protocol() {
116 return protocol;
117 }
118
119 /**
120 * Gets the request that triggered the protocol upgrade.
121 */
122 public FullHttpRequest upgradeRequest() {
123 return upgradeRequest;
124 }
125
126 @Override
127 public Send<UpgradeEvent> send() {
128 return upgradeRequest.send().map(UpgradeEvent.class, req -> new UpgradeEvent(protocol, req));
129 }
130
131 public UpgradeEvent copy() {
132 return new UpgradeEvent(protocol, upgradeRequest.copy());
133 }
134
135 @Override
136 public void close() {
137 upgradeRequest.close();
138 }
139
140 @Override
141 public boolean isAccessible() {
142 return upgradeRequest.isAccessible();
143 }
144
145 @Override
146 public UpgradeEvent touch(Object hint) {
147 upgradeRequest.touch(hint);
148 return this;
149 }
150
151 @Override
152 public String toString() {
153 return "UpgradeEvent [protocol=" + protocol + ", upgradeRequest=" + upgradeRequest + ']';
154 }
155 }
156
157 private final SourceCodec sourceCodec;
158 private final UpgradeCodecFactory upgradeCodecFactory;
159 private final boolean validateHeaders;
160 private boolean handlingUpgrade;
161
162 /**
163 * Constructs the upgrader with the supported codecs.
164 * <p>
165 * The handler instantiated by this constructor will reject an upgrade request with non-empty content.
166 * It should not be a concern because an upgrade request is most likely a GET request.
167 * If you have a client that sends a non-GET upgrade request, please consider using
168 * {@link #HttpServerUpgradeHandler(SourceCodec, UpgradeCodecFactory, int)} to specify the maximum
169 * length of the content of an upgrade request.
170 * </p>
171 *
172 * @param sourceCodec the codec that is being used initially
173 * @param upgradeCodecFactory the factory that creates a new upgrade codec
174 * for one of the requested upgrade protocols
175 */
176 public HttpServerUpgradeHandler(SourceCodec sourceCodec, UpgradeCodecFactory upgradeCodecFactory) {
177 this(sourceCodec, upgradeCodecFactory, 0);
178 }
179
180 /**
181 * Constructs the upgrader with the supported codecs.
182 *
183 * @param sourceCodec the codec that is being used initially
184 * @param upgradeCodecFactory the factory that creates a new upgrade codec
185 * for one of the requested upgrade protocols
186 * @param maxContentLength the maximum length of the content of an upgrade request
187 */
188 public HttpServerUpgradeHandler(
189 SourceCodec sourceCodec, UpgradeCodecFactory upgradeCodecFactory, int maxContentLength) {
190 this(sourceCodec, upgradeCodecFactory, maxContentLength, true);
191 }
192
193 /**
194 * Constructs the upgrader with the supported codecs.
195 *
196 * @param sourceCodec the codec that is being used initially
197 * @param upgradeCodecFactory the factory that creates a new upgrade codec
198 * for one of the requested upgrade protocols
199 * @param maxContentLength the maximum length of the content of an upgrade request
200 * @param validateHeaders validate the header names and values of the upgrade response.
201 */
202 public HttpServerUpgradeHandler(SourceCodec sourceCodec, UpgradeCodecFactory upgradeCodecFactory,
203 int maxContentLength, boolean validateHeaders) {
204 super(maxContentLength);
205
206 this.sourceCodec = requireNonNull(sourceCodec, "sourceCodec");
207 this.upgradeCodecFactory = requireNonNull(upgradeCodecFactory, "upgradeCodecFactory");
208 this.validateHeaders = validateHeaders;
209 }
210
211 @Override
212 protected void decodeAndClose(final ChannelHandlerContext ctx, HttpObject msg)
213 throws Exception {
214
215 if (!handlingUpgrade) {
216 // Not handling an upgrade request yet. Check if we received a new upgrade request.
217 if (msg instanceof HttpRequest) {
218 HttpRequest req = (HttpRequest) msg;
219 if (req.headers().contains(HttpHeaderNames.UPGRADE) &&
220 shouldHandleUpgradeRequest(req)) {
221 handlingUpgrade = true;
222 } else {
223 ctx.fireChannelRead(msg);
224 return;
225 }
226 } else {
227 ctx.fireChannelRead(msg);
228 return;
229 }
230 }
231
232 FullHttpRequest fullRequest;
233 if (msg instanceof FullHttpRequest) {
234 fullRequest = (FullHttpRequest) msg;
235 tryUpgrade(ctx, fullRequest);
236 } else {
237 // Call the base class to handle the aggregation of the full request.
238 super.decodeAndClose(new DelegatingChannelHandlerContext(ctx) {
239 @Override
240 public ChannelHandlerContext fireChannelRead(Object msg) {
241 // Finished aggregating the full request, get it from the output list.
242 handlingUpgrade = false;
243 tryUpgrade(ctx, (FullHttpRequest) msg);
244 return this;
245 }
246 }, msg);
247 }
248 }
249
250 private void tryUpgrade(ChannelHandlerContext ctx, FullHttpRequest request) {
251 if (!upgrade(ctx, request)) {
252
253 // The upgrade did not succeed, just allow the full request to propagate to the
254 // next handler.
255 ctx.fireChannelRead(request);
256 }
257 }
258
259 /**
260 * Determines whether the specified upgrade {@link HttpRequest} should be handled by this handler or not.
261 * This method will be invoked only when the request contains an {@code Upgrade} header.
262 * It always returns {@code true} by default, which means any request with an {@code Upgrade} header
263 * will be handled. You can override this method to ignore certain {@code Upgrade} headers, for example:
264 * <pre>{@code
265 * @Override
266 * protected boolean isUpgradeRequest(HttpRequest req) {
267 * // Do not handle WebSocket upgrades.
268 * return !req.headers().contains(HttpHeaderNames.UPGRADE, "websocket", false);
269 * }
270 * }</pre>
271 */
272 protected boolean shouldHandleUpgradeRequest(HttpRequest req) {
273 return true;
274 }
275
276 /**
277 * Attempts to upgrade to the protocol(s) identified by the {@link HttpHeaderNames#UPGRADE} header (if provided
278 * in the request).
279 *
280 * @param ctx the context for this handler.
281 * @param request the HTTP request.
282 * @return {@code true} if the upgrade occurred, otherwise {@code false}.
283 */
284 private boolean upgrade(final ChannelHandlerContext ctx, final FullHttpRequest request) {
285 // Select the best protocol based on those requested in the UPGRADE header.
286 final List<CharSequence> requestedProtocols = splitHeader(request.headers().get(HttpHeaderNames.UPGRADE));
287 final int numRequestedProtocols = requestedProtocols.size();
288 UpgradeCodec upgradeCodec = null;
289 CharSequence upgradeProtocol = null;
290 for (int i = 0; i < numRequestedProtocols; i ++) {
291 final CharSequence p = requestedProtocols.get(i);
292 final UpgradeCodec c = upgradeCodecFactory.newUpgradeCodec(p);
293 if (c != null) {
294 upgradeProtocol = p;
295 upgradeCodec = c;
296 break;
297 }
298 }
299
300 if (upgradeCodec == null) {
301 // None of the requested protocols are supported, don't upgrade.
302 return false;
303 }
304
305 // Make sure the CONNECTION header is present.
306 List<String> connectionHeaderValues = request.headers().getAll(HttpHeaderNames.CONNECTION);
307
308 if (connectionHeaderValues == null || connectionHeaderValues.isEmpty()) {
309 return false;
310 }
311
312 final StringBuilder concatenatedConnectionValue = new StringBuilder(connectionHeaderValues.size() * 10);
313 for (CharSequence connectionHeaderValue : connectionHeaderValues) {
314 concatenatedConnectionValue.append(connectionHeaderValue).append(COMMA);
315 }
316 concatenatedConnectionValue.setLength(concatenatedConnectionValue.length() - 1);
317
318 // Make sure the CONNECTION header contains UPGRADE as well as all protocol-specific headers.
319 Collection<CharSequence> requiredHeaders = upgradeCodec.requiredUpgradeHeaders();
320 List<CharSequence> values = splitHeader(concatenatedConnectionValue);
321 if (!containsContentEqualsIgnoreCase(values, HttpHeaderNames.UPGRADE) ||
322 !containsAllContentEqualsIgnoreCase(values, requiredHeaders)) {
323 return false;
324 }
325
326 // Ensure that all required protocol-specific headers are found in the request.
327 for (CharSequence requiredHeader : requiredHeaders) {
328 if (!request.headers().contains(requiredHeader)) {
329 return false;
330 }
331 }
332
333 // Prepare and send the upgrade response. Wait for this write to complete before upgrading,
334 // since we need the old codec in-place to properly encode the response.
335 final FullHttpResponse upgradeResponse = createUpgradeResponse(ctx.bufferAllocator(), upgradeProtocol);
336 if (!upgradeCodec.prepareUpgradeResponse(ctx, request, upgradeResponse.headers())) {
337 return false;
338 }
339
340 // After writing the upgrade response we immediately prepare the
341 // pipeline for the next protocol to avoid a race between completion
342 // of the write future and receiving data before the pipeline is
343 // restructured.
344 Future<Void> writeComplete = ctx.writeAndFlush(upgradeResponse);
345 // Perform the upgrade to the new protocol.
346 sourceCodec.upgradeFrom(ctx);
347 upgradeCodec.upgradeTo(ctx, request);
348
349 // Notify that the upgrade has occurred.
350 ctx.fireChannelInboundEvent(new UpgradeEvent(upgradeProtocol, request));
351
352 // Remove this handler from the pipeline.
353 ctx.pipeline().remove(HttpServerUpgradeHandler.this);
354
355 // Add the listener last to avoid firing upgrade logic after
356 // the channel is already closed since the listener may fire
357 // immediately if the write failed eagerly.
358 writeComplete.addListener(ctx.channel(), ChannelFutureListeners.CLOSE_ON_FAILURE);
359 return true;
360 }
361
362 /**
363 * Creates the 101 Switching Protocols response message.
364 */
365 private FullHttpResponse createUpgradeResponse(BufferAllocator allocator, CharSequence upgradeProtocol) {
366 DefaultFullHttpResponse res = new DefaultFullHttpResponse(
367 HTTP_1_1, SWITCHING_PROTOCOLS, allocator.allocate(0), validateHeaders);
368 res.headers().add(HttpHeaderNames.CONNECTION, HttpHeaderValues.UPGRADE);
369 res.headers().add(HttpHeaderNames.UPGRADE, upgradeProtocol);
370 return res;
371 }
372
373 /**
374 * Splits a comma-separated header value. The returned set is case-insensitive and contains each
375 * part with whitespace removed.
376 */
377 private static List<CharSequence> splitHeader(CharSequence header) {
378 final StringBuilder builder = new StringBuilder(header.length());
379 final List<CharSequence> protocols = new ArrayList<>(4);
380 for (int i = 0; i < header.length(); ++i) {
381 char c = header.charAt(i);
382 if (Character.isWhitespace(c)) {
383 // Don't include any whitespace.
384 continue;
385 }
386 if (c == ',') {
387 // Add the string and reset the builder for the next protocol.
388 protocols.add(builder.toString());
389 builder.setLength(0);
390 } else {
391 builder.append(c);
392 }
393 }
394
395 // Add the last protocol
396 if (builder.length() > 0) {
397 protocols.add(builder.toString());
398 }
399
400 return protocols;
401 }
402 }