/* * Text ban. (C) Copyright 2004-2016 Bram Matthys. * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ #include "unrealircd.h" /** Max number of text bans per channel. * This is basically the most important setting. It directly affects * how much CPU you want to spend on text processing. * For comparison: with 10 textbans of max length (150), and messages said in * the channel with max length (~500 bytes), on an A1800+ (1.53GHz) machine * this consumes 30 usec per-channel message PEAK/MAX (usec = 1/1000000 of a * second), and in normal (non-supersize messages) count on 10-15 usec. * Basically this means this allows for like >25000 messages per second at * 100% CPU usage in a worth case scenario. Which seems by far sufficient to me. * Also note that (naturally) only local clients are processed, only people * that do not have halfops or higher, and only channels that have any * textbans set. * UPDATE: The speed impact for 15 bans per channel is 42 usec PEAK. * HINT: If you are hitting the "normal banlimit" before you actually hit this * one, then you might want to tweak the #define MAXBANS and #define * MAXBANLENGTH in include/struct.h. Doubling MAXBANLENGTH is usually * a good idea, and then you can enlarge MAXBANS too a bit if you want to. */ #define MAX_EXTBANT_PER_CHAN 15 /* Max number of ~T bans in a channel. */ /** Max length of a ban. * NOTE: This is mainly for 'cosmetic' purposes. Lowering it does not * decrease CPU usage for text processing. */ #define MAX_LENGTH 150 /* Max length of a ban */ /** Allow user@host in the textban? This changes the syntax! */ #undef UHOSTFEATURE /** Enable 'censor' support. What this type will do is replace the * matched word with "" (or another word, see later) * Like: * hey check out my fucking new car * will become: * hey check out my new car * * SPEED: See README */ #define CENSORFEATURE /** Which censor replace word to use when CENSORFEATURE is enabled. */ #define CENSORWORD "" ModuleHeader MOD_HEADER = { "extbans/textban", "2.2", "ExtBan ~T (textban) by Syzop", "UnrealIRCd Team", "unrealircd-6", }; /* Forward declarations */ const char *extban_modeT_conv_param(BanContext *b, Extban *extban); int textban_check_ban(Client *client, Channel *channel, const char *ban, const char **msg, const char **errmsg); int textban_can_send_to_channel(Client *client, Channel *channel, Membership *lp, const char **msg, const char **errmsg, SendType sendtype); int extban_modeT_is_ok(BanContext *b); void parse_word(const char *s, char **word, int *type); MOD_INIT() { ExtbanInfo req; MARK_AS_OFFICIAL_MODULE(modinfo); memset(&req, 0, sizeof(ExtbanInfo)); req.letter = 'T'; req.name = "text"; req.options = EXTBOPT_NOSTACKCHILD; /* disallow things like ~n:~T, as we only affect text. */ req.conv_param = extban_modeT_conv_param; req.is_ok = extban_modeT_is_ok; if (!ExtbanAdd(modinfo->handle, req)) { config_error("textban module: adding extban ~T failed! module NOT loaded"); return MOD_FAILED; } HookAdd(modinfo->handle, HOOKTYPE_CAN_SEND_TO_CHANNEL, 0, textban_can_send_to_channel); return MOD_SUCCESS; } MOD_LOAD() { return MOD_SUCCESS; } MOD_UNLOAD() { return MOD_SUCCESS; } #if defined(CENSORFEATURE) || defined(STRIPFEATURE) static char *my_strcasestr(char *haystack, char *needle) { int i; int nlength = strlen (needle); int hlength = strlen (haystack); if (nlength > hlength) return NULL; if (hlength <= 0) return NULL; if (nlength <= 0) return haystack; for (i = 0; i <= (hlength - nlength); i++) { if (strncasecmp (haystack + i, needle, nlength) == 0) return haystack + i; } return NULL; /* not found */ } #define TEXTBAN_WORD_LEFT 0x1 #define TEXTBAN_WORD_RIGHT 0x2 /* textban_replace: * a fast replace routine written by Syzop used for replacing. * searches in line for huntw and replaces it with replacew, * buf is used for the result and max is sizeof(buf). * (Internal assumptions: size of 'buf' is 512 characters or more) */ int textban_replace(int type, char *badword, char *line, char *buf) { char *replacew; char *pold = line, *pnew = buf; /* Pointers to old string and new string */ char *poldx = line; int replacen; int searchn = -1; char *startw, *endw; char *c_eol = buf + 510 - 1; /* Cached end of (new) line */ int cleaned = 0; replacew = CENSORWORD; replacen = sizeof(CENSORWORD)-1; while (1) { pold = my_strcasestr(pold, badword); if (!pold) break; if (searchn == -1) searchn = strlen(badword); /* Hunt for start of word */ if (pold > line) { for (startw = pold; (!iswseperator(*startw) && (startw != line)); startw--); if (iswseperator(*startw)) startw++; /* Don't point at the space/seperator but at the word! */ } else { startw = pold; } if (!(type & TEXTBAN_WORD_LEFT) && (pold != startw)) { /* not matched */ pold++; continue; } /* Hunt for end of word * Fix for bug #4909: word will be at least 'searchn' long so we can skip * 'searchn' bytes and avoid stopping half-way the badword. */ for (endw = pold+searchn; ((*endw != '\0') && (!iswseperator(*endw))); endw++); if (!(type & TEXTBAN_WORD_RIGHT) && (pold+searchn != endw)) { /* not matched */ pold++; continue; } cleaned = 1; /* still too soon? Syzop/20050227 */ /* Do we have any not-copied-yet data? */ if (poldx != startw) { int tmp_n = startw - poldx; if (pnew + tmp_n >= c_eol) { /* Partial copy and return... */ memcpy(pnew, poldx, c_eol - pnew); *c_eol = '\0'; return 1; } memcpy(pnew, poldx, tmp_n); pnew += tmp_n; } /* Now update the word in buf (pnew is now something like startw-in-new-buffer */ if (replacen) { if ((pnew + replacen) >= c_eol) { /* Partial copy and return... */ memcpy(pnew, replacew, c_eol - pnew); *c_eol = '\0'; return 1; } memcpy(pnew, replacew, replacen); pnew += replacen; } poldx = pold = endw; } /* Copy the last part */ if (*poldx) { strncpy(pnew, poldx, c_eol - pnew); *(c_eol) = '\0'; } else { *pnew = '\0'; } return cleaned; } #endif unsigned int counttextbans(Channel *channel) { Ban *ban; unsigned int cnt = 0; for (ban = channel->banlist; ban; ban=ban->next) if ((ban->banstr[0] == '~') && (ban->banstr[1] == 'T') && (ban->banstr[2] == ':')) cnt++; for (ban = channel->exlist; ban; ban=ban->next) if ((ban->banstr[0] == '~') && (ban->banstr[1] == 'T') && (ban->banstr[2] == ':')) cnt++; return cnt; } int extban_modeT_is_ok(BanContext *b) { int n; if ((b->what == MODE_ADD) && (b->ban_type == EXBTYPE_EXCEPT) && MyUser(b->client)) return 0; /* except is not supported */ /* We check the # of bans in the channel, may not exceed MAX_EXTBANT_PER_CHAN */ if ((b->what == MODE_ADD) && (b->is_ok_check == EXBCHK_PARAM) && MyUser(b->client) && !IsOper(b->client) && ((n = counttextbans(b->channel)) >= MAX_EXTBANT_PER_CHAN)) { /* We check the # of bans in the channel, may not exceed MAX_EXTBANT_PER_CHAN */ sendnumeric(b->client, ERR_BANLISTFULL, b->channel->name, b->banstr); // FIXME: wants b->full_banstr here sendnotice(b->client, "Too many textbans for this channel"); return 0; } return 1; } char *conv_pattern_asterisks(const char *pattern) { static char buf[512]; char missing_prefix = 0, missing_suffix = 0; if (*pattern != '*') missing_prefix = 1; if (*pattern && (pattern[strlen(pattern)-1] != '*')) missing_suffix = 1; snprintf(buf, sizeof(buf), "%s%s%s", missing_prefix ? "*" : "", pattern, missing_suffix ? "*" : ""); return buf; } /** Ban callbacks */ const char *extban_modeT_conv_param(BanContext *b, Extban *extban) { static char retbuf[MAX_LENGTH+1]; char para[MAX_LENGTH+1], *action, *text, *p; #ifdef UHOSTFEATURE char *uhost; int ap = 0; #endif strlcpy(para, b->banstr, sizeof(para)); /* work on a copy (and truncate it) */ /* ~T:: * ~T:user@host:: if UHOSTFEATURE is enabled */ #ifdef UHOSTFEATURE action = strchr(para, ':'); if (!action) return NULL; *action++ = '\0'; if (!*action) return NULL; text = strchr(action, ':'); if (!text || !text[1]) return NULL; *text++ = '\0'; uhost = para; for (p = uhost; *p; p++) { if (*p == '@') ap++; else if ((*p <= ' ') || (*p > 128)) return NULL; /* cannot be in a username/host */ } if (ap != 1) return NULL; /* no @ */ #else text = strchr(para, ':'); if (!text) return NULL; *text++ = '\0'; /* para=action, text=text */ if (!*text) return NULL; /* empty text */ action = para; #endif /* ~T:: */ if (!strcasecmp(action, "block")) { action = "block"; /* ok */ text = conv_pattern_asterisks(text); } #ifdef CENSORFEATURE else if (!strcasecmp(action, "censor")) { char *p; action = "censor"; for (p = text; *p; p++) if ((*p == '*') && !(p == text) && !(p[1] == '\0')) return NULL; /* can only be *word, word* or *word* or word */ if (!strcmp(p, "*") || !strcmp(p, "**")) return NULL; /* cannot match everything ;p */ } #endif else return NULL; /* unknown action */ /* check the string.. */ for (p=text; *p; p++) { if ((*p == '\003') || (*p == '\002') || (*p == '\037') || (*p == '\026') || (*p == ' ')) { return NULL; /* codes not permitted, would be confusing since they are stripped */ } } /* Rebuild the string.. can be cut off if too long. */ #ifdef UHOSTFEATURE snprintf(retbuf, sizeof(retbuf), "%s:%s:%s", uhost, action, text); #else snprintf(retbuf, sizeof(retbuf), "%s:%s", action, text); #endif return retbuf; } /** Check for text bans (censor and block) */ int textban_can_send_to_channel(Client *client, Channel *channel, Membership *lp, const char **msg, const char **errmsg, SendType sendtype) { Ban *ban; /* +h/+o/+a/+q users bypass textbans */ if (check_channel_access(client, channel, "hoaq")) return HOOK_CONTINUE; /* IRCOps with these privileges bypass textbans too */ if (op_can_override("channel:override:message:ban", client, channel, NULL)) return HOOK_CONTINUE; /* Now we have to manually walk the banlist and check if things match */ for (ban = channel->banlist; ban; ban=ban->next) { char *banstr = ban->banstr; /* Pretend time does not exist... */ if (!strncmp(banstr, "~t:", 3)) { banstr = strchr(banstr+3, ':'); if (!banstr) continue; banstr++; } else if (!strncmp(banstr, "~time:", 6)) { banstr = strchr(banstr+6, ':'); if (!banstr) continue; banstr++; } if (!strncmp(banstr, "~T:", 3) || !strncmp(banstr, "~text:", 6)) { /* text ban */ if (textban_check_ban(client, channel, banstr, msg, errmsg)) return HOOK_DENY; } } return HOOK_CONTINUE; } int textban_check_ban(Client *client, Channel *channel, const char *ban, const char **msg, const char **errmsg) { static char retbuf[512]; char filtered[512]; /* temp input buffer */ long fl; int cleaned=0; const char *p; #ifdef UHOSTFEATURE char buf[512], uhost[USERLEN + HOSTLEN + 16]; #endif char tmp[1024], *word; int type; /* We can only filter on non-NULL text of course */ if ((msg == NULL) || (*msg == NULL)) return 0; filtered[0] = '\0'; /* NOT needed, but... :P */ #ifdef UHOSTFEATURE ircsprintf(uhost, "%s@%s", client->user->username, GetHost(client)); #endif strlcpy(filtered, StripControlCodes(*msg), sizeof(filtered)); p = strchr(ban, ':'); if (!p) return 0; /* "impossible" */ p++; #ifdef UHOSTFEATURE /* First.. deal with userhost... */ strcpy(buf, p); p = strchr(buf, ':'); if (!p) return 0; /* invalid format */ *p++ = '\0'; if (match_simple(buf, uhost)) #else if (1) #endif { if (!strncasecmp(p, "block:", 6)) { if (match_simple(p+6, filtered)) { if (errmsg) *errmsg = "Message blocked due to a text ban"; return 1; /* BLOCK */ } } #ifdef CENSORFEATURE else if (!strncasecmp(p, "censor:", 7)) { parse_word(p+7, &word, &type); if (textban_replace(type, word, filtered, tmp)) { strlcpy(filtered, tmp, sizeof(filtered)); cleaned = 1; } } #endif } if (cleaned) { /* check for null string */ char *p; for (p = filtered; *p; p++) { if (*p != ' ') { strlcpy(retbuf, filtered, sizeof(retbuf)); *msg = retbuf; return 0; /* allow through, but filtered */ } } return 1; /* nothing but spaces found.. */ } return 0; /* nothing blocked */ } #ifdef CENSORFEATURE void parse_word(const char *s, char **word, int *type) { static char buf[512]; const char *tmp; int len; int tpe = 0; char *o = buf; for (tmp = s; *tmp; tmp++) { if (*tmp != '*') *o++ = *tmp; else { if (s == tmp) tpe |= TEXTBAN_WORD_LEFT; if (*(tmp + 1) == '\0') tpe |= TEXTBAN_WORD_RIGHT; } } *o = '\0'; *word = buf; *type = tpe; } #endif