Page 1 of 1

Anti-spam on Typed*Command

Posted: Sun Feb 25, 2018 3:58 pm
by Doldol
Is that implemented? I can't find anything on that, so I'm guessing it's not?

I think this would be a good feature to add because it is often something when not implemented that can lead to a DDoS vulnerability or unexpected results.

Perhaps implement it as a kwarg in the decorator where the value would indicate how long to wait between commands in seconds? With a reasonable default value like 1.0 & anything that equates to False disables the feature? Everything within that duration would be ignored.

It could be beneficial to implement this on a per client basis where applicable.

Re: Anti-spam on Typed*Command

Posted: Sun Feb 25, 2018 4:04 pm
by Doldol
I noticed this: viewtopic.php?f=38&t=1198&p=7730&hilit=spam#p7730

However I think this is important enough to be implemented directly into SP.

Re: Anti-spam on Typed*Command

Posted: Sun Feb 25, 2018 7:04 pm
by decompile
Not sure if implementing an anti-spam directly into the decorator is the right thing here.

Anti-Spam can be used much more than just adding a cooldown in seconds. Like using it once a round, once every XX seconds or maybe even once every day. (I know it doesnt realy relate towards Doldol's request)

In my opinion, I would keep it as it is, and the script-writer has to implement his own anti-spam, which would be simply tracking the timestamp when it has been first written, and check afterwards if the current timestamp is more than XX seconds compared to the tracked timestamp.

Or if you need a global anti-spam, you can simply hook SayText2 Filter and to the same.

Interested in what others think about that.

Re: Anti-spam on Typed*Command

Posted: Mon Feb 26, 2018 8:05 pm
by Ayuto
I generally like the idea of having an anti-spam system built-in. Why let the plugin authors reinvent the wheel? Though, there are multiple points that need to be considered:
  1. Server owners might want to customize the spam detection sensibility.
  2. Plugin developers shouldn't need extra knowledge to implement the system. It should be enabled by default.
  3. Every command might need a different spam detection sensibility. There might be commands that should only be executed once per 3 seconds, while others can be executed 5 times per second.
  4. Spam detection shouldn't only get implemented in say, server and client commands. It might also need to be added to menu options or places we can't even think of right now. Thus, the system should be easy to be reused.
This might be able to fulfill the points above:

Syntax: Select all

# A copy and pastable plugin. Test it out :-)

import time

from collections import deque

class SpamTracker(object):
"""A general purpose spam tracker."""

def __init__(self, action_count, time_frame):
"""Initialize the spam tracker.

:param int action_count:
Number of actions that can be executed in a specific time frame.
:param float time_frame:
The time frame (in seconds) in which the number of action can be
executed.
"""
self.last_actions = deque(maxlen=action_count)
self.action_count = action_count
self.time_frame = time_frame

def is_spamming(self):
"""Return ``True`` if the action is being spammed.

:rtype: bool
"""
self.cleanup()
return len(self.last_actions) >= self.action_count

def cleanup(self):
"""Remove all actions older than now."""
now = time.time()
while self.last_actions and self.last_actions[0] < now:
self.last_actions.popleft()

def add_action(self):
"""Add a new action to the tracker."""
self.last_actions.append(time.time() + self.time_frame)


trackers = {}

def get_spam_tracker(identifier, action_count, time_frame):
try:
tracker = trackers[identifier]
except KeyError:
tracker = trackers[identifier] = SpamTracker(action_count, time_frame)

return tracker

# Test
from commands.typed import TypedServerCommand

@TypedServerCommand('spam')
def on_spam(info):
# Get the spam tracker for this command. If it doesn't exist, it creates
# one that allows executing the commands up to 5 times in 3 seconds.
tracker = get_spam_tracker(on_spam, 5, 3)
if tracker.is_spamming():
print('The command is being spammed')
return

tracker.add_action()
print('Executed')
The example/test shows the reusability and it only takes 3 lines to implement it.

To fulfill the first 3 points, the spam detection system could be integrated like this:

Syntax: Select all

# anti_spam=True is the default value. It will use the spam detection values
# from core_settings.ini, which can be customized by the server owner.
@TypedServerCommand('spam', anti_spam=True)
def on_spam(info):
pass

# If a developer knows that this is a very critical command, he can use his own
# spam detection values by passing a tuple (action_count, time_frame).
# Now, the server owner lost the ability to customize the spam detection for
# this command. Thus, it's up to the developer to create two convars for these
# two values.
@TypedServerCommand('spam', anti_spam=(2, 5)) # 2 times in 5 seconds
def on_spam(info):
pass

# anti_spam=False will completely disable spam detection
@TypedServerCommand('spam', anti_spam=False)
def on_spam(info):
pass

I would love to hear your opinions on this.

Re: Anti-spam on Typed*Command

Posted: Tue Feb 27, 2018 5:01 pm
by Doldol
I like where you're going Ayuto, but the more I think about it, the more important I think it is to implement this on a per user basis.

I'm of the opinion that Source game servers focus on providing an equal experience to all players, so imo it wouldn't be good design to let one player spam a command so that others can't access it anymore, maybe the server won't lag now, but it's still a form of Denial of Service, since now the game server won't let other people use that command.

Also, I'm thinking of a different way of counting when something is getting spammed I'm not 100% if I find it better though, but it roughly goes as follows.
    - Every command has a call_expense value assigned to it.
    - Server op decides on a call_max_budget, call_budget_decrease_per_sec and call_budget_exceeded_cooldown.
    - Every player has a call_expended_budget.

    - When player calls any command the commands's call_expense gets added to player's call_expended_budget
    - Every second every player's call_expended_budget goes down by call_budget_decrease_per_sec until 0. (= behaviour, implemented more efficiently)
    - When player's call_expended_budget goes over call_max_budget they can't use any command that has call_expense > 0 for the duration of call_budget_exceeded_cooldown

This might introduce too much overhead than desired and is more complicated however I think it might better resemble the actual cost of using a command/menu page/anything else that shouldn't be used continuously.

Re: Anti-spam on Typed*Command

Posted: Tue Feb 27, 2018 5:30 pm
by Ayuto
Sorry, I didn't want to create the impression that this system can't be used a per user basis. Since get_spam_tracker accepts any identifier, you can easily create a per user tracker by passing an identifier that is unique to a user. E. g. you could pass a tuple (a constant + player index):

Syntax: Select all

tracker = get_spam_tracker((on_spam, index), 5, 3)
if tracker.is_spamming():
print('SPAM')
return

tracker.add_action()
print('Executed')