1 // Copyright: Hiroshi Ichikawa <http://gimite.net/en/>
2 // License: New BSD License
3 // Reference: http://dev.w3.org/html5/websockets/
4 // Reference: http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76
8 import com.adobe.net.proxies.RFC2817Socket;
9 import com.gsolo.encryption.MD5;
10 import com.hurlant.crypto.tls.TLSConfig;
11 import com.hurlant.crypto.tls.TLSEngine;
12 import com.hurlant.crypto.tls.TLSSecurityParameters;
13 import com.hurlant.crypto.tls.TLSSocket;
15 import flash.display.*;
16 import flash.events.*;
17 import flash.external.*;
19 import flash.system.*;
27 public class WebSocket extends EventDispatcher {
29 private static var CONNECTING:int = 0;
30 private static var OPEN:int = 1;
31 private static var CLOSING:int = 2;
32 private static var CLOSED:int = 3;
35 private var rawSocket:Socket;
36 private var tlsSocket:TLSSocket;
37 private var tlsConfig:TLSConfig;
38 private var socket:Socket;
39 private var url:String;
40 private var scheme:String;
41 private var host:String;
42 private var port:uint;
43 private var path:String;
44 private var origin:String;
45 private var requestedProtocols:Array;
46 private var acceptedProtocol:String;
47 private var buffer:ByteArray = new ByteArray();
48 private var headerState:int = 0;
49 private var readyState:int = CONNECTING;
50 private var cookie:String;
51 private var headers:String;
52 private var noiseChars:Array;
53 private var expectedDigest:String;
54 private var logger:IWebSocketLogger;
56 public function WebSocket(
57 id:int, url:String, protocols:Array, origin:String,
58 proxyHost:String, proxyPort:int,
59 cookie:String, headers:String,
60 logger:IWebSocketLogger) {
65 var m:Array = url.match(/^(\w+):\/\/([^\/:]+)(:(\d+))?(\/.*)?(\?.*)?$/);
66 if (!m) fatal("SYNTAX_ERR: invalid url: " + url);
69 var defaultPort:int = scheme == "wss" ? 443 : 80;
70 this.port = parseInt(m[4]) || defaultPort;
71 this.path = (m[5] || "/") + (m[6] || "");
73 this.requestedProtocols = protocols;
75 // if present and not the empty string, headers MUST end with \r\n
76 // headers should be zero or more complete lines, for example
77 // "Header1: xxx\r\nHeader2: yyyy\r\n"
78 this.headers = headers;
80 if (proxyHost != null && proxyPort != 0){
81 if (scheme == "wss") {
82 fatal("wss with proxy is not supported");
84 var proxySocket:RFC2817Socket = new RFC2817Socket();
85 proxySocket.setProxyInfo(proxyHost, proxyPort);
86 proxySocket.addEventListener(ProgressEvent.SOCKET_DATA, onSocketData);
87 rawSocket = socket = proxySocket;
89 rawSocket = new Socket();
90 if (scheme == "wss") {
91 tlsConfig= new TLSConfig(TLSEngine.CLIENT,
92 null, null, null, null, null,
93 TLSSecurityParameters.PROTOCOL_VERSION);
94 tlsConfig.trustAllCertificates = true;
95 tlsConfig.ignoreCommonNameMismatch = true;
96 tlsSocket = new TLSSocket();
97 tlsSocket.addEventListener(ProgressEvent.SOCKET_DATA, onSocketData);
100 rawSocket.addEventListener(ProgressEvent.SOCKET_DATA, onSocketData);
104 rawSocket.addEventListener(Event.CLOSE, onSocketClose);
105 rawSocket.addEventListener(Event.CONNECT, onSocketConnect);
106 rawSocket.addEventListener(IOErrorEvent.IO_ERROR, onSocketIoError);
107 rawSocket.addEventListener(SecurityErrorEvent.SECURITY_ERROR, onSocketSecurityError);
108 rawSocket.connect(host, port);
112 * @return This WebSocket's ID.
114 public function getId():int {
119 * @return this WebSocket's readyState.
121 public function getReadyState():int {
122 return this.readyState;
125 public function getAcceptedProtocol():String {
126 return this.acceptedProtocol;
129 public function send(encData:String):int {
130 var data:String = decodeURIComponent(encData);
131 if (readyState == OPEN) {
132 socket.writeByte(0x00);
133 socket.writeUTFBytes(data);
134 socket.writeByte(0xff);
136 logger.log("sent: " + data);
138 } else if (readyState == CLOSING || readyState == CLOSED) {
139 var bytes:ByteArray = new ByteArray();
140 bytes.writeUTFBytes(data);
141 return bytes.length; // not sure whether it should include \x00 and \xff
143 fatal("invalid state");
148 public function close(isError:Boolean = false):void {
151 if (readyState == OPEN && !isError) {
152 socket.writeByte(0xff);
153 socket.writeByte(0x00);
157 } catch (ex:Error) { }
159 this.dispatchEvent(new WebSocketEvent(isError ? "error" : "close"));
162 private function onSocketConnect(event:Event):void {
163 logger.log("connected");
165 if (scheme == "wss") {
166 logger.log("starting SSL/TLS");
167 tlsSocket.startTLS(rawSocket, host, tlsConfig);
170 var defaultPort:int = scheme == "wss" ? 443 : 80;
171 var hostValue:String = host + (port == defaultPort ? "" : ":" + port);
172 var key1:String = generateKey();
173 var key2:String = generateKey();
174 var key3:String = generateKey3();
175 expectedDigest = getSecurityDigest(key1, key2, key3);
177 if (requestedProtocols.length > 0) {
178 opt += "Sec-WebSocket-Protocol: " + requestedProtocols.join(",") + "\r\n";
180 // if caller passes additional headers they must end with "\r\n"
181 if (headers) opt += headers;
183 var req:String = StringUtil.substitute(
184 "GET {0} HTTP/1.1\r\n" +
185 "Upgrade: WebSocket\r\n" +
186 "Connection: Upgrade\r\n" +
190 "Sec-WebSocket-Key1: {4}\r\n" +
191 "Sec-WebSocket-Key2: {5}\r\n" +
194 path, hostValue, origin, cookie, key1, key2, opt);
195 logger.log("request header:\n" + req);
196 socket.writeUTFBytes(req);
197 logger.log("sent key3: " + key3);
202 private function onSocketClose(event:Event):void {
203 logger.log("closed");
205 this.dispatchEvent(new WebSocketEvent("close"));
208 private function onSocketIoError(event:IOErrorEvent):void {
210 if (readyState == CONNECTING) {
211 message = "cannot connect to Web Socket server at " + url + " (IoError)";
213 message = "error communicating with Web Socket server at " + url + " (IoError)";
218 private function onSocketSecurityError(event:SecurityErrorEvent):void {
220 if (readyState == CONNECTING) {
222 "cannot connect to Web Socket server at " + url + " (SecurityError)\n" +
223 "make sure the server is running and Flash socket policy file is correctly placed";
225 message = "error communicating with Web Socket server at " + url + " (SecurityError)";
230 private function onError(message:String):void {
231 if (readyState == CLOSED) return;
232 logger.error(message);
233 close(readyState != CONNECTING);
236 private function onSocketData(event:ProgressEvent):void {
237 var pos:int = buffer.length;
238 socket.readBytes(buffer, pos);
239 for (; pos < buffer.length; ++pos) {
240 if (headerState < 4) {
241 // try to find "\r\n\r\n"
242 if ((headerState == 0 || headerState == 2) && buffer[pos] == 0x0d) {
244 } else if ((headerState == 1 || headerState == 3) && buffer[pos] == 0x0a) {
249 if (headerState == 4) {
250 var headerStr:String = readUTFBytes(buffer, 0, pos + 1);
251 logger.log("response header:\n" + headerStr);
252 if (!validateHeader(headerStr)) return;
253 removeBufferBefore(pos + 1);
256 } else if (headerState == 4) {
258 var replyDigest:String = readBytes(buffer, 0, 16);
259 logger.log("reply digest: " + replyDigest);
260 if (replyDigest != expectedDigest) {
261 onError("digest doesn't match: " + replyDigest + " != " + expectedDigest);
265 removeBufferBefore(pos + 1);
268 this.dispatchEvent(new WebSocketEvent("open"));
271 if (buffer[pos] == 0xff && pos > 0) {
272 if (buffer[0] != 0x00) {
273 onError("data must start with \\x00");
276 var data:String = readUTFBytes(buffer, 1, pos - 1);
277 logger.log("received: " + data);
278 this.dispatchEvent(new WebSocketEvent("message", encodeURIComponent(data)));
279 removeBufferBefore(pos + 1);
281 } else if (pos == 1 && buffer[0] == 0xff && buffer[1] == 0x00) { // closing
282 logger.log("received closing packet");
283 removeBufferBefore(pos + 1);
291 private function validateHeader(headerStr:String):Boolean {
292 var lines:Array = headerStr.split(/\r\n/);
293 if (!lines[0].match(/^HTTP\/1.1 101 /)) {
294 onError("bad response: " + lines[0]);
297 var header:Object = {};
298 var lowerHeader:Object = {};
299 for (var i:int = 1; i < lines.length; ++i) {
300 if (lines[i].length == 0) continue;
301 var m:Array = lines[i].match(/^(\S+): (.*)$/);
303 onError("failed to parse response header line: " + lines[i]);
306 header[m[1].toLowerCase()] = m[2];
307 lowerHeader[m[1].toLowerCase()] = m[2].toLowerCase();
309 if (lowerHeader["upgrade"] != "websocket") {
310 onError("invalid Upgrade: " + header["Upgrade"]);
313 if (lowerHeader["connection"] != "upgrade") {
314 onError("invalid Connection: " + header["Connection"]);
317 if (!lowerHeader["sec-websocket-origin"]) {
318 if (lowerHeader["websocket-origin"]) {
320 "The WebSocket server speaks old WebSocket protocol, " +
321 "which is not supported by web-socket-js. " +
322 "It requires WebSocket protocol 76 or later. " +
323 "Try newer version of the server if available.");
325 onError("header Sec-WebSocket-Origin is missing");
329 var resOrigin:String = lowerHeader["sec-websocket-origin"];
330 if (resOrigin != origin) {
331 onError("origin doesn't match: '" + resOrigin + "' != '" + origin + "'");
334 if (requestedProtocols.length > 0) {
335 acceptedProtocol = header["sec-websocket-protocol"];
336 if (requestedProtocols.indexOf(acceptedProtocol) < 0) {
337 onError("protocol doesn't match: '" +
338 acceptedProtocol + "' not in '" + requestedProtocols.join(",") + "'");
345 private function removeBufferBefore(pos:int):void {
346 if (pos == 0) return;
347 var nextBuffer:ByteArray = new ByteArray();
348 buffer.position = pos;
349 buffer.readBytes(nextBuffer);
353 private function initNoiseChars():void {
354 noiseChars = new Array();
355 for (var i:int = 0x21; i <= 0x2f; ++i) {
356 noiseChars.push(String.fromCharCode(i));
358 for (var j:int = 0x3a; j <= 0x7a; ++j) {
359 noiseChars.push(String.fromCharCode(j));
363 private function generateKey():String {
364 var spaces:uint = randomInt(1, 12);
365 var max:uint = uint.MAX_VALUE / spaces;
366 var number:uint = randomInt(0, max);
367 var key:String = (number * spaces).toString();
368 var noises:int = randomInt(1, 12);
370 for (var i:int = 0; i < noises; ++i) {
371 var char:String = noiseChars[randomInt(0, noiseChars.length - 1)];
372 pos = randomInt(0, key.length);
373 key = key.substr(0, pos) + char + key.substr(pos);
375 for (var j:int = 0; j < spaces; ++j) {
376 pos = randomInt(1, key.length - 1);
377 key = key.substr(0, pos) + " " + key.substr(pos);
382 private function generateKey3():String {
383 var key3:String = "";
384 for (var i:int = 0; i < 8; ++i) {
385 key3 += String.fromCharCode(randomInt(0, 255));
390 private function getSecurityDigest(key1:String, key2:String, key3:String):String {
391 var bytes1:String = keyToBytes(key1);
392 var bytes2:String = keyToBytes(key2);
393 return MD5.rstr_md5(bytes1 + bytes2 + key3);
396 private function keyToBytes(key:String):String {
397 var keyNum:uint = parseInt(key.replace(/[^\d]/g, ""));
399 for (var i:int = 0; i < key.length; ++i) {
400 if (key.charAt(i) == " ") ++spaces;
402 var resultNum:uint = keyNum / spaces;
403 var bytes:String = "";
404 for (var j:int = 3; j >= 0; --j) {
405 bytes += String.fromCharCode((resultNum >> (j * 8)) & 0xff);
410 // Writes byte sequence to socket.
411 // bytes is String in special format where bytes[i] is i-th byte, not i-th character.
412 private function writeBytes(bytes:String):void {
413 for (var i:int = 0; i < bytes.length; ++i) {
414 socket.writeByte(bytes.charCodeAt(i));
418 // Reads specified number of bytes from buffer, and returns it as special format String
419 // where bytes[i] is i-th byte (not i-th character).
420 private function readBytes(buffer:ByteArray, start:int, numBytes:int):String {
421 buffer.position = start;
422 var bytes:String = "";
423 for (var i:int = 0; i < numBytes; ++i) {
424 // & 0xff is to make \x80-\xff positive number.
425 bytes += String.fromCharCode(buffer.readByte() & 0xff);
430 private function readUTFBytes(buffer:ByteArray, start:int, numBytes:int):String {
431 buffer.position = start;
432 var data:String = "";
433 for(var i:int = start; i < start + numBytes; ++i) {
434 // Workaround of a bug of ByteArray#readUTFBytes() that bytes after "\x00" is discarded.
435 if (buffer[i] == 0x00) {
436 data += buffer.readUTFBytes(i - buffer.position) + "\x00";
437 buffer.position = i + 1;
440 data += buffer.readUTFBytes(start + numBytes - buffer.position);
444 private function randomInt(min:uint, max:uint):uint {
445 return min + Math.floor(Math.random() * (Number(max) - min + 1));
448 private function fatal(message:String):void {
449 logger.error(message);
454 private function dumpBytes(bytes:String):void {
455 var output:String = "";
456 for (var i:int = 0; i < bytes.length; ++i) {
457 output += bytes.charCodeAt(i).toString() + ", ";