Skip to content
Closed
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
21 changes: 17 additions & 4 deletions ircd/m_cap.c
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,21 @@ static struct subcmd {
{ "REQ", cap_req }
};

/** Check whether a local connection should be told about NEW/DEL.
* cap-notify takes effect when it is negotiated (implicitly by CAP LS
* 302), not at registration: a client still registering must hear that a
* capability appeared, or it registers without ever learning of it.
* @param[in] acptr Local connection to test.
* @return Non-zero if \a acptr is a (possibly unregistered) user with
* cap-notify active.
*/
static int cap_notify_target(struct Client *acptr)
{
return MyConnect(acptr)
&& (IsUser(acptr) || IsUserPort(acptr) || IsWebsocketPort(acptr))
&& CapHas(cli_active(acptr), CAP_CAPNOTIFY);
}

/** Send CAP NEW to all clients with cap-notify capability
* @param[in] cap Capability enum value
*/
Expand Down Expand Up @@ -400,8 +415,7 @@ void cap_new(enum Capab cap)
if (!(acptr = LocalClientArray[i]))
continue;

/* Only send to registered users with cap-notify capability */
if (!IsUser(acptr) || !MyConnect(acptr) || !CapHas(cli_active(acptr), CAP_CAPNOTIFY))
if (!cap_notify_target(acptr))
continue;

/* Send CAP NEW message */
Expand Down Expand Up @@ -442,8 +456,7 @@ void cap_del(enum Capab cap)
if (!(acptr = LocalClientArray[i]))
continue;

/* Only send to registered users with cap-notify capability */
if (!IsUser(acptr) || !MyConnect(acptr) || !CapHas(cli_active(acptr), CAP_CAPNOTIFY))
if (!cap_notify_target(acptr))
continue;

/* Send CAP DEL message */
Expand Down
74 changes: 74 additions & 0 deletions tests/pr66_capsasl/test_cap_notify_registering.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
"""CAP NEW for a cap-notify client that has not finished registering.

cap-notify is enabled by negotiation (implicitly by ``CAP LS 302``), not by
registration, and the IRCv3 capability-negotiation spec lets ``CAP NEW`` be
"sent at any time", with ``*`` as the target while no nick is available.

A restarted leaf takes its clients back while it is still linking to the
network: they see no ``sasl`` in ``CAP LS``, and the SASL server becomes
reachable before they are registered. If the NEW is only sent to registered
users they register without ever learning that sasl exists.
"""

import asyncio

import pytest

from irc_client import IRCClient
from p10_server import P10Server


pytestmark = pytest.mark.single_server

SASL_SERVER = "services.test.net"
MECHANISMS = "PLAIN"


async def _collect_cap(client: IRCClient, timeout: float) -> list[tuple[str, str]]:
"""Return every (subcommand, argument) CAP message seen within timeout."""
seen = []
loop = asyncio.get_running_loop()
deadline = loop.time() + timeout
while True:
remaining = deadline - loop.time()
if remaining <= 0:
return seen
try:
msg = await client.wait_for("CAP", timeout=remaining)
except asyncio.TimeoutError:
return seen
seen.append((msg.params[1], msg.params[-1]))


async def test_cap_new_reaches_client_still_registering(ircd_hub):
client = IRCClient()
await client.connect(ircd_hub["host"], ircd_hub["port"])
srv = None
try:
await client.send("CAP LS 302")
msg = await client.wait_for("CAP", timeout=5.0)
assert "sasl" not in msg.params[-1], msg.params
# Registration stays suspended: no CAP END yet.
await client.send("NICK capreg1")
await client.send("USER testuser 0 * :Test User")

# The SASL server links and is configured while we are still
# negotiating.
srv = P10Server(name=SASL_SERVER, numeric=4, password="testpass")
await srv.connect(ircd_hub["host"], ircd_hub["server_port"])
await srv.handshake()
await srv.send_config("sasl.mechanisms", MECHANISMS)
await srv.send_config("sasl.server", SASL_SERVER)

caps = await _collect_cap(client, 3.0)
assert caps and caps[-1] == ("NEW", f"sasl={MECHANISMS}"), caps

# The announced capability is usable before registration completes.
await client.send("CAP REQ :sasl")
msg = await client.wait_for("CAP", timeout=5.0)
assert msg.params[1] == "ACK", msg.params

finally:
if srv is not None:
await srv.disconnect()
await client.disconnect()
Loading