3
0
mirror of https://github.com/jlu5/PyLink.git synced 2024-11-01 09:19:23 +01:00

relay: fix many bugs with nick collision handling, and add flood prevention for fixing nicks

Closes #85. Nick collisions caused by internal clients are handled fine now, including the following cases:
- Abusing spawnClient to create a user with the same nick as a relay client, but with a lower TS.
- When both an UID (e.g. 42XAAAAAA) and a tagged UID (_42XAAAAAA) exist on the same network (the two will both try to take the same nick of _42XAAAAAA/net over the relay).

The case where changing NICK from a long cut-off nick to another long cut-off nick is also mitigated. somelongnick/net won't show nick changes to somelongnic//net if the old and new nicks have give the same normalized relay nick.

This introduces a new dependency, expiringdict, from https://pypi.python.org/pypi/expiringdict/1.1.2, which is used as a cache for flood prevention.
This commit is contained in:
James Lu 2015-08-22 20:51:50 -07:00
parent 1e95f4b3df
commit 20474dabac
2 changed files with 38 additions and 19 deletions

View File

@ -14,6 +14,7 @@ Dependencies currently include:
* Python 3.4+ * Python 3.4+
* PyYAML (`pip install pyyaml` or `apt-get install python3-yaml`) * PyYAML (`pip install pyyaml` or `apt-get install python3-yaml`)
* *For the relay plugin only*: expiringdict (`pip install expiringdict`/`apt-get install python3-expiringdict`)
#### Supported IRCds #### Supported IRCds
@ -23,7 +24,7 @@ Dependencies currently include:
### Installation ### Installation
1) Rename `config.yml.example` to `config.yml` and configure your instance there. Not all options are properly implemented yet, and the configuration schema isn't finalized yet - this means your configuration may break in an update! 1) Rename `config.yml.example` to `config.yml` and configure your instance there. Not all options are properly implemented yet, and the configuration schema isn't finalized yet - this means that your configuration may break in an update!
2) Run `main.py` from the command line. 2) Run `main.py` from the command line.

View File

