Example #1
0
    def handle_metadata(self, numeric, command, args):
        """
        Handles the METADATA command, used by servers to send metadata (services
        login name, certfp data, etc.) for clients.
        """
        uid = args[0]

        if args[1] == 'accountname' and uid in self.users:
            # <- :00A METADATA 1MLAAAJET accountname :
            # <- :00A METADATA 1MLAAAJET accountname :tester
            # Sets the services login name of the client.

            self.call_hooks([uid, 'CLIENT_SERVICES_LOGIN', {'text': args[-1]}])
        elif args[1] == 'modules' and numeric == self.uplink:
            # Note: only handle METADATA from our uplink; otherwise leaf servers unloading modules
            # while shutting down will corrupt the state.
            # <- :70M METADATA * modules :-m_chghost.so
            # <- :70M METADATA * modules :+m_chghost.so
            for module in args[-1].split():
                if module.startswith('-'):
                    log.debug('(%s) Removing module %s', self.name, module[1:])
                    self._modsupport.discard(module[1:])
                elif module.startswith('+'):
                    log.debug('(%s) Adding module %s', self.name, module[1:])
                    self._modsupport.add(module[1:])
                else:
                    log.warning('(%s) Got unknown METADATA modules string: %r', self.name, args[-1])
Example #2
0
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
Example #3
0
    def handle_metadata(self, source, command, args):
        """Handles various user metadata for ngIRCd (cloaked host, account name, etc.)"""
        # <- :ngircd.midnight.local METADATA GL cloakhost :hidden-3a2a739e.ngircd.midnight.local
        target = self._get_UID(args[0])

        if target not in self.users:
            log.warning("(%s) Ignoring METADATA to missing user %r?", self.name, target)
            return

        datatype = args[1]
        u = self.users[target]

        if datatype == 'cloakhost':  # Set cloaked host
            u.cloaked_host = args[-1]
            self._check_cloak_change(target)

        elif datatype == 'host':  # Host changing. This actually sets the "real host" that ngIRCd stores
            u.realhost = args[-1]
            self._check_cloak_change(target)

        elif datatype == 'user':  # Ident changing
            u.ident = args[-1]
            self.call_hooks([target, 'CHGIDENT', {'target': target, 'newident': args[-1]}])

        elif datatype == 'info':  # Realname changing
            u.realname = args[-1]
            self.call_hooks([target, 'CHGNAME', {'target': target, 'newgecos': args[-1]}])

        elif datatype == 'accountname':  # Services account
            self.call_hooks([target, 'CLIENT_SERVICES_LOGIN', {'text': args[-1]}])
Example #4
0
def identify(irc, source, args):
    """<username> <password>

    Logs in to PyLink using the configured administrator account."""
    if utils.isChannel(irc.called_in):
        irc.reply('Error: This command must be sent in private. '
                  '(Would you really type a password inside a channel?)')
        return
    try:
        username, password = args[0], args[1]
    except IndexError:
        irc.reply('Error: Not enough arguments.')
        return
    # Usernames are case-insensitive, passwords are NOT.
    if username.lower() == irc.conf['login']['user'].lower(
    ) and password == irc.conf['login']['password']:
        realuser = irc.conf['login']['user']
        irc.users[source].identified = realuser
        irc.reply('Successfully logged in as %s.' % realuser)
        log.info("(%s) Successful login to %r by %s", irc.name, username,
                 irc.getHostmask(source))
    else:
        irc.reply('Error: Incorrect credentials.')
        u = irc.users[source]
        log.warning("(%s) Failed login to %r from %s", irc.name, username,
                    irc.getHostmask(source))
Example #5
0
def identify(irc, source, args):
    """<username> <password>

    Logs in to PyLink using the configured administrator account."""
    if irc.is_channel(irc.called_in):
        irc.reply('Error: This command must be sent in private. '
                  '(Would you really type a password inside a channel?)')
        return
    try:
        username, password = args[0], args[1]
    except IndexError:
        irc.reply('Error: Not enough arguments.')
        return

    # Process new-style accounts.
    if check_login(username, password):
        _irc_try_login(irc, source, username)
        return

    # Process legacy logins (login:user).
    if username.lower() == conf.conf['login'].get(
            'user',
            '').lower() and password == conf.conf['login'].get('password'):
        realuser = conf.conf['login']['user']
        _irc_try_login(irc, source, realuser, skip_checks=True)
        return

    # Username not found or password incorrect.
    log.warning("(%s) Failed login to %r from %s", irc.name, username,
                irc.get_hostmask(source))
    raise utils.NotAuthorizedError('Bad username or password.')
