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