def _sayit(irc, sid, userdict, channel, nick, text, action=False):
    """Mimic core function."""

    # Fix imperfections in the REGEX matching.
    nick = nick.strip(('\x03\x02\x01'))

    lowernick = irc.toLower(nick)
    uid = userdict.get(lowernick)
    log.debug('(%s) mimic: got UID %s for nick %s', irc.name, uid, nick)
    if not uid:  # Nick doesn't exist; make a new client for it.
        ircnick = nick
        ircnick = ircnick.replace('/', '|')
        if irc.nickToUid(nick):
            # Nick exists, but is not one of our temp users. Tag it with |mimic
            ircnick += MIMICSUFFIX

        if nick.startswith(tuple(string.digits)):
            ircnick = '_' + ircnick

        if not utils.isNick(ircnick):
            log.warning('(%s) mimic: Bad nick %s, ignoring text: <%s> %s', irc.name, ircnick, nick, text)
            return userdict

        userdict[lowernick] = uid = irc.proto.spawnClient(ircnick, 'mimic', HOSTNAME, server=sid).uid
        log.debug('(%s) mimic: spawning client %s for nick %s (really %s)', irc.name, uid, ircnick, nick)
        irc.proto.join(uid, channel)

    if action:  # Format CTCP action
        text = '\x01ACTION %s\x01' % text

    irc.proto.message(uid, channel, text)
    return userdict
Exemple #2
0
def handle_join(irc, source, command, args):
    """Monitors channel joins for dynamic service bot joining."""
    if irc.has_cap('visible-state-only'):
        # No-op on bot-only servers.
        return

    channel = args['channel']
    users = irc.channels[channel].users
    for servicename, sbot in world.services.items():
        if channel in sbot.get_persistent_channels(irc) and \
                sbot.uids.get(irc.name) not in users:
            log.debug('(%s) Dynamically joining service %r to channel %r.',
                      irc.name, servicename, channel)
            sbot.join(irc, channel)
Exemple #3
0
    def _burst_new_client(self, guild, member, pylink_netobj):
        """Bursts the given member as a new PyLink client."""
        uid = member.id

        if not member.name:
            log.debug('(%s) Not bursting user %s as their data is not ready yet', self.protocol.name, member)
            return

        if uid in pylink_netobj.users:
            log.debug('(%s) Not reintroducing user %s/%s', self.protocol.name, uid, member.user.username)
            pylink_user = pylink_netobj.users[uid]
        elif uid != self.me.id:
            tag = str(member.user)  # get their name#1234 tag
            username = member.user.username  # this is just the name portion
            realname = '%s @ Discord/%s' % (tag, guild.name)
            # Prefer the person's guild nick (nick=member.name) if defined
            pylink_netobj.users[uid] = pylink_user = User(pylink_netobj, nick=member.name,
                                                          ident=username, realname=realname,
                                                          host='discord/user/%s' % tag, # XXX make this configurable
                                                          ts=calendar.timegm(member.joined_at.timetuple()), uid=uid, server=guild.id)
            pylink_user.modes.add(('i', None))
            pylink_user.services_account = str(uid)  # Expose their UID as a services account
            if member.user.bot:
                pylink_user.modes.add(('B', None))
            pylink_user.discord_user = member

            pylink_netobj.call_hooks([
                guild.id,
                'UID',
                {
                    'uid': uid,
                    'ts': pylink_user.ts,
                    'nick': pylink_user.nick,
                    'realhost': pylink_user.realhost,
                    'host': pylink_user.host,
                    'ident': pylink_user.ident,
                    'ip': pylink_user.ip
                }
            ])
        else:
            return
        # Update user presence
        self._update_user_status(guild, uid, member.user.presence)

        # Calculate which channels the user belongs to
        for channel in guild.channels.values():
            if channel.type == ChannelType.GUILD_TEXT:
                self._update_channel_presence(guild, channel, member)
        return pylink_user
Exemple #4
0
    def handle_euid(self, numeric, command, args):
        """Handles incoming EUID commands (user introduction)."""
        # <- :42X EUID GL 1 1437505322 +ailoswz ~gl 127.0.0.1 127.0.0.1 42XAAAAAB * * :realname
        nick = args[0]
        self.check_nick_collision(nick)
        ts, modes, ident, host, ip, uid, realhost, accountname, realname = args[
            2:11]
        if realhost == '*':
            realhost = None

        log.debug(
            '(%s) handle_euid got args: nick=%s ts=%s uid=%s ident=%s '
            'host=%s realname=%s realhost=%s ip=%s', self.irc.name, nick, ts,
            uid, ident, host, realname, realhost, ip)
        assert ts != 0, "Bad TS 0 for user %s" % uid

        if ip == '0':  # IP was invalid; something used for services.
            ip = '0.0.0.0'

        self.irc.users[uid] = IrcUser(nick, ts, uid, numeric, ident, host,
                                      realname, realhost, ip)

        parsedmodes = self.irc.parseModes(uid, [modes])
        log.debug('Applying modes %s for %s', parsedmodes, uid)
        self.irc.applyModes(uid, parsedmodes)
        self.irc.servers[numeric].users.add(uid)

        # Call the OPERED UP hook if +o is being added to the mode list.
        if ('+o', None) in parsedmodes:
            otype = 'Server Administrator' if (
                '+a', None) in parsedmodes else 'IRC Operator'
            self.irc.callHooks([uid, 'CLIENT_OPERED', {'text': otype}])

        # Set the accountname if present
        if accountname != "*":
            self.irc.callHooks(
                [uid, 'CLIENT_SERVICES_LOGIN', {
                    'text': accountname
                }])

        return {
            'uid': uid,
            'ts': ts,
            'nick': nick,
            'realhost': realhost,
            'host': host,
            'ident': ident,
            'ip': ip
        }
