]> jfr.im git - irc/freenode/syn.git/blob - masks.c
add licence info
[irc/freenode/syn.git] / masks.c
1 /*
2 * syn: a utility bot to manage IRC network access
3 * Copyright (C) 2009-2016 Stephen Bennett
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU Affero General Public License as
7 * published by the Free Software Foundation, either version 3 of the
8 * License, or (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU Affero General Public License for more details.
14 *
15 * You should have received a copy of the GNU Affero General Public License
16 * along with this program. If not, see <https://www.gnu.org/licenses/>.
17 */
18
19
20 #include "atheme.h"
21
22 #include "syn.h"
23
24 static void masks_newuser(hook_user_nick_t *data);
25
26 static void syn_cmd_addmask(sourceinfo_t *si, int parc, char **parv);
27 static void syn_cmd_delmask(sourceinfo_t *si, int parc, char **parv);
28 static void syn_cmd_setmask(sourceinfo_t *si, int parc, char **parv);
29 static void syn_cmd_listmask(sourceinfo_t *si, int parc, char **parv);
30
31 command_t syn_addmask = { "ADDMASK", N_("Adds a lethal, suspicious or exempt mask"), "syn:general", 1, syn_cmd_addmask, { .path = "syn/addmask" } };
32 command_t syn_delmask = { "DELMASK", N_("Removes a lethal, suspicious or exempt mask"), "syn:general", 1, syn_cmd_delmask, { .path = "syn/delmask" } };
33 command_t syn_setmask = { "SETMASK", N_("Modifies settings for a lethal, suspicious or exempt mask"), "syn:general", 1, syn_cmd_setmask, { .path = "syn/setmask" } };
34 command_t syn_listmask = { "LISTMASK", N_("Displays configured mask lists"), "syn:general", 1, syn_cmd_listmask, { .path = "syn/listmask" } };
35
36 static unsigned int lethal_mask_duration = 3600*24;
37 static char *lethal_mask_message = NULL;
38
39 static mowgli_eventloop_timer_t *expire_masks_timer;
40
41 typedef enum
42 {
43 mask_exempt,
44 mask_suspicious,
45 mask_lethal,
46 mask_unknown
47 } mask_type;
48
49 typedef struct
50 {
51 char *regex;
52 atheme_regex_t *re;
53 int reflags;
54
55 mask_type type;
56
57 time_t expires;
58 time_t added;
59 time_t last_match;
60 char setter[NICKLEN*2+2];
61 } mask_t;
62
63 mowgli_list_t masks;
64
65 struct {
66 const char *s;
67 mask_type t;
68 } mask_string_map[] = {
69 { "exempt", mask_exempt },
70 { "suspicious", mask_suspicious },
71 { "lethal", mask_lethal },
72 { NULL, 0 }
73 };
74
75 const char *string_from_mask_type(mask_type t)
76 {
77 for (int i=0; mask_string_map[i].s != NULL; ++i)
78 if (mask_string_map[i].t == t)
79 return mask_string_map[i].s;
80 return NULL;
81 }
82
83 mask_type mask_type_from_string(const char *s)
84 {
85 for (int i=0; mask_string_map[i].s != NULL; ++i)
86 if (0 == strcasecmp(mask_string_map[i].s, s))
87 return mask_string_map[i].t;
88 return mask_unknown;
89 }
90
91 static void check_expiry(void *v)
92 {
93 mowgli_node_t *n, *tn;
94 MOWGLI_LIST_FOREACH_SAFE(n, tn, masks.head)
95 {
96 mask_t *m = n->data;
97
98 if (m->expires == 0)
99 continue;
100 if (m->expires > CURRTIME)
101 continue;
102
103 syn_report("Expiring %s mask \2%s\2", string_from_mask_type(m->type), m->regex);
104 regex_destroy(m->re);
105 free(m->regex);
106 free(m);
107 mowgli_node_delete(n, &masks);
108 }
109 }
110
111 static void save_maskdb()
112 {
113 mowgli_node_t *n;
114 FILE *f = fopen(DATADIR "/masks.db", "w");
115 if (!f)
116 {
117 slog(LG_ERROR, "Couldn't open masks.db for writing: %s", strerror(errno));
118 return;
119 }
120
121 MOWGLI_LIST_FOREACH(n, masks.head)
122 {
123 mask_t *m = n->data;
124
125 fprintf(f, "/%s/%s %d %s %lu %lu %lu\n",
126 m->regex, m->reflags & AREGEX_ICASE ? "i" : "",
127 m->type, m->setter, m->added, m->expires, m->last_match);
128 }
129 fclose(f);
130 }
131
132 static void load_maskdb()
133 {
134 FILE *f = fopen(DATADIR "/masks.db", "r");
135 if (!f)
136 {
137 slog(LG_DEBUG, "Couldn't open masks db for reading: %s", strerror(errno));
138 return;
139 }
140
141 char line[BUFSIZE*2];
142 while(fgets(line, sizeof(line), f))
143 {
144 char *args = line;
145 int flags = 0;
146 char *regex = regex_extract(args, &args, &flags);
147
148 atheme_regex_t *re= regex_create(regex, flags);
149
150 if (!re || !regex)
151 {
152 slog(LG_DEBUG, "Invalid entry %s in masks db", line);
153 continue;
154 }
155
156 char setter[BUFSIZE*2];
157 int type;
158 time_t added, expires, last_match = 0;
159
160 sscanf(args, "%d %s %lu %lu %lu", &type, setter, &added, &expires, &last_match);
161
162 mask_t *mask = malloc(sizeof(mask_t));
163 mask->regex = sstrdup(regex);
164 mask->reflags = flags;
165 mask->re = re;
166 strncpy(mask->setter, setter, sizeof(mask->setter));
167 mask->added = added;
168 mask->expires = expires;
169 mask->last_match = last_match;
170 mask->type = type;
171
172 mowgli_node_add(mask, mowgli_node_create(), &masks);
173 }
174
175 fclose(f);
176 }
177
178 static void mod_init(module_t *m)
179 {
180 use_syn_main_symbols(m);
181 use_syn_util_symbols(m);
182 use_syn_kline_symbols(m);
183
184 add_uint_conf_item("lethalmask_duration", &syn->conf_table, 0, &lethal_mask_duration, 0, (unsigned int)-1, 3600*24);
185 add_dupstr_conf_item("lethalmask_message", &syn->conf_table, 0, &lethal_mask_message, "Banned");
186
187 service_named_bind_command("syn", &syn_addmask);
188 service_named_bind_command("syn", &syn_delmask);
189 service_named_bind_command("syn", &syn_setmask);
190 service_named_bind_command("syn", &syn_listmask);
191
192 hook_add_event("user_nickchange");
193 hook_add_user_nickchange(masks_newuser);
194 hook_add_event("user_add");
195 hook_add_user_add(masks_newuser);
196
197 expire_masks_timer = mowgli_timer_add(base_eventloop, "masks_check_expiry", check_expiry, NULL, 60);
198
199 load_maskdb();
200 }
201
202 static void mod_deinit(module_unload_intent_t intent)
203 {
204 save_maskdb();
205
206 service_named_unbind_command("syn", &syn_addmask);
207 service_named_unbind_command("syn", &syn_delmask);
208 service_named_unbind_command("syn", &syn_setmask);
209 service_named_unbind_command("syn", &syn_listmask);
210
211 del_conf_item("lethalmask_duration", &syn->conf_table);
212 del_conf_item("lethalmask_message", &syn->conf_table);
213
214 hook_del_user_add(masks_newuser);
215 hook_del_user_nickchange(masks_newuser);
216
217 mowgli_timer_destroy(base_eventloop, expire_masks_timer);
218 }
219
220 void masks_newuser(hook_user_nick_t *data)
221 {
222 user_t *u = data->u;
223
224 /* If the user has already been killed, don't try to do anything */
225 if (!u)
226 return;
227
228 char nuh[NICKLEN+USERLEN+HOSTLEN+GECOSLEN];
229 snprintf(nuh, sizeof(nuh), "%s!%s@%s %s", u->nick, u->user, u->host, u->gecos);
230
231 int blocked = 0, exempt = 0;
232 char *suspicious_regex = NULL, *blocked_regex = NULL;
233
234 mowgli_node_t *n;
235 mask_t *m;
236 MOWGLI_LIST_FOREACH(n, masks.head)
237 {
238 m = n->data;
239
240 if (! regex_match(m->re, nuh))
241 continue;
242
243 m->last_match = CURRTIME;
244
245 switch (m->type)
246 {
247 case mask_exempt:
248 exempt = 1;
249 break;
250 case mask_suspicious:
251 suspicious_regex = m->regex;
252 break;
253 case mask_lethal:
254 blocked = 1;
255 blocked_regex = m->regex;
256 break;
257 case mask_unknown:
258 break;
259 }
260 }
261
262 if (exempt == 1)
263 return;
264
265 if (blocked == 1)
266 {
267 syn_report("Killing client %s(%s@%s) due to lethal mask %s",
268 u->nick, u->user, u->host, blocked_regex);
269 syn_kill_or_kline(u, lethal_mask_duration, lethal_mask_message);
270 data->u = NULL;
271 return;
272 }
273
274 if (suspicious_regex)
275 {
276 syn_report("Client %s(%s@%s) matches suspicious mask %s",
277 u->nick, u->user, u->host, suspicious_regex);
278 return;
279 }
280 }
281
282 void syn_cmd_addmask(sourceinfo_t *si, int parc, char **parv)
283 {
284 char *pattern;
285 char *stype;
286 mask_type type;
287 mask_t *newmask;
288 int flags;
289 time_t duration = 0;
290
291 char *args = parv[0];
292
293 if (args == NULL)
294 {
295 command_fail(si, fault_needmoreparams, STR_INSUFFICIENT_PARAMS, "ADDMASK");
296 command_fail(si, fault_needmoreparams, "Syntax: ADDMASK /<regex>/[i] <type>");
297 return;
298 }
299
300 pattern = regex_extract(args, &args, &flags);
301 if (pattern == NULL)
302 {
303 command_fail(si, fault_badparams, STR_INVALID_PARAMS, "ADDMASK");
304 command_fail(si, fault_badparams, "Syntax: ADDMASK /<regex>/[i] <type>");
305 return;
306 }
307
308 stype = strtok(args, " ");
309
310 if (!stype || *stype == '\0')
311 {
312 command_fail(si, fault_needmoreparams, STR_INSUFFICIENT_PARAMS, "ADDMASK");
313 command_fail(si, fault_needmoreparams, "Syntax: ADDMASK /<regex>/[i] <type>");
314 return;
315 }
316
317 type = mask_type_from_string(stype);
318 if (type == mask_unknown)
319 {
320 command_fail(si, fault_badparams, "Invalid mask type \2%s\2.", stype);
321 return;
322 }
323
324 char *sduration = strtok(NULL, " ");
325 if (sduration && *sduration == '~')
326 {
327 duration = syn_parse_duration(++sduration);
328 }
329
330 mowgli_node_t *n;
331 MOWGLI_LIST_FOREACH(n, masks.head)
332 {
333 mask_t *m = n->data;
334
335 if (0 == strcmp(m->regex, pattern))
336 {
337 command_fail(si, fault_nochange, "\2%s\2 was already added (%s); not re-adding", pattern, string_from_mask_type(m->type));
338 return;
339 }
340 }
341
342 atheme_regex_t *regex = regex_create(pattern, flags);
343 if (regex == NULL)
344 {
345 command_fail(si, fault_badparams, "The provided regex \2%s\2 is invalid.", pattern);
346 return;
347 }
348
349 newmask = malloc(sizeof(mask_t));
350 newmask->regex = sstrdup(pattern);
351 newmask->reflags = flags;
352 newmask->re = regex;
353 newmask->type = type;
354 if (duration > 0)
355 newmask->expires = CURRTIME + 60*duration;
356 else
357 newmask->expires = 0;
358
359 newmask->added = CURRTIME;
360 newmask->last_match = 0;
361 strncpy(newmask->setter, get_oper_name(si), sizeof(newmask->setter));
362
363 mowgli_node_add(newmask, mowgli_node_create(), &masks);
364
365 syn_report("\002ADDMASK\002 /%s/%s (%s) by %s, expires %s",
366 pattern, (flags & AREGEX_ICASE) ? "i" : "", stype, get_oper_name(si), syn_format_expiry(newmask->expires));
367 command_success_nodata(si, "Added \2%s\2 to %s mask list, expiring %s.",
368 pattern, stype, syn_format_expiry(newmask->expires));
369
370 save_maskdb();
371 }
372
373 void syn_cmd_delmask(sourceinfo_t *si, int parc, char **parv)
374 {
375 char *args = parv[0];
376
377 if (!args)
378 {
379 command_fail(si, fault_needmoreparams, STR_INSUFFICIENT_PARAMS, "DELMASK");
380 command_fail(si, fault_needmoreparams, "Syntax: DELMASK /<regex>/");
381 return;
382 }
383
384 int flags = 0;
385 char *pattern = regex_extract(args, &args, &flags);
386
387 if (!pattern)
388 {
389 command_fail(si, fault_badparams, STR_INVALID_PARAMS, "DELMASK");
390 command_fail(si, fault_needmoreparams, "Syntax: DELMASK /<regex>/");
391 return;
392 }
393
394 mowgli_node_t *n, *tn;
395 MOWGLI_LIST_FOREACH_SAFE(n, tn, masks.head)
396 {
397 mask_t *m = n->data;
398 if (0 == strcmp(pattern, m->regex))
399 {
400 syn_report("\002DELMASK\002 /%s/%s (%s) by %s", pattern, (m->reflags & AREGEX_ICASE) ? "i" : "", string_from_mask_type(m->type), get_oper_name(si));
401 command_success_nodata(si, "Removing \2%s\2 from %s mask list", pattern, string_from_mask_type(m->type));
402 regex_destroy(m->re);
403 free(m->regex);
404 free(m);
405 mowgli_node_delete(n, &masks);
406
407 save_maskdb();
408
409 return;
410 }
411 }
412
413 command_fail(si, fault_nochange, "\2%s\2 was not found in any mask list", pattern);
414 }
415
416 void syn_cmd_setmask(sourceinfo_t *si, int parc, char **parv)
417 {
418 char *args = parv[0];
419
420 if (!args)
421 {
422 command_fail(si, fault_needmoreparams, STR_INSUFFICIENT_PARAMS, "SETMASK");
423 command_fail(si, fault_needmoreparams, "Syntax: SETMASK /<regex>/ <type|~expiry>");
424 return;
425 }
426
427 int flags = 0;
428 char *pattern = regex_extract(args, &args, &flags);
429
430 if (!pattern)
431 {
432 command_fail(si, fault_badparams, STR_INVALID_PARAMS, "SETMASK");
433 command_fail(si, fault_needmoreparams, "Syntax: SETMASK /<regex>/ <type|~expiry>");
434 return;
435 }
436
437 mowgli_node_t *n, *tn;
438 mask_t *m;
439 MOWGLI_LIST_FOREACH_SAFE(n, tn, masks.head)
440 {
441 m = n->data;
442 if (0 == strcmp(pattern, m->regex))
443 break;
444 m = NULL;
445 }
446
447 if (!m)
448 {
449 command_fail(si, fault_nochange, "\2%s\2 was not found in any mask list", pattern);
450 return;
451 }
452
453 char *nextarg = strtok(args, " ");
454
455 if (!nextarg)
456 {
457 command_fail(si, fault_needmoreparams, STR_INSUFFICIENT_PARAMS, "SETMASK");
458 command_fail(si, fault_needmoreparams, "Syntax: SETMASK /<regex>/ <type|~expiry>");
459 return;
460 }
461
462 mask_type t = mask_type_from_string(nextarg);
463 if (t != mask_unknown)
464 {
465 m->type = t;
466 syn_report("\002SETMASK\002 /%s/%s type->%s by %s", pattern, (m->reflags & AREGEX_ICASE) ? "i" : "", nextarg, get_oper_name(si));
467 command_success_nodata(si, "Changed type of mask \2%s\2 to %s", pattern, nextarg);
468
469 save_maskdb();
470
471 return;
472 }
473
474 if (*nextarg == '~')
475 {
476 time_t duration = syn_parse_duration(++nextarg);
477 if (duration > 0)
478 {
479 m->expires = CURRTIME + duration * 60;
480 syn_report("\002SETMASK\002 /%s/%s duration->%d by %s", pattern, (m->reflags & AREGEX_ICASE) ? "i" : "", duration, get_oper_name(si));
481 command_success_nodata(si, "Changed expiry of mask \2%s\2 to %ld minutes", pattern, duration);
482 }
483 else
484 {
485 m->expires = 0;
486 syn_report("\002SETMASK\002 /%s/%s expiry->off by %s", pattern, (m->reflags & AREGEX_ICASE) ? "i" : "", get_oper_name(si));
487 command_success_nodata(si, "Expiry disabled for mask \2%s\2.", pattern);
488 }
489
490 save_maskdb();
491
492 return;
493 }
494
495 command_fail(si, fault_badparams, STR_INVALID_PARAMS, "SETMASK");
496 command_fail(si, fault_badparams, "Syntax: SETMASK /<regex>/ <type|~expiry>");
497 }
498
499 void syn_cmd_listmask(sourceinfo_t *si, int parc, char **parv)
500 {
501 mask_type t = mask_unknown;
502
503 if (parc > 0)
504 {
505 t = mask_type_from_string(parv[0]);
506 }
507
508 int count = 0;
509
510 mowgli_node_t *n;
511 MOWGLI_LIST_FOREACH(n, masks.head)
512 {
513 mask_t *m = n->data;
514
515 if (t != mask_unknown && t != m->type)
516 continue;
517
518 char added[BUFSIZE], expires[BUFSIZE], last_match[BUFSIZE];
519 strncpy(added, syn_format_expiry(m->added), BUFSIZE);
520 strncpy(expires, syn_format_expiry(m->expires), BUFSIZE);
521 strncpy(last_match, syn_format_expiry(m->last_match), BUFSIZE);
522 command_success_nodata(si, "\2/%s/%s\2 (%s), set by %s on %s, expires %s, last matched %s",
523 m->regex, (m->reflags & AREGEX_ICASE) ? "i" : "", string_from_mask_type(m->type), m->setter,
524 added, expires, last_match);
525
526 ++count;
527 }
528
529 command_success_nodata(si, "%d masks found", count);
530 }
531
532 DECLARE_MODULE_V1
533 (
534 "syn/masks", false, mod_init, mod_deinit,
535 "$Revision$",
536 "Stephen Bennett <stephen -at- freenode.net>"
537 );