From 1c1450c22cb364a15c44e01adca76a520f2a2f22 Mon Sep 17 00:00:00 2001 From: Andrew Cook Date: Tue, 8 Apr 2014 18:43:39 +1000 Subject: [PATCH 1/7] IRCClient: remove old activeTimers code This became unused at some point but was never removed --- js/irc/ircclient.js | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/js/irc/ircclient.js b/js/irc/ircclient.js index a71fd6f..ef66ce1 100644 --- a/js/irc/ircclient.js +++ b/js/irc/ircclient.js @@ -16,7 +16,6 @@ qwebirc.irc.IRCClient = new Class({ this.lastNicks = []; this.inviteChanList = []; - this.activeTimers = {}; this.tracker = new qwebirc.irc.IRCTracker(this); }, @@ -401,7 +400,6 @@ qwebirc.irc.IRCClient = new Class({ __joinInvited: function() { this.exec("/JOIN " + this.inviteChanList.join(",")); this.inviteChanList = []; - delete this.activeTimers["serviceInvite"]; }, userInvite: function(user, channel) { var nick = user.hostToNick(); @@ -504,14 +502,6 @@ qwebirc.irc.IRCClient = new Class({ this.send("QUIT :" + message, true); this.disconnect(); }, - disconnect: function() { - for(var k in this.activeTimers) { - this.activeTimers[k].cancel(); - }; - this.activeTimers = {}; - - this.parent(); - }, awayMessage: function(nick, message) { this.newQueryLine(nick, "AWAY", {"n": nick, "m": message}, true); }, From 77b3059aa2873c6d7ba27132bc6f3b7db7fe0265 Mon Sep 17 00:00:00 2001 From: Andrew Cook Date: Tue, 8 Apr 2014 19:10:48 +1000 Subject: [PATCH 2/7] IRCClient: Move sasl authentication into javascript This is the start of work attempting to remove the javascript sides dependance on twisted. The goal is to allow the client to directly contact the irc server over tcp or websockets. --- js/base64.js | 62 +++++++++++++++++++++++++ js/irc/baseircclient.js | 71 ++++++++++++++++++++++++----- js/irc/ircconnection.js | 8 +--- js/irc/numerics.js | 11 ++++- qwebirc/engines/ajaxengine.py | 10 ---- qwebirc/ircclient.py | 86 ----------------------------------- 6 files changed, 132 insertions(+), 116 deletions(-) create mode 100644 js/base64.js diff --git a/js/base64.js b/js/base64.js new file mode 100644 index 0000000..e2a6911 --- /dev/null +++ b/js/base64.js @@ -0,0 +1,62 @@ +/* Base64 polyfill from https://github.com/davidchambers/Base64.js/ */ + +;(function () { + + var object = typeof exports != 'undefined' ? exports : this; // #8: web workers + var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; + + function InvalidCharacterError(message) { + this.message = message; + } + InvalidCharacterError.prototype = new Error; + InvalidCharacterError.prototype.name = 'InvalidCharacterError'; + + // encoder + // [https://gist.github.com/999166] by [https://github.com/nignag] + object.btoa || ( + object.btoa = function (input) { + for ( + // initialize result and counter + var block, charCode, idx = 0, map = chars, output = ''; + // if the next input index does not exist: + // change the mapping table to "=" + // check if d has no fractional digits + input.charAt(idx | 0) || (map = '=', idx % 1); + // "8 - idx % 1 * 8" generates the sequence 2, 4, 6, 8 + output += map.charAt(63 & block >> 8 - idx % 1 * 8) + ) { + charCode = input.charCodeAt(idx += 3/4); + if (charCode > 0xFF) { + throw new InvalidCharacterError("'btoa' failed: The string to be encoded contains characters outside of the Latin1 range."); + } + block = block << 8 | charCode; + } + return output; + }); + + // decoder + // [https://gist.github.com/1020396] by [https://github.com/atk] + object.atob || ( + object.atob = function (input) { + input = input.replace(/=+$/, '') + if (input.length % 4 == 1) { + throw new InvalidCharacterError("'atob' failed: The string to be decoded is not correctly encoded."); + } + for ( + // initialize result and counters + var bc = 0, bs, buffer, idx = 0, output = ''; + // get next character + buffer = input.charAt(idx++); + // character found in table? initialize bit storage and add its ascii value; + ~buffer && (bs = bc % 4 ? bs * 64 + buffer : buffer, + // and if not first of each 4 characters, + // convert the first 8 bits to one ascii character + bc++ % 4) ? output += String.fromCharCode(255 & bs >> (-2 * bc & 6)) : 0 + ) { + // try to find character in table (0-63, not found => -1) + buffer = chars.indexOf(buffer); + } + return output; + }); + +}()); diff --git a/js/irc/baseircclient.js b/js/irc/baseircclient.js index a735489..816c898 100644 --- a/js/irc/baseircclient.js +++ b/js/irc/baseircclient.js @@ -22,10 +22,13 @@ qwebirc.irc.BaseIRCClient = new Class({ this.toIRCLower = qwebirc.irc.RFC1459toIRCLower; this.nickname = connOptions.nickname; + this.authUser = connOptions.authUser; + this.authSecret = connOptions.authSecret; this.lowerNickname = this.toIRCLower(this.nickname); this.__signedOn = false; this.caps = {}; + this.sasl_timeout = false; this.pmodes = {b: qwebirc.irc.PMODE_LIST, l: qwebirc.irc.PMODE_SET_ONLY, k: qwebirc.irc.PMODE_SET_UNSET, o: qwebirc.irc.PMODE_SET_UNSET, v: qwebirc.irc.PMODE_SET_UNSET}; this.channels = {} this.nextctcp = 0; @@ -102,22 +105,63 @@ qwebirc.irc.BaseIRCClient = new Class({ } }, irc_AUTHENTICATE: function(prefix, params) { - /* Silently hide. */ + this.send("AUTHENTICATE "+btoa([this.authUser, this.authUser, this.authSecret].join('\0'))); return true; }, + irc_saslFinished: function(prefix, params) { + this.send("CAP END"); + $clear(this.sasl_timeout); + return false; + }, + __saslTimeout: function() { + this.send("CAP END"); + }, irc_CAP: function(prefix, params) { - if(params[1] == "ACK") { - var capslist = []; - if (params[2] == "*") - capslist = params[3].split(" "); - else - capslist = params[2].split(" "); + var caplist; + switch(params[1]) { + case "ACK": + if (params[2] == "*") { + caplist = params[3].split(" "); + } else { + caplist = params[2].split(" "); + } + + for (i = 0; i < caplist.length; i++) + this.caps[caplist[i]] = true + + if (params[2] != "*") { + if(this.caps.sasl && this.authUser) { + this.send("AUTHENTICATE "+conf.atheme.sasl_type); + this.sasl_timeout = this.__saslTimeout.delay(15000, this); + } else { + this.send("CAP END"); + } + } + break; + case "NAK": + this.send("CAP END"); + break; + case "LS": + if (params[2] == "*") { + caplist = params[3].split(" "); + } else { + caplist = params[2].split(" "); + } - var i; - for (i = 0; i < capslist.length; i++) { - this.caps[capslist[i]] = true; - if (capslist[i] == "sasl") - this.rawNumeric("AUTHENTICATE", prefix, ["*", "Attempting SASL authentication..."]); + for (i = 0; i < caplist.length; i++) { + if (caplist[i] == "sasl") + this.caps[caplist[i]] = false; + if (caplist[i] == "multi-prefix") + this.caps[caplist[i]] = false; + } + + if (params[2] != "*") { + caplist = Object.keys(this.caps); + if(caplist.length) { + this.send("CAP REQ :"+caplist.join(" ")); + } else { + this.send("CAP END"); + } } } @@ -514,6 +558,9 @@ qwebirc.irc.BaseIRCClient = new Class({ this.irc_ERR_CHANOPPRIVSNEEDED = this.irc_ERR_CANNOTSENDTOCHAN = this.irc_genericError; this.irc_ERR_NOSUCHNICK = this.irc_genericQueryError; this.irc_ERR_NICKNAMEINUSE = this.irc_ERR_UNAVAILRESOURCE = this.irc_genericNickInUse; + this.irc_RPL_LOGGEDIN = this.irc_ERR_NICKLOCKED = this.irc_saslFinished; + this.irc_ERR_SASLFAIL = this.irc_ERR_SASLTOOLONG = this.irc_saslFinished; + this.irc_ERR_SASLABORTED = this.irc_ERR_SASLALREADY = this.irc_saslFinished; return true; }, irc_RPL_AWAY: function(prefix, params) { diff --git a/js/irc/ircconnection.js b/js/irc/ircconnection.js index 710a039..50dd8d9 100644 --- a/js/irc/ircconnection.js +++ b/js/irc/ircconnection.js @@ -11,9 +11,7 @@ qwebirc.irc.IRCConnection = new Class({ floodReset: 5000, errorAlert: true, maxRetries: 5, - serverPassword: null, - authUser: null, - authSecret: null + serverPassword: null }, initialize: function(session, options) { this.session = session; @@ -256,10 +254,6 @@ qwebirc.irc.IRCConnection = new Class({ var postdata = "nick=" + encodeURIComponent(this.initialNickname); if($defined(this.options.serverPassword)) postdata+="&password=" + encodeURIComponent(this.options.serverPassword); - if($defined(this.options.authUser) && $defined(this.options.authSecret)) { - postdata+="&authUser=" + encodeURIComponent(this.options.authUser); - postdata+="&authSecret=" + encodeURIComponent(this.options.authSecret); - } r.send(postdata); }, diff --git a/js/irc/numerics.js b/js/irc/numerics.js index b44d2f3..6676218 100644 --- a/js/irc/numerics.js +++ b/js/irc/numerics.js @@ -32,5 +32,14 @@ qwebirc.irc.Numerics = { "305": "RPL_UNAWAY", "306": "RPL_NOWAWAY", "324": "RPL_CHANNELMODEIS", - "329": "RPL_CREATIONTIME" + "329": "RPL_CREATIONTIME", + "900": "RPL_LOGGEDIN", + "901": "RPL_LOGGEDOUT", + "902": "ERR_NICKLOCKED", + "903": "RPL_SASLSUCCESS", + "904": "ERR_SASLFAIL", + "905": "ERR_SASLTOOLONG", + "906": "ERR_SASLABORTED", + "907": "ERR_SASLALREADY", + "908": "RPL_SASLMECHS" }; diff --git a/qwebirc/engines/ajaxengine.py b/qwebirc/engines/ajaxengine.py index 46c543d..b8653be 100644 --- a/qwebirc/engines/ajaxengine.py +++ b/qwebirc/engines/ajaxengine.py @@ -194,13 +194,6 @@ def newConnection(self, request): if password is not None: password = ircclient.irc_decode(password[0]) - authUser = request.args.get("authUser") - if authUser is not None: - authUser = ircclient.irc_decode(authUser[0]) - authSecret = request.args.get("authSecret") - if authSecret is not None: - authSecret = ircclient.irc_decode(authSecret[0]) - for i in xrange(10): id = get_session_id() if not Sessions.get(id): @@ -223,9 +216,6 @@ def proceed(hostname): kwargs = dict(nick=nick, ident=ident, ip=ip, realname=realname, perform=perform, hostname=hostname) if password is not None: kwargs["password"] = password - if ((authUser is not None) and (authSecret is not None)): - kwargs["authUser"] = authUser - kwargs["authSecret"] = authSecret client = ircclient.createIRC(session, **kwargs) diff --git a/qwebirc/ircclient.py b/qwebirc/ircclient.py index 33324ee..21c0b56 100644 --- a/qwebirc/ircclient.py +++ b/qwebirc/ircclient.py @@ -24,12 +24,9 @@ class QWebIRCClient(basic.LineReceiver): def __init__(self, *args, **kwargs): self.__nickname = "(unregistered)" self.registered = False - self.saslauth = False self.authUser = None self.authSecret = None self.cap = [] - self.saslTimer = Timer(15.0, self.stopSasl) - self.saslTimer.start() def dataReceived(self, data): basic.LineReceiver.dataReceived(self, data.replace("\r", "")) @@ -44,79 +41,7 @@ def lineReceived(self, line): # emit and ignore traceback.print_exc() return - - if command == "CAP": - if (self.registered): - return - - # We're performing CAP negotiation. - # We're receiving the list. Wait until its complete, then request what - # we want. - if (params[1] == "LS"): - if (self.saslauth): - return - if (params[2] == "*"): - self.cap.extend(params[3].split(" ")) - else: - self.cap.extend(params[2].split(" ")) - reqlist = [] - if ("multi-prefix" in self.cap): - reqlist.append("multi-prefix") - if ("sasl" in self.cap): - if (self.authUser and self.authSecret): - self.saslauth = True - reqlist.append("sasl") - if (reqlist): - self.write("CAP REQ :" + ' '.join(reqlist)) - self.cap = reqlist - else: - self.write("CAP END") - - # We're receiving acknowledgement of requested features. Handle it. - # Once all acknowledgements are received, end CAP is SASL is not - # underway. - if "ACK" in params: - if "sasl" in params[-1].split(" "): - if (self.authUser and self.authSecret): - self.write("AUTHENTICATE "+config.atheme["sasl_type"]) - self.saslauth = True - if (not self.saslauth): - self.write("CAP END") - - # We're receiving negative acknowledgement; a feature upgrade was denied. - # Once all acknowledgements are received, end CAP is SASL is not - # underway. - if (params[1] == "NAK"): - for item in params[2].split(" "): - self.cap.remove(item) - if (not self.saslauth): - self.write("CAP END") - - # Handle SASL authentication requests. - if (command == "AUTHENTICATE"): - if (not self.saslauth): - return - - # We're performing SASL auth. Send them our authcookie. - authtext = base64.b64encode('\0'.join([self.authUser, self.authUser, self.authSecret])) - while (len(authtext) >= 400): - self.write("AUTHENTICATE " + authtext[0:400]) - authtext = authtext[400:] - if (len(authtext) != 0): - self.write("AUTHENTICATE " + authtext) - else: - self.write("AUTHENTICATE +") - - # Handle SASL result messages. - # End CAP after one of these, unless an acknowledgement message is still - # waiting. - if (command in ["903", "904", "905","906","907"]): - if (self.saslauth): - self.saslauth = False - if (not self.saslauth): - self.write("CAP END") - if command == "001": self.registered = True self.__nickname = params[0] @@ -130,15 +55,6 @@ def lineReceived(self, line): if nick == self.__nickname: self.__nickname = params[0] - def stopSasl(self): - """Cancels SASL authentication. Useful if Services are absent.""" - if (self.saslauth): - self.saslauth = False - self.write("CAP END") - - # Send an internally-generated failure response to the client. - self.handleCommand("904", "QWebIRC", ["*", "SASL authentication timed out"]) - def handleCommand(self, command, prefix, params): self("c", command, prefix, params) @@ -160,8 +76,6 @@ def connectionMade(self): nick, ident, ip, realname, hostname, pass_ = f["nick"], f["ident"], f["ip"], f["realname"], f["hostname"], f.get("password") self.__nickname = nick self.__perform = f.get("perform") - self.authUser = f.get("authUser") - self.authSecret = f.get("authSecret") if config.irc["webirc_mode"] == "webirc": self.write("WEBIRC %s qwebirc %s %s" % (config.irc["webirc_password"], hostname, ip)) From a6e642c9e0e4f75f61a2f2d76445efb82d9378f1 Mon Sep 17 00:00:00 2001 From: Andrew Cook Date: Tue, 8 Apr 2014 20:44:16 +1000 Subject: [PATCH 3/7] IRCClient: Send unparsed lines to the client --- js/irc/baseircclient.js | 29 +++++++++++++++++++++++------ qwebirc/engines/ajaxengine.py | 2 +- qwebirc/ircclient.py | 10 +++++----- 3 files changed, 29 insertions(+), 12 deletions(-) diff --git a/js/irc/baseircclient.js b/js/irc/baseircclient.js index 816c898..f6840f3 100644 --- a/js/irc/baseircclient.js +++ b/js/irc/baseircclient.js @@ -55,10 +55,27 @@ qwebirc.irc.BaseIRCClient = new Class({ } this.disconnect(); } else if(message == "c") { - var command = data[1].toUpperCase(); + var line = data[1]; + var command = ""; + var prefix = ""; + var params = []; + var trailing = ""; + + if (line[0] == ":") { + var index = line.indexOf(" "); + prefix = line.substring(1, index); + line = line.substring(index + 1); + } + if (line.indexOf(" :") != -1) { + var index = line.indexOf(" :"); + trailing = line.substring(index + 2); + params = line.substring(0, index).split(" "); + params.push(trailing); + } else { + params = line.split(" "); + } + command = params.splice(0, 1)[0].toUpperCase(); - var prefix = data[2]; - var sl = data[3]; var n = qwebirc.irc.Numerics[command]; var x = n; @@ -68,11 +85,11 @@ qwebirc.irc.BaseIRCClient = new Class({ var o = this["irc_" + n]; if(o) { - var r = o.run([prefix, sl], this); + var r = o.run([prefix, params], this); if(!r) - this.rawNumeric(command, prefix, sl); + this.rawNumeric(command, prefix, params); } else { - this.rawNumeric(command, prefix, sl); + this.rawNumeric(command, prefix, params); } } }, diff --git a/qwebirc/engines/ajaxengine.py b/qwebirc/engines/ajaxengine.py index b8653be..b0ab034 100644 --- a/qwebirc/engines/ajaxengine.py +++ b/qwebirc/engines/ajaxengine.py @@ -144,7 +144,7 @@ def disconnect(self): # DANGER! Breach of encapsulation! def connect_notice(line): - return "c", "NOTICE", "", ("AUTH", "*** (qwebirc) %s" % line) + return "c", "NOTICE AUTH :*** (qwebirc) %s" % line class Channel: def __init__(self, request): diff --git a/qwebirc/ircclient.py b/qwebirc/ircclient.py index 21c0b56..9daf44f 100644 --- a/qwebirc/ircclient.py +++ b/qwebirc/ircclient.py @@ -36,7 +36,7 @@ def lineReceived(self, line): try: prefix, command, params = irc.parsemsg(line) - self.handleCommand(command, prefix, params) + self.handleLine(line) except irc.IRCBadMessage: # emit and ignore traceback.print_exc() @@ -55,12 +55,12 @@ def lineReceived(self, line): if nick == self.__nickname: self.__nickname = params[0] - def handleCommand(self, command, prefix, params): - self("c", command, prefix, params) - + def handleLine(self, line): + self("c", line) + def __call__(self, *args): self.factory.publisher.event(args) - + def write(self, data): self.transport.write("%s\r\n" % irc.lowQuote(data.encode("utf-8"))) From 74efd7b7d32c87e480759177a078123b02a50c06 Mon Sep 17 00:00:00 2001 From: Andrew Cook Date: Wed, 9 Apr 2014 00:02:23 +1000 Subject: [PATCH 4/7] IRCClient: Support attempting multiple connections Currently unused. it will attempt anything in BaseIRCClient.connections as a constructor for a connection class. --- js/irc/baseircclient.js | 37 +++++++++++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/js/irc/baseircclient.js b/js/irc/baseircclient.js index f6840f3..095ccb7 100644 --- a/js/irc/baseircclient.js +++ b/js/irc/baseircclient.js @@ -27,25 +27,42 @@ qwebirc.irc.BaseIRCClient = new Class({ this.lowerNickname = this.toIRCLower(this.nickname); this.__signedOn = false; + this.__connected = false; this.caps = {}; this.sasl_timeout = false; this.pmodes = {b: qwebirc.irc.PMODE_LIST, l: qwebirc.irc.PMODE_SET_ONLY, k: qwebirc.irc.PMODE_SET_UNSET, o: qwebirc.irc.PMODE_SET_UNSET, v: qwebirc.irc.PMODE_SET_UNSET}; this.channels = {} this.nextctcp = 0; - connOptions.initialNickname = this.nickname; - connOptions.onRecv = this.dispatch.bind(this); - this.connection = new qwebirc.irc.IRCConnection(session, connOptions); - - this.send = this.connection.send.bind(this.connection); - this.connect = this.connection.connect.bind(this.connection); - this.disconnect = this.connection.disconnect.bind(this.connection); + this.connections = [qwebirc.irc.IRCConnection]; this.setupGenericErrors(); }, + send: function(data) { + return this.connection.send(data); + }, + connect: function() { + this.tryConnect(); + }, + disconnect: function() { + this.connection.disconnect(); + }, + tryConnect: function() { + var options = {}; + var Connection = this.connections.pop(); + if(Connection) { + options.initialNickname = this.nickname; + options.onRecv = this.dispatch.bind(this); + this.connection = new Connection(this.session, options); + this.connection.connect(); + } else { + this.disconnected("Unable to connect") + } + }, dispatch: function(data) { var message = data[0]; if(message == "connect") { + this.__connected = true; this.connected(); } else if(message == "disconnect") { if(data.length == 0) { @@ -53,7 +70,11 @@ qwebirc.irc.BaseIRCClient = new Class({ } else { this.disconnected(data[1]); } - this.disconnect(); + if(this.__connected) { + this.disconnect(); + } else { + this.tryConnect(); + } } else if(message == "c") { var line = data[1]; var command = ""; From 93340b0aec04ef9248ab7291c2d7e905bfb47902 Mon Sep 17 00:00:00 2001 From: Andrew Cook Date: Wed, 9 Apr 2014 20:48:13 +1000 Subject: [PATCH 5/7] QUI: Move NBSPCreate into url.js, not used anywhere else --- js/jslib.js | 14 -------------- js/ui/url.js | 13 +++++++++++-- 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/js/jslib.js b/js/jslib.js index dfaccb6..0753332 100644 --- a/js/jslib.js +++ b/js/jslib.js @@ -86,20 +86,6 @@ qwebirc.util.MonthsOfYear = { 11: "Dec" }; -qwebirc.util.NBSPCreate = function(text, element) { - var e = text.split(" "); - for(var i=0;i Date: Thu, 10 Apr 2014 18:14:29 +1000 Subject: [PATCH 6/7] Add FlashSocket This lets the client attempt to connect to the irc server directly, which performs better for everyone. will fall back to ajax when flash is unavaliable. UTF-8 decoding happens in javascript to avoid flash<->javascript stupidity and handle partial codepoints. --- TODO | 3 + bin/compile.py | 20 +- bin/pagegen.py | 16 +- iris.conf.example | 19 + js/irc/baseircclient.js | 8 +- js/irc/flashconnection.js | 113 ++++++ js/swfobject.js | 777 ++++++++++++++++++++++++++++++++++++++ qwebirc/config.py | 1 + qwebirc/config_options.py | 5 + swf/flashsocket.as | 91 +++++ 10 files changed, 1041 insertions(+), 12 deletions(-) create mode 100644 js/irc/flashconnection.js create mode 100644 js/swfobject.js create mode 100644 swf/flashsocket.as diff --git a/TODO b/TODO index 598edda..90c9beb 100644 --- a/TODO +++ b/TODO @@ -6,3 +6,6 @@ General: UI: - replace qwebirc UI elements (logo, pages, etc) + +README: +- Document dependencies (swftools added by flashsocket) diff --git a/bin/compile.py b/bin/compile.py index 14a72c4..d68c610 100644 --- a/bin/compile.py +++ b/bin/compile.py @@ -75,21 +75,21 @@ def merge_files(output, files, root_path=lambda x: x): f2.close() f.close() +def mkdir(path): + try: + os.mkdir(path) + except: + pass + def main(outputdir=".", produce_debug=True): ID = pagegen.getgitid() pagegen.main(outputdir, produce_debug=produce_debug) coutputdir = os.path.join(outputdir, "compiled") - try: - os.mkdir(coutputdir) - except: - pass - - try: - os.mkdir(os.path.join(outputdir, "static", "css")) - except: - pass + mkdir(coutputdir) + mkdir(os.path.join(outputdir, "static", "css")) + mkdir(os.path.join(outputdir, "static", "swf")) for uiname, value in pages.UIs.items(): csssrc = pagegen.csslist(uiname, True) @@ -112,6 +112,8 @@ def main(outputdir=".", produce_debug=True): alljs.append(os.path.join("js", "ui", "frontends", y + ".js")) jmerge_files(outputdir, "js", uiname + "-" + ID, alljs, file_prefix="QWEBIRC_BUILD=\"" + ID + "\";\n") + subprocess.call(["as3compile", os.path.join(outputdir, "swf", "flashsocket.as"), "-o", os.path.join(outputdir, "static", "swf", "flashsocket.swf")]) + os.rmdir(coutputdir) f = open(".compiled", "w") diff --git a/bin/pagegen.py b/bin/pagegen.py index d963698..869efaf 100755 --- a/bin/pagegen.py +++ b/bin/pagegen.py @@ -61,7 +61,18 @@ def producehtml(name, debug): div = ui.get("div", "") customcss = "\n".join(" " % (config.frontend["static_base_url"], x) for x in ui.get("customcss", [])) customjs = "\n".join(" " % (config.frontend["static_base_url"], x) for x in ui.get("customjs", [])) - + flash = """ +
+
+ +
""" % (config.frontend["static_base_url"]) return """%s @@ -84,9 +95,10 @@ def producehtml(name, debug):
Javascript is required to use IRC.
%s +%s -""" % (ui["doctype"], config.frontend["app_title"], config.frontend["static_base_url"], config.frontend["extra_html"], csshtml, customcss, jshtml, customjs, config.js_config(), ui["class"], div) +""" % (ui["doctype"], config.frontend["app_title"], config.frontend["static_base_url"], config.frontend["extra_html"], csshtml, customcss, jshtml, customjs, config.js_config(), ui["class"], div, flash) def main(outputdir=".", produce_debug=True): p = os.path.join(outputdir, "static") diff --git a/iris.conf.example b/iris.conf.example index 0732925..be8baee 100644 --- a/iris.conf.example +++ b/iris.conf.example @@ -94,6 +94,21 @@ webirc_mode: webirc_password: fish +# FRONTEND IRC CONNECTION OPTIONS +# These options provide the needed information for the frontend to connect to +# the IRC server via tcp using a flash plugin. +# They require a backend restart and a rerun of compile.py to update. +[flash] + +# SERVER: Hostname (or IP address) of IRC server to connect to. +server: irc.myserver.com + +# PORT: Port of IRC server to connect to. +port: 6667 + +# XMLPORT: Port of IRC servers flash policy daemon +xmlport: 8430 + # ATHEME ENGINE OPTIONS # These options control communication with Atheme by the Atheme engine backend, @@ -221,6 +236,10 @@ static_base_url: / # Iris instances on the same host. dynamic_base_url: / +# CONNECTIONS: What order to attempt methods of connection in +# space seperated list of methods +# valid values: ajax flash +connections: ajax # ATHEME INTEGRATION OPTIONS diff --git a/js/irc/baseircclient.js b/js/irc/baseircclient.js index 095ccb7..527e07f 100644 --- a/js/irc/baseircclient.js +++ b/js/irc/baseircclient.js @@ -34,7 +34,13 @@ qwebirc.irc.BaseIRCClient = new Class({ this.channels = {} this.nextctcp = 0; - this.connections = [qwebirc.irc.IRCConnection]; + this.connections = []; + for(var x = 0; x < conf.frontend.connections.length; x++) { + switch(conf.frontend.connections[x]) { + case "ajax": this.connections.unshift(qwebirc.irc.IRCConnection); break; + case "flash": this.connections.unshift(qwebirc.irc.FlashConnection); break; + } + } this.setupGenericErrors(); }, diff --git a/js/irc/flashconnection.js b/js/irc/flashconnection.js new file mode 100644 index 0000000..b69179a --- /dev/null +++ b/js/irc/flashconnection.js @@ -0,0 +1,113 @@ + +qwebirc.irc.FlashConnection = new Class({ + Implements: [Events, Options], + options: { + initialNickname: "ircconnX", + server: "irc.example.com", + port: 6667, + xmlport: 8430, + timeout: 45000, + maxRetries: 5, + serverPassword: null + }, + initialize: function(session, options) { + this.setOptions(options, conf.flash); + }, + connect: function() { + this.buffer = []; + if(!FlashSocket.connect) { + this.fireEvent("recv", [["disconnect", "No Flash support"]]); + return; + } + FlashSocket.state = this.__state.bind(this); + FlashSocket.connect(this.options.server, this.options.port, this.options.xmlport); + }, + connected: function() { + this.send("CAP LS"); + this.send("USER "+this.options.initialNickname+" 0 * :qwebirc"); + if(this.options.serverPassword) + this.send("PASS :"+this.options.serverPassword); + this.send("NICK "+this.options.initialNickname); + this.fireEvent("recv", [["connect"]]); + }, + disconnect: function() { + FlashSocket.disconnect(); + }, + disconnected: function(reason) { + reason = reason || "Connection Closed"; + this.fireEvent("recv", [["disconnect", reason]]); + }, + send: function(data, synchronous) { + FlashSocket.write(String(data)+"\r\n"); + return true; + }, + recv: function(data) { + var LF = 10; + var buffer = this.buffer.concat(data); + var i = buffer.indexOf(LF); + while(i != -1) { + var msg = buffer.splice(0, i+1); + msg.pop(); //LF + msg.pop(); //CR + this.fireEvent("recv", [["c", this.decode(msg)]]); + i = buffer.indexOf(LF); + } + this.buffer = buffer; + }, + decode: function(buffer) { + var replace = 65533; //U+FFFD 'REPLACEMENT CHARACTER' + var points = []; + var i = 0; + while(i < buffer.length) { + var len = 0; + var point = 0; + if ((buffer[i] & 0x80) == 0x00) { + point = buffer[i++] + } else if((buffer[i] & 0xE0) == 0xC0) { + len = 1; + point = (buffer[i++] & 0x1F); + } else if((buffer[i] & 0xF0) == 0xE0) { + len = 2; + point = (buffer[i++] & 0x0F) + } else if((buffer[i] & 0xF8) == 0xF0) { + len = 3; + point = (buffer[i++] & 0x07) + } else { + point = replace; + i++; + } + for(x = 0; x < len && i < buffer.length; x++) { + var octet = buffer[i++]; + if((octet & 0xC0) != 0x80) + break; + point = (point << 6) | (octet & 0x3F); + } + /* Prevent ascii being snuck past in unicode */ + if(len != 0 && point < 0x80) + point = replace; + /* Replace partial characters */ + if(x != len) + point = replace; + + if(point >= 0x10000) { + point -= 0x10000; + points.push((point >> 10) + 0xD800); + points.push((point % 0x400) + 0xDC00); + } else { + points.push(point); + } + } + return String.fromCharCode.apply(null, points); + }, + __state: function(state, msg) { + if(state == 1 /* OPEN */) + this.connected(); + if(state == 3 /* CLOSED */) + this.disconnected(); + if(state == 4 /* ERROR */) + this.disconnected(msg); + if(state == 5 /* MESSAGE */) { + this.recv(JSON.parse(msg)); + } + } +}); diff --git a/js/swfobject.js b/js/swfobject.js new file mode 100644 index 0000000..6880d3e --- /dev/null +++ b/js/swfobject.js @@ -0,0 +1,777 @@ +/*! SWFObject v2.2 + is released under the MIT License +*/ + +var swfobject = function() { + + var UNDEF = "undefined", + OBJECT = "object", + SHOCKWAVE_FLASH = "Shockwave Flash", + SHOCKWAVE_FLASH_AX = "ShockwaveFlash.ShockwaveFlash", + FLASH_MIME_TYPE = "application/x-shockwave-flash", + EXPRESS_INSTALL_ID = "SWFObjectExprInst", + ON_READY_STATE_CHANGE = "onreadystatechange", + + win = window, + doc = document, + nav = navigator, + + plugin = false, + domLoadFnArr = [main], + regObjArr = [], + objIdArr = [], + listenersArr = [], + storedAltContent, + storedAltContentId, + storedCallbackFn, + storedCallbackObj, + isDomLoaded = false, + isExpressInstallActive = false, + dynamicStylesheet, + dynamicStylesheetMedia, + autoHideShow = true, + + /* Centralized function for browser feature detection + - User agent string detection is only used when no good alternative is possible + - Is executed directly for optimal performance + */ + ua = function() { + var w3cdom = typeof doc.getElementById != UNDEF && typeof doc.getElementsByTagName != UNDEF && typeof doc.createElement != UNDEF, + u = nav.userAgent.toLowerCase(), + p = nav.platform.toLowerCase(), + windows = p ? /win/.test(p) : /win/.test(u), + mac = p ? /mac/.test(p) : /mac/.test(u), + webkit = /webkit/.test(u) ? parseFloat(u.replace(/^.*webkit\/(\d+(\.\d+)?).*$/, "$1")) : false, // returns either the webkit version or false if not webkit + ie = !+"\v1", // feature detection based on Andrea Giammarchi's solution: http://webreflection.blogspot.com/2009/01/32-bytes-to-know-if-your-browser-is-ie.html + playerVersion = [0,0,0], + d = null; + if (typeof nav.plugins != UNDEF && typeof nav.plugins[SHOCKWAVE_FLASH] == OBJECT) { + d = nav.plugins[SHOCKWAVE_FLASH].description; + if (d && !(typeof nav.mimeTypes != UNDEF && nav.mimeTypes[FLASH_MIME_TYPE] && !nav.mimeTypes[FLASH_MIME_TYPE].enabledPlugin)) { // navigator.mimeTypes["application/x-shockwave-flash"].enabledPlugin indicates whether plug-ins are enabled or disabled in Safari 3+ + plugin = true; + ie = false; // cascaded feature detection for Internet Explorer + d = d.replace(/^.*\s+(\S+\s+\S+$)/, "$1"); + playerVersion[0] = parseInt(d.replace(/^(.*)\..*$/, "$1"), 10); + playerVersion[1] = parseInt(d.replace(/^.*\.(.*)\s.*$/, "$1"), 10); + playerVersion[2] = /[a-zA-Z]/.test(d) ? parseInt(d.replace(/^.*[a-zA-Z]+(.*)$/, "$1"), 10) : 0; + } + } + else if (typeof win.ActiveXObject != UNDEF) { + try { + var a = new ActiveXObject(SHOCKWAVE_FLASH_AX); + if (a) { // a will return null when ActiveX is disabled + d = a.GetVariable("$version"); + if (d) { + ie = true; // cascaded feature detection for Internet Explorer + d = d.split(" ")[1].split(","); + playerVersion = [parseInt(d[0], 10), parseInt(d[1], 10), parseInt(d[2], 10)]; + } + } + } + catch(e) {} + } + return { w3:w3cdom, pv:playerVersion, wk:webkit, ie:ie, win:windows, mac:mac }; + }(), + + /* Cross-browser onDomLoad + - Will fire an event as soon as the DOM of a web page is loaded + - Internet Explorer workaround based on Diego Perini's solution: http://javascript.nwbox.com/IEContentLoaded/ + - Regular onload serves as fallback + */ + onDomLoad = function() { + if (!ua.w3) { return; } + if ((typeof doc.readyState != UNDEF && doc.readyState == "complete") || (typeof doc.readyState == UNDEF && (doc.getElementsByTagName("body")[0] || doc.body))) { // function is fired after onload, e.g. when script is inserted dynamically + callDomLoadFunctions(); + } + if (!isDomLoaded) { + if (typeof doc.addEventListener != UNDEF) { + doc.addEventListener("DOMContentLoaded", callDomLoadFunctions, false); + } + if (ua.ie && ua.win) { + doc.attachEvent(ON_READY_STATE_CHANGE, function() { + if (doc.readyState == "complete") { + doc.detachEvent(ON_READY_STATE_CHANGE, arguments.callee); + callDomLoadFunctions(); + } + }); + if (win == top) { // if not inside an iframe + (function(){ + if (isDomLoaded) { return; } + try { + doc.documentElement.doScroll("left"); + } + catch(e) { + setTimeout(arguments.callee, 0); + return; + } + callDomLoadFunctions(); + })(); + } + } + if (ua.wk) { + (function(){ + if (isDomLoaded) { return; } + if (!/loaded|complete/.test(doc.readyState)) { + setTimeout(arguments.callee, 0); + return; + } + callDomLoadFunctions(); + })(); + } + addLoadEvent(callDomLoadFunctions); + } + }(); + + function callDomLoadFunctions() { + if (isDomLoaded) { return; } + try { // test if we can really add/remove elements to/from the DOM; we don't want to fire it too early + var t = doc.getElementsByTagName("body")[0].appendChild(createElement("span")); + t.parentNode.removeChild(t); + } + catch (e) { return; } + isDomLoaded = true; + var dl = domLoadFnArr.length; + for (var i = 0; i < dl; i++) { + domLoadFnArr[i](); + } + } + + function addDomLoadEvent(fn) { + if (isDomLoaded) { + fn(); + } + else { + domLoadFnArr[domLoadFnArr.length] = fn; // Array.push() is only available in IE5.5+ + } + } + + /* Cross-browser onload + - Based on James Edwards' solution: http://brothercake.com/site/resources/scripts/onload/ + - Will fire an event as soon as a web page including all of its assets are loaded + */ + function addLoadEvent(fn) { + if (typeof win.addEventListener != UNDEF) { + win.addEventListener("load", fn, false); + } + else if (typeof doc.addEventListener != UNDEF) { + doc.addEventListener("load", fn, false); + } + else if (typeof win.attachEvent != UNDEF) { + addListener(win, "onload", fn); + } + else if (typeof win.onload == "function") { + var fnOld = win.onload; + win.onload = function() { + fnOld(); + fn(); + }; + } + else { + win.onload = fn; + } + } + + /* Main function + - Will preferably execute onDomLoad, otherwise onload (as a fallback) + */ + function main() { + if (plugin) { + testPlayerVersion(); + } + else { + matchVersions(); + } + } + + /* Detect the Flash Player version for non-Internet Explorer browsers + - Detecting the plug-in version via the object element is more precise than using the plugins collection item's description: + a. Both release and build numbers can be detected + b. Avoid wrong descriptions by corrupt installers provided by Adobe + c. Avoid wrong descriptions by multiple Flash Player entries in the plugin Array, caused by incorrect browser imports + - Disadvantage of this method is that it depends on the availability of the DOM, while the plugins collection is immediately available + */ + function testPlayerVersion() { + var b = doc.getElementsByTagName("body")[0]; + var o = createElement(OBJECT); + o.setAttribute("type", FLASH_MIME_TYPE); + var t = b.appendChild(o); + if (t) { + var counter = 0; + (function(){ + if (typeof t.GetVariable != UNDEF) { + var d = t.GetVariable("$version"); + if (d) { + d = d.split(" ")[1].split(","); + ua.pv = [parseInt(d[0], 10), parseInt(d[1], 10), parseInt(d[2], 10)]; + } + } + else if (counter < 10) { + counter++; + setTimeout(arguments.callee, 10); + return; + } + b.removeChild(o); + t = null; + matchVersions(); + })(); + } + else { + matchVersions(); + } + } + + /* Perform Flash Player and SWF version matching; static publishing only + */ + function matchVersions() { + var rl = regObjArr.length; + if (rl > 0) { + for (var i = 0; i < rl; i++) { // for each registered object element + var id = regObjArr[i].id; + var cb = regObjArr[i].callbackFn; + var cbObj = {success:false, id:id}; + if (ua.pv[0] > 0) { + var obj = getElementById(id); + if (obj) { + if (hasPlayerVersion(regObjArr[i].swfVersion) && !(ua.wk && ua.wk < 312)) { // Flash Player version >= published SWF version: Houston, we have a match! + setVisibility(id, true); + if (cb) { + cbObj.success = true; + cbObj.ref = getObjectById(id); + cb(cbObj); + } + } + else if (regObjArr[i].expressInstall && canExpressInstall()) { // show the Adobe Express Install dialog if set by the web page author and if supported + var att = {}; + att.data = regObjArr[i].expressInstall; + att.width = obj.getAttribute("width") || "0"; + att.height = obj.getAttribute("height") || "0"; + if (obj.getAttribute("class")) { att.styleclass = obj.getAttribute("class"); } + if (obj.getAttribute("align")) { att.align = obj.getAttribute("align"); } + // parse HTML object param element's name-value pairs + var par = {}; + var p = obj.getElementsByTagName("param"); + var pl = p.length; + for (var j = 0; j < pl; j++) { + if (p[j].getAttribute("name").toLowerCase() != "movie") { + par[p[j].getAttribute("name")] = p[j].getAttribute("value"); + } + } + showExpressInstall(att, par, id, cb); + } + else { // Flash Player and SWF version mismatch or an older Webkit engine that ignores the HTML object element's nested param elements: display alternative content instead of SWF + displayAltContent(obj); + if (cb) { cb(cbObj); } + } + } + } + else { // if no Flash Player is installed or the fp version cannot be detected we let the HTML object element do its job (either show a SWF or alternative content) + setVisibility(id, true); + if (cb) { + var o = getObjectById(id); // test whether there is an HTML object element or not + if (o && typeof o.SetVariable != UNDEF) { + cbObj.success = true; + cbObj.ref = o; + } + cb(cbObj); + } + } + } + } + } + + function getObjectById(objectIdStr) { + var r = null; + var o = getElementById(objectIdStr); + if (o && o.nodeName == "OBJECT") { + if (typeof o.SetVariable != UNDEF) { + r = o; + } + else { + var n = o.getElementsByTagName(OBJECT)[0]; + if (n) { + r = n; + } + } + } + return r; + } + + /* Requirements for Adobe Express Install + - only one instance can be active at a time + - fp 6.0.65 or higher + - Win/Mac OS only + - no Webkit engines older than version 312 + */ + function canExpressInstall() { + return !isExpressInstallActive && hasPlayerVersion("6.0.65") && (ua.win || ua.mac) && !(ua.wk && ua.wk < 312); + } + + /* Show the Adobe Express Install dialog + - Reference: http://www.adobe.com/cfusion/knowledgebase/index.cfm?id=6a253b75 + */ + function showExpressInstall(att, par, replaceElemIdStr, callbackFn) { + isExpressInstallActive = true; + storedCallbackFn = callbackFn || null; + storedCallbackObj = {success:false, id:replaceElemIdStr}; + var obj = getElementById(replaceElemIdStr); + if (obj) { + if (obj.nodeName == "OBJECT") { // static publishing + storedAltContent = abstractAltContent(obj); + storedAltContentId = null; + } + else { // dynamic publishing + storedAltContent = obj; + storedAltContentId = replaceElemIdStr; + } + att.id = EXPRESS_INSTALL_ID; + if (typeof att.width == UNDEF || (!/%$/.test(att.width) && parseInt(att.width, 10) < 310)) { att.width = "310"; } + if (typeof att.height == UNDEF || (!/%$/.test(att.height) && parseInt(att.height, 10) < 137)) { att.height = "137"; } + doc.title = doc.title.slice(0, 47) + " - Flash Player Installation"; + var pt = ua.ie && ua.win ? "ActiveX" : "PlugIn", + fv = "MMredirectURL=" + encodeURI(win.location).toString().replace(/&/g,"%26") + "&MMplayerType=" + pt + "&MMdoctitle=" + doc.title; + if (typeof par.flashvars != UNDEF) { + par.flashvars += "&" + fv; + } + else { + par.flashvars = fv; + } + // IE only: when a SWF is loading (AND: not available in cache) wait for the readyState of the object element to become 4 before removing it, + // because you cannot properly cancel a loading SWF file without breaking browser load references, also obj.onreadystatechange doesn't work + if (ua.ie && ua.win && obj.readyState != 4) { + var newObj = createElement("div"); + replaceElemIdStr += "SWFObjectNew"; + newObj.setAttribute("id", replaceElemIdStr); + obj.parentNode.insertBefore(newObj, obj); // insert placeholder div that will be replaced by the object element that loads expressinstall.swf + obj.style.display = "none"; + (function(){ + if (obj.readyState == 4) { + obj.parentNode.removeChild(obj); + } + else { + setTimeout(arguments.callee, 10); + } + })(); + } + createSWF(att, par, replaceElemIdStr); + } + } + + /* Functions to abstract and display alternative content + */ + function displayAltContent(obj) { + if (ua.ie && ua.win && obj.readyState != 4) { + // IE only: when a SWF is loading (AND: not available in cache) wait for the readyState of the object element to become 4 before removing it, + // because you cannot properly cancel a loading SWF file without breaking browser load references, also obj.onreadystatechange doesn't work + var el = createElement("div"); + obj.parentNode.insertBefore(el, obj); // insert placeholder div that will be replaced by the alternative content + el.parentNode.replaceChild(abstractAltContent(obj), el); + obj.style.display = "none"; + (function(){ + if (obj.readyState == 4) { + obj.parentNode.removeChild(obj); + } + else { + setTimeout(arguments.callee, 10); + } + })(); + } + else { + obj.parentNode.replaceChild(abstractAltContent(obj), obj); + } + } + + function abstractAltContent(obj) { + var ac = createElement("div"); + if (ua.win && ua.ie) { + ac.innerHTML = obj.innerHTML; + } + else { + var nestedObj = obj.getElementsByTagName(OBJECT)[0]; + if (nestedObj) { + var c = nestedObj.childNodes; + if (c) { + var cl = c.length; + for (var i = 0; i < cl; i++) { + if (!(c[i].nodeType == 1 && c[i].nodeName == "PARAM") && !(c[i].nodeType == 8)) { + ac.appendChild(c[i].cloneNode(true)); + } + } + } + } + } + return ac; + } + + /* Cross-browser dynamic SWF creation + */ + function createSWF(attObj, parObj, id) { + var r, el = getElementById(id); + if (ua.wk && ua.wk < 312) { return r; } + if (el) { + if (typeof attObj.id == UNDEF) { // if no 'id' is defined for the object element, it will inherit the 'id' from the alternative content + attObj.id = id; + } + if (ua.ie && ua.win) { // Internet Explorer + the HTML object element + W3C DOM methods do not combine: fall back to outerHTML + var att = ""; + for (var i in attObj) { + if (attObj[i] != Object.prototype[i]) { // filter out prototype additions from other potential libraries + if (i.toLowerCase() == "data") { + parObj.movie = attObj[i]; + } + else if (i.toLowerCase() == "styleclass") { // 'class' is an ECMA4 reserved keyword + att += ' class="' + attObj[i] + '"'; + } + else if (i.toLowerCase() != "classid") { + att += ' ' + i + '="' + attObj[i] + '"'; + } + } + } + var par = ""; + for (var j in parObj) { + if (parObj[j] != Object.prototype[j]) { // filter out prototype additions from other potential libraries + par += ''; + } + } + el.outerHTML = '' + par + ''; + objIdArr[objIdArr.length] = attObj.id; // stored to fix object 'leaks' on unload (dynamic publishing only) + r = getElementById(attObj.id); + } + else { // well-behaving browsers + var o = createElement(OBJECT); + o.setAttribute("type", FLASH_MIME_TYPE); + for (var m in attObj) { + if (attObj[m] != Object.prototype[m]) { // filter out prototype additions from other potential libraries + if (m.toLowerCase() == "styleclass") { // 'class' is an ECMA4 reserved keyword + o.setAttribute("class", attObj[m]); + } + else if (m.toLowerCase() != "classid") { // filter out IE specific attribute + o.setAttribute(m, attObj[m]); + } + } + } + for (var n in parObj) { + if (parObj[n] != Object.prototype[n] && n.toLowerCase() != "movie") { // filter out prototype additions from other potential libraries and IE specific param element + createObjParam(o, n, parObj[n]); + } + } + el.parentNode.replaceChild(o, el); + r = o; + } + } + return r; + } + + function createObjParam(el, pName, pValue) { + var p = createElement("param"); + p.setAttribute("name", pName); + p.setAttribute("value", pValue); + el.appendChild(p); + } + + /* Cross-browser SWF removal + - Especially needed to safely and completely remove a SWF in Internet Explorer + */ + function removeSWF(id) { + var obj = getElementById(id); + if (obj && obj.nodeName == "OBJECT") { + if (ua.ie && ua.win) { + obj.style.display = "none"; + (function(){ + if (obj.readyState == 4) { + removeObjectInIE(id); + } + else { + setTimeout(arguments.callee, 10); + } + })(); + } + else { + obj.parentNode.removeChild(obj); + } + } + } + + function removeObjectInIE(id) { + var obj = getElementById(id); + if (obj) { + for (var i in obj) { + if (typeof obj[i] == "function") { + obj[i] = null; + } + } + obj.parentNode.removeChild(obj); + } + } + + /* Functions to optimize JavaScript compression + */ + function getElementById(id) { + var el = null; + try { + el = doc.getElementById(id); + } + catch (e) {} + return el; + } + + function createElement(el) { + return doc.createElement(el); + } + + /* Updated attachEvent function for Internet Explorer + - Stores attachEvent information in an Array, so on unload the detachEvent functions can be called to avoid memory leaks + */ + function addListener(target, eventType, fn) { + target.attachEvent(eventType, fn); + listenersArr[listenersArr.length] = [target, eventType, fn]; + } + + /* Flash Player and SWF content version matching + */ + function hasPlayerVersion(rv) { + var pv = ua.pv, v = rv.split("."); + v[0] = parseInt(v[0], 10); + v[1] = parseInt(v[1], 10) || 0; // supports short notation, e.g. "9" instead of "9.0.0" + v[2] = parseInt(v[2], 10) || 0; + return (pv[0] > v[0] || (pv[0] == v[0] && pv[1] > v[1]) || (pv[0] == v[0] && pv[1] == v[1] && pv[2] >= v[2])) ? true : false; + } + + /* Cross-browser dynamic CSS creation + - Based on Bobby van der Sluis' solution: http://www.bobbyvandersluis.com/articles/dynamicCSS.php + */ + function createCSS(sel, decl, media, newStyle) { + if (ua.ie && ua.mac) { return; } + var h = doc.getElementsByTagName("head")[0]; + if (!h) { return; } // to also support badly authored HTML pages that lack a head element + var m = (media && typeof media == "string") ? media : "screen"; + if (newStyle) { + dynamicStylesheet = null; + dynamicStylesheetMedia = null; + } + if (!dynamicStylesheet || dynamicStylesheetMedia != m) { + // create dynamic stylesheet + get a global reference to it + var s = createElement("style"); + s.setAttribute("type", "text/css"); + s.setAttribute("media", m); + dynamicStylesheet = h.appendChild(s); + if (ua.ie && ua.win && typeof doc.styleSheets != UNDEF && doc.styleSheets.length > 0) { + dynamicStylesheet = doc.styleSheets[doc.styleSheets.length - 1]; + } + dynamicStylesheetMedia = m; + } + // add style rule + if (ua.ie && ua.win) { + if (dynamicStylesheet && typeof dynamicStylesheet.addRule == OBJECT) { + dynamicStylesheet.addRule(sel, decl); + } + } + else { + if (dynamicStylesheet && typeof doc.createTextNode != UNDEF) { + dynamicStylesheet.appendChild(doc.createTextNode(sel + " {" + decl + "}")); + } + } + } + + function setVisibility(id, isVisible) { + if (!autoHideShow) { return; } + var v = isVisible ? "visible" : "hidden"; + if (isDomLoaded && getElementById(id)) { + getElementById(id).style.visibility = v; + } + else { + createCSS("#" + id, "visibility:" + v); + } + } + + /* Filter to avoid XSS attacks + */ + function urlEncodeIfNecessary(s) { + var regex = /[\\\"<>\.;]/; + var hasBadChars = regex.exec(s) != null; + return hasBadChars && typeof encodeURIComponent != UNDEF ? encodeURIComponent(s) : s; + } + + /* Release memory to avoid memory leaks caused by closures, fix hanging audio/video threads and force open sockets/NetConnections to disconnect (Internet Explorer only) + */ + var cleanup = function() { + if (ua.ie && ua.win) { + window.attachEvent("onunload", function() { + // remove listeners to avoid memory leaks + var ll = listenersArr.length; + for (var i = 0; i < ll; i++) { + listenersArr[i][0].detachEvent(listenersArr[i][1], listenersArr[i][2]); + } + // cleanup dynamically embedded objects to fix audio/video threads and force open sockets and NetConnections to disconnect + var il = objIdArr.length; + for (var j = 0; j < il; j++) { + removeSWF(objIdArr[j]); + } + // cleanup library's main closures to avoid memory leaks + for (var k in ua) { + ua[k] = null; + } + ua = null; + for (var l in swfobject) { + swfobject[l] = null; + } + swfobject = null; + }); + } + }(); + + return { + /* Public API + - Reference: http://code.google.com/p/swfobject/wiki/documentation + */ + registerObject: function(objectIdStr, swfVersionStr, xiSwfUrlStr, callbackFn) { + if (ua.w3 && objectIdStr && swfVersionStr) { + var regObj = {}; + regObj.id = objectIdStr; + regObj.swfVersion = swfVersionStr; + regObj.expressInstall = xiSwfUrlStr; + regObj.callbackFn = callbackFn; + regObjArr[regObjArr.length] = regObj; + setVisibility(objectIdStr, false); + } + else if (callbackFn) { + callbackFn({success:false, id:objectIdStr}); + } + }, + + getObjectById: function(objectIdStr) { + if (ua.w3) { + return getObjectById(objectIdStr); + } + }, + + embedSWF: function(swfUrlStr, replaceElemIdStr, widthStr, heightStr, swfVersionStr, xiSwfUrlStr, flashvarsObj, parObj, attObj, callbackFn) { + var callbackObj = {success:false, id:replaceElemIdStr}; + if (ua.w3 && !(ua.wk && ua.wk < 312) && swfUrlStr && replaceElemIdStr && widthStr && heightStr && swfVersionStr) { + setVisibility(replaceElemIdStr, false); + addDomLoadEvent(function() { + widthStr += ""; // auto-convert to string + heightStr += ""; + var att = {}; + if (attObj && typeof attObj === OBJECT) { + for (var i in attObj) { // copy object to avoid the use of references, because web authors often reuse attObj for multiple SWFs + att[i] = attObj[i]; + } + } + att.data = swfUrlStr; + att.width = widthStr; + att.height = heightStr; + var par = {}; + if (parObj && typeof parObj === OBJECT) { + for (var j in parObj) { // copy object to avoid the use of references, because web authors often reuse parObj for multiple SWFs + par[j] = parObj[j]; + } + } + if (flashvarsObj && typeof flashvarsObj === OBJECT) { + for (var k in flashvarsObj) { // copy object to avoid the use of references, because web authors often reuse flashvarsObj for multiple SWFs + if (typeof par.flashvars != UNDEF) { + par.flashvars += "&" + k + "=" + flashvarsObj[k]; + } + else { + par.flashvars = k + "=" + flashvarsObj[k]; + } + } + } + if (hasPlayerVersion(swfVersionStr)) { // create SWF + var obj = createSWF(att, par, replaceElemIdStr); + if (att.id == replaceElemIdStr) { + setVisibility(replaceElemIdStr, true); + } + callbackObj.success = true; + callbackObj.ref = obj; + } + else if (xiSwfUrlStr && canExpressInstall()) { // show Adobe Express Install + att.data = xiSwfUrlStr; + showExpressInstall(att, par, replaceElemIdStr, callbackFn); + return; + } + else { // show alternative content + setVisibility(replaceElemIdStr, true); + } + if (callbackFn) { callbackFn(callbackObj); } + }); + } + else if (callbackFn) { callbackFn(callbackObj); } + }, + + switchOffAutoHideShow: function() { + autoHideShow = false; + }, + + ua: ua, + + getFlashPlayerVersion: function() { + return { major:ua.pv[0], minor:ua.pv[1], release:ua.pv[2] }; + }, + + hasFlashPlayerVersion: hasPlayerVersion, + + createSWF: function(attObj, parObj, replaceElemIdStr) { + if (ua.w3) { + return createSWF(attObj, parObj, replaceElemIdStr); + } + else { + return undefined; + } + }, + + showExpressInstall: function(att, par, replaceElemIdStr, callbackFn) { + if (ua.w3 && canExpressInstall()) { + showExpressInstall(att, par, replaceElemIdStr, callbackFn); + } + }, + + removeSWF: function(objElemIdStr) { + if (ua.w3) { + removeSWF(objElemIdStr); + } + }, + + createCSS: function(selStr, declStr, mediaStr, newStyleBoolean) { + if (ua.w3) { + createCSS(selStr, declStr, mediaStr, newStyleBoolean); + } + }, + + addDomLoadEvent: addDomLoadEvent, + + addLoadEvent: addLoadEvent, + + getQueryParamValue: function(param) { + var q = doc.location.search || doc.location.hash; + if (q) { + if (/\?/.test(q)) { q = q.split("?")[1]; } // strip question mark + if (param == null) { + return urlEncodeIfNecessary(q); + } + var pairs = q.split("&"); + for (var i = 0; i < pairs.length; i++) { + if (pairs[i].substring(0, pairs[i].indexOf("=")) == param) { + return urlEncodeIfNecessary(pairs[i].substring((pairs[i].indexOf("=") + 1))); + } + } + } + return ""; + }, + + // For internal usage only + expressInstallCallback: function() { + if (isExpressInstallActive) { + var obj = getElementById(EXPRESS_INSTALL_ID); + if (obj && storedAltContent) { + obj.parentNode.replaceChild(storedAltContent, obj); + if (storedAltContentId) { + setVisibility(storedAltContentId, true); + if (ua.ie && ua.win) { storedAltContent.style.display = "block"; } + } + if (storedCallbackFn) { storedCallbackFn(storedCallbackObj); } + } + isExpressInstallActive = false; + } + } + }; +}(); diff --git a/qwebirc/config.py b/qwebirc/config.py index 4b0488c..0d56b51 100644 --- a/qwebirc/config.py +++ b/qwebirc/config.py @@ -135,6 +135,7 @@ def js_config(): 'atheme': atheme, 'frontend': f, 'ui': ui, + 'flash': flash, } return json.dumps(options) diff --git a/qwebirc/config_options.py b/qwebirc/config_options.py index 48deb25..bfaa273 100644 --- a/qwebirc/config_options.py +++ b/qwebirc/config_options.py @@ -7,6 +7,7 @@ "execution", "feedbackengine", "frontend", + "flash", "irc", "proxy", "tuneback", @@ -50,12 +51,15 @@ ("tuneback", "maxbuflen"), ("tuneback", "maxlinelen"), ("tuneback", "maxsubscriptions"), + ("flash", "port"), + ("flash", "xmlport"), ] lists = [ ("adminengine", "hosts"), ("execution", "syslog_addr"), ("proxy", "forwarded_for_ips"), + ("frontend", "connections"), ] strings = [ @@ -82,4 +86,5 @@ ("ui", "fg_color"), ("ui", "fg_sec_color"), ("ui", "bg_color"), + ("flash", "server"), ] diff --git a/swf/flashsocket.as b/swf/flashsocket.as new file mode 100644 index 0000000..15b818d --- /dev/null +++ b/swf/flashsocket.as @@ -0,0 +1,91 @@ +package +{ + import flash.utils.ByteArray; + import flash.display.MovieClip; + import flash.events.Event; + import flash.events.ErrorEvent; + import flash.events.IOErrorEvent; + import flash.events.SecurityErrorEvent; + import flash.events.ProgressEvent; + import flash.net.Socket; + import flash.system.Security; + import flash.external.ExternalInterface; + + public class JsSocket + { + private var id:int; + private var host:String; + private var port:int; + private var xmlport:int; + private var socket:Socket; + + private const CONNECTING:int = 0; + private const OPEN:int = 1; + private const CLOSING:int = 2; + private const CLOSED:int = 3; + private const ERROR:int = 4; + private const MESSAGE:int = 5; + + public function JsSocket():void { + socket = new Socket(); + socket.addEventListener(Event.CONNECT, onConnect); + socket.addEventListener(Event.CLOSE, onDisconnect); + socket.addEventListener(SecurityErrorEvent.SECURITY_ERROR, onError); + socket.addEventListener(IOErrorEvent.IO_ERROR, onError); + socket.addEventListener(ProgressEvent.SOCKET_DATA, onData); + } + + public function connect(host:String, port:int, xmlport):void { + Security.loadPolicyFile("xmlsocket://"+host+":"+xmlport); + socket.connect(host, port); + ExternalInterface.call("FlashSocket.state", CONNECTING, "CONNECTING"); + } + + public function disconnect():void { + socket.close(); + ExternalInterface.call("FlashSocket.state", CLOSED, ""); + } + + private function write(data:String):void { + socket.writeUTFBytes(data); + socket.flush(); + } + + private function onConnect(e:Event):void { + ExternalInterface.call("FlashSocket.state", OPEN, e.toString()); + } + + private function onDisconnect(e:Event):void { + ExternalInterface.call("FlashSocket.state", CLOSED, e.toString()); + } + + private function onError(e:ErrorEvent):void { + ExternalInterface.call("FlashSocket.state", ERROR, e.toString()); + socket.close(); + } + + private function onData(e:ProgressEvent):void { + var out:String = "["; + out += socket.readUnsignedByte(); + while(socket.bytesAvailable) + out += ","+socket.readUnsignedByte(); + out += "]"; + ExternalInterface.call("FlashSocket.state", MESSAGE, out); + } + + } + + public class Main extends MovieClip + { + private function Main():void { + var socket:JsSocket = new JsSocket(); + flash.system.Security.allowDomain("*"); + flash.system.Security.allowInsecureDomain("*"); + ExternalInterface.marshallExceptions = true; + + ExternalInterface.addCallback("connect", socket.connect); + ExternalInterface.addCallback("disconnect", socket.disconnect); + ExternalInterface.addCallback("write", socket.write); + } + } +} From 1f91263ac55e40e9a7e09d2d60f685f408e14865 Mon Sep 17 00:00:00 2001 From: Andrew Cook Date: Thu, 10 Apr 2014 23:23:16 +1000 Subject: [PATCH 7/7] Add WebSocket support Currently this uses protocol name "irc" and expects one irc message per websocket message with no terminating CR-LF. --- iris.conf.example | 11 +++++++++- js/irc/baseircclient.js | 1 + js/irc/wsconnection.js | 45 +++++++++++++++++++++++++++++++++++++++ qwebirc/config.py | 1 + qwebirc/config_options.py | 2 ++ 5 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 js/irc/wsconnection.js diff --git a/iris.conf.example b/iris.conf.example index be8baee..1231411 100644 --- a/iris.conf.example +++ b/iris.conf.example @@ -109,6 +109,15 @@ port: 6667 # XMLPORT: Port of IRC servers flash policy daemon xmlport: 8430 +# FRONTEND IRC CONNECTION OPTIONS +# These options provide the needed information for the frontend to connect to +# the IRC server via websocket +# They require a backend restart and a rerun of compile.py to update. +[websocket] + +# URL: URL of IRC server to connect to. +url: ws://irc.myserver.com/ + # ATHEME ENGINE OPTIONS # These options control communication with Atheme by the Atheme engine backend, @@ -238,7 +247,7 @@ dynamic_base_url: / # CONNECTIONS: What order to attempt methods of connection in # space seperated list of methods -# valid values: ajax flash +# valid values: ajax flash websocket connections: ajax diff --git a/js/irc/baseircclient.js b/js/irc/baseircclient.js index 527e07f..65711d6 100644 --- a/js/irc/baseircclient.js +++ b/js/irc/baseircclient.js @@ -39,6 +39,7 @@ qwebirc.irc.BaseIRCClient = new Class({ switch(conf.frontend.connections[x]) { case "ajax": this.connections.unshift(qwebirc.irc.IRCConnection); break; case "flash": this.connections.unshift(qwebirc.irc.FlashConnection); break; + case "websocket": this.connections.unshift(qwebirc.irc.WSConnection); break; } } diff --git a/js/irc/wsconnection.js b/js/irc/wsconnection.js new file mode 100644 index 0000000..1d3439b --- /dev/null +++ b/js/irc/wsconnection.js @@ -0,0 +1,45 @@ + +qwebirc.irc.WSConnection = new Class({ + Implements: [Events, Options], + options: { + initialNickname: "ircconnX", + url: "ws://irc.example.com/", + timeout: 45000, + maxRetries: 5, + serverPassword: null + }, + initialize: function(session, options) { + this.setOptions(options, conf.websocket); + }, + connect: function() { + if(!window.WebSocket) { + this.fireEvent("recv", [["disconnect", "No WebSocket Support"]]); + return; + } + this.socket = new WebSocket(this.options.url, "irc"); + this.socket.onopen = this.connected.bind(this); + this.socket.onclose = this.disconnected.bind(this); + this.socket.onmessage = this.recv.bind(this); + }, + connected: function(e) { + this.send("CAP LS"); + this.send("USER "+this.options.initialNickname+" 0 * :qwebirc"); + if(this.options.serverPassword) + this.send("PASS :"+this.options.serverPassword); + this.send("NICK "+this.options.initialNickname); + this.fireEvent("recv", [["connect"]]); + }, + disconnect: function() { + this.socket.close(); + }, + disconnected: function(e) { + this.fireEvent("recv", [["disconnect", e.reason ? e.reason : "Unknown reason"]]); + }, + send: function(data, synchronous) { + this.socket.send(String(data)); + return true; + }, + recv: function recv(message) { + this.fireEvent("recv", [["c", message.data]]); + } +}); diff --git a/qwebirc/config.py b/qwebirc/config.py index 0d56b51..b1664a9 100644 --- a/qwebirc/config.py +++ b/qwebirc/config.py @@ -136,6 +136,7 @@ def js_config(): 'frontend': f, 'ui': ui, 'flash': flash, + 'websocket': websocket, } return json.dumps(options) diff --git a/qwebirc/config_options.py b/qwebirc/config_options.py index bfaa273..7340972 100644 --- a/qwebirc/config_options.py +++ b/qwebirc/config_options.py @@ -8,6 +8,7 @@ "feedbackengine", "frontend", "flash", + "websocket", "irc", "proxy", "tuneback", @@ -87,4 +88,5 @@ ("ui", "fg_sec_color"), ("ui", "bg_color"), ("flash", "server"), + ("websocket", "url"), ]