diff --git a/docker/accessbot/accessbot.py b/docker/accessbot/accessbot.py index 0459ccdd30..38edc16ff2 100755 --- a/docker/accessbot/accessbot.py +++ b/docker/accessbot/accessbot.py @@ -34,7 +34,6 @@ class SetAccess(irc.client.SimpleIRCClient): def __init__(self, config, noop, nick, password, server, port): irc.client.SimpleIRCClient.__init__(self) - self.identify_msg_cap = False self.config = config self.nick = nick self.password = password @@ -44,6 +43,7 @@ class SetAccess(irc.client.SimpleIRCClient): self.channels = [x['name'] for x in self.config['channels']] self.current_channel = None self.current_list = [] + self.current_mode = '' self.changes = [] self.identified = False if self.port == 6697: @@ -56,30 +56,19 @@ class SetAccess(irc.client.SimpleIRCClient): def on_disconnect(self, connection, event): sys.exit(0) - def on_welcome(self, c, e): - self.identify_msg_cap = False - self.log.debug("Requesting identify-msg capability") - c.cap('REQ', 'identify-msg') - c.cap('END') - - def on_cap(self, c, e): - self.log.debug("Received cap response %s" % repr(e.arguments)) - if e.arguments[0] == 'ACK' and 'identify-msg' in e.arguments[1]: - self.log.debug("identify-msg cap acked") - self.identify_msg_cap = True - self.log.debug("Identifying to nickserv") - c.privmsg("nickserv", "identify %s " % self.password) - def on_privnotice(self, c, e): - if not self.identify_msg_cap: - self.log.debug("Ignoring message because identify-msg " - "cap not enabled") - return nick = e.source.split('!')[0] - auth = e.arguments[0][0] - msg = e.arguments[0][1:] - if auth == '+' and nick == 'NickServ' and not self.identified: - if msg.startswith('You are now identified'): + msg = e.arguments[0] + if nick == 'NickServ' and not self.identified: + if msg.startswith('authenticate yourself to services'): + self.log.debug("Identifying to nickserv") + # TODO (fungi): We should protect against sending our + # password to a false NickServ, perhaps with + # https://www.oftc.net/NickServ/CertFP/ or eventually + # SASL once the ircd implements that + c.privmsg("nickserv", "identify %s " % self.password) + return + elif msg.startswith('You are successfully identified'): self.identified = True # Prejoin and set ourselves as op in these channels, # to facilitate +f forwarding. @@ -88,8 +77,10 @@ class SetAccess(irc.client.SimpleIRCClient): c.privmsg("chanserv", "op #%s" % channel) self.advance() return - if auth != '+' or nick != 'ChanServ': - self.log.debug("Ignoring message from unauthenticated " + else: + return + if nick not in ('ChanServ', 'NickServ'): + self.log.debug("Ignoring message from non-ChanServ " "user %s" % nick) return self.failed = False @@ -99,95 +90,78 @@ class SetAccess(irc.client.SimpleIRCClient): ret = {} alumni = [] mode = '' + level = '' channel = None for c in self.config['channels']: if c['name'] == channel_name: channel = c if channel is None: raise Exception("Unknown channel %s" % (channel_name,)) - mask = '' - for access, nicks in (list(self.config['global'].items()) + + for key, value in (list(self.config['global'].items()) + list(channel.items())): - if access == 'mask': - mask = self.config['access'].get(nicks) + if key == 'alumni': + alumni += value continue - if access == 'alumni': - alumni += nicks + if key == 'mode': + mode = value continue - if access == 'mode': - mode = nicks - continue - flags = self.config['access'].get(access) - if flags is None: - continue - for nick in nicks: - ret[nick] = flags - return mask, ret, alumni, mode - def _get_access_change(self, current, target, mask): - remove = '' - add = '' - change = '' - for x in current: - if x in '+-': + # If we get this far, we assume the key is an access + # level matching an entry in the access list + level = self.config['access'].get(key) + if level is None: + # Skip if this doesn't match a defined access level continue - if target: - if x not in target: - remove += x - else: - if x not in mask: - remove += x - for x in target: - if x in '+-': - continue - if x not in current: - add += x - if remove: - change += '-' + remove - if add: - change += '+' + add - return change + for nick in value: + ret[nick] = level + return ret, alumni, mode + + def _get_access_change(self, current, target): + if current != target: + return target def _get_access_changes(self): - mask, target, alumni, mode = self._get_access_list(self.current_channel) - self.log.debug("Mask for %s: %s" % (self.current_channel, mask)) - self.log.debug("Target for %s: %s" % (self.current_channel, target)) + target, alumni, mode = self._get_access_list( + self.current_channel) + self.log.debug("Target #%s ACL: %s" % (self.current_channel, target)) all_nicks = set() global_alumni = self.config.get('alumni', {}) global_mode = self.config.get('mode', '') current = {} changes = [] - for nick, flags, msg in self.current_list: + for nick, level, msg in self.current_list: if nick in global_alumni or nick in alumni : self.log.debug("%s is an alumni; removing access", nick) changes.append('access #%s del %s' % (self.current_channel, nick)) continue all_nicks.add(nick) - current[nick] = flags + current[nick] = level for nick in target.keys(): all_nicks.add(nick) for nick in all_nicks: change = self._get_access_change(current.get(nick, ''), - target.get(nick, ''), mask) + target.get(nick, '')) if change: changes.append('access #%s add %s %s' % (self.current_channel, nick, change)) - # Set the mode. Note we always just hard-set the mode for - # simplicity (per the man page mlock always clears and sets - # anyway). Channel mode overrides global mode. - # - # Note for +f you need to be op in the target channel; see - # op_channel option. + # Set the mode if what we want differs from what's already there. + # Channel mode overrides global mode. if not mode and global_mode: mode = global_mode - self.log.debug("Setting mode to : %s" % mode) - if mode: + if not mode: + mode = '+' + if sorted(mode) != sorted(self.current_mode): + self.log.debug("Current mode for #%s is %s, replacing with %s" % ( + self.current_channel, self.current_mode, mode)) changes.append('set #%s mlock %s' % (self.current_channel, mode)) return changes def advance(self, msg=None): + # Some service responses include a number of embedded 0x02 bytes + if msg: + msg = msg.replace('\x02', '') if self.changes: if self.noop: for change in self.changes: @@ -204,19 +178,41 @@ class SetAccess(irc.client.SimpleIRCClient): self.connection.quit() return self.current_channel = self.channels.pop() + # Clear the mode string before we request it, so if we get + # no response we won't have the modes from an earlier channel + self.current_mode = '' + # Sending a set mlock with no value prompts the service to + # respond with the current mlock value so we can compare + # against it later + self.connection.privmsg('chanserv', 'set #%s mlock' % + self.current_channel) + # Clear the access list before we request it, so if we get + # no response we won't have the list from an earlier channel self.current_list = [] - self.connection.privmsg('chanserv', 'access list #%s' % + self.connection.privmsg('chanserv', 'access #%s list' % self.current_channel) time.sleep(1) return - if msg.startswith('End of'): + # We tokenize every server message, and perform some rough + # heuristics in order to determine what kind of response we're + # dealing with and whether it's something we know how to parse + parts = msg.split() + # If the third word look like an access level, assume this is + # an access list entry and that the second word is a + # corresponding nick + if parts[2] in ('MASTER', 'CHANOP', 'MEMBER'): + self.current_list.append((parts[1], parts[2], msg)) + # If the message starts with "MLOCK is SET to" then assume the + # fifth word is the channel's mode string + elif msg.startswith('MLOCK is SET to'): + self.current_mode = parts[4] + # If the message starts with "End of" then assume this marks + # the end of an access list + elif msg.startswith('End of'): self.changes = self._get_access_changes() self.current_channel = None self.advance() return - parts = msg.split() - if parts[2].startswith('+'): - self.current_list.append((parts[1], parts[2], msg)) def main():