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 "sm_stringutil.h"
#include "ConCmdManager.h" #include "ConCmdManager.h"
#include "PlayerManager.h" #include "PlayerManager.h"
#include "Translator.h"
#include "HalfLife2.h"
/* :HACKHACK: We can't SH_DECL here because ConCmdManager.cpp does. /* :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 * 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; ChatTriggers g_ChatTriggers;
bool g_bSupressSilentFails = false; bool g_bSupressSilentFails = false;
CPhraseFile *g_pFloodPhrases = NULL;
ChatTriggers::ChatTriggers() : m_pSayCmd(NULL), m_bWillProcessInPost(false), ChatTriggers::ChatTriggers() : m_pSayCmd(NULL), m_bWillProcessInPost(false),
m_bTriggerWasSilent(false), m_ReplyTo(SM_REPLY_CONSOLE) m_bTriggerWasSilent(false), m_ReplyTo(SM_REPLY_CONSOLE)
@ -103,6 +106,20 @@ ConfigResult ChatTriggers::OnSourceModConfigChanged(const char *key,
return ConfigResult_Ignore; 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() void ChatTriggers::OnSourceModGameInitialized()
{ {
unsigned int total = 2; 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_Post, true);
SH_REMOVE_HOOK_MEMFUNC(ConCommand, Dispatch, m_pSayCmd, this, &ChatTriggers::OnSayCommand_Pre, false); 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 #if defined ORANGEBOX_BUILD
@ -167,6 +187,7 @@ void ChatTriggers::OnSayCommand_Pre()
#endif #endif
int client = g_ConCmds.GetCommandClient(); int client = g_ConCmds.GetCommandClient();
m_bIsChatTrigger = false; m_bIsChatTrigger = false;
m_bWasFloodedMessage = false;
/* The server console cannot do this */ /* The server console cannot do this */
if (client == 0) if (client == 0)
@ -181,6 +202,35 @@ void ChatTriggers::OnSayCommand_Pre()
RETURN_META(MRES_IGNORED); 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 */ /* Handle quoted string sets */
bool is_quoted = false; bool is_quoted = false;
if (args[0] == '"') if (args[0] == '"')
@ -197,7 +247,9 @@ void ChatTriggers::OnSayCommand_Pre()
{ {
is_trigger = true; is_trigger = true;
args = &args[m_PubTriggerSize]; 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_trigger = true;
is_silent = true; is_silent = true;
args = &args[m_PrivTriggerSize]; args = &args[m_PrivTriggerSize];
@ -250,6 +302,7 @@ void ChatTriggers::OnSayCommand_Post()
#endif #endif
{ {
m_bIsChatTrigger = false; m_bIsChatTrigger = false;
m_bWasFloodedMessage = false;
if (m_bWillProcessInPost) if (m_bWillProcessInPost)
{ {
/* Reset this for re-entrancy */ /* Reset this for re-entrancy */
@ -355,3 +408,35 @@ bool ChatTriggers::IsChatTrigger()
{ {
return m_bIsChatTrigger; 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 "sourcemm_api.h"
#include <IGameHelpers.h> #include <IGameHelpers.h>
#include <compat_wrappers.h> #include <compat_wrappers.h>
#include <IForwardSys.h>
class ChatTriggers : public SMGlobalClass class ChatTriggers : public SMGlobalClass
{ {
@ -43,6 +44,8 @@ public:
ChatTriggers(); ChatTriggers();
~ChatTriggers(); ~ChatTriggers();
public: //SMGlobalClass public: //SMGlobalClass
void OnSourceModAllInitialized();
void OnSourceModAllInitialized_Post();
void OnSourceModGameInitialized(); void OnSourceModGameInitialized();
void OnSourceModShutdown(); void OnSourceModShutdown();
ConfigResult OnSourceModConfigChanged(const char *key, ConfigResult OnSourceModConfigChanged(const char *key,
@ -62,8 +65,10 @@ public:
unsigned int GetReplyTo(); unsigned int GetReplyTo();
unsigned int SetReplyTo(unsigned int reply); unsigned int SetReplyTo(unsigned int reply);
bool IsChatTrigger(); bool IsChatTrigger();
bool WasFloodedMessage();
private: private:
bool PreProcessTrigger(edict_t *pEdict, const char *args, bool is_quoted); bool PreProcessTrigger(edict_t *pEdict, const char *args, bool is_quoted);
bool ClientIsFlooding(int client);
private: private:
ConCommand *m_pSayCmd; ConCommand *m_pSayCmd;
ConCommand *m_pSayTeamCmd; ConCommand *m_pSayTeamCmd;
@ -74,8 +79,11 @@ private:
bool m_bWillProcessInPost; bool m_bWillProcessInPost;
bool m_bTriggerWasSilent; bool m_bTriggerWasSilent;
bool m_bIsChatTrigger; bool m_bIsChatTrigger;
bool m_bWasFloodedMessage;
unsigned int m_ReplyTo; unsigned int m_ReplyTo;
char m_ToExecute[300]; char m_ToExecute[300];
IForward *m_pShouldFloodBlock;
IForward *m_pDidFloodBlock;
}; };
extern ChatTriggers g_ChatTriggers; extern ChatTriggers g_ChatTriggers;

View File

@ -299,6 +299,16 @@ void ConCmdManager::InternalDispatch(const CCommand &command)
return; 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; cell_t result = Pl_Continue;
int args = command.ArgC() - 1; int args = command.ArgC() - 1;
@ -456,7 +466,9 @@ bool ConCmdManager::CheckCommandAccess(int client, const char *cmd, FlagBits cmd
if (rule == Command_Allow) if (rule == Command_Allow)
{ {
return true; return true;
} else if (rule == Command_Deny) { }
else if (rule == Command_Deny)
{
return false; 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) if (g_Translator.CoreTrans(client, buffer, sizeof(buffer), "No Access", NULL, NULL)
!= Trans_Okay) != 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(); unsigned int replyto = g_ChatTriggers.GetReplyTo();
if (replyto == SM_REPLY_CONSOLE) if (replyto == SM_REPLY_CONSOLE)
{ {
char fullbuffer[192]; char fullbuffer[192];
snprintf(fullbuffer, sizeof(fullbuffer), "[SM] %s.\n", buffer); UTIL_Format(fullbuffer, sizeof(fullbuffer), "[SM] %s.\n", buffer);
engine->ClientPrintf(pEdict, fullbuffer); engine->ClientPrintf(pEdict, fullbuffer);
} else if (replyto == SM_REPLY_CHAT) { }
else if (replyto == SM_REPLY_CHAT)
{
char fullbuffer[192]; 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); 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); return gnprintf(buffer, maxlength, pTrans->szPhrase, new_params);
} }
TransError Translator::CoreTrans(int client, TransError Translator::CoreTransEx(CPhraseFile *pFile,
char *buffer, int client,
size_t maxlength, char *buffer,
const char *phrase, size_t maxlength,
void **params, const char *phrase,
size_t *outlen) void **params,
size_t *outlen)
{ {
/* :TODO: do language stuff here */
if (!g_pCorePhrases)
{
return Trans_BadPhraseFile;
}
Translation trans; Translation trans;
TransError err; TransError err;
/* Using server lang temporarily until client lang stuff is implemented */ /* 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; return err;
} }
@ -982,6 +976,21 @@ TransError Translator::CoreTrans(int client,
return Trans_Okay; 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() unsigned int Translator::GetServerLanguage()
{ {
return m_ServerLang; return m_ServerLang;

View File

@ -140,6 +140,13 @@ public:
bool GetLanguageByName(const char *name, unsigned int *index); bool GetLanguageByName(const char *name, unsigned int *index);
size_t Translate(char *buffer, size_t maxlength, void **params, const Translation *pTrans); size_t Translate(char *buffer, size_t maxlength, void **params, const Translation *pTrans);
CPhraseFile *GetFileByIndex(unsigned int index); 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, TransError CoreTrans(int client,
char *buffer, char *buffer,
size_t maxlength, size_t maxlength,

View File

@ -51,10 +51,6 @@ new Handle:sm_flood_time; /* Handle to sm_flood_time convar */
public OnPluginStart() 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"); 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; 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 */ max_chat = GetConVarFloat(sm_flood_time);
if (client == 0)
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 player has 3 or more flood tokens, block their message */
if (g_FloodTokens[client] >= 3)
if (g_LastTime[client] > curTime)
{ {
/* If player has 3 or more flood tokens, block their message */ return true;
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]--;
} }
/* 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[]); 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 <helpers>
#include <entity> #include <entity>
#include <entity_prop_stocks> #include <entity_prop_stocks>