irctest/irctest/controllers/unrealircd.py

302 lines
8.0 KiB
Python

import contextlib
import fcntl
import functools
from pathlib import Path
import shutil
import subprocess
import textwrap
from typing import Callable, ContextManager, Iterator, 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";
include "operclass.default.conf";
{extras}
include "help/help.conf";
me {{
name "My.Little.Server";
info "test 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 *;
}}
password "password";
class servers;
}}
ulines {{
services.example.org;
}}
set {{
sasl-server services.example.org;
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 {{
nick-flood 255:10;
lag-penalty 1;
lag-penalty-bytes 10000;
}}
}}
modes-on-join "+H 100:1d"; // Enables CHATHISTORY
{set_extras}
}}
tld {{
mask *;
motd "{empty_file}";
botmotd "{empty_file}";
rules "{empty_file}";
}}
files {{
tunefile "{empty_file}";
}}
oper "operuser" {{
password = "operpassword";
mask *;
class clients;
operclass netadmin;
}}
"""
def _filelock(path: Path) -> Callable[[], ContextManager]:
"""Alternative to :cls:`multiprocessing.Lock` that works with pytest-xdist"""
@contextlib.contextmanager
def f() -> Iterator[None]:
with open(path, "a") as fd:
fcntl.flock(fd, fcntl.LOCK_EX)
yield
return f
_UNREALIRCD_BIN = shutil.which("unrealircd")
if _UNREALIRCD_BIN:
_UNREALIRCD_PREFIX = Path(_UNREALIRCD_BIN).parent.parent
# Try to keep that lock file specific to this Unrealircd instance
_LOCK_PATH = _UNREALIRCD_PREFIX / "irctest-unrealircd-startstop.lock"
else:
# unrealircd not found; we are probably going to crash later anyway...
_LOCK_PATH = Path("/tmp/irctest-unrealircd-startstop.lock")
_STARTSTOP_LOCK = _filelock(_LOCK_PATH)
"""
Unreal cleans its tmp/ directory after each run, which prevents
multiple processes from starting/stopping at the same time.
"""
@functools.lru_cache()
def installed_version() -> int:
output = subprocess.check_output(["unrealircd", "-v"], universal_newlines=True)
if output.startswith("UnrealIRCd-5."):
return 5
elif output.startswith("UnrealIRCd-6."):
return 6
else:
assert False, f"unexpected version: {output}"
class UnrealircdController(BaseServerController, DirectoryBasedController):
software_name = "UnrealIRCd"
supported_sasl_mechanisms = {"PLAIN"}
supports_sts = False
extban_mute_char = "quiet" if installed_version() >= 6 else "q"
software_version = installed_version()
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,
faketime: Optional[str],
) -> 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()
if installed_version() >= 6:
extras = textwrap.dedent(
"""
include "snomasks.default.conf";
loadmodule "cloak_md5";
"""
)
set_extras = textwrap.indent(
textwrap.dedent(
"""
// Remove RPL_WHOISSPECIAL used to advertise security groups
whois-details {
security-groups { everyone none; self none; oper none; }
}
"""
),
" ",
)
else:
extras = ""
set_extras = ""
with self.open_file("empty.txt") as fd:
fd.write("\n")
password_field = 'password "{}";'.format(password) if password else ""
with _STARTSTOP_LOCK():
(services_hostname, services_port) = find_hostname_and_port()
(unused_hostname, unused_port) = find_hostname_and_port()
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)
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=self.directory / "empty.txt",
extras=extras,
set_extras=set_extras,
)
)
if faketime and shutil.which("faketime"):
faketime_cmd = ["faketime", "-f", faketime]
self.faketime_enabled = True
else:
faketime_cmd = []
self.proc = subprocess.Popen(
[
*faketime_cmd,
"unrealircd",
"-t",
"-F", # BOOT_NOFORK
"-f",
self.directory / "unrealircd.conf",
],
# stdout=subprocess.DEVNULL,
)
self.wait_for_port()
if run_services:
self.services_controller = self.services_controller_class(
self.test_config, self
)
self.services_controller.run(
protocol="unreal4",
server_hostname=services_hostname,
server_port=services_port,
)
def kill_proc(self) -> None:
assert self.proc
with _STARTSTOP_LOCK():
self.proc.kill()
self.proc.wait(5) # wait for it to actually die
self.proc = None
def get_irctest_controller_class() -> Type[UnrealircdController]:
return UnrealircdController