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