/flash-src/src/net/gimite/websocket/WebSocket.as

http://github.com/gimite/web-socket-js · ActionScript · 613 lines · 528 code · 61 blank · 24 comment · 149 complexity · 682118586ca5e88e6db9c59fa040f4e1 MD5 · raw file

  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/rfc6455
  5. package net.gimite.websocket {
  6. import com.adobe.net.proxies.RFC2817Socket;
  7. import com.gsolo.encryption.SHA1;
  8. import com.hurlant.crypto.tls.TLSConfig;
  9. import com.hurlant.crypto.tls.TLSEngine;
  10. import com.hurlant.crypto.tls.TLSSecurityParameters;
  11. import com.hurlant.crypto.tls.TLSSocket;
  12. import flash.display.*;
  13. import flash.errors.*;
  14. import flash.events.*;
  15. import flash.external.*;
  16. import flash.net.*;
  17. import flash.system.*;
  18. import flash.utils.*;
  19. import mx.controls.*;
  20. import mx.core.*;
  21. import mx.events.*;
  22. import mx.utils.*;
  23. public class WebSocket extends EventDispatcher {
  24. private static const WEB_SOCKET_GUID:String = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
  25. private static const CONNECTING:int = 0;
  26. private static const OPEN:int = 1;
  27. private static const CLOSING:int = 2;
  28. private static const CLOSED:int = 3;
  29. private static const OPCODE_CONTINUATION:int = 0x00;
  30. private static const OPCODE_TEXT:int = 0x01;
  31. private static const OPCODE_BINARY:int = 0x02;
  32. private static const OPCODE_CLOSE:int = 0x08;
  33. private static const OPCODE_PING:int = 0x09;
  34. private static const OPCODE_PONG:int = 0x0a;
  35. private static const STATUS_NORMAL_CLOSURE:int = 1000;
  36. private static const STATUS_NO_CODE:int = 1005;
  37. private static const STATUS_CLOSED_ABNORMALLY:int = 1006;
  38. private static const STATUS_CONNECTION_ERROR:int = 5000;
  39. private var id:int;
  40. private var url:String;
  41. private var scheme:String;
  42. private var host:String;
  43. private var port:uint;
  44. private var path:String;
  45. private var origin:String;
  46. private var requestedProtocols:Array;
  47. private var cookie:String;
  48. private var headers:String;
  49. private var rawSocket:Socket;
  50. private var tlsSocket:TLSSocket;
  51. private var tlsConfig:TLSConfig;
  52. private var socket:Socket;
  53. private var acceptedProtocol:String;
  54. private var expectedDigest:String;
  55. private var buffer:ByteArray = new ByteArray();
  56. private var fragmentsBuffer:ByteArray = null;
  57. private var headerState:int = 0;
  58. private var readyState:int = CONNECTING;
  59. private var logger:IWebSocketLogger;
  60. private var base64Encoder:Base64Encoder = new Base64Encoder();
  61. public function WebSocket(
  62. id:int, url:String, protocols:Array, origin:String,
  63. proxyHost:String, proxyPort:int,
  64. cookie:String, headers:String,
  65. logger:IWebSocketLogger) {
  66. this.logger = logger;
  67. this.id = id;
  68. this.url = url;
  69. var m:Array = url.match(/^(\w+):\/\/([^\/:]+)(:(\d+))?(\/.*)?(\?.*)?$/);
  70. if (!m) fatal("SYNTAX_ERR: invalid url: " + url);
  71. this.scheme = m[1];
  72. this.host = m[2];
  73. var defaultPort:int = scheme == "wss" ? 443 : 80;
  74. this.port = parseInt(m[4]) || defaultPort;
  75. this.path = (m[5] || "/") + (m[6] || "");
  76. this.origin = origin;
  77. this.requestedProtocols = protocols;
  78. this.cookie = cookie;
  79. // if present and not the empty string, headers MUST end with \r\n
  80. // headers should be zero or more complete lines, for example
  81. // "Header1: xxx\r\nHeader2: yyyy\r\n"
  82. this.headers = headers;
  83. if (proxyHost != null && proxyPort != 0){
  84. if (scheme == "wss") {
  85. fatal("wss with proxy is not supported");
  86. }
  87. var proxySocket:RFC2817Socket = new RFC2817Socket();
  88. proxySocket.setProxyInfo(proxyHost, proxyPort);
  89. proxySocket.addEventListener(ProgressEvent.SOCKET_DATA, onSocketData);
  90. rawSocket = socket = proxySocket;
  91. } else {
  92. rawSocket = new Socket();
  93. if (scheme == "wss") {
  94. tlsConfig= new TLSConfig(TLSEngine.CLIENT,
  95. null, null, null, null, null,
  96. TLSSecurityParameters.PROTOCOL_VERSION);
  97. tlsConfig.trustAllCertificates = true;
  98. tlsConfig.ignoreCommonNameMismatch = true;
  99. tlsSocket = new TLSSocket();
  100. tlsSocket.addEventListener(ProgressEvent.SOCKET_DATA, onSocketData);
  101. socket = tlsSocket;
  102. } else {
  103. rawSocket.addEventListener(ProgressEvent.SOCKET_DATA, onSocketData);
  104. socket = rawSocket;
  105. }
  106. }
  107. rawSocket.addEventListener(Event.CLOSE, onSocketClose);
  108. rawSocket.addEventListener(Event.CONNECT, onSocketConnect);
  109. rawSocket.addEventListener(IOErrorEvent.IO_ERROR, onSocketIoError);
  110. rawSocket.addEventListener(SecurityErrorEvent.SECURITY_ERROR, onSocketSecurityError);
  111. rawSocket.connect(host, port);
  112. }
  113. /**
  114. * @return This WebSocket's ID.
  115. */
  116. public function getId():int {
  117. return this.id;
  118. }
  119. /**
  120. * @return this WebSocket's readyState.
  121. */
  122. public function getReadyState():int {
  123. return this.readyState;
  124. }
  125. public function getAcceptedProtocol():String {
  126. return this.acceptedProtocol;
  127. }
  128. public function send(encData:String):int {
  129. var data:String;
  130. try {
  131. data = decodeURIComponent(encData);
  132. } catch (ex:URIError) {
  133. logger.error("SYNTAX_ERR: URIError in send()");
  134. return 0;
  135. }
  136. logger.log("send: " + data);
  137. var dataBytes:ByteArray = new ByteArray();
  138. dataBytes.writeUTFBytes(data);
  139. if (readyState == OPEN) {
  140. var frame:WebSocketFrame = new WebSocketFrame();
  141. frame.opcode = OPCODE_TEXT;
  142. frame.payload = dataBytes;
  143. if (sendFrame(frame)) {
  144. return -1;
  145. } else {
  146. return dataBytes.length;
  147. }
  148. } else if (readyState == CLOSING || readyState == CLOSED) {
  149. return dataBytes.length;
  150. } else {
  151. fatal("invalid state");
  152. return 0;
  153. }
  154. }
  155. public function close(
  156. code:int = STATUS_NO_CODE, reason:String = "", origin:String = "client"):void {
  157. if (code != STATUS_NORMAL_CLOSURE &&
  158. code != STATUS_NO_CODE &&
  159. code != STATUS_CONNECTION_ERROR) {
  160. logger.error(StringUtil.substitute(
  161. "Fail connection by {0}: code={1} reason={2}", origin, code, reason));
  162. }
  163. var closeConnection:Boolean =
  164. code == STATUS_CONNECTION_ERROR || origin == "server";
  165. try {
  166. if (readyState == OPEN && code != STATUS_CONNECTION_ERROR) {
  167. var frame:WebSocketFrame = new WebSocketFrame();
  168. frame.opcode = OPCODE_CLOSE;
  169. frame.payload = new ByteArray();
  170. if (origin == "client" && code != STATUS_NO_CODE) {
  171. frame.payload.writeShort(code);
  172. frame.payload.writeUTFBytes(reason);
  173. }
  174. sendFrame(frame);
  175. }
  176. if (closeConnection) {
  177. socket.close();
  178. }
  179. } catch (ex:Error) {
  180. logger.error("Error: " + ex.message);
  181. }
  182. if (closeConnection) {
  183. logger.log("closed");
  184. var fireErrorEvent:Boolean = readyState != CONNECTING && code == STATUS_CONNECTION_ERROR;
  185. readyState = CLOSED;
  186. if (fireErrorEvent) {
  187. dispatchEvent(new WebSocketEvent("error"));
  188. }
  189. var wasClean:Boolean = code != STATUS_CLOSED_ABNORMALLY && code != STATUS_CONNECTION_ERROR;
  190. var eventCode:int = code == STATUS_CONNECTION_ERROR ? STATUS_CLOSED_ABNORMALLY : code;
  191. dispatchCloseEvent(wasClean, eventCode, reason);
  192. } else {
  193. logger.log("closing");
  194. readyState = CLOSING;
  195. }
  196. }
  197. private function onSocketConnect(event:Event):void {
  198. logger.log("connected");
  199. if (scheme == "wss") {
  200. logger.log("starting SSL/TLS");
  201. tlsSocket.startTLS(rawSocket, host, tlsConfig);
  202. }
  203. var defaultPort:int = scheme == "wss" ? 443 : 80;
  204. var hostValue:String = host + (port == defaultPort ? "" : ":" + port);
  205. var key:String = generateKey();
  206. SHA1.b64pad = "=";
  207. expectedDigest = SHA1.b64_sha1(key + WEB_SOCKET_GUID);
  208. var opt:String = "";
  209. if (requestedProtocols.length > 0) {
  210. opt += "Sec-WebSocket-Protocol: " + requestedProtocols.join(",") + "\r\n";
  211. }
  212. // if caller passes additional headers they must end with "\r\n"
  213. if (headers) opt += headers;
  214. var req:String = StringUtil.substitute(
  215. "GET {0} HTTP/1.1\r\n" +
  216. "Host: {1}\r\n" +
  217. "Upgrade: websocket\r\n" +
  218. "Connection: Upgrade\r\n" +
  219. "Sec-WebSocket-Key: {2}\r\n" +
  220. "Origin: {3}\r\n" +
  221. "Sec-WebSocket-Version: 13\r\n" +
  222. (cookie == "" ? "" : "Cookie: {4}\r\n") +
  223. "{5}" +
  224. "\r\n",
  225. path, hostValue, key, origin, cookie, opt);
  226. logger.log("request header:\n" + req);
  227. socket.writeUTFBytes(req);
  228. socket.flush();
  229. }
  230. private function onSocketClose(event:Event):void {
  231. logger.log("closed");
  232. readyState = CLOSED;
  233. dispatchCloseEvent(false, STATUS_CLOSED_ABNORMALLY, "");
  234. }
  235. private function onSocketIoError(event:IOErrorEvent):void {
  236. var message:String;
  237. if (readyState == CONNECTING) {
  238. message = "cannot connect to Web Socket server at " + url + " (IoError: " + event.text + ")";
  239. } else {
  240. message =
  241. "error communicating with Web Socket server at " + url +
  242. " (IoError: " + event.text + ")";
  243. }
  244. onConnectionError(message);
  245. }
  246. private function onSocketSecurityError(event:SecurityErrorEvent):void {
  247. var message:String;
  248. if (readyState == CONNECTING) {
  249. message =
  250. "cannot connect to Web Socket server at " + url + " (SecurityError: " + event.text + ")\n" +
  251. "make sure the server is running and Flash socket policy file is correctly placed";
  252. } else {
  253. message =
  254. "error communicating with Web Socket server at " + url +
  255. " (SecurityError: " + event.text + ")";
  256. }
  257. onConnectionError(message);
  258. }
  259. private function onConnectionError(message:String):void {
  260. if (readyState == CLOSED) return;
  261. logger.error(message);
  262. close(STATUS_CONNECTION_ERROR);
  263. }
  264. private function onSocketData(event:ProgressEvent):void {
  265. var pos:int = buffer.length;
  266. socket.readBytes(buffer, pos);
  267. for (; pos < buffer.length; ++pos) {
  268. if (headerState < 4) {
  269. // try to find "\r\n\r\n"
  270. if ((headerState == 0 || headerState == 2) && buffer[pos] == 0x0d) {
  271. ++headerState;
  272. } else if ((headerState == 1 || headerState == 3) && buffer[pos] == 0x0a) {
  273. ++headerState;
  274. } else {
  275. headerState = 0;
  276. }
  277. if (headerState == 4) {
  278. var headerStr:String = readUTFBytes(buffer, 0, pos + 1);
  279. logger.log("response header:\n" + headerStr);
  280. if (!validateHandshake(headerStr)) return;
  281. removeBufferBefore(pos + 1);
  282. pos = -1;
  283. readyState = OPEN;
  284. this.dispatchEvent(new WebSocketEvent("open"));
  285. }
  286. } else {
  287. var frame:WebSocketFrame = parseFrame();
  288. if (frame) {
  289. removeBufferBefore(frame.length);
  290. pos = -1;
  291. if (frame.rsv != 0) {
  292. close(1002, "RSV must be 0.");
  293. } else if (frame.mask) {
  294. close(1002, "Frame from server must not be masked.");
  295. } else if (frame.opcode >= 0x08 && frame.opcode <= 0x0f && frame.payload.length >= 126) {
  296. close(1004, "Payload of control frame must be less than 126 bytes.");
  297. } else {
  298. switch (frame.opcode) {
  299. case OPCODE_CONTINUATION:
  300. if (fragmentsBuffer == null) {
  301. close(1002, "Unexpected continuation frame");
  302. } else {
  303. fragmentsBuffer.writeBytes(frame.payload);
  304. if (frame.fin) {
  305. data = readUTFBytes(fragmentsBuffer, 0, fragmentsBuffer.length);
  306. try {
  307. this.dispatchEvent(new WebSocketEvent("message", encodeURIComponent(data)));
  308. } catch (ex:URIError) {
  309. close(1007, "URIError while encoding the received data.");
  310. }
  311. fragmentsBuffer = null;
  312. }
  313. }
  314. break;
  315. case OPCODE_TEXT:
  316. if (frame.fin) {
  317. var data:String = readUTFBytes(frame.payload, 0, frame.payload.length);
  318. try {
  319. this.dispatchEvent(new WebSocketEvent("message", encodeURIComponent(data)));
  320. } catch (ex:URIError) {
  321. close(1007, "URIError while encoding the received data.");
  322. }
  323. } else {
  324. fragmentsBuffer = new ByteArray();
  325. fragmentsBuffer.writeBytes(frame.payload);
  326. }
  327. break;
  328. case OPCODE_BINARY:
  329. // See https://github.com/gimite/web-socket-js/pull/89
  330. // for discussion about supporting binary data.
  331. close(1003, "Received binary data, which is not supported.");
  332. break;
  333. case OPCODE_CLOSE:
  334. // Extracts code and reason string.
  335. var code:int = STATUS_NO_CODE;
  336. var reason:String = "";
  337. if (frame.payload.length >= 2) {
  338. frame.payload.endian = Endian.BIG_ENDIAN;
  339. frame.payload.position = 0;
  340. code = frame.payload.readUnsignedShort();
  341. reason = readUTFBytes(frame.payload, 2, frame.payload.length - 2);
  342. }
  343. logger.log("received closing frame");
  344. close(code, reason, "server");
  345. break;
  346. case OPCODE_PING:
  347. sendPong(frame.payload);
  348. break;
  349. case OPCODE_PONG:
  350. break;
  351. default:
  352. close(1002, "Received unknown opcode: " + frame.opcode);
  353. break;
  354. }
  355. }
  356. }
  357. }
  358. }
  359. }
  360. private function validateHandshake(headerStr:String):Boolean {
  361. var lines:Array = headerStr.split(/\r\n/);
  362. if (!lines[0].match(/^HTTP\/1.1 101 /)) {
  363. onConnectionError("bad response: " + lines[0]);
  364. return false;
  365. }
  366. var header:Object = {};
  367. var lowerHeader:Object = {};
  368. for (var i:int = 1; i < lines.length; ++i) {
  369. if (lines[i].length == 0) continue;
  370. var m:Array = lines[i].match(/^(\S+):(.*)$/);
  371. if (!m) {
  372. onConnectionError("failed to parse response header line: " + lines[i]);
  373. return false;
  374. }
  375. var key:String = m[1].toLowerCase();
  376. var value:String = StringUtil.trim(m[2]);
  377. header[key] = value;
  378. lowerHeader[key] = value.toLowerCase();
  379. }
  380. if (lowerHeader["upgrade"] != "websocket") {
  381. onConnectionError("invalid Upgrade: " + header["Upgrade"]);
  382. return false;
  383. }
  384. if (lowerHeader["connection"] != "upgrade") {
  385. onConnectionError("invalid Connection: " + header["Connection"]);
  386. return false;
  387. }
  388. if (!lowerHeader["sec-websocket-accept"]) {
  389. onConnectionError(
  390. "The WebSocket server speaks old WebSocket protocol, " +
  391. "which is not supported by web-socket-js. " +
  392. "It requires WebSocket protocol HyBi 10. " +
  393. "Try newer version of the server if available.");
  394. return false;
  395. }
  396. var replyDigest:String = header["sec-websocket-accept"]
  397. if (replyDigest != expectedDigest) {
  398. onConnectionError("digest doesn't match: " + replyDigest + " != " + expectedDigest);
  399. return false;
  400. }
  401. if (requestedProtocols.length > 0) {
  402. acceptedProtocol = header["sec-websocket-protocol"];
  403. if (requestedProtocols.indexOf(acceptedProtocol) < 0) {
  404. onConnectionError("protocol doesn't match: '" +
  405. acceptedProtocol + "' not in '" + requestedProtocols.join(",") + "'");
  406. return false;
  407. }
  408. }
  409. return true;
  410. }
  411. private function sendPong(payload:ByteArray):Boolean {
  412. var frame:WebSocketFrame = new WebSocketFrame();
  413. frame.opcode = OPCODE_PONG;
  414. frame.payload = payload;
  415. return sendFrame(frame);
  416. }
  417. private function sendFrame(frame:WebSocketFrame):Boolean {
  418. var plength:uint = frame.payload.length;
  419. // Generates a mask.
  420. var mask:ByteArray = new ByteArray();
  421. for (var i:int = 0; i < 4; i++) {
  422. mask.writeByte(randomInt(0, 255));
  423. }
  424. var header:ByteArray = new ByteArray();
  425. // FIN + RSV + opcode
  426. header.writeByte((frame.fin ? 0x80 : 0x00) | (frame.rsv << 4) | frame.opcode);
  427. if (plength <= 125) {
  428. header.writeByte(0x80 | plength); // Masked + length
  429. } else if (plength > 125 && plength < 65536) {
  430. header.writeByte(0x80 | 126); // Masked + 126
  431. header.writeShort(plength);
  432. } else if (plength >= 65536 && plength < 4294967296) {
  433. header.writeByte(0x80 | 127); // Masked + 127
  434. header.writeUnsignedInt(0); // zero high order bits
  435. header.writeUnsignedInt(plength);
  436. } else {
  437. fatal("Send frame size too large");
  438. }
  439. header.writeBytes(mask);
  440. var maskedPayload:ByteArray = new ByteArray();
  441. maskedPayload.length = frame.payload.length;
  442. for (i = 0; i < frame.payload.length; i++) {
  443. maskedPayload[i] = mask[i % 4] ^ frame.payload[i];
  444. }
  445. try {
  446. socket.writeBytes(header);
  447. socket.writeBytes(maskedPayload);
  448. socket.flush();
  449. } catch (ex:Error) {
  450. logger.error("Error while sending frame: " + ex.message);
  451. setTimeout(function():void {
  452. if (readyState != CLOSED) {
  453. close(STATUS_CONNECTION_ERROR);
  454. }
  455. }, 0);
  456. return false;
  457. }
  458. return true;
  459. }
  460. private function parseFrame():WebSocketFrame {
  461. var frame:WebSocketFrame = new WebSocketFrame();
  462. var hlength:uint = 0;
  463. var plength:uint = 0;
  464. hlength = 2;
  465. if (buffer.length < hlength) {
  466. return null;
  467. }
  468. frame.fin = (buffer[0] & 0x80) != 0;
  469. frame.rsv = (buffer[0] & 0x70) >> 4;
  470. frame.opcode = buffer[0] & 0x0f;
  471. // Payload unmasking is not implemented because masking frames from server
  472. // is not allowed. This field is used only for error checking.
  473. frame.mask = (buffer[1] & 0x80) != 0;
  474. plength = buffer[1] & 0x7f;
  475. if (plength == 126) {
  476. hlength = 4;
  477. if (buffer.length < hlength) {
  478. return null;
  479. }
  480. buffer.endian = Endian.BIG_ENDIAN;
  481. buffer.position = 2;
  482. plength = buffer.readUnsignedShort();
  483. } else if (plength == 127) {
  484. hlength = 10;
  485. if (buffer.length < hlength) {
  486. return null;
  487. }
  488. buffer.endian = Endian.BIG_ENDIAN;
  489. buffer.position = 2;
  490. // Protocol allows 64-bit length, but we only handle 32-bit
  491. var big:uint = buffer.readUnsignedInt(); // Skip high 32-bits
  492. plength = buffer.readUnsignedInt(); // Low 32-bits
  493. if (big != 0) {
  494. fatal("Frame length exceeds 4294967295. Bailing out!");
  495. return null;
  496. }
  497. }
  498. if (buffer.length < hlength + plength) {
  499. return null;
  500. }
  501. frame.length = hlength + plength;
  502. frame.payload = new ByteArray();
  503. buffer.position = hlength;
  504. buffer.readBytes(frame.payload, 0, plength);
  505. return frame;
  506. }
  507. private function dispatchCloseEvent(wasClean:Boolean, code:int, reason:String):void {
  508. var event:WebSocketEvent = new WebSocketEvent("close");
  509. event.wasClean = wasClean;
  510. event.code = code;
  511. event.reason = reason;
  512. dispatchEvent(event);
  513. }
  514. private function removeBufferBefore(pos:int):void {
  515. if (pos == 0) return;
  516. var nextBuffer:ByteArray = new ByteArray();
  517. buffer.position = pos;
  518. buffer.readBytes(nextBuffer);
  519. buffer = nextBuffer;
  520. }
  521. private function generateKey():String {
  522. var vals:ByteArray = new ByteArray();
  523. vals.length = 16;
  524. for (var i:int = 0; i < vals.length; ++i) {
  525. vals[i] = randomInt(0, 127);
  526. }
  527. base64Encoder.reset();
  528. base64Encoder.encodeBytes(vals);
  529. return base64Encoder.toString();
  530. }
  531. private function readUTFBytes(buffer:ByteArray, start:int, numBytes:int):String {
  532. buffer.position = start;
  533. var data:String = "";
  534. for(var i:int = start; i < start + numBytes; ++i) {
  535. // Workaround of a bug of ByteArray#readUTFBytes() that bytes after "\x00" is discarded.
  536. if (buffer[i] == 0x00) {
  537. data += buffer.readUTFBytes(i - buffer.position) + "\x00";
  538. buffer.position = i + 1;
  539. }
  540. }
  541. data += buffer.readUTFBytes(start + numBytes - buffer.position);
  542. return data;
  543. }
  544. private function randomInt(min:uint, max:uint):uint {
  545. return min + Math.floor(Math.random() * (Number(max) - min + 1));
  546. }
  547. private function fatal(message:String):void {
  548. logger.error(message);
  549. throw message;
  550. }
  551. }
  552. }