Example #6
0
    def handle_472(self, numeric, command, args):
        """Handles the incoming 472 numeric.

        472 is sent to us when one of our clients tries to set a mode the uplink
        server doesn't support. In this case, we'll raise a warning to alert
        the administrator that certain extensions should be loaded for the best
        compatibility.
        """
        # <- :charybdis.midnight.vpn 472 GL|devel O :is an unknown mode char to me
        badmode = args[1]
        reason = args[-1]
        setter = args[0]
        charlist = {
            'A': 'chm_adminonly',
            'O': 'chm_operonly',
            'S': 'chm_sslonly',
            'T': 'chm_nonotice'
        }
        if badmode in charlist:
            log.warning(
                '(%s) User %r attempted to set channel mode %r, but the '
                'extension providing it isn\'t loaded! To prevent possible'
                ' desyncs, try adding the line "loadmodule "extensions/%s.so";" to '
                'your IRCd configuration.', self.irc.name, setter, badmode,
                charlist[badmode])
Example #7
0
 def __init__(self, irc):
     super().__init__(irc)
     log.warning(
         "(%s) protocols/nefarious.py has been renamed to protocols/p10.py, which "
         "now also supports other IRCu variants. Please update your configuration, "
         "as this migration stub will be removed in a future version.",
         self.irc.name)
Example #8
0
    def kill(self, numeric, target, reason):
        """Sends a kill from a PyLink client/server."""

        if (not self.is_internal_client(numeric)) and \
                (not self.is_internal_server(numeric)):
            raise LookupError('No such PyLink client/server exists.')

        # From TS6 docs:
        # KILL:
        # parameters: target user, path

        # The format of the path parameter is some sort of description of the source of
        # the kill followed by a space and a parenthesized reason. To avoid overflow,
        # it is recommended not to add anything to the path.

        assert target in self.users, "Unknown target %r for kill()!" % target

        if numeric in self.users:
            # Killer was an user. Follow examples of setting the path to be "killer.host!killer.nick".
            userobj = self.users[numeric]
            killpath = '%s!%s' % (userobj.host, userobj.nick)
        elif numeric in self.servers:
            # Sender was a server; killpath is just its name.
            killpath = self.servers[numeric].name
        else:
            # Invalid sender?! This shouldn't happen, but make the killpath our server name anyways.
            log.warning(
                '(%s) Invalid sender %s for kill(); using our server name instead.',
                self.name, numeric)
            killpath = self.servers[self.sid].name

        self._send_with_prefix(numeric,
                               'KILL %s :%s (%s)' % (target, killpath, reason))
        self._remove_client(target)
Example #9
0
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self._ircd = self.serverdata.get(
            'ircd', 'elemental'
            if self.serverdata.get('use_elemental_modes') else 'charybdis')
        self._ircd = self._ircd.lower()
        if self._ircd not in self.SUPPORTED_IRCDS:
            log.warning(
                "(%s) Unsupported IRCd %r; falling back to 'charybdis' instead",
                self.name, target_ircd)
            self._ircd = 'charybdis'

        self._can_chghost = False
        if self._ircd in ('charybdis', 'elemental', 'chatircd'):
            # Charybdis and derivatives allow slashes in hosts. Ratbox does not.
            self.protocol_caps |= {'slash-in-hosts'}
            self._can_chghost = True

        self.casemapping = 'rfc1459'
        self.hook_map = {
            'SJOIN': 'JOIN',
            'TB': 'TOPIC',
            'TMODE': 'MODE',
            'BMASK': 'MODE',
            'EUID': 'UID',
            'RSFNC': 'SVSNICK',
            'ETB': 'TOPIC',
            # ENCAP LOGIN is used on burst for EUID-less servers
            'LOGIN': '******'
        }

        self.required_caps = {'TB', 'ENCAP', 'QS', 'CHW'}
