// 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; }