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-ietf-hybi-thewebsocketprotocol-10 |
|
5 |
|
6 (function() { |
|
7 |
|
8 if (window.WebSocket && !window.WEB_SOCKET_FORCE_FLASH) return; |
|
9 |
|
10 var logger; |
|
11 if (window.WEB_SOCKET_LOGGER) { |
|
12 logger = WEB_SOCKET_LOGGER; |
|
13 } else if (window.console && window.console.log && window.console.error) { |
|
14 // In some environment, console is defined but console.log or console.error is missing. |
|
15 logger = window.console; |
|
16 } else { |
|
17 logger = {log: function(){ }, error: function(){ }}; |
|
18 } |
|
19 |
|
20 // swfobject.hasFlashPlayerVersion("10.0.0") doesn't work with Gnash. |
|
21 if (swfobject.getFlashPlayerVersion().major < 10) { |
|
22 logger.error("Flash Player >= 10.0.0 is required."); |
|
23 return; |
|
24 } |
|
25 if (location.protocol == "file:") { |
|
26 logger.error( |
|
27 "WARNING: web-socket-js doesn't work in file:///... URL " + |
|
28 "unless you set Flash Security Settings properly. " + |
|
29 "Open the page via Web server i.e. http://..."); |
|
30 } |
|
31 |
|
32 /** |
|
33 * This class represents a faux web socket. |
|
34 * @param {string} url |
|
35 * @param {array or string} protocols |
|
36 * @param {string} proxyHost |
|
37 * @param {int} proxyPort |
|
38 * @param {string} headers |
|
39 */ |
|
40 WebSocket = function(url, protocols, proxyHost, proxyPort, headers) { |
|
41 var self = this; |
|
42 self.__id = WebSocket.__nextId++; |
|
43 WebSocket.__instances[self.__id] = self; |
|
44 self.readyState = WebSocket.CONNECTING; |
|
45 self.bufferedAmount = 0; |
|
46 self.__events = {}; |
|
47 if (!protocols) { |
|
48 protocols = []; |
|
49 } else if (typeof protocols == "string") { |
|
50 protocols = [protocols]; |
|
51 } |
|
52 // Uses setTimeout() to make sure __createFlash() runs after the caller sets ws.onopen etc. |
|
53 // Otherwise, when onopen fires immediately, onopen is called before it is set. |
|
54 self.__createTask = setTimeout(function() { |
|
55 WebSocket.__addTask(function() { |
|
56 self.__createTask = null; |
|
57 WebSocket.__flash.create( |
|
58 self.__id, url, protocols, proxyHost || null, proxyPort || 0, headers || null); |
|
59 }); |
|
60 }, 0); |
|
61 }; |
|
62 |
|
63 /** |
|
64 * Send data to the web socket. |
|
65 * @param {string} data The data to send to the socket. |
|
66 * @return {boolean} True for success, false for failure. |
|
67 */ |
|
68 WebSocket.prototype.send = function(data) { |
|
69 if (this.readyState == WebSocket.CONNECTING) { |
|
70 throw "INVALID_STATE_ERR: Web Socket connection has not been established"; |
|
71 } |
|
72 // We use encodeURIComponent() here, because FABridge doesn't work if |
|
73 // the argument includes some characters. We don't use escape() here |
|
74 // because of this: |
|
75 // https://developer.mozilla.org/en/Core_JavaScript_1.5_Guide/Functions#escape_and_unescape_Functions |
|
76 // But it looks decodeURIComponent(encodeURIComponent(s)) doesn't |
|
77 // preserve all Unicode characters either e.g. "\uffff" in Firefox. |
|
78 // Note by wtritch: Hopefully this will not be necessary using ExternalInterface. Will require |
|
79 // additional testing. |
|
80 var result = WebSocket.__flash.send(this.__id, encodeURIComponent(data)); |
|
81 if (result < 0) { // success |
|
82 return true; |
|
83 } else { |
|
84 this.bufferedAmount += result; |
|
85 return false; |
|
86 } |
|
87 }; |
|
88 |
|
89 /** |
|
90 * Close this web socket gracefully. |
|
91 */ |
|
92 WebSocket.prototype.close = function() { |
|
93 if (this.__createTask) { |
|
94 clearTimeout(this.__createTask); |
|
95 this.__createTask = null; |
|
96 this.readyState = WebSocket.CLOSED; |
|
97 return; |
|
98 } |
|
99 if (this.readyState == WebSocket.CLOSED || this.readyState == WebSocket.CLOSING) { |
|
100 return; |
|
101 } |
|
102 this.readyState = WebSocket.CLOSING; |
|
103 WebSocket.__flash.close(this.__id); |
|
104 }; |
|
105 |
|
106 /** |
|
107 * Implementation of {@link <a href="http://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-registration">DOM 2 EventTarget Interface</a>} |
|
108 * |
|
109 * @param {string} type |
|
110 * @param {function} listener |
|
111 * @param {boolean} useCapture |
|
112 * @return void |
|
113 */ |
|
114 WebSocket.prototype.addEventListener = function(type, listener, useCapture) { |
|
115 if (!(type in this.__events)) { |
|
116 this.__events[type] = []; |
|
117 } |
|
118 this.__events[type].push(listener); |
|
119 }; |
|
120 |
|
121 /** |
|
122 * Implementation of {@link <a href="http://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-registration">DOM 2 EventTarget Interface</a>} |
|
123 * |
|
124 * @param {string} type |
|
125 * @param {function} listener |
|
126 * @param {boolean} useCapture |
|
127 * @return void |
|
128 */ |
|
129 WebSocket.prototype.removeEventListener = function(type, listener, useCapture) { |
|
130 if (!(type in this.__events)) return; |
|
131 var events = this.__events[type]; |
|
132 for (var i = events.length - 1; i >= 0; --i) { |
|
133 if (events[i] === listener) { |
|
134 events.splice(i, 1); |
|
135 break; |
|
136 } |
|
137 } |
|
138 }; |
|
139 |
|
140 /** |
|
141 * Implementation of {@link <a href="http://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-registration">DOM 2 EventTarget Interface</a>} |
|
142 * |
|
143 * @param {Event} event |
|
144 * @return void |
|
145 */ |
|
146 WebSocket.prototype.dispatchEvent = function(event) { |
|
147 var events = this.__events[event.type] || []; |
|
148 for (var i = 0; i < events.length; ++i) { |
|
149 events[i](event); |
|
150 } |
|
151 var handler = this["on" + event.type]; |
|
152 if (handler) handler.apply(this, [event]); |
|
153 }; |
|
154 |
|
155 /** |
|
156 * Handles an event from Flash. |
|
157 * @param {Object} flashEvent |
|
158 */ |
|
159 WebSocket.prototype.__handleEvent = function(flashEvent) { |
|
160 |
|
161 if ("readyState" in flashEvent) { |
|
162 this.readyState = flashEvent.readyState; |
|
163 } |
|
164 if ("protocol" in flashEvent) { |
|
165 this.protocol = flashEvent.protocol; |
|
166 } |
|
167 |
|
168 var jsEvent; |
|
169 if (flashEvent.type == "open" || flashEvent.type == "error") { |
|
170 jsEvent = this.__createSimpleEvent(flashEvent.type); |
|
171 } else if (flashEvent.type == "close") { |
|
172 jsEvent = this.__createSimpleEvent("close"); |
|
173 jsEvent.wasClean = flashEvent.wasClean ? true : false; |
|
174 jsEvent.code = flashEvent.code; |
|
175 jsEvent.reason = flashEvent.reason; |
|
176 } else if (flashEvent.type == "message") { |
|
177 var data = decodeURIComponent(flashEvent.message); |
|
178 jsEvent = this.__createMessageEvent("message", data); |
|
179 } else { |
|
180 throw "unknown event type: " + flashEvent.type; |
|
181 } |
|
182 |
|
183 this.dispatchEvent(jsEvent); |
|
184 |
|
185 }; |
|
186 |
|
187 WebSocket.prototype.__createSimpleEvent = function(type) { |
|
188 if (document.createEvent && window.Event) { |
|
189 var event = document.createEvent("Event"); |
|
190 event.initEvent(type, false, false); |
|
191 return event; |
|
192 } else { |
|
193 return {type: type, bubbles: false, cancelable: false}; |
|
194 } |
|
195 }; |
|
196 |
|
197 WebSocket.prototype.__createMessageEvent = function(type, data) { |
|
198 if (document.createEvent && window.MessageEvent && !window.opera) { |
|
199 var event = document.createEvent("MessageEvent"); |
|
200 event.initMessageEvent("message", false, false, data, null, null, window, null); |
|
201 return event; |
|
202 } else { |
|
203 // IE and Opera, the latter one truncates the data parameter after any 0x00 bytes. |
|
204 return {type: type, data: data, bubbles: false, cancelable: false}; |
|
205 } |
|
206 }; |
|
207 |
|
208 /** |
|
209 * Define the WebSocket readyState enumeration. |
|
210 */ |
|
211 WebSocket.CONNECTING = 0; |
|
212 WebSocket.OPEN = 1; |
|
213 WebSocket.CLOSING = 2; |
|
214 WebSocket.CLOSED = 3; |
|
215 |
|
216 WebSocket.__flash = null; |
|
217 WebSocket.__instances = {}; |
|
218 WebSocket.__tasks = []; |
|
219 WebSocket.__nextId = 0; |
|
220 |
|
221 /** |
|
222 * Load a new flash security policy file. |
|
223 * @param {string} url |
|
224 */ |
|
225 WebSocket.loadFlashPolicyFile = function(url){ |
|
226 WebSocket.__addTask(function() { |
|
227 WebSocket.__flash.loadManualPolicyFile(url); |
|
228 }); |
|
229 }; |
|
230 |
|
231 /** |
|
232 * Loads WebSocketMain.swf and creates WebSocketMain object in Flash. |
|
233 */ |
|
234 WebSocket.__initialize = function() { |
|
235 if (WebSocket.__flash) return; |
|
236 |
|
237 if (WebSocket.__swfLocation) { |
|
238 // For backword compatibility. |
|
239 window.WEB_SOCKET_SWF_LOCATION = WebSocket.__swfLocation; |
|
240 } |
|
241 if (!window.WEB_SOCKET_SWF_LOCATION) { |
|
242 logger.error("[WebSocket] set WEB_SOCKET_SWF_LOCATION to location of WebSocketMain.swf"); |
|
243 return; |
|
244 } |
|
245 if (!window.WEB_SOCKET_SUPPRESS_CROSS_DOMAIN_SWF_ERROR && |
|
246 !WEB_SOCKET_SWF_LOCATION.match(/(^|\/)WebSocketMainInsecure\.swf(\?.*)?$/) && |
|
247 WEB_SOCKET_SWF_LOCATION.match(/^\w+:\/\/([^\/]+)/)) { |
|
248 var swfHost = RegExp.$1; |
|
249 if (location.host != swfHost) { |
|
250 logger.error( |
|
251 "[WebSocket] You must host HTML and WebSocketMain.swf in the same host " + |
|
252 "('" + location.host + "' != '" + swfHost + "'). " + |
|
253 "See also 'How to host HTML file and SWF file in different domains' section " + |
|
254 "in README.md. If you use WebSocketMainInsecure.swf, you can suppress this message " + |
|
255 "by WEB_SOCKET_SUPPRESS_CROSS_DOMAIN_SWF_ERROR = true;"); |
|
256 } |
|
257 } |
|
258 var container = document.createElement("div"); |
|
259 container.id = "webSocketContainer"; |
|
260 // Hides Flash box. We cannot use display: none or visibility: hidden because it prevents |
|
261 // Flash from loading at least in IE. So we move it out of the screen at (-100, -100). |
|
262 // But this even doesn't work with Flash Lite (e.g. in Droid Incredible). So with Flash |
|
263 // Lite, we put it at (0, 0). This shows 1x1 box visible at left-top corner but this is |
|
264 // the best we can do as far as we know now. |
|
265 container.style.position = "absolute"; |
|
266 if (WebSocket.__isFlashLite()) { |
|
267 container.style.left = "0px"; |
|
268 container.style.top = "0px"; |
|
269 } else { |
|
270 container.style.left = "-100px"; |
|
271 container.style.top = "-100px"; |
|
272 } |
|
273 var holder = document.createElement("div"); |
|
274 holder.id = "webSocketFlash"; |
|
275 container.appendChild(holder); |
|
276 document.body.appendChild(container); |
|
277 // See this article for hasPriority: |
|
278 // http://help.adobe.com/en_US/as3/mobile/WS4bebcd66a74275c36cfb8137124318eebc6-7ffd.html |
|
279 swfobject.embedSWF( |
|
280 WEB_SOCKET_SWF_LOCATION, |
|
281 "webSocketFlash", |
|
282 "1" /* width */, |
|
283 "1" /* height */, |
|
284 "10.0.0" /* SWF version */, |
|
285 null, |
|
286 null, |
|
287 {hasPriority: true, swliveconnect : true, allowScriptAccess: "always"}, |
|
288 null, |
|
289 function(e) { |
|
290 if (!e.success) { |
|
291 logger.error("[WebSocket] swfobject.embedSWF failed"); |
|
292 } |
|
293 }); |
|
294 }; |
|
295 |
|
296 /** |
|
297 * Called by Flash to notify JS that it's fully loaded and ready |
|
298 * for communication. |
|
299 */ |
|
300 WebSocket.__onFlashInitialized = function() { |
|
301 // We need to set a timeout here to avoid round-trip calls |
|
302 // to flash during the initialization process. |
|
303 setTimeout(function() { |
|
304 WebSocket.__flash = document.getElementById("webSocketFlash"); |
|
305 WebSocket.__flash.setCallerUrl(location.href); |
|
306 WebSocket.__flash.setDebug(!!window.WEB_SOCKET_DEBUG); |
|
307 for (var i = 0; i < WebSocket.__tasks.length; ++i) { |
|
308 WebSocket.__tasks[i](); |
|
309 } |
|
310 WebSocket.__tasks = []; |
|
311 }, 0); |
|
312 }; |
|
313 |
|
314 /** |
|
315 * Called by Flash to notify WebSockets events are fired. |
|
316 */ |
|
317 WebSocket.__onFlashEvent = function() { |
|
318 setTimeout(function() { |
|
319 try { |
|
320 // Gets events using receiveEvents() instead of getting it from event object |
|
321 // of Flash event. This is to make sure to keep message order. |
|
322 // It seems sometimes Flash events don't arrive in the same order as they are sent. |
|
323 var events = WebSocket.__flash.receiveEvents(); |
|
324 for (var i = 0; i < events.length; ++i) { |
|
325 WebSocket.__instances[events[i].webSocketId].__handleEvent(events[i]); |
|
326 } |
|
327 } catch (e) { |
|
328 logger.error(e); |
|
329 } |
|
330 }, 0); |
|
331 return true; |
|
332 }; |
|
333 |
|
334 // Called by Flash. |
|
335 WebSocket.__log = function(message) { |
|
336 logger.log(decodeURIComponent(message)); |
|
337 }; |
|
338 |
|
339 // Called by Flash. |
|
340 WebSocket.__error = function(message) { |
|
341 logger.error(decodeURIComponent(message)); |
|
342 }; |
|
343 |
|
344 WebSocket.__addTask = function(task) { |
|
345 if (WebSocket.__flash) { |
|
346 task(); |
|
347 } else { |
|
348 WebSocket.__tasks.push(task); |
|
349 } |
|
350 }; |
|
351 |
|
352 /** |
|
353 * Test if the browser is running flash lite. |
|
354 * @return {boolean} True if flash lite is running, false otherwise. |
|
355 */ |
|
356 WebSocket.__isFlashLite = function() { |
|
357 if (!window.navigator || !window.navigator.mimeTypes) { |
|
358 return false; |
|
359 } |
|
360 var mimeType = window.navigator.mimeTypes["application/x-shockwave-flash"]; |
|
361 if (!mimeType || !mimeType.enabledPlugin || !mimeType.enabledPlugin.filename) { |
|
362 return false; |
|
363 } |
|
364 return mimeType.enabledPlugin.filename.match(/flashlite/i) ? true : false; |
|
365 }; |
|
366 |
|
367 if (!window.WEB_SOCKET_DISABLE_AUTO_INITIALIZATION) { |
|
368 if (window.addEventListener) { |
|
369 window.addEventListener("load", function(){ |
|
370 WebSocket.__initialize(); |
|
371 }, false); |
|
372 } else { |
|
373 window.attachEvent("onload", function(){ |
|
374 WebSocket.__initialize(); |
|
375 }); |
|
376 } |
|
377 } |
|
378 |
|
379 })(); |
|