WebSocket.as 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440
  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-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. import com.gsolo.encryption.MD5;
  22. [Event(name="message", type="WebSocketMessageEvent")]
  23. [Event(name="open", type="flash.events.Event")]
  24. [Event(name="close", type="flash.events.Event")]
  25. [Event(name="error", type="flash.events.Event")]
  26. [Event(name="stateChange", type="WebSocketStateEvent")]
  27. public class WebSocket extends EventDispatcher {
  28. private static var CONNECTING:int = 0;
  29. private static var OPEN:int = 1;
  30. private static var CLOSING:int = 2;
  31. private static var CLOSED:int = 3;
  32. //private var rawSocket:RFC2817Socket;
  33. private var rawSocket:Socket;
  34. private var tlsSocket:TLSSocket;
  35. private var tlsConfig:TLSConfig;
  36. private var socket:Socket;
  37. private var main:WebSocketMain;
  38. private var url:String;
  39. private var scheme:String;
  40. private var host:String;
  41. private var port:uint;
  42. private var path:String;
  43. private var origin:String;
  44. private var protocol:String;
  45. private var buffer:ByteArray = new ByteArray();
  46. private var headerState:int = 0;
  47. private var readyState:int = CONNECTING;
  48. private var bufferedAmount:int = 0;
  49. private var headers:String;
  50. private var noiseChars:Array;
  51. private var expectedDigest:String;
  52. public function WebSocket(
  53. main:WebSocketMain, url:String, protocol:String,
  54. proxyHost:String = null, proxyPort:int = 0,
  55. headers:String = null) {
  56. this.main = main;
  57. initNoiseChars();
  58. this.url = url;
  59. var m:Array = url.match(/^(\w+):\/\/([^\/:]+)(:(\d+))?(\/.*)?$/);
  60. if (!m) main.fatal("SYNTAX_ERR: invalid url: " + url);
  61. this.scheme = m[1];
  62. this.host = m[2];
  63. this.port = parseInt(m[4] || "80");
  64. this.path = m[5] || "/";
  65. this.origin = main.getOrigin();
  66. this.protocol = protocol;
  67. // if present and not the empty string, headers MUST end with \r\n
  68. // headers should be zero or more complete lines, for example
  69. // "Header1: xxx\r\nHeader2: yyyy\r\n"
  70. this.headers = headers;
  71. /*
  72. socket = new RFC2817Socket();
  73. // if no proxy information is supplied, it acts like a normal Socket
  74. // @see RFC2817Socket::connect
  75. if (proxyHost != null && proxyPort != 0){
  76. socket.setProxyInfo(proxyHost, proxyPort);
  77. }
  78. */
  79. ExternalInterface.call("console.log", "[WebSocket] scheme: " + scheme);
  80. rawSocket = new Socket();
  81. rawSocket.addEventListener(Event.CLOSE, onSocketClose);
  82. rawSocket.addEventListener(Event.CONNECT, onSocketConnect);
  83. rawSocket.addEventListener(IOErrorEvent.IO_ERROR, onSocketIoError);
  84. rawSocket.addEventListener(SecurityErrorEvent.SECURITY_ERROR, onSocketSecurityError);
  85. if (scheme == "wss") {
  86. tlsConfig= new TLSConfig(TLSEngine.CLIENT,
  87. null, null, null, null, null,
  88. TLSSecurityParameters.PROTOCOL_VERSION);
  89. tlsConfig.trustSelfSignedCertificates = true;
  90. tlsConfig.ignoreCommonNameMismatch = true;
  91. tlsSocket = new TLSSocket();
  92. tlsSocket.addEventListener(ProgressEvent.SOCKET_DATA, onSocketData);
  93. socket = (tlsSocket as Socket);
  94. } else {
  95. rawSocket.addEventListener(ProgressEvent.SOCKET_DATA, onSocketData);
  96. socket = (rawSocket as Socket);
  97. }
  98. rawSocket.connect(host, port);
  99. }
  100. public function send(data:String):int {
  101. if (readyState == OPEN) {
  102. socket.writeByte(0x00);
  103. socket.writeUTFBytes(decodeURIComponent(data));
  104. socket.writeByte(0xff);
  105. socket.flush();
  106. main.log("sent: " + data);
  107. return -1;
  108. } else if (readyState == CLOSED) {
  109. var bytes:ByteArray = new ByteArray();
  110. bytes.writeUTFBytes(decodeURIComponent(data));
  111. bufferedAmount += bytes.length; // not sure whether it should include \x00 and \xff
  112. // We use return value to let caller know bufferedAmount because we cannot fire
  113. // stateChange event here which causes weird error:
  114. // > You are trying to call recursively into the Flash Player which is not allowed.
  115. return bufferedAmount;
  116. } else {
  117. main.fatal("INVALID_STATE_ERR: invalid state");
  118. return 0;
  119. }
  120. }
  121. public function close():void {
  122. main.log("close");
  123. try {
  124. socket.close();
  125. } catch (ex:Error) { }
  126. readyState = CLOSED;
  127. // We don't fire any events here because it causes weird error:
  128. // > You are trying to call recursively into the Flash Player which is not allowed.
  129. // We do something equivalent in JavaScript WebSocket#close instead.
  130. }
  131. public function getReadyState():int {
  132. return readyState;
  133. }
  134. public function getBufferedAmount():int {
  135. return bufferedAmount;
  136. }
  137. private function onSocketConnect(event:Event):void {
  138. main.log("connected");
  139. if (scheme == "wss") {
  140. ExternalInterface.call("console.log", "[WebSocket] starting SSL/TLS");
  141. tlsSocket.startTLS(rawSocket, host, tlsConfig);
  142. }
  143. var hostValue:String = host + (port == 80 ? "" : ":" + port);
  144. var cookie:String = "";
  145. if (main.getCallerHost() == host) {
  146. cookie = ExternalInterface.call("function(){return document.cookie}");
  147. }
  148. var key1:String = generateKey();
  149. var key2:String = generateKey();
  150. var key3:String = generateKey3();
  151. expectedDigest = getSecurityDigest(key1, key2, key3);
  152. var opt:String = "";
  153. if (protocol) opt += "WebSocket-Protocol: " + protocol + "\r\n";
  154. // if caller passes additional headers they must end with "\r\n"
  155. if (headers) opt += headers;
  156. var req:String = StringUtil.substitute(
  157. "GET {0} HTTP/1.1\r\n" +
  158. "Upgrade: WebSocket\r\n" +
  159. "Connection: Upgrade\r\n" +
  160. "Host: {1}\r\n" +
  161. "Origin: {2}\r\n" +
  162. "Cookie: {3}\r\n" +
  163. "Sec-WebSocket-Key1: {4}\r\n" +
  164. "Sec-WebSocket-Key2: {5}\r\n" +
  165. "{6}" +
  166. "\r\n",
  167. path, hostValue, origin, cookie, key1, key2, opt);
  168. main.log("request header:\n" + req);
  169. socket.writeUTFBytes(req);
  170. main.log("sent key3: " + key3);
  171. main.log("expected digest: " + expectedDigest);
  172. writeBytes(key3);
  173. socket.flush();
  174. }
  175. private function onSocketClose(event:Event):void {
  176. main.log("closed");
  177. readyState = CLOSED;
  178. notifyStateChange();
  179. dispatchEvent(new Event("close"));
  180. }
  181. private function onSocketIoError(event:IOErrorEvent):void {
  182. var message:String;
  183. if (readyState == CONNECTING) {
  184. message = "cannot connect to Web Socket server at " + url + " (IoError)";
  185. } else {
  186. message = "error communicating with Web Socket server at " + url + " (IoError)";
  187. }
  188. onError(message);
  189. }
  190. private function onSocketSecurityError(event:SecurityErrorEvent):void {
  191. var message:String;
  192. if (readyState == CONNECTING) {
  193. message =
  194. "cannot connect to Web Socket server at " + url + " (SecurityError)\n" +
  195. "make sure the server is running and Flash socket policy file is correctly placed";
  196. } else {
  197. message = "error communicating with Web Socket server at " + url + " (SecurityError)";
  198. }
  199. onError(message);
  200. }
  201. private function onError(message:String):void {
  202. var state:int = readyState;
  203. if (state == CLOSED) return;
  204. main.error(message);
  205. close();
  206. notifyStateChange();
  207. dispatchEvent(new Event(state == CONNECTING ? "close" : "error"));
  208. }
  209. private function onSocketData(event:ProgressEvent):void {
  210. var pos:int = buffer.length;
  211. socket.readBytes(buffer, pos);
  212. for (; pos < buffer.length; ++pos) {
  213. if (headerState < 4) {
  214. // try to find "\r\n\r\n"
  215. if ((headerState == 0 || headerState == 2) && buffer[pos] == 0x0d) {
  216. ++headerState;
  217. } else if ((headerState == 1 || headerState == 3) && buffer[pos] == 0x0a) {
  218. ++headerState;
  219. } else {
  220. headerState = 0;
  221. }
  222. if (headerState == 4) {
  223. var headerStr:String = buffer.readUTFBytes(pos + 1);
  224. main.log("response header:\n" + headerStr);
  225. if (!validateHeader(headerStr)) return;
  226. makeBufferCompact();
  227. pos = -1;
  228. }
  229. } else if (headerState == 4) {
  230. var replyDigest:String = readBytes(buffer, 16);
  231. main.log("reply digest: " + replyDigest);
  232. if (replyDigest != expectedDigest) {
  233. onError("digest doesn't match: " + replyDigest + " != " + expectedDigest);
  234. return;
  235. }
  236. headerState = 5;
  237. makeBufferCompact();
  238. pos = -1;
  239. readyState = OPEN;
  240. notifyStateChange();
  241. dispatchEvent(new Event("open"));
  242. } else {
  243. if (buffer[pos] == 0xff) {
  244. //if (buffer.bytesAvailable > 1) {
  245. if (buffer.readByte() != 0x00) {
  246. onError("data must start with \\x00");
  247. return;
  248. }
  249. /*
  250. var data:String = "", byte:uint;
  251. while (buffer.bytesAvailable > 1) {
  252. byte = buffer[buffer.position];
  253. if (byte === 0x00) {
  254. // readUTFBytes mishandles 0x00
  255. data = data + "\x00";
  256. buffer.position++;
  257. } else if (byte === 0xff) {
  258. // End of WebSocket frame
  259. //ExternalInterface.call("console.log", "[WebSocket] early 0xff found");
  260. break;
  261. } else if ((byte & 0x80) === 0x00) {
  262. // One UTF-8 input byte to one output byte
  263. data = data + buffer.readUTFBytes(1);
  264. } else {
  265. // Assume two UTF-8 input bytes to one output byte
  266. data = data + buffer.readUTFBytes(2);
  267. }
  268. }
  269. */
  270. var data:String = buffer.readUTFBytes(pos - 1);
  271. main.log("received: " + data);
  272. dispatchEvent(new WebSocketMessageEvent("message", encodeURIComponent(data)));
  273. buffer.readByte();
  274. makeBufferCompact();
  275. pos = -1;
  276. }
  277. }
  278. }
  279. }
  280. private function validateHeader(headerStr:String):Boolean {
  281. var lines:Array = headerStr.split(/\r\n/);
  282. if (!lines[0].match(/^HTTP\/1.1 101 /)) {
  283. onError("bad response: " + lines[0]);
  284. return false;
  285. }
  286. var header:Object = {};
  287. for (var i:int = 1; i < lines.length; ++i) {
  288. if (lines[i].length == 0) continue;
  289. var m:Array = lines[i].match(/^(\S+): (.*)$/);
  290. if (!m) {
  291. onError("failed to parse response header line: " + lines[i]);
  292. return false;
  293. }
  294. header[m[1]] = m[2];
  295. }
  296. if (header["Upgrade"] != "WebSocket") {
  297. onError("invalid Upgrade: " + header["Upgrade"]);
  298. return false;
  299. }
  300. if (header["Connection"] != "Upgrade") {
  301. onError("invalid Connection: " + header["Connection"]);
  302. return false;
  303. }
  304. var resOrigin:String = header["Sec-WebSocket-Origin"].toLowerCase();
  305. if (resOrigin != origin) {
  306. onError("origin doesn't match: '" + resOrigin + "' != '" + origin + "'");
  307. return false;
  308. }
  309. if (protocol && header["Sec-WebSocket-Protocol"] != protocol) {
  310. onError("protocol doesn't match: '" +
  311. header["WebSocket-Protocol"] + "' != '" + protocol + "'");
  312. return false;
  313. }
  314. return true;
  315. }
  316. private function makeBufferCompact():void {
  317. if (buffer.position == 0) return;
  318. var nextBuffer:ByteArray = new ByteArray();
  319. buffer.readBytes(nextBuffer);
  320. buffer = nextBuffer;
  321. }
  322. private function notifyStateChange():void {
  323. dispatchEvent(new WebSocketStateEvent("stateChange", readyState, bufferedAmount));
  324. }
  325. private function initNoiseChars():void {
  326. noiseChars = new Array();
  327. for (var i:int = 0x21; i <= 0x2f; ++i) {
  328. noiseChars.push(String.fromCharCode(i));
  329. }
  330. for (var j:int = 0x3a; j <= 0x7a; ++j) {
  331. noiseChars.push(String.fromCharCode(j));
  332. }
  333. }
  334. private function generateKey():String {
  335. var spaces:uint = randomInt(1, 12);
  336. var max:uint = uint.MAX_VALUE / spaces;
  337. var number:uint = randomInt(0, max);
  338. var key:String = (number * spaces).toString();
  339. var noises:int = randomInt(1, 12);
  340. var pos:int;
  341. for (var i:int = 0; i < noises; ++i) {
  342. var char:String = noiseChars[randomInt(0, noiseChars.length - 1)];
  343. pos = randomInt(0, key.length);
  344. key = key.substr(0, pos) + char + key.substr(pos);
  345. }
  346. for (var j:int = 0; j < spaces; ++j) {
  347. pos = randomInt(1, key.length - 1);
  348. key = key.substr(0, pos) + " " + key.substr(pos);
  349. }
  350. return key;
  351. }
  352. private function generateKey3():String {
  353. var key3:String = "";
  354. for (var i:int = 0; i < 8; ++i) {
  355. key3 += String.fromCharCode(randomInt(0, 255));
  356. }
  357. return key3;
  358. }
  359. private function getSecurityDigest(key1:String, key2:String, key3:String):String {
  360. var bytes1:String = keyToBytes(key1);
  361. var bytes2:String = keyToBytes(key2);
  362. return MD5.rstr_md5(bytes1 + bytes2 + key3);
  363. }
  364. private function keyToBytes(key:String):String {
  365. var keyNum:uint = parseInt(key.replace(/[^\d]/g, ""));
  366. var spaces:uint = 0;
  367. for (var i:int = 0; i < key.length; ++i) {
  368. if (key.charAt(i) == " ") ++spaces;
  369. }
  370. var resultNum:uint = keyNum / spaces;
  371. var bytes:String = "";
  372. for (var j:int = 3; j >= 0; --j) {
  373. bytes += String.fromCharCode((resultNum >> (j * 8)) & 0xff);
  374. }
  375. return bytes;
  376. }
  377. private function writeBytes(bytes:String):void {
  378. for (var i:int = 0; i < bytes.length; ++i) {
  379. socket.writeByte(bytes.charCodeAt(i));
  380. }
  381. }
  382. private function readBytes(buffer:ByteArray, numBytes:int):String {
  383. var bytes:String = "";
  384. for (var i:int = 0; i < numBytes; ++i) {
  385. // & 0xff is to make \x80-\xff positive number.
  386. bytes += String.fromCharCode(buffer.readByte() & 0xff);
  387. }
  388. return bytes;
  389. }
  390. private function randomInt(min:uint, max:uint):uint {
  391. return min + Math.floor(Math.random() * (Number(max) - min + 1));
  392. }
  393. // for debug
  394. private function dumpBytes(bytes:String):void {
  395. var output:String = "";
  396. for (var i:int = 0; i < bytes.length; ++i) {
  397. output += bytes.charCodeAt(i).toString() + ", ";
  398. }
  399. main.log(output);
  400. }
  401. }
  402. }