Example #10
0
    def handle_cap(self, source, command, args):
        """
        Handles IRCv3 capabilities transmission.
        """
        subcmd = args[1]

        if subcmd == 'LS':
            # Server: CAP * LS * :multi-prefix extended-join account-notify batch invite-notify tls
            # Server: CAP * LS * :cap-notify server-time example.org/dummy-cap=dummyvalue example.org/second-dummy-cap
            # Server: CAP * LS :userhost-in-names sasl=EXTERNAL,DH-AES,DH-BLOWFISH,ECDSA-NIST256P-CHALLENGE,PLAIN
            log.debug('(%s) Got new capabilities %s', self.irc.name, args[-1])
            self.ircv3_caps_available.update(
                self.parseCapabilities(args[-1], None))
            if args[2] != '*':
                self.requestNewCaps()

        elif subcmd == 'ACK':
            # Server: CAP * ACK :multi-prefix sasl
            newcaps = set(args[-1].split())
            log.debug('(%s) Received ACK for IRCv3 capabilities %s',
                      self.irc.name, newcaps)
            self.ircv3_caps |= newcaps

            # Only send CAP END immediately if SASL is disabled. Otherwise, wait for the 90x responses
            # to do so.
            if not self.saslAuth():
                if not self.has_eob:
                    self.capEnd()
        elif subcmd == 'NAK':
            log.warning(
                '(%s) Got NAK for IRCv3 capabilities %s, even though they were supposedly available',
                self.irc.name, args[-1])
            if not self.has_eob:
                self.capEnd()
        elif subcmd == 'NEW':
            # :irc.example.com CAP modernclient NEW :batch
            # :irc.example.com CAP tester NEW :away-notify extended-join
            # Note: CAP NEW allows capabilities with values (e.g. sasl=mech1,mech2), while CAP DEL
            # does not.
            log.debug('(%s) Got new capabilities %s', self.irc.name, args[-1])
            newcaps = self.parseCapabilities(args[-1], None)
            self.ircv3_caps_available.update(newcaps)
            self.requestNewCaps()

            # Attempt SASL auth routines when sasl is added/removed, if doing so is enabled.
            if 'sasl' in newcaps and self.irc.serverdata.get('sasl_reauth'):
                log.debug('(%s) Attempting SASL reauth due to CAP NEW',
                          self.irc.name)
                self.saslAuth()

        elif subcmd == 'DEL':
            # :irc.example.com CAP modernclient DEL :userhost-in-names multi-prefix away-notify
            log.debug('(%s) Removing capabilities %s', self.irc.name, args[-1])
            for cap in args[-1].split():
                # Remove the capabilities from the list available, and return None (ignore) if any fail
                self.ircv3_caps_available.pop(cap, None)
                self.ircv3_caps.discard(cap)
Example #11
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)
Example #12
0
def handle_fantasy(irc, source, command, args):
    """Fantasy command handler."""

    if not irc.connected.is_set():
        # Break if the IRC network isn't ready.
        return

    respondtonick = irc.botdata.get("respondtonick")

    channel = args['target']
    orig_text = args['text']

    if utils.isChannel(channel) and not irc.isInternalClient(source):
        # The following conditions must be met for an incoming message for
        # fantasy to trigger:
        #   1) The message target is a channel.
        #   2) A PyLink service client exists in the channel.
        #   3) The message starts with one of our fantasy prefixes.
        #   4) The sender is NOT a PyLink client (this prevents infinite
        #      message loops).
        for botname, sbot in world.services.items():
            log.debug('(%s) fantasy: checking bot %s', irc.name, botname)
            servuid = sbot.uids.get(irc.name)
            if servuid in irc.channels[channel].users:

                # Try to look up a prefix specific for this bot in
                # bot: prefixes: <botname>, falling back to the default prefix if not
                # specified.
                prefixes = [irc.botdata.get('prefixes', {}).get(botname) or
                            irc.botdata.get('prefix')]

                # If responding to nick is enabled, add variations of the current nick
                # to the prefix list: "<nick>," and "<nick>:"
                nick = irc.users[servuid].nick

                if respondtonick:
                    prefixes += [nick+',', nick+':']

                if not any(prefixes):
                    # We finished with an empty prefixes list, meaning fantasy is misconfigured!
                    log.warning("(%s) Fantasy prefix for bot %s was not set in configuration - "
                                "fantasy commands will not work!", irc.name, botname)
                    continue

                for prefix in prefixes:  # Cycle through the prefixes list we finished with.
                     if prefix and orig_text.startswith(prefix):

                        # Cut off the length of the prefix from the text.
                        text = orig_text[len(prefix):]

                        # Finally, call the bot command and loop to the next bot.
                        sbot.call_cmd(irc, source, text, called_in=channel)
                        continue
