unrealircd

- supernets unrealircd source & configuration
git clone git://git.acid.vegas/unrealircd.git
Log | Files | Refs | Archive | README | LICENSE

textban.c (13578B)

      1 /*
      2  * Text ban. (C) Copyright 2004-2016 Bram Matthys.
      3  * 
      4  * This program is free software; you can redistribute it and/or
      5  * modify it under the terms of the GNU General Public License
      6  * as published by the Free Software Foundation; either version 2
      7  * of the License, or (at your option) any later version.
      8  * 
      9  * This program is distributed in the hope that it will be useful,
     10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
     11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     12  * GNU General Public License for more details.
     13  * 
     14  * You should have received a copy of the GNU General Public License
     15  * along with this program; if not, write to the Free Software
     16  * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
     17  */
     18 
     19 #include "unrealircd.h"
     20 
     21 /** Max number of text bans per channel.
     22  * This is basically the most important setting. It directly affects
     23  * how much CPU you want to spend on text processing.
     24  * For comparison: with 10 textbans of max length (150), and messages said in
     25  * the channel with max length (~500 bytes), on an A1800+ (1.53GHz) machine
     26  * this consumes 30 usec per-channel message PEAK/MAX (usec = 1/1000000 of a
     27  * second), and in normal (non-supersize messages) count on 10-15 usec.
     28  * Basically this means this allows for like >25000 messages per second at
     29  * 100% CPU usage in a worth case scenario. Which seems by far sufficient to me.
     30  * Also note that (naturally) only local clients are processed, only people
     31  * that do not have halfops or higher, and only channels that have any
     32  * textbans set.
     33  * UPDATE: The speed impact for 15 bans per channel is 42 usec PEAK.
     34  * HINT: If you are hitting the "normal banlimit" before you actually hit this
     35  *       one, then you might want to tweak the #define MAXBANS and #define
     36  *       MAXBANLENGTH in include/struct.h. Doubling MAXBANLENGTH is usually
     37  *       a good idea, and then you can enlarge MAXBANS too a bit if you want to.
     38  */
     39 #define MAX_EXTBANT_PER_CHAN     15 /* Max number of ~T bans in a channel. */
     40 
     41 /** Max length of a ban.
     42  * NOTE: This is mainly for 'cosmetic' purposes. Lowering it does not
     43  *       decrease CPU usage for text processing.
     44  */
     45 #define MAX_LENGTH               150 /* Max length of a ban */
     46 
     47 /** Allow user@host in the textban? This changes the syntax! */
     48 #undef UHOSTFEATURE
     49 
     50 /** Enable 'censor' support. What this type will do is replace the
     51  * matched word with "<censored>" (or another word, see later)
     52  * Like:
     53  * <Idiot> hey check out my fucking new car
     54  * will become:
     55  * <Idiot> hey check out my <censored> new car
     56  *
     57  * SPEED: See README
     58  */
     59 #define CENSORFEATURE
     60 
     61 /** Which censor replace word to use when CENSORFEATURE is enabled. */
     62 #define CENSORWORD "<censored>"
     63 
     64 ModuleHeader MOD_HEADER
     65   = {
     66 	"extbans/textban",
     67 	"2.2",
     68 	"ExtBan ~T (textban) by Syzop",
     69 	"UnrealIRCd Team",
     70 	"unrealircd-6",
     71     };
     72 
     73 /* Forward declarations */
     74 const char *extban_modeT_conv_param(BanContext *b, Extban *extban);
     75 int textban_check_ban(Client *client, Channel *channel, const char *ban, const char **msg, const char **errmsg);
     76 int textban_can_send_to_channel(Client *client, Channel *channel, Membership *lp, const char **msg, const char **errmsg, SendType sendtype);
     77 int extban_modeT_is_ok(BanContext *b);
     78 void parse_word(const char *s, char **word, int *type);
     79 
     80 MOD_INIT()
     81 {
     82 	ExtbanInfo req;
     83 
     84 	MARK_AS_OFFICIAL_MODULE(modinfo);
     85 
     86 	memset(&req, 0, sizeof(ExtbanInfo));
     87 	req.letter = 'T';
     88 	req.name = "text";
     89 	req.options = EXTBOPT_NOSTACKCHILD; /* disallow things like ~n:~T, as we only affect text. */
     90 	req.conv_param = extban_modeT_conv_param;
     91 	req.is_ok = extban_modeT_is_ok;
     92 
     93 	if (!ExtbanAdd(modinfo->handle, req))
     94 	{
     95 		config_error("textban module: adding extban ~T failed! module NOT loaded");
     96 		return MOD_FAILED;
     97 	}
     98 
     99 	HookAdd(modinfo->handle, HOOKTYPE_CAN_SEND_TO_CHANNEL, 0, textban_can_send_to_channel);
    100 
    101 	return MOD_SUCCESS;
    102 }
    103 
    104 MOD_LOAD()
    105 {
    106 	return MOD_SUCCESS;
    107 }
    108 
    109 MOD_UNLOAD()
    110 {
    111 	return MOD_SUCCESS;
    112 }
    113 
    114 #if defined(CENSORFEATURE) || defined(STRIPFEATURE)
    115 static char *my_strcasestr(char *haystack, char *needle)
    116 {
    117 	int i;
    118 	int nlength = strlen (needle);
    119 	int hlength = strlen (haystack);
    120 
    121 	if (nlength > hlength)
    122 		return NULL;
    123 	if (hlength <= 0)
    124 		return NULL;
    125 	if (nlength <= 0)
    126 		return haystack;
    127 	for (i = 0; i <= (hlength - nlength); i++)
    128 	{
    129 		if (strncasecmp (haystack + i, needle, nlength) == 0)
    130 			return haystack + i;
    131 	}
    132 	return NULL; /* not found */
    133 }
    134 
    135 #define TEXTBAN_WORD_LEFT	0x1
    136 #define TEXTBAN_WORD_RIGHT	0x2
    137 
    138 /* textban_replace:
    139  * a fast replace routine written by Syzop used for replacing.
    140  * searches in line for huntw and replaces it with replacew,
    141  * buf is used for the result and max is sizeof(buf).
    142  * (Internal assumptions: size of 'buf' is 512 characters or more)
    143  */
    144 int textban_replace(int type, char *badword, char *line, char *buf)
    145 {
    146 	char *replacew;
    147 	char *pold = line, *pnew = buf; /* Pointers to old string and new string */
    148 	char *poldx = line;
    149 	int replacen;
    150 	int searchn = -1;
    151 	char *startw, *endw;
    152 	char *c_eol = buf + 510 - 1; /* Cached end of (new) line */
    153 	int cleaned = 0;
    154 
    155 	replacew = CENSORWORD;
    156 	replacen = sizeof(CENSORWORD)-1;
    157 
    158 	while (1)
    159 	{
    160 		pold = my_strcasestr(pold, badword);
    161 		if (!pold)
    162 			break;
    163 		if (searchn == -1)
    164 			searchn = strlen(badword);
    165 		/* Hunt for start of word */
    166  		if (pold > line)
    167  		{
    168 			for (startw = pold; (!iswseperator(*startw) && (startw != line)); startw--);
    169 			if (iswseperator(*startw))
    170 				startw++; /* Don't point at the space/seperator but at the word! */
    171 		} else {
    172 			startw = pold;
    173 		}
    174 
    175 		if (!(type & TEXTBAN_WORD_LEFT) && (pold != startw))
    176 		{
    177 			/* not matched */
    178 			pold++;
    179 			continue;
    180 		}
    181 
    182 		/* Hunt for end of word
    183 		 * Fix for bug #4909: word will be at least 'searchn' long so we can skip
    184 		 * 'searchn' bytes and avoid stopping half-way the badword.
    185 		 */
    186 		for (endw = pold+searchn; ((*endw != '\0') && (!iswseperator(*endw))); endw++);
    187 
    188 		if (!(type & TEXTBAN_WORD_RIGHT) && (pold+searchn != endw))
    189 		{
    190 			/* not matched */
    191 			pold++;
    192 			continue;
    193 		}
    194 
    195 		cleaned = 1; /* still too soon? Syzop/20050227 */
    196 
    197 		/* Do we have any not-copied-yet data? */
    198 		if (poldx != startw)
    199 		{
    200 			int tmp_n = startw - poldx;
    201 			if (pnew + tmp_n >= c_eol)
    202 			{
    203 				/* Partial copy and return... */
    204 				memcpy(pnew, poldx, c_eol - pnew);
    205 				*c_eol = '\0';
    206 				return 1;
    207 			}
    208 
    209 			memcpy(pnew, poldx, tmp_n);
    210 			pnew += tmp_n;
    211 		}
    212 		/* Now update the word in buf (pnew is now something like startw-in-new-buffer */
    213 
    214 		if (replacen)
    215 		{
    216 			if ((pnew + replacen) >= c_eol)
    217 			{
    218 				/* Partial copy and return... */
    219 				memcpy(pnew, replacew, c_eol - pnew);
    220 				*c_eol = '\0';
    221 				return 1;
    222 			}
    223 			memcpy(pnew, replacew, replacen);
    224 			pnew += replacen;
    225 		}
    226 		poldx = pold = endw;
    227 	}
    228 	/* Copy the last part */
    229 	if (*poldx)
    230 	{
    231 		strncpy(pnew, poldx, c_eol - pnew);
    232 		*(c_eol) = '\0';
    233 	} else {
    234 		*pnew = '\0';
    235 	}
    236 	return cleaned;
    237 }
    238 #endif
    239 
    240 unsigned int counttextbans(Channel *channel)
    241 {
    242 	Ban *ban;
    243 	unsigned int cnt = 0;
    244 
    245 	for (ban = channel->banlist; ban; ban=ban->next)
    246 		if ((ban->banstr[0] == '~') && (ban->banstr[1] == 'T') && (ban->banstr[2] == ':'))
    247 			cnt++;
    248 	for (ban = channel->exlist; ban; ban=ban->next)
    249 		if ((ban->banstr[0] == '~') && (ban->banstr[1] == 'T') && (ban->banstr[2] == ':'))
    250 			cnt++;
    251 	return cnt;
    252 }
    253 
    254 
    255 int extban_modeT_is_ok(BanContext *b)
    256 {
    257 	int n;
    258 
    259 	if ((b->what == MODE_ADD) && (b->ban_type == EXBTYPE_EXCEPT) && MyUser(b->client))
    260 		return 0; /* except is not supported */
    261 
    262 	/* We check the # of bans in the channel, may not exceed MAX_EXTBANT_PER_CHAN */
    263 	if ((b->what == MODE_ADD) && (b->is_ok_check == EXBCHK_PARAM) &&
    264 	     MyUser(b->client) && !IsOper(b->client) &&
    265 	    ((n = counttextbans(b->channel)) >= MAX_EXTBANT_PER_CHAN))
    266 	{
    267 		/* We check the # of bans in the channel, may not exceed MAX_EXTBANT_PER_CHAN */
    268 		sendnumeric(b->client, ERR_BANLISTFULL, b->channel->name, b->banstr); // FIXME: wants b->full_banstr here
    269 		sendnotice(b->client, "Too many textbans for this channel");
    270 		return 0;
    271 	}
    272 	return 1;
    273 }
    274 
    275 char *conv_pattern_asterisks(const char *pattern)
    276 {
    277 	static char buf[512];
    278 	char missing_prefix = 0, missing_suffix = 0;
    279 	if (*pattern != '*')
    280 		missing_prefix = 1;
    281 	if (*pattern && (pattern[strlen(pattern)-1] != '*'))
    282 		missing_suffix = 1;
    283 	snprintf(buf, sizeof(buf), "%s%s%s",
    284 		missing_prefix ? "*" : "",
    285 		pattern,
    286 		missing_suffix ? "*" : "");
    287 	return buf;
    288 }
    289 
    290 /** Ban callbacks */
    291 const char *extban_modeT_conv_param(BanContext *b, Extban *extban)
    292 {
    293 	static char retbuf[MAX_LENGTH+1];
    294 	char para[MAX_LENGTH+1], *action, *text, *p;
    295 #ifdef UHOSTFEATURE
    296 	char *uhost;
    297 	int ap = 0;
    298 #endif
    299 
    300 	strlcpy(para, b->banstr, sizeof(para)); /* work on a copy (and truncate it) */
    301 
    302 	/* ~T:<action>:<text>
    303 	 * ~T:user@host:<action>:<text> if UHOSTFEATURE is enabled
    304 	 */
    305 
    306 #ifdef UHOSTFEATURE
    307 	action = strchr(para, ':');
    308 	if (!action)
    309 		return NULL;
    310 	*action++ = '\0';
    311 	if (!*action)
    312 		return NULL;
    313 	text = strchr(action, ':');
    314 	if (!text || !text[1])
    315 		return NULL;
    316 	*text++ = '\0';
    317 	uhost = para;
    318 
    319 	for (p = uhost; *p; p++)
    320 	{
    321 		if (*p == '@')
    322 			ap++;
    323 		else if ((*p <= ' ') || (*p > 128))
    324 			return NULL; /* cannot be in a username/host */
    325 	}
    326 	if (ap != 1)
    327 		return NULL; /* no @ */
    328 #else
    329 	text = strchr(para, ':');
    330 	if (!text)
    331 		return NULL;
    332 	*text++ = '\0';
    333 	/* para=action, text=text */
    334 	if (!*text)
    335 		return NULL; /* empty text */
    336 	action = para;
    337 #endif
    338 
    339 	/* ~T:<action>:<text> */
    340 	if (!strcasecmp(action, "block"))
    341 	{
    342 		action = "block"; /* ok */
    343 		text = conv_pattern_asterisks(text);
    344 	}
    345 #ifdef CENSORFEATURE
    346 	else if (!strcasecmp(action, "censor"))
    347 	{
    348 		char *p;
    349 		action = "censor";
    350 		for (p = text; *p; p++)
    351 			if ((*p == '*') && !(p == text) && !(p[1] == '\0'))
    352 				return NULL; /* can only be *word, word* or *word* or word */
    353 		if (!strcmp(p, "*") || !strcmp(p, "**"))
    354 			return NULL; /* cannot match everything ;p */
    355 	}
    356 #endif
    357 	else
    358 		return NULL; /* unknown action */
    359 
    360 	/* check the string.. */
    361 	for (p=text; *p; p++)
    362 	{
    363 		if ((*p == '\003') || (*p == '\002') || 
    364 		    (*p == '\037') || (*p == '\026') ||
    365 		    (*p == ' '))
    366 		{
    367 			return NULL; /* codes not permitted, would be confusing since they are stripped */
    368 		}
    369 	}
    370 
    371 	/* Rebuild the string.. can be cut off if too long. */
    372 #ifdef UHOSTFEATURE
    373 	snprintf(retbuf, sizeof(retbuf), "%s:%s:%s", uhost, action, text);
    374 #else
    375 	snprintf(retbuf, sizeof(retbuf), "%s:%s", action, text);
    376 #endif
    377 	return retbuf;
    378 }
    379 
    380 /** Check for text bans (censor and block) */
    381 int textban_can_send_to_channel(Client *client, Channel *channel, Membership *lp, const char **msg, const char **errmsg, SendType sendtype)
    382 {
    383 	Ban *ban;
    384 
    385 	/* +h/+o/+a/+q users bypass textbans */
    386 	if (check_channel_access(client, channel, "hoaq"))
    387 		return HOOK_CONTINUE;
    388 
    389 	/* IRCOps with these privileges bypass textbans too */
    390 	if (op_can_override("channel:override:message:ban", client, channel, NULL))
    391 		return HOOK_CONTINUE;
    392 
    393 	/* Now we have to manually walk the banlist and check if things match */
    394 	for (ban = channel->banlist; ban; ban=ban->next)
    395 	{
    396 		char *banstr = ban->banstr;
    397 
    398 		/* Pretend time does not exist... */
    399 		if (!strncmp(banstr, "~t:", 3))
    400 		{
    401 			banstr = strchr(banstr+3, ':');
    402 			if (!banstr)
    403 				continue;
    404 			banstr++;
    405 		}
    406 		else if (!strncmp(banstr, "~time:", 6))
    407 		{
    408 			banstr = strchr(banstr+6, ':');
    409 			if (!banstr)
    410 				continue;
    411 			banstr++;
    412 		}
    413 
    414 		if (!strncmp(banstr, "~T:", 3) || !strncmp(banstr, "~text:", 6))
    415 		{
    416 			/* text ban */
    417 			if (textban_check_ban(client, channel, banstr, msg, errmsg))
    418 				return HOOK_DENY;
    419 		}
    420 	}
    421 
    422 	return HOOK_CONTINUE;
    423 }
    424 
    425 
    426 int textban_check_ban(Client *client, Channel *channel, const char *ban, const char **msg, const char **errmsg)
    427 {
    428 	static char retbuf[512];
    429 	char filtered[512]; /* temp input buffer */
    430 	long fl;
    431 	int cleaned=0;
    432 	const char *p;
    433 #ifdef UHOSTFEATURE
    434 	char buf[512], uhost[USERLEN + HOSTLEN + 16];
    435 #endif
    436 	char tmp[1024], *word;
    437 	int type;
    438 
    439 	/* We can only filter on non-NULL text of course */
    440 	if ((msg == NULL) || (*msg == NULL))
    441 		return 0;
    442 
    443 	filtered[0] = '\0'; /* NOT needed, but... :P */
    444 
    445 #ifdef UHOSTFEATURE
    446 	ircsprintf(uhost, "%s@%s", client->user->username, GetHost(client));
    447 #endif
    448 	strlcpy(filtered, StripControlCodes(*msg), sizeof(filtered));
    449 
    450 	p = strchr(ban, ':');
    451 	if (!p)
    452 		return 0; /* "impossible" */
    453 	p++;
    454 #ifdef UHOSTFEATURE
    455 	/* First.. deal with userhost... */
    456 	strcpy(buf, p);
    457 	p = strchr(buf, ':');
    458 	if (!p)
    459 		return 0; /* invalid format */
    460 	*p++ = '\0';
    461 
    462 	if (match_simple(buf, uhost))
    463 #else
    464 	if (1)
    465 #endif
    466 	{
    467 		if (!strncasecmp(p, "block:", 6))
    468 		{
    469 			if (match_simple(p+6, filtered))
    470 			{
    471 				if (errmsg)
    472 					*errmsg = "Message blocked due to a text ban";
    473 				return 1; /* BLOCK */
    474 			}
    475 		}
    476 #ifdef CENSORFEATURE
    477 		else if (!strncasecmp(p, "censor:", 7))
    478 		{
    479 			parse_word(p+7, &word, &type);
    480 			if (textban_replace(type, word, filtered, tmp))
    481 			{
    482 				strlcpy(filtered, tmp, sizeof(filtered));
    483 				cleaned = 1;
    484 			}
    485 		}
    486 #endif
    487 	}
    488 
    489 	if (cleaned)
    490 	{
    491 		/* check for null string */
    492 		char *p;
    493 		for (p = filtered; *p; p++)
    494 		{
    495 			if (*p != ' ')
    496 			{
    497 				strlcpy(retbuf, filtered, sizeof(retbuf));
    498 				*msg = retbuf;
    499 				return 0; /* allow through, but filtered */
    500 			}
    501 		}
    502 		return 1; /* nothing but spaces found.. */
    503 	}
    504 	return 0; /* nothing blocked */
    505 }
    506 
    507 #ifdef CENSORFEATURE
    508 void parse_word(const char *s, char **word, int *type)
    509 {
    510 	static char buf[512];
    511 	const char *tmp;
    512 	int len;
    513 	int tpe = 0;
    514 	char *o = buf;
    515 
    516 	for (tmp = s; *tmp; tmp++)
    517 	{
    518 		if (*tmp != '*')
    519 			*o++ = *tmp;
    520 		else
    521 		{
    522 			if (s == tmp)
    523 				tpe |= TEXTBAN_WORD_LEFT;
    524 			if (*(tmp + 1) == '\0')
    525 				tpe |= TEXTBAN_WORD_RIGHT;
    526 		}
    527 	}
    528 	*o = '\0';
    529 
    530 	*word = buf;
    531 	*type = tpe;
    532 }
    533 #endif