WebSocket.as 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458
  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. 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:Socket;
  33. private var tlsSocket:TLSSocket;
  34. private var tlsConfig:TLSConfig;
  35. private var socket:Socket;
  36. private var main:WebSocketMain;
  37. private var url:String;
  38. private var scheme:String;
  39. private var host:String;
  40. private var port:uint;
  41. private var path:String;
  42. private var origin:String;
  43. private var protocol:String;
  44. private var buffer:ByteArray = new ByteArray();
  45. private var dataQueue:Array;
  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. if (proxyHost != null && proxyPort != 0){
  72. if (scheme == "wss") {
  73. main.fatal("wss with proxy is not supported");
  74. }
  75. var proxySocket:RFC2817Socket = new RFC2817Socket();
  76. proxySocket.setProxyInfo(proxyHost, proxyPort);
  77. proxySocket.addEventListener(ProgressEvent.SOCKET_DATA, onSocketData);
  78. rawSocket = socket = proxySocket;
  79. } else {
  80. rawSocket = new Socket();
  81. if (scheme == "wss") {
  82. tlsConfig= new TLSConfig(TLSEngine.CLIENT,
  83. null, null, null, null, null,
  84. TLSSecurityParameters.PROTOCOL_VERSION);
  85. tlsConfig.trustSelfSignedCertificates = true;
  86. tlsConfig.ignoreCommonNameMismatch = true;
  87. tlsSocket = new TLSSocket();
  88. tlsSocket.addEventListener(ProgressEvent.SOCKET_DATA, onSocketData);
  89. socket = tlsSocket;
  90. } else {
  91. rawSocket.addEventListener(ProgressEvent.SOCKET_DATA, onSocketData);
  92. socket = rawSocket;
  93. }
  94. }
  95. rawSocket.addEventListener(Event.CLOSE, onSocketClose);
  96. rawSocket.addEventListener(Event.CONNECT, onSocketConnect);
  97. rawSocket.addEventListener(IOErrorEvent.IO_ERROR, onSocketIoError);
  98. rawSocket.addEventListener(SecurityErrorEvent.SECURITY_ERROR, onSocketSecurityError);
  99. rawSocket.connect(host, port);
  100. }
  101. public function send(encData:String):int {
  102. var data:String = decodeURIComponent(encData);
  103. if (readyState == OPEN) {
  104. socket.writeByte(0x00);
  105. socket.writeUTFBytes(data);
  106. socket.writeByte(0xff);
  107. socket.flush();
  108. main.log("sent: " + data);
  109. return -1;
  110. } else if (readyState == CLOSED) {
  111. var bytes:ByteArray = new ByteArray();
  112. bytes.writeUTFBytes(data);
  113. bufferedAmount += bytes.length; // not sure whether it should include \x00 and \xff
  114. // We use return value to let caller know bufferedAmount because we cannot fire
  115. // stateChange event here which causes weird error:
  116. // > You are trying to call recursively into the Flash Player which is not allowed.
  117. return bufferedAmount;
  118. } else {
  119. main.fatal("INVALID_STATE_ERR: invalid state");
  120. return 0;
  121. }
  122. }
  123. public function close():void {
  124. main.log("close");
  125. dataQueue = [];
  126. try {
  127. socket.writeByte(0xff);
  128. socket.writeByte(0x00);
  129. socket.flush();
  130. socket.close();
  131. } catch (ex:Error) { }
  132. readyState = CLOSED;
  133. // We don't fire any events here because it causes weird error:
  134. // > You are trying to call recursively into the Flash Player which is not allowed.
  135. // We do something equivalent in JavaScript WebSocket#close instead.
  136. }
  137. public function getReadyState():int {
  138. return readyState;
  139. }
  140. public function getBufferedAmount():int {
  141. return bufferedAmount;
  142. }
  143. private function onSocketConnect(event:Event):void {
  144. main.log("connected");
  145. if (scheme == "wss") {
  146. main.log("starting SSL/TLS");
  147. tlsSocket.startTLS(rawSocket, host, tlsConfig);
  148. }
  149. dataQueue = [];
  150. var hostValue:String = host + (port == 80 ? "" : ":" + port);
  151. var cookie:String = "";
  152. if (main.getCallerHost() == host) {
  153. cookie = ExternalInterface.call("function(){return document.cookie}");
  154. }
  155. var key1:String = generateKey();
  156. var key2:String = generateKey();
  157. var key3:String = generateKey3();
  158. expectedDigest = getSecurityDigest(key1, key2, key3);
  159. var opt:String = "";
  160. if (protocol) opt += "WebSocket-Protocol: " + protocol + "\r\n";
  161. // if caller passes additional headers they must end with "\r\n"
  162. if (headers) opt += headers;
  163. var req:String = StringUtil.substitute(
  164. "GET {0} HTTP/1.1\r\n" +
  165. "Upgrade: WebSocket\r\n" +
  166. "Connection: Upgrade\r\n" +
  167. "Host: {1}\r\n" +
  168. "Origin: {2}\r\n" +
  169. "Cookie: {3}\r\n" +
  170. "Sec-WebSocket-Key1: {4}\r\n" +
  171. "Sec-WebSocket-Key2: {5}\r\n" +
  172. "{6}" +
  173. "\r\n",
  174. path, hostValue, origin, cookie, key1, key2, opt);
  175. main.log("request header:\n" + req);
  176. socket.writeUTFBytes(req);
  177. main.log("sent key3: " + key3);
  178. writeBytes(key3);
  179. socket.flush();
  180. }
  181. private function onSocketClose(event:Event):void {
  182. main.log("closed");
  183. readyState = CLOSED;
  184. notifyStateChange();
  185. dispatchEvent(new Event("close"));
  186. }
  187. private function onSocketIoError(event:IOErrorEvent):void {
  188. var message:String;
  189. if (readyState == CONNECTING) {
  190. message = "cannot connect to Web Socket server at " + url + " (IoError)";
  191. } else {
  192. message = "error communicating with Web Socket server at " + url + " (IoError)";
  193. }
  194. onError(message);
  195. }
  196. private function onSocketSecurityError(event:SecurityErrorEvent):void {
  197. var message:String;
  198. if (readyState == CONNECTING) {
  199. message =
  200. "cannot connect to Web Socket server at " + url + " (SecurityError)\n" +
  201. "make sure the server is running and Flash socket policy file is correctly placed";
  202. } else {
  203. message = "error communicating with Web Socket server at " + url + " (SecurityError)";
  204. }
  205. onError(message);
  206. }
  207. private function onError(message:String):void {
  208. var state:int = readyState;
  209. if (state == CLOSED) return;
  210. main.error(message);
  211. close();
  212. notifyStateChange();
  213. dispatchEvent(new Event(state == CONNECTING ? "close" : "error"));
  214. }
  215. private function onSocketData(event:ProgressEvent):void {
  216. var pos:int = buffer.length;
  217. socket.readBytes(buffer, pos);
  218. for (; pos < buffer.length; ++pos) {
  219. if (headerState < 4) {
  220. // try to find "\r\n\r\n"
  221. if ((headerState == 0 || headerState == 2) && buffer[pos] == 0x0d) {
  222. ++headerState;
  223. } else if ((headerState == 1 || headerState == 3) && buffer[pos] == 0x0a) {
  224. ++headerState;
  225. } else {
  226. headerState = 0;
  227. }
  228. if (headerState == 4) {
  229. buffer.position = 0;
  230. var headerStr:String = buffer.readUTFBytes(pos + 1);
  231. main.log("response header:\n" + headerStr);
  232. if (!validateHeader(headerStr)) return;
  233. removeBufferBefore(pos + 1);
  234. pos = -1;
  235. }
  236. } else if (headerState == 4) {
  237. if (pos == 15) {
  238. buffer.position = 0;
  239. var replyDigest:String = readBytes(buffer, 16);
  240. main.log("reply digest: " + replyDigest);
  241. if (replyDigest != expectedDigest) {
  242. onError("digest doesn't match: " + replyDigest + " != " + expectedDigest);
  243. return;
  244. }
  245. headerState = 5;
  246. removeBufferBefore(pos + 1);
  247. pos = -1;
  248. readyState = OPEN;
  249. notifyStateChange();
  250. dispatchEvent(new Event("open"));
  251. }
  252. } else {
  253. if (buffer[pos] == 0xff && pos > 0) {
  254. if (buffer[0] != 0x00) {
  255. onError("data must start with \\x00");
  256. return;
  257. }
  258. buffer.position = 1;
  259. var data:String = buffer.readUTFBytes(pos - 1);
  260. main.log("received: " + data);
  261. dataQueue.push(encodeURIComponent(data));
  262. dispatchEvent(new WebSocketMessageEvent("message", data.length.toString()));
  263. removeBufferBefore(pos + 1);
  264. pos = -1;
  265. } else if (pos == 1 && buffer[0] == 0xff && buffer[1] == 0x00) { // closing
  266. main.log("received closing packet");
  267. removeBufferBefore(pos + 1);
  268. pos = -1;
  269. close();
  270. notifyStateChange();
  271. dispatchEvent(new Event("close"));
  272. }
  273. }
  274. }
  275. }
  276. public function readSocketData():Array {
  277. var q:Array = dataQueue;
  278. if (dataQueue.length > 0) {
  279. // Reset to empty
  280. dataQueue = [];
  281. }
  282. return q;
  283. }
  284. private function validateHeader(headerStr:String):Boolean {
  285. var lines:Array = headerStr.split(/\r\n/);
  286. if (!lines[0].match(/^HTTP\/1.1 101 /)) {
  287. onError("bad response: " + lines[0]);
  288. return false;
  289. }
  290. var header:Object = {};
  291. for (var i:int = 1; i < lines.length; ++i) {
  292. if (lines[i].length == 0) continue;
  293. var m:Array = lines[i].match(/^(\S+): (.*)$/);
  294. if (!m) {
  295. onError("failed to parse response header line: " + lines[i]);
  296. return false;
  297. }
  298. header[m[1]] = m[2];
  299. }
  300. if (header["Upgrade"] != "WebSocket") {
  301. onError("invalid Upgrade: " + header["Upgrade"]);
  302. return false;
  303. }
  304. if (header["Connection"] != "Upgrade") {
  305. onError("invalid Connection: " + header["Connection"]);
  306. return false;
  307. }
  308. if (!header["Sec-WebSocket-Origin"]) {
  309. if (header["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 = header["Sec-WebSocket-Origin"].toLowerCase();
  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. private function removeBufferBefore(pos:int):void {
  333. if (pos == 0) return;
  334. var nextBuffer:ByteArray = new ByteArray();
  335. buffer.position = pos;
  336. buffer.readBytes(nextBuffer);
  337. buffer = nextBuffer;
  338. }
  339. private function notifyStateChange():void {
  340. dispatchEvent(new WebSocketStateEvent("stateChange", readyState, bufferedAmount));
  341. }
  342. private function initNoiseChars():void {
  343. noiseChars = new Array();
  344. for (var i:int = 0x21; i <= 0x2f; ++i) {
  345. noiseChars.push(String.fromCharCode(i));
  346. }
  347. for (var j:int = 0x3a; j <= 0x7a; ++j) {
  348. noiseChars.push(String.fromCharCode(j));
  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. private function generateKey3():String {
  370. var key3:String = "";
  371. for (var i:int = 0; i < 8; ++i) {
  372. key3 += String.fromCharCode(randomInt(0, 255));
  373. }
  374. return key3;
  375. }
  376. private function getSecurityDigest(key1:String, key2:String, key3:String):String {
  377. var bytes1:String = keyToBytes(key1);
  378. var bytes2:String = keyToBytes(key2);
  379. return MD5.rstr_md5(bytes1 + bytes2 + key3);
  380. }
  381. private function keyToBytes(key:String):String {
  382. var keyNum:uint = parseInt(key.replace(/[^\d]/g, ""));
  383. var spaces:uint = 0;
  384. for (var i:int = 0; i < key.length; ++i) {
  385. if (key.charAt(i) == " ") ++spaces;
  386. }
  387. var resultNum:uint = keyNum / spaces;
  388. var bytes:String = "";
  389. for (var j:int = 3; j >= 0; --j) {
  390. bytes += String.fromCharCode((resultNum >> (j * 8)) & 0xff);
  391. }
  392. return bytes;
  393. }
  394. // Writes byte sequence to socket.
  395. // bytes is String in special format where bytes[i] is i-th byte, not i-th character.
  396. private function writeBytes(bytes:String):void {
  397. for (var i:int = 0; i < bytes.length; ++i) {
  398. socket.writeByte(bytes.charCodeAt(i));
  399. }
  400. }
  401. // Reads specified number of bytes from buffer, and returns it as special format String
  402. // where bytes[i] is i-th byte (not i-th character).
  403. private function readBytes(buffer:ByteArray, numBytes:int):String {
  404. var bytes:String = "";
  405. for (var i:int = 0; i < numBytes; ++i) {
  406. // & 0xff is to make \x80-\xff positive number.
  407. bytes += String.fromCharCode(buffer.readByte() & 0xff);
  408. }
  409. return bytes;
  410. }
  411. private function randomInt(min:uint, max:uint):uint {
  412. return min + Math.floor(Math.random() * (Number(max) - min + 1));
  413. }
  414. // for debug
  415. private function dumpBytes(bytes:String):void {
  416. var output:String = "";
  417. for (var i:int = 0; i < bytes.length; ++i) {
  418. output += bytes.charCodeAt(i).toString() + ", ";
  419. }
  420. main.log(output);
  421. }
  422. }
  423. }