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