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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions doc/example.conf
Original file line number Diff line number Diff line change
Expand Up @@ -1159,6 +1159,8 @@ features
# "CAP_MESSAGE_TAGS" = "TRUE";
# "CAP_SERVER_TIME" = "TRUE";
# "CAP_ACCOUNT_TAG" = "TRUE";
# "CAP_BATCH" = "TRUE";
# "CAP_LABELED_RESPONSE" = "TRUE";
# Deny all client-only message tags by default (IRCv3 CLIENTTAGDENY).
# "CLIENTTAGDENY" = "*";
# These were introduced by Undernet CFV-165 to add "Head-In-Sand" (HIS)
Expand Down
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ services:
- "7000:7000"
- "7001:7001"
- "7002:7002"
- "6695:6695"
networks:
ircu-test-net:
ipv4_address: 10.55.0.10
Expand Down
30 changes: 16 additions & 14 deletions include/capab.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,30 +40,32 @@
#define CAPFL_UNAVAILABLE (CAPFL_HIDDEN | CAPFL_PROHIBIT)

#define CAPLIST \
_CAP(ACCOUNTNOTIFY, FEAT_CAP_ACCOUNTNOTIFY, 0, "account-notify"), \
_CAP(AWAYNOTIFY, FEAT_CAP_AWAYNOTIFY, 0 , "away-notify"), \
_CAP(CHGHOST, FEAT_CAP_CHGHOST, 0, "chghost"), \
_CAP(ECHOMESSAGE, FEAT_CAP_ECHOMESSAGE, 0, "echo-message"), \
_CAP(EXTJOIN, FEAT_CAP_EXTJOIN, 0, "extended-join"), \
_CAP(INVITENOTIFY, FEAT_CAP_INVITENOTIFY, 0, "invite-notify"), \
_CAP(UHNAMES, FEAT_CAP_UHNAMES, 0, "userhost-in-names"), \
_CAP(MESSAGE_TAGS, FEAT_CAP_MESSAGE_TAGS, 0, "message-tags"), \
_CAP(SERVER_TIME, FEAT_CAP_SERVER_TIME, 0, "server-time"), \
_CAP(ACCOUNT_TAG, FEAT_CAP_ACCOUNT_TAG, 0, "account-tag"), \
_CAP(CAPNOTIFY, 0, CAPFL_HIDDEN_302 | CAPFL_STICKY_302, "cap-notify"), \
_CAP(SASL, FEAT_CAP_SASL, CAPFL_UNAVAILABLE, "sasl")
_CAP(ACCOUNTNOTIFY, FEAT_CAP_ACCOUNTNOTIFY, 0, 0, "account-notify"), \
_CAP(AWAYNOTIFY, FEAT_CAP_AWAYNOTIFY, 0, 0, "away-notify"), \
_CAP(CHGHOST, FEAT_CAP_CHGHOST, 0, 0, "chghost"), \
_CAP(ECHOMESSAGE, FEAT_CAP_ECHOMESSAGE, 0, 0, "echo-message"), \
_CAP(EXTJOIN, FEAT_CAP_EXTJOIN, 0, 0, "extended-join"), \
_CAP(INVITENOTIFY, FEAT_CAP_INVITENOTIFY, 0, 0, "invite-notify"), \
_CAP(UHNAMES, FEAT_CAP_UHNAMES, 0, 0, "userhost-in-names"), \
_CAP(MESSAGE_TAGS, FEAT_CAP_MESSAGE_TAGS, 0, 0, "message-tags"), \
_CAP(SERVER_TIME, FEAT_CAP_SERVER_TIME, 0, 0, "server-time"), \
_CAP(ACCOUNT_TAG, FEAT_CAP_ACCOUNT_TAG, 0, 0, "account-tag"), \
_CAP(BATCH, FEAT_CAP_BATCH, 0, 0, "batch"), \
_CAP(LABELED_RESPONSE, FEAT_CAP_LABELED_RESPONSE, 0, CAP_BATCH, "labeled-response"), \
_CAP(CAPNOTIFY, 0, CAPFL_HIDDEN_302 | CAPFL_STICKY_302, 0, "cap-notify"), \
_CAP(SASL, FEAT_CAP_SASL, CAPFL_UNAVAILABLE, 0, "sasl")