Exemple #5
0
    def on_channel_delete(self, event, *args, **kwargs):
        channel = event.channel
        try:
            pylink_netobj = self.protocol._children[event.channel.guild_id]
        except KeyError:
            log.debug("(%s) Could not delete channel %s as the parent network object does not exist", self.protocol.name, event.channel)
            return

        if channel.id not in pylink_netobj.channels:  # wasn't a type of channel we track
            return

        # Remove the channel from everyone's channel list
        for u in pylink_netobj.channels[channel.id].users:
            pylink_netobj.users[u].channels.discard(channel.id)
        del pylink_netobj.channels[channel.id]
Exemple #6
0
    def handle_kick(self, source, command, args):
        """Handles incoming KICKs."""
        # :70MAAAAAA KICK #test 70MAAAAAA :some reason
        channel = args[0]
        kicked = self._get_UID(args[1])

        try:
            reason = args[2]
        except IndexError:
            reason = ''

        log.debug('(%s) Removing kick target %s from %s', self.name, kicked,
                  channel)
        self.handle_part(kicked, 'KICK', [channel, reason])
        return {'channel': channel, 'target': kicked, 'text': reason}
Exemple #7
0
 def _kill():
     if target not in irc.users:
         log.debug('(%s) antispam: not killing %s/%s; they already left',
                   irc.name, target, irc.get_friendly_name(target))
         return
     userdata = irc.users[target]
     irc.kill(my_uid, target, reason)
     irc.call_hooks([
         my_uid, 'ANTISPAM_KILL', {
             'target': target,
             'text': reason,
             'userdata': userdata,
             'parse_as': 'KILL'
         }
     ])
Exemple #8
0
def handle_kill(irc, numeric, command, args):
    """
    Tracks kills against PyLink clients. If too many are received,
    automatically disconnects from the network.
    """

    if (args['userdata'] and irc.isInternalServer(
            args['userdata'].server)) or irc.isInternalClient(args['target']):
        if killcache.setdefault(irc.name, 1) >= length:
            log.error('(%s) servprotect: Too many kills received, aborting!',
                      irc.name)
            irc.disconnect()

        log.debug('(%s) servprotect: Incrementing killcache by 1', irc.name)
        killcache[irc.name] += 1
Exemple #9
0
def _state_cleanup_mode(irc, source, command, args):
    """
    Cleans up and removes empty channels when -P (permanent mode) is removed from them.
    """
    target = args['target']
    if target in irc.channels and 'permanent' in irc.cmodes:
        c = irc.channels[target]
        mode = '-%s' % irc.cmodes['permanent']

        if (not c.users) and (mode, None) in args['modes']:
            log.debug(
                '(%s) _state_cleanup_mode: deleting empty channel %s as %s was set',
                irc.name, target, mode)
            del irc._channels[target]
            return False  # Block further hooks from running
Exemple #10
0
def main(irc=None):
    """Main function, called during plugin loading at start."""

    # Load the automode database.
    datastore.load()

    # Register our permissions.
    permissions.addDefaultPermissions(default_permissions)

    # Queue joins to all channels where Automode has entries.
    for entry in db:
        netname, channel = entry.split('#', 1)
        channel = '#' + channel
        log.debug('automode: auto-joining %s on %s', channel, netname)
        modebot.join(netname, channel)
Exemple #11
0
    def handle_mode(self, numeric, command, args):
        # <- :unreal.midnight.vpn MODE #test +bb test!*@* *!*@bad.net
        # <- :unreal.midnight.vpn MODE #test +q jlu5 1444361345
        # <- :unreal.midnight.vpn MODE #test +ntCo jlu5 1444361345
        # <- :unreal.midnight.vpn MODE #test +mntClfo 5 [10t]:5  jlu5 1444361345
        # <- :jlu5 MODE #services +v jlu5

        # This seems pretty relatively inconsistent - why do some commands have a TS at the end while others don't?
        # Answer: the first syntax (MODE sent by SERVER) is used for channel bursts - according to Unreal 3.2 docs,
        # the last argument should be interpreted as a timestamp ONLY if it is a number and the sender is a server.
        # Ban bursting does not give any TS, nor do normal users setting modes. SAMODE is special though, it will
        # send 0 as a TS argument (which should be ignored unless breaking the internal channel TS is desired).

        # Also, we need to get rid of that extra space following the +f argument. :|
        if self.is_channel(args[0]):
            channel = args[0]
            oldobj = self._channels[channel].deepcopy()

            modes = [arg for arg in args[1:] if arg]  # normalize whitespace
            parsedmodes = self.parse_modes(channel, modes)

            if parsedmodes:
                if parsedmodes[0][0] == '+&':
                    # UnrealIRCd uses a & virtual mode to denote mode bounces, meaning that an
                    # attempt to set modes by us was rejected for some reason (usually due to
                    # timestamps). Drop the mode change to prevent mode floods.
                    log.debug(
                        "(%s) Received mode bounce %s in channel %s! Our TS: %s",
                        self.name, modes, channel, self._channels[channel].ts)
                    return

                self.apply_modes(channel, parsedmodes)

            if numeric in self.servers and args[-1].isdigit():
                # Sender is a server AND last arg is number. Perform TS updates.
                their_ts = int(args[-1])
                if their_ts > 0:
                    self.updateTS(numeric, channel, their_ts)
            return {
                'target': channel,
                'modes': parsedmodes,
                'channeldata': oldobj
            }
        else:
            # User mode change
            target = self._get_UID(args[0])
            return self._handle_umode(target,
                                      self.parse_modes(target, args[1:]))
