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 io.netty.buffer.ByteBuf;
19  import io.netty.buffer.Unpooled;
20  import io.netty.channel.Channel;
21  import io.netty.channel.ChannelFuture;
22  import io.netty.channel.ChannelHandlerContext;
23  import io.netty.channel.ChannelPromise;
24  import io.netty.handler.codec.http.DefaultFullHttpResponse;
25  import io.netty.handler.codec.http.FullHttpRequest;
26  import io.netty.handler.codec.http.FullHttpResponse;
27  import io.netty.handler.codec.http.HttpHeaderNames;
28  import io.netty.handler.codec.http.HttpHeaderValues;
29  import io.netty.handler.codec.http.HttpHeaders;
30  import io.netty.handler.codec.http.HttpMethod;
31  import io.netty.handler.codec.http.HttpResponseStatus;
32  
33  import java.util.regex.Pattern;
34  
35  import static io.netty.handler.codec.http.HttpMethod.GET;
36  import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
37  
38  /**
39   * <p>
40   * Performs server side opening and closing handshakes for web socket specification version <a
41   * href="https://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-00" >draft-ietf-hybi-thewebsocketprotocol-
42   * 00</a>
43   * </p>
44   * <p>
45   * A very large portion of this code was taken from the Netty 3.2 HTTP example.
46   * </p>
47   */
48  public class WebSocketServerHandshaker00 extends WebSocketServerHandshaker {
49  
50      private static final Pattern BEGINNING_DIGIT = Pattern.compile("[^0-9]");
51      private static final Pattern BEGINNING_SPACE = Pattern.compile("[^ ]");
52  
53      /**
54       * Constructor specifying the destination web socket location
55       *
56       * @param webSocketURL
57       *            URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be
58       *            sent to this URL.
59       * @param subprotocols
60       *            CSV of supported protocols
61       * @param maxFramePayloadLength
62       *            Maximum allowable frame payload length. Setting this value to your application's requirement may
63       *            reduce denial of service attacks using long data frames.
64       */
65      public WebSocketServerHandshaker00(String webSocketURL, String subprotocols, int maxFramePayloadLength) {
66          this(webSocketURL, subprotocols, WebSocketDecoderConfig.newBuilder()
67              .maxFramePayloadLength(maxFramePayloadLength)
68              .build());
69      }
70  
71      /**
72       * Constructor specifying the destination web socket location
73       *
74       * @param webSocketURL
75       *            URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be
76       *            sent to this URL.
77       * @param subprotocols
78       *            CSV of supported protocols
79       * @param decoderConfig
80       *            Frames decoder configuration.
81       */
82      public WebSocketServerHandshaker00(String webSocketURL, String subprotocols, WebSocketDecoderConfig decoderConfig) {
83          super(WebSocketVersion.V00, webSocketURL, subprotocols, decoderConfig);
84      }
85  
86      WebSocketServerHandshaker00(boolean isSecure, String host, String path,
87                                  String subprotocols, WebSocketDecoderConfig decoderConfig) {
88          super(WebSocketVersion.V00, isSecure, host, path, subprotocols, decoderConfig);
89      }
90  
91      /**
92       * <p>
93       * Handle the web socket handshake for the web socket specification <a href=
94       * "https://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-00">HyBi version 0</a> and lower. This standard
95       * is really a rehash of <a href="https://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76" >hixie-76</a> and
96       * <a href="https://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-75" >hixie-75</a>.
97       * </p>
98       *
99       * <p>
100      * Browser request to the server:
101      * </p>
102      *
103      * <pre>
104      * GET /demo HTTP/1.1
105      * Upgrade: WebSocket
106      * Connection: Upgrade
107      * Host: example.com
108      * Origin: http://example.com
109      * Sec-WebSocket-Protocol: chat, sample
110      * Sec-WebSocket-Key1: 4 @1  46546xW%0l 1 5
111      * Sec-WebSocket-Key2: 12998 5 Y3 1  .P00
112      *
113      * ^n:ds[4U
114      * </pre>
115      *
116      * <p>
117      * Server response:
118      * </p>
119      *
120      * <pre>
121      * HTTP/1.1 101 WebSocket Protocol Handshake
122      * Upgrade: WebSocket
123      * Connection: Upgrade
124      * Sec-WebSocket-Origin: http://example.com
125      * Sec-WebSocket-Location: ws://example.com/demo
126      * Sec-WebSocket-Protocol: sample
127      *
128      * 8jKS'y:G*Co,Wxa-
129      * </pre>
130      */
131     @Override
132     protected FullHttpResponse newHandshakeResponse(FullHttpRequest req, HttpHeaders headers) {
133         HttpMethod method = req.method();
134         if (!GET.equals(method)) {
135             throw new WebSocketServerHandshakeException("Invalid WebSocket handshake method: " + method, req);
136         }
137 
138         // Serve the WebSocket handshake request.
139         if (!req.headers().containsValue(HttpHeaderNames.CONNECTION, HttpHeaderValues.UPGRADE, true)
140                 || !HttpHeaderValues.WEBSOCKET.contentEqualsIgnoreCase(req.headers().get(HttpHeaderNames.UPGRADE))) {
141             throw new WebSocketServerHandshakeException("not a WebSocket handshake request: missing upgrade", req);
142         }
143 
144         // Hixie 75 does not contain these headers while Hixie 76 does
145         boolean isHixie76 = req.headers().contains(HttpHeaderNames.SEC_WEBSOCKET_KEY1) &&
146                             req.headers().contains(HttpHeaderNames.SEC_WEBSOCKET_KEY2);
147 
148         String origin = req.headers().get(HttpHeaderNames.ORIGIN);
149         //throw before allocating FullHttpResponse
150         if (origin == null && !isHixie76) {
151             throw new WebSocketServerHandshakeException("Missing origin header, got only " + req.headers().names(),
152                                                         req);
153         }
154 
155         // Create the WebSocket handshake response.
156         FullHttpResponse res = new DefaultFullHttpResponse(HTTP_1_1, new HttpResponseStatus(101,
157                 isHixie76 ? "WebSocket Protocol Handshake" : "Web Socket Protocol Handshake"),
158                 req.content().alloc().buffer(0));
159         if (headers != null) {
160             res.headers().add(headers);
161         }
162 
163         res.headers().set(HttpHeaderNames.UPGRADE, HttpHeaderValues.WEBSOCKET)
164                      .set(HttpHeaderNames.CONNECTION, HttpHeaderValues.UPGRADE);
165 
166         // Fill in the headers and contents depending on handshake getMethod.
167         if (isHixie76) {
168             // New handshake getMethod with a challenge:
169             res.headers().add(HttpHeaderNames.SEC_WEBSOCKET_ORIGIN, origin);
170             res.headers().add(HttpHeaderNames.SEC_WEBSOCKET_LOCATION, uri());
171 
172             String subprotocols = req.headers().get(HttpHeaderNames.SEC_WEBSOCKET_PROTOCOL);
173             if (subprotocols != null) {
174                 String selectedSubprotocol = selectSubprotocol(subprotocols);
175                 if (selectedSubprotocol == null) {
176                     if (logger.isDebugEnabled()) {
177                         logger.debug("Requested subprotocol(s) not supported: {}", subprotocols);
178                     }
179                 } else {
180                     res.headers().set(HttpHeaderNames.SEC_WEBSOCKET_PROTOCOL, selectedSubprotocol);
181                 }
182             }
183 
184             // Calculate the answer of the challenge.
185             String key1 = req.headers().get(HttpHeaderNames.SEC_WEBSOCKET_KEY1);
186             String key2 = req.headers().get(HttpHeaderNames.SEC_WEBSOCKET_KEY2);
187             int a = (int) (Long.parseLong(BEGINNING_DIGIT.matcher(key1).replaceAll("")) /
188                            BEGINNING_SPACE.matcher(key1).replaceAll("").length());
189             int b = (int) (Long.parseLong(BEGINNING_DIGIT.matcher(key2).replaceAll("")) /
190                            BEGINNING_SPACE.matcher(key2).replaceAll("").length());
191             long c = req.content().readLong();
192             ByteBuf input = Unpooled.wrappedBuffer(new byte[16]).setIndex(0, 0);
193             input.writeInt(a);
194             input.writeInt(b);
195             input.writeLong(c);
196             res.content().writeBytes(WebSocketUtil.md5(input.array()));
197         } else {
198             // Old Hixie 75 handshake getMethod with no challenge:
199             res.headers().add(HttpHeaderNames.WEBSOCKET_ORIGIN, origin);
200             res.headers().add(HttpHeaderNames.WEBSOCKET_LOCATION, uri());
201 
202             String protocol = req.headers().get(HttpHeaderNames.WEBSOCKET_PROTOCOL);
203             if (protocol != null) {
204                 res.headers().set(HttpHeaderNames.WEBSOCKET_PROTOCOL, selectSubprotocol(protocol));
205             }
206         }
207         return res;
208     }
209 
210     /**
211      * Echo back the closing frame
212      *
213      * @param channel
214      *            the {@link Channel} to use.
215      * @param frame
216      *            Web Socket frame that was received.
217      * @param promise
218      *            the {@link ChannelPromise} to be notified when the closing handshake is done.
219      */
220     @Override
221     public ChannelFuture close(Channel channel, CloseWebSocketFrame frame, ChannelPromise promise) {
222         return channel.writeAndFlush(frame, promise);
223     }
224 
225     /**
226      * Echo back the closing frame
227      *
228      * @param ctx
229      *            the {@link ChannelHandlerContext} to use.
230      * @param frame
231      *            Closing Frame that was received.
232      * @param promise
233      *            the {@link ChannelPromise} to be notified when the closing handshake is done.
234      */
235     @Override
236     public ChannelFuture close(ChannelHandlerContext ctx, CloseWebSocketFrame frame,
237                                ChannelPromise promise) {
238         return ctx.writeAndFlush(frame, promise);
239     }
240 
241     @Override
242     protected WebSocketFrameDecoder newWebsocketDecoder() {
243         return new WebSocket00FrameDecoder(decoderConfig());
244     }
245 
246     @Override
247     protected WebSocketFrameEncoder newWebSocketEncoder() {
248         return new WebSocket00FrameEncoder();
249     }
250 }