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.ByteBuf;
19  import io.netty.buffer.Unpooled;
20  import io.netty.handler.codec.http.DefaultFullHttpRequest;
21  import io.netty.handler.codec.http.FullHttpRequest;
22  import io.netty.handler.codec.http.FullHttpResponse;
23  import io.netty.handler.codec.http.HttpHeaderNames;
24  import io.netty.handler.codec.http.HttpHeaderValues;
25  import io.netty.handler.codec.http.HttpHeaders;
26  import io.netty.handler.codec.http.HttpMethod;
27  import io.netty.handler.codec.http.HttpResponseStatus;
28  import io.netty.handler.codec.http.HttpVersion;
29  import io.netty.util.internal.PlatformDependent;
30  
31  import java.net.URI;
32  import java.nio.ByteBuffer;
33  
34  /**
35   * <p>
36   * Performs client side opening and closing handshakes for web socket specification version <a
37   * href="https://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-00" >draft-ietf-hybi-thewebsocketprotocol-
38   * 00</a>
39   * </p>
40   * <p>
41   * A very large portion of this code was taken from the Netty 3.2 HTTP example.
42   * </p>
43   */
44  public class WebSocketClientHandshaker00 extends WebSocketClientHandshaker {
45  
46      private ByteBuf expectedChallengeResponseBytes;
47  
48      /**
49       * Creates a new instance with the specified destination WebSocket location and version to initiate.
50       *
51       * @param webSocketURL
52       *            URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be
53       *            sent to this URL.
54       * @param version
55       *            Version of web socket specification to use to connect to the server
56       * @param subprotocol
57       *            Sub protocol request sent to the server.
58       * @param customHeaders
59       *            Map of custom headers to add to the client request
60       * @param maxFramePayloadLength
61       *            Maximum length of a frame's payload
62       */
63      public WebSocketClientHandshaker00(URI webSocketURL, WebSocketVersion version, String subprotocol,
64              HttpHeaders customHeaders, int maxFramePayloadLength) {
65          this(webSocketURL, version, subprotocol, customHeaders, maxFramePayloadLength,
66                  DEFAULT_FORCE_CLOSE_TIMEOUT_MILLIS);
67      }
68  
69      /**
70       * Creates a new instance with the specified destination WebSocket location and version to initiate.
71       *
72       * @param webSocketURL
73       *            URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be
74       *            sent to this URL.
75       * @param version
76       *            Version of web socket specification to use to connect to the server
77       * @param subprotocol
78       *            Sub protocol request sent to the server.
79       * @param customHeaders
80       *            Map of custom headers to add to the client request
81       * @param maxFramePayloadLength
82       *            Maximum length of a frame's payload
83       * @param forceCloseTimeoutMillis
84       *            Close the connection if it was not closed by the server after timeout specified
85       */
86      public WebSocketClientHandshaker00(URI webSocketURL, WebSocketVersion version, String subprotocol,
87                                         HttpHeaders customHeaders, int maxFramePayloadLength,
88                                         long forceCloseTimeoutMillis) {
89          this(webSocketURL, version, subprotocol, customHeaders, maxFramePayloadLength, forceCloseTimeoutMillis, false);
90      }
91  
92      /**
93       * Creates a new instance with the specified destination WebSocket location and version to initiate.
94       *
95       * @param webSocketURL
96       *            URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be
97       *            sent to this URL.
98       * @param version
99       *            Version of web socket specification to use to connect to the server
100      * @param subprotocol
101      *            Sub protocol request sent to the server.
102      * @param customHeaders
103      *            Map of custom headers to add to the client request
104      * @param maxFramePayloadLength
105      *            Maximum length of a frame's payload
106      * @param forceCloseTimeoutMillis
107      *            Close the connection if it was not closed by the server after timeout specified
108      * @param  absoluteUpgradeUrl
109      *            Use an absolute url for the Upgrade request, typically when connecting through an HTTP proxy over
110      *            clear HTTP
111      */
112     WebSocketClientHandshaker00(URI webSocketURL, WebSocketVersion version, String subprotocol,
113             HttpHeaders customHeaders, int maxFramePayloadLength,
114             long forceCloseTimeoutMillis, boolean absoluteUpgradeUrl) {
115         this(webSocketURL, version, subprotocol, customHeaders, maxFramePayloadLength, forceCloseTimeoutMillis,
116                 absoluteUpgradeUrl, true);
117     }
118 
119     /**
120      * Creates a new instance with the specified destination WebSocket location and version to initiate.
121      *
122      * @param webSocketURL
123      *            URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be
124      *            sent to this URL.
125      * @param version
126      *            Version of web socket specification to use to connect to the server
127      * @param subprotocol
128      *            Sub protocol request sent to the server.
129      * @param customHeaders
130      *            Map of custom headers to add to the client request
131      * @param maxFramePayloadLength
132      *            Maximum length of a frame's payload
133      * @param forceCloseTimeoutMillis
134      *            Close the connection if it was not closed by the server after timeout specified
135      * @param  absoluteUpgradeUrl
136      *            Use an absolute url for the Upgrade request, typically when connecting through an HTTP proxy over
137      *            clear HTTP
138      * @param generateOriginHeader
139      *            Allows to generate the `Origin` header value for handshake request
140      *            according to the given webSocketURL
141      */
142     WebSocketClientHandshaker00(URI webSocketURL, WebSocketVersion version, String subprotocol,
143                                 HttpHeaders customHeaders, int maxFramePayloadLength,
144                                 long forceCloseTimeoutMillis, boolean absoluteUpgradeUrl,
145                                 boolean generateOriginHeader) {
146         super(webSocketURL, version, subprotocol, customHeaders, maxFramePayloadLength, forceCloseTimeoutMillis,
147                 absoluteUpgradeUrl, generateOriginHeader);
148     }
149 
150     /**
151      * <p>
152      * Sends the opening request to the server:
153      * </p>
154      *
155      * <pre>
156      * GET /demo HTTP/1.1
157      * Upgrade: WebSocket
158      * Connection: Upgrade
159      * Host: example.com
160      * Origin: http://example.com
161      * Sec-WebSocket-Key1: 4 @1  46546xW%0l 1 5
162      * Sec-WebSocket-Key2: 12998 5 Y3 1  .P00
163      *
164      * ^n:ds[4U
165      * </pre>
166      *
167      */
168     @Override
169     protected FullHttpRequest newHandshakeRequest() {
170         // Make keys
171         int spaces1 = WebSocketUtil.randomNumber(1, 12);
172         int spaces2 = WebSocketUtil.randomNumber(1, 12);
173 
174         int max1 = Integer.MAX_VALUE / spaces1;
175         int max2 = Integer.MAX_VALUE / spaces2;
176 
177         int number1 = WebSocketUtil.randomNumber(0, max1);
178         int number2 = WebSocketUtil.randomNumber(0, max2);
179 
180         int product1 = number1 * spaces1;
181         int product2 = number2 * spaces2;
182 
183         String key1 = Integer.toString(product1);
184         String key2 = Integer.toString(product2);
185 
186         key1 = insertRandomCharacters(key1);
187         key2 = insertRandomCharacters(key2);
188 
189         key1 = insertSpaces(key1, spaces1);
190         key2 = insertSpaces(key2, spaces2);
191 
192         byte[] key3 = WebSocketUtil.randomBytes(8);
193 
194         ByteBuffer buffer = ByteBuffer.allocate(4);
195         buffer.putInt(number1);
196         byte[] number1Array = buffer.array();
197         buffer = ByteBuffer.allocate(4);
198         buffer.putInt(number2);
199         byte[] number2Array = buffer.array();
200 
201         byte[] challenge = new byte[16];
202         System.arraycopy(number1Array, 0, challenge, 0, 4);
203         System.arraycopy(number2Array, 0, challenge, 4, 4);
204         System.arraycopy(key3, 0, challenge, 8, 8);
205         expectedChallengeResponseBytes = Unpooled.wrappedBuffer(WebSocketUtil.md5(challenge));
206 
207         URI wsURL = uri();
208 
209         // Format request
210         FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, upgradeUrl(wsURL),
211                 Unpooled.wrappedBuffer(key3));
212         HttpHeaders headers = request.headers();
213 
214         if (customHeaders != null) {
215             headers.add(customHeaders);
216             if (!headers.contains(HttpHeaderNames.HOST)) {
217                 // Only add HOST header if customHeaders did not contain it.
218                 //
219                 // See https://github.com/netty/netty/issues/10101
220                 headers.set(HttpHeaderNames.HOST, websocketHostValue(wsURL));
221             }
222         } else {
223             headers.set(HttpHeaderNames.HOST, websocketHostValue(wsURL));
224         }
225 
226         headers.set(HttpHeaderNames.UPGRADE, HttpHeaderValues.WEBSOCKET)
227                .set(HttpHeaderNames.CONNECTION, HttpHeaderValues.UPGRADE)
228                .set(HttpHeaderNames.SEC_WEBSOCKET_KEY1, key1)
229                .set(HttpHeaderNames.SEC_WEBSOCKET_KEY2, key2);
230 
231         if (generateOriginHeader && !headers.contains(HttpHeaderNames.ORIGIN)) {
232             headers.set(HttpHeaderNames.ORIGIN, websocketOriginValue(wsURL));
233         }
234 
235         String expectedSubprotocol = expectedSubprotocol();
236         if (expectedSubprotocol != null && !expectedSubprotocol.isEmpty()) {
237             headers.set(HttpHeaderNames.SEC_WEBSOCKET_PROTOCOL, expectedSubprotocol);
238         }
239 
240         // Set Content-Length to workaround some known defect.
241         // See also: https://www.ietf.org/mail-archive/web/hybi/current/msg02149.html
242         headers.set(HttpHeaderNames.CONTENT_LENGTH, key3.length);
243         return request;
244     }
245 
246     /**
247      * <p>
248      * Process server response:
249      * </p>
250      *
251      * <pre>
252      * HTTP/1.1 101 WebSocket Protocol Handshake
253      * Upgrade: WebSocket
254      * Connection: Upgrade
255      * Sec-WebSocket-Origin: http://example.com
256      * Sec-WebSocket-Location: ws://example.com/demo
257      * Sec-WebSocket-Protocol: sample
258      *
259      * 8jKS'y:G*Co,Wxa-
260      * </pre>
261      *
262      * @param response
263      *            HTTP response returned from the server for the request sent by beginOpeningHandshake00().
264      * @throws WebSocketHandshakeException
265      */
266     @Override
267     protected void verify(FullHttpResponse response) {
268         HttpResponseStatus status = response.status();
269         if (!HttpResponseStatus.SWITCHING_PROTOCOLS.equals(status)) {
270             throw new WebSocketClientHandshakeException("Invalid handshake response getStatus: " + status, response);
271         }
272 
273         HttpHeaders headers = response.headers();
274         CharSequence upgrade = headers.get(HttpHeaderNames.UPGRADE);
275         if (!HttpHeaderValues.WEBSOCKET.contentEqualsIgnoreCase(upgrade)) {
276             throw new WebSocketClientHandshakeException("Invalid handshake response upgrade: " + upgrade, response);
277         }
278 
279         if (!headers.containsValue(HttpHeaderNames.CONNECTION, HttpHeaderValues.UPGRADE, true)) {
280             throw new WebSocketClientHandshakeException("Invalid handshake response connection: "
281                     + headers.get(HttpHeaderNames.CONNECTION), response);
282         }
283 
284         ByteBuf challenge = response.content();
285         if (!challenge.equals(expectedChallengeResponseBytes)) {
286             throw new WebSocketClientHandshakeException("Invalid challenge", response);
287         }
288     }
289 
290     private static String insertRandomCharacters(String key) {
291         int count = WebSocketUtil.randomNumber(1, 12);
292 
293         char[] randomChars = new char[count];
294         int randCount = 0;
295         while (randCount < count) {
296             int rand = PlatformDependent.threadLocalRandom().nextInt(0x7e) + 0x21;
297             if (0x21 < rand && rand < 0x2f || 0x3a < rand && rand < 0x7e) {
298                 randomChars[randCount] = (char) rand;
299                 randCount += 1;
300             }
301         }
302 
303         for (int i = 0; i < count; i++) {
304             int split = WebSocketUtil.randomNumber(0, key.length());
305             String part1 = key.substring(0, split);
306             String part2 = key.substring(split);
307             key = part1 + randomChars[i] + part2;
308         }
309 
310         return key;
311     }
312 
313     private static String insertSpaces(String key, int spaces) {
314         for (int i = 0; i < spaces; i++) {
315             int split = WebSocketUtil.randomNumber(1, key.length() - 1);
316             String part1 = key.substring(0, split);
317             String part2 = key.substring(split);
318             key = part1 + ' ' + part2;
319         }
320 
321         return key;
322     }
323 
324     @Override
325     protected WebSocketFrameDecoder newWebsocketDecoder() {
326         return new WebSocket00FrameDecoder(maxFramePayloadLength());
327     }
328 
329     @Override
330     protected WebSocketFrameEncoder newWebSocketEncoder() {
331         return new WebSocket00FrameEncoder();
332     }
333 
334     @Override
335     public WebSocketClientHandshaker00 setForceCloseTimeoutMillis(long forceCloseTimeoutMillis) {
336         super.setForceCloseTimeoutMillis(forceCloseTimeoutMillis);
337         return this;
338     }
339 
340 }