// 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 map transition / server restart. 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 time 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 #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; 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); } public void OnAllPluginsLoaded() { if(loadFromDatabase()) logAndFail(DATABASE_FAIL_MSG); // 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 OnConfigsExecuted() { // I'm not sure when this is executed in relation to when OnClientPostAdminCheck is. Sourcemod's API reference says it's ran // once after OnMapStart, but idk if "Map Start" is a sufficient enough game state for players to join // Let me illustrate my concern: // Server starts normally, plugin is working // Players join. Nothing weird happens because the plugin loaded everything before a player could have joined // Players/Server initiates a map change WHILE players are still connected. If OnMapStart is when players can start // connecting, and is before OnConfigsExecuted, the following scenarios could happen: // 1: OnMapStart fires, players may join // 2: Player joins. This fires OnClientPostAdminCheck // 3: OnClientPostAdminCheck fires checkName, which tries to querry the array lists // 4: ArrayLists are empty or null, causing unexpected behavior // 4.1: ArrayLists are empty, nothing major happens, but a player with an invalid name gets through. Not ideal, but // also not the end of the world // 4.2: ArrayLists are null, trying to use them causes a null pointer dereference, either crashing the plugin &/or // server, or resulting in some other undefined behavior for sourcemod to deal with // If OnMapStart doesn't let players join, or rather OnConfigsExecuted fires before players can join, then there's no // problem. Alternatively, I can write something to kick/retry players until OnConfigsExecuted fires // As of now, I will simply leave this as a note. No need to go making weird systems if they may not be necessary if(loadFromDatabase()) logAndFail(DATABASE_FAIL_MSG); } 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_INSERT | DBM_DELETE, index, pattern)) { ReplyToCommand(client, "Error: could not remove pattern from 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; } int aInsertPattern(char pattern[PATTERN_MAX_LEN], Regex res, int index) { if(IsNullString(pattern) || 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; } enum DBMOD_MODE { DBM_UNDEF, DBM_INSERT = (1<<0), DBM_DELETE = (1<<1), 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 modification = SQL_CreateTransaction(); if(modification == null) return -1; // 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 transaction // Update lists // Try to delete before inserting return 0; } int loadFromDatabase() { logAndFail("function \"loadFromDatabase\" not implemented"); // 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 // 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 // compile each pattern // insert pattern & regex into respective lists 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); }