Example #13
0
    def _get_webhook_fields(self, user):
        """
        Returns a dict of Relay substitution fields for the given User object.
        This attempts to find the original user via Relay if the .remote metadata field is set.

        The output includes all keys provided in User.get_fields(), plus the following:
            netname: The full network name of the network 'user' belongs to
            nettag: The short network tag of the network 'user' belongs to
            avatar: The URL to the user's avatar (str), or None if no avatar is specified
        """
        # Try to lookup the remote user data via relay metadata
        if hasattr(user, 'remote'):
            remotenet, remoteuid = user.remote
            try:
                netobj = world.networkobjects[remotenet]
                user = netobj.users[remoteuid]
            except LookupError:
                netobj = user._irc

        fields = user.get_fields()
        fields['netname'] = netobj.get_full_network_name()
        fields['nettag'] = netobj.name

        default_avatar_url = self.serverdata.get('default_avatar_url')
        avatar = None
        # XXX: we'll have a more rigorous matching system later on
        if user.services_account in self.serverdata.get('avatars', {}):
            avatar_url = self.serverdata['avatars'][user.services_account]
            p = urllib.parse.urlparse(avatar_url)
            log.debug('(%s) Got raw avatar URL %s for user %s', self.name, avatar_url, user)

            if p.scheme == 'gravatar' and libgravatar:  # gravatar:[email protected]
                try:
                    g = libgravatar.Gravatar(p.path)
                    log.debug('(%s) Using Gravatar email %s for user %s', self.name, p.path, user)
                    avatar = g.get_image(use_ssl=True)
                except:
                    log.exception('Failed to obtain Gravatar image for user %s/%s', user, p.path)

            elif p.scheme in ('http', 'https'):  # a direct image link
                avatar = avatar_url

            else:
                log.warning('(%s) Unknown avatar URI %s for user %s', self.name, avatar_url, user)
        elif default_avatar_url:
            log.debug('(%s) Avatar not defined for user %s; using default avatar %s', self.name, user, default_avatar_url)
            avatar = default_avatar_url
        else:
            log.debug('(%s) Avatar not defined for user %s; using default webhook avatar', self.name, user)
        fields['avatar'] = avatar
        return fields
Example #14
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}])
Example #15
0
    def handle_chgname(self, source, command, args):
        """Handles CHGNAME, used for denoting real name/gecos changes."""
        # <- :jlu5 CHGNAME jlu5 :afdsafasf
        target = self._get_UID(args[0])
        # Bounce attempts to change fields of protected PyLink clients
        if self.is_internal_client(target):
            log.warning(
                "(%s) Bouncing attempt from %s to change gecos of PyLink client %s",
                self.name, self.get_friendly_name(source),
                self.get_friendly_name(target))
            self.update_client(target, 'REALNAME', self.users[target].realname)
            return

        self.users[target].realname = newgecos = args[1]
        return {'target': target, 'newgecos': newgecos}
Example #16
0
    def handle_chgident(self, source, command, args):
        """Handles CHGIDENT, used for denoting ident changes."""
        # <- :jlu5 CHGIDENT jlu5 test
        target = self._get_UID(args[0])

        # Bounce attempts to change fields of protected PyLink clients
        if self.is_internal_client(target):
            log.warning(
                "(%s) Bouncing attempt from %s to change ident of PyLink client %s",
                self.name, self.get_friendly_name(source),
                self.get_friendly_name(target))
            self.update_client(target, 'IDENT', self.users[target].ident)
            return

        self.users[target].ident = newident = args[1]
        return {'target': target, 'newident': newident}
Example #17
0
def handle_part(irc, source, command, args):
    """Force joins users who try to leave a designated staff channel."""
    staffchans = irc.get_service_option('operlock', 'channels', default=None) or []
    staffchans = list(map(irc.to_lower, staffchans))
    if not staffchans:
        return
    elif not _should_enforce(irc, source, args):
        return

    for channel in args['channels']:
        if irc.to_lower(channel) in staffchans and irc.is_oper(source):
            if irc.protoname in ('inspircd', 'unreal'):
                irc.msg(source, "Warning: You must deoper to leave %r." % channel, notice=True)
                irc._send_with_prefix(irc.sid, 'SAJOIN %s %s' % (source, channel))
            else:
                log.warning('(%s) Force join is not supported on this IRCd %r!', irc.name, irc.protoname)
