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