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 org.jboss.netty.handler.codec.http.websocketx;
17  
18  import java.net.URI;
19  import java.nio.ByteBuffer;
20  import java.util.Arrays;
21  import java.util.Map;
22  
23  import org.jboss.netty.buffer.ChannelBuffers;
24  import org.jboss.netty.channel.Channel;
25  import org.jboss.netty.channel.ChannelFuture;
26  import org.jboss.netty.handler.codec.http.DefaultHttpRequest;
27  import org.jboss.netty.handler.codec.http.HttpHeaders.Names;
28  import org.jboss.netty.handler.codec.http.HttpHeaders.Values;
29  import org.jboss.netty.handler.codec.http.HttpMethod;
30  import org.jboss.netty.handler.codec.http.HttpRequest;
31  import org.jboss.netty.handler.codec.http.HttpRequestEncoder;
32  import org.jboss.netty.handler.codec.http.HttpResponse;
33  import org.jboss.netty.handler.codec.http.HttpResponseDecoder;
34  import org.jboss.netty.handler.codec.http.HttpResponseStatus;
35  import org.jboss.netty.handler.codec.http.HttpVersion;
36  
37  /**
38   * <p>
39   * Performs client side opening and closing handshakes for web socket specification version <a
40   * href="http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-00" >draft-ietf-hybi-thewebsocketprotocol-
41   * 00</a>
42   * </p>
43   * <p>
44   * A very large portion of this code was taken from the Netty 3.2 HTTP example.
45   * </p>
46   */
47  public class WebSocketClientHandshaker00 extends WebSocketClientHandshaker {
48  
49      private byte[] expectedChallengeResponseBytes;
50  
51      /**
52       * Constructor with default values
53       *
54       * @param webSocketURL
55       *            URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be
56       *            sent to this URL.
57       * @param version
58       *            Version of web socket specification to use to connect to the server
59       * @param subprotocol
60       *            Sub protocol request sent to the server.
61       * @param customHeaders
62       *            Map of custom headers to add to the client request
63       */
64      public WebSocketClientHandshaker00(URI webSocketURL, WebSocketVersion version, String subprotocol,
65              Map<String, String> customHeaders) {
66          this(webSocketURL, version, subprotocol, customHeaders, Long.MAX_VALUE);
67      }
68  
69      /**
70       * Constructor
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       */
84      public WebSocketClientHandshaker00(URI webSocketURL, WebSocketVersion version, String subprotocol,
85              Map<String, String> customHeaders, long maxFramePayloadLength) {
86          super(webSocketURL, version, subprotocol, customHeaders, maxFramePayloadLength);
87      }
88  
89  
90      /**
91       * <p>
92       * Sends the opening request to the server:
93       * </p>
94       *
95       * <pre>
96       * GET /demo HTTP/1.1
97       * Upgrade: WebSocket
98       * Connection: Upgrade
99       * Host: example.com
100      * Origin: http://example.com
101      * Sec-WebSocket-Key1: 4 @1  46546xW%0l 1 5
102      * Sec-WebSocket-Key2: 12998 5 Y3 1  .P00
103      *
104      * ^n:ds[4U
105      * </pre>
106      *
107      * @param channel
108      *            Channel into which we can write our request
109      */
110     @Override
111     public ChannelFuture handshake(Channel channel) {
112         // Make keys
113         int spaces1 = WebSocketUtil.randomNumber(1, 12);
114         int spaces2 = WebSocketUtil.randomNumber(1, 12);
115 
116         int max1 = Integer.MAX_VALUE / spaces1;
117         int max2 = Integer.MAX_VALUE / spaces2;
118 
119         int number1 = WebSocketUtil.randomNumber(0, max1);
120         int number2 = WebSocketUtil.randomNumber(0, max2);
121 
122         int product1 = number1 * spaces1;
123         int product2 = number2 * spaces2;
124 
125         String key1 = Integer.toString(product1);
126         String key2 = Integer.toString(product2);
127 
128         key1 = insertRandomCharacters(key1);
129         key2 = insertRandomCharacters(key2);
130 
131         key1 = insertSpaces(key1, spaces1);
132         key2 = insertSpaces(key2, spaces2);
133 
134         byte[] key3 = WebSocketUtil.randomBytes(8);
135 
136         ByteBuffer buffer = ByteBuffer.allocate(4);
137         buffer.putInt(number1);
138         byte[] number1Array = buffer.array();
139         buffer = ByteBuffer.allocate(4);
140         buffer.putInt(number2);
141         byte[] number2Array = buffer.array();
142 
143         byte[] challenge = new byte[16];
144         System.arraycopy(number1Array, 0, challenge, 0, 4);
145         System.arraycopy(number2Array, 0, challenge, 4, 4);
146         System.arraycopy(key3, 0, challenge, 8, 8);
147         expectedChallengeResponseBytes = WebSocketUtil.md5(challenge);
148 
149         // Get path
150         URI wsURL = getWebSocketUrl();
151         String path = wsURL.getPath();
152         if (wsURL.getQuery() != null && wsURL.getQuery().length() > 0) {
153             path = wsURL.getPath() + "?" + wsURL.getQuery();
154         }
155 
156         // Format request
157         HttpRequest request = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, path);
158         request.addHeader(Names.UPGRADE, Values.WEBSOCKET);
159         request.addHeader(Names.CONNECTION, Values.UPGRADE);
160         request.addHeader(Names.HOST, wsURL.getHost());
161 
162         int wsPort = wsURL.getPort();
163         String originValue = "http://" + wsURL.getHost();
164         if (wsPort != 80 && wsPort != 443) {
165             // if the port is not standard (80/443) its needed to add the port to the header.
166             // See http://tools.ietf.org/html/rfc6454#section-6.2
167             originValue = originValue + ":" + wsPort;
168         }
169         request.addHeader(Names.ORIGIN, originValue);
170 
171         request.addHeader(Names.SEC_WEBSOCKET_KEY1, key1);
172         request.addHeader(Names.SEC_WEBSOCKET_KEY2, key2);
173         String expectedSubprotocol = getExpectedSubprotocol();
174         if (expectedSubprotocol != null && !expectedSubprotocol.equals("")) {
175             request.addHeader(Names.SEC_WEBSOCKET_PROTOCOL, expectedSubprotocol);
176         }
177 
178         if (customHeaders != null) {
179             for (String header : customHeaders.keySet()) {
180                 request.addHeader(header, customHeaders.get(header));
181             }
182         }
183 
184         request.setContent(ChannelBuffers.copiedBuffer(key3));
185 
186         ChannelFuture future = channel.write(request);
187 
188         channel.getPipeline().replace(HttpRequestEncoder.class, "ws-encoder", new WebSocket00FrameEncoder());
189 
190         return future;
191     }
192 
193     /**
194      * <p>
195      * Process server response:
196      * </p>
197      *
198      * <pre>
199      * HTTP/1.1 101 WebSocket Protocol Handshake
200      * Upgrade: WebSocket
201      * Connection: Upgrade
202      * Sec-WebSocket-Origin: http://example.com
203      * Sec-WebSocket-Location: ws://example.com/demo
204      * Sec-WebSocket-Protocol: sample
205      *
206      * 8jKS'y:G*Co,Wxa-
207      * </pre>
208      *
209      * @param channel
210      *            Channel
211      * @param response
212      *            HTTP response returned from the server for the request sent by beginOpeningHandshake00().
213      * @throws WebSocketHandshakeException
214      */
215     @Override
216     public void finishHandshake(Channel channel, HttpResponse response) throws WebSocketHandshakeException {
217         final HttpResponseStatus status = new HttpResponseStatus(101, "WebSocket Protocol Handshake");
218 
219         if (!response.getStatus().equals(status)) {
220             throw new WebSocketHandshakeException("Invalid handshake response status: " + response.getStatus());
221         }
222 
223         String upgrade = response.getHeader(Names.UPGRADE);
224         if (upgrade == null || !upgrade.equals(Values.WEBSOCKET)) {
225             throw new WebSocketHandshakeException("Invalid handshake response upgrade: "
226                     + response.getHeader(Names.UPGRADE));
227         }
228 
229         String connection = response.getHeader(Names.CONNECTION);
230         if (connection == null || !connection.equals(Values.UPGRADE)) {
231             throw new WebSocketHandshakeException("Invalid handshake response connection: "
232                     + response.getHeader(Names.CONNECTION));
233         }
234 
235         byte[] challenge = response.getContent().array();
236         if (!Arrays.equals(challenge, expectedChallengeResponseBytes)) {
237             throw new WebSocketHandshakeException("Invalid challenge");
238         }
239 
240         String subprotocol = response.getHeader(Names.SEC_WEBSOCKET_PROTOCOL);
241         setActualSubprotocol(subprotocol);
242 
243         setHandshakeComplete();
244 
245         channel.getPipeline().get(HttpResponseDecoder.class).replace("ws-decoder",
246                 new WebSocket00FrameDecoder(getMaxFramePayloadLength()));
247 
248 
249     }
250 
251     private static String insertRandomCharacters(String key) {
252         int count = WebSocketUtil.randomNumber(1, 12);
253 
254         char[] randomChars = new char[count];
255         int randCount = 0;
256         while (randCount < count) {
257             int rand = (int) (Math.random() * 0x7e + 0x21);
258             if (0x21 < rand && rand < 0x2f || 0x3a < rand && rand < 0x7e) {
259                 randomChars[randCount] = (char) rand;
260                 randCount += 1;
261             }
262         }
263 
264         for (int i = 0; i < count; i++) {
265             int split = WebSocketUtil.randomNumber(0, key.length());
266             String part1 = key.substring(0, split);
267             String part2 = key.substring(split);
268             key = part1 + randomChars[i] + part2;
269         }
270 
271         return key;
272     }
273 
274     private static String insertSpaces(String key, int spaces) {
275         for (int i = 0; i < spaces; i++) {
276             int split = WebSocketUtil.randomNumber(1, key.length() - 1);
277             String part1 = key.substring(0, split);
278             String part2 = key.substring(split);
279             key = part1 + " " + part2;
280         }
281 
282         return key;
283     }
284 
285 }