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         super(webSocketURL, version, subprotocol, customHeaders, maxFramePayloadLength, forceCloseTimeoutMillis,
116                 absoluteUpgradeUrl);
117     }
118 
119     /**
120      * <p>
121      * Sends the opening request to the server:
122      * </p>
123      *
124      * <pre>
125      * GET /demo HTTP/1.1
126      * Upgrade: WebSocket
127      * Connection: Upgrade
128      * Host: example.com
129      * Origin: http://example.com
130      * Sec-WebSocket-Key1: 4 @1  46546xW%0l 1 5
131      * Sec-WebSocket-Key2: 12998 5 Y3 1  .P00
132      *
133      * ^n:ds[4U
134      * </pre>
135      *
136      */
137     @Override
138     protected FullHttpRequest newHandshakeRequest() {
139         // Make keys
140         int spaces1 = WebSocketUtil.randomNumber(1, 12);
141         int spaces2 = WebSocketUtil.randomNumber(1, 12);
142 
143         int max1 = Integer.MAX_VALUE / spaces1;
144         int max2 = Integer.MAX_VALUE / spaces2;
145 
146         int number1 = WebSocketUtil.randomNumber(0, max1);
147         int number2 = WebSocketUtil.randomNumber(0, max2);
148 
149         int product1 = number1 * spaces1;
150         int product2 = number2 * spaces2;
151 
152         String key1 = Integer.toString(product1);
153         String key2 = Integer.toString(product2);
154 
155         key1 = insertRandomCharacters(key1);
156         key2 = insertRandomCharacters(key2);
157 
158         key1 = insertSpaces(key1, spaces1);
159         key2 = insertSpaces(key2, spaces2);
160 
161         byte[] key3 = WebSocketUtil.randomBytes(8);
162 
163         ByteBuffer buffer = ByteBuffer.allocate(4);
164         buffer.putInt(number1);
165         byte[] number1Array = buffer.array();
166         buffer = ByteBuffer.allocate(4);
167         buffer.putInt(number2);
168         byte[] number2Array = buffer.array();
169 
170         byte[] challenge = new byte[16];
171         System.arraycopy(number1Array, 0, challenge, 0, 4);
172         System.arraycopy(number2Array, 0, challenge, 4, 4);
173         System.arraycopy(key3, 0, challenge, 8, 8);
174         expectedChallengeResponseBytes = Unpooled.wrappedBuffer(WebSocketUtil.md5(challenge));
175 
176         URI wsURL = uri();
177 
178         // Format request
179         FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, upgradeUrl(wsURL),
180                 Unpooled.wrappedBuffer(key3));
181         HttpHeaders headers = request.headers();
182 
183         if (customHeaders != null) {
184             headers.add(customHeaders);
185         }
186 
187         headers.set(HttpHeaderNames.UPGRADE, HttpHeaderValues.WEBSOCKET)
188                .set(HttpHeaderNames.CONNECTION, HttpHeaderValues.UPGRADE)
189                .set(HttpHeaderNames.HOST, websocketHostValue(wsURL))
190                .set(HttpHeaderNames.SEC_WEBSOCKET_KEY1, key1)
191                .set(HttpHeaderNames.SEC_WEBSOCKET_KEY2, key2);
192 
193         if (!headers.contains(HttpHeaderNames.ORIGIN)) {
194             headers.set(HttpHeaderNames.ORIGIN, websocketOriginValue(wsURL));
195         }
196 
197         String expectedSubprotocol = expectedSubprotocol();
198         if (expectedSubprotocol != null && !expectedSubprotocol.isEmpty()) {
199             headers.set(HttpHeaderNames.SEC_WEBSOCKET_PROTOCOL, expectedSubprotocol);
200         }
201 
202         // Set Content-Length to workaround some known defect.
203         // See also: https://www.ietf.org/mail-archive/web/hybi/current/msg02149.html
204         headers.set(HttpHeaderNames.CONTENT_LENGTH, key3.length);
205         return request;
206     }
207 
208     /**
209      * <p>
210      * Process server response:
211      * </p>
212      *
213      * <pre>
214      * HTTP/1.1 101 WebSocket Protocol Handshake
215      * Upgrade: WebSocket
216      * Connection: Upgrade
217      * Sec-WebSocket-Origin: http://example.com
218      * Sec-WebSocket-Location: ws://example.com/demo
219      * Sec-WebSocket-Protocol: sample
220      *
221      * 8jKS'y:G*Co,Wxa-
222      * </pre>
223      *
224      * @param response
225      *            HTTP response returned from the server for the request sent by beginOpeningHandshake00().
226      * @throws WebSocketHandshakeException
227      */
228     @Override
229     protected void verify(FullHttpResponse response) {
230         HttpResponseStatus status = response.status();
231         if (!HttpResponseStatus.SWITCHING_PROTOCOLS.equals(status)) {
232             throw new WebSocketClientHandshakeException("Invalid handshake response getStatus: " + status, response);
233         }
234 
235         HttpHeaders headers = response.headers();
236         CharSequence upgrade = headers.get(HttpHeaderNames.UPGRADE);
237         if (!HttpHeaderValues.WEBSOCKET.contentEqualsIgnoreCase(upgrade)) {
238             throw new WebSocketClientHandshakeException("Invalid handshake response upgrade: " + upgrade, response);
239         }
240 
241         if (!headers.containsValue(HttpHeaderNames.CONNECTION, HttpHeaderValues.UPGRADE, true)) {
242             throw new WebSocketClientHandshakeException("Invalid handshake response connection: "
243                     + headers.get(HttpHeaderNames.CONNECTION), response);
244         }
245 
246         ByteBuf challenge = response.content();
247         if (!challenge.equals(expectedChallengeResponseBytes)) {
248             throw new WebSocketClientHandshakeException("Invalid challenge", response);
249         }
250     }
251 
252     private static String insertRandomCharacters(String key) {
253         int count = WebSocketUtil.randomNumber(1, 12);
254 
255         char[] randomChars = new char[count];
256         int randCount = 0;
257         while (randCount < count) {
258             int rand = PlatformDependent.threadLocalRandom().nextInt(0x7e) + 0x21;
259             if (0x21 < rand && rand < 0x2f || 0x3a < rand && rand < 0x7e) {
260                 randomChars[randCount] = (char) rand;
261                 randCount += 1;
262             }
263         }
264 
265         for (int i = 0; i < count; i++) {
266             int split = WebSocketUtil.randomNumber(0, key.length());
267             String part1 = key.substring(0, split);
268             String part2 = key.substring(split);
269             key = part1 + randomChars[i] + part2;
270         }
271 
272         return key;
273     }
274 
275     private static String insertSpaces(String key, int spaces) {
276         for (int i = 0; i < spaces; i++) {
277             int split = WebSocketUtil.randomNumber(1, key.length() - 1);
278             String part1 = key.substring(0, split);
279             String part2 = key.substring(split);
280             key = part1 + ' ' + part2;
281         }
282 
283         return key;
284     }
285 
286     @Override
287     protected WebSocketFrameDecoder newWebsocketDecoder() {
288         return new WebSocket00FrameDecoder(maxFramePayloadLength());
289     }
290 
291     @Override
292     protected WebSocketFrameEncoder newWebSocketEncoder() {
293         return new WebSocket00FrameEncoder();
294     }
295 
296     @Override
297     public WebSocketClientHandshaker00 setForceCloseTimeoutMillis(long forceCloseTimeoutMillis) {
298         super.setForceCloseTimeoutMillis(forceCloseTimeoutMillis);
299         return this;
300     }
301 
302 }