Exemple #12
0
    def handle_uid(self, numeric, command, args):
        """
        Handles Hybrid-style UID commands (user introduction). This is INCOMPATIBLE
        with standard TS6 implementations, as the arguments are slightly different.
        """
        # <- :0UY UID dan 1 1451041551 +Facdeiklosuw ~ident localhost 127.0.0.1 0UYAAAAAB * :realname
        nick = args[0]
        self.check_nick_collision(nick)
        ts, modes, ident, host, ip, uid, account, realname = args[2:10]
        if account == '*':
            account = None
        log.debug(
            '(%s) handle_uid: got args nick=%s ts=%s uid=%s ident=%s '
            'host=%s realname=%s ip=%s', self.irc.name, nick, ts, uid, ident,
            host, realname, ip)

        self.irc.users[uid] = IrcUser(nick, ts, uid, numeric, ident, host,
                                      realname, host, ip)

        parsedmodes = self.irc.parseModes(uid, [modes])
        log.debug('(%s) handle_uid: Applying modes %s for %s', self.irc.name,
                  parsedmodes, uid)
        self.irc.applyModes(uid, parsedmodes)
        self.irc.servers[numeric].users.add(uid)

        # Call the OPERED UP hook if +o is being added to the mode list.
        if ('+o', None) in parsedmodes:
            self.irc.callHooks(
                [uid, 'CLIENT_OPERED', {
                    'text': 'IRC_Operator'
                }])

        # Set the account name if present
        if account:
            self.irc.callHooks(
                [uid, 'CLIENT_SERVICES_LOGIN', {
                    'text': account
                }])

        return {
            'uid': uid,
            'ts': ts,
            'nick': nick,
            'realname': realname,
            'host': host,
            'ident': ident,
            'ip': ip
        }
Exemple #13
0
    def handle_kick(self, source, command, args):
        """
        Handles incoming KICKs.
        """
        # <- :[email protected] KICK #whatever GL| :xd
        channel = self.irc.toLower(args[0])
        target = self.irc.nickToUid(args[1])

        try:
            reason = args[2]
        except IndexError:
            reason = ''

        if channel in self.kick_queue:
            # Remove this client from the kick queue if present there.
            log.debug('(%s) kick: removing %s from kick queue for channel %s',
                      self.irc.name, target, channel)
            self.kick_queue[channel][0].discard(target)

            if not self.kick_queue[channel][0]:
                log.debug(
                    '(%s) kick: cancelling kick timer for channel %s (all kicks accounted for)',
                    self.irc.name, channel)
                # There aren't any kicks that failed to be acknowledged. We can remove the timer now
                self.kick_queue[channel][1].cancel()
                del self.kick_queue[channel]

        # Statekeeping: remove the target from the channel they were previously in.
        self.irc.channels[channel].removeuser(target)
        try:
            self.irc.users[target].channels.remove(channel)
        except KeyError:
            pass

        if (not self.irc.isInternalClient(source)
            ) and not self.irc.isInternalServer(source):
            # Don't repeat hooks if we're the kicker.
            self.irc.callHooks([
                source, 'KICK', {
                    'channel': channel,
                    'target': target,
                    'text': reason
                }
            ])

        # Delete channels that we were kicked from, for better state keeping.
        if self.irc.pseudoclient and target == self.irc.pseudoclient.uid:
            del self.irc.channels[channel]
def handle_uid(irc, source, command, args):
    """Checks incoming connections against SSHBL."""
    ip = args['ip']
    exemptions = irc.get_service_option('sshbl', 'exempt_hosts', default=None) or []
    if not irc.connected.is_set():
        # Don't scan users until we've finished bursting.
        return
    elif not ipaddress.ip_address(ip).is_global:
        log.debug("sshbl: skipping scanning local address %s", ip)
        return
    elif args['uid'] in irc.users and exemptions:
        for glob in exemptions:
            if irc.match_host(glob, args['uid']):
                log.debug("sshbl: skipping scanning exempt address %s (%s/%s)", ip, irc.name, args['nick'])
                return
    pool.submit(_check_connection, irc, args)
Exemple #15
0
    def join(self, client, channel):
        """STUB: Joins a user to a channel."""
        if self.pseudoclient and client == self.pseudoclient.uid:
            log.debug("(%s) discord: ignoring explicit channel join to %s",
                      self.name, channel)
            return
        elif channel not in self.channels:
            log.warning("(%s) Ignoring attempt to join unknown channel ID %s",
                        self.name, channel)
            return
        self.channels[channel].users.add(client)
        self.users[client].channels.add(channel)

        log.debug('(%s) join: faking JOIN of client %s/%s to %s', self.name,
                  client, self.get_friendly_name(client), channel)
        self.call_hooks([client, 'CLIENTBOT_JOIN', {'channel': channel}])
