]> jfr.im git - irc/freenode/solanum.git/blame - extensions/filter.c
make more snotes L_NETWIDE
[irc/freenode/solanum.git] / extensions / filter.c
CommitLineData
a2d9c494
EK
1/*
2 * ircd-ratbox: A slightly useful ircd.
3 * filter.c: Drop messages we don't like
4 *
5 * Copyright (C) 1990 Jarkko Oikarinen and University of Oulu, Co Center
6 * Copyright (C) 1996-2002 Hybrid Development Team
7 * Copyright (C) 2002-2005 ircd-ratbox development team
8 *
9 * This program is free software; you can redistribute it and/or modify
10 * it under the terms of the GNU General Public License as published by
11 * the Free Software Foundation; either version 2 of the License, or
12 * (at your option) any later version.
13 *
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 * GNU General Public License for more details.
18 *
19 * You should have received a copy of the GNU General Public License
20 * along with this program; if not, write to the Free Software
21 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
22 * USA
23 *
24 * $Id$
25 */
26
27#include "stdinc.h"
28#include "channel.h"
29#include "client.h"
8692240f 30#include "chmode.h"
a2d9c494
EK
31#include "match.h"
32#include "ircd.h"
33#include "numeric.h"
34#include "send.h"
a2d9c494 35#include "s_newconf.h"
8692240f
EK
36#include "s_serv.h"
37#include "s_user.h"
a2d9c494
EK
38#include "msg.h"
39#include "parse.h"
40#include "modules.h"
41#include "operhash.h"
42#include "inline/stringops.h"
43#include "msgbuf.h"
44
45#include <hs_common.h>
46#include <hs_runtime.h>
47
81e41406
EK
48#define FILTER_NICK 0
49#define FILTER_USER 0
50#define FILTER_HOST 0
51
649bda6d
EK
52#define FILTER_EXIT_MSG "Connection closed"
53
a8f402e9
EK
54static const char filter_desc[] = "Filter messages using a precompiled Hyperscan database";
55
a2d9c494
EK
56static void filter_msg_user(void *data);
57static void filter_msg_channel(void *data);
211c25b1 58static void filter_client_quit(void *data);
a2d9c494
EK
59static void on_client_exit(void *data);
60
61static void mo_setfilter(struct MsgBuf *, struct Client *, struct Client *, int, const char **);
62static void me_setfilter(struct MsgBuf *, struct Client *, struct Client *, int, const char **);
63
64static char *filter_data = NULL;
65static size_t filter_data_len = 0;
66static hs_database_t *filter_db;
67static hs_scratch_t *filter_scratch;
68
69static int filter_enable = 1;
70
71static const char *cmdname[MESSAGE_TYPE_COUNT] = {
72 [MESSAGE_TYPE_PRIVMSG] = "PRIVMSG",
73 [MESSAGE_TYPE_NOTICE] = "NOTICE",
e9408bc4 74 [MESSAGE_TYPE_PART] = "PART",
a2d9c494
EK
75};
76
77enum filter_state {
78 FILTER_EMPTY,
79 FILTER_FILLING,
80 FILTER_LOADED
81};
82
83#define ACT_DROP (1 << 0)
84#define ACT_KILL (1 << 1)
85#define ACT_ALARM (1 << 2)
86
87static enum filter_state state = FILTER_EMPTY;
88static char check_str[21] = "";
89
8692240f
EK
90static unsigned filter_chmode, filter_umode;
91
a2d9c494
EK
92mapi_hfn_list_av1 filter_hfnlist[] = {
93 { "privmsg_user", (hookfn) filter_msg_user },
94 { "privmsg_channel", (hookfn) filter_msg_channel },
211c25b1 95 { "client_quit", (hookfn) filter_client_quit },
a2d9c494
EK
96 { "client_exit", (hookfn) on_client_exit },
97 { NULL, NULL }
98};
99
100
101struct Message setfilter_msgtab = {
102 "SETFILTER", 0, 0, 0, 0,
103 {mg_unreg, mg_not_oper, mg_ignore, mg_ignore, {me_setfilter, 2}, {mo_setfilter, 2}}
104};
105
eb06afc3 106static int
8692240f
EK
107modinit(void)
108{
109 filter_umode = user_modes['u'] = find_umode_slot();
110 construct_umodebuf();
111 filter_chmode = cflag_add('u', chm_simple);
eb06afc3 112 return 0;
8692240f
EK
113}
114
a2d9c494
EK
115static void
116moddeinit(void)
117{
8692240f
EK
118 if (filter_umode) {
119 user_modes['u'] = 0;
120 construct_umodebuf();
121 }
122 if (filter_chmode)
123 cflag_orphan('u');
a2d9c494
EK
124 if (filter_scratch)
125 hs_free_scratch(filter_scratch);
126 if (filter_db)
127 hs_free_database(filter_db);
128 if (filter_data)
129 rb_free(filter_data);
130}
131
132
133mapi_clist_av1 filter_clist[] = { &setfilter_msgtab, NULL };
134
a8f402e9 135DECLARE_MODULE_AV2(filter, modinit, moddeinit, filter_clist, NULL, filter_hfnlist, NULL, "0.4", filter_desc);
a2d9c494
EK
136
137static int
138setfilter(const char *check, const char *data, const char **error)
139{
140 if (error) *error = "unknown";
141
142 if (!strcasecmp(data, "disable")) {
143 filter_enable = 0;
a9227555 144 sendto_realops_snomask(SNO_GENERAL, L_NETWIDE,
a2d9c494
EK
145 "Filtering disabled.");
146 return 0;
147 }
148 if (!strcasecmp(data, "enable")) {
149 filter_enable = 1;
a9227555 150 sendto_realops_snomask(SNO_GENERAL, L_NETWIDE,
a2d9c494
EK
151 "Filtering enabled.");
152 return 0;
153 }
154
155 if (strlen(check) > sizeof check_str - 1) {
156 if (error) *error = "check string too long";
157 return -1;
158 }
159
160 if (!strcasecmp(data, "new")) {
161 if (state == FILTER_FILLING) {
162 rb_free(filter_data);
163 filter_data = 0;
164 filter_data_len = 0;
165 }
166 state = FILTER_FILLING;
167 strcpy(check_str, check);
168 return 0;
169 }
170
344af24c
EK
171 if (!strcasecmp(data, "drop")) {
172 if (!filter_db) {
173 if (error) *error = "no database to drop";
174 return -1;
175 }
176 hs_free_database(filter_db);
177 filter_db = 0;
178 return 0;
179 }
180
181 if (!strcasecmp(data, "abort")) {
182 if (state != FILTER_FILLING) {
183 if (error) *error = "not filling";
184 return -1;
185 }
186 state = filter_db ? FILTER_LOADED : FILTER_EMPTY;
187 rb_free(filter_data);
188 filter_data = 0;
189 filter_data_len = 0;
190 return 0;
191 }
192
a2d9c494
EK
193 if (strcmp(check, check_str) != 0) {
194 if (error) *error = "check strings don't match";
195 return -1;
196 }
197
198 if (!strcasecmp(data, "apply")) {
199 if (state != FILTER_FILLING) {
200 if (error) *error = "not loading anything";
201 return -1;
202 }
203 hs_database_t *db;
204 hs_error_t r = hs_deserialize_database(filter_data, filter_data_len, &db);
205 if (r != HS_SUCCESS) {
206 if (error) *error = "couldn't deserialize db";
207 return -1;
208 }
209 r = hs_alloc_scratch(db, &filter_scratch);
210 if (r != HS_SUCCESS) {
211 if (error) *error = "couldn't allocate scratch";
09784400 212 hs_free_database(db);
a2d9c494
EK
213 return -1;
214 }
215 if (filter_db) {
216 hs_free_database(filter_db);
217 }
218 state = FILTER_LOADED;
219 filter_db = db;
a9227555 220 sendto_realops_snomask(SNO_GENERAL, L_NETWIDE,
a2d9c494
EK
221 "New filters loaded.");
222 rb_free(filter_data);
223 filter_data = 0;
224 filter_data_len = 0;
225 return 0;
226 }
227
dc141aad
EK
228 if (*data != '+') {
229 if (error) *error = "unknown command or data doesn't start with +";
230 return -1;
231 }
232
233 data += 1;
234
a2d9c494
EK
235 if (state == FILTER_FILLING) {
236 int dl;
0cbb1ba9 237 unsigned char *d = rb_base64_decode((unsigned char *)data, strlen(data), &dl);
a2d9c494
EK
238 if (!d) {
239 if (error) *error = "invalid data";
240 return -1;
241 }
242 if (filter_data_len + dl > 10000000ul) {
243 if (error) *error = "data over size limit";
244 rb_free(d);
245 return -1;
246 }
247 filter_data = rb_realloc(filter_data, filter_data_len + dl);
248 memcpy(filter_data + filter_data_len, d, dl);
249 rb_free(d);
250 filter_data_len += dl;
251 } else {
252 if (error) *error = "send \"new\" first";
253 return -1;
254 }
255 return 0;
256}
257
258/* /SETFILTER [server-mask] <check> { NEW | APPLY | <data> }
259 * <check> must be the same for the entirety of a new...data...apply run,
260 * and exists just to ensure runs don't mix
261 * NEW prepares a buffer to receive a hyperscan database
262 * <data> is base64 encoded chunks of hyperscan database, which are decoded
263 * and appended to the buffer
264 * APPLY deserialises the buffer and sets the resulting hyperscan database
265 * as the one to use for filtering */
266static void
267mo_setfilter(struct MsgBuf *msgbuf, struct Client *client_p, struct Client *source_p, int parc, const char **parv)
268{
269 int for_me = 0;
270 const char *check;
271 const char *data;
272 if (!IsOperAdmin(source_p)) {
273 sendto_one(source_p, form_str(ERR_NOPRIVS), me.name, source_p->name, "admin");
274 return;
275 }
276 if (parc == 4) {
277 check = parv[2];
278 data = parv[3];
279 if(match(parv[1], me.name)) {
280 for_me = 1;
281 }
282 sendto_match_servs(source_p, parv[1],
283 CAP_ENCAP, NOCAPS,
284 "ENCAP %s SETFILTER %s :%s", parv[1], check, data);
285 } else if (parc == 3) {
286 check = parv[1];
287 data = parv[2];
288 for_me = 1;
289 } else {
290 sendto_one_notice(source_p, ":SETFILTER needs 2 or 3 params, have %d", parc - 1);
291 return;
292 }
293 if (for_me) {
294 const char *error;
295 int r = setfilter(check, data, &error);
296 if (r) {
297 sendto_one_notice(source_p, ":SETFILTER failed: %s", error);
298 } else {
299 sendto_one_notice(source_p, ":SETFILTER ok");
300 }
301 }
302}
303
304static void
305me_setfilter(struct MsgBuf *msgbuf, struct Client *client_p, struct Client *source_p, int parc, const char **parv)
306{
307 if(!IsPerson(source_p))
308 return;
309
310 const char *error;
311 int r = setfilter(parv[1], parv[2], &error);
312 if (r) {
313 sendto_one_notice(source_p, ":SETFILTER failed: %s", error);
314 }
315
316 return;
317}
318
319/* will be called for every match
320 * hyperscan provides us one piece of information about the expression
321 * matched, an integer ID. we're co-opting the lowest 3 bits of this
322 * as a flag set. conveniently, this means all we really need to do
323 * here is or the IDs together. */
324int match_callback(unsigned id,
325 unsigned long long from,
326 unsigned long long to,
327 unsigned flags,
328 void *context_)
329{
330 unsigned *context = context_;
331 *context |= id;
332 return 0;
333}
334
744ac308
EK
335static char check_buffer[2000];
336static char clean_buffer[BUFSIZE];
a2d9c494 337
744ac308
EK
338unsigned match_message(const char *prefix,
339 struct Client *source,
340 const char *command,
341 const char *target,
342 const char *msg)
a2d9c494
EK
343{
344 unsigned state = 0;
345 if (!filter_enable)
346 return 0;
347 if (!filter_db)
348 return 0;
e9408bc4
EK
349 if (!command)
350 return 0;
211c25b1 351 snprintf(check_buffer, sizeof check_buffer, "%s:%s!%s@%s#%c %s%s%s :%s",
744ac308
EK
352 prefix,
353#if FILTER_NICK
354 source->name,
355#else
356 "*",
357#endif
358#if FILTER_USER
359 source->username,
360#else
361 "*",
362#endif
363#if FILTER_HOST
364 source->host,
365#else
366 "*",
367#endif
368 source->user && source->user->suser[0] != '\0' ? '1' : '0',
211c25b1
EK
369 command,
370 target ? " " : "",
371 target ? target : "",
372 msg);
744ac308 373 hs_error_t r = hs_scan(filter_db, check_buffer, strlen(check_buffer), 0, filter_scratch, match_callback, &state);
a2d9c494
EK
374 if (r != HS_SUCCESS && r != HS_SCAN_TERMINATED)
375 return 0;
376 return state;
377}
378
a2d9c494
EK
379void
380filter_msg_user(void *data_)
381{
382 hook_data_privmsg_user *data = data_;
383 struct Client *s = data->source_p;
86ee00db
EK
384 /* we only need to filter once */
385 if (!MyClient(s)) {
386 return;
387 }
a2d9c494
EK
388 /* opers are immune to checking, for obvious reasons
389 * anything sent to an oper is also immune, because that should make it
390 * less impossible to deal with reports. */
391 if (IsOper(s) || IsOper(data->target_p)) {
392 return;
393 }
8692240f
EK
394 if (data->target_p->umodes & filter_umode) {
395 return;
396 }
f982238e 397 char *text = strcpy(clean_buffer, data->text);
a2d9c494
EK
398 strip_colour(text);
399 strip_unprintable(text);
744ac308
EK
400 unsigned r = match_message("0", s, cmdname[data->msgtype], "0", data->text) |
401 match_message("1", s, cmdname[data->msgtype], "0", text);
7bb7f899 402 if (r & ACT_DROP) {
d928bc7a
EK
403 if (data->msgtype == MESSAGE_TYPE_PRIVMSG) {
404 sendto_one_numeric(s, ERR_CANNOTSENDTOCHAN,
405 form_str(ERR_CANNOTSENDTOCHAN),
406 data->target_p->name);
407 }
a2d9c494
EK
408 data->approved = 1;
409 }
86ee00db 410 if (r & ACT_ALARM) {
a9227555 411 sendto_realops_snomask(SNO_GENERAL, L_NETWIDE,
a2d9c494
EK
412 "FILTER: %s!%s@%s [%s]",
413 s->name, s->username, s->host, s->sockhost);
414 }
6a14bf78 415 if (r & ACT_KILL) {
7bb7f899 416 data->approved = 1;
649bda6d 417 exit_client(NULL, s, s, FILTER_EXIT_MSG);
6a14bf78 418 }
a2d9c494
EK
419}
420
421void
422filter_msg_channel(void *data_)
423{
424 hook_data_privmsg_channel *data = data_;
425 struct Client *s = data->source_p;
86ee00db
EK
426 /* we only need to filter once */
427 if (!MyClient(s)) {
428 return;
429 }
a2d9c494
EK
430 /* just normal oper immunity for channels. i'd like to have a mode that
431 * disables the filter per-channel, but that's for the future */
432 if (IsOper(s)) {
433 return;
434 }
8692240f
EK
435 if (data->chptr->mode.mode & filter_chmode) {
436 return;
437 }
f982238e 438 char *text = strcpy(clean_buffer, data->text);
a2d9c494
EK
439 strip_colour(text);
440 strip_unprintable(text);
744ac308
EK
441 unsigned r = match_message("0", s, cmdname[data->msgtype], data->chptr->chname, data->text) |
442 match_message("1", s, cmdname[data->msgtype], data->chptr->chname, text);
7bb7f899 443 if (r & ACT_DROP) {
d928bc7a
EK
444 if (data->msgtype == MESSAGE_TYPE_PRIVMSG) {
445 sendto_one_numeric(s, ERR_CANNOTSENDTOCHAN,
446 form_str(ERR_CANNOTSENDTOCHAN),
447 data->chptr->chname);
448 }
a2d9c494
EK
449 data->approved = 1;
450 }
86ee00db 451 if (r & ACT_ALARM) {
a9227555 452 sendto_realops_snomask(SNO_GENERAL, L_NETWIDE,
a2d9c494
EK
453 "FILTER: %s!%s@%s [%s]",
454 s->name, s->username, s->host, s->sockhost);
455 }
6a14bf78 456 if (r & ACT_KILL) {
7bb7f899 457 data->approved = 1;
649bda6d 458 exit_client(NULL, s, s, FILTER_EXIT_MSG);
6a14bf78 459 }
a2d9c494
EK
460}
461
211c25b1
EK
462void
463filter_client_quit(void *data_)
464{
465 hook_data_client_quit *data = data_;
466 struct Client *s = data->client;
467 if (IsOper(s)) {
468 return;
469 }
470 char *text = strcpy(clean_buffer, data->orig_reason);
471 strip_colour(text);
472 strip_unprintable(text);
473 unsigned r = match_message("0", s, "QUIT", NULL, data->orig_reason) |
474 match_message("1", s, "QUIT", NULL, text);
475 if (r & ACT_DROP) {
476 data->reason = NULL;
477 }
478 if (r & ACT_ALARM) {
479 sendto_realops_snomask(SNO_GENERAL, L_ALL | L_NETWIDE,
480 "FILTER: %s!%s@%s [%s]",
481 s->name, s->username, s->host, s->sockhost);
482 }
483 /* No point in doing anything with ACT_KILL */
484}
485
a2d9c494
EK
486void
487on_client_exit(void *data_)
488{
489 /* If we see a netsplit, abort the current FILTER_FILLING attempt */
490 hook_data_client_exit *data = data_;
491
492 if (!IsServer(data->target)) return;
493
494 if (state == FILTER_FILLING) {
495 state = filter_db ? FILTER_LOADED : FILTER_EMPTY;
496 }
497}