class UnrealIRCServer(LineOnlyReceiver): showirc = False def __init__(self, main): self.ism = IRCStateManager(main, self) self.server_name = None self.shutdown_deferred = None self.ping_dcall = None self.ping_waiting = False def connectionMade(self): scfg = getServiceConfig() LOG.info("Connected to IRC server.") self.sendLine("PASS :%s" % (scfg.sendpass,)) self.sendLine( "SERVER %s 1 :%s" % (scfg.my_host, scfg.my_name)) def sendLine(self, line): line = line.replace('\r', '').replace('\n', '') if self.showirc: LOG.log(5, "<: %s" % line) LineOnlyReceiver.sendLine(self, line) def lineReceived(self, line): if not line: return if self.showirc: LOG.log(5, ">: %s" % line) if line[0] == ':': try: prefix, line = line[1:].split(' ', 1) except ValueError: return else: prefix = '' try: line, trailing = line.split(' :', 1) except ValueError: args = line.split() else: args = line.split() args.append(trailing) try: f = getattr(self, 'handleCmd_%s' % args[0].upper()) except AttributeError: pass else: f(prefix, args[1:]) def handleCmd_PING(self, prefix, args): LOG.info("PING? PONG!") scfg = getServiceConfig() if len(args) == 1: self.sendLine("PONG %s :%s" % (scfg.my_host, args[0])) elif len(args) == 2: self.sendLine("PONG %s :%s" % (args[1], args[0])) def handleCmd_PONG(self, prefix, args): if self.ping_waiting: self.ping_waiting = False self.schedulePing() def handleCmd_NICK(self, prefix, args): oldnick = prefix newnick = args[0] if oldnick: self.ism.changeNick(self.ism.findUser(oldnick), newnick) else: self.ism.addUser(newnick) def handleCmd_SVSNICK(self, prefix, args): # :services.dhirc.com SVSNICK |foo Guest33400 :1236660594 oldnick = args[0] newnick = args[1] # Is the source user a Dtella node? Freak out. n = self.ism.findDtellaNode(inick=oldnick) if n: message = "Nick change to %s is impossible" % irc_to_dc(newnick) self.ism.kickDtellaNode(n, prefix, message) def handleCmd_JOIN(self, prefix, args): nick = prefix chans = args[0].split(',') scfg = getServiceConfig() if scfg.channel in chans: self.ism.joinChannel(self.ism.findUser(nick)) def handleCmd_PART(self, prefix, args): nick = prefix chans = args[0].split(',') scfg = getServiceConfig() if scfg.channel in chans: CHECK(self.ism.partChannel(self.ism.findUser(nick))) def handleCmd_QUIT(self, prefix, args): nick = prefix self.ism.removeUser(self.ism.findUser(nick)) def handleCmd_KICK(self, prefix, args): chan = args[0] l33t = prefix n00b = args[1] reason = irc_strip(args[2]) scfg = getServiceConfig() if chan != scfg.channel: return if n00b.lower() == self.ism.bot_user.inick.lower(): if self.ism.syncd: self.pushBotJoin() return n = self.ism.findDtellaNode(inick=n00b) if n: self.ism.kickDtellaNode(n, l33t, reason) else: message = ( "%s has kicked %s: %s" % (irc_to_dc(l33t), irc_to_dc(n00b), reason)) CHECK(self.ism.partChannel(self.ism.findUser(n00b), message)) def handleCmd_KILL(self, prefix, args): # :darkhorse KILL }darkhorse :dhirc.com!darkhorse (TEST!!!) l33t = prefix n00b = args[0] reason = "KILL: " + irc_strip(args[1]) if n00b.lower() == self.ism.bot_user.inick.lower(): if self.ism.syncd: self.pushBotJoin(do_nick=True) return n = self.ism.findDtellaNode(inick=n00b) if n: self.ism.kickDtellaNode(n, l33t, reason, send_quit=False) else: message = ( "%s has KILL'd %s: %s" % (irc_to_dc(l33t), irc_to_dc(n00b), reason)) self.ism.removeUser(self.ism.findUser(n00b), message) # Treat SVSKILL the same as KILL. handleCmd_SVSKILL = handleCmd_KILL def handleCmd_TOPIC(self, prefix, args): # :Paul TOPIC #dtella Paul 1169420711 :Dtella :: Development Stage chan = args[0] whoset = args[1] text = irc_strip(args[-1]) scfg = getServiceConfig() if chan == scfg.channel: self.ism.setTopic(whoset, text) def handleCmd_MODE(self, prefix, args): # :Paul MODE #dtella +vv aaahhh Big_Guy whoset = prefix chan = args[0] change = args[1] nicks = args[2:] scfg = getServiceConfig() if chan != scfg.channel: return on_off = True i = 0 # User() -> {mode -> on_off} user_changes = {} # Dtella node modes that need unsetting. unset_modes = [] unset_nicks = [] for c in change: if c == '+': on_off = True elif c == '-': on_off = False elif c == 't': self.ism.setTopicLocked(whoset, on_off) elif c == 'm': self.ism.setModerated(whoset, on_off) elif c == 'k': # Skip over channel key i += 1 elif c == 'l': # Skip over channel user limit i += 1 elif c == 'b': banmask = nicks[i] i += 1 self.ism.setChannelBan(whoset, on_off, banmask) elif c in scfg.chan_umodes.modes: # Grab affected nick nick = nicks[i] i += 1 n = self.ism.findDtellaNode(inick=nick) if n: # If someone set a mode for a Dt node, unset it. if on_off: unset_modes.append(c) unset_nicks.append(nick) continue # Get the IRC user we're modifying. try: u = self.ism.findUser(nick) except KeyError: LOG.error("MODE: unknown nick: %s" % nick) continue # Schedule a mode change for this user. user_changes.setdefault(u, {})[c] = on_off # Undo mode changes for Dtella nodes. if unset_modes: self.sendLine( ":%s MODE %s -%s %s" % ( self.ism.bot_user.inick, scfg.channel, ''.join(unset_modes), ' '.join(unset_nicks))) # Send IRC user mode changes to Dtella for u, changes in user_changes.iteritems(): self.ism.setChannelUserModes(whoset, u, changes) def handleCmd_TKL(self, prefix, args): addrem = args[0] kind = args[1] if addrem == '+': on_off = True elif addrem == '-': on_off = False else: LOG.error("TKL: invalid modifier: '%s'" % addrem) return # :irc1.dhirc.com TKL + Z * 128.10.12.0/24 [email protected] 0 1171427130 :no reason if kind == 'Z' and args[2] == '*': cidr = args[3] self.ism.setNetworkBan(cidr, on_off) # :%s TKL + Q * %s* %s 0 %d :Reserved for Dtella elif kind == 'Q': nickmask = args[3] if on_off: reason = args[-1] self.ism.addQLine(nickmask, reason) else: self.ism.removeQLine(nickmask) def handleCmd_SERVER(self, prefix, args): if prefix: # Not from our connected server return if self.server_name: # Could be a dupe? Ignore it. return # We got a reply from the our connected IRC server, so our password # was just accepted. Send the Dtella state information into IRC. # Save server name CHECK(args[0]) self.server_name = args[0] LOG.info("IRC Server Name: %s" % self.server_name) # Tell the ReconnectingClientFactory that we're cool self.factory.resetDelay() # This isn't very correct, because the Dtella nicks # haven't been sent yet, but it's the best we can practically do. scfg = getServiceConfig() cloak_checksum = scfg.hostmasker.getChecksum() self.sendLine("NETINFO 0 %d 0 %s 0 0 0 :%s" % (time.time(), cloak_checksum, scfg.network_name)) self.sendLine(":%s EOS" % scfg.my_host) def handleCmd_EOS(self, prefix, args): if prefix != self.server_name: return if self.ism.syncd: return LOG.info("Finished receiving IRC sync data.") self.showirc = True # Check for conflicting bridges. if self.ism.findConflictingBridge(): LOG.error("My nick prefix is in use! Terminating.") self.transport.loseConnection() reactor.stop() return # Set up nick reservation scfg = getServiceConfig() self.sendLine( "TKL + Q * %s* %s 0 %d :Reserved for Dtella" % (cfg.dc_to_irc_prefix, scfg.my_host, time.time())) self.ism.killConflictingUsers() # Send my own bridge nick self.pushBotJoin(do_nick=True) # When we enter the syncd state, register this instance with Dtella. # This will eventually trigger event_DtellaUp, where we send our state. self.schedulePing() self.ism.addMeToMain() def handleCmd_WHOIS(self, prefix, args): # Somewhat simplistic handling of WHOIS requests if not (prefix and len(args) >= 1): return src = prefix who = args[-1] scfg = getServiceConfig() if who.lower() == self.ism.bot_user.inick.lower(): self.pushWhoisReply( 311, src, who, B_USER, scfg.my_host, '*', B_REALNAME) self.pushWhoisReply( 312, src, who, scfg.my_host, scfg.my_name) self.pushWhoisReply( 319, src, who, scfg.channel) else: n = self.ism.findDtellaNode(inick=who) if not (n and hasattr(n, 'hostmask')): return self.pushWhoisReply( 311, src, who, n_user(n.ipp), n.hostmask, '*', "Dtella %s" % n.dttag[3:]) self.pushWhoisReply( 312, src, who, scfg.my_host, scfg.my_name) self.pushWhoisReply( 319, src, who, scfg.channel) if local.use_locations: self.pushWhoisReply( 320, src, who, "Location: %s" % local.hostnameToLocation(n.hostname)) self.pushWhoisReply( 318, src, who, "End of /WHOIS list.") def pushWhoisReply(self, code, target, who, *strings): scfg = getServiceConfig() line = ":%s %d %s %s " % (scfg.my_host, code, target, who) strings = list(strings) strings[-1] = ":" + strings[-1] line += ' '.join(strings) self.sendLine(line) def handleCmd_PRIVMSG(self, prefix, args): src_nick = prefix target = args[0] text = args[1] flags = 0 if (text[:8], text[-1:]) == ('\001ACTION ', '\001'): text = text[8:-1] flags |= core.SLASHME_BIT text = irc_strip(text) scfg = getServiceConfig() if target == scfg.channel: self.ism.sendChannelMessage(src_nick, text, flags) #Format> :Global PRIVMSG $irc3.dhirc.com :TESTING.... #Handle global messages delivered to the bridge. elif target == "$" + scfg.my_host: flags |= core.NOTICE_BIT self.ism.sendChannelMessage(src_nick, text, flags) else: n = self.ism.findDtellaNode(inick=target) if n: self.ism.sendPrivateMessage(n, src_nick, text, flags) def handleCmd_NOTICE(self, prefix, args): src_nick = prefix target = args[0] text = irc_strip(args[1]) flags = core.NOTICE_BIT scfg = getServiceConfig() if target == scfg.channel: self.ism.sendChannelMessage(src_nick, text, flags) else: n = self.ism.findDtellaNode(inick=target) if n: self.ism.sendPrivateMessage(n, src_nick, text, flags) def pushNick(self, nick, ident, host, modes, ip, name): # If an IP was provided, convert to a base64 parameter. if ip: ip = ' ' + binascii.b2a_base64(ip).rstrip() else: ip = '' scfg = getServiceConfig() self.sendLine( "NICK %s 1 %d %s %s %s 1 +%s *%s :%s" % (nick, time.time(), ident, host, scfg.my_host, modes, ip, name)) def pushJoin(self, nick): scfg = getServiceConfig() self.sendLine(":%s JOIN %s" % (nick, scfg.channel)) def pushQuit(self, nick, reason=""): self.sendLine(":%s QUIT :%s" % (nick, reason)) def pushBotJoin(self, do_nick=False): scfg = getServiceConfig() if do_nick: self.pushNick( self.ism.bot_user.inick, B_USER, scfg.my_host, "Sq", None, B_REALNAME) # Join channel, and grant ops. self.pushJoin(self.ism.bot_user.inick) self.sendLine( ":%s MODE %s +ao %s %s" % (scfg.my_host, scfg.channel, self.ism.bot_user.inick, self.ism.bot_user.inick)) def pushRemoveQLine(self, nickmask): scfg = getServiceConfig() LOG.info("Telling network to remove Q-line: %s" % nickmask) self.sendLine("TKL - Q * %s %s" % (nickmask, scfg.my_host)) def schedulePing(self): if self.ping_dcall: self.ping_dcall.reset(60.0) return def cb(): self.ping_dcall = None if self.ping_waiting: LOG.error("Ping timeout!") self.transport.loseConnection() else: scfg = getServiceConfig() self.sendLine("PING :%s" % scfg.my_host) self.ping_waiting = True self.ping_dcall = reactor.callLater(60.0, cb) self.ping_dcall = reactor.callLater(60.0, cb) def event_AddDtNode(self, n, ident): self.pushNick( n.inick, ident, n.hostname, "iwx", n.ipp[:4], "Dtella %s" % n.dttag[3:]) self.pushJoin(n.inick) def event_RemoveDtNode(self, n, reason): self.pushQuit(n.inick, reason) def event_KillUser(self, u): scfg = getServiceConfig() LOG.info("Killing nick: " + u.inick) self.sendLine(":%s KILL %s :%s (nick reserved for Dtella)" % (scfg.my_host, u.inick, scfg.my_host)) def event_NodeSetTopic(self, n, topic): scfg = getServiceConfig() self.sendLine( ":%s TOPIC %s %s %d :%s" % (n.inick, scfg.channel, n.inick, time.time(), topic)) def event_Message(self, src_n, dst_u, text, action=False): scfg = getServiceConfig() if dst_u is None: target = scfg.channel else: target = dst_u.inick if action: text = "\001ACTION %s\001" % text self.sendLine(":%s PRIVMSG %s :%s" % (src_n.inick, target, text)) def event_Notice(self, src_n, dst_u, text): scfg = getServiceConfig() if dst_u is None: target = scfg.channel else: target = dst_u.inick self.sendLine(":%s NOTICE %s :%s" % (src_n.nick, target, text)) def shutdown(self): if self.shutdown_deferred: return self.shutdown_deferred # Remove nick ban self.pushRemoveQLine(cfg.dc_to_irc_prefix + "*") # Scream self.pushQuit(self.ism.bot_user.inick, "AIEEEEEEE!") # Send SQUIT for completeness scfg = getServiceConfig() self.sendLine(":%s SQUIT %s :Bridge Shutting Down" % (scfg.my_host, scfg.my_host)) # Close connection self.transport.loseConnection() # This will complete after loseConnection fires self.shutdown_deferred = defer.Deferred() return self.shutdown_deferred def connectionLost(self, result): LOG.info("Lost IRC connection.") if self.ism.syncd: self.ism.removeMeFromMain() if self.shutdown_deferred: self.shutdown_deferred.callback("Bye!")
class UnrealIRCServer(LineOnlyReceiver): showirc = False def __init__(self, main): self.ism = IRCStateManager(main, self) self.server_name = None self.shutdown_deferred = None self.ping_dcall = None self.ping_waiting = False def connectionMade(self): scfg = getServiceConfig() LOG.info("Connected to IRC server.") self.sendLine("PASS :%s" % (scfg.sendpass, )) self.sendLine("SERVER %s 1 :%s" % (scfg.my_host, scfg.my_name)) def sendLine(self, line): line = line.replace('\r', '').replace('\n', '') if self.showirc: LOG.log(5, "<: %s" % line) LineOnlyReceiver.sendLine(self, line) def lineReceived(self, line): if not line: return if self.showirc: LOG.log(5, ">: %s" % line) if line[0] == ':': try: prefix, line = line[1:].split(' ', 1) except ValueError: return else: prefix = '' try: line, trailing = line.split(' :', 1) except ValueError: args = line.split() else: args = line.split() args.append(trailing) try: f = getattr(self, 'handleCmd_%s' % args[0].upper()) except AttributeError: pass else: f(prefix, args[1:]) def handleCmd_PING(self, prefix, args): LOG.info("PING? PONG!") scfg = getServiceConfig() if len(args) == 1: self.sendLine("PONG %s :%s" % (scfg.my_host, args[0])) elif len(args) == 2: self.sendLine("PONG %s :%s" % (args[1], args[0])) def handleCmd_PONG(self, prefix, args): if self.ping_waiting: self.ping_waiting = False self.schedulePing() def handleCmd_NICK(self, prefix, args): oldnick = prefix newnick = args[0] if oldnick: self.ism.changeNick(self.ism.findUser(oldnick), newnick) else: self.ism.addUser(newnick) def handleCmd_SVSNICK(self, prefix, args): # :services.dhirc.com SVSNICK |foo Guest33400 :1236660594 oldnick = args[0] newnick = args[1] # Is the source user a Dtella node? Freak out. n = self.ism.findDtellaNode(inick=oldnick) if n: message = "Nick change to %s is impossible" % irc_to_dc(newnick) self.ism.kickDtellaNode(n, prefix, message) def handleCmd_JOIN(self, prefix, args): nick = prefix chans = args[0].split(',') scfg = getServiceConfig() if scfg.channel in chans: self.ism.joinChannel(self.ism.findUser(nick)) def handleCmd_PART(self, prefix, args): nick = prefix chans = args[0].split(',') scfg = getServiceConfig() if scfg.channel in chans: CHECK(self.ism.partChannel(self.ism.findUser(nick))) def handleCmd_QUIT(self, prefix, args): nick = prefix self.ism.removeUser(self.ism.findUser(nick)) def handleCmd_KICK(self, prefix, args): chan = args[0] l33t = prefix n00b = args[1] reason = irc_strip(args[2]) scfg = getServiceConfig() if chan != scfg.channel: return if n00b.lower() == self.ism.bot_user.inick.lower(): if self.ism.syncd: self.pushBotJoin() return n = self.ism.findDtellaNode(inick=n00b) if n: self.ism.kickDtellaNode(n, l33t, reason) else: message = ("%s has kicked %s: %s" % (irc_to_dc(l33t), irc_to_dc(n00b), reason)) CHECK(self.ism.partChannel(self.ism.findUser(n00b), message)) def handleCmd_KILL(self, prefix, args): # :darkhorse KILL }darkhorse :dhirc.com!darkhorse (TEST!!!) l33t = prefix n00b = args[0] reason = "KILL: " + irc_strip(args[1]) if n00b.lower() == self.ism.bot_user.inick.lower(): if self.ism.syncd: self.pushBotJoin(do_nick=True) return n = self.ism.findDtellaNode(inick=n00b) if n: self.ism.kickDtellaNode(n, l33t, reason, send_quit=False) else: message = ("%s has KILL'd %s: %s" % (irc_to_dc(l33t), irc_to_dc(n00b), reason)) self.ism.removeUser(self.ism.findUser(n00b), message) # Treat SVSKILL the same as KILL. handleCmd_SVSKILL = handleCmd_KILL def handleCmd_TOPIC(self, prefix, args): # :Paul TOPIC #dtella Paul 1169420711 :Dtella :: Development Stage chan = args[0] whoset = args[1] text = irc_strip(args[-1]) scfg = getServiceConfig() if chan == scfg.channel: self.ism.setTopic(whoset, text) def handleCmd_MODE(self, prefix, args): # :Paul MODE #dtella +vv aaahhh Big_Guy whoset = prefix chan = args[0] change = args[1] nicks = args[2:] scfg = getServiceConfig() if chan != scfg.channel: return on_off = True i = 0 # User() -> {mode -> on_off} user_changes = {} # Dtella node modes that need unsetting. unset_modes = [] unset_nicks = [] for c in change: if c == '+': on_off = True elif c == '-': on_off = False elif c == 't': self.ism.setTopicLocked(whoset, on_off) elif c == 'm': self.ism.setModerated(whoset, on_off) elif c == 'k': # Skip over channel key i += 1 elif c == 'l': # Skip over channel user limit i += 1 elif c == 'b': banmask = nicks[i] i += 1 self.ism.setChannelBan(whoset, on_off, banmask) elif c in scfg.chan_umodes.modes: # Grab affected nick nick = nicks[i] i += 1 n = self.ism.findDtellaNode(inick=nick) if n: # If someone set a mode for a Dt node, unset it. if on_off: unset_modes.append(c) unset_nicks.append(nick) continue # Get the IRC user we're modifying. try: u = self.ism.findUser(nick) except KeyError: LOG.error("MODE: unknown nick: %s" % nick) continue # Schedule a mode change for this user. user_changes.setdefault(u, {})[c] = on_off # Undo mode changes for Dtella nodes. if unset_modes: self.sendLine(":%s MODE %s -%s %s" % (self.ism.bot_user.inick, scfg.channel, ''.join(unset_modes), ' '.join(unset_nicks))) # Send IRC user mode changes to Dtella for u, changes in user_changes.iteritems(): self.ism.setChannelUserModes(whoset, u, changes) def handleCmd_TKL(self, prefix, args): addrem = args[0] kind = args[1] if addrem == '+': on_off = True elif addrem == '-': on_off = False else: LOG.error("TKL: invalid modifier: '%s'" % addrem) return # :irc1.dhirc.com TKL + Z * 128.10.12.0/24 [email protected] 0 1171427130 :no reason if kind == 'Z' and args[2] == '*': cidr = args[3] self.ism.setNetworkBan(cidr, on_off) # :%s TKL + Q * %s* %s 0 %d :Reserved for Dtella elif kind == 'Q': nickmask = args[3] if on_off: reason = args[-1] self.ism.addQLine(nickmask, reason) else: self.ism.removeQLine(nickmask) def handleCmd_SERVER(self, prefix, args): if prefix: # Not from our connected server return if self.server_name: # Could be a dupe? Ignore it. return # We got a reply from the our connected IRC server, so our password # was just accepted. Send the Dtella state information into IRC. # Save server name CHECK(args[0]) self.server_name = args[0] LOG.info("IRC Server Name: %s" % self.server_name) # Tell the ReconnectingClientFactory that we're cool self.factory.resetDelay() # This isn't very correct, because the Dtella nicks # haven't been sent yet, but it's the best we can practically do. scfg = getServiceConfig() cloak_checksum = scfg.hostmasker.getChecksum() self.sendLine("NETINFO 0 %d 0 %s 0 0 0 :%s" % (time.time(), cloak_checksum, scfg.network_name)) self.sendLine(":%s EOS" % scfg.my_host) def handleCmd_EOS(self, prefix, args): if prefix != self.server_name: return if self.ism.syncd: return LOG.info("Finished receiving IRC sync data.") self.showirc = True # Check for conflicting bridges. if self.ism.findConflictingBridge(): LOG.error("My nick prefix is in use! Terminating.") self.transport.loseConnection() reactor.stop() return # Set up nick reservation scfg = getServiceConfig() self.sendLine("TKL + Q * %s* %s 0 %d :Reserved for Dtella" % (cfg.dc_to_irc_prefix, scfg.my_host, time.time())) self.ism.killConflictingUsers() # Send my own bridge nick self.pushBotJoin(do_nick=True) # When we enter the syncd state, register this instance with Dtella. # This will eventually trigger event_DtellaUp, where we send our state. self.schedulePing() self.ism.addMeToMain() def handleCmd_WHOIS(self, prefix, args): # Somewhat simplistic handling of WHOIS requests if not (prefix and len(args) >= 1): return src = prefix who = args[-1] scfg = getServiceConfig() if who.lower() == self.ism.bot_user.inick.lower(): self.pushWhoisReply(311, src, who, B_USER, scfg.my_host, '*', B_REALNAME) self.pushWhoisReply(312, src, who, scfg.my_host, scfg.my_name) self.pushWhoisReply(319, src, who, scfg.channel) else: n = self.ism.findDtellaNode(inick=who) if not (n and hasattr(n, 'hostmask')): return self.pushWhoisReply(311, src, who, n_user(n.ipp), n.hostmask, '*', "Dtella %s" % n.dttag[3:]) self.pushWhoisReply(312, src, who, scfg.my_host, scfg.my_name) self.pushWhoisReply(319, src, who, scfg.channel) if local.use_locations: self.pushWhoisReply( 320, src, who, "Location: %s" % local.hostnameToLocation(n.hostname)) self.pushWhoisReply(318, src, who, "End of /WHOIS list.") def pushWhoisReply(self, code, target, who, *strings): scfg = getServiceConfig() line = ":%s %d %s %s " % (scfg.my_host, code, target, who) strings = list(strings) strings[-1] = ":" + strings[-1] line += ' '.join(strings) self.sendLine(line) def handleCmd_PRIVMSG(self, prefix, args): src_nick = prefix target = args[0] text = args[1] flags = 0 if (text[:8], text[-1:]) == ('\001ACTION ', '\001'): text = text[8:-1] flags |= core.SLASHME_BIT text = irc_strip(text) scfg = getServiceConfig() if target == scfg.channel: self.ism.sendChannelMessage(src_nick, text, flags) #Format> :Global PRIVMSG $irc3.dhirc.com :TESTING.... #Handle global messages delivered to the bridge. elif target == "$" + scfg.my_host: flags |= core.NOTICE_BIT self.ism.sendChannelMessage(src_nick, text, flags) else: n = self.ism.findDtellaNode(inick=target) if n: self.ism.sendPrivateMessage(n, src_nick, text, flags) def handleCmd_NOTICE(self, prefix, args): src_nick = prefix target = args[0] text = irc_strip(args[1]) flags = core.NOTICE_BIT scfg = getServiceConfig() if target == scfg.channel: self.ism.sendChannelMessage(src_nick, text, flags) else: n = self.ism.findDtellaNode(inick=target) if n: self.ism.sendPrivateMessage(n, src_nick, text, flags) def pushNick(self, nick, ident, host, modes, ip, name): # If an IP was provided, convert to a base64 parameter. if ip: ip = ' ' + binascii.b2a_base64(ip).rstrip() else: ip = '' scfg = getServiceConfig() self.sendLine( "NICK %s 1 %d %s %s %s 1 +%s *%s :%s" % (nick, time.time(), ident, host, scfg.my_host, modes, ip, name)) def pushJoin(self, nick): scfg = getServiceConfig() self.sendLine(":%s JOIN %s" % (nick, scfg.channel)) def pushQuit(self, nick, reason=""): self.sendLine(":%s QUIT :%s" % (nick, reason)) def pushBotJoin(self, do_nick=False): scfg = getServiceConfig() if do_nick: self.pushNick(self.ism.bot_user.inick, B_USER, scfg.my_host, "Sq", None, B_REALNAME) # Join channel, and grant ops. self.pushJoin(self.ism.bot_user.inick) self.sendLine(":%s MODE %s +ao %s %s" % (scfg.my_host, scfg.channel, self.ism.bot_user.inick, self.ism.bot_user.inick)) def pushRemoveQLine(self, nickmask): scfg = getServiceConfig() LOG.info("Telling network to remove Q-line: %s" % nickmask) self.sendLine("TKL - Q * %s %s" % (nickmask, scfg.my_host)) def schedulePing(self): if self.ping_dcall: self.ping_dcall.reset(60.0) return def cb(): self.ping_dcall = None if self.ping_waiting: LOG.error("Ping timeout!") self.transport.loseConnection() else: scfg = getServiceConfig() self.sendLine("PING :%s" % scfg.my_host) self.ping_waiting = True self.ping_dcall = reactor.callLater(60.0, cb) self.ping_dcall = reactor.callLater(60.0, cb) def event_AddDtNode(self, n, ident): self.pushNick(n.inick, ident, n.hostname, "iwx", n.ipp[:4], "Dtella %s" % n.dttag[3:]) self.pushJoin(n.inick) def event_RemoveDtNode(self, n, reason): self.pushQuit(n.inick, reason) def event_KillUser(self, u): scfg = getServiceConfig() LOG.info("Killing nick: " + u.inick) self.sendLine(":%s KILL %s :%s (nick reserved for Dtella)" % (scfg.my_host, u.inick, scfg.my_host)) def event_NodeSetTopic(self, n, topic): scfg = getServiceConfig() self.sendLine(":%s TOPIC %s %s %d :%s" % (n.inick, scfg.channel, n.inick, time.time(), topic)) def event_Message(self, src_n, dst_u, text, action=False): scfg = getServiceConfig() if dst_u is None: target = scfg.channel else: target = dst_u.inick if action: text = "\001ACTION %s\001" % text self.sendLine(":%s PRIVMSG %s :%s" % (src_n.inick, target, text)) def event_Notice(self, src_n, dst_u, text): scfg = getServiceConfig() if dst_u is None: target = scfg.channel else: target = dst_u.inick self.sendLine(":%s NOTICE %s :%s" % (src_n.nick, target, text)) def shutdown(self): if self.shutdown_deferred: return self.shutdown_deferred # Remove nick ban self.pushRemoveQLine(cfg.dc_to_irc_prefix + "*") # Scream self.pushQuit(self.ism.bot_user.inick, "AIEEEEEEE!") # Send SQUIT for completeness scfg = getServiceConfig() self.sendLine(":%s SQUIT %s :Bridge Shutting Down" % (scfg.my_host, scfg.my_host)) # Close connection self.transport.loseConnection() # This will complete after loseConnection fires self.shutdown_deferred = defer.Deferred() return self.shutdown_deferred def connectionLost(self, result): LOG.info("Lost IRC connection.") if self.ism.syncd: self.ism.removeMeFromMain() if self.shutdown_deferred: self.shutdown_deferred.callback("Bye!")
class InspIRCdServer(LineOnlyReceiver): showirc = False def __init__(self, main): self.setupSID() self.uuid_counter = 0 self.ism = IRCStateManager( main=main, ircs=self, uuid_generator=self.generateUUID) self.server_name = None self.shutdown_deferred = None self.ping_dcall = None self.ping_waiting = False self.capabs = {} # The earliest observed channel creation time. self.chan_time = int(time.time()) # Generate a unique Q-line reason string, so we can detect if # another bridge races and steals our prefix. self.qline_reason = ( "Reserved for Dtella (%08X)" % random.randint(0, 0xFFFFFFFF)) def setupSID(self): scfg = getServiceConfig() if scfg.sid: self.sid = scfg.sid return # Generate a 3-digit SID, based on some config constants. # This isn't the same algorithm as inspircd.cpp, but it doesn't # really matter. input_str = "%s\0%s" % (scfg.my_host, scfg.my_name) sid, = struct.unpack("!I", sha256(input_str).digest()[:4]) self.sid = "%03d" % (sid % 999) def generateUUID(self): ctr = self.uuid_counter self.uuid_counter += 1 uuid = "" for i in range(6): uuid = UUID_DIGITS[ctr % len(UUID_DIGITS)] + uuid ctr /= len(UUID_DIGITS) # There are over 2 billion UUIDs; just reconnect if we run out. if ctr > 0: self.transport.loseConnection() raise ValueError("Ran out of UUIDs") return self.sid + uuid def connectionMade(self): scfg = getServiceConfig() LOG.info("Connected to IRC server.") self.challenge = make_challenge() my_capabs = [ ("NICKMAX", 32), ("HALFOP", 1), ("CHANMAX", 65), ("MAXMODES", 20), ("IDENTMAX", 12), ("MAXQUIT", 255), ("MAXTOPIC", 307), ("MAXKICK", 255), ("MAXGECOS", 128), ("MAXAWAY", 200), ("IP6SUPPORT", 1), ("PROTOCOL", 1201), ("CHALLENGE", self.challenge), ] my_capabs_str = ' '.join("%s=%s" % x for x in my_capabs) self.sendLine("CAPAB START") self.sendLine("CAPAB CAPABILITIES :%s" % my_capabs_str) self.sendLine("CAPAB END") def sendLine(self, line): line = line.replace('\r', '').replace('\n', '') print "<: %s" % line if self.showirc: LOG.log(5, "<: %s" % line) LineOnlyReceiver.sendLine(self, line) def lineReceived(self, line): if not line: return print ">: %s" % line if self.showirc: LOG.log(5, ">: %s" % line) if line[0] == ':': try: prefix, line = line[1:].split(' ', 1) except ValueError: return else: prefix = '' try: line, trailing = line.split(' :', 1) except ValueError: args = line.split() else: args = line.split() args.append(trailing) try: f = getattr(self, 'handleCmd_%s' % args[0].upper()) except AttributeError: pass else: f(prefix, args[1:]) def handleCmd_CAPAB(self, prefix, args): capab_mode = args[0] scfg = getServiceConfig() if capab_mode == "START": CHECK(not self.capabs) elif capab_mode == "CAPABILITIES": for capab in args[1].split(): k, v = capab.split("=", 1) self.capabs[k] = v elif capab_mode == "END": challenge = self.capabs["CHALLENGE"] response = ihmac(scfg.sendpass, challenge) self.sendLine( "SERVER %s %s 0 %s :%s" % (scfg.my_host, response, self.sid, scfg.my_name)) def handleCmd_PING(self, prefix, args): LOG.info("PING? PONG!") scfg = getServiceConfig() if len(args) == 1: self.sendLine(":%s PONG :%s" % (scfg.my_host, args[0])) elif len(args) == 2: self.sendLine("PONG %s :%s" % (args[1], args[0])) def handleCmd_PONG(self, prefix, args): if self.ping_waiting: self.ping_waiting = False self.schedulePing() def handleCmd_NICK(self, prefix, args): # :268AAAAAF NICK Paul 1238303566 old_uuid = prefix new_nick = args[0] try: u = self.ism.findUser(old_uuid) except KeyError: # This might be an echo from our KICK. LOG.warning("NICK: can't find source: %s" % old_uuid) return self.ism.changeNick(u, new_nick) def handleCmd_UID(self, prefix, args): pass # :268 UID 268AAAAAF 1238242868 Paul <host> <host> # paul <ip> 1238242873 +o :gecos nick = args[2] uuid = args[0] self.ism.addUser(nick, uuid) """ def handleCmd_SVSNICK(self, prefix, args): # :services.dhirc.com SVSNICK |foo Guest33400 :1236660594 oldnick = args[0] newnick = args[1] # Is the source user a Dtella node? Freak out. n = self.ism.findDtellaNode(inick=oldnick) if n: message = "Nick change to %s is impossible" % irc_to_dc(newnick) self.ism.kickDtellaNode(n, prefix, message) """ def handleCmd_JOIN(self, prefix, args): # :Paul JOIN #dtella 1237797449 nick = prefix chans = args[0].split(',') scfg = getServiceConfig() if scfg.channel in chans: self.ism.joinChannel(self.ism.findUser(nick)) def handleCmd_FJOIN(self, prefix, args): # :268 FJOIN #opers 1238298761 +nt :o,268AAAAAF v,268AAAAAE scfg = getServiceConfig() chan = args[0] if chan != scfg.channel: return # If we're not syncd yet, process the channel modes. if not self.ism.syncd: self.handleCmd_FMODE(prefix, args[:-1]) # Keep track of earliest creation time for this channel. self.chan_time = min(self.chan_time, int(args[1])) for uinfo in args[-1].split(): modes, uuid = uinfo.split(",", 1) u = self.ism.findUser(uuid) self.ism.joinChannel(u) changes = {} for m in modes: if m in scfg.chan_umodes.modes: changes[m] = True if changes: self.ism.setChannelUserModes("", u, changes) def handleCmd_PART(self, prefix, args): uuid = prefix chans = args[0].split(',') scfg = getServiceConfig() if scfg.channel in chans: CHECK(self.ism.partChannel(self.ism.findUser(uuid))) def handleCmd_QUIT(self, prefix, args): uuid = prefix try: u = self.ism.findUser(uuid) except KeyError: LOG.warning("Can't quit user: %s" % uuid) else: self.ism.removeUser(u) def handleCmd_KICK(self, prefix, args): chan = args[0] l33t = prefix n00b = args[1] reason = irc_strip(args[2]) scfg = getServiceConfig() if chan != scfg.channel: return # Rejoin the bot if it's killed. if self.ism.isMyBot(n00b): if self.ism.syncd: self.pushBotJoin() return l33t = self.ism.findUser(l33t).inick n = self.ism.findDtellaNode(inick=n00b) if n: self.ism.kickDtellaNode(n, l33t, reason) else: n00b_u = self.ism.findUser(n00b) message = ( "%s has kicked %s: %s" % (irc_to_dc(l33t), irc_to_dc(n00b_u.inick), reason)) CHECK(self.ism.partChannel(n00b_u, message)) def handleCmd_KILL(self, prefix, args): l33t = prefix n00b = args[0] reason = irc_strip(args[1]) # In most cases, n00b is a UUID, but Anope seems to still use a nick. # Thus, we have to try both everywhere :-/ # Reconnect the bot if it's killed. if self.ism.isMyBot(n00b): if self.ism.syncd: self.pushBotJoin(do_nick=True) return l33t = self.ism.findUser(l33t).inick # If n00b is a Dtella node, kick 'em. n = self.ism.findDtellaNode(inick=n00b) if n: self.ism.kickDtellaNode( n, l33t, "KILL: " + reason, send_quit=False) return # If n00b is an IRC user, treat it like a QUIT. try: n00b_u = self.ism.findUser(n00b) except KeyError: LOG.warning("Tried to KILL unknown user: %s" % n00b) return message = ( "%s has KILL'd %s: %s" % (irc_to_dc(l33t), irc_to_dc(n00b_u.inick), reason)) self.ism.removeUser(n00b_u, message) """ # Treat SVSKILL the same as KILL. handleCmd_SVSKILL = handleCmd_KILL """ def handleCmd_TOPIC(self, prefix, args): # :268AAAAAO TOPIC #dtella :hello world whoset_uuid = prefix chan = args[0] text = irc_strip(args[-1]) scfg = getServiceConfig() if chan == scfg.channel: whoset = self.ism.findUser(whoset_uuid).inick self.ism.setTopic(whoset, text) def handleCmd_FTOPIC(self, prefix, args): # :268 FTOPIC #dtella 1238306219 nick!host :hello chan = args[0] whoset = args[2].split('!', 1)[0] text = irc_strip(args[-1]) scfg = getServiceConfig() if chan == scfg.channel: self.ism.setTopic(whoset, text) def handleCmd_FMODE(self, prefix, args): # :268AAAAAF FMODE #dtella 1238298761 +h-o 268AAAAAF 268AAAAAF whoset_uuid = prefix chan = args[0] change = args[2] margs = args[3:] scfg = getServiceConfig() if chan != scfg.channel: return try: whoset = self.ism.findUser(whoset_uuid).inick except KeyError: # Could be a server? whoset = "" on_off = True i = 0 # User() -> {mode -> on_off} user_changes = {} # Dtella node modes that need unsetting. unset_modes = [] unset_uuids = [] for c in change: if c == '+': on_off = True elif c == '-': on_off = False elif c == 't': self.ism.setTopicLocked(whoset, on_off) elif c == 'm': self.ism.setModerated(whoset, on_off) elif c == 'k': # Skip over channel key i += 1 elif c == 'l': # Skip over channel user limit i += 1 elif c == 'b': banmask = margs[i] i += 1 self.ism.setChannelBan(whoset, on_off, banmask) elif c in scfg.chan_umodes.modes: # Grab affected user uuid = margs[i] i += 1 n = self.ism.findDtellaNode(inick=uuid) if n: # If someone set a mode for a Dt node, unset it. if on_off: unset_modes.append(c) unset_uuids.append(uuid) continue # Get the IRC user we're modifying. try: u = self.ism.findUser(uuid) except KeyError: LOG.error("MODE: unknown user: %s" % uuid) continue # Schedule a mode change for this user. user_changes.setdefault(u, {})[c] = on_off # Undo mode changes for Dtella nodes. if unset_modes: self.sendLine( ":%s FMODE %s %d -%s %s" % (self.ism.bot_user.uuid, scfg.channel, self.chan_time, ''.join(unset_modes), ' '.join(unset_uuids))) # Send IRC user mode changes to Dtella for u, changes in user_changes.iteritems(): self.ism.setChannelUserModes(whoset, u, changes) def handleCmd_ADDLINE(self, prefix, args): kind = args[0] # :268 ADDLINE Z 69.69.69.69 <Config> 1237917434 0 :hello if kind == 'Z': cidr = args[1] self.ism.setNetworkBan(cidr, True) # :268 ADDLINE Q [P]* setter 1238300707 0 :Reserved elif kind == 'Q': nickmask = args[1] timeset = int(args[3]) reason = args[-1] if self.ism.syncd and nickmask == cfg.dc_to_irc_prefix + "*": # If reason matches, it's a self-echo. Otherwise, someone # took my prefix first. if reason != self.qline_reason: LOG.error("Someone stole my Q-line! Terminating.") self.transport.loseConnection() reactor.stop() return self.ism.addQLine(nickmask, reason) def handleCmd_DELLINE(self, prefix, args): # :268 DELLINE <linetype> <mask> kind = args[0] if kind == 'Z': cidr = args[1] self.ism.setNetworkBan(cidr, False) elif kind == 'Q': nickmask = args[1] # Ignore the unsetting of my own prefix. # It might be an echo from my own unsetting during startup. if self.ism.syncd and nickmask == cfg.dc_to_irc_prefix + "*": return self.ism.removeQLine(nickmask) def handleCmd_SERVER(self, prefix, args): # :foo.dhirc.com SERVER s.dhirc.com * 1 00A :Services hostname = args[0] password = args[1] sid = args[3] # Register each server as a user, because it's easier. LOG.info("Recording server: hostname=%s sid=%s" % (hostname, sid)) self.ism.addUser(hostname, sid) if prefix: # Not my directly-connected server. return if self.server_name: # Could be a dupe? Ignore it. return # We got a reply from the our connected IRC server, so our password # was just accepted. Send the Dtella state information into IRC. scfg = getServiceConfig() # Save server name CHECK(args[0]) self.server_name = args[0] # Verify challenge response. if scfg.recvpass is None: LOG.info("Skipping validation of recvpass") else: ch_response = args[1] if ch_response != ihmac(scfg.recvpass, self.challenge): raise ValueError("Incorrect recvpass") LOG.info("Correct recvpass") LOG.info("IRC Server Name: %s" % self.server_name) # Tell the ReconnectingClientFactory that we're cool self.factory.resetDelay() self.sendLine("BURST %d" % time.time()) self.sendLine("ENDBURST") def handleCmd_SQUIT(self, prefix, args): # :n.dhirc.com SQUIT remote.dtella.org :Remote host closed hostname = args[0] try: # All servers have been registered as users. sid = self.ism.findUser(hostname).uuid.lower() if len(sid) != 3: raise KeyError except KeyError: LOG.error("SQUIT: unknown server: %s" % hostname) return # Find all the users belonging to this server. This should # never match nicks, because only UUIDs start with a number. remove_uuids = [uuid for uuid in self.ism.users if uuid.startswith(sid)] # Drop them off the network. for uuid in remove_uuids: LOG.info("SQUIT: removing user: %s" % uuid) self.ism.removeUser(self.ism.findUser(uuid)) def handleCmd_BURST(self, prefix, args): return #FIXME if self.ism.syncd: LOG.error("Can't handle BURST after sync. Restarting.") self.transport.loseConnection() def handleCmd_ENDBURST(self, prefix, args): if self.ism.syncd: # FIXME LOG.warning("Ignoring ENDBURST") return CHECK(self.server_name) LOG.info("Finished receiving IRC sync data.") self.showirc = True # Check for conflicting bridges. if self.ism.findConflictingBridge(): LOG.error("My nick prefix is in use! Terminating.") self.transport.loseConnection() reactor.stop() return # Set up nick reservation scfg = getServiceConfig() self.ism.killConflictingUsers() self.sendLine( ":%s ADDLINE Q %s* %s %d 0 :%s" % (self.sid, cfg.dc_to_irc_prefix, scfg.my_host, time.time(), self.qline_reason)) # Send my own bridge nick self.pushBotJoin(do_nick=True) # When we enter the syncd state, register this instance with Dtella. # This will eventually trigger event_DtellaUp, where we send our state. self.schedulePing() self.ism.addMeToMain() # FIXME: implement WHOIS """ def handleCmd_WHOIS(self, prefix, args): # Somewhat simplistic handling of WHOIS requests if not (prefix and len(args) >= 1): return src = prefix who = args[-1] scfg = getServiceConfig() if who == cfg.dc_to_irc_bot: self.pushWhoisReply( 311, src, who, B_USER, scfg.my_host, '*', B_REALNAME) self.pushWhoisReply( 312, src, who, scfg.my_host, scfg.my_name) self.pushWhoisReply( 319, src, who, scfg.channel) else: n = self.ism.findDtellaNode(inick=who) if not (n and hasattr(n, 'hostmask')): return self.pushWhoisReply( 311, src, who, n_user(n.ipp), n.hostmask, '*', "Dtella %s" % n.dttag[3:]) self.pushWhoisReply( 312, src, who, scfg.my_host, scfg.my_name) self.pushWhoisReply( 319, src, who, scfg.channel) if local.use_locations: self.pushWhoisReply( 320, src, who, "Location: %s" % local.hostnameToLocation(n.hostname)) self.pushWhoisReply( 318, src, who, "End of /WHOIS list.") def pushWhoisReply(self, code, target, who, *strings): scfg = getServiceConfig() line = ":%s %d %s %s " % (scfg.my_host, code, target, who) strings = list(strings) strings[-1] = ":" + strings[-1] line += ' '.join(strings) self.sendLine(line) """ def handleCmd_PRIVMSG(self, prefix, args): src_uuid = prefix target = args[0] text = args[1] flags = 0 if (text[:8], text[-1:]) == ('\001ACTION ', '\001'): text = text[8:-1] flags |= core.SLASHME_BIT text = irc_strip(text) src_nick = self.ism.findUser(src_uuid).inick scfg = getServiceConfig() if target == scfg.channel: self.ism.sendChannelMessage(src_nick, text, flags) # :Global PRIVMSG $irc3.dhirc.com :TESTING.... # Handle global messages delivered to the bridge. # FIXME: does this work with InspIRCd? elif target == "$" + scfg.my_host: flags |= core.NOTICE_BIT self.ism.sendChannelMessage(src_nick, text, flags) else: n = self.ism.findDtellaNode(inick=target) if n: self.ism.sendPrivateMessage(n, src_nick, text, flags) def handleCmd_NOTICE(self, prefix, args): src_uuid = prefix target = args[0] text = irc_strip(args[1]) flags = core.NOTICE_BIT src_nick = self.ism.findUser(src_uuid).inick scfg = getServiceConfig() if target == scfg.channel: self.ism.sendChannelMessage(src_nick, text, flags) else: n = self.ism.findDtellaNode(inick=target) if n: self.ism.sendPrivateMessage(n, src_nick, text, flags) def pushUID(self, uuid, nick, ident, host, cloak_host, modes, ip, gecos): # If an IP was provided, convert to a base64 parameter. if not ip: ip = '0.0.0.0' if not cloak_host: cloak_host = host scfg = getServiceConfig() now = time.time() self.sendLine( ":%s UID %s %d %s %s %s %s %s %d +%s :%s" % (self.sid, uuid, now, nick, host, cloak_host, ident, ip, now, modes, gecos)) def pushJoin(self, uuid, modes=""): scfg = getServiceConfig() self.sendLine( ":%s FJOIN %s %d + %s,%s" % (self.sid, scfg.channel, self.chan_time, modes, uuid)) def pushQuit(self, uuid, reason=""): self.sendLine(":%s QUIT :%s" % (uuid, reason)) def pushBotJoin(self, do_nick=False): scfg = getServiceConfig() uuid = self.ism.bot_user.uuid if do_nick: self.pushUID( uuid, self.ism.bot_user.inick, B_USER, scfg.my_host, None, "", None, B_REALNAME) # Join channel, and grant ops. self.pushJoin(uuid, "o") def pushRemoveQLine(self, nickmask): scfg = getServiceConfig() LOG.info("Telling network to remove Q-line: %s" % nickmask) self.sendLine(":%s DELLINE Q %s" % (self.sid, nickmask)) def schedulePing(self): if self.ping_dcall: self.ping_dcall.reset(60.0) return def cb(): self.ping_dcall = None if self.ping_waiting: LOG.error("Ping timeout!") self.transport.loseConnection() else: scfg = getServiceConfig() self.sendLine( ":%s PING %s" % (scfg.my_host, self.server_name)) self.ping_waiting = True self.ping_dcall = reactor.callLater(60.0, cb) self.ping_dcall = reactor.callLater(60.0, cb) def event_AddDtNode(self, n, ident): self.pushUID( n.uuid, n.inick, ident, n.hostname, n.hostmask, "iwx", Ad().setRawIPPort(n.ipp).getTextIP(), "Dtella %s" % n.dttag[3:]) self.pushJoin(n.uuid) def event_RemoveDtNode(self, n, reason): self.pushQuit(n.uuid, reason) def event_KillUser(self, u): scfg = getServiceConfig() LOG.info("Killing nick: " + u.inick) self.sendLine(":%s KILL %s :%s (nick reserved for Dtella)" % (self.sid, u.uuid, scfg.my_host)) def event_NodeSetTopic(self, n, topic): scfg = getServiceConfig() self.sendLine( ":%s TOPIC %s :%s" % (n.uuid, scfg.channel, topic)) def event_Message(self, src_n, dst_u, text, action=False): scfg = getServiceConfig() if dst_u is None: target = scfg.channel else: target = dst_u.uuid if action: text = "\001ACTION %s\001" % text self.sendLine(":%s PRIVMSG %s :%s" % (src_n.uuid, target, text)) def event_Notice(self, src_n, dst_u, text): scfg = getServiceConfig() if dst_u is None: target = scfg.channel else: target = dst_u.uuid self.sendLine(":%s NOTICE %s :%s" % (src_n.uuid, target, text)) def shutdown(self): if self.shutdown_deferred: return self.shutdown_deferred # Remove nick ban self.pushRemoveQLine(cfg.dc_to_irc_prefix + "*") # Scream self.pushQuit(self.ism.bot_user.uuid, "AIEEEEEEE!") # Send SQUIT for completeness scfg = getServiceConfig() self.sendLine(":%s SQUIT %s :Bridge Shutting Down" % (self.sid, scfg.my_host)) # Close connection self.transport.loseConnection() # This will complete after loseConnection fires self.shutdown_deferred = defer.Deferred() return self.shutdown_deferred def connectionLost(self, result): LOG.info("Lost IRC connection.") if self.ism.syncd: self.ism.removeMeFromMain() if self.shutdown_deferred: self.shutdown_deferred.callback("Bye!")
class InspIRCdServer(LineOnlyReceiver): showirc = False def __init__(self, main): self.setupSID() self.uuid_counter = 0 self.ism = IRCStateManager(main=main, ircs=self, uuid_generator=self.generateUUID) self.server_name = None self.shutdown_deferred = None self.ping_dcall = None self.ping_waiting = False self.capabs = {} # The earliest observed channel creation time. self.chan_time = int(time.time()) # Generate a unique Q-line reason string, so we can detect if # another bridge races and steals our prefix. self.qline_reason = ("Reserved for Dtella (%08X)" % random.randint(0, 0xFFFFFFFF)) def setupSID(self): scfg = getServiceConfig() if scfg.sid: self.sid = scfg.sid return # Generate a 3-digit SID, based on some config constants. # This isn't the same algorithm as inspircd.cpp, but it doesn't # really matter. input_str = "%s\0%s" % (scfg.my_host, scfg.my_name) sid, = struct.unpack("!I", sha256(input_str).digest()[:4]) self.sid = "%03d" % (sid % 999) def generateUUID(self): ctr = self.uuid_counter self.uuid_counter += 1 uuid = "" for i in range(6): uuid = UUID_DIGITS[ctr % len(UUID_DIGITS)] + uuid ctr /= len(UUID_DIGITS) # There are over 2 billion UUIDs; just reconnect if we run out. if ctr > 0: self.transport.loseConnection() raise ValueError("Ran out of UUIDs") return self.sid + uuid def connectionMade(self): scfg = getServiceConfig() LOG.info("Connected to IRC server.") self.challenge = make_challenge() my_capabs = [ ("NICKMAX", 32), ("HALFOP", 1), ("CHANMAX", 65), ("MAXMODES", 20), ("IDENTMAX", 12), ("MAXQUIT", 255), ("MAXTOPIC", 307), ("MAXKICK", 255), ("MAXGECOS", 128), ("MAXAWAY", 200), ("IP6SUPPORT", 1), ("PROTOCOL", 1201), ("CHALLENGE", self.challenge), ] my_capabs_str = ' '.join("%s=%s" % x for x in my_capabs) self.sendLine("CAPAB START") self.sendLine("CAPAB CAPABILITIES :%s" % my_capabs_str) self.sendLine("CAPAB END") def sendLine(self, line): line = line.replace('\r', '').replace('\n', '') print "<: %s" % line if self.showirc: LOG.log(5, "<: %s" % line) LineOnlyReceiver.sendLine(self, line) def lineReceived(self, line): if not line: return print ">: %s" % line if self.showirc: LOG.log(5, ">: %s" % line) if line[0] == ':': try: prefix, line = line[1:].split(' ', 1) except ValueError: return else: prefix = '' try: line, trailing = line.split(' :', 1) except ValueError: args = line.split() else: args = line.split() args.append(trailing) try: f = getattr(self, 'handleCmd_%s' % args[0].upper()) except AttributeError: pass else: f(prefix, args[1:]) def handleCmd_CAPAB(self, prefix, args): capab_mode = args[0] scfg = getServiceConfig() if capab_mode == "START": CHECK(not self.capabs) elif capab_mode == "CAPABILITIES": for capab in args[1].split(): k, v = capab.split("=", 1) self.capabs[k] = v elif capab_mode == "END": challenge = self.capabs["CHALLENGE"] response = ihmac(scfg.sendpass, challenge) self.sendLine("SERVER %s %s 0 %s :%s" % (scfg.my_host, response, self.sid, scfg.my_name)) def handleCmd_PING(self, prefix, args): LOG.info("PING? PONG!") scfg = getServiceConfig() if len(args) == 1: self.sendLine(":%s PONG :%s" % (scfg.my_host, args[0])) elif len(args) == 2: self.sendLine("PONG %s :%s" % (args[1], args[0])) def handleCmd_PONG(self, prefix, args): if self.ping_waiting: self.ping_waiting = False self.schedulePing() def handleCmd_NICK(self, prefix, args): # :268AAAAAF NICK Paul 1238303566 old_uuid = prefix new_nick = args[0] try: u = self.ism.findUser(old_uuid) except KeyError: # This might be an echo from our KICK. LOG.warning("NICK: can't find source: %s" % old_uuid) return self.ism.changeNick(u, new_nick) def handleCmd_UID(self, prefix, args): pass # :268 UID 268AAAAAF 1238242868 Paul <host> <host> # paul <ip> 1238242873 +o :gecos nick = args[2] uuid = args[0] self.ism.addUser(nick, uuid) """ def handleCmd_SVSNICK(self, prefix, args): # :services.dhirc.com SVSNICK |foo Guest33400 :1236660594 oldnick = args[0] newnick = args[1] # Is the source user a Dtella node? Freak out. n = self.ism.findDtellaNode(inick=oldnick) if n: message = "Nick change to %s is impossible" % irc_to_dc(newnick) self.ism.kickDtellaNode(n, prefix, message) """ def handleCmd_JOIN(self, prefix, args): # :Paul JOIN #dtella 1237797449 nick = prefix chans = args[0].split(',') scfg = getServiceConfig() if scfg.channel in chans: self.ism.joinChannel(self.ism.findUser(nick)) def handleCmd_FJOIN(self, prefix, args): # :268 FJOIN #opers 1238298761 +nt :o,268AAAAAF v,268AAAAAE scfg = getServiceConfig() chan = args[0] if chan != scfg.channel: return # If we're not syncd yet, process the channel modes. if not self.ism.syncd: self.handleCmd_FMODE(prefix, args[:-1]) # Keep track of earliest creation time for this channel. self.chan_time = min(self.chan_time, int(args[1])) for uinfo in args[-1].split(): modes, uuid = uinfo.split(",", 1) u = self.ism.findUser(uuid) self.ism.joinChannel(u) changes = {} for m in modes: if m in scfg.chan_umodes.modes: changes[m] = True if changes: self.ism.setChannelUserModes("", u, changes) def handleCmd_PART(self, prefix, args): uuid = prefix chans = args[0].split(',') scfg = getServiceConfig() if scfg.channel in chans: CHECK(self.ism.partChannel(self.ism.findUser(uuid))) def handleCmd_QUIT(self, prefix, args): uuid = prefix try: u = self.ism.findUser(uuid) except KeyError: LOG.warning("Can't quit user: %s" % uuid) else: self.ism.removeUser(u) def handleCmd_KICK(self, prefix, args): chan = args[0] l33t = prefix n00b = args[1] reason = irc_strip(args[2]) scfg = getServiceConfig() if chan != scfg.channel: return # Rejoin the bot if it's killed. if self.ism.isMyBot(n00b): if self.ism.syncd: self.pushBotJoin() return l33t = self.ism.findUser(l33t).inick n = self.ism.findDtellaNode(inick=n00b) if n: self.ism.kickDtellaNode(n, l33t, reason) else: n00b_u = self.ism.findUser(n00b) message = ("%s has kicked %s: %s" % (irc_to_dc(l33t), irc_to_dc(n00b_u.inick), reason)) CHECK(self.ism.partChannel(n00b_u, message)) def handleCmd_KILL(self, prefix, args): l33t = prefix n00b = args[0] reason = irc_strip(args[1]) # In most cases, n00b is a UUID, but Anope seems to still use a nick. # Thus, we have to try both everywhere :-/ # Reconnect the bot if it's killed. if self.ism.isMyBot(n00b): if self.ism.syncd: self.pushBotJoin(do_nick=True) return l33t = self.ism.findUser(l33t).inick # If n00b is a Dtella node, kick 'em. n = self.ism.findDtellaNode(inick=n00b) if n: self.ism.kickDtellaNode(n, l33t, "KILL: " + reason, send_quit=False) return # If n00b is an IRC user, treat it like a QUIT. try: n00b_u = self.ism.findUser(n00b) except KeyError: LOG.warning("Tried to KILL unknown user: %s" % n00b) return message = ("%s has KILL'd %s: %s" % (irc_to_dc(l33t), irc_to_dc(n00b_u.inick), reason)) self.ism.removeUser(n00b_u, message) """ # Treat SVSKILL the same as KILL. handleCmd_SVSKILL = handleCmd_KILL """ def handleCmd_TOPIC(self, prefix, args): # :268AAAAAO TOPIC #dtella :hello world whoset_uuid = prefix chan = args[0] text = irc_strip(args[-1]) scfg = getServiceConfig() if chan == scfg.channel: whoset = self.ism.findUser(whoset_uuid).inick self.ism.setTopic(whoset, text) def handleCmd_FTOPIC(self, prefix, args): # :268 FTOPIC #dtella 1238306219 nick!host :hello chan = args[0] whoset = args[2].split('!', 1)[0] text = irc_strip(args[-1]) scfg = getServiceConfig() if chan == scfg.channel: self.ism.setTopic(whoset, text) def handleCmd_FMODE(self, prefix, args): # :268AAAAAF FMODE #dtella 1238298761 +h-o 268AAAAAF 268AAAAAF whoset_uuid = prefix chan = args[0] change = args[2] margs = args[3:] scfg = getServiceConfig() if chan != scfg.channel: return try: whoset = self.ism.findUser(whoset_uuid).inick except KeyError: # Could be a server? whoset = "" on_off = True i = 0 # User() -> {mode -> on_off} user_changes = {} # Dtella node modes that need unsetting. unset_modes = [] unset_uuids = [] for c in change: if c == '+': on_off = True elif c == '-': on_off = False elif c == 't': self.ism.setTopicLocked(whoset, on_off) elif c == 'm': self.ism.setModerated(whoset, on_off) elif c == 'k': # Skip over channel key i += 1 elif c == 'l': # Skip over channel user limit i += 1 elif c == 'b': banmask = margs[i] i += 1 self.ism.setChannelBan(whoset, on_off, banmask) elif c in scfg.chan_umodes.modes: # Grab affected user uuid = margs[i] i += 1 n = self.ism.findDtellaNode(inick=uuid) if n: # If someone set a mode for a Dt node, unset it. if on_off: unset_modes.append(c) unset_uuids.append(uuid) continue # Get the IRC user we're modifying. try: u = self.ism.findUser(uuid) except KeyError: LOG.error("MODE: unknown user: %s" % uuid) continue # Schedule a mode change for this user. user_changes.setdefault(u, {})[c] = on_off # Undo mode changes for Dtella nodes. if unset_modes: self.sendLine( ":%s FMODE %s %d -%s %s" % (self.ism.bot_user.uuid, scfg.channel, self.chan_time, ''.join(unset_modes), ' '.join(unset_uuids))) # Send IRC user mode changes to Dtella for u, changes in user_changes.iteritems(): self.ism.setChannelUserModes(whoset, u, changes) def handleCmd_ADDLINE(self, prefix, args): kind = args[0] # :268 ADDLINE Z 69.69.69.69 <Config> 1237917434 0 :hello if kind == 'Z': cidr = args[1] self.ism.setNetworkBan(cidr, True) # :268 ADDLINE Q [P]* setter 1238300707 0 :Reserved elif kind == 'Q': nickmask = args[1] timeset = int(args[3]) reason = args[-1] if self.ism.syncd and nickmask == cfg.dc_to_irc_prefix + "*": # If reason matches, it's a self-echo. Otherwise, someone # took my prefix first. if reason != self.qline_reason: LOG.error("Someone stole my Q-line! Terminating.") self.transport.loseConnection() reactor.stop() return self.ism.addQLine(nickmask, reason) def handleCmd_DELLINE(self, prefix, args): # :268 DELLINE <linetype> <mask> kind = args[0] if kind == 'Z': cidr = args[1] self.ism.setNetworkBan(cidr, False) elif kind == 'Q': nickmask = args[1] # Ignore the unsetting of my own prefix. # It might be an echo from my own unsetting during startup. if self.ism.syncd and nickmask == cfg.dc_to_irc_prefix + "*": return self.ism.removeQLine(nickmask) def handleCmd_SERVER(self, prefix, args): # :foo.dhirc.com SERVER s.dhirc.com * 1 00A :Services hostname = args[0] password = args[1] sid = args[3] # Register each server as a user, because it's easier. LOG.info("Recording server: hostname=%s sid=%s" % (hostname, sid)) self.ism.addUser(hostname, sid) if prefix: # Not my directly-connected server. return if self.server_name: # Could be a dupe? Ignore it. return # We got a reply from the our connected IRC server, so our password # was just accepted. Send the Dtella state information into IRC. scfg = getServiceConfig() # Save server name CHECK(args[0]) self.server_name = args[0] # Verify challenge response. if scfg.recvpass is None: LOG.info("Skipping validation of recvpass") else: ch_response = args[1] if ch_response != ihmac(scfg.recvpass, self.challenge): raise ValueError("Incorrect recvpass") LOG.info("Correct recvpass") LOG.info("IRC Server Name: %s" % self.server_name) # Tell the ReconnectingClientFactory that we're cool self.factory.resetDelay() self.sendLine("BURST %d" % time.time()) self.sendLine("ENDBURST") def handleCmd_SQUIT(self, prefix, args): # :n.dhirc.com SQUIT remote.dtella.org :Remote host closed hostname = args[0] try: # All servers have been registered as users. sid = self.ism.findUser(hostname).uuid.lower() if len(sid) != 3: raise KeyError except KeyError: LOG.error("SQUIT: unknown server: %s" % hostname) return # Find all the users belonging to this server. This should # never match nicks, because only UUIDs start with a number. remove_uuids = [ uuid for uuid in self.ism.users if uuid.startswith(sid) ] # Drop them off the network. for uuid in remove_uuids: LOG.info("SQUIT: removing user: %s" % uuid) self.ism.removeUser(self.ism.findUser(uuid)) def handleCmd_BURST(self, prefix, args): return #FIXME if self.ism.syncd: LOG.error("Can't handle BURST after sync. Restarting.") self.transport.loseConnection() def handleCmd_ENDBURST(self, prefix, args): if self.ism.syncd: # FIXME LOG.warning("Ignoring ENDBURST") return CHECK(self.server_name) LOG.info("Finished receiving IRC sync data.") self.showirc = True # Check for conflicting bridges. if self.ism.findConflictingBridge(): LOG.error("My nick prefix is in use! Terminating.") self.transport.loseConnection() reactor.stop() return # Set up nick reservation scfg = getServiceConfig() self.ism.killConflictingUsers() self.sendLine(":%s ADDLINE Q %s* %s %d 0 :%s" % (self.sid, cfg.dc_to_irc_prefix, scfg.my_host, time.time(), self.qline_reason)) # Send my own bridge nick self.pushBotJoin(do_nick=True) # When we enter the syncd state, register this instance with Dtella. # This will eventually trigger event_DtellaUp, where we send our state. self.schedulePing() self.ism.addMeToMain() # FIXME: implement WHOIS """ def handleCmd_WHOIS(self, prefix, args): # Somewhat simplistic handling of WHOIS requests if not (prefix and len(args) >= 1): return src = prefix who = args[-1] scfg = getServiceConfig() if who == cfg.dc_to_irc_bot: self.pushWhoisReply( 311, src, who, B_USER, scfg.my_host, '*', B_REALNAME) self.pushWhoisReply( 312, src, who, scfg.my_host, scfg.my_name) self.pushWhoisReply( 319, src, who, scfg.channel) else: n = self.ism.findDtellaNode(inick=who) if not (n and hasattr(n, 'hostmask')): return self.pushWhoisReply( 311, src, who, n_user(n.ipp), n.hostmask, '*', "Dtella %s" % n.dttag[3:]) self.pushWhoisReply( 312, src, who, scfg.my_host, scfg.my_name) self.pushWhoisReply( 319, src, who, scfg.channel) if local.use_locations: self.pushWhoisReply( 320, src, who, "Location: %s" % local.hostnameToLocation(n.hostname)) self.pushWhoisReply( 318, src, who, "End of /WHOIS list.") def pushWhoisReply(self, code, target, who, *strings): scfg = getServiceConfig() line = ":%s %d %s %s " % (scfg.my_host, code, target, who) strings = list(strings) strings[-1] = ":" + strings[-1] line += ' '.join(strings) self.sendLine(line) """ def handleCmd_PRIVMSG(self, prefix, args): src_uuid = prefix target = args[0] text = args[1] flags = 0 if (text[:8], text[-1:]) == ('\001ACTION ', '\001'): text = text[8:-1] flags |= core.SLASHME_BIT text = irc_strip(text) src_nick = self.ism.findUser(src_uuid).inick scfg = getServiceConfig() if target == scfg.channel: self.ism.sendChannelMessage(src_nick, text, flags) # :Global PRIVMSG $irc3.dhirc.com :TESTING.... # Handle global messages delivered to the bridge. # FIXME: does this work with InspIRCd? elif target == "$" + scfg.my_host: flags |= core.NOTICE_BIT self.ism.sendChannelMessage(src_nick, text, flags) else: n = self.ism.findDtellaNode(inick=target) if n: self.ism.sendPrivateMessage(n, src_nick, text, flags) def handleCmd_NOTICE(self, prefix, args): src_uuid = prefix target = args[0] text = irc_strip(args[1]) flags = core.NOTICE_BIT src_nick = self.ism.findUser(src_uuid).inick scfg = getServiceConfig() if target == scfg.channel: self.ism.sendChannelMessage(src_nick, text, flags) else: n = self.ism.findDtellaNode(inick=target) if n: self.ism.sendPrivateMessage(n, src_nick, text, flags) def pushUID(self, uuid, nick, ident, host, cloak_host, modes, ip, gecos): # If an IP was provided, convert to a base64 parameter. if not ip: ip = '0.0.0.0' if not cloak_host: cloak_host = host scfg = getServiceConfig() now = time.time() self.sendLine(":%s UID %s %d %s %s %s %s %s %d +%s :%s" % (self.sid, uuid, now, nick, host, cloak_host, ident, ip, now, modes, gecos)) def pushJoin(self, uuid, modes=""): scfg = getServiceConfig() self.sendLine(":%s FJOIN %s %d + %s,%s" % (self.sid, scfg.channel, self.chan_time, modes, uuid)) def pushQuit(self, uuid, reason=""): self.sendLine(":%s QUIT :%s" % (uuid, reason)) def pushBotJoin(self, do_nick=False): scfg = getServiceConfig() uuid = self.ism.bot_user.uuid if do_nick: self.pushUID(uuid, self.ism.bot_user.inick, B_USER, scfg.my_host, None, "", None, B_REALNAME) # Join channel, and grant ops. self.pushJoin(uuid, "o") def pushRemoveQLine(self, nickmask): scfg = getServiceConfig() LOG.info("Telling network to remove Q-line: %s" % nickmask) self.sendLine(":%s DELLINE Q %s" % (self.sid, nickmask)) def schedulePing(self): if self.ping_dcall: self.ping_dcall.reset(60.0) return def cb(): self.ping_dcall = None if self.ping_waiting: LOG.error("Ping timeout!") self.transport.loseConnection() else: scfg = getServiceConfig() self.sendLine(":%s PING %s" % (scfg.my_host, self.server_name)) self.ping_waiting = True self.ping_dcall = reactor.callLater(60.0, cb) self.ping_dcall = reactor.callLater(60.0, cb) def event_AddDtNode(self, n, ident): self.pushUID(n.uuid, n.inick, ident, n.hostname, n.hostmask, "iwx", Ad().setRawIPPort(n.ipp).getTextIP(), "Dtella %s" % n.dttag[3:]) self.pushJoin(n.uuid) def event_RemoveDtNode(self, n, reason): self.pushQuit(n.uuid, reason) def event_KillUser(self, u): scfg = getServiceConfig() LOG.info("Killing nick: " + u.inick) self.sendLine(":%s KILL %s :%s (nick reserved for Dtella)" % (self.sid, u.uuid, scfg.my_host)) def event_NodeSetTopic(self, n, topic): scfg = getServiceConfig() self.sendLine(":%s TOPIC %s :%s" % (n.uuid, scfg.channel, topic)) def event_Message(self, src_n, dst_u, text, action=False): scfg = getServiceConfig() if dst_u is None: target = scfg.channel else: target = dst_u.uuid if action: text = "\001ACTION %s\001" % text self.sendLine(":%s PRIVMSG %s :%s" % (src_n.uuid, target, text)) def event_Notice(self, src_n, dst_u, text): scfg = getServiceConfig() if dst_u is None: target = scfg.channel else: target = dst_u.uuid self.sendLine(":%s NOTICE %s :%s" % (src_n.uuid, target, text)) def shutdown(self): if self.shutdown_deferred: return self.shutdown_deferred # Remove nick ban self.pushRemoveQLine(cfg.dc_to_irc_prefix + "*") # Scream self.pushQuit(self.ism.bot_user.uuid, "AIEEEEEEE!") # Send SQUIT for completeness scfg = getServiceConfig() self.sendLine(":%s SQUIT %s :Bridge Shutting Down" % (self.sid, scfg.my_host)) # Close connection self.transport.loseConnection() # This will complete after loseConnection fires self.shutdown_deferred = defer.Deferred() return self.shutdown_deferred def connectionLost(self, result): LOG.info("Lost IRC connection.") if self.ism.syncd: self.ism.removeMeFromMain() if self.shutdown_deferred: self.shutdown_deferred.callback("Bye!")