def _services_dynamic_part(irc, channel):
    """Dynamically removes service bots from empty channels."""
    if irc.has_cap('visible-state-only'):
        # No-op on bot-only servers.
        return
    if irc.serverdata.get('join_empty_channels', conf.conf['pylink'].get('join_empty_channels', False)):
        return

    # If all remaining users in the channel are service bots, make them all part.
    if all(irc.get_service_bot(u) for u in irc.channels[channel].users):
        for u in irc.channels[channel].users.copy():
            sbot = irc.get_service_bot(u)
            if sbot:
                log.debug('(%s) Dynamically parting service %r from channel %r.', irc.name, sbot.name, channel)
                irc.part(u, channel)
        return True
Exemple #17
0
    def sjoin(self, server, channel, users, ts=None, modes=set()):
        """Sends an SJOIN for a group of users to a channel.

        The sender should always be a Server ID (SID). TS is optional, and defaults
        to the one we've stored in the channel state if not given.
        <users> is a list of (prefix mode, UID) pairs:

        Example uses:
            sjoin('100', '#test', [('', 'user0@0'), ('o', user1@1'), ('v', 'someone@2')])
            sjoin(self.sid, '#test', [('o', self.pseudoclient.uid)])
        """

        server = server or self.sid
        if not server:
            raise LookupError('No such PyLink client exists.')
        log.debug('(%s) sjoin: got %r for users', self.name, users)

        njoin_prefix = ':%s NJOIN %s :' % (self._expandPUID(server), channel)
        # Format the user list into strings such as @user1, +user2, user3, etc.
        nicks_to_send = []
        for userpair in users:
            prefixes, uid = userpair

            if uid not in self.users:
                log.warning('(%s) Trying to NJOIN missing user %s?', self.name, uid)
                continue
            elif uid in self._channels[channel].users:
                # Don't rejoin users already in the channel, this causes errors with ngIRCd.
                continue

            self._channels[channel].users.add(uid)
            self.users[uid].channels.add(channel)

            self.apply_modes(channel, (('+%s' % prefix, uid) for prefix in userpair[0]))

            nicks_to_send.append(''.join(self.prefixmodes[modechar] for modechar in userpair[0]) + \
                                 self._expandPUID(userpair[1]))

        if nicks_to_send:
            # Use 13 args max per line: this is equal to the max of 15 minus the command name and target channel.
            for message in utils.wrap_arguments(njoin_prefix, nicks_to_send, self.S2S_BUFSIZE, separator=',', max_args_per_line=13):
                self.send(message)

        if modes:
            # Burst modes separately if there are any.
            log.debug("(%s) sjoin: bursting modes %r for channel %r now", self.name, modes, channel)
            self.mode(server, channel, modes)
Exemple #18
0
def account(irc, host, uid):
    """
    $account exttarget handler. The following forms are supported, with groups separated by a
    literal colon. Account matching is case insensitive, while network name matching IS case
    sensitive.

    $account -> Returns True (a match) if the target is registered.
    $account:accountname -> Returns True if the target's account name matches the one given, and the
    target is connected to the local network..
    $account:accountname:netname -> Returns True if both the target's account name and origin
    network name match the ones given.
    $account:*:netname -> Matches all logged in users on the given network.
    """
    userobj = irc.users[uid]
    homenet = irc.name
    if hasattr(userobj, 'remote'):
        # User is a PyLink Relay pseudoclient. Use their real services account on their
        # origin network.
        homenet, realuid = userobj.remote
        log.debug(
            '(%s) exttargets.account: Changing UID of relay client %s to %s/%s',
            irc.name, uid, homenet, realuid)
        try:
            userobj = world.networkobjects[homenet].users[realuid]
        except KeyError:  # User lookup failed. Bail and return False.
            log.exception('(%s) exttargets.account: KeyError finding %s/%s:',
                          irc.name, homenet, realuid)
            return False

    slogin = irc.toLower(userobj.services_account)

    # Split the given exttarget host into parts, so we know how many to look for.
    groups = list(map(irc.toLower, host.split(':')))
    log.debug('(%s) exttargets.account: groups to match: %s', irc.name, groups)

    if len(groups) == 1:
        # First scenario. Return True if user is logged in.
        return bool(slogin)
    elif len(groups) == 2:
        # Second scenario. Return True if the user's account matches the one given.
        return slogin == groups[1] and homenet == irc.name
    else:
        # Third or fourth scenario. If there are more than 3 groups, the rest are ignored.
        # In other words: Return True if the user is logged in, the query matches either '*' or the
        # user's login, and the user is connected on the network requested.
        return slogin and (groups[1] in ('*', slogin)) and (homenet
                                                            == groups[2])
