// Forlix FloodCheck
// http://forlix.org/, df@forlix.org
//
// Copyright (c) 2008-2009 Dominik Friedrichs
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
#include
#pragma semicolon 1
#define PLUGIN_VERSION "1.54"
#define PLUGIN_VERSION_CVAR "forlix_floodcheck_version"
#define FLOOD_CHAT_MSG "No spam!"
#define NON_PRINTABLE_MSG "Non-printable chars!"
#define NON_PRINTABLE_NAME_MSG "Remove non-printable characters from your name"
#define FLOOD_HARD_MSG_KICK "Kicked for hard-flooding, don't do this again"
#define FLOOD_HARD_MSG_BAN "Kicked and banned for %s, hard-flooding twice"
#define NETWORKID_TRACK 8
#define MAX_NAME_LEN 32
#define MAX_MSG_LEN 128
// convar defaults
#define FLOOD_CHAT_INTERVAL "4"
#define FLOOD_CHAT_NUM "3"
#define FLOOD_HARD_INTERVAL "2"
#define FLOOD_HARD_NUM "200"
#define FLOOD_HARD_BAN_TIME "2880"
#define EXCLUDE_CHAT_TRIGGERS "1"
public Plugin:myinfo =
{
name = "Forlix FloodCheck",
author = "Forlix (Dominik Friedrichs)",
description = "Protects against chat-, radio- and hard-command-flooding",
version = PLUGIN_VERSION,
url = "http://forlix.org/"
};
new Handle:h_chat_interval = INVALID_HANDLE;
new Handle:h_chat_num = INVALID_HANDLE;
new Handle:h_hard_interval = INVALID_HANDLE;
new Handle:h_hard_num = INVALID_HANDLE;
new Handle:h_hard_ban_time = INVALID_HANDLE;
new Handle:h_exclude_chat_triggers = INVALID_HANDLE;
new Handle:h_minfailures = INVALID_HANDLE;
new Handle:h_maxfailures = INVALID_HANDLE;
new Float:chat_interval = 0.0;
new chat_num = 0;
new Float:hard_interval = 0.0;
new hard_num = 0;
new hard_ban_time = 0;
new exclude_chat_triggers = 0;
new Float:game_chat_deadtime = 0.0;
new Float:game_radio_deadtime = 0.0;
new Float:p_time_lastchatmsg[MAXPLAYERS+1];
new p_cmdcnt_chat[MAXPLAYERS+1];
new Float:p_time_lasthardfld[MAXPLAYERS+1];
new p_cmdcnt_hard[MAXPLAYERS+1];
new bool:p_floodstate[MAXPLAYERS+1];
new bool:p_hardflood_kick[MAXPLAYERS+1];
new hardflood_id_index = 0;
new String:hardflood_id[NETWORKID_TRACK][32];
public bool:AskPluginLoad(Handle:myself,
bool:late,
String:error[],
err_max)
{
CreateNative("IsClientFlooding", Native_IsClientFlooding);
return(true);
}
public Native_IsClientFlooding(Handle:plugin,
numParams)
{
return(_:p_floodstate[GetNativeCell(1)]);
}
public OnPluginStart()
{
RegPluginLibrary("forlix_floodcheck");
// chat and radio flood checking
// FLOOD_CHAT_MSG and command blocking
RegConsoleCmd("say", FloodCheckChat);
RegConsoleCmd("say_team", FloodCheckChat);
// game-specific setup
decl String:gamedir[16];
GetGameFolderName(gamedir, sizeof(gamedir));
if(StrEqual(gamedir, "cstrike"))
// counter-strike: source
{
RegConsoleCmd("coverme", FloodCheckRadio);
RegConsoleCmd("enemydown", FloodCheckRadio);
RegConsoleCmd("enemyspot", FloodCheckRadio);
RegConsoleCmd("fallback", FloodCheckRadio);
RegConsoleCmd("followme", FloodCheckRadio);
RegConsoleCmd("getinpos", FloodCheckRadio);
RegConsoleCmd("getout", FloodCheckRadio);
RegConsoleCmd("go", FloodCheckRadio);
RegConsoleCmd("holdpos", FloodCheckRadio);
RegConsoleCmd("inposition", FloodCheckRadio);
RegConsoleCmd("needbackup", FloodCheckRadio);
RegConsoleCmd("negative", FloodCheckRadio);
RegConsoleCmd("regroup", FloodCheckRadio);
RegConsoleCmd("report", FloodCheckRadio);
RegConsoleCmd("reportingin", FloodCheckRadio);
RegConsoleCmd("roger", FloodCheckRadio);
RegConsoleCmd("sectorclear", FloodCheckRadio);
RegConsoleCmd("sticktog", FloodCheckRadio);
RegConsoleCmd("stormfront", FloodCheckRadio);
RegConsoleCmd("takepoint", FloodCheckRadio);
RegConsoleCmd("takingfire", FloodCheckRadio);
game_chat_deadtime = 0.75;
game_radio_deadtime = 1.5;
}
else
if(StrEqual(gamedir, "dod"))
// day of defeat: source
{
RegConsoleCmd("voice_areaclear", FloodCheckRadio);
RegConsoleCmd("voice_attack", FloodCheckRadio);
RegConsoleCmd("voice_backup", FloodCheckRadio);
RegConsoleCmd("voice_bazookaspotted", FloodCheckRadio);
RegConsoleCmd("voice_ceasefire", FloodCheckRadio);
RegConsoleCmd("voice_cover", FloodCheckRadio);
RegConsoleCmd("voice_coverflanks", FloodCheckRadio);
RegConsoleCmd("voice_displace", FloodCheckRadio);
RegConsoleCmd("voice_dropweapons", FloodCheckRadio);
RegConsoleCmd("voice_enemyahead", FloodCheckRadio);
RegConsoleCmd("voice_enemybehind", FloodCheckRadio);
RegConsoleCmd("voice_fallback", FloodCheckRadio);
RegConsoleCmd("voice_fireinhole", FloodCheckRadio);
RegConsoleCmd("voice_fireleft", FloodCheckRadio);
RegConsoleCmd("voice_fireright", FloodCheckRadio);
RegConsoleCmd("voice_gogogo", FloodCheckRadio);
RegConsoleCmd("voice_grenade", FloodCheckRadio);
RegConsoleCmd("voice_hold", FloodCheckRadio);
RegConsoleCmd("voice_left", FloodCheckRadio);
RegConsoleCmd("voice_medic", FloodCheckRadio);
RegConsoleCmd("voice_mgahead", FloodCheckRadio);
RegConsoleCmd("voice_moveupmg", FloodCheckRadio);
RegConsoleCmd("voice_needammo", FloodCheckRadio);
RegConsoleCmd("voice_negative", FloodCheckRadio);
RegConsoleCmd("voice_niceshot", FloodCheckRadio);
RegConsoleCmd("voice_right", FloodCheckRadio);
RegConsoleCmd("voice_sniper", FloodCheckRadio);
RegConsoleCmd("voice_sticktogether", FloodCheckRadio);
RegConsoleCmd("voice_takeammo", FloodCheckRadio);
RegConsoleCmd("voice_thanks", FloodCheckRadio);
RegConsoleCmd("voice_usebazooka", FloodCheckRadio);
RegConsoleCmd("voice_usegrens", FloodCheckRadio);
RegConsoleCmd("voice_usesmoke", FloodCheckRadio);
RegConsoleCmd("voice_wegothim", FloodCheckRadio);
RegConsoleCmd("voice_wtf", FloodCheckRadio);
RegConsoleCmd("voice_yessir", FloodCheckRadio);
game_chat_deadtime = 0.75;
game_radio_deadtime = 2.5;
}
else
if(StrEqual(gamedir, "tf"))
// team fortress 2
{
RegConsoleCmd("voicemenu", FloodCheckRadio);
game_chat_deadtime = 0.75;
game_radio_deadtime = 2.5;
}
else
// default values for all other games
// and no radio detection
{
game_chat_deadtime = 0.75;
game_radio_deadtime = 0.0;
}
new Handle:version_cvar = CreateConVar(PLUGIN_VERSION_CVAR,
PLUGIN_VERSION,
"Forlix FloodCheck plugin version",
FCVAR_PLUGIN|FCVAR_SPONLY|FCVAR_NOTIFY|FCVAR_PRINTABLEONLY);
SetConVarString(version_cvar, PLUGIN_VERSION, false, false);
h_chat_interval = CreateConVar("forlix_floodcheck_chat_interval",
FLOOD_CHAT_INTERVAL,
"Minimum average interval in seconds between a players chat- and radio-messages (0 to disable)",
0, true, 0.0, true, 20.0);
h_chat_num = CreateConVar("forlix_floodcheck_chat_num",
FLOOD_CHAT_NUM,
"Player is considered spamming after undershooting this many times",
0, true, 1.0, true, 75.0);
h_hard_interval = CreateConVar("forlix_floodcheck_hard_interval",
FLOOD_HARD_INTERVAL,
"Time in seconds in which commands are allowed (0 to disable)",
0, true, 0.0, true, 20.0);
h_hard_num = CreateConVar("forlix_floodcheck_hard_num",
FLOOD_HARD_NUM,
"Maximum number of client commands allowed in seconds",
0, true, 10.0, true, 750.0);
h_hard_ban_time = CreateConVar("forlix_floodcheck_hard_ban_time",
FLOOD_HARD_BAN_TIME,
"Number of minutes a client is banned for when hard-flooding twice in a row (0 to disable)",
0, true, 0.0, true, 20160.0);
h_exclude_chat_triggers = CreateConVar("forlix_floodcheck_exclude_chat_triggers",
EXCLUDE_CHAT_TRIGGERS,
"Excludes (1) or includes (0) SourceMod chat triggers in the chat flood detection",
0, true, 0.0, true, 1.0);
HookConVarChange(h_chat_interval, MyConVarChanged);
HookConVarChange(h_chat_num, MyConVarChanged);
HookConVarChange(h_hard_interval, MyConVarChanged);
HookConVarChange(h_hard_num, MyConVarChanged);
HookConVarChange(h_hard_ban_time, MyConVarChanged);
HookConVarChange(h_exclude_chat_triggers, MyConVarChanged);
// manually trigger convar readout
MyConVarChanged(INVALID_HANDLE, "0", "0");
// mark some dangerous commands as cheat
// some of these even cause a server to crash
SetCheatFlag("dbghist_addline");
SetCheatFlag("dbghist_dump");
SetCheatFlag("dump_entity_sizes");
SetCheatFlag("dump_globals");
SetCheatFlag("dump_terrain");
SetCheatFlag("dumpcountedstrings");
SetCheatFlag("dumpentityfactories");
SetCheatFlag("dumpeventqueue");
SetCheatFlag("groundlist");
SetCheatFlag("listmodels");
SetCheatFlag("mem_dump");
SetCheatFlag("mp_dump_timers");
SetCheatFlag("npc_ammo_deplete");
SetCheatFlag("npc_heal");
SetCheatFlag("npc_speakall");
SetCheatFlag("npc_thinknow");
SetCheatFlag("physics_budget");
SetCheatFlag("physics_debug_entity");
SetCheatFlag("physics_report_active");
SetCheatFlag("physics_select");
SetCheatFlag("report_entities");
SetCheatFlag("report_touchlinks");
SetCheatFlag("rr_reloadresponsesystems");
SetCheatFlag("scene_flush");
SetCheatFlag("soundlist");
SetCheatFlag("soundscape_flush");
SetCheatFlag("sv_findsoundname");
SetCheatFlag("sv_soundemitter_filecheck");
SetCheatFlag("sv_soundemitter_flush");
SetCheatFlag("sv_soundscape_printdebuginfo");
SetCheatFlag("wc_update_entity");
// up the rcon failure limits to be able to set them higher
// in OnConfigsExecuted in order to prevent server crashes
h_minfailures = FindConVar("sv_rcon_minfailures");
h_maxfailures = FindConVar("sv_rcon_maxfailures");
SetConVarBounds(h_minfailures, ConVarBound_Upper, true, 20000.0);
SetConVarBounds(h_maxfailures, ConVarBound_Upper, true, 20000.0);
return;
}
public OnConfigsExecuted()
{
// up the rcon failure values to avoid server crashes
SetConVarFloat(h_minfailures, 20000.0);
SetConVarFloat(h_maxfailures, 20000.0);
return;
}
public bool:OnClientConnect(client,
String:rejectmsg[],
maxlen)
{
if(!IsClientNamePrintable(client))
{
strcopy(rejectmsg, maxlen, NON_PRINTABLE_NAME_MSG);
return(false);
}
return(true);
}
public OnClientSettingsChanged(client)
{
if(!IsClientNamePrintable(client))
KickClient(client, NON_PRINTABLE_NAME_MSG);
return;
}
public OnClientConnected(client)
{
p_time_lastchatmsg[client] = 0.0;
p_cmdcnt_chat[client] = 0;
p_time_lasthardfld[client] = 0.0;
p_cmdcnt_hard[client] = 0;
p_floodstate[client] = false;
p_hardflood_kick[client] = false;
return;
}
public OnClientDisconnect(client)
{
decl String:str_networkid[32];
if(p_hardflood_kick[client]
|| !hard_ban_time
|| !GetClientAuthString(client, str_networkid, sizeof(str_networkid)))
// dont try to remove networkid if we kicked client,
// just cant read the id, or banning is disabled
return;
for(new i = 0; i < NETWORKID_TRACK; i++)
if(hardflood_id[i][0]
&& StrEqual(hardflood_id[i], str_networkid))
hardflood_id[i][0] = '\0';
return;
}
public Action:FloodCheckChat(client, args)
{
if(!client)
return(Plugin_Continue);
new excl = 0;
if(IsChatTrigger())
excl = exclude_chat_triggers;
else
OnClientCommand(client, args);
if(FloodDeadtime(client,
game_chat_deadtime))
return(Plugin_Handled);
if(!excl
&& FloodCheck(client))
return(Plugin_Handled);
if(FilterChat(client))
return(Plugin_Handled);
return(Plugin_Continue);
}
public Action:FloodCheckRadio(client, args)
{
if(!client)
return(Plugin_Continue);
if(FloodDeadtime(client,
game_radio_deadtime))
return(Plugin_Handled);
if(IsPlayerAlive(client)
&& FloodCheck(client))
return(Plugin_Handled);
return(Plugin_Continue);
}
bool:FloodDeadtime(client,
Float:deadtime)
{
static Float:time_nc[MAXPLAYERS+1];
new Float:time_c = GetTickedTime();
if(time_c < time_nc[client])
// ignore and swallow calls within this deadtime
// this is built into the engine as well
return(true);
time_nc[client] = time_c + deadtime;
return(false);
}
bool:FloodCheck(client)
{
if(!client
|| !chat_interval)
return(false);
new Float:time_c = GetTickedTime();
if(time_c < p_time_lastchatmsg[client] + chat_interval)
// client has undershot the chat msg interval
{
p_time_lastchatmsg[client] = time_c;
if(p_cmdcnt_chat[client] < chat_num)
// add a flood token
p_cmdcnt_chat[client]++;
if(p_cmdcnt_chat[client] >= chat_num)
// maximum tokens accumulated
// client is now flooding
{
p_floodstate[client] = true;
PrintToChat(client, FLOOD_CHAT_MSG);
return(true);
}
}
else
// clients chat msg frequency is below the maximum
{
p_time_lastchatmsg[client] = time_c;
if(p_cmdcnt_chat[client] > 0)
// remove a flood token
p_cmdcnt_chat[client]--;
if(p_cmdcnt_chat[client] < 0)
// level out at zero
p_cmdcnt_chat[client] = 0;
}
p_floodstate[client] = false;
return(false);
}
bool:FilterChat(client)
{
decl String:text[MAX_MSG_LEN];
text[0] = '\0';
GetCmdArgString(text, sizeof(text));
if(!IsStringPrintable(text))
{
PrintToChat(client, NON_PRINTABLE_MSG);
return(true);
}
return(false);
}
public Action:OnClientCommand(client, args)
{
if(!client
|| !hard_interval
|| ++p_cmdcnt_hard[client] < hard_num)
return(Plugin_Continue);
new Float:time_c = GetTickedTime();
if(time_c >= p_time_lasthardfld[client] + hard_interval
|| IsFakeClient(client)
|| IsClientInKickQueue(client))
// client command frequency ok
// or client already about to be kicked
{
p_time_lasthardfld[client] = time_c;
p_cmdcnt_hard[client] = 0;
return(Plugin_Continue);
}
// reaching this, we need to kick the client
decl String:str_networkid[32];
p_hardflood_kick[client] = true;
if(hard_ban_time
&& GetClientAuthString(client, str_networkid, sizeof(str_networkid)))
// if banning is enabled and we got the networkid
// check if we already kicked the client
// for flooding within his last session
{
for(new i = 0; i < NETWORKID_TRACK; i++)
if(hardflood_id[i][0]
&& StrEqual(hardflood_id[i], str_networkid))
// client has flooded before, this time we ban
{
hardflood_id[i][0] = '\0';
decl String:reason[128];
decl String:ban_time[32];
FriendlyTime(hard_ban_time*60, ban_time, sizeof(ban_time), false);
Format(reason, sizeof(reason), FLOOD_HARD_MSG_BAN, ban_time);
BanClient(client, hard_ban_time, BANFLAG_AUTO, reason, reason, "Hard-flooding");
return(Plugin_Handled);
}
// store clients networkid in rolling buffer
strcopy(hardflood_id[hardflood_id_index++], sizeof(hardflood_id[]), str_networkid);
if(hardflood_id_index >= NETWORKID_TRACK)
// rolling buffer - when end is reached, continue at the beginning
hardflood_id_index = 0;
}
KickClient(client, FLOOD_HARD_MSG_KICK);
return(Plugin_Handled);
}
bool:FriendlyTime(time_s,
String:str_ftime[],
str_ftime_len,
bool:compact=false)
{
if(time_s < 0)
{
str_ftime[0] = '\0';
return(false);
}
new String:days_postfix[] = " days";
new String:hrs_postfix[] = " hours";
new String:mins_postfix[] = " minutes";
if(compact)
{
days_postfix = "d";
hrs_postfix = "h";
mins_postfix = "m";
}
new days = time_s/86400;
new hrs = (time_s/3600)%24;
new mins = (time_s/60)%60;
if(time_s < 60)
Format(str_ftime, str_ftime_len,
"< 1%s",
mins_postfix);
else
if(time_s < 3600)
Format(str_ftime, str_ftime_len,
"%d%s",
mins, mins_postfix);
else
if(time_s < 86400)
{
if(mins)
Format(str_ftime, str_ftime_len,
"%d%s %d%s",
hrs, hrs_postfix, mins, mins_postfix);
else
Format(str_ftime, str_ftime_len,
"%d%s",
hrs, hrs_postfix);
}
else
{
if(hrs)
Format(str_ftime, str_ftime_len,
"%d%s %d%s",
days, days_postfix, hrs, hrs_postfix);
else
Format(str_ftime, str_ftime_len,
"%d%s",
days, days_postfix);
}
return(true);
}
bool:IsClientNamePrintable(client)
{
decl String:name[MAX_NAME_LEN];
name[0] = '\0';
GetClientName(client, name, sizeof(name));
return(IsStringPrintable(name));
}
bool:IsStringPrintable(const String:str[])
{
// returns false if string contains control chars:
// - line breaks
// - bell char (causes severe lags on windows servers)
// - CS:S color codes (0x01 etc...)
// - or if string has zero length
new c = 0;
do
{
if(str[c] < '\x20')
return(false);
}
while(str[++c]);
return(true);
}
bool:SetCheatFlag(const String:cvar[])
{
new flags = GetCommandFlags(cvar);
if(flags == INVALID_FCVAR_FLAGS)
return(false);
SetCommandFlags(cvar, flags|FCVAR_CHEAT);
return(true);
}
public MyConVarChanged(Handle:convar,
const String:oldValue[],
const String:newValue[])
{
chat_interval = GetConVarFloat(h_chat_interval);
chat_num = GetConVarInt(h_chat_num);
hard_interval = GetConVarFloat(h_hard_interval);
hard_num = GetConVarInt(h_hard_num);
hard_ban_time = GetConVarInt(h_hard_ban_time);
exclude_chat_triggers = GetConVarInt(h_exclude_chat_triggers);
return;
}