]> jfr.im git - solanum.git/blob - extensions/filter.c
m_stats: z: remove unnecessary casting and fix format strings
[solanum.git] / extensions / filter.c
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"
30 #include "chmode.h"
31 #include "match.h"
32 #include "ircd.h"
33 #include "numeric.h"
34 #include "send.h"
35 #include "s_newconf.h"
36 #include "s_serv.h"
37 #include "s_user.h"
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
48 #define FILTER_NICK 0
49 #define FILTER_USER 0
50 #define FILTER_HOST 0
51
52 #define FILTER_EXIT_MSG "Connection closed"
53
54 static const char filter_desc[] = "Filter messages using a precompiled Hyperscan database";
55
56 static void filter_msg_user(void *data);
57 static void filter_msg_channel(void *data);
58 static void filter_client_quit(void *data);
59 static void on_client_exit(void *data);
60
61 static void mo_setfilter(struct MsgBuf *, struct Client *, struct Client *, int, const char **);
62 static void me_setfilter(struct MsgBuf *, struct Client *, struct Client *, int, const char **);
63
64 static char *filter_data = NULL;
65 static size_t filter_data_len = 0;
66 static hs_database_t *filter_db;
67 static hs_scratch_t *filter_scratch;
68
69 static int filter_enable = 1;
70
71 static const char *cmdname[MESSAGE_TYPE_COUNT] = {
72 [MESSAGE_TYPE_PRIVMSG] = "PRIVMSG",
73 [MESSAGE_TYPE_NOTICE] = "NOTICE",
74 [MESSAGE_TYPE_PART] = "PART",
75 };
76
77 enum 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
87 static enum filter_state state = FILTER_EMPTY;
88 static char check_str[21] = "";
89
90 static unsigned filter_chmode, filter_umode;
91
92 mapi_hfn_list_av1 filter_hfnlist[] = {
93 { "privmsg_user", filter_msg_user },
94 { "privmsg_channel", filter_msg_channel },
95 { "client_quit", filter_client_quit },
96 { "client_exit", on_client_exit },
97 { NULL, NULL }
98 };
99
100
101 struct 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
106 static int
107 modinit(void)
108 {
109 filter_umode = user_modes['u'] = find_umode_slot();
110 construct_umodebuf();
111 filter_chmode = cflag_add('u', chm_simple);
112 return 0;
113 }
114
115 static void
116 moddeinit(void)
117 {
118 if (filter_umode) {
119 user_modes['u'] = 0;
120 construct_umodebuf();
121 }
122 if (filter_chmode)
123 cflag_orphan('u');
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
133 mapi_clist_av1 filter_clist[] = { &setfilter_msgtab, NULL };
134
135 DECLARE_MODULE_AV2(filter, modinit, moddeinit, filter_clist, NULL, filter_hfnlist, NULL, "0.4", filter_desc);
136
137 static int
138 setfilter(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;
144 sendto_realops_snomask(SNO_GENERAL, L_NETWIDE,
145 "Filtering disabled.");
146 return 0;
147 }
148 if (!strcasecmp(data, "enable")) {
149 filter_enable = 1;
150 sendto_realops_snomask(SNO_GENERAL, L_NETWIDE,
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
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
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";
212 hs_free_database(db);
213 return -1;
214 }
215 if (filter_db) {
216 hs_free_database(filter_db);
217 }
218 state = FILTER_LOADED;
219 filter_db = db;
220 sendto_realops_snomask(SNO_GENERAL, L_NETWIDE,
221 "New filters loaded.");
222 rb_free(filter_data);
223 filter_data = 0;
224 filter_data_len = 0;
225 return 0;
226 }
227
228 if (*data != '+') {
229 if (error) *error = "unknown command or data doesn't start with +";
230 return -1;
231 }
232
233 data += 1;
234
235 if (state == FILTER_FILLING) {
236 int dl;
237 unsigned char *d = rb_base64_decode((unsigned char *)data, strlen(data), &dl);
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 */
266 static void
267 mo_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
304 static void
305 me_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. */
324 int 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
335 static char check_buffer[2000];
336 static char clean_buffer[BUFSIZE];
337
338 unsigned match_message(const char *prefix,
339 struct Client *source,
340 const char *command,
341 const char *target,
342 const char *msg)
343 {
344 unsigned state = 0;
345 if (!filter_enable)
346 return 0;
347 if (!filter_db)
348 return 0;
349 if (!command)
350 return 0;
351 snprintf(check_buffer, sizeof check_buffer, "%s:%s!%s@%s#%c %s%s%s :%s",
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',
369 command,
370 target ? " " : "",
371 target ? target : "",
372 msg);
373 hs_error_t r = hs_scan(filter_db, check_buffer, strlen(check_buffer), 0, filter_scratch, match_callback, &state);
374 if (r != HS_SUCCESS && r != HS_SCAN_TERMINATED)
375 return 0;
376 return state;
377 }
378
379 void
380 filter_msg_user(void *data_)
381 {
382 hook_data_privmsg_user *data = data_;
383 struct Client *s = data->source_p;
384 /* we only need to filter once */
385 if (!MyClient(s)) {
386 return;
387 }
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 }
394 if (data->target_p->umodes & filter_umode) {
395 return;
396 }
397 char *text = strcpy(clean_buffer, data->text);
398 strip_colour(text);
399 strip_unprintable(text);
400 unsigned r = match_message("0", s, cmdname[data->msgtype], "0", data->text) |
401 match_message("1", s, cmdname[data->msgtype], "0", text);
402 if (r & ACT_DROP) {
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 }
408 data->approved = 1;
409 }
410 if (r & ACT_ALARM) {
411 sendto_realops_snomask(SNO_GENERAL, L_NETWIDE,
412 "FILTER: %s!%s@%s [%s]",
413 s->name, s->username, s->host, s->sockhost);
414 }
415 if (r & ACT_KILL) {
416 data->approved = 1;
417 exit_client(NULL, s, s, FILTER_EXIT_MSG);
418 }
419 }
420
421 void
422 filter_msg_channel(void *data_)
423 {
424 hook_data_privmsg_channel *data = data_;
425 struct Client *s = data->source_p;
426 /* we only need to filter once */
427 if (!MyClient(s)) {
428 return;
429 }
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 }
435 if (data->chptr->mode.mode & filter_chmode) {
436 return;
437 }
438 char *text = strcpy(clean_buffer, data->text);
439 strip_colour(text);
440 strip_unprintable(text);
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);
443 if (r & ACT_DROP) {
444 if (data->msgtype == MESSAGE_TYPE_PRIVMSG) {
445 sendto_one_numeric(s, ERR_CANNOTSENDTOCHAN,
446 form_str(ERR_CANNOTSENDTOCHAN),
447 data->chptr->chname);
448 }
449 data->approved = 1;
450 }
451 if (r & ACT_ALARM) {
452 sendto_realops_snomask(SNO_GENERAL, L_NETWIDE,
453 "FILTER: %s!%s@%s [%s]",
454 s->name, s->username, s->host, s->sockhost);
455 }
456 if (r & ACT_KILL) {
457 data->approved = 1;
458 exit_client(NULL, s, s, FILTER_EXIT_MSG);
459 }
460 }
461
462 void
463 filter_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
486 void
487 on_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 }