/** Client capabilities, counting by index. */
enum Capab {
#define _CAP(cap, config, flags, name) E_CAP_ ## cap
#define _CAP(cap, config, flags, dependencies, name) E_CAP_ ## cap
CAPLIST,
#undef _CAP
_E_CAP_LAST_CAP
};

/** Client capabilities, bit mask version. */
enum CapabBits {
#define _CAP(cap, config, flags, name) CAP_ ## cap = 1u << E_CAP_ ## cap
#define _CAP(cap, config, flags, dependencies, name) CAP_ ## cap = 1u << E_CAP_ ## cap
CAPLIST,
#undef _CAP
_CAP_LAST_CAP = 1u << _E_CAP_LAST_CAP
Expand Down
6 changes: 6 additions & 0 deletions include/channel.h
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,12 @@ struct ListingArgs {
time_t min_topic_time;
unsigned int bucket;
char wildcard[CHANNELLEN];
/** ref of the LabelCapture this listing is continuing on behalf of, or
* an empty string if this LIST wasn't labeled. Set by parse.c once the
* initial dispatch leaves a listing running past its own return; read
* by list_next_channels() (natural completion) and by m_list.c's
* already-listing/STOP path (interrupted early). */
char label_ref[16];
};

struct ModeBuf {
Expand Down
44 changes: 44 additions & 0 deletions include/client.h
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,46 @@ struct Whowas;
struct hostent;
struct Privs;
struct AuthRequest;
struct LabelDeferred; /* opaque; defined in label.c */

/** One outstanding labeled-response capture for a client.
*
* A client may have several of these at once (e.g. a parked LIST and an
* unrelated command both labeled). Each is independently identified by
* \a ref, the eventual client-facing BATCH reference.
*
* The list hangs off the struct Client itself (cli_labelcap()), not the
* Connection: a *remote* requester whose hunted command this server
* answers has no Connection of its own here (cli_connect() aliases the
* S2S link), and its captures must not be confused with those of other
* remote users behind the same link. Only lines addressed to that exact
* client are captured (label_capture_intercept() runs on the intended
* recipient, before cli_from() resolution) -- never other traffic that
* merely travels down the same link.
*
* Briefly "active" (the current recipient of anything sent to its owner)
* during a synchronous command dispatch or a single continuation tick
* (e.g. one call to list_next_channels()); "parked" the rest of the
* time, waiting for whatever will eventually finish it.
*/
struct LabelCapture {
struct LabelCapture *next;
char ref[16];
char value[LABEL_VALUE_MAX + 1];
struct LabelDeferred *head;
struct LabelDeferred **tail;
unsigned int count;
unsigned int bytes;
/** If set, this capture streams: its BATCH open line has already been
* emitted, and every line sent while it's active goes straight to the
* wire tagged batch=ref instead of being deferred into head/tail (so
* count/bytes and the capture-overflow safety valve do not apply to
* it). Finishing it only emits the BATCH close. For a response that's
* unconditionally multi-line and may span many event-loop ticks (LIST)
* rather than one where the eventual line count decides ACK vs.
* single-line vs. BATCH. See label_capture_stream_active() in label.c. */
int streaming;
};

/*
* Structures
Expand Down Expand Up @@ -280,6 +320,7 @@ struct Client {
struct Client* cli_hnext; /**< link in hash table bucket or this */
struct Connection* cli_connect; /**< Connection structure associated with us */
struct User* cli_user; /**< Defined if this client is a user */
struct LabelCapture* cli_labelcap; /**< Outstanding labeled-response captures. */
struct Server* cli_serv; /**< Defined if this client is a server */
struct Whowas* cli_whowas; /**< Pointer to ww struct to be freed on quit */
char cli_yxx[4]; /**< Numeric Nick: YY if this is a
Expand Down Expand Up @@ -393,6 +434,9 @@ struct Client {
#define cli_handler(cli) con_handler(cli_connect(cli))
/** Get LIST status for client. */
#define cli_listing(cli) con_listing(cli_connect(cli))
/** Get outstanding labeled-response captures for client (per client, not
* per connection: a remote requester has no connection of its own). */
#define cli_labelcap(cli) ((cli)->cli_labelcap)
/** Get cached max SendQ for client. */
#define cli_max_sendq(cli) con_max_sendq(cli_connect(cli))
/** Get cached flood limit for client. */
Expand Down
2 changes: 2 additions & 0 deletions include/handlers.h
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,9 @@ extern int mr_server(struct Client*, struct Client*, int, char*[]);
extern int ms_account(struct Client*, struct Client*, int, char*[]);
extern int ms_admin(struct Client*, struct Client*, int, char*[]);
extern int ms_asll(struct Client*, struct Client*, int, char*[]);
extern int ms_ack(struct Client*, struct Client*, int, char*[]);
extern int ms_away(struct Client*, struct Client*, int, char*[]);
extern int ms_batch(struct Client*, struct Client*, int, char*[]);
extern int ms_burst(struct Client*, struct Client*, int, char*[]);
extern int ms_clearmode(struct Client*, struct Client*, int, char*[]);
extern int ms_connect(struct Client*, struct Client*, int, char*[]);
Expand Down
4 changes: 4 additions & 0 deletions include/ircd_defs.h
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,10 @@
* protocol message body (BUFSIZE).
*/
#define READBUFSIZE (TAGSLEN + BUFSIZE)
/** Maximum accepted label= tag value length, in bytes (IRCv3
* labeled-response: "The value MUST NOT exceed 64 bytes").
*/
#define LABEL_VALUE_MAX 64
/** Maximum length of a formatted OUTBOUND message-tags prefix (from the
* leading '@' through the separating space). An outbound prefix can never
* exceed the largest tag data a client is allowed to send inbound
Expand Down
2 changes: 2 additions & 0 deletions include/ircd_features.h
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ enum Feature {
FEAT_CAP_MESSAGE_TAGS,
FEAT_CAP_SERVER_TIME,
FEAT_CAP_ACCOUNT_TAG,
FEAT_CAP_BATCH,
FEAT_CAP_LABELED_RESPONSE,
FEAT_CAP_SASL,

/* IRCv3 CLIENTTAGDENY: deny-list / allow-list for client-only (+) tags */
Expand Down
110 changes: 110 additions & 0 deletions include/label.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*
* IRC - Internet Relay Chat, include/label.h
* Copyright (C) 2026 MrIron <mriron@undernet.org>
*
* 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 IRCv3 labeled-response capture interface (see ircd/label.c).
*/
#ifndef INCLUDED_label_h
#define INCLUDED_label_h

struct Client;
struct LabelCapture;
struct MsgBuf;
struct MsgTagCtx;

/* IRCv3 labeled-response: a connection may have several outstanding
* captures at once (struct LabelCapture, see client.h), each independently
* identified by its ref. At most one is ever "active" (the current
* recipient of anything cptr sends) at a time, for the duration of a
* synchronous command dispatch or a single continuation tick; the rest
* are parked, waiting for whatever will eventually finish them (a later
* list_next_channels() tick, or -- once S2S support lands -- a matching
* inbound batch=ref close from a remote server). */

/* Create a new capture for \a cptr, push it onto its outstanding list, and
* mark it active. Returns the new capture (owned by \a cptr's list; valid
* until finished/aborted/dropped by label_capture_client_gone()). */
extern struct LabelCapture *label_capture_start(struct Client *cptr,
const char *label);
/* Convert the capture currently active for \a cptr into a streaming one
* and emit its BATCH open line immediately, instead of deferring the
* ACK/single-line/BATCH decision to finish() -- for a response that's
* unconditionally multi-line and may span many event-loop ticks (LIST).
* Must be called with a capture already active for cptr. Returns the ref
* to remember (e.g. into ListingArgs.label_ref), or NULL if there was no
* active capture (the command wasn't labeled). */
extern const char *label_capture_stream_active(struct Client *cptr);
/* Resume an existing parked capture (by ref) as the active one for a new
* continuation tick. No-op if not found (e.g. it was already dropped by
* label_capture_client_gone()) -- callers that are about to send
* something meant specifically for that capture (not just "whatever's
* currently active") must check the return value before doing so; a
* silent no-op leaves the *previous* active window (if any) unchanged,
* which is very likely the wrong destination. Returns 1 if reopened,
* 0 if ref didn't resolve to anything. */
extern int label_capture_reopen(struct Client *cptr, const char *ref);
/* End the current dispatch/tick: nothing sent to a client is captured
* again until label_capture_start()/reopen() is called anew. Always safe
* to call (touches no Client), so it can run unconditionally even when
* the handler that just ran may have freed cptr (CPTR_KILLED). */
extern void label_capture_close_window(void);
/* Snapshot/restore the active window around a temporary redirect (e.g.
* reopening a *different* capture to fold one more line into it before
* finishing it) -- unlike finish()/abort(), which only protect their own
* internal replay sends, this covers sends the caller makes itself
* before invoking finish()/abort(). See m_list.c's superseded-listing
* handling for the motivating case. */
extern void label_capture_save_active(struct Client **client_out,
struct LabelCapture **node_out);
extern void label_capture_restore_active(struct Client *client,
struct LabelCapture *node);

/* Normal completion: decide ACK / single-tag / BATCH-wrap for the capture
* \a ref on \a cptr based on how many lines were produced, release them
* labeled, and free the capture. Only valid when the response is known to
* be complete. Call label_capture_close_window() first. */
extern void label_capture_finish(struct Client *cptr, const char *ref);
/* The response for capture \a ref could not be honestly labeled as
* complete (e.g. it yields more output on a later event-loop tick, as
* LIST does, or the capture buffer overflowed) -- release whatever was
* captured as plain, unlabeled output instead of misrepresenting it with
* a closed batch, and free the capture. Call label_capture_close_window()
* first. */
extern void label_capture_abort(struct Client *cptr, const char *ref);
/* cptr is about to be freed: drop every capture still outstanding for it
* (no attempt to send anything -- cptr's socket is already gone). Call
* from exit_one_client() while cptr is still valid memory, before
* free_client() runs. */
extern void label_capture_client_gone(struct Client *cptr);

/* send_buffer() hook: if \a to -- the *intended recipient*, before
* cli_from() resolution, so a remote user is distinguishable from the
* link it sits behind -- is the owner of the active capture, take the
* line into that capture and return 1; otherwise return 0 and let it go
* to the wire. \a tctx is the effective tag context for the line (cache
* ctx or explicit ctx). */
extern int label_capture_intercept(struct Client *to, struct Client *from,
struct MsgBuf *buf, int prio,
const struct MsgTagCtx *tctx);
/* The capture currently active for \a owner (the requesting client
* itself, local or remote), or NULL if the active window belongs to
* someone else or is closed. For callers that need to hand a capture off (see
* sendcmdto_one_hunted() in send.c). */
extern struct LabelCapture *label_capture_active_for(struct Client *owner);

#endif /* INCLUDED_label_h */
23 changes: 23 additions & 0 deletions include/msg.h
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,19 @@ struct Client;
#define TOK_TAGMSG "TM"
#define CMD_TAGMSG MSG_TAGMSG, TOK_TAGMSG

/* Note: not named MSG_BATCH -- glibc's <bits/socket.h> already defines
* MSG_BATCH as a sendmmsg(2) flag (0x40000); reusing that identifier here
* would silently clobber it in every translation unit that pulls in both
* headers (which is most of this codebase, via client.h -> res.h ->
* sys/socket.h). */
#define MSG_BATCH_CMD "BATCH"
#define TOK_BATCH "BA"
#define CMD_BATCH MSG_BATCH_CMD, TOK_BATCH

#define MSG_ACK "ACK"
#define TOK_ACK "AK"
#define CMD_ACK MSG_ACK, TOK_ACK

#define MSG_WALLCHOPS "WALLCHOPS" /* WC */
#define TOK_WALLCHOPS "WC"
#define CMD_WALLCHOPS MSG_WALLCHOPS, TOK_WALLCHOPS
Expand Down Expand Up @@ -405,6 +418,16 @@ struct Client;
#define MFLG_EXTRA 0x08 /** Handler requests that
* mptr->extra be passed in
* parv[1]. */
#define MFLG_NO_S2S_TIME 0x10 /** Never invent @time= on the
* S2S wire for this command:
* link/state and net-admin
* protocol, and server<->services
* RPC parsed positionally by
* software that does not strip
* tags (see msg_tag_s2s_needs_
* time()). Everything else that
* hits S2S is treated as
* (eventually) client-visible. */

/*
* Structures
Expand Down
1 change: 1 addition & 0 deletions include/parse.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ extern int parse_server(struct Client *cptr, char *buffer, char *bufend);
/** Tags parsed from the current input line (valid only during handler). */
extern struct MsgTag *parse_tags(void);
extern void initmsgtree(void);
extern struct Message *msg_find_by_tok(const char *tok);

extern int register_mapping(struct s_map *map);
extern int unregister_mapping(struct s_map *map);
Expand Down
26 changes: 25 additions & 1 deletion include/send.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,21 @@ struct Channel;
struct Client;
struct DBuf;
struct MsgBuf;
struct MsgTagCtx;
struct MsgTag;
struct TagSendCache;

/** Immutable per-message tag context. Small enough to stack on any send
* path (single-recipient sends carry only this, not the full cache).
* Defined here (not in send.c) because label.c snapshots it by value for
* every captured line. */
struct MsgTagCtx {
struct MsgTag *tags; /**< Tags parsed from the current input line. */
time_t local_time; /**< Delivery time for server-time / @time=. */
const char *tok; /**< Command token for S2S policy (or NULL). */
int client_relay; /**< Has relayable client-only (+) tags. */
int s2s_needs_time; /**< Invent/forward @time= on S2S for this command. */
};

/*
* Prototypes
*/
Expand All @@ -33,6 +45,10 @@ extern void send_buffer(struct Client* to, struct Client* from, struct MsgBuf* b
int prio, const struct MsgTagCtx *ctx,
struct TagSendCache *cache);

/* Populate a per-message tag context from the current input line's tags.
* \a tok is the command token (for S2S @time= / TAGMSG policy), or NULL. */
extern void msgtagctx_init(struct MsgTagCtx *ctx, const char *tok);

/** Queue raw octets on a sendq (no IRC CRLF, no WebSocket framing). */
extern void send_raw_buffer(struct Client *to, struct MsgBuf *mb, int prio);

Expand All @@ -55,6 +71,14 @@ extern void sendcmdto_prio_one(struct Client *from, const char *cmd,
const char *tok, struct Client *to,
const char *pattern, ...);

/* Like sendcmdto_one(), but for hunt_server_cmd()-style forwarding: propagates
* an active labeled-response capture for \a from as @label= on the forwarded
* line (when FEAT_NETWORK_FEATURES is on), handing the local capture off
* instead of leaving it to close as a premature, empty ACK. See send.c and label.c. */
extern void sendcmdto_one_hunted(struct Client *from, const char *cmd,
const char *tok, struct Client *to,
const char *pattern, ...);

/* Send command to servers by flags except one */
extern void sendcmdto_flag_serv_butone(struct Client *from, const char *cmd,
const char *tok, struct Client *one,
Expand Down
3 changes: 3 additions & 0 deletions ircd/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,15 @@ ircd_SOURCES = \
ircd_snprintf.c \
ircd_string.c \
jupe.c \
label.c \
list.c \
listener.c \
m_account.c \
m_ack.c \
m_admin.c \
m_asll.c \
m_away.c \
m_batch.c \
m_burst.c \
m_cap.c \
m_clearmode.c \
Expand Down
Loading
Loading