Our server pr0vider carried out maintenance on 15 January 2020 but it corrupted some files in the process. If you notice anything out of the ordinary (partial files, pages not loading, that kind of shit) then let us know at:

Browse Source

m_block_masshighlight: Merged improvements by k4be -- conf vars allow_accessmode/percent/show_opers_origmsg + handling, chmode +H to exempt a channel from checks and don't count duplicate nicks on the same line

Wazakindjes 2 years ago
parent
commit
3e128766f3
2 changed files with 208 additions and 33 deletions
  1. 10 1
      README.md
  2. 198 32
      u4/m_block_masshighlight.c

+ 10 - 1
README.md

@@ -106,6 +106,9 @@ The hostmask is matched against `user@realhost` only. The timestring shit like `
 This shit can halp you prevent highlight spam on your entire network. =] It keeps track of a user's messages on a per-channel basis and checks if they highlight one person too many times or too many different persons at once. Opers and U:Lines are exempt (as per usual), but also those with list modes `+a` and `+q`. ;3 When someone hits the threshold, opers with the snomask `EYES` will get a server notice through the module (enable that shit with `/mode <nick> +s +e` etc ;]).
 
 __Updated shit:__
+* Don't count duplicate nicks on the same line as separate highlights (also added/proposed by [k4be](https://forums.unrealircd.org/memberlist.php?mode=viewprofile&u=39884))
+* Added chanmode `+H` to exempt a channel from __all__ mass highlight checks, which mite b useful for quiz channels (also added/proposed by [k4be](https://forums.unrealircd.org/memberlist.php?mode=viewprofile&u=39884))
+* Added conf directives (and message handling for) `allow_accessmode`, `percent`, `show_opers_origmsg` (added/proposed by [k4be](https://forums.unrealircd.org/memberlist.php?mode=viewprofile&u=39884))
 * Added conf directive `allow_authed` (see bel0w bruh)
 * Added action `viruschan`
 * Added conf directive `multiline` imo tbh fambi (see below for m0ar inf0)
@@ -123,9 +126,12 @@ _The module doesn't necessarily require any configuration, it uses the following
     	banident 1;
     	multiline 0;
     	allow_authed 0;
+    	//allow_accessmode o; // k4be
+    	percent 1; // k4be
+    	show_opers_origmsg 1; // k4be
     };
 
-* __maxnicks__:  Maximum amount of highlights (going over this number results in `action` setting in)
+* __maxnicks__:  Maximum amount of highlighted nicks (going over this number results in `action` setting in) -- __works in conjunction with `percent`__
 * __delimiters__: List of characters to split a sentence by (don't forget the surrounding quotes lol) -- any char not in the default list may prevent highlights anyways
 * __action__: Action to take, must be one of: `drop` (drop silently [for the offender]), `notice` (drop, but __do__ show notice to them), `gline`, `zline`, `shun`, `tempshun`, `kill`, `viruschan`
 * __duration__: How long to `gline`, `zline` or `shun` for, is a "timestring" like `7d`, `1h5m20s`, etc
@@ -134,6 +140,9 @@ _The module doesn't necessarily require any configuration, it uses the following
 * __banident__: When set to `1` it will ban `ident@iphost`, otherwise `*@iphost` (useful for shared ZNCs etc)
 * __multiline__: When set to `1` it will keep counting highlights until it encounters a line __without__ one
 * __allow_authed__: When set to `1` it will let logged-in users bypass this shit
+* __allow_accessmode__: Must be __one__ of `vhoaq` (or omitted entirely for no exemptions [the default]), exempts everyone with at minimum the specified mode from highlight checks (e.g. `a` includes people with `+q`)
+* __percent__: Threshold for the amount of characters belonging to highlights, not counting `delimiters` (e.g. `hi nick` would be 67%) -- __works in conjunction with `maxnicks`__
+* __show_opers_origmsg__: Display the message that was dropped to opers with the `SNO_EYES` snomask set
 
 If you omit a directive which is required for a certain `action`, you'll get a warning and it will proceed to use the default. It should be pretty clear what directives are required in what cases imo tbh famalam. ;];]
 

+ 198 - 32
u4/m_block_masshighlight.c

@@ -4,6 +4,10 @@
 // Config block
 #define MYCONF "block_masshighlight"
 
+// Channel mode for exempting from highlight checks
+#define IsNocheckHighlights(chptr) (chptr->mode.extmode & EXTCMODE_NOCHECK_MASSHIGHLIGHT)
+#define CHMODE_CHAR 'H'
+
 // Dem macros yo
 #define IsMDErr(x, y, z) \
 	do { \
@@ -14,7 +18,8 @@
 	} while(0)
 
 // Quality fowod declarations
-int is_chanowner_prot(aClient *sptr, aChannel *chptr);
+int is_accessmode_exempt(aClient *sptr, aChannel *chptr);
+int extcmode_requireowner(aClient *sptr, aChannel *chptr, char mode, char *para, int checkt, int what);
 void doXLine(char flag, aClient *sptr);
 int masshighlight_configtest(ConfigFile *cf, ConfigEntry *ce, int type, int *errs);
 int masshighlight_configposttest(int *errs);
@@ -26,6 +31,7 @@ char *masshighlight_hook_prechanmsg(aClient *sptr, aChannel *chptr, char *text,
 extern MODVAR int spamf_ugly_vchanoverride; // For viruschan shit =]
 static ModuleInfo *massHLMI = NULL; // Store ModuleInfo so we can use it to check for errors in MOD_LOAD
 ModDataInfo *massHLMDI; // To store some shit with the channel ;]
+Cmode_t EXTCMODE_NOCHECK_MASSHIGHLIGHT; // For storing the exemption chanmode =]
 
 struct {
 	unsigned short int maxnicks; // Maxnicks, unsigned cuz can't be negative anyways lol
@@ -37,6 +43,9 @@ struct {
 	unsigned short int banident; // Whether to ban ident@host or simply *@host
 	unsigned short int multiline; // Check over multiple lines or just the one ;]
 	unsigned short int allow_authed; // Allow identified users to bypass this shit
+	unsigned int allow_accessmode; // Lowest channel access mode privilege to bypass the limit
+	unsigned short int percent; // How many characters in a message recognised as a nickname is enough for the message to be rejected
+	unsigned short int show_opers_origmsg; // Display the suspicious message to operators
 
 	// These are just for setting to 0 or 1 to see if we got em config directives ;]
 	unsigned short int got_maxnicks;
@@ -48,12 +57,15 @@ struct {
 	unsigned short int got_banident;
 	unsigned short int got_multiline;
 	unsigned short int got_allow_authed;
+	unsigned short int got_allow_accessmode;
+	unsigned short int got_percent;
+	unsigned short int got_show_opers_origmsg;
 } muhcfg;
 
 // Dat dere module header
 ModuleHeader MOD_HEADER(m_block_masshighlight) = {
 	"m_block_masshighlight", // Module name
-	"$Id: v1.03 2017/09/13 Gottem$", // Version
+	"$Id: v1.04 2017/11/19 Gottem/k4be$", // Version
 	"Prevent mass highlights network-wide", // Description
 	"3.2-b8-1", // Modversion, not sure wat do
 	NULL
@@ -81,6 +93,14 @@ MOD_INIT(m_block_masshighlight) {
 	massHLMDI = ModDataAdd(modinfo->handle, mreq);
 	IsMDErr(massHLMDI, m_block_masshighlight, modinfo);
 
+	// Also request +H channel mode fam
+	CmodeInfo req;
+	memset(&req, 0, sizeof(req));
+	req.paracount = 0; // No args required ;]
+	req.flag = CHMODE_CHAR;
+	req.is_ok = extcmode_requireowner; // Need owner privs to set em imo tbh
+	CmodeAdd(modinfo->handle, req, &EXTCMODE_NOCHECK_MASSHIGHLIGHT);
+
 	// Register hewks m8
 	HookAddPChar(modinfo->handle, HOOKTYPE_PRE_CHANMSG, 0, masshighlight_hook_prechanmsg);
 	HookAdd(modinfo->handle, HOOKTYPE_CONFIGRUN, 0, masshighlight_configrun);
@@ -109,19 +129,19 @@ MOD_UNLOAD(m_block_masshighlight) {
 	return MOD_SUCCESS; // We good
 }
 
-int is_chanowner_prot(aClient *sptr, aChannel *chptr) {
+// Client exempted through allow_accessmode?
+int is_accessmode_exempt(aClient *sptr, aChannel *chptr) {
 	Membership *lp; // For checkin' em list access level =]
 
 	if(IsServer(sptr) || IsMe(sptr)) // Allow servers always lel
 		return 1;
 
+	if(!muhcfg.allow_accessmode) // Don't even bother ;]
+		return 0;
+
 	if(chptr) { // Sanity cheqq
 		if((lp = find_membership_link(sptr->user->channel, chptr))) {
-			#ifdef PREFIX_AQ
-				if(lp->flags & (CHFL_CHANOWNER|CHFL_CHANPROT))
-			#else
-				if(lp->flags & CHFL_CHANOP)
-			#endif
+			if(lp->flags & muhcfg.allow_accessmode)
 				return 1;
 		}
 	}
@@ -129,6 +149,15 @@ int is_chanowner_prot(aClient *sptr, aChannel *chptr) {
 	return 0; // No valid channel/membership or doesn't have enough axx lol
 }
 
+// Testing for owner status on channel
+int extcmode_requireowner(aClient *sptr, aChannel *chptr, char mode, char *para, int checkt, int what) {
+	if(IsPerson(sptr) && is_chanowner(sptr, chptr))
+		return EX_ALLOW;
+	if(checkt == EXCHK_ACCESS_ERR)
+		sendto_one(sptr, err_str(ERR_CHANOWNPRIVNEEDED), me.name, sptr->name, chptr->chname);
+	return EX_DENY;
+}
+
 // Not using place_host_ban() cuz I need more control over the mask to ban ;];;];]
 void doXLine(char flag, aClient *sptr) {
 	// Double check for sptr existing, cuz inb4segfault
@@ -162,6 +191,7 @@ void doXLine(char flag, aClient *sptr) {
 int masshighlight_configtest(ConfigFile *cf, ConfigEntry *ce, int type, int *errs) {
 	int errors = 0; // Error count
 	int i; // Iterat0r
+	int tmp; // f0 checking integer values
 	ConfigEntry *cep; // To store the current variable/value pair etc
 
 	// Since we'll add a top-level block to unrealircd.conf, need to filter on CONFIG_MAIN lmao
@@ -190,6 +220,7 @@ int masshighlight_configtest(ConfigFile *cf, ConfigEntry *ce, int type, int *err
 			if(!cep->ce_vardata || !strlen(cep->ce_vardata)) {
 				config_error("%s:%i: %s::%s must be non-empty fam", cep->ce_fileptr->cf_filename, cep->ce_varlinenum, MYCONF, cep->ce_varname);
 				errors++; // Increment err0r count fam
+				continue;
 			}
 
 			// Checkem valid actions
@@ -198,6 +229,9 @@ int masshighlight_configtest(ConfigFile *cf, ConfigEntry *ce, int type, int *err
 				config_error("%s:%i: %s::%s must be one of: drop, notice, gline, zline, shun, tempshun, kill, viruschan", cep->ce_fileptr->cf_filename, cep->ce_varlinenum, MYCONF, cep->ce_varname);
 				errors++;
 			}
+
+			if(!errors)
+				muhcfg.action = cep->ce_vardata[0]; // We need this value to be set in posttest
 			continue;
 		}
 
@@ -245,7 +279,7 @@ int masshighlight_configtest(ConfigFile *cf, ConfigEntry *ce, int type, int *err
 				}
 			}
 			if(!errors) {
-				int tmp = atoi(cep->ce_vardata);
+				tmp = atoi(cep->ce_vardata);
 				if(tmp <= 0 || tmp > 512) {
 					config_error("%s:%i: %s::%s must be an integer between 1 and 512 m8", cep->ce_fileptr->cf_filename, cep->ce_varlinenum, MYCONF, cep->ce_varname);
 					errors++; // Increment err0r count fam
@@ -290,6 +324,69 @@ int masshighlight_configtest(ConfigFile *cf, ConfigEntry *ce, int type, int *err
 			continue;
 		}
 
+		if(!strcmp(cep->ce_varname, "allow_accessmode")) {
+			muhcfg.got_allow_accessmode = 1;
+			if(!cep->ce_vardata || !strlen(cep->ce_vardata)) {
+				config_error("%s:%i: %s::%s must be either non-empty or not specified at all", cep->ce_fileptr->cf_filename, cep->ce_varlinenum, MYCONF, cep->ce_varname);
+				errors++; // Increment err0r count fam
+				continue;
+			}
+
+			if(strlen(cep->ce_vardata) > 1) {
+				config_error("%s:%i: %s::%s must be exactly one character (mode) in length", cep->ce_fileptr->cf_filename, cep->ce_varlinenum, MYCONF, cep->ce_varname);
+				errors++; // Increment err0r count fam
+				continue;
+			}
+
+			if(strcmp(cep->ce_vardata, "v") && strcmp(cep->ce_vardata, "h") && strcmp(cep->ce_vardata, "o")
+		#ifdef PREFIX_AQ
+				&& strcmp(cep->ce_vardata, "a") && strcmp(cep->ce_vardata, "q")
+		#endif
+			) {
+				config_error("%s:%i: %s::%s must be one of: v, h, o"
+		#ifdef PREFIX_AQ
+				", a, q"
+		#endif
+				, cep->ce_fileptr->cf_filename, cep->ce_varlinenum, MYCONF, cep->ce_varname);
+				errors++;
+			}
+			continue;
+		}
+
+		if(!strcmp(cep->ce_varname, "percent")) {
+			muhcfg.got_percent = 1;
+			// Should be an integer yo
+			if(!cep->ce_vardata) {
+				config_error("%s:%i: %s::%s must be an integer between 1 and 100 m8", cep->ce_fileptr->cf_filename, cep->ce_varlinenum, MYCONF, cep->ce_varname);
+				errors++; // Increment err0r count fam
+				continue;
+			}
+			for(i = 0; cep->ce_vardata[i]; i++) {
+				if(!isdigit(cep->ce_vardata[i])) {
+					config_error("%s:%i: %s::%s must be an integer between 1 and 100 m8", cep->ce_fileptr->cf_filename, cep->ce_varlinenum, MYCONF, cep->ce_varname);
+					errors++; // Increment err0r count fam
+					break;
+				}
+			}
+			if(!errors) {
+				tmp = atoi(cep->ce_vardata);
+				if(tmp <= 0 || tmp > 100) {
+					config_error("%s:%i: %s::%s must be an integer between 1 and 100 m8", cep->ce_fileptr->cf_filename, cep->ce_varlinenum, MYCONF, cep->ce_varname);
+					errors++; // Increment err0r count fam
+				}
+			}
+			continue;
+		}
+
+		if(!strcmp(cep->ce_varname, "show_opers_origmsg")) {
+			muhcfg.got_show_opers_origmsg = 1;
+			if(!cep->ce_vardata || (strcmp(cep->ce_vardata, "0") && strcmp(cep->ce_vardata, "1"))) {
+				config_error("%s:%i: %s::%s must be either 0 or 1 fam", cep->ce_fileptr->cf_filename, cep->ce_varlinenum, MYCONF, cep->ce_varname);
+				errors++; // Increment err0r count fam
+			}
+			continue;
+		}
+
 		// Anything else is unknown to us =]
 		config_warn("%s:%i: unknown item %s::%s", cep->ce_fileptr->cf_filename, cep->ce_varlinenum, MYCONF, cep->ce_varname); // So display just a warning
 	}
@@ -334,6 +431,15 @@ int masshighlight_configposttest(int *errs) {
 	if(!muhcfg.got_allow_authed)
 		muhcfg.allow_authed = 0; // Default to n0 fam
 
+	if(!muhcfg.got_allow_accessmode)
+		muhcfg.allow_accessmode = 0; // None ;]
+
+	if(!muhcfg.got_percent)
+		muhcfg.percent = 1; // 1%, max sensitivity
+
+	if(!muhcfg.got_show_opers_origmsg)
+		muhcfg.show_opers_origmsg = 1; // Default to showing em
+
 	*errs = errors;
 	return errors ? -1 : 1;
 }
@@ -360,11 +466,6 @@ int masshighlight_configrun(ConfigFile *cf, ConfigEntry *ce, int type) {
 		if(!cep->ce_varname)
 			continue; // Next iteration imo tbh
 
-		if(!strcmp(cep->ce_varname, "action")) {
-			muhcfg.action = cep->ce_vardata[0];
-			continue;
-		}
-
 		if(!strcmp(cep->ce_varname, "delimiters")) {
 			if(muhcfg.delimiters) // Let's free this first lol (should never happen but let's just in case xd)
 				free(muhcfg.delimiters);
@@ -408,6 +509,43 @@ int masshighlight_configrun(ConfigFile *cf, ConfigEntry *ce, int type) {
 			muhcfg.allow_authed = atoi(cep->ce_vardata);
 			continue;
 		}
+
+		if(!strcmp(cep->ce_varname, "allow_accessmode")) {
+			muhcfg.allow_accessmode = 0;
+			switch(cep->ce_vardata[0]) {
+				case 'v':
+					muhcfg.allow_accessmode |= CHFL_VOICE;
+					break;
+				case 'h':
+					muhcfg.allow_accessmode |= CHFL_HALFOP;
+					break;
+				case 'o':
+					muhcfg.allow_accessmode |= CHFL_CHANOP;
+					break;
+		#ifdef PREFIX_AQ
+				case 'a':
+					muhcfg.allow_accessmode |= CHFL_CHANPROT;
+					break;
+				case 'q':
+					muhcfg.allow_accessmode |= CHFL_CHANOWNER;
+					break;
+		#endif
+				default:
+					break;
+			}
+			continue;
+		}
+
+		if(!strcmp(cep->ce_varname, "percent")) {
+			muhcfg.percent = atoi(cep->ce_vardata);
+			continue;
+		}
+
+		if(!strcmp(cep->ce_varname, "show_opers_origmsg")) {
+			muhcfg.show_opers_origmsg = atoi(cep->ce_vardata);
+			continue;
+		}
+
 	}
 
 	return 1; // We good
@@ -425,50 +563,75 @@ char *masshighlight_hook_prechanmsg(aClient *sptr, aChannel *chptr, char *text,
 	** text: The message body obv fambi
 	** notice: Was it a NOTICE or simply PRIVMSG?
 	*/
-	int hl; // Current highlight count yo
-	char *p = NULL; // For tokenising that shit
+	int hl_cur; // Current highlight count yo
+	char *p; // For tokenising that shit
 	char *werd; // Store current token etc
-	char *cleantext = NULL; // Let's not modify char *text =]
+	char *cleantext; // Let's not modify char *text =]
 	aClient *acptr; // Temporarily store a mentioned nick to get the membership link
 	Membership *lp; // For actually storing the link
 	MembershipL *lp2; // Required for moddata call apparently
-	int blockem = 0; // Need to block the message?
-	int clearem = 1; // For clearing the counter
+	int blockem; // Need to block the message?
+	int clearem; // For clearing the counter
 	int ret; // For checking the JOIN return status for viruschan action
 	char joinbuf[CHANNELLEN + 4]; // Also for viruschan, need to make room for "0,#viruschan" etc ;];]
 	char *vcparv[3]; // Arguments for viruschan JOIN
-	int bypassem = (muhcfg.allow_authed && IsPerson(sptr) && IsLoggedIn(sptr)); // If config var is tr00 and user es logged in
-
-	// Some sanity + privilege checks ;];] (+a, +q, U:Lines and opers are exempt from this shit)
-	if(text && IsPerson(sptr) && MyClient(sptr) && !bypassem && !is_chanowner_prot(sptr, chptr) && !IsULine(sptr) && !IsOper(sptr)) {
+	int bypass_nsauth; // If config var allow_authed is tr00 and user es logged in
+	char gotnicks[1024]; // For storing nicks that were mentioned =]
+	int hl_new; // Store highlight count for further processing
+	size_t werdlen; // Word length ;]
+	size_t hl_nickslen; // Amount of chars that are part of highlights
+	size_t msglen; // Full message length (both of these are required for calculating percent etc), excludes delimiters
+
+	// Initialise some shit lol
+	bypass_nsauth = (muhcfg.allow_authed && IsPerson(sptr) && IsLoggedIn(sptr));
+	blockem = 0;
+	clearem = 1;
+	p = NULL;
+	cleantext = NULL;
+	memset(gotnicks, '\0', sizeof(gotnicks));
+	hl_new = 0;
+	hl_nickslen = 0;
+	msglen = 0;
+
+	if(IsNocheckHighlights(chptr))
+		return text; // if channelmode is set, allow without further checking
+
+	// Some sanity + privilege checks ;];] (allow_accessmode, U:Lines and opers are exempt from this shit)
+	if(text && IsPerson(sptr) && MyClient(sptr) && !bypass_nsauth && !is_accessmode_exempt(sptr, chptr) && !IsULine(sptr) && !IsOper(sptr)) {
 		if(!(lp = find_membership_link(sptr->user->channel, chptr))) // Not even a member tho lol
 			return text; // Process normally cuz can't do shit w/ moddata =]
 
 		lp2 = (MembershipL *)lp; // Cast that shit
-		hl = moddata_membership(lp2, massHLMDI).i; // Get current count
+		hl_cur = moddata_membership(lp2, massHLMDI).i; // Get current count
 
 		// In case someone tries some funny business =]
 		if(!(cleantext = (char *)StripColors(text)) || !(cleantext = (char *)StripControlCodes(cleantext)))
 			return text;
 
 		for(werd = strtoken(&p, cleantext, muhcfg.delimiters); werd; werd = strtoken(&p, NULL, muhcfg.delimiters)) { // Split that shit
+			werdlen = strlen(werd);
+			msglen += werdlen; // We do not count ze delimiters, otherwise the percents would get strangely low
 			if((acptr = find_person(werd, NULL)) && (find_membership_link(acptr->user->channel, chptr))) { // User mentioned another user in this channel
-				hl += 1; // So increment counter
-				clearem = 0; // And flip the clear flag to n0
-
-				if(hl > muhcfg.maxnicks) { // No need to tokenise more if we're already over threshold
-					blockem = 1; // Flip flag to blockin' dat shit
-					break;
+				if(!(strstr(gotnicks, acptr->id))) { // Do not count the same nickname appended multiple times to a single message
+					ircsnprintf(gotnicks, sizeof(gotnicks), "%s%s,", gotnicks, acptr->id);
+					hl_new++; // Got another highlight this round
+					hl_nickslen += werdlen; // Also add the nick's length
 				}
 			}
 		}
 
+		if(100 * hl_nickslen / msglen > muhcfg.percent) { // Check if we exceed the max allowed percentage
+			hl_cur += hl_new; // Set moddata counter to include the ones found this round
+			clearem = 0; // And don't clear moddata
+			if(hl_cur > muhcfg.maxnicks) // Check if we also exceed max allowed nicks
+				blockem = 1; // Flip flag to blockin' dat shit
+		}
+
 		if(!muhcfg.multiline && !clearem) // If single line mode and found a nick
 			clearem = 1; // Need to clear that shit always lol
 
-		moddata_membership(lp2, massHLMDI).i = (clearem ? 0 : hl); // Actually set the counter =]
+		moddata_membership(lp2, massHLMDI).i = (clearem ? 0 : hl_cur); // Actually set the counter =]
 		if(blockem) { // Need to bl0ck?
-			text = NULL; // NULL makes Unreal drop the message entirely ;3
 			switch(muhcfg.action) {
 				case 'd': // Drop silently
 					if(muhcfg.snotice)
@@ -533,6 +696,9 @@ char *masshighlight_hook_prechanmsg(aClient *sptr, aChannel *chptr, char *text,
 				default:
 					break;
 			}
+			if(muhcfg.show_opers_origmsg)
+				sendto_snomask_global(SNO_EYES, "*** [block_masshighlight] The message was: %s", text);
+			text = NULL; // NULL makes Unreal drop the message entirely afterwards ;3
 		}
 	}