WebSocket.as 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. // Copyright: Hiroshi Ichikawa <http://gimite.net/en/>
  2. // Lincense: New BSD Lincense
  3. // Reference: http://dev.w3.org/html5/websockets/
  4. // Reference: http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-31
  5. package {
  6. import flash.display.*;
  7. import flash.events.*;
  8. import flash.external.*;
  9. import flash.net.*;
  10. import flash.system.*;
  11. import flash.utils.*;
  12. import mx.core.*;
  13. import mx.controls.*;
  14. import mx.events.*;
  15. import mx.utils.*;
  16. import com.adobe.net.proxies.RFC2817Socket;
  17. import com.hurlant.crypto.tls.TLSSocket;
  18. import com.hurlant.crypto.tls.TLSConfig;
  19. import com.hurlant.crypto.tls.TLSEngine;
  20. import com.hurlant.crypto.tls.TLSSecurityParameters;
  21. [Event(name="message", type="WebSocketMessageEvent")]
  22. [Event(name="open", type="flash.events.Event")]
  23. [Event(name="close", type="flash.events.Event")]
  24. [Event(name="stateChange", type="WebSocketStateEvent")]
  25. public class WebSocket extends EventDispatcher {
  26. private static var CONNECTING:int = 0;
  27. private static var OPEN:int = 1;
  28. private static var CLOSED:int = 2;
  29. //private var rawSocket:RFC2817Socket;
  30. private var rawSocket:Socket;
  31. private var tlsSocket:TLSSocket;
  32. private var tlsConfig:TLSConfig;
  33. private var socket:Socket;
  34. private var main:WebSocketMain;
  35. private var scheme:String;
  36. private var host:String;
  37. private var port:uint;
  38. private var path:String;
  39. private var origin:String;
  40. private var protocol:String;
  41. private var buffer:ByteArray = new ByteArray();
  42. private var headerState:int = 0;
  43. private var readyState:int = CONNECTING;
  44. private var bufferedAmount:int = 0;
  45. private var headers:String;
  46. public function WebSocket(
  47. main:WebSocketMain, url:String, protocol:String,
  48. proxyHost:String = null, proxyPort:int = 0,
  49. headers:String = null) {
  50. this.main = main;
  51. var m:Array = url.match(/^(\w+):\/\/([^\/:]+)(:(\d+))?(\/.*)?$/);
  52. if (!m) main.fatal("invalid url: " + url);
  53. this.scheme = m[1];
  54. this.host = m[2];
  55. this.port = parseInt(m[4] || "80");
  56. this.path = m[5] || "/";
  57. this.origin = main.getOrigin();
  58. this.protocol = protocol;
  59. // if present and not the empty string, headers MUST end with \r\n
  60. // headers should be zero or more complete lines, for example
  61. // "Header1: xxx\r\nHeader2: yyyy\r\n"
  62. this.headers = headers;
  63. /*
  64. socket = new RFC2817Socket();
  65. // if no proxy information is supplied, it acts like a normal Socket
  66. // @see RFC2817Socket::connect
  67. if (proxyHost != null && proxyPort != 0){
  68. socket.setProxyInfo(proxyHost, proxyPort);
  69. }
  70. */
  71. ExternalInterface.call("console.log", "[WebSocket] scheme: " + scheme);
  72. rawSocket = new Socket();
  73. rawSocket.addEventListener(Event.CLOSE, onSocketClose);
  74. rawSocket.addEventListener(Event.CONNECT, onSocketConnect);
  75. rawSocket.addEventListener(IOErrorEvent.IO_ERROR, onSocketIoError);
  76. rawSocket.addEventListener(SecurityErrorEvent.SECURITY_ERROR, onSocketSecurityError);
  77. if (scheme == "wss") {
  78. tlsConfig= new TLSConfig(TLSEngine.CLIENT,
  79. null, null, null, null, null,
  80. TLSSecurityParameters.PROTOCOL_VERSION);
  81. tlsConfig.trustSelfSignedCertificates = true;
  82. tlsConfig.ignoreCommonNameMismatch = true;
  83. tlsSocket = new TLSSocket();
  84. tlsSocket.addEventListener(ProgressEvent.SOCKET_DATA, onSocketData);
  85. socket = (tlsSocket as Socket);
  86. } else {
  87. rawSocket.addEventListener(ProgressEvent.SOCKET_DATA, onSocketData);
  88. socket = (rawSocket as Socket);
  89. }
  90. rawSocket.connect(host, port);
  91. }
  92. public function send(data:String):int {
  93. if (readyState == OPEN) {
  94. socket.writeByte(0x00);
  95. socket.writeUTFBytes(data);
  96. socket.writeByte(0xff);
  97. socket.flush();
  98. main.log("sent: " + data);
  99. return -1;
  100. } else if (readyState == CLOSED) {
  101. var bytes:ByteArray = new ByteArray();
  102. bytes.writeUTFBytes(data);
  103. bufferedAmount += bytes.length; // not sure whether it should include \x00 and \xff
  104. // We use return value to let caller know bufferedAmount because we cannot fire
  105. // stateChange event here which causes weird error:
  106. // > You are trying to call recursively into the Flash Player which is not allowed.
  107. return bufferedAmount;
  108. } else {
  109. main.fatal("invalid state");
  110. return 0;
  111. }
  112. }
  113. public function close():void {
  114. main.log("close");
  115. try {
  116. socket.close();
  117. } catch (ex:Error) { }
  118. readyState = CLOSED;
  119. // We don't fire any events here because it causes weird error:
  120. // > You are trying to call recursively into the Flash Player which is not allowed.
  121. // We do something equivalent in JavaScript WebSocket#close instead.
  122. }
  123. public function getReadyState():int {
  124. return readyState;
  125. }
  126. public function getBufferedAmount():int {
  127. return bufferedAmount;
  128. }
  129. private function onSocketConnect(event:Event):void {
  130. main.log("connected");
  131. if (scheme == "wss") {
  132. ExternalInterface.call("console.log", "[WebSocket] starting SSL/TLS");
  133. tlsSocket.startTLS(rawSocket, host, tlsConfig);
  134. }
  135. var hostValue:String = host + (port == 80 ? "" : ":" + port);
  136. var cookie:String = "";
  137. if (main.getCallerHost() == host) {
  138. cookie = ExternalInterface.call("function(){return document.cookie}");
  139. }
  140. var opt:String = "";
  141. if (protocol) opt += "WebSocket-Protocol: " + protocol + "\r\n";
  142. // if caller passes additional headers they must end with "\r\n"
  143. if (headers) opt += headers;
  144. var req:String = StringUtil.substitute(
  145. "GET {0} HTTP/1.1\r\n" +
  146. "Upgrade: WebSocket\r\n" +
  147. "Connection: Upgrade\r\n" +
  148. "Host: {1}\r\n" +
  149. "Origin: {2}\r\n" +
  150. "Cookie: {4}\r\n" +
  151. "{3}" +
  152. "\r\n",
  153. path, hostValue, origin, opt, cookie);
  154. main.log("request header:\n" + req);
  155. socket.writeUTFBytes(req);
  156. socket.flush();
  157. }
  158. private function onSocketClose(event:Event):void {
  159. main.log("closed");
  160. readyState = CLOSED;
  161. notifyStateChange();
  162. dispatchEvent(new Event("close"));
  163. }
  164. private function onSocketIoError(event:IOErrorEvent):void {
  165. close();
  166. main.fatal("failed to connect Web Socket server (IoError)");
  167. }
  168. private function onSocketSecurityError(event:SecurityErrorEvent):void {
  169. close();
  170. main.fatal(
  171. "failed to connect Web Socket server (SecurityError)\n" +
  172. "make sure the server is running and Flash socket policy file is correctly placed");
  173. }
  174. private function onSocketData(event:ProgressEvent):void {
  175. var pos:int = buffer.length;
  176. socket.readBytes(buffer, pos);
  177. for (; pos < buffer.length; ++pos) {
  178. if (headerState != 4) {
  179. // try to find "\r\n\r\n"
  180. if ((headerState == 0 || headerState == 2) && buffer[pos] == 0x0d) {
  181. ++headerState;
  182. } else if ((headerState == 1 || headerState == 3) && buffer[pos] == 0x0a) {
  183. ++headerState;
  184. } else {
  185. headerState = 0;
  186. }
  187. if (headerState == 4) {
  188. var headerStr:String = buffer.readUTFBytes(pos + 1);
  189. main.log("response header:\n" + headerStr);
  190. validateHeader(headerStr);
  191. makeBufferCompact();
  192. pos = -1;
  193. readyState = OPEN;
  194. notifyStateChange();
  195. dispatchEvent(new Event("open"));
  196. }
  197. } else {
  198. if (buffer[pos] == 0xff) {
  199. if (buffer.readByte() != 0x00) {
  200. close();
  201. main.fatal("data must start with \\x00");
  202. }
  203. var data:String = buffer.readUTFBytes(pos - 1);
  204. main.log("received: " + data);
  205. dispatchEvent(new WebSocketMessageEvent("message", encodeURIComponent(data)));
  206. buffer.readByte();
  207. makeBufferCompact();
  208. pos = -1;
  209. }
  210. }
  211. }
  212. }
  213. private function validateHeader(headerStr:String):void {
  214. var lines:Array = headerStr.split(/\r\n/);
  215. if (!lines[0].match(/^HTTP\/1.1 101 /)) {
  216. close();
  217. main.fatal("bad response: " + lines[0]);
  218. }
  219. var header:Object = {};
  220. for (var i:int = 1; i < lines.length; ++i) {
  221. if (lines[i].length == 0) continue;
  222. var m:Array = lines[i].match(/^(\S+): (.*)$/);
  223. if (!m) {
  224. close();
  225. main.fatal("failed to parse response header line: " + lines[i]);
  226. }
  227. header[m[1]] = m[2];
  228. }
  229. if (header["Upgrade"] != "WebSocket") {
  230. close();
  231. main.fatal("invalid Upgrade: " + header["Upgrade"]);
  232. }
  233. if (header["Connection"] != "Upgrade") {
  234. close();
  235. main.fatal("invalid Connection: " + header["Connection"]);
  236. }
  237. var resOrigin:String = header["WebSocket-Origin"].toLowerCase();
  238. if (resOrigin != origin) {
  239. close();
  240. main.fatal("origin doesn't match: '" + resOrigin + "' != '" + origin + "'");
  241. }
  242. if (protocol && header["WebSocket-Protocol"] != protocol) {
  243. close();
  244. main.fatal("protocol doesn't match: '" +
  245. header["WebSocket-Protocol"] + "' != '" + protocol + "'");
  246. }
  247. }
  248. private function makeBufferCompact():void {
  249. if (buffer.position == 0) return;
  250. var nextBuffer:ByteArray = new ByteArray();
  251. buffer.readBytes(nextBuffer);
  252. buffer = nextBuffer;
  253. }
  254. private function notifyStateChange():void {
  255. dispatchEvent(new WebSocketStateEvent("stateChange", readyState, bufferedAmount));
  256. }
  257. }
  258. }