WebSocket.as 8.1 KB

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