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])
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
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]}])
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))
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.')
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])
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)
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)
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'}
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)
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)
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
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
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 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}
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}
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)
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}
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
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}
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_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]}
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)
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)
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}
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)
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))
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
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
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}
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}
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
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)