diff --git a/include/capab.h b/include/capab.h index feac53bc..13cb395a 100644 --- a/include/capab.h +++ b/include/capab.h @@ -45,6 +45,7 @@ enum Capab { _CAP(AWAYNOTIFY, 0, "away-notify", 0), _CAP(ACCNOTIFY, 0, "account-notify", 0), _CAP(SASL, 0, "sasl", 0), + _CAP(DRAFT_CHANRENAME, 0, "draft/channel-rename", 0), #ifdef USE_SSL _CAP(TLS, 0, "tls", 0), #endif diff --git a/include/channel.h b/include/channel.h index 137a5ab6..f9088dfa 100644 --- a/include/channel.h +++ b/include/channel.h @@ -515,6 +515,16 @@ extern int IsInvited(struct Client* cptr, const void* chptr); extern void send_channel_modes(struct Client *cptr, struct Channel *chptr); extern char *pretty_mask(char *mask); extern char *pretty_extmask(char *mask); +extern int rename_channel(struct Channel **chptr_p, const char *newname); + +/* Pending channel-rename requests (defined in m_rename.c; consumed by the + * ACCOUNT A/D reply path in m_account.c and client exit in s_misc.c). */ +struct PendingRename; +extern struct PendingRename *pending_rename_find(unsigned int cookie); +extern void pending_rename_complete(struct PendingRename *pr); +extern void pending_rename_deny(struct PendingRename *pr, const char *reason); +extern void pending_rename_client_exit(struct Client *cptr); + extern void del_invite(struct Client *cptr, struct Channel *chptr); extern void list_set_default(void); /* this belongs elsewhere! */ diff --git a/include/client.h b/include/client.h index bd6974df..257ace1f 100644 --- a/include/client.h +++ b/include/client.h @@ -171,6 +171,7 @@ enum Flag FLAG_IPV6, /**< server understands P10 IPv6 addrs */ FLAG_SERVICE, /**< server is a service */ FLAG_OPLEVELS, /**< server has oplevels support */ + FLAG_RENAME_CAPABLE, /**< server applies and relays RENAME */ FLAG_GOTID, /**< successful ident lookup achieved */ FLAG_DOID, /**< I-lines say must use ident return */ FLAG_NONL, /**< No \n in buffer */ @@ -735,6 +736,8 @@ struct Client { #define IsService(x) HasFlag(x, FLAG_SERVICE) /** Return non-zero if the client has oplevels support. */ #define IsOpLevels(x) HasFlag(x, FLAG_OPLEVELS) +/** Return non-zero if the client applies and relays RENAME. */ +#define IsRenameCapable(x) HasFlag(x, FLAG_RENAME_CAPABLE) /** Return non-zero if the client has an account stamp. */ #define IsAccount(x) HasFlag(x, FLAG_ACCOUNT) /** Return non-zero if the client has set mode +x (hidden host). */ @@ -855,6 +858,8 @@ struct Client { #define SetService(x) SetFlag(x, FLAG_SERVICE) /** Mark a client as having oplevels support. */ #define SetOpLevels(x) SetFlag(x, FLAG_OPLEVELS) +/** Mark a client as applying and relaying RENAME. */ +#define SetRenameCapable(x) SetFlag(x, FLAG_RENAME_CAPABLE) /** Mark a client as having an account stamp. */ #define SetAccount(x) SetFlag(x, FLAG_ACCOUNT) /** Mark a client as having mode +x (hidden host). */ diff --git a/include/handlers.h b/include/handlers.h index b542a194..403e1a63 100644 --- a/include/handlers.h +++ b/include/handlers.h @@ -131,6 +131,7 @@ extern int m_protoctl(struct Client*, struct Client*, int, char*[]); extern int m_pseudo(struct Client*, struct Client*, int, char*[]); extern int m_quit(struct Client*, struct Client*, int, char*[]); extern int m_registered(struct Client*, struct Client*, int, char*[]); +extern int m_rename(struct Client*, struct Client*, int, char*[]); extern int m_rules(struct Client*, struct Client*, int, char*[]); extern int m_sethost(struct Client*, struct Client*, int, char*[]); extern int m_shun(struct Client*, struct Client*, int, char*[]); @@ -237,6 +238,7 @@ extern int ms_privs(struct Client*, struct Client*, int, char*[]); extern int ms_quit(struct Client*, struct Client*, int, char*[]); extern int ms_rehash(struct Client*, struct Client*, int, char*[]); extern int ms_remove(struct Client*, struct Client*, int, char*[]); +extern int ms_rename(struct Client*, struct Client*, int, char*[]); extern int ms_rping(struct Client*, struct Client*, int, char*[]); extern int ms_rpong(struct Client*, struct Client*, int, char*[]); extern int ms_rules(struct Client*, struct Client*, int, char*[]); diff --git a/include/hash.h b/include/hash.h index 44ce997a..382f6df4 100644 --- a/include/hash.h +++ b/include/hash.h @@ -86,6 +86,7 @@ extern int hAddWatch(struct Watch *wptr); extern int hRemClient(struct Client *cptr); extern int hChangeClient(struct Client *cptr, const char *newname); extern int hRemChannel(struct Channel *chptr); +extern int hChangeChannel(struct Channel *chptr, const char *newname); extern int hRemWatch(struct Watch *wptr); extern struct Client *hSeekClient(const char *name, int TMask); extern struct Channel *hSeekChannel(const char *name); diff --git a/include/ircd_features.h b/include/ircd_features.h index f79e26d6..0c5f93fc 100644 --- a/include/ircd_features.h +++ b/include/ircd_features.h @@ -313,6 +313,8 @@ enum Feature { FEAT_CAP_away_notify, FEAT_CAP_account_notify, FEAT_CAP_sasl, + FEAT_CAP_draft_channel_rename, + FEAT_RENAME_SERVICES, #ifdef USE_SSL FEAT_CAP_tls, #endif diff --git a/include/msg.h b/include/msg.h index 4d73cbed..74670c66 100644 --- a/include/msg.h +++ b/include/msg.h @@ -336,6 +336,10 @@ struct Client; #define TOK_ACCOUNT "AC" #define CMD_ACCOUNT MSG_ACCOUNT, TOK_ACCOUNT +#define MSG_RENAME "RENAME" /* RENA */ +#define TOK_RENAME "RN" +#define CMD_RENAME MSG_RENAME, TOK_RENAME + #define MSG_ASLL "ASLL" /* ASLL */ #define TOK_ASLL "LL" #define CMD_ASLL MSG_ASLL, TOK_ASLL diff --git a/include/send.h b/include/send.h index 1f3fe053..2c93e9fb 100644 --- a/include/send.h +++ b/include/send.h @@ -157,4 +157,8 @@ extern void sendto_mode_butone(struct Client *one, struct Client *from, const ch extern void vsendto_mode_butone(struct Client *one, struct Client *from, const char *mode, const char *pattern, va_list vl); +/* IRCv3 standard-replies FAIL line (tag-free minimal form) */ +extern void send_fail(struct Client *to, const char *command, const char *code, + const char *context, const char *description); + #endif /* INCLUDED_send_h */ diff --git a/ircd/Makefile.in b/ircd/Makefile.in index 2b6e173a..b0574b20 100644 --- a/ircd/Makefile.in +++ b/ircd/Makefile.in @@ -176,6 +176,7 @@ IRCD_SRC = \ m_quit.c \ m_rehash.c \ m_remove.c \ + m_rename.c \ m_reset.c \ m_restart.c \ m_rping.c \ @@ -888,6 +889,13 @@ m_remove.o: m_remove.c ../config.h ../include/client.h ../include/gline.h \ ../include/ircd_reply.h ../include/ircd_string.h ../include/msg.h \ ../include/numeric.h ../include/numnicks.h ../include/send.h \ ../include/shun.h ../include/zline.h +m_rename.o: m_rename.c ../config.h ../include/channel.h \ + ../include/client.h ../include/hash.h ../include/ircd.h \ + ../include/ircd_events.h ../include/ircd_features.h ../include/ircd_log.h \ + ../include/ircd_reply.h ../include/ircd_snprintf.h ../include/ircd_string.h \ + ../include/ircd_alloc.h ../include/list.h ../include/msg.h \ + ../include/numeric.h ../include/numnicks.h ../include/s_user.h \ + ../include/send.h m_reset.o: m_reset.c ../config.h ../include/client.h ../include/hash.h \ ../include/ircd.h ../include/ircd_features.h ../include/ircd_log.h \ ../include/ircd_reply.h ../include/ircd_string.h ../include/numeric.h \ diff --git a/ircd/channel.c b/ircd/channel.c index 194c09c6..054096bb 100644 --- a/ircd/channel.c +++ b/ircd/channel.c @@ -1842,6 +1842,95 @@ struct Channel *get_channel(struct Client *cptr, char *chname, ChannelGetType fl return chptr; } +/** Rename a channel. + * Updates the channel name in the hash table and channel structure. + * Reallocates the channel structure if the new name is longer. + * + * @param[in,out] chptr_p Pointer to channel pointer. Updated if reallocation occurs. + * @param[in] newname New name for the channel. + * @return 0 on success, -1 if invalid params, -2 if new name exists. + */ +int rename_channel(struct Channel **chptr_p, const char *newname) +{ + struct Channel *chptr; + size_t oldlen, newlen; + struct Channel *newchptr; + struct Membership *member; + struct SLink *link; + + if (!chptr_p || !*chptr_p || !newname || !*newname) + return -1; + + chptr = *chptr_p; + + newlen = strlen(newname); + if (newlen > CHANNELLEN) + return -1; + + /* Check if new name already exists */ + if (FindChannel(newname)) + return -2; + + oldlen = strlen(chptr->chname); + + /* If new name fits in existing allocation, just update in place */ + if (newlen <= oldlen) { + hChangeChannel(chptr, newname); + strcpy(chptr->chname, newname); + return 0; + } + + /* New name is longer - need to reallocate */ + newchptr = (struct Channel*) MyMalloc(sizeof(struct Channel) + newlen); + if (!newchptr) + return -1; + + /* Copy all data from old to new */ + memcpy(newchptr, chptr, sizeof(struct Channel)); + strcpy(newchptr->chname, newname); + + /* Update hash table */ + hRemChannel(chptr); + hAddChannel(newchptr); + + /* Update global linked list */ + if (chptr->prev) + chptr->prev->next = newchptr; + else + GlobalChannelList = newchptr; + if (chptr->next) + chptr->next->prev = newchptr; + + /* Update all membership pointers */ + for (member = newchptr->members; member; member = member->next_member) + member->channel = newchptr; + + /* Update user invite lists that point to this channel */ + for (link = newchptr->invites; link; link = link->next) { + struct Client *cptr = link->value.cptr; + struct SLink *inv; + /* Find and update user's invite pointer to this channel */ + for (inv = cli_user(cptr)->invited; inv; inv = inv->next) { + if (inv->value.chptr == chptr) { + inv->value.chptr = newchptr; + break; + } + } + } + + /* Update destruct event if pending */ + if (newchptr->destruct_event) + newchptr->destruct_event->chptr = newchptr; + + /* Free old channel structure */ + MyFree(chptr); + + /* Update caller's pointer */ + *chptr_p = newchptr; + + return 0; +} + /** invite a user to a channel. * * Adds an invite for a user to a channel. Limits the number of invites diff --git a/ircd/hash.c b/ircd/hash.c index 98c5de7f..905d9235 100644 --- a/ircd/hash.c +++ b/ircd/hash.c @@ -214,6 +214,23 @@ int hRemChannel(struct Channel *chptr) return -1; } +/** Rename a channel in the hash table. + * @param[in] chptr Channel whose name is changing. + * @param[in] newname New name for channel. + * @return Zero on success. + */ +int hChangeChannel(struct Channel *chptr, const char *newname) +{ + HASHREGS newhash = strhash(newname); + + assert(0 != chptr); + hRemChannel(chptr); + + chptr->hnext = channelTable[newhash]; + channelTable[newhash] = chptr; + return 0; +} + /** Find a client by name, filtered by status mask. * If a client is found, it is moved to the top of its hash bucket. * @param[in] name Client name to search for. diff --git a/ircd/ircd.c b/ircd/ircd.c index 39f897ad..80ee5fd6 100644 --- a/ircd/ircd.c +++ b/ircd/ircd.c @@ -768,6 +768,10 @@ int main(int argc, char **argv) { hAddClient(&me); SetIPv6(&me); + /* We advertise the 'r' (rename-capable) SERVER flag on our own SERVER + * lines; mirror it locally so scans over GlobalClientList (which + * includes &me) see this server as rename-capable too. */ + SetRenameCapable(&me); write_pidfile(); init_counters(); diff --git a/ircd/ircd_features.c b/ircd/ircd_features.c index adf32201..a9488b0f 100644 --- a/ircd/ircd_features.c +++ b/ircd/ircd_features.c @@ -796,6 +796,12 @@ static struct FeatureDesc { F_B(CAP_away_notify, 0, 1, 0), F_B(CAP_account_notify, 0, 1, 0), F_B(CAP_sasl, 0, 1, 0), + F_B(CAP_draft_channel_rename, 0, 0, 0), + /* Off by default: only a services build that disambiguates RENAME + * requests from account-stamp notifications on the AC 'R' subcommand + * (see the protocol-history comment in m_rename.c) should have this + * enabled. */ + F_B(RENAME_SERVICES, 0, 0, 0), #ifdef USE_SSL F_B(CAP_tls, 0, 1, 0), #endif diff --git a/ircd/m_account.c b/ircd/m_account.c index 9dea7cfd..baf19a5a 100644 --- a/ircd/m_account.c +++ b/ircd/m_account.c @@ -80,6 +80,7 @@ */ #include "config.h" +#include "channel.h" #include "client.h" #include "ircd.h" #include "ircd_alloc.h" @@ -241,12 +242,40 @@ int ms_account(struct Client* cptr, struct Client* sptr, int parc, parv[1], parv[2], parv[3], parv[4], parv[5]); return 0; } else if (type == 'A' || type == 'D') { - /* LOC Replies (A=accept, D=deny) */ - if (parc < 4) + /* LOC Replies (A=accept, D=deny) or Rename Permission Replies */ + if (parc < 3) return need_more_params(sptr, "ACCOUNT"); - if (!(acptr = FindNServer(parv[1]))) - return 0; /* target not online, ignore */ + /* Rename reply carries the explicit RENAME discriminator (parv[3]); + * route by cookie without FindNServer so a decimal cookie can't + * alias a server numeric (F2). */ + if (parc > 3 && !ircd_strcmp(parv[3], "RENAME")) { + unsigned int cookie = atoi(parv[1]); + struct PendingRename *pr = pending_rename_find(cookie); + + if (pr) { + if (type == 'A') { + Debug((DEBUG_DEBUG, "ACCOUNT rename allow cookie=%u", cookie)); + pending_rename_complete(pr); + } else { + /* Deny response: parv[4] contains the reason (trailing param) */ + const char *reason = (parc > 4) ? parv[4] : "Permission denied"; + Debug((DEBUG_DEBUG, "ACCOUNT rename deny cookie=%u reason=%s", cookie, reason)); + pending_rename_deny(pr, reason); + } + } + return 0; + } + + /* First check if this is a server numeric (LOC reply) */ + acptr = FindNServer(parv[1]); + + if (!acptr) + return 0; /* unknown numeric - ignore */ + + /* LOC reply - need at least 4 params */ + if (parc < 4) + return need_more_params(sptr, "ACCOUNT"); if (!IsMe(acptr)) { /* in-transit message, forward it */ diff --git a/ircd/m_cap.c b/ircd/m_cap.c index 2403bf56..3302b384 100644 --- a/ircd/m_cap.c +++ b/ircd/m_cap.c @@ -62,6 +62,7 @@ static struct capabilities { _CAP(AWAYNOTIFY, 0, "away-notify", FEAT_CAP_away_notify), _CAP(ACCNOTIFY, 0, "account-notify", FEAT_CAP_account_notify), _CAP(SASL, 0, "sasl", FEAT_CAP_sasl), + _CAP(DRAFT_CHANRENAME, 0, "draft/channel-rename", FEAT_CAP_draft_channel_rename), #ifdef USE_SSL _CAP(TLS, 0, "tls", FEAT_CAP_tls), #endif diff --git a/ircd/m_rename.c b/ircd/m_rename.c new file mode 100644 index 00000000..386d9bd8 --- /dev/null +++ b/ircd/m_rename.c @@ -0,0 +1,786 @@ +/* + * IRC - Internet Relay Chat, ircd/m_rename.c + * Copyright (C) 2024 Nefarious Development Team + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 1, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ +/** @file + * @brief Handler for RENAME command (IRCv3 draft/channel-rename). + * + * Specification: https://ircv3.net/specs/extensions/channel-rename + * + * RENAME [:] + * + * Renames a channel while preserving all state (members, modes, bans, etc). + */ +#include "config.h" + +#include "channel.h" +#include "client.h" /* also provides capab.h (CAP_* / CapActive) */ +#include "gline.h" +#include "hash.h" +#include "ircd.h" +#include "ircd_events.h" +#include "ircd_features.h" +#include "ircd_log.h" +#include "ircd_reply.h" +#include "ircd_snprintf.h" +#include "ircd_string.h" +#include "ircd_alloc.h" +#include "list.h" +#include "msg.h" +#include "numeric.h" +#include "numnicks.h" +#include "s_user.h" +#include "send.h" + +#include +#include + +/* ========== Pending Rename Infrastructure ========== */ + +/** Maximum pending rename requests */ +#define RENAME_MAX_PENDING 100 + +/** Timeout for services response (seconds) */ +#define RENAME_TIMEOUT 10 + +/** Pending channel rename request. + * + * Deliberately holds NO struct Channel * across the AC round-trip: the + * channel can be freed/reallocated (rename_channel() MyFree()s the old + * node when the new name is longer than the old one — channel.c) or + * destroyed outright (last member parts mid-flight) while this request + * is waiting on services, up to RENAME_TIMEOUT seconds away. Only the + * name survives that wait; every consumer re-resolves via + * FindChannel(oldname) at the point it actually needs the channel. */ +struct PendingRename { + struct Client *client; /**< Client waiting for response */ + char oldname[CHANNELLEN + 1]; /**< Original channel name */ + char newname[CHANNELLEN + 1]; /**< Requested new name */ + char reason[TOPICLEN + 1]; /**< Rename reason */ + unsigned int cookie; /**< Unique identifier for matching response */ + struct Timer timeout; /**< Timeout timer */ + int timer_active; /**< Whether the timer still owns this + struct's lifetime -- see the + free-once note on + pending_rename_timeout_cb() */ + struct PendingRename *next; /**< Linked list */ +}; + +/** Global pending rename list */ +static struct PendingRename *pending_renames = NULL; +static int pending_rename_count = 0; +static unsigned int rename_cookie_counter = 1; + +/* Forward declarations */ +static void pending_rename_timeout_cb(struct Event *ev); +static void send_rename_to_members(struct Client *sptr, struct Channel *chptr, + const char *oldname, const char *reason); + +/** Find the services server. + * @return Pointer to services server, or NULL if not connected. + */ +static struct Client *find_services_server(void) +{ + struct Client *acptr; + + for (acptr = GlobalClientList; acptr; acptr = cli_next(acptr)) { + if (IsServer(acptr) && IsService(acptr)) + return acptr; + } + + return NULL; +} + +/** Channel-oplevel gate for rename authorization. When a channel has + * oplevels active (MODE_APASS / +A can only be set with FEAT_OPLEVELS), + * ownership is founder-based: only the founder at oplevel 0 may rename it, + * not an arbitrary chanop opped by the founder. Without oplevels there is + * no founder notion and any chanop may rename (traditional behaviour). + * @return non-zero if \a member is permitted to rename \a chptr. + */ +static int rename_oplevel_ok(struct Channel *chptr, struct Membership *member) +{ + /* Apass presence is the mode.apass string being non-empty, not a + * mode.mode bit (the codebase tests it this way everywhere, e.g. + * channel.c:390/1055/1327). Founder == oplevel 0 / channel manager + * (m_join.c:280 assigns the manager 0 and other ops 1 on apass chans); + * accept either so post-burst oplevel drift can't lock the founder out. */ + return !chptr->mode.apass[0] + || (member && (OpLevel(member) == 0 || IsChannelManager(member))); +} + +/** RENAME is only sound when every server can apply RN. Legacy servers + * neither relay nor apply unknown tokens, so refuse while any + * non-rename-capable, non-service server is linked. Rename-capable + * peers advertise the 'r' SERVER flag (FLAG_RENAME_CAPABLE); services + * are exempt: a services package that cares about RENAME advertises 'r' + * itself and then receives RN like any other capable peer, while one + * that does not is simply left out of the broadcast. + * @return First blocking legacy server found, or NULL if none. + */ +static struct Client *rename_legacy_blocker(void) +{ + struct Client *acptr; + + for (acptr = GlobalClientList; acptr; acptr = cli_next(acptr)) + if (IsServer(acptr) && !IsRenameCapable(acptr) && !IsService(acptr)) + return acptr; + + return NULL; +} + +/** Add a pending rename request. + * @param[in] client Client requesting the rename. + * @param[in] channel Channel to be renamed; only its current name is + * snapshotted here — the pointer itself is not kept + * (see the PendingRename struct comment). + * @param[in] newname New channel name. + * @param[in] reason Rename reason. + * @return Pointer to new pending request, or NULL on error. + */ +static struct PendingRename *pending_rename_add(struct Client *client, + struct Channel *channel, + const char *newname, + const char *reason) +{ + struct PendingRename *pr; + + if (pending_rename_count >= RENAME_MAX_PENDING) + return NULL; + + pr = (struct PendingRename *)MyMalloc(sizeof(struct PendingRename)); + if (!pr) + return NULL; + + memset(pr, 0, sizeof(*pr)); + pr->client = client; + ircd_strncpy(pr->oldname, channel->chname, CHANNELLEN + 1); + ircd_strncpy(pr->newname, newname, CHANNELLEN + 1); + if (reason && *reason) { + ircd_strncpy(pr->reason, reason, TOPICLEN + 1); + } + pr->cookie = rename_cookie_counter++; + + /* Add to list */ + pr->next = pending_renames; + pending_renames = pr; + pending_rename_count++; + + /* Start timeout timer */ + timer_add(timer_init(&pr->timeout), pending_rename_timeout_cb, (void *)pr, + TT_RELATIVE, RENAME_TIMEOUT); + pr->timer_active = 1; + + log_write(LS_DEBUG, L_DEBUG, 0, + "pending_rename_add: cookie=%u channel=%s newname=%s client=%C", + pr->cookie, pr->oldname, pr->newname, client); + + return pr; +} + +/** Find a pending rename by cookie. + * @param[in] cookie Cookie to search for. + * @return Pointer to pending request, or NULL if not found. + */ +struct PendingRename *pending_rename_find(unsigned int cookie) +{ + struct PendingRename *pr; + + for (pr = pending_renames; pr; pr = pr->next) { + if (pr->cookie == cookie) + return pr; + } + + return NULL; +} + +/** Find a pending rename by the channel's original name, case-insensitive. + * Used at request time to refuse a second concurrent RENAME on a channel + * that already has one in flight — without this, two pendings for the + * same channel can both complete, and the first completion's + * rename_channel() can free the node the second one is about to touch. + * @param[in] name Channel name to search for. + * @return Pointer to pending request, or NULL if not found. + */ +static struct PendingRename *pending_rename_find_by_name(const char *name) +{ + struct PendingRename *pr; + + for (pr = pending_renames; pr; pr = pr->next) { + if (0 == ircd_strcmp(pr->oldname, name)) + return pr; + } + + return NULL; +} + +/** Unlink a pending rename from the global list. Does NOT touch the + * timer and does NOT free pr -- see the free-once note on + * pending_rename_timeout_cb() for why the actual MyFree() is deferred to + * that function's ET_DESTROY case. + * @param[in] pr Pending request to unlink. + */ +static void pending_rename_unlink(struct PendingRename *pr) +{ + struct PendingRename **pp; + + for (pp = &pending_renames; *pp; pp = &(*pp)->next) { + if (*pp == pr) { + *pp = pr->next; + pending_rename_count--; + break; + } + } +} + +/** Remove a pending rename request: unlink it and cancel its timer. + * + * pr's struct Timer is embedded in pr itself, so pr cannot be freed until + * the timer generator is actually torn down. timer_del() on a timer + * that is NOT mid-dispatch synchronously fires ET_DESTROY right here + * (see ircd_events.c timer_del()/event_generate()), and + * pending_rename_timeout_cb()'s ET_DESTROY case does the MyFree() -- so + * by the time this call returns, pr is already gone. This function is + * only ever called from contexts that are NOT inside the timer callback + * (pending_rename_complete(), pending_rename_deny()), so timer_active is + * always true here; the check is defensive. + * @param[in] pr Pending request to remove. + */ +static void pending_rename_remove(struct PendingRename *pr) +{ + if (!pr) + return; + + log_write(LS_DEBUG, L_DEBUG, 0, + "pending_rename_remove: cookie=%u", pr->cookie); + + pending_rename_unlink(pr); + + if (pr->timer_active) { + pr->timer_active = 0; + timer_del(&pr->timeout); /* -> ET_DESTROY -> MyFree(pr), synchronously */ + } +} + +/** Complete a pending rename (called when services approves). + * @param[in] pr Pending request to complete. + */ +void pending_rename_complete(struct PendingRename *pr) +{ + struct Channel *chptr; + int rc; + + if (!pr || !pr->client) + return; + + log_write(LS_DEBUG, L_DEBUG, 0, + "pending_rename_complete: cookie=%u oldname=%s newname=%s", + pr->cookie, pr->oldname, pr->newname); + + /* Re-check for a legacy blocker: a non-rename-capable, non-service + * server can link during the up-to-RENAME_TIMEOUT-second AC + * round-trip, after m_rename's request-time check already passed. + * Nothing has been applied anywhere on the network yet at this point + * (the rename below is the first mutation), so deny rather than + * complete — that's the conservative move that keeps every server + * consistent. */ + if (rename_legacy_blocker()) { + pending_rename_deny(pr, "A linked server does not support channel rename"); + return; + } + + /* Re-resolve the channel by name instead of trusting a cached pointer: + * across the round-trip the channel may have been destroyed (last + * member parted mid-flight) or, absent the request-time dedup, freed + * and reallocated by a same-channel completion that ran first + * (rename_channel() MyFree()s the old node when the new name is + * longer). A miss here — channel gone, or since renamed out from + * under this name by some other path — is treated as a deny, not a + * silent drop, so the requester gets a FAIL instead of nothing. */ + if (!(chptr = FindChannel(pr->oldname))) { + log_write(LS_DEBUG, L_WARNING, 0, + "pending_rename_complete: channel %s no longer exists", + pr->oldname); + pending_rename_deny(pr, "Channel no longer exists"); + return; + } + + /* A name match alone isn't enough: with FEAT_ZANNELS off, an empty + * registered channel destructs synchronously when its last member + * parts, and a DIFFERENT user can recreate that exact name before this + * completion runs — FindChannel() above would then resolve to that + * unrelated fresh channel (mode 0, no +R, pr->client not even a + * member). Re-validate the same authorization m_rename required at + * request time — requester still a chanop member, channel still + * MODE_REGISTERED — before letting the approved rename land on it. */ + { + struct Membership *member = find_channel_member(pr->client, chptr); + if (!member || !IsChanOp(member) || !rename_oplevel_ok(chptr, member) + || !(chptr->mode.mode & MODE_REGISTERED)) { + log_write(LS_DEBUG, L_WARNING, 0, + "pending_rename_complete: channel %s changed identity while " + "awaiting services", pr->oldname); + pending_rename_deny(pr, "Channel changed while awaiting services"); + return; + } + } + + /* Re-run the BADCHAN check from m_rename: a badchan on the target can + * land during the services round-trip, and the approval must not + * override it (services arbitrate ownership, not G-line policy). */ + { + struct Gline *gline = gline_find(pr->newname, GLINE_BADCHAN | GLINE_EXACT); + if (gline && GlineIsActive(gline) && !IsAnOper(pr->client)) { + pending_rename_deny(pr, "Target channel name is banned"); + return; + } + } + + /* Perform the rename (updates chptr if reallocated) */ + rc = rename_channel(&chptr, pr->newname); + if (rc != 0) { + send_fail(pr->client, "RENAME", "CANNOT_RENAME", pr->oldname, + "Rename failed"); + pending_rename_remove(pr); + return; + } + + /* Send to local channel members */ + send_rename_to_members(pr->client, chptr, pr->oldname, pr->reason); + + /* Propagate to rename-capable ('r') servers; a peer that never + * advertised the flag would neither apply nor relay RN, and the + * legacy-blocker guard above guarantees no such peer (services + * excepted) is linked. */ + sendcmdto_flag_serv_butone(pr->client, CMD_RENAME, pr->client, + FLAG_RENAME_CAPABLE, FLAG_LAST_FLAG, + "%s %s :%s", pr->oldname, chptr->chname, + pr->reason); + + pending_rename_remove(pr); +} + +/** Deny a pending rename (called when services denies). + * @param[in] pr Pending request to deny. + * @param[in] reason Reason for denial. + */ +void pending_rename_deny(struct PendingRename *pr, const char *reason) +{ + if (!pr || !pr->client) + return; + + log_write(LS_DEBUG, L_DEBUG, 0, + "pending_rename_deny: cookie=%u reason=%s", + pr->cookie, reason ? reason : "Permission denied"); + + send_fail(pr->client, "RENAME", "CANNOT_RENAME", pr->oldname, + reason ? reason : "Permission denied"); + + pending_rename_remove(pr); +} + +/** Timeout callback for pending rename. + * + * pr's struct Timer is embedded in pr (see the PendingRename struct + * comment), so pr must NOT be freed from the ET_EXPIRE branch below: + * timer_run() (ircd_events.c) sets GEN_MARKED on the timer generator + * before calling this callback and keeps touching that generator after + * we return (clearing GEN_MARKED, then generating ET_DESTROY) -- freeing + * pr here would make all of that a use-after-free. timer_del() called + * from within an ET_EXPIRE dispatch hits its own "timer is being used" + * guard and does nothing, so it can't be used to neutralize this either. + * + * Instead: ET_EXPIRE does the user-facing work and unlinks pr from the + * list (so it can't be found or double-completed), but leaves the + * actual free to the ET_DESTROY case, which fires either automatically + * right after ET_EXPIRE returns (the timeout path) or synchronously + * from pending_rename_remove()'s timer_del() call (the complete/deny/ + * client-exit paths). MyFree(pr) happens in exactly one place -- the + * ET_DESTROY case below -- so there is exactly one free per pending + * rename no matter which of the five teardown paths (complete-success, + * complete-failure, deny, timeout, client-exit) retires it. + * @param[in] ev Timer event. + */ +static void pending_rename_timeout_cb(struct Event *ev) +{ + struct PendingRename *pr; + + assert(0 != ev_timer(ev)); + assert(0 != t_data(ev_timer(ev))); + + pr = (struct PendingRename *)t_data(ev_timer(ev)); + + switch (ev_type(ev)) { + case ET_EXPIRE: + log_write(LS_DEBUG, L_DEBUG, 0, + "pending_rename_timeout: cookie=%u channel=%s", + pr->cookie, pr->oldname); + + send_fail(pr->client, "RENAME", "CANNOT_RENAME", pr->oldname, + "Services response timeout"); + + /* Do NOT call timer_del() (a no-op while mid-dispatch, see above) + * and do NOT MyFree(pr) here -- just make pr unreachable until + * ET_DESTROY frees it below. */ + pending_rename_unlink(pr); + pr->timer_active = 0; + break; + + case ET_DESTROY: + /* The only place pr is freed -- see the function comment above. */ + MyFree(pr); + break; + + default: + break; + } +} + +/** Cleanup pending renames for a disconnecting client. + * @param[in] cptr Client that is disconnecting. + */ +void pending_rename_client_exit(struct Client *cptr) +{ + struct PendingRename *pr, *next; + + /* Cache pr->next before unlinking/triggering the free below -- pr may + * already be gone (freed via the synchronous ET_DESTROY that + * timer_del() triggers) by the time a naive iteration would otherwise + * need to read pr->next. */ + for (pr = pending_renames; pr; pr = next) { + next = pr->next; + + if (pr->client == cptr) { + log_write(LS_DEBUG, L_DEBUG, 0, + "pending_rename_client_exit: removed cookie=%u for %C", + pr->cookie, cptr); + + pending_rename_unlink(pr); + + if (pr->timer_active) { + pr->timer_active = 0; + timer_del(&pr->timeout); /* -> ET_DESTROY -> MyFree(pr) */ + } + } + } +} + +/* ========== End Pending Rename Infrastructure ========== */ + +/** Send RENAME to clients with the capability, fallback PART/JOIN to others. + * @param[in] sptr Client that initiated the rename. + * @param[in] chptr Channel being renamed (already has new name). + * @param[in] oldname The old channel name. + * @param[in] reason Reason for rename (may be empty string). + */ +static void send_rename_to_members(struct Client *sptr, struct Channel *chptr, + const char *oldname, const char *reason) +{ + struct Membership *member; + struct Client *acptr; + + for (member = chptr->members; member; member = member->next_member) { + acptr = member->user; + + if (!MyUser(acptr)) + continue; + + if (CapActive(acptr, CAP_DRAFT_CHANRENAME)) { + /* Client with cap — send RENAME */ + sendcmdto_one(sptr, CMD_RENAME, acptr, "%s %s :%s", + oldname, chptr->chname, reason ? reason : ""); + } else { + /* Client without cap — PART/JOIN fallback */ + sendcmdto_one(acptr, CMD_PART, acptr, "%s :Channel renamed to %s%s%s", + oldname, chptr->chname, + (reason && *reason) ? ": " : "", + (reason && *reason) ? reason : ""); + sendcmdto_one(acptr, CMD_JOIN, acptr, "%s", chptr->chname); + if (chptr->topic[0]) { + send_reply(acptr, RPL_TOPIC, chptr->chname, chptr->topic); + send_reply(acptr, RPL_TOPICWHOTIME, chptr->chname, chptr->topic_nick, + chptr->topic_time); + } + do_names(acptr, chptr, NAMES_ALL|NAMES_EON); + } + } +} + +/** m_rename - Handle RENAME command from local client. + * + * parv[0] = sender prefix + * parv[1] = old channel name + * parv[2] = new channel name + * parv[3] = reason (optional, trailing) + * + * @param[in] cptr Client that sent us the message. + * @param[in] sptr Original source of message. + * @param[in] parc Number of arguments. + * @param[in] parv Argument vector. + */ +int m_rename(struct Client *cptr, struct Client *sptr, int parc, char *parv[]) +{ + struct Channel *chptr; + struct Membership *member; + const char *oldname; + const char *newname; + const char *reason; + char oldname_buf[CHANNELLEN + 1]; + int rc; + + /* Per the draft/channel-rename spec the capability governs only how a + * client is *notified* of a rename (the RENAME message vs the PART/JOIN + * legacy fallback, handled per-member in send_rename_to_members) — it is + * NOT required to *issue* the command. A client without the cap may + * rename and simply receives the fallback for itself; do not reject with + * ERR_UNKNOWNCOMMAND. */ + + /* Need at least old and new channel names */ + if (parc < 3 || EmptyString(parv[1]) || EmptyString(parv[2])) { + return send_reply(sptr, ERR_NEEDMOREPARAMS, "RENAME"); + } + + oldname = parv[1]; + newname = parv[2]; + reason = (parc > 3 && !EmptyString(parv[3])) ? parv[3] : ""; + + /* Check if old channel exists */ + chptr = FindChannel(oldname); + if (!chptr) { + return send_reply(sptr, ERR_NOSUCHCHANNEL, oldname); + } + + /* Check if user is on the channel */ + member = find_channel_member(sptr, chptr); + if (!member) { + return send_reply(sptr, ERR_NOTONCHANNEL, oldname); + } + + /* Check if user is a channel operator */ + if (!IsChanOp(member)) { + return send_reply(sptr, ERR_CHANOPRIVSNEEDED, oldname); + } + + /* With channel oplevels active (+A), only the founder may rename. */ + if (!rename_oplevel_ok(chptr, member)) { + send_fail(sptr, "RENAME", "CANNOT_RENAME", oldname, + "Only the channel founder may rename this channel"); + return 0; + } + + /* Validate new channel name */ + if (!IsChannelName(newname)) { + send_fail(sptr, "RENAME", "CANNOT_RENAME", oldname, + "Invalid channel name"); + return 0; + } + + /* Check if new channel name already exists */ + if (FindChannel(newname)) { + send_fail(sptr, "RENAME", "CHANNEL_NAME_IN_USE", oldname, + "Channel name already in use"); + return 0; + } + + /* BADCHAN-glined target: renaming onto a banned name would move the + * whole membership into a channel none of them could have JOINed — + * do_join() enforces this per-joiner (m_join.c, GLINE_BADCHAN branch); + * without this check RENAME is a wholesale bypass. Mirror do_join's + * exact test, including the oper exemption. Deliberately NOT enforced + * in ms_rename: local badchans never blocked remote users' presence in + * a channel (join-time, local policy), so a remote-origin rename is + * accepted the same way remote joins are. */ + { + struct Gline *gline = gline_find(newname, GLINE_BADCHAN | GLINE_EXACT); + if (gline && GlineIsActive(gline) && !IsAnOper(sptr)) { + send_fail(sptr, "RENAME", "CANNOT_RENAME", oldname, + "Target channel name is banned"); + return 0; + } + } + + /* RENAME is only sound when every linked server can apply RN. Refuse + * up front while a legacy (non-rename-capable, non-service) server is + * on the network, rather than only in the registered-channel branch + * below — an unregistered-channel rename that a legacy peer can + * neither relay nor apply diverges it from the rest of the network + * just as surely. */ + { + struct Client *blocker = rename_legacy_blocker(); + if (blocker) { + send_fail(sptr, "RENAME", "CANNOT_RENAME", oldname, + "A linked server does not support channel rename"); + return 0; + } + } + + /* Registered channels (+R): authority is the channel's founder/access list, + * which lives in services, not here. We must ask services. + * + * We used to ask services with + * AC R RENAME + * and wait for an AC A/D reply. That query is HARMFUL against the services + * package that actually ships: its AC handler treats subcommand 'R' as an + * account-stamp notification and reads our as the stamp, then sets + * FLAGS_STAMPED on the querying user unconditionally. Because a stamped + * user is skipped by later StampUser() calls, one RENAME attempt silently + * discards that user's *legitimate* account stamp for the rest of their + * session — a real side effect from a command the user only sees time out. + * The 'R' subcommand letter was simply already taken; this is a protocol + * collision on our side, not services mishandling a well-formed query. + * + * A disambiguating services build now exists: it requires argc >= 7 and + * checks for the literal "RENAME" keyword before falling through to the + * account-stamp path, so the two no longer collide. FEAT_RENAME_SERVICES + * records which deployments are running that build and may safely be + * asked — it defaults OFF, so every deployment still running the old + * services package keeps today's honest local refusal instead of + * re-triggering the stamp-clobbering bug. + * + * DO NOT delete as dead code: find_services_server() and + * pending_rename_add() above are used by the emit below, and + * pending_rename_find/complete/deny are still called from m_account.c's + * AC A/D reply path, with pending_rename_client_exit called from s_misc.c. + */ + if (chptr->mode.mode & MODE_REGISTERED) { + struct Client *services; + struct PendingRename *pr; + + if (!feature_bool(FEAT_RENAME_SERVICES)) { + send_fail(sptr, "RENAME", "CANNOT_RENAME", oldname, + "Renaming a registered channel requires services support, " + "which is unavailable"); + return 0; + } + + if (!(services = find_services_server())) { + send_fail(sptr, "RENAME", "TEMPORARILY_UNAVAILABLE", oldname, + "Registration service is not available"); + return 0; + } + + /* Cheap request-time dedup: a second concurrent RENAME on the same + * channel would produce two pendings racing the same completion path + * (see the PendingRename struct comment) — refuse it up front rather + * than relying solely on pending_rename_complete()'s re-resolve to + * paper over it. */ + if (pending_rename_find_by_name(chptr->chname)) { + send_fail(sptr, "RENAME", "TEMPORARILY_UNAVAILABLE", oldname, + "A rename is already in progress for this channel"); + return 0; + } + + if (!(pr = pending_rename_add(sptr, chptr, newname, reason))) { + send_fail(sptr, "RENAME", "TEMPORARILY_UNAVAILABLE", oldname, + "Too many renames in progress"); + return 0; + } + + sendcmdto_one(&me, CMD_ACCOUNT, services, "%C R %u %s RENAME %s", + sptr, pr->cookie, chptr->chname, newname); + return 0; + } + + /* Unregistered channel - proceed with rename immediately */ + + /* Store old name before rename */ + ircd_strncpy(oldname_buf, chptr->chname, CHANNELLEN + 1); + + /* Perform the rename (updates chptr if reallocated) */ + rc = rename_channel(&chptr, newname); + if (rc == -1) { + send_fail(sptr, "RENAME", "CANNOT_RENAME", oldname, + "New channel name is too long"); + return 0; + } else if (rc == -2) { + send_fail(sptr, "RENAME", "CHANNEL_NAME_IN_USE", oldname, + "Channel name already in use"); + return 0; + } + + /* Send to local channel members */ + send_rename_to_members(sptr, chptr, oldname_buf, reason); + + /* Propagate to rename-capable ('r') servers; the legacy-blocker guard + * above guarantees every non-service peer has the flag. */ + sendcmdto_flag_serv_butone(sptr, CMD_RENAME, cptr, + FLAG_RENAME_CAPABLE, FLAG_LAST_FLAG, + "%s %s :%s", oldname_buf, chptr->chname, reason); + + return 0; +} + +/** ms_rename - Handle RENAME command from a server. + * + * parv[0] = sender prefix (numeric) + * parv[1] = old channel name + * parv[2] = new channel name + * parv[3] = reason (optional, trailing) + * + * @param[in] cptr Client that sent us the message. + * @param[in] sptr Original source of message. + * @param[in] parc Number of arguments. + * @param[in] parv Argument vector. + */ +int ms_rename(struct Client *cptr, struct Client *sptr, int parc, char *parv[]) +{ + struct Channel *chptr; + const char *oldname; + const char *newname; + const char *reason; + char oldname_buf[CHANNELLEN + 1]; + int rc; + + /* Need at least old and new channel names */ + if (parc < 3 || EmptyString(parv[1]) || EmptyString(parv[2])) { + return 0; /* Silently ignore malformed S2S messages */ + } + + oldname = parv[1]; + newname = parv[2]; + reason = (parc > 3 && !EmptyString(parv[3])) ? parv[3] : ""; + + /* Find the channel */ + chptr = FindChannel(oldname); + if (!chptr) { + return 0; /* Channel doesn't exist on this server */ + } + + /* Store old name before rename */ + ircd_strncpy(oldname_buf, chptr->chname, CHANNELLEN + 1); + + /* Perform the rename (updates chptr if reallocated) */ + rc = rename_channel(&chptr, newname); + if (rc != 0) { + /* Rename failed - log and continue */ + log_write(LS_DEBUG, L_ERROR, 0, + "RENAME failed from %#C: %s -> %s (rc=%d)", + sptr, oldname, newname, rc); + return 0; + } + + /* Send to local channel members */ + send_rename_to_members(sptr, chptr, oldname_buf, reason); + + /* Propagate to rename-capable ('r') servers, skipping the link this + * RENAME arrived on. */ + sendcmdto_flag_serv_butone(sptr, CMD_RENAME, cptr, + FLAG_RENAME_CAPABLE, FLAG_LAST_FLAG, + "%s %s :%s", oldname_buf, chptr->chname, reason); + + return 0; +} diff --git a/ircd/m_server.c b/ircd/m_server.c index 989612e5..0b377544 100644 --- a/ircd/m_server.c +++ b/ircd/m_server.c @@ -493,6 +493,7 @@ void set_server_flags(struct Client *cptr, const char *flags) case 's': SetService(cptr); break; case '6': SetIPv6(cptr); break; case 'o': SetOpLevels(cptr); break; + case 'r': SetRenameCapable(cptr); break; } } @@ -783,11 +784,12 @@ int ms_server(struct Client* cptr, struct Client* sptr, int parc, char* parv[]) continue; if (0 == match(cli_name(&me), cli_name(acptr))) continue; - sendcmdto_one(sptr, CMD_SERVER, bcptr, "%s %d 0 %s %s %s%s +%s%s%s%s :%s", + sendcmdto_one(sptr, CMD_SERVER, bcptr, "%s %d 0 %s %s %s%s +%s%s%s%s%s :%s", cli_name(acptr), hop + 1, parv[4], parv[5], NumServCap(acptr), IsHub(acptr) ? "h" : "", IsService(acptr) ? "s" : "", IsIPv6(acptr) ? "6" : "", - IsOpLevels(acptr) ? "o" : "", cli_info(acptr)); + IsOpLevels(acptr) ? "o" : "", + IsRenameCapable(acptr) ? "r" : "", cli_info(acptr)); } return 0; } diff --git a/ircd/parse.c b/ircd/parse.c index e19cc09a..34da06a5 100644 --- a/ircd/parse.c +++ b/ircd/parse.c @@ -940,6 +940,14 @@ struct Message msgtab[] = { { m_unregistered, m_not_oper, ms_remove, mo_remove, m_ignore }, "" }, + { + MSG_RENAME, + TOK_RENAME, + 0, MAXPARA, MFLG_SLOW, 0, NULL, + /* UNREG, CLIENT, SERVER, OPER, SERVICE */ + { m_unregistered, m_rename, ms_rename, m_rename, m_ignore }, + " [:] - Rename a channel" + }, { MSG_WATCH, TOK_WATCH, diff --git a/ircd/s_bsd.c b/ircd/s_bsd.c index 759a3d31..0303a233 100644 --- a/ircd/s_bsd.c +++ b/ircd/s_bsd.c @@ -405,7 +405,7 @@ static int completed_connection(struct Client* cptr) cli_lasttime(cptr) = CurrentTime; ClearPingSent(cptr); - sendrawto_one(cptr, MSG_SERVER " %s 1 %Tu %Tu J%s %s%s +%s6%s :%s", + sendrawto_one(cptr, MSG_SERVER " %s 1 %Tu %Tu J%s %s%s +%s6%sr :%s", cli_name(&me), cli_serv(&me)->timestamp, newts, MAJOR_PROTOCOL, NumServCap(&me), feature_bool(FEAT_HUB) ? "h" : "", diff --git a/ircd/s_misc.c b/ircd/s_misc.c index 2ad144f3..f0c737e6 100644 --- a/ircd/s_misc.c +++ b/ircd/s_misc.c @@ -394,6 +394,7 @@ int exit_client(struct Client *cptr, if (IsUser(victim) || IsUserPort(victim)) { abort_sasl(victim, 0); auth_send_exit(victim); + pending_rename_client_exit(victim); } if (IsUser(victim)) diff --git a/ircd/s_serv.c b/ircd/s_serv.c index 7e0c627c..d5d6b1fb 100644 --- a/ircd/s_serv.c +++ b/ircd/s_serv.c @@ -132,7 +132,7 @@ int server_estab(struct Client *cptr, struct ConfItem *aconf) /* * Pass my info to the new server */ - sendrawto_one(cptr, MSG_SERVER " %s 1 %Tu %Tu J%s %s%s +%s6%s :%s", + sendrawto_one(cptr, MSG_SERVER " %s 1 %Tu %Tu J%s %s%s +%s6%sr :%s", cli_name(&me), cli_serv(&me)->timestamp, cli_serv(cptr)->timestamp, MAJOR_PROTOCOL, NumServCap(&me), feature_bool(FEAT_HUB) ? "h" : "", @@ -189,11 +189,11 @@ int server_estab(struct Client *cptr, struct ConfItem *aconf) if (!match(cli_name(&me), cli_name(cptr))) continue; sendcmdto_one(&me, CMD_SERVER, acptr, - "%s 2 0 %Tu J%02u %s%s +%s%s%s%s :%s", cli_name(cptr), + "%s 2 0 %Tu J%02u %s%s +%s%s%s%s%s :%s", cli_name(cptr), cli_serv(cptr)->timestamp, Protocol(cptr), NumServCap(cptr), IsHub(cptr) ? "h" : "", IsService(cptr) ? "s" : "", IsIPv6(cptr) ? "6" : "", IsOpLevels(cptr) ? "o" : "", - cli_info(cptr)); + IsRenameCapable(cptr) ? "r" : "", cli_info(cptr)); } /* Send these as early as possible so that glined users/zlined users/juped servers can @@ -235,12 +235,12 @@ int server_estab(struct Client *cptr, struct ConfItem *aconf) if (0 == match(cli_name(&me), cli_name(acptr))) continue; sendcmdto_one(cli_serv(acptr)->up, CMD_SERVER, cptr, - "%s %d 0 %Tu %s%u %s%s +%s%s%s%s :%s", cli_name(acptr), + "%s %d 0 %Tu %s%u %s%s +%s%s%s%s%s :%s", cli_name(acptr), cli_hopcount(acptr) + 1, cli_serv(acptr)->timestamp, protocol_str, Protocol(acptr), NumServCap(acptr), IsHub(acptr) ? "h" : "", IsService(acptr) ? "s" : "", IsIPv6(acptr) ? "6" : "", IsOpLevels(acptr) ? "o" : "", - cli_info(acptr)); + IsRenameCapable(acptr) ? "r" : "", cli_info(acptr)); } } diff --git a/ircd/send.c b/ircd/send.c index 9371ef16..b442a5c2 100644 --- a/ircd/send.c +++ b/ircd/send.c @@ -1089,3 +1089,34 @@ void vsendto_mode_butone(struct Client *one, struct Client *from, const char *mo msgq_clean(mb); } + +/** + * Send a FAIL reply to a local client (IRCv3 standard-replies). + * Minimal tag-free port of the fork's send_standard_reply(): emits the + * bare FAIL line with no label/server-time tag decoration (upstream has + * no message-tag machinery). The wire format matches the fork's untagged + * branch exactly so clients see identical strings on both. + * @param[in] to Client to send to. + * @param[in] command Command name (or "*" for general failure). + * @param[in] code Machine-readable error code. + * @param[in] context Optional context (NULL if none). + * @param[in] description Human-readable error message. + */ +void send_fail(struct Client *to, const char *command, const char *code, + const char *context, const char *description) +{ + struct MsgBuf *mb; + + if (!MyConnect(to)) + return; + + /* Send FAIL as an actual IRC command per IRCv3 standard-replies; + * legacy clients simply ignore unknown commands. */ + if (context && *context) + mb = msgq_make(to, "%:#C FAIL %s %s %s :%s", &me, command, code, context, description); + else + mb = msgq_make(to, "%:#C FAIL %s %s :%s", &me, command, code, description); + + send_buffer(to, mb, 0); + msgq_clean(mb); +}