Exemple #19
0
    def handle_server(self, numeric, command, args):
        """Handles the SERVER command, which is used for both authentication and
        introducing legacy (non-SID) servers."""
        # <- SERVER unreal.midnight.vpn 1 :U3999-Fhin6OoEM UnrealIRCd test server
        sname = args[0]
        if numeric == self.irc.uplink and not self.irc.connected.is_set(
        ):  # We're doing authentication
            for cap in self.needed_caps:
                if cap not in self.caps:
                    raise ProtocolError(
                        "Not all required capabilities were met "
                        "by the remote server. Your version of UnrealIRCd "
                        "is probably too old! (Got: %s, needed: %s)" %
                        (sorted(self.caps), sorted(self.needed_caps)))

            sdesc = args[-1].split(" ", 1)
            # Get our protocol version. I really don't know why the version and the server
            # description aren't two arguments instead of one... -GLolol
            vline = sdesc[0].split('-', 1)
            sdesc = " ".join(sdesc[1:])

            try:
                protover = int(vline[0].strip('U'))
            except ValueError:
                raise ProtocolError(
                    "Protocol version too old! (needs at least %s "
                    "(Unreal 4.x), got something invalid; "
                    "is VL being sent?)" % self.min_proto_ver)

            if protover < self.min_proto_ver:
                raise ProtocolError(
                    "Protocol version too old! (needs at least %s "
                    "(Unreal 4.x), got %s)" % (self.min_proto_ver, protover))
            self.irc.servers[numeric] = IrcServer(None, sname, desc=sdesc)

            # Set irc.connected to True, meaning that protocol negotiation passed.
            log.debug('(%s) self.irc.connected set!', self.irc.name)
            self.irc.connected.set()
        else:
            # Legacy (non-SID) servers can still be introduced using the SERVER command.
            # <- :services.int SERVER a.bc 2 :(H) [GL] a
            servername = args[0].lower()
            sdesc = args[-1]
            self.irc.servers[servername] = IrcServer(numeric,
                                                     servername,
                                                     desc=sdesc)
            return {'name': servername, 'sid': None, 'text': sdesc}
Exemple #20
0
    def handle_nick(self, numeric, command, args):
        """Handles NICK changes, and legacy NICK introductions from pre-4.0 servers."""
        if self.mixed_link and len(args) > 2:
            # Handle legacy NICK introduction here.
            # I don't want to rewrite all the user introduction stuff, so I'll just reorder the arguments
            # so that handle_uid can handle this instead.
            # But since legacy nicks don't have any UIDs attached, we'll have to store the users
            # internally by their nicks. In other words, we need to convert from this:
            #   <- NICK Global 3 1456843578 services novernet.com services.novernet.com 0 +ioS * :Global Noticer
            #   & nick hopcount timestamp username hostname server service-identifier-token :realname
            #   With NICKIP and VHP enabled:
            #   <- NICK GL32 2 1470699865 gl localhost unreal32.midnight.vpn GL +iowx hidden-1C620195 AAAAAAAAAAAAAAAAAAAAAQ== :realname
            # to this:
            #   <- :001 UID GL 0 1441306929 gl localhost 0018S7901 0 +iowx * hidden-1C620195 fwAAAQ== :realname
            log.debug('(%s) got legacy NICK args: %s', self.irc.name,
                      ' '.join(args))

            new_args = args[:]  # Clone the old args list
            servername = new_args[5].lower(
            )  # Get the name of the users' server.

            # Fake a UID and put it where it belongs in the new-style UID command.
            fake_uid = '%s@%s' % (args[0], self.legacy_nickcount)
            self.legacy_nickcount += 1
            new_args[5] = fake_uid

            # This adds a dummy cloaked host (equal the real host) to put the displayed host in the
            # right position. As long as the VHP capability is respected, this will propagate +x cloaked
            # hosts from UnrealIRCd 3.2 users. Otherwise, +x host cloaking won't work!
            new_args.insert(-2, args[4])

            log.debug('(%s) translating legacy NICK args to: %s',
                      self.irc.name, ' '.join(new_args))

            return self.handle_uid(servername, 'UID_LEGACY', new_args)
        else:
            # Normal NICK change, just let ts6_common handle it.
            # :70MAAAAAA NICK GL-devel 1434744242
            try:
                return super().handle_nick(numeric, command, args)
            except KeyError:
                log.exception(
                    '(%s) Malformed NICK command received. If you are linking PyLink to a '
                    'mixed UnrealIRCd 3.2/4.0 network, enable the mixed_link option in the '
                    'server config and restart your PyLink daemon.',
                    self.irc.name)
                self.irc.disconnect()
def _submit_dnsblim(irc, ip, apikey, nickuserhost=None):
    reason = irc.get_service_option('badchans', 'dnsbl_reason', DEFAULT_DNSBL_REASON)

    request = {
       'key': apikey,
       'addresses': [{
           'ip': ip,
           'type': str(DNSBLIM_TYPE),
           'reason': reason,
       }],
    }
    headers = {'Content-Type': 'application/json'}

    log.debug('(%s) badchans: posting to dnsblim: %s', irc.name, request)

    # Expecting this to block
    r = requests.post('https://api.dnsbl.im/import', data=json.dumps(request), headers=headers)
Exemple #22
0
    def serverdata(self):
        """
        Implements serverdata property for Discord subservers. This merges in the root serverdata config
        block, plus any guild-specific settings for this guild.

        NOTE: serverdata in DiscordServer is not modifiable because it is dynamically generated.
        Changes should instead be made to self.virtual_parent.serverdata
        """
        if getattr(self, 'sid', None):
            data = self.virtual_parent.serverdata.copy()
            guild_data = data.get('guilds', {}).get(self.sid, {})
            log.debug('serverdata: merging data %s with guild_data %s', data, guild_data)
            data.update(guild_data)
            return data
        else:
            log.debug('serverdata: sid not set, using parent data only')
            return self.virtual_parent.serverdata
Exemple #23
0
    def _process_hooks(self):
        """Loop to process incoming hook data."""
        while not self._aborted.is_set():
            data = self._hooks_queue.get()
            if data is None:
                log.debug('(%s) Stopping queue thread due to getting None as item', self.name)
                break
            elif self not in world.networkobjects.values():
                log.debug('(%s) Stopping stale queue thread; no longer matches world.networkobjects', self.name)
                break

            subserver, data = data
            if subserver not in world.networkobjects:
                log.error('(%s) Not queuing hook for subserver %r no longer in networks list.',
                          self.name, subserver)
            elif subserver in self._children:
                self._children[subserver].call_hooks(data)
