// Notes: // Memory allocated via the "new" keyword IS garbage collected. Handles are not. Close handles when you're done with them // Handles are closed when the plugin is unloaded, so if the lifetime of a handle is the plugin's lifetime, don't worry about // closing it // The contents of ArrayList objects are lost on SERVER RESTART ONLY. Any persistent data should be stored in a key-value file // OR in a database // Basically every string function will add a null terminator, so add `+ 1` to the end of any defined size string definition // It is possible to throw an error with the ThrowError function, but anything that would bother should probably disable the // plugin entirely, so SetFailState is better // Large local var allocations if done commonly should be made static. Increases baseline memory usage, but lowers time spent // allocating memory. That being said, don't bother unless it's actually a problem. Name checks should be relatively // infrequent, and everything else exponentially less frequent. Dropping a frame or 2 to allocate 2048 bytes of memory // every century shouldn't be the end of the world // The database should contain: // The string used to compile a regex pattern, sanitized to avoid sql injection attacks // The steamid of the admin who added a regex pattern // The date (YYYY-MM-DD) a regex pattern was added // The time (HH:MM:SS.SSS) a regex pattern was added // In the future, this may change to include regex compilation flags, kick/ban modes, etc. . Depends on how many features // I want to cram into this given how fundamentally shitty SourcePawn as a language is. I'm not trying to keep track of 17 // different arrays and make sure that the indicies between them never get out of date. Maybe enum structs will solve my // concerns, maybe not // Plugins are loaded/unloaded on server start/end, and on map changes IF the plugin has been modified. This means, because I'm // first writing to the database and then updating the lists, there realistically shouldn't be any weird memory problems. // That makes this a whole lot easier // If database locking takes too long I might need to use threaded calls, which sounds like a fucking nightmare // TODO: Overhaul logging and print everything to console. I can't debug this as of now #pragma newdecls required #pragma semicolon 1 #include #include #define _SMNB__DEBUG_304142124110815__VERBOSE 1 public Plugin myinfo = { name = "SM Name Blocker", description = "A simple plugin to stop people with blacklisted names from joining a server", author = "NW/RL", version = "alpha-0.1", url = "git.dabikers.online/smnameblocker" }; // Size of a handle, because the actual sizeof() function/macro doesn't work on "scalar"s (which is fucking stupid because I'm asking for the literal size of the variable, not the length. If I wanted length, I'd use something like lengthof) #define HANDLE_SIZE (32) // The literal character width of a steamid in 64bit representation. Not necessarily large enough to store a steamid64 in a string #define LITERALSTEAMID64LENGTH (17) // Length of a steamid64, suitably large enough to store in a string #define STEAMID64LENGTH (LITERALSTEAMID64LENGTH + 1) #define PATTERN_MAX_LEN (512 + 1) /* 512 chars + null terminator */ #define DATABASE_FAIL_MSG ("Could not populate regex & pattern lists from database") enum OperatingMode { OP_DISABLED, OP_KICK, OP_BAN, OP_TOOBIG } ArrayList regexlist; ArrayList patternlist; #define DBTABLENAME "\"163687013_SMNameBlocker\"" #define DBCREATETABLE "CREATE TABLE IF NOT EXISTS " ... DBTABLENAME ... " (id INTEGER NOT NULL, regexstr TEXT NOT NULL ON CONFLICT IGNORE, steamid64 TEXT NOT NULL ON CONFLICT IGNORE, dateof TEXT DEFAULT CURRENT_DATE, timeof TEXT DEFAULT CURRENT_TIME, PRIMARY KEY (id), UNIQUE (regexstr));" #define DBINSERTSTATEMENT "INSERT OR IGNORE INTO " ... DBTABLENAME ... " (regexstr, steamid64) VALUES (?, ?);" #define DBDELETESTATEMENT "DELETE FROM " ... DBTABLENAME ... " WHERE regexstr=?;" #define DBREPLACESTATEMENT "UPDATE OR IGNORE " ... DBTABLENAME ... " SET regexstr=?, steamid64=? WHERE regexstr=?;" #define DBPOPULATESTATEMENT "SELECT regexstr FROM " ... DBTABLENAME ... ";" Database db; DBStatement dbInsert; DBStatement dbDelete; DBStatement dbReplace; DBStatement dbPopulate; ConVar gcvarOperMode; static const char OPERMODENAME[] = "nameblock_OperatingMode"; const OperatingMode DEFAULTOPERMODE = OP_KICK; ConVar gcvarAdmCmdFlag; static const char ADMCMDFLAGNAME[] = "nameblock_AdminCommandFlag"; const int DEFAULTADMCMDFLAG = ADMFLAG_BAN; ConVar gcvarRegexCompFlags; static const char REGEXCOMPFLAGSNAME[] = "nameblock_RegexCompilationFlags"; const int DEFAULTREGEXCOMPFLAGS = (PCRE_CASELESS | PCRE_DOTALL | PCRE_EXTENDED | PCRE_UTF8); ConVar gcvarAmdImmFlag; static const char ADMINIMMUNITYFLAGNAME[] = "nameblock_AdminImmunityFlag"; const int DEFAULTADMIMMFLAG = ADMFLAG_GENERIC; ConVar gcvarBanTime; static const char BANTIMENAME[] = "nameblock_BanTime"; const int DEFAULTBANTIME = 10080; // 1 week in minutes ConVar gcvarKickMsg; static const char KICKMSGNAME[] = "nameblock_KickMessage"; static const char DEFAULTKICKMSG[] = "Failed name check"; ConVar gcvarBanMsg; static const char BANMSGNAME[] = "nameblock_BanMessage"; static const char DEFAULTBANMESSAGE[] = "Failed name check"; enum LOGMODE { LM_MSG = (1<<0), LM_ERROR = (1<<1), LM_ACTION = (1<<2), // Unused, may implement later LM_TOFILE = (1<<3), // Unused, may implement later }; void doSimpleLog(LOGMODE modeflags, bool fail=false, const char[] fmt, any ...) { if(modeflags < LM_MSG || modeflags > (LM_MSG | LM_ERROR)) { doSimpleLog(LM_ERROR, false, " Mode oob. Expected: [%d, %d], Got: %d", LM_MSG, (LM_MSG | LM_ERROR), modeflags); return; } /* Got fed up enough to look at sourcemod's code as to how they deal with Format type functions and they just make a 2048 char // long buffer instead of just using vsnprintf twice to automatically determine the size of the buffer, which is fucking lame // as hell. Literally, just make a copy of the va_args via va_copy(), run vsnprintf() to get the number of // characters to be written, then run it again with the copy to actually put it into a buffer. I'm sure there's a better way // to reimplement asprintf, but it's easy as balls and takes 2 seconds. Instead we get this, and they don't even offer the // same functionality where VFormat determines the number of bytes to be written, so I have to settle for some bullshit */ // This language sucks fuck dude char buf[2048]; VFormat(buf, sizeof(buf), fmt, 3); if(modeflags & LM_MSG) {LogMessage(buf);} if(modeflags & LM_ERROR) {LogError(buf);} #if defined _SMNB__DEBUG_304142124110815__VERBOSE PrintToServer(buf); #endif if(fail) SetFailState(buf); return; } // Concatenate arguments into a single buffer int concatArgs(char[] buf, int maxbuflen, int maxarglen, int end, int start=1) { if(maxbuflen <= 0 || start < 0) return -1; char[] arg = new char[maxarglen]; char[] tmp = new char[maxbuflen]; GetCmdArg(start, tmp, maxarglen); // Priming the first argument to prevent weirdness for(int i = start + 1; i <= end; i++) { GetCmdArg(i, arg, maxarglen); Format(tmp, maxbuflen, "%s %s", tmp, arg); } #if defined _SMNB__DEBUG_304142124110815__VERBOSE doSimpleLog(LM_MSG, false, " Preformed concat: %s", tmp); #endif return strcopy(buf, maxbuflen, tmp); } // Register a convar who's default value is an integer int registerIntConVar(ConVar& cv, int defaultVal, const char[] name, const char[] desc) { char tmp[32]; // Consider this a byte array Format(tmp, sizeof(tmp), "%d", defaultVal); cv = CreateConVar(name, tmp, desc); if(cv == null) return -1; cv.IntValue = defaultVal; return 0; } // `registerIntConVar` wrapper. Calls `doSimpleLog` if the convar couldn't be registered void xRegisterIntConVar(ConVar& cv, int defaultVal, const char[] name, const char[] desc) { if(registerIntConVar(cv, defaultVal, name, desc)) doSimpleLog(LM_ERROR, true, " Error: could not register cvar \"%s\"", name); } void initDatabase() { // Get database handle char sqlerr[256 + 1]; bool err = false; db = SQLite_UseDatabase("sourcemod-local", sqlerr, sizeof(sqlerr)); if(db == null) doSimpleLog(LM_ERROR, true, " Error: Could not connect to sql database: %s", sqlerr); // Prep table SQL_LockDatabase(db); if(!SQL_FastQuery(db, DBCREATETABLE)) { SQL_GetError(db, sqlerr, sizeof(sqlerr)); err = true; } SQL_UnlockDatabase(db); if(err) doSimpleLog(LM_ERROR, true, " Error: Could not create table: %s", sqlerr); return; } void initPrepStatements() { char sqlerr[256 + 1]; int err = 0; SQL_LockDatabase(db); if((dbInsert = SQL_PrepareQuery(db, DBINSERTSTATEMENT, sqlerr, sizeof(sqlerr))) == null && !err) err = 1; if((dbDelete = SQL_PrepareQuery(db, DBDELETESTATEMENT, sqlerr, sizeof(sqlerr))) == null && !err) err = 2; if((dbReplace = SQL_PrepareQuery(db, DBREPLACESTATEMENT, sqlerr, sizeof(sqlerr))) == null && !err) err = 3; if((dbPopulate = SQL_PrepareQuery(db, DBPOPULATESTATEMENT, sqlerr, sizeof(sqlerr))) == null && !err) err = 4; SQL_UnlockDatabase(db); switch(err) { case 1: {doSimpleLog(LM_ERROR, true, " Error: Could not prepare insert statement: %s", sqlerr);} // Doesn't recognize "163687013_SMNameBlocker" as a valid token case 2: {doSimpleLog(LM_ERROR, true, " Error: Could not prepare delete statement: %s", sqlerr);} case 3: {doSimpleLog(LM_ERROR, true, " Error: Could not prepare replace statement: %s", sqlerr);} case 4: {doSimpleLog(LM_ERROR, true, " Error: Could not prepare populate statement: %s", sqlerr);} } return; } void loadFromDatabase() { // Initialize and populate datatypes regexlist = new ArrayList(ByteCountToCells(HANDLE_SIZE)); if(regexlist == null) doSimpleLog(LM_ERROR, true, " Error: Could not initialize regexlist ArrayList"); patternlist = new ArrayList(ByteCountToCells(PATTERN_MAX_LEN)); if(patternlist == null) doSimpleLog(LM_ERROR, true, " Error: Could not initialize patternlist ArrayList"); // Initialize table if it doesn't exist int err = 0; char sqlerr[256 + 1]; SQL_LockDatabase(db); // select patterns from nameblock table/database if(!SQL_Execute(dbPopulate) && !err) { SQL_GetError(dbPopulate, sqlerr, sizeof(sqlerr)); err = 2; } SQL_UnlockDatabase(db); switch(err) { case 1: {doSimpleLog(LM_ERROR, true, " Error: Could not initialize nameblocker table: %s", sqlerr);} case 2: {doSimpleLog(LM_ERROR, true, " Error: Population query failed: %s", sqlerr);} } // compile each pattern & insert Regex cur; char reerr[256]; RegexError reenum; for(char pattern[PATTERN_MAX_LEN]; SQL_FetchRow(dbPopulate);) { SQL_FetchString(dbPopulate, 1, pattern, sizeof(pattern)); cur = CompileRegex(pattern, gcvarRegexCompFlags, reerr, sizeof(reerr), reenum); if(cur == null) { doSimpleLog(LM_ERROR, false, " Error: Could not compile regex pattern \"%s\": %s (%d)", pattern, reerr, reenum); continue; } if(aModPattern(MM_INSERT, regexlist.Length, pattern)) { doSimpleLog(LM_ERROR, false, " Error: Couldn't add regex \"%s\" to arraylists, continuing", pattern); CloseHandle(cur); } } return; } public void OnAllPluginsLoaded() { initDatabase(); initPrepStatements(); loadFromDatabase(); // Register convars xRegisterIntConVar(gcvarOperMode, DEFAULTOPERMODE, OPERMODENAME, "Operating mode (disabled, kick, ban, etc.)"); xRegisterIntConVar(gcvarAdmCmdFlag, DEFAULTADMCMDFLAG, ADMCMDFLAGNAME, "Admin flag to modify pattern list"); xRegisterIntConVar(gcvarRegexCompFlags, DEFAULTREGEXCOMPFLAGS, REGEXCOMPFLAGSNAME, "Regular expression compilation flags"); xRegisterIntConVar(gcvarAmdImmFlag, DEFAULTADMIMMFLAG, ADMINIMMUNITYFLAGNAME, "Admin immunity flag"); xRegisterIntConVar(gcvarBanTime, DEFAULTBANTIME, BANTIMENAME, "Time (in minutes) to ban players when operating in ban mode. 0 for permaban"); gcvarKickMsg = CreateConVar(KICKMSGNAME, DEFAULTKICKMSG, "Default kick message"); if(gcvarKickMsg == null) doSimpleLog(LM_ERROR, true, " Error: Could not init convar \"gcvarKickMsg\""); gcvarBanMsg = CreateConVar(BANMSGNAME, DEFAULTBANMESSAGE, "Default ban message"); if(gcvarBanMsg == null) doSimpleLog(LM_ERROR, true, " Error: Could not init convar \"gcvarBanMsg\""); AutoExecConfig(true, "nameblocker_cvars"); // Register commands RegAdminCmd("nb_addpattern", cmdRegisterPattern, gcvarAdmCmdFlag.IntValue, "Add a regex pattern to the filter list"); RegAdminCmd("nb_removepattern", cmdDeletePattern, gcvarAdmCmdFlag.IntValue, "Remove a regex pattern from the filter list"); RegAdminCmd("nb_modifypattern", cmdReplacePattern, gcvarAdmCmdFlag.IntValue, "Replace an existing regex pattern with a new pattern"); RegAdminCmd("nb_listpatterns", cmdListPatterns, gcvarAdmCmdFlag.IntValue, "List current regex patterns and their indicies"); } public void OnClientPostAdminCheck(int client) { checkName(client); } public void OnClientSettingsChanged(int client) { checkName(client); } public Action cmdRegisterPattern(int client, int args) { if(args < 1) { ReplyToCommand(client, "Error: missing regex pattern"); return Plugin_Handled; } char pattern[PATTERN_MAX_LEN]; if(concatArgs(pattern, sizeof(pattern), PATTERN_MAX_LEN, args) <= 0) { ReplyToCommand(client, "Error: could not get argument list"); return Plugin_Handled; } if(modPattern(MM_INSERT, regexlist.Length, pattern)) { ReplyToCommand(client, "Error: could not register pattern"); return Plugin_Handled; } return Plugin_Handled; } public Action cmdDeletePattern(int client, int args) { if(args < 2) { ReplyToCommand(client, "Error: no pattern index given"); return Plugin_Handled; } int index; if(!GetCmdArgIntEx(1, index)) { ReplyToCommand(client, "Error: index argument not numerical"); return Plugin_Handled; } if(modPattern(MM_DELETE, index)) { ReplyToCommand(client, "Error: could not remove pattern"); return Plugin_Handled; } return Plugin_Handled; } public Action cmdReplacePattern(int client, int args) { if(args < 3) { ReplyToCommand(client, "Error: missing index, replacement pattern, or both"); return Plugin_Handled; } int index; if(!GetCmdArgIntEx(1, index)) { ReplyToCommand(client, "Error: index argument not numerical"); return Plugin_Handled; } char pattern[PATTERN_MAX_LEN]; if(concatArgs(pattern, sizeof(pattern), PATTERN_MAX_LEN, args, 2) <= 0) { ReplyToCommand(client, "Error: could not get argument list"); return Plugin_Handled; } if(modPattern(MM_REPLACE, index, pattern)) { ReplyToCommand(client, "Error: could not replace pattern in list"); return Plugin_Handled; } return Plugin_Handled; } public Action cmdListPatterns(int client, int args) { for(int i = 0; i < patternlist.Length; i++) { ReplyToCommand(client, "[%d] %s", i, patternlist.Get(i)); } return Plugin_Handled; } enum MOD_MODE { MM_UNDEF, MM_DELETE, MM_INSERT, MM_REPLACE, MM_TOOBIG }; int __modPattern__insert(int index, char[] pattern, int patternlen, int client) { if(IsNullString(pattern) || patternlen < 0 || patternlen > PATTERN_MAX_LEN || client < 0) {return -1;} char steamid64[STEAMID64LENGTH]; char sqlerr[512 + 1]; bool flag = false; if(!GetClientAuthId(client, AuthId_SteamID64, steamid64, sizeof(steamid64))) { doSimpleLog(LM_ERROR, false, " Error: Could not get client \"%N\"'s steamid64", client); return -1; } SQL_LockDatabase(db); SQL_BindParamString(dbInsert, 0, pattern, false); SQL_BindParamString(dbInsert, 1, steamid64, false); if(!SQL_Execute(dbInsert)) { SQL_GetError(dbInsert, sqlerr, sizeof(sqlerr)); doSimpleLog(LM_ERROR, false, " Error: Could not insert values into database: %s", sqlerr); flag = true; } SQL_UnlockDatabase(db); if(flag) return -1; if(aModPattern(MM_INSERT, index, pattern)) {} // TODO: Error handling return 0; } int __modPattern__delete(int index) { char pattern[PATTERN_MAX_LEN]; char sqlerr[512 + 1]; bool flag = false; patternlist.GetString(index, pattern, sizeof(pattern), ByteCountToCells(PATTERN_MAX_LEN)); SQL_LockDatabase(db); SQL_BindParamString(dbDelete, 0, pattern, false); if(!SQL_Execute(dbDelete)) { SQL_GetError(dbDelete, sqlerr, sizeof(sqlerr)); doSimpleLog(LM_ERROR, false, " Error: Could not delete from database: %s", sqlerr); flag = true; } SQL_UnlockDatabase(db); if(flag) return -1; if(aModPattern(MM_DELETE, index)) {} // TODO: Error handling return 0; } int __modPattern__replace(int index, char[] pattern, int patternlen, int client) { if(IsNullString(pattern) || patternlen < 0 || patternlen > PATTERN_MAX_LEN || client < 0) {return -1;} char oldpattern[PATTERN_MAX_LEN]; char steamid64[STEAMID64LENGTH]; char sqlerr[512]; bool flag = false; patternlist.GetString(index, oldpattern, sizeof(oldpattern), ByteCountToCells(PATTERN_MAX_LEN)); if(!GetClientAuthId(client, AuthId_SteamID64, steamid64, sizeof(steamid64))) { doSimpleLog(LM_ERROR, false, " Error: Could not get \"%N\"'s steamid64"); return -1; } SQL_LockDatabase(db); SQL_BindParamString(dbReplace, 0, pattern, false); SQL_BindParamString(dbReplace, 1, steamid64, false); SQL_BindParamString(dbReplace, 2, oldpattern, false); if(!SQL_Execute(dbReplace)) { SQL_GetError(dbReplace, sqlerr, sizeof(sqlerr)); doSimpleLog(LM_ERROR, false, " Error: Could not replace in database: %s", sqlerr); flag = true; } SQL_UnlockDatabase(db); if(flag) return -1; if(aModPattern(MM_REPLACE, index, pattern)) {} // TODO: Error handling return 0; } int modPattern(MOD_MODE mode, int index, char[] pattern="", int patternlen=-1, int client=-1) { if(index < 0 || index > regexlist.Length) return -1; switch(mode) { case MM_INSERT: {return __modPattern__insert(index, pattern, patternlen, client);} case MM_DELETE: {return __modPattern__delete(index);} case MM_REPLACE: {return __modPattern__replace(index, pattern, patternlen, client);} default: { doSimpleLog(LM_ERROR, false, " Error: Given invalid DBMOD_MODE"); return -1; } } } int aModPattern(MOD_MODE mode, int index, char[] pattern="", int patternlen=0) { if(mode <= MM_UNDEF || mode >= MM_TOOBIG || index < 0 || index > patternlist.Length) return -1; Regex res; if(mode > MM_DELETE) { if(IsNullString(pattern) || patternlen < 0 || patternlen > PATTERN_MAX_LEN) return -1; char errstr[512]; RegexError reerr; res = CompileRegex(pattern, gcvarRegexCompFlags.IntValue, errstr, sizeof(errstr), reerr); if(res == null) { doSimpleLog(LM_ERROR, false, " Error: Could not compile regex pattern \"%s\": %s (%d)", pattern, errstr, reerr); return -1; } } switch(mode) { case MM_DELETE: { regexlist.Erase(index); patternlist.Erase(index); } case MM_INSERT: { if(index == regexlist.Length) { regexlist.Push(res); patternlist.Push(pattern); return 0; } regexlist.ShiftUp(index); patternlist.ShiftUp(index); regexlist.Set(index, res); patternlist.Set(index, pattern); } case MM_REPLACE: { if(index == regexlist.Length) { regexlist.Push(res); patternlist.Push(pattern); return 0; } CloseHandle(view_as(regexlist.Get(index, ByteCountToCells(HANDLE_SIZE)))); patternlist.Set(index, pattern, ByteCountToCells(PATTERN_MAX_LEN)); regexlist.Set(index, res, ByteCountToCells(HANDLE_SIZE)); } default: { doSimpleLog(LM_ERROR, true, " Error: Got impossible state"); } } return 0; } // Check's a user's name against the regex list. Returns -1 on error, 0 if skipped, 1 on hit int checkName(int client) { if(client <= 0) return 0; if(gcvarOperMode.IntValue == view_as(OP_DISABLED)) return 0; if(CheckCommandAccess(client, "", gcvarAmdImmFlag.IntValue, true)) return 0; char name[64 + 1]; if(getName(client, name, sizeof(name)) <= 0) { doSimpleLog(LM_ERROR, false, " Error: Tried to get a client's name for a name check, but could not"); return -1; } RegexError reerr; for(int i = 0, m = 0; i < regexlist.Length; i++) { m = MatchRegex(regexlist.Get(i), name, reerr); if(m < 0) { handleFailedRegex(client); return -1; } if(m == 0) continue; handleNameHit(client); break; } return 1; } int getName(int client, char[] buf, int buflen) { if(client <= 0 || buflen <= 0) return -1; if(IsNullString(buf)) return -1; return Format(buf, buflen, "%N", client); } int handleNameHit(int client) { if(client <= 0) return -1; switch(gcvarOperMode.IntValue) { case OP_DISABLED: {return 0;} case OP_KICK: { char kickmsg[512]; gcvarKickMsg.GetString(kickmsg, sizeof(kickmsg)); KickClient(client, kickmsg); LogAction(0, client, "Kicked %L for failing a name check", client); return 0; } case OP_BAN: { char banmsg[512]; gcvarBanMsg.GetString(banmsg, sizeof(banmsg)); BanClient(client, gcvarBanTime.IntValue, BANFLAG_AUTHID, "Failed name check", banmsg); LogAction(0, client, "Banned %L for failing a name check", client); // TODO: Interop with other ban systems return 0; } default: { doSimpleLog(LM_ERROR, false, " Error: %L failed a name check, but the operating mode in an invalid state", client); return -1; } } } int handleFailedRegex(int client) { doSimpleLog(LM_ERROR, false, " Error: Ran into regex error when trying to check user %L's name", client); return 0; }