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