Example #18
0
    def handle_kill(self, source, command, args):
        """Handles incoming KILLs."""
        killed = self._get_UID(args[0])
        # Some IRCds send explicit QUIT messages for their killed clients in addition to KILL,
        # meaning that our target client may have been removed already. If this is the case,
        # don't bother forwarding this message on.
        # Generally, we only need to distinguish between KILL and QUIT if the target is
        # one of our clients, in which case the above statement isn't really applicable.
        if killed in self.users:
            userdata = self._remove_client(killed)
        else:
            return

        # TS6-style kills look something like this:
        # <- :GL KILL 38QAAAAAA :hidden-1C620195!GL (test)
        # What we actually want is to format a pretty kill message, in the form
        # "Killed (killername (reason))".

        if '!' in args[1].split(" ", 1)[0]:
            try:
                # Get the nick or server name of the caller.
                killer = self.get_friendly_name(source)
            except KeyError:
                # Killer was... neither? We must have aliens or something. Fallback
                # to the given "UID".
                killer = source

            # Get the reason, which is enclosed in brackets.
            killmsg = ' '.join(args[1].split(" ")[1:])[1:-1]
            if not killmsg:
                log.warning('(%s) Failed to extract kill reason: %r',
                            self.name, args)
                killmsg = args[1]
        else:
            # We already have a preformatted kill, so just pass it on as is.
            # XXX: this does create a convoluted kill string if we want to forward kills
            # over relay.
            # InspIRCd:
            # <- :1MLAAAAA1 KILL 0ALAAAAAC :Killed (GL (test))
            # ngIRCd:
            # <- :GL KILL PyLink-devel :KILLed by GL: ?
            killmsg = args[1]

        return {'target': killed, 'text': killmsg, 'userdata': userdata}
Example #19
0
    def handle_mode(self, numeric, command, args):
        # <- :unreal.midnight.vpn MODE #test +bb test!*@* *!*@bad.net
        # <- :unreal.midnight.vpn MODE #test +q GL 1444361345
        # <- :unreal.midnight.vpn MODE #test +ntCo GL 1444361345
        # <- :unreal.midnight.vpn MODE #test +mntClfo 5 [10t]:5  GL 1444361345
        # <- :GL MODE #services +v GL

        # 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 utils.isChannel(args[0]):
            channel = self.irc.toLower(args[0])
            oldobj = self.irc.channels[channel].deepcopy()

            modes = list(filter(None, args[1:]))  # normalize whitespace
            parsedmodes = self.irc.parseModes(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.irc.name, modes, channel,
                        self.irc.channels[channel].ts)
                    return

                self.irc.applyModes(channel, parsedmodes)

            if numeric in self.irc.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, 'oldchan': oldobj}
        else:
            log.warning("(%s) received MODE for non-channel target: %r",
                        self.irc.name, args)
            raise NotImplementedError
Example #20
0
    def handle_kill(self, source, command, args):
        """Handles incoming KILLs."""
        killed = self._get_UID(args[0])
        # Depending on whether the IRCd sends explicit QUIT messages for
        # killed clients, the user may or may not have automatically been
        # removed from our user list.
        # If not, we have to assume that KILL = QUIT and remove them
        # ourselves.
        data = self.users.get(killed)
        if data:
            self._remove_client(killed)

        # TS6-style kills look something like this:
        # <- :GL KILL 38QAAAAAA :hidden-1C620195!GL (test)
        # What we actually want is to format a pretty kill message, in the form
        # "Killed (killername (reason))".

        if '!' in args[1].split(" ", 1)[0]:
            try:
                # Get the nick or server name of the caller.
                killer = self.get_friendly_name(source)
            except KeyError:
                # Killer was... neither? We must have aliens or something. Fallback
                # to the given "UID".
                killer = source

            # Get the reason, which is enclosed in brackets.
            killmsg = ' '.join(args[1].split(" ")[1:])[1:-1]
            if not killmsg:
                log.warning('(%s) Failed to extract kill reason: %r', irc.name,
                            args)
                killmsg = '<No reason given>'
        else:
            # We already have a preformatted kill, so just pass it on as is.
            # XXX: this does create a convoluted kill string if we want to forward kills
            # over relay.
            # InspIRCd:
            # <- :1MLAAAAA1 KILL 0ALAAAAAC :Killed (GL (test))
            # ngIRCd:
            # <- :GL KILL PyLink-devel :KILLed by GL: ?
            killmsg = args[1]

        return {'target': killed, 'text': killmsg, 'userdata': data}
