diff options
Diffstat (limited to 'bouncer.py')
-rw-r--r-- | bouncer.py | 1550 |
1 files changed, 1006 insertions, 544 deletions
@@ -9,40 +9,67 @@ import string import hashlib import traceback import irc -from threading import Thread, Lock +import getpass +from threading import Thread +from threading import RLock as Lock import Queue +import chardet +import modjson + +dec = modjson.ModJSONDecoder() +enc = modjson.ModJSONEncoder(indent=3) + +# TODO: Rewrite this *entire* module and make more efficient. + +_listnumerics = dict(b=(367, 368, "channel ban list"), + e=(348, 349, "Channel Exception List"), + I=(346, 347, "Channel Invite Exception List"), + w=(910, 911, "Channel Access List"), + g=(941, 940, "chanel spamfilter list"), + X=(954, 953, "channel exemptchanops list")) + + +def BouncerReload(BNC): + networks, configs = zip(*BNC.conf.items()) + json = enc.encode([BNC, configs]) + if BNC.isAlive(): + BNC.stop() + newBNC, newconfs = dec.decode(json) + for network, newconf in zip(networks, newconfs): + network.rmAddon(BNC) + network.addAddon(**newconf) + return newBNC class Bouncer (Thread): - def __init__(self, addr="", port=16667, ssl=False, ipv6=False, certfile=None, keyfile=None, ignore=None, debug=False, log=sys.stderr, timeout=300, BNC=None, autoaway=None): - self.__name__ = "Bouncer for pyIRC" - self.__version__ = "1.1" - self.__author__ = "Brian Sherson" - self.__date__ = "December 1, 2013" + __name__ = "Bouncer for pyIRC" + __version__ = "2.0" + __author__ = "Brian Sherson" + __date__ = "February 21, 2014" + def __init__(self, addr="", port=16667, secure=False, ipv6=False, certfile=None, keyfile=None, ignore=None, debug=False, timeout=300, autoaway=None, servname="bouncer.site"): self.addr = addr self.port = port - self.servers = {} + self.conf = {} self.passwd = {} - self.socket = socket.socket( - socket.AF_INET6 if ipv6 else socket.AF_INET) - self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - self.ssl = ssl + self.socket = None + self.secure = secure self.ipv6 = ipv6 self.certfile = certfile self.keyfile = keyfile - self.socket.bind((self.addr, self.port)) - self.connections = [] + self.clients = [] self.ignore = ignore self.debug = debug self.timeout = timeout self.autoaway = autoaway - - ### Keep track of what extensions/connections are requesting WHO, WHOIS, and LIST, because we don't want to spam every bouncer connection with the server's replies. - ### In the future, MAY implement this idea in the irc module. - self.whoexpected = {} - self.whoisexpected = {} - self.listexpected = {} + self.servname = servname + self._stopexpected = False + + # Keep track of what extensions/clients are requesting WHO, WHOIS, and LIST, because we don't want to spam every bouncer connection with the server's replies. + # In the future, MAY implement this idea in the irc module. + self._whoexpected = {} + self._whoisexpected = {} + self._listexpected = {} self.lock = Lock() self.starttime = int(time.time()) Thread.__init__(self) @@ -50,22 +77,33 @@ class Bouncer (Thread): self.start() def __repr__(self): - return "<Bouncer listening on port %(addr)s:%(port)s>" % vars(self) + h = hash(self) + return "<Bouncer listening on {self.addr}:{self.port} at 0x{h:x}0>".format(**vars()) def run(self): + self.socket = socket.socket( + socket.AF_INET6 if self.ipv6 else socket.AF_INET) + self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self.socket.bind((self.addr, self.port)) self.socket.listen(5) #print ((self,"Now listening on port "+str(self.port))) while True: try: (connection, addr) = self.socket.accept() - if self.ssl: - connection = ssl.wrap_socket(connection, server_side=True, certfile=self.certfile, keyfile=self.keyfile, ssl_version=ssl.PROTOCOL_SSLv23) + if self.secure: + connection = ssl.wrap_socket( + connection, server_side=True, certfile=self.certfile, keyfile=self.keyfile, ssl_version=ssl.PROTOCOL_SSLv23) #print ((self,"New client connecting from %s:%s"%addr)) except socket.error: - #print "Shutting down Listener" + # print "Shutting down Listener" self.socket.close() - #raise + if not self._stopexpected: + raise sys.exit() + except: + tb = traceback.format_exc() + print >>sys.stderr, tb + continue connection.settimeout(self.timeout) bouncer = BouncerConnection( self, connection, addr, debug=self.debug) @@ -74,282 +112,333 @@ class Bouncer (Thread): self.socket.close() except: pass + self.socket = None + Thread.__init__(self) + self.daemon = True - def onAddonAdd(self, IRC, label, passwd, hashtype="md5"): - if IRC in [connection for (connection, passwd, hashtype) in self.servers.values()]: - return # Silently do nothing - if label in self.servers.keys(): - return - self.servers[label] = (IRC, passwd, hashtype) - self.whoexpected[IRC] = [] + def onAddonAdd(self, context, label, passwd=None, hashtype="sha512", ignore=None, autoaway=None, translations=[], hidden=[]): + for (context2, conf2) in self.conf.items(): + if context == context2: + raise ValueError, "Context already exists in config." + if label == conf2.label: + raise ValueError, "Unique label required." + if passwd == None: + while True: + passwd = getpass.getpass("Enter new password: ") + if passwd == getpass.getpass("Confirm new password: "): + break + print "Passwords do not match!" + passwd = hashlib.new(hashtype, passwd).hexdigest() + conf = irc.Config(self, label=label, passwd=passwd, hashtype=hashtype, ignore=ignore, autoaway=autoaway, translations=[ + (key if type(key) == irc.Channel else context[key], value) for key, value in translations], hidden=irc.ChanList(hidden, context=context)) + self.conf[context] = conf + self._whoexpected[context] = [] if self.debug: - IRC.logwrite("dbg [Bouncer.onAddonAdd] Clearing WHO expected list." % vars()) - self.whoisexpected[IRC] = [] - self.listexpected[IRC] = [] - - def onAddonRem(self, IRC): - for bouncerconnection in self.connections: - if bouncerconnection.IRC == IRC: - bouncerconnection.quit(quitmsg="Bouncer extension removed") - for (label, (connection, passwd, hashtype)) in self.servers.items(): - if connection == IRC: - del self.servers[label] - - def stop(self): + context.logwrite( + "dbg [Bouncer.onAddonAdd] Clearing WHO expected list." % vars()) + self._whoisexpected[context] = [] + self._listexpected[context] = [] + return conf + + def onAddonRem(self, context): + for client in self.clients: + if client.context == context: + client.quit(quitmsg="Bouncer extension removed") + del self.conf[context] + del self._whoexpected[context], self._whoisexpected[ + context], self._listexpected[context] + + def stop(self, disconnectall=False): + self._stopexpected = True self.socket.shutdown(0) + if disconnectall: + self.disconnectall() def disconnectall(self, quitmsg="Disconnecting all sessions"): - for bouncerconnection in self.connections: - bouncerconnection.stop(quitmsg=quitmsg) - - def onDisconnect(self, IRC, expected=False): - self.whoexpected[IRC] = [] - if self.debug: - IRC.logwrite("dbg [Bouncer.onDisconnect] Clearing WHO expected list.") - self.whoisexpected[IRC] = [] - self.listexpected[IRC] = [] - for bouncerconnection in self.connections: - if bouncerconnection.IRC == IRC: - bouncerconnection.quit(quitmsg="IRC connection lost") - - def onSendChanMsg(self, IRC, origin, channel, targetprefix, msg): - ### Called when bot sends a PRIVMSG to channel. - ### The variable origin refers to a class instance voluntarily identifying itself as that which requested data be sent. - for bouncerconnection in self.connections: - if IRC == bouncerconnection.IRC and origin != bouncerconnection: - bouncerconnection.send(":%s!%s@%s PRIVMSG %s%s :%s\n"%(IRC.identity.nick, IRC.identity.username, IRC.identity.host, targetprefix, channel.name, msg)) - - def onSendChanAction(self, IRC, origin, channel, targetprefix, action): - self.onSendChanMsg(IRC, origin, channel, targetprefix, - "\x01ACTION %s\x01"%action) - - def onSendChanNotice(self, IRC, origin, channel, targetprefix, msg): - ### Called when bot sends a NOTICE to channel. - ### The variable origin refers to a class instance voluntarily identifying itself as that which requested data be sent. - for bouncerconnection in self.connections: - if IRC == bouncerconnection.IRC and origin != bouncerconnection: - bouncerconnection.send(":%s!%s@%s NOTICE %s%s :%s\n"%(IRC.identity.nick, IRC.identity.username, IRC.identity.host, targetprefix, channel.name, msg)) - - def onSend(self, IRC, origin, line, cmd, target, params, extinfo): + for client in self.clients: + client.quit(quitmsg=quitmsg) + + def onDisconnect(self, context, expected=False): + self._whoexpected[context] = [] + self._whoisexpected[context] = [] + self._listexpected[context] = [] + if context.identity: + for channel in context.identity.channels: + self.broadcast(context, origin=context.identity, cmd="PART", target=channel, extinfo="Bouncer Connection Lost", clients=[ + client for client in self.clients if channel not in client.hidden]) + self.broadcast(context, origin=context.identity, + cmd="QUIT", extinfo="Bouncer Connection Lost") + self.broadcast( + context, origin=self.servname, cmd="NOTICE", target=context.identity, + extinfo=":Connection to %s:%s has been lost." % (context.server, context.port)) + + def onQuit(self, context, user, quitmsg): + # For some odd reason, certain networks (*cough*Freenode*cough*) will send a quit message for the user, causing context.identity.channels to be cleared + # before onDisconnect can be executed. This is the remedy. + if user == context.identity: + for channel in context.identity.channels: + self.broadcast(context, origin=user, cmd="PART", target=channel, extinfo="Bouncer Connection Lost", clients=[ + client for client in self.clients if channel not in client.hidden]) + self.broadcast(context, origin=user, cmd="QUIT", extinfo=quitmsg, clients=[ + client for client in self.clients if any([user in channel for channel in context.channels if channel not in client.hidden])]) + + def onConnectAttempt(self, context): + self.broadcast( + context, origin=self.servname, cmd="NOTICE", target=context.identity, + extinfo="Attempting connection to %s:%s." % (context.server, context.port)) + + def onConnect(self, context): + self.broadcast( + context, origin=self.servname, cmd="NOTICE", target=context.identity, + extinfo="Connection to %s:%s established." % (context.server, context.port)) + + def onMeNickChange(self, context, newnick): + for client in self.clients: + if client.context == context: + client.send( + origin=context.identity, cmd="NICK", target=newnick) + client.nick = newnick + + def onNickChange(self, context, user, newnick): + self.broadcast(context, origin=user, cmd="NICK", target=newnick, clients=[ + client for client in self.clients if any([user in channel for channel in context.channels if channel not in client.hidden])]) + + def onRegistered(self, context): + for client in self.clients: + if client.context == context: + if client.nick != context.identity.nick: + client.send(origin="%s!%s@%s" % + (client.nick, client.username, client.host), cmd="NICK", target=context.identity.nick) + client.nick = context.identity.nick + + def onConnectFail(self, context, exc, excmsg, tb): + for client in self.clients: + if client.context == context: + client.send( + origin=self.servname, cmd="NOTICE", target=client.nick, + extinfo="Connection to %s:%s failed: %s." % (context.server, context.port, excmsg)) + + def onSendChanMsg(self, context, origin, channel, targetprefix, msg): + # Called when bot sends a PRIVMSG to channel. + # The variable origin refers to a class instance voluntarily + # identifying itself as that which requested data be sent. + self.broadcast( + context, origin=context.identity, cmd="PRIVMSG", targetprefix=targetprefix, + target=channel, extinfo=msg, clients=[client for client in self.clients if client != origin]) + + def onSendChanAction(self, context, origin, channel, targetprefix, action): + self.onSendChanMsg( + context, origin, channel, targetprefix, u"\x01ACTION {action}\x01".format(**vars())) + + def onSendChanNotice(self, context, origin, channel, targetprefix, msg): + # Called when bot sends a NOTICE to channel. + # The variable origin refers to a class instance voluntarily + # identifying itself as that which requested data be sent. + self.broadcast( + context, origin=context.identity, cmd="NOTICE", targetprefix=targetprefix, + target=channel, extinfo=msg, clients=[client for client in self.clients if client != origin]) + + def onSend(self, context, origin, line, cmd, target, targetprefix, params, extinfo): if cmd.upper() == "WHO": - self.whoexpected[IRC].append(origin) + self._whoexpected[context].append(origin) if self.debug: - with IRC.loglock: - timestamp = irc.timestamp() - if issubclass(type(origin), Thread): - name = origin.name - print >>IRC.log, "%(timestamp)s dbg [Bouncer.onSend] Adding %(origin)s (%(name)s) to WHO expected list." % vars() - else: - print >>IRC.log, "%(timestamp)s dbg [Bouncer.onSend] Adding %(origin)s to WHO expected list." % vars() - for obj in self.whoexpected[IRC]: - if issubclass(type(obj), Thread): - name = obj.name - print >>IRC.log, "%(timestamp)s dbg [Bouncer.onSend] WHO expected: %(obj)s (%(name)s)" % vars() - else: - print >>IRC.log, "%(timestamp)s dbg [Bouncer.onSend] WHO expected: %(obj)s" % vars() - IRC.log.flush() + if issubclass(type(origin), Thread): + name = origin.name + context.logwrite( + "dbg [Bouncer.onSend] Adding {origin} ({name}) to WHO expected list.".format(**vars())) + else: + context.logwrite( + "dbg [Bouncer.onSend] Adding %(origin)s to WHO expected list." % vars()) + context.logwrite( + "dbg [Bouncer.onSend] WHO expected list size: %d" % len(self._whoexpected[context])) elif cmd.upper() == "WHOIS": - self.whoisexpected[IRC].append(origin) + self._whoisexpected[context].append(origin) elif cmd.upper() == "LIST": - self.listexpected[IRC].append(origin) - - def onWhoEntry(self, IRC, origin, channel, user, channame, username, host, serv, nick, flags, hops, realname): - ### Called when a WHO list is received. - if len(self.whoexpected[IRC]) and self.whoexpected[IRC][0] in self.connections: - bncconnection = self.whoexpected[IRC][0] - try: - bncconnection.send(":%s 352 %s %s %s %s %s %s %s :%s %s\n"%(origin, IRC.identity.nick, channame, username, host, serv, nick, flags, hops, realname)) - except socket.error: - pass - - def onWhoEnd(self, IRC, origin, param, endmsg): - ### Called when a WHO list is received. - if len(self.whoexpected[IRC]) and self.whoexpected[IRC][0] in self.connections: - bncconnection = self.whoexpected[IRC][0] - try: - bncconnection.send(":%s 315 %s %s :%s\n"%( - origin, IRC.identity.nick, param, endmsg)) - except socket.error: - pass - finally: - del self.whoexpected[IRC][0] - - def onWho(self, IRC, params, wholist, endmsg): - ### Called when a WHO list is received. - if len(self.whoexpected[IRC]): - try: - if self.whoexpected[IRC][0] in self.connections: - bncconnection = self.whoexpected[IRC][0] - lines = [":%s 352 %s %s %s %s %s %s %s :%s %s\n"%(IRC.serv, IRC.identity.nick, channame, username, host, serv, nick, flags, hops, realname) for (channel, user, channame, username, host, serv, nick, flags, hops, realname) in wholist]+[":%s 315 %s %s :%s\n"%(IRC.serv, IRC.identity.nick, params, endmsg)] - bncconnection.send("".join(lines)) - finally: - del self.whoexpected[IRC][0] - - def onListStart(self, IRC, origin, params, extinfo): - ### Called when a WHO list is received. - if len(self.listexpected[IRC]) and self.listexpected[IRC][0] in self.connections: - bncconnection = self.listexpected[IRC][0] - try: - bncconnection.send(":%s 321 %s %s :%s\n"%(origin, - IRC.identity.nick, params, extinfo)) - except socket.error: - pass - - def onListEntry(self, IRC, origin, channel, population, extinfo): - ### Called when a WHO list is received. - if len(self.listexpected[IRC]) and self.listexpected[IRC][0] in self.connections: - bncconnection = self.listexpected[IRC][0] - try: - bncconnection.send(":%s 322 %s %s %d :%s\n"%(origin, IRC.identity.nick, channel.name, population, extinfo)) - except socket.error: - pass - - def onListEnd(self, IRC, origin, endmsg): - ### Called when a WHO list is received. - if len(self.listexpected[IRC]) and self.listexpected[IRC][0] in self.connections: - bncconnection = self.listexpected[IRC][0] - try: - bncconnection.send(":%s 323 %s :%s\n"%( - origin, IRC.identity.nick, endmsg)) - except socket.error: - pass - finally: - del self.listexpected[IRC][0] - - def onWhoisStart(self, IRC, origin, user, nickname, username, host, realname): - ### Called when a WHOIS reply is received. - if len(self.whoisexpected[IRC]): - if self.whoisexpected[IRC][0] in self.connections: - bncconnection = self.whoisexpected[IRC][0] - try: - bncconnection.send(":%s 311 %s %s %s %s * :%s\n" % (origin, IRC.identity.nick, nickname, username, host, realname)) - except socket.error: - pass - - def onWhoisRegisteredNick(self, IRC, origin, user, nickname, msg): - ### Called when a WHOIS reply is received. - if len(self.whoisexpected[IRC]) and self.whoisexpected[IRC][0] in self.connections: - bncconnection = self.whoisexpected[IRC][0] - try: - bncconnection.send(":%s 307 %s %s :%s\n" % ( - origin, IRC.identity.nick, nickname, msg)) - except socket.error: - pass - - def onWhoisConnectingFrom(self, IRC, origin, user, nickname, msg): - ### Called when a WHOIS reply is received. - if len(self.whoisexpected[IRC]) and self.whoisexpected[IRC][0] in self.connections: - bncconnection = self.whoisexpected[IRC][0] - try: - bncconnection.send(":%s 378 %s %s :%s\n" % ( - origin, IRC.identity.nick, nickname, msg)) - except socket.error: - pass - - def onWhoisChannels(self, IRC, origin, user, nickname, chanlist): - ### Called when a WHOIS reply is received. - if len(self.whoisexpected[IRC]) and self.whoisexpected[IRC][0] in self.connections: - bncconnection = self.whoisexpected[IRC][0] - try: - bncconnection.send(":%s 319 %s %s :%s\n" % (origin, IRC.identity.nick, nickname, " ".join(chanlist))) - except socket.error: - pass - - def onWhoisAvailability(self, IRC, origin, user, nickname, msg): - ### Called when a WHOIS reply is received. - if len(self.whoisexpected[IRC]) and self.whoisexpected[IRC][0] in self.connections: - bncconnection = self.whoisexpected[IRC][0] - try: - bncconnection.send(":%s 310 %s %s :%s\n" % ( - origin, IRC.identity.nick, nickname, msg)) - except socket.error: - pass - - def onWhoisServer(self, IRC, origin, user, nickname, server, servername): - ### Called when a WHOIS reply is received. - if len(self.whoisexpected[IRC]) and self.whoisexpected[IRC][0] in self.connections: - bncconnection = self.whoisexpected[IRC][0] - try: - bncconnection.send(":%s 312 %s %s %s :%s\n" % (origin, IRC.identity.nick, nickname, server, servername)) - except socket.error: - pass - - def onWhoisOp(self, IRC, origin, user, nickname, msg): - if len(self.whoisexpected[IRC]) and self.whoisexpected[IRC][0] in self.connections: - bncconnection = self.whoisexpected[IRC][0] - try: - bncconnection.send(":%s 313 %s %s :%s\n" % ( - origin, IRC.identity.nick, nickname, msg)) - except socket.error: - pass - - def onWhoisAway(self, IRC, origin, user, nickname, awaymsg): - if len(self.whoisexpected[IRC]) and self.whoisexpected[IRC][0] in self.connections: - bncconnection = self.whoisexpected[IRC][0] - try: - bncconnection.send(":%s 301 %s %s :%s\n" % (origin, - IRC.identity.nick, nickname, awaymsg)) - except socket.error: - pass - - def onWhoisTimes(self, IRC, origin, user, nickname, idletime, signontime, msg): - if len(self.whoisexpected[IRC]) and self.whoisexpected[IRC][0] in self.connections: - bncconnection = self.whoisexpected[IRC][0] - try: - bncconnection.send(":%s 317 %s %s %d %d :%s\n" % (origin, IRC.identity.nick, nickname, idletime, signontime, msg)) - except socket.error: - pass - - def onWhoisSSL(self, IRC, origin, user, nickname, msg): - if len(self.whoisexpected[IRC]) and self.whoisexpected[IRC][0] in self.connections: - bncconnection = self.whoisexpected[IRC][0] - try: - bncconnection.send(":%s 671 %s %s :%s\n" % ( - origin, IRC.identity.nick, nickname, msg)) - except socket.error: - pass - - def onWhoisModes(self, IRC, origin, user, nickname, msg): - if len(self.whoisexpected[IRC]) and self.whoisexpected[IRC][0] in self.connections: - bncconnection = self.whoisexpected[IRC][0] - try: - bncconnection.send(":%s 339 %s %s :%s\n" % ( - origin, IRC.identity.nick, nickname, msg)) - except socket.error: - pass - - def onWhoisLoggedInAs(self, IRC, origin, user, nickname, loggedinas, msg): - if len(self.whoisexpected[IRC]) and self.whoisexpected[IRC][0] in self.connections: - bncconnection = self.whoisexpected[IRC][0] - try: - bncconnection.send(":%s 330 %s %s %s :%s\n" % (origin, IRC.identity.nick, nickname, loggedinas, msg)) - except socket.error: - pass - - def onWhoisEnd(self, IRC, origin, user, nickname, msg): - if len(self.whoisexpected[IRC]) and self.whoisexpected[IRC][0] in self.connections: - bncconnection = self.whoisexpected[IRC][0] - try: - bncconnection.send(":%s 318 %s %s :%s\n" % ( - origin, IRC.identity.nick, nickname, msg)) - except socket.error: - pass - finally: - del self.whoisexpected[IRC][0] - - def onUnhandled(self, IRC, line, origin, cmd, target, params, extinfo): - for bouncerconnection in self.connections: - if bouncerconnection.IRC == IRC: - bouncerconnection.send("%s\n"%line) + self._listexpected[context].append(origin) + + def onWhoEntry(self, context, origin, channel, user, channame, username, host, serv, nick, flags, hops, realname): + # Called when a WHO list is received. + if len(self._whoexpected[context]): + client = self._whoexpected[context][0] + if client in self.clients: + client.send(origin=origin, cmd=352, target=context.identity, params=u"{channame} {username} {host} {serv} {nick} {flags}".format( + **vars()), extinfo=u"{hops} {realname}".format(**vars())) + # client.send(":%s 352 %s %s %s %s %s %s %s :%s %s\n"%(origin, context.identity.nick, channame, username, host, serv, nick, flags, hops, realname)) + + def onWhoEnd(self, context, origin, param, endmsg): + # Called when a WHO list is received. + if len(self._whoexpected[context]) and self._whoexpected[context][0] in self.clients: + client = self._whoexpected[context][0] + client.send( + origin=origin, cmd=315, target=context.identity, params=param, extinfo=endmsg) + #client.send(":%s 315 %s %s :%s\n"%(origin, context.identity.nick, param, endmsg)) + if self.debug: + if issubclass(type(self._whoexpected[context][0]), Thread): + name = self._whoexpected[context][0].name + context.logwrite( + "dbg [Bouncer.onWhoEnd] Removing %s (%s) from WHO expected list." % + (self._whoexpected[context][0], name)) + else: + context.logwrite( + "dbg [Bouncer.onWhoEnd] Removing %s from WHO expected list." % self._whoexpected[context][0]) + del self._whoexpected[context][0] + if self.debug: + context.logwrite( + "dbg [Bouncer.onWhoEnd] WHO expected list size: %d" % + len(self._whoexpected[context])) + + def onListStart(self, context, origin, params, extinfo): + # Called when a WHO list is received. + if len(self._listexpected[context]) and self._listexpected[context][0] in self.clients: + client = self._listexpected[context][0] + client.send(origin=origin, cmd=321, + target=context.identity, params=params, extinfo=extinfo) + #client.send(":%s 321 %s %s :%s\n"%(origin, context.identity.nick, params, extinfo)) + + def onListEntry(self, context, origin, channel, population, extinfo): + # Called when a WHO list is received. + if len(self._listexpected[context]) and self._listexpected[context][0] in self.clients: + client = self._listexpected[context][0] + client.send(origin=origin, cmd=322, target=context.identity, + params=u"{channel.name} {population}".format(**vars()), extinfo=extinfo) + # client.send(":%s 322 %s %s %d :%s\n"%(origin, context.identity.nick, channame, population, extinfo)) + + def onListEnd(self, context, origin, endmsg): + # Called when a WHO list is received. + if len(self._listexpected[context]) and self._listexpected[context][0] in self.clients: + client = self._listexpected[context][0] + client.send( + origin=origin, cmd=323, target=context.identity, extinfo=endmsg) + # client.send(":%s 323 %s :%s\n"%(origin, context.identity.nick, endmsg)) + del self._listexpected[context][0] + + def onWhoisStart(self, context, origin, user, nickname, username, host, realname): + # Called when a WHOIS reply is received. + if len(self._whoisexpected[context]) and self._whoisexpected[context][0] in self.clients: + client = self._whoisexpected[context][0] + client.send(origin=origin, cmd=311, target=context.identity, + params=u"{nickname} {username} {host} *".format(**vars()), extinfo=realname) + # client.send(":%s 311 %s %s %s %s * :%s\n" % (origin, context.identity.nick, nickname, username, host, realname)) + + def onWhoisRegisteredNick(self, context, origin, user, nickname, msg): + # Called when a WHOIS reply is received. + if len(self._whoisexpected[context]) and self._whoisexpected[context][0] in self.clients: + client = self._whoisexpected[context][0] + client.send( + origin=origin, cmd=307, target=context.identity, params=nickname, extinfo=msg) + # client.send(":%s 307 %s %s :%s\n" % (origin, context.identity.nick, nickname, msg)) + + def onWhoisConnectingFrom(self, context, origin, user, nickname, msg): + # Called when a WHOIS reply is received. + if len(self._whoisexpected[context]) and self._whoisexpected[context][0] in self.clients: + client = self._whoisexpected[context][0] + client.send(origin=origin, cmd=378, + target=context.identity, params=nickname, extinfo=msg) + # client.send(":%s 378 %s %s :%s\n" % (origin, context.identity.nick, nickname, msg)) + + def onWhoisChannels(self, context, origin, user, nickname, chanlist): + # Called when a WHOIS reply is received. + # TODO: Translations implementation + if len(self._whoisexpected[context]) and self._whoisexpected[context][0] in self.clients: + client = self._whoisexpected[context][0] + client.send(origin=origin, cmd=319, target=context.identity, + params=nickname, extinfo=" ".join(chanlist)) + # client.send(":%s 319 %s %s :%s\n" % (origin, context.identity.nick, nickname, " ".join(chanlist))) + + def onWhoisAvailability(self, context, origin, user, nickname, msg): + # Called when a WHOIS reply is received. + if len(self._whoisexpected[context]) and self._whoisexpected[context][0] in self.clients: + client = self._whoisexpected[context][0] + client.send( + origin=origin, cmd=310, target=context.identity, params=nickname, extinfo=msg) + # client.send(":%s 310 %s %s :%s\n" % (origin, context.identity.nick, nickname, msg)) + + def onWhoisServer(self, context, origin, user, nickname, server, servername): + # Called when a WHOIS reply is received. + if len(self._whoisexpected[context]) and self._whoisexpected[context][0] in self.clients: + client = self._whoisexpected[context][0] + client.send(origin=origin, cmd=312, target=context.identity, + params=u"{nickname} {server}".format(**vars()), extinfo=servername) + # client.send(":%s 312 %s %s %s :%s\n" % (origin, context.identity.nick, nickname, server, servername)) + + def onWhoisOp(self, context, origin, user, nickname, msg): + if len(self._whoisexpected[context]) and self._whoisexpected[context][0] in self.clients: + client = self._whoisexpected[context][0] + client.send( + origin=origin, cmd=313, target=context.identity, params=nickname, extinfo=msg) + # client.send(":%s 313 %s %s :%s\n" % (origin, context.identity.nick, nickname, msg)) + + def onWhoisAway(self, context, origin, user, nickname, awaymsg): + if len(self._whoisexpected[context]) and self._whoisexpected[context][0] in self.clients: + client = self._whoisexpected[context][0] + client.send(origin=origin, cmd=301, target=context.identity, + params=u"{nickname} {idletime} {signontime}".format(**vars()), extinfo=awaymsg) + # client.send(":%s 301 %s %s :%s\n" % (origin, context.identity.nick, nickname, awaymsg)) + + def onWhoisTimes(self, context, origin, user, nickname, idletime, signontime, msg): + if len(self._whoisexpected[context]) and self._whoisexpected[context][0] in self.clients: + client = self._whoisexpected[context][0] + client.send(origin=origin, cmd=317, target=context.identity, + params=u"{nickname} {idletime} {signontime}".format(**vars()), extinfo=msg) + # client.send(":%s 317 %s %s %d %d :%s\n" % (origin, context.identity.nick, nickname, idletime, signontime, msg)) + + def onWhoisSSL(self, context, origin, user, nickname, msg): + if len(self._whoisexpected[context]) and self._whoisexpected[context][0] in self.clients: + client = self._whoisexpected[context][0] + client.send(origin=origin, cmd=671, + target=context.identity, params=nickname, extinfo=msg) + # client.send(":%s 671 %s %s :%s\n" % (origin, context.identity.nick, nickname, msg)) + + def onWhoisModes(self, context, origin, user, nickname, msg): + if len(self._whoisexpected[context]) and self._whoisexpected[context][0] in self.clients: + client = self._whoisexpected[context][0] + client.send( + origin=origin, cmd=339, target=context.identity, params=nickname, extinfo=msg) + # ":%s 339 %s %s :%s\n" % (origin, context.identity.nick, nickname, msg)) + + def onWhoisLoggedInAs(self, context, origin, user, nickname, loggedinas, msg): + if len(self._whoisexpected[context]) and self._whoisexpected[context][0] in self.clients: + client = self._whoisexpected[context][0] + client.send(origin=origin, cmd=330, target=context.identity, + params=" ".join((nickname, loggedinas)), extinfo=msg) + # ":%s 330 %s %s %s :%s\n" % (origin, context.identity.nick, nickname, loggedinas, msg)) + + def onWhoisEnd(self, context, origin, user, nickname, msg): + if len(self._whoisexpected[context]) and self._whoisexpected[context][0] in self.clients: + client = self._whoisexpected[context][0] + client.send(origin=origin, cmd=318, + target=context.identity, params=nickname, extinfo=msg) + # ":%s 318 %s %s :%s\n" % (origin, context.identity.nick, nickname, msg) + del self._whoisexpected[context][0] + + def onJoin(self, context, user, channel): + self.broadcast(context, origin=user, cmd="JOIN", target=channel, clients=[ + client for client in self.clients if channel not in client.hidden]) + + def onOther(self, context, line, origin, cmd, target, params, extinfo, targetprefix): + conf = self.conf[context] + self.broadcast( + context, origin=origin, cmd=cmd, target=target, params=params, extinfo=extinfo, + targetprefix=targetprefix, clients=[client for client in self.clients if target not in client.hidden]) + + def broadcast(self, context, origin=None, cmd=None, target=None, params=None, extinfo=None, targetprefix=None, clients=None): + if clients == None: + clients = self.clients + for client in clients: + with client.lock: + if client.context == context and not client.quitting: + client.send( + origin, cmd, target, params, extinfo, targetprefix) class BouncerConnection (Thread): + def __init__(self, bouncer, connection, addr, debug=False): - #print "Initializing ListenThread..." + # print "Initializing ListenThread..." self.bouncer = bouncer self.connection = connection self.host, self.port = self.addr = addr[:2] - self.IRC = None + self.context = None self.pwd = None self.nick = None self.label = None @@ -360,59 +449,286 @@ class BouncerConnection (Thread): self.lock = Lock() self.quitmsg = "Connection Closed" self.quitting = False + self.hidden = irc.ChanList() + self.translations = {} + self.namesx = False + self.uhnames = False Thread.__init__(self) self.daemon = True self.start() - def send(self, data, flags=0): - try: - with self.lock: - self.connection.send(data) - except socket.error: - exc, excmsg, tb = sys.exc_info() - print >>self.IRC.logwrite(*["!!! [BouncerConnection.send] Exception in thread %(self)s" % vars()]+["!!! [BouncerConnection.send] %(tbline)s" % vars() for tbline in traceback.format_exc().split("\n")]) - self.quit(quitmsg=excmsg.message) - - def __repr__(self): - server = self.IRC.server if self.IRC else "*" - port = self.IRC.port if self.IRC else "*" - if self.IRC and self.IRC.identity: - nick = self.IRC.identity.nick - ident = self.IRC.identity.username if self.IRC.identity.username else "*" - host = self.IRC.identity.host if self.IRC.identity.host else "*" - else: - nick = "*" - ident = "*" - host = "*" - if self.IRC.ssl and self.IRC.ipv6: - protocol = "ircs6" - elif self.IRC.ssl: - protocol = "ircs" - elif self.IRC.ipv6: - protocol = "irc6" + def sendstr(self, data, flags=0): + with self.lock: + try: + self.connection.send(data.encode("utf8")) + except socket.error: + exc, excmsg, tb = sys.exc_info() + print >>self.context.logwrite(*["!!! [BouncerConnection.send] Exception in thread %(self)s" % vars()] + [ + "!!! [BouncerConnection.send] %(tbline)s" % vars() for tbline in traceback.format_exc().split("\n")]) + self.quit(quitmsg=excmsg.message) + + # Format and send a string to the client + def send(self, origin=None, cmd=None, target=None, params=None, extinfo=None, targetprefix=None, flags=0): + if type(target) == irc.Channel: + if targetprefix == None: + targetprefix = "" + # if target in self.translations.keys(): + # target=targetprefix+self.translations[target] + # else: + # target=targetprefix+target.name + target = targetprefix + target.name + elif type(target) == irc.User: + target = target.nick + + if type(cmd) == int: + cmd = "%03d" % cmd + + # translated=[] + # if params: + # for param in params.split(" "): + #chantypes=self.context.supports.get("CHANTYPES", irc._defaultchantypes) + # if re.match(irc._chanmatch % re.escape(chantypes), param) and self.context[param] in self.translations.keys(): + # translated.append(self.translations[self.context[param]]) + # else: + # translated.append(param) + #params=" ".join(translated) + + if params: + line = u"{cmd} {target} {params}".format(**vars()) + elif target: + line = u"{cmd} {target}".format(**vars()) else: - protocol = "irc" - addr = self.host - return "<Bouncer connection from %(addr)s to %(nick)s!%(ident)s@%(host)s on %(protocol)s://%(server)s:%(port)s>" % locals() + line = cmd + + if extinfo != None: + line = u"{line} :{extinfo}".format(**vars()) + + if type(origin) == irc.User: + line = u":{origin:full} {line}".format(**vars()) + elif origin: + line = u":{origin} {line}".format(**vars()) + self.sendstr(u"{line}\n".format(**vars())) + + #server=self.context.server if self.context else "*" + #port=self.context.port if self.context else "*" + # if self.context and self.context.identity: + # nick=self.context.identity.nick + #ident=self.context.identity.username if self.context.identity.username else "*" + #host=self.context.identity.host if self.context.identity.host else "*" + # else: + # nick="*" + # ident="*" + # host="*" + # if self.context.ssl and self.context.ipv6: + # protocol="ircs6" + # elif self.context.ssl: + # protocol="ircs" + # elif self.context.ipv6: + # protocol="irc6" + # else: + # protocol="irc" + # addr=self.host + def __repr__(self): + return "<Bouncer connection from {self.host} to {self.context.identity} on {self.context:uri}>".format(**vars()) def quit(self, quitmsg="Disconnected"): - if not self.quitting: - self.quitmsg = quitmsg - with self.lock: + with self.lock: + if not self.quitting: + self.quitmsg = quitmsg try: - self.connection.send("ERROR :Closing link: (%s@%s) [%s]\n" % (self.IRC.identity.nick if self.IRC else "*", self.host, quitmsg)) + self.send(cmd="ERROR", extinfo="Closing link: (%s@%s) [%s]\n" % ( + self.context.identity.nick if self.context else "*", self.host, quitmsg)) except: pass try: - self.connection.shutdown(1) + self.connection.shutdown(socket.SHUT_WR) self.connection.close() except: pass self.quitting = True + def showchannel(self, channel): + with self.context.lock, self.lock: + if channel in self.hidden: + self.hidden.remove(channel) + if self.context.identity in channel.users: + self.send( + origin=self.context.identity, cmd="JOIN", target=channel) + self.sendchanneltopic(channel) + self.sendchannelnames(channel) + + def sendchanneltopic(self, channel): + with self.context.lock, self.lock: + if channel.topic and channel.topictime: + self.send(origin=self.bouncer.servname, cmd=332, + target=self.context.identity, params=channel.name, extinfo=channel.topic) + # u":{self.context.serv} 332 {self.context.identity.nick} {self.name} :{self.topic}".format(**vars()) + self.send( + origin=self.bouncer.servname, cmd=333, target=self.context.identity, + params="{channel.name} {channel.topicsetby} {channel.topictime}".format(**vars())) + # u":{self.context.serv} 333 {self.context.identity.nick} {self.name} {self.topicsetby.nick} {self.topictime}".format(**vars()) + else: + self.send(origin=self.bouncer.servname, cmd=331, + target=self.context.identity, params=channel.name, extinfo="No topic is set") + # u":{self.context.serv} 331 {self.context.identity.nick} + # {self.name} :No topic is set".format(**vars())] + + def sendchannelnames(self, channel): + with self.context.lock, self.lock: + secret = "s" in channel.modes.keys() and channel.modes["s"] + private = "p" in channel.modes.keys() and channel.modes["p"] + flag = "@" if secret else ("*" if private else "=") + + modes, symbols = supports = self.context.supports.get( + "PREFIX", irc._defaultprefix) + users = list(channel.users) + users.sort(key=lambda user: ([user not in channel.modes.get(mode, []) + for mode, char in zip(*supports)], user.nick.lower())) + if self.uhnames: + template = u"{prefixes}{user:full}" + else: + template = u"{prefixes}{user}" + + nameslist = [] + for user in users: + prefixes = u"".join( + [prefix if mode in channel.modes.keys() and user in channel.modes[mode] else "" for prefix, mode in zip(symbols, modes)]) + if not self.namesx: + prefixes = prefixes[:1] + nameslist.append(template.format(**vars())) + names = " ".join(nameslist) + + lines = [] + while len(names) > 196: + index = names.rfind(" ", 0, 196) + slice = names[:index] + self.send( + origin=self.bouncer.servname, cmd=353, target=self.context.identity, + params="{flag} {channel.name}".format(**vars()), extinfo=slice) + #u":{channel.context.serv} 353 {channel.context.identity.nick} {flag} {channel.name} :{slice}".format(**vars()) + names = names[index + 1:] + if len(names): + self.send( + origin=self.bouncer.servname, cmd=353, target=self.context.identity, + params="{flag} {channel.name}".format(**vars()), extinfo=names) + #u":{channel.context.serv} 353 {channel.context.identity.nick} {flag} {channel.name} :{names}".format(**vars()) + + self.send( + origin=self.bouncer.servname, cmd=366, target=self.context.identity, + params=channel.name, extinfo="End of /NAMES list.") + # u":{channel.context.serv} 366 {channel.context.identity.nick} {channel.name} :End of /NAMES list.".format(**vars()) + + def sendchannelmodes(self, channel, modechars=None): + with self.context.lock, self.lock: + if modechars: + for mode in modechars: + if mode not in _listnumerics.keys(): + continue + i, e, l = _listnumerics[mode] + if mode in channel.modes.keys(): + for (mask, setby, settime) in channel.modes[mode]: + self.send( + origin=self.bouncer.servname, cmd=i, target=self.context.identity, + params=u"{channel.name} {mask} {setby} {settime}".format(**vars())) + self.send(origin=self.bouncer.servname, cmd=e, + target=self.context.identity, params=u"{channel.name} {l}".format(**vars())) + else: + items = channel.modes.items() + chanmodes = self.context.supports.get( + "CHANMODES", irc._defaultchanmodes) + prefix = self.context.supports.get( + "PREFIX", irc._defaultprefix) + modes = "".join( + [mode for (mode, val) in items if mode not in chanmodes[0] + prefix[0] and val]) + params = " ".join( + [val for (mode, val) in items if mode in chanmodes[1] + chanmodes[2] and val]) + if modes and params: + self.send( + origin=self.bouncer.servname, cmd=324, target=self.context.identity, + params="{channel.name} +{modes} {params}".format(**vars())) + # u":{channel.context.identity.server} 324 {channel.context.identity.nick} {channel.name} +{modes} {params}".format(**vars()) + elif modes: + self.send( + origin=self.bouncer.servname, cmd=324, target=self.context.identity, + params="{channel.name} +{modes}".format(**vars())) + # u":{channel.context.identity.server} 324 {channel.context.identity.nick} {channel.name} +{modes}".format(**vars()) + + def sendsupports(self): + with self.context.lock, self.lock: + supports = [ + "CHANMODES=%s" % (",".join(value)) if name == "CHANMODES" else "PREFIX=(%s)%s" % + value if name == "PREFIX" else "%s=%s" % (name, value) if value else name for name, value in self.context.supports.items()] + if "UHNAMES" not in supports: + supports.append("UHNAMES") + if "NAMESX" not in supports: + supports.append("NAMESX") + supports.sort() + supports = " ".join(supports) + lines = [] + while len(supports) > 196: + index = supports.rfind(" ", 0, 196) + slice = supports[:index] + self.send( + origin=self.bouncer.servname, cmd=5, target=self.context.identity, + params=slice, extinfo="are supported by this server") + # u":{self.context.serv} 005 {self.context.identity.nick} {slice} :are supported by this server".format(**vars()) + supports = supports[index + 1:] + if supports: + self.send( + origin=self.bouncer.servname, cmd=5, target=self.context.identity, + params=supports, extinfo="are supported by this server") + # u":{self.context.serv} 005 {self.context.identity.nick} {supports} :are supported by this server".format(**vars()) + + def sendgreeting(self): + with self.context.lock, self.lock: + if self.context.welcome: + self.send(origin=self.bouncer.servname, cmd=1, + target=self.context.identity, extinfo=self.context.welcome) + # u":{self.context.serv} 001 {self.context.identity.nick} :{self.context.welcome}".format(**vars()) + if self.context.hostinfo: + self.send(origin=self.bouncer.servname, cmd=2, + target=self.context.identity, extinfo=self.context.hostinfo) + # u":{self.context.serv} 002 {self.context.identity.nick} :{self.context.hostinfo}".format(**vars()) + if self.context.servcreated: + self.send(origin=self.bouncer.servname, cmd=3, + target=self.context.identity, extinfo=self.context.servcreated) + # u":{self.context.serv} 003 {self.context.identity.nick} :{self.context.servcreated}".format(**vars()) + if self.context.servinfo: + self.send(origin=self.bouncer.servname, cmd=4, + target=self.context.identity, params=self.context.servinfo) + # u":{self.context.serv} 004 {self.context.identity.nick} {self.context.servinfo}".format(**vars()) + + def sendmotd(self): + with self.context.lock, self.lock: + if self.context.motdgreet and self.context.motd and self.context.motdend: + self.send(origin=self.bouncer.servname, cmd=375, + target=self.context.identity, extinfo=self.context.motdgreet) + # u":{server} 375 {self.identity.nick} :{self.motdgreet}".format(**vars()) + for motdline in self.context.motd: + self.send(origin=self.bouncer.servname, cmd=372, + target=self.context.identity, extinfo=motdline) + # u":{server} 372 {self.identity.nick} :{motdline}".format(**vars()) + self.send(origin=self.bouncer.servname, cmd=376, + target=self.context.identity, extinfo=self.context.motdend) + # u":{server} 376 {self.identity.nick} :{self.motdend}".format(**vars()) + else: + self.send(origin=self.bouncer.servname, cmd=422, + target=self.context.identity, extinfo="MOTD File is missing") + # u":{server} 422 {self.identity.nick} :MOTD File is missing".format(**vars()) + + def sendusermodes(self): + with self.context.lock, self.lock: + self.send( + origin=self.bouncer.servname, cmd=221, target=self.context.identity, + params="+{self.context.identity.modes}".format(**vars())) + if "s" in self.context.identity.modes: + self.send( + origin=self.bouncer.servname, cmd=8, target=self.context.identity, + params="+{self.context.identity.snomask}".format(**vars()), extinfo="Server notice mask") + def run(self): - ### Name loopup should happen here instead + # Name loopup should happen here instead ipv4match = re.findall( r"^::ffff:((\d+)\.(\d+)\.(\d+)\.(\d+))$", self.host) if self.bouncer.ipv6 and ipv4match: @@ -428,14 +744,7 @@ class BouncerConnection (Thread): except: pass - ### Add connection to connection list. - - listnumerics = dict(b=(367, 368, "channel ban list"), - e=(348, 349, "Channel Exception List"), - I=(346, 347, "Channel Invite Exception List"), - w=(910, 911, "Channel Access List"), - g=(941, 940, "chanel spamfilter list"), - X=(954, 953, "channel exemptchanops list")) + # Add connection to connection list. passwd = None nick = None @@ -447,10 +756,18 @@ class BouncerConnection (Thread): try: while True: - ### Read data (appending) into readbuf, then break lines and append lines to linebuf + # Read data (appending) into readbuf, then break lines and + # append lines to linebuf while len(linebuf) == 0: timestamp = irc.timestamp() - read = self.connection.recv(512) + try: + read = self.connection.recv(512) + except socket.error, msg: + self.quit(msg) + sys.exit() + except ssl.SSLError, msg: + self.quit(msg) + sys.exit() if read == "" and len(linebuf) == 0: # No more data to process. #self.quitmsg="Connection Closed" sys.exit() @@ -460,10 +777,18 @@ class BouncerConnection (Thread): if lastlf >= 0: linebuf.extend(string.split(readbuf[0:lastlf], "\n")) - readbuf = readbuf[lastlf+1:] + readbuf = readbuf[lastlf + 1:] line = string.rstrip(linebuf.pop(0)) - match = re.findall("^(.+?)(?:\\s+(.+?)(?:\\s+(.+?))??)??(?:\\s+:(.*))?$", line, re.I) + try: + line = line.decode("utf8") + except UnicodeDecodeError: + # Attempt to figure encoding + charset = chardet.detect(line)['encoding'] + line = line.decode(charset) + match = re.findall( + "^(.+?)(?:\\s+(.+?)(?:\\s+(.+?))??)??(?:\\s+:(.*))?$", line, re.I) + # print match if len(match) == 0: continue @@ -474,218 +799,176 @@ class BouncerConnection (Thread): passwd = target if target else extinfo else: self.quit("Access Denied") + print "*** [BouncerConnection] Incoming connection from %s failed: Expected PASS." % (self.host) break - elif not nick: # Bouncer expects a NICK command + elif not self.nick: # Bouncer expects a NICK command if cmd.upper() == "NICK": - nick = target if target else extinfo + self.nick = target if target else extinfo else: self.quit("Access Denied") + print "*** [BouncerConnection] Incoming connection from %s failed: Expected NICK." % (self.host) break elif not self.username: # Bouncer expects a USER command to finish registration if cmd.upper() == "USER": self.username = target - #print self.username - if self.username in self.bouncer.servers.keys(): - self.IRC, passwdhash, hashtype = self.bouncer.servers[self.username] - passmatch = hashlib.new(hashtype, passwd).hexdigest() == passwdhash - with self.IRC.lock: - self.IRC.logwrite("*** [BouncerConnection] Incoming connection from %s to %s." % (self.host, self.IRC)) - with self.bouncer.lock: - ### Announce connection to all other bouncer connections. - if self.debug: - self.IRC.logwrite("dbg [BouncerConnection] Attempting to broadcast incoming connection %(self)s." % vars()) - for bouncerconnection in self.bouncer.connections: - if bouncerconnection.IRC != self.IRC: - continue - if self.debug: - self.IRC.logwrite("dbg [BouncerConnection] Broadcasting to %(bouncerconnection)s." % vars()) - if not bouncerconnection.quitting: - bouncerconnection.send(":*Bouncer* NOTICE %s :Incoming connection from %s to %s\n" % (bouncerconnection.IRC.identity.nick, self.host, self.IRC)) - if self.debug: - self.IRC.logwrite("dbg [BouncerConnection] Success: %(bouncerconnection)s." % vars()) - if len([bncconnection for bncconnection in self.bouncer.connections if bncconnection.IRC == self.IRC]) == 0 and self.IRC.identity.away: - ### Bouncer connection should automatically return from away status. - self.IRC.raw("AWAY") - self.bouncer.connections.append(self) - - if not (self.IRC.connected and self.IRC.registered and type(self.IRC.supports) == dict and "CHANMODES" in self.IRC.supports.keys() and passmatch): - self.quit("Access Denied") - break - - ### If we have made it to this point, then access has been granted. - labels = [bouncerconnection.label for bouncerconnection in self.bouncer.connections if bouncerconnection.IRC == self.IRC and bouncerconnection.label] - n = 1 - while "*%s_%d"%(self.username, n) in labels: - n += 1 - self.label = "*%s_%d"%(self.username, n) - - ### Request Version info. - #self.connection.send(":$bouncer PRIVMSG %s :\x01VERSION\x01\n" % (self.IRC.identity.nick)) - - ### Log incoming connection - - ### Send Greeting. - with self.lock: - self.connection.send(":%s 001 %s :%s\n" % (self.IRC.serv, self.IRC.identity.nick, self.IRC.welcome)) - self.connection.send(":%s 002 %s :%s\n" % (self.IRC.serv, self.IRC.identity.nick, self.IRC.hostinfo)) - self.connection.send(":%s 003 %s :%s\n" % (self.IRC.serv, self.IRC.identity.nick, self.IRC.servcreated)) - self.connection.send(":%s 004 %s %s\n" % (self.IRC.serv, self.IRC.identity.nick, self.IRC.servinfo)) - - ### Send 005 response. - supports = ["CHANMODES=%s"%(",".join(value)) if name == "CHANMODES" else "PREFIX=(%s)%s"%value if name == "PREFIX" else "%s=%s"%(name, value) if value else name for name, value in self.IRC.supports.items()] - supports.sort() - supportsreply = [] - supportsstr = " ".join(supports) - index = 0 - while True: - if len(supportsstr)-index > 196: - nextindex = supportsstr.rfind(" ", index, index+196) - supportsreply.append(supportsstr[index:nextindex]) - index = nextindex+1 - else: - supportsreply.append(supportsstr[index:]) - break - for support in supportsreply: - self.connection.send(":%s 005 %s %s :are supported by this server\n" % (self.IRC.serv, self.IRC.identity.nick, support)) - - ### Send MOTD - if self.IRC.motdgreet and self.IRC.motd and self.IRC.motdend: - self.connection.send(":%s 375 %s :%s\n" % (self.IRC.serv, self.IRC.identity.nick, self.IRC.motdgreet)) - for motdline in self.IRC.motd: - self.connection.send(":%s 372 %s :%s\n" % (self.IRC.serv, self.IRC.identity.nick, motdline)) - try: - self.connection.send(":%s 376 %s :%s\n" % (self.IRC.serv, self.IRC.identity.nick, self.IRC.motdend)) - except AttributeError: - self.connection.send(":%s 376 %s\n" % (self.IRC.serv, self.IRC.identity.nick)) - else: - self.connection.send(":%s 422 %s :MOTD File is missing\n" % (self.IRC.serv, self.IRC.identity.nick)) - - ### Send user modes and snomasks. - self.connection.send(":%s 221 %s +%s\n" % (self.IRC.serv, self.IRC.identity.nick, self.IRC.identity.modes)) - if "s" in self.IRC.identity.modes and self.IRC.identity.snomask: - self.connection.send(":%s 008 %s +%s :Server notice mask\n" % (self.IRC.server, self.IRC.identity.nick, self.IRC.identity.snomask)) - - # ### Join user to internal bouncer channel. - # self.connection.send(":%s!%s@%s JOIN :$bouncer\n" % (self.IRC.identity.nick, self.IRC.identity.username, self.IRC.identity.host)) - - # ### Set internal bouncer topic. - # self.connection.send(":$bouncer 332 %s $bouncer :Bouncer internal channel. Enter bouncer commands here.\n" % (self.IRC.identity.nick)) - # self.connection.send(":$bouncer 333 %s $bouncer $bouncer %s\n" % (self.IRC.identity.nick, self.bouncer.starttime)) - - # ### Send NAMES for internal bouncer channel. - # self.connection.send(":$bouncer 353 %s @ $bouncer :%s\n" % ( - # self.IRC.identity.nick, - # string.join(["@*Bouncer*"]+["@%s"%bouncerconnection.label for bouncerconnection in self.bouncer.connections])) - # ) - # self.connection.send(":$bouncer 366 %s $bouncer :End of /NAMES list.\n" % (self.IRC.identity.nick)) - - # ### Give operator mode to user. - # self.connection.send(":*Bouncer* MODE $bouncer +o %s\n" % (self.IRC.identity.nick)) - - ### Join user to channels. - for channel in self.IRC.identity.channels: - ### JOIN command - self.connection.send(":%s!%s@%s JOIN :%s\n" % (self.IRC.identity.nick, self.IRC.identity.username, self.IRC.identity.host, channel.name)) - - ### Topic - self.connection.send(":%s 332 %s %s :%s\n" % (self.IRC.serv, self.IRC.identity.nick, channel.name, channel.topic)) - self.connection.send(":%s 333 %s %s %s %s\n" % (self.IRC.serv, self.IRC.identity.nick, channel.name, channel.topicsetby, channel.topictime)) - - ### Determine if +s or +p modes are set in channel - secret = "s" in channel.modes.keys() and channel.modes["s"] - private = "p" in channel.modes.keys() and channel.modes["p"] - - ### Construct NAMES for channel. - namesusers = [] - modes, symbols = self.IRC.supports["PREFIX"] - self.connection.send(":%s 353 %s %s %s :%s\n" % ( - self.IRC.serv, - self.IRC.identity.nick, - "@" if secret else ("*" if private else "="), - channel.name, - string.join([string.join([symbols[k] if modes[k] in channel.modes.keys() and user in channel.modes[modes[k]] else "" for k in xrange(len(modes))], "")+user.nick for user in channel.users])) - ) - self.connection.send(":%s 366 %s %s :End of /NAMES list.\n" % (self.IRC.serv, self.IRC.identity.nick, channel.name)) - - else: # User not found + contextfound = False + for self.context, conf in self.bouncer.conf.items(): + # print conf.label, self.username + if conf.label == self.username: + contextfound = True + break + if not contextfound: self.quit("Access Denied") + print >>sys.stderr, "*** [BouncerConnection] Incoming connection from %s denied: Context not found." % ( + self.host) break + passmatch = hashlib.new( + conf.hashtype, passwd).hexdigest() == conf.passwd + with self.context.lock: + if not passmatch: + self.quit("Access Denied") + self.context.logwrite( + "*** [BouncerConnection] Incoming connection from %s to %s denied: Invalid password." % (self.host, self.context)) + self.bouncer.broadcast( + self.context, origin=self.bouncer.servname, cmd="NOTICE", target=self.context.identity, + extinfo="Incoming connection from %s to %s denied: Invalid password." % (self.host, self.context)) + # for client in self.bouncer.clients: + # if client.context!=self.context: + # continue + # if not client.quitting: + #client.send(origin=self.bouncer.servname, cmd="NOTICE", target=client.context.identity, extinfo="Incoming connection from %s to %s dened: Invalid password.\n" % (self.host, self.context)) + break + + self.context.logwrite( + "*** [BouncerConnection] Incoming connection from %s to %s established." % (self.host, self.context)) + with self.bouncer.lock: + self.translations = dict( + self.bouncer.conf[self.context].translations) + # Announce connection to all other bouncer + # clients. + self.bouncer.broadcast( + self.context, origin=self.bouncer.servname, cmd="NOTICE", target=self.context.identity, + extinfo="Incoming connection from %s to %s established." % (self.host, self.context)) + # for client in self.bouncer.clients: + # if client.context!=self.context: + # continue + # if not client.quitting: + #client.send(":*Bouncer* NOTICE %s :Incoming connection from %s to %s\n" % (client.context.identity.nick, self.host, self.context)) + if len([client for client in self.bouncer.clients if client.context == self.context]) == 0 and self.context.registered and type(self.context.identity) == irc.User and self.context.identity.away: + # Bouncer connection should + # automatically return from away + # status. + self.context.raw("AWAY") + self.hidden = irc.ChanList( + self.bouncer.conf[self.context].hidden, context=self.context) + self.bouncer.clients.append(self) + + if self.context.registered: + # Send Greeting. + with self.lock: + self.sendgreeting() + self.sendsupports() + self.sendmotd() + self.sendusermodes() + + # Join user to channels. + for channel in self.context.identity.channels: + if channel not in self.hidden: + self.showchannel(channel) + else: + self.send( + origin=self.bouncer.servname, cmd="NOTICE", target=self.nick, + extinfo="Not connected to server. Type /bncconnect to attempt connection.") + #self.send(u":%s 001 %s :Welcome to the Bouncer context Network %s!%s@%s\n" % ("*Bouncer*", self.nick, self.nick, self.username, self.host)) else: # Client did not send USER command when expected self.quit("Access Denied") + print "*** [BouncerConnection] Incoming connection from %s failed: Expected USER." % (self.host) break - elif cmd.upper() == "QUIT": - self.quit(extinfo) - break - - elif cmd.upper() == "PING": - self.send(":%s PONG %s :%s\n" % (self.IRC.serv, self.IRC.serv, self.IRC.identity.nick)) - - elif cmd.upper() in ("PRIVMSG", "NOTICE"): - ### Check if CTCP - ctcp = re.findall("^\x01(.*?)(?:\\s+(.*?)\\s*)?\x01$", - extinfo) - - if ctcp: # If CTCP, only want to - (ctcptype, ext) = ctcp[0] # Unpack CTCP info - - if ctcptype == "LAGCHECK": # Client is doing a lag check. No need to send to IRC network, just reply back. - self.send(":%s!%s@%s %s\n" % (self.IRC.identity.nick, self.IRC.identity.username, self.IRC.identity.host, line)) - else: - self.IRC.raw(line, origin=self) - else: - self.IRC.raw(line, origin=self) - - elif cmd.upper() == "MODE": # Will want to determine is requesting modes, or attempting to modify modes. - if target and "CHANTYPES" in self.IRC.supports.keys() and target[0] in self.IRC.supports["CHANTYPES"]: - if params == "": - channel = self.IRC.channel(target) - modes = channel.modes.keys() - modestr = "".join([mode for mode in modes if mode not in self.IRC.supports["CHANMODES"][0]+self.IRC.supports["PREFIX"][0] and channel.modes[mode]]) - params = " ".join([channel.modes[mode] for mode in modes if mode in self.IRC.supports["CHANMODES"][1]+self.IRC.supports["CHANMODES"][2] and channel.modes[mode]]) - with self.lock: - if len(modestr): - self.connection.send(":%s 324 %s %s +%s %s\n" % (self.IRC.serv, self.IRC.identity.nick, channel.name, modestr, params)) - if channel.created: - self.connection.send(":%s 329 %s %s %s\n" % (self.IRC.serv, self.IRC.identity.nick, channel.name, channel.created)) - elif re.match("^\\+?[%s]+$"%self.IRC.supports["CHANMODES"][0], params) and extinfo == "": - #print "ddd Mode List Request", params - channel = self.IRC.channel(target) - redundant = [] - for mode in params.lstrip("+"): - if mode in redundant or mode not in listnumerics.keys(): - continue - i, e, l = listnumerics[mode] - with self.lock: - if mode in channel.modes.keys(): - for (mask, setby, settime) in channel.modes[mode]: - self.connection.send(":%s %d %s %s %s %s %s\n" % (self.IRC.serv, i, channel.context.identity.nick, channel.name, mask, setby, settime)) - self.connection.send(":%s %d %s %s :End of %s\n" % (self.IRC.serv, e, channel.context.identity.nick, channel.name, l)) - redundant.append(mode) - else: - self.IRC.raw(line, origin=self) - elif params == "" and target.lower() == self.IRC.identity.nick.lower(): - with self.lock: - self.connection.send(":%s 221 %s +%s\n" % (self.IRC.serv, self.IRC.identity.nick, self.IRC.identity.modes)) - if "s" in self.IRC.identity.modes and self.IRC.identity.snomask: - self.connection.send(":%s 008 %s +%s :Server notice mask\n" % (self.IRC.serv, self.IRC.identity.nick, self.IRC.identity.snomask)) - else: - self.IRC.raw(line, origin=self) else: - self.IRC.raw(line, origin=self) + chantypes = self.context.supports.get( + "CHANTYPES", irc._defaultchantypes) + # Disable translating for now. + if False and cmd.upper() not in ("SETTRANSLATE", "RMTRANSLATE"): + translated = [] + for targ in target.split(","): + translatefound = False + if re.match(irc._chanmatch % re.escape(chantypes), targ): + for channel, translate in self.translations.items(): + if targ.lower() == translate.lower(): + translated.append(channel.name) + translatefound = True + break + if not translatefound: + translated.append(targ) + target = ",".join(translated) + + translated = [] + for param in params.split(" "): + translatefound = False + if re.match(irc._chanmatch % re.escape(chantypes), param): + for channel, translate in self.translations.items(): + if param.lower() == translate.lower(): + translated.append(channel.name) + translatefound = True + break + if not translatefound: + translated.append(param) + params = " ".join(translated) + + if params: + line = u"{cmd} {target} {params}".format(**vars()) + elif target: + line = u"{cmd} {target}".format(**vars()) + else: + line = cmd + + if extinfo: + line = u"{line} :{extinfo}".format(**vars()) + + cmdmethod = "cmd%s" % cmd.upper() + if hasattr(self, cmdmethod): + method = getattr(self, cmdmethod) + try: + method(line, target, params, extinfo) + except SystemExit: + sys.exit() + except: + if self.context: + exc, excmsg, tb = sys.exc_info() + self.context.logwrite(*[u"!!! [BouncerConnection] Exception in thread %(self)s" % vars()] + [ + u"!!! [BouncerConnection] %(tbline)s" % vars() for tbline in traceback.format_exc().split("\n")]) + print >>sys.stderr, "Exception in thread %(self)s" % vars( + ) + print >>sys.stderr, traceback.format_exc() + elif not self.context.connected: + self.send( + origin=self.bouncer.servname, cmd="NOTICE", target=self.nick, + extinfo="Not connected to server. Type /bncconnect to attempt connection.") + continue + + elif not self.context.registered: + self.send(origin=self.bouncer.servname, cmd="NOTICE", + target=self.nick, extinfo="Not registered.") + continue + else: + self.context.raw(line, origin=self) except SystemExit: pass # No need to pass error message if break resulted from sys.exit() except: exc, excmsg, tb = sys.exc_info() self.quitmsg = str(excmsg) - if self.IRC: + if self.context: exc, excmsg, tb = sys.exc_info() - self.IRC.logwrite(*["!!! [BouncerConnection] Exception in thread %(self)s" % vars()]+["!!! [BouncerConnection] %(tbline)s" % vars() for tbline in traceback.format_exc().split("\n")]) + self.context.logwrite(*["!!! [BouncerConnection] Exception in thread %(self)s" % vars()] + [ + "!!! [BouncerConnection] %(tbline)s" % vars() for tbline in traceback.format_exc().split("\n")]) + print >>sys.stderr, "Exception in thread %(self)s" % vars() + print >>sys.stderr, traceback.format_exc() finally: - ### Juuuuuuust in case. + # Juuuuuuust in case. with self.lock: try: self.connection.shutdown(1) @@ -693,29 +976,208 @@ class BouncerConnection (Thread): except: pass - if self.IRC: - self.IRC.logwrite("*** [BouncerConnection] Connection from %s terminated (%s)." % (self.host, self.quitmsg)) - - if self in self.bouncer.connections: - with self.bouncer.lock: - self.bouncer.connections.remove(self) - if self.IRC.connected and self.IRC.identity and len([bncconnection for bncconnection in self.bouncer.connections if bncconnection.IRC == self.IRC]) == 0 and not self.IRC.identity.away and self.bouncer.autoaway: - ### Bouncer automatically sets away status. - self.IRC.raw("AWAY :%s"%self.bouncer.autoaway) - if self.debug: - self.IRC.logwrite("dbg [BouncerConnection] Attempting to broadcast terminated connection %(self)s." % vars()) - for bouncerconnection in self.bouncer.connections: - if bouncerconnection.IRC == self.IRC: - if self.debug: - self.IRC.logwrite("dbg [BouncerConnection] Broadcasting to %(bouncerconnection)s." % vars()) - if not bouncerconnection.quitting: - bouncerconnection.connection.send(":*Bouncer* NOTICE %s :Connection from %s to %s terminated (%s)\n" % (bouncerconnection.IRC.identity.nick, self.host, self.IRC, self.quitmsg)) - if self.debug: - self.IRC.logwrite("dbg [BouncerConnection] Success: %(bouncerconnection)s." % vars()) - -# ### Announce QUIT to other bouncer connections. -# for bouncerconnection in self.bouncer.connections: -# try: -# bouncerconnection.connection.send(":%s!%s@%s QUIT :%s\n" % (self.label, self.username, self.host, self.quitmsg)) -# except: -# pass + if self.context: + self.context.logwrite( + "*** [BouncerConnection] Connection from %s terminated (%s)." % (self.host, self.quitmsg)) + + with self.bouncer.lock: + if self in self.bouncer.clients: + self.bouncer.clients.remove(self) + if self.context.connected and self.context.identity and len([client for client in self.bouncer.clients if client.context == self.context]) == 0 and self.context.registered and type(self.context.identity) == irc.User and not self.context.identity.away and self.bouncer.autoaway: + # Bouncer automatically sets away status. + self.context.raw("AWAY :%s" % self.bouncer.autoaway) + self.bouncer.broadcast( + self.context, origin=self.bouncer.servname, cmd="NOTICE", target=self.context.identity, + extinfo="Connection from %s to %s terminated (%s)\n" % (self.host, self.context, self.quitmsg)) + # ":*Bouncer* NOTICE %s :Connection from %s to %s terminated (%s)\n" % (client.context.identity.nick, self.host, self.context, self.quitmsg)) + + def cmdQUIT(self, line, target, params, extinfo): + self.quit(extinfo) + sys.exit() + + def cmdPROTOCTL(self, line, target, params, extinfo): + protoparams = [target.upper()] + params.upper().split() + if "NAMESX" in protoparams: + self.namesx = True + if "UHNAMES" in protoparams: + self.uhnames = True + + def cmdPING(self, line, target, params, extinfo): + with self.context.lock: + if True or (self.context.identity and type(self.context.identity) == irc.User): + self.send(origin=self.bouncer.servname, + cmd="PONG", target=params, extinfo=target) + # u":{self.context.identity.server} PONG {params} + # :{target}\n".format(**vars()).encode("utf8")) + else: + self.send(origin=self.bouncer.servname, + cmd="PONG", params=params, extinfo=target) + self.send( + u":{self.context.server} PONG {params} :{target}\n".format(**vars()).encode("utf8")) + + def cmdPRIVMSG(self, line, target, params, extinfo): + # Check if CTCP + ctcp = re.findall("^\x01(.*?)(?:\\s+(.*?)\\s*)?\x01$", extinfo) + + if ctcp: + (ctcptype, ext) = ctcp[0] # Unpack CTCP info + + if ctcptype == "LAGCHECK": # Client is doing a lag check. No need to send to context network, just reply back. + self.send( + u":{self.context.identity:full} {line}\n".format(**vars()).encode("utf8")) + else: + self.context.raw(line, origin=self) + else: + self.context.raw(line, origin=self) + + def cmdMODE(self, line, target, params, extinfo): # Will want to determine is requesting modes, or attempting to modify modes. + # if target and "CHANTYPES" in self.context.supports.keys() and + # target[0] in self.context.supports["CHANTYPES"]: + chantypes = self.context.supports.get( + "CHANTYPES", irc._defaultchantypes) + chanmodes = self.context.supports.get( + "CHANMODES", irc._defaultchanmodes) + prefix = self.context.supports.get("PREFIX", irc._defaultprefix) + if re.match(irc._chanmatch % re.escape(chantypes), target): + channel = self.context[target] + + if params == "": + # We are requesting the modes for the channel + if self.context.identity in channel.users: + # We are in the channel, and we know the channel modes + self.sendchannelmodes(channel) + else: + # We are NOT in the channel, so we will forward the request + # to the server. + self.context.raw( + u"MODE {channel.name}".format(**vars()), origin=self) + + elif re.match("^\\+?[%s]+$" % chanmodes[0], params) and extinfo == "": + # We are requesting one or more mode lists. + modechars = "" + for mode in params.lstrip("+"): + if mode not in modechars: + modechars += mode + if self.context.identity in channel.users: + self.sendchannelmodes(channel, modechars) + else: + self.context.raw( + u"MODE {channel.name} {params}".format(**vars()), origin=self) + else: + self.context.raw(line, origin=self) + elif params == "" and target.lower() == self.context.identity.nick.lower(): + self.sendusermodes() + else: + self.context.raw( + u"MODE {target} {params}".format(**vars()), origin=self) + + def cmdNAMES(self, line, target, params, extinfo): + chantypes = self.context.supports.get( + "CHANTYPES", irc._defaultchantypes) + chanmodes = self.context.supports.get( + "CHANMODES", irc._defaultchanmodes) + prefix = self.context.supports.get("PREFIX", irc._defaultprefix) + fallback = [] + with self.lock: + for channame in target.split(): + if re.match(irc._chanmatch % re.escape(chantypes), channame): + channel = self.context[channame] + with self.lock: + if self.context.identity in channel: + self.sendchannelnames(channel) + else: + fallback.append(channame) + else: + fallback.append(channame) + if fallback: + self.context.raw("NAMES %s" % + (",".join(fallback)), origin=self) + + def cmdSHOW(self, line, target, params, extinfo): + chantypes = self.context.supports.get( + "CHANTYPES", irc._defaultchantypes) + with self.context.lock, self.lock: + for channame in target.split(): + if re.match(irc._chanmatch % re.escape(chantypes), channame): + channel = self.context[channame] + if channel in self.hidden: + if self.context.identity in channel: + self.showchannel(channel) + else: + self.hidden.remove(channel) + self.send( + origin=self.bouncer.servname, cmd="NOTICE", target=self.context.identity, + extinfo="{channel.name} removed from hidden list, but not joined.".format(**vars())) + else: + self.send( + origin=self.bouncer.servname, cmd="NOTICE", target=self.context.identity, + extinfo="{channel.name} not in hidden list.".format(**vars())) + else: + self.send( + origin=self.bouncer.servname, cmd="NOTICE", target=self.context.identity, + extinfo="{channame}: invalid channel name.".format(**vars())) + + def cmdHIDE(self, line, target, params, extinfo): + chantypes = self.context.supports.get( + "CHANTYPES", irc._defaultchantypes) + with self.context.lock, self.lock: + for channame in target.split(): + if re.match(irc._chanmatch % re.escape(chantypes), channame): + channel = self.context[channame] + if channel not in self.hidden: + if self.context.identity in channel: + self.send( + origin=self.context.identity, cmd="PART", target=channel, extinfo="Hiding channel") + else: + self.send( + origin=self.bouncer.servname, cmd="NOTICE", target=self.context.identity, + extinfo="{channel.name} added to the hidden list, but not joined.".format(**vars())) + self.hidden.append(channel) + else: + self.send( + origin=self.bouncer.servname, cmd="NOTICE", target=self.context.identity, + extinfo="{channel.name} already in hidden list.".format(**vars())) + else: + self.send( + origin=self.bouncer.servname, cmd="NOTICE", target=self.context.identity, + extinfo="{channame}: invalid channel name.".format(**vars())) + + def cmdSETTRANSLATE(self, line, target, params, extinfo): + chantypes = self.context.supports.get( + "CHANTYPES", irc._defaultchantypes) + with self.context.lock, self.lock: + if re.match(irc._chanmatch % re.escape(chantypes), target) and re.match(irc._chanmatch % re.escape(chantypes), target): + channel = self.context[target] + if self.context.supports.get("CASEMAPPING", "rfc1459") == "ascii": + translations_lower = [translation.translate(irc._rfc1459casemapping) + for translation in self.translations.values()] + params_lower = params.translate(irc._rfc1459casemapping) + else: + translations_lower = [translation.lower() + for translation in self.translations.values()] + params_lower = params.lower() + if params_lower in translations_lower: + self.send( + origin=self.bouncer.servname, cmd="NOTICE", target=self.context.identity, + extinfo="Cannot set translation for {channel.name} to {param}.".format(**vars())) + else: + self.send(origin=self.context.identity, cmd="PART", + target=channel, extinfo="Translating...") + self.translations[channel] = params + self.showchannel(channel) + + def cmdRMTRANSLATE(self, line, target, params, extinfo): + chantypes = self.context.supports.get( + "CHANTYPES", irc._defaultchantypes) + with self.context.lock, self.lock: + if re.match(irc._chanmatch % re.escape(chantypes), target): + channel = self.context[target] + if channel not in self.translations.keys(): + self.send( + origin=self.bouncer.servname, cmd="NOTICE", target=self.context.identity, + extinfo="Cannot remove translation for {channel.name}.".format(**vars())) + else: + self.send(origin=self.context.identity, cmd="PART", + target=channel, extinfo="Translating...") + del self.translations[channel] + self.showchannel(channel) |