Exemple #24
0
    def handle_server(self, source, command, args):
        """
        Handles the SERVER command.
        """
        # <- :ngircd.midnight.local SERVER ngircd.midnight.local 1 :ngIRCd dev server
        servername = args[0].lower()
        serverdesc = args[-1]

        # The uplink should be set to None for the uplink; otherwise, set it equal to the sender server.
        self.servers[servername] = Server(self, source if source != servername else None, servername, desc=serverdesc)

        if self.uplink is None:
            self.uplink = servername
            log.debug('(%s) Got %s as uplink', self.name, servername)
        else:
            # Only send the SERVER hook if this isn't the initial connection.
            return {'name': servername, 'sid': None, 'text': serverdesc}
Exemple #25
0
def handle_ctcp(irc, source, command, args):
    """
    CTCP event handler.
    """
    text = args['text']
    if not (text.startswith('\x01') and text.endswith('\x01')):
        return None  # Pass through to other plugins

    target = args['target']
    if not irc.get_service_bot(target):
        # Ignore this message if the target isn't a service bot
        return None

    text = text.strip('\x01')
    try:
        ctcp_command, data = text.split(" ", 1)
    except ValueError:
        ctcp_command = text
        data = ''

    ctcp_command = ctcp_command.upper()
    log.debug('(%s) ctcp: got CTCP command %r, data %r', irc.name,
              ctcp_command, data)

    if ctcp_command in SUPPORTED_COMMANDS:
        log.info('(%s) Received CTCP %s from %s to %s', irc.name, ctcp_command,
                 irc.get_hostmask(source), irc.get_friendly_name(target))

        # Call the helper function and display its result.
        result = SUPPORTED_COMMANDS[ctcp_command](irc, source, ctcp_command,
                                                  data)
        if result and source in irc.users:
            # Note, do NOT use irc.reply() in hook handlers because nothing except the
            # command handler system actually updates the last caller.
            irc.msg(source,
                    '\x01%s %s\x01' % (ctcp_command, result),
                    notice=True,
                    source=target)

        return False  # Block this message from reaching the general command handler
    else:
        log.info('(%s) Received unknown CTCP %s from %s to %s', irc.name,
                 ctcp_command, irc.get_hostmask(source),
                 irc.get_friendly_name(target))
        return False
Exemple #26
0
def match(irc, channel, uids=None):
    """
    Automode matcher engine.
    """
    dbentry = db.get(irc.name + channel)
    if dbentry is None:
        return

    modebot_uid = modebot.uids.get(irc.name)

    # Check every mask defined in the channel ACL.
    outgoing_modes = []

    # If UIDs are given, match those. Otherwise, match all users in the given channel.
    uids = uids or irc.channels[channel].users

    for mask, modes in dbentry.items():
        for uid in uids:
            if irc.matchHost(mask, uid):
                # User matched a mask. Filter the mode list given to only those that are valid
                # prefix mode characters.
                outgoing_modes += [('+' + mode, uid) for mode in modes
                                   if mode in irc.prefixmodes]
                log.debug(
                    "(%s) automode: Filtered mode list of %s to %s (protocol:%s)",
                    irc.name, modes, outgoing_modes, irc.protoname)

    if outgoing_modes:
        # If the Automode bot is missing, send the mode through the PyLink server.
        if modebot_uid not in irc.users:
            modebot_uid = irc.sid

        log.debug("(%s) automode: sending modes from modebot_uid %s", irc.name,
                  modebot_uid)

        irc.proto.mode(modebot_uid, channel, outgoing_modes)

        # Create a hook payload to support plugins like relay.
        irc.callHooks([
            modebot_uid, 'AUTOMODE_MODE', {
                'target': channel,
                'modes': outgoing_modes,
                'parse_as': 'MODE'
            }
        ])
Exemple #27
0
def _rehash():
    """Rehashes the PyLink daemon."""
    log.info('Reloading PyLink configuration...')
    old_conf = conf.conf.copy()
    fname = conf.fname
    new_conf = conf.loadConf(fname, errors_fatal=False, logger=log)
    conf.conf = new_conf

    # Reset any file logger options.
    stopFileLoggers()
    files = new_conf['logging'].get('files')
    if files:
        for filename, config in files.items():
            makeFileLogger(filename, config.get('loglevel'))

    log.debug('rehash: updating console log level')
    world.console_handler.setLevel(getConsoleLogLevel())

    # Reset permissions.
    log.debug('rehash: resetting permissions')
    permissions.resetPermissions()

    for network, ircobj in world.networkobjects.copy().items():
        # Server was removed from the config file, disconnect them.
        log.debug('rehash: checking if %r is in new conf still.', network)
        if network not in new_conf['servers']:
            log.debug(
                'rehash: removing connection to %r (removed from config).',
                network)
            remove_network(ircobj)
        else:
            # XXX: we should really just add abstraction to Irc to update config settings...
            ircobj.conf = new_conf
            ircobj.serverdata = new_conf['servers'][network]
            ircobj.botdata = new_conf['bot']
            ircobj.autoconnect_active_multiplier = 1

            # Clear the IRC object's channel loggers and replace them with
            # new ones by re-running logSetup().
            while ircobj.loghandlers:
                log.removeHandler(ircobj.loghandlers.pop())

            ircobj.logSetup()

    utils.resetModuleDirs()

    for network, sdata in new_conf['servers'].items():
        # Connect any new networks or disconnected networks if they aren't already.
        if (network not in world.networkobjects) or (
                not world.networkobjects[network].connection_thread.is_alive()
        ):
            proto = utils.getProtocolModule(sdata['protocol'])
            world.networkobjects[network] = classes.Irc(
                network, proto, new_conf)
    log.info('Finished reloading PyLink configuration.')
