Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions doc/reference.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 2 additions & 0 deletions doc/server-version-info.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
+----------------------------+
| 'e' | USE_EXCEPT |
|------+---------------------|
| 'F' | FILTER_CAN_SPY |
|------+---------------------|
| 'I' | USE_INVEX |
|------+---------------------|
| 'K' | USE_KNOCK |
Expand Down
173 changes: 129 additions & 44 deletions extensions/filter.c
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -41,22 +42,27 @@
#include "operhash.h"
#include "inline/stringops.h"
#include "msgbuf.h"
#include "newconf.h"

#include <hs_common.h>
#include <hs_runtime.h>

#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 **);
Expand All @@ -67,6 +73,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",
Expand All @@ -81,9 +90,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] = "";
Expand All @@ -95,21 +110,30 @@ 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_chmode = cflag_add('u', chm_simple);
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;
}

Expand All @@ -128,12 +152,73 @@ moddeinit(void)
hs_free_database(filter_db);
if (filter_data)
rb_free(filter_data);
if (filter_exit_message)
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)
{
if (filter_exit_message)
{
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");
}

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;
}

DECLARE_MODULE_AV2(filter, modinit, moddeinit, filter_clist, NULL, filter_hfnlist, NULL, "0.4", filter_desc);
static void
filter_conf_set_bypass_all(void *data)
{
filter_bypass_all = *(int *)data;
}

static void
filter_conf_set_exit_message(void *data)
{
filter_exit_message = (char *)data;
}

static int
setfilter(const char *check, const char *data, const char **error)
Expand Down Expand Up @@ -313,8 +398,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
Expand All @@ -328,8 +411,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;
}

Expand All @@ -338,50 +430,42 @@ 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
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;
Expand All @@ -392,14 +476,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,
Expand All @@ -415,7 +499,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);
}
}

Expand All @@ -424,6 +508,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;
Expand All @@ -433,14 +518,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,
Expand All @@ -456,7 +541,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);
}
}

Expand All @@ -471,8 +556,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;
}
Expand Down