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