Example #21
0
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)
Example #22
0
    def handle_privmsg(self, source, command, args):
        """Handles incoming PRIVMSG/NOTICE."""
        # <- :sender PRIVMSG #dev :afasfsa
        # <- :sender NOTICE somenick :afasfsa
        target = args[0]

        if self.irc.isInternalClient(source) or self.irc.isInternalServer(
                source):
            log.warning('(%s) Received %s to %s being routed the wrong way!',
                        self.irc.name, command, target)
            return

        # We use lowercase channels internally.
        if utils.isChannel(target):
            target = self.irc.toLower(target)
        else:
            target = self.irc.nickToUid(target)
        if target:
            return {'target': target, 'text': args[1]}
Example #23
0
    def on_message_update(self, event):
        message = event.message
        if not message.content:
            # Message updates do not necessarily contain all fields, per
            # https://discordapp.com/developers/docs/topics/gateway#message-update
            log.debug('discord: Ignoring message update for %s since the content has not been changed', message)
            return

        if message.guild:
            # Optionally, allow marking edited channel messages as such.
            subserver = message.guild.id
            pylink_netobj = self.protocol._children[subserver]
            editmsg_format = pylink_netobj.serverdata.get('editmsg_format')
            if editmsg_format:
                try:
                    message.content = editmsg_format % message.content
                except TypeError:
                    log.warning('(%s) Invalid editmsg_format format, it should contain a %%s', pylink_netobj.name)
        return self.on_message(event)
Example #24
0
def _make_cryptcontext():
    try:
        from passlib.context import CryptContext
    except ImportError:
        log.warning(
            "Hashed passwords are disabled because passlib is not installed. Please install "
            "it (pip3 install passlib) and rehash for this feature to work.")
        return

    context_settings = conf.conf.get(
        'login',
        {}).get('cryptcontext_settings') or _DEFAULT_CRYPTCONTEXT_SETTINGS
    global pwd_context
    if pwd_context is None:
        log.debug("Initialized new CryptContext with settings: %s",
                  context_settings)
        pwd_context = CryptContext(**context_settings)
    else:
        log.debug("Updated CryptContext with settings: %s", context_settings)
        pwd_context.update(**context_settings)
Example #25
0
    def handle_chghost(self, source, command, args):
        """Handles CHGHOST, used for denoting hostname changes."""
        # <- :jlu5 CHGHOST jlu5 some.host
        target = self._get_UID(args[0])
        # Bounce attempts to change fields of protected PyLink clients
        if self.is_internal_client(target):
            log.warning(
                "(%s) Bouncing attempt from %s to change host of PyLink client %s",
                self.name, self.get_friendly_name(source),
                self.get_friendly_name(target))
            self.update_client(target, 'HOST', self.users[target].host)
            return

        self.users[target].host = newhost = args[1]

        # When SETHOST or CHGHOST is used, modes +xt are implicitly set on the
        # target.
        self.apply_modes(target, [('+x', None), ('+t', None)])

        return {'target': target, 'newhost': newhost}
Example #26
0
    def __init__(self, irc):
        super().__init__(irc)
        # Set our case mapping (rfc1459 maps "\" and "|" together, for example)
        self.casemapping = 'ascii'
        self.proto_ver = 4000
        self.min_proto_ver = 4000
        self.hook_map = {
            'UMODE2': 'MODE',
            'SVSKILL': 'KILL',
            'SVSMODE': 'MODE',
            'SVS2MODE': 'MODE',
            'SJOIN': 'JOIN',
            'SETHOST': 'CHGHOST',
            'SETIDENT': 'CHGIDENT',
            'SETNAME': 'CHGNAME',
            'EOS': 'ENDBURST'
        }

        self.caps = []
        self.irc.prefixmodes = {
            'q': '~',
            'a': '&',
            'o': '@',
            'h': '%',
            'v': '+'
        }

        self.needed_caps = [
            "VL", "SID", "CHANMODES", "NOQUIT", "SJ3", "NICKIP", "UMODE2"
        ]

        # Some command aliases
        self.handle_svskill = self.handle_kill

        # Toggle whether we're using super hack mode for Unreal 3.2 mixed links.
        self.mixed_link = self.irc.serverdata.get('mixed_link')

        if self.mixed_link:
            log.warning(
                '(%s) mixed_link is experimental and may cause problems. '
                'You have been warned!', self.irc.name)
