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       * <p>
94       * Sends the opening request to the server:
95       * </p>
96       *
97       * <pre>
98       * GET /demo HTTP/1.1
99       * Upgrade: WebSocket
100      * Connection: Upgrade
101      * Host: example.com
102      * Origin: http://example.com
103      * Sec-WebSocket-Key1: 4 @1  46546xW%0l 1 5
104      * Sec-WebSocket-Key2: 12998 5 Y3 1  .P00
105      *
106      * ^n:ds[4U
107      * </pre>
108      *
109      * @param channel
110      *            Channel into which we can write our request
111      */
112     @Override
113     public ChannelFuture handshake(Channel channel) {
114         // Make keys
115         int spaces1 = WebSocketUtil.randomNumber(1, 12);
116         int spaces2 = WebSocketUtil.randomNumber(1, 12);
117 
118         int max1 = Integer.MAX_VALUE / spaces1;
119         int max2 = Integer.MAX_VALUE / spaces2;
120 
121         int number1 = WebSocketUtil.randomNumber(0, max1);
122         int number2 = WebSocketUtil.randomNumber(0, max2);
123 
124         int product1 = number1 * spaces1;
125         int product2 = number2 * spaces2;
126 
127         String key1 = Integer.toString(product1);
128         String key2 = Integer.toString(product2);
129 
130         key1 = insertRandomCharacters(key1);
131         key2 = insertRandomCharacters(key2);
132 
133         key1 = insertSpaces(key1, spaces1);
134         key2 = insertSpaces(key2, spaces2);
135 
136         byte[] key3 = WebSocketUtil.randomBytes(8);
137 
138         ByteBuffer buffer = ByteBuffer.allocate(4);
139         buffer.putInt(number1);
140         byte[] number1Array = buffer.array();
141         buffer = ByteBuffer.allocate(4);
142         buffer.putInt(number2);
143         byte[] number2Array = buffer.array();
144 
145         byte[] challenge = new byte[16];
146         System.arraycopy(number1Array, 0, challenge, 0, 4);
147         System.arraycopy(number2Array, 0, challenge, 4, 4);
148         System.arraycopy(key3, 0, challenge, 8, 8);
149         expectedChallengeResponseBytes = WebSocketUtil.md5(ChannelBuffers.wrappedBuffer(challenge));
150 
151         // Get path
152         URI wsURL = getWebSocketUrl();
153         String path = wsURL.getPath();
154         if (wsURL.getQuery() != null && wsURL.getQuery().length() > 0) {
155             path = wsURL.getPath() + '?' + wsURL.getQuery();
156         }
157 
158         if (path == null || path.length() == 0) {
159             path = "/";
160         }
161 
162         // Format request
163         HttpRequest request = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, path);
164         request.addHeader(Names.UPGRADE, Values.WEBSOCKET);
165         request.addHeader(Names.CONNECTION, Values.UPGRADE);
166         request.addHeader(Names.HOST, wsURL.getHost());
167 
168         int wsPort = wsURL.getPort();
169         String originValue = "http://" + wsURL.getHost();
170         if (wsPort != 80 && wsPort != 443) {
171             // if the port is not standard (80/443) its needed to add the port to the header.
172             // See http://tools.ietf.org/html/rfc6454#section-6.2
173             originValue = originValue + ':' + wsPort;
174         }
175         request.addHeader(Names.ORIGIN, originValue);
176 
177         request.addHeader(Names.SEC_WEBSOCKET_KEY1, key1);
178         request.addHeader(Names.SEC_WEBSOCKET_KEY2, key2);
179         String expectedSubprotocol = getExpectedSubprotocol();
180         if (expectedSubprotocol != null && expectedSubprotocol.length() != 0) {
181             request.addHeader(Names.SEC_WEBSOCKET_PROTOCOL, expectedSubprotocol);
182         }
183 
184         if (customHeaders != null) {
185             for (Map.Entry<String, String> e: customHeaders.entrySet()) {
186                 request.addHeader(e.getKey(), e.getValue());
187             }
188         }
189 
190         // Set Content-Length to workaround some known defect.
191         // See also: http://www.ietf.org/mail-archive/web/hybi/current/msg02149.html
192         request.setHeader(Names.CONTENT_LENGTH, key3.length);
193         request.setContent(ChannelBuffers.copiedBuffer(key3));
194 
195         final ChannelFuture handshakeFuture = new DefaultChannelFuture(channel, false);
196         ChannelFuture future = channel.write(request);
197 
198         future.addListener(new ChannelFutureListener() {
199             public void operationComplete(ChannelFuture future) {
200                 ChannelPipeline p = future.getChannel().getPipeline();
201                 p.replace(HttpRequestEncoder.class, "ws-encoder", new WebSocket00FrameEncoder());
202 
203                 if (future.isSuccess()) {
204                     handshakeFuture.setSuccess();
205                 } else {
206                     handshakeFuture.setFailure(future.getCause());
207                 }
208             }
209         });
210 
211         return handshakeFuture;
212     }
213 
214     /**
215      * <p>
216      * Process server response:
217      * </p>
218      *
219      * <pre>
220      * HTTP/1.1 101 WebSocket Protocol Handshake
221      * Upgrade: WebSocket
222      * Connection: Upgrade
223      * Sec-WebSocket-Origin: http://example.com
224      * Sec-WebSocket-Location: ws://example.com/demo
225      * Sec-WebSocket-Protocol: sample
226      *
227      * 8jKS'y:G*Co,Wxa-
228      * </pre>
229      *
230      * @param channel
231      *            Channel
232      * @param response
233      *            HTTP response returned from the server for the request sent by beginOpeningHandshake00().
234      * @throws WebSocketHandshakeException
235      */
236     @Override
237     public void finishHandshake(Channel channel, HttpResponse response) {
238         final HttpResponseStatus status = new HttpResponseStatus(101, "WebSocket Protocol Handshake");
239 
240         if (!response.getStatus().equals(status)) {
241             throw new WebSocketHandshakeException("Invalid handshake response status: " + response.getStatus());
242         }
243 
244         String upgrade = response.getHeader(Names.UPGRADE);
245         if (!Values.WEBSOCKET.equals(upgrade)) {
246             throw new WebSocketHandshakeException("Invalid handshake response upgrade: "
247                     + upgrade);
248         }
249 
250         String connection = response.getHeader(Names.CONNECTION);
251         if (!Values.UPGRADE.equals(connection)) {
252             throw new WebSocketHandshakeException("Invalid handshake response connection: "
253                     + connection);
254         }
255 
256         ChannelBuffer challenge = response.getContent();
257         if (!challenge.equals(expectedChallengeResponseBytes)) {
258             throw new WebSocketHandshakeException("Invalid challenge");
259         }
260 
261         String subprotocol = response.getHeader(Names.SEC_WEBSOCKET_PROTOCOL);
262         setActualSubprotocol(subprotocol);
263 
264         setHandshakeComplete();
265 
266         channel.getPipeline().get(HttpResponseDecoder.class).replace("ws-decoder",
267                 new WebSocket00FrameDecoder(getMaxFramePayloadLength()));
268     }
269 
270     private static String insertRandomCharacters(String key) {
271         int count = WebSocketUtil.randomNumber(1, 12);
272 
273         char[] randomChars = new char[count];
274         int randCount = 0;
275         while (randCount < count) {
276             int rand = (int) (Math.random() * 0x7e + 0x21);
277             if (0x21 < rand && rand < 0x2f || 0x3a < rand && rand < 0x7e) {
278                 randomChars[randCount] = (char) rand;
279                 randCount += 1;
280             }
281         }
282 
283         for (int i = 0; i < count; i++) {
284             int split = WebSocketUtil.randomNumber(0, key.length());
285             String part1 = key.substring(0, split);
286             String part2 = key.substring(split);
287             key = part1 + randomChars[i] + part2;
288         }
289 
290         return key;
291     }
292 
293     private static String insertSpaces(String key, int spaces) {
294         for (int i = 0; i < spaces; i++) {
295             int split = WebSocketUtil.randomNumber(1, key.length() - 1);
296             String part1 = key.substring(0, split);
297             String part2 = key.substring(split);
298             key = part1 + ' ' + part2;
299         }
300 
301         return key;
302     }
303 
304 }