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