Exemple #28
0
def fml(irc, source, args):
    """[<id>]

    Displays an entry from fmylife.com. If <id> is not given, fetch a random entry from the API."""
    try:
        query = args[0]
    except IndexError:
        # Get a random FML from the API.
        query = 'random'

    # TODO: configurable language?
    url = ('http://api.betacie.com/view/%s/nocomment'
           '?key=4be9c43fc03fe&language=en' % query)
    try:
        data = urllib.request.urlopen(url).read()
    except urllib.error as e:
        error(irc, '%s' % e)
        return

    tree = ElementTree.fromstring(data.decode('utf-8'))
    tree = tree.find('items/item')

    try:
        category = tree.find('category').text
        text = tree.find('text').text
        fmlid = tree.attrib['id']
        url = tree.find('short_url').text
    except AttributeError as e:
        log.debug("games.FML: Error fetching FML %s from URL %s: %s", query,
                  url, e)
        error(
            irc, "That FML does not exist or there was an error "
            "fetching data from the API.")
        return

    if not fmlid:
        error(irc, "That FML does not exist.")
        return

    # TODO: customizable formatting
    votes = "\x02[Agreed: %s / Deserved: %s]\x02" % \
            (tree.find('agree').text, tree.find('deserved').text)
    s = '\x02#%s [%s]\x02: %s - %s \x02<\x0311%s\x03>\x02' % \
        (fmlid, category, text, votes, url)
    reply(irc, s)
Exemple #29
0
def ircop(irc, host, uid):
    """
    $ircop exttarget handler. The following forms are supported, with groups separated by a
    literal colon. Oper types are matched case insensitively.

    $ircop -> Returns True (a match) if the target is opered.
    $ircop:*admin* -> Returns True if the target's is opered and their opertype matches the glob
    given.
    """
    groups = host.split(':')
    log.debug('(%s) exttargets.ircop: groups to match: %s', irc.name, groups)

    if len(groups) == 1:
        # 1st scenario.
        return irc.is_oper(uid)
    else:
        # 2nd scenario. Use match_host (ircmatch) to match the opertype glob to the opertype.
        return irc.match_host(groups[1], irc.users[uid].opertype)
Exemple #30
0
def pylinkacc(irc, host, uid):
    """
    $pylinkacc (PyLink account) exttarget handler. The following forms are supported, with groups
    separated by a literal colon. Account matching is case insensitive.

    $pylinkacc -> Returns True if the target is logged in to PyLink.
    $pylinkacc:accountname -> Returns True if the target's PyLink login matches the one given.
    """
    login = irc.to_lower(irc.users[uid].account)
    groups = list(map(irc.to_lower, host.split(':')))
    log.debug('(%s) exttargets.pylinkacc: groups to match: %s', irc.name, groups)

    if len(groups) == 1:
        # First scenario. Return True if user is logged in.
        return bool(login)
    elif len(groups) == 2:
        # Second scenario. Return True if the user's login matches the one given.
        return login == groups[1]
Exemple #31
0
def _kill_plugins(irc=None):
    if not world.plugins:
        # No plugins were loaded or we were in a pre-initialized state, ignore.
        return

    log.info("Shutting down plugins.")
    for name, plugin in world.plugins.items():
        # Before closing connections, tell all plugins to shutdown cleanly first.
        if hasattr(plugin, 'die'):
            log.debug(
                'coremods.control: Running die() on plugin %s due to shutdown.',
                name)
            try:
                plugin.die(irc)
            except:  # But don't allow it to crash the server.
                log.exception(
                    'coremods.control: Error occurred in die() of plugin %s, skipping...',
                    name)
Exemple #32
0
def raw(irc, source, args):
    """<text>

    Admin-only. Sends raw text to the uplink IRC server.

    \x02**WARNING: THIS CAN BREAK YOUR NETWORK IF USED IMPROPERLY!**\x02"""
    permissions.checkPermissions(irc, source, ['exec.raw'])

    args = ' '.join(args)
    if not args.strip():
        irc.reply('No text entered!')
        return

    log.debug('(%s) Sending raw text %r to IRC for %s', irc.name, args,
              irc.getHostmask(source))
    irc.send(args)

    irc.reply("Done.")
def _should_enforce(irc, source, args):
    exemptions = irc.get_service_option('operlock', 'exempt_hosts', default=None) or []
    if not irc.connected.is_set():
        # Don't act users until we've finished bursting.
        return False
    elif source not in irc.users:
        # Target isn't a user
        return False
    elif irc.is_internal_client(source):
        # Don't enforce against internal clients.
        return

    # Ignore exempt hosts
    for glob in exemptions:
        if irc.match_host(glob, source):
            log.debug("(%s) operlock: skipping exempt user %s", irc.name, irc.get_friendly_name(source))
            return False
    return True
