def addUser(self, inick, uuid=None): # Start tracking a new IRC user. CHECK(inick.lower() not in self.users) if self.uuid_generator: CHECK(number_prefix(uuid)) CHECK(uuid.lower() not in self.users) # If the nick is a uuid, it must be its own uuid. if number_prefix(inick): CHECK(inick.lower() == uuid.lower()) else: CHECK(uuid is None) u = User(inick, uuid) # Don't allow nicks which match my prefix. if self.syncd and matches_dc_to_irc_prefix(u.inick): if self.ircs: self.ircs.event_KillUser(u) return self.users[inick.lower()] = u if self.uuid_generator: self.users[uuid.lower()] = u return u
def event_KickMe(self, lines, rejoin_time): # Sequence of events during a kick: # 1. event_RemoveNick(*) # 2. event_DtellaDown() # 3. event_KickMe() # 4. stateChange_ObserverDown() # Node will become visible again if: # - Dtella node loses its connection # - User types !REJOIN # - DC client reconnects (creates a new DCHandler) CHECK(self.state == 'ready') self.state = 'invisible' for line in lines: self.pushStatus(line) if rejoin_time is None: return # Automatically rejoin the chat after a timeout period. def cb(): self.autoRejoin_dcall = None self.pushStatus("Automatically rejoining...") self.doRejoin() CHECK(self.autoRejoin_dcall is None) self.autoRejoin_dcall = reactor.callLater(rejoin_time, cb)
def removeUser(self, u, message=None): self.partChannel(u, message) # If inick is not a uuid, remove from index. if not (self.uuid_generator and number_prefix(u.inick)): CHECK(self.users.pop(u.inick.lower()) is u) # If a uuid is defined, remove from index. if self.uuid_generator: CHECK(self.users.pop(u.uuid.lower()) is u)
def changeNick(self, u, new_inick): old_inick = u.inick if old_inick.lower() == new_inick.lower(): # TODO: report case changes to Dtella? u.inick = new_inick return if self.uuid_generator and number_prefix(new_inick): # If nick is changing to a uuid, it must be its own uuid. CHECK(new_inick.lower() == u.uuid.lower()) else: # Otherwise, the nick must not already exist. CHECK(new_inick.lower() not in self.users) if not (self.uuid_generator and number_prefix(u.inick)): # If old nick is not a uuid, remove it from the index. CHECK(self.users.pop(u.inick.lower()) is u) # Don't allow IRC nicks which match my prefix. conflicted = (self.syncd and matches_dc_to_irc_prefix(new_inick)) if conflicted: # Report exit from Dtella before updating nick. self.partChannel(u) # Update nick. Note that UUID is unchanged. u.inick = new_inick self.users[new_inick.lower()] = u if conflicted: # Nick matches my prefix, diediedie! if self.ircs: self.ircs.event_KillUser(u) self.removeUser(u) return scfg = getServiceConfig() # Report the change to Dtella. if u in self.chanusers: try: osm = self.getOnlineStateManager() except NotOnline: return infoindex = scfg.chan_umodes.getUserInfoIndex(u) chunks = [] osm.bsm.addChatChunk( chunks, cfg.irc_to_dc_bot, "%s is now known as %s" % (irc_to_dc(old_inick), irc_to_dc(new_inick))) osm.bsm.addNickChunk(chunks, irc_to_dc(old_inick), 0xFF) osm.bsm.addNickChunk(chunks, irc_to_dc(new_inick), infoindex) osm.bsm.sendBridgeChange(chunks)
def findConflictingBridge(self): # Determine if another bridge conflicts with me. # Return True if we need to abort. CHECK(self.ircs and not self.syncd) stale_qlines = [] for nickmask, (q, reason) in self.qlines.iteritems(): # Look for Q-lines which conflict with my prefix. if not q.match(cfg.dc_to_irc_prefix): continue LOG.info("Found a conflicting Q-line: %s" % nickmask) # If any nicks exist under that Q-line, we'll need to abort. for u in set(self.users.itervalues()): if q.match(u.inick): LOG.info("... and a nick to go with it: %s" % u.inick) return True stale_qlines.append(nickmask) # Remove all stale Q-lines from the network. LOG.info("Stale qlines: %r" % stale_qlines) for nickmask in stale_qlines: del self.qlines[nickmask] self.ircs.pushRemoveQLine(nickmask) # Conflict has been neutralized. return False
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_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 cb(): self.sendState_dcall = None osm = self.main.osm CHECK(osm and osm.syncd) # Decide when to retransmit next when = 60 * 5 self.sendState_dcall = reactor.callLater(when, cb) # Broadcast header packet = osm.mrm.broadcastHeader('BS', osm.me.ipp) # The meat block_hashes, blocks = self.getStateData(packet) # Signature self.signPacket(packet, broadcast=True) # Broadcast status message osm.mrm.newMessage(''.join(packet), tries=8) # Broadcast data blocks # This could potentially be a bottleneck for slow connections for b in blocks: packet = osm.mrm.broadcastHeader('BB', osm.me.ipp) packet.append(self.nextPktNum()) packet.append(struct.pack("!H", len(b))) packet.append(b) osm.mrm.newMessage(''.join(packet), tries=4)
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()
def broadcastChatMessage(self, flags, text): CHECK(self.isOnline()) osm = self.main.osm if osm.isModerated(): # If the channel went moderated with something in the queue, # wipe it out and don't send. del self.chatq[:] return packet = osm.mrm.broadcastHeader('CH', osm.me.ipp) packet.append(struct.pack('!I', osm.mrm.getPacketNumber_chat())) packet.append(osm.me.nickHash()) packet.append(struct.pack('!BH', flags, len(text))) packet.append(text) osm.mrm.newMessage(''.join(packet), tries=4) # Echo back to the DC client if flags & core.SLASHME_BIT: nick = "*" text = "%s %s" % (osm.me.nick, text) else: nick = osm.me.nick self.pushChatMessage(nick, text)
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 event_DtellaDown(self): CHECK(self.isOnline()) # Wipe out the topic self.pushTopic() # Wipe out my outgoing chat queue del self.chatq[:]
def event_DtellaUp(self): CHECK(self.isOnline()) self.d_GetNickList() # Grab the current topic from Dtella. tm = self.main.osm.tm self.pushTopic(tm.topic) if tm.topic: self.pushStatus(tm.getFormattedTopic())
def __init__(self, *data): # *data is a sequence of (mode, friendly_name, info), # in order of decreasing awesomeness. # # mode is: # - A single character, for normal user modes. # - ":P", for plain users who have no modes assigned. # - ":V" for virtual nicks, not present in the channel. # # friendly_name is a string used in mode-change messages. # info is a DC-formatted info string. # # All modes must be unique. "" and None must be present. # string of mode characters, decreasing in awesomeness. self.modes = "" # Mapping from 'mode' -> infoindex. # Includes ":P" for plain, and ":V" for virtual. self.mode_to_index = {} self.friendly = [] self.info = [] for i, (mode, friendly, info) in enumerate(data): if len(mode) == 1: # Keep sorted string of normal modes. self.modes += mode elif mode in (":P", ":V"): pass else: raise ValueError("Invalid mode: " + mode) CHECK(mode not in self.mode_to_index) self.mode_to_index[mode] = i self.friendly.append(friendly) self.info.append(info) # Make sure plain and virtual were defined. CHECK(":P" in self.mode_to_index) CHECK(":V" in self.mode_to_index)
def killConflictingUsers(self): # Find any reserved nicks, and KILL them. CHECK(self.ircs and not self.syncd) bad_users = [ u for u in set(self.users.itervalues()) if matches_dc_to_irc_prefix(u.inick) ] LOG.info("Conflicting users: %r" % bad_users) for u in bad_users: self.ircs.event_KillUser(u) self.removeUser(u)
def removeIRCStateManager(self, ism): CHECK(ism and (self.ism is ism)) self.ism = None # Send empty IRC state to Dtella. self.stateChange_ObserverDown() osm = self.osm if osm: # Rebuild ban table. osm.banm.scheduleRebuildBans()
def event_ChatMessage(self, n, nick, text, flags): CHECK(not hasattr(n, 'dns_pending')) if not self.ircs: return if flags & core.NOTICE_BIT: self.ircs.event_Notice(n, None, text) elif flags & core.SLASHME_BIT: self.ircs.event_Message(n, None, text, action=True) else: self.ircs.event_Message(n, None, text)
def startConnecting(self): udp_state = self.ph.getSocketState() if udp_state == 'dead': try: reactor.listenUDP(cfg.udp_port, self.ph) except twisted.internet.error.BindError: LOG.error("Failed to bind UDP port!") raise SystemExit elif udp_state == 'dying': return CHECK(self.ph.getSocketState() == 'alive') self.startInitialContact()
def bridgeevent_TopicChange(self, n, topic): # Topic change, from a Dtella node. # Return True if successful. CHECK(self.syncd) if self.topic_locked: return False if self.ircs: self.ircs.event_NodeSetTopic(n, topic) self.setTopic(n.inick, topic) return True
def addDCHandler(self, dch): CHECK(not self.dch) CHECK(dch.state == 'ready') self.dch = dch # Cancel the disconnect timeout dcall_discard(self, 'disconnect_dcall') # Start connecting, or get status of current connection text = self.startConnecting() if text: # We must already be connecting/online. # Show the last status message. LOG.debug(text) dch.pushStatus(text) # Send a message if there's a newer version self.dcfg.resetReportedVersion() self.dcfg.reportNewVersion() self.stateChange_ObserverUp()
def sendBridgeChange(self, chunks): osm = self.main.osm CHECK(osm and osm.syncd) packet = osm.mrm.broadcastHeader('BC', osm.me.ipp) packet.append(self.nextPktNum()) chunks = ''.join(chunks) packet.append(struct.pack("!H", len(chunks))) packet.append(chunks) self.signPacket(packet, broadcast=True) osm.mrm.newMessage(''.join(packet), tries=4)
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 bridgeevent_PrivMsg(self, n, dst_inick, text): # Send a private message from Dtella to IRC. # Return True if successful. CHECK(self.syncd) try: u = self.findUser(dst_inick) except KeyError: return False if self.ircs: self.ircs.event_Message(n, u, text) return True return False
def queryLocation(self, my_ipp): # Try to convert the IP address into a human-readable location name. # This might be slightly more complicated than it really needs to be. CHECK(local.use_locations) ad = Ad().setRawIPPort(my_ipp) my_ip = ad.getTextIP() # Set my local ip in the state # self.state.local_ip = my_ip # self.state.saveState() skip = False for ip, loc in self.location.items(): if ip == my_ip: skip = True elif loc: # Forget old entries del self.location[ip] # If we already had an entry for this IP, then don't start # another lookup. if skip: return # A location of None indicates that a lookup is in progress self.location[my_ip] = None def cb(hostname): # Use local_config to transform this hostname into a # human-readable location loc = local.hostnameToLocation(hostname) # If we got a location, save it, otherwise dump the # dictionary entry if loc: self.location[my_ip] = loc else: del self.location[my_ip] # Maybe send an info update if self.osm: self.osm.updateMyInfo() # Start lookup ipToHostname(ad).addCallback(cb)
def bindUDPPort(self): # Returns True if the UDP port is bound udp_state = self.ph.getSocketState() if udp_state == 'alive': # Port already bound, yay! return True elif udp_state == 'dying': # Port is busy disconnecting. Wait. return False # Otherwise, the port is dead, so try to rebind it. CHECK(udp_state == 'dead') # Anhad addition, Uncomment for debugging try: # print 'port debugging' # print self.state.udp_port self.state.udp_port = local.default_udpport # print 'done port debugging' except: print 'Error setting default port' try: reactor.listenUDP(self.state.udp_port, self.ph) except twisted.internet.error.BindError: self.showLoginStatus("*** FAILED TO BIND UDP PORT ***") text = ( "Dtella was not able to listen on UDP port %d. One possible " "reason for this is that you've tried to make your DC " "client use the same UDP port as Dtella. Two programs " "are not allowed listen on the same port. To tell Dtella " "to use a different port, type !UDP followed by a number. " "Note that if you have a firewall or router, you will have " "to tell it to let traffic through on this port." % self.state.udp_port) for line in word_wrap(text): self.showLoginStatus(line) return self.ph.getSocketState() == 'alive'
def setDNSIPCache(self, data): CHECK(len(data) % 6 == 4) # 2011-08-21: New nodes ignore the value of 'when'. when, = struct.unpack("!I", data[:4]) ipps = [data[i:i + 6] for i in range(4, len(data), 6)] self.dns_ipcache = when, ipps # If DNS contains a foreign IP, add it to the exemption # list, so that it can function as a bridge or cache node. self.exempt_ips.clear() for ipp in ipps: ad = Ad().setRawIPPort(ipp) self.addExemptIP(ad)
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 sendPrivateBridgeChange(self, n, chunks): osm = self.main.osm ph = self.main.ph CHECK(osm and osm.syncd) chunks = ''.join(chunks) ack_key = self.nextPktNum() packet = ['bC'] packet.append(osm.me.ipp) packet.append(ack_key) packet.append(n.nickHash()) packet.append(struct.pack('!H', len(chunks))) packet.append(chunks) self.signPacket(packet, broadcast=False) packet = ''.join(packet) def fail_cb(detail): LOG.debug("bC failed: %s" % detail) n.sendPrivateMessage(ph, ack_key, packet, fail_cb)
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 showStats(self, out, title, compute, format, peers_only): CHECK(self.dch.isOnline()) # Count users and bytes ucount = {} bcount = {} # Collect user count and share size for n in self.main.osm.nkm.nickmap.values(): if peers_only and not n.is_peer: continue try: ucount[n.location] += 1 bcount[n.location] += n.shared except KeyError: ucount[n.location] = 1 bcount[n.location] = n.shared # Collect final values values = {} for loc in ucount: values[loc] = compute(ucount[loc], bcount[loc]) # Sort by value, in descending order locs = values.keys() locs.sort(key=lambda loc: values[loc], reverse=True) overall = compute(sum(ucount.values()), sum(bcount.values())) # Build info string and send it out("/== %s, by Location ==\\" % title) for loc in locs: out("| %s <= %s" % (format(values[loc]), loc)) out("|") out("\\_ Overall: %s _/" % format(overall))