]> jfr.im git - solanum.git/blame - extensions/filter.c
filter: send the failure response only on PRIVMSG
[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"
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
81e41406
EK
46#define FILTER_NICK 0
47#define FILTER_USER 0
48#define FILTER_HOST 0
49
a2d9c494
EK
50static void filter_msg_user(void *data);
51static void filter_msg_channel(void *data);
52static void on_client_exit(void *data);
53
54static void mo_setfilter(struct MsgBuf *, struct Client *, struct Client *, int, const char **);
55static void me_setfilter(struct MsgBuf *, struct Client *, struct Client *, int, const char **);
56
57static char *filter_data = NULL;
58static size_t filter_data_len = 0;
59static hs_database_t *filter_db;
60static hs_scratch_t *filter_scratch;
61
62static int filter_enable = 1;
63
64static const char *cmdname[MESSAGE_TYPE_COUNT] = {
65 [MESSAGE_TYPE_PRIVMSG] = "PRIVMSG",
66 [MESSAGE_TYPE_NOTICE] = "NOTICE",
67};
68
69enum 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
79static enum filter_state state = FILTER_EMPTY;
80static char check_str[21] = "";
81
82mapi_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
90struct 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
95static void
96moddeinit(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
107mapi_clist_av1 filter_clist[] = { &setfilter_msgtab, NULL };
108
4950a943 109DECLARE_MODULE_AV1(filter, NULL, moddeinit, filter_clist, NULL, filter_hfnlist, "0.3");
a2d9c494
EK
110
111static int
112setfilter(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 */
210static void
211mo_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
248static void
249me_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. */
268int 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
744ac308
EK
279static char check_buffer[2000];
280static char clean_buffer[BUFSIZE];
a2d9c494 281
744ac308
EK
282unsigned match_message(const char *prefix,
283 struct Client *source,
284 const char *command,
285 const char *target,
286 const char *msg)
a2d9c494
EK
287{
288 unsigned state = 0;
289 if (!filter_enable)
290 return 0;
291 if (!filter_db)
292 return 0;
744ac308
EK
293 snprintf(check_buffer, sizeof check_buffer, "%s:%s!%s@%s#%c %s %s :%s",
294 prefix,
295#if FILTER_NICK
296 source->name,
297#else
298 "*",
299#endif
300#if FILTER_USER
301 source->username,
302#else
303 "*",
304#endif
305#if FILTER_HOST
306 source->host,
307#else
308 "*",
309#endif
310 source->user && source->user->suser[0] != '\0' ? '1' : '0',
311 command, target,
312 msg);
313 hs_error_t r = hs_scan(filter_db, check_buffer, strlen(check_buffer), 0, filter_scratch, match_callback, &state);
a2d9c494
EK
314 if (r != HS_SUCCESS && r != HS_SCAN_TERMINATED)
315 return 0;
316 return state;
317}
318
a2d9c494
EK
319void
320filter_msg_user(void *data_)
321{
322 hook_data_privmsg_user *data = data_;
323 struct Client *s = data->source_p;
86ee00db
EK
324 /* we only need to filter once */
325 if (!MyClient(s)) {
326 return;
327 }
a2d9c494
EK
328 /* opers are immune to checking, for obvious reasons
329 * anything sent to an oper is also immune, because that should make it
330 * less impossible to deal with reports. */
331 if (IsOper(s) || IsOper(data->target_p)) {
332 return;
333 }
f982238e 334 char *text = strcpy(clean_buffer, data->text);
a2d9c494
EK
335 strip_colour(text);
336 strip_unprintable(text);
744ac308
EK
337 unsigned r = match_message("0", s, cmdname[data->msgtype], "0", data->text) |
338 match_message("1", s, cmdname[data->msgtype], "0", text);
7bb7f899 339 if (r & ACT_DROP) {
d928bc7a
EK
340 if (data->msgtype == MESSAGE_TYPE_PRIVMSG) {
341 sendto_one_numeric(s, ERR_CANNOTSENDTOCHAN,
342 form_str(ERR_CANNOTSENDTOCHAN),
343 data->target_p->name);
344 }
a2d9c494
EK
345 data->approved = 1;
346 }
86ee00db 347 if (r & ACT_ALARM) {
6a14bf78 348 sendto_realops_snomask(SNO_GENERAL, L_ALL | L_NETWIDE,
a2d9c494
EK
349 "FILTER: %s!%s@%s [%s]",
350 s->name, s->username, s->host, s->sockhost);
351 }
6a14bf78 352 if (r & ACT_KILL) {
7bb7f899 353 data->approved = 1;
6a14bf78
EK
354 exit_client(NULL, s, s, "Excess flood");
355 }
a2d9c494
EK
356}
357
358void
359filter_msg_channel(void *data_)
360{
361 hook_data_privmsg_channel *data = data_;
362 struct Client *s = data->source_p;
86ee00db
EK
363 /* we only need to filter once */
364 if (!MyClient(s)) {
365 return;
366 }
a2d9c494
EK
367 /* just normal oper immunity for channels. i'd like to have a mode that
368 * disables the filter per-channel, but that's for the future */
369 if (IsOper(s)) {
370 return;
371 }
f982238e 372 char *text = strcpy(clean_buffer, data->text);
a2d9c494
EK
373 strip_colour(text);
374 strip_unprintable(text);
744ac308
EK
375 unsigned r = match_message("0", s, cmdname[data->msgtype], data->chptr->chname, data->text) |
376 match_message("1", s, cmdname[data->msgtype], data->chptr->chname, text);
7bb7f899 377 if (r & ACT_DROP) {
d928bc7a
EK
378 if (data->msgtype == MESSAGE_TYPE_PRIVMSG) {
379 sendto_one_numeric(s, ERR_CANNOTSENDTOCHAN,
380 form_str(ERR_CANNOTSENDTOCHAN),
381 data->chptr->chname);
382 }
a2d9c494
EK
383 data->approved = 1;
384 }
86ee00db 385 if (r & ACT_ALARM) {
6a14bf78 386 sendto_realops_snomask(SNO_GENERAL, L_ALL | L_NETWIDE,
a2d9c494
EK
387 "FILTER: %s!%s@%s [%s]",
388 s->name, s->username, s->host, s->sockhost);
389 }
6a14bf78 390 if (r & ACT_KILL) {
7bb7f899 391 data->approved = 1;
6a14bf78
EK
392 exit_client(NULL, s, s, "Excess flood");
393 }
a2d9c494
EK
394}
395
396void
397on_client_exit(void *data_)
398{
399 /* If we see a netsplit, abort the current FILTER_FILLING attempt */
400 hook_data_client_exit *data = data_;
401
402 if (!IsServer(data->target)) return;
403
404 if (state == FILTER_FILLING) {
405 state = filter_db ? FILTER_LOADED : FILTER_EMPTY;
406 }
407}
408