From 7929f5f9f4c9a21cb0b64bc81e846d52fb4005fc Mon Sep 17 00:00:00 2001 From: Jean Date: Sat, 11 Jul 2026 19:59:47 +0000 Subject: [PATCH] Fix InspIRCd UID and message-tag parsing; add irctest controller The UID introduction omitted the second user field (insp sends both a real and a displayed user), and the parser didn't strip IRCv3 message-tag prefixes, so tagged PRIVMSGs addressed to a service were dropped. With both fixed a service links to a real insp4 uplink and answers commands. Adds an irctest services controller so this is verified against an isolated, throwaway ircd. --- src/proto/inspircd.rs | 10 ++++++- testing/fedserv_services.py | 57 +++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 testing/fedserv_services.py diff --git a/src/proto/inspircd.rs b/src/proto/inspircd.rs index 069a61d..279e202 100644 --- a/src/proto/inspircd.rs +++ b/src/proto/inspircd.rs @@ -32,6 +32,12 @@ impl Protocol for InspIrcd { } fn parse(&mut self, line: &str) -> Vec { + // Strip an optional IRCv3 message-tag prefix (@k=v;… ) — insp tags PRIVMSGs + // with time/msgid, and it sits before the :source. + let line = match line.strip_prefix('@') { + Some(rest) => rest.splitn(2, ' ').nth(1).unwrap_or(""), + None => line, + }; let (source, rest) = match line.strip_prefix(':') { Some(s) => { let mut it = s.splitn(2, ' '); @@ -76,8 +82,10 @@ impl Protocol for InspIrcd { let dest = from.clone().unwrap_or_else(|| self.sid.clone()); vec![self.from_us(format!("PONG {} {}", dest, token))] } + // insp4 UID: uuid nickts nick realhost disphost realuser dispuser ip signonts +modes :gecos + // (both a real AND a displayed user field — see m_spanningtree/uid.cpp Builder). NetAction::IntroduceUser { uid, nick, ident, host, gecos } => vec![self.from_us(format!( - "UID {uid} {ts} {nick} {host} {host} {ident} 0.0.0.0 {ts} +ioS :{gecos}", + "UID {uid} {ts} {nick} {host} {host} {ident} {ident} 0.0.0.0 {ts} +i :{gecos}", uid = uid, ts = self.ts, nick = nick, host = host, ident = ident, gecos = gecos ))], NetAction::Privmsg { from, to, text } => { diff --git a/testing/fedserv_services.py b/testing/fedserv_services.py new file mode 100644 index 0000000..233cab9 --- /dev/null +++ b/testing/fedserv_services.py @@ -0,0 +1,57 @@ +"""irctest controller for fedserv. + +Copy (or symlink) this file into irctest's ``irctest/controllers/`` directory, +then run the services tests against an isolated InspIRCd + fedserv: + + FEDSERV_BIN=/path/to/federated-services/target/release/fedserv \\ + PATH=/path/to/inspircd/run/bin:$PATH \\ + python -m pytest \\ + --controller=irctest.controllers.inspircd \\ + --services-controller=irctest.controllers.fedserv_services \\ + irctest/server_tests/account_registration.py + +irctest spawns its own ircd + services on random ports, so this never touches a +real network. See https://github.com/progval/irctest +""" +import os +import shutil +from typing import Type + +from irctest.basecontrollers import BaseServicesController, DirectoryBasedController + +CONFIG = """\ +[uplink] +host = "{server_hostname}" +port = {server_port} +password = "password" + +[server] +name = "My.Little.Services" +sid = "42S" +description = "Federated Services" +protocol = 1206 +""" + + +class FedservController(BaseServicesController, DirectoryBasedController): + software_name = "fedserv" + saslserv = "NickServ" + + def run(self, protocol: str, server_hostname: str, server_port: int) -> None: + self.create_config() + assert protocol in ("inspircd", "inspircd3"), f"fedserv speaks inspircd, not {protocol}" + + binary = ( + os.environ.get("FEDSERV_BIN") + or shutil.which("fedserv") + or "target/release/fedserv" + ) + + with self.open_file("config.toml") as fd: + fd.write(CONFIG.format(server_hostname=server_hostname, server_port=server_port)) + + self.proc = self.execute([binary, "config.toml"], cwd=self.directory) + + +def get_irctest_controller_class() -> Type[FedservController]: + return FedservController