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