Example #27
0
def _login(irc, source, username):
    """Internal function to process logins."""
    # Mangle case before we start checking for login data.
    accounts = {k.lower(): v for k, v in conf.conf['login'].get('accounts', {}).items()}

    logindata = accounts.get(username.lower(), {})
    network_filter = logindata.get('networks')
    require_oper = logindata.get('require_oper', False)
    hosts_filter = logindata.get('hosts', [])

    if network_filter and irc.name not in network_filter:
        irc.error("You are not authorized to log in to %r on this network." % username)
        log.warning("(%s) Failed login to %r from %s (wrong network: networks filter says %r but we got %r)", irc.name, username, irc.getHostmask(source), ', '.join(network_filter), irc.name)
        return

    elif require_oper and not irc.isOper(source, allowAuthed=False):
        irc.error("You must be opered to log in to %r." % username)
        log.warning("(%s) Failed login to %r from %s (needs oper)", irc.name, username, irc.getHostmask(source))
        return

    elif hosts_filter and not any(irc.matchHost(host, source) for host in hosts_filter):
        irc.error("Failed to log in to %r: hostname mismatch." % username)
        log.warning("(%s) Failed login to %r from %s (hostname mismatch)", irc.name, username, irc.getHostmask(source))
        return

    irc.users[source].account = username
    irc.reply('Successfully logged in as %s.' % username)
    log.info("(%s) Successful login to %r by %s",
             irc.name, username, irc.getHostmask(source))
Example #28
0
    def updateClient(self, target, field, text):
        """Updates the known ident, host, or realname of a client."""
        if target not in self.irc.users:
            log.warning("(%s) Unknown target %s for updateClient()",
                        self.irc.name, target)
            return

        u = self.irc.users[target]

        if field == 'IDENT' and u.ident != text:
            u.ident = text
            if not self.irc.isInternalClient(target):
                # We're updating the host of an external client in our state, so send the appropriate
                # hook payloads.
                self.irc.callHooks([
                    self.irc.sid, 'CHGIDENT', {
                        'target': target,
                        'newident': text
                    }
                ])
        elif field == 'HOST' and u.host != text:
            u.host = text
            if not self.irc.isInternalClient(target):
                self.irc.callHooks([
                    self.irc.sid, 'CHGHOST', {
                        'target': target,
                        'newhost': text
                    }
                ])
        elif field in ('REALNAME', 'GECOS') and u.realname != text:
            u.realname = text
            if not self.irc.isInternalClient(target):
                self.irc.callHooks([
                    self.irc.sid, 'CHGNAME', {
                        'target': target,
                        'newgecos': text
                    }
                ])
        else:
            return  # Nothing changed
Example #29
0
def _changehost(irc, target, args):
    changehost_conf = irc.conf.get("changehost")

    if not changehost_conf:
        log.warning(
            "(%s) Missing 'changehost:' configuration block; "
            "Changehost will not function correctly!", irc.name)
        return
    elif irc.name not in changehost_conf.get('enabled_nets'):
        # We're not enabled on the network, break.
        return

    changehost_hosts = changehost_conf.get('hosts')
    if not changehost_hosts:
        log.warning(
            "(%s) No hosts were defined in changehost::hosts; "
            "Changehost will not function correctly!", irc.name)
        return

    for host_glob, host_template in changehost_hosts.items():
        if irc.matchHost(host_glob, target):
            # This uses template strings for simple substitution:
            # https://docs.python.org/3/library/string.html#template-strings
            template = string.Template(host_template)

            # Substitute using the fields provided the hook data. This means
            # that the following variables are available for substitution:
            # $uid, $ts, $nick, $realhost, $host, $ident, $ip
            new_host = template.substitute(args)

            # Replace characters that are not allowed in hosts with "-".
            for char in new_host:
                if char not in allowed_chars:
                    new_host = new_host.replace(char, '-')

            irc.proto.updateClient(target, 'HOST', new_host)

            # Only operate on the first match.
            break
