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.netty5.handler.codec.http.websocketx;
17  
18  import io.netty5.channel.ChannelFutureListeners;
19  import io.netty5.channel.ChannelHandler;
20  import io.netty5.channel.ChannelHandlerContext;
21  import io.netty5.channel.ChannelPipeline;
22  import io.netty5.handler.codec.http.DefaultFullHttpResponse;
23  import io.netty5.handler.codec.http.HttpHeaderNames;
24  import io.netty5.handler.codec.http.HttpObject;
25  import io.netty5.handler.codec.http.HttpRequest;
26  import io.netty5.handler.codec.http.HttpResponse;
27  import io.netty5.handler.ssl.SslHandler;
28  import io.netty5.util.concurrent.Future;
29  import io.netty5.util.concurrent.Promise;
30  
31  import java.util.Objects;
32  import java.util.concurrent.TimeUnit;
33  
34  import static io.netty5.handler.codec.http.HttpMethod.GET;
35  import static io.netty5.handler.codec.http.HttpResponseStatus.FORBIDDEN;
36  import static io.netty5.handler.codec.http.HttpUtil.isKeepAlive;
37  import static io.netty5.handler.codec.http.HttpVersion.HTTP_1_1;
38  
39  /**
40   * Handles the HTTP handshake (the HTTP Upgrade request) for {@link WebSocketServerProtocolHandler}.
41   */
42  class WebSocketServerProtocolHandshakeHandler implements ChannelHandler {
43  
44      private final WebSocketServerProtocolConfig serverConfig;
45      private Promise<Void> handshakePromise;
46      private boolean isWebSocketPath;
47  
48      WebSocketServerProtocolHandshakeHandler(WebSocketServerProtocolConfig serverConfig) {
49          this.serverConfig = Objects.requireNonNull(serverConfig, "serverConfig");
50      }
51  
52      @Override
53      public void handlerAdded(ChannelHandlerContext ctx) {
54          handshakePromise = ctx.newPromise();
55      }
56  
57      @Override
58      public void channelRead(final ChannelHandlerContext ctx, Object msg) throws Exception {
59          final HttpObject httpObject = (HttpObject) msg;
60  
61          if (httpObject instanceof HttpRequest) {
62              final HttpRequest req = (HttpRequest) httpObject;
63              isWebSocketPath = isWebSocketPath(req);
64              if (!isWebSocketPath) {
65                  ctx.fireChannelRead(msg);
66                  return;
67              }
68  
69              try {
70                  if (!GET.equals(req.method())) {
71                      sendHttpResponse(ctx, req, new DefaultFullHttpResponse(HTTP_1_1, FORBIDDEN,
72                              ctx.bufferAllocator().allocate(0)));
73                      return;
74                  }
75  
76                  final WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory(
77                          getWebSocketLocation(ctx.pipeline(), req, serverConfig.websocketPath()),
78                          serverConfig.subprotocols(), serverConfig.decoderConfig());
79                  final WebSocketServerHandshaker handshaker = wsFactory.newHandshaker(req);
80                  Promise<Void> localHandshakePromise = handshakePromise;
81                  if (handshaker == null) {
82                      WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel());
83                  } else {
84                      // Ensure we set the handshaker and replace this handler before we
85                      // trigger the actual handshake. Otherwise we may receive websocket bytes in this handler
86                      // before we had a chance to replace it.
87                      //
88                      // See https://github.com/netty/netty/issues/9471.
89                      WebSocketServerProtocolHandler.setHandshaker(ctx.channel(), handshaker);
90  
91                      Future<Void> handshakeFuture = handshaker.handshake(ctx.channel(), req);
92                      handshakeFuture.addListener(future -> {
93                          if (future.isFailed()) {
94                              localHandshakePromise.tryFailure(future.cause());
95                              ctx.fireChannelExceptionCaught(future.cause());
96                          } else {
97                              localHandshakePromise.trySuccess(null);
98                              ctx.fireChannelInboundEvent(
99                                      new WebSocketServerHandshakeCompletionEvent(handshaker.version(),
100                                             req.uri(), req.headers(), handshaker.selectedSubprotocol()));
101                         }
102                         ctx.pipeline().remove(this);
103                     });
104                     applyHandshakeTimeout(ctx);
105                 }
106             } finally {
107                 if (req instanceof AutoCloseable) {
108                     ((AutoCloseable) req).close();
109                 }
110             }
111         } else if (!isWebSocketPath) {
112             ctx.fireChannelRead(msg);
113         } else {
114             ctx.fireChannelRead(msg);
115         }
116     }
117 
118     private boolean isWebSocketPath(HttpRequest req) {
119         String websocketPath = serverConfig.websocketPath();
120         String uri = req.uri();
121         boolean checkStartUri = uri.startsWith(websocketPath);
122         boolean checkNextUri = "/".equals(websocketPath) || checkNextUri(uri, websocketPath);
123         return serverConfig.checkStartsWith() ? (checkStartUri && checkNextUri) : uri.equals(websocketPath);
124     }
125 
126     private boolean checkNextUri(String uri, String websocketPath) {
127         int len = websocketPath.length();
128         if (uri.length() > len) {
129             char nextUri = uri.charAt(len);
130             return nextUri == '/' || nextUri == '?';
131         }
132         return true;
133     }
134 
135     private static void sendHttpResponse(ChannelHandlerContext ctx, HttpRequest req, HttpResponse res) {
136         Future<Void> f = ctx.writeAndFlush(res);
137         if (!isKeepAlive(req) || res.status().code() != 200) {
138             f.addListener(ctx, ChannelFutureListeners.CLOSE);
139         }
140     }
141 
142     private static String getWebSocketLocation(ChannelPipeline cp, HttpRequest req, String path) {
143         String protocol = "ws";
144         if (cp.get(SslHandler.class) != null) {
145             // SSL in use so use Secure WebSockets
146             protocol = "wss";
147         }
148         String host = req.headers().get(HttpHeaderNames.HOST);
149         return protocol + "://" + host + path;
150     }
151 
152     private void applyHandshakeTimeout(ChannelHandlerContext ctx) {
153         Promise<Void> localHandshakePromise = handshakePromise;
154         final long handshakeTimeoutMillis = serverConfig.handshakeTimeoutMillis();
155         if (handshakeTimeoutMillis <= 0 || localHandshakePromise.isDone()) {
156             return;
157         }
158 
159         final Future<?> timeoutFuture = ctx.executor().schedule(() -> {
160             if (localHandshakePromise.isDone()) {
161                 return;
162             }
163             WebSocketHandshakeException exception = new WebSocketHandshakeTimeoutException(
164                     "handshake timed out after " + handshakeTimeoutMillis + "ms");
165             if (localHandshakePromise.tryFailure(exception)) {
166                 ctx.flush()
167                    .fireChannelInboundEvent(new WebSocketServerHandshakeCompletionEvent(exception))
168                    .close();
169             }
170         }, handshakeTimeoutMillis, TimeUnit.MILLISECONDS);
171 
172         // Cancel the handshake timeout when handshake is finished.
173         localHandshakePromise.asFuture().addListener(f -> timeoutFuture.cancel());
174     }
175 }