diff --git a/.github/workflows/unrealircd.yml b/.github/workflows/unrealircd.yml new file mode 100644 index 0000000..0edbd0c --- /dev/null +++ b/.github/workflows/unrealircd.yml @@ -0,0 +1,54 @@ +name: irctest with UnrealIRCd + +on: + push: + pull_request: + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + + - uses: actions/checkout@v2 + + - name: Set up Python 3.7 + uses: actions/setup-python@v2 + with: + python-version: 3.7 + + - name: Cache dependencies + uses: actions/cache@v2 + with: + path: | + ~/.cache + $GITHUB_WORKSPACE/unrealircd + key: ${{ runner.os }}-unrealircd + + - name: Install dependencies + run: | + sudo apt-get install atheme-services + python -m pip install --upgrade pip + pip install pytest -r requirements.txt + + - name: Checkout UnrealIRCd + uses: actions/checkout@v2 + with: + repository: unrealircd/unrealircd + ref: unreal52 + path: unrealircd + + - name: Build UnrealIRCd + run: | + cd $GITHUB_WORKSPACE/unrealircd/ + cp $GITHUB_WORKSPACE/unreal/* . + CFLAGS=-O0 ./Config -quick + make -j 4 + make install + + - name: Test with pytest + run: | + PATH=~/.local/unrealircd/bin:$PATH make unrealircd + + diff --git a/Makefile b/Makefile index 98d011b..85b29ce 100644 --- a/Makefile +++ b/Makefile @@ -60,9 +60,27 @@ SOPEL_SELECTORS := \ not testPlainNotAvailable \ $(EXTRA_SELECTORS) -.PHONY: all flakes ergo charybdis +# testNoticeNonexistentChannel fails: https://bugs.unrealircd.org/view.php?id=5949 +# test_regressions::testTagCap fails: https://bugs.unrealircd.org/view.php?id=5948 +# test_messages::testLineTooLong fails: https://bugs.unrealircd.org/view.php?id=5947 +# testCapRemovalByClient and testNakWhole fail pending https://github.com/unrealircd/unrealircd/pull/148 +# Tests marked with arbitrary_client_tags can't pass because Unreal whitelists which tags it relays +# Tests marked with react_tag can't pass because Unreal blocks +draft/react https://github.com/unrealircd/unrealircd/pull/149 +UNREALIRCD_SELECTORS := \ + not Ergo \ + and not deprecated \ + and not strict \ + and not testNoticeNonexistentChannel \ + and not (test_regressions and testTagCap) \ + and not (test_messages and testLineTooLong) \ + and not (test_cap and (testCapRemovalByClient or testNakWhole)) \ + and not arbitrary_client_tags \ + and not react_tag \ + $(EXTRA_SELECTORS) -all: flakes ergo inspircd limnoria sopel solanum +.PHONY: all flakes charybdis ergo inspircd mammon limnoria sopel solanum unrealircd + +all: flakes charybdis ergo inspircd mammon limnoria sopel solanum unrealircd flakes: pyflakes3 irctest @@ -87,3 +105,6 @@ solanum: sopel: $(PYTEST) $(PYTEST_ARGS) --controller=irctest.controllers.sopel -k '$(SOPEL_SELECTORS)' + +unrealircd: + $(PYTEST) $(PYTEST_ARGS) --controller=irctest.controllers.unrealircd -k '$(UNREALIRCD_SELECTORS)' diff --git a/irctest/basecontrollers.py b/irctest/basecontrollers.py index adc5371..fa59bbd 100644 --- a/irctest/basecontrollers.py +++ b/irctest/basecontrollers.py @@ -183,6 +183,8 @@ class BaseServerController(_BaseController): port: int hostname: str services_controller: BaseServicesController + extban_mute_char: Optional[str] = None + """Character used for the 'mute' extban""" def run( self, diff --git a/irctest/cases.py b/irctest/cases.py index ea3f06d..9e3fddc 100644 --- a/irctest/cases.py +++ b/irctest/cases.py @@ -517,9 +517,15 @@ class BaseServerTestCase( def getRegistrationMessage(self, client: TClientName) -> Message: """Filter notices, do not send pings.""" - return self.getMessage( - client, synchronize=False, filter_pred=lambda m: m.command != "NOTICE" - ) + while True: + msg = self.getMessage( + client, synchronize=False, filter_pred=lambda m: m.command != "NOTICE" + ) + if msg.command == "PING": + # Hi Unreal + self.sendLine(client, "PONG :" + msg.params[0]) + else: + return msg def sendLine(self, client: TClientName, line: Union[str, bytes]) -> None: return self.clients[client].sendLine(line) @@ -565,6 +571,9 @@ class BaseServerTestCase( result.append(m) if m.command == "001": return result + elif m.command == "PING": + # Hi, Unreal + self.sendLine(client, "PONG :" + m.params[0]) def requestCapabilities( self, diff --git a/irctest/controllers/charybdis.py b/irctest/controllers/charybdis.py index 3e84c5b..5094e94 100644 --- a/irctest/controllers/charybdis.py +++ b/irctest/controllers/charybdis.py @@ -73,6 +73,7 @@ class CharybdisController(BaseServerController, DirectoryBasedController): binary_name = "charybdis" supported_sasl_mechanisms = {"PLAIN"} supports_sts = False + extban_mute_char = None def create_config(self) -> None: super().create_config() diff --git a/irctest/controllers/inspircd.py b/irctest/controllers/inspircd.py index d28f13f..2186483 100644 --- a/irctest/controllers/inspircd.py +++ b/irctest/controllers/inspircd.py @@ -68,6 +68,7 @@ class InspircdController(BaseServerController, DirectoryBasedController): software_name = "InspIRCd" supported_sasl_mechanisms = {"PLAIN"} supports_sts = False + extban_mute_char = "m" def create_config(self) -> None: super().create_config() diff --git a/irctest/controllers/unrealircd.py b/irctest/controllers/unrealircd.py new file mode 100644 index 0000000..da859cd --- /dev/null +++ b/irctest/controllers/unrealircd.py @@ -0,0 +1,182 @@ +import os +import subprocess +from typing import Optional, Set, Type + +from irctest.basecontrollers import ( + BaseServerController, + DirectoryBasedController, + NotImplementedByController, +) +from irctest.irc_utils.junkdrawer import find_hostname_and_port + +TEMPLATE_CONFIG = """ +include "modules.default.conf"; + +me {{ + name "My.Little.Server"; + info "ExampleNET Server"; + sid "001"; +}} +admin {{ + "Bob Smith"; + "bob"; + "email@example.org"; +}} +class clients {{ + pingfreq 90; + maxclients 1000; + sendq 200k; + recvq 8000; +}} +class servers {{ + pingfreq 60; + connfreq 15; /* try to connect every 15 seconds */ + maxclients 10; /* max servers */ + sendq 20M; +}} +allow {{ + mask *; + class clients; + maxperip 50; + {password_field} +}} +listen {{ + ip {hostname}; + port {port}; +}} +listen {{ + ip {tls_hostname}; + port {tls_port}; + options {{ tls; }} + tls-options {{ + certificate "{pem_path}"; + key "{key_path}"; + }}; +}} + +/* Special SSL/TLS servers-only port for linking */ +listen {{ + ip {services_hostname}; + port {services_port}; + options {{ serversonly; }} +}} + +link services.example.org {{ + incoming {{ + mask localhost; + }} + password "password"; + class servers; +}} +ulines {{ + services.example.org; +}} + +set {{ + kline-address "example@example.org"; + network-name "ExampleNET"; + default-server "irc.example.org"; + help-channel "#Help"; + cloak-keys {{ "aaaA1"; "bbbB2"; "cccC3"; }} + options {{ + identd-check; // Disable it, so it doesn't prefix idents with a tilde + }} + anti-flood {{ + // Prevent throttling, especially test_buffering.py which + // triggers anti-flood with its very long lines + unknown-users {{ + lag-penalty 1; + lag-penalty-bytes 10000; + }} + }} +}} + +tld {{ + mask *; + motd "{empty_file}"; + botmotd "{empty_file}"; + rules "{empty_file}"; +}} +""" + + +class UnrealircdController(BaseServerController, DirectoryBasedController): + software_name = "InspIRCd" + supported_sasl_mechanisms = {"PLAIN"} + supports_sts = False + + extban_mute_char = "q" + + def create_config(self) -> None: + super().create_config() + with self.open_file("server.conf"): + pass + + def run( + self, + hostname: str, + port: int, + *, + password: Optional[str], + ssl: bool, + run_services: bool, + valid_metadata_keys: Optional[Set[str]] = None, + invalid_metadata_keys: Optional[Set[str]] = None, + restricted_metadata_keys: Optional[Set[str]] = None, + ) -> None: + if valid_metadata_keys or invalid_metadata_keys: + raise NotImplementedByController( + "Defining valid and invalid METADATA keys." + ) + assert self.proc is None + self.port = port + self.hostname = hostname + self.create_config() + (unused_hostname, unused_port) = find_hostname_and_port() + (services_hostname, services_port) = find_hostname_and_port() + + password_field = 'password "{}";'.format(password) if password else "" + + self.gen_ssl() + if ssl: + (tls_hostname, tls_port) = (hostname, port) + (hostname, port) = (unused_hostname, unused_port) + else: + # Unreal refuses to start without TLS enabled + (tls_hostname, tls_port) = (unused_hostname, unused_port) + + with self.open_file("empty.txt") as fd: + fd.write("\n") + + assert self.directory + with self.open_file("unrealircd.conf") as fd: + fd.write( + TEMPLATE_CONFIG.format( + hostname=hostname, + port=port, + services_hostname=services_hostname, + services_port=services_port, + tls_hostname=tls_hostname, + tls_port=tls_port, + password_field=password_field, + key_path=self.key_path, + pem_path=self.pem_path, + empty_file=os.path.join(self.directory, "empty.txt"), + ) + ) + self.proc = subprocess.Popen( + [ + "unrealircd", + "-F", # BOOT_NOFORK + "-f", + os.path.join(self.directory, "unrealircd.conf"), + ], + stdout=subprocess.DEVNULL, + ) + + if run_services: + raise NotImplementedByController("Registration services") + + +def get_irctest_controller_class() -> Type[UnrealircdController]: + return UnrealircdController diff --git a/irctest/patma.py b/irctest/patma.py index 37225ba..fd9db2b 100644 --- a/irctest/patma.py +++ b/irctest/patma.py @@ -35,6 +35,14 @@ class StrRe(Operator): return f"StrRe(r'{self.regexp}')" +@dataclasses.dataclass(frozen=True) +class NotStrRe(Operator): + regexp: str + + def __repr__(self) -> str: + return f"NotStrRe(r'{self.regexp}')" + + @dataclasses.dataclass(frozen=True) class RemainingKeys(Operator): """Used in a dict pattern to match all remaining keys. @@ -54,6 +62,15 @@ ANYDICT = {RemainingKeys(ANYSTR): AnyOptStr()} `match_dict(got_tags, {"label": "foo", **ANYDICT})`""" +class _AnyListRemainder: + def __repr__(self) -> str: + return "*ANYLIST" + + +ANYLIST = [_AnyListRemainder()] +"""Matches any list remainder""" + + def match_string(got: Optional[str], expected: Union[str, Operator, None]) -> bool: if isinstance(expected, AnyOptStr): return True @@ -62,6 +79,9 @@ def match_string(got: Optional[str], expected: Union[str, Operator, None]) -> bo elif isinstance(expected, StrRe): if got is None or not re.match(expected.regexp, got): return False + elif isinstance(expected, NotStrRe): + if got is None or re.match(expected.regexp, got): + return False elif isinstance(expected, Operator): raise NotImplementedError(f"Unsupported operator: {expected}") elif got != expected: @@ -78,6 +98,9 @@ def match_list( The ANYSTR operator can be used on the 'expected' side as a wildcard, matching any *single* value; and StrRe("") can be used to match regular expressions""" + if expected[-1] is ANYLIST[0]: + expected = expected[0:-1] + got = got[0 : len(expected)] # Ignore remaining if len(got) != len(expected): return False return all( diff --git a/irctest/self_tests/test_cases.py b/irctest/self_tests/test_cases.py index 9bd479e..e31ea51 100644 --- a/irctest/self_tests/test_cases.py +++ b/irctest/self_tests/test_cases.py @@ -4,7 +4,7 @@ import pytest from irctest import cases from irctest.irc_utils.message_parser import parse_message -from irctest.patma import ANYDICT, ANYSTR, StrRe +from irctest.patma import ANYDICT, ANYSTR, AnyOptStr, NotStrRe, RemainingKeys, StrRe # fmt: off MESSAGE_SPECS: List[Tuple[Dict, List[str], List[str]]] = [ @@ -131,6 +131,27 @@ MESSAGE_SPECS: List[Tuple[Dict, List[str], List[str]]] = [ ":foo!baz@qux PRIVMSG #chan hello", ] ), + ( + # the specification: + dict( + tags={"tag1": "bar", RemainingKeys(NotStrRe("tag2")): AnyOptStr()}, + command="PRIVMSG", + params=["#chan", "hello"], + ), + # matches: + [ + "@tag1=bar PRIVMSG #chan :hello", + "@tag1=bar :foo!baz@qux PRIVMSG #chan :hello", + "@tag1=bar;tag3= PRIVMSG #chan :hello", + ], + # and does not match: + [ + "PRIVMG #chan :hello", + "@tag1=value1 PRIVMSG #chan :hello", + "@tag1=bar;tag2= PRIVMSG #chan :hello", + "@tag1=bar;tag2=baz PRIVMSG #chan :hello", + ] + ), ] # fmt: on diff --git a/irctest/server_tests/test_bot_mode.py b/irctest/server_tests/test_bot_mode.py index 23cdd5c..24cdce7 100644 --- a/irctest/server_tests/test_bot_mode.py +++ b/irctest/server_tests/test_bot_mode.py @@ -33,11 +33,16 @@ class BotModeTestCase(cases.BaseServerTestCase): self.sendLine("bot", f"MODE botnick +{self._mode_char}") # Check echoed mode - self.assertMessageMatch( - self.getMessage("bot"), - command="MODE", - params=["botnick", StrRe(r"\+?" + self._mode_char)], - ) + while True: + msg = self.getMessage("bot") + if msg.command != "NOTICE": + # Unreal sends the BOTMOTD here + self.assertMessageMatch( + msg, + command="MODE", + params=["botnick", StrRe(r"\+?" + self._mode_char)], + ) + break def testBotMode(self): self._initBot() diff --git a/irctest/server_tests/test_cap.py b/irctest/server_tests/test_cap.py index 7a7b74e..e177d91 100644 --- a/irctest/server_tests/test_cap.py +++ b/irctest/server_tests/test_cap.py @@ -125,6 +125,7 @@ class CapTestCase(cases.BaseServerTestCase, cases.OptionalityHelper): cap1 = "echo-message" cap2 = "server-time" self.addClient(1) + self.connectClient("sender") self.sendLine(1, "CAP LS 302") m = self.getRegistrationMessage(1) if not ({cap1, cap2} <= set(m.params[2].split())): @@ -146,7 +147,10 @@ class CapTestCase(cases.BaseServerTestCase, cases.OptionalityHelper): enabled_caps = set(cap_list.params[2].split()) enabled_caps.discard("cap-notify") # implicitly added by some impls self.assertEqual(enabled_caps, {cap1, cap2}) - self.assertIn("time", cap_list.tags, cap_list) + + self.sendLine(2, "PRIVMSG bar :hi") + m = self.getMessage(1) + self.assertIn("time", m.tags, m) # remove the server-time cap self.sendLine(1, f"CAP REQ :-{cap2}") diff --git a/irctest/server_tests/test_channel_operations.py b/irctest/server_tests/test_channel_operations.py index 8415441..e0b3e54 100644 --- a/irctest/server_tests/test_channel_operations.py +++ b/irctest/server_tests/test_channel_operations.py @@ -26,7 +26,7 @@ from irctest.numerics import ( RPL_TOPIC, RPL_TOPICTIME, ) -from irctest.patma import ANYSTR, StrRe +from irctest.patma import ANYLIST, ANYSTR, StrRe MODERN_CAPS = [ "server-time", @@ -1296,16 +1296,13 @@ class OpModerated(cases.BaseServerTestCase): class MuteExtban(cases.BaseServerTestCase): """https://defs.ircdocs.horse/defs/isupport.html#extban - These tests assume that if the server advertizes the 'm' extban, - then it supports mute. + It magically guesses what char the IRCd uses for mutes.""" - This is not true of Charybdis, which introduced a conflicting 'm' - exban for matching hostmasks in 2015 - (e2a9fa9cab3720215d8081e940109416e8214a29). - - But Unreal was already using 'm' for muting since 2008 - (f474e7e6dc2d36f96150ebe33b23b4ea76814415) and it is the most popular - definition so we're going with that one.""" + def char(self): + if self.controller.extban_mute_char is None: + raise runner.ExtbanNotSupported("", "mute") + else: + return self.controller.extban_mute_char @cases.mark_specifications("Ergo") def testISupport(self): @@ -1313,7 +1310,7 @@ class MuteExtban(cases.BaseServerTestCase): isupport = self.server_support token = isupport["EXTBAN"] prefix, comma, types = token.partition(",") - self.assertIn("m", types, "Missing 'm' in ISUPPORT EXTBAN") + self.assertIn(self.char, types, f"Missing '{self.char()}' in ISUPPORT EXTBAN") self.assertEqual(prefix, "") self.assertEqual(comma, ",") @@ -1325,15 +1322,15 @@ class MuteExtban(cases.BaseServerTestCase): isupport = self.server_support token = isupport.get("EXTBAN", "") prefix, comma, types = token.partition(",") - if "m" not in types: - raise runner.ExtbanNotSupported("m", "mute") + if self.char() not in types: + raise runner.ExtbanNotSupported(self.char(), "mute") clients = ("chanop", "bar") # Mute "bar" self.joinChannel("chanop", "#chan") self.getMessages("chanop") - self.sendLine("chanop", "MODE #chan +b m:bar!*@*") + self.sendLine("chanop", f"MODE #chan +b {prefix}{self.char()}:bar!*@*") replies = {msg.command for msg in self.getMessages("chanop")} self.assertIn("MODE", replies) self.assertNotIn(ERR_CHANOPRIVSNEEDED, replies) @@ -1344,6 +1341,21 @@ class MuteExtban(cases.BaseServerTestCase): for client in clients: self.getMessages(client) + # "bar" sees the MODE too + self.sendLine("bar", "MODE #chan +b") + self.assertMessageMatch( + self.getMessage("bar"), + command="367", + params=[ + "bar", + "#chan", + f"{prefix}{self.char()}:bar!*@*", + "chanop", + *ANYLIST, + ], + ) + self.getMessages("bar") + # "bar" talks: rejected self.sendLine("bar", "PRIVMSG #chan :hi from bar") replies = self.getMessages("bar") @@ -1354,7 +1366,7 @@ class MuteExtban(cases.BaseServerTestCase): # remove mute on "bar" with -b self.getMessages("chanop") - self.sendLine("chanop", "MODE #chan -b m:bar!*@*") + self.sendLine("chanop", f"MODE #chan -b {prefix}{self.char()}:bar!*@*") replies = {msg.command for msg in self.getMessages("chanop")} self.assertIn("MODE", replies) self.assertNotIn(ERR_CHANOPRIVSNEEDED, replies) @@ -1378,15 +1390,15 @@ class MuteExtban(cases.BaseServerTestCase): isupport = self.server_support token = isupport.get("EXTBAN", "") prefix, comma, types = token.partition(",") - if "m" not in types: - raise runner.ExtbanNotSupported("m", "mute") + if self.char() not in types: + raise runner.ExtbanNotSupported(self.char(), "mute") clients = ("chanop", "qux") # Mute "qux" self.joinChannel("chanop", "#chan") self.getMessages("chanop") - self.sendLine("chanop", "MODE #chan +b m:qux!*@*") + self.sendLine("chanop", f"MODE #chan +b {prefix}{self.char()}:qux!*@*") replies = {msg.command for msg in self.getMessages("chanop")} self.assertIn("MODE", replies) self.assertNotIn(ERR_CHANOPRIVSNEEDED, replies) @@ -1437,17 +1449,17 @@ class MuteExtban(cases.BaseServerTestCase): isupport = self.server_support token = isupport.get("EXTBAN", "") prefix, comma, types = token.partition(",") - if "m" not in types: - raise runner.ExtbanNotSupported("m", "mute") + if self.char() not in types: + raise runner.ExtbanNotSupported(self.char(), "mute") if "e" not in self.server_support["CHANMODES"]: - raise runner.ChannelModeNotSupported("m", "mute") + raise runner.ChannelModeNotSupported(self.char(), "mute") clients = ("chanop", "qux") # Mute "qux" self.joinChannel("chanop", "#chan") self.getMessages("chanop") - self.sendLine("chanop", "MODE #chan +b m:qux!*@*") + self.sendLine("chanop", f"MODE #chan +b {prefix}{self.char()}:qux!*@*") replies = {msg.command for msg in self.getMessages("chanop")} self.assertIn("MODE", replies) self.assertNotIn(ERR_CHANOPRIVSNEEDED, replies) @@ -1472,11 +1484,13 @@ class MuteExtban(cases.BaseServerTestCase): self.getMessages(client) # +e grants an exemption to +b - self.sendLine("chanop", "MODE #chan +e m:*!~evan@*") + self.sendLine("chanop", f"MODE #chan +e {prefix}{self.char()}:*!~evan@*") replies = {msg.command for msg in self.getMessages("chanop")} self.assertIn("MODE", replies) self.assertNotIn(ERR_CHANOPRIVSNEEDED, replies) + self.getMessages("qux") + # so "qux" can now talk self.sendLine("qux", "PRIVMSG #chan :thanks for mute-excepting me") replies = self.getMessages("qux") @@ -1500,9 +1514,14 @@ class MuteExtban(cases.BaseServerTestCase): clients = ("chanop", "bar") self.connectClient("chanop", name="chanop") + + isupport = self.server_support + token = isupport.get("EXTBAN", "") + prefix, comma, types = token.partition(",") + self.joinChannel("chanop", "#chan") self.getMessages("chanop") - self.sendLine("chanop", "MODE #chan +b m:BAR!*@*") + self.sendLine("chanop", f"MODE #chan +b {prefix}{self.char()}:BAR!*@*") replies = {msg.command for msg in self.getMessages("chanop")} self.assertIn("MODE", replies) self.assertNotIn(ERR_CHANOPRIVSNEEDED, replies) @@ -1521,7 +1540,7 @@ class MuteExtban(cases.BaseServerTestCase): self.assertEqual(self.getMessages("chanop"), []) # remove mute with -b - self.sendLine("chanop", "MODE #chan -b m:bar!*@*") + self.sendLine("chanop", f"MODE #chan -b {prefix}{self.char()}:bar!*@*") replies = {msg.command for msg in self.getMessages("chanop")} self.assertIn("MODE", replies) self.assertNotIn(ERR_CHANOPRIVSNEEDED, replies) diff --git a/irctest/server_tests/test_connection_registration.py b/irctest/server_tests/test_connection_registration.py index ce2acde..40b8b59 100644 --- a/irctest/server_tests/test_connection_registration.py +++ b/irctest/server_tests/test_connection_registration.py @@ -119,15 +119,38 @@ class ConnectionRegistrationTestCase(cases.BaseServerTestCase): self.sendLine(2, "NICK foo") self.sendLine(1, "USER username * * :Realname") self.sendLine(2, "USER username * * :Realname") - m1 = self.getRegistrationMessage(1) - m2 = self.getRegistrationMessage(2) + + try: + m1 = self.getRegistrationMessage(1) + except (ConnectionClosed, ConnectionResetError): + # Unreal closes the connection, see + # https://bugs.unrealircd.org/view.php?id=5950 + command1 = None + else: + command1 = m1.command + + try: + m2 = self.getRegistrationMessage(2) + except (ConnectionClosed, ConnectionResetError): + # ditto + command2 = None + else: + command2 = m2.command + self.assertNotEqual( - (m1.command, m2.command), + (command1, command2), ("001", "001"), "Two concurrently registering requesting the same nickname " "both got 001.", ) + self.assertIn( + "001", + (command1, command2), + "Two concurrently registering requesting the same nickname " + "neither got 001.", + ) + @cases.mark_specifications("IRCv3") def testIrc301CapLs(self): """ diff --git a/irctest/server_tests/test_echo_message.py b/irctest/server_tests/test_echo_message.py index d43aafa..4226697 100644 --- a/irctest/server_tests/test_echo_message.py +++ b/irctest/server_tests/test_echo_message.py @@ -2,6 +2,8 @@ """ +import pytest + from irctest import cases from irctest.basecontrollers import NotImplementedByController from irctest.irc_utils.junkdrawer import random_name @@ -97,6 +99,7 @@ def _testEchoMessage(command, solo, server_time): class EchoMessageTestCase(cases.BaseServerTestCase): + @pytest.mark.arbitrary_client_tags @cases.mark_capabilities( "batch", "labeled-response", "echo-message", "message-tags" ) diff --git a/irctest/server_tests/test_labeled_responses.py b/irctest/server_tests/test_labeled_responses.py index ad4274c..0cc1bf5 100644 --- a/irctest/server_tests/test_labeled_responses.py +++ b/irctest/server_tests/test_labeled_responses.py @@ -7,8 +7,10 @@ so there may be many false positives. import re +import pytest + from irctest import cases -from irctest.patma import ANYDICT, StrRe +from irctest.patma import ANYDICT, AnyOptStr, NotStrRe, RemainingKeys, StrRe class LabeledResponsesTestCase(cases.BaseServerTestCase, cases.OptionalityHelper): @@ -89,6 +91,7 @@ class LabeledResponsesTestCase(cases.BaseServerTestCase, cases.OptionalityHelper self.assertMessageMatch(m, command="PRIVMSG", tags={"label": "12345"}) + @pytest.mark.react_tag @cases.mark_capabilities("echo-message", "batch", "labeled-response") def testLabeledPrivmsgResponsesToChannel(self): self.connectClient( @@ -190,6 +193,7 @@ class LabeledResponsesTestCase(cases.BaseServerTestCase, cases.OptionalityHelper self.assertMessageMatch(m, command="NOTICE", tags={"label": "12345"}) + @pytest.mark.react_tag @cases.mark_capabilities("echo-message", "batch", "labeled-response") def testLabeledNoticeResponsesToChannel(self): self.connectClient( @@ -265,6 +269,7 @@ class LabeledResponsesTestCase(cases.BaseServerTestCase, cases.OptionalityHelper ).format(number_of_labels), ) + @pytest.mark.react_tag @cases.mark_capabilities( "echo-message", "batch", "labeled-response", "message-tags" ) @@ -282,7 +287,14 @@ class LabeledResponsesTestCase(cases.BaseServerTestCase, cases.OptionalityHelper ) self.getMessages(2) - self.sendLine(1, "@label=12345;+draft/reply=123;+draft/react=l😃l TAGMSG bar") + # Need to get a valid msgid because Unreal validates them + self.sendLine(1, "PRIVMSG bar :hi") + msgid = self.getMessage(1).tags["msgid"] + assert msgid == self.getMessage(2).tags["msgid"] + + self.sendLine( + 1, f"@label=12345;+draft/reply={msgid};+draft/react=l😃l TAGMSG bar" + ) m = self.getMessage(1) m2 = self.getMessage(2) @@ -290,7 +302,11 @@ class LabeledResponsesTestCase(cases.BaseServerTestCase, cases.OptionalityHelper self.assertMessageMatch( m2, command="TAGMSG", - tags={"+draft/reply": "123", "+draft/react": "l😃l", **ANYDICT}, + tags={ + "+draft/reply": msgid, + "+draft/react": "l😃l", + RemainingKeys(NotStrRe("label")): AnyOptStr(), + }, ) self.assertNotIn( "label", @@ -308,12 +324,13 @@ class LabeledResponsesTestCase(cases.BaseServerTestCase, cases.OptionalityHelper command="TAGMSG", tags={ "label": "12345", - "+draft/reply": "123", + "+draft/reply": msgid, "+draft/react": "l😃l", **ANYDICT, }, ) + @pytest.mark.react_tag @cases.mark_capabilities( "echo-message", "batch", "labeled-response", "message-tags" ) @@ -338,7 +355,14 @@ class LabeledResponsesTestCase(cases.BaseServerTestCase, cases.OptionalityHelper self.getMessages(2) self.getMessages(1) - self.sendLine(1, "@label=12345;+draft/reply=123;+draft/react=l😃l TAGMSG #test") + # Need to get a valid msgid because Unreal validates them + self.sendLine(1, "PRIVMSG #test :hi") + msgid = self.getMessage(1).tags["msgid"] + assert msgid == self.getMessage(2).tags["msgid"] + + self.sendLine( + 1, f"@label=12345;+draft/reply={msgid};+draft/react=l😃l TAGMSG #test" + ) ms = self.getMessage(1) mt = self.getMessage(2) @@ -346,6 +370,11 @@ class LabeledResponsesTestCase(cases.BaseServerTestCase, cases.OptionalityHelper self.assertMessageMatch( mt, command="TAGMSG", + tags={ + "+draft/reply": msgid, + "+draft/react": "l😃l", + RemainingKeys(NotStrRe("label")): AnyOptStr(), + }, fail_msg="No TAGMSG received by the target after sending one out", ) self.assertNotIn( @@ -361,9 +390,12 @@ class LabeledResponsesTestCase(cases.BaseServerTestCase, cases.OptionalityHelper # ensure sender correctly receives msg self.assertMessageMatch( - ms, command="TAGMSG", tags={"label": "12345", **ANYDICT} + ms, + command="TAGMSG", + tags={"label": "12345", "+draft/reply": msgid, **ANYDICT}, ) + @pytest.mark.react_tag @cases.mark_capabilities( "echo-message", "batch", "labeled-response", "message-tags" ) diff --git a/irctest/server_tests/test_message_tags.py b/irctest/server_tests/test_message_tags.py index e4dd53c..9d7ba14 100644 --- a/irctest/server_tests/test_message_tags.py +++ b/irctest/server_tests/test_message_tags.py @@ -2,6 +2,8 @@ https://ircv3.net/specs/extensions/message-tags.html """ +import pytest + from irctest import cases from irctest.irc_utils.message_parser import parse_message from irctest.numerics import ERR_INPUTTOOLONG @@ -9,6 +11,7 @@ from irctest.patma import ANYDICT, ANYSTR, StrRe class MessageTagsTestCase(cases.BaseServerTestCase, cases.OptionalityHelper): + @pytest.mark.arbitrary_client_tags @cases.mark_capabilities("message-tags") def testBasic(self): def getAllMessages(): @@ -107,6 +110,7 @@ class MessageTagsTestCase(cases.BaseServerTestCase, cases.OptionalityHelper): self.assertNotIn("cat", msg.tags) self.assertEqual(alice_msg.tags["msgid"], bob_msg.tags["msgid"]) + @pytest.mark.arbitrary_client_tags @cases.mark_capabilities("message-tags") @cases.mark_specifications("ircdocs") def testLengthLimits(self): diff --git a/irctest/server_tests/test_messages.py b/irctest/server_tests/test_messages.py index c8989a7..d4721a4 100644 --- a/irctest/server_tests/test_messages.py +++ b/irctest/server_tests/test_messages.py @@ -74,9 +74,13 @@ class TagsTestCase(cases.BaseServerTestCase): @cases.mark_capabilities("message-tags") def testLineTooLong(self): self.connectClient("bar", capabilities=["message-tags"], skip_if_cap_nak=True) + self.connectClient( + "recver", capabilities=["message-tags"], skip_if_cap_nak=True + ) self.joinChannel(1, "#xyz") monsterMessage = "@+clientOnlyTagExample=" + "a" * 4096 + " PRIVMSG #xyz hi!" self.sendLine(1, monsterMessage) + self.assertEqual(self.getMessages(2), [], "overflowing message was relayed") replies = self.getMessages(1) self.assertIn(ERR_INPUTTOOLONG, set(reply.command for reply in replies)) diff --git a/irctest/server_tests/test_regressions.py b/irctest/server_tests/test_regressions.py index 6ccc3f8..8c752cb 100644 --- a/irctest/server_tests/test_regressions.py +++ b/irctest/server_tests/test_regressions.py @@ -109,8 +109,13 @@ class RegressionsTestCase(cases.BaseServerTestCase): self.sendLine(1, "NICK valid") replies = {"NOTICE"} - while replies <= {"NOTICE"}: - replies = set(msg.command for msg in self.getMessages(1, synchronize=False)) + while replies <= {"NOTICE", "PING"}: + msgs = self.getMessages(1, synchronize=False) + for msg in msgs: + if msg.command == "PING": + # Hi Unreal + self.sendLine(1, "PONG :" + msg.params[0]) + replies = set(msg.command for msg in msgs) self.assertNotIn(ERR_ERRONEUSNICKNAME, replies) self.assertIn(RPL_WELCOME, replies) diff --git a/pytest.ini b/pytest.ini index 9eb742d..21bec0b 100644 --- a/pytest.ini +++ b/pytest.ini @@ -12,6 +12,8 @@ markers = strict deprecated services + arbitrary_client_tags + react_tag # capabilities account-tag diff --git a/unreal/config.settings b/unreal/config.settings new file mode 100644 index 0000000..e4d15b1 --- /dev/null +++ b/unreal/config.settings @@ -0,0 +1,24 @@ +BASEPATH="$HOME/.local/unrealircd" +BINDIR="$HOME/.local/unrealircd/bin" +DATADIR="$HOME/.local/unrealircd/data" +CONFDIR="$HOME/.local/unrealircd/conf" +MODULESDIR="$HOME/.local/unrealircd/modules" +LOGDIR="$HOME/.local/unrealircd/logs" +CACHEDIR="$HOME/.local/unrealircd/cache" +DOCDIR="$HOME/.local/unrealircd/doc" +TMPDIR="$HOME/.local/unrealircd/tmp" +PRIVATELIBDIR="$HOME/.local/unrealircd/lib" +PREFIXAQ="1" +MAXCONNECTIONS_REQUEST="auto" +NICKNAMEHISTORYLENGTH="2000" +DEFPERM="0600" +SSLDIR="" +REMOTEINC="" +CURLDIR="" +SHOWLISTMODES="1" +NOOPEROVERRIDE="" +OPEROVERRIDEVERIFY="" +GENCERTIFICATE="1" +EXTRAPARA="" +ADVANCED="" + diff --git a/unreal/server.cert.pem b/unreal/server.cert.pem new file mode 100644 index 0000000..28a521d --- /dev/null +++ b/unreal/server.cert.pem @@ -0,0 +1,14 @@ +-----BEGIN CERTIFICATE----- +MIICGDCCAZ6gAwIBAgIUeHAOQnvT7N9kCmUuIklelkzz8SUwCgYIKoZIzj0EAwIw +QzELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE5ldyBZb3JrMRIwEAYDVQQKDAlJUkMg +Z2Vla3MxDTALBgNVBAsMBElSQ2QwHhcNMjEwNzAyMTk1MTM5WhcNMzEwNjMwMTk1 +MTM5WjBDMQswCQYDVQQGEwJVUzERMA8GA1UECAwITmV3IFlvcmsxEjAQBgNVBAoM +CUlSQyBnZWVrczENMAsGA1UECwwESVJDZDB2MBAGByqGSM49AgEGBSuBBAAiA2IA +BHA6iqLQgkS42xHg/dEPq9dKjlLi0HWvCM7nOCXAyFy1DjrmbFoSCQBCUbISsk/C +Txru3YIfXe6jSCS8UTb15m70mrmmiUr/umxiqjAOiso051hCrzxVmjTpEAqMSnrc +zKNTMFEwHQYDVR0OBBYEFFNHqsBNxDNhVxfAgdv6/y4Xd6/ZMB8GA1UdIwQYMBaA +FFNHqsBNxDNhVxfAgdv6/y4Xd6/ZMA8GA1UdEwEB/wQFMAMBAf8wCgYIKoZIzj0E +AwIDaAAwZQIwAo29xUEAzqOMgPAWtMifHFLuPQPuWcNGbaI5S4W81NO8uIcNv/kM +mFocuITr76p0AjEApzGjc5wM+KydwoVTP+fg1aGQA13Ba2nCzN3R5XwR/USCigjv +na1QtWAKjpvR/rsp +-----END CERTIFICATE----- diff --git a/unreal/server.key.pem b/unreal/server.key.pem new file mode 100644 index 0000000..2f1cc45 --- /dev/null +++ b/unreal/server.key.pem @@ -0,0 +1,9 @@ +-----BEGIN EC PARAMETERS----- +BgUrgQQAIg== +-----END EC PARAMETERS----- +-----BEGIN EC PRIVATE KEY----- +MIGkAgEBBDCWkDHktJiTqC7im+Ni37fbXxtMBqIKPwkAItpKMeuh28QrXWwNE1a5 +wSa38C1nd8igBwYFK4EEACKhZANiAARwOoqi0IJEuNsR4P3RD6vXSo5S4tB1rwjO +5zglwMhctQ465mxaEgkAQlGyErJPwk8a7t2CH13uo0gkvFE29eZu9Jq5polK/7ps +YqowDorKNOdYQq88VZo06RAKjEp63Mw= +-----END EC PRIVATE KEY----- diff --git a/unreal/server.req.pem b/unreal/server.req.pem new file mode 100644 index 0000000..49a4f8f --- /dev/null +++ b/unreal/server.req.pem @@ -0,0 +1,9 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIBOjCBwgIBADBDMQswCQYDVQQGEwJVUzERMA8GA1UECAwITmV3IFlvcmsxEjAQ +BgNVBAoMCUlSQyBnZWVrczENMAsGA1UECwwESVJDZDB2MBAGByqGSM49AgEGBSuB +BAAiA2IABHA6iqLQgkS42xHg/dEPq9dKjlLi0HWvCM7nOCXAyFy1DjrmbFoSCQBC +UbISsk/CTxru3YIfXe6jSCS8UTb15m70mrmmiUr/umxiqjAOiso051hCrzxVmjTp +EAqMSnrczKAAMAoGCCqGSM49BAMCA2cAMGQCMEL5ezlauGUaxh+pXt897ffmzqci +fqYm3FgVW5x6EdtCxtcwwAwnR84LKcd/YRKOygIwNmZiRVKeSeC7Ess1PxuzT1Mu +Cw3bBqkE5LmO1hu/+0lK+QoFPEeLDrygIh+SDdGH +-----END CERTIFICATE REQUEST-----