1 /*
2 * Copyright 2019 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.websocketx;
17
18 import java.nio.channels.ClosedChannelException;
19 import java.util.Collections;
20 import java.util.LinkedHashSet;
21 import java.util.Set;
22
23 import io.netty.buffer.Unpooled;
24 import io.netty.channel.Channel;
25 import io.netty.channel.ChannelFuture;
26 import io.netty.channel.ChannelFutureListener;
27 import io.netty.channel.ChannelHandler;
28 import io.netty.channel.ChannelHandlerContext;
29 import io.netty.channel.ChannelInboundHandlerAdapter;
30 import io.netty.channel.ChannelOutboundInvoker;
31 import io.netty.channel.ChannelPipeline;
32 import io.netty.channel.ChannelPromise;
33 import io.netty.handler.codec.http.DefaultFullHttpRequest;
34 import io.netty.handler.codec.http.EmptyHttpHeaders;
35 import io.netty.handler.codec.http.FullHttpRequest;
36 import io.netty.handler.codec.http.FullHttpResponse;
37 import io.netty.handler.codec.http.HttpContentCompressor;
38 import io.netty.handler.codec.http.HttpHeaders;
39 import io.netty.handler.codec.http.HttpObject;
40 import io.netty.handler.codec.http.HttpObjectAggregator;
41 import io.netty.handler.codec.http.HttpRequest;
42 import io.netty.handler.codec.http.HttpRequestDecoder;
43 import io.netty.handler.codec.http.HttpResponseEncoder;
44 import io.netty.handler.codec.http.HttpServerCodec;
45 import io.netty.handler.codec.http.HttpUtil;
46 import io.netty.handler.codec.http.LastHttpContent;
47 import io.netty.util.ReferenceCountUtil;
48 import io.netty.util.internal.EmptyArrays;
49 import io.netty.util.internal.ObjectUtil;
50 import io.netty.util.internal.logging.InternalLogger;
51 import io.netty.util.internal.logging.InternalLoggerFactory;
52
53 /**
54 * Base class for server side web socket opening and closing handshakes
55 */
56 public abstract class WebSocketServerHandshaker {
57 protected static final InternalLogger logger = InternalLoggerFactory.getInstance(WebSocketServerHandshaker.class);
58
59 private final String uri;
60
61 private final boolean isSecure;
62
63 private final String host;
64
65 private final String path;
66
67 private final String[] subprotocols;
68
69 private final WebSocketVersion version;
70
71 private final WebSocketDecoderConfig decoderConfig;
72
73 private String selectedSubprotocol;
74
75 /**
76 * Use this as wildcard to support all requested sub-protocols
77 */
78 public static final String SUB_PROTOCOL_WILDCARD = "*";
79
80 /**
81 * Constructor specifying the destination web socket location
82 *
83 * @param version
84 * the protocol version
85 * @param uri
86 * URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be
87 * sent to this URL.
88 * @param subprotocols
89 * CSV of supported protocols. Null if sub protocols not supported.
90 * @param maxFramePayloadLength
91 * Maximum length of a frame's payload
92 */
93 protected WebSocketServerHandshaker(
94 WebSocketVersion version, String uri, String subprotocols,
95 int maxFramePayloadLength) {
96 this(version, uri, subprotocols, WebSocketDecoderConfig.newBuilder()
97 .maxFramePayloadLength(maxFramePayloadLength)
98 .build());
99 }
100
101 /**
102 * Constructor specifying the destination web socket location
103 *
104 * @param version
105 * the protocol version
106 * @param uri
107 * URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be
108 * sent to this URL.
109 * @param subprotocols
110 * CSV of supported protocols. Null if sub protocols not supported.
111 * @param decoderConfig
112 * Frames decoder configuration.
113 */
114 protected WebSocketServerHandshaker(
115 WebSocketVersion version, String uri, String subprotocols, WebSocketDecoderConfig decoderConfig) {
116 this.version = version;
117 this.uri = uri;
118 this.isSecure = false;
119 this.host = null;
120 this.path = null;
121 this.subprotocols = parseSubprotocols(subprotocols);
122 this.decoderConfig = ObjectUtil.checkNotNull(decoderConfig, "decoderConfig");
123 }
124
125 // Package-private: defers URI construction to avoid String allocation.
126 WebSocketServerHandshaker(
127 WebSocketVersion version, boolean isSecure, String host, String path,
128 String subprotocols, WebSocketDecoderConfig decoderConfig) {
129 this.version = version;
130 this.uri = null;
131 this.isSecure = isSecure;
132 this.host = host;
133 this.path = path;
134 this.subprotocols = parseSubprotocols(subprotocols);
135 this.decoderConfig = ObjectUtil.checkNotNull(decoderConfig, "decoderConfig");
136 }
137
138 private static String[] parseSubprotocols(String subprotocols) {
139 if (subprotocols != null) {
140 String[] subprotocolArray = subprotocols.split(",");
141 for (int i = 0; i < subprotocolArray.length; i++) {
142 subprotocolArray[i] = subprotocolArray[i].trim();
143 }
144 return subprotocolArray;
145 }
146 return EmptyArrays.EMPTY_STRINGS;
147 }
148
149 /**
150 * Returns the URL of the web socket
151 */
152 @Deprecated
153 public String uri() {
154 if (uri != null) {
155 return uri;
156 }
157 return (isSecure ? "wss" : "ws") + "://" + host + path;
158 }
159
160 /**
161 * Returns the CSV of supported sub protocols
162 */
163 public Set<String> subprotocols() {
164 Set<String> ret = new LinkedHashSet<String>();
165 Collections.addAll(ret, subprotocols);
166 return ret;
167 }
168
169 /**
170 * Returns the version of the specification being supported
171 */
172 public WebSocketVersion version() {
173 return version;
174 }
175
176 /**
177 * Gets the maximum length for any frame's payload.
178 *
179 * @return The maximum length for a frame's payload
180 */
181 public int maxFramePayloadLength() {
182 return decoderConfig.maxFramePayloadLength();
183 }
184
185 /**
186 * Gets this decoder configuration.
187 *
188 * @return This decoder configuration.
189 */
190 public WebSocketDecoderConfig decoderConfig() {
191 return decoderConfig;
192 }
193
194 /**
195 * Performs the opening handshake. When call this method you <strong>MUST NOT</strong> retain the
196 * {@link FullHttpRequest} which is passed in.
197 *
198 * When called from within a {@link ChannelHandler} you most likely want to use
199 * {@link #handshake(ChannelHandlerContext, FullHttpRequest)}.
200 *
201 * @param channel
202 * Channel
203 * @param req
204 * HTTP Request
205 * @return future
206 * The {@link ChannelFuture} which is notified once the opening handshake completes
207 */
208 public ChannelFuture handshake(Channel channel, FullHttpRequest req) {
209 return handshake(channel, req, null, channel.newPromise());
210 }
211
212 /**
213 * Performs the opening handshake
214 *
215 * When call this method you <strong>MUST NOT</strong> retain the {@link FullHttpRequest} which is passed in.
216 *
217 * When called from within a {@link ChannelHandler} you most likely want to use
218 * {@link #handshake(ChannelHandlerContext, FullHttpRequest, HttpHeaders, ChannelPromise)}.
219 *
220 * @param channel
221 * Channel
222 * @param req
223 * HTTP Request
224 * @param responseHeaders
225 * Extra headers to add to the handshake response or {@code null} if no extra headers should be added
226 * @param promise
227 * the {@link ChannelPromise} to be notified when the opening handshake is done
228 * @return future
229 * the {@link ChannelFuture} which is notified when the opening handshake is done
230 */
231 public final ChannelFuture handshake(Channel channel, FullHttpRequest req,
232 HttpHeaders responseHeaders, final ChannelPromise promise) {
233 return handshake0(channel, channel, req, responseHeaders, promise);
234 }
235
236 /**
237 * Performs the opening handshake. When call this method you <strong>MUST NOT</strong> retain the
238 * {@link FullHttpRequest} which is passed in.
239 *
240 * The handshake response is written to the given {@link ChannelHandlerContext}, so handlers placed
241 * after the {@link ChannelHandler} the context belongs to will not see the response. The handler the
242 * context belongs to must be placed after the HTTP encoder as the response still needs to be encoded.
243 *
244 * @param ctx
245 * the {@link ChannelHandlerContext} to use.
246 * @param req
247 * HTTP Request
248 * @return future
249 * The {@link ChannelFuture} which is notified once the opening handshake completes
250 */
251 public ChannelFuture handshake(ChannelHandlerContext ctx, FullHttpRequest req) {
252 ObjectUtil.checkNotNull(ctx, "ctx");
253 return handshake(ctx, req, null, ctx.newPromise());
254 }
255
256 /**
257 * Performs the opening handshake
258 *
259 * When call this method you <strong>MUST NOT</strong> retain the {@link FullHttpRequest} which is passed in.
260 *
261 * The handshake response is written to the given {@link ChannelHandlerContext}, so handlers placed
262 * after the {@link ChannelHandler} the context belongs to will not see the response. The handler the
263 * context belongs to must be placed after the HTTP encoder as the response still needs to be encoded.
264 *
265 * @param ctx
266 * the {@link ChannelHandlerContext} to use.
267 * @param req
268 * HTTP Request
269 * @param responseHeaders
270 * Extra headers to add to the handshake response or {@code null} if no extra headers should be added
271 * @param promise
272 * the {@link ChannelPromise} to be notified when the opening handshake is done
273 * @return future
274 * the {@link ChannelFuture} which is notified when the opening handshake is done
275 */
276 public ChannelFuture handshake(ChannelHandlerContext ctx, FullHttpRequest req,
277 HttpHeaders responseHeaders, final ChannelPromise promise) {
278 ObjectUtil.checkNotNull(ctx, "ctx");
279 return handshake0(ctx, ctx.channel(), req, responseHeaders, promise);
280 }
281
282 private ChannelFuture handshake0(final ChannelOutboundInvoker invoker, final Channel channel, FullHttpRequest req,
283 HttpHeaders responseHeaders, final ChannelPromise promise) {
284
285 if (logger.isDebugEnabled()) {
286 logger.debug("{} WebSocket version {} server handshake", channel, version());
287 }
288 FullHttpResponse response = newHandshakeResponse(req, responseHeaders);
289 ChannelPipeline p = channel.pipeline();
290 if (p.get(HttpObjectAggregator.class) != null) {
291 p.remove(HttpObjectAggregator.class);
292 }
293 if (p.get(HttpContentCompressor.class) != null) {
294 p.remove(HttpContentCompressor.class);
295 }
296 ChannelHandlerContext ctx = p.context(HttpRequestDecoder.class);
297 final String encoderName;
298 if (ctx == null) {
299 // this means the user use an HttpServerCodec
300 ctx = p.context(HttpServerCodec.class);
301 if (ctx == null) {
302 promise.setFailure(
303 new IllegalStateException("No HttpDecoder and no HttpServerCodec in the pipeline"));
304 response.release();
305 return promise;
306 }
307 p.addBefore(ctx.name(), "wsencoder", newWebSocketEncoder());
308 p.addBefore(ctx.name(), "wsdecoder", newWebsocketDecoder());
309 encoderName = ctx.name();
310 } else {
311 p.replace(ctx.name(), "wsdecoder", newWebsocketDecoder());
312
313 encoderName = p.context(HttpResponseEncoder.class).name();
314 p.addBefore(encoderName, "wsencoder", newWebSocketEncoder());
315 }
316 invoker.writeAndFlush(response).addListener(future -> {
317 if (future.isSuccess()) {
318 ChannelPipeline p1 = channel.pipeline();
319 p1.remove(encoderName);
320 promise.setSuccess();
321 } else {
322 promise.setFailure(future.cause());
323 }
324 });
325 return promise;
326 }
327
328 /**
329 * Performs the opening handshake. When call this method you <strong>MUST NOT</strong> retain the
330 * {@link FullHttpRequest} which is passed in.
331 *
332 * When called from within a {@link ChannelHandler} you most likely want to use
333 * {@link #handshake(ChannelHandlerContext, HttpRequest)}.
334 *
335 * @param channel
336 * Channel
337 * @param req
338 * HTTP Request
339 * @return future
340 * The {@link ChannelFuture} which is notified once the opening handshake completes
341 */
342 public ChannelFuture handshake(Channel channel, HttpRequest req) {
343 return handshake(channel, req, null, channel.newPromise());
344 }
345
346 /**
347 * Performs the opening handshake
348 *
349 * When call this method you <strong>MUST NOT</strong> retain the {@link HttpRequest} which is passed in.
350 *
351 * When called from within a {@link ChannelHandler} you most likely want to use
352 * {@link #handshake(ChannelHandlerContext, HttpRequest, HttpHeaders, ChannelPromise)}.
353 *
354 * @param channel
355 * Channel
356 * @param req
357 * HTTP Request
358 * @param responseHeaders
359 * Extra headers to add to the handshake response or {@code null} if no extra headers should be added
360 * @param promise
361 * the {@link ChannelPromise} to be notified when the opening handshake is done
362 * @return future
363 * the {@link ChannelFuture} which is notified when the opening handshake is done
364 */
365 public final ChannelFuture handshake(final Channel channel, HttpRequest req,
366 final HttpHeaders responseHeaders, final ChannelPromise promise) {
367 return handshake0(channel, channel, req, responseHeaders, promise);
368 }
369
370 /**
371 * Performs the opening handshake. When call this method you <strong>MUST NOT</strong> retain the
372 * {@link HttpRequest} which is passed in.
373 *
374 * The handshake response is written to the given {@link ChannelHandlerContext}, so handlers placed
375 * after the {@link ChannelHandler} the context belongs to will not see the response. The handler the
376 * context belongs to must be placed after the HTTP encoder as the response still needs to be encoded.
377 *
378 * @param ctx
379 * the {@link ChannelHandlerContext} to use.
380 * @param req
381 * HTTP Request
382 * @return future
383 * The {@link ChannelFuture} which is notified once the opening handshake completes
384 */
385 public ChannelFuture handshake(ChannelHandlerContext ctx, HttpRequest req) {
386 ObjectUtil.checkNotNull(ctx, "ctx");
387 return handshake(ctx, req, null, ctx.newPromise());
388 }
389
390 /**
391 * Performs the opening handshake
392 *
393 * When call this method you <strong>MUST NOT</strong> retain the {@link HttpRequest} which is passed in.
394 *
395 * The handshake response is written to the given {@link ChannelHandlerContext}, so handlers placed
396 * after the {@link ChannelHandler} the context belongs to will not see the response. The handler the
397 * context belongs to must be placed after the HTTP encoder as the response still needs to be encoded.
398 *
399 * @param ctx
400 * the {@link ChannelHandlerContext} to use.
401 * @param req
402 * HTTP Request
403 * @param responseHeaders
404 * Extra headers to add to the handshake response or {@code null} if no extra headers should be added
405 * @param promise
406 * the {@link ChannelPromise} to be notified when the opening handshake is done
407 * @return future
408 * the {@link ChannelFuture} which is notified when the opening handshake is done
409 */
410 public ChannelFuture handshake(ChannelHandlerContext ctx, HttpRequest req,
411 final HttpHeaders responseHeaders, final ChannelPromise promise) {
412 ObjectUtil.checkNotNull(ctx, "ctx");
413 return handshake0(ctx, ctx.channel(), req, responseHeaders, promise);
414 }
415
416 private ChannelFuture handshake0(final ChannelOutboundInvoker invoker, final Channel channel, HttpRequest req,
417 final HttpHeaders responseHeaders, final ChannelPromise promise) {
418 if (req instanceof FullHttpRequest) {
419 return handshake0(invoker, channel, (FullHttpRequest) req, responseHeaders, promise);
420 }
421
422 if (logger.isDebugEnabled()) {
423 logger.debug("{} WebSocket version {} server handshake", channel, version());
424 }
425
426 ChannelPipeline p = channel.pipeline();
427 ChannelHandlerContext ctx = p.context(HttpRequestDecoder.class);
428 if (ctx == null) {
429 // this means the user use an HttpServerCodec
430 ctx = p.context(HttpServerCodec.class);
431 if (ctx == null) {
432 promise.setFailure(
433 new IllegalStateException("No HttpDecoder and no HttpServerCodec in the pipeline"));
434 return promise;
435 }
436 }
437
438 String aggregatorCtx = ctx.name();
439 if (HttpUtil.isContentLengthSet(req) || HttpUtil.isTransferEncodingChunked(req) ||
440 version == WebSocketVersion.V00) {
441 // Add aggregator and ensure we feed the HttpRequest so it is aggregated. A limit of 8192 should be
442 // more then enough for the websockets handshake payload.
443 aggregatorCtx = "httpAggregator";
444 p.addAfter(ctx.name(), aggregatorCtx, new HttpObjectAggregator(8192));
445 }
446
447 p.addAfter(aggregatorCtx, "handshaker", new ChannelInboundHandlerAdapter() {
448
449 private FullHttpRequest fullHttpRequest;
450
451 @Override
452 public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
453 if (msg instanceof HttpObject) {
454 try {
455 handleHandshakeRequest(ctx, (HttpObject) msg);
456 } finally {
457 ReferenceCountUtil.release(msg);
458 }
459 } else {
460 super.channelRead(ctx, msg);
461 }
462 }
463
464 @Override
465 public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
466 // Remove ourself and fail the handshake promise.
467 ctx.pipeline().remove(this);
468 promise.tryFailure(cause);
469 ctx.fireExceptionCaught(cause);
470 }
471
472 @Override
473 public void channelInactive(ChannelHandlerContext ctx) throws Exception {
474 try {
475 // Fail promise if Channel was closed
476 if (!promise.isDone()) {
477 promise.tryFailure(new ClosedChannelException());
478 }
479 ctx.fireChannelInactive();
480 } finally {
481 releaseFullHttpRequest();
482 }
483 }
484
485 @Override
486 public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
487 releaseFullHttpRequest();
488 }
489
490 private void handleHandshakeRequest(ChannelHandlerContext ctx, HttpObject httpObject) {
491 if (httpObject instanceof FullHttpRequest) {
492 ctx.pipeline().remove(this);
493 handshake0(invoker, channel, (FullHttpRequest) httpObject, responseHeaders, promise);
494 return;
495 }
496
497 if (httpObject instanceof LastHttpContent) {
498 assert fullHttpRequest != null;
499 FullHttpRequest handshakeRequest = fullHttpRequest;
500 fullHttpRequest = null;
501 try {
502 ctx.pipeline().remove(this);
503 handshake0(invoker, channel, handshakeRequest, responseHeaders, promise);
504 } finally {
505 handshakeRequest.release();
506 }
507 return;
508 }
509
510 if (httpObject instanceof HttpRequest) {
511 HttpRequest httpRequest = (HttpRequest) httpObject;
512 fullHttpRequest = new DefaultFullHttpRequest(httpRequest.protocolVersion(), httpRequest.method(),
513 httpRequest.uri(), Unpooled.EMPTY_BUFFER, httpRequest.headers(), EmptyHttpHeaders.INSTANCE);
514 if (httpRequest.decoderResult().isFailure()) {
515 fullHttpRequest.setDecoderResult(httpRequest.decoderResult());
516 }
517 }
518 }
519
520 private void releaseFullHttpRequest() {
521 if (fullHttpRequest != null) {
522 fullHttpRequest.release();
523 fullHttpRequest = null;
524 }
525 }
526 });
527 try {
528 ctx.fireChannelRead(ReferenceCountUtil.retain(req));
529 } catch (Throwable cause) {
530 promise.setFailure(cause);
531 }
532 return promise;
533 }
534
535 /**
536 * Returns a new {@link FullHttpResponse) which will be used for as response to the handshake request.
537 */
538 protected abstract FullHttpResponse newHandshakeResponse(FullHttpRequest req,
539 HttpHeaders responseHeaders);
540 /**
541 * Performs the closing handshake.
542 *
543 * When called from within a {@link ChannelHandler} you most likely want to use
544 * {@link #close(ChannelHandlerContext, CloseWebSocketFrame)}.
545 *
546 * @param channel
547 * the {@link Channel} to use.
548 * @param frame
549 * Closing Frame that was received.
550 */
551 public ChannelFuture close(Channel channel, CloseWebSocketFrame frame) {
552 ObjectUtil.checkNotNull(channel, "channel");
553 return close(channel, frame, channel.newPromise());
554 }
555
556 /**
557 * Performs the closing handshake.
558 *
559 * When called from within a {@link ChannelHandler} you most likely want to use
560 * {@link #close(ChannelHandlerContext, CloseWebSocketFrame, ChannelPromise)}.
561 *
562 * @param channel
563 * the {@link Channel} to use.
564 * @param frame
565 * Closing Frame that was received.
566 * @param promise
567 * the {@link ChannelPromise} to be notified when the closing handshake is done
568 */
569 public ChannelFuture close(Channel channel, CloseWebSocketFrame frame, ChannelPromise promise) {
570 return close0(channel, frame, promise);
571 }
572
573 /**
574 * Performs the closing handshake.
575 *
576 * @param ctx
577 * the {@link ChannelHandlerContext} to use.
578 * @param frame
579 * Closing Frame that was received.
580 */
581 public ChannelFuture close(ChannelHandlerContext ctx, CloseWebSocketFrame frame) {
582 ObjectUtil.checkNotNull(ctx, "ctx");
583 return close(ctx, frame, ctx.newPromise());
584 }
585
586 /**
587 * Performs the closing handshake.
588 *
589 * @param ctx
590 * the {@link ChannelHandlerContext} to use.
591 * @param frame
592 * Closing Frame that was received.
593 * @param promise
594 * the {@link ChannelPromise} to be notified when the closing handshake is done.
595 */
596 public ChannelFuture close(ChannelHandlerContext ctx, CloseWebSocketFrame frame, ChannelPromise promise) {
597 ObjectUtil.checkNotNull(ctx, "ctx");
598 return close0(ctx, frame, promise).addListener(ChannelFutureListener.CLOSE);
599 }
600
601 private ChannelFuture close0(ChannelOutboundInvoker invoker, CloseWebSocketFrame frame, ChannelPromise promise) {
602 return invoker.writeAndFlush(frame, promise).addListener(ChannelFutureListener.CLOSE);
603 }
604
605 /**
606 * Selects the first matching supported sub protocol
607 *
608 * @param requestedSubprotocols
609 * CSV of protocols to be supported. e.g. "chat, superchat"
610 * @return First matching supported sub protocol. Null if not found.
611 */
612 protected String selectSubprotocol(String requestedSubprotocols) {
613 if (requestedSubprotocols == null || subprotocols.length == 0) {
614 return null;
615 }
616
617 String[] requestedSubprotocolArray = requestedSubprotocols.split(",");
618 for (String p: requestedSubprotocolArray) {
619 String requestedSubprotocol = p.trim();
620
621 for (String supportedSubprotocol: subprotocols) {
622 if (SUB_PROTOCOL_WILDCARD.equals(supportedSubprotocol)
623 || requestedSubprotocol.equals(supportedSubprotocol)) {
624 selectedSubprotocol = requestedSubprotocol;
625 return requestedSubprotocol;
626 }
627 }
628 }
629
630 // No match found
631 return null;
632 }
633
634 /**
635 * Returns the selected subprotocol. Null if no subprotocol has been selected.
636 * <p>
637 * This is only available AFTER <tt>handshake()</tt> has been called.
638 * </p>
639 */
640 public String selectedSubprotocol() {
641 return selectedSubprotocol;
642 }
643
644 /**
645 * Returns the decoder to use after handshake is complete.
646 */
647 protected abstract WebSocketFrameDecoder newWebsocketDecoder();
648
649 /**
650 * Returns the encoder to use after the handshake is complete.
651 */
652 protected abstract WebSocketFrameEncoder newWebSocketEncoder();
653
654 }