]> jfr.im git - irc/quakenet/qwebirc.git/commitdiff
Merge.
authorChris Porter <redacted>
Mon, 18 Aug 2014 18:46:10 +0000 (19:46 +0100)
committerChris Porter <redacted>
Mon, 18 Aug 2014 18:46:10 +0000 (19:46 +0100)
20 files changed:
LICENCE
bin/compile.py
bin/dependencies_b.py
bin/pagegen.py
config.py.example
js/debugdisabled.js [new file with mode: 0644]
js/irc/baseircclient.js
js/irc/ircconnection.js
js/jslib.js
js/sound.js
js/ui/baseui.js
qwebirc/engines/__init__.py
qwebirc/engines/ajaxengine.py
qwebirc/root.py
run.py
static/WebSocketMain.swf [new file with mode: 0644]
static/js/flash_web_socket-nc.js [new file with mode: 0644]
static/js/flash_web_socket.js [new file with mode: 0644]
static/panes/about.html
twisted/plugins/webirc.py

diff --git a/LICENCE b/LICENCE
index 00a5e9f0489572a7b6fcd9f30e62bcfa2c348653..62e7452f63aa63664cb91933ab55145388b1146b 100644 (file)
--- a/LICENCE
+++ b/LICENCE
@@ -118,3 +118,24 @@ Use an excerpt from the webpage if necessary (don't copy the full article and di
 You Can Not:
 You can NOT sell the resources directly for profit (eg. Selling the items on stock resource websites)
 You can NOT copy the full webpage and display it on your own website. (Use an excerpt by all means, but please link to the original resource download page - not the actual file.)
+
+web-socket-js
+-------------
+
+https://github.com/gimite/web-socket-js/
+
+Copyright (c) 2013, Hiroshi Ichikawa
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+
+Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+Neither the name of the Hiroshi Ichikawa nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+SWFObject
+---------
+
+SWFObject v2.2 <http://code.google.com/p/swfobject/> is released under the MIT License <http://www.opensource.org/licenses/mit-license.php>.
+
index f037e93532fc02d916455827adfac548c491d787..482255ddb6bebb5c425bb4796191d307bfb00ce1 100644 (file)
@@ -97,7 +97,7 @@ def main(outputdir=".", produce_debug=True):
     
     #jmerge_files(outputdir, "js", uiname, value["uifiles"], lambda x: os.path.join("js", "ui", "frontends", x + ".js"))
     
-    alljs = []
+    alljs = ["js/debugdisabled.js"]
     for y in pages.JS_BASE:
       alljs.append(os.path.join("static", "js", y + ".js"))
     for y in value.get("buildextra", []):
@@ -107,7 +107,7 @@ def main(outputdir=".", produce_debug=True):
     for y in value["uifiles"]:
       alljs.append(os.path.join("js", "ui", "frontends", y + ".js"))
     jmerge_files(outputdir, "js", uiname + "-" + ID, alljs, file_prefix="QWEBIRC_BUILD=\"" + ID + "\";\n")
-    
+
   os.rmdir(coutputdir)
   
   f = open(".compiled", "w")
@@ -139,4 +139,4 @@ def vcheck():
   
 if __name__ == "__main__":
   main()
-  
\ No newline at end of file
+  
index 66a26de7e827e5603f10435cbc02a0305e63d767..b3b82b0b11b8521f884b8497837089614f3ccaaa 100644 (file)
@@ -17,10 +17,11 @@ def check_dependencies():
   check_twisted()\r
   check_zope()\r
   check_win32()\r
+  i+=check_autobahn()\r
   i+=check_json()\r
   i+=check_java()\r
   i+=check_hg()\r
-  \r
+\r
   print "0 errors, %d warnings." % i\r
   \r
   if i == 0:\r
@@ -83,7 +84,7 @@ def check_zope():
       fail("qwebirc requires zope interface.",\r
            "this should normally come with twisted, but can be downloaded",\r
            "from pypi: http://pypi.python.org/pypi/zope.interface")\r
-           \r
+\r
 def check_twisted():\r
   try:\r
     import twisted\r
@@ -122,7 +123,22 @@ def check_json():
          "http://pypi.python.org/pypi/simplejson/")\r
     return 1\r
   return 0\r
-  \r
+\r
+def check_autobahn():\r
+  try:\r
+    import autobahn, autobahn.websocket\r
+    x = autobahn.version.split(".")\r
+    if len(x) != 3:\r
+      raise ImportError("Unknown version: %s", autobahn.vesrion)\r
+    if (int(x[1]) < 8) or (int(x[1]) == 8 and int(x[2]) < 14):\r
+      raise ImportError()\r
+    return 0\r
+  except ImportError:\r
+    warn("autobahn 0.8.14 (minimum) not installed; websocket support will be disabled.",\r
+         "consider installing autobahn from:",\r
+         "http://autobahn.ws/python/getstarted/")\r
+    return 1\r
+\r
 if __name__ == "__main__":\r
   import dependencies\r
   dependencies.check_dependencies()\r
index 0dd57c5bc5b3f89ff5113e907be67ec8acf0aaad..7da601ea0da26ce2cfb2ba68f128d0396015c5be 100755 (executable)
@@ -70,7 +70,7 @@ def producehtml(name, debug):
   <meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
   <meta name="viewport" content="width=device-width; initial-scale=1.0; maximum-scale=1.0; user-scalable=0;" />
   <link rel="shortcut icon" type="image/png" href="%simages/favicon.png"/>
-%s%s
+%s<script type="text/javascript">QWEBIRC_DEBUG=%s;</script>%s
 %s
   <script type="text/javascript">
     var ui = new qwebirc.ui.Interface("ircui", qwebirc.ui.%s, %s);
@@ -84,7 +84,7 @@ def producehtml(name, debug):
   </div>
 </body>
 </html>
-""" % (ui["doctype"], config.APP_TITLE, config.STATIC_BASE_URL, csshtml, customjs, jshtml, ui["class"], optionsgen.get_options(), div)
+""" % (ui["doctype"], config.APP_TITLE, config.STATIC_BASE_URL, csshtml, debug and "true" or "false", customjs, jshtml, ui["class"], optionsgen.get_options(), div)
 
 def main(outputdir=".", produce_debug=True):
   p = os.path.join(outputdir, "static")
index 779226e2035edf764df32d477c3b5ae032cec59d..3626a1104d93e1157ec7b121e8b10c2cfc6c47bb 100644 (file)
@@ -238,7 +238,7 @@ DNS_TIMEOUT = 5
 #         Note that this value is intimately linked with the client
 #         AJAX code at this time, changing it will result in bad
 #         things happening.
-HTTP_AJAX_REQUEST_TIMEOUT = 30
+HTTP_AJAX_REQUEST_TIMEOUT = 295
 
 # OPTION: HTTP_REQUEST_TIMEOUT
 #         Connections made to everything but the AJAX engine will
