mirror of
https://github.com/jlu5/PyLink.git
synced 2024-12-11 20:49:33 +01:00
relay: implement kick/mode/topic war prevention (#23)
This adds cachetools as a dependency for Relay.
This commit is contained in:
parent
042d11d7ba
commit
94cd1d8f22
129
plugins/relay.py
129
plugins/relay.py
@ -13,6 +13,11 @@ from pylinkirc.coremods import permissions
|
|||||||
CHANNEL_DELINKED_MSG = "Channel delinked."
|
CHANNEL_DELINKED_MSG = "Channel delinked."
|
||||||
RELAY_UNLOADED_MSG = "Relay plugin unloaded."
|
RELAY_UNLOADED_MSG = "Relay plugin unloaded."
|
||||||
|
|
||||||
|
try:
|
||||||
|
import cachetools
|
||||||
|
except ImportError as e:
|
||||||
|
raise ImportError("PyLink Relay requires cachetools as of PyLink 2.1: https://pypi.org/project/cachetools/") from e
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import unidecode
|
import unidecode
|
||||||
except ImportError:
|
except ImportError:
|
||||||
@ -27,6 +32,11 @@ relayservers = defaultdict(dict)
|
|||||||
spawnlocks = defaultdict(threading.Lock)
|
spawnlocks = defaultdict(threading.Lock)
|
||||||
spawnlocks_servers = defaultdict(threading.Lock)
|
spawnlocks_servers = defaultdict(threading.Lock)
|
||||||
|
|
||||||
|
# Claim bounce cache to prevent kick/mode/topic loops
|
||||||
|
__claim_bounce_timeout = conf.conf.get('relay', {}).get('claim_bounce_timeout', 5)
|
||||||
|
claim_bounce_cache = cachetools.TTLCache(float('inf'), __claim_bounce_timeout)
|
||||||
|
claim_bounce_cache_lock = threading.Lock()
|
||||||
|
|
||||||
dbname = conf.get_database_name('pylinkrelay')
|
dbname = conf.get_database_name('pylinkrelay')
|
||||||
datastore = structures.PickleDataStore('pylinkrelay', dbname)
|
datastore = structures.PickleDataStore('pylinkrelay', dbname)
|
||||||
db = datastore.store
|
db = datastore.store
|
||||||
@ -646,9 +656,32 @@ def remove_channel(irc, channel):
|
|||||||
del relayusers[remoteuser][irc.name]
|
del relayusers[remoteuser][irc.name]
|
||||||
irc.quit(user, 'Left all shared channels.')
|
irc.quit(user, 'Left all shared channels.')
|
||||||
|
|
||||||
|
def _claim_should_bounce(irc, channel):
|
||||||
|
"""
|
||||||
|
Returns whether we should bounce the next action that fails CLAIM.
|
||||||
|
This is used to prevent kick/mode/topic wars with services.
|
||||||
|
"""
|
||||||
|
with claim_bounce_cache_lock:
|
||||||
|
if irc.name not in claim_bounce_cache: # Nothing in the cache to worry about
|
||||||
|
return True
|
||||||
|
|
||||||
|
limit = irc.get_service_option('relay', 'claim_bounce_limit', default=15)
|
||||||
|
if limit < 0: # Disabled
|
||||||
|
return True
|
||||||
|
elif limit < 5: # Anything below this is just asking for desyncs...
|
||||||
|
log.warning('(%s) relay: the minimum supported value for relay::claim_bounce_limit is 5.', irc.name)
|
||||||
|
limit = 5
|
||||||
|
|
||||||
|
success = claim_bounce_cache[irc.name] <= limit
|
||||||
|
ttl = claim_bounce_cache.ttl
|
||||||
|
if not success:
|
||||||
|
log.warning("(%s) relay: %s received more than %s claim bounces in %s seconds - your channel may be desynced!",
|
||||||
|
irc.name, channel, limit, ttl)
|
||||||
|
return success
|
||||||
|
|
||||||
def check_claim(irc, channel, sender, chanobj=None):
|
def check_claim(irc, channel, sender, chanobj=None):
|
||||||
"""
|
"""
|
||||||
Checks whether the sender of a kick/mode change passes CLAIM checks for
|
Checks whether the sender of a kick/mode/topic change passes CLAIM checks for
|
||||||
a given channel. This returns True if any of the following criteria are met:
|
a given channel. This returns True if any of the following criteria are met:
|
||||||
|
|
||||||
1) No relay exists for the channel in question.
|
1) No relay exists for the channel in question.
|
||||||
@ -669,13 +702,23 @@ def check_claim(irc, channel, sender, chanobj=None):
|
|||||||
log.debug('(%s) relay.check_claim: sender modes (%s/%s) are %s (mlist=%s)', irc.name,
|
log.debug('(%s) relay.check_claim: sender modes (%s/%s) are %s (mlist=%s)', irc.name,
|
||||||
sender, channel, sender_modes, mlist)
|
sender, channel, sender_modes, mlist)
|
||||||
# XXX: stop hardcoding modes to check for and support mlist in isHalfopPlus and friends
|
# XXX: stop hardcoding modes to check for and support mlist in isHalfopPlus and friends
|
||||||
return (not relay) or irc.name == relay[0] or not db[relay]['claim'] or \
|
success = (not relay) or irc.name == relay[0] or not db[relay]['claim'] or \
|
||||||
irc.name in db[relay]['claim'] or \
|
irc.name in db[relay]['claim'] or \
|
||||||
(any([mode in sender_modes for mode in ('y', 'q', 'a', 'o', 'h')])
|
(any([mode in sender_modes for mode in {'y', 'q', 'a', 'o', 'h'}])
|
||||||
and not irc.is_privileged_service(sender)) \
|
and not irc.is_privileged_service(sender)) \
|
||||||
or irc.is_internal_client(sender) or \
|
or irc.is_internal_client(sender) or \
|
||||||
irc.is_internal_server(sender)
|
irc.is_internal_server(sender)
|
||||||
|
|
||||||
|
# Increment claim_bounce_cache, checked in _claim_should_bounce()
|
||||||
|
if not success:
|
||||||
|
with claim_bounce_cache_lock:
|
||||||
|
if irc.name not in claim_bounce_cache:
|
||||||
|
claim_bounce_cache[irc.name] = 1
|
||||||
|
else:
|
||||||
|
claim_bounce_cache[irc.name] += 1
|
||||||
|
|
||||||
|
return success
|
||||||
|
|
||||||
def get_supported_umodes(irc, remoteirc, modes):
|
def get_supported_umodes(irc, remoteirc, modes):
|
||||||
"""Given a list of user modes, filters out all of those not supported by the
|
"""Given a list of user modes, filters out all of those not supported by the
|
||||||
remote network."""
|
remote network."""
|
||||||
@ -1318,7 +1361,7 @@ def handle_join(irc, numeric, command, args):
|
|||||||
if modechar and not irc.is_privileged_service(numeric):
|
if modechar and not irc.is_privileged_service(numeric):
|
||||||
modes.append(('-%s' % modechar, user))
|
modes.append(('-%s' % modechar, user))
|
||||||
|
|
||||||
if modes:
|
if modes and _claim_should_bounce(irc, channel):
|
||||||
log.debug('(%s) relay.handle_join: reverting modes on BURST: %s', irc.name, irc.join_modes(modes))
|
log.debug('(%s) relay.handle_join: reverting modes on BURST: %s', irc.name, irc.join_modes(modes))
|
||||||
irc.mode(irc.sid, channel, modes)
|
irc.mode(irc.sid, channel, modes)
|
||||||
|
|
||||||
@ -1695,32 +1738,31 @@ def handle_kick(irc, source, command, args):
|
|||||||
del relayusers[(irc.name, target)][remoteirc.name]
|
del relayusers[(irc.name, target)][remoteirc.name]
|
||||||
remoteirc.quit(real_target, 'Left all shared channels.')
|
remoteirc.quit(real_target, 'Left all shared channels.')
|
||||||
|
|
||||||
|
# Kick was a relay client but sender does not pass CLAIM restrictions. Bounce a rejoin unless we've reached our limit.
|
||||||
if is_relay_client(irc, target) and not check_claim(irc, channel, kicker):
|
if is_relay_client(irc, target) and not check_claim(irc, channel, kicker):
|
||||||
homenet, real_target = get_orig_user(irc, target)
|
if _claim_should_bounce(irc, channel):
|
||||||
homeirc = world.networkobjects.get(homenet)
|
homenet, real_target = get_orig_user(irc, target)
|
||||||
homenick = homeirc.users[real_target].nick if homeirc else '<ghost user>'
|
homeirc = world.networkobjects.get(homenet)
|
||||||
homechan = get_remote_channel(irc, homeirc, channel)
|
homenick = homeirc.users[real_target].nick if homeirc else '<ghost user>'
|
||||||
|
homechan = get_remote_channel(irc, homeirc, channel)
|
||||||
|
|
||||||
log.debug('(%s) relay.handle_kick: kicker %s is not opped... We should rejoin the target user %s', irc.name, kicker, real_target)
|
log.debug('(%s) relay.handle_kick: kicker %s is not opped... We should rejoin the target user %s', irc.name, kicker, real_target)
|
||||||
# Home network is not in the channel's claim AND the kicker is not
|
# FIXME: make the check slightly more advanced: i.e. halfops can't kick ops, admins can't kick owners, etc.
|
||||||
# opped. We won't propograte the kick then.
|
modes = get_prefix_modes(homeirc, irc, homechan, real_target)
|
||||||
# TODO: make the check slightly more advanced: i.e. halfops can't
|
|
||||||
# kick ops, admins can't kick owners, etc.
|
|
||||||
modes = get_prefix_modes(homeirc, irc, homechan, real_target)
|
|
||||||
|
|
||||||
# Join the kicked client back with its respective modes.
|
# Join the kicked client back with its respective modes.
|
||||||
irc.sjoin(irc.sid, channel, [(modes, target)])
|
irc.sjoin(irc.sid, channel, [(modes, target)])
|
||||||
if kicker in irc.users:
|
if kicker in irc.users:
|
||||||
log.info('(%s) relay: Blocked KICK (reason %r) from %s/%s to %s/%s on %s.',
|
log.info('(%s) relay: Blocked KICK (reason %r) from %s/%s to %s/%s on %s.',
|
||||||
irc.name, args['text'], irc.users[source].nick, irc.name,
|
irc.name, args['text'], irc.users[source].nick, irc.name,
|
||||||
homenick, homenet, channel)
|
homenick, homenet, channel)
|
||||||
irc.msg(kicker, "This channel is claimed; your kick to "
|
irc.msg(kicker, "This channel is claimed; your kick to "
|
||||||
"%s has been blocked because you are not "
|
"%s has been blocked because you are not "
|
||||||
"(half)opped." % channel, notice=True)
|
"(half)opped." % channel, notice=True)
|
||||||
else:
|
else:
|
||||||
log.info('(%s) relay: Blocked KICK (reason %r) from server %s to %s/%s on %s.',
|
log.info('(%s) relay: Blocked KICK (reason %r) from server %s to %s/%s on %s.',
|
||||||
irc.name, args['text'], irc.servers[source].name ,
|
irc.name, args['text'], irc.servers[source].name ,
|
||||||
homenick, homenet, channel)
|
homenick, homenet, channel)
|
||||||
return
|
return
|
||||||
|
|
||||||
iterate_all(irc, _handle_kick_loop, extra_args=(source, command, args))
|
iterate_all(irc, _handle_kick_loop, extra_args=(source, command, args))
|
||||||
@ -1838,22 +1880,23 @@ def handle_mode(irc, numeric, command, args):
|
|||||||
for named_modepair in modedelta_modes]))
|
for named_modepair in modedelta_modes]))
|
||||||
|
|
||||||
if not check_claim(irc, target, numeric, chanobj=oldchan):
|
if not check_claim(irc, target, numeric, chanobj=oldchan):
|
||||||
# Mode change blocked by CLAIM.
|
if _claim_should_bounce(irc, target):
|
||||||
reversed_modes = irc.reverse_modes(target, modes, oldobj=oldchan)
|
# Mode change blocked by CLAIM.
|
||||||
|
reversed_modes = irc.reverse_modes(target, modes, oldobj=oldchan)
|
||||||
|
|
||||||
if irc.is_privileged_service(numeric):
|
if irc.is_privileged_service(numeric):
|
||||||
# Special hack for "U-lined" servers - ignore changes to SIMPLE modes and
|
# Special hack for "U-lined" servers - ignore changes to SIMPLE modes and
|
||||||
# attempts to op u-lined clients (trying to change status for others
|
# attempts to op its own clients (trying to change status for others
|
||||||
# SHOULD be reverted).
|
# SHOULD be reverted).
|
||||||
# This is for compatibility with Anope's DEFCON for the most part, as well as
|
# This is for compatibility with Anope's DEFCON for the most part, as well as
|
||||||
# silly people who try to register a channel multiple times via relay.
|
# silly people who try to register a channel multiple times via relay.
|
||||||
reversed_modes = [modepair for modepair in reversed_modes if
|
reversed_modes = [modepair for modepair in reversed_modes if
|
||||||
# Mode is a prefix mode but target isn't ulined, revert
|
# Include prefix modes if target isn't also U-lined
|
||||||
((modepair[0][-1] in irc.prefixmodes and not
|
((modepair[0][-1] in irc.prefixmodes and not
|
||||||
irc.is_privileged_service(modepair[1]))
|
irc.is_privileged_service(modepair[1]))
|
||||||
# Tried to set a list mode, revert
|
# Include all list modes (bans, etc.)
|
||||||
or modepair[0][-1] in irc.cmodes['*A'])
|
or modepair[0][-1] in irc.cmodes['*A'])
|
||||||
]
|
]
|
||||||
modes.clear() # Clear the mode list so nothing is relayed below
|
modes.clear() # Clear the mode list so nothing is relayed below
|
||||||
|
|
||||||
for modepair in modes.copy():
|
for modepair in modes.copy():
|
||||||
@ -1920,7 +1963,7 @@ def handle_topic(irc, numeric, command, args):
|
|||||||
remoteirc.topic_burst(rsid, remotechan, topic)
|
remoteirc.topic_burst(rsid, remotechan, topic)
|
||||||
iterate_all(irc, _handle_topic_loop, extra_args=(numeric, command, args))
|
iterate_all(irc, _handle_topic_loop, extra_args=(numeric, command, args))
|
||||||
|
|
||||||
elif oldtopic: # Topic change blocked by claim.
|
elif oldtopic and _claim_should_bounce(irc, channel): # Topic change blocked by claim.
|
||||||
irc.topic_burst(irc.sid, channel, oldtopic)
|
irc.topic_burst(irc.sid, channel, oldtopic)
|
||||||
|
|
||||||
utils.add_hook(handle_topic, 'TOPIC')
|
utils.add_hook(handle_topic, 'TOPIC')
|
||||||
|
Loading…
Reference in New Issue
Block a user