#pragma newdecls required #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 */ enum OperatingMode { OP_DISABLED, OP_KICK, OP_BAN, OP_TOOBIG } ArrayList regexlist; ArrayList patternlist; // Note: Contents of ArrayLists are lost on map change / reload. I should store everything in a database, // then use the database to populate the list whenever it is cleared 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; int registerIntConVar(ConVar& cv, int defaultVal, const char[] name, const char[] desc) { static char tmp[32]; // Consider this a byte array Format(tmp, sizeof(tmp), "%d", defaultVal); cv = CreateConVar(name, tmp, desc); cv.IntValue = defaultVal; return 0; } public void OnAllPluginsLoaded() { // Initialize and populate datatypes regexlist = new ArrayList(ByteCountToCells(HANDLE_SIZE)); patternlist = new ArrayList(ByteCountToCells(PATTERN_MAX_LEN)); // db = registerIntConVar(gcvarOperMode, view_as(DEFAULTOPERMODE), OPERMODENAME, "Operating mode (disabled, kick, ban, etc.)"); registerIntConVar(gcvarAdmCmdFlag, DEFAULTADMCMDFLAG, ADMCMDFLAGNAME, "Admin flag to modify pattern list"); registerIntConVar(gcvarRegexCompFlags, DEFAULTREGEXCOMPFLAGS, REGEXCOMPFLAGSNAME, "Regular expression compilation flags"); registerIntConVar(gcvarAmdImmFlag, DEFAULTADMIMMFLAG, ADMINIMMUNITYFLAGNAME, "Admin immunity flag"); AutoExecConfig(true, "nameblocker_cvars"); // Register commands RegAdminCmd("nb_addpattern", registerPattern, gcvarAdmCmdFlag.IntValue, "Add a regex pattern to the filter list"); RegAdminCmd("nb_removepattern", deletePattern, gcvarAdmCmdFlag.IntValue, "Remove a regex pattern from the filter list"); RegAdminCmd("nb_modifypattern", replacePattern, gcvarAdmCmdFlag.IntValue, "Replace an existing regex pattern with a new pattern"); RegAdminCmd("nb_listpatterns", listPatterns, 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 registerPattern(int client, int args) { if(args < 2) { ReplyToCommand(client, "Error: missing regex pattern"); return Plugin_Handled; } char pattern[PATTERN_MAX_LEN]; GetCmdArg(1, pattern, sizeof(pattern)); // TODO: Error handling if(insertPattern(pattern, regexlist.Length - 1)) { ReplyToCommand(client, "Error: could not register pattern"); return Plugin_Handled } return Plugin_Handled; } public Action deletePattern(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(removePattern(index)) { ReplyToCommand(client, "Error: could not remove pattern"); return Plugin_Handled; } return Plugin_Handled; } public Action replacePattern(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]; GetCmdArg(2, pattern, sizeof(pattern)); // TODO: Error handling if(removePattern(index)) { ReplyToCommand(client, "Error: could not remove pattern"); return Plugin_Handled; } if(insertPattern(pattern, regexlist.Length - 1)) { ReplyToCommand(client, "Error: could not register pattern"); return Plugin_Handled } // Preferably this would be atomic as to not lose a pattern, but that's something I can do later // I know I can make the database change "atomic" via the use of a transaction, but handling that would necessitate a different // function return Plugin_Handled; } public Action listPatterns(int client, int args) { for(int i = 0; i < patternlist.Length; i++) { ReplyToCommand(client, "[%d] %s", i, patternlist.Get(i)); } return Plugin_Handled; } int insertPattern(char pattern[PATTERN_MAX_LEN], int index) { if(IsNullString(pattern) || index < 0 || index >= regexlist.Length) return -1; static char errstr[512]; static 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; } if(index != (regexlist.Length - 1)) { regexlist.ShiftUp(index); patternlist.ShiftUp(index); regexlist.Set(index, res); patternlist.Set(index, pattern); } else { regexlist.Push(res); patternlist.Push(pattern); } // TODO: This should also insert the pattern into the database return 0; } int removePattern(int index) { if(index < 0 || index >= regexlist.Length) return -1; regexlist.Erase(index); patternlist.Erase(index); // TODO: This should also remove the pattern from the database return 0; } int checkName(int client) { if(client <= 0) return -1; if(gcvarOperMode.IntValue == view_as(OP_DISABLED)) return -1; if(CheckCommandAccess(client, "", gcvarAmdImmFlag.IntValue, true)) return -1; 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 0; } 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) { static 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; switch(err) { case REGEX_ERROR_NONE: {Format(buf, buflen, "No error");} /* No error */ case REGEX_ERROR_ASSERT: {Format(buf, buflen, "Internal error");} case REGEX_ERROR_BADBR: {Format(buf, buflen, "Invalid repeat counts in {}");} case REGEX_ERROR_BADPAT: {Format(buf, buflen, "Pattern error");} case REGEX_ERROR_BADRPT: {Format(buf, buflen, "? * + invalid");} case REGEX_ERROR_EBRACE: {Format(buf, buflen, "Unbalanced {}");} case REGEX_ERROR_EBRACK: {Format(buf, buflen, "Unbalanced []");} case REGEX_ERROR_ECOLLATE: {Format(buf, buflen, "Collation error - not relevant");} case REGEX_ERROR_ECTYPE: {Format(buf, buflen, "Bad class");} case REGEX_ERROR_EESCAPE: {Format(buf, buflen, "Bad escape sequence");} case REGEX_ERROR_EMPTY: {Format(buf, buflen, "Empty expression");} case REGEX_ERROR_EPAREN: {Format(buf, buflen, "Unbalanced ()");} case REGEX_ERROR_ERANGE: {Format(buf, buflen, "Bad range inside []");} case REGEX_ERROR_ESIZE: {Format(buf, buflen, "Expression too big");} case REGEX_ERROR_ESPACE: {Format(buf, buflen, "Failed to get memory");} case REGEX_ERROR_ESUBREG: {Format(buf, buflen, "Bad back reference");} case REGEX_ERROR_INVARG: {Format(buf, buflen, "Bad argument");} case REGEX_ERROR_NOMATCH: {Format(buf, buflen, "No match was found");} case REGEX_ERROR_NULL: {Format(buf, buflen, "Null");} case REGEX_ERROR_BADOPTION: {Format(buf, buflen, "Bad Option");} case REGEX_ERROR_BADMAGIC: {Format(buf, buflen, "Bad Magic");} case REGEX_ERROR_UNKNOWN_OPCODE: {Format(buf, buflen, "Unknown OpCode");} case REGEX_ERROR_NOMEMORY: {Format(buf, buflen, "No Memory");} case REGEX_ERROR_NOSUBSTRING: {Format(buf, buflen, "No substring");} case REGEX_ERROR_MATCHLIMIT: {Format(buf, buflen, "Match limit");} case REGEX_ERROR_CALLOUT: {Format(buf, buflen, "Callout");} // Never used by PCRE itself case REGEX_ERROR_BADUTF8: {Format(buf, buflen, "Bad UTF8");} case REGEX_ERROR_BADUTF8_OFFSET: {Format(buf, buflen, "Bad UTF8 offset");} case REGEX_ERROR_PARTIAL: {Format(buf, buflen, "Partial");} case REGEX_ERROR_BADPARTIAL: {Format(buf, buflen, "Bad Partial");} case REGEX_ERROR_INTERNAL: {Format(buf, buflen, "Internal error");} case REGEX_ERROR_BADCOUNT: {Format(buf, buflen, "Bad count");} case REGEX_ERROR_DFA_UITEM: {Format(buf, buflen, "DFA UItem");} case REGEX_ERROR_DFA_UCOND: {Format(buf, buflen, "DFA UCOND");} case REGEX_ERROR_DFA_UMLIMIT: {Format(buf, buflen, "DFA UMLIMIT");} case REGEX_ERROR_DFA_WSSIZE: {Format(buf, buflen, "DFA WSSIZE");} case REGEX_ERROR_DFA_RECURSE: {Format(buf, buflen, "DFA recurse");} case REGEX_ERROR_RECURSIONLIMIT: {Format(buf, buflen, "Recursion Limit");} case REGEX_ERROR_NULLWSLIMIT: {Format(buf, buflen, "NULL WSLIMIT");} /* No longer actually used */ case REGEX_ERROR_BADNEWLINE: {Format(buf, buflen, "Bad newline");} case REGEX_ERROR_BADOFFSET: {Format(buf, buflen, "Bad offset");} case REGEX_ERROR_SHORTUTF8: {Format(buf, buflen, "Short UFT8");} case REGEX_ERROR_RECURSELOOP: {Format(buf, buflen, "Recurse loop");} case REGEX_ERROR_JIT_STACKLIMIT: {Format(buf, buflen, "JIT Stacklimit");} case REGEX_ERROR_BADMODE: {Format(buf, buflen, "Bad mode");} case REGEX_ERROR_BADENDIANNESS: {Format(buf, buflen, "Bad endianness");} case REGEX_ERROR_DFA_BADRESTART: {Format(buf, buflen, "DFA Bad Restart");} case REGEX_ERROR_JIT_BADOPTION: {Format(buf, buflen, "JIT bad option");} case REGEX_ERROR_BADLENGTH: {Format(buf, buflen, "Bad length");} default: {Format(buf, buflen, "Unknown Error");} } // Yeah I know this is terrible, and it's entirely because I'm not certain how string assignments work. If this were C I would // make a normal char* string and make it point to different text depending on the case return 0; }