diff --git a/doc/reference.conf b/doc/reference.conf index f946871f..5dcbdd0f 100644 --- a/doc/reference.conf +++ b/doc/reference.conf @@ -1535,6 +1535,23 @@ general { * requires extensions/drain to be loaded. */ drain_reason = "This server is not accepting connections."; + + /* filter_sees_user_info: Whether the filter is given the nick!user@host of message senders. + * If set to NO, the filter is given a literal "*!*@*" instead. + * Requires extension/filter to be loaded. + */ + #filter_sees_user_info = no; + + /* filter_bypass_all: If set to NO, the only filter action that can bypass +u is DROP when paired with BYPASS. + * If set to YES, all filter actions can bypass +u when paired with BYPASS. + * Requires extension/filter to be loaded. + */ + #filter_bypass_all = no; + + /* filter_exit_message: The QUIT message shown when a user is disconnected due to the filter KILL action. + * Requires extension/filter to be loaded. + */ + #filter_exit_message = "Connection closed"; }; modules { diff --git a/doc/server-version-info.txt b/doc/server-version-info.txt index 194c6b35..c81fe1af 100644 --- a/doc/server-version-info.txt +++ b/doc/server-version-info.txt @@ -17,6 +17,8 @@ +----------------------------+ | 'e' | USE_EXCEPT | |------+---------------------| + | 'F' | FILTER_CAN_SPY | + |------+---------------------| | 'I' | USE_INVEX | |------+---------------------| | 'K' | USE_KNOCK | diff --git a/extensions/filter.c b/extensions/filter.c index 9fcb49c8..a57b2204 100644 --- a/extensions/filter.c +++ b/extensions/filter.c @@ -33,6 +33,7 @@ #include "numeric.h" #include "send.h" #include "s_newconf.h" +#include "s_conf.h" #include "s_serv.h" #include "s_user.h" #include "msg.h" @@ -41,22 +42,28 @@ #include "operhash.h" #include "inline/stringops.h" #include "msgbuf.h" +#include "newconf.h" +#include "logger.h" #include #include -#define FILTER_NICK 0 -#define FILTER_USER 0 -#define FILTER_HOST 0 - -#define FILTER_EXIT_MSG "Connection closed" +#define FILTER_DEFAULT_EXIT_MSG "Connection closed" static const char filter_desc[] = "Filter messages using a precompiled Hyperscan database"; +static int modinit(void); +static void moddeinit(void); static void filter_msg_user(void *data); static void filter_msg_channel(void *data); static void filter_client_quit(void *data); static void on_client_exit(void *data); +static void filter_init_conf(void *data); +static void filter_conf_info(void *data); +static void filter_version_info(void *data); +static void filter_conf_set_sees_user_info(void *data); +static void filter_conf_set_bypass_all(void *data); +static void filter_conf_set_exit_message(void *data); static void mo_setfilter(struct MsgBuf *, struct Client *, struct Client *, int, const char **); static void me_setfilter(struct MsgBuf *, struct Client *, struct Client *, int, const char **); @@ -67,6 +74,9 @@ static hs_database_t *filter_db; static hs_scratch_t *filter_scratch; static int filter_enable = 1; +static int filter_sees_user_info = 0; +static int filter_bypass_all = 0; +static char *filter_exit_message = NULL; static const char *cmdname[MESSAGE_TYPE_COUNT] = { [MESSAGE_TYPE_PRIVMSG] = "PRIVMSG", @@ -81,9 +91,15 @@ enum filter_state { FILTER_LOADED }; +struct match_context { + unsigned int actions; + bool require_bypass; +}; + #define ACT_DROP (1 << 0) #define ACT_KILL (1 << 1) #define ACT_ALARM (1 << 2) +#define ACT_BYPASS (1 << 3) static enum filter_state state = FILTER_EMPTY; static char check_str[21] = ""; @@ -95,45 +111,119 @@ mapi_hfn_list_av1 filter_hfnlist[] = { { "privmsg_channel", filter_msg_channel }, { "client_quit", filter_client_quit }, { "client_exit", on_client_exit }, + { "conf_read_start", filter_init_conf }, + { "doing_info_conf", filter_conf_info }, + { "doing_version_confopts", filter_version_info }, { NULL, NULL } }; - struct Message setfilter_msgtab = { "SETFILTER", 0, 0, 0, 0, {mg_unreg, mg_not_oper, mg_ignore, mg_ignore, {me_setfilter, 2}, {mo_setfilter, 2}} }; +mapi_clist_av1 filter_clist[] = { &setfilter_msgtab, NULL }; + +DECLARE_MODULE_AV2(filter, modinit, moddeinit, filter_clist, NULL, filter_hfnlist, NULL, "0.5", filter_desc); + static int modinit(void) { - filter_umode = user_modes['u'] = find_umode_slot(); - construct_umodebuf(); + filter_umode = find_umode_slot(); + if (!filter_umode) + { + ierror("filter: unable to allocate umode slot for +u, unloading module"); + return -1; + } + filter_chmode = cflag_add('u', chm_simple); + if (!filter_chmode) + { + ierror("filter: unable to allocate cmode slot for +u, unloading module"); + return -1; + } + + user_modes['u'] = filter_umode; + construct_umodebuf(); + add_conf_item("general", "filter_sees_user_info", CF_YESNO, filter_conf_set_sees_user_info); + add_conf_item("general", "filter_bypass_all", CF_YESNO, filter_conf_set_bypass_all); + add_conf_item("general", "filter_exit_message", CF_QSTRING, filter_conf_set_exit_message); return 0; } static void moddeinit(void) { - if (filter_umode) { - user_modes['u'] = 0; - construct_umodebuf(); - } - if (filter_chmode) - cflag_orphan('u'); - if (filter_scratch) - hs_free_scratch(filter_scratch); - if (filter_db) - hs_free_database(filter_db); - if (filter_data) - rb_free(filter_data); + user_modes['u'] = 0; + construct_umodebuf(); + cflag_orphan('u'); + hs_free_scratch(filter_scratch); + hs_free_database(filter_db); + rb_free(filter_data); + rb_free(filter_exit_message); + remove_conf_item("general", "filter_sees_user_info"); + remove_conf_item("general", "filter_bypass_all"); + remove_conf_item("general", "filter_exit_message"); } +static void +filter_init_conf(void *data) +{ + rb_free(filter_exit_message); + filter_exit_message = NULL; +} -mapi_clist_av1 filter_clist[] = { &setfilter_msgtab, NULL }; +static void +filter_conf_info(void *data_) +{ + hook_data *data = data_; + sendto_one(data->client, ":%s %d %s :%-30s %-16s [%s]", + get_id(&me, data->client), RPL_INFO, + get_id(data->client, data->client), + "filter_sees_user_info", + filter_sees_user_info ? "YES" : "NO", + "Let the spamfilter engine see the hostmasks of senders"); + + sendto_one(data->client, ":%s %d %s :%-30s %-16s [%s]", + get_id(&me, data->client), RPL_INFO, + get_id(data->client, data->client), + "filter_bypass_all", + filter_bypass_all ? "YES" : "NO", + "Let the spamfilter BYPASS action work on KILL/ALARM in addition to DROP"); + + sendto_one(data->client, ":%s %d %s :%-30s %-16s [%s]", + get_id(&me, data->client), RPL_INFO, + get_id(data->client, data->client), + "filter_exit_message", + filter_exit_message != NULL ? filter_exit_message : FILTER_DEFAULT_EXIT_MSG, + "Message to quit users with if they hit a filter KILL action"); +} -DECLARE_MODULE_AV2(filter, modinit, moddeinit, filter_clist, NULL, filter_hfnlist, NULL, "0.4", filter_desc); +static void +filter_version_info(void *data) +{ + if (filter_sees_user_info || filter_bypass_all) + ((char *)data)['F'] = 1; +} + +static void +filter_conf_set_sees_user_info(void *data) +{ + filter_sees_user_info = *(int *)data; +} + +static void +filter_conf_set_bypass_all(void *data) +{ + filter_bypass_all = *(int *)data; +} + +static void +filter_conf_set_exit_message(void *data) +{ + rb_free(filter_exit_message); + filter_exit_message = rb_strdup(data); +} static int setfilter(const char *check, const char *data, const char **error) @@ -313,8 +403,6 @@ me_setfilter(struct MsgBuf *msgbuf, struct Client *client_p, struct Client *sour if (r) { sendto_one_notice(source_p, ":SETFILTER failed: %s", error); } - - return; } /* will be called for every match @@ -328,8 +416,17 @@ int match_callback(unsigned id, unsigned flags, void *context_) { - unsigned *context = context_; - *context |= id; + struct match_context *context = context_; + + if (context->require_bypass) + { + unsigned int bypass_mask = filter_bypass_all ? UINT_MAX : ACT_DROP; + if (id & ACT_BYPASS) + context->actions |= id & bypass_mask; + } + else + context->actions |= id; + return 0; } @@ -338,43 +435,34 @@ static char clean_buffer[BUFSIZE]; unsigned match_message(const char *prefix, struct Client *source, + bool require_bypass, const char *command, const char *target, const char *msg) { - unsigned state = 0; + struct match_context ctx = { 0, require_bypass }; + if (!filter_enable) return 0; if (!filter_db) return 0; if (!command) return 0; + snprintf(check_buffer, sizeof check_buffer, "%s:%s!%s@%s#%c %s%s%s :%s", prefix, -#if FILTER_NICK - source->name, -#else - "*", -#endif -#if FILTER_USER - source->username, -#else - "*", -#endif -#if FILTER_HOST - source->host, -#else - "*", -#endif + filter_sees_user_info ? source->name : "*", + filter_sees_user_info ? source->username : "*", + filter_sees_user_info ? source->host : "*", source->user && source->user->suser[0] != '\0' ? '1' : '0', command, target ? " " : "", target ? target : "", msg); - hs_error_t r = hs_scan(filter_db, check_buffer, strlen(check_buffer), 0, filter_scratch, match_callback, &state); + hs_error_t r = hs_scan(filter_db, check_buffer, strlen(check_buffer), 0, filter_scratch, match_callback, &ctx); if (r != HS_SUCCESS && r != HS_SCAN_TERMINATED) return 0; - return state; + return ctx.actions; } void @@ -382,6 +470,7 @@ filter_msg_user(void *data_) { hook_data_privmsg_user *data = data_; struct Client *s = data->source_p; + /* we only need to filter once */ if (!MyClient(s)) { return; @@ -392,14 +481,14 @@ filter_msg_user(void *data_) if (IsOper(s) || IsOper(data->target_p)) { return; } - if (data->target_p->umodes & filter_umode) { - return; - } + + bool require_bypass = (data->target_p->umodes & filter_umode) == filter_umode; char *text = strcpy(clean_buffer, data->text); strip_colour(text); strip_unprintable(text); - unsigned r = match_message("0", s, cmdname[data->msgtype], "0", data->text) | - match_message("1", s, cmdname[data->msgtype], "0", text); + unsigned r = match_message("0", s, require_bypass, cmdname[data->msgtype], "0", data->text) | + match_message("1", s, require_bypass, cmdname[data->msgtype], "0", text); + if (r & ACT_DROP) { if (data->msgtype == MESSAGE_TYPE_PRIVMSG) { sendto_one_numeric(s, ERR_CANNOTSENDTOCHAN, @@ -415,7 +504,7 @@ filter_msg_user(void *data_) } if (r & ACT_KILL) { data->approved = 1; - exit_client(NULL, s, s, FILTER_EXIT_MSG); + exit_client(NULL, s, s, filter_exit_message != NULL ? filter_exit_message : FILTER_DEFAULT_EXIT_MSG); } } @@ -424,6 +513,7 @@ filter_msg_channel(void *data_) { hook_data_privmsg_channel *data = data_; struct Client *s = data->source_p; + /* we only need to filter once */ if (!MyClient(s)) { return; @@ -433,14 +523,14 @@ filter_msg_channel(void *data_) if (IsOper(s)) { return; } - if (data->chptr->mode.mode & filter_chmode) { - return; - } + + bool require_bypass = (data->chptr->mode.mode & filter_chmode) == filter_chmode; char *text = strcpy(clean_buffer, data->text); strip_colour(text); strip_unprintable(text); - unsigned r = match_message("0", s, cmdname[data->msgtype], data->chptr->chname, data->text) | - match_message("1", s, cmdname[data->msgtype], data->chptr->chname, text); + unsigned r = match_message("0", s, require_bypass, cmdname[data->msgtype], data->chptr->chname, data->text) | + match_message("1", s, require_bypass, cmdname[data->msgtype], data->chptr->chname, text); + if (r & ACT_DROP) { if (data->msgtype == MESSAGE_TYPE_PRIVMSG) { sendto_one_numeric(s, ERR_CANNOTSENDTOCHAN, @@ -456,7 +546,7 @@ filter_msg_channel(void *data_) } if (r & ACT_KILL) { data->approved = 1; - exit_client(NULL, s, s, FILTER_EXIT_MSG); + exit_client(NULL, s, s, filter_exit_message != NULL ? filter_exit_message : FILTER_DEFAULT_EXIT_MSG); } } @@ -471,8 +561,8 @@ filter_client_quit(void *data_) char *text = strcpy(clean_buffer, data->orig_reason); strip_colour(text); strip_unprintable(text); - unsigned r = match_message("0", s, "QUIT", NULL, data->orig_reason) | - match_message("1", s, "QUIT", NULL, text); + unsigned r = match_message("0", s, false, "QUIT", NULL, data->orig_reason) | + match_message("1", s, false, "QUIT", NULL, text); if (r & ACT_DROP) { data->reason = NULL; }