Start adding support for Unreal

Not all tests pass yet, Unreal uses the protocol in ways we did not anticipate.
This commit is contained in:
Valentin Lorentz 2021-07-01 23:10:37 +02:00
parent cd58d14608
commit 2d2e788275
8 changed files with 280 additions and 11 deletions

55
.github/workflows/unrealircd.yml vendored Normal file
View File

@ -0,0 +1,55 @@
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_config.settings config.settings
CFLAGS=-O0 ./Config -quick
make -j 4
echo "\n\n\n\n\n\n" | make pem
make install
- name: Test with pytest
run: |
PATH=~/.local/unrealircd/bin:$PATH make unreal

View File

@ -60,6 +60,16 @@ SOPEL_SELECTORS := \
not testPlainNotAvailable \
$(EXTRA_SELECTORS)
# 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
UNREAL_SELECTORS := \
not Ergo \
and not testNoticeNonexistentChannel \
and not (test_regressions and testTagCap) \
and not (test_messages and testLineTooLong) \
$(EXTRA_SELECTORS)
.PHONY: all flakes ergo charybdis
all: flakes ergo inspircd limnoria sopel solanum

View File

@ -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,

View File

@ -0,0 +1,177 @@
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"; }}
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
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:
assert False
def get_irctest_controller_class() -> Type[UnrealircdController]:
return UnrealircdController

View File

@ -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()

View File

@ -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}")

View File

@ -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))

View File

@ -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)