// 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 #pragma newdecls required #pragma semicolon 1 #include #include 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" }; #define HANDLE_SIZE (32) // This exists because you can't sizeof() a Handle, but it IS specified to be a 32bit integer. This should also equal the size of // any other methodmap or descendant of Handle (like Regex) #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; Database db; static const char DBTABLENAME[] = "163687013_SMNameBlocker"; DBStatement dbInsert; static const char DBINSERTSTATEMENT[] = "INSERT OR IGNORE INTO ? (regexstr, steamid64) VALUES (?, ?);"; DBStatement dbDelete; static const char DBDELETESTATEMENT[] = "DELETE FROM ? WHERE regexstr=?;"; DBStatement dbReplace; static const char DBREPLACESTATEMENT[] = "UPDATE OR IGNORE ? SET regexstr=?, steamid64=? WHERE regexstr=?;"; DBStatement dbPopulate; static const char DBPOPULATESTATEMENT[] = "SELECT regexstr FROM ?"; 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; // Logs and throws an error with the same message of upto 2048 characters void logAndFail(const char[] format, any ...) { char buf[2048 + 1]; VFormat(buf, sizeof(buf), format, 2); LogError(buf); SetFailState(buf); } // 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); } 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 `logAndFail` 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)) logAndFail("Could not register cvar \"%s\"", name); } int initPrepStatements() { char sqlerr[256 + 1]; if((dbInsert = SQL_PrepareQuery(db, DBINSERTSTATEMENT, sqlerr, sizeof(sqlerr))) == null) logAndFail("Could not prepare insert statement: %s", sqlerr); if((dbDelete = SQL_PrepareQuery(db, DBDELETESTATEMENT, sqlerr, sizeof(sqlerr))) == null) logAndFail("Could not prepare delete statement: %s", sqlerr); if((dbReplace = SQL_PrepareQuery(db, DBREPLACESTATEMENT, sqlerr, sizeof(sqlerr))) == null) logAndFail("Could not prepare replace statement: %s", sqlerr); if((dbPopulate = SQL_PrepareQuery(db, DBPOPULATESTATEMENT, sqlerr, sizeof(sqlerr))) == null) logAndFail("Could not prepare populate statement: %s", sqlerr); // This might not work / I might have to use Format() instead of binding. We will see if(SQL_BindParamString(dbInsert, 0, DBTABLENAME, true)) { SQL_GetError(dbInsert, sqlerr, sizeof(sqlerr)); logAndFail("Could not bind tablename to insert statement: %s", sqlerr); } if(SQL_BindParamString(dbDelete, 0, DBTABLENAME, true)) { SQL_GetError(dbDelete, sqlerr, sizeof(sqlerr)); logAndFail("Could not bind tablename to delete statement: %s", sqlerr); } if( SQL_BindParamString(dbReplace, 0, DBTABLENAME, true)) { SQL_GetError(dbReplace, sqlerr, sizeof(sqlerr)); logAndFail("Could not bind tablename to replace statement: %s", sqlerr); } if(SQL_BindParamString(dbPopulate, 0, DBTABLENAME, true)) { SQL_GetError(dbPopulate, sqlerr, sizeof(sqlerr)); logAndFail("Could not bind tablename to populate statement: %s", sqlerr); } // If I knew how to do macros this would be much nicer return 0; } int loadFromDatabase() { // Get database handle char sqlerr[256 + 1]; db = SQLite_UseDatabase("sourcemod-local", sqlerr, sizeof(sqlerr)); if(db == null) logAndFail("Could not connect to sql database: %s", sqlerr); // Initialize table if it doesn't exist // I could make this a prepared statement, but I don't believe it's entirely necessary char sqlcbuf[256 + 1]; Format(sqlcbuf, sizeof(sqlcbuf), "CREATE TABLE IF NOT EXISTS \"%s\" (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));", DBTABLENAME); if(!SQL_FastQuery(db, sqlcbuf)) { SQL_GetError(db, sqlerr, sizeof(sqlerr)); logAndFail("Could not initialize nameblocker table: %s", sqlerr); } // Initialize and populate datatypes regexlist = new ArrayList(ByteCountToCells(HANDLE_SIZE)); if(regexlist == null) logAndFail("Could not initialize regexlist ArrayList"); patternlist = new ArrayList(ByteCountToCells(PATTERN_MAX_LEN)); if(patternlist == null) logAndFail("Could not initialize patternlist ArrayList"); // select patterns from nameblock table/database if(SQL_Execute(dbPopulate)) { SQL_GetError(dbPopulate, sqlerr, sizeof(sqlerr)); logAndFail("Population query failed"); } // 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) { LogError("Could not compile regex pattern \"%s\": %s (%d)", pattern, reerr, reenum); continue; } if(aInsertPattern(pattern, cur, patternlist.Length)) { LogError("Couldn't add regex \"%s\" to arraylists, continuing", pattern); CloseHandle(cur); } } return 0; } public void OnAllPluginsLoaded() { initPrepStatements(); loadFromDatabase(); // Register convars xRegisterIntConVar(gcvarOperMode, view_as(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"); 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 < 2) { 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(DBM_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(DBM_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(DBM_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 DBMOD_MODE { DBM_UNDEF, DBM_INSERT, DBM_DELETE, DBM_REPLACE, DBM_TOOBIG }; int modPattern(DBMOD_MODE mode, int index, char[] pattern="") { if(index < 0 || index > regexlist.Length) return -1; if(mode <= DBM_UNDEF || mode >= DBM_TOOBIG) return -1; Transaction dbmod = SQL_CreateTransaction(); if(dbmod == null) return -1; // Update transaction // char errstr[512]; RegexError reerr; // Regex res = CompileRegex(pattern, gcvarRegexCompFlags.IntValue, errstr, sizeof(errstr), reerr); // if(res == null) { // LogError("Error: Could not compile regex pattern \"%s\": %s (%d)", pattern, errstr, reerr); // return -1; // } // Update lists // Try to delete before inserting return 0; } int aInsertPattern(char pattern[PATTERN_MAX_LEN], Regex res, int index) { if(IsNullString(pattern) || res == null || index < 0 || index > regexlist.Length) return -1; if(index == regexlist.Length) { regexlist.Push(res); patternlist.Push(pattern); } else { regexlist.ShiftUp(index); patternlist.ShiftUp(index); regexlist.Set(index, res); patternlist.Set(index, pattern); } return 0; } int aRemovePattern(int index) { if(index < 0 || index >= regexlist.Length) return -1; regexlist.Erase(index); patternlist.Erase(index); 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) { LogError("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, reerr); 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: { KickClient(client, "Failed name check"); LogAction(0, client, "Kicked %L for failing a name check", client); return 0; } case OP_BAN: { // BanClient() // TODO: Interop with other ban systems // Log ban } default: { LogError("%L failed a name check, but the operating mode in an invalid state", client); return -1; } } LogError("Broke out of switch statement that shouldn't have happened"); return -1; // Shouldn't get to this point } int handleFailedRegex(int client, RegexError reerr) { char regstr[128]; RegexStrError(reerr, regstr, sizeof(regstr)); LogError("Ran into regex error when trying to check user %L's name. Reported regex error: %s", client, regstr); return 0; } // Note: May or may not be particularly descriptive for any given error int RegexStrError(RegexError err, char[] buf, int buflen) { if(IsNullString(buf)) return -1; char tmp[64]; switch(err) { case REGEX_ERROR_NONE: {tmp = "No error";} case REGEX_ERROR_ASSERT: {tmp = "Internal error";} case REGEX_ERROR_BADBR: {tmp = "Invalid repeat counts in {}";} case REGEX_ERROR_BADPAT: {tmp = "Pattern error";} case REGEX_ERROR_BADRPT: {tmp = "? * + invalid";} case REGEX_ERROR_EBRACE: {tmp = "Unbalanced {}";} case REGEX_ERROR_EBRACK: {tmp = "Unbalanced []";} case REGEX_ERROR_ECOLLATE: {tmp = "Collation error - not relevant";} case REGEX_ERROR_ECTYPE: {tmp = "Bad class";} case REGEX_ERROR_EESCAPE: {tmp = "Bad escape sequence";} case REGEX_ERROR_EMPTY: {tmp = "Empty expression";} case REGEX_ERROR_EPAREN: {tmp = "Unbalanced ()";} case REGEX_ERROR_ERANGE: {tmp = "Bad range inside []";} case REGEX_ERROR_ESIZE: {tmp = "Expression too big";} case REGEX_ERROR_ESPACE: {tmp = "Failed to get memory";} case REGEX_ERROR_ESUBREG: {tmp = "Bad back reference";} case REGEX_ERROR_INVARG: {tmp = "Bad argument";} case REGEX_ERROR_NOMATCH: {tmp = "No match was found";} case REGEX_ERROR_NULL: {tmp = "Null";} case REGEX_ERROR_BADOPTION: {tmp = "Bad Option";} case REGEX_ERROR_BADMAGIC: {tmp = "Bad Magic";} case REGEX_ERROR_UNKNOWN_OPCODE: {tmp = "Unknown OpCode";} case REGEX_ERROR_NOMEMORY: {tmp = "No Memory";} case REGEX_ERROR_NOSUBSTRING: {tmp = "No substring";} case REGEX_ERROR_MATCHLIMIT: {tmp = "Match limit";} case REGEX_ERROR_CALLOUT: {tmp = "Callout";} // Never used by PCRE itself case REGEX_ERROR_BADUTF8: {tmp = "Bad UTF8";} case REGEX_ERROR_BADUTF8_OFFSET: {tmp = "Bad UTF8 offset";} case REGEX_ERROR_PARTIAL: {tmp = "Partial";} case REGEX_ERROR_BADPARTIAL: {tmp = "Bad Partial";} case REGEX_ERROR_INTERNAL: {tmp = "Internal error";} case REGEX_ERROR_BADCOUNT: {tmp = "Bad count";} case REGEX_ERROR_DFA_UITEM: {tmp = "DFA UItem";} case REGEX_ERROR_DFA_UCOND: {tmp = "DFA UCOND";} case REGEX_ERROR_DFA_UMLIMIT: {tmp = "DFA UMLIMIT";} case REGEX_ERROR_DFA_WSSIZE: {tmp = "DFA WSSIZE";} case REGEX_ERROR_DFA_RECURSE: {tmp = "DFA recurse";} case REGEX_ERROR_RECURSIONLIMIT: {tmp = "Recursion Limit";} case REGEX_ERROR_NULLWSLIMIT: {tmp = "NULL WSLIMIT";} /* No longer actually used */ case REGEX_ERROR_BADNEWLINE: {tmp = "Bad newline";} case REGEX_ERROR_BADOFFSET: {tmp = "Bad offset";} case REGEX_ERROR_SHORTUTF8: {tmp = "Short UFT8";} case REGEX_ERROR_RECURSELOOP: {tmp = "Recurse loop";} case REGEX_ERROR_JIT_STACKLIMIT: {tmp = "JIT Stacklimit";} case REGEX_ERROR_BADMODE: {tmp = "Bad mode";} case REGEX_ERROR_BADENDIANNESS: {tmp = "Bad endianness";} case REGEX_ERROR_DFA_BADRESTART: {tmp = "DFA Bad Restart";} case REGEX_ERROR_JIT_BADOPTION: {tmp = "JIT bad option";} case REGEX_ERROR_BADLENGTH: {tmp = "Bad length";} default: {tmp = "Unknown Error";} } return strcopy(buf, buflen, tmp); }