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