diff --git a/doc/RELEASE-NOTES.md b/doc/RELEASE-NOTES.md index 7de58af5b..0dd4b0d29 100644 --- a/doc/RELEASE-NOTES.md +++ b/doc/RELEASE-NOTES.md @@ -5,6 +5,28 @@ This is the git version (development version) for future UnrealIRCd 6.2.6. This is work in progress and may not always be a stable version. ### Enhancements: +* Server bans and Spamfilters now have a unique ID, like `G7K2MP9WQX3`: + * The first letter denotes the type: `G` for gline, `K` for kline, + `Z` for (g)zline. + * The ID is shown to the affected user, so they can paste the ID back to + network staff. It is `$banid` in + [set::reject-message](https://www.unrealircd.org/docs/Set_block#set::reject-message) + and included by default. If you have a custom set::reject-message in + your config file then you will have to add it in manually. + * IRCOps see the ID in `STATS gline` and similar and can search for it + with e.g. `STATS gline +i G7K2MP9WQX3`. + * Spamfilters also have an ID, and we already had something similar that + could be used for `SPAMFILTER del ` that was only local-server. + For new spamfilters this is now a network wide ID as well. + * When a server ban was placed by a spamfilter, `STATS gline` shows the + originating spamfilter's ID, so it can be traced back. + * The `STATS gline` and other TKL stats changed format, see + *Developers and protocol* if you use scripts or your client depends + on the exact numeric format. + * For TKL IDs to reach all servers, all servers need to be on 6.2.6 + or later, especially the hubs. If there is one server in-between + that is older, then TKL IDs don't propagate properly and the ID + will be empty. ### Changes: * Spamfilter regexes now use more sensible defaults in terms of "max effort", @@ -34,6 +56,31 @@ This is work in progress and may not always be a stable version. `update_known_user_cache(client);` to update the known users cache. For example, if you have some authentication module that marks a user as IsLoggedIn. +* TKL entries can now use message tags to carry extra data, similar to how + `s2s-md` worked for early moddata, we have `s2s-tkl`. We have two at + the moment: + * `s2s-tkl/id`: network-wide unique ID for the TKL entry + * `s2s-tkl/spamfilter_id`: for server bans, if they were set by spamfilter, + the spamfilter TKL ID. + * Example: `@s2s-tkl/id=xxxx;s2s-tkl/spamfilter_id=yyyy :server TKL .....` + * Older servers will not pass these along, so for this functionality to + work all servers need to be on latest version, especially your hub. +* The TKL `STATS` numerics has more fields now, they are added right before + last (so before the `:reason` or `:regex`): + * `RPL_STATSGLINE` (223) ends with `hits lasthit spamfilter_id id :reason` + * `RPL_STATSQLINE` (217) ends with `hits lasthit id :reason` + * `RPL_STATSSPAMF` (229) ends with `lasthit lasthit_except id :regex` + (which comes right after `hits hits_except`, which was already there) + * `RPL_STATSEXCEPTTKL` (230) ends with `id :reason` + * An absent id or spamfilter_id is sent as `-`. The hits/lasthit/lasthit_except + fields are reserved and currently always `0`; FIXME in later commit. +* `banned_client()` has an extra parameter `const char *tklid` for the + TKL ID (or NULL if none). +* The tkldb database version is now 6260 and stores the id and spamfilter_id. + FIXME: also prepared for hit stats, so update this release note line in later commit. + Older databases still load. Downside: you cannot downgrade UnrealIRCd. +* JSON for TKL entries (server logs and JSON-RPC) now includes `id`, and + `spamfilter_id` for spamfilter-created server bans. UnrealIRCd 6.2.5 ----------------- diff --git a/include/h.h b/include/h.h index d24ff3b39..6cad8cd4e 100644 --- a/include/h.h +++ b/include/h.h @@ -953,7 +953,7 @@ extern MODVAR int (*decode_authenticate_plain)(const char *param, char **authori extern MODVAR void (*exit_client)(Client *client, MessageTag *recv_mtags, const char *comment); extern MODVAR void (*exit_client_fmt)(Client *client, MessageTag *recv_mtags, FORMAT_STRING(const char *pattern), ...) __attribute__((format(printf, 3, 4))); extern MODVAR void (*exit_client_ex)(Client *client, Client *origin, MessageTag *recv_mtags, const char *comment); -extern MODVAR void (*banned_client)(Client *client, const char *bantype, const char *reason, int global, int noexit); +extern MODVAR void (*banned_client)(Client *client, const char *bantype, const char *reason, const char *tklid, int global, int noexit); extern MODVAR char *(*unreal_expand_string)(const char *str, char *buf, size_t buflen, NameValuePrioList *nvp, int buildvarstring_options, Client *client); extern MODVAR char *(*utf8_convert_confusables)(const char *i, char *obuf, int olen); extern MODVAR const char *(*utf8_get_block_name)(int i); diff --git a/include/numeric.h b/include/numeric.h index 928525915..dd8b42baa 100644 --- a/include/numeric.h +++ b/include/numeric.h @@ -372,17 +372,17 @@ #define STR_RPL_STATSCOMMANDS /* 212 */ "%s %u %lu" #define STR_RPL_STATSCLINE /* 213 */ "%c %s * %s %d %d %s" #define STR_RPL_STATSILINE /* 215 */ "I %s %s %d %d %s %s %d" -#define STR_RPL_STATSQLINE /* 217 */ "%c %s %lld %lld %s :%s" +#define STR_RPL_STATSQLINE /* 217 */ "%c %s %lld %lld %s %lld %lld %s :%s" #define STR_RPL_STATSYLINE /* 218 */ "Y %s %d %d %d %d %d" #define STR_RPL_ENDOFSTATS /* 219 */ "%c :End of /STATS report" #define STR_RPL_UMODEIS /* 221 */ "%s" -#define STR_RPL_STATSGLINE /* 223 */ "%c %s %lld %lld %s :%s" +#define STR_RPL_STATSGLINE /* 223 */ "%c %s %lld %lld %s %lld %lld %s %s :%s" #define STR_RPL_STATSTLINE /* 224 */ "T %s %s %s" #define STR_RPL_STATSNLINE /* 226 */ "n %s %s" #define STR_RPL_STATSVLINE /* 227 */ "v %s %s %s" #define STR_RPL_STATSBANVER /* 228 */ "%s %s" -#define STR_RPL_STATSSPAMF /* 229 */ "%c %s %s %s %lld %lld %lld %s %s %lld %lld :%s" -#define STR_RPL_STATSEXCEPTTKL /* 230 */ "%s %s %lld %lld %s :%s" +#define STR_RPL_STATSSPAMF /* 229 */ "%c %s %s %s %lld %lld %lld %s %s %lld %lld %lld %lld %s :%s" +#define STR_RPL_STATSEXCEPTTKL /* 230 */ "%s %s %lld %lld %s %s :%s" #define STR_RPL_RULES /* 232 */ ":- %s" #define STR_RPL_STATSLLINE /* 241 */ "%c %s * %s %d %d" #define STR_RPL_STATSUPTIME /* 242 */ ":Server Up %lld days, %lld:%02lld:%02lld" diff --git a/include/struct.h b/include/struct.h index 5ec08e1b1..2f8e617bd 100644 --- a/include/struct.h +++ b/include/struct.h @@ -204,7 +204,6 @@ typedef OperPermission (*OperClassEntryEvalCallback)(OperClassACLEntryVar* varia #define MAXCCUSERS 20 /* Maximum for set::anti-flood::max-concurrent-conversations */ #define BATCHLEN 22 #define MAXBATCHREFLEN 48 /* Max length of client-sent batch reference tags */ -#define MAXSPAMFILTERIDLEN 24 /* * Watch it - Don't change this unless you also change the ERR_TOOMANYWATCH @@ -1319,6 +1318,8 @@ struct BanException { #define TKL_FLAG_CONFIG 0x0001 /* Entry from configuration file. Cannot be removed by using commands. */ #define TKL_FLAG_CENTRAL_SPAMFILTER 0x0002 /* Entry from central spamfilter. */ +#define TKLIDLEN 16 /**< TKL id buffer size: native 'prefix+body' or an external id. Max 15 chars + NUL. */ + /** A TKL entry, such as a KLINE, GLINE, Spamfilter, QLINE, Exception, .. */ struct TKL { TKL *prev, *next; @@ -1327,6 +1328,8 @@ struct TKL { char *set_by; /**< By who was this entry added */ time_t set_at; /**< When this entry was added */ time_t expire_at; /**< When this entry will expire */ + char id[TKLIDLEN]; /**< Unique ID: assigned (random, generated by originating server) or external (eg from services), or empty string if none */ + char spamfilter_id[TKLIDLEN]; /**< For server bans created by a spamfilter: that spamfilter's id. Empty otherwise. */ union { Spamfilter *spamfilter; ServerBan *serverban; diff --git a/src/api-efunctions.c b/src/api-efunctions.c index 359a775f4..31e205f17 100644 --- a/src/api-efunctions.c +++ b/src/api-efunctions.c @@ -184,7 +184,7 @@ int (*decode_authenticate_plain)(const char *param, char **authorization_id, cha void (*exit_client)(Client *client, MessageTag *recv_mtags, const char *comment); void (*exit_client_fmt)(Client *client, MessageTag *recv_mtags, FORMAT_STRING(const char *pattern), ...); void (*exit_client_ex)(Client *client, Client *origin, MessageTag *recv_mtags, const char *comment); -void (*banned_client)(Client *client, const char *bantype, const char *reason, int global, int noexit); +void (*banned_client)(Client *client, const char *bantype, const char *reason, const char *tklid, int global, int noexit); char *(*unreal_expand_string)(const char *str, char *buf, size_t buflen, NameValuePrioList *nvp, int buildvarstring_options, Client *client); char *(*utf8_convert_confusables)(const char *i, char *obuf, int olen); const char *(*utf8_get_block_name)(int i); diff --git a/src/conf.c b/src/conf.c index 3afb3d8c8..0e8e7c968 100644 --- a/src/conf.c +++ b/src/conf.c @@ -1935,8 +1935,8 @@ void config_setdefaultsettings(Configuration *i) safe_strdup(i->reject_message_too_many_new_connections_ipv6_range, "Too many new connections from this IPv6 range ($prefix_addr/$prefix_len) [connthrottle]"); safe_strdup(i->reject_message_server_full, "This server is full"); safe_strdup(i->reject_message_unauthorized, "You are not authorized to connect to this server"); - safe_strdup(i->reject_message_kline, "You are not welcome on this server. $bantype: $banreason. Email $klineaddr for more information."); - safe_strdup(i->reject_message_gline, "You are not welcome on this network. $bantype: $banreason. Email $glineaddr for more information."); + safe_strdup(i->reject_message_kline, "You are not welcome on this server. $bantype: $banreason. Email $klineaddr for more information.$banid"); + safe_strdup(i->reject_message_gline, "You are not welcome on this network. $bantype: $banreason. Email $glineaddr for more information.$banid"); i->topic_setter = SETTER_NICK_USER_HOST; i->ban_setter = SETTER_NICK_USER_HOST; diff --git a/src/json.c b/src/json.c index a586567b1..61ac6f476 100644 --- a/src/json.c +++ b/src/json.c @@ -565,6 +565,8 @@ void json_expand_tkl(json_t *root, const char *key, TKL *tkl, int detail) json_object_set_new(j, "type", json_string_unreal(tkl_type_config_string(tkl))); // Eg 'kline' json_object_set_new(j, "type_string", json_string_unreal(tkl_type_string(tkl))); // Eg 'Soft K-Line' json_object_set_new(j, "set_by", json_string_unreal(tkl->set_by)); + if (tkl->id[0]) + json_object_set_new(j, "id", json_string_unreal(tkl->id)); json_object_set_new(j, "set_at", json_timestamp(tkl->set_at)); json_object_set_new(j, "expire_at", json_timestamp(tkl->expire_at)); *buf = '\0'; @@ -592,6 +594,8 @@ void json_expand_tkl(json_t *root, const char *key, TKL *tkl, int detail) else json_object_set_new(j, "name", json_string_unreal(tkl_uhost(tkl, buf, sizeof(buf), 0))); json_object_set_new(j, "reason", json_string_unreal(tkl->ptr.serverban->reason)); + if (tkl->spamfilter_id[0]) + json_object_set_new(j, "spamfilter_id", json_string_unreal(tkl->spamfilter_id)); } else if (TKLIsNameBan(tkl)) { diff --git a/src/misc.c b/src/misc.c index a6c93585f..af3362486 100644 --- a/src/misc.c +++ b/src/misc.c @@ -2967,7 +2967,7 @@ const char *command_issued_by_rpc(MessageTag *mtags) /** Is 's' a valid spamfilter id? A-Z, 0-9 and _ and with a max length. */ int valid_spamfilter_id(const char *s) { - if (strlen(s) > MAXSPAMFILTERIDLEN) + if (strlen(s) >= TKLIDLEN) return 0; return 1; } diff --git a/src/modules/chgname.c b/src/modules/chgname.c index bac4fcd1d..b4b628f9b 100644 --- a/src/modules/chgname.c +++ b/src/modules/chgname.c @@ -124,7 +124,7 @@ CMD_FUNC(cmd_chgname) if (!ValidatePermissionsForPath("immune:server-ban:ban-realname",target,NULL,NULL,NULL) && ((bconf = find_ban(NULL, target->info, CONF_BAN_REALNAME)))) { - banned_client(target, "realname", bconf->reason?bconf->reason:"", 0, 0); + banned_client(target, "realname", bconf->reason?bconf->reason:"", NULL, 0, 0); return; } } diff --git a/src/modules/nick.c b/src/modules/nick.c index cc252cafd..75dee86e4 100644 --- a/src/modules/nick.c +++ b/src/modules/nick.c @@ -1097,7 +1097,7 @@ int _register_user(Client *client) if ((bconf = find_ban(NULL, client->info, CONF_BAN_REALNAME))) { ircstats.is_ref++; - banned_client(client, "realname", bconf->reason?bconf->reason:"", 0, 0); + banned_client(client, "realname", bconf->reason?bconf->reason:"", NULL, 0, 0); return 0; } /* Check G/Z lines before shuns -- kill before quite -- codemastr */ diff --git a/src/modules/quit.c b/src/modules/quit.c index 54e62232b..c4767e07c 100644 --- a/src/modules/quit.c +++ b/src/modules/quit.c @@ -38,7 +38,7 @@ CMD_FUNC(cmd_quit); void _exit_client(Client *client, MessageTag *recv_mtags, const char *comment); void _exit_client_fmt(Client *client, MessageTag *recv_mtags, FORMAT_STRING(const char *pattern), ...) __attribute__((format(printf, 3, 4))); void _exit_client_ex(Client *client, Client *origin, MessageTag *recv_mtags, const char *comment); -void _banned_client(Client *client, const char *bantype, const char *reason, int global, int noexit); +void _banned_client(Client *client, const char *bantype, const char *reason, const char *tklid, int global, int noexit); static void remove_dependents(Client *client, Client *from, MessageTag *mtags, const char *comment, const char *splitstr); static void exit_one_client(Client *, MessageTag *mtags_i, const char *); static int should_hide_ban_reason(Client *client, const char *reason); @@ -491,11 +491,12 @@ static void exit_one_client(Client *client, MessageTag *mtags_i, const char *com * * @note This function will call exit_client() appropriately. */ -void _banned_client(Client *client, const char *bantype, const char *reason, int global, int noexit) +void _banned_client(Client *client, const char *bantype, const char *reason, const char *tklid, int global, int noexit) { char buf[512]; + char idbuf[64]; char *fmt = global ? iConf.reject_message_gline : iConf.reject_message_kline; - const char *vars[6], *values[6]; + const char *vars[7], *values[7]; MessageTag *mtags = NULL; if (!MyConnect(client)) @@ -503,6 +504,14 @@ void _banned_client(Client *client, const char *bantype, const char *reason, int RunHook(HOOKTYPE_BANNED_CLIENT, client, bantype, reason, global); + /* The " [ID: xxx]" fragment, empty when there is no id. Used both as the $banid + * reject-message variable and appended to the quit reason / real-quit-reason mtag. + */ + if (!BadPtr(tklid)) + snprintf(idbuf, sizeof(idbuf), " [ID: %s]", tklid); + else + idbuf[0] = '\0'; + /* This was: "You are not welcome on this %s. %s: %s. %s" but is now dynamic: */ vars[0] = "bantype"; values[0] = bantype; @@ -514,8 +523,10 @@ void _banned_client(Client *client, const char *bantype, const char *reason, int values[3] = GLINE_ADDRESS ? GLINE_ADDRESS : KLINE_ADDRESS; /* fallback to klineaddr */ vars[4] = "ip"; values[4] = GetIP(client); - vars[5] = NULL; - values[5] = NULL; + vars[5] = "banid"; + values[5] = idbuf; + vars[6] = NULL; + values[6] = NULL; buildvarstring(fmt, buf, sizeof(buf), vars, values); /* This is a bit extensive but we will send both a YOUAREBANNEDCREEP @@ -537,13 +548,13 @@ void _banned_client(Client *client, const char *bantype, const char *reason, int /* Hide the ban reason, but put the real reason in unrealircd.org/real-quit-reason */ MessageTag *m = safe_alloc(sizeof(MessageTag)); safe_strdup(m->name, "unrealircd.org/real-quit-reason"); - snprintf(buf, sizeof(buf), "Banned (%s): %s", bantype, reason); + snprintf(buf, sizeof(buf), "Banned (%s): %s%s", bantype, reason, idbuf); safe_strdup(m->value, buf); AddListItem(m, mtags); /* And the quit reason for anyone else, goes here.. */ - snprintf(buf, sizeof(buf), "Banned (%s)", bantype); + snprintf(buf, sizeof(buf), "Banned (%s)%s", bantype, idbuf); } else { - snprintf(buf, sizeof(buf), "Banned (%s): %s", bantype, reason); + snprintf(buf, sizeof(buf), "Banned (%s): %s%s", bantype, reason, idbuf); } if (noexit != NO_EXIT_CLIENT) diff --git a/src/modules/setname.c b/src/modules/setname.c index 6fa190e82..426ad6832 100644 --- a/src/modules/setname.c +++ b/src/modules/setname.c @@ -145,7 +145,7 @@ CMD_FUNC(cmd_setname) if (!ValidatePermissionsForPath("immune:server-ban:ban-realname",client,NULL,NULL,NULL) && ((bconf = find_ban(NULL, client->info, CONF_BAN_REALNAME)))) { - banned_client(client, "realname", bconf->reason?bconf->reason:"", 0, 0); + banned_client(client, "realname", bconf->reason?bconf->reason:"", NULL, 0, 0); return; } } else { diff --git a/src/modules/tkl.c b/src/modules/tkl.c index d03e76af0..543d25f2c 100644 --- a/src/modules/tkl.c +++ b/src/modules/tkl.c @@ -175,6 +175,25 @@ int confusables_spamfilters_present = 0; /**< Are any spamfilters with input-con long previous_spamfilter_utf8 = 0; static int firstboot = 0; +/* s2s-tkl/: per-TKL fields carried over S2S as @s2s-tkl/= on the + * TKL command (modeled on s2s-md/). Old servers ignore unknown tags. To add a field, + * add a row here plus its serialize/unserialize pair (defined further down). + */ +typedef struct { + const char *name; + const char *(*serialize)(TKL *tkl); /**< value to send, or NULL to omit the tag */ + void (*unserialize)(TKL *tkl, const char *value); +} TKLS2SField; +static const char *tkl_s2s_get_id(TKL *tkl); +static void tkl_s2s_set_id(TKL *tkl, const char *value); +static const char *tkl_s2s_get_spamfilter_id(TKL *tkl); +static void tkl_s2s_set_spamfilter_id(TKL *tkl, const char *value); +static const TKLS2SField tkl_s2s_fields[] = { + { "id", tkl_s2s_get_id, tkl_s2s_set_id }, + { "spamfilter_id", tkl_s2s_get_spamfilter_id, tkl_s2s_set_spamfilter_id }, + { NULL, NULL, NULL }, +}; + MOD_TEST() { MARK_AS_OFFICIAL_MODULE(modinfo); @@ -320,7 +339,7 @@ int tkl_config_test_spamfilter(ConfigFile *cf, ConfigEntry *ce, int type, int *e { config_error("%s:%i: spamfilter::id invalid: maximum size (%d chars) exceeded " "or forbidden characters encountered: only A-Z, 0-9 and _ are permitted.", - cep->file->filename, cep->line_number, MAXSPAMFILTERIDLEN); + cep->file->filename, cep->line_number, TKLIDLEN-1); errors++; } } else @@ -1272,12 +1291,209 @@ void check_set_spamfilter_utf8_setting_changed(void) previous_spamfilter_utf8 = iConf.spamfilter_utf8; } -/** Return unique spamfilter id for TKL */ -char *spamfilter_id(TKL *tk) -{ - static char buf[128]; +/* === TKL unique IDs === + * Every TKL has a unique id (struct TKL.id): either assigned (a random id generated + * by the server that originates the entry, or an id supplied by an external setter + * such as services), or an empty string if none is known. A native (assigned-here) + * id is <10 x base32>, eg "G7K2MP9WQX3". It is carried over S2S in the + * s2s-tkl/id message tag and persisted by tkldb; there is no derivation. + */ - snprintf(buf, sizeof(buf), "%p", (void *)tk); +/** Single-letter prefix for a native TKL id, derived from the TKL type. + * Uppercase, with global/local collapsed (eg gzline and zline both 'Z', spamfilter + * and local-spamfilter both 'F'). gline stays 'G' and kline 'K' as their letters differ. + */ +static char tkl_id_prefix(int type) +{ + return toupper(_tkl_typetochar(type)); +} + +/** Find a TKL by its exact id (case-insensitive). Returns NULL on no match or empty id. */ +static TKL *find_tkl_by_id(const char *id) +{ + TKL *tkl; + int index, index2; + + if (BadPtr(id)) + return NULL; + + for (index = 0; index < TKLIPHASHLEN1; index++) + for (index2 = 0; index2 < TKLIPHASHLEN2; index2++) + for (tkl = tklines_ip_hash[index][index2]; tkl; tkl = tkl->next) + if (!strcasecmp(tkl->id, id)) + return tkl; + + for (index = 0; index < TKLISTLEN; index++) + for (tkl = tklines[index]; tkl; tkl = tkl->next) + if (!strcasecmp(tkl->id, id)) + return tkl; + + return NULL; +} + +/** Assign a fresh, locally-unique random id to 'tkl' (overwrites tkl->id). + * Called when this server is the origin of the entry. + */ +static void tkl_generate_id(TKL *tkl) +{ + /* Crockford base32 (no I/L/O/U) so the id survives being read aloud */ + static const char b32[] = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"; + char prefix = tkl_id_prefix(tkl->type); + TKL *other; + int attempt, i; + + for (attempt = 0; attempt < 16; attempt++) + { + tkl->id[0] = prefix; + for (i = 1; i <= 10; i++) + tkl->id[i] = b32[getrandom8() % 32]; + tkl->id[i] = '\0'; + other = find_tkl_by_id(tkl->id); + if (!other || (other == tkl)) + return; /* unique (or only matches ourselves, if already linked) */ + } + /* 16 collisions in a row is astronomically unlikely; keep the last candidate. */ +} + +/* s2s-tkl/xxx message-tag field handlers */ + +static const char *tkl_s2s_get_id(TKL *tkl) +{ + return tkl->id[0] ? tkl->id : NULL; +} + +static void tkl_s2s_set_id(TKL *tkl, const char *value) +{ + if (!BadPtr(value) && (strlen(value) < sizeof(tkl->id))) + strlcpy(tkl->id, value, sizeof(tkl->id)); +} + +static const char *tkl_s2s_get_spamfilter_id(TKL *tkl) +{ + return tkl->spamfilter_id[0] ? tkl->spamfilter_id : NULL; +} + +static void tkl_s2s_set_spamfilter_id(TKL *tkl, const char *value) +{ + if (!BadPtr(value) && (strlen(value) < sizeof(tkl->spamfilter_id))) + strlcpy(tkl->spamfilter_id, value, sizeof(tkl->spamfilter_id)); +} + +/** Append s2s-tkl/ message tags describing 'tkl' onto '*mtags' (for sending over S2S). */ +static void tkl_add_s2s_mtags(TKL *tkl, MessageTag **mtags) +{ + const TKLS2SField *f; + const char *value; + MessageTag *m; + char name[64]; + + for (f = tkl_s2s_fields; f->name; f++) + { + value = f->serialize(tkl); + if (!value) + continue; + snprintf(name, sizeof(name), "s2s-tkl/%s", f->name); + m = safe_alloc(sizeof(MessageTag)); + safe_strdup(m->name, name); + safe_strdup(m->value, value); + AddListItem(m, *mtags); + } +} + +/** Build a one-element s2s-tkl/spamfilter_id message tag from 'spamfilter_id', or NULL if it is empty. + * Lets a locally-created server ban carry its spamfilter id through the same path as a remote one. + */ +static MessageTag *tkl_spamfilter_id_mtag(const char *spamfilter_id) +{ + MessageTag *m; + + if (BadPtr(spamfilter_id)) + return NULL; + m = safe_alloc(sizeof(MessageTag)); + safe_strdup(m->name, "s2s-tkl/spamfilter_id"); + safe_strdup(m->value, spamfilter_id); + return m; +} + +/** Read s2s-tkl/ message tags from 'mtags' into 'tkl' (on receiving an S2S TKL command). */ +static void tkl_extract_s2s_mtags(TKL *tkl, MessageTag *mtags) +{ + MessageTag *m; + const TKLS2SField *f; + + for (m = mtags; m; m = m->next) + { + if (strncmp(m->name, "s2s-tkl/", 8)) + continue; + for (f = tkl_s2s_fields; f->name; f++) + { + if (!strcmp(f->name, m->name + 8)) + { + f->unserialize(tkl, m->value); + break; + } + } + } +} + +/** Return the value of the s2s-tkl/ message tag in 'mtags', or NULL if absent. + * Used by the merge logic to peek at an incoming field without overwriting the existing entry. + */ +static const char *tkl_s2s_mtag_value(MessageTag *mtags, const char *field) +{ + MessageTag *m; + char name[64]; + + snprintf(name, sizeof(name), "s2s-tkl/%s", field); + for (m = mtags; m; m = m->next) + if (!strcmp(m->name, name)) + return m->value; + return NULL; +} + +/** A fallback spamfilter id (not the normal randomly generated ID). + * This ID is a hash based on (target, action, match method+string). So when those + * properties are the same, it will create the same ID. Used for: + * - Config spamfilter or Central Spamfilter without an explicit id. + * For these it behaves very much like a regular tkl->id, is shown in STATS etc. + * These entries never propagate servers, as they are local, but it is still nice + * for the ID to be unique on each server if you use the exact same spamfilter + * (think remote includes or central spamfilter). + * - Global spamfilter with a blank id (eg from an older server). + * In this case it is ONLY used for '/SPAMFILTER del' and not in regular STATS + * or used as a true id. + * And to be clear: for both cases they never appear in S2S and not on disk either + * (it is computed, each time). + */ +static const char *spamfilter_fallback_id(TKL *tkl) +{ + static const char b32[] = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"; + /* Fixed, non-secret key: the fallback id is opers-only and not security-sensitive, the + * key just makes the hash deterministic and identical across servers. */ + static const char key[16] = "UnrealIRCd rocks"; + static char buf[16]; + char content[8192]; + char actbuf[2]; + uint64_t h; + int i; + + actbuf[0] = banact_valtochar(tkl->ptr.spamfilter->action->action); + actbuf[1] = '\0'; + snprintf(content, sizeof(content), "%s\t%s\t%s\t%s", + spamfilter_target_inttostring(tkl->ptr.spamfilter->target), + actbuf, + unreal_match_method_valtostr(tkl->ptr.spamfilter->match->type), + tkl->ptr.spamfilter->match->str); + + h = siphash(content, key); + + buf[0] = tkl_id_prefix(tkl->type); + for (i = 1; i <= 10; i++) + { + buf[i] = b32[h & 31]; + h >>= 5; + } + buf[i] = '\0'; return buf; } @@ -1291,9 +1507,8 @@ static void spamfilter_regex_error(TKL *tkl, const char *regex_error) { unreal_log(ULOG_WARNING, "tkl", "SPAMFILTER_REGEX_ERROR", NULL, "[Spamfilter] Regex aborted ($regex_error) for '$tkl'. Possibly too complex regex? " - "To delete, use: /SPAMFILTER del $spamfilter_id", + "To delete, use: /SPAMFILTER del $tkl.id", log_data_string("regex_error", regex_error), - log_data_string("spamfilter_id", spamfilter_id(tkl)), log_data_tkl("tkl", tkl)); } else { unreal_log(ULOG_WARNING, "tkl", "SPAMFILTER_REGEX_ERROR", NULL, @@ -1308,7 +1523,7 @@ int tkl_ip_change(Client *client, const char *oldip) { TKL *tkl; if ((tkl = find_tkline_match_zap(client))) - banned_client(client, "Z-Lined", tkl->ptr.serverban->reason, (tkl->type & TKL_GLOBAL)?1:0, NO_EXIT_CLIENT); + banned_client(client, "Z-Lined", tkl->ptr.serverban->reason, tkl->id, (tkl->type & TKL_GLOBAL)?1:0, NO_EXIT_CLIENT); return 0; } @@ -1317,7 +1532,7 @@ int tkl_accept(Client *client) TKL *tkl; if ((tkl = find_tkline_match_zap(client))) { - banned_client(client, "Z-Lined", tkl->ptr.serverban->reason, (tkl->type & TKL_GLOBAL)?1:0, NO_EXIT_CLIENT); + banned_client(client, "Z-Lined", tkl->ptr.serverban->reason, tkl->id, (tkl->type & TKL_GLOBAL)?1:0, NO_EXIT_CLIENT); return 2; // TODO: HOOK_DENY_ALWAYS; } return 0; @@ -2431,7 +2646,8 @@ void spamfilter_del_by_id(Client *client, const char *id) { int index; TKL *tk; - int found = 0; + int matches = 0; + TKL *match = NULL; char mo[32], mo2[32]; const char *tkllayer[13] = { me.name, /* 0 server.name */ @@ -2453,21 +2669,30 @@ void spamfilter_del_by_id(Client *client, const char *id) { for (tk = tklines[index]; tk; tk = tk->next) { - if (((tk->type & (TKL_GLOBAL|TKL_SPAMF)) == (TKL_GLOBAL|TKL_SPAMF)) && !strcmp(spamfilter_id(tk), id)) + if ((tk->type & (TKL_GLOBAL|TKL_SPAMF)) != (TKL_GLOBAL|TKL_SPAMF)) + continue; + /* Match the real id, or for an id-less spamfilter (eg from an old server) + * its locally-computed fallback handle. + */ + if (tk->id[0] ? !strcmp(tk->id, id) : !strcmp(spamfilter_fallback_id(tk), id)) { - found = 1; - break; + match = tk; + matches++; } } - if (found) - break; /* break outer loop */ } - if (!tk) + if (matches == 0) { sendnotice(client, "Sorry, no spamfilter found with that ID. Did you run '/spamfilter del' to get the appropriate id?"); return; } + if (matches > 1) + { + sendnotice(client, "That ID is ambiguous (it matches multiple spamfilters). Please remove the spamfilter by its full definition instead."); + return; + } + tk = match; /* Spamfilter found. Now fill the tkllayer */ tkllayer[1] = "-"; @@ -3000,6 +3225,19 @@ TKL *_tkl_add_spamfilter(int type, const char *id, unsigned short target, BanAct safe_strdup(tkl->ptr.spamfilter->id, id); tkl->ptr.spamfilter->show_message_content_on_hit = show_message_content_on_hit; + /* Config (and central) spamfilters are local per server, so they never receive + * a random network id. Give them a deterministic id instead, identical on every + * server: the config/central id label if set, otherwise a content hash. This is + * what lets a central spamfilter be referenced by the same id on every server. + */ + if (flags & (TKL_FLAG_CONFIG | TKL_FLAG_CENTRAL_SPAMFILTER)) + { + if (!BadPtr(id)) + strlcpy(tkl->id, id, sizeof(tkl->id)); + else + strlcpy(tkl->id, spamfilter_fallback_id(tkl), sizeof(tkl->id)); + } + if (tkl->ptr.spamfilter->target & SPAMF_USER) loop.do_bancheck_spamf_user = 1; if (tkl->ptr.spamfilter->target & SPAMF_AWAY) @@ -3679,15 +3917,15 @@ int _find_tkline_match(Client *client, int skip_soft) { ircstats.is_ref++; if (tkl->type & TKL_GLOBAL) - banned_client(client, "G-Lined", tkl->ptr.serverban->reason, 1, 0); + banned_client(client, "G-Lined", tkl->ptr.serverban->reason, tkl->id, 1, 0); else - banned_client(client, "K-Lined", tkl->ptr.serverban->reason, 0, 0); + banned_client(client, "K-Lined", tkl->ptr.serverban->reason, tkl->id, 0, 0); return 1; /* killed */ } else if (tkl->type & TKL_ZAP) { ircstats.is_ref++; - banned_client(client, "Z-Lined", tkl->ptr.serverban->reason, (tkl->type & TKL_GLOBAL)?1:0, 0); + banned_client(client, "Z-Lined", tkl->ptr.serverban->reason, tkl->id, (tkl->type & TKL_GLOBAL)?1:0, 0); return 1; /* killed */ } @@ -3929,12 +4167,15 @@ TKL *_find_tkline_match_zap(Client *client) #define NOT_BY_REASON 0x8 #define BY_SETBY 0x10 #define NOT_BY_SETBY 0x20 +#define BY_ID 0x40 +#define NOT_BY_ID 0x80 typedef struct { int flags; const char *mask; const char *reason; const char *set_by; + const char *id; } TKLFlag; /** Parse STATS tkl parameters. @@ -3989,6 +4230,15 @@ static void parse_stats_params(const char *para, TKLFlag *flag) flag->flags |= NOT_BY_SETBY; flag->set_by = tmp; break; + case 'i': + if (flag->id || !(tmp = strtok(NULL, " "))) + continue; + if (what == '+') + flag->flags |= BY_ID; + else + flag->flags |= NOT_BY_ID; + flag->id = tmp; + break; } } } @@ -3998,6 +4248,9 @@ static void parse_stats_params(const char *para, TKLFlag *flag) */ int tkl_stats_matcher(Client *client, int type, const char *para, TKLFlag *tklflags, TKL *tkl) { + const char *id_str = tkl->id[0] ? tkl->id : "-"; + const char *spamfilter_id_str = tkl->spamfilter_id[0] ? tkl->spamfilter_id : "-"; + /***** First, handle the selection ******/ if (!BadPtr(para)) @@ -4008,6 +4261,12 @@ int tkl_stats_matcher(Client *client, int type, const char *para, TKLFlag *tklfl if (tklflags->flags & NOT_BY_SETBY) if (match_simple(tklflags->set_by, tkl->set_by)) return 0; + if (tklflags->flags & BY_ID) + if (!match_simple(tklflags->id, tkl->id)) + return 0; + if (tklflags->flags & NOT_BY_ID) + if (match_simple(tklflags->id, tkl->id)) + return 0; if (TKLIsServerBan(tkl)) { if (tklflags->flags & BY_MASK) @@ -4078,7 +4337,7 @@ int tkl_stats_matcher(Client *client, int type, const char *para, TKLFlag *tklfl { sendnumeric(client, RPL_STATSGLINE, 'K', namevalue_nospaces(m), (tkl->expire_at != 0) ? (long long)(tkl->expire_at - TStime()) : 0, - (long long)(TStime() - tkl->set_at), tkl->set_by, tkl->ptr.serverban->reason); + (long long)(TStime() - tkl->set_at), tkl->set_by, (long long)0, (long long)0, spamfilter_id_str, id_str, tkl->ptr.serverban->reason); } } else { @@ -4088,31 +4347,31 @@ int tkl_stats_matcher(Client *client, int type, const char *para, TKLFlag *tklfl { sendnumeric(client, RPL_STATSGLINE, 'G', uhost, (tkl->expire_at != 0) ? (long long)(tkl->expire_at - TStime()) : 0, - (long long)(TStime() - tkl->set_at), tkl->set_by, tkl->ptr.serverban->reason); + (long long)(TStime() - tkl->set_at), tkl->set_by, (long long)0, (long long)0, spamfilter_id_str, id_str, tkl->ptr.serverban->reason); } else if (tkl->type == (TKL_ZAP | TKL_GLOBAL)) { sendnumeric(client, RPL_STATSGLINE, 'Z', uhost, (tkl->expire_at != 0) ? (long long)(tkl->expire_at - TStime()) : 0, - (long long)(TStime() - tkl->set_at), tkl->set_by, tkl->ptr.serverban->reason); + (long long)(TStime() - tkl->set_at), tkl->set_by, (long long)0, (long long)0, spamfilter_id_str, id_str, tkl->ptr.serverban->reason); } else if (tkl->type == (TKL_SHUN | TKL_GLOBAL)) { sendnumeric(client, RPL_STATSGLINE, 's', uhost, (tkl->expire_at != 0) ? (long long)(tkl->expire_at - TStime()) : 0, - (long long)(TStime() - tkl->set_at), tkl->set_by, tkl->ptr.serverban->reason); + (long long)(TStime() - tkl->set_at), tkl->set_by, (long long)0, (long long)0, spamfilter_id_str, id_str, tkl->ptr.serverban->reason); } else if (tkl->type == (TKL_KILL)) { sendnumeric(client, RPL_STATSGLINE, 'K', uhost, (tkl->expire_at != 0) ? (long long)(tkl->expire_at - TStime()) : 0, - (long long)(TStime() - tkl->set_at), tkl->set_by, tkl->ptr.serverban->reason); + (long long)(TStime() - tkl->set_at), tkl->set_by, (long long)0, (long long)0, spamfilter_id_str, id_str, tkl->ptr.serverban->reason); } else if (tkl->type == (TKL_ZAP)) { sendnumeric(client, RPL_STATSGLINE, 'z', uhost, (tkl->expire_at != 0) ? (long long)(tkl->expire_at - TStime()) : 0, - (long long)(TStime() - tkl->set_at), tkl->set_by, tkl->ptr.serverban->reason); + (long long)(TStime() - tkl->set_at), tkl->set_by, (long long)0, (long long)0, spamfilter_id_str, id_str, tkl->ptr.serverban->reason); } } } else @@ -4130,18 +4389,17 @@ int tkl_stats_matcher(Client *client, int type, const char *para, TKLFlag *tklfl tkl->set_by, tkl->ptr.spamfilter->hits, tkl->ptr.spamfilter->hits_except, + (long long)0, + (long long)0, + id_str, tkl->ptr.spamfilter->match->str); if (para && !strcasecmp(para, "del")) { - char *hash = spamfilter_id(tkl); - if (tkl->type & TKL_GLOBAL) - { - sendtxtnumeric(client, "To delete this spamfilter, use /SPAMFILTER del %s", hash); - sendtxtnumeric(client, "-"); - } else { + if (!(tkl->type & TKL_GLOBAL)) sendtxtnumeric(client, "This spamfilter is stored in the configuration file and cannot be removed with /SPAMFILTER del"); - sendtxtnumeric(client, "-"); - } + else + sendtxtnumeric(client, "To delete this spamfilter, use /SPAMFILTER del %s", tkl->id[0] ? tkl->id : spamfilter_fallback_id(tkl)); + sendtxtnumeric(client, "-"); } } else if (TKLIsNameBan(tkl)) @@ -4152,6 +4410,9 @@ int tkl_stats_matcher(Client *client, int type, const char *para, TKLFlag *tklfl (tkl->expire_at != 0) ? (long long)(tkl->expire_at - TStime()) : 0, (long long)(TStime() - tkl->set_at), tkl->set_by, + (long long)0, + (long long)0, + id_str, tkl->ptr.nameban->reason); } else if (TKLIsBanException(tkl)) @@ -4165,7 +4426,7 @@ int tkl_stats_matcher(Client *client, int type, const char *para, TKLFlag *tklfl sendnumeric(client, RPL_STATSEXCEPTTKL, namevalue_nospaces(m), tkl->ptr.banexception->bantypes, (tkl->expire_at != 0) ? (long long)(tkl->expire_at - TStime()) : 0, - (long long)(TStime() - tkl->set_at), tkl->set_by, tkl->ptr.banexception->reason); + (long long)(TStime() - tkl->set_at), tkl->set_by, id_str, tkl->ptr.banexception->reason); } } else { /* IRC-added: uses simple user/host mask */ @@ -4174,7 +4435,7 @@ int tkl_stats_matcher(Client *client, int type, const char *para, TKLFlag *tklfl sendnumeric(client, RPL_STATSEXCEPTTKL, uhost, tkl->ptr.banexception->bantypes, (tkl->expire_at != 0) ? (long long)(tkl->expire_at - TStime()) : 0, - (long long)(TStime() - tkl->set_at), tkl->set_by, tkl->ptr.banexception->reason); + (long long)(TStime() - tkl->set_at), tkl->set_by, id_str, tkl->ptr.banexception->reason); } } else { @@ -4255,6 +4516,7 @@ void _tkl_stats(Client *client, int type, const char *para, int *cnt) */ void tkl_sync_send_entry(int add, Client *sender, Client *to, TKL *tkl) { + MessageTag *mtags = NULL; char typ; if (!(tkl->type & TKL_GLOBAL)) @@ -4262,9 +4524,13 @@ void tkl_sync_send_entry(int add, Client *sender, Client *to, TKL *tkl) typ = tkl_typetochar(tkl->type); + /* Carry the s2s-tkl/ fields (id, spamfilter_id) on add; del matches by mask so needs no tags. */ + if (add) + tkl_add_s2s_mtags(tkl, &mtags); + if (TKLIsServerBan(tkl)) { - sendto_one(to, NULL, ":%s TKL %c %c %s%s %s %s %lld %lld :%s", sender->name, + sendto_one(to, mtags, ":%s TKL %c %c %s%s %s %s %lld %lld :%s", sender->name, add ? '+' : '-', typ, (tkl->ptr.serverban->subtype & TKL_SUBTYPE_SOFT) ? "%" : "", @@ -4275,7 +4541,7 @@ void tkl_sync_send_entry(int add, Client *sender, Client *to, TKL *tkl) } else if (TKLIsNameBan(tkl)) { - sendto_one(to, NULL, ":%s TKL %c %c %c %s %s %lld %lld :%s", sender->name, + sendto_one(to, mtags, ":%s TKL %c %c %c %s %s %lld %lld :%s", sender->name, add ? '+' : '-', typ, tkl->ptr.nameban->hold ? 'H' : '*', @@ -4286,7 +4552,7 @@ void tkl_sync_send_entry(int add, Client *sender, Client *to, TKL *tkl) } else if (TKLIsSpamfilter(tkl)) { - sendto_one(to, NULL, ":%s TKL %c %c %s %c %s %lld %lld %lld %s %s :%s", sender->name, + sendto_one(to, mtags, ":%s TKL %c %c %s %c %s %lld %lld %lld %s %s :%s", sender->name, add ? '+' : '-', typ, spamfilter_target_inttostring(tkl->ptr.spamfilter->target), @@ -4299,7 +4565,7 @@ void tkl_sync_send_entry(int add, Client *sender, Client *to, TKL *tkl) } else if (TKLIsBanException(tkl)) { - sendto_one(to, NULL, ":%s TKL %c %c %s%s %s %s %lld %lld %s :%s", sender->name, + sendto_one(to, mtags, ":%s TKL %c %c %s%s %s %s %lld %lld %s :%s", sender->name, add ? '+' : '-', typ, (tkl->ptr.banexception->subtype & TKL_SUBTYPE_SOFT) ? "%" : "", @@ -4316,6 +4582,8 @@ void tkl_sync_send_entry(int add, Client *sender, Client *to, TKL *tkl) log_data_integer("tkl_type_int", typ)); abort(); } + + safe_free_message_tags(mtags); } /** Broadcast a TKL entry. @@ -4822,6 +5090,30 @@ CMD_FUNC(cmd_tkl_add) * Note that we only update common fields, * which is acceptable to me. -- Syzop */ + const char *incoming_id = tkl_s2s_mtag_value(recv_mtags, "id"); + const char *incoming_spamfilter_id = NULL; + int changed = 0; + + /* id: an assigned id beats a blank one; two assigned ids tie-break by older + * set_at, then lexicographically. The "spamfilter_id" travels with the winning "id". + * This is done before set_at is lowered below, so the tie-break sees the original set_at. + */ + if (!BadPtr(incoming_id) && (strlen(incoming_id) < sizeof(tkl->id)) && strcmp(tkl->id, incoming_id)) + { + if (!tkl->id[0] || + (set_at < tkl->set_at) || + ((set_at == tkl->set_at) && (strcmp(incoming_id, tkl->id) < 0))) + { + strlcpy(tkl->id, incoming_id, sizeof(tkl->id)); + incoming_spamfilter_id = tkl_s2s_mtag_value(recv_mtags, "spamfilter_id"); + if (!BadPtr(incoming_spamfilter_id) && (strlen(incoming_spamfilter_id) < sizeof(tkl->spamfilter_id))) + strlcpy(tkl->spamfilter_id, incoming_spamfilter_id, sizeof(tkl->spamfilter_id)); + else + tkl->spamfilter_id[0] = '\0'; + changed = 1; + } + } + if ((set_at < tkl->set_at) || (expire_at != tkl->expire_at) || strcmp(tkl->set_by, parv[5])) { /* here's how it goes: @@ -4844,12 +5136,22 @@ CMD_FUNC(cmd_tkl_add) if (strcmp(tkl->set_by, parv[5]) < 0) safe_strdup(tkl->set_by, parv[5]); - if (type & TKL_GLOBAL) - tkl_broadcast_entry(1, client, client, tkl); + changed = 1; } + + if (changed && (type & TKL_GLOBAL)) + tkl_broadcast_entry(1, client, client, tkl); return; } + /* New entry: assign its unique id. Honor a supplied s2s-tkl/id (and spamfilter_id) from a + * remote server or services; if none and we are the originating server, generate a random one; + * otherwise leave it blank (eg the tag was dropped by an old server in the path). + */ + tkl_extract_s2s_mtags(tkl, recv_mtags); + if (!tkl->id[0] && IsMe(client)) + tkl_generate_id(tkl); + tkl_added(client, tkl); } @@ -5169,7 +5471,18 @@ void ban_action_run_all_sets_and_stops(Client *client, BanAction *action, int *s * @note Be sure to check IsDead(client) if return value is 1 and you are * considering to continue processing. */ +static int take_action_ex(Client *client, BanAction *actions, const char *reason, long duration, int take_action_flags, int *stopped, const char *spamfilter_id); + int _take_action(Client *client, BanAction *actions, const char *reason, long duration, int take_action_flags, int *stopped) +{ + return take_action_ex(client, actions, reason, duration, take_action_flags, stopped, NULL); +} + +/** Internal variant of take_action() that records the spamfilter id on any + * *LINE/SHUN it creates, for the gline->spamfilter trace. Kept internal so the public + * take_action() efunc stays free of this spamfilter-specific concept. + */ +static int take_action_ex(Client *client, BanAction *actions, const char *reason, long duration, int take_action_flags, int *stopped, const char *spamfilter_id) { BanAction *action; int previous_highest = 0; @@ -5244,7 +5557,11 @@ int _take_action(Client *client, BanAction *actions, const char *reason, long du tkllayer[6] = mo; tkllayer[7] = mo2; tkllayer[8] = reason; - cmd_tkl(NULL, &me, NULL, 9, tkllayer); + { + MessageTag *m = tkl_spamfilter_id_mtag(spamfilter_id); /* NULL unless set by a spamfilter */ + cmd_tkl(NULL, &me, m, 9, tkllayer); + safe_free_message_tags(m); + } RunHookReturnInt(HOOKTYPE_TAKE_ACTION, !=99, client, action->action, reason, duration); if ((action->action == BAN_ACT_SHUN) || (action->action == BAN_ACT_SOFT_SHUN)) { @@ -5754,7 +6071,7 @@ int _match_spamfilter(Client *client, const char *str_in, int target, const char /* Spamfilter matched */ reason = unreal_decodespace(tkl->ptr.spamfilter->tkl_reason); - ret = take_action(client, tkl->ptr.spamfilter->action, reason, tkl->ptr.spamfilter->tkl_duration, TAKE_ACTION_SKIP_SET, NULL); + ret = take_action_ex(client, tkl->ptr.spamfilter->action, reason, tkl->ptr.spamfilter->tkl_duration, TAKE_ACTION_SKIP_SET, NULL, tkl->id); if (!IsDead(client)) { if ((ret == BAN_ACT_BLOCK) || (ret == BAN_ACT_SOFT_BLOCK)) diff --git a/src/modules/tkldb.c b/src/modules/tkldb.c index 21f9baa7a..9a65ede79 100644 --- a/src/modules/tkldb.c +++ b/src/modules/tkldb.c @@ -29,7 +29,7 @@ ModuleHeader MOD_HEADER = { #define TKLDB_MAGIC 0x10101010 /* Database version */ -#define TKLDB_VERSION 4999 +#define TKLDB_VERSION 6260 /* Save tkls to file every seconds */ #define TKLDB_SAVE_EVERY 300 /* The very first save after boot, apply this delta, this @@ -401,6 +401,15 @@ int write_tkline(UnrealDB *db, const char *tmpfname, TKL *tkl) W_SAFE(unrealdb_write_str(db, tkl->set_by)); W_SAFE(unrealdb_write_int64(db, tkl->set_at)); W_SAFE(unrealdb_write_int64(db, tkl->expire_at)); + W_SAFE(unrealdb_write_str(db, tkl->id)); /* since TKLDB_VERSION 6260 */ + W_SAFE(unrealdb_write_str(db, tkl->spamfilter_id)); /* since TKLDB_VERSION 6260 */ + + /* Reserved hit-stat fields for all TKL types (since TKLDB_VERSION 6260): + * hits, lasthit. Written as 0 for now; a later release will populate them + * (for non-config TKLs). + */ + W_SAFE(unrealdb_write_int64(db, 0)); + W_SAFE(unrealdb_write_int64(db, 0)); if (TKLIsServerBan(tkl)) { @@ -446,6 +455,11 @@ int write_tkline(UnrealDB *db, const char *tmpfname, TKL *tkl) W_SAFE(unrealdb_write_char(db, action)); W_SAFE(unrealdb_write_str(db, tkl->ptr.spamfilter->tkl_reason)); W_SAFE(unrealdb_write_int64(db, tkl->ptr.spamfilter->tkl_duration)); + /* Reserved spamfilter-only hit-stat fields (since TKLDB_VERSION 6260): + * hits_except, lasthit_except. Written as 0 for now. + */ + W_SAFE(unrealdb_write_int64(db, 0)); + W_SAFE(unrealdb_write_int64(db, 0)); } return 1; @@ -530,6 +544,7 @@ int read_tkldb(void) for (cnt = 0; cnt < tklcount; cnt++) { int do_not_add = 0; + TKL *added = NULL; tkl = safe_alloc(sizeof(TKL)); @@ -556,6 +571,22 @@ int read_tkldb(void) R_SAFE(unrealdb_read_int64(db, &v)); tkl->expire_at = v; + /* id and spamfilter_id were added in TKLDB_VERSION 6260 */ + if (version >= 6260) + { + R_SAFE(unrealdb_read_str(db, &str)); + strlcpy(tkl->id, str, sizeof(tkl->id)); + safe_free(str); + R_SAFE(unrealdb_read_str(db, &str)); + strlcpy(tkl->spamfilter_id, str, sizeof(tkl->spamfilter_id)); + safe_free(str); + /* Reserved hit-stat fields for all TKL types (hits, lasthit). + * Read and discarded for now; a later release will use them. + */ + R_SAFE(unrealdb_read_int64(db, &v)); + R_SAFE(unrealdb_read_int64(db, &v)); + } + /* Save some CPU... if it's already expired then don't bother adding */ if (tkl->expire_at != 0 && tkl->expire_at <= TStime()) do_not_add = 1; @@ -599,7 +630,7 @@ int read_tkldb(void) if (!do_not_add) { - tkl_add_serverban(tkl->type, tkl->ptr.serverban->usermask, + added = tkl_add_serverban(tkl->type, tkl->ptr.serverban->usermask, tkl->ptr.serverban->hostmask, NULL, tkl->ptr.serverban->reason, @@ -639,7 +670,7 @@ int read_tkldb(void) if (!do_not_add) { - tkl_add_banexception(tkl->type, tkl->ptr.banexception->usermask, + added = tkl_add_banexception(tkl->type, tkl->ptr.banexception->usermask, tkl->ptr.banexception->hostmask, NULL, tkl->ptr.banexception->reason, @@ -668,7 +699,7 @@ int read_tkldb(void) if (!do_not_add) { - tkl_add_nameban(tkl->type, tkl->ptr.nameban->name, + added = tkl_add_nameban(tkl->type, tkl->ptr.nameban->name, tkl->ptr.nameban->hold, tkl->ptr.nameban->reason, tkl->set_by, tkl->expire_at, @@ -726,6 +757,13 @@ int read_tkldb(void) R_SAFE(unrealdb_read_str(db, &tkl->ptr.spamfilter->tkl_reason)); R_SAFE(unrealdb_read_int64(db, &v)); tkl->ptr.spamfilter->tkl_duration = v; + /* Reserved spamfilter-only hit-stat fields (hits_except, + * lasthit_except). Read and discarded for now. */ + if (version >= 6260) + { + R_SAFE(unrealdb_read_int64(db, &v)); + R_SAFE(unrealdb_read_int64(db, &v)); + } if (!do_not_add && find_tkl_spamfilter(tkl->type, tkl->ptr.spamfilter->match->str, @@ -744,7 +782,7 @@ int read_tkldb(void) if (!do_not_add) { - tkl_add_spamfilter(tkl->type, NULL, tkl->ptr.spamfilter->target, + added = tkl_add_spamfilter(tkl->type, NULL, tkl->ptr.spamfilter->target, tkl->ptr.spamfilter->action, tkl->ptr.spamfilter->match, NULL, @@ -771,6 +809,12 @@ int read_tkldb(void) break; /* we MUST stop reading */ } + if (added) + { + strlcpy(added->id, tkl->id, sizeof(added->id)); + strlcpy(added->spamfilter_id, tkl->spamfilter_id, sizeof(added->spamfilter_id)); + } + if (!do_not_add) added_cnt++;