]> jfr.im git - solanum.git/blob - extensions/filter.c
Merge pull request #321 from edk0/hook-priorities
[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 on_client_exit(void *data);
59
60 static void mo_setfilter(struct MsgBuf *, struct Client *, struct Client *, int, const char **);
61 static void me_setfilter(struct MsgBuf *, struct Client *, struct Client *, int, const char **);
62
63 static char *filter_data = NULL;
64 static size_t filter_data_len = 0;
65 static hs_database_t *filter_db;
66 static hs_scratch_t *filter_scratch;
67
68 static int filter_enable = 1;
69
70 static const char *cmdname[MESSAGE_TYPE_COUNT] = {
71 [MESSAGE_TYPE_PRIVMSG] = "PRIVMSG",
72 [MESSAGE_TYPE_NOTICE] = "NOTICE",
73 [MESSAGE_TYPE_PART] = "PART",
74 };
75
76 enum filter_state {
77 FILTER_EMPTY,
78 FILTER_FILLING,
79 FILTER_LOADED
80 };
81
82 #define ACT_DROP (1 << 0)
83 #define ACT_KILL (1 << 1)
84 #define ACT_ALARM (1 << 2)
85
86 static enum filter_state state = FILTER_EMPTY;
87 static char check_str[21] = "";
88
89 static unsigned filter_chmode, filter_umode;
90
91 mapi_hfn_list_av1 filter_hfnlist[] = {
92 { "privmsg_user", (hookfn) filter_msg_user },
93 { "privmsg_channel", (hookfn) filter_msg_channel },
94 { "client_exit", (hookfn) on_client_exit },
95 { NULL, NULL }
96 };
97
98
99 struct Message setfilter_msgtab = {
100 "SETFILTER", 0, 0, 0, 0,
101 {mg_unreg, mg_not_oper, mg_ignore, mg_ignore, {me_setfilter, 2}, {mo_setfilter, 2}}
102 };
103
104 static int
105 modinit(void)
106 {
107 filter_umode = user_modes['u'] = find_umode_slot();
108 construct_umodebuf();
109 filter_chmode = cflag_add('u', chm_simple);
110 return 0;
111 }
112
113 static void
114 moddeinit(void)
115 {
116 if (filter_umode) {
117 user_modes['u'] = 0;
118 construct_umodebuf();
119 }
120 if (filter_chmode)
121 cflag_orphan('u');
122 if (filter_scratch)
123 hs_free_scratch(filter_scratch);
124 if (filter_db)
125 hs_free_database(filter_db);
126 if (filter_data)
127 rb_free(filter_data);
128 }
129
130
131 mapi_clist_av1 filter_clist[] = { &setfilter_msgtab, NULL };
132
133 DECLARE_MODULE_AV2(filter, modinit, moddeinit, filter_clist, NULL, filter_hfnlist, NULL, "0.4", filter_desc);
134
135 static int
136 setfilter(const char *check, const char *data, const char **error)
137 {
138 if (error) *error = "unknown";
139
140 if (!strcasecmp(data, "disable")) {
141 filter_enable = 0;
142 sendto_realops_snomask(SNO_GENERAL, L_ALL | L_NETWIDE,
143 "Filtering disabled.");
144 return 0;
145 }
146 if (!strcasecmp(data, "enable")) {
147 filter_enable = 1;
148 sendto_realops_snomask(SNO_GENERAL, L_ALL | L_NETWIDE,
149 "Filtering enabled.");
150 return 0;
151 }
152
153 if (strlen(check) > sizeof check_str - 1) {
154 if (error) *error = "check string too long";
155 return -1;
156 }
157
158 if (!strcasecmp(data, "new")) {
159 if (state == FILTER_FILLING) {
160 rb_free(filter_data);
161 filter_data = 0;
162 filter_data_len = 0;
163 }
164 state = FILTER_FILLING;
165 strcpy(check_str, check);
166 return 0;
167 }
168
169 if (!strcasecmp(data, "drop")) {
170 if (!filter_db) {
171 if (error) *error = "no database to drop";
172 return -1;
173 }
174 hs_free_database(filter_db);
175 filter_db = 0;
176 return 0;
177 }
178
179 if (!strcasecmp(data, "abort")) {
180 if (state != FILTER_FILLING) {
181 if (error) *error = "not filling";
182 return -1;
183 }
184 state = filter_db ? FILTER_LOADED : FILTER_EMPTY;
185 rb_free(filter_data);
186 filter_data = 0;
187 filter_data_len = 0;
188 return 0;
189 }
190
191 if (strcmp(check, check_str) != 0) {
192 if (error) *error = "check strings don't match";
193 return -1;
194 }
195
196 if (!strcasecmp(data, "apply")) {
197 if (state != FILTER_FILLING) {
198 if (error) *error = "not loading anything";
199 return -1;
200 }
201 hs_database_t *db;
202 hs_error_t r = hs_deserialize_database(filter_data, filter_data_len, &db);
203 if (r != HS_SUCCESS) {
204 if (error) *error = "couldn't deserialize db";
205 return -1;
206 }
207 r = hs_alloc_scratch(db, &filter_scratch);
208 if (r != HS_SUCCESS) {
209 if (error) *error = "couldn't allocate scratch";
210 hs_free_database(db);
211 return -1;
212 }
213 if (filter_db) {
214 hs_free_database(filter_db);
215 }
216 state = FILTER_LOADED;
217 filter_db = db;
218 sendto_realops_snomask(SNO_GENERAL, L_ALL | L_NETWIDE,
219 "New filters loaded.");
220 rb_free(filter_data);
221 filter_data = 0;
222 filter_data_len = 0;
223 return 0;
224 }
225
226 if (*data != '+') {
227 if (error) *error = "unknown command or data doesn't start with +";
228 return -1;
229 }
230
231 data += 1;
232
233 if (state == FILTER_FILLING) {
234 int dl;
235 unsigned char *d = rb_base64_decode((unsigned char *)data, strlen(data), &dl);
236 if (!d) {
237 if (error) *error = "invalid data";
238 return -1;
239 }
240 if (filter_data_len + dl > 10000000ul) {
241 if (error) *error = "data over size limit";
242 rb_free(d);
243 return -1;
244 }
245 filter_data = rb_realloc(filter_data, filter_data_len + dl);
246 memcpy(filter_data + filter_data_len, d, dl);
247 rb_free(d);
248 filter_data_len += dl;
249 } else {
250 if (error) *error = "send \"new\" first";
251 return -1;
252 }
253 return 0;
254 }
255
256 /* /SETFILTER [server-mask] <check> { NEW | APPLY | <data> }
257 * <check> must be the same for the entirety of a new...data...apply run,
258 * and exists just to ensure runs don't mix
259 * NEW prepares a buffer to receive a hyperscan database
260 * <data> is base64 encoded chunks of hyperscan database, which are decoded
261 * and appended to the buffer
262 * APPLY deserialises the buffer and sets the resulting hyperscan database
263 * as the one to use for filtering */
264 static void
265 mo_setfilter(struct MsgBuf *msgbuf, struct Client *client_p, struct Client *source_p, int parc, const char **parv)
266 {
267 int for_me = 0;
268 const char *check;
269 const char *data;
270 if (!IsOperAdmin(source_p)) {
271 sendto_one(source_p, form_str(ERR_NOPRIVS), me.name, source_p->name, "admin");
272 return;
273 }
274 if (parc == 4) {
275 check = parv[2];
276 data = parv[3];
277 if(match(parv[1], me.name)) {
278 for_me = 1;
279 }
280 sendto_match_servs(source_p, parv[1],
281 CAP_ENCAP, NOCAPS,
282 "ENCAP %s SETFILTER %s :%s", parv[1], check, data);
283 } else if (parc == 3) {
284 check = parv[1];
285 data = parv[2];
286 for_me = 1;
287 } else {
288 sendto_one_notice(source_p, ":SETFILTER needs 2 or 3 params, have %d", parc - 1);
289 return;
290 }
291 if (for_me) {
292 const char *error;
293 int r = setfilter(check, data, &error);
294 if (r) {
295 sendto_one_notice(source_p, ":SETFILTER failed: %s", error);
296 } else {
297 sendto_one_notice(source_p, ":SETFILTER ok");
298 }
299 }
300 }
301
302 static void
303 me_setfilter(struct MsgBuf *msgbuf, struct Client *client_p, struct Client *source_p, int parc, const char **parv)
304 {
305 if(!IsPerson(source_p))
306 return;
307
308 const char *error;
309 int r = setfilter(parv[1], parv[2], &error);
310 if (r) {
311 sendto_one_notice(source_p, ":SETFILTER failed: %s", error);
312 }
313
314 return;
315 }
316
317 /* will be called for every match
318 * hyperscan provides us one piece of information about the expression
319 * matched, an integer ID. we're co-opting the lowest 3 bits of this
320 * as a flag set. conveniently, this means all we really need to do
321 * here is or the IDs together. */
322 int match_callback(unsigned id,
323 unsigned long long from,
324 unsigned long long to,
325 unsigned flags,
326 void *context_)
327 {
328 unsigned *context = context_;
329 *context |= id;
330 return 0;
331 }
332
333 static char check_buffer[2000];
334 static char clean_buffer[BUFSIZE];
335
336 unsigned match_message(const char *prefix,
337 struct Client *source,
338 const char *command,
339 const char *target,
340 const char *msg)
341 {
342 unsigned state = 0;
343 if (!filter_enable)
344 return 0;
345 if (!filter_db)
346 return 0;
347 if (!command)
348 return 0;
349 snprintf(check_buffer, sizeof check_buffer, "%s:%s!%s@%s#%c %s %s :%s",
350 prefix,
351 #if FILTER_NICK
352 source->name,
353 #else
354 "*",
355 #endif
356 #if FILTER_USER
357 source->username,
358 #else
359 "*",
360 #endif
361 #if FILTER_HOST
362 source->host,
363 #else
364 "*",
365 #endif
366 source->user && source->user->suser[0] != '\0' ? '1' : '0',
367 command, target, msg);
368 hs_error_t r = hs_scan(filter_db, check_buffer, strlen(check_buffer), 0, filter_scratch, match_callback, &state);
369 if (r != HS_SUCCESS && r != HS_SCAN_TERMINATED)
370 return 0;
371 return state;
372 }
373
374 void
375 filter_msg_user(void *data_)
376 {
377 hook_data_privmsg_user *data = data_;
378 struct Client *s = data->source_p;
379 /* we only need to filter once */
380 if (!MyClient(s)) {
381 return;
382 }
383 /* opers are immune to checking, for obvious reasons
384 * anything sent to an oper is also immune, because that should make it
385 * less impossible to deal with reports. */
386 if (IsOper(s) || IsOper(data->target_p)) {
387 return;
388 }
389 if (data->target_p->umodes & filter_umode) {
390 return;
391 }
392 char *text = strcpy(clean_buffer, data->text);
393 strip_colour(text);
394 strip_unprintable(text);
395 unsigned r = match_message("0", s, cmdname[data->msgtype], "0", data->text) |
396 match_message("1", s, cmdname[data->msgtype], "0", text);
397 if (r & ACT_DROP) {
398 if (data->msgtype == MESSAGE_TYPE_PRIVMSG) {
399 sendto_one_numeric(s, ERR_CANNOTSENDTOCHAN,
400 form_str(ERR_CANNOTSENDTOCHAN),
401 data->target_p->name);
402 }
403 data->approved = 1;
404 }
405 if (r & ACT_ALARM) {
406 sendto_realops_snomask(SNO_GENERAL, L_ALL | L_NETWIDE,
407 "FILTER: %s!%s@%s [%s]",
408 s->name, s->username, s->host, s->sockhost);
409 }
410 if (r & ACT_KILL) {
411 data->approved = 1;
412 exit_client(NULL, s, s, FILTER_EXIT_MSG);
413 }
414 }
415
416 void
417 filter_msg_channel(void *data_)
418 {
419 hook_data_privmsg_channel *data = data_;
420 struct Client *s = data->source_p;
421 /* we only need to filter once */
422 if (!MyClient(s)) {
423 return;
424 }
425 /* just normal oper immunity for channels. i'd like to have a mode that
426 * disables the filter per-channel, but that's for the future */
427 if (IsOper(s)) {
428 return;
429 }
430 if (data->chptr->mode.mode & filter_chmode) {
431 return;
432 }
433 char *text = strcpy(clean_buffer, data->text);
434 strip_colour(text);
435 strip_unprintable(text);
436 unsigned r = match_message("0", s, cmdname[data->msgtype], data->chptr->chname, data->text) |
437 match_message("1", s, cmdname[data->msgtype], data->chptr->chname, text);
438 if (r & ACT_DROP) {
439 if (data->msgtype == MESSAGE_TYPE_PRIVMSG) {
440 sendto_one_numeric(s, ERR_CANNOTSENDTOCHAN,
441 form_str(ERR_CANNOTSENDTOCHAN),
442 data->chptr->chname);
443 }
444 data->approved = 1;
445 }
446 if (r & ACT_ALARM) {
447 sendto_realops_snomask(SNO_GENERAL, L_ALL | L_NETWIDE,
448 "FILTER: %s!%s@%s [%s]",
449 s->name, s->username, s->host, s->sockhost);
450 }
451 if (r & ACT_KILL) {
452 data->approved = 1;
453 exit_client(NULL, s, s, FILTER_EXIT_MSG);
454 }
455 }
456
457 void
458 on_client_exit(void *data_)
459 {
460 /* If we see a netsplit, abort the current FILTER_FILLING attempt */
461 hook_data_client_exit *data = data_;
462
463 if (!IsServer(data->target)) return;
464
465 if (state == FILTER_FILLING) {
466 state = filter_db ? FILTER_LOADED : FILTER_EMPTY;
467 }
468 }