diff --git a/Makefile b/Makefile index b118e9b..50e50c9 100644 --- a/Makefile +++ b/Makefile @@ -23,6 +23,7 @@ BAHAMUT_SELECTORS := \ # testQuitErrors is very flaky # AccountTagTestCase.testInvite fails because https://github.com/solanum-ircd/solanum/issues/166 # testKickDefaultComment fails because it uses the nick of the kickee rather than the kicker. +# testWhoisNumerics[oper] fails because charybdis uses RPL_WHOISSPECIAL instead of RPL_WHOISOPERATOR CHARYBDIS_SELECTORS := \ not Ergo \ and not deprecated \ @@ -30,6 +31,7 @@ CHARYBDIS_SELECTORS := \ and not testQuitErrors \ and not testKickDefaultComment \ and not (AccountTagTestCase and testInvite) \ + and not (testWhoisNumerics and oper) \ $(EXTRA_SELECTORS) ERGO_SELECTORS := \ diff --git a/irctest/cases.py b/irctest/cases.py index 569bc94..e3c24d4 100644 --- a/irctest/cases.py +++ b/irctest/cases.py @@ -750,29 +750,27 @@ class OptionalityHelper(Generic[TController]): @staticmethod def skipUnlessHasMechanism( mech: str, - ) -> Callable[[Callable[[_TSelf], _TReturn]], Callable[[_TSelf], _TReturn]]: + ) -> Callable[[Callable[..., _TReturn]], Callable[..., _TReturn]]: # Just a function returning a function that takes functions and # returns functions, nothing to see here. # If Python didn't have such an awful syntax for callables, it would be: # str -> ((TSelf -> TReturn) -> (TSelf -> TReturn)) - def decorator(f: Callable[[_TSelf], _TReturn]) -> Callable[[_TSelf], _TReturn]: + def decorator(f: Callable[..., _TReturn]) -> Callable[..., _TReturn]: @functools.wraps(f) - def newf(self: _TSelf) -> _TReturn: + def newf(self: _TSelf, *args: Any, **kwargs: Any) -> _TReturn: self.checkMechanismSupport(mech) - return f(self) + return f(self, *args, **kwargs) return newf return decorator @staticmethod - def skipUnlessHasSasl( - f: Callable[[_TSelf], _TReturn] - ) -> Callable[[_TSelf], _TReturn]: + def skipUnlessHasSasl(f: Callable[..., _TReturn]) -> Callable[..., _TReturn]: @functools.wraps(f) - def newf(self: _TSelf) -> _TReturn: + def newf(self: _TSelf, *args: Any, **kwargs: Any) -> _TReturn: self.checkSaslSupport() - return f(self) + return f(self, *args, **kwargs) return newf diff --git a/irctest/controllers/bahamut.py b/irctest/controllers/bahamut.py index baa877a..01cd4dd 100644 --- a/irctest/controllers/bahamut.py +++ b/irctest/controllers/bahamut.py @@ -69,6 +69,14 @@ connect {{ cpasswd password; class services; }}; + +oper {{ + name operuser; + host *@*; + passwd operpassword; + access *Aa; + class users; +}}; """ diff --git a/irctest/controllers/charybdis.py b/irctest/controllers/charybdis.py index 2569699..489d36b 100644 --- a/irctest/controllers/charybdis.py +++ b/irctest/controllers/charybdis.py @@ -52,6 +52,21 @@ connect "services.example.org" {{ service {{ name = "services.example.org"; }}; + +privset "omnioper" {{ + privs = oper:general, oper:privs, oper:testline, oper:kill, oper:operwall, oper:message, + oper:routing, oper:kline, oper:unkline, oper:xline, + oper:resv, oper:cmodes, oper:mass_notice, oper:wallops, + oper:remoteban, + usermode:servnotice, auspex:oper, auspex:hostname, auspex:umodes, auspex:cmodes, + oper:admin, oper:die, oper:rehash, oper:spy, oper:grant; +}}; +operator "operuser" {{ + user = "*@*"; + password = "operpassword"; + privset = "omnioper"; + flags = ~encrypted; +}}; """ diff --git a/irctest/controllers/ergo.py b/irctest/controllers/ergo.py index 1b276c0..d6e80ab 100644 --- a/irctest/controllers/ergo.py +++ b/irctest/controllers/ergo.py @@ -11,8 +11,6 @@ from irctest.basecontrollers import ( ) from irctest.cases import BaseServerTestCase -OPER_PWD = "frenchfries" - BASE_CONFIG = { "network": {"name": "ErgoTest"}, "server": { @@ -110,11 +108,11 @@ BASE_CONFIG = { } }, "opers": { - "root": { + "operuser": { "class": "server-admin", "whois-line": "is a server admin", - # OPER_PWD - "password": "$2a$04$3GzUZB5JapaAbwn7sogpOu9NSiLOgnozVllm2e96LiNPrm61ZsZSq", + # "operpassword" + "password": "$2a$04$bKb6k5A6yuFA2wx.iJtxcuT2dojHQAjHd5ZPK/I2sjJml7p4spxjG", } }, } @@ -291,7 +289,7 @@ class ErgoController(BaseServerController, DirectoryBasedController): self._write_config() client = "operator_for_rehash" case.connectClient(nick=client, name=client) - case.sendLine(client, "OPER root %s" % (OPER_PWD,)) + case.sendLine(client, "OPER operuser operpassword") case.sendLine(client, "REHASH") case.getMessages(client) case.sendLine(client, "QUIT") diff --git a/irctest/controllers/hybrid.py b/irctest/controllers/hybrid.py index 38cd212..919ce8b 100644 --- a/irctest/controllers/hybrid.py +++ b/irctest/controllers/hybrid.py @@ -55,6 +55,16 @@ auth {{ flags = exceed_limit; {password_field} }}; + +operator {{ + name = "operuser"; + user = "*@*"; + password = "operpassword"; + encrypted = no; + umodes = locops, servnotice, wallop; + flags = admin, connect, connect:remote, die, globops, kill, kill:remote, + kline, module, rehash, restart, set, unkline, unxline, xline; +}}; """ diff --git a/irctest/controllers/inspircd.py b/irctest/controllers/inspircd.py index 669f0bf..e7d1120 100644 --- a/irctest/controllers/inspircd.py +++ b/irctest/controllers/inspircd.py @@ -19,6 +19,21 @@ TEMPLATE_CONFIG = """ timeout="10" # So tests don't hang too long {password_field}> + + + + # Disable 'NOTICE #chan :*** foo invited bar into the channel- diff --git a/irctest/controllers/irc2.py b/irctest/controllers/irc2.py index 57eebca..56defc8 100644 --- a/irctest/controllers/irc2.py +++ b/irctest/controllers/irc2.py @@ -23,6 +23,9 @@ Y:10:90::100:512000:100.100:100.100: # I::::::: I::{password_field}:::10:: + +# O::::::: +O:*:operpassword:operuser:::: """ diff --git a/irctest/controllers/ircu2.py b/irctest/controllers/ircu2.py index 23c7563..592cfd2 100644 --- a/irctest/controllers/ircu2.py +++ b/irctest/controllers/ircu2.py @@ -33,8 +33,19 @@ Client {{ {password_field} }}; +Operator {{ + local = no; + host = "*@*"; + password = "$PLAIN$operpassword"; + name = "operuser"; + class = "Client"; +}}; + features {{ "PPATH" = "{pidfile}"; + + # workaround for whois tests, checking the server name + "HIS_SERVERNAME" = "My.Little.Server"; }}; """ diff --git a/irctest/controllers/plexus4.py b/irctest/controllers/plexus4.py index c38d107..1037a9f 100644 --- a/irctest/controllers/plexus4.py +++ b/irctest/controllers/plexus4.py @@ -60,6 +60,16 @@ auth {{ flags = exceed_limit; {password_field} }}; + +operator {{ + name = "operuser"; + user = "*@*"; + password = "operpassword"; + encrypted = no; + umodes = locops, servnotice, wallop; + flags = admin, connect, connect:remote, die, globops, kill, kill:remote, + kline, module, rehash, restart, set, unkline, unxline, xline; +}}; """ diff --git a/irctest/controllers/snircd.py b/irctest/controllers/snircd.py index da62d22..7fa9acc 100644 --- a/irctest/controllers/snircd.py +++ b/irctest/controllers/snircd.py @@ -33,6 +33,14 @@ Client {{ {password_field} }}; +Operator {{ + local = no; + host = "*@*"; + password = "$PLAIN$operpassword"; + name = "operuser"; + class = "Client"; +}}; + features {{ "PPATH" = "{pidfile}"; diff --git a/irctest/controllers/unrealircd.py b/irctest/controllers/unrealircd.py index d421abb..64f7559 100644 --- a/irctest/controllers/unrealircd.py +++ b/irctest/controllers/unrealircd.py @@ -11,6 +11,7 @@ from irctest.irc_utils.junkdrawer import find_hostname_and_port TEMPLATE_CONFIG = """ include "modules.default.conf"; +include "operclass.default.conf"; me {{ name "My.Little.Server"; @@ -99,6 +100,13 @@ tld {{ botmotd "{empty_file}"; rules "{empty_file}"; }} + +oper "operuser" {{ + password = "operpassword"; + mask *; + class clients; + operclass netadmin; +}} """ diff --git a/irctest/numerics.py b/irctest/numerics.py index 23cc0f8..6193243 100644 --- a/irctest/numerics.py +++ b/irctest/numerics.py @@ -56,6 +56,7 @@ RPL_USERHOST = "302" RPL_ISON = "303" RPL_UNAWAY = "305" RPL_NOWAWAY = "306" +RPL_WHOISREGNICK = "307" RPL_WHOISUSER = "311" RPL_WHOISSERVER = "312" RPL_WHOISOPERATOR = "313" @@ -64,6 +65,7 @@ RPL_ENDOFWHO = "315" RPL_WHOISIDLE = "317" RPL_ENDOFWHOIS = "318" RPL_WHOISCHANNELS = "319" +RPL_WHOISSPECIAL = "320" RPL_LIST = "322" RPL_LISTEND = "323" RPL_CHANNELMODEIS = "324" @@ -95,6 +97,8 @@ RPL_MOTD = "372" RPL_ENDOFINFO = "374" RPL_MOTDSTART = "375" RPL_ENDOFMOTD = "376" +RPL_WHOISHOST = "378" +RPL_WHOISMODES = "379" RPL_YOUREOPER = "381" RPL_REHASHING = "382" RPL_YOURESERVICE = "383" diff --git a/irctest/server_tests/whois.py b/irctest/server_tests/whois.py index caed495..962a3cd 100644 --- a/irctest/server_tests/whois.py +++ b/irctest/server_tests/whois.py @@ -1,15 +1,175 @@ +import pytest + from irctest import cases -from irctest.numerics import RPL_WHOISCHANNELS, RPL_WHOISUSER +from irctest.numerics import ( + RPL_AWAY, + RPL_ENDOFWHOIS, + RPL_WHOISACCOUNT, + RPL_WHOISACTUALLY, + RPL_WHOISCHANNELS, + RPL_WHOISHOST, + RPL_WHOISIDLE, + RPL_WHOISMODES, + RPL_WHOISOPERATOR, + RPL_WHOISREGNICK, + RPL_WHOISSECURE, + RPL_WHOISSERVER, + RPL_WHOISSPECIAL, + RPL_WHOISUSER, + RPL_YOUREOPER, +) +from irctest.patma import ANYSTR, StrRe -@cases.mark_services -class WhoisTestCase(cases.BaseServerTestCase, cases.OptionalityHelper): +class _WhoisTestMixin(cases.BaseServerTestCase): + def _testWhoisNumerics(self, authenticate, away, oper): + if authenticate: + self.connectClient("nick1") + self.controller.registerUser(self, "val", "sesame") + self.connectClient( + "nick2", account="val", password="sesame", capabilities=["sasl"] + ) + else: + self.connectClient("nick1") + self.connectClient("nick2") + + self.sendLine(2, "JOIN #chan1") + self.sendLine(2, "JOIN #chan2") + if away: + self.sendLine(2, "AWAY :I'm on a break") + self.getMessages(2) + + self.getMessages(1) + if oper: + self.sendLine(1, "OPER operuser operpassword") + self.assertIn( + RPL_YOUREOPER, + [m.command for m in self.getMessages(1)], + fail_msg="OPER failed", + ) + + self.sendLine(1, "WHOIS nick2") + + messages = [] + for _ in range(10): + messages.extend(self.getMessages(1)) + if RPL_ENDOFWHOIS in (m.command for m in messages): + break + + last_message = messages.pop() + + self.assertMessageMatch( + last_message, + command=RPL_ENDOFWHOIS, + params=["nick1", "nick2", ANYSTR], + fail_msg=f"Last message was not RPL_ENDOFWHOIS ({RPL_ENDOFWHOIS})", + ) + + unexpected_messages = [] + + # Straight from the Modern spec + for m in messages: + if m.command == RPL_AWAY and away: + self.assertMessageMatch(m, params=["nick1", "nick2", "I'm on a break"]) + elif m.command == RPL_WHOISREGNICK and authenticate: + self.assertMessageMatch(m, params=["nick1", "nick2", ANYSTR]) + elif m.command == RPL_WHOISUSER: + self.assertMessageMatch( + m, params=["nick1", "nick2", ANYSTR, ANYSTR, "*", ANYSTR] + ) + elif m.command == RPL_WHOISCHANNELS: + self.assertMessageMatch( + m, + params=[ + "nick1", + "nick2", + StrRe("(@#chan1 @#chan2|@#chan2 @#chan1)"), + ], + ) + elif m.command == RPL_WHOISSPECIAL: + # Technically allowed, but it's a bad style to use this without + # explicit configuration by the operators. + assert False, "RPL_WHOISSPECIAL in use with default configuration" + elif m.command == RPL_WHOISSERVER: + self.assertMessageMatch( + m, params=["nick1", "nick2", "My.Little.Server", ANYSTR] + ) + elif m.command == RPL_WHOISOPERATOR: + # TODO: unlikely to ever send this, we should oper up nick2 first + self.assertMessageMatch(m, params=["nick1", "nick2", ANYSTR]) + elif m.command == RPL_WHOISIDLE: + self.assertMessageMatch( + m, + params=["nick1", "nick2", StrRe("[0-9]+"), StrRe("[0-9]+"), ANYSTR], + ) + elif m.command == RPL_WHOISACCOUNT and authenticate: + self.assertMessageMatch(m, params=["nick1", "nick2", "val", ANYSTR]) + elif m.command == RPL_WHOISACTUALLY: + host_re = "[0-9a-z_:.-]+" + if len(m.params) == 4: + # Most common + self.assertMessageMatch( + m, + params=[ + "nick1", + "nick2", + StrRe(host_re), + ANYSTR, + ], + ) + elif len(m.params) == 5: + # eg. Hybrid, Unreal + self.assertMessageMatch( + m, + params=[ + "nick1", + "nick2", + StrRe(r"(~?username|\*)@" + host_re), + StrRe(host_re), + ANYSTR, + ], + ) + elif len(m.params) == 3: + # eg. Plexus4 + self.assertMessageMatch( + m, + params=[ + "nick1", + "nick2", + ANYSTR, + ], + ) + else: + assert ( + False + ), f"Unexpected number of params for RPL_WHOISACTUALLY: {m.params}" + elif m.command == RPL_WHOISHOST: + self.assertMessageMatch(m, params=["nick1", "nick2", ANYSTR]) + elif m.command == RPL_WHOISMODES: + self.assertMessageMatch(m, params=["nick1", "nick2", ANYSTR]) + elif m.command == RPL_WHOISSECURE: + # TODO: unlikely to ever send this, we should oper up nick2 first + self.assertMessageMatch(m, params=["nick1", "nick2", ANYSTR]) + else: + unexpected_messages.append(m) + + self.assertEqual( + unexpected_messages, [], fail_msg="Unexpected numeric messages: {got}" + ) + + +class WhoisTestCase(_WhoisTestMixin, cases.BaseServerTestCase, cases.OptionalityHelper): + @pytest.mark.parametrize( + "server", + ["", "My.Little.Server", "myCoolNick"], + ids=["no-target", "target_server", "target-nick"], + ) @cases.mark_specifications("RFC2812") - def testWhoisUser(self): + def testWhoisUser(self, server): """Test basic WHOIS behavior""" - nick = "myCoolNickname" - username = "myUsernam" # may be truncated if longer than this - realname = "My Real Name" + nick = "myCoolNick" + username = "myusernam" # may be truncated if longer than this + realname = "My User Name" self.addClient() self.sendLine(1, f"NICK {nick}") self.sendLine(1, f"USER {username} 0 * :{realname}") @@ -17,7 +177,7 @@ class WhoisTestCase(cases.BaseServerTestCase, cases.OptionalityHelper): self.connectClient("otherNickname") self.getMessages(2) - self.sendLine(2, "WHOIS mycoolnickname") + self.sendLine(2, f"WHOIS {server} mycoolnick") messages = self.getMessages(2) whois_user = messages[0] self.assertEqual(whois_user.command, RPL_WHOISUSER) @@ -30,6 +190,33 @@ class WhoisTestCase(cases.BaseServerTestCase, cases.OptionalityHelper): ) self.assertEqual(whois_user.params[5], realname) + @pytest.mark.parametrize( + "away,oper", + [(False, False), (True, False), (False, True)], + ids=["normal", "away", "oper"], + ) + @cases.mark_specifications("Modern") + def testWhoisNumerics(self, away, oper): + """Tests all numerics are in the exhaustive list defined in the Modern spec. + + TBD modern PR""" + self._testWhoisNumerics(authenticate=False, away=away, oper=oper) + + +@cases.mark_services +class ServicesWhoisTestCase( + _WhoisTestMixin, cases.BaseServerTestCase, cases.OptionalityHelper +): + @pytest.mark.parametrize("oper", [False, True], ids=["normal", "oper"]) + @cases.OptionalityHelper.skipUnlessHasMechanism("PLAIN") + @cases.mark_specifications("Modern") + def testWhoisNumerics(self, oper): + """Tests all numerics are in the exhaustive list defined in the Modern spec, + on an authenticated user. + + TBD modern PR""" + self._testWhoisNumerics(oper=oper, authenticate=True, away=False) + @cases.mark_specifications("Ergo") def testInvisibleWhois(self): """Test interaction between MODE +i and RPL_WHOISCHANNELS.""" diff --git a/setup.cfg b/setup.cfg index 8d79b7e..dae1fc1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,7 @@ [flake8] # E203: whitespaces before ':' # E231: missing whitespace after ',' +# E501: line too long # W503: line break before binary operator -ignore = E203,E231,W503 +ignore = E203,E231,E501,W503 max-line-length = 88