irctest/irctest/controllers/ergo.py

306 lines
9.6 KiB
Python
Raw Normal View History

import copy
import json
2016-11-29 12:36:32 +00:00
import os
import subprocess
from typing import Any, Dict, Optional, Set, Type, Union
2016-11-29 12:36:32 +00:00
2021-02-22 18:04:23 +00:00
from irctest.basecontrollers import (
BaseServerController,
DirectoryBasedController,
NotImplementedByController,
)
from irctest.cases import BaseServerTestCase
2016-11-29 12:36:32 +00:00
BASE_CONFIG = {
2021-05-27 03:55:21 +00:00
"network": {"name": "ErgoTest"},
"server": {
2021-08-28 16:03:15 +00:00
"name": "My.Little.Server",
"listeners": {},
"max-sendq": "16k",
"connection-limits": {
"enabled": True,
"cidr-len-ipv4": 32,
"cidr-len-ipv6": 64,
"ips-per-subnet": 1,
"exempted": ["localhost"],
},
"connection-throttling": {
"enabled": True,
"cidr-len-ipv4": 32,
"cidr-len-ipv6": 64,
"ips-per-subnet": 16,
"duration": "10m",
"max-connections": 1,
"ban-duration": "10m",
"ban-message": "Try again later",
"exempted": ["localhost"],
},
"lookup-hostnames": False,
2021-02-22 18:02:13 +00:00
"enforce-utf8": True,
"relaymsg": {"enabled": True, "separators": "/", "available-to-chanops": True},
"compatibility": {
"allow-truncation": False,
},
},
2021-02-22 18:02:13 +00:00
"accounts": {
"authentication-enabled": True,
2021-08-25 22:37:05 +00:00
"advertise-scram": True,
2021-02-22 18:02:13 +00:00
"multiclient": {
"allowed-by-default": True,
"enabled": True,
"always-on": "disabled",
2020-02-21 05:08:50 +00:00
},
2021-02-22 18:02:13 +00:00
"registration": {
"bcrypt-cost": 4,
"enabled": True,
"enabled-callbacks": ["none"],
"verify-timeout": "120h",
},
"nick-reservation": {
"enabled": True,
"method": "strict",
2020-10-07 12:51:29 +00:00
},
},
"channels": {"registration": {"enabled": True}},
"datastore": {"path": None},
2021-02-22 18:02:13 +00:00
"limits": {
"awaylen": 200,
"chan-list-modes": 60,
"channellen": 64,
"kicklen": 390,
"linelen": {"rest": 2048},
2021-02-22 18:02:13 +00:00
"monitor-entries": 100,
"nicklen": 32,
"topiclen": 390,
"whowas-entries": 100,
"multiline": {"max-bytes": 4096, "max-lines": 32},
2021-02-22 18:02:13 +00:00
},
"history": {
"enabled": True,
"channel-length": 128,
"client-length": 128,
"chathistory-maxmessages": 100,
"tagmsg-storage": {
"default": False,
"whitelist": ["+draft/persist", "+persist"],
},
},
"oper-classes": {
"server-admin": {
"title": "Server Admin",
"capabilities": [
"oper:local_kill",
"oper:local_ban",
"oper:local_unban",
"nofakelag",
"oper:remote_kill",
"oper:remote_ban",
"oper:remote_unban",
"oper:rehash",
"oper:die",
"accreg",
"sajoin",
"samode",
"vhosts",
"chanreg",
"relaymsg",
2020-02-17 09:05:21 +00:00
],
}
2020-02-17 09:05:21 +00:00
},
2021-02-22 18:02:13 +00:00
"opers": {
"operuser": {
2021-02-22 18:02:13 +00:00
"class": "server-admin",
"whois-line": "is a server admin",
# "operpassword"
"password": "$2a$04$bKb6k5A6yuFA2wx.iJtxcuT2dojHQAjHd5ZPK/I2sjJml7p4spxjG",
}
2020-02-17 09:05:21 +00:00
},
}
LOGGING_CONFIG = {"logging": [{"method": "stderr", "level": "debug", "type": "*"}]}
2016-11-29 12:36:32 +00:00
2021-02-22 18:02:13 +00:00
def hash_password(password: Union[str, bytes]) -> str:
2018-12-28 18:42:26 +00:00
if isinstance(password, str):
2021-02-22 18:02:13 +00:00
password = password.encode("utf-8")
# simulate entry of password and confirmation:
2021-02-22 18:02:13 +00:00
input_ = password + b"\n" + password + b"\n"
p = subprocess.Popen(
2021-05-26 20:02:22 +00:00
["ergo", "genpasswd"], stdin=subprocess.PIPE, stdout=subprocess.PIPE
2021-02-22 18:02:13 +00:00
)
out, _ = p.communicate(input_)
2021-02-22 18:02:13 +00:00
return out.decode("utf-8")
2018-12-28 18:42:26 +00:00
2021-05-27 03:55:21 +00:00
class ErgoController(BaseServerController, DirectoryBasedController):
software_name = "Ergo"
2021-02-22 18:02:13 +00:00
_port_wait_interval = 0.01
supported_sasl_mechanisms = {"PLAIN", "SCRAM-SHA-256"}
supports_sts = True
2021-07-03 22:06:26 +00:00
extban_mute_char = "m"
2019-12-08 20:26:21 +00:00
def create_config(self) -> None:
2019-12-08 20:26:21 +00:00
super().create_config()
2021-02-22 18:02:13 +00:00
with self.open_file("ircd.yaml"):
2019-12-08 20:26:21 +00:00
pass
2016-11-29 12:36:32 +00:00
2021-02-22 18:02:13 +00:00
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,
config: Optional[Any] = None,
) -> None:
2016-11-29 12:36:32 +00:00
if valid_metadata_keys or invalid_metadata_keys:
raise NotImplementedByController(
2021-02-22 18:02:13 +00:00
"Defining valid and invalid METADATA keys."
)
2017-09-10 23:15:18 +00:00
self.create_config()
2020-02-17 09:05:21 +00:00
if config is None:
config = copy.deepcopy(BASE_CONFIG)
assert self.directory
enable_chathistory = self.test_config.chathistory
2021-05-27 03:55:21 +00:00
enable_roleplay = self.test_config.ergo_roleplay
if enable_chathistory or enable_roleplay:
config = self.addMysqlToConfig(config)
if enable_roleplay:
config["roleplay"] = {"enabled": True}
2021-05-27 03:55:21 +00:00
if self.test_config.ergo_config:
self.test_config.ergo_config(config)
self.port = port
2020-02-17 09:05:21 +00:00
bind_address = "127.0.0.1:%s" % (port,)
2021-02-22 18:02:13 +00:00
listener_conf = None # plaintext
2016-11-29 12:36:32 +00:00
if ssl:
2021-02-22 18:02:13 +00:00
self.key_path = os.path.join(self.directory, "ssl.key")
self.pem_path = os.path.join(self.directory, "ssl.pem")
listener_conf = {"tls": {"cert": self.pem_path, "key": self.key_path}}
config["server"]["listeners"][bind_address] = listener_conf # type: ignore
config["datastore"]["path"] = os.path.join( # type: ignore
self.directory, "ircd.db"
)
2018-12-28 18:42:26 +00:00
if password is not None:
config["server"]["password"] = hash_password(password) # type: ignore
assert self.proc is None
2021-02-22 18:02:13 +00:00
self._config_path = os.path.join(self.directory, "server.yml")
2020-02-17 09:05:21 +00:00
self._config = config
self._write_config()
2021-05-26 20:02:22 +00:00
subprocess.call(["ergo", "initdb", "--conf", self._config_path, "--quiet"])
subprocess.call(["ergo", "mkcerts", "--conf", self._config_path, "--quiet"])
2021-02-22 18:02:13 +00:00
self.proc = subprocess.Popen(
2021-05-26 20:02:22 +00:00
["ergo", "run", "--conf", self._config_path, "--quiet"]
2021-02-22 18:02:13 +00:00
)
2016-11-29 12:36:32 +00:00
def wait_for_services(self) -> None:
# Nothing to wait for, they start at the same time as Ergo.
pass
def registerUser(
self,
case: BaseServerTestCase,
username: str,
password: Optional[str] = None,
) -> None:
2016-11-29 12:36:32 +00:00
# XXX: Move this somewhere else when
# https://github.com/ircv3/ircv3-specifications/pull/152 becomes
# part of the specification
if not case.run_services:
# Ergo does not actually need this, but other controllers do, so we
# are checking it here as well for tests that aren't tested with other
# controllers.
raise ValueError(
"Attempted to register a nick, but `run_services` it not True."
)
2016-11-29 12:36:32 +00:00
client = case.addClient(show_io=False)
2021-02-22 18:02:13 +00:00
case.sendLine(client, "CAP LS 302")
case.sendLine(client, "NICK " + username)
case.sendLine(client, "USER r e g :user")
case.sendLine(client, "CAP END")
while case.getRegistrationMessage(client).command != "001":
2016-11-29 12:36:32 +00:00
pass
case.getMessages(client)
assert password
2021-02-22 18:02:13 +00:00
case.sendLine(client, "NS REGISTER " + password)
2016-11-29 12:36:32 +00:00
msg = case.getMessage(client)
2021-02-22 18:02:13 +00:00
assert msg.params == [username, "Account created"]
case.sendLine(client, "QUIT")
case.assertDisconnected(client)
2016-11-29 12:36:32 +00:00
def _write_config(self) -> None:
2021-02-22 18:02:13 +00:00
with open(self._config_path, "w") as fd:
2020-02-17 09:05:21 +00:00
json.dump(self._config, fd)
def baseConfig(self) -> Dict:
2020-02-17 09:05:21 +00:00
return copy.deepcopy(BASE_CONFIG)
def getConfig(self) -> Dict:
2020-02-17 09:05:21 +00:00
return copy.deepcopy(self._config)
def addLoggingToConfig(self, config: Optional[Dict] = None) -> Dict:
2020-03-26 23:37:26 +00:00
if config is None:
config = self.baseConfig()
2020-02-17 09:05:21 +00:00
config.update(LOGGING_CONFIG)
return config
def addMysqlToConfig(self, config: Optional[Dict] = None) -> Dict:
2021-02-22 18:02:13 +00:00
mysql_password = os.getenv("MYSQL_PASSWORD")
2020-03-19 21:00:01 +00:00
if config is None:
config = self.baseConfig()
if not mysql_password:
return config
2021-02-22 18:02:13 +00:00
config["datastore"]["mysql"] = {
"enabled": True,
"host": "localhost",
2021-05-26 20:02:22 +00:00
"user": "ergo",
2021-02-22 18:02:13 +00:00
"password": mysql_password,
2021-05-26 20:02:22 +00:00
"history-database": "ergo_history",
2021-02-22 18:02:13 +00:00
"timeout": "3s",
}
2021-02-22 18:02:13 +00:00
config["accounts"]["multiclient"] = {
"enabled": True,
"allowed-by-default": True,
"always-on": "disabled",
}
2021-02-22 18:02:13 +00:00
config["history"]["persistent"] = {
"enabled": True,
"unregistered-channels": True,
"registered-channels": "opt-out",
"direct-messages": "opt-out",
}
return config
def rehash(self, case: BaseServerTestCase, config: Dict) -> None:
2020-02-17 09:05:21 +00:00
self._config = config
self._write_config()
2021-02-22 18:02:13 +00:00
client = "operator_for_rehash"
2020-02-17 09:05:21 +00:00
case.connectClient(nick=client, name=client)
case.sendLine(client, "OPER operuser operpassword")
2021-02-22 18:02:13 +00:00
case.sendLine(client, "REHASH")
2020-02-17 09:05:21 +00:00
case.getMessages(client)
2021-02-22 18:02:13 +00:00
case.sendLine(client, "QUIT")
2020-02-17 09:05:21 +00:00
case.assertDisconnected(client)
def enable_debug_logging(self, case: BaseServerTestCase) -> None:
2020-02-17 09:05:21 +00:00
config = self.getConfig()
config.update(LOGGING_CONFIG)
self.rehash(case, config)
2021-02-22 18:02:13 +00:00
2021-05-27 03:55:21 +00:00
def get_irctest_controller_class() -> Type[ErgoController]:
return ErgoController