View Javadoc
1   /*
2    * Copyright 2012 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 io.netty.buffer.Unpooled;
19  import io.netty.channel.Channel;
20  import io.netty.channel.ChannelFuture;
21  import io.netty.channel.ChannelFutureListener;
22  import io.netty.channel.ChannelHandler;
23  import io.netty.channel.ChannelHandlerContext;
24  import io.netty.channel.ChannelInboundHandlerAdapter;
25  import io.netty.channel.ChannelOutboundInvoker;
26  import io.netty.channel.ChannelPipeline;
27  import io.netty.channel.ChannelPromise;
28  import io.netty.handler.codec.http.DefaultFullHttpResponse;
29  import io.netty.handler.codec.http.EmptyHttpHeaders;
30  import io.netty.handler.codec.http.FullHttpRequest;
31  import io.netty.handler.codec.http.FullHttpResponse;
32  import io.netty.handler.codec.http.HttpClientCodec;
33  import io.netty.handler.codec.http.HttpContentDecompressor;
34  import io.netty.handler.codec.http.HttpHeaderNames;
35  import io.netty.handler.codec.http.HttpHeaders;
36  import io.netty.handler.codec.http.HttpObject;
37  import io.netty.handler.codec.http.HttpObjectAggregator;
38  import io.netty.handler.codec.http.HttpRequestEncoder;
39  import io.netty.handler.codec.http.HttpResponse;
40  import io.netty.handler.codec.http.HttpResponseDecoder;
41  import io.netty.handler.codec.http.HttpScheme;
42  import io.netty.handler.codec.http.LastHttpContent;
43  import io.netty.util.NetUtil;
44  import io.netty.util.ReferenceCountUtil;
45  import io.netty.util.internal.ObjectUtil;
46  
47  import java.net.URI;
48  import java.nio.channels.ClosedChannelException;
49  import java.util.Locale;
50  import java.util.concurrent.Future;
51  import java.util.concurrent.TimeUnit;
52  import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
53  
54  /**
55   * Base class for web socket client handshake implementations
56   */
57  public abstract class WebSocketClientHandshaker {
58  
59      private static final String HTTP_SCHEME_PREFIX = HttpScheme.HTTP + "://";
60      private static final String HTTPS_SCHEME_PREFIX = HttpScheme.HTTPS + "://";
61      protected static final int DEFAULT_FORCE_CLOSE_TIMEOUT_MILLIS = 10000;
62  
63      private final URI uri;
64  
65      private final WebSocketVersion version;
66  
67      private volatile boolean handshakeComplete;
68  
69      private volatile long forceCloseTimeoutMillis = DEFAULT_FORCE_CLOSE_TIMEOUT_MILLIS;
70  
71      private volatile int forceCloseInit;
72  
73      private static final AtomicIntegerFieldUpdater<WebSocketClientHandshaker> FORCE_CLOSE_INIT_UPDATER =
74              AtomicIntegerFieldUpdater.newUpdater(WebSocketClientHandshaker.class, "forceCloseInit");
75  
76      private volatile boolean forceCloseComplete;
77  
78      private final String expectedSubprotocol;
79  
80      private volatile String actualSubprotocol;
81  
82      protected final HttpHeaders customHeaders;
83  
84      private final int maxFramePayloadLength;
85  
86      private final boolean absoluteUpgradeUrl;
87  
88      /**
89       * Base constructor
90       *
91       * @param uri
92       *            URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be
93       *            sent to this URL.
94       * @param version
95       *            Version of web socket specification to use to connect to the server
96       * @param subprotocol
97       *            Sub protocol request sent to the server.
98       * @param customHeaders
99       *            Map of custom headers to add to the client request
100      * @param maxFramePayloadLength
101      *            Maximum length of a frame's payload
102      */
103     protected WebSocketClientHandshaker(URI uri, WebSocketVersion version, String subprotocol,
104                                         HttpHeaders customHeaders, int maxFramePayloadLength) {
105         this(uri, version, subprotocol, customHeaders, maxFramePayloadLength, DEFAULT_FORCE_CLOSE_TIMEOUT_MILLIS);
106     }
107 
108     /**
109      * Base constructor
110      *
111      * @param uri
112      *            URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be
113      *            sent to this URL.
114      * @param version
115      *            Version of web socket specification to use to connect to the server
116      * @param subprotocol
117      *            Sub protocol request sent to the server.
118      * @param customHeaders
119      *            Map of custom headers to add to the client request
120      * @param maxFramePayloadLength
121      *            Maximum length of a frame's payload
122      * @param forceCloseTimeoutMillis
123      *            Close the connection if it was not closed by the server after timeout specified
124      */
125     protected WebSocketClientHandshaker(URI uri, WebSocketVersion version, String subprotocol,
126                                         HttpHeaders customHeaders, int maxFramePayloadLength,
127                                         long forceCloseTimeoutMillis) {
128         this(uri, version, subprotocol, customHeaders, maxFramePayloadLength, forceCloseTimeoutMillis, false);
129     }
130 
131     /**
132      * Base constructor
133      *
134      * @param uri
135      *            URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be
136      *            sent to this URL.
137      * @param version
138      *            Version of web socket specification to use to connect to the server
139      * @param subprotocol
140      *            Sub protocol request sent to the server.
141      * @param customHeaders
142      *            Map of custom headers to add to the client request
143      * @param maxFramePayloadLength
144      *            Maximum length of a frame's payload
145      * @param forceCloseTimeoutMillis
146      *            Close the connection if it was not closed by the server after timeout specified
147      * @param  absoluteUpgradeUrl
148      *            Use an absolute url for the Upgrade request, typically when connecting through an HTTP proxy over
149      *            clear HTTP
150      */
151     protected WebSocketClientHandshaker(URI uri, WebSocketVersion version, String subprotocol,
152                                         HttpHeaders customHeaders, int maxFramePayloadLength,
153                                         long forceCloseTimeoutMillis, boolean absoluteUpgradeUrl) {
154         this.uri = uri;
155         this.version = version;
156         expectedSubprotocol = subprotocol;
157         this.customHeaders = customHeaders;
158         this.maxFramePayloadLength = maxFramePayloadLength;
159         this.forceCloseTimeoutMillis = forceCloseTimeoutMillis;
160         this.absoluteUpgradeUrl = absoluteUpgradeUrl;
161     }
162 
163     /**
164      * Returns the URI to the web socket. e.g. "ws://myhost.com/path"
165      */
166     public URI uri() {
167         return uri;
168     }
169 
170     /**
171      * Version of the web socket specification that is being used
172      */
173     public WebSocketVersion version() {
174         return version;
175     }
176 
177     /**
178      * Returns the max length for any frame's payload
179      */
180     public int maxFramePayloadLength() {
181         return maxFramePayloadLength;
182     }
183 
184     /**
185      * Flag to indicate if the opening handshake is complete
186      */
187     public boolean isHandshakeComplete() {
188         return handshakeComplete;
189     }
190 
191     private void setHandshakeComplete() {
192         handshakeComplete = true;
193     }
194 
195     /**
196      * Returns the CSV of requested subprotocol(s) sent to the server as specified in the constructor
197      */
198     public String expectedSubprotocol() {
199         return expectedSubprotocol;
200     }
201 
202     /**
203      * Returns the subprotocol response sent by the server. Only available after end of handshake.
204      * Null if no subprotocol was requested or confirmed by the server.
205      */
206     public String actualSubprotocol() {
207         return actualSubprotocol;
208     }
209 
210     private void setActualSubprotocol(String actualSubprotocol) {
211         this.actualSubprotocol = actualSubprotocol;
212     }
213 
214     public long forceCloseTimeoutMillis() {
215         return forceCloseTimeoutMillis;
216     }
217 
218     /**
219      * Flag to indicate if the closing handshake was initiated because of timeout.
220      * For testing only.
221      */
222     protected boolean isForceCloseComplete() {
223         return forceCloseComplete;
224     }
225 
226     /**
227      * Sets timeout to close the connection if it was not closed by the server.
228      *
229      * @param forceCloseTimeoutMillis
230      *            Close the connection if it was not closed by the server after timeout specified
231      */
232     public WebSocketClientHandshaker setForceCloseTimeoutMillis(long forceCloseTimeoutMillis) {
233         this.forceCloseTimeoutMillis = forceCloseTimeoutMillis;
234         return this;
235     }
236 
237     /**
238      * Begins the opening handshake
239      *
240      * @param channel
241      *            Channel
242      */
243     public ChannelFuture handshake(Channel channel) {
244         ObjectUtil.checkNotNull(channel, "channel");
245         return handshake(channel, channel.newPromise());
246     }
247 
248     /**
249      * Begins the opening handshake
250      *
251      * @param channel
252      *            Channel
253      * @param promise
254      *            the {@link ChannelPromise} to be notified when the opening handshake is sent
255      */
256     public final ChannelFuture handshake(Channel channel, final ChannelPromise promise) {
257         ChannelPipeline pipeline = channel.pipeline();
258         HttpResponseDecoder decoder = pipeline.get(HttpResponseDecoder.class);
259         if (decoder == null) {
260             HttpClientCodec codec = pipeline.get(HttpClientCodec.class);
261             if (codec == null) {
262                promise.setFailure(new IllegalStateException("ChannelPipeline does not contain " +
263                        "an HttpResponseDecoder or HttpClientCodec"));
264                return promise;
265             }
266         }
267 
268         FullHttpRequest request = newHandshakeRequest();
269 
270         channel.writeAndFlush(request).addListener(new ChannelFutureListener() {
271             @Override
272             public void operationComplete(ChannelFuture future) {
273                 if (future.isSuccess()) {
274                     ChannelPipeline p = future.channel().pipeline();
275                     ChannelHandlerContext ctx = p.context(HttpRequestEncoder.class);
276                     if (ctx == null) {
277                         ctx = p.context(HttpClientCodec.class);
278                     }
279                     if (ctx == null) {
280                         promise.setFailure(new IllegalStateException("ChannelPipeline does not contain " +
281                                 "an HttpRequestEncoder or HttpClientCodec"));
282                         return;
283                     }
284                     p.addAfter(ctx.name(), "ws-encoder", newWebSocketEncoder());
285 
286                     promise.setSuccess();
287                 } else {
288                     promise.setFailure(future.cause());
289                 }
290             }
291         });
292         return promise;
293     }
294 
295     /**
296      * Returns a new {@link FullHttpRequest) which will be used for the handshake.
297      */
298     protected abstract FullHttpRequest newHandshakeRequest();
299 
300     /**
301      * Validates and finishes the opening handshake initiated by {@link #handshake}}.
302      *
303      * @param channel
304      *            Channel
305      * @param response
306      *            HTTP response containing the closing handshake details
307      */
308     public final void finishHandshake(Channel channel, FullHttpResponse response) {
309         verify(response);
310 
311         // Verify the subprotocol that we received from the server.
312         // This must be one of our expected subprotocols - or null/empty if we didn't want to speak a subprotocol
313         String receivedProtocol = response.headers().get(HttpHeaderNames.SEC_WEBSOCKET_PROTOCOL);
314         receivedProtocol = receivedProtocol != null ? receivedProtocol.trim() : null;
315         String expectedProtocol = expectedSubprotocol != null ? expectedSubprotocol : "";
316         boolean protocolValid = false;
317 
318         if (expectedProtocol.isEmpty() && receivedProtocol == null) {
319             // No subprotocol required and none received
320             protocolValid = true;
321             setActualSubprotocol(expectedSubprotocol); // null or "" - we echo what the user requested
322         } else if (!expectedProtocol.isEmpty() && receivedProtocol != null && !receivedProtocol.isEmpty()) {
323             // We require a subprotocol and received one -> verify it
324             for (String protocol : expectedProtocol.split(",")) {
325                 if (protocol.trim().equals(receivedProtocol)) {
326                     protocolValid = true;
327                     setActualSubprotocol(receivedProtocol);
328                     break;
329                 }
330             }
331         } // else mixed cases - which are all errors
332 
333         if (!protocolValid) {
334             throw new WebSocketClientHandshakeException(String.format(
335                     "Invalid subprotocol. Actual: %s. Expected one of: %s",
336                     receivedProtocol, expectedSubprotocol), response);
337         }
338 
339         setHandshakeComplete();
340 
341         final ChannelPipeline p = channel.pipeline();
342         // Remove decompressor from pipeline if its in use
343         HttpContentDecompressor decompressor = p.get(HttpContentDecompressor.class);
344         if (decompressor != null) {
345             p.remove(decompressor);
346         }
347 
348         // Remove aggregator if present before
349         HttpObjectAggregator aggregator = p.get(HttpObjectAggregator.class);
350         if (aggregator != null) {
351             p.remove(aggregator);
352         }
353 
354         ChannelHandlerContext ctx = p.context(HttpResponseDecoder.class);
355         if (ctx == null) {
356             ctx = p.context(HttpClientCodec.class);
357             if (ctx == null) {
358                 throw new IllegalStateException("ChannelPipeline does not contain " +
359                         "an HttpRequestEncoder or HttpClientCodec");
360             }
361             final HttpClientCodec codec =  (HttpClientCodec) ctx.handler();
362             // Remove the encoder part of the codec as the user may start writing frames after this method returns.
363             codec.removeOutboundHandler();
364 
365             p.addAfter(ctx.name(), "ws-decoder", newWebsocketDecoder());
366 
367             // Delay the removal of the decoder so the user can setup the pipeline if needed to handle
368             // WebSocketFrame messages.
369             // See https://github.com/netty/netty/issues/4533
370             channel.eventLoop().execute(new Runnable() {
371                 @Override
372                 public void run() {
373                     p.remove(codec);
374                 }
375             });
376         } else {
377             if (p.get(HttpRequestEncoder.class) != null) {
378                 // Remove the encoder part of the codec as the user may start writing frames after this method returns.
379                 p.remove(HttpRequestEncoder.class);
380             }
381             final ChannelHandlerContext context = ctx;
382             p.addAfter(context.name(), "ws-decoder", newWebsocketDecoder());
383 
384             // Delay the removal of the decoder so the user can setup the pipeline if needed to handle
385             // WebSocketFrame messages.
386             // See https://github.com/netty/netty/issues/4533
387             channel.eventLoop().execute(new Runnable() {
388                 @Override
389                 public void run() {
390                     p.remove(context.handler());
391                 }
392             });
393         }
394     }
395 
396     /**
397      * Process the opening handshake initiated by {@link #handshake}}.
398      *
399      * @param channel
400      *            Channel
401      * @param response
402      *            HTTP response containing the closing handshake details
403      * @return future
404      *            the {@link ChannelFuture} which is notified once the handshake completes.
405      */
406     public final ChannelFuture processHandshake(final Channel channel, HttpResponse response) {
407         return processHandshake(channel, response, channel.newPromise());
408     }
409 
410     /**
411      * Process the opening handshake initiated by {@link #handshake}}.
412      *
413      * @param channel
414      *            Channel
415      * @param response
416      *            HTTP response containing the closing handshake details
417      * @param promise
418      *            the {@link ChannelPromise} to notify once the handshake completes.
419      * @return future
420      *            the {@link ChannelFuture} which is notified once the handshake completes.
421      */
422     public final ChannelFuture processHandshake(final Channel channel, HttpResponse response,
423                                                 final ChannelPromise promise) {
424         if (response instanceof FullHttpResponse) {
425             try {
426                 finishHandshake(channel, (FullHttpResponse) response);
427                 promise.setSuccess();
428             } catch (Throwable cause) {
429                 promise.setFailure(cause);
430             }
431         } else {
432             ChannelPipeline p = channel.pipeline();
433             ChannelHandlerContext ctx = p.context(HttpResponseDecoder.class);
434             if (ctx == null) {
435                 ctx = p.context(HttpClientCodec.class);
436                 if (ctx == null) {
437                     return promise.setFailure(new IllegalStateException("ChannelPipeline does not contain " +
438                             "an HttpResponseDecoder or HttpClientCodec"));
439                 }
440             }
441 
442             String aggregatorCtx = ctx.name();
443             // Content-Length and Transfer-Encoding must not be sent in any response with a status code of 1xx or 204.
444             if (version == WebSocketVersion.V00) {
445                 // Add aggregator and ensure we feed the HttpResponse so it is aggregated. A limit of 8192 should be
446                 // more then enough for the websockets handshake payload.
447                 aggregatorCtx = "httpAggregator";
448                 p.addAfter(ctx.name(), aggregatorCtx, new HttpObjectAggregator(8192));
449             }
450 
451             p.addAfter(aggregatorCtx, "handshaker", new ChannelInboundHandlerAdapter() {
452 
453                 private FullHttpResponse fullHttpResponse;
454 
455                 @Override
456                 public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
457                     if (msg instanceof HttpObject) {
458                         try {
459                             handleHandshakeResponse(ctx, (HttpObject) msg);
460                         } finally {
461                             ReferenceCountUtil.release(msg);
462                         }
463                     } else {
464                         super.channelRead(ctx, msg);
465                     }
466                 }
467 
468                 @Override
469                 public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
470                     // Remove ourself and fail the handshake promise.
471                     ctx.pipeline().remove(this);
472                     promise.setFailure(cause);
473                 }
474 
475                 @Override
476                 public void channelInactive(ChannelHandlerContext ctx) throws Exception {
477                     try {
478                         // Fail promise if Channel was closed
479                         if (!promise.isDone()) {
480                             promise.tryFailure(new ClosedChannelException());
481                         }
482                         ctx.fireChannelInactive();
483                     } finally {
484                         releaseFullHttpResponse();
485                     }
486                 }
487 
488                 @Override
489                 public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
490                     releaseFullHttpResponse();
491                 }
492 
493                 private void handleHandshakeResponse(ChannelHandlerContext ctx, HttpObject response) {
494                     if (response instanceof FullHttpResponse) {
495                         ctx.pipeline().remove(this);
496                         tryFinishHandshake((FullHttpResponse) response);
497                         return;
498                     }
499 
500                     if (response instanceof LastHttpContent) {
501                         assert fullHttpResponse != null;
502                         FullHttpResponse handshakeResponse = fullHttpResponse;
503                         fullHttpResponse = null;
504                         try {
505                             ctx.pipeline().remove(this);
506                             tryFinishHandshake(handshakeResponse);
507                         } finally {
508                             handshakeResponse.release();
509                         }
510                         return;
511                     }
512 
513                     if (response instanceof HttpResponse) {
514                         HttpResponse httpResponse = (HttpResponse) response;
515                         fullHttpResponse = new DefaultFullHttpResponse(httpResponse.protocolVersion(),
516                             httpResponse.status(), Unpooled.EMPTY_BUFFER, httpResponse.headers(),
517                             EmptyHttpHeaders.INSTANCE);
518                         if (httpResponse.decoderResult().isFailure()) {
519                             fullHttpResponse.setDecoderResult(httpResponse.decoderResult());
520                         }
521                     }
522                 }
523 
524                 private void tryFinishHandshake(FullHttpResponse fullHttpResponse) {
525                     try {
526                         finishHandshake(channel, fullHttpResponse);
527                         promise.setSuccess();
528                     } catch (Throwable cause) {
529                         promise.setFailure(cause);
530                     }
531                 }
532 
533                 private void releaseFullHttpResponse() {
534                     if (fullHttpResponse != null) {
535                         fullHttpResponse.release();
536                         fullHttpResponse = null;
537                     }
538                 }
539             });
540             try {
541                 ctx.fireChannelRead(ReferenceCountUtil.retain(response));
542             } catch (Throwable cause) {
543                 promise.setFailure(cause);
544             }
545         }
546         return promise;
547     }
548 
549     /**
550      * Verify the {@link FullHttpResponse} and throws a {@link WebSocketHandshakeException} if something is wrong.
551      */
552     protected abstract void verify(FullHttpResponse response);
553 
554     /**
555      * Returns the decoder to use after handshake is complete.
556      */
557     protected abstract WebSocketFrameDecoder newWebsocketDecoder();
558 
559     /**
560      * Returns the encoder to use after the handshake is complete.
561      */
562     protected abstract WebSocketFrameEncoder newWebSocketEncoder();
563 
564     /**
565      * Performs the closing handshake.
566      *
567      * When called from within a {@link ChannelHandler} you most likely want to use
568      * {@link #close(ChannelHandlerContext, CloseWebSocketFrame)}.
569      *
570      * @param channel
571      *            Channel
572      * @param frame
573      *            Closing Frame that was received
574      */
575     public ChannelFuture close(Channel channel, CloseWebSocketFrame frame) {
576         ObjectUtil.checkNotNull(channel, "channel");
577         return close(channel, frame, channel.newPromise());
578     }
579 
580     /**
581      * Performs the closing handshake
582      *
583      * When called from within a {@link ChannelHandler} you most likely want to use
584      * {@link #close(ChannelHandlerContext, CloseWebSocketFrame, ChannelPromise)}.
585      *
586      * @param channel
587      *            Channel
588      * @param frame
589      *            Closing Frame that was received
590      * @param promise
591      *            the {@link ChannelPromise} to be notified when the closing handshake is done
592      */
593     public ChannelFuture close(Channel channel, CloseWebSocketFrame frame, ChannelPromise promise) {
594         ObjectUtil.checkNotNull(channel, "channel");
595         return close0(channel, channel, frame, promise);
596     }
597 
598     /**
599      * Performs the closing handshake
600      *
601      * @param ctx
602      *            the {@link ChannelHandlerContext} to use.
603      * @param frame
604      *            Closing Frame that was received
605      */
606     public ChannelFuture close(ChannelHandlerContext ctx, CloseWebSocketFrame frame) {
607         ObjectUtil.checkNotNull(ctx, "ctx");
608         return close(ctx, frame, ctx.newPromise());
609     }
610 
611     /**
612      * Performs the closing handshake
613      *
614      * @param ctx
615      *            the {@link ChannelHandlerContext} to use.
616      * @param frame
617      *            Closing Frame that was received
618      * @param promise
619      *            the {@link ChannelPromise} to be notified when the closing handshake is done
620      */
621     public ChannelFuture close(ChannelHandlerContext ctx, CloseWebSocketFrame frame, ChannelPromise promise) {
622         ObjectUtil.checkNotNull(ctx, "ctx");
623         return close0(ctx, ctx.channel(), frame, promise);
624     }
625 
626     private ChannelFuture close0(final ChannelOutboundInvoker invoker, final Channel channel,
627                                  CloseWebSocketFrame frame, ChannelPromise promise) {
628         invoker.writeAndFlush(frame, promise);
629         final long forceCloseTimeoutMillis = this.forceCloseTimeoutMillis;
630         final WebSocketClientHandshaker handshaker = this;
631         if (forceCloseTimeoutMillis <= 0 || !channel.isActive() || forceCloseInit != 0) {
632             return promise;
633         }
634 
635         promise.addListener(new ChannelFutureListener() {
636             @Override
637             public void operationComplete(ChannelFuture future) {
638                 // If flush operation failed, there is no reason to expect
639                 // a server to receive CloseFrame. Thus this should be handled
640                 // by the application separately.
641                 // Also, close might be called twice from different threads.
642                 if (future.isSuccess() && channel.isActive() &&
643                         FORCE_CLOSE_INIT_UPDATER.compareAndSet(handshaker, 0, 1)) {
644                     final Future<?> forceCloseFuture = channel.eventLoop().schedule(new Runnable() {
645                         @Override
646                         public void run() {
647                             if (channel.isActive()) {
648                                 invoker.close();
649                                 forceCloseComplete = true;
650                             }
651                         }
652                     }, forceCloseTimeoutMillis, TimeUnit.MILLISECONDS);
653 
654                     channel.closeFuture().addListener(new ChannelFutureListener() {
655                         @Override
656                         public void operationComplete(ChannelFuture future) throws Exception {
657                             forceCloseFuture.cancel(false);
658                         }
659                     });
660                 }
661             }
662         });
663         return promise;
664     }
665 
666     /**
667      * Return the constructed raw path for the give {@link URI}.
668      */
669     protected String upgradeUrl(URI wsURL) {
670         if (absoluteUpgradeUrl) {
671             return wsURL.toString();
672         }
673 
674         String path = wsURL.getRawPath();
675         path = path == null || path.isEmpty() ? "/" : path;
676         String query = wsURL.getRawQuery();
677         return query != null && !query.isEmpty() ? path + '?' + query : path;
678     }
679 
680     static CharSequence websocketHostValue(URI wsURL) {
681         int port = wsURL.getPort();
682         if (port == -1) {
683             return wsURL.getHost();
684         }
685         String host = wsURL.getHost();
686         String scheme = wsURL.getScheme();
687         if (port == HttpScheme.HTTP.port()) {
688             return HttpScheme.HTTP.name().contentEquals(scheme)
689                     || WebSocketScheme.WS.name().contentEquals(scheme) ?
690                     host : NetUtil.toSocketAddressString(host, port);
691         }
692         if (port == HttpScheme.HTTPS.port()) {
693             return HttpScheme.HTTPS.name().contentEquals(scheme)
694                     || WebSocketScheme.WSS.name().contentEquals(scheme) ?
695                     host : NetUtil.toSocketAddressString(host, port);
696         }
697 
698         // if the port is not standard (80/443) its needed to add the port to the header.
699         // See https://tools.ietf.org/html/rfc6454#section-6.2
700         return NetUtil.toSocketAddressString(host, port);
701     }
702 
703     static CharSequence websocketOriginValue(URI wsURL) {
704         String scheme = wsURL.getScheme();
705         final String schemePrefix;
706         int port = wsURL.getPort();
707         final int defaultPort;
708         if (WebSocketScheme.WSS.name().contentEquals(scheme)
709             || HttpScheme.HTTPS.name().contentEquals(scheme)
710             || (scheme == null && port == WebSocketScheme.WSS.port())) {
711 
712             schemePrefix = HTTPS_SCHEME_PREFIX;
713             defaultPort = WebSocketScheme.WSS.port();
714         } else {
715             schemePrefix = HTTP_SCHEME_PREFIX;
716             defaultPort = WebSocketScheme.WS.port();
717         }
718 
719         // Convert uri-host to lower case (by RFC 6454, chapter 4 "Origin of a URI")
720         String host = wsURL.getHost().toLowerCase(Locale.US);
721 
722         if (port != defaultPort && port != -1) {
723             // if the port is not standard (80/443) its needed to add the port to the header.
724             // See https://tools.ietf.org/html/rfc6454#section-6.2
725             return schemePrefix + NetUtil.toSocketAddressString(host, port);
726         }
727         return schemePrefix + host;
728     }
729 }