reworked antiflood. it now has some logic in core to take care of loading order nastiness, and to fully prevent trigger spamming

--HG--
extra : convert_revision : svn%3A39bc706e-5318-0410-9160-8a85361fbb7c/trunk%401931
This commit is contained in:
David Anderson 2008-03-12 03:33:52 +00:00
parent b10bf9d31a
commit aed775162c
7 changed files with 219 additions and 52 deletions

View File

@ -34,6 +34,8 @@
#include "sm_stringutil.h"
#include "ConCmdManager.h"
#include "PlayerManager.h"
#include "Translator.h"
#include "HalfLife2.h"
/* :HACKHACK: We can't SH_DECL here because ConCmdManager.cpp does.
* While the OB build only runs on MM:S 1.6.0+ (SH 5+), the older one
@ -55,6 +57,7 @@ extern bool __SourceHook_FHAddConCommandDispatch(void *, bool, class fastdelegat
ChatTriggers g_ChatTriggers;
bool g_bSupressSilentFails = false;
CPhraseFile *g_pFloodPhrases = NULL;
ChatTriggers::ChatTriggers() : m_pSayCmd(NULL), m_bWillProcessInPost(false),
m_bTriggerWasSilent(false), m_ReplyTo(SM_REPLY_CONSOLE)
@ -103,6 +106,20 @@ ConfigResult ChatTriggers::OnSourceModConfigChanged(const char *key,
return ConfigResult_Ignore;
}
void ChatTriggers::OnSourceModAllInitialized()
{
m_pShouldFloodBlock = g_Forwards.CreateForward("OnClientFloodCheck", ET_Event, 1, NULL, Param_Cell);
m_pDidFloodBlock = g_Forwards.CreateForward("OnClientFloodResult", ET_Event, 2, NULL, Param_Cell, Param_Cell);
}
void ChatTriggers::OnSourceModAllInitialized_Post()
{
unsigned int file_id;
file_id = g_Translator.FindOrAddPhraseFile("antiflood.phrases.txt");
g_pFloodPhrases = g_Translator.GetFileByIndex(file_id);
}
void ChatTriggers::OnSourceModGameInitialized()
{
unsigned int total = 2;
@ -155,6 +172,9 @@ void ChatTriggers::OnSourceModShutdown()
SH_REMOVE_HOOK_MEMFUNC(ConCommand, Dispatch, m_pSayCmd, this, &ChatTriggers::OnSayCommand_Post, true);
SH_REMOVE_HOOK_MEMFUNC(ConCommand, Dispatch, m_pSayCmd, this, &ChatTriggers::OnSayCommand_Pre, false);
}
g_Forwards.ReleaseForward(m_pShouldFloodBlock);
g_Forwards.ReleaseForward(m_pDidFloodBlock);
}
#if defined ORANGEBOX_BUILD
@ -167,6 +187,7 @@ void ChatTriggers::OnSayCommand_Pre()
#endif
int client = g_ConCmds.GetCommandClient();
m_bIsChatTrigger = false;
m_bWasFloodedMessage = false;
/* The server console cannot do this */
if (client == 0)
@ -181,6 +202,35 @@ void ChatTriggers::OnSayCommand_Pre()
RETURN_META(MRES_IGNORED);
}
/* Check if we need to block this message from being sent */
if (ClientIsFlooding(client))
{
char buffer[128];
/* :TODO: log an error? */
if (g_Translator.CoreTransEx(g_pFloodPhrases,
client,
buffer,
sizeof(buffer),
"Flooding the server",
NULL,
NULL)
!= Trans_Okay)
{
UTIL_Format(buffer, sizeof(buffer), "You are flooding the server!");
}
/* :TODO: we should probably kick people who spam too much. */
char fullbuffer[192];
UTIL_Format(fullbuffer, sizeof(fullbuffer), "[SM] %s", buffer);
g_HL2.TextMsg(client, HUD_PRINTTALK, fullbuffer);
m_bWasFloodedMessage = true;
RETURN_META(MRES_SUPERCEDE);
}
/* Handle quoted string sets */
bool is_quoted = false;
if (args[0] == '"')
@ -197,7 +247,9 @@ void ChatTriggers::OnSayCommand_Pre()
{
is_trigger = true;
args = &args[m_PubTriggerSize];
} else if (m_PrivTriggerSize && strncmp(args, m_PrivTrigger, m_PrivTriggerSize) == 0) {
}
else if (m_PrivTriggerSize && strncmp(args, m_PrivTrigger, m_PrivTriggerSize) == 0)
{
is_trigger = true;
is_silent = true;
args = &args[m_PrivTriggerSize];
@ -250,6 +302,7 @@ void ChatTriggers::OnSayCommand_Post()
#endif
{
m_bIsChatTrigger = false;
m_bWasFloodedMessage = false;
if (m_bWillProcessInPost)
{
/* Reset this for re-entrancy */
@ -355,3 +408,35 @@ bool ChatTriggers::IsChatTrigger()
{
return m_bIsChatTrigger;
}
bool ChatTriggers::ClientIsFlooding(int client)
{
bool is_flooding = false;
if (m_pShouldFloodBlock->GetFunctionCount() != 0)
{
cell_t res = 0;
m_pShouldFloodBlock->PushCell(client);
m_pShouldFloodBlock->Execute(&res);
if (res != 0)
{
is_flooding = true;
}
}
if (m_pDidFloodBlock->GetFunctionCount() != 0)
{
m_pDidFloodBlock->PushCell(client);
m_pDidFloodBlock->PushCell(is_flooding ? 1 : 0);
m_pDidFloodBlock->Execute(NULL);
}
return is_flooding;
}
bool ChatTriggers::WasFloodedMessage()
{
return m_bWasFloodedMessage;
}

View File

@ -36,6 +36,7 @@
#include "sourcemm_api.h"
#include <IGameHelpers.h>
#include <compat_wrappers.h>
#include <IForwardSys.h>
class ChatTriggers : public SMGlobalClass
{
@ -43,6 +44,8 @@ public:
ChatTriggers();
~ChatTriggers();
public: //SMGlobalClass
void OnSourceModAllInitialized();
void OnSourceModAllInitialized_Post();
void OnSourceModGameInitialized();
void OnSourceModShutdown();
ConfigResult OnSourceModConfigChanged(const char *key,
@ -62,8 +65,10 @@ public:
unsigned int GetReplyTo();
unsigned int SetReplyTo(unsigned int reply);
bool IsChatTrigger();
bool WasFloodedMessage();
private:
bool PreProcessTrigger(edict_t *pEdict, const char *args, bool is_quoted);
bool ClientIsFlooding(int client);
private:
ConCommand *m_pSayCmd;
ConCommand *m_pSayTeamCmd;
@ -74,8 +79,11 @@ private:
bool m_bWillProcessInPost;
bool m_bTriggerWasSilent;
bool m_bIsChatTrigger;
bool m_bWasFloodedMessage;
unsigned int m_ReplyTo;
char m_ToExecute[300];
IForward *m_pShouldFloodBlock;
IForward *m_pDidFloodBlock;
};
extern ChatTriggers g_ChatTriggers;

View File

@ -299,6 +299,16 @@ void ConCmdManager::InternalDispatch(const CCommand &command)
return;
}
/* This is a hack to prevent say triggers from firing on messages that were
* blocked because of flooding. We won't remove this, but the hack will get
* "nicer" when we expose explicit say hooks.
*/
if (META_RESULT_STATUS == MRES_SUPERCEDE
&& g_ChatTriggers.WasFloodedMessage())
{
return;
}
cell_t result = Pl_Continue;
int args = command.ArgC() - 1;
@ -456,7 +466,9 @@ bool ConCmdManager::CheckCommandAccess(int client, const char *cmd, FlagBits cmd
if (rule == Command_Allow)
{
return true;
} else if (rule == Command_Deny) {
}
else if (rule == Command_Deny)
{
return false;
}
}
@ -486,18 +498,20 @@ bool ConCmdManager::CheckAccess(int client, const char *cmd, AdminCmdInfo *pAdmi
if (g_Translator.CoreTrans(client, buffer, sizeof(buffer), "No Access", NULL, NULL)
!= Trans_Okay)
{
snprintf(buffer, sizeof(buffer), "You do not have access to this command");
UTIL_Format(buffer, sizeof(buffer), "You do not have access to this command");
}
unsigned int replyto = g_ChatTriggers.GetReplyTo();
if (replyto == SM_REPLY_CONSOLE)
{
char fullbuffer[192];
snprintf(fullbuffer, sizeof(fullbuffer), "[SM] %s.\n", buffer);
UTIL_Format(fullbuffer, sizeof(fullbuffer), "[SM] %s.\n", buffer);
engine->ClientPrintf(pEdict, fullbuffer);
} else if (replyto == SM_REPLY_CHAT) {
}
else if (replyto == SM_REPLY_CHAT)
{
char fullbuffer[192];
snprintf(fullbuffer, sizeof(fullbuffer), "[SM] %s.", buffer);
UTIL_Format(fullbuffer, sizeof(fullbuffer), "[SM] %s.", buffer);
g_HL2.TextMsg(client, HUD_PRINTTALK, fullbuffer);
}

View File

@ -949,25 +949,19 @@ size_t Translator::Translate(char *buffer, size_t maxlength, void **params, cons
return gnprintf(buffer, maxlength, pTrans->szPhrase, new_params);
}
TransError Translator::CoreTrans(int client,
char *buffer,
size_t maxlength,
const char *phrase,
void **params,
size_t *outlen)
TransError Translator::CoreTransEx(CPhraseFile *pFile,
int client,
char *buffer,
size_t maxlength,
const char *phrase,
void **params,
size_t *outlen)
{
/* :TODO: do language stuff here */
if (!g_pCorePhrases)
{
return Trans_BadPhraseFile;
}
Translation trans;
TransError err;
/* Using server lang temporarily until client lang stuff is implemented */
if ((err=g_pCorePhrases->GetTranslation(phrase, m_ServerLang, &trans)) != Trans_Okay)
if ((err = pFile->GetTranslation(phrase, m_ServerLang, &trans)) != Trans_Okay)
{
return err;
}
@ -982,6 +976,21 @@ TransError Translator::CoreTrans(int client,
return Trans_Okay;
}
TransError Translator::CoreTrans(int client,
char *buffer,
size_t maxlength,
const char *phrase,
void **params,
size_t *outlen)
{
if (!g_pCorePhrases)
{
return Trans_BadPhraseFile;
}
return CoreTransEx(g_pCorePhrases, client, buffer, maxlength, phrase, params, outlen);
}
unsigned int Translator::GetServerLanguage()
{
return m_ServerLang;

View File

@ -140,6 +140,13 @@ public:
bool GetLanguageByName(const char *name, unsigned int *index);
size_t Translate(char *buffer, size_t maxlength, void **params, const Translation *pTrans);
CPhraseFile *GetFileByIndex(unsigned int index);
TransError CoreTransEx(CPhraseFile *pFile,
int client,
char *buffer,
size_t maxlength,
const char *phrase,
void **params,
size_t *outlen=NULL);
TransError CoreTrans(int client,
char *buffer,
size_t maxlength,

View File

@ -51,10 +51,6 @@ new Handle:sm_flood_time; /* Handle to sm_flood_time convar */
public OnPluginStart()
{
LoadTranslations("antiflood.phrases");
RegConsoleCmd("say", CheckChatFlood);
RegConsoleCmd("say_team", CheckChatFlood);
sm_flood_time = CreateConVar("sm_flood_time", "0.75", "Amount of time allowed between chat messages");
}
@ -64,41 +60,63 @@ public OnClientPutInServer(client)
g_FloodTokens[client] = 0;
}
public Action:CheckChatFlood(client, args)
new Float:max_chat;
public bool:OnClientFloodCheck(client)
{
/* Chat from server console shouldn't be checked for flooding */
if (client == 0)
max_chat = GetConVarFloat(sm_flood_time);
if (max_chat <= 0.0
|| (GetUserFlagBits(client) & ADMFLAG_ROOT) == ADMFLAG_ROOT)
{
return Plugin_Continue;
return false;
}
new Float:maxChat = GetConVarFloat(sm_flood_time);
PrintToServer("OCFC: %f %f %d", g_LastTime[client], GetGameTime(), g_FloodTokens[client]);
if (maxChat > 0.0)
if (g_LastTime[client] > GetGameTime())
{
new Float:curTime = GetGameTime();
if (g_LastTime[client] > curTime)
/* If player has 3 or more flood tokens, block their message */
if (g_FloodTokens[client] >= 3)
{
/* If player has 3 or more flood tokens, block their message */
if (g_FloodTokens[client] >= 3)
{
PrintToChat(client, "[SM] %t", "Flooding the server");
g_LastTime[client] = curTime + maxChat + 3.0;
return Plugin_Stop;
}
/* Add one flood token when player goes over chat time limit */
g_FloodTokens[client]++;
} else if (g_FloodTokens[client]) {
/* Remove one flood token when player chats within time limit (slow decay) */
g_FloodTokens[client]--;
return true;
}
/* Store last time of chat usage */
g_LastTime[client] = curTime + maxChat;
}
return Plugin_Continue;
return false;
}
public OnClientFloodResult(client, bool:blocked)
{
if (max_chat <= 0.0
|| (GetUserFlagBits(client) & ADMFLAG_ROOT) == ADMFLAG_ROOT)
{
return;
}
new Float:curTime = GetGameTime();
new Float:newTime = curTime + max_chat;
PrintToServer("OCFR: %f, %f", g_LastTime[client], GetGameTime());
if (g_LastTime[client] > curTime)
{
/* If the last message was blocked, update their time limit */
if (blocked)
{
newTime += 3.0;
}
/* Add one flood token when player goes over chat time limit */
else if (g_FloodTokens[client] < 3)
{
g_FloodTokens[client]++;
}
}
else if (g_FloodTokens[client] > 0)
{
/* Remove one flood token when player chats within time limit (slow decay) */
g_FloodTokens[client]--;
}
g_LastTime[client] = newTime;
}

View File

@ -505,6 +505,32 @@ native Handle:ReadMapList(Handle:array=INVALID_HANDLE,
*/
native SetMapListCompatBind(const String:name[], const String:file[]);
/**
* Called when a client has sent chat text. This must return either true or
* false to indicate that a client is or is not spamming the server.
*
* The return value is a hint only. Core or another plugin may decide
* otherwise.
*
* @param client Client index. The server (0) will never be passed.
* @return True if client is spamming the server, false otherwise.
*/
forward bool:OnClientFloodCheck(client);
/**
* Called after a client's flood check has been computed. This can be used
* by antiflood algorithms to decay/increase flooding weights.
*
* Since the result from "OnClientFloodCheck" isn't guaranteed to be the
* final result, it is generally a good idea to use this to play with other
* algorithms nicely.
*
* @param client Client index. The server (0) will never be passed.
* @param blocked True if client flooded last "say", false otherwise.
* @noreturn
*/
forward OnClientFloodResult(client, bool:blocked);
#include <helpers>
#include <entity>
#include <entity_prop_stocks>