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.HttpHeaders;
24  import io.netty.handler.codec.http.HttpHeaders.Names;
25  import io.netty.handler.codec.http.HttpHeaders.Values;
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  
30  import java.net.URI;
31  import java.nio.ByteBuffer;
32  
33  /**
34   * <p>
35   * Performs client side opening and closing handshakes for web socket specification version <a
36   * href="http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-00" >draft-ietf-hybi-thewebsocketprotocol-
37   * 00</a>
38   * </p>
39   * <p>
40   * A very large portion of this code was taken from the Netty 3.2 HTTP example.
41   * </p>
42   */
43  public class WebSocketClientHandshaker00 extends WebSocketClientHandshaker {
44  
45      private ByteBuf expectedChallengeResponseBytes;
46  
47      /**
48       * Constructor specifying the destination web socket location and version to initiate
49       *
50       * @param webSocketURL
51       *            URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be
52       *            sent to this URL.
53       * @param version
54       *            Version of web socket specification to use to connect to the server
55       * @param subprotocol
56       *            Sub protocol request sent to the server.
57       * @param customHeaders
58       *            Map of custom headers to add to the client request
59       * @param maxFramePayloadLength
60       *            Maximum length of a frame's payload
61       */
62      public WebSocketClientHandshaker00(URI webSocketURL, WebSocketVersion version, String subprotocol,
63              HttpHeaders customHeaders, int maxFramePayloadLength) {
64          super(webSocketURL, version, subprotocol, customHeaders, maxFramePayloadLength);
65      }
66  
67      /**
68       * <p>
69       * Sends the opening request to the server:
70       * </p>
71       *
72       * <pre>
73       * GET /demo HTTP/1.1
74       * Upgrade: WebSocket
75       * Connection: Upgrade
76       * Host: example.com
77       * Origin: http://example.com
78       * Sec-WebSocket-Key1: 4 @1  46546xW%0l 1 5
79       * Sec-WebSocket-Key2: 12998 5 Y3 1  .P00
80       *
81       * ^n:ds[4U
82       * </pre>
83       *
84       */
85      @Override
86      protected FullHttpRequest newHandshakeRequest() {
87          // Make keys
88          int spaces1 = WebSocketUtil.randomNumber(1, 12);
89          int spaces2 = WebSocketUtil.randomNumber(1, 12);
90  
91          int max1 = Integer.MAX_VALUE / spaces1;
92          int max2 = Integer.MAX_VALUE / spaces2;
93  
94          int number1 = WebSocketUtil.randomNumber(0, max1);
95          int number2 = WebSocketUtil.randomNumber(0, max2);
96  
97          int product1 = number1 * spaces1;
98          int product2 = number2 * spaces2;
99  
100         String key1 = Integer.toString(product1);
101         String key2 = Integer.toString(product2);
102 
103         key1 = insertRandomCharacters(key1);
104         key2 = insertRandomCharacters(key2);
105 
106         key1 = insertSpaces(key1, spaces1);
107         key2 = insertSpaces(key2, spaces2);
108 
109         byte[] key3 = WebSocketUtil.randomBytes(8);
110 
111         ByteBuffer buffer = ByteBuffer.allocate(4);
112         buffer.putInt(number1);
113         byte[] number1Array = buffer.array();
114         buffer = ByteBuffer.allocate(4);
115         buffer.putInt(number2);
116         byte[] number2Array = buffer.array();
117 
118         byte[] challenge = new byte[16];
119         System.arraycopy(number1Array, 0, challenge, 0, 4);
120         System.arraycopy(number2Array, 0, challenge, 4, 4);
121         System.arraycopy(key3, 0, challenge, 8, 8);
122         expectedChallengeResponseBytes = Unpooled.wrappedBuffer(WebSocketUtil.md5(challenge));
123 
124         // Get path
125         URI wsURL = uri();
126         String path = rawPath(wsURL);
127 
128         // Format request
129         FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, path);
130         HttpHeaders headers = request.headers();
131 
132         headers.add(HttpHeaders.Names.UPGRADE, HttpHeaders.Values.WEBSOCKET)
133                .add(HttpHeaders.Names.CONNECTION, HttpHeaders.Values.UPGRADE)
134                .add(HttpHeaders.Names.HOST, websocketHostValue(wsURL))
135                .add(HttpHeaders.Names.ORIGIN, websocketOriginValue(wsURL))
136                .add(HttpHeaders.Names.SEC_WEBSOCKET_KEY1, key1)
137                .add(HttpHeaders.Names.SEC_WEBSOCKET_KEY2, key2);
138 
139         String expectedSubprotocol = expectedSubprotocol();
140         if (expectedSubprotocol != null && !expectedSubprotocol.isEmpty()) {
141             headers.add(Names.SEC_WEBSOCKET_PROTOCOL, expectedSubprotocol);
142         }
143 
144         if (customHeaders != null) {
145             headers.add(customHeaders);
146         }
147 
148         // Set Content-Length to workaround some known defect.
149         // See also: http://www.ietf.org/mail-archive/web/hybi/current/msg02149.html
150         headers.set(Names.CONTENT_LENGTH, key3.length);
151         request.content().writeBytes(key3);
152         return request;
153     }
154 
155     /**
156      * <p>
157      * Process server response:
158      * </p>
159      *
160      * <pre>
161      * HTTP/1.1 101 WebSocket Protocol Handshake
162      * Upgrade: WebSocket
163      * Connection: Upgrade
164      * Sec-WebSocket-Origin: http://example.com
165      * Sec-WebSocket-Location: ws://example.com/demo
166      * Sec-WebSocket-Protocol: sample
167      *
168      * 8jKS'y:G*Co,Wxa-
169      * </pre>
170      *
171      * @param response
172      *            HTTP response returned from the server for the request sent by beginOpeningHandshake00().
173      * @throws WebSocketHandshakeException
174      */
175     @Override
176     protected void verify(FullHttpResponse response) {
177         if (response.getStatus().equals(HttpResponseStatus.SWITCHING_PROTOCOLS)) {
178             throw new WebSocketHandshakeException("Invalid handshake response status: " + response.getStatus());
179         }
180 
181         HttpHeaders headers = response.headers();
182 
183         String upgrade = headers.get(Names.UPGRADE);
184         if (!Values.WEBSOCKET.equalsIgnoreCase(upgrade)) {
185             throw new WebSocketHandshakeException("Invalid handshake response upgrade: "
186                     + upgrade);
187         }
188 
189         if (!headers.containsValue(Names.CONNECTION, Values.UPGRADE, true)) {
190             throw new WebSocketHandshakeException("Invalid handshake response connection: "
191                     + headers.get(Names.CONNECTION));
192         }
193 
194         ByteBuf challenge = response.content();
195         if (!challenge.equals(expectedChallengeResponseBytes)) {
196             throw new WebSocketHandshakeException("Invalid challenge");
197         }
198     }
199 
200     private static String insertRandomCharacters(String key) {
201         int count = WebSocketUtil.randomNumber(1, 12);
202 
203         char[] randomChars = new char[count];
204         int randCount = 0;
205         while (randCount < count) {
206             int rand = (int) (Math.random() * 0x7e + 0x21);
207             if (0x21 < rand && rand < 0x2f || 0x3a < rand && rand < 0x7e) {
208                 randomChars[randCount] = (char) rand;
209                 randCount += 1;
210             }
211         }
212 
213         for (int i = 0; i < count; i++) {
214             int split = WebSocketUtil.randomNumber(0, key.length());
215             String part1 = key.substring(0, split);
216             String part2 = key.substring(split);
217             key = part1 + randomChars[i] + part2;
218         }
219 
220         return key;
221     }
222 
223     private static String insertSpaces(String key, int spaces) {
224         for (int i = 0; i < spaces; i++) {
225             int split = WebSocketUtil.randomNumber(1, key.length() - 1);
226             String part1 = key.substring(0, split);
227             String part2 = key.substring(split);
228             key = part1 + ' ' + part2;
229         }
230 
231         return key;
232     }
233 
234     @Override
235     protected WebSocketFrameDecoder newWebsocketDecoder() {
236         return new WebSocket00FrameDecoder(maxFramePayloadLength());
237     }
238 
239     @Override
240     protected WebSocketFrameEncoder newWebSocketEncoder() {
241         return new WebSocket00FrameEncoder();
242     }
243 }