]> jfr.im git - solanum.git/blame - extensions/filter.c
Merge pull request #321 from edk0/hook-priorities
[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);
58static void on_client_exit(void *data);
59
60static void mo_setfilter(struct MsgBuf *, struct Client *, struct Client *, int, const char **);
61static void me_setfilter(struct MsgBuf *, struct Client *, struct Client *, int, const char **);
62
63static char *filter_data = NULL;
64static size_t filter_data_len = 0;
65static hs_database_t *filter_db;
66static hs_scratch_t *filter_scratch;
67
68static int filter_enable = 1;
69
70static const char *cmdname[MESSAGE_TYPE_COUNT] = {
71 [MESSAGE_TYPE_PRIVMSG] = "PRIVMSG",
72 [MESSAGE_TYPE_NOTICE] = "NOTICE",
e9408bc4 73 [MESSAGE_TYPE_PART] = "PART",
a2d9c494
EK
74};
75
76enum 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
86static enum filter_state state = FILTER_EMPTY;
87static char check_str[21] = "";
88
8692240f
EK
89static unsigned filter_chmode, filter_umode;
90
a2d9c494
EK
91mapi_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
99struct 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
eb06afc3 104static int
8692240f
EK
105modinit(void)
106{
107 filter_umode = user_modes['u'] = find_umode_slot();
108 construct_umodebuf();
109 filter_chmode = cflag_add('u', chm_simple);
eb06afc3 110 return 0;
8692240f
EK
111}
112
a2d9c494
EK
113static void
114moddeinit(void)
115{
8692240f
EK
116 if (filter_umode) {
117 user_modes['u'] = 0;
118 construct_umodebuf();
119 }
120 if (filter_chmode)
121 cflag_orphan('u');
a2d9c494
EK
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
131mapi_clist_av1 filter_clist[] = { &setfilter_msgtab, NULL };
132
a8f402e9 133DECLARE_MODULE_AV2(filter, modinit, moddeinit, filter_clist, NULL, filter_hfnlist, NULL, "0.4", filter_desc);
a2d9c494
EK
134
135static int
136setfilter(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
344af24c
EK
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
a2d9c494
EK
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";
09784400 210 hs_free_database(db);
a2d9c494
EK
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
dc141aad
EK
226 if (*data != '+') {
227 if (error) *error = "unknown command or data doesn't start with +";
228 return -1;
229 }
230
231 data += 1;
232
a2d9c494
EK
233 if (state == FILTER_FILLING) {
234 int dl;
0cbb1ba9 235 unsigned char *d = rb_base64_decode((unsigned char *)data, strlen(data), &dl);
a2d9c494
EK
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 */
264static void
265mo_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
302static void
303me_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. */
322int 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
744ac308
EK
333static char check_buffer[2000];
334static char clean_buffer[BUFSIZE];
a2d9c494 335
744ac308
EK
336unsigned match_message(const char *prefix,
337 struct Client *source,
338 const char *command,
339 const char *target,
340 const char *msg)
a2d9c494
EK
341{
342 unsigned state = 0;
343 if (!filter_enable)
344 return 0;
345 if (!filter_db)
346 return 0;
e9408bc4
EK
347 if (!command)
348 return 0;
744ac308
EK
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',
e9408bc4 367 command, target, msg);
744ac308 368 hs_error_t r = hs_scan(filter_db, check_buffer, strlen(check_buffer), 0, filter_scratch, match_callback, &state);
a2d9c494
EK
369 if (r != HS_SUCCESS && r != HS_SCAN_TERMINATED)
370 return 0;
371 return state;
372}
373
a2d9c494
EK
374void
375filter_msg_user(void *data_)
376{
377 hook_data_privmsg_user *data = data_;
378 struct Client *s = data->source_p;
86ee00db
EK
379 /* we only need to filter once */
380 if (!MyClient(s)) {
381 return;
382 }
a2d9c494
EK
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 }
8692240f
EK
389 if (data->target_p->umodes & filter_umode) {
390 return;
391 }
f982238e 392 char *text = strcpy(clean_buffer, data->text);
a2d9c494
EK
393 strip_colour(text);
394 strip_unprintable(text);
744ac308
EK
395 unsigned r = match_message("0", s, cmdname[data->msgtype], "0", data->text) |
396 match_message("1", s, cmdname[data->msgtype], "0", text);
7bb7f899 397 if (r & ACT_DROP) {
d928bc7a
EK
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 }
a2d9c494
EK
403 data->approved = 1;
404 }
86ee00db 405 if (r & ACT_ALARM) {
6a14bf78 406 sendto_realops_snomask(SNO_GENERAL, L_ALL | L_NETWIDE,
a2d9c494
EK
407 "FILTER: %s!%s@%s [%s]",
408 s->name, s->username, s->host, s->sockhost);
409 }
6a14bf78 410 if (r & ACT_KILL) {
7bb7f899 411 data->approved = 1;
649bda6d 412 exit_client(NULL, s, s, FILTER_EXIT_MSG);
6a14bf78 413 }
a2d9c494
EK
414}
415
416void
417filter_msg_channel(void *data_)
418{
419 hook_data_privmsg_channel *data = data_;
420 struct Client *s = data->source_p;
86ee00db
EK
421 /* we only need to filter once */
422 if (!MyClient(s)) {
423 return;
424 }
a2d9c494
EK
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 }
8692240f
EK
430 if (data->chptr->mode.mode & filter_chmode) {
431 return;
432 }
f982238e 433 char *text = strcpy(clean_buffer, data->text);
a2d9c494
EK
434 strip_colour(text);
435 strip_unprintable(text);
744ac308
EK
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);
7bb7f899 438 if (r & ACT_DROP) {
d928bc7a
EK
439 if (data->msgtype == MESSAGE_TYPE_PRIVMSG) {
440 sendto_one_numeric(s, ERR_CANNOTSENDTOCHAN,
441 form_str(ERR_CANNOTSENDTOCHAN),
442 data->chptr->chname);
443 }
a2d9c494
EK
444 data->approved = 1;
445 }
86ee00db 446 if (r & ACT_ALARM) {
6a14bf78 447 sendto_realops_snomask(SNO_GENERAL, L_ALL | L_NETWIDE,
a2d9c494
EK
448 "FILTER: %s!%s@%s [%s]",
449 s->name, s->username, s->host, s->sockhost);
450 }
6a14bf78 451 if (r & ACT_KILL) {
7bb7f899 452 data->approved = 1;
649bda6d 453 exit_client(NULL, s, s, FILTER_EXIT_MSG);
6a14bf78 454 }
a2d9c494
EK
455}
456
457void
458on_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}