1
0
mirror of https://github.com/unrealircd/unrealircd.git synced 2026-07-04 13:13:13 +02:00
Files
unrealircd/src/modules/central-blocklist.c
T
Bram Matthys 28a8bee041 Don't use 'client' in CENTRAL_BLOCKLIST_ERROR, prolly copy-paste error.
Not really important as it is not part of the normal log message (only JSON).
2026-02-21 13:49:26 +01:00

1224 lines
35 KiB
C

/* Central blocklist
* (C) Copyright 2023 Bram Matthys and The UnrealIRCd Team
* License: GPLv2
*/
#include "unrealircd.h"
ModuleHeader MOD_HEADER
= {
"central-blocklist",
"1.0.8",
"Check users at central blocklist",
"UnrealIRCd Team",
"unrealircd-6",
};
ModDataInfo *centralblocklist_md = NULL;
Module *cbl_module = NULL;
#define CBL_URL "https://centralblocklist.unrealircd-api.org/api/v1"
#define SPAMREPORT_URL "https://spamreport.unrealircd-api.org/api/spamreport-v1"
#define CBL_TRANSFER_TIMEOUT 10
#define SPAMREPORT_NUM_REMEMBERED_CMDS 20
#define WEB(client) ((WebRequest *)moddata_local_client(client, webserver_md).ptr)
#define WSU(client) ((WebSocketUser *)moddata_client(client, websocket_md).ptr)
typedef struct CBLUser CBLUser;
struct CBLUser
{
json_t *handshake;
time_t request_sent;
char request_pending;
char allowed_in;
int last_cmds_slot;
char *last_cmds[SPAMREPORT_NUM_REMEMBERED_CMDS];
TextAnalysis last_cmds_textanalysis[SPAMREPORT_NUM_REMEMBERED_CMDS];
};
/* For tracking current HTTPS requests */
typedef struct CBLTransfer CBLTransfer;
struct CBLTransfer
{
CBLTransfer *prev, *next;
time_t started;
NameList *clients;
};
typedef struct ScoreAction ScoreAction;
struct ScoreAction {
ScoreAction *prev, *next;
int priority;
int score;
BanAction *ban_action;
char *ban_reason;
long ban_time;
};
struct cfgstruct {
char *url;
char *spamreport_url;
char *api_key;
int max_downloads;
int blocklist_enabled;
SecurityGroup *except;
ScoreAction *actions;
};
static struct cfgstruct cfg;
struct reqstruct {
char custom_score_blocks;
};
static struct reqstruct req;
CBLTransfer *cbltransfers = NULL;
ModDataInfo *webserver_md = NULL; /* (external module, looked up) */
ModDataInfo *websocket_md = NULL; /* (external module, looked up) */
/* Forward declarations */
int _central_spamreport(Client *client, Client *by, const char *url);
int cbl_config_test(ConfigFile *cf, ConfigEntry *ce, int type, int *errs);
int cbl_config_posttest(int *errs);
int cbl_config_run(ConfigFile *cf, ConfigEntry *ce, int type);
int cbl_packet(Client *from, Client *to, Client *intended_to, char **msg, int *len);
//int cbl_prelocalconnect(Client *client);
int cbl_is_handshake_finished(Client *client);
void cbl_download_complete(OutgoingWebRequest *request, OutgoingWebResponse *response);
void cbl_mdata_free(ModData *m);
int cbl_start_request(Client *client);
void cbl_cancel_all_transfers(void);
EVENT(centralblocklist_bundle_requests);
EVENT(centralblocklist_timeout_evt);
void cbl_allow(Client *client);
void send_request_for_pending_clients(void);
const char *get_api_key(void);
void set_tag(Client *client, const char *tag, int value);
#define CBLRAW(x) (moddata_local_client(x, centralblocklist_md).ptr)
#define CBL(x) ((CBLUser *)(moddata_local_client(x, centralblocklist_md).ptr))
#define alloc_cbl_if_needed(x) do { \
if (!moddata_local_client(x, centralblocklist_md).ptr) \
{ \
CBLUser *u = safe_alloc(sizeof(CBLUser)); \
u->handshake = json_object(); \
moddata_local_client(x, centralblocklist_md).ptr = u; \
} \
} while(0)
#define AddScoreAction(item,list) do { item->priority = 0 - item->score; AddListItemPrio(item, list, item->priority); } while(0)
CMD_OVERRIDE_FUNC(cbl_override);
CMD_OVERRIDE_FUNC(cbl_override_spamreport_gather);
static void set_default_score_action(ScoreAction *action)
{
action->ban_action = banact_value_to_struct(BAN_ACT_KILL);
action->ban_time = 900;
safe_strdup(action->ban_reason, "Rejected by central blocklist");
}
/* Default config */
static void init_config(void)
{
memset(&cfg, 0, sizeof(cfg));
safe_strdup(cfg.url, CBL_URL);
safe_strdup(cfg.spamreport_url, SPAMREPORT_URL);
cfg.max_downloads = 100;
cfg.blocklist_enabled = 1;
// default action
if (!req.custom_score_blocks)
{
ScoreAction *action;
/* score 5+ */
action = safe_alloc(sizeof(ScoreAction));
action->score = 5;
action->ban_action = banact_value_to_struct(BAN_ACT_KLINE);
action->ban_time = 900; /* 15m */
safe_strdup(action->ban_reason, "Rejected by central blocklist");
AddScoreAction(action, cfg.actions);
/* score 10+ */
action = safe_alloc(sizeof(ScoreAction));
action->score = 10;
action->ban_action = banact_value_to_struct(BAN_ACT_SHUN);
action->ban_time = 3600; /* 1h */
safe_strdup(action->ban_reason, "Rejected by central blocklist");
AddScoreAction(action, cfg.actions);
}
// and the default except block
cfg.except = safe_alloc(sizeof(SecurityGroup));
cfg.except->reputation_score = 2016; /* 7 days unregged, or 3.5 days identified */
cfg.except->identified = 1;
// exception masks
unreal_add_mask_string(&cfg.except->mask, "*.irccloud.com");
// exception IPs
#ifndef DEBUGMODE
add_name_list(cfg.except->ip, "127.0.0.1");
add_name_list(cfg.except->ip, "192.168.*");
add_name_list(cfg.except->ip, "10.*");
#endif
}
static void free_config(void)
{
ScoreAction *s, *s_next;
for (s = cfg.actions; s; s = s_next)
{
s_next = s->next;
safe_free(s->ban_reason);
safe_free_all_ban_actions(s->ban_action);
safe_free(s);
}
cfg.actions = NULL;
free_security_group(cfg.except);
safe_free(cfg.url);
safe_free(cfg.spamreport_url);
safe_free(cfg.api_key);
memset(&cfg, 0, sizeof(cfg)); /* needed! */
}
MOD_TEST()
{
memset(&req, 0, sizeof(req));
MARK_AS_OFFICIAL_MODULE(modinfo);
HookAdd(modinfo->handle, HOOKTYPE_CONFIGTEST, 0, cbl_config_test);
HookAdd(modinfo->handle, HOOKTYPE_CONFIGPOSTTEST, 0, cbl_config_posttest);
EfunctionAdd(modinfo->handle, EFUNC_CENTRAL_SPAMREPORT, _central_spamreport);
return MOD_SUCCESS;
}
MOD_INIT()
{
ModDataInfo mreq;
cbl_module = modinfo->handle;
MARK_AS_OFFICIAL_MODULE(modinfo);
init_config();
memset(&mreq, 0, sizeof(mreq));
mreq.name = "central-blocklist-user";
mreq.type = MODDATATYPE_LOCAL_CLIENT;
mreq.free = cbl_mdata_free;
centralblocklist_md = ModDataAdd(modinfo->handle, mreq);
if (!centralblocklist_md)
{
config_error("[central-blocklist] failed adding moddata");
return MOD_FAILED;
}
HookAdd(modinfo->handle, HOOKTYPE_CONFIGRUN, 0, cbl_config_run);
//HookAdd(modinfo->handle, HOOKTYPE_PRE_LOCAL_CONNECT, 0, cbl_prelocalconnect);
HookAdd(modinfo->handle, HOOKTYPE_IS_HANDSHAKE_FINISHED, INT_MAX, cbl_is_handshake_finished);
RegisterApiCallbackWebResponse(modinfo->handle, "cbl_download_complete", cbl_download_complete);
return MOD_SUCCESS;
}
void do_command_overrides(ModuleInfo *modinfo)
{
RealCommand *cmd;
int i;
for (i = 0; i < 256; i++)
{
for (cmd = CommandHash[i]; cmd; cmd = cmd->next)
{
if (cmd->flags & CMD_UNREGISTERED)
CommandOverrideAdd(modinfo->handle, cmd->cmd, -1, cbl_override);
}
}
}
MOD_LOAD()
{
const char *central_api_key = get_central_api_key();
if (!central_api_key)
{
config_warn("The centralblocklist module is inactive because the central api key is not set. "
"Acquire a key via https://www.unrealircd.org/central-api/ and then "
"make sure the central-api-key module is loaded and set::central-api::api-key set.");
return MOD_SUCCESS;
} else {
safe_strdup(cfg.api_key, central_api_key);
}
do_command_overrides(modinfo);
webserver_md = findmoddata_byname("web", MODDATATYPE_LOCAL_CLIENT);
websocket_md = findmoddata_byname("websocket", MODDATATYPE_CLIENT);
/* Enable gathering of "last 20 lines" for SPAMREPORT, only if SPAMREPORT is enabled: */
if (central_spamreport_enabled())
{
CommandOverrideAdd(modinfo->handle, "NICK", -2, cbl_override_spamreport_gather);
CommandOverrideAdd(modinfo->handle, "PRIVMSG", -2, cbl_override_spamreport_gather);
CommandOverrideAdd(modinfo->handle, "NOTICE", -2, cbl_override_spamreport_gather);
CommandOverrideAdd(modinfo->handle, "PART", -2, cbl_override_spamreport_gather);
CommandOverrideAdd(modinfo->handle, "INVITE", -2, cbl_override_spamreport_gather);
CommandOverrideAdd(modinfo->handle, "KNOCK", -2, cbl_override_spamreport_gather);
}
EventAdd(modinfo->handle, "centralblocklist_timeout_evt", centralblocklist_timeout_evt, NULL, 1000, 0);
EventAdd(modinfo->handle, "centralblocklist_bundle_requests", centralblocklist_bundle_requests, NULL, 1000, 0);
return MOD_SUCCESS;
}
MOD_UNLOAD()
{
// No longer needed thanks to RegisterApiCallbackXX -- cbl_cancel_all_transfers();
free_config();
return MOD_SUCCESS;
}
/** Test the set::central-blocklist configuration */
int cbl_config_test(ConfigFile *cf, ConfigEntry *ce, int type, int *errs)
{
int errors = 0;
ConfigEntry *cep, *cepp;
if (type != CONFIG_SET)
return 0;
/* We are only interrested in set::central-blocklist.. */
if (!ce || !ce->name || strcmp(ce->name, "central-blocklist"))
return 0;
for (cep = ce->items; cep; cep = cep->next)
{
if (!strcmp(cep->name, "api-key"))
{
config_error("%s:%i: the api-key is no longer configured at this place. "
"Remove set::central-blocklist::api-key, load the "
"central-api module and put the key in set::central-api::api-key",
cep->file->filename, cep->line_number);
errors++;
} else
if (!strcmp(cep->name, "except"))
{
test_match_block(cf, cep, &errors);
} else
if (!strcmp(cep->name, "score"))
{
int v = atoi(cep->value);
if ((v < 1) || (v > 99))
{
config_error("%s:%i: set::central-blocklist::score: must be between 1 - 99 (got: %d)",
cep->file->filename, cep->line_number, v);
errors++;
}
if (cep->items)
{
req.custom_score_blocks = 1;
for (cepp = cep->items; cepp; cepp = cepp->next)
{
if (!strcmp(cepp->name, "ban-action"))
{
errors += test_ban_action_config(cepp);
} else
if (!strcmp(cepp->name, "ban-reason"))
{
} else
if (!strcmp(cepp->name, "ban-time"))
{
} else
{
config_error("%s:%i: unknown directive set::central-blocklist::score::%s",
cepp->file->filename, cepp->line_number, cepp->name);
errors++;
continue;
}
}
}
} else
if (!cep->value)
{
config_error("%s:%i: set::central-blocklist::%s with no value",
cep->file->filename, cep->line_number, cep->name);
errors++;
} else
if (!strcmp(cep->name, "url"))
{
} else
if (!strcmp(cep->name, "spamreport") || !strcmp(cep->name, "spamreport-enabled"))
{
config_error("%s:%i: set::central-blocklist::%s: This setting is deprecated. "
"Please remove this setting, and, if you wish to use spamreport, add a "
"spamreport unrealircd { type central-spamreport; } block in your main config. "
"See https://www.unrealircd.org/docs/Central_spamreport",
cep->file->filename, cep->line_number, cep->name);
errors++;
} else
if (!strcmp(cep->name, "blocklist") || !strcmp(cep->name, "blocklist-enabled"))
{
} else
if (!strcmp(cep->name, "spamreport-url"))
{
} else
if (!strcmp(cep->name, "max-downloads"))
{
int v = atoi(cep->value);
if ((v < 1) || (v > 500))
{
config_error("%s:%i: set::central-blocklist::score: must be between 1 - 500 (got: %d)",
cep->file->filename, cep->line_number, v);
errors++;
}
} else
if (!strcmp(cep->name, "ban-action") || !strcmp(cep->name, "ban-reason") || !strcmp(cep->name, "ban-time"))
{
config_error("%s:%i: set::central-blocklist: you cannot use ban-action/ban-reason/ban-time here. "
"There are now multiple score blocks. "
"See https://www.unrealircd.org/docs/Central_Blocklist#Configuration",
cep->file->filename, cep->line_number);
errors++;
} else
{
config_error("%s:%i: unknown directive set::central-blocklist::%s",
cep->file->filename, cep->line_number, cep->name);
errors++;
continue;
}
}
*errs = errors;
return errors ? -1 : 1;
}
int cbl_config_posttest(int *errs)
{
int errors = 0;
*errs = errors;
return errors ? -1 : 1;
}
/* Configure ourselves based on the set::central-blocklist settings */
int cbl_config_run(ConfigFile *cf, ConfigEntry *ce, int type)
{
ConfigEntry *cep, *cepp;
if (type != CONFIG_SET)
return 0;
/* We are only interrested in set::central-blocklist.. */
if (!ce || !ce->name || strcmp(ce->name, "central-blocklist"))
return 0;
for (cep = ce->items; cep; cep = cep->next)
{
if (!strcmp(cep->name, "api-key"))
{
safe_strdup(cfg.api_key, cep->value);
} else
if (!strcmp(cep->name, "score"))
{
if (!cep->items)
{
cfg.actions->score = atoi(cep->value);
} else
{
ScoreAction *action = safe_alloc(sizeof(ScoreAction));
set_default_score_action(action);
action->score = atoi(cep->value);
AddScoreAction(action, cfg.actions);
for (cepp = cep->items; cepp; cepp = cepp->next)
{
if (!strcmp(cepp->name, "ban-action"))
{
parse_ban_action_config(cepp, &action->ban_action);
} else
if (!strcmp(cepp->name, "ban-reason"))
{
safe_strdup(action->ban_reason, cepp->value);
} else
if (!strcmp(cepp->name, "ban-time"))
{
action->ban_time = config_checkval(cepp->value, CFG_TIME);
}
}
}
} else
if (!strcmp(cep->name, "url"))
{
safe_strdup(cfg.url, cep->value);
} else
if (!strcmp(cep->name, "blocklist-enabled"))
{
cfg.blocklist_enabled = config_checkval(cep->value, CFG_YESNO);
} else
if (!strcmp(cep->name, "spamreport-url"))
{
safe_strdup(cfg.spamreport_url, cep->value);
} else
if (!strcmp(cep->name, "max-downloads"))
{
cfg.max_downloads = atoi(cep->value);
} else
if (!strcmp(cep->name, "ban-action"))
{
parse_ban_action_config(cep, &cfg.actions->ban_action);
} else
if (!strcmp(cep->name, "ban-reason"))
{
safe_strdup(cfg.actions->ban_reason, cep->value);
} else
if (!strcmp(cep->name, "ban-time"))
{
cfg.actions->ban_time = config_checkval(cep->value, CFG_TIME);
} else
if (!strcmp(cep->name, "except"))
{
if (cfg.except)
{
free_security_group(cfg.except);
cfg.except = NULL;
}
conf_match_block(cf, cep, &cfg.except);
}
}
return 1;
}
CBLTransfer *add_cbl_transfer(NameList *clients)
{
CBLTransfer *c = safe_alloc(sizeof(CBLTransfer));
c->started = TStime();
c->clients = clients;
AddListItem(c, cbltransfers);
return c;
}
void del_cbl_transfer(CBLTransfer *c)
{
free_entire_name_list(c->clients);
DelListItem(c, cbltransfers);
safe_free(c);
}
void cbl_cancel_all_transfers(void)
{
CBLTransfer *c, *c_next;
Client *client, *client_next;
for (c = cbltransfers; c; c = c_next)
{
json_t *cbl;
c_next = c->next;
url_cancel_handle_by_callback_data(c);
safe_free(c);
}
cbltransfers = NULL;
list_for_each_entry_safe(client, client_next, &unknown_list, lclient_node)
{
CBLUser *cbl = CBL(client);
if (cbl && cbl->request_sent)
{
cbl->request_sent = 0;
cbl->request_pending = 1;
}
}
}
EVENT(centralblocklist_timeout_evt)
{
Client *client, *client_next;
list_for_each_entry_safe(client, client_next, &unknown_list, lclient_node)
{
CBLUser *cbl = CBL(client);
if (cbl &&
cbl->request_sent &&
!cbl->allowed_in &&
!IsShunned(client) &&
(TStime() - cbl->request_sent > CBL_TRANSFER_TIMEOUT))
{
unreal_log(ULOG_WARNING, "central-blocklist", "CENTRAL_BLOCKLIST_TIMEOUT", client,
"Central blocklist too slow to respond. "
"Possible problem with infrastructure at unrealircd.org. "
"Allowing user $client.details in unchecked.");
cbl_allow(client);
}
}
/* NOTE: We did not cancel the HTTPS request, so the result may come in later
* when the user is already allowed in.
*/
}
void show_client_json(Client *client)
{
char *json_serialized;
json_serialized = json_dumps(CBL(client)->handshake, JSON_COMPACT);
unreal_log(ULOG_DEBUG, "central-blocklist", "DEBUG_CENTRAL_BLOCKLIST", client,
"OUT: $data",
log_data_string("data", json_serialized));
safe_free(json_serialized);
}
void cbl_add_client_info(Client *client)
{
char buf[BUFSIZE+1];
json_t *cbl = CBL(client)->handshake;
json_t *child = json_object();
const char *str;
int i;
json_object_set_new(cbl, "client", child);
//// THE FOLLOWING IS TAKEN FROM src/json.c AND MODIFIED /////
/* First the information that is available for ALL client types: */
json_object_set_new(child, "name", json_string_unreal(client->name));
json_object_set_new(child, "id", json_string_unreal(client->id));
/* hostname is available for all, it just depends a bit on whether it is DNS or IP */
if (client->user && *client->user->realhost)
json_object_set_new(child, "hostname", json_string_unreal(client->user->realhost));
else if (client->local && *client->local->sockhost)
json_object_set_new(child, "hostname", json_string_unreal(client->local->sockhost));
else
json_object_set_new(child, "hostname", json_string_unreal(GetIP(client)));
/* same for ip, is there for all (well, some services pseudo-users may not have one) */
json_object_set_new(child, "ip", json_string_unreal(client->ip));
/* client.details is always available: it is nick!user@host, nick@host, server@host
* server@ip, or just server.
*/
if (client->user)
{
snprintf(buf, sizeof(buf), "%s!%s@%s", client->name, client->user->username, client->user->realhost);
json_object_set_new(child, "details", json_string_unreal(buf));
} else if (client->ip) {
if (*client->name)
snprintf(buf, sizeof(buf), "%s@%s", client->name, client->ip);
else
snprintf(buf, sizeof(buf), "[%s]", client->ip);
json_object_set_new(child, "details", json_string_unreal(buf));
} else {
json_object_set_new(child, "details", json_string_unreal(client->name));
}
if ((i = get_server_port(client)))
json_object_set_new(child, "server_port", json_integer(i));
if ((i = get_client_port(client)))
json_object_set_new(child, "client_port", json_integer(i));
if (client->user)
{
char buf[512];
const char *str;
/* client.user */
json_t *user = json_object();
json_object_set_new(child, "user", user);
json_object_set_new(user, "username", json_string_unreal(client->user->username));
if (!BadPtr(client->info))
json_object_set_new(user, "realname", json_string_unreal(client->info));
json_object_set_new(user, "reputation", json_integer(GetReputation(client)));
}
if (webserver_md && WEB(client))
{
json_t *web = json_object();
json_t *headers = json_object();
NameValuePrioList *nv;
json_object_set_new(child, "web", web);
json_object_set_new(web, "headers", headers);
for (nv = WEB(client)->headers; nv; nv = nv->next)
json_object_set_new(headers, nv->name, json_string_unreal(nv->value));
}
if (websocket_md && WSU(client))
{
json_t *websocket = json_object();
json_object_set_new(child, "websocket", websocket);
if (WSU(client)->type == WEBSOCKET_TYPE_TEXT)
json_object_set_new(websocket, "protocol", json_string_unreal("text"));
else if (WSU(client)->type == WEBSOCKET_TYPE_BINARY)
json_object_set_new(websocket, "protocol", json_string_unreal("binary"));
}
if ((str = moddata_client_get(client, "tls_cipher")))
{
json_t *tls = json_object();
json_object_set_new(child, "tls", tls);
json_object_set_new(tls, "cipher", json_string_unreal(str));
if (client->local->sni_servername)
json_object_set_new(tls, "sni_servername", json_string_unreal(client->local->sni_servername));
}
#ifdef HAVE_TCP_INFO
if (client->local->fd >= 0)
{
socklen_t optlen = sizeof(struct tcp_info);
struct tcp_info tcp_info;
optlen = sizeof(tcp_info);
memset(&tcp_info, 0, sizeof(tcp_info));
if (getsockopt(client->local->fd, IPPROTO_TCP, TCP_INFO, (void *)&tcp_info, &optlen) == 0)
{
json_t *j = json_object();
json_object_set_new(child, "tcp_info", j);
json_object_set_new(j, "rtt", json_integer(MAX(tcp_info.tcpi_rtt,1)/1000));
json_object_set_new(j, "rtt_var", json_integer(MAX(tcp_info.tcpi_rttvar,1)/1000));
#if defined(__FreeBSD__)
json_object_set_new(j, "pmtu", json_integer(tcp_info.__tcpi_pmtu));
#else
json_object_set_new(j, "pmtu", json_integer(tcp_info.tcpi_pmtu));
#endif
json_object_set_new(j, "snd_cwnd", json_integer(tcp_info.tcpi_snd_cwnd));
json_object_set_new(j, "snd_mss", json_integer(tcp_info.tcpi_snd_mss));
json_object_set_new(j, "rcv_mss", json_integer(tcp_info.tcpi_rcv_mss));
}
}
#endif
}
CMD_OVERRIDE_FUNC(cbl_override)
{
json_t *cbl;
json_t *handshake;
json_t *cmds;
json_t *item;
char timebuf[64];
char number[32];
char isnick = 0;
uint32_t nospoof = 0;
if (!MyConnect(client) ||
!IsUnknown(client) ||
!strcmp(ovr->command->cmd, "PASS") ||
!strcmp(ovr->command->cmd, "WEBIRC") ||
!strcmp(ovr->command->cmd, "AUTHENTICATE"))
{
CALL_NEXT_COMMAND_OVERRIDE();
return;
}
alloc_cbl_if_needed(client);
cbl = CBL(client)->handshake;
/* Create "handshake" if it does not exist yet */
handshake = json_object_get(cbl, "handshake");
if (!handshake)
{
handshake = json_object();
json_object_set_new(cbl, "handshake", handshake);
}
/* Create handshake->commands if it does not exist yet */
cmds = json_object_get(handshake, "commands");
if (!cmds)
{
cmds = json_object();
json_object_set_new(handshake, "commands", cmds);
}
strlcpy(timebuf, timestamp_iso8601_now(), sizeof(timebuf));
snprintf(number, sizeof(number), "%lld", client->local->traffic.messages_received);
item = json_object();
json_object_set_new(item, "time", json_string_unreal(timebuf));
json_object_set_new(item, "command", json_string_unreal(ovr->command->cmd));
json_object_set_new(item, "raw", json_string_unreal(backupbuf));
json_object_set_new(cmds, number, item);
if (!strcmp(ovr->command->cmd, "NICK"))
{
isnick = 1;
nospoof = client->local->nospoof;
} else
if (!strcmp(ovr->command->cmd, "PONG") && (parc > 1) && !BadPtr(parv[1]))
{
unsigned long result = strtoul(parv[1], NULL, 16);
if (client->local->nospoof && (client->local->nospoof == result))
{
json_object_del(handshake, "pong_received");
json_object_set_new(handshake, "pong_received", json_string_unreal(timebuf));
}
}
#if UNREAL_VERSION < 0x06010300
/* Meh... bug in UnrealIRCd <6.1.3 */
else if (!strcmp(ovr->command->cmd, "CAP") && (parc > 1) && !strcasecmp(parv[1], "END") && !IsUser(client))
{
ClearCapability(client, "cap");
if (is_handshake_finished(client))
register_user(client);
return; // we handled it
}
#endif
CALL_NEXT_COMMAND_OVERRIDE();
if (isnick && !IsDead(client) && (nospoof != client->local->nospoof))
{
json_object_del(handshake, "ping_sent");
json_object_set_new(handshake, "ping_sent", json_string_unreal(timebuf));
}
}
int cbl_start_request(Client *client)
{
CBLUser *cbl = CBL(client);
if (cbl->request_sent || cbl->request_pending)
return 0; /* Handshake is NOT finished yet, HTTP request already in progress */
cbl->request_pending = 1;
#ifdef DEBUGMODE
show_client_json(client);
#endif
return 0; /* Handshake is NOT finished yet, request will be sent to server */
}
int cbl_is_handshake_finished(Client *client)
{
if (!CBL(client) || CBL(client)->allowed_in)
return 1; // something went wrong or we are finished with this, let the user through
/* Missing something, pretend we are finished and don't handle */
if (!(client->user && *client->user->username && client->name[0] && IsNotSpoof(client)))
return 1;
/* User is exempt */
if (user_allowed_by_security_group(client, cfg.except))
return 1;
if (!json_object_get(CBL(client)->handshake, "client"))
cbl_add_client_info(client);
if (cfg.blocklist_enabled)
return cbl_start_request(client);
else
return 1; /* CBL is not in use, we are done */
}
void cbl_allow(Client *client)
{
if (CBL(client))
{
if (CBL(client)->allowed_in)
return; /* Already allowed in */
CBL(client)->allowed_in = 1;
}
if (is_handshake_finished(client))
register_user(client);
}
void set_tag(Client *client, const char *name, int value)
{
Tag *tag = find_tag(client, name);
if (tag)
tag->value = value;
else
add_tag(client, name, value);
}
void cbl_handle_response(Client *client, json_t *response)
{
int spam_score = 0; // spam score, can be negative too
json_error_t jerr;
Tag *tag;
ScoreAction *action;
json_t *j, *obj;
spam_score = json_object_get_integer(response, "score", 0);
set_tag(client, "CBL_SCORE", spam_score);
obj = json_object_get(response, "set-variables");
if (obj)
{
const char *key;
json_t *value;
json_object_foreach(obj, key, value)
{
if (!key || !value || !json_is_integer(value))
continue;
if (!strcmp(key, "REPUTATION"))
continue; // reserved variable name (FIXME: not hardcoded)
set_tag(client, key, json_integer_value(value));
}
}
for (action = cfg.actions; action; action = action->next)
{
if (spam_score >= action->score)
{
if (highest_ban_action(action->ban_action) <= BAN_ACT_WARN)
{
unreal_log(ULOG_INFO, "central-blocklist", "CBL_HIT", client,
"CBL: Client $client.details flagged by central-blocklist, but allowed in (score $spam_score)",
log_data_integer("spam_score", spam_score));
} else {
unreal_log(ULOG_INFO, "central-blocklist", "CBL_HIT_REJECTED_USER", client,
"CBL: Client $client.details is rejected by central-blocklist (score $spam_score)",
log_data_integer("spam_score", spam_score));
}
if (take_action(client, action->ban_action, action->ban_reason, action->ban_time, 0, NULL) <= BAN_ACT_WARN)
cbl_allow(client);
return;
}
}
unreal_log(ULOG_DEBUG, "central-blocklist", "DEBUG_CENTRAL_BLOCKLIST", client,
"CBL: Client $client.details is allowed (score $spam_score)",
log_data_integer("spam_score", spam_score));
cbl_allow(client);
}
void cbl_error_response(CBLTransfer *transfer, const char *error)
{
NameList *n;
Client *client;
int num = 0;
for (n = transfer->clients; n; n = n->next)
{
client = hash_find_id(n->name, NULL);
if (!client)
continue; /* Client disconnected already */
if (CBL(client) && CBL(client)->allowed_in)
continue; /* Client allowed in already (eg due to timeout) */
unreal_log(ULOG_DEBUG, "central-blocklist", "DEBUG_CENTRAL_BLOCKLIST_ERROR", client,
"CBL: Client $client.details allowed in due to CBL error: $error",
log_data_string("error", error));
cbl_allow(client);
num++;
}
if (num > 0)
{
unreal_log(ULOG_INFO, "central-blocklist", "CENTRAL_BLOCKLIST_ERROR", NULL,
"CBL: Allowed $num_clients client(s) in due to CBL error: $error",
log_data_integer("num_clients", num),
log_data_string("error", error));
}
del_cbl_transfer(transfer);
}
void cbl_download_complete(OutgoingWebRequest *request, OutgoingWebResponse *response)
{
CBLTransfer *transfer;
json_t *result; // complete JSON result
json_t *responses; // result->responses
json_error_t jerr;
const char *str;
const char *key;
json_t *value;
transfer = (CBLTransfer *)request->callback_data;
// !!!!! IMPORTANT !!!!!
//
// Do NOT 'return' without calling cbl_error_response(transfer)
//
// !!!!! IMPORTANT !!!!!
if (response->errorbuf || !response->memory)
{
char buf[512];
unreal_log(ULOG_DEBUG, "central-blocklist", "DEBUG_CENTRAL_BLOCKLIST", NULL,
"CBL ERROR: $error",
log_data_string("error", response->errorbuf ? response->errorbuf : "No data returned"));
snprintf(buf, sizeof(buf), "error contacting CBL: %s", response->errorbuf ? response->errorbuf : "No data returned");
cbl_error_response(transfer, buf);
return;
}
#ifdef DEBUGMODE
unreal_log(ULOG_DEBUG, "central-blocklist", "DEBUG_CENTRAL_BLOCKLIST", NULL,
"CBL Got result: $buf",
log_data_string("buf", response->memory));
#endif
// NOTE: if we didn't have that debug from above, we could avoid the strlncpy and use json_loadb here
result = json_loads(response->memory, JSON_REJECT_DUPLICATES, &jerr);
if (!result)
{
unreal_log(ULOG_DEBUG, "central-blocklist", "DEBUG_CENTRAL_BLOCKLIST", NULL,
"CBL ERROR: JSON parse error");
cbl_error_response(transfer, "invalid CBL response (JSON parse error)");
return;
}
/* Errors are fatal, we display, allow clients in and stop */
if ((str = json_object_get_string(result, "error")))
{
cbl_error_response(transfer, str);
json_decref(result);
return;
}
/* Warnings are non-fatal, we display it and continue.
* These could be used, for example for deprecation warnings
* (eg: ancient module version) before we make it a hard
* error weeks/months later.
*/
if ((str = json_object_get_string(result, "warning")))
{
unreal_log(ULOG_WARNING, "central-blocklist", "CENTRAL_BLOCKLIST_WARNING", NULL,
"CBL Server gave a warning: $warning",
log_data_string("warning", str));
}
responses = json_object_get(result, "responses");
if (!responses)
{
cbl_error_response(transfer, "no spam scores calculated for users");
json_decref(result);
return; /* Nothing to do */
}
/* Now iterate through each */
json_object_foreach(responses, key, value)
{
Client *client = hash_find_id(key, NULL);
if (!client)
continue; /* Client disconnected already */
cbl_handle_response(client, value);
}
json_decref(result);
del_cbl_transfer(transfer);
}
void cbl_mdata_free(ModData *m)
{
CBLUser *cbl = (CBLUser *)m->ptr;
int i;
if (cbl)
{
json_decref(cbl->handshake);
for (i = 0; i < SPAMREPORT_NUM_REMEMBERED_CMDS; i++)
safe_free(cbl->last_cmds[i]);
safe_free(cbl);
m->ptr = NULL;
}
}
void send_request_for_pending_clients(void)
{
Client *client, *next;
OutgoingWebRequest *w;
json_t *j, *requests;
NameValuePrioList *headers = NULL;
int num;
char *json_serialized;
CBLTransfer *c;
NameList *clientlist = NULL;
num = downloads_in_progress();
if (num > cfg.max_downloads)
{
unreal_log(ULOG_WARNING, "central-blocklist", "CENTRAL_BLOCKLIST_TOO_MANY_CONCURRENT_REQUESTS", NULL,
"Already $num_requests HTTP(S) requests in progress.",
log_data_integer("num_requests", num));
return;
}
j = json_object();
json_object_set_new(j, "server", json_string_unreal(me.name));
json_object_set_new(j, "module_version", json_string_unreal(cbl_module->header->version));
json_object_set_new(j, "unrealircd_version", json_string_unreal(VERSIONONLY));
requests = json_object();
json_object_set_new(j, "requests", requests);
list_for_each_entry_safe(client, next, &unknown_list, lclient_node)
{
CBLUser *cbl = CBL(client);
if (cbl && cbl->request_pending)
{
// requests[clientid] => ["client"=>["nick"=>"xyz"...etc...
json_object_set_new(requests, client->id, json_deep_copy(cbl->handshake));
cbl->request_pending = 0;
cbl->request_sent = TStime();
add_name_list(clientlist, client->id);
}
}
json_serialized = json_dumps(j, JSON_COMPACT);
if (!json_serialized)
{
unreal_log(ULOG_WARNING, "central-blocklist", "CENTRAL_BLOCKLIST_BUG_SERIALIZE", client,
"Unable to serialize JSON request. Weird.");
json_decref(j);
free_entire_name_list(clientlist);
return;
}
json_decref(j);
add_nvplist(&headers, 0, "Content-Type", "application/json; charset=utf-8");
add_nvplist(&headers, 0, "X-API-Key", cfg.api_key);
c = add_cbl_transfer(clientlist);
/* Do the web request */
w = safe_alloc(sizeof(OutgoingWebRequest));
safe_strdup(w->url, cfg.url);
w->http_method = HTTP_METHOD_POST;
w->body = json_serialized;
w->headers = headers;
w->max_redirects = 1;
//w->callback = cbl_download_complete;
safe_strdup(w->apicallback, "cbl_download_complete");
w->callback_data = c;
#ifdef TLS1_3_VERSION
w->minimum_tls_version = TLS1_3_VERSION;
#endif
url_start_async(w);
}
int cbl_any_pending_clients(void)
{
Client *client, *next;
list_for_each_entry_safe(client, next, &unknown_list, lclient_node)
{
CBLUser *cbl = CBL(client);
if (cbl && cbl->request_pending)
return 1;
}
return 0;
}
EVENT(centralblocklist_bundle_requests)
{
if (cbl_any_pending_clients())
send_request_for_pending_clients();
}
/** Remember last # commands for SPAMREPORT */
CMD_OVERRIDE_FUNC(cbl_override_spamreport_gather)
{
if (MyUser(client) && CBL(client))
{
char record_cmd = 1;
if ((!strcmp(ovr->command->cmd, "PRIVMSG") || !strcmp(ovr->command->cmd, "NOTICE")) &&
(parc > 2) && !strchr(parv[1], '#'))
{
/* This is a private PRIVMSG/NOTICE */
record_cmd = 0;
}
if (record_cmd)
{
int slot = CBL(client)->last_cmds_slot; // just for readability below
safe_strdup(CBL(client)->last_cmds[slot], backupbuf);
if (clictx && clictx->textanalysis)
{
memcpy(&CBL(client)->last_cmds_textanalysis[slot], clictx->textanalysis, sizeof(TextAnalysis));
} else {
memset(&CBL(client)->last_cmds_textanalysis[slot], 0, sizeof(TextAnalysis));
}
CBL(client)->last_cmds_slot++;
if (CBL(client)->last_cmds_slot >= SPAMREPORT_NUM_REMEMBERED_CMDS)
CBL(client)->last_cmds_slot = 0;
}
}
CALL_NEXT_COMMAND_OVERRIDE();
}
int _central_spamreport(Client *client, Client *by, const char *url)
{
json_t *j, *requests, *data, *cmds, *item;
OutgoingWebRequest *w;
NameValuePrioList *headers = NULL;
int num;
char *json_serialized;
int i, start;
char number[16];
int cnt = 0;
if (!client)
return 0; /* We only support reporting clients, not clientless IP addresses */
if (!MyUser(client) || !CBL(client))
return 0; /* Only possible if hot-loading */
num = downloads_in_progress();
if (num > cfg.max_downloads)
{
unreal_log(ULOG_WARNING, "central-blocklist", "CENTRAL_BLOCKLIST_TOO_MANY_CONCURRENT_REQUESTS", NULL,
"Already $num_requests HTTP(S) requests in progress.",
log_data_integer("num_requests", num));
return 0;
}
j = json_object();
json_object_set_new(j, "server", json_string_unreal(me.name));
json_object_set_new(j, "module_version", json_string_unreal(cbl_module->header->version));
json_object_set_new(j, "unrealircd_version", json_string_unreal(VERSIONONLY));
if (by)
json_object_set_new(j, "reporter", json_string_unreal(by->name));
requests = json_object();
json_object_set_new(j, "reports", requests);
data = json_deep_copy(CBL(client)->handshake); /* .. deep copy. */
json_object_set_new(requests, client->id, data); /* ..and steal reference */
cmds = json_object();
json_object_set_new(data, "commands", cmds);
start = CBL(client)->last_cmds_slot;
for (i = start; i < SPAMREPORT_NUM_REMEMBERED_CMDS; i++)
{
if (CBL(client)->last_cmds[i])
{
// WARNING: duplicate code #1 of #2
snprintf(number, sizeof(number), "%d", ++cnt);
item = json_object();
json_object_set_new(item, "raw", json_string_unreal(CBL(client)->last_cmds[i]));
if (CBL(client)->last_cmds_textanalysis[i].num_bytes)
json_expand_textanalysis(item, "textanalysis", &CBL(client)->last_cmds_textanalysis[i], 2);
json_object_set_new(cmds, number, item);
}
}
for (i = 0; i < start; i++)
{
if (CBL(client)->last_cmds[i])
{
// WARNING: duplicate code #2 of #2
snprintf(number, sizeof(number), "%d", ++cnt);
item = json_object();
json_object_set_new(item, "raw", json_string_unreal(CBL(client)->last_cmds[i]));
if (CBL(client)->last_cmds_textanalysis[i].num_bytes)
json_expand_textanalysis(item, "textanalysis", &CBL(client)->last_cmds_textanalysis[i], 2);
json_object_set_new(cmds, number, item);
}
}
json_serialized = json_dumps(j, JSON_COMPACT);
if (!json_serialized)
{
unreal_log(ULOG_WARNING, "central-blocklist", "CENTRAL_BLOCKLIST_BUG_SERIALIZE", client,
"Unable to serialize JSON request. Weird.");
json_decref(j);
return 0;
}
json_decref(j);
add_nvplist(&headers, 0, "Content-Type", "application/json; charset=utf-8");
add_nvplist(&headers, 0, "X-API-Key", cfg.api_key);
/* Do the web request */
w = safe_alloc(sizeof(OutgoingWebRequest));
safe_strdup(w->url, url ? url : cfg.spamreport_url);
w->http_method = HTTP_METHOD_POST;
w->body = json_serialized;
w->headers = headers;
w->max_redirects = 1;
w->callback = download_complete_dontcare;
#ifdef TLS1_3_VERSION
w->minimum_tls_version = TLS1_3_VERSION;
#endif
url_start_async(w);
return 1;
}