diff -r 2ddd11ec2da2 -r 436a31d11f1d tweetcast/client/lib/gimite-web-socket-js-55ae639/flash-src/src/net/gimite/websocket/WebSocket.as --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tweetcast/client/lib/gimite-web-socket-js-55ae639/flash-src/src/net/gimite/websocket/WebSocket.as Thu Oct 06 11:56:48 2011 +0200 @@ -0,0 +1,584 @@ +// Copyright: Hiroshi Ichikawa +// License: New BSD License +// Reference: http://dev.w3.org/html5/websockets/ +// Reference: http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-10 + +package net.gimite.websocket { + +import com.adobe.net.proxies.RFC2817Socket; +import com.gsolo.encryption.SHA1; +import com.hurlant.crypto.tls.TLSConfig; +import com.hurlant.crypto.tls.TLSEngine; +import com.hurlant.crypto.tls.TLSSecurityParameters; +import com.hurlant.crypto.tls.TLSSocket; + +import flash.display.*; +import flash.errors.*; +import flash.events.*; +import flash.external.*; +import flash.net.*; +import flash.system.*; +import flash.utils.*; + +import mx.controls.*; +import mx.core.*; +import mx.events.*; +import mx.utils.*; + +public class WebSocket extends EventDispatcher { + + private static const WEB_SOCKET_GUID:String = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + + private static const CONNECTING:int = 0; + private static const OPEN:int = 1; + private static const CLOSING:int = 2; + private static const CLOSED:int = 3; + + private static const OPCODE_CONTINUATION:int = 0x00; + private static const OPCODE_TEXT:int = 0x01; + private static const OPCODE_BINARY:int = 0x02; + private static const OPCODE_CLOSE:int = 0x08; + private static const OPCODE_PING:int = 0x09; + private static const OPCODE_PONG:int = 0x0a; + + private static const STATUS_NORMAL_CLOSURE:int = 1000; + private static const STATUS_NO_CODE:int = 1005; + private static const STATUS_CLOSED_ABNORMALLY:int = 1006; + private static const STATUS_CONNECTION_ERROR:int = 5000; + + private var id:int; + private var url:String; + private var scheme:String; + private var host:String; + private var port:uint; + private var path:String; + private var origin:String; + private var requestedProtocols:Array; + private var cookie:String; + private var headers:String; + + private var rawSocket:Socket; + private var tlsSocket:TLSSocket; + private var tlsConfig:TLSConfig; + private var socket:Socket; + + private var acceptedProtocol:String; + private var expectedDigest:String; + + private var buffer:ByteArray = new ByteArray(); + private var headerState:int = 0; + private var readyState:int = CONNECTING; + + private var logger:IWebSocketLogger; + private var base64Encoder:Base64Encoder = new Base64Encoder(); + + public function WebSocket( + id:int, url:String, protocols:Array, origin:String, + proxyHost:String, proxyPort:int, + cookie:String, headers:String, + logger:IWebSocketLogger) { + this.logger = logger; + this.id = id; + this.url = url; + var m:Array = url.match(/^(\w+):\/\/([^\/:]+)(:(\d+))?(\/.*)?(\?.*)?$/); + if (!m) fatal("SYNTAX_ERR: invalid url: " + url); + this.scheme = m[1]; + this.host = m[2]; + var defaultPort:int = scheme == "wss" ? 443 : 80; + this.port = parseInt(m[4]) || defaultPort; + this.path = (m[5] || "/") + (m[6] || ""); + this.origin = origin; + this.requestedProtocols = protocols; + this.cookie = cookie; + // if present and not the empty string, headers MUST end with \r\n + // headers should be zero or more complete lines, for example + // "Header1: xxx\r\nHeader2: yyyy\r\n" + this.headers = headers; + + if (proxyHost != null && proxyPort != 0){ + if (scheme == "wss") { + fatal("wss with proxy is not supported"); + } + var proxySocket:RFC2817Socket = new RFC2817Socket(); + proxySocket.setProxyInfo(proxyHost, proxyPort); + proxySocket.addEventListener(ProgressEvent.SOCKET_DATA, onSocketData); + rawSocket = socket = proxySocket; + } else { + rawSocket = new Socket(); + if (scheme == "wss") { + tlsConfig= new TLSConfig(TLSEngine.CLIENT, + null, null, null, null, null, + TLSSecurityParameters.PROTOCOL_VERSION); + tlsConfig.trustAllCertificates = true; + tlsConfig.ignoreCommonNameMismatch = true; + tlsSocket = new TLSSocket(); + tlsSocket.addEventListener(ProgressEvent.SOCKET_DATA, onSocketData); + socket = tlsSocket; + } else { + rawSocket.addEventListener(ProgressEvent.SOCKET_DATA, onSocketData); + socket = rawSocket; + } + } + rawSocket.addEventListener(Event.CLOSE, onSocketClose); + rawSocket.addEventListener(Event.CONNECT, onSocketConnect); + rawSocket.addEventListener(IOErrorEvent.IO_ERROR, onSocketIoError); + rawSocket.addEventListener(SecurityErrorEvent.SECURITY_ERROR, onSocketSecurityError); + rawSocket.connect(host, port); + } + + /** + * @return This WebSocket's ID. + */ + public function getId():int { + return this.id; + } + + /** + * @return this WebSocket's readyState. + */ + public function getReadyState():int { + return this.readyState; + } + + public function getAcceptedProtocol():String { + return this.acceptedProtocol; + } + + public function send(encData:String):int { + var data:String; + try { + data = decodeURIComponent(encData); + } catch (ex:URIError) { + logger.error("SYNTAX_ERR: URIError in send()"); + return 0; + } + logger.log("send: " + data); + var dataBytes:ByteArray = new ByteArray(); + dataBytes.writeUTFBytes(data); + if (readyState == OPEN) { + // TODO: binary API support + var frame:WebSocketFrame = new WebSocketFrame(); + frame.opcode = OPCODE_TEXT; + frame.payload = dataBytes; + if (sendFrame(frame)) { + return -1; + } else { + return dataBytes.length; + } + } else if (readyState == CLOSING || readyState == CLOSED) { + return dataBytes.length; + } else { + fatal("invalid state"); + return 0; + } + } + + public function close( + code:int = STATUS_NO_CODE, reason:String = "", origin:String = "client"):void { + if (code != STATUS_NORMAL_CLOSURE && + code != STATUS_NO_CODE && + code != STATUS_CONNECTION_ERROR) { + logger.error(StringUtil.substitute( + "Fail connection by {0}: code={1} reason={2}", origin, code, reason)); + } + var closeConnection:Boolean = + code == STATUS_CONNECTION_ERROR || origin == "server"; + try { + if (readyState == OPEN && code != STATUS_CONNECTION_ERROR) { + var frame:WebSocketFrame = new WebSocketFrame(); + frame.opcode = OPCODE_CLOSE; + frame.payload = new ByteArray(); + if (origin == "client" && code != STATUS_NO_CODE) { + frame.payload.writeShort(code); + frame.payload.writeUTFBytes(reason); + } + sendFrame(frame); + } + if (closeConnection) { + socket.close(); + } + } catch (ex:Error) { + logger.error("Error: " + ex.message); + } + if (closeConnection) { + logger.log("closed"); + var fireErrorEvent:Boolean = readyState != CONNECTING && code == STATUS_CONNECTION_ERROR; + readyState = CLOSED; + if (fireErrorEvent) { + dispatchEvent(new WebSocketEvent("error")); + } else { + var wasClean:Boolean = code != STATUS_CLOSED_ABNORMALLY && code != STATUS_CONNECTION_ERROR; + var eventCode:int = code == STATUS_CONNECTION_ERROR ? STATUS_CLOSED_ABNORMALLY : code; + dispatchCloseEvent(wasClean, eventCode, reason); + } + } else { + logger.log("closing"); + readyState = CLOSING; + } + } + + private function onSocketConnect(event:Event):void { + logger.log("connected"); + + if (scheme == "wss") { + logger.log("starting SSL/TLS"); + tlsSocket.startTLS(rawSocket, host, tlsConfig); + } + + var defaultPort:int = scheme == "wss" ? 443 : 80; + var hostValue:String = host + (port == defaultPort ? "" : ":" + port); + var key:String = generateKey(); + + SHA1.b64pad = "="; + expectedDigest = SHA1.b64_sha1(key + WEB_SOCKET_GUID); + + var opt:String = ""; + if (requestedProtocols.length > 0) { + opt += "Sec-WebSocket-Protocol: " + requestedProtocols.join(",") + "\r\n"; + } + // if caller passes additional headers they must end with "\r\n" + if (headers) opt += headers; + + var req:String = StringUtil.substitute( + "GET {0} HTTP/1.1\r\n" + + "Host: {1}\r\n" + + "Upgrade: websocket\r\n" + + "Connection: Upgrade\r\n" + + "Sec-WebSocket-Key: {2}\r\n" + + "Sec-WebSocket-Origin: {3}\r\n" + + "Sec-WebSocket-Version: 8\r\n" + + "Cookie: {4}\r\n" + + "{5}" + + "\r\n", + path, hostValue, key, origin, cookie, opt); + logger.log("request header:\n" + req); + socket.writeUTFBytes(req); + socket.flush(); + } + + private function onSocketClose(event:Event):void { + logger.log("closed"); + readyState = CLOSED; + dispatchCloseEvent(false, STATUS_CLOSED_ABNORMALLY, ""); + } + + private function onSocketIoError(event:IOErrorEvent):void { + var message:String; + if (readyState == CONNECTING) { + message = "cannot connect to Web Socket server at " + url + " (IoError: " + event.text + ")"; + } else { + message = + "error communicating with Web Socket server at " + url + + " (IoError: " + event.text + ")"; + } + onConnectionError(message); + } + + private function onSocketSecurityError(event:SecurityErrorEvent):void { + var message:String; + if (readyState == CONNECTING) { + message = + "cannot connect to Web Socket server at " + url + " (SecurityError: " + event.text + ")\n" + + "make sure the server is running and Flash socket policy file is correctly placed"; + } else { + message = + "error communicating with Web Socket server at " + url + + " (SecurityError: " + event.text + ")"; + } + onConnectionError(message); + } + + private function onConnectionError(message:String):void { + if (readyState == CLOSED) return; + logger.error(message); + close(STATUS_CONNECTION_ERROR); + } + + private function onSocketData(event:ProgressEvent):void { + var pos:int = buffer.length; + socket.readBytes(buffer, pos); + for (; pos < buffer.length; ++pos) { + if (headerState < 4) { + // try to find "\r\n\r\n" + if ((headerState == 0 || headerState == 2) && buffer[pos] == 0x0d) { + ++headerState; + } else if ((headerState == 1 || headerState == 3) && buffer[pos] == 0x0a) { + ++headerState; + } else { + headerState = 0; + } + if (headerState == 4) { + var headerStr:String = readUTFBytes(buffer, 0, pos + 1); + logger.log("response header:\n" + headerStr); + if (!validateHandshake(headerStr)) return; + removeBufferBefore(pos + 1); + pos = -1; + readyState = OPEN; + this.dispatchEvent(new WebSocketEvent("open")); + } + } else { + var frame:WebSocketFrame = parseFrame(); + if (frame) { + removeBufferBefore(frame.length); + pos = -1; + if (frame.rsv != 0) { + close(1002, "RSV must be 0."); + } else if (frame.opcode >= 0x08 && frame.opcode <= 0x0f && frame.payload.length >= 126) { + close(1004, "Payload of control frame must be less than 126 bytes."); + } else { + switch (frame.opcode) { + case OPCODE_CONTINUATION: + close(1003, "Received continuation frame, which is not implemented."); + break; + case OPCODE_TEXT: + var data:String = readUTFBytes(frame.payload, 0, frame.payload.length); + try { + this.dispatchEvent(new WebSocketEvent("message", encodeURIComponent(data))); + } catch (ex:URIError) { + close(1007, "URIError while encoding the received data."); + } + break; + case OPCODE_BINARY: + close(1003, "Received binary data, which is not supported."); + break; + case OPCODE_CLOSE: + // Extracts code and reason string. + var code:int = STATUS_NO_CODE; + var reason:String = ""; + if (frame.payload.length >= 2) { + frame.payload.endian = Endian.BIG_ENDIAN; + frame.payload.position = 0; + code = frame.payload.readUnsignedShort(); + reason = readUTFBytes(frame.payload, 2, frame.payload.length - 2); + } + logger.log("received closing frame"); + close(code, reason, "server"); + break; + case OPCODE_PING: + sendPong(frame.payload); + break; + case OPCODE_PONG: + break; + default: + close(1002, "Received unknown opcode: " + frame.opcode); + break; + } + } + } + } + } + } + + private function validateHandshake(headerStr:String):Boolean { + var lines:Array = headerStr.split(/\r\n/); + if (!lines[0].match(/^HTTP\/1.1 101 /)) { + onConnectionError("bad response: " + lines[0]); + return false; + } + var header:Object = {}; + var lowerHeader:Object = {}; + for (var i:int = 1; i < lines.length; ++i) { + if (lines[i].length == 0) continue; + var m:Array = lines[i].match(/^(\S+): (.*)$/); + if (!m) { + onConnectionError("failed to parse response header line: " + lines[i]); + return false; + } + header[m[1].toLowerCase()] = m[2]; + lowerHeader[m[1].toLowerCase()] = m[2].toLowerCase(); + } + if (lowerHeader["upgrade"] != "websocket") { + onConnectionError("invalid Upgrade: " + header["Upgrade"]); + return false; + } + if (lowerHeader["connection"] != "upgrade") { + onConnectionError("invalid Connection: " + header["Connection"]); + return false; + } + if (!lowerHeader["sec-websocket-accept"]) { + onConnectionError( + "The WebSocket server speaks old WebSocket protocol, " + + "which is not supported by web-socket-js. " + + "It requires WebSocket protocol HyBi 10. " + + "Try newer version of the server if available."); + return false; + } + var replyDigest:String = header["sec-websocket-accept"] + if (replyDigest != expectedDigest) { + onConnectionError("digest doesn't match: " + replyDigest + " != " + expectedDigest); + return false; + } + if (requestedProtocols.length > 0) { + acceptedProtocol = header["sec-websocket-protocol"]; + if (requestedProtocols.indexOf(acceptedProtocol) < 0) { + onConnectionError("protocol doesn't match: '" + + acceptedProtocol + "' not in '" + requestedProtocols.join(",") + "'"); + return false; + } + } + return true; + } + + private function sendPong(payload:ByteArray):Boolean { + var frame:WebSocketFrame = new WebSocketFrame(); + frame.opcode = OPCODE_PONG; + frame.payload = payload; + return sendFrame(frame); + } + + private function sendFrame(frame:WebSocketFrame):Boolean { + + var plength:uint = frame.payload.length; + + // Generates a mask. + var mask:ByteArray = new ByteArray(); + for (var i:int = 0; i < 4; i++) { + mask.writeByte(randomInt(0, 255)); + } + + var header:ByteArray = new ByteArray(); + // FIN + RSV + opcode + header.writeByte((frame.fin ? 0x80 : 0x00) | (frame.rsv << 4) | frame.opcode); + if (plength <= 125) { + header.writeByte(0x80 | plength); // Masked + length + } else if (plength > 125 && plength < 65536) { + header.writeByte(0x80 | 126); // Masked + 126 + header.writeShort(plength); + } else if (plength >= 65536 && plength < 4294967296) { + header.writeByte(0x80 | 127); // Masked + 127 + header.writeUnsignedInt(0); // zero high order bits + header.writeUnsignedInt(plength); + } else { + fatal("Send frame size too large"); + } + header.writeBytes(mask); + + var maskedPayload:ByteArray = new ByteArray(); + maskedPayload.length = frame.payload.length; + for (i = 0; i < frame.payload.length; i++) { + maskedPayload[i] = mask[i % 4] ^ frame.payload[i]; + } + + try { + socket.writeBytes(header); + socket.writeBytes(maskedPayload); + socket.flush(); + } catch (ex:IOError) { + logger.error("IOError while sending frame"); + // TODO Fire close event if it hasn't + readyState = CLOSED; + return false; + } + return true; + + } + + private function parseFrame():WebSocketFrame { + + var frame:WebSocketFrame = new WebSocketFrame(); + var hlength:uint = 0; + var plength:uint = 0; + + hlength = 2; + if (buffer.length < hlength) { + return null; + } + + frame.fin = (buffer[0] & 0x80) != 0; + frame.rsv = (buffer[0] & 0x70) >> 4; + frame.opcode = buffer[0] & 0x0f; + plength = buffer[1] & 0x7f; + + if (plength == 126) { + + hlength = 4; + if (buffer.length < hlength) { + return null; + } + buffer.endian = Endian.BIG_ENDIAN; + buffer.position = 2; + plength = buffer.readUnsignedShort(); + + } else if (plength == 127) { + + hlength = 10; + if (buffer.length < hlength) { + return null; + } + buffer.endian = Endian.BIG_ENDIAN; + buffer.position = 2; + // Protocol allows 64-bit length, but we only handle 32-bit + var big:uint = buffer.readUnsignedInt(); // Skip high 32-bits + plength = buffer.readUnsignedInt(); // Low 32-bits + if (big != 0) { + fatal("Frame length exceeds 4294967295. Bailing out!"); + return null; + } + + } + + if (buffer.length < hlength + plength) { + return null; + } + + frame.length = hlength + plength; + frame.payload = new ByteArray(); + buffer.position = hlength; + buffer.readBytes(frame.payload, 0, plength); + return frame; + + } + + private function dispatchCloseEvent(wasClean:Boolean, code:int, reason:String):void { + var event:WebSocketEvent = new WebSocketEvent("close"); + event.wasClean = wasClean; + event.code = code; + event.reason = reason; + dispatchEvent(event); + } + + private function removeBufferBefore(pos:int):void { + if (pos == 0) return; + var nextBuffer:ByteArray = new ByteArray(); + buffer.position = pos; + buffer.readBytes(nextBuffer); + buffer = nextBuffer; + } + + private function generateKey():String { + var vals:ByteArray = new ByteArray(); + vals.length = 16; + for (var i:int = 0; i < vals.length; ++i) { + vals[i] = randomInt(0, 127); + } + base64Encoder.reset(); + base64Encoder.encodeBytes(vals); + return base64Encoder.toString(); + } + + private function readUTFBytes(buffer:ByteArray, start:int, numBytes:int):String { + buffer.position = start; + var data:String = ""; + for(var i:int = start; i < start + numBytes; ++i) { + // Workaround of a bug of ByteArray#readUTFBytes() that bytes after "\x00" is discarded. + if (buffer[i] == 0x00) { + data += buffer.readUTFBytes(i - buffer.position) + "\x00"; + buffer.position = i + 1; + } + } + data += buffer.readUTFBytes(start + numBytes - buffer.position); + return data; + } + + private function randomInt(min:uint, max:uint):uint { + return min + Math.floor(Math.random() * (Number(max) - min + 1)); + } + + private function fatal(message:String):void { + logger.error(message); + throw message; + } + +} + +}