1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
|
// 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
//
#pragma newdecls required
#pragma semicolon 1
#include <sourcemod>
#include <regex>
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;
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() {
// 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");
char sqlerr[256 + 1];
db = SQL_DefConnect(sqlerr, sizeof(sqlerr)); // Default connect until I get a little more acquainted with sm's sql api
if(db == null) logAndFail("Could not connect to sql database: %s", sqlerr);
// Register convars
xRegisterIntConVar(gcvarOperMode, view_as<int>(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", 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];
if(concatArgs(pattern, sizeof(pattern), PATTERN_MAX_LEN, args) <= 0) {
ReplyToCommand(client, "Error: could not get argument list");
return Plugin_Handled;
}
if(insertPattern(pattern, regexlist.Length)) {
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];
if(concatArgs(pattern, sizeof(pattern), PATTERN_MAX_LEN, args, 2) <= 0) {
ReplyToCommand(client, "Error: could not get argument list");
return Plugin_Handled;
}
if(removePattern(index)) {
ReplyToCommand(client, "Error: could not remove pattern");
return Plugin_Handled;
}
if(insertPattern(pattern, index)) {
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;
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;
}
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);
}
// 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;
}
// 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<int>(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);
}
|