Exemple #34
0
    def on_member_remove(self, event: GuildMemberRemove, *args, **kwargs):
        log.info('(%s) got GuildMemberRemove event for guild %s: %s',
                 self.protocol.name, event.guild_id, event.user)
        try:
            pylink_netobj = self.protocol._children[event.guild_id]
        except KeyError:
            log.debug(
                "(%s) Could not remove user %s as the parent network object does not exist",
                self.protocol.name, event.user)
            return

        if event.user.id in pylink_netobj.users:
            pylink_netobj._remove_client(event.user.id)
            # XXX: make the message configurable
            pylink_netobj.call_hooks(
                [event.user.id, 'QUIT', {
                    'text': 'User left the guild'
                }])
def _submit_dronebl(irc, ip, apikey, nickuserhost=None):
    reason = irc.get_service_option('badchans', 'dnsbl_reason', DEFAULT_DNSBL_REASON)

    request = '<add ip="%s" type="%s" comment="%s" />' % (ip, DRONEBL_TYPE, reason)
    xml_data = '<?xml version="1.0"?><request key="%s">%s</request>' % (apikey, request)
    headers = {'Content-Type': 'text/xml'}

    log.debug('(%s) badchans: posting to dronebl: %s', irc.name, xml_data)

    # Expecting this to block
    r = requests.post('https://dronebl.org/RPC2', data=xml_data, headers=headers)

    dronebl_response = r.text

    log.debug('(%s) badchans: got response from dronebl: %s', irc.name, dronebl_response)
    if '<success' in dronebl_response:
        log.info('(%s) badchans: got success for DroneBL on %s (%s)', irc.name, ip, nickuserhost or 'some n!u@h')
    else:
        log.warning('(%s) badchans: dronebl submission error: %s', irc.name, dronebl_response)
def handle_join(irc, source, command, args):
    """
    killonjoin JOIN listener.
    """
    # Ignore our own clients and other Ulines
    if irc.is_privileged_service(source) or irc.is_internal_server(source):
        return

    badchans = irc.serverdata.get('badchans')
    if not badchans:
        return
    elif not isinstance(badchans, list):
        log.error("(%s) badchans: the 'badchans' option must be a list of strings, not a %s", irc.name, type(badchans))
        return

    use_kline = irc.get_service_option('badchans', 'use_kline', False)
    kline_duration = irc.get_service_option('badchans', 'kline_duration', DEFAULT_BAN_DURATION)
    try:
        kline_duration = utils.parse_duration(kline_duration)
    except ValueError:
        log.warning('(%s) badchans: invalid kline duration %s', irc.name, kline_duration, exc_info=True)
        kline_duration = DEFAULT_BAN_DURATION

    channel = args['channel']
    for badchan in badchans:
        if irc.match_text(badchan, channel):
            asm_uid = None
            # Try to kill from the antispam service if available
            if 'antispam' in world.services:
                asm_uid = world.services['antispam'].uids.get(irc.name)

            for user in args['users']:
                try:
                    ip = irc.users[user].ip
                    ipa = ipaddress.ip_address(ip)
                except (KeyError, ValueError):
                    log.error("(%s) badchans: could not obtain IP of user %s", irc.name, user)
                    continue
                nuh = irc.get_hostmask(user)

                exempt_hosts = set(conf.conf.get('badchans', {}).get('exempt_hosts', [])) | \
                             set(irc.serverdata.get('badchans_exempt_hosts', []))

                if not ipa.is_global:
                    irc.msg(user, "Warning: %s kills unopered users, but non-public addresses are exempt." % channel,
                            notice=True,
                            source=asm_uid or irc.pseudoclient.uid)
                    continue

                if exempt_hosts:
                    skip = False
                    for glob in exempt_hosts:
                        if irc.match_host(glob, user):
                            log.info("(%s) badchans: ignoring exempt user %s on %s (%s)", irc.name, nuh, channel, ip)
                            irc.msg(user, "Warning: %s kills unopered users, but your host is exempt." % channel,
                                    notice=True,
                                    source=asm_uid or irc.pseudoclient.uid)
                            skip = True
                            break
                    if skip:
                        continue

                if irc.is_oper(user):
                    irc.msg(user, "Warning: %s kills unopered users!" % channel,
                            notice=True,
                            source=asm_uid or irc.pseudoclient.uid)
                else:
                    log.info('(%s) badchans: punishing user %s (server: %s) for joining channel %s',
                             irc.name, nuh, irc.get_friendly_name(irc.get_server(user)), channel)
                    if use_kline:
                        irc.set_server_ban(asm_uid or irc.sid, kline_duration, host=ip, reason=REASON)
                    else:
                        irc.kill(asm_uid or irc.sid, user, REASON)

                    if ip not in seen_ips:
                        dronebl_key = irc.get_service_option('badchans', 'dronebl_key')
                        if dronebl_key:
                            log.info('(%s) badchans: submitting IP %s (%s) to DroneBL', irc.name, ip, nuh)
                            pool.submit(_submit_dronebl, irc, ip, dronebl_key, nuh)

                        dnsblim_key = irc.get_service_option('badchans', 'dnsblim_key')
                        if dnsblim_key:
                            log.info('(%s) badchans: submitting IP %s (%s) to DNSBL.im', irc.name, ip, nuh)
                            pool.submit(_submit_dnsblim, irc, ip, dnsblim_key, nuh)

                        seen_ips[ip] = time.time()

                    else:
                        log.debug('(%s) badchans: ignoring already submitted IP %s', irc.name, ip)