Example #30
0
    def handle_sakick(self, source, command, args):
        """Handles forced kicks (SAKICK)."""
        # <- :1MLAAAAAD ENCAP 0AL SAKICK #test 0ALAAAAAB :test
        # ENCAP -> SAKICK args: ['#test', '0ALAAAAAB', 'test']

        target = args[1]
        channel = self.irc.toLower(args[0])
        try:
            reason = args[2]
        except IndexError:
            # Kick reason is optional, strange...
            reason = self.irc.getFriendlyName(source)

        if not self.irc.isInternalClient(target):
            log.warning("(%s) Got SAKICK for client that not one of ours: %s", self.irc.name, target)
            return
        else:
            # Like RSQUIT, SAKICK requires that the receiving server acknowledge that a kick has
            # happened. This comes from the server hosting the target client.
            server = self.irc.getServer(target)

        self.kick(server, channel, target, reason)
        return {'channel': channel, 'target': target, 'text': reason}
Example #31
0
    def handle_squit(self, numeric, command, args):
        """Handles incoming SQUITs."""
        # <- ABAAE SQ nefarious.midnight.vpn 0 :test

        split_server = self._getSid(args[0])

        affected_users = []
        log.debug('(%s) Splitting server %s (reason: %s)', self.irc.name, split_server, args[-1])

        if split_server not in self.irc.servers:
            log.warning("(%s) Tried to split a server (%s) that didn't exist!", self.irc.name, split_server)
            return

        # Prevent RuntimeError: dictionary changed size during iteration
        old_servers = self.irc.servers.copy()
        # Cycle through our list of servers. If any server's uplink is the one that is being SQUIT,
        # remove them and all their users too.
        for sid, data in old_servers.items():
            if data.uplink == split_server:
                log.debug('Server %s also hosts server %s, removing those users too...', split_server, sid)
                # Recursively run SQUIT on any other hubs this server may have been connected to.
                args = self.handle_squit(sid, 'SQUIT', [sid, "0",
                                         "PyLink: Automatically splitting leaf servers of %s" % sid])
                affected_users += args['users']

        for user in self.irc.servers[split_server].users.copy():
            affected_users.append(user)
            log.debug('Removing client %s (%s)', user, self.irc.users[user].nick)
            self.removeClient(user)

        sname = self.irc.servers[split_server].name
        uplink = self.irc.servers[split_server].uplink
        del self.irc.servers[split_server]
        log.debug('(%s) Netsplit affected users: %s', self.irc.name, affected_users)

        return {'target': split_server, 'users': affected_users, 'name': sname,
                'uplink': uplink}
Example #32
0
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
Example #33
0
    def saslAuth(self):
        """
        Starts an authentication attempt via SASL. This returns True if SASL
        is enabled and correctly configured, and False otherwise.
        """
        if 'sasl' not in self.ircv3_caps:
            log.info(
                "(%s) Skipping SASL auth since the IRCd doesn't support it.",
                self.irc.name)
            return

        sasl_mech = self.irc.serverdata.get('sasl_mechanism')
        if sasl_mech:
            sasl_mech = sasl_mech.upper()
            sasl_user = self.irc.serverdata.get('sasl_username')
            sasl_pass = self.irc.serverdata.get('sasl_password')
            ssl_cert = self.irc.serverdata.get('ssl_certfile')
            ssl_key = self.irc.serverdata.get('ssl_keyfile')
            ssl = self.irc.serverdata.get('ssl')

            if sasl_mech == 'PLAIN':
                if not (sasl_user and sasl_pass):
                    log.warning(
                        "(%s) Not attempting PLAIN authentication; sasl_username and/or "
                        "sasl_password aren't correctly set.", self.irc.name)
                    return False
            elif sasl_mech == 'EXTERNAL':
                if not ssl:
                    log.warning(
                        "(%s) Not attempting EXTERNAL authentication; SASL external requires "
                        "SSL, but it isn't enabled.", self.irc.name)
                    return False
                elif not (ssl_cert and ssl_key):
                    log.warning(
                        "(%s) Not attempting EXTERNAL authentication; ssl_certfile and/or "
                        "ssl_keyfile aren't correctly set.", self.irc.name)
                    return False
            else:
                log.warning(
                    '(%s) Unsupported SASL mechanism %s; aborting SASL.',
                    self.irc.name, sasl_mech)
                return False
            self.irc.send('AUTHENTICATE %s' % sasl_mech, queue=False)
            return True
        return False
Example #34
0
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)