/js/lib/Socket.IO-node/support/socket.io-client/lib/vendor/web-socket-js/flash-src/WebSocket.as
ActionScript | 452 lines | 393 code | 37 blank | 22 comment | 112 complexity | ce2d8e229ce9b11b52479ab2dd495d27 MD5 | raw file
Possible License(s): GPL-2.0, LGPL-2.1, MPL-2.0-no-copyleft-exception, BSD-3-Clause
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 5 6package { 7 8import com.adobe.net.proxies.RFC2817Socket; 9import com.gsolo.encryption.MD5; 10import com.hurlant.crypto.tls.TLSConfig; 11import com.hurlant.crypto.tls.TLSEngine; 12import com.hurlant.crypto.tls.TLSSecurityParameters; 13import com.hurlant.crypto.tls.TLSSocket; 14 15import flash.display.*; 16import flash.events.*; 17import flash.external.*; 18import flash.net.*; 19import flash.system.*; 20import flash.utils.*; 21 22import mx.controls.*; 23import mx.core.*; 24import mx.events.*; 25import mx.utils.*; 26 27public class WebSocket extends EventDispatcher { 28 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; 33 34 private var id:int; 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 protocol:String; 46 private var buffer:ByteArray = new ByteArray(); 47 private var headerState:int = 0; 48 private var readyState:int = CONNECTING; 49 private var cookie:String; 50 private var headers:String; 51 private var noiseChars:Array; 52 private var expectedDigest:String; 53 private var logger:IWebSocketLogger; 54 55 public function WebSocket( 56 id:int, url:String, protocol:String, origin:String, 57 proxyHost:String, proxyPort:int, 58 cookie:String, headers:String, 59 logger:IWebSocketLogger) { 60 this.logger = logger; 61 this.id = id; 62 initNoiseChars(); 63 this.url = url; 64 var m:Array = url.match(/^(\w+):\/\/([^\/:]+)(:(\d+))?(\/.*)?(\?.*)?$/); 65 if (!m) fatal("SYNTAX_ERR: invalid url: " + url); 66 this.scheme = m[1]; 67 this.host = m[2]; 68 this.port = parseInt(m[4] || "80"); 69 this.path = (m[5] || "/") + (m[6] || ""); 70 this.origin = origin; 71 this.protocol = protocol; 72 this.cookie = cookie; 73 // if present and not the empty string, headers MUST end with \r\n 74 // headers should be zero or more complete lines, for example 75 // "Header1: xxx\r\nHeader2: yyyy\r\n" 76 this.headers = headers; 77 78 if (proxyHost != null && proxyPort != 0){ 79 if (scheme == "wss") { 80 fatal("wss with proxy is not supported"); 81 } 82 var proxySocket:RFC2817Socket = new RFC2817Socket(); 83 proxySocket.setProxyInfo(proxyHost, proxyPort); 84 proxySocket.addEventListener(ProgressEvent.SOCKET_DATA, onSocketData); 85 rawSocket = socket = proxySocket; 86 } else { 87 rawSocket = new Socket(); 88 if (scheme == "wss") { 89 tlsConfig= new TLSConfig(TLSEngine.CLIENT, 90 null, null, null, null, null, 91 TLSSecurityParameters.PROTOCOL_VERSION); 92 tlsConfig.trustAllCertificates = true; 93 tlsConfig.ignoreCommonNameMismatch = true; 94 tlsSocket = new TLSSocket(); 95 tlsSocket.addEventListener(ProgressEvent.SOCKET_DATA, onSocketData); 96 socket = tlsSocket; 97 } else { 98 rawSocket.addEventListener(ProgressEvent.SOCKET_DATA, onSocketData); 99 socket = rawSocket; 100 } 101 } 102 rawSocket.addEventListener(Event.CLOSE, onSocketClose); 103 rawSocket.addEventListener(Event.CONNECT, onSocketConnect); 104 rawSocket.addEventListener(IOErrorEvent.IO_ERROR, onSocketIoError); 105 rawSocket.addEventListener(SecurityErrorEvent.SECURITY_ERROR, onSocketSecurityError); 106 rawSocket.connect(host, port); 107 } 108 109 /** 110 * @return This WebSocket's ID. 111 */ 112 public function getId():int { 113 return this.id; 114 } 115 116 /** 117 * @return this WebSocket's readyState. 118 */ 119 public function getReadyState():int { 120 return this.readyState; 121 } 122 123 public function send(encData:String):int { 124 var data:String = decodeURIComponent(encData); 125 if (readyState == OPEN) { 126 socket.writeByte(0x00); 127 socket.writeUTFBytes(data); 128 socket.writeByte(0xff); 129 socket.flush(); 130 logger.log("sent: " + data); 131 return -1; 132 } else if (readyState == CLOSING || readyState == CLOSED) { 133 var bytes:ByteArray = new ByteArray(); 134 bytes.writeUTFBytes(data); 135 return bytes.length; // not sure whether it should include \x00 and \xff 136 } else { 137 fatal("invalid state"); 138 return 0; 139 } 140 } 141 142 public function close(isError:Boolean = false):void { 143 logger.log("close"); 144 try { 145 if (readyState == OPEN && !isError) { 146 socket.writeByte(0xff); 147 socket.writeByte(0x00); 148 socket.flush(); 149 } 150 socket.close(); 151 } catch (ex:Error) { } 152 readyState = CLOSED; 153 this.dispatchEvent(new WebSocketEvent(isError ? "error" : "close")); 154 } 155 156 private function onSocketConnect(event:Event):void { 157 logger.log("connected"); 158 159 if (scheme == "wss") { 160 logger.log("starting SSL/TLS"); 161 tlsSocket.startTLS(rawSocket, host, tlsConfig); 162 } 163 164 var hostValue:String = host + (port == 80 ? "" : ":" + port); 165 var key1:String = generateKey(); 166 var key2:String = generateKey(); 167 var key3:String = generateKey3(); 168 expectedDigest = getSecurityDigest(key1, key2, key3); 169 var opt:String = ""; 170 if (protocol) opt += "WebSocket-Protocol: " + protocol + "\r\n"; 171 // if caller passes additional headers they must end with "\r\n" 172 if (headers) opt += headers; 173 174 var req:String = StringUtil.substitute( 175 "GET {0} HTTP/1.1\r\n" + 176 "Upgrade: WebSocket\r\n" + 177 "Connection: Upgrade\r\n" + 178 "Host: {1}\r\n" + 179 "Origin: {2}\r\n" + 180 "Cookie: {3}\r\n" + 181 "Sec-WebSocket-Key1: {4}\r\n" + 182 "Sec-WebSocket-Key2: {5}\r\n" + 183 "{6}" + 184 "\r\n", 185 path, hostValue, origin, cookie, key1, key2, opt); 186 logger.log("request header:\n" + req); 187 socket.writeUTFBytes(req); 188 logger.log("sent key3: " + key3); 189 writeBytes(key3); 190 socket.flush(); 191 } 192 193 private function onSocketClose(event:Event):void { 194 logger.log("closed"); 195 readyState = CLOSED; 196 this.dispatchEvent(new WebSocketEvent("close")); 197 } 198 199 private function onSocketIoError(event:IOErrorEvent):void { 200 var message:String; 201 if (readyState == CONNECTING) { 202 message = "cannot connect to Web Socket server at " + url + " (IoError)"; 203 } else { 204 message = "error communicating with Web Socket server at " + url + " (IoError)"; 205 } 206 onError(message); 207 } 208 209 private function onSocketSecurityError(event:SecurityErrorEvent):void { 210 var message:String; 211 if (readyState == CONNECTING) { 212 message = 213 "cannot connect to Web Socket server at " + url + " (SecurityError)\n" + 214 "make sure the server is running and Flash socket policy file is correctly placed"; 215 } else { 216 message = "error communicating with Web Socket server at " + url + " (SecurityError)"; 217 } 218 onError(message); 219 } 220 221 private function onError(message:String):void { 222 if (readyState == CLOSED) return; 223 logger.error(message); 224 close(readyState != CONNECTING); 225 } 226 227 private function onSocketData(event:ProgressEvent):void { 228 var pos:int = buffer.length; 229 socket.readBytes(buffer, pos); 230 for (; pos < buffer.length; ++pos) { 231 if (headerState < 4) { 232 // try to find "\r\n\r\n" 233 if ((headerState == 0 || headerState == 2) && buffer[pos] == 0x0d) { 234 ++headerState; 235 } else if ((headerState == 1 || headerState == 3) && buffer[pos] == 0x0a) { 236 ++headerState; 237 } else { 238 headerState = 0; 239 } 240 if (headerState == 4) { 241 var headerStr:String = readUTFBytes(buffer, 0, pos + 1); 242 logger.log("response header:\n" + headerStr); 243 if (!validateHeader(headerStr)) return; 244 removeBufferBefore(pos + 1); 245 pos = -1; 246 } 247 } else if (headerState == 4) { 248 if (pos == 15) { 249 var replyDigest:String = readBytes(buffer, 0, 16); 250 logger.log("reply digest: " + replyDigest); 251 if (replyDigest != expectedDigest) { 252 onError("digest doesn't match: " + replyDigest + " != " + expectedDigest); 253 return; 254 } 255 headerState = 5; 256 removeBufferBefore(pos + 1); 257 pos = -1; 258 readyState = OPEN; 259 this.dispatchEvent(new WebSocketEvent("open")); 260 } 261 } else { 262 if (buffer[pos] == 0xff && pos > 0) { 263 if (buffer[0] != 0x00) { 264 onError("data must start with \\x00"); 265 return; 266 } 267 var data:String = readUTFBytes(buffer, 1, pos - 1); 268 logger.log("received: " + data); 269 this.dispatchEvent(new WebSocketEvent("message", encodeURIComponent(data))); 270 removeBufferBefore(pos + 1); 271 pos = -1; 272 } else if (pos == 1 && buffer[0] == 0xff && buffer[1] == 0x00) { // closing 273 logger.log("received closing packet"); 274 removeBufferBefore(pos + 1); 275 pos = -1; 276 close(); 277 } 278 } 279 } 280 } 281 282 private function validateHeader(headerStr:String):Boolean { 283 var lines:Array = headerStr.split(/\r\n/); 284 if (!lines[0].match(/^HTTP\/1.1 101 /)) { 285 onError("bad response: " + lines[0]); 286 return false; 287 } 288 var header:Object = {}; 289 var lowerHeader:Object = {}; 290 for (var i:int = 1; i < lines.length; ++i) { 291 if (lines[i].length == 0) continue; 292 var m:Array = lines[i].match(/^(\S+): (.*)$/); 293 if (!m) { 294 onError("failed to parse response header line: " + lines[i]); 295 return false; 296 } 297 header[m[1].toLowerCase()] = m[2]; 298 lowerHeader[m[1].toLowerCase()] = m[2].toLowerCase(); 299 } 300 if (lowerHeader["upgrade"] != "websocket") { 301 onError("invalid Upgrade: " + header["Upgrade"]); 302 return false; 303 } 304 if (lowerHeader["connection"] != "upgrade") { 305 onError("invalid Connection: " + header["Connection"]); 306 return false; 307 } 308 if (!lowerHeader["sec-websocket-origin"]) { 309 if (lowerHeader["websocket-origin"]) { 310 onError( 311 "The WebSocket server speaks old WebSocket protocol, " + 312 "which is not supported by web-socket-js. " + 313 "It requires WebSocket protocol 76 or later. " + 314 "Try newer version of the server if available."); 315 } else { 316 onError("header Sec-WebSocket-Origin is missing"); 317 } 318 return false; 319 } 320 var resOrigin:String = lowerHeader["sec-websocket-origin"]; 321 if (resOrigin != origin) { 322 onError("origin doesn't match: '" + resOrigin + "' != '" + origin + "'"); 323 return false; 324 } 325 if (protocol && header["sec-websocket-protocol"] != protocol) { 326 onError("protocol doesn't match: '" + 327 header["websocket-protocol"] + "' != '" + protocol + "'"); 328 return false; 329 } 330 return true; 331 } 332 333 private function removeBufferBefore(pos:int):void { 334 if (pos == 0) return; 335 var nextBuffer:ByteArray = new ByteArray(); 336 buffer.position = pos; 337 buffer.readBytes(nextBuffer); 338 buffer = nextBuffer; 339 } 340 341 private function initNoiseChars():void { 342 noiseChars = new Array(); 343 for (var i:int = 0x21; i <= 0x2f; ++i) { 344 noiseChars.push(String.fromCharCode(i)); 345 } 346 for (var j:int = 0x3a; j <= 0x7a; ++j) { 347 noiseChars.push(String.fromCharCode(j)); 348 } 349 } 350 351 private function generateKey():String { 352 var spaces:uint = randomInt(1, 12); 353 var max:uint = uint.MAX_VALUE / spaces; 354 var number:uint = randomInt(0, max); 355 var key:String = (number * spaces).toString(); 356 var noises:int = randomInt(1, 12); 357 var pos:int; 358 for (var i:int = 0; i < noises; ++i) { 359 var char:String = noiseChars[randomInt(0, noiseChars.length - 1)]; 360 pos = randomInt(0, key.length); 361 key = key.substr(0, pos) + char + key.substr(pos); 362 } 363 for (var j:int = 0; j < spaces; ++j) { 364 pos = randomInt(1, key.length - 1); 365 key = key.substr(0, pos) + " " + key.substr(pos); 366 } 367 return key; 368 } 369 370 private function generateKey3():String { 371 var key3:String = ""; 372 for (var i:int = 0; i < 8; ++i) { 373 key3 += String.fromCharCode(randomInt(0, 255)); 374 } 375 return key3; 376 } 377 378 private function getSecurityDigest(key1:String, key2:String, key3:String):String { 379 var bytes1:String = keyToBytes(key1); 380 var bytes2:String = keyToBytes(key2); 381 return MD5.rstr_md5(bytes1 + bytes2 + key3); 382 } 383 384 private function keyToBytes(key:String):String { 385 var keyNum:uint = parseInt(key.replace(/[^\d]/g, "")); 386 var spaces:uint = 0; 387 for (var i:int = 0; i < key.length; ++i) { 388 if (key.charAt(i) == " ") ++spaces; 389 } 390 var resultNum:uint = keyNum / spaces; 391 var bytes:String = ""; 392 for (var j:int = 3; j >= 0; --j) { 393 bytes += String.fromCharCode((resultNum >> (j * 8)) & 0xff); 394 } 395 return bytes; 396 } 397 398 // Writes byte sequence to socket. 399 // bytes is String in special format where bytes[i] is i-th byte, not i-th character. 400 private function writeBytes(bytes:String):void { 401 for (var i:int = 0; i < bytes.length; ++i) { 402 socket.writeByte(bytes.charCodeAt(i)); 403 } 404 } 405 406 // Reads specified number of bytes from buffer, and returns it as special format String 407 // where bytes[i] is i-th byte (not i-th character). 408 private function readBytes(buffer:ByteArray, start:int, numBytes:int):String { 409 buffer.position = start; 410 var bytes:String = ""; 411 for (var i:int = 0; i < numBytes; ++i) { 412 // & 0xff is to make \x80-\xff positive number. 413 bytes += String.fromCharCode(buffer.readByte() & 0xff); 414 } 415 return bytes; 416 } 417 418 private function readUTFBytes(buffer:ByteArray, start:int, numBytes:int):String { 419 buffer.position = start; 420 var data:String = ""; 421 for(var i:int = start; i < start + numBytes; ++i) { 422 // Workaround of a bug of ByteArray#readUTFBytes() that bytes after "\x00" is discarded. 423 if (buffer[i] == 0x00) { 424 data += buffer.readUTFBytes(i - buffer.position) + "\x00"; 425 buffer.position = i + 1; 426 } 427 } 428 data += buffer.readUTFBytes(start + numBytes - buffer.position); 429 return data; 430 } 431 432 private function randomInt(min:uint, max:uint):uint { 433 return min + Math.floor(Math.random() * (Number(max) - min + 1)); 434 } 435 436 private function fatal(message:String):void { 437 logger.error(message); 438 throw message; 439 } 440 441 // for debug 442 private function dumpBytes(bytes:String):void { 443 var output:String = ""; 444 for (var i:int = 0; i < bytes.length; ++i) { 445 output += bytes.charCodeAt(i).toString() + ", "; 446 } 447 logger.log(output); 448 } 449 450} 451 452}