@ -8,6 +8,8 @@ import threading
import string import string
from collections import defaultdict from collections import defaultdict
from expiringdict import ExpiringDict
import utils import utils
from log import log from log import log
from conf import confname from conf import confname
@ -19,6 +21,7 @@ dbname += '.db'
relayusers = defaultdict(dict) relayusers = defaultdict(dict)
spawnlocks = defaultdict(threading.Lock) spawnlocks = defaultdict(threading.Lock)
savecache = ExpiringDict(max_len=5, max_age_seconds=10)
def relayWhoisHandlers(irc, target): def relayWhoisHandlers(irc, target):
user = irc.users[target] user = irc.users[target]
@ -31,41 +34,46 @@ def relayWhoisHandlers(irc, target):
remotenick)] remotenick)]
utils.whois_handlers.append(relayWhoisHandlers) utils.whois_handlers.append(relayWhoisHandlers)
def normalizeNick(irc, netname, nick, separator=None, oldnick=''): def normalizeNick(irc, netname, nick, separator=None, uid=''):
separator = separator or irc.serverdata.get('separator') or "/" separator = separator or irc.serverdata.get('separator') or "/"
log.debug('(%s) normalizeNick: using %r as separator.', irc.name, separator) log.debug('(%s) normalizeNick: using %r as separator.', irc.name, separator)
orig_nick = nick orig_nick = nick
protoname = irc.proto.__name__ protoname = irc.proto.__name__
maxnicklen = irc.maxnicklen maxnicklen = irc.maxnicklen
if not protoname.startswith(('insp', 'unreal')): if not protoname.startswith(('insp', 'unreal')):
# Charybdis doesn't allow / in usernames, and will quit with # Charybdis doesn't allow / in usernames, and will SQUIT with
# a protocol violation if there is one. # a protocol violation if it sees one.
separator = separator.replace('/', '|') separator = separator.replace('/', '|')
nick = nick.replace('/', '|') nick = nick.replace('/', '|')
if nick.startswith(tuple(string.digits)): if nick.startswith(tuple(string.digits)):
# On TS6 IRCds, nicks that start with 0-9 are only allowed if # On TS6 IRCds, nicks that start with 0-9 are only allowed if
# they match the UID of the originating server. Otherwise, you'll # they match the UID of the originating server. Otherwise, you'll
# get nasty protocol violations! # get nasty protocol violation SQUITs!
nick = '_' + nick nick = '_' + nick
tagnicks = True tagnicks = True
suffix = separator + netname suffix = separator + netname
nick = nick[:maxnicklen] nick = nick[:maxnicklen]
# Maximum allowed length of a nickname. # Maximum allowed length of a nickname, minus the obligatory /network tag.
allowedlength = maxnicklen - len(suffix) allowedlength = maxnicklen - len(suffix)
# If a nick is too long, the real nick portion must be cut off, but the
# /network suffix must remain the same.
# If a nick is too long, the real nick portion will be cut off, but the
# /network suffix MUST remain the same.
nick = nick[:allowedlength] nick = nick[:allowedlength]
nick += suffix nick += suffix
# FIXME: factorize
while utils.nickToUid(irc, nick) or utils.nickToUid(irc, oldnick) and not \ # The nick we want exists? Darn, create another one then.
isRelayClient(irc, utils.nickToUid(irc, nick)):
# The nick we want exists? Darn, create another one then, but only if
# the target isn't an internal client!
# Increase the separator length by 1 if the user was already tagged, # Increase the separator length by 1 if the user was already tagged,
# but couldn't be created due to a nick conflict. # but couldn't be created due to a nick conflict.
# This can happen when someone steals a relay user's nick. # This can happen when someone steals a relay user's nick.
# However, if the user is changing from, say, a long, cut-off nick to another long,
# cut-off nick, we don't need to check for duplicates and tag the nick twice.
# somecutoffnick/net would otherwise be erroneous NICK'ed to somecutoffnic//net,
# even though there would be no collision because the old and new nicks are from
# the same client.
while utils.nickToUid(irc, nick) and utils.nickToUid(irc, nick) != uid:
new_sep = separator + separator[-1] new_sep = separator + separator[-1]
log.debug('(%s) normalizeNick: nick %r is in use; using %r as new_sep.', irc.name, nick, new_sep) log.debug('(%s) normalizeNick: nick %r is in use; using %r as new_sep.', irc.name, nick, new_sep)
nick = normalizeNick(irc, netname, orig_nick, separator=new_sep) nick = normalizeNick(irc, netname, orig_nick, separator=new_sep)
@ -271,7 +279,7 @@ utils.add_hook(handle_squit, 'SQUIT')
def handle_nick(irc, numeric, command, args): def handle_nick(irc, numeric, command, args):
for netname, user in relayusers[(irc.name, numeric)].items(): for netname, user in relayusers[(irc.name, numeric)].items():
remoteirc = utils.networkobjects[netname] remoteirc = utils.networkobjects[netname]
newnick = normalizeNick(remoteirc, irc.name, args['newnick']) newnick = normalizeNick(remoteirc, irc.name, args['newnick'], uid=user)
if remoteirc.users[user].nick != newnick: if remoteirc.users[user].nick != newnick:
remoteirc.proto.nickClient(remoteirc, user, newnick) remoteirc.proto.nickClient(remoteirc, user, newnick)
utils.add_hook(handle_nick, 'NICK') utils.add_hook(handle_nick, 'NICK')
@ -912,8 +920,18 @@ def handle_save(irc, numeric, command, args):
remotenet, remoteuser = realuser remotenet, remoteuser = realuser
remoteirc = utils.networkobjects[remotenet] remoteirc = utils.networkobjects[remotenet]
nick = remoteirc.users[remoteuser].nick nick = remoteirc.users[remoteuser].nick
newnick = normalizeNick(irc, remotenet, nick, oldnick=args['oldnick']) # Limit how many times we can attempt to fix our nick, to prevent
# floods and such.
if savecache.setdefault(target, 0) <= 5:
newnick = normalizeNick(irc, remotenet, nick)
log.info('(%s) SAVE received for relay client %r (%s), fixing nick to %s',
irc.name, target, nick, newnick)
irc.proto.nickClient(irc, target, newnick) irc.proto.nickClient(irc, target, newnick)
else:
log.warning('(%s) SAVE received for relay client %r (%s), not '
'fixing nick again due to 5 failed attempts in '
'the last 10 seconds!', irc.name, target, nick)
savecache[target] += 1
else: else:
# Somebody else on the network (not a PyLink client) had a nick collision; # Somebody else on the network (not a PyLink client) had a nick collision;
# relay this as a nick change appropriately. # relay this as a nick change appropriately.