diff --git a/src/chanserv.c b/src/chanserv.c index 2303a761..036eff44 100644 --- a/src/chanserv.c +++ b/src/chanserv.c @@ -61,6 +61,7 @@ #define KEY_NODELETE_LEVEL "nodelete_level" #define KEY_MAX_USERINFO_LENGTH "max_userinfo_length" #define KEY_GIVEOWNERSHIP_PERIOD "giveownership_timeout" +#define KEY_RENAME_DNR_DURATION "rename_dnr_duration" #define KEY_VALID_CHANNEL_REGEX "valid_channel_regex" /* ChanServ database */ @@ -631,6 +632,7 @@ static struct unsigned int greeting_length; unsigned int refresh_period; unsigned int giveownership_period; + unsigned long rename_dnr_duration; unsigned int max_owned; unsigned int max_chan_users; @@ -1735,12 +1737,14 @@ unregister_channel(struct chanData *channel, const char *reason) timeq_del(0, NULL, channel, TIMEQ_IGNORE_FUNC | TIMEQ_IGNORE_WHEN); - if(off_channel > 0) - { - mod_chanmode_init(&change); - change.modes_clear |= MODE_REGISTERED; - mod_chanmode_announce(chanserv, channel->channel, &change); - } + /* Always clear the ircd's registration marker (+R) on unregistration, + * independent of off_channel (which only governs whether ChanServ + * itself leaves the channel). Clear the persist exmode (+z) too: + * an unregistered channel must be able to die when it empties (the + * ircd schedules the destruct when -z lands on an empty channel). */ + mod_chanmode_init(&change); + change.modes_clear |= MODE_REGISTERED | MODE_PERSIST; + mod_chanmode_announce(chanserv, channel->channel, &change); wipe_adduser_pending(channel->channel, NULL); @@ -2107,6 +2111,27 @@ chanserv_is_dnr(const char *chan_name, struct handle_info *handle) return dnr; } +void +chanserv_rename_dnr(const char *old_name) +{ + const char *setter; + + if(!chanserv_conf.rename_dnr_duration) + return; + if(chanserv_is_dnr(old_name, NULL)) + return; /* already covered by a DNR (plain or mask); don't stack */ + /* chanserv is NULL when the bot's nick is disabled via the "." + * convention (init_chanserv only AddLocalUser()s it when nick != + * NULL) -- but a channel can still be registered (saxdb load doesn't + * care whether the bot exists) and RN can still arrive over the + * wire. Fall back to a literal setter string rather than dereference + * a NULL chanserv; it's a plain string and serializes to saxdb the + * same as any other setter value. */ + setter = chanserv ? chanserv->nick : "ChanServ"; + chanserv_add_dnr(old_name, setter, now + chanserv_conf.rename_dnr_duration, + "Channel was renamed"); +} + static unsigned int send_dnrs(struct userNode *user, dict_t dict) { struct do_not_register *dnr; @@ -2618,8 +2643,15 @@ static CHANSERV_FUNC(cmd_register) cData = register_channel(channel, user->handle_info->handle); scan_user_presence(add_channel_user(cData, handle, UL_OWNER, 0, NULL, 0), NULL); cData->modes = chanserv_conf.default_modes; + /* Always announce the ircd's registration marker (+R) on registration; + * off_channel only governs whether ChanServ itself joins/leaves the + * channel below, not whether the ircd learns it's registered. + * With off_channel>0 there is no bot presence to hold the channel + * open, so also set the persist exmode (+z) -- its original intended + * use -- to keep the registered channel alive while empty. */ + cData->modes.modes_set |= MODE_REGISTERED; if(off_channel > 0) - cData->modes.modes_set |= MODE_REGISTERED; + cData->modes.modes_set |= MODE_PERSIST; if (IsOffChannel(cData)) { mod_chanmode_announce(chanserv, channel, &cData->modes); @@ -2809,16 +2841,19 @@ static CHANSERV_FUNC(cmd_move) else if(!IsSuspended(channel->channel_info)) chanserv_join = 1; + /* Clear the server-managed markers from the old channel, add them to + * the new. Always, not just under off_channel -- the ircd's +R must + * follow registration regardless of whether ChanServ itself occupies + * the channel. The persist exmode (+z) moves too, but is only SET + * when off_channel>0 (bot presence otherwise holds the channel). */ + change.argc = 0; + change.modes_clear = MODE_REGISTERED | MODE_PERSIST; + mod_chanmode_announce(chanserv, channel, &change); + change.modes_clear = 0; + change.modes_set = MODE_REGISTERED; if(off_channel > 0) - { - /* Clear MODE_REGISTERED from old channel, add it to new. */ - change.argc = 0; - change.modes_clear = MODE_REGISTERED; - mod_chanmode_announce(chanserv, channel, &change); - change.modes_clear = 0; - change.modes_set = MODE_REGISTERED; - mod_chanmode_announce(chanserv, target, &change); - } + change.modes_set |= MODE_PERSIST; + mod_chanmode_announce(chanserv, target, &change); /* Move the channel_info to the target channel; it shouldn't be necessary to clear timeq callbacks @@ -8321,6 +8356,90 @@ handle_new_channel(struct chanNode *channel, UNUSED_ARG(void *extra)) SetChannelTopic(channel, chanserv, chanserv, channel->channel_info->topic, 1); } +static void +chanserv_channel_rename(struct chanNode *old_chan, struct chanNode *new_chan, UNUSED_ARG(void *extra)) +{ + struct adduserPending *ap; + unsigned int ii; + + /* memcpy() moved channel_info onto new_chan, but the chanData's back + * pointer still targets the old node. */ + if (new_chan->channel_info) + new_chan->channel_info->channel = new_chan; + + for (ap = adduser_pendings; ap; ap = ap->next) + if (ap->channel == old_chan) + ap->channel = new_chan; + + for (ii = 0; ii < chanserv_conf.support_channels.used; ++ii) + if (chanserv_conf.support_channels.list[ii] == old_chan) + chanserv_conf.support_channels.list[ii] = new_chan; +} + +/* Rename authorization check for the AC R RENAME query (proto-p10.c + * cmd_account). Owner-only, mirroring cmd_move's DNR gating against the + * NEW name — minus the IsHelping/"force" bypass, since this path has no + * force; staff bypass comes only from _GetChannelUser()'s override=1 + * synthetic access entry. */ +int +chanserv_rename_allowed(struct userNode *user, struct chanNode *chan, const char *new_name, const char **reason) +{ + struct chanData *cData; + struct userData *uData; + struct do_not_register *dnr; + + if(!user->handle_info) + { + *reason = "You must be authenticated"; + return 0; + } + + if(strlen(new_name) > CHANNELLEN) + { + *reason = "New channel name is too long"; + return 0; + } + + if(!(cData = chan->channel_info)) + return 1; /* Nothing registered here to protect. */ + + if(IsProtected(cData) || IsSuspended(cData)) + { + *reason = "Channel may not be renamed"; + return 0; + } + + uData = _GetChannelUser(cData, user->handle_info, 1, 0); + if(!uData || (uData->access < UL_OWNER)) + { + *reason = "You must be the channel owner"; + return 0; + } + + if(opserv_bad_channel(new_name)) + { + *reason = "New channel name is not allowed"; + return 0; + } + + if(GetChannel(new_name) && GetChannel(new_name)->channel_info) + { + *reason = "New channel name is already registered"; + return 0; + } + + for(uData = cData->users; uData; uData = uData->next) + { + if((uData->access == UL_OWNER) && (dnr = chanserv_is_dnr(new_name, uData->handle))) + { + *reason = "New channel name is blocked (do-not-register)"; + return 0; + } + } + + return 1; +} + int trace_check_bans(struct userNode *user, struct chanNode *chan) { @@ -8438,6 +8557,33 @@ handle_join(struct modeNode *mNode, UNUSED_ARG(void *extra)) if(channel->members.used > cData->max) cData->max = channel->members.used; + /* Self-heal the ircd's server-managed markers (+R always; +z persist + * when off_channel>0, since no bot presence then) for a channel we + * know is registered (channel_info set, not suspended -- both already + * checked above) but which the ircd doesn't currently show as such. + * This covers ircd restarts (fresh channel, no memory of +R), X3 + * restarts racing a channel's first post-restart JOIN before the + * BURST/DB-load path re-set it, and channel re-creation. Unlike the + * join-flood/burst guards below (dynamic-limit timer resets, + * automode, greetings) this isn't gated on user->uplink->burst: it + * sends no message to the user and touches no timer, it only + * corrects channel state, and mod_chanmode_announce() applies the + * change to channel->modes immediately, so only the first joiner + * (burst or not) actually triggers the wire MODE -- every later + * handle_join() call in the same burst sees MODE_REGISTERED already + * set and no-ops here. */ + { + unsigned int needed = MODE_REGISTERED + | ((off_channel > 0) ? MODE_PERSIST : 0); + if((channel->modes & needed) != needed) + { + struct mod_chanmode reg_change; + mod_chanmode_init(®_change); + reg_change.modes_set = needed & ~channel->modes; + mod_chanmode_announce(chanserv, channel, ®_change); + } + } + #ifdef notdef /* Check for bans. If they're joining through a ban, one of two * cases applies: @@ -9124,6 +9270,8 @@ chanserv_conf_read(void) chanserv_conf.refresh_period = str ? ParseInterval(str) : 3*60*60; str = database_get_data(conf_node, KEY_GIVEOWNERSHIP_PERIOD, RECDB_QSTRING); chanserv_conf.giveownership_period = str ? ParseInterval(str) : 0; + str = database_get_data(conf_node, KEY_RENAME_DNR_DURATION, RECDB_QSTRING); + chanserv_conf.rename_dnr_duration = str ? ParseInterval(str) : 86400; str = database_get_data(conf_node, KEY_CTCP_SHORT_BAN_DURATION, RECDB_QSTRING); chanserv_conf.ctcp_short_ban_duration = str ? str : "3m"; str = database_get_data(conf_node, KEY_CTCP_LONG_BAN_DURATION, RECDB_QSTRING); @@ -9617,8 +9765,14 @@ chanserv_channel_read(const char *key, struct record_data *hir) && (modes = mod_chanmode_parse(cNode, argv, argc, MCP_KEY_FREE, 0))) { cData->modes = *modes; + /* Always re-assert +R on DB-load reregistration; see + * unregister_channel()/register path. When this block doesn't run + * at all (channel has no stored KEY_MODES), handle_join()'s + * heal-on-join covers it instead. +z persist rides along when + * off_channel>0 (no bot presence to keep the channel alive). */ + cData->modes.modes_set |= MODE_REGISTERED; if(off_channel > 0) - cData->modes.modes_set |= MODE_REGISTERED; + cData->modes.modes_set |= MODE_PERSIST; if(cData->modes.argc > 1) cData->modes.argc = 1; mod_chanmode_announce(chanserv, cNode, &cData->modes); @@ -10044,6 +10198,13 @@ init_chanserv(const char *nick) reg_auth_func(handle_auth, NULL); } + /* Registered channels load from the DB regardless of whether the + * ChanServ bot nick is enabled ("." convention); an RN arriving over + * the wire must still repoint channel_info->channel via RenameChannel, + * or the DB is left holding a stale pointer into a freed chanNode + * (use-after-free at the next saxdb write). Keep this outside if(nick). + */ + reg_channel_rename_func(chanserv_channel_rename, NULL); reg_handle_rename_func(handle_rename, NULL); reg_unreg_func(handle_unreg, NULL); diff --git a/src/chanserv.h b/src/chanserv.h index 1bd58689..e66b3476 100644 --- a/src/chanserv.h +++ b/src/chanserv.h @@ -216,6 +216,19 @@ struct do_not_register struct userData *_GetChannelUser(struct chanData *channel, struct handle_info *handle, int override, int allow_suspended); struct banData *add_channel_ban(struct chanData *channel, const char *mask, char *owner, time_t set, time_t triggered, time_t expires, char *reason); +/* Rename authorization check, used by the AC R RENAME query handler in + * proto-p10.c (cmd_account). Returns 1 = allow; 0 = deny with *reason + * set to a static user-visible string. */ +int chanserv_rename_allowed(struct userNode *user, struct chanNode *chan, const char *new_name, const char **reason); + +/* Called by the RN handler in proto-p10.c (cmd_rename) after a registered + * channel is renamed, to place a timed do-not-register entry on the old + * name. No-op if chanserv_conf.rename_dnr_duration is 0, or if old_name + * is already covered by an existing (non-expired) DNR. proto-p10.c must + * not reach into chanserv's DNR internals (plain_dnrs/mask_dnrs/etc are + * file-static) — this wrapper is the exported escape hatch. */ +void chanserv_rename_dnr(const char *old_name); + void init_chanserv(const char *nick); void del_channel_user(struct userData *user, int do_gc); struct channelList *chanserv_support_channels(void); diff --git a/src/hash.c b/src/hash.c index 8f6a3dfe..6b6d951b 100644 --- a/src/hash.c +++ b/src/hash.c @@ -555,7 +555,9 @@ wipeout_channel(struct chanNode *cNode, time_t new_time, char **modes, unsigned strcpy(orig_upass, cNode->upass); strcpy(orig_apass, cNode->apass); cNode->modes = 0; - mod_chanmode(NULL, cNode, modes, modec, 0); + /* burst data IS server-origin; without the flag one unknown letter + * aborts the whole parse and strands modes at zero */ + mod_chanmode(NULL, cNode, modes, modec, MCP_FROM_SERVER); cNode->timestamp = new_time; /* remove our old ban list, replace it with the new one */ @@ -704,6 +706,28 @@ reg_del_channel_func(del_channel_func_t handler, void *extra) dcf_list_extra[dcf_used++] = extra; } +static channel_rename_func_t *crf_list; +static void **crf_list_extra; +static unsigned int crf_size = 0, crf_used = 0; + +void +reg_channel_rename_func(channel_rename_func_t handler, void *extra) +{ + if (crf_used == crf_size) { + if (crf_size) { + crf_size <<= 1; + crf_list = realloc(crf_list, crf_size*sizeof(crf_list[0])); + crf_list_extra = realloc(crf_list_extra, crf_size*sizeof(void*)); + } else { + crf_size = 8; + crf_list = malloc(crf_size*sizeof(crf_list[0])); + crf_list_extra = malloc(crf_size*sizeof(void*)); + } + } + crf_list[crf_used] = handler; + crf_list_extra[crf_used++] = extra; +} + static void DelChannel(struct chanNode *channel) { @@ -739,6 +763,36 @@ DelChannel(struct chanNode *channel) free(channel); } +struct chanNode * +RenameChannel(struct chanNode *channel, const char *new_name) +{ + struct chanNode *nNode; + unsigned int n; + + if (!IsChannelName(new_name) || GetChannel(new_name) || strlen(new_name) > CHANNELLEN) + return NULL; /* IsChannelName has no length cap; an overlong name would enter fixed-buffer sprintf paths downstream */ + nNode = calloc(1, sizeof(*nNode) + strlen(new_name)); + /* Copy the fixed head wholesale: modes, limit, LOCKS (inherited — chanserv + * registration lock + alert/support locks count on this node), keys, + * timestamp, topic, list headers (heap arrays move ownership), and the + * channel_info pointer. name[] is then overwritten. */ + memcpy(nNode, channel, sizeof(*channel)); + strcpy(nNode->name, new_name); + for (n = 0; n < nNode->members.used; n++) + nNode->members.list[n]->channel = nNode; + /* Old node's dict key is an interior pointer into its name[] — remove + * while it is still alive. */ + dict_remove(channels, channel->name); + dict_insert(channels, nNode->name, nNode); + /* Modules re-point their own holders (design doc §8) with BOTH nodes + * alive: pointer-compare holders need old; name-keyed dicts need + * old->name intact. */ + for (n = 0; n < crf_used; n++) + crf_list[n](channel, nNode, crf_list_extra[n]); + free(channel); + return nNode; +} + struct modeNode * AddChannelUser(struct userNode *user, struct chanNode* channel) { @@ -1103,6 +1157,8 @@ hash_cleanup(UNUSED_ARG(void *extra)) free_hook_func_list(&jf_list); free(dcf_list); free(dcf_list_extra); + free(crf_list); + free(crf_list_extra); free(pf_list); free(pf_list_extra); free(kf_list); diff --git a/src/hash.h b/src/hash.h index 3c368233..a42797b4 100644 --- a/src/hash.h +++ b/src/hash.h @@ -42,7 +42,7 @@ #define MODE_REGONLY 0x00001000 /* ircu +r */ #define MODE_NOCOLORS 0x00002000 /* +c */ #define MODE_NOCTCPS 0x00004000 /* +C */ -#define MODE_REGISTERED 0x00008000 /* Bahamut +r */ +#define MODE_REGISTERED 0x00008000 /* server-settable channel registration marker; wire letter +R (nefarious channel.c MODE_REGISTERED) -- NOT Bahamut +r, and NOT the fork's +z persist exmode */ #define MODE_STRIPCOLOR 0x00010000 /* +S Strip mirc color codes */ #define MODE_MODUNREG 0x00020000 /* +M mod unregister */ #define MODE_NONOTICE 0x00040000 /* +N no notices */ @@ -56,6 +56,10 @@ #define MODE_APASS 0x04000000 /* +A adminpass */ #define MODE_UPASS 0x08000000 /* +U userpass */ #define MODE_ADMINSONLY 0x10000000 /* +a Admins only */ +#define MODE_PERSIST 0x20000000 /* +z nefarious persist exmode (server-settable; channel survives while + * empty). ChanServ sets it alongside +R when off_channel>0 -- i.e. + * exactly when no bot presence holds the channel open. NOT the + * registered marker; that is MODE_REGISTERED (+R). */ #define MODE_REMOVE 0x80000000 #define FLAGS_OPER 0x00000001 /* Operator +o */ @@ -445,6 +449,12 @@ void reg_join_func(join_func_t handler, void *extra); typedef void (*del_channel_func_t) (struct chanNode *chan, void *extra); void reg_del_channel_func(del_channel_func_t handler, void *extra); +typedef void (*channel_rename_func_t)(struct chanNode *old_chan, + struct chanNode *new_chan, void *extra); +void reg_channel_rename_func(channel_rename_func_t handler, void *extra); +/* Returns the NEW node, or NULL (bad name / target exists). Old node freed. */ +struct chanNode *RenameChannel(struct chanNode *channel, const char *new_name); + struct chanNode* AddChannel(const char *name, time_t time_, const char *modes, char *banlist, char *exemptlist); void LockChannel(struct chanNode *channel); void UnlockChannel(struct chanNode *channel); diff --git a/src/mod-blacklist.c b/src/mod-blacklist.c index 39b2d5e5..d9aeadcd 100644 --- a/src/mod-blacklist.c +++ b/src/mod-blacklist.c @@ -393,6 +393,13 @@ blacklist_cleanup(void) dict_delete(blacklist_reasons); } +static void +blacklist_channel_rename(struct chanNode *old_chan, struct chanNode *new_chan, UNUSED_ARG(void *extra)) +{ + if (conf.debug_channel == old_chan) + conf.debug_channel = new_chan; +} + int blacklist_init(void) { @@ -400,6 +407,7 @@ blacklist_init(void) conf_register_reload(blacklist_conf_read); reg_new_user_func(blacklist_check_user); reg_exit_func(blacklist_cleanup); + reg_channel_rename_func(blacklist_channel_rename, NULL); return 1; } diff --git a/src/mod-helpserv.c b/src/mod-helpserv.c index 23d0e7e0..58eb4c4b 100644 --- a/src/mod-helpserv.c +++ b/src/mod-helpserv.c @@ -4915,6 +4915,36 @@ static void helpserv_db_cleanup(UNUSED_ARG(void *extra)) { fclose(reqlog_f); } +static void +helpserv_channel_rename(struct chanNode *old_chan, struct chanNode *new_chan, UNUSED_ARG(void *extra)) +{ + dict_iterator_t it; + struct helpserv_bot *hs; + struct helpserv_botlist *botlist; + unsigned int i; + + for (it = dict_first(helpserv_bots_dict); it; it = iter_next(it)) { + hs = iter_data(it); + + if (hs->helpchan == old_chan) + hs->helpchan = new_chan; + + for (i = 0; i < PGSRC_COUNT; i++) + if (hs->page_targets[i] == old_chan) + hs->page_targets[i] = new_chan; + } + + /* helpserv_bots_bychan_dict is keyed on the interior helpchan->name + * pointer; only bots whose helpchan was the renamed channel need the + * dict entry re-keyed (there is at most one entry, shared by every bot + * on that channel). */ + botlist = dict_find(helpserv_bots_bychan_dict, old_chan->name, NULL); + if (botlist) { + dict_remove2(helpserv_bots_bychan_dict, old_chan->name, 1); + dict_insert(helpserv_bots_bychan_dict, new_chan->name, botlist); + } +} + int helpserv_init() { HS_LOG = log_register_type("HelpServ", "file:helpserv.log"); conf_register_reload(helpserv_conf_read); @@ -5022,6 +5052,7 @@ int helpserv_init() { reg_part_func(handle_part, NULL); /* also deals with kick */ reg_nick_change_func(handle_nickchange, NULL); reg_del_user_func(handle_quit, NULL); + reg_channel_rename_func(helpserv_channel_rename, NULL); reg_auth_func(handle_nickserv_auth, NULL); reg_handle_rename_func(handle_nickserv_rename, NULL); diff --git a/src/mod-snoop.c b/src/mod-snoop.c index 4eb39694..d97f1410 100644 --- a/src/mod-snoop.c +++ b/src/mod-snoop.c @@ -311,6 +311,12 @@ snoop_cleanup(UNUSED_ARG(void *extra)) { unreg_del_user_func(snoop_del_user, NULL); } +static void +snoop_channel_rename(struct chanNode *old_chan, struct chanNode *new_chan, UNUSED_ARG(void *extra)) { + if (snoop_cfg.channel == old_chan) + snoop_cfg.channel = new_chan; +} + int snoop_init(void) { reg_exit_func(snoop_cleanup, NULL); @@ -325,6 +331,7 @@ snoop_init(void) { reg_channel_mode_func(snoop_channel_mode, NULL); reg_user_mode_func(snoop_user_mode, NULL); reg_oper_func(snoop_oper, NULL); + reg_channel_rename_func(snoop_channel_rename, NULL); return 1; } diff --git a/src/mod-track.c b/src/mod-track.c index a14d8ec4..35095687 100644 --- a/src/mod-track.c +++ b/src/mod-track.c @@ -657,6 +657,12 @@ track_cleanup(UNUSED_ARG(void *extra)) { dict_delete(track_db); } +static void +track_channel_rename(struct chanNode *old_chan, struct chanNode *new_chan, UNUSED_ARG(void *extra)) { + if (track_cfg.channel == old_chan) + track_cfg.channel = new_chan; +} + int track_init(void) { track_db = dict_new(); @@ -674,6 +680,7 @@ track_init(void) { reg_channel_mode_func(track_channel_mode, NULL); reg_user_mode_func(track_user_mode, NULL); reg_oper_func(track_oper, NULL); + reg_channel_rename_func(track_channel_rename, NULL); opserv_define_func("TRACK", cmd_track, 800, 0, 0); opserv_define_func("DELTRACK", cmd_deltrack, 800, 0, 0); opserv_define_func("ADDTRACK", cmd_addtrack, 800, 0, 0); diff --git a/src/opserv.c b/src/opserv.c index 69fff832..3776c8fc 100644 --- a/src/opserv.c +++ b/src/opserv.c @@ -2989,6 +2989,34 @@ opserv_channel_delete(struct chanNode *chan, UNUSED_ARG(void *extra)) timeq_del(0, opserv_part_channel, chan, TIMEQ_IGNORE_WHEN); } +static void +opserv_channel_rename(struct chanNode *old_chan, struct chanNode *new_chan, UNUSED_ARG(void *extra)) +{ + dict_iterator_t it; + unsigned int i; + + if (opserv_conf.debug_channel == old_chan) + opserv_conf.debug_channel = new_chan; + if (opserv_conf.alert_channel == old_chan) + opserv_conf.alert_channel = new_chan; + if (opserv_conf.staff_auth_channel == old_chan) + opserv_conf.staff_auth_channel = new_chan; + + for (it = dict_first(opserv_user_alerts); it; it = iter_next(it)) { + struct opserv_user_alert *alert = iter_data(it); + for (i = 0; i < alert->discrim->channel_count; i++) + if (alert->discrim->channels[i] == old_chan) + alert->discrim->channels[i] = new_chan; + } + + /* Same removal shape as opserv_channel_delete's purge-lock timer, but we + * do NOT re-add: the purge-lock re-evaluates against the renamed node on + * its own schedule. */ + timeq_del(0, opserv_part_channel, old_chan, TIMEQ_IGNORE_WHEN | TIMEQ_IGNORE_FUNC); + + new_chan->bad_channel = opserv_bad_channel(new_chan->name); +} + static void opserv_notice_handler(struct userNode *user, struct userNode *bot, const char *text, UNUSED_ARG(int server_qualified)) { @@ -7565,8 +7593,9 @@ init_opserv(const char *nick) reg_new_user_func(opserv_new_user_check, NULL); reg_nick_change_func(opserv_alert_check_nick, NULL); reg_del_user_func(opserv_user_cleanup, NULL); - reg_new_channel_func(opserv_channel_check, NULL); + reg_new_channel_func(opserv_channel_check, NULL); reg_del_channel_func(opserv_channel_delete, NULL); + reg_channel_rename_func(opserv_channel_rename, NULL); reg_join_func(opserv_join_check, NULL); reg_auth_func(opserv_staff_alert, NULL); reg_auth_func(opserv_alert_check_account, NULL); diff --git a/src/proto-p10.c b/src/proto-p10.c index 44a7cb90..2cea87c4 100644 --- a/src/proto-p10.c +++ b/src/proto-p10.c @@ -81,6 +81,7 @@ #define CMD_QUIT "QUIT" #define CMD_REHASH "REHASH" #define CMD_REMOVE "REMOVE" +#define CMD_RENAME "RENAME" #define CMD_RESET "RESET" #define CMD_RESTART "RESTART" #define CMD_RPING "RPING" @@ -183,6 +184,7 @@ #define TOK_QUIT "Q" #define TOK_REHASH "REHASH" #define TOK_REMOVE "RM" +#define TOK_RENAME "RN" #define TOK_RESET "RESET" #define TOK_RESTART "RESTART" #define TOK_RPING "RI" @@ -388,8 +390,13 @@ GetServerN(const char *numeric) } } -struct userNode* -GetUserN(const char *numeric) /* using numeric */ +/* Shared internal numeric-to-userNode lookup. Wraps both the noisy + * GetUserN (callers expect success — a miss is bug-worthy) and the + * quiet GetUserN_silent (callers know the target can legitimately be + * absent — e.g., snoop/track on transient targets, BX P probing for + * the alias/primary pair). */ +static struct userNode * +GetUserN_impl(const char *numeric, int quiet) { struct userNode *un; struct server *s; @@ -397,26 +404,46 @@ GetUserN(const char *numeric) /* using numeric */ switch (strlen(numeric)) { default: - log_module(MAIN_LOG, LOG_WARNING, "GetUserN(%s): numeric too long!", numeric); + if (!quiet) + log_module(MAIN_LOG, LOG_WARNING, "GetUserN(%s): numeric too long!", numeric); return NULL; case 5: slen = 2; ulen = 3; break; case 4: slen = 1; ulen = 3; break; case 3: slen = 1; ulen = 2; break; case 2: case 1: case 0: - log_module(MAIN_LOG, LOG_WARNING, "GetUserN(%s): numeric too short!", numeric); + if (!quiet) + log_module(MAIN_LOG, LOG_WARNING, "GetUserN(%s): numeric too short!", numeric); return NULL; } if (!(s = servers_num[base64toint(numeric, slen)])) { - log_module(MAIN_LOG, LOG_WARNING, "GetUserN(%s): couldn't find server (len=%d)!", numeric, slen); + if (!quiet) + log_module(MAIN_LOG, LOG_WARNING, "GetUserN(%s): couldn't find server (len=%d)!", numeric, slen); return NULL; } n = base64toint(numeric+slen, ulen) & s->num_mask; if (!(un = s->users[n])) { - log_module(MAIN_LOG, LOG_WARNING, "GetUserN(%s) couldn't find user!", numeric); + if (!quiet) + log_module(MAIN_LOG, LOG_WARNING, "GetUserN(%s) couldn't find user!", numeric); } return un; } +struct userNode* +GetUserN(const char *numeric) /* using numeric */ +{ + return GetUserN_impl(numeric, 0); +} + +/* Same as GetUserN but suppresses all warnings on lookup miss. Use + * for callers that legitimately probe numerics which may not exist + * (snoop / track tracking transient PRIVMSG/NOTICE targets, BX P + * lookup probes during alias reconciliation, etc.). */ +struct userNode* +GetUserN_silent(const char *numeric) +{ + return GetUserN_impl(numeric, 1); +} + extern struct userNode *opserv; static void check_ctcp(struct userNode *user, struct userNode *bot, char *text, UNUSED_ARG(int server_qualified)) @@ -504,10 +531,12 @@ irc_server(struct server *srv) inttobase64(extranum, srv->num_mask, (srv->numeric[1] || (srv->num_mask >= 64*64)) ? 3 : 2); if (srv == self) { - putsock(P10_SERVER " %s %d " FMT_TIME_T " " FMT_TIME_T " J10 %s%s +s6o :%s", + /* r = rename-capable: ircd delivers RN only to r-advertising peers + * (upstream); harmless on the fork which routes via IsService */ + putsock(P10_SERVER " %s %d " FMT_TIME_T " " FMT_TIME_T " J10 %s%s +s6or :%s", srv->name, srv->hops+1, srv->boot, srv->link_time, srv->numeric, extranum, srv->description); } else { - putsock("%s " P10_SERVER " %s %d " FMT_TIME_T " " FMT_TIME_T " %c10 %s%s +s6o :%s", + putsock("%s " P10_SERVER " %s %d " FMT_TIME_T " " FMT_TIME_T " %c10 %s%s +s6or :%s", self->numeric, srv->name, srv->hops+1, srv->boot, srv->link_time, (srv->self_burst ? 'J' : 'P'), srv->numeric, extranum, srv->description); } } @@ -1706,7 +1735,33 @@ static CMD_FUNC(cmd_account) return 1; } else if(!strcmp(argv[2],"R")) - call_account_func(user, argv[3]); + { + if(argc >= 7 && !strcmp(argv[5],"RENAME")) + { + /* Rename permission query: AC R <#chan> RENAME . + * Reply shape: cookie FIRST (ircd m_account.c keys pending renames on + * parv[1] not being a server numeric) — deliberately NOT the LOC reply + * shape (AC A ) used above. Never GetUserN() the + * cookie; argv[3] is opaque to us. This disambiguates from the legit + * legacy account stamp "AC R " (argc==4), which + * keeps falling through to call_account_func() below unchanged. + * + * The reply itself carries an explicit RENAME discriminator token + * after the A/D type so ircd m_account.c can route it by cookie + * without a FindNServer() guess — a decimal cookie can otherwise + * alias a server numeric (F2). */ + const char *reason = "Permission denied"; + struct chanNode *chan = GetChannel(argv[4]); + /* user is GetUserN(argv[1]) from the prologue above, which already + * returns early when NULL — the check here is paranoia. */ + if(user && chan && chanserv_rename_allowed(user, chan, argv[6], &reason)) + putsock("%s " P10_ACCOUNT " %s A RENAME", self->numeric, argv[3]); + else + putsock("%s " P10_ACCOUNT " %s D RENAME :%s", self->numeric, argv[3], reason); + return 1; + } + call_account_func(user, argv[3]); /* legacy account stamp — unchanged */ + } else call_account_func(user, argv[2]); /* For backward compatability */ return 1; @@ -1735,22 +1790,113 @@ static CMD_FUNC(cmd_bouncer_transfer) if (argc < 6) return 0; - old_primary = GetUserN(argv[2]); - new_node = GetUserN(argv[3]); + /* Both lookups can legitimately miss — old_primary may have + * already been cleaned up by a prior event, new_node is + * normally absent (the swap path is the common case) but + * may exist for the in-place-conversion / merge case below. + * Use the silent variant so neither probe spams the snoop + * channel. */ + old_primary = GetUserN_silent(argv[2]); + new_node = GetUserN_silent(argv[3]); if (!old_primary) return 1; /* Already gone, nothing to do */ - /* Numeric swap: move old_primary's numeric routing to the new - * server/slot. The userNode keeps its nick, clients dict entry, - * handle_info, channels — only the P10 numeric changes. + /* Two BX P shapes need handling: + * + * 1. **Numeric swap (new_node absent)** — classic promote-or- + * transfer. Move old_primary's numeric routing to the new + * server/slot. The userNode keeps its nick, clients dict + * entry, handle_info, channels — only the P10 numeric changes. + * This is the case the original handler was written for, and + * is what fires when nefarious never bursts an alias to us. * - * Nefarious never bursts aliases as N tokens (they're introduced - * via BX C only), so X3 won't have a node for the alias numeric. - * If new_node somehow exists (shouldn't happen), log and ignore. */ + * 2. **In-place conversion / merge (new_node exists with same + * account)** — modern fork peers emit BX P for the case where + * an N-introduced client (old) is being absorbed into an + * existing primary (new), e.g. burst-ordering caused us to + * receive N for the would-be-alias before its BX C. Both + * nodes exist on X3. We merge: delete old_primary's + * userNode (channels and dict entry cleaned up via DelUser, + * no QUIT broadcast), keep new_node intact as the surviving + * identity. Gate strictly on same-handle to avoid + * accidentally merging unrelated collisions. + * + * 3. **new_node exists but different/no handle** — genuinely + * unexpected. Keep the original "log and ignore" behaviour + * so we don't silently corrupt state across an account + * mismatch. */ if (new_node) { + if (old_primary->handle_info + && old_primary->handle_info == new_node->handle_info) { + /* Merge: old absorbed into new. Before deleting + * old_primary, transfer its channel memberships onto + * new_node. This mirrors nefarious's own both-exist + * swap semantics (upstream m_bouncer_transfer.c:88-113, + * fork bouncer_session.c:7070-7078): the ghost's + * channels must survive on the merged identity. + * + * Ordering is load-bearing: this MUST run before + * DelUser(). DelUser's own cleanup loop + * (DelChannelUser(..., NULL, 0)) lets an unregistered + * channel self-destruct when its last member leaves + * (see DelChannelUser's tail, hash.c). If old_primary + * were deleted first while still holding memberships + * new_node lacks, any unregistered channel where + * old_primary was the only member in X3's view would + * be destroyed here — even though the surviving + * identity (and the rest of the network) is still in + * it. Transferring first means old_primary's channel + * list is already empty by the time DelUser runs, so + * that loop body never executes. + * + * Walk old_primary->channels the same way DelUser's + * loop does: always pop the last element, since + * AddChannelUser/DelChannelUser mutate both the + * channel's member list and the user's channel list + * out from under us. */ + while (old_primary->channels.used > 0) { + struct modeNode *mn = old_primary->channels.list[old_primary->channels.used - 1]; + struct chanNode *chan = mn->channel; + + if (!GetUserMode(chan, new_node)) { + /* AddChannelUser() only fires irc_join() when + * the joining user IsLocal() (i.e. an X3 + * service bot) — new_node here is always a + * network user, so no JOIN hits the wire. It + * unconditionally runs call_join_funcs() + * though, which fires the same on-join hooks + * (chanserv presence/ban/oplevel bookkeeping) + * a real join would. The numeric-swap path + * above never touches channel membership at + * all, so there's no existing both-exist + * precedent that's hook-free; there's no + * lower-level "add to channel, no hooks" + * primitive to reach for instead. Correctness + * of membership state takes priority, so we + * accept the hook firing here as a known + * side effect rather than leaving the ghost's + * channels to be silently dropped. */ + struct modeNode *new_mn = AddChannelUser(new_node, chan); + new_mn->modes = mn->modes; + new_mn->oplevel = mn->oplevel; + } + + /* No announce (reason NULL), same call shape + * DelUser's own loop uses: this is internal + * bookkeeping, not a real part. */ + DelChannelUser(old_primary, chan, NULL, 0); + } + + /* DelUser with announce=0 suppresses both QUIT and KILL + * emission — this is internal cleanup, the network + * isn't supposed to see the alias's identity leave. */ + DelUser(old_primary, NULL, 0, "Bouncer transfer"); + return 1; + } log_module(MAIN_LOG, LOG_WARNING, - "BX P: new_node %s already exists as %s — ignoring promote", + "BX P: new_node %s already exists as %s with mismatched " + "handle — ignoring promote", argv[3], new_node->nick); return 1; } @@ -2032,9 +2178,12 @@ static CMD_FUNC(cmd_burst) cData = cNode->channel_info; if (!cData) { - if (cNode->modes & MODE_REGISTERED) { + if (cNode->modes & (MODE_REGISTERED | MODE_PERSIST)) { + /* Channel is not registered with us but carries server-managed + * markers: strip the registration marker AND any stale persist + * exmode (only ChanServ sets +z, on registered channels). */ irc_join(opserv, cNode); - irc_mode(opserv, cNode, "-z"); + irc_mode(opserv, cNode, "-zR"); irc_part(opserv, cNode, ""); } } @@ -2316,6 +2465,43 @@ static CMD_FUNC(cmd_topic) return 1; } +/* RN : -- the ircd broadcasts this after an approved + * rename has already executed. Authorization happened at AC R query time + * (chanserv_rename_allowed, see cmd_account); this handler only migrates + * state (design §3a: authorize-at-query, apply-at-RN). The source prefix + * is the renaming user and may be unknown to us in edge cases, so it is + * not used here. */ +static CMD_FUNC(cmd_rename) +{ + struct chanNode *chan; + char old_name[CHANNELLEN+1]; + int was_registered; + + if(argc < 3) return 0; + if(!(chan = GetChannel(argv[1]))) return 1; /* never knew it; nothing to move */ + if(GetChannel(argv[2])) { + log_module(MAIN_LOG, LOG_ERROR, + "RENAME %s -> %s: target already exists, state diverged", + argv[1], argv[2]); + return 1; + } + was_registered = chan->channel_info != NULL; + safestrncpy(old_name, argv[1], sizeof(old_name)); + if(!RenameChannel(chan, argv[2])) { + /* RenameChannel rejected it (e.g. !IsChannelName(new_name)) and + * left the old node untouched/unfreed -- nothing was actually + * renamed, so don't mark the (still current) old name do-not- + * register. */ + log_module(MAIN_LOG, LOG_ERROR, + "RENAME %s -> %s: rejected by RenameChannel", + argv[1], argv[2]); + return 1; + } + if(was_registered) + chanserv_rename_dnr(old_name); + return 1; +} + static CMD_FUNC(cmd_num_topic) { struct chanNode *cn; @@ -2794,6 +2980,8 @@ init_parse(void) dict_insert(irc_func_dict, TOK_ERROR, cmd_error); dict_insert(irc_func_dict, CMD_TOPIC, cmd_topic); dict_insert(irc_func_dict, TOK_TOPIC, cmd_topic); + dict_insert(irc_func_dict, CMD_RENAME, cmd_rename); + dict_insert(irc_func_dict, TOK_RENAME, cmd_rename); dict_insert(irc_func_dict, CMD_AWAY, cmd_away); dict_insert(irc_func_dict, TOK_AWAY, cmd_away); dict_insert(irc_func_dict, CMD_SILENCE, cmd_silence); @@ -3617,7 +3805,13 @@ mod_chanmode_parse(struct chanNode *channel, char **modes, unsigned int argc, un case 'a': do_chan_mode(MODE_ADMINSONLY); break; case 'Z': do_chan_mode(MODE_SSLONLY); break; case 'L': do_chan_mode(MODE_HIDEMODE); break; - case 'z': + case 'R': + /* MODE_REGISTERED wire letter (nefarious channel.c MODE_REGISTERED, + * server-origin-only). MCP_REGISTERED is passed by the ChanServ + * MODE-lock/mode-command call sites to forbid a user-typed mode + * string from toggling registration state; server-origin calls + * (cmd_mode, AddChannel/BURST via MCP_FROM_SERVER) don't set it, + * so the ircd is free to correct our copy. */ if (!(flags & MCP_REGISTERED)) { do_chan_mode(MODE_REGISTERED); } else { @@ -3625,6 +3819,21 @@ mod_chanmode_parse(struct chanNode *channel, char **modes, unsigned int argc, un return NULL; } break; + case 'z': + /* Nefarious persist exmode: server-settable-only channel + * keep-alive, tracked as MODE_PERSIST -- NOT the registered + * marker (historical X3 misread it as MODE_REGISTERED). + * Same guard as 'R': user-typed mode strings (MCP_REGISTERED + * call sites, i.e. ChanServ modelock/mode-command) may not + * toggle a server-managed marker; server-origin strings parse + * it so our copy tracks the ircd. */ + if (!(flags & MCP_REGISTERED)) { + do_chan_mode(MODE_PERSIST); + } else { + mod_chanmode_free(change); + return NULL; + } + break; #undef do_chan_mode case 'l': if (add) { @@ -3850,7 +4059,8 @@ mod_chanmode_announce(struct userNode *who, struct chanNode *channel, struct mod DO_MODE_CHAR(NOAMSG, 'T'); DO_MODE_CHAR(OPERSONLY, 'O'); DO_MODE_CHAR(ADMINSONLY, 'a'); - DO_MODE_CHAR(REGISTERED, 'z'); + DO_MODE_CHAR(REGISTERED, 'R'); + DO_MODE_CHAR(PERSIST, 'z'); DO_MODE_CHAR(SSLONLY, 'Z'); DO_MODE_CHAR(HIDEMODE, 'L'); #undef DO_MODE_CHAR @@ -3907,7 +4117,8 @@ mod_chanmode_announce(struct userNode *who, struct chanNode *channel, struct mod DO_MODE_CHAR(NOAMSG, 'T'); DO_MODE_CHAR(OPERSONLY, 'O'); DO_MODE_CHAR(ADMINSONLY, 'a'); - DO_MODE_CHAR(REGISTERED, 'z'); + DO_MODE_CHAR(REGISTERED, 'R'); + DO_MODE_CHAR(PERSIST, 'z'); DO_MODE_CHAR(SSLONLY, 'Z'); DO_MODE_CHAR(HIDEMODE, 'L'); #undef DO_MODE_CHAR @@ -3983,7 +4194,8 @@ mod_chanmode_format(struct mod_chanmode *change, char *outbuff) DO_MODE_CHAR(NOAMSG, 'T'); DO_MODE_CHAR(OPERSONLY, 'O'); DO_MODE_CHAR(ADMINSONLY, 'a'); - DO_MODE_CHAR(REGISTERED, 'z'); + DO_MODE_CHAR(REGISTERED, 'R'); + DO_MODE_CHAR(PERSIST, 'z'); DO_MODE_CHAR(SSLONLY, 'Z'); DO_MODE_CHAR(HIDEMODE, 'L'); #undef DO_MODE_CHAR @@ -4008,7 +4220,8 @@ mod_chanmode_format(struct mod_chanmode *change, char *outbuff) DO_MODE_CHAR(NOAMSG, 'T'); DO_MODE_CHAR(OPERSONLY, 'O'); DO_MODE_CHAR(ADMINSONLY, 'a'); - DO_MODE_CHAR(REGISTERED, 'z'); + DO_MODE_CHAR(REGISTERED, 'R'); + DO_MODE_CHAR(PERSIST, 'z'); DO_MODE_CHAR(SSLONLY, 'Z'); DO_MODE_CHAR(HIDEMODE, 'L'); @@ -4074,7 +4287,8 @@ clear_chanmode(struct chanNode *channel, const char *modes) case 'T': cleared |= MODE_NOAMSG; break; case 'O': cleared |= MODE_OPERSONLY; break; case 'a': cleared |= MODE_ADMINSONLY; break; - case 'z': cleared |= MODE_REGISTERED; break; + case 'R': cleared |= MODE_REGISTERED; break; + case 'z': cleared |= MODE_PERSIST; break; case 'Z': cleared |= MODE_SSLONLY; break; case 'L': cleared |= MODE_HIDEMODE; break; } diff --git a/src/proto.h b/src/proto.h index 75a5e249..cf272044 100644 --- a/src/proto.h +++ b/src/proto.h @@ -84,6 +84,7 @@ struct cManagerNode #ifdef WITH_PROTOCOL_P10 struct server* GetServerN(const char *numeric); struct userNode* GetUserN(const char *numeric); +struct userNode* GetUserN_silent(const char *numeric); #endif /* Basic protocol parsing support. */ diff --git a/src/spamserv.c b/src/spamserv.c index eeff76ce..af34164c 100644 --- a/src/spamserv.c +++ b/src/spamserv.c @@ -398,6 +398,43 @@ spamserv_cs_move_merge(struct userNode *user, struct chanNode *channel, struct c return 0; } +static void +spamserv_channel_rename(struct chanNode *old_chan, struct chanNode *new_chan, UNUSED_ARG(void *extra)) +{ + struct chanInfo *cInfo = get_chanInfo(old_chan->name); + dict_iterator_t it; + struct userInfo *uInfo; + struct spamNode *sNode; + struct floodNode *fNode; + + if(cInfo) + { + cInfo->channel = new_chan; + + dict_remove(registered_channels_dict, old_chan->name); + dict_insert(registered_channels_dict, strdup(new_chan->name), cInfo); + } + + for(it = dict_first(connected_users_dict); it; it = iter_next(it)) + { + uInfo = iter_data(it); + if(!uInfo) + continue; + + for(sNode = uInfo->spam; sNode; sNode = sNode->next) + if(sNode->channel == old_chan) + sNode->channel = new_chan; + + for(fNode = uInfo->flood; fNode; fNode = fNode->next) + if(fNode->channel == old_chan) + fNode->channel = new_chan; + + for(fNode = uInfo->joinflood; fNode; fNode = fNode->next) + if(fNode->channel == old_chan) + fNode->channel = new_chan; + } +} + void spamserv_cs_unregister(struct userNode *user, struct chanNode *channel, enum cs_unreg type, char *reason) { @@ -3249,6 +3286,7 @@ init_spamserv(const char *nick) reg_nick_change_func(spamserv_nick_change_func, NULL); reg_join_func(spamserv_user_join, NULL); reg_part_func(spamserv_user_part, NULL); + reg_channel_rename_func(spamserv_channel_rename, NULL); timeq_add(now + FLOOD_TIMEQ_FREQ, timeq_flood, NULL); timeq_add(now + JOINFLOOD_TIMEQ_FREQ, timeq_joinflood, NULL);