diff --git a/js/debugdisabled.js b/js/debugdisabled.js
new file mode 100644 (file)
index 0000000..2b13542
--- /dev/null
@@ -0,0 +1 @@
+QWEBIRC_DEBUG = false;\r
index d721ed2225b3932817d3bdd7b738c1462e91e442..90886f9913c93748624f969aaf11dcf94b979017 100644 (file)
@@ -5,7 +5,7 @@ qwebirc.irc.PMODE_REGULAR_MODE = 3;
 
 qwebirc.irc.RegisteredCTCPs = new QHash({
   "VERSION": function(x) {
-    return "qwebirc v" + qwebirc.VERSION + ", copyright (C) 2008-2014 Chris Porter and the qwebirc project -- " + qwebirc.util.browserVersion();
+    return "qwebirc v" + qwebirc.VERSION + ", copyright (C) 2008-2014 Chris Porter and the qwebirc project -- transport: " + this.connection.transportStatus + " -- " + qwebirc.util.browserVersion();
   },
   "USERINFO": function(x) { return "qwebirc"; },
   "TIME": function(x) { return qwebirc.irc.IRCDate(new Date()); },
@@ -240,9 +240,10 @@ qwebirc.irc.BaseIRCClient = new Class({
       var replyfn = qwebirc.irc.RegisteredCTCPs.get(type);
       if(replyfn) {
         var t = new Date().getTime() / 1000;
-        if(t > this.nextctcp)
-          this.send("NOTICE " + user.hostToNick() + " :\x01" + type + " " + replyfn(ctcp[1]) + "\x01");
-        this.nextctcp = t + 5;
+        if(t > this.nextctcp) {
+          this.send("NOTICE " + user.hostToNick() + " :\x01" + type + " " + replyfn.call(this, ctcp[1]) + "\x01");
+          this.nextctcp = t + 10;
+        }
       }
       
       if(target == this.nickname) {
index 8d2088385be80cd0264aeec0125fc6d015fc7ac8..e6cb69cd5f0a5fe6b1ab9fee89459ceb4f948486 100644 (file)
@@ -1,4 +1,8 @@
-/* This could do with a rewrite from scratch. */
+/* This could do with a rewrite from scratch... by splitting into about 10 classes... */
+
+//WEB_SOCKET_DEBUG = QWEBIRC_DEBUG;
+//WEB_SOCKET_FORCE_FLASH = true;
+//FORCE_LONGPOLL = true;
 
 qwebirc.irc.IRCConnection = new Class({
   Implements: [Events, Options],
@@ -7,7 +11,7 @@ qwebirc.irc.IRCConnection = new Class({
     minTimeout: 45000,
     maxTimeout: 5 * 60000,
     timeoutIncrement: 10000,
-    initialTimeout: 65000,
+    initialTimeout: 30000,
     floodInterval: 200,
     floodMax: 10,
     floodReset: 5000,
@@ -15,6 +19,9 @@ qwebirc.irc.IRCConnection = new Class({
     maxRetries: 5,
     serverPassword: null
   },
+  log: function(x) {
+    qwebirc.util.log("IRCConnection " + x);
+  },
   initialize: function(options) {
     this.setOptions(options);
     
@@ -31,14 +38,24 @@ qwebirc.irc.IRCConnection = new Class({
     
     this.__timeoutId = null;
     this.__timeout = this.options.initialTimeout;
-    this.__lastActiveRequest = null;
     this.__activeRequest = null;
-    
     this.__sendQueue = [];
     this.__sendQueueActive = false;
+    this.__wsAttempted = false;
+    this.__wsSupported = false;
+    this.__wsEverConnected = false;
+    this.__ws = null;
+    this.__wsAuthed = false;
+
+    this.__pubSeqNo = 0;
+    this.__subSeqNo = 0;
+    this.__sendRetries = 0;
+
+    this.transportStatus = "unknown";
   },
   __error: function(text) {
     this.fireEvent("error", text);
+    this.log("ERROR: " + text);
     if(this.options.errorAlert)
       alert(text);
   },
@@ -103,13 +120,17 @@ qwebirc.irc.IRCConnection = new Class({
     return false;
   },
   send: function(data, synchronous) {
+    this.__pubSeqNo++;
     if(this.disconnected)
       return false;
-    
+
     if(synchronous) {
-      this.__send(data, false);
+      this.__send([this.__pubSeqNo, data], false);
+    } else if(this.__ws && this.__wsAuthed) {
+      this.__ws.send("p" + this.__pubSeqNo + "," + data);
     } else {
-      this.__sendQueue.push(data);
+      /* seqno here is currently pointless but it's nice to enforce it in the protocol */
+      this.__sendQueue.push([this.__pubSeqNo, data]);
       this.__processSendQueue();
     }
     
@@ -119,38 +140,75 @@ qwebirc.irc.IRCConnection = new Class({
     if(this.__sendQueueActive || this.__sendQueue.length == 0)
       return;
 
-    this.sendQueueActive = true;      
-    this.__send(this.__sendQueue.shift(), true);
+    this.__sendQueueActive = true;
+    this.__send(this.__sendQueue[0], true);
   },
   __send: function(data, queued) {
+    this.log("called send(" + data[1] + ")");
     var r = this.newRequest("p", false, !queued); /* !queued == synchronous */
     if(r === null)
       return;
-      
+
+    var handled = false;
+    var retryEvent = null;
+    if(queued) {
+      var timeout = function() {
+        this.log("timeout for " + data[1] + " fired");
+        r.cancel();
+        retry();
+      }.delay(7500, this);
+    }
     r.addEvent("complete", function(o) {
-      if(queued)
+      this.log("complete for " + data[1] + " fired");
+      if(queued) {
+        $clear(timeout);
         this.__sendQueueActive = false;
+      }
+      if(retryEvent) {
+        this.log("cleared retry event");
+        $clear(retryEvent);
+      }
+      this.__sendRetries = 0;
+      this.__sendQueue.shift();
 
       if(!o || (o[0] == false)) {
         this.__sendQueue = [];
         
         if(!this.disconnected) {
-          this.disconnected = true;
-          this.__error("An error occured: " + o[1]);
+          this.disconnect();
+          this.__error("An error occurred: " + (o ? o[1] : "(unknown error)"));
         }
         return false;
       }
       
       this.__processSendQueue();
     }.bind(this));
-    
-    r.send("s=" + this.sessionid + "&c=" + encodeURIComponent(data));
+
+    if(queued) {
+      var retry = function() {
+        this.log("retry for " + data[1] + " fired... handled is " + handled);
+        if(handled)
+          return;
+        handled = true;
+        $clear(timeout);
+        this.log("Unable to send command " + data + "... retrying (attempt " + this.__sendRetries + ")");
+        if(this.__sendRetries++ < 3) {
+          retryEvent = this.__send.delay(1500 * this.__sendRetries + Math.random() * 1000, this, [data, queued]);
+        } else {
+          this.disconnect();
+          this.__error("Unable to send command after multiple retries.");
+        }
+      }.bind(this);
+      r.addEvent("error", retry);
+      r.addEvent("failure", retry);
+    }
+    r.send("s=" + this.sessionid + "&c=" + encodeURIComponent(data[1]) + "&n=" + data[0]);
   },
   __processData: function(o) {
-    if(o[0] == false) {
+    if(!o || o[0] == false) {
       if(!this.disconnected) {
-        this.disconnected = true;
-        this.__error("An error occured: " + o[1]);
+        this.disconnect();
+        this.__error("An error occurred: " + (o ? o[1] : "(unknown error)"));
       }
       return false;
     }
@@ -162,9 +220,6 @@ qwebirc.irc.IRCConnection = new Class({
     
     return true;
   },
-  __scheduleTimeout: function() {
-    this.__timeoutId = this.__timeoutEvent.delay(this.__timeout, this);
-  },
   __cancelTimeout: function() {
     if($defined(this.__timeoutId)) {
       $clear(this.__timeoutId);
@@ -173,20 +228,18 @@ qwebirc.irc.IRCConnection = new Class({
   },
   __timeoutEvent: function() {
     this.__timeoutId = null;
-    
+
     if(!$defined(this.__activeRequest))
       return;
       
-    if(this.__lastActiveRequest)
-      this.__lastActiveRequest.cancel();
-        
     this.__activeRequest.__replaced = true;
-    this.__lastActiveRequest = this.__activeRequest;
-      
+    this.__activeRequest.cancel();
+    this.__activeRequest = null;
+
     if(this.__timeout + this.options.timeoutIncrement <= this.options.maxTimeout)
       this.__timeout+=this.options.timeoutIncrement;
-        
-    this.recv();
+
+    this.__recvLongPoll();
   },
   __checkRetries: function() {
     /* hmm, something went wrong! */
@@ -203,6 +256,112 @@ qwebirc.irc.IRCConnection = new Class({
     return true;
   },
   recv: function() {
+    if(this.__wsSupported) {
+      this.__recvWebSocket();
+    } else {
+      this.__recvLongPoll();
+    }
+  },
+  __wsURL: function() {
+    var wsproto;
+    if (window.location.protocol === "https:") {
+      wsproto = "wss";
+    } else {
+      wsproto = "ws";
+    }
+    return wsproto + "://" + window.location.host + (qwebirc.global.dynamicBaseURL ? qwebirc.global.dynamicBaseURL : "/") + "w";
+  },
+  __recvWebSocket: function() {
+    if(this.disconnected)
+      return;
+
+    if(this.__wsAttempted) {
+      if(!this.__wsEverConnected) {
+        this.log("Failed first websocket connection... falling back to longpoll");
+        this.transportStatus += "(nowDisabled)";
+        /* give up and use long polling */
+        this.__recvLongPoll();
+        return;
+      }
+      this.log("Reconnecting to websocket...");
+    }
+
+    if(this.__isFlooding()) {
+      this.disconnect();
+      this.__error("BUG: uncontrolled flood detected -- disconnected.");
+    }
+
+    var ws = new WebSocket(this.__wsURL());
+    var retryExecuted = false;
+    var doRetry = function(e) {
+      if(retryExecuted)
+        return;
+      retryExecuted = true;
+
+      ws.onerror = ws.onclose = ws.onopen = null;
+      this.__ws = null;
+      if(this.disconnected)
+        return;
+
+      if(this.__checkRetries())
+        this.__recvWebSocket();
+    }.bind(this);
+
+    this.__wsAttempted = true;
+    this.__wsAuthed = false;
+    ws.onerror = function(e) {
+      this.log("websocket error");
+      doRetry(this, e);
+    }.bind(this);
+    ws.onclose = function(e) {
+      this.log("websocket closed");
+
+      if(e.wasClean && (e.code == 4999 || e.code == 4998)) {
+        if(e.reason) {
+          this.disconnect();
+          this.__error("An error occurred: " + (e.reason ? e.reason : "(no reason returned)"));
+          return;
+        }
+      }
+
+      doRetry(this, e);
+    }.bind(this);
+    ws.onmessage = function(m) {
+      var data = m.data;
+      if(!this.__wsAuthed) {
+        if(data == "sTrue") {
+          this.__wsAuthed = true;
+          this.__wsEverConnected = true;
+          return;
+        }
+      } else {
+        if(data.charAt(0) == "c") {
+          var message = data.substr(1);
+          var tokens = message.splitMax(",", 2);
+          this.__subSeqNo = Number(tokens[0]);
+          this.__processData(JSON.decode(tokens[1]));
+          return;
+        }
+      }
+
+      this.disconnect();
+      this.__error("An error occurred: bad message type");
+    }.bind(this);
+    var connectionTimeout = function() {
+      this.log("Websocket connection timeout...");
+      ws.close();
+      doRetry(this);
+    }.delay(5000, this);
+    ws.onopen = function() {
+      if(retryExecuted || this.disconnected || this.__ws == null)
+        return;
+      $clear(connectionTimeout);
+      this.log("websocket connected");
+      ws.send("s" + this.__subSeqNo + "," + this.sessionid);
+    }.bind(this);
+    this.__ws = ws;
+  },
+  __recvLongPoll: function() {
     var r = this.newRequest("s", true);
     if(!$defined(r))
       return;
@@ -211,16 +370,9 @@ qwebirc.irc.IRCConnection = new Class({
     r.__replaced = false;
     
     var onComplete = function(o) {
-      /* if we're a replaced requests... */
-      if(r.__replaced) {
-        this.__lastActiveRequest = null;
-        
-        if(o)          
-          this.__processData(o);
+      if(r.__replaced)
         return;
-      }
-    
-      /* ok, we're the main request */
+
       this.__activeRequest = null;
       this.__cancelTimeout();
       
@@ -229,18 +381,19 @@ qwebirc.irc.IRCConnection = new Class({
           return;
           
         if(this.__checkRetries())
-          this.recv();
+          this.__recvLongPoll();
         return;
       }
-      
+
+      this.__subSeqNo = Number(r.xhr.getResponseHeader("N"));
       if(this.__processData(o))
-        this.recv();
+        this.__recvLongPoll();
     };
 
     r.addEvent("complete", onComplete.bind(this));
 
-    this.__scheduleTimeout();
-    r.send("s=" + this.sessionid);
+    this.__timeoutId = this.__timeoutEvent.delay(this.__timeout, this);
+    r.send("s=" + this.sessionid + "&n=" + this.__subSeqNo);
   },
   connect: function() {
     this.cacheAvoidance = qwebirc.util.randHexString(16);
@@ -248,18 +401,20 @@ qwebirc.irc.IRCConnection = new Class({
     var r = this.newRequest("n");
     r.addEvent("complete", function(o) {
       if(!o) {
-        this.disconnected = true;
+        this.disconnect();
         this.__error("Couldn't connect to remote server.");
         return;
       }
       if(o[0] == false) {
         this.disconnect();
-        this.__error("An error occured: " + o[1]);
+        this.__error("An error occurred: " + o[1]);
         return;
       }
       this.sessionid = o[1];
-      
-      this.recv();    
+      var transports = o[2];
+
+      this.__wsSupported = false;
+      this.__decideTransport(transports);
     }.bind(this));
     
     var postdata = "nick=" + encodeURIComponent(this.initialNickname);
@@ -268,15 +423,35 @@ qwebirc.irc.IRCConnection = new Class({
       
     r.send(postdata);
   },
-  __cancelRequests: function() {
-    if($defined(this.__lastActiveRequest)) {
-      this.__lastActiveRequest.cancel();
-      this.__lastActiveRequest = null;
+  __decideTransport: function(transports) {
+    this.log("server supports " + transports);
+    if(transports.indexOf("websocket") == -1) {
+      this.log("no websocket on server: using longpoll");
+      this.transportStatus = "longpoll(serverNoWS)";
+      this.recv();
+      return;
     }
+    qwebirc.util.WebSocket(function(supported, transport) {
+      this.transportStatus = transport;
+      if(supported) {
+        this.log("websocket present on client and server: using websocket");
+        this.__wsSupported = true;
+      } else {
+        this.log("websocket present on server but not client: using longpoll");
+      }
+      this.recv();
+    }.bind(this));
+  },
+  __cancelRequests: function() {
     if($defined(this.__activeRequest)) {
+      this.__activeRequest.__replaced = true;
       this.__activeRequest.cancel();
       this.__activeRequest = null;
     }
+    if($defined(this.__ws)) {
+      this.__ws.close();
+      this.__ws = null;
+    }
   },
   disconnect: function() {
     this.disconnected = true;
@@ -284,3 +459,81 @@ qwebirc.irc.IRCConnection = new Class({
     this.__cancelRequests();
   }
 });
+
+qwebirc.util.__WebSocketState = { "loading": false, "result": undefined, "callbacks": [] };
+qwebirc.util.WebSocket = function(callback) {
+  var log = qwebirc.util.log;
+  var state = qwebirc.util.__WebSocketState;
+  var fire = latch = function(x, y) {
+    if($defined(x)) {
+      state.result = [x, y];
+    }
+    callback(state.result[0], state.result[1]);
+  };
+
+  if($defined(state.result))
+    return fire();
+
+  if(state.loading)
+    return;
+
+  if(window.FORCE_LONGPOLL) {
+    log("FORCE_LONGPOLL set");
+    return latch(false, "longPoll(forced)");
+  }
+
+  var modeSuffix = "";
+  if(!window.WEB_SOCKET_FORCE_FLASH) {
+    if(window.WebSocket) {
+      log("WebSocket detected");
+      return latch(true, "native");
+    } if(window.MozWebSocket) {
+      log("MozWebSocket detected");
+      window.WebSocket = MozWebSocket;
+      return latch(true, "nativeMoz");
+    }
+  } else {
+    log("FORCE_FLASH enabled");
+    modeSuffix = "(forced)";
+  }
+
+  if(!$defined(Browser.Plugins.Flash)) {
+    log("no WebSocket support in browser and no Flash");
+    return latch(false, "noFlash" + modeSuffix);
+  }
+
+  log("No WebSocket support present in client, but flash enabled... attempting to load FlashWebSocket...");
+  state.callbacks.push(callback);
+  state.loading = true;
+
+  var fireCallbacks = function() {
+    for(var i=0;i<state.callbacks.length;i++) {
+      state.callbacks[i](state.result[0], state.result[1]);
+    }
+    state.callbacks = [];
+  };
+  var timeout = function() {
+    log("timed out waiting for flash socket to load");
+    state.loading = false;
+    state.result = [false, "longPoll(flashTimeout)" + modeSuffix];
+    fireCallbacks();
+  }.delay(3000);
+
+  qwebirc.util.importJS(qwebirc.global.staticBaseURL + "js/flash_web_socket" + (QWEBIRC_DEBUG ? "-nc" : "") + ".js", "FLASH_WEBSOCKET_LOADED", function() {
+    $clear(timeout);
+    state.loading = false;
+    if(!window.WebSocket) {
+      state.result = [false, "longPoll(flashFailed)" + modeSuffix];
+      log("unable to install FlashWebSocket");
+    } else {
+      var ws = window.WebSocket;
+      if(window.location.scheme == "http") /* no point trying port sharing for https */
+        WebSocket.loadFlashPolicyFile("xmlsocket://" + window.location.host + "/");
+
+      log("FlashWebSocket loaded and installed");
+      state.result = [true, "flash" + modeSuffix];
+    }
+    fireCallbacks();
+  });
+};
+
index 9a7b26dd2ee65cacc22ab82491126f323bd47a40..719f3b2bfe1cedfecd6996ea2e2940ce088f6b66 100644 (file)
@@ -416,4 +416,23 @@ qwebirc.util.deviceHasKeyboard = function() {
 qwebirc.util.generateID_ID = 0;
 qwebirc.util.generateID = function() {
   return "qqa-" + qwebirc.util.generateID_ID++;
-}
\ No newline at end of file
+}
+
+qwebirc.util.__log = function(x) {
+  if(QWEBIRC_DEBUG) {
+    if(typeof console == "undefined") {
+      alert("log: " + x);
+    } else {
+      console.log(x);
+    }
+  }
+};
+
+qwebirc.util.logger = {
+  log: function(x) { qwebirc.util.__log("L " + x) },
+  info: function(x) { qwebirc.util.__log("I " + x) },
+  error: function(x) { qwebirc.util.__log("E " + x) },
+  warn: function(x) { qwebirc.util.__log("W " + x) }
+};
+
+qwebirc.util.log = qwebirc.util.logger.log;
index 08316ae731bdb6cd549724bd8602b7d6e4f8aa64..ba8c1a37b6cd4a68869594e41e857a52408d0ddb 100644 (file)
@@ -28,10 +28,10 @@ qwebirc.sound.SoundPlayer = new Class({
       return;
     }
     
-    var debugMode = false;
-    qwebirc.util.importJS(qwebirc.global.staticBaseURL + "js/" + (debugMode?"soundmanager2":"soundmanager2-nodebug-jsmin") + ".js", "soundManager", function() {
+    qwebirc.util.importJS(qwebirc.global.staticBaseURL + "js/" + (QWEBIRC_DEBUG?"soundmanager2":"soundmanager2-nodebug-jsmin") + ".js", "soundManager", function() {
       soundManager.url = qwebirc.global.staticBaseURL + "sound/";
-      
+
+      var debugMode = false;
       soundManager.debugMode = debugMode;
       soundManager.useConsole = debugMode;
       soundManager.onload = function() {
index 34623fbf786470545db29907b07f6fa7ce8c33f3..80ffdd485d755e6849284604a077bd31afa194ee 100644 (file)
@@ -47,6 +47,14 @@ qwebirc.ui.BaseUI = new Class({
       document.addEvent("focus", focus);
       window.addEvent("focus", focus);
     }
+
+    qwebirc.util.__log = function(x) {
+      if(QWEBIRC_DEBUG) {
+        if(typeof console != "undefined")
+          console.log(x);
+        this.getActiveWindow().addLine(null, x);
+      }
+    }.bind(this);
   },
   newClient: function(client) {
     client.id = String(this.clientId++);
@@ -367,7 +375,7 @@ qwebirc.ui.StandardUI = new Class({
     this.tabCompleter.reset();
   },
   setModifiableStylesheet: function(name) {
-    this.__styleSheet = new qwebirc.ui.style.ModifiableStylesheet(qwebirc.global.staticBaseURL + "css/" + name + qwebirc.FILE_SUFFIX + ".mcss");
+    this.__styleSheet = new qwebirc.ui.style.ModifiableStylesheet(qwebirc.global.staticBaseURL + "css/" + (QWEBIRC_DEBUG ? "debug/" : "") + name + qwebirc.FILE_SUFFIX + ".mcss");
     this.setModifiableStylesheetValues({});
   },
   setModifiableStylesheetValues: function(values) {
index 72594976f085a6f6f9a6659a2647562a50438216..9c3a089a42407b428a793d32f7d08733d61c1b9f 100644 (file)
@@ -3,3 +3,9 @@ from adminengine import AdminEngine
 from staticengine import StaticEngine
 from feedbackengine import FeedbackEngine
 from authgateengine import AuthgateEngine
+
+try:
+  from ajaxengine import WebSocketEngine
+except ImportError:
+  pass
+
index 324de11dd00c313bcb6b2bbcf9fb3db12356d6ea..2d3769e3dbdd6ccd80400084c72cdaccf17903f9 100644 (file)
@@ -8,6 +8,27 @@ from adminengine import AdminEngineAction
 from qwebirc.util import HitCounter
 import qwebirc.dns as qdns
 import qwebirc.util.qjson as json
+import urlparse
+
+TRANSPORTS = ["longpoll"]
+
+try:
+  import autobahn
+  x = autobahn.version.split(".")
+  if len(x) != 3:
+    raise ImportError("Unknown version: %s", autobahn.vesrion)
+  if (int(x[1]) < 8) or (int(x[1]) == 8 and int(x[2]) < 14):
+    raise ImportError()
+
+  import autobahn.twisted.websocket
+  import autobahn.twisted.resource
+  has_websocket = True
+  TRANSPORTS.append("websocket")
+except ImportError:
+  has_websocket = False
+
+BAD_SESSION_MESSAGE = "Invalid session, this most likely means the server has restarted; close this dialog and then try refreshing the page."
+MAX_SEQNO = 9223372036854775807  # 2**63 - 1... yeah it doesn't wrap
 Sessions = {}
 
 def get_session_id():
@@ -22,26 +43,10 @@ class AJAXException(Exception):
 class IDGenerationException(Exception):
   pass
 
-class PassthruException(Exception):
+class LineTooLongException(Exception):
   pass
-  
-NOT_DONE_YET = None
-EMPTY_JSON_LIST = json.dumps([])
 
-def jsondump(fn):
-  def decorator(*args, **kwargs):
-    try:
-      x = fn(*args, **kwargs)
-      if x is None:
-        return server.NOT_DONE_YET
-      x = (True, x)
-    except AJAXException, e:
-      x = (False, e[0])
-    except PassthruException, e:
-      return str(e)
-      
-    return json.dumps(x)
-  return decorator
+EMPTY_JSON_LIST = json.dumps([])
 
 def cleanupSession(id):
   try:
@@ -54,37 +59,43 @@ class IRCSession:
     self.id = id
     self.subscriptions = []
     self.buffer = []
+    self.old_buffer = None
     self.buflen = 0
     self.throttle = 0
     self.schedule = None
     self.closed = False
     self.cleanupschedule = None
+    self.pubSeqNo = -1
+    self.subSeqNo = 0
 
-  def subscribe(self, channel, notifier):
-    timeout_entry = reactor.callLater(config.HTTP_AJAX_REQUEST_TIMEOUT, self.timeout, channel)
-    def cancel_timeout(result):
-      if channel in self.subscriptions:
-        self.subscriptions.remove(channel)
-      try:
-        timeout_entry.cancel()
-      except error.AlreadyCalled:
-        pass
-    notifier.addCallbacks(cancel_timeout, cancel_timeout)
-    
+  def subscribe(self, channel, seqNo=None):
     if len(self.subscriptions) >= config.MAXSUBSCRIPTIONS:
       self.subscriptions.pop(0).close()
 
+    if seqNo is not None and seqNo < self.subSeqNo:
+      if self.old_buffer is None or seqNo != self.old_buffer[0]:
+        channel.write(json.dumps([False, "Unable to reconnect -- sequence number too old."]), seqNo + 1)
+        return
+
+      if not channel.write(self.old_buffer[1], self.old_buffer[0] + 1):
+        return
+
     self.subscriptions.append(channel)
-    self.flush()
-      
+    self.flush(seqNo)
+
+  def unsubscribe(self, channel):
+    try:
+      self.subscriptions.remove(channel)
+    except ValueError:
+      pass
+
   def timeout(self, channel):
     if self.schedule:
       return
-      
-    channel.write(EMPTY_JSON_LIST)
-    if channel in self.subscriptions:
-      self.subscriptions.remove(channel)
-      
+
+    self.unsubscribe(channel)
+    channel.write(EMPTY_JSON_LIST, self.subSeqNo)
+
   def flush(self, scheduled=False):
     if scheduled:
       self.schedule = None
@@ -104,20 +115,23 @@ class IRCSession:
         if not self.schedule:
           self.schedule = reactor.callLater(0, self.flush, True)
         return
-        
+
     self.throttle = t + config.UPDATE_FREQ
 
     encdata = json.dumps(self.buffer)
+    self.old_buffer = (self.subSeqNo, encdata)
+    self.subSeqNo+=1
     self.buffer = []
     self.buflen = 0
 
-    newsubs = []
-    for x in self.subscriptions:
-      if x.write(encdata):
+    subs = self.subscriptions
+    self.subscriptions = newsubs = []
+
+    for x in subs:
+      if x.write(encdata, self.subSeqNo):
         newsubs.append(x)
 
-    self.subscriptions = newsubs
-    if self.closed and not self.subscriptions:
+    if self.closed and not newsubs:
       cleanupSession(self.id)
 
   def event(self, data):
@@ -131,9 +145,18 @@ class IRCSession:
     self.buflen = newbuflen
     self.flush()
     
-  def push(self, data):
-    if not self.closed:
-      self.client.write(data)
+  def push(self, data, seq_no=None):
+    if self.closed:
+      return
+
+    if len(data) > config.MAXLINELEN:
+      raise LineTooLongException
+
+    if seq_no is not None:
+      if seq_no <= self.pubSeqNo:
+        return
+      self.pubSeqNo = seq_no
+    self.client.write(data)
 
   def disconnect(self):
     # keep the session hanging around for a few seconds so the
@@ -146,12 +169,12 @@ class IRCSession:
 def connect_notice(line):
   return "c", "NOTICE", "", ("AUTH", "*** (qwebirc) %s" % line)
 
-class Channel:
+class RequestChannel(object):
   def __init__(self, request):
     self.request = request
-  
-class SingleUseChannel(Channel):
-  def write(self, data):
+
+  def write(self, data, seqNo):
+    self.request.setHeader("n", str(seqNo))
     self.request.write(data)
     self.request.finish()
     return False
@@ -159,11 +182,6 @@ class SingleUseChannel(Channel):
   def close(self):
     self.request.finish()
     
-class MultipleUseChannel(Channel):
-  def write(self, data):
-    self.request.write(data)
-    return True
-
 class AJAXEngine(resource.Resource):
   isLeaf = True
   
@@ -172,15 +190,17 @@ class AJAXEngine(resource.Resource):
     self.__connect_hit = HitCounter()
     self.__total_hit = HitCounter()
     
-  @jsondump
   def render_POST(self, request):
     path = request.path[len(self.prefix):]
     if path[0] == "/":
       handler = self.COMMANDS.get(path[1:])
       if handler is not None:
-        return handler(self, request)
-        
-    raise PassthruException, http_error.NoResource().render(request)
+        try:
+          return handler(self, request)
+        except AJAXException, e:
+          return json.dumps((False, e[0]))
+
+    return "404" ## TODO: tidy up
 
   def newConnection(self, request):
     ticket = login_optional(request)
@@ -196,7 +216,7 @@ class AJAXEngine(resource.Resource):
     if password is not None:
       password = ircclient.irc_decode(password[0])
       
-    for i in xrange(10):
+    for i in range(10):
       id = get_session_id()
       if not Sessions.get(id):
         break
@@ -244,8 +264,8 @@ class AJAXEngine(resource.Resource):
 
     Sessions[id] = session
     
-    return id
-  
+    return json.dumps((True, id, TRANSPORTS))
+
   def getSession(self, request):
     bad_session_message = "Invalid session, this most likely means the server has restarted; close this dialog and then try refreshing the page."
     
@@ -259,26 +279,51 @@ class AJAXEngine(resource.Resource):
     return session
     
   def subscribe(self, request):
-    request.channel.cancelTimeout()
-    self.getSession(request).subscribe(SingleUseChannel(request), request.notifyFinish())
-    return NOT_DONE_YET
+    request.channel.setTimeout(None)
+
+    channel = RequestChannel(request)
+    session = self.getSession(request)
+    notifier = request.notifyFinish()
+
+    seq_no = request.args.get("n")
+    try:
+      if seq_no is not None:
+        seq_no = int(seq_no[0])
+        if seq_no < 0 or seq_no > MAX_SEQNO:
+          raise ValueError
+    except ValueError:
+      raise AJAXEngine, "Bad sequence number"
+
+    session.subscribe(channel, seq_no)
+
+    timeout_entry = reactor.callLater(config.HTTP_AJAX_REQUEST_TIMEOUT, session.timeout, channel)
+    def cancel_timeout(result):
+      try:
+        timeout_entry.cancel()
+      except error.AlreadyCalled:
+        pass
+      session.unsubscribe(channel)
+    notifier.addCallbacks(cancel_timeout, cancel_timeout)
+    return server.NOT_DONE_YET
 
   def push(self, request):
     command = request.args.get("c")
     if command is None:
       raise AJAXException, "No command specified."
     self.__total_hit()
-    
-    decoded = ircclient.irc_decode(command[0])
-    
-    session = self.getSession(request)
 
-    if len(decoded) > config.MAXLINELEN:
-      session.disconnect()
-      raise AJAXException, "Line too long."
+    seq_no = request.args.get("n")
+    try:
+      if seq_no is not None:
+        seq_no = int(seq_no[0])
+        if seq_no < 0 or seq_no > MAX_SEQNO:
+          raise ValueError
+    except ValueError:
+      raise AJAXEngine("Bad sequence number %r" % seq_no)
 
+    session = self.getSession(request)
     try:
-      session.push(decoded)
+      session.push(ircclient.irc_decode(command[0]), seq_no)
     except AttributeError: # occurs when we haven't noticed an error
       session.disconnect()
       raise AJAXException, "Connection closed by server; try reconnecting by reloading the page."
@@ -287,7 +332,7 @@ class AJAXEngine(resource.Resource):
       traceback.print_exc(file=sys.stderr)
       raise AJAXException, "Unknown error."
   
-    return True
+    return json.dumps((True, True))
   
   def closeById(self, k):
     s = Sessions.get(k)
@@ -305,3 +350,118 @@ class AJAXEngine(resource.Resource):
     
   COMMANDS = dict(p=push, n=newConnection, s=subscribe)
   
+if has_websocket:
+  class WebSocketChannel(object):
+    def __init__(self, channel):
+      self.channel = channel
+
+    def write(self, data, seqNo):
+      self.channel.send("c", "%d,%s" % (seqNo, data))
+      return True
+
+    def close(self):
+      self.channel.close()
+
+  class WebSocketEngineProtocol(autobahn.twisted.websocket.WebSocketServerProtocol):
+    AWAITING_AUTH, AUTHED = 0, 1
+
+    def __init__(self, *args, **kwargs):
+      self.__state = self.AWAITING_AUTH
+      self.__session = None
+      self.__channel = None
+      self.__timeout = None
+
+    def onOpen(self):
+      self.__timeout = reactor.callLater(5, self.close, "Authentication timeout")
+
+    def onClose(self, wasClean, code, reason):
+      self.__cancelTimeout()
+      if self.__session:
+        self.__session.unsubscribe(self.__channel)
+        self.__session = None
+
+    def onMessage(self, msg, binary):
+      # we don't bother checking the Origin header, as if you can auth then you've been able to pass the browser's
+      # normal origin handling (POSTed the new connection request and managed to get the session id)
+      state = self.__state
+      message_type, message = msg[:1], msg[1:]
+      if state == self.AWAITING_AUTH:
+        if message_type == "s":  # subscribe
+          tokens = message.split(",", 1)
+          if len(tokens) != 2:
+            self.close("Bad tokens")
+            return
+
+          seq_no, message = tokens[0], tokens[1]
+          try:
+            seq_no = int(seq_no)
+            if seq_no < 0 or seq_no > MAX_SEQNO:
+              raise ValueError
+          except ValueError:
+            self.close("Bad value")
+
+          session = Sessions.get(message)
+          if not session:
+            self.close(BAD_SESSION_MESSAGE)
+            return
+
+          self.__cancelTimeout()
+          self.__session = session
+          self.send("s", "True")
+          self.__state = self.AUTHED
+          self.__channel = WebSocketChannel(self)
+          session.subscribe(self.__channel, seq_no)
+          return
+      elif state == self.AUTHED:
+        if message_type == "p":  # push
+          tokens = message.split(",", 1)
+          if len(tokens) != 2:
+            self.close("Bad tokens")
+            return
+
+          seq_no, message = tokens[0], tokens[1]
+          try:
+            seq_no = int(seq_no)
+            if seq_no < 0 or seq_no > MAX_SEQNO:
+              raise ValueError
+          except ValueError:
+            self.close("Bad value")
+          self.__session.push(ircclient.irc_decode(message))
+          return
+
+      self.close("Bad message type")
+
+    def __cancelTimeout(self):
+      if self.__timeout is not None:
+        try:
+          self.__timeout.cancel()
+        except error.AlreadyCalled:
+          pass
+        self.__timeout = None
+
+    def close(self, reason=None):
+      self.__cancelTimeout()
+      if reason:
+        self.sendClose(4999, reason)
+      else:
+        self.sendClose(4998)
+
+      if self.__session:
+        self.__session.unsubscribe(self.__channel)
+        self.__session = None
+
+    def send(self, message_type, message):
+      self.sendMessage(message_type + message)
+
+  class WebSocketResource(autobahn.twisted.resource.WebSocketResource):
+    def render(self, request):
+      request.channel.setTimeout(None)
+      return autobahn.twisted.resource.WebSocketResource.render(self, request)
+
+  def WebSocketEngine(path=None):
+    factory = autobahn.twisted.websocket.WebSocketServerFactory("ws://localhost")
+    factory.externalPort = None
+    factory.protocol = WebSocketEngineProtocol
+    factory.setProtocolOptions(maxMessagePayloadSize=512, maxFramePayloadSize=512, tcpNoDelay=False)
+    resource = WebSocketResource(factory)
+    return resource
index fe6fc9513a9f956a9f7943363d3c678645d18a45..847648fb181c6f09b7e0e10d1584a886e6ba5eb3 100644 (file)
@@ -1,3 +1,4 @@
+from twisted.protocols.policies import TimeoutMixin
 from twisted.web import resource, server, static, http
 from twisted.internet import error, reactor
 import engines
@@ -12,30 +13,6 @@ class RootResource(resource.Resource):
       name = "qui.html"
     return self.primaryChild.getChild(name, request)
 
-# we do NOT use the built-in timeOut mixin as it's very very buggy!
-class TimeoutHTTPChannel(http.HTTPChannel):
-  timeout = config.HTTP_REQUEST_TIMEOUT
-
-  def connectionMade(self):
-    self.customTimeout = reactor.callLater(self.timeout, self.timeoutOccured)
-    http.HTTPChannel.connectionMade(self)
-    
-  def timeoutOccured(self):
-    self.customTimeout = None
-    self.transport.loseConnection()
-    
-  def cancelTimeout(self):
-    if self.customTimeout is not None:
-      try:
-        self.customTimeout.cancel()
-        self.customTimeout = None
-      except error.AlreadyCalled:
-        pass
-
-  def connectionLost(self, reason):
-    self.cancelTimeout()
-    http.HTTPChannel.connectionLost(self, reason)
-
 class ProxyRequest(server.Request):
   ip_re = re.compile(r"^((25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2})[.](25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2})[.](25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2})[.](25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2})|(::|(([a-fA-F0-9]{1,4}):){7}(([a-fA-F0-9]{1,4}))|(:(:([a-fA-F0-9]{1,4})){1,6})|((([a-fA-F0-9]{1,4}):){1,6}:)|((([a-fA-F0-9]{1,4}):)(:([a-fA-F0-9]{1,4})){1,6})|((([a-fA-F0-9]{1,4}):){2}(:([a-fA-F0-9]{1,4})){1,5})|((([a-fA-F0-9]{1,4}):){3}(:([a-fA-F0-9]{1,4})){1,4})|((([a-fA-F0-9]{1,4}):){4}(:([a-fA-F0-9]{1,4})){1,3})|((([a-fA-F0-9]{1,4}):){5}(:([a-fA-F0-9]{1,4})){1,2})))$", re.IGNORECASE)
   def validIP(self, ip):
@@ -58,18 +35,21 @@ class ProxyRequest(server.Request):
       return real_ip
       
     return fake_ip
-    
+
+class HTTPChannel(http.HTTPChannel):
+  def timeoutConnection(self):
+    self.transport.abortConnection()
+
 class RootSite(server.Site):
-  # we do this ourselves as the built in timeout stuff is really really buggy
-  protocol = TimeoutHTTPChannel
-  
+  protocol = HTTPChannel
+
   if hasattr(config, "FORWARDED_FOR_HEADER"):
     requestFactory = ProxyRequest
 
   def __init__(self, path, *args, **kwargs):
     root = RootResource()
+    kwargs["timeout"] = config.HTTP_REQUEST_TIMEOUT
     server.Site.__init__(self, root, *args, **kwargs)
-
     services = {}
     services["StaticEngine"] = root.primaryChild = engines.StaticEngine(path)
 
@@ -79,6 +59,10 @@ class RootSite(server.Site):
       root.putChild(path, sobj)
       
     register(engines.AJAXEngine, "e")
+    try:
+      register(engines.WebSocketEngine, "w")
+    except AttributeError:
+      pass
     register(engines.FeedbackEngine, "feedback")
     register(engines.AuthgateEngine, "auth")
     register(engines.AdminEngine, "adminengine", services)
diff --git a/run.py b/run.py
index 2d44a3aea6e42385d3c39d4d736290d31cdf7237..4a00ad051c446dcc93590eafccde1f7fa63b723d 100755 (executable)
--- a/run.py
+++ b/run.py
@@ -23,7 +23,19 @@ def help_reactors(*args):
   run_twistd(["--help-reactors"])
   sys.exit(1)
 
-DEFAULT_REACTOR = "select" if os.name == "nt" else "poll"  
+try:
+  from select import epoll
+  DEFAULT_REACTOR = "epoll"
+except ImportError:
+  try:
+    from select import kqueue
+    DEFAULT_REACTOR = "kqueue"
+  except ImportError:
+    try:
+      from select import poll
+      DEFAULT_REACTOR = "poll"
+    except ImportError:
+      DEFAULT_REACTOR = "select"
 
 parser = OptionParser()
 parser.add_option("-n", "--no-daemon", help="Don't run in the background.", action="store_false", dest="daemonise", default=True)
@@ -40,6 +52,7 @@ parser.add_option("-k", "--key", help="Path to SSL key.", dest="sslkey")
 parser.add_option("-H", "--certificate-chain", help="Path to SSL certificate chain file.", dest="sslchain")
 parser.add_option("-P", "--pidfile", help="Path to store PID file", dest="pidfile")
 parser.add_option("-s", "--syslog", help="Log to syslog", action="store_true", dest="syslog", default=False)
+parser.add_option("-f", "--flash-port", help="Port to listen for flash policy connections on.", type="int", dest="flashPort")
 parser.add_option("--profile", help="Run in profile mode, dumping results to this file", dest="profile")
 parser.add_option("--profiler", help="Name of profiler to use", dest="profiler")
 parser.add_option("--syslog-prefix", help="Syslog prefix", dest="syslog_prefix", default="qwebirc")
@@ -81,6 +94,9 @@ if not options.tracebacks:
 if options.clogfile:
   args2+=["--logfile", options.clogfile]
 
+if options.flashPort:
+  args2+=["--flashPort", options.flashPort]
+
 if options.sslcertificate and options.sslkey:
   args2+=["--certificate", options.sslcertificate, "--privkey", options.sslkey, "--https", options.port]
   if options.sslchain:
diff --git a/static/WebSocketMain.swf b/static/WebSocketMain.swf
new file mode 100644 (file)
index 0000000..05e751b
Binary files /dev/null and b/static/WebSocketMain.swf differ
diff --git a/static/js/flash_web_socket-nc.js b/static/js/flash_web_socket-nc.js
new file mode 100644 (file)
index 0000000..a9208c6
--- /dev/null
@@ -0,0 +1,420 @@
+window.WEB_SOCKET_SWF_LOCATION = qwebirc.global.staticBaseURL + "/WebSocketMain.swf";
+window.WEB_SOCKET_LOGGER = qwebirc.util.logger;
+/* note: FLASH_WEBSOCKET_LOADED = 1; at end */
+
+/* SWFObject v2.2 <http://code.google.com/p/swfobject/>
+ is released under the MIT License <http://www.opensource.org/licenses/mit-license.php>
+ */
+var swfobject=function(){var D="undefined",r="object",S="Shockwave Flash",W="ShockwaveFlash.ShockwaveFlash",q="application/x-shockwave-flash",R="SWFObjectExprInst",x="onreadystatechange",O=window,j=document,t=navigator,T=false,U=[h],o=[],N=[],I=[],l,Q,E,B,J=false,a=false,n,G,m=true,M=function(){var aa=typeof j.getElementById!=D&&typeof j.getElementsByTagName!=D&&typeof j.createElement!=D,ah=t.userAgent.toLowerCase(),Y=t.platform.toLowerCase(),ae=Y?/win/.test(Y):/win/.test(ah),ac=Y?/mac/.test(Y):/mac/.test(ah),af=/webkit/.test(ah)?parseFloat(ah.replace(/^.*webkit\/(\d+(\.\d+)?).*$/,"$1")):false,X=!+"\v1",ag=[0,0,0],ab=null;if(typeof t.plugins!=D&&typeof t.plugins[S]==r){ab=t.plugins[S].description;if(ab&&!(typeof t.mimeTypes!=D&&t.mimeTypes[q]&&!t.mimeTypes[q].enabledPlugin)){T=true;X=false;ab=ab.replace(/^.*\s+(\S+\s+\S+$)/,"$1");ag[0]=parseInt(ab.replace(/^(.*)\..*$/,"$1"),10);ag[1]=parseInt(ab.replace(/^.*\.(.*)\s.*$/,"$1"),10);ag[2]=/[a-zA-Z]/.test(ab)?parseInt(ab.replace(/^.*[a-zA-Z]+(.*)$/,"$1"),10):0}}else{if(typeof O.ActiveXObject!=D){try{var ad=new ActiveXObject(W);if(ad){ab=ad.GetVariable("$version");if(ab){X=true;ab=ab.split(" ")[1].split(",");ag=[parseInt(ab[0],10),parseInt(ab[1],10),parseInt(ab[2],10)]}}}catch(Z){}}}return{w3:aa,pv:ag,wk:af,ie:X,win:ae,mac:ac}}(),k=function(){if(!M.w3){return}if((typeof j.readyState!=D&&j.readyState=="complete")||(typeof j.readyState==D&&(j.getElementsByTagName("body")[0]||j.body))){f()}if(!J){if(typeof j.addEventListener!=D){j.addEventListener("DOMContentLoaded",f,false)}if(M.ie&&M.win){j.attachEvent(x,function(){if(j.readyState=="complete"){j.detachEvent(x,arguments.callee);f()}});if(O==top){(function(){if(J){return}try{j.documentElement.doScroll("left")}catch(X){setTimeout(arguments.callee,0);return}f()})()}}if(M.wk){(function(){if(J){return}if(!/loaded|complete/.test(j.readyState)){setTimeout(arguments.callee,0);return}f()})()}s(f)}}();function f(){if(J){return}try{var Z=j.getElementsByTagName("body")[0].appendChild(C("span"));Z.parentNode.removeChild(Z)}catch(aa){return}J=true;var X=U.length;for(var Y=0;Y<X;Y++){U[Y]()}}function K(X){if(J){X()}else{U[U.length]=X}}function s(Y){if(typeof O.addEventListener!=D){O.addEventListener("load",Y,false)}else{if(typeof j.addEventListener!=D){j.addEventListener("load",Y,false)}else{if(typeof O.attachEvent!=D){i(O,"onload",Y)}else{if(typeof O.onload=="function"){var X=O.onload;O.onload=function(){X();Y()}}else{O.onload=Y}}}}}function h(){if(T){V()}else{H()}}function V(){var X=j.getElementsByTagName("body")[0];var aa=C(r);aa.setAttribute("type",q);var Z=X.appendChild(aa);if(Z){var Y=0;(function(){if(typeof Z.GetVariable!=D){var ab=Z.GetVariable("$version");if(ab){ab=ab.split(" ")[1].split(",");M.pv=[parseInt(ab[0],10),parseInt(ab[1],10),parseInt(ab[2],10)]}}else{if(Y<10){Y++;setTimeout(arguments.callee,10);return}}X.removeChild(aa);Z=null;H()})()}else{H()}}function H(){var ag=o.length;if(ag>0){for(var af=0;af<ag;af++){var Y=o[af].id;var ab=o[af].callbackFn;var aa={success:false,id:Y};if(M.pv[0]>0){var ae=c(Y);if(ae){if(F(o[af].swfVersion)&&!(M.wk&&M.wk<312)){w(Y,true);if(ab){aa.success=true;aa.ref=z(Y);ab(aa)}}else{if(o[af].expressInstall&&A()){var ai={};ai.data=o[af].expressInstall;ai.width=ae.getAttribute("width")||"0";ai.height=ae.getAttribute("height")||"0";if(ae.getAttribute("class")){ai.styleclass=ae.getAttribute("class")}if(ae.getAttribute("align")){ai.align=ae.getAttribute("align")}var ah={};var X=ae.getElementsByTagName("param");var ac=X.length;for(var ad=0;ad<ac;ad++){if(X[ad].getAttribute("name").toLowerCase()!="movie"){ah[X[ad].getAttribute("name")]=X[ad].getAttribute("value")}}P(ai,ah,Y,ab)}else{p(ae);if(ab){ab(aa)}}}}}else{w(Y,true);if(ab){var Z=z(Y);if(Z&&typeof Z.SetVariable!=D){aa.success=true;aa.ref=Z}ab(aa)}}}}}function z(aa){var X=null;var Y=c(aa);if(Y&&Y.nodeName=="OBJECT"){if(typeof Y.SetVariable!=D){X=Y}else{var Z=Y.getElementsByTagName(r)[0];if(Z){X=Z}}}return X}function A(){return !a&&F("6.0.65")&&(M.win||M.mac)&&!(M.wk&&M.wk<312)}function P(aa,ab,X,Z){a=true;E=Z||null;B={success:false,id:X};var ae=c(X);if(ae){if(ae.nodeName=="OBJECT"){l=g(ae);Q=null}else{l=ae;Q=X}aa.id=R;if(typeof aa.width==D||(!/%$/.test(aa.width)&&parseInt(aa.width,10)<310)){aa.width="310"}if(typeof aa.height==D||(!/%$/.test(aa.height)&&parseInt(aa.height,10)<137)){aa.height="137"}j.title=j.title.slice(0,47)+" - Flash Player Installation";var ad=M.ie&&M.win?"ActiveX":"PlugIn",ac="MMredirectURL="+O.location.toString().replace(/&/g,"%26")+"&MMplayerType="+ad+"&MMdoctitle="+j.title;if(typeof ab.flashvars!=D){ab.flashvars+="&"+ac}else{ab.flashvars=ac}if(M.ie&&M.win&&ae.readyState!=4){var Y=C("div");X+="SWFObjectNew";Y.setAttribute("id",X);ae.parentNode.insertBefore(Y,ae);ae.style.display="none";(function(){if(ae.readyState==4){ae.parentNode.removeChild(ae)}else{setTimeout(arguments.callee,10)}})()}u(aa,ab,X)}}function p(Y){if(M.ie&&M.win&&Y.readyState!=4){var X=C("div");Y.parentNode.insertBefore(X,Y);X.parentNode.replaceChild(g(Y),X);Y.style.display="none";(function(){if(Y.readyState==4){Y.parentNode.removeChild(Y)}else{setTimeout(arguments.callee,10)}})()}else{Y.parentNode.replaceChild(g(Y),Y)}}function g(ab){var aa=C("div");if(M.win&&M.ie){aa.innerHTML=ab.innerHTML}else{var Y=ab.getElementsByTagName(r)[0];if(Y){var ad=Y.childNodes;if(ad){var X=ad.length;for(var Z=0;Z<X;Z++){if(!(ad[Z].nodeType==1&&ad[Z].nodeName=="PARAM")&&!(ad[Z].nodeType==8)){aa.appendChild(ad[Z].cloneNode(true))}}}}}return aa}function u(ai,ag,Y){var X,aa=c(Y);if(M.wk&&M.wk<312){return X}if(aa){if(typeof ai.id==D){ai.id=Y}if(M.ie&&M.win){var ah="";for(var ae in ai){if(ai[ae]!=Object.prototype[ae]){if(ae.toLowerCase()=="data"){ag.movie=ai[ae]}else{if(ae.toLowerCase()=="styleclass"){ah+=' class="'+ai[ae]+'"'}else{if(ae.toLowerCase()!="classid"){ah+=" "+ae+'="'+ai[ae]+'"'}}}}}var af="";for(var ad in ag){if(ag[ad]!=Object.prototype[ad]){af+='<param name="'+ad+'" value="'+ag[ad]+'" />'}}aa.outerHTML='<object classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"'+ah+">"+af+"</object>";N[N.length]=ai.id;X=c(ai.id)}else{var Z=C(r);Z.setAttribute("type",q);for(var ac in ai){if(ai[ac]!=Object.prototype[ac]){if(ac.toLowerCase()=="styleclass"){Z.setAttribute("class",ai[ac])}else{if(ac.toLowerCase()!="classid"){Z.setAttribute(ac,ai[ac])}}}}for(var ab in ag){if(ag[ab]!=Object.prototype[ab]&&ab.toLowerCase()!="movie"){e(Z,ab,ag[ab])}}aa.parentNode.replaceChild(Z,aa);X=Z}}return X}function e(Z,X,Y){var aa=C("param");aa.setAttribute("name",X);aa.setAttribute("value",Y);Z.appendChild(aa)}function y(Y){var X=c(Y);if(X&&X.nodeName=="OBJECT"){if(M.ie&&M.win){X.style.display="none";(function(){if(X.readyState==4){b(Y)}else{setTimeout(arguments.callee,10)}})()}else{X.parentNode.removeChild(X)}}}function b(Z){var Y=c(Z);if(Y){for(var X in Y){if(typeof Y[X]=="function"){Y[X]=null}}Y.parentNode.removeChild(Y)}}function c(Z){var X=null;try{X=j.getElementById(Z)}catch(Y){}return X}function C(X){return j.createElement(X)}function i(Z,X,Y){Z.attachEvent(X,Y);I[I.length]=[Z,X,Y]}function F(Z){var Y=M.pv,X=Z.split(".");X[0]=parseInt(X[0],10);X[1]=parseInt(X[1],10)||0;X[2]=parseInt(X[2],10)||0;return(Y[0]>X[0]||(Y[0]==X[0]&&Y[1]>X[1])||(Y[0]==X[0]&&Y[1]==X[1]&&Y[2]>=X[2]))?true:false}function v(ac,Y,ad,ab){if(M.ie&&M.mac){return}var aa=j.getElementsByTagName("head")[0];if(!aa){return}var X=(ad&&typeof ad=="string")?ad:"screen";if(ab){n=null;G=null}if(!n||G!=X){var Z=C("style");Z.setAttribute("type","text/css");Z.setAttribute("media",X);n=aa.appendChild(Z);if(M.ie&&M.win&&typeof j.styleSheets!=D&&j.styleSheets.length>0){n=j.styleSheets[j.styleSheets.length-1]}G=X}if(M.ie&&M.win){if(n&&typeof n.addRule==r){n.addRule(ac,Y)}}else{if(n&&typeof j.createTextNode!=D){n.appendChild(j.createTextNode(ac+" {"+Y+"}"))}}}function w(Z,X){if(!m){return}var Y=X?"visible":"hidden";if(J&&c(Z)){c(Z).style.visibility=Y}else{v("#"+Z,"visibility:"+Y)}}function L(Y){var Z=/[\\\"<>\.;]/;var X=Z.exec(Y)!=null;return X&&typeof encodeURIComponent!=D?encodeURIComponent(Y):Y}var d=function(){if(M.ie&&M.win){window.attachEvent("onunload",function(){var ac=I.length;for(var ab=0;ab<ac;ab++){I[ab][0].detachEvent(I[ab][1],I[ab][2])}var Z=N.length;for(var aa=0;aa<Z;aa++){y(N[aa])}for(var Y in M){M[Y]=null}M=null;for(var X in swfobject){swfobject[X]=null}swfobject=null})}}();return{registerObject:function(ab,X,aa,Z){if(M.w3&&ab&&X){var Y={};Y.id=ab;Y.swfVersion=X;Y.expressInstall=aa;Y.callbackFn=Z;o[o.length]=Y;w(ab,false)}else{if(Z){Z({success:false,id:ab})}}},getObjectById:function(X){if(M.w3){return z(X)}},embedSWF:function(ab,ah,ae,ag,Y,aa,Z,ad,af,ac){var X={success:false,id:ah};if(M.w3&&!(M.wk&&M.wk<312)&&ab&&ah&&ae&&ag&&Y){w(ah,false);K(function(){ae+="";ag+="";var aj={};if(af&&typeof af===r){for(var al in af){aj[al]=af[al]}}aj.data=ab;aj.width=ae;aj.height=ag;var am={};if(ad&&typeof ad===r){for(var ak in ad){am[ak]=ad[ak]}}if(Z&&typeof Z===r){for(var ai in Z){if(typeof am.flashvars!=D){am.flashvars+="&"+ai+"="+Z[ai]}else{am.flashvars=ai+"="+Z[ai]}}}if(F(Y)){var an=u(aj,am,ah);if(aj.id==ah){w(ah,true)}X.success=true;X.ref=an}else{if(aa&&A()){aj.data=aa;P(aj,am,ah,ac);return}else{w(ah,true)}}if(ac){ac(X)}})}else{if(ac){ac(X)}}},switchOffAutoHideShow:function(){m=false},ua:M,getFlashPlayerVersion:function(){return{major:M.pv[0],minor:M.pv[1],release:M.pv[2]}},hasFlashPlayerVersion:F,createSWF:function(Z,Y,X){if(M.w3){return u(Z,Y,X)}else{return undefined}},showExpressInstall:function(Z,aa,X,Y){if(M.w3&&A()){P(Z,aa,X,Y)}},removeSWF:function(X){if(M.w3){y(X)}},createCSS:function(aa,Z,Y,X){if(M.w3){v(aa,Z,Y,X)}},addDomLoadEvent:K,addLoadEvent:s,getQueryParamValue:function(aa){var Z=j.location.search||j.location.hash;if(Z){if(/\?/.test(Z)){Z=Z.split("?")[1]}if(aa==null){return L(Z)}var Y=Z.split("&");for(var X=0;X<Y.length;X++){if(Y[X].substring(0,Y[X].indexOf("="))==aa){return L(Y[X].substring((Y[X].indexOf("=")+1)))}}}return""},expressInstallCallback:function(){if(a){var X=c(R);if(X&&l){X.parentNode.replaceChild(l,X);if(Q){w(Q,true);if(M.ie&&M.win){l.style.display="block"}}if(E){E(B)}}a=false}}}}();
+
+//Copyright (c) 2013, Hiroshi Ichikawa
+//All rights reserved.
+//
+//Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+//
+//Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+//Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+//Neither the name of the Hiroshi Ichikawa nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
+//THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+/* PASTE START revision  f64a2b962be1b9c3d569fa375b47b0de0427adad  (including swf file) */
+// Copyright: Hiroshi Ichikawa <http://gimite.net/en/>
+// License: New BSD License
+// Reference: http://dev.w3.org/html5/websockets/
+// Reference: http://tools.ietf.org/html/rfc6455
+
+(function() {
+
+  if (window.WEB_SOCKET_FORCE_FLASH) {
+    // Keeps going.
+  } else if (window.WebSocket) {
+    return;
+  } else if (window.MozWebSocket) {
+    // Firefox.
+    window.WebSocket = MozWebSocket;
+    return;
+  }
+
+  var logger;
+  if (window.WEB_SOCKET_LOGGER) {
+    logger = WEB_SOCKET_LOGGER;
+  } else if (window.console && window.console.log && window.console.error) {
+    // In some environment, console is defined but console.log or console.error is missing.
+    logger = window.console;
+  } else {
+    logger = {log: function(){ }, error: function(){ }};
+  }
+
+  // swfobject.hasFlashPlayerVersion("10.0.0") doesn't work with Gnash.
+  if (swfobject.getFlashPlayerVersion().major < 10) {
+    logger.error("Flash Player >= 10.0.0 is required.");
+    return;
+  }
+  if (location.protocol == "file:") {
+    logger.error(
+        "WARNING: web-socket-js doesn't work in file:///... URL " +
+        "unless you set Flash Security Settings properly. " +
+        "Open the page via Web server i.e. http://...");
+  }
+
+  /**
+   * Our own implementation of WebSocket class using Flash.
+   * @param {string} url
+   * @param {array or string} protocols
+   * @param {string} proxyHost
+   * @param {int} proxyPort
+   * @param {string} headers
+   */
+  window.WebSocket = function(url, protocols, proxyHost, proxyPort, headers) {
+    var self = this;
+    self.__id = WebSocket.__nextId++;
+    WebSocket.__instances[self.__id] = self;
+    self.readyState = WebSocket.CONNECTING;
+    self.bufferedAmount = 0;
+    self.__events = {};
+    if (!protocols) {
+      protocols = [];
+    } else if (typeof protocols == "string") {
+      protocols = [protocols];
+    }
+    // Uses setTimeout() to make sure __createFlash() runs after the caller sets ws.onopen etc.
+    // Otherwise, when onopen fires immediately, onopen is called before it is set.
+    self.__createTask = setTimeout(function() {
+      WebSocket.__addTask(function() {
+        self.__createTask = null;
+        WebSocket.__flash.create(
+          self.__id, url, protocols, proxyHost || null, proxyPort || 0, headers || null);
+      });
+    }, 0);
+  };
+
+  /**
+   * Send data to the web socket.
+   * @param {string} data  The data to send to the socket.
+   * @return {boolean}  True for success, false for failure.
+   */
+  WebSocket.prototype.send = function(data) {
+    if (this.readyState == WebSocket.CONNECTING) {
+      throw "INVALID_STATE_ERR: Web Socket connection has not been established";
+    }
+    // We use encodeURIComponent() here, because FABridge doesn't work if
+    // the argument includes some characters. We don't use escape() here
+    // because of this:
+    // https://developer.mozilla.org/en/Core_JavaScript_1.5_Guide/Functions#escape_and_unescape_Functions
+    // But it looks decodeURIComponent(encodeURIComponent(s)) doesn't
+    // preserve all Unicode characters either e.g. "\uffff" in Firefox.
+    // Note by wtritch: Hopefully this will not be necessary using ExternalInterface.  Will require
+    // additional testing.
+    var result = WebSocket.__flash.send(this.__id, encodeURIComponent(data));
+    if (result < 0) { // success
+      return true;
+    } else {
+      this.bufferedAmount += result;
+      return false;
+    }
+  };
+
+  /**
+   * Close this web socket gracefully.
+   */
+  WebSocket.prototype.close = function() {
+    if (this.__createTask) {
+      clearTimeout(this.__createTask);
+      this.__createTask = null;
+      this.readyState = WebSocket.CLOSED;
+      return;
+    }
+    if (this.readyState == WebSocket.CLOSED || this.readyState == WebSocket.CLOSING) {
+      return;
+    }
+    this.readyState = WebSocket.CLOSING;
+    WebSocket.__flash.close(this.__id);
+  };
+
+  /**
+   * Implementation of {@link <a href="http://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-registration">DOM 2 EventTarget Interface</a>}
+   *
+   * @param {string} type
+   * @param {function} listener
+   * @param {boolean} useCapture
+   * @return void
+   */
+  WebSocket.prototype.addEventListener = function(type, listener, useCapture) {
+    if (!(type in this.__events)) {
+      this.__events[type] = [];
+    }
+    this.__events[type].push(listener);
+  };
+
+  /**
+   * Implementation of {@link <a href="http://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-registration">DOM 2 EventTarget Interface</a>}
+   *
+   * @param {string} type
+   * @param {function} listener
+   * @param {boolean} useCapture
+   * @return void
+   */
+  WebSocket.prototype.removeEventListener = function(type, listener, useCapture) {
+    if (!(type in this.__events)) return;
+    var events = this.__events[type];
+    for (var i = events.length - 1; i >= 0; --i) {
+      if (events[i] === listener) {
+        events.splice(i, 1);
+        break;
+      }
+    }
+  };
+
+  /**
+   * Implementation of {@link <a href="http://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-registration">DOM 2 EventTarget Interface</a>}
+   *
+   * @param {Event} event
+   * @return void
+   */
+  WebSocket.prototype.dispatchEvent = function(event) {
+    var events = this.__events[event.type] || [];
+    for (var i = 0; i < events.length; ++i) {
+      events[i](event);
+    }
+    var handler = this["on" + event.type];
+    if (handler) handler.apply(this, [event]);
+  };
+
+  /**
+   * Handles an event from Flash.
+   * @param {Object} flashEvent
+   */
+  WebSocket.prototype.__handleEvent = function(flashEvent) {
+
+    if ("readyState" in flashEvent) {
+      this.readyState = flashEvent.readyState;
+    }
+    if ("protocol" in flashEvent) {
+      this.protocol = flashEvent.protocol;
+    }
+
+    var jsEvent;
+    if (flashEvent.type == "open" || flashEvent.type == "error") {
+      jsEvent = this.__createSimpleEvent(flashEvent.type);
+    } else if (flashEvent.type == "close") {
+      jsEvent = this.__createSimpleEvent("close");
+      jsEvent.wasClean = flashEvent.wasClean ? true : false;
+      jsEvent.code = flashEvent.code;
+      jsEvent.reason = flashEvent.reason;
+    } else if (flashEvent.type == "message") {
+      var data = decodeURIComponent(flashEvent.message);
+      jsEvent = this.__createMessageEvent("message", data);
+    } else {
+      throw "unknown event type: " + flashEvent.type;
+    }
+
+    this.dispatchEvent(jsEvent);
+
+  };
+
+  WebSocket.prototype.__createSimpleEvent = function(type) {
+    if (document.createEvent && window.Event) {
+      var event = document.createEvent("Event");
+      event.initEvent(type, false, false);
+      return event;
+    } else {
+      return {type: type, bubbles: false, cancelable: false};
+    }
+  };
+
+  WebSocket.prototype.__createMessageEvent = function(type, data) {
+    if (window.MessageEvent && typeof(MessageEvent) == "function" && !window.opera) {
+      return new MessageEvent("message", {
+        "view": window,
+        "bubbles": false,
+        "cancelable": false,
+        "data": data
+      });
+    } else if (document.createEvent && window.MessageEvent && !window.opera) {
+      var event = document.createEvent("MessageEvent");
+      event.initMessageEvent("message", false, false, data, null, null, window, null);
+      return event;
+    } else {
+      // Old IE and Opera, the latter one truncates the data parameter after any 0x00 bytes.
+      return {type: type, data: data, bubbles: false, cancelable: false};
+    }
+  };
+
+  /**
+   * Define the WebSocket readyState enumeration.
+   */
+  WebSocket.CONNECTING = 0;
+  WebSocket.OPEN = 1;
+  WebSocket.CLOSING = 2;
+  WebSocket.CLOSED = 3;
+
+  // Field to check implementation of WebSocket.
+  WebSocket.__isFlashImplementation = true;
+  WebSocket.__initialized = false;
+  WebSocket.__flash = null;
+  WebSocket.__instances = {};
+  WebSocket.__tasks = [];
+  WebSocket.__nextId = 0;
+
+  /**
+   * Load a new flash security policy file.
+   * @param {string} url
+   */
+  WebSocket.loadFlashPolicyFile = function(url){
+    WebSocket.__addTask(function() {
+      WebSocket.__flash.loadManualPolicyFile(url);
+    });
+  };
+
+  /**
+   * Loads WebSocketMain.swf and creates WebSocketMain object in Flash.
+   */
+  WebSocket.__initialize = function() {
+
+    if (WebSocket.__initialized) return;
+    WebSocket.__initialized = true;
+
+    if (WebSocket.__swfLocation) {
+      // For backword compatibility.
+      window.WEB_SOCKET_SWF_LOCATION = WebSocket.__swfLocation;
+    }
+    if (!window.WEB_SOCKET_SWF_LOCATION) {
+      logger.error("[WebSocket] set WEB_SOCKET_SWF_LOCATION to location of WebSocketMain.swf");
+      return;
+    }
+    if (!window.WEB_SOCKET_SUPPRESS_CROSS_DOMAIN_SWF_ERROR &&
+      !WEB_SOCKET_SWF_LOCATION.match(/(^|\/)WebSocketMainInsecure\.swf(\?.*)?$/) &&
+      WEB_SOCKET_SWF_LOCATION.match(/^\w+:\/\/([^\/]+)/)) {
+      var swfHost = RegExp.$1;
+      if (location.host != swfHost) {
+        logger.error(
+            "[WebSocket] You must host HTML and WebSocketMain.swf in the same host " +
+            "('" + location.host + "' != '" + swfHost + "'). " +
+            "See also 'How to host HTML file and SWF file in different domains' section " +
+            "in README.md. If you use WebSocketMainInsecure.swf, you can suppress this message " +
+            "by WEB_SOCKET_SUPPRESS_CROSS_DOMAIN_SWF_ERROR = true;");
+      }
+    }
+    var container = document.createElement("div");
+    container.id = "webSocketContainer";
+    // Hides Flash box. We cannot use display: none or visibility: hidden because it prevents
+    // Flash from loading at least in IE. So we move it out of the screen at (-100, -100).
+    // But this even doesn't work with Flash Lite (e.g. in Droid Incredible). So with Flash
+    // Lite, we put it at (0, 0). This shows 1x1 box visible at left-top corner but this is
+    // the best we can do as far as we know now.
+    container.style.position = "absolute";
+    if (WebSocket.__isFlashLite()) {
+      container.style.left = "0px";
+      container.style.top = "0px";
+    } else {
+      container.style.left = "-100px";
+      container.style.top = "-100px";
+    }
+    var holder = document.createElement("div");
+    holder.id = "webSocketFlash";
+    container.appendChild(holder);
+    document.body.appendChild(container);
+    // See this article for hasPriority:
+    // http://help.adobe.com/en_US/as3/mobile/WS4bebcd66a74275c36cfb8137124318eebc6-7ffd.html
+    swfobject.embedSWF(
+      WEB_SOCKET_SWF_LOCATION,
+      "webSocketFlash",
+      "1" /* width */,
+      "1" /* height */,
+      "10.0.0" /* SWF version */,
+      null,
+      null,
+      {hasPriority: true, swliveconnect : true, allowScriptAccess: "always"},
+      null,
+      function(e) {
+        if (!e.success) {
+          logger.error("[WebSocket] swfobject.embedSWF failed");
+        }
+      }
+    );
+
+  };
+
+  /**
+   * Called by Flash to notify JS that it's fully loaded and ready
+   * for communication.
+   */
+  WebSocket.__onFlashInitialized = function() {
+    // We need to set a timeout here to avoid round-trip calls
+    // to flash during the initialization process.
+    setTimeout(function() {
+      WebSocket.__flash = document.getElementById("webSocketFlash");
+      WebSocket.__flash.setCallerUrl(location.href);
+      WebSocket.__flash.setDebug(!!window.WEB_SOCKET_DEBUG);
+      for (var i = 0; i < WebSocket.__tasks.length; ++i) {
+        WebSocket.__tasks[i]();
+      }
+      WebSocket.__tasks = [];
+    }, 0);
+  };
+
+  /**
+   * Called by Flash to notify WebSockets events are fired.
+   */
+  WebSocket.__onFlashEvent = function() {
+    setTimeout(function() {
+      try {
+        // Gets events using receiveEvents() instead of getting it from event object
+        // of Flash event. This is to make sure to keep message order.
+        // It seems sometimes Flash events don't arrive in the same order as they are sent.
+        var events = WebSocket.__flash.receiveEvents();
+        for (var i = 0; i < events.length; ++i) {
+          WebSocket.__instances[events[i].webSocketId].__handleEvent(events[i]);
+        }
+      } catch (e) {
+        logger.error(e);
+      }
+    }, 0);
+    return true;
+  };
+
+  // Called by Flash.
+  WebSocket.__log = function(message) {
+    logger.log(decodeURIComponent(message));
+  };
+
+  // Called by Flash.
+  WebSocket.__error = function(message) {
+    logger.error(decodeURIComponent(message));
+  };
+
+  WebSocket.__addTask = function(task) {
+    if (WebSocket.__flash) {
+      task();
+    } else {
+      WebSocket.__tasks.push(task);
+    }
+  };
+
+  /**
+   * Test if the browser is running flash lite.
+   * @return {boolean} True if flash lite is running, false otherwise.
+   */
+  WebSocket.__isFlashLite = function() {
+    if (!window.navigator || !window.navigator.mimeTypes) {
+      return false;
+    }
+    var mimeType = window.navigator.mimeTypes["application/x-shockwave-flash"];
+    if (!mimeType || !mimeType.enabledPlugin || !mimeType.enabledPlugin.filename) {
+      return false;
+    }
+    return mimeType.enabledPlugin.filename.match(/flashlite/i) ? true : false;
+  };
+
+  if (!window.WEB_SOCKET_DISABLE_AUTO_INITIALIZATION) {
+    // NOTE:
+    //   This fires immediately if web_socket.js is dynamically loaded after
+    //   the document is loaded.
+    swfobject.addDomLoadEvent(function() {
+      WebSocket.__initialize();
+    });
+  }
+
+})();
+/* PASTE END */
+FLASH_WEBSOCKET_LOADED = 1;
diff --git a/static/js/flash_web_socket.js b/static/js/flash_web_socket.js
new file mode 100644 (file)
index 0000000..e366754
--- /dev/null
@@ -0,0 +1 @@
+window.WEB_SOCKET_SWF_LOCATION=qwebirc.global.staticBaseURL+"/WebSocketMain.swf";window.WEB_SOCKET_LOGGER=qwebirc.util.logger;var swfobject=function(){var aq="undefined",aD="object",ab="Shockwave Flash",X="ShockwaveFlash.ShockwaveFlash",aE="application/x-shockwave-flash",ac="SWFObjectExprInst",ax="onreadystatechange",af=window,aL=document,aB=navigator,aa=false,Z=[aN],aG=[],ag=[],al=[],aJ,ad,ap,at,ak=false,aU=false,aH,an,aI=true,ah=function(){var a=typeof aL.getElementById!=aq&&typeof aL.getElementsByTagName!=aq&&typeof aL.createElement!=aq,e=aB.userAgent.toLowerCase(),c=aB.platform.toLowerCase(),h=c?/win/.test(c):/win/.test(e),j=c?/mac/.test(c):/mac/.test(e),g=/webkit/.test(e)?parseFloat(e.replace(/^.*webkit\/(\d+(\.\d+)?).*$/,"$1")):false,d=!+"\v1",f=[0,0,0],k=null;if(typeof aB.plugins!=aq&&typeof aB.plugins[ab]==aD){k=aB.plugins[ab].description;if(k&&!(typeof aB.mimeTypes!=aq&&aB.mimeTypes[aE]&&!aB.mimeTypes[aE].enabledPlugin)){aa=true;d=false;k=k.replace(/^.*\s+(\S+\s+\S+$)/,"$1");f[0]=parseInt(k.replace(/^(.*)\..*$/,"$1"),10);f[1]=parseInt(k.replace(/^.*\.(.*)\s.*$/,"$1"),10);f[2]=/[a-zA-Z]/.test(k)?parseInt(k.replace(/^.*[a-zA-Z]+(.*)$/,"$1"),10):0}}else{if(typeof af.ActiveXObject!=aq){try{var i=new ActiveXObject(X);if(i){k=i.GetVariable("$version");if(k){d=true;k=k.split(" ")[1].split(",");f=[parseInt(k[0],10),parseInt(k[1],10),parseInt(k[2],10)]}}}catch(b){}}}return{w3:a,pv:f,wk:g,ie:d,win:h,mac:j}}(),aK=function(){if(!ah.w3){return}if((typeof aL.readyState!=aq&&aL.readyState=="complete")||(typeof aL.readyState==aq&&(aL.getElementsByTagName("body")[0]||aL.body))){aP()}if(!ak){if(typeof aL.addEventListener!=aq){aL.addEventListener("DOMContentLoaded",aP,false)}if(ah.ie&&ah.win){aL.attachEvent(ax,function(){if(aL.readyState=="complete"){aL.detachEvent(ax,arguments.callee);aP()}});if(af==top){(function(){if(ak){return}try{aL.documentElement.doScroll("left")}catch(a){setTimeout(arguments.callee,0);return}aP()})()}}if(ah.wk){(function(){if(ak){return}if(!/loaded|complete/.test(aL.readyState)){setTimeout(arguments.callee,0);return}aP()})()}aC(aP)}}();function aP(){if(ak){return}try{var b=aL.getElementsByTagName("body")[0].appendChild(ar("span"));b.parentNode.removeChild(b)}catch(a){return}ak=true;var d=Z.length;for(var c=0;c<d;c++){Z[c]()}}function aj(a){if(ak){a()}else{Z[Z.length]=a}}function aC(a){if(typeof af.addEventListener!=aq){af.addEventListener("load",a,false)}else{if(typeof aL.addEventListener!=aq){aL.addEventListener("load",a,false)}else{if(typeof af.attachEvent!=aq){aM(af,"onload",a)}else{if(typeof af.onload=="function"){var b=af.onload;af.onload=function(){b();a()}}else{af.onload=a}}}}}function aN(){if(aa){Y()}else{am()}}function Y(){var d=aL.getElementsByTagName("body")[0];var b=ar(aD);b.setAttribute("type",aE);var a=d.appendChild(b);if(a){var c=0;(function(){if(typeof a.GetVariable!=aq){var e=a.GetVariable("$version");if(e){e=e.split(" ")[1].split(",");ah.pv=[parseInt(e[0],10),parseInt(e[1],10),parseInt(e[2],10)]}}else{if(c<10){c++;setTimeout(arguments.callee,10);return}}d.removeChild(b);a=null;am()})()}else{am()}}function am(){var g=aG.length;if(g>0){for(var h=0;h<g;h++){var c=aG[h].id;var l=aG[h].callbackFn;var a={success:false,id:c};if(ah.pv[0]>0){var i=aS(c);if(i){if(ao(aG[h].swfVersion)&&!(ah.wk&&ah.wk<312)){ay(c,true);if(l){a.success=true;a.ref=av(c);l(a)}}else{if(aG[h].expressInstall&&au()){var e={};e.data=aG[h].expressInstall;e.width=i.getAttribute("width")||"0";e.height=i.getAttribute("height")||"0";if(i.getAttribute("class")){e.styleclass=i.getAttribute("class")}if(i.getAttribute("align")){e.align=i.getAttribute("align")}var f={};var d=i.getElementsByTagName("param");var k=d.length;for(var j=0;j<k;j++){if(d[j].getAttribute("name").toLowerCase()!="movie"){f[d[j].getAttribute("name")]=d[j].getAttribute("value")}}ae(e,f,c,l)}else{aF(i);if(l){l(a)}}}}}else{ay(c,true);if(l){var b=av(c);if(b&&typeof b.SetVariable!=aq){a.success=true;a.ref=b}l(a)}}}}}function av(b){var d=null;var c=aS(b);if(c&&c.nodeName=="OBJECT"){if(typeof c.SetVariable!=aq){d=c}else{var a=c.getElementsByTagName(aD)[0];if(a){d=a}}}return d}function au(){return !aU&&ao("6.0.65")&&(ah.win||ah.mac)&&!(ah.wk&&ah.wk<312)}function ae(f,d,h,e){aU=true;ap=e||null;at={success:false,id:h};var a=aS(h);if(a){if(a.nodeName=="OBJECT"){aJ=aO(a);ad=null}else{aJ=a;ad=h}f.id=ac;if(typeof f.width==aq||(!/%$/.test(f.width)&&parseInt(f.width,10)<310)){f.width="310"}if(typeof f.height==aq||(!/%$/.test(f.height)&&parseInt(f.height,10)<137)){f.height="137"}aL.title=aL.title.slice(0,47)+" - Flash Player Installation";var b=ah.ie&&ah.win?"ActiveX":"PlugIn",c="MMredirectURL="+af.location.toString().replace(/&/g,"%26")+"&MMplayerType="+b+"&MMdoctitle="+aL.title;if(typeof d.flashvars!=aq){d.flashvars+="&"+c}else{d.flashvars=c}if(ah.ie&&ah.win&&a.readyState!=4){var g=ar("div");h+="SWFObjectNew";g.setAttribute("id",h);a.parentNode.insertBefore(g,a);a.style.display="none";(function(){if(a.readyState==4){a.parentNode.removeChild(a)}else{setTimeout(arguments.callee,10)}})()}aA(f,d,h)}}function aF(a){if(ah.ie&&ah.win&&a.readyState!=4){var b=ar("div");a.parentNode.insertBefore(b,a);b.parentNode.replaceChild(aO(a),b);a.style.display="none";(function(){if(a.readyState==4){a.parentNode.removeChild(a)}else{setTimeout(arguments.callee,10)}})()}else{a.parentNode.replaceChild(aO(a),a)}}function aO(b){var d=ar("div");if(ah.win&&ah.ie){d.innerHTML=b.innerHTML}else{var e=b.getElementsByTagName(aD)[0];if(e){var a=e.childNodes;if(a){var f=a.length;for(var c=0;c<f;c++){if(!(a[c].nodeType==1&&a[c].nodeName=="PARAM")&&!(a[c].nodeType==8)){d.appendChild(a[c].cloneNode(true))}}}}}return d}function aA(e,g,c){var d,a=aS(c);if(ah.wk&&ah.wk<312){return d}if(a){if(typeof e.id==aq){e.id=c}if(ah.ie&&ah.win){var f="";for(var i in e){if(e[i]!=Object.prototype[i]){if(i.toLowerCase()=="data"){g.movie=e[i]}else{if(i.toLowerCase()=="styleclass"){f+=' class="'+e[i]+'"'}else{if(i.toLowerCase()!="classid"){f+=" "+i+'="'+e[i]+'"'}}}}}var h="";for(var j in g){if(g[j]!=Object.prototype[j]){h+='<param name="'+j+'" value="'+g[j]+'" />'}}a.outerHTML='<object classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"'+f+">"+h+"</object>";ag[ag.length]=e.id;d=aS(e.id)}else{var b=ar(aD);b.setAttribute("type",aE);for(var k in e){if(e[k]!=Object.prototype[k]){if(k.toLowerCase()=="styleclass"){b.setAttribute("class",e[k])}else{if(k.toLowerCase()!="classid"){b.setAttribute(k,e[k])}}}}for(var l in g){if(g[l]!=Object.prototype[l]&&l.toLowerCase()!="movie"){aQ(b,l,g[l])}}a.parentNode.replaceChild(b,a);d=b}}return d}function aQ(b,d,c){var a=ar("param");a.setAttribute("name",d);a.setAttribute("value",c);b.appendChild(a)}function aw(a){var b=aS(a);if(b&&b.nodeName=="OBJECT"){if(ah.ie&&ah.win){b.style.display="none";(function(){if(b.readyState==4){aT(a)}else{setTimeout(arguments.callee,10)}})()}else{b.parentNode.removeChild(b)}}}function aT(a){var b=aS(a);if(b){for(var c in b){if(typeof b[c]=="function"){b[c]=null}}b.parentNode.removeChild(b)}}function aS(a){var c=null;try{c=aL.getElementById(a)}catch(b){}return c}function ar(a){return aL.createElement(a)}function aM(a,c,b){a.attachEvent(c,b);al[al.length]=[a,c,b]}function ao(a){var b=ah.pv,c=a.split(".");c[0]=parseInt(c[0],10);c[1]=parseInt(c[1],10)||0;c[2]=parseInt(c[2],10)||0;return(b[0]>c[0]||(b[0]==c[0]&&b[1]>c[1])||(b[0]==c[0]&&b[1]==c[1]&&b[2]>=c[2]))?true:false}function az(b,f,a,c){if(ah.ie&&ah.mac){return}var e=aL.getElementsByTagName("head")[0];if(!e){return}var g=(a&&typeof a=="string")?a:"screen";if(c){aH=null;an=null}if(!aH||an!=g){var d=ar("style");d.setAttribute("type","text/css");d.setAttribute("media",g);aH=e.appendChild(d);if(ah.ie&&ah.win&&typeof aL.styleSheets!=aq&&aL.styleSheets.length>0){aH=aL.styleSheets[aL.styleSheets.length-1]}an=g}if(ah.ie&&ah.win){if(aH&&typeof aH.addRule==aD){aH.addRule(b,f)}}else{if(aH&&typeof aL.createTextNode!=aq){aH.appendChild(aL.createTextNode(b+" {"+f+"}"))}}}function ay(a,c){if(!aI){return}var b=c?"visible":"hidden";if(ak&&aS(a)){aS(a).style.visibility=b}else{az("#"+a,"visibility:"+b)}}function ai(b){var a=/[\\\"<>\.;]/;var c=a.exec(b)!=null;return c&&typeof encodeURIComponent!=aq?encodeURIComponent(b):b}var aR=function(){if(ah.ie&&ah.win){window.attachEvent("onunload",function(){var a=al.length;for(var b=0;b<a;b++){al[b][0].detachEvent(al[b][1],al[b][2])}var d=ag.length;for(var c=0;c<d;c++){aw(ag[c])}for(var e in ah){ah[e]=null}ah=null;for(var f in swfobject){swfobject[f]=null}swfobject=null})}}();return{registerObject:function(a,e,c,b){if(ah.w3&&a&&e){var d={};d.id=a;d.swfVersion=e;d.expressInstall=c;d.callbackFn=b;aG[aG.length]=d;ay(a,false)}else{if(b){b({success:false,id:a})}}},getObjectById:function(a){if(ah.w3){return av(a)}},embedSWF:function(k,e,h,f,c,a,b,i,g,j){var d={success:false,id:e};if(ah.w3&&!(ah.wk&&ah.wk<312)&&k&&e&&h&&f&&c){ay(e,false);aj(function(){h+="";f+="";var q={};if(g&&typeof g===aD){for(var o in g){q[o]=g[o]}}q.data=k;q.width=h;q.height=f;var n={};if(i&&typeof i===aD){for(var p in i){n[p]=i[p]}}if(b&&typeof b===aD){for(var l in b){if(typeof n.flashvars!=aq){n.flashvars+="&"+l+"="+b[l]}else{n.flashvars=l+"="+b[l]}}}if(ao(c)){var m=aA(q,n,e);if(q.id==e){ay(e,true)}d.success=true;d.ref=m}else{if(a&&au()){q.data=a;ae(q,n,e,j);return}else{ay(e,true)}}if(j){j(d)}})}else{if(j){j(d)}}},switchOffAutoHideShow:function(){aI=false},ua:ah,getFlashPlayerVersion:function(){return{major:ah.pv[0],minor:ah.pv[1],release:ah.pv[2]}},hasFlashPlayerVersion:ao,createSWF:function(a,b,c){if(ah.w3){return aA(a,b,c)}else{return undefined}},showExpressInstall:function(b,a,d,c){if(ah.w3&&au()){ae(b,a,d,c)}},removeSWF:function(a){if(ah.w3){aw(a)}},createCSS:function(b,a,c,d){if(ah.w3){az(b,a,c,d)}},addDomLoadEvent:aj,addLoadEvent:aC,getQueryParamValue:function(b){var a=aL.location.search||aL.location.hash;if(a){if(/\?/.test(a)){a=a.split("?")[1]}if(b==null){return ai(a)}var c=a.split("&");for(var d=0;d<c.length;d++){if(c[d].substring(0,c[d].indexOf("="))==b){return ai(c[d].substring((c[d].indexOf("=")+1)))}}}return""},expressInstallCallback:function(){if(aU){var a=aS(ac);if(a&&aJ){a.parentNode.replaceChild(aJ,a);if(ad){ay(ad,true);if(ah.ie&&ah.win){aJ.style.display="block"}}if(ap){ap(at)}}aU=false}}}}();(function(){if(window.WEB_SOCKET_FORCE_FLASH){}else{if(window.WebSocket){return}else{if(window.MozWebSocket){window.WebSocket=MozWebSocket;return}}}var a;if(window.WEB_SOCKET_LOGGER){a=WEB_SOCKET_LOGGER}else{if(window.console&&window.console.log&&window.console.error){a=window.console}else{a={log:function(){},error:function(){}}}}if(swfobject.getFlashPlayerVersion().major<10){a.error("Flash Player >= 10.0.0 is required.");return}if(location.protocol=="file:"){a.error("WARNING: web-socket-js doesn't work in file:///... URL unless you set Flash Security Settings properly. Open the page via Web server i.e. http://...")}window.WebSocket=function(d,e,c,g,f){var b=this;b.__id=WebSocket.__nextId++;WebSocket.__instances[b.__id]=b;b.readyState=WebSocket.CONNECTING;b.bufferedAmount=0;b.__events={};if(!e){e=[]}else{if(typeof e=="string"){e=[e]}}b.__createTask=setTimeout(function(){WebSocket.__addTask(function(){b.__createTask=null;WebSocket.__flash.create(b.__id,d,e,c||null,g||0,f||null)})},0)};WebSocket.prototype.send=function(c){if(this.readyState==WebSocket.CONNECTING){throw"INVALID_STATE_ERR: Web Socket connection has not been established"}var b=WebSocket.__flash.send(this.__id,encodeURIComponent(c));if(b<0){return true}else{this.bufferedAmount+=b;return false}};WebSocket.prototype.close=function(){if(this.__createTask){clearTimeout(this.__createTask);this.__createTask=null;this.readyState=WebSocket.CLOSED;return}if(this.readyState==WebSocket.CLOSED||this.readyState==WebSocket.CLOSING){return}this.readyState=WebSocket.CLOSING;WebSocket.__flash.close(this.__id)};WebSocket.prototype.addEventListener=function(c,d,b){if(!(c in this.__events)){this.__events[c]=[]}this.__events[c].push(d)};WebSocket.prototype.removeEventListener=function(e,f,b){if(!(e in this.__events)){return}var d=this.__events[e];for(var c=d.length-1;c>=0;--c){if(d[c]===f){d.splice(c,1);break}}};WebSocket.prototype.dispatchEvent=function(e){var c=this.__events[e.type]||[];for(var b=0;b<c.length;++b){c[b](e)}var d=this["on"+e.type];if(d){d.apply(this,[e])}};WebSocket.prototype.__handleEvent=function(d){if("readyState" in d){this.readyState=d.readyState}if("protocol" in d){this.protocol=d.protocol}var b;if(d.type=="open"||d.type=="error"){b=this.__createSimpleEvent(d.type)}else{if(d.type=="close"){b=this.__createSimpleEvent("close");b.wasClean=d.wasClean?true:false;b.code=d.code;b.reason=d.reason}else{if(d.type=="message"){var c=decodeURIComponent(d.message);b=this.__createMessageEvent("message",c)}else{throw"unknown event type: "+d.type}}}this.dispatchEvent(b)};WebSocket.prototype.__createSimpleEvent=function(b){if(document.createEvent&&window.Event){var c=document.createEvent("Event");c.initEvent(b,false,false);return c}else{return{type:b,bubbles:false,cancelable:false}}};WebSocket.prototype.__createMessageEvent=function(b,d){if(window.MessageEvent&&typeof(MessageEvent)=="function"&&!window.opera){return new MessageEvent("message",{view:window,bubbles:false,cancelable:false,data:d})}else{if(document.createEvent&&window.MessageEvent&&!window.opera){var c=document.createEvent("MessageEvent");c.initMessageEvent("message",false,false,d,null,null,window,null);return c}else{return{type:b,data:d,bubbles:false,cancelable:false}}}};WebSocket.CONNECTING=0;WebSocket.OPEN=1;WebSocket.CLOSING=2;WebSocket.CLOSED=3;WebSocket.__isFlashImplementation=true;WebSocket.__initialized=false;WebSocket.__flash=null;WebSocket.__instances={};WebSocket.__tasks=[];WebSocket.__nextId=0;WebSocket.loadFlashPolicyFile=function(b){WebSocket.__addTask(function(){WebSocket.__flash.loadManualPolicyFile(b)})};WebSocket.__initialize=function(){if(WebSocket.__initialized){return}WebSocket.__initialized=true;if(WebSocket.__swfLocation){window.WEB_SOCKET_SWF_LOCATION=WebSocket.__swfLocation}if(!window.WEB_SOCKET_SWF_LOCATION){a.error("[WebSocket] set WEB_SOCKET_SWF_LOCATION to location of WebSocketMain.swf");return}if(!window.WEB_SOCKET_SUPPRESS_CROSS_DOMAIN_SWF_ERROR&&!WEB_SOCKET_SWF_LOCATION.match(/(^|\/)WebSocketMainInsecure\.swf(\?.*)?$/)&&WEB_SOCKET_SWF_LOCATION.match(/^\w+:\/\/([^\/]+)/)){var d=RegExp.$1;if(location.host!=d){a.error("[WebSocket] You must host HTML and WebSocketMain.swf in the same host ('"+location.host+"' != '"+d+"'). See also 'How to host HTML file and SWF file in different domains' section in README.md. If you use WebSocketMainInsecure.swf, you can suppress this message by WEB_SOCKET_SUPPRESS_CROSS_DOMAIN_SWF_ERROR = true;")}}var b=document.createElement("div");b.id="webSocketContainer";b.style.position="absolute";if(WebSocket.__isFlashLite()){b.style.left="0px";b.style.top="0px"}else{b.style.left="-100px";b.style.top="-100px"}var c=document.createElement("div");c.id="webSocketFlash";b.appendChild(c);document.body.appendChild(b);swfobject.embedSWF(WEB_SOCKET_SWF_LOCATION,"webSocketFlash","1","1","10.0.0",null,null,{hasPriority:true,swliveconnect:true,allowScriptAccess:"always"},null,function(f){if(!f.success){a.error("[WebSocket] swfobject.embedSWF failed")}})};WebSocket.__onFlashInitialized=function(){setTimeout(function(){WebSocket.__flash=document.getElementById("webSocketFlash");WebSocket.__flash.setCallerUrl(location.href);WebSocket.__flash.setDebug(!!window.WEB_SOCKET_DEBUG);for(var b=0;b<WebSocket.__tasks.length;++b){WebSocket.__tasks[b]()}WebSocket.__tasks=[]},0)};WebSocket.__onFlashEvent=function(){setTimeout(function(){try{var c=WebSocket.__flash.receiveEvents();for(var b=0;b<c.length;++b){WebSocket.__instances[c[b].webSocketId].__handleEvent(c[b])}}catch(d){a.error(d)}},0);return true};WebSocket.__log=function(b){a.log(decodeURIComponent(b))};WebSocket.__error=function(b){a.error(decodeURIComponent(b))};WebSocket.__addTask=function(b){if(WebSocket.__flash){b()}else{WebSocket.__tasks.push(b)}};WebSocket.__isFlashLite=function(){if(!window.navigator||!window.navigator.mimeTypes){return false}var b=window.navigator.mimeTypes["application/x-shockwave-flash"];if(!b||!b.enabledPlugin||!b.enabledPlugin.filename){return false}return b.enabledPlugin.filename.match(/flashlite/i)?true:false};if(!window.WEB_SOCKET_DISABLE_AUTO_INITIALIZATION){swfobject.addDomLoadEvent(function(){WebSocket.__initialize()})}})();FLASH_WEBSOCKET_LOADED=1;
\ No newline at end of file
index 604bbc11aaf0881380889672ac0f82e980bae7b7..df3f3eafce36bde5cfe810afeef3b90864f73217 100644 (file)
           <a href="http://www.webtoolkit.info" target="_blank">Base64 functions</a><br/>
           Copyright &copy; webtoolkit.info Inc.
         </li>
+        <li>
+          <a href="https://github.com/gimite/web-socket-js" target="_blank">web-socket-js</a><br/>
+          Copyright &copy; 2013 Hiroshi Ichikawa, BSD license.
+        </li>
+        <li>
+          <a href="http://code.google.com/p/swfobject/" target="_blank">SWFObject v2.2</a><br/>
+          Copyright &copy; SWFObject contributors, MIT license.
+        </li>
       </td>
     </tr>
   </table>
index 2d8ac2091cd3b1d7738f26c7f6037e34945f2a38..81657008cb5cc08e3a5dcf5337933d2f42b792b7 100644 (file)
@@ -1,11 +1,15 @@
 from zope.interface import implements
 
 from twisted.python import usage
-from twisted.internet import task
+
+from twisted.internet import task, protocol
+from twisted.protocols import basic, policies
 from twisted.plugin import IPlugin
 from twisted.application.service import IServiceMaker
-from twisted.application import internet, strports
+from twisted.application import internet, strports, service
 from twisted.web import static, server
+import urlparse
+import urllib
 
 from qwebirc.root import RootSite
 
@@ -18,6 +22,7 @@ class Options(usage.Options):
     ["privkey", "k", "server.pem", "SSL certificate to use for HTTPS."],
     ["certificate-chain", "C", None, "Chain SSL certificate"],
     ["staticpath", "s", "static", "Path to static content"],
+    ["flashPort", None, None, "Port to listen on for flash policy connections."],
   ]
 
   optFlags = [["notracebacks", "n", "Display tracebacks in broken web pages. " +
@@ -30,7 +35,46 @@ class Options(usage.Options):
         get_ssl_factory_factory()
       except ImportError:
         raise usage.UsageError("SSL support not installed")
-        
+
+class FlashPolicyProtocol(protocol.Protocol, policies.TimeoutMixin):
+  def connectionMade(self):
+    self.setTimeout(5)
+
+  def dataReceived(self, data):
+    if data == '<policy-file-request/>\0':
+      self.transport.write(self.factory.response_body)
+      self.transport.loseConnection()
+      return
+    elif self.factory.childProtocol:
+      self.setTimeout(None)
+      p = self.factory.childProtocol.buildProtocol(self.transport.client)
+      p.transport = self.transport
+      self.transport.protocol = p
+      p.connectionMade()
+      p.dataReceived(data)
+    else:
+      self.transport.loseConnection()
+
+class FlashPolicyFactory(protocol.ServerFactory):
+  protocol = FlashPolicyProtocol
+
+  def __init__(self, childProtocol=None):
+    import config
+    base_url = urlparse.urlparse(config.BASE_URL)
+    port = base_url.port
+    if port is None:
+      if base_url.scheme == "http":
+        port = 80
+      elif base_url.scheme == "https":
+        port = 443
+      else:
+        raise Exception("Unknown scheme: " + base_url.scheme)
+
+    self.childProtocol = childProtocol
+    self.response_body = """<cross-domain-policy>
+    <allow-access-from domain="%s" to-ports="%d" />
+</cross-domain-policy>""" % (urllib.quote(base_url.hostname), port) + '\0'
+
 class QWebIRCServiceMaker(object):
   implements(IServiceMaker, IPlugin)
   tapname = "qwebirc"
@@ -42,15 +86,21 @@ class QWebIRCServiceMaker(object):
       site = RootSite(config['staticpath'], logPath=config['logfile'])
     else:
       site = RootSite(config['staticpath'])
-    
+
+    s = service.MultiService()
     site.displayTracebacks = not config["notracebacks"]
     if config['https']:
       ssl_factory = get_ssl_factory_factory()
       i = internet.SSLServer(int(config['https']), site, ssl_factory(config['privkey'], config['certificate'], certificateChainFile=config["certificate-chain"]), interface=config['ip'])
     else:
-      i = internet.TCPServer(int(config['port']), site, interface=config['ip'])
-      
-    return i
+      i = internet.TCPServer(int(config['port']), FlashPolicyFactory(site), interface=config['ip'])
+
+    i.setServiceParent(s)
+    if config["flashPort"]:
+      f = internet.TCPServer(int(config['flashPort']), FlashPolicyFactory(), interface=config['ip'])
+      f.setServiceParent(s)
+
+    return s
 
 def get_ssl_factory_factory():
   from twisted.internet.ssl import DefaultOpenSSLContextFactory