1 Commits

Author SHA1 Message Date
6243908ecc Add tests for SASL-IR 2023-03-21 19:58:39 +01:00
49 changed files with 1079 additions and 2304 deletions

File diff suppressed because it is too large Load Diff

View File

@ -3,12 +3,12 @@
jobs:
build-anope:
runs-on: ubuntu-22.04
runs-on: ubuntu-20.04
steps:
- name: Create directories
run: cd ~/; mkdir -p .local/ go/
- name: Cache dependencies
uses: actions/cache@v3
uses: actions/cache@v2
with:
key: 3-${{ runner.os }}-anope-devel_release
path: '~/.cache
@ -16,13 +16,13 @@ jobs:
${ github.workspace }/anope
'
- uses: actions/checkout@v3
- name: Set up Python 3.11
uses: actions/setup-python@v4
- uses: actions/checkout@v2
- name: Set up Python 3.7
uses: actions/setup-python@v2
with:
python-version: 3.11
python-version: 3.7
- name: Checkout Anope
uses: actions/checkout@v3
uses: actions/checkout@v2
with:
path: anope
ref: 2.0.9
@ -37,23 +37,23 @@ jobs:
- name: Make artefact tarball
run: cd ~; tar -czf artefacts-anope.tar.gz .local/ go/
- name: Upload build artefacts
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v2
with:
name: installed-anope
path: ~/artefacts-*.tar.gz
retention-days: 1
build-inspircd:
runs-on: ubuntu-22.04
runs-on: ubuntu-20.04
steps:
- name: Create directories
run: cd ~/; mkdir -p .local/ go/
- uses: actions/checkout@v3
- name: Set up Python 3.11
uses: actions/setup-python@v4
- uses: actions/checkout@v2
- name: Set up Python 3.7
uses: actions/setup-python@v2
with:
python-version: 3.11
python-version: 3.7
- name: Checkout InspIRCd
uses: actions/checkout@v3
uses: actions/checkout@v2
with:
path: inspircd
ref: insp3
@ -61,18 +61,14 @@ jobs:
- name: Build InspIRCd
run: |
cd $GITHUB_WORKSPACE/inspircd/
# Insp3 <= 3.16.0 and Insp4 <= 4.0.0a21 don't support -DINSPIRCD_UNLIMITED_MAINLOOP
patch src/inspircd.cpp < $GITHUB_WORKSPACE/patches/inspircd_mainloop.patch || true
patch src/inspircd.cpp < $GITHUB_WORKSPACE/patches/inspircd_mainloop.patch
./configure --prefix=$HOME/.local/inspircd --development
CXXFLAGS=-DINSPIRCD_UNLIMITED_MAINLOOP make -j 4
make -j 4
make install
- name: Make artefact tarball
run: cd ~; tar -czf artefacts-inspircd.tar.gz .local/ go/
- name: Upload build artefacts
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v2
with:
name: installed-inspircd
path: ~/artefacts-*.tar.gz
@ -84,11 +80,11 @@ jobs:
- test-inspircd
- test-inspircd-anope
- test-inspircd-atheme
runs-on: ubuntu-22.04
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v2
- name: Download Artifacts
uses: actions/download-artifact@v3
uses: actions/download-artifact@v2
with:
path: artifacts
- name: Install dashboard dependencies
@ -111,15 +107,15 @@ jobs:
test-inspircd:
needs:
- build-inspircd
runs-on: ubuntu-22.04
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
- name: Set up Python 3.11
uses: actions/setup-python@v4
- uses: actions/checkout@v2
- name: Set up Python 3.7
uses: actions/setup-python@v2
with:
python-version: 3.11
python-version: 3.7
- name: Download build artefacts
uses: actions/download-artifact@v3
uses: actions/download-artifact@v2
with:
name: installed-inspircd
path: '~'
@ -137,7 +133,7 @@ jobs:
timeout-minutes: 30
- if: always()
name: Publish results
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v2
with:
name: pytest-results_inspircd_devel_release
path: pytest.xml
@ -145,20 +141,20 @@ jobs:
needs:
- build-inspircd
- build-anope
runs-on: ubuntu-22.04
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
- name: Set up Python 3.11
uses: actions/setup-python@v4
- uses: actions/checkout@v2
- name: Set up Python 3.7
uses: actions/setup-python@v2
with:
python-version: 3.11
python-version: 3.7
- name: Download build artefacts
uses: actions/download-artifact@v3
uses: actions/download-artifact@v2
with:
name: installed-inspircd
path: '~'
- name: Download build artefacts
uses: actions/download-artifact@v3
uses: actions/download-artifact@v2
with:
name: installed-anope
path: '~'
@ -176,22 +172,22 @@ jobs:
timeout-minutes: 30
- if: always()
name: Publish results
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v2
with:
name: pytest-results_inspircd-anope_devel_release
path: pytest.xml
test-inspircd-atheme:
needs:
- build-inspircd
runs-on: ubuntu-22.04
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
- name: Set up Python 3.11
uses: actions/setup-python@v4
- uses: actions/checkout@v2
- name: Set up Python 3.7
uses: actions/setup-python@v2
with:
python-version: 3.11
python-version: 3.7
- name: Download build artefacts
uses: actions/download-artifact@v3
uses: actions/download-artifact@v2
with:
name: installed-inspircd
path: '~'
@ -209,7 +205,7 @@ jobs:
timeout-minutes: 30
- if: always()
name: Publish results
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v2
with:
name: pytest-results_inspircd-atheme_devel_release
path: pytest.xml

File diff suppressed because it is too large Load Diff

View File

@ -2,13 +2,13 @@ exclude: ^irctest/scram
repos:
- repo: https://github.com/psf/black
rev: 23.1.0
rev: 22.3.0
hooks:
- id: black
language_version: python3
- repo: https://github.com/PyCQA/isort
rev: 5.11.5
rev: 5.5.2
hooks:
- id: isort
@ -18,7 +18,6 @@ repos:
- id: flake8
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.0.1
rev: v0.812
hooks:
- id: mypy
additional_dependencies: [types-PyYAML, types-docutils]

View File

@ -98,13 +98,6 @@ SOPEL_SELECTORS := \
(foo or not foo) \
$(EXTRA_SELECTORS)
# TheLounge can actually pass all the test so there is none to exclude.
# `(foo or not foo)` serves as a `true` value so it doesn't break when
# $(EXTRA_SELECTORS) is non-empty
THELOUNGE_SELECTORS := \
(foo or not foo) \
$(EXTRA_SELECTORS)
# 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
# Tests marked with private_chathistory can't pass because Unreal does not implement CHATHISTORY for DMs
@ -260,11 +253,6 @@ sopel:
--controller=irctest.controllers.sopel \
-k '$(SOPEL_SELECTORS)'
thelounge:
$(PYTEST) $(PYTEST_ARGS) \
--controller=irctest.controllers.thelounge \
-k '$(THELOUNGE_SELECTORS)'
unrealircd:
$(PYTEST) $(PYTEST_ARGS) \
--controller=irctest.controllers.unrealircd \

View File

@ -110,11 +110,8 @@ cd /tmp/
git clone https://github.com/inspircd/inspircd.git
cd inspircd
# Optional, makes tests run considerably faster. Pick one depending on the InspIRCd version:
# on Insp3 <= 3.16.0 and Insp4 <= 4.0.0a21:
# optional, makes tests run considerably faster
patch src/inspircd.cpp < ~/irctest/patches/inspircd_mainloop.patch
# on Insp3 >= 3.17.0 and Insp4 >= 4.0.0a22:
export CXXFLAGS=-DINSPIRCD_UNLIMITED_MAINLOOP
./configure --prefix=$HOME/.local/ --development
make -j 4

View File

@ -106,10 +106,13 @@ def pytest_collection_modifyitems(session, config, items):
assert isinstance(item, _pytest.python.Function)
# unittest-style test functions have the node of UnitTest class as parent
if tuple(map(int, _pytest.__version__.split("."))) >= (7,):
assert isinstance(item.parent, _pytest.python.Class)
else:
assert isinstance(item.parent, _pytest.python.Instance)
assert isinstance(
item.parent,
(
_pytest.python.Class, # pytest >= 7.0.0
_pytest.python.Instance, # pytest < 7.0.0
),
)
# and that node references the UnitTest class
assert issubclass(item.parent.cls, _IrcTestCase)

View File

@ -19,10 +19,6 @@ SHOWLISTMODES="1"
NOOPEROVERRIDE=""
OPEROVERRIDEVERIFY=""
GENCERTIFICATE="1"
# Use system argon to avoid getting SIGILLed if the build machine has a more recent
# CPU than the one running the tests.
EXTRAPARA="--with-system-argon2"
EXTRAPARA=""
ADVANCED=""

View File

@ -1,7 +1,6 @@
from __future__ import annotations
import dataclasses
import multiprocessing
import os
from pathlib import Path
import shutil
@ -10,18 +9,7 @@ import subprocess
import tempfile
import textwrap
import time
from typing import (
IO,
Any,
Callable,
Dict,
List,
MutableMapping,
Optional,
Set,
Tuple,
Type,
)
from typing import IO, Any, Callable, Dict, List, Optional, Set, Tuple, Type
import irctest
@ -70,49 +58,15 @@ class _BaseController:
supported_sasl_mechanisms: Set[str]
proc: Optional[subprocess.Popen]
_used_ports: Set[Tuple[str, int]]
"""``(hostname, port))`` used by this controller."""
# the following need to be shared between processes in case we are running in
# parallel (with pytest-xdist)
# The dicts are used as a set of (hostname, port), because _manager.set() doesn't
# exist.
_manager = multiprocessing.Manager()
_port_lock = _manager.Lock()
"""Lock for access to ``_all_used_ports`` and ``_available_ports``."""
_all_used_ports: MutableMapping[Tuple[str, int], None] = _manager.dict()
"""``(hostname, port)`` used by all controllers."""
_available_ports: MutableMapping[Tuple[str, int], None] = _manager.dict()
"""``(hostname, port)`` available to any controller."""
def __init__(self, test_config: TestCaseControllerConfig):
self.test_config = test_config
self.proc = None
self._used_ports = set()
def get_hostname_and_port(self) -> Tuple[str, int]:
with self._port_lock:
try:
# try to get a known available port
((hostname, port), _) = self._available_ports.popitem()
except KeyError:
# if there aren't any, iterate while we get a fresh one.
while True:
(hostname, port) = find_hostname_and_port()
if (hostname, port) not in self._all_used_ports:
# double-checking in self._used_ports to prevent collisions
# between controllers starting at the same time.
break
# Make this port unavailable to other processes
self._all_used_ports[(hostname, port)] = None
return (hostname, port)
def check_is_alive(self) -> None:
assert self.proc
self.proc.poll()
if self.proc.returncode is not None:
raise ProcessStopped(f"process returned {self.proc.returncode}")
raise ProcessStopped()
def kill_proc(self) -> None:
"""Terminates the controlled process, waits for it to exit, and
@ -130,11 +84,6 @@ class _BaseController:
if self.proc:
self.kill_proc()
# move this controller's ports from _all_used_ports to _available_ports
for hostname, port in self._used_ports:
del self._all_used_ports[(hostname, port)]
self._available_ports[(hostname, port)] = None
class DirectoryBasedController(_BaseController):
"""Helper for controllers whose software configuration is based on an
@ -253,6 +202,9 @@ class BaseServerController(_BaseController):
super().__init__(*args, **kwargs)
self.faketime_enabled = False
def get_hostname_and_port(self) -> Tuple[str, int]:
return find_hostname_and_port()
def run(
self,
hostname: str,
@ -261,6 +213,8 @@ class BaseServerController(_BaseController):
password: Optional[str],
ssl: bool,
run_services: bool,
valid_metadata_keys: Optional[Set[str]],
invalid_metadata_keys: Optional[Set[str]],
faketime: Optional[str],
) -> None:
raise NotImplementedError()

View File

@ -173,7 +173,7 @@ class _IrcTestCase(Generic[TController]):
) -> Optional[str]:
"""Returns an error message if the message doesn't match the given arguments,
or None if it matches."""
for key, value in kwargs.items():
for (key, value) in kwargs.items():
if getattr(msg, key) != value:
fail_msg = (
fail_msg or "expected {param} to be {expects}, got {got}: {msg}"
@ -351,8 +351,8 @@ class BaseClientTestCase(_IrcTestCase[basecontrollers.BaseClientController]):
nick: Optional[str] = None
user: Optional[List[str]] = None
server: socket.socket
protocol_version: Optional[str]
acked_capabilities: Optional[Set[str]]
protocol_version = Optional[str]
acked_capabilities = Optional[Set[str]]
__new__ = object.__new__ # pytest won't collect Generic[] subclasses otherwise
@ -448,9 +448,7 @@ class BaseClientTestCase(_IrcTestCase[basecontrollers.BaseClientController]):
print("{:.3f} S: {}".format(time.time(), line.strip()))
def readCapLs(
self,
auth: Optional[Authentication] = None,
tls_config: Optional[tls.TlsConfig] = None,
self, auth: Optional[Authentication] = None, tls_config: tls.TlsConfig = None
) -> None:
(hostname, port) = self.server.getsockname()
self.controller.run(
@ -460,9 +458,9 @@ class BaseClientTestCase(_IrcTestCase[basecontrollers.BaseClientController]):
m = self.getMessage()
self.assertEqual(m.command, "CAP", "First message is not CAP LS.")
if m.params == ["LS"]:
self.protocol_version = "301"
self.protocol_version = 301
elif m.params == ["LS", "302"]:
self.protocol_version = "302"
self.protocol_version = 302
elif m.params == ["END"]:
self.protocol_version = None
else:
@ -529,6 +527,8 @@ class BaseServerTestCase(
password: Optional[str] = None
ssl = False
valid_metadata_keys: Set[str] = set()
invalid_metadata_keys: Set[str] = set()
server_support: Optional[Dict[str, Optional[str]]]
run_services = False
@ -548,6 +548,8 @@ class BaseServerTestCase(
self.hostname,
self.port,
password=self.password,
valid_metadata_keys=self.valid_metadata_keys,
invalid_metadata_keys=self.invalid_metadata_keys,
ssl=self.ssl,
run_services=self.run_services,
faketime=self.faketime,
@ -687,7 +689,7 @@ class BaseServerTestCase(
def connectClient(
self,
nick: str,
name: Optional[TClientName] = None,
name: TClientName = None,
capabilities: Optional[List[str]] = None,
skip_if_cap_nak: bool = False,
show_io: Optional[bool] = None,
@ -732,8 +734,8 @@ class BaseServerTestCase(
self.server_support[param] = None
welcome.append(m)
self.targmax: Dict[str, Optional[str]] = dict( # type: ignore[assignment]
item.split(":", 1)
self.targmax: Dict[str, Optional[str]] = dict(
item.split(":", 1) # type: ignore
for item in (self.server_support.get("TARGMAX") or "").split(",")
if item
)

View File

@ -228,7 +228,7 @@ class SaslTestCase(cases.BaseClientTestCase):
self.assertEqual(m.params, ["+"], m)
@cases.skipUnlessHasMechanism("SCRAM-SHA-256")
def testScramBadPassword(self, server_fakes_success=False, fake_response=None):
def testScramBadPassword(self):
"""Test SCRAM-SHA-256 authentication with a bad password."""
auth = authentication.Authentication(
mechanisms=[authentication.Mechanisms.scram_sha_256],
@ -261,36 +261,6 @@ class SaslTestCase(cases.BaseClientTestCase):
with self.assertRaises(scram.NotAuthorizedException):
authenticator.response(msg)
if server_fakes_success:
self.sendLine(f"AUTHENTICATE :{fake_response}")
m = self.getMessage()
while m.command == "PING":
self.sendLine(f"PONG server. {m.params[-1]}")
m = self.getMessage()
self.assertMessageMatch(
m,
command="AUTHENTICATE",
params=["*"],
fail_msg="Client did not abort: {msg}",
)
@cases.skipUnlessHasMechanism("SCRAM-SHA-256")
@pytest.mark.parametrize(
"fake_response",
[
"",
"AAAA",
"dj1ubU1mM1FIV2NKUWk5cE1ndHFLU0tQclZueUk2c3FOTzZJN3BFLzBveUdjPQ==",
],
)
def testScramMaliciousServer(self, fake_response):
"""Test SCRAM-SHA-256 authentication to a server which pretends to know
the password"""
self.testScramBadPassword(
server_fakes_success=True, fake_response=fake_response
)
class Irc302SaslTestCase(cases.BaseClientTestCase):
@cases.skipUnlessHasMechanism("PLAIN")

View File

@ -3,7 +3,12 @@ import shutil
import subprocess
from typing import Optional, Set, Type
from irctest.basecontrollers import BaseServerController, DirectoryBasedController
from irctest.basecontrollers import (
BaseServerController,
DirectoryBasedController,
NotImplementedByController,
)
from irctest.irc_utils.junkdrawer import find_hostname_and_port
TEMPLATE_CONFIG = """
global {{
@ -107,14 +112,21 @@ class BahamutController(BaseServerController, DirectoryBasedController):
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()
(unused_hostname, unused_port) = self.get_hostname_and_port()
(services_hostname, services_port) = self.get_hostname_and_port()
(unused_hostname, unused_port) = find_hostname_and_port()
(services_hostname, services_port) = find_hostname_and_port()
password_field = "passwd {};".format(password) if password else ""

View File

@ -1,8 +1,13 @@
import shutil
import subprocess
from typing import Optional
from typing import Optional, Set
from irctest.basecontrollers import BaseServerController, DirectoryBasedController
from irctest.basecontrollers import (
BaseServerController,
DirectoryBasedController,
NotImplementedByController,
)
from irctest.irc_utils.junkdrawer import find_hostname_and_port
TEMPLATE_SSL_CONFIG = """
ssl_private_key = "{key_path}";
@ -36,13 +41,19 @@ class BaseHybridController(BaseServerController, DirectoryBasedController):
password: Optional[str],
ssl: bool,
run_services: bool,
valid_metadata_keys: Optional[Set[str]] = None,
invalid_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()
(services_hostname, services_port) = self.get_hostname_and_port()
(services_hostname, services_port) = find_hostname_and_port()
password_field = 'password = "{}";'.format(password) if password else ""
if ssl:
self.gen_ssl()

View File

@ -3,9 +3,13 @@ import json
import os
import shutil
import subprocess
from typing import Any, Dict, Optional, Type, Union
from typing import Any, Dict, Optional, Set, Type, Union
from irctest.basecontrollers import BaseServerController, DirectoryBasedController
from irctest.basecontrollers import (
BaseServerController,
DirectoryBasedController,
NotImplementedByController,
)
from irctest.cases import BaseServerTestCase
BASE_CONFIG = {
@ -126,7 +130,7 @@ def hash_password(password: Union[str, bytes]) -> str:
["ergo", "genpasswd"], stdin=subprocess.PIPE, stdout=subprocess.PIPE
)
out, _ = p.communicate(input_)
return out.decode("utf-8").strip()
return out.decode("utf-8")
class ErgoController(BaseServerController, DirectoryBasedController):
@ -149,9 +153,17 @@ class ErgoController(BaseServerController, DirectoryBasedController):
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],
config: Optional[Any] = None,
) -> None:
if valid_metadata_keys or invalid_metadata_keys:
raise NotImplementedByController(
"Defining valid and invalid METADATA keys."
)
self.create_config()
if config is None:
config = copy.deepcopy(BASE_CONFIG)

View File

@ -1,5 +1,5 @@
import os
from typing import Optional, Tuple, Type
from typing import Optional, Set, Tuple, Type
from irctest.basecontrollers import BaseServerController
@ -39,6 +39,9 @@ class ExternalServerController(BaseServerController):
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:
pass

View File

@ -1,9 +1,14 @@
import functools
import shutil
import subprocess
from typing import Optional, Type
from typing import Optional, Set, Type
from irctest.basecontrollers import BaseServerController, DirectoryBasedController
from irctest.basecontrollers import (
BaseServerController,
DirectoryBasedController,
NotImplementedByController,
)
from irctest.irc_utils.junkdrawer import find_hostname_and_port
TEMPLATE_CONFIG = """
# Clients:
@ -73,7 +78,6 @@ TEMPLATE_CONFIG = """
<module name="m_muteban"> # for testing mute extbans
<module name="namesx"> # For multi-prefix
<module name="sasl">
<module name="uhnames"> # For userhost-in-names
# HELP/HELPOP
<module name="alias"> # for the HELP alias
@ -121,13 +125,20 @@ class InspircdController(BaseServerController, DirectoryBasedController):
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,
) -> 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()
(services_hostname, services_port) = self.get_hostname_and_port()
(services_hostname, services_port) = find_hostname_and_port()
password_field = 'password="{}"'.format(password) if password else ""

View File

@ -1,6 +1,6 @@
import shutil
import subprocess
from typing import Optional, Type
from typing import Optional, Set, Type
from irctest.basecontrollers import (
BaseServerController,
@ -49,8 +49,14 @@ class Irc2Controller(BaseServerController, DirectoryBasedController):
password: Optional[str],
ssl: bool,
run_services: bool,
valid_metadata_keys: Optional[Set[str]] = None,
invalid_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."
)
if ssl:
raise NotImplementedByController("TLS")
if run_services:

View File

@ -1,6 +1,6 @@
import shutil
import subprocess
from typing import Optional, Type
from typing import Optional, Set, Type
from irctest.basecontrollers import (
BaseServerController,
@ -68,8 +68,14 @@ class Ircu2Controller(BaseServerController, DirectoryBasedController):
password: Optional[str],
ssl: bool,
run_services: bool,
valid_metadata_keys: Optional[Set[str]] = None,
invalid_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."
)
if ssl:
raise NotImplementedByController("TLS")
if run_services:

View File

@ -33,10 +33,10 @@ extensions:
- mammon.ext.ircv3.sasl
- mammon.ext.misc.nopost
metadata:
restricted_keys: []
restricted_keys:
{restricted_keys}
whitelist:
- display-name
- avatar
{authorized_keys}
monitor:
limit: 20
motd:
@ -89,6 +89,9 @@ class MammonController(BaseServerController, DirectoryBasedController):
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 password is not None:
@ -104,6 +107,8 @@ class MammonController(BaseServerController, DirectoryBasedController):
directory=self.directory,
hostname=hostname,
port=port,
authorized_keys=make_list(valid_metadata_keys or set()),
restricted_keys=make_list(restricted_metadata_keys or set()),
)
)
# with self.open_file('server.yml', 'r') as fd:

View File

@ -2,7 +2,12 @@ import shutil
import subprocess
from typing import Optional, Set, Type
from irctest.basecontrollers import BaseServerController, DirectoryBasedController
from irctest.basecontrollers import (
BaseServerController,
DirectoryBasedController,
NotImplementedByController,
)
from irctest.irc_utils.junkdrawer import find_hostname_and_port
TEMPLATE_CONFIG = """
[Global]
@ -48,13 +53,20 @@ class NgircdController(BaseServerController, DirectoryBasedController):
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()
(unused_hostname, unused_port) = self.get_hostname_and_port()
(unused_hostname, unused_port) = find_hostname_and_port()
password_field = "Password = {}".format(password) if password else ""

View File

@ -1,6 +1,6 @@
import shutil
import subprocess
from typing import Optional, Type
from typing import Optional, Set, Type
from irctest.basecontrollers import (
BaseServerController,
@ -67,8 +67,14 @@ class SnircdController(BaseServerController, DirectoryBasedController):
password: Optional[str],
ssl: bool,
run_services: bool,
valid_metadata_keys: Optional[Set[str]] = None,
invalid_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."
)
if ssl:
raise NotImplementedByController("TLS")
if run_services:

View File

@ -1,106 +0,0 @@
import json
import os
import subprocess
from typing import Optional, Type
from irctest import authentication, tls
from irctest.basecontrollers import (
BaseClientController,
DirectoryBasedController,
NotImplementedByController,
)
TEMPLATE_CONFIG = """
"use strict";
module.exports = {config};
"""
class TheLoungeController(BaseClientController, DirectoryBasedController):
software_name = "TheLounge"
supported_sasl_mechanisms = {
"PLAIN",
"ECDSA-NIST256P-CHALLENGE",
"SCRAM-SHA-256",
"EXTERNAL",
}
supports_sts = True
def create_config(self) -> None:
super().create_config()
with self.open_file("bot.conf"):
pass
with self.open_file("conf/users.conf"):
pass
def run(
self,
hostname: str,
port: int,
auth: Optional[authentication.Authentication],
tls_config: Optional[tls.TlsConfig] = None,
) -> None:
if tls_config is None:
tls_config = tls.TlsConfig(enable=False, trusted_fingerprints=[])
if tls_config and tls_config.trusted_fingerprints:
raise NotImplementedByController("Trusted fingerprints.")
if auth and any(
mech.to_string().startswith(("SCRAM-", "ECDSA-"))
for mech in auth.mechanisms
):
raise NotImplementedByController("ecdsa")
if auth and auth.password and len(auth.password) > 300:
# https://github.com/thelounge/thelounge/pull/4480
# Note that The Lounge truncates on 300 characters, not bytes.
raise NotImplementedByController("Passwords longer than 300 chars")
# Runs a client with the config given as arguments
assert self.proc is None
self.create_config()
if auth:
mechanisms = " ".join(mech.to_string() for mech in auth.mechanisms)
if auth.ecdsa_key:
with self.open_file("ecdsa_key.pem") as fd:
fd.write(auth.ecdsa_key)
else:
mechanisms = ""
assert self.directory
with self.open_file("config.js") as fd:
fd.write(
TEMPLATE_CONFIG.format(
config=json.dumps(
dict(
public=False,
host=f"unix:{self.directory}/sock", # prevents binding
)
)
)
)
with self.open_file("users/testuser.json") as fd:
json.dump(
dict(
networks=[
dict(
name="testnet",
host=hostname,
port=port,
tls=tls_config.enable if tls_config else "False",
sasl=mechanisms.lower(),
saslAccount=auth.username if auth else "",
saslPassword=auth.password if auth else "",
)
]
),
fd,
)
with self.open_file("users/testuser.json", "r") as fd:
print("config", json.load(fd)["networks"][0]["saslPassword"])
self.proc = subprocess.Popen(
[os.environ.get("THELOUNGE_BIN", "thelounge"), "start"],
env={**os.environ, "THELOUNGE_HOME": str(self.directory)},
)
def get_irctest_controller_class() -> Type[TheLoungeController]:
return TheLoungeController

View File

@ -5,9 +5,14 @@ from pathlib import Path
import shutil
import subprocess
import textwrap
from typing import Callable, ContextManager, Iterator, Optional, Type
from typing import Callable, ContextManager, Iterator, Optional, Set, Type
from irctest.basecontrollers import BaseServerController, DirectoryBasedController
from irctest.basecontrollers import (
BaseServerController,
DirectoryBasedController,
NotImplementedByController,
)
from irctest.irc_utils.junkdrawer import find_hostname_and_port
TEMPLATE_CONFIG = """
include "modules.default.conf";
@ -96,7 +101,7 @@ set {{
}}
modes-on-join "+H 100:1d"; // Enables CHATHISTORY
{set_v6only}
{set_extras}
}}
@ -112,31 +117,13 @@ files {{
}}
oper "operuser" {{
password "operpassword";
password = "operpassword";
mask *;
class clients;
operclass netadmin;
}}
"""
SET_V6ONLY = """
// Remove RPL_WHOISSPECIAL used to advertise security groups
whois-details {
security-groups { everyone none; self none; oper none; }
}
plaintext-policy {
server warn; // https://www.unrealircd.org/docs/FAQ#server-requires-tls
oper warn; // https://www.unrealircd.org/docs/FAQ#oper-requires-tls
}
anti-flood {
everyone {
connect-flood 255:10;
}
}
"""
def _filelock(path: Path) -> Callable[[], ContextManager]:
"""Alternative to :cls:`multiprocessing.Lock` that works with pytest-xdist"""
@ -199,8 +186,15 @@ class UnrealircdController(BaseServerController, DirectoryBasedController):
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
@ -213,54 +207,64 @@ class UnrealircdController(BaseServerController, DirectoryBasedController):
loadmodule "cloak_md5";
"""
)
set_v6only = SET_V6ONLY
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_v6only = ""
set_extras = ""
with self.open_file("empty.txt") as fd:
fd.write("\n")
password_field = 'password "{}";'.format(password) if password else ""
(services_hostname, services_port) = self.get_hostname_and_port()
(unused_hostname, unused_port) = self.get_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",
set_v6only=set_v6only,
extras=extras,
)
)
if faketime and shutil.which("faketime"):
faketime_cmd = ["faketime", "-f", faketime]
self.faketime_enabled = True
else:
faketime_cmd = []
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,

View File

@ -16,22 +16,16 @@ from typing import (
Optional,
Tuple,
TypeVar,
Union,
)
import xml.etree.ElementTree as ET
from defusedxml.ElementTree import parse as parse_xml
import docutils.core
from .shortxml import Namespace
NETLIFY_CHAR_BLACKLIST = frozenset('":<>|*?\r\n#')
"""Characters not allowed in output filenames"""
HTML = Namespace("http://www.w3.org/1999/xhtml")
@dataclasses.dataclass
class CaseResult:
module_name: str
@ -45,7 +39,7 @@ class CaseResult:
type: Optional[str] = None
message: Optional[str] = None
def output_filename(self) -> str:
def output_filename(self):
test_name = self.test_name
if len(test_name) > 50 or set(test_name) & NETLIFY_CHAR_BLACKLIST:
# File name too long or otherwise invalid. This should be good enough:
@ -81,7 +75,7 @@ def iter_job_results(job_file_name: Path, job: ET.ElementTree) -> Iterator[CaseR
skipped = False
details = None
system_out = None
extra: Dict[str, str] = {}
extra = {}
for child in case:
if child.tag == "skipped":
success = True
@ -126,43 +120,33 @@ def iter_job_results(job_file_name: Path, job: ET.ElementTree) -> Iterator[CaseR
def rst_to_element(s: str) -> ET.Element:
html = docutils.core.publish_parts(s, writer_name="xhtml")["html_body"]
# Force the HTML namespace on all elements produced by docutils, which are
# unqualified
tree_builder = ET.TreeBuilder(
element_factory=lambda tag, attrib: ET.Element(
"{%s}%s" % (HTML.uri, tag),
{"{%s}%s" % (HTML.uri, k): v for (k, v) in attrib.items()},
)
)
parser = ET.XMLParser(target=tree_builder)
htmltree = ET.fromstring(html, parser=parser)
htmltree = ET.fromstring(html)
return htmltree
def docstring(obj: object) -> Optional[ET.Element]:
def append_docstring(element: ET.Element, obj: object) -> None:
if obj.__doc__ is None:
return None
return
return rst_to_element(obj.__doc__)
element.append(rst_to_element(obj.__doc__))
def build_job_html(job: str, results: List[CaseResult]) -> ET.Element:
jobs = sorted({result.job for result in results})
root = ET.Element("html")
head = ET.SubElement(root, "head")
ET.SubElement(head, "title").text = job
ET.SubElement(head, "link", rel="stylesheet", type="text/css", href="./style.css")
table = build_test_table(jobs, results, "job-results test-matrix")
body = ET.SubElement(root, "body")
return HTML.html(
HTML.head(
HTML.title(job),
HTML.link(rel="stylesheet", type="text/css", href="./style.css"),
),
HTML.body(
HTML.h1(job),
table,
),
)
ET.SubElement(body, "h1").text = job
table = build_test_table(jobs, results)
table.set("class", "job-results test-matrix")
body.append(table)
return root
def build_module_html(
@ -170,37 +154,40 @@ def build_module_html(
) -> ET.Element:
module = importlib.import_module(module_name)
table = build_test_table(jobs, results, "module-results test-matrix")
root = ET.Element("html")
head = ET.SubElement(root, "head")
ET.SubElement(head, "title").text = module_name
ET.SubElement(head, "link", rel="stylesheet", type="text/css", href="./style.css")
return HTML.html(
HTML.head(
HTML.title(module_name),
HTML.link(rel="stylesheet", type="text/css", href="./style.css"),
),
HTML.body(
HTML.h1(module_name),
docstring(module),
table,
),
)
body = ET.SubElement(root, "body")
ET.SubElement(body, "h1").text = module_name
append_docstring(body, module)
table = build_test_table(jobs, results)
table.set("class", "module-results test-matrix")
body.append(table)
return root
def build_test_table(
jobs: List[str], results: List[CaseResult], class_: str
) -> ET.Element:
def build_test_table(jobs: List[str], results: List[CaseResult]) -> ET.Element:
multiple_modules = len({r.module_name for r in results}) > 1
results_by_module_and_class = group_by(
results, lambda r: (r.module_name, r.class_name)
)
job_row = HTML.tr(
HTML.th(), # column of case name
[HTML.th(HTML.div(HTML.span(job)), class_="job-name") for job in jobs],
)
table = ET.Element("table")
rows = []
job_row = ET.Element("tr")
ET.SubElement(job_row, "th") # column of case name
for job in jobs:
cell = ET.SubElement(job_row, "th")
ET.SubElement(ET.SubElement(cell, "div"), "span").text = job
cell.set("class", "job-name")
for (module_name, class_name), class_results in sorted(
for ((module_name, class_name), class_results) in sorted(
results_by_module_and_class.items()
):
if multiple_modules:
@ -216,70 +203,67 @@ def build_test_table(
module = importlib.import_module(module_name)
# Header row: class name
header_row = ET.SubElement(table, "tr")
th = ET.SubElement(header_row, "th", colspan=str(len(jobs) + 1))
row_anchor = f"{qualified_class_name}"
rows.append(
HTML.tr(
HTML.th(
HTML.h2(
HTML.a(
qualified_class_name,
href=f"#{row_anchor}",
id=row_anchor,
),
),
docstring(getattr(module, class_name)),
colspan=str(len(jobs) + 1),
)
)
section_header = ET.SubElement(
ET.SubElement(th, "h2"),
"a",
href=f"#{row_anchor}",
id=row_anchor,
)
section_header.text = qualified_class_name
append_docstring(th, getattr(module, class_name))
# Header row: one column for each implementation
rows.append(job_row)
table.append(job_row)
# One row for each test:
results_by_test = group_by(class_results, key=lambda r: r.test_name)
for test_name, test_results in sorted(results_by_test.items()):
for (test_name, test_results) in sorted(results_by_test.items()):
row_anchor = f"{qualified_class_name}.{test_name}"
if len(row_anchor) >= 50:
# Too long; give up on generating readable URL
# TODO: only hash test parameter
row_anchor = md5sum(row_anchor)
row = HTML.tr(
HTML.th(HTML.a(test_name, href=f"#{row_anchor}"), class_="test-name"),
id=row_anchor,
)
rows.append(row)
row = ET.SubElement(table, "tr", id=row_anchor)
cell = ET.SubElement(row, "th")
cell.set("class", "test-name")
cell_link = ET.SubElement(cell, "a", href=f"#{row_anchor}")
cell_link.text = test_name
results_by_job = group_by(test_results, key=lambda r: r.job)
for job_name in jobs:
cell = ET.SubElement(row, "td")
try:
(result,) = results_by_job[job_name]
except KeyError:
row.append(HTML.td("d", class_="deselected"))
cell.set("class", "deselected")
cell.text = "d"
continue
text: Union[str, None, ET.Element]
attrib = {}
text: Optional[str]
if result.skipped:
attrib["class"] = "skipped"
cell.set("class", "skipped")
if result.type == "pytest.skip":
text = "s"
elif result.type == "pytest.xfail":
text = "X"
attrib["class"] = "expected-failure"
cell.set("class", "expected-failure")
else:
text = result.type
elif result.success:
attrib["class"] = "success"
cell.set("class", "success")
if result.type:
# dead code?
text = result.type
else:
text = "."
else:
attrib["class"] = "failure"
cell.set("class", "failure")
if result.type:
# dead code?
text = result.type
@ -288,15 +272,14 @@ def build_test_table(
if result.system_out:
# There is a log file; link to it.
text = HTML.a(text or "?", href=f"./{result.output_filename()}")
a = ET.SubElement(cell, "a", href=f"./{result.output_filename()}")
a.text = text or "?"
else:
text = text or "?"
cell.text = text or "?"
if result.message:
attrib["title"] = result.message
cell.set("title", result.message)
row.append(HTML.td(text, attrib))
return HTML.table(*rows, class_=class_)
return table
def write_html_pages(
@ -331,7 +314,7 @@ def write_html_pages(
pages = []
for module_name, module_results in sorted(results_by_module.items()):
for (module_name, module_results) in sorted(results_by_module.items()):
# Filter out client jobs if this is a server test module, and vice versa
module_categories = {
job_categories[result.job]
@ -372,9 +355,18 @@ def write_test_outputs(output_dir: Path, results: List[CaseResult]) -> None:
def write_html_index(output_dir: Path, pages: List[Tuple[str, str, str]]) -> None:
root = ET.Element("html")
head = ET.SubElement(root, "head")
ET.SubElement(head, "title").text = "irctest dashboard"
ET.SubElement(head, "link", rel="stylesheet", type="text/css", href="./style.css")
body = ET.SubElement(root, "body")
ET.SubElement(body, "h1").text = "irctest dashboard"
module_pages = []
job_pages = []
for page_type, title, file_name in sorted(pages):
for (page_type, title, file_name) in sorted(pages):
if page_type == "module":
module_pages.append((title, file_name))
elif page_type == "job":
@ -382,36 +374,28 @@ def write_html_index(output_dir: Path, pages: List[Tuple[str, str, str]]) -> Non
else:
assert False, page_type
page = HTML.html(
HTML.head(
HTML.title("irctest dashboard"),
HTML.link(rel="stylesheet", type="text/css", href="./style.css"),
),
HTML.body(
HTML.h1("irctest dashboard"),
HTML.h2("Tests by command/specification"),
HTML.dl(
[
(
HTML.dt(HTML.a(module_name, href=f"./{file_name}")),
HTML.dd(docstring(importlib.import_module(module_name))),
)
for module_name, file_name in sorted(module_pages)
],
class_="module-index",
),
HTML.h2("Tests by implementation"),
HTML.ul(
[
HTML.li(HTML.a(job, href=f"./{file_name}"))
for job, file_name in sorted(job_pages)
],
class_="job-index",
),
),
)
ET.SubElement(body, "h2").text = "Tests by command/specification"
write_xml_file(output_dir / "index.xhtml", page)
dl = ET.SubElement(body, "dl")
dl.set("class", "module-index")
for (module_name, file_name) in sorted(module_pages):
module = importlib.import_module(module_name)
link = ET.SubElement(ET.SubElement(dl, "dt"), "a", href=f"./{file_name}")
link.text = module_name
append_docstring(ET.SubElement(dl, "dd"), module)
ET.SubElement(body, "h2").text = "Tests by implementation"
ul = ET.SubElement(body, "ul")
ul.set("class", "job-index")
for (job, file_name) in sorted(job_pages):
link = ET.SubElement(ET.SubElement(ul, "li"), "a", href=f"./{file_name}")
link.text = job
write_xml_file(output_dir / "index.xhtml", root)
def write_assets(output_dir: Path) -> None:
@ -423,12 +407,12 @@ def write_assets(output_dir: Path) -> None:
def write_xml_file(filename: Path, root: ET.Element) -> None:
# Hacky: ET expects the namespace to be present in every tag we create instead;
# but it would be excessively verbose.
root.set("xmlns", "http://www.w3.org/1999/xhtml")
# Serialize
if sys.version_info >= (3, 8):
s = ET.tostring(root, default_namespace=HTML.uri)
else:
# default_namespace not supported
s = ET.tostring(root)
s = ET.tostring(root)
with filename.open("wb") as fd:
fd.write(s)

View File

@ -18,7 +18,7 @@ class Artifact:
download_url: str
@property
def public_download_url(self) -> str:
def public_download_url(self):
# GitHub API is not available publicly for artifacts, we need to use
# a third-party proxy to access it...
name = urllib.parse.quote(self.name)

View File

@ -1,126 +0,0 @@
# Copyright (c) 2023 Valentin Lorentz
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
"""This module allows writing XML ASTs in a way that is more concise than the default
:mod:`xml.etree.ElementTree` interface.
For example:
.. code-block:: python
from .shortxml import Namespace
HTML = Namespace("http://www.w3.org/1999/xhtml")
page = HTML.html(
HTML.head(
HTML.title("irctest dashboard"),
HTML.link(rel="stylesheet", type="text/css", href="./style.css"),
),
HTML.body(
HTML.h1("irctest dashboard"),
HTML.h2("Tests by command/specification"),
HTML.dl(
[
( # elements can be arbitrarily nested in lists
HTML.dt(HTML.a(title, href=f"./{title}.xhtml")),
HTML.dd(defintion),
)
for title, definition in sorted(definitions)
],
class_="module-index",
),
HTML.h2("Tests by implementation"),
HTML.ul(
[
HTML.li(HTML.a(job, href=f"./{file_name}"))
for job, file_name in sorted(job_pages)
],
class_="job-index",
),
),
)
print(ET.tostring(page, default_namespace=HTML.uri))
Attributes can be passed either as dictionaries or as kwargs, and can be mixed
with child elements.
Trailing underscores are stripped from attributes, which allows passing reserved
Python keywords (eg. ``class_`` instead of ``class``)
Attributes are always qualified, and share the namespace of the element they are
attached to.
Mixed content (elements containing both text and child elements) is not supported.
"""
from typing import Dict, Sequence, Union
import xml.etree.ElementTree as ET
def _namespacify(ns: str, s: str) -> str:
return "{%s}%s" % (ns, s)
_Children = Union[None, Dict[str, str], ET.Element, Sequence["_Children"]]
class ElementFactory:
def __init__(self, namespace: str, tag: str):
self._tag = _namespacify(namespace, tag)
self._namespace = namespace
def __call__(self, *args: Union[str, _Children], **kwargs: str) -> ET.Element:
e = ET.Element(self._tag)
attributes = {k.rstrip("_"): v for (k, v) in kwargs.items()}
children = [*args, attributes]
if args and isinstance(children[0], str):
e.text = children[0]
children.pop(0)
for child in children:
self._append_child(e, child)
return e
def _append_child(self, e: ET.Element, child: _Children) -> None:
if isinstance(child, ET.Element):
e.append(child)
elif child is None:
pass
elif isinstance(child, dict):
for k, v in child.items():
e.set(_namespacify(self._namespace, k), str(v))
elif isinstance(child, str):
raise ValueError("Mixed content is not supported")
else:
for grandchild in child:
self._append_child(e, grandchild)
class Namespace:
def __init__(self, uri: str):
self.uri = uri
def __getattr__(self, tag: str) -> ElementFactory:
return ElementFactory(self.uri, tag)

View File

@ -152,7 +152,7 @@ def match_dict(
# Set to not-None if we find a Keys() operator in the dict keys
remaining_keys_wildcard = None
for expected_key, expected_value in expected.items():
for (expected_key, expected_value) in expected.items():
if isinstance(expected_key, RemainingKeys):
remaining_keys_wildcard = (expected_key.key, expected_value)
else:
@ -168,7 +168,7 @@ def match_dict(
if remaining_keys_wildcard:
(expected_key, expected_value) = remaining_keys_wildcard
for key, value in got.items():
for (key, value) in got.items():
if not match_string(key, expected_key):
return False
if not match_string(value, expected_value):

View File

@ -4,32 +4,11 @@
"""
from irctest import cases
from irctest.patma import ANYSTR, StrRe
from irctest.patma import ANYSTR
from irctest.runner import CapabilityNotSupported, ImplementationChoice
class CapTestCase(cases.BaseServerTestCase):
@cases.mark_specifications("IRCv3")
def testInvalidCapSubcommand(self):
"""“If no capabilities are active, an empty parameter must be sent.”
-- <http://ircv3.net/specs/core/capability-negotiation-3.1.html#the-cap-list-subcommand>
""" # noqa
self.addClient()
self.sendLine(1, "CAP NOTACOMMAND")
self.sendLine(1, "PING test123")
m = self.getRegistrationMessage(1)
self.assertTrue(
self.messageDiffers(m, command="PONG", params=[ANYSTR, "test123"]),
"Sending “CAP NOTACOMMAND” as first message got no reply",
)
self.assertMessageMatch(
m,
command="410",
params=["*", "NOTACOMMAND", ANYSTR],
fail_msg="Sending “CAP NOTACOMMAND” as first message got a reply "
"that is not ERR_INVALIDCAPCMD: {msg}",
)
@cases.mark_specifications("IRCv3")
def testNoReq(self):
"""Test the server handles gracefully clients which do not send
@ -44,206 +23,12 @@ class CapTestCase(cases.BaseServerTestCase):
self.getCapLs(1)
self.sendLine(1, "USER foo foo foo :foo")
self.sendLine(1, "NICK foo")
# Make sure the server didn't send anything yet
self.sendLine(1, "CAP LS 302")
self.getCapLs(1)
self.sendLine(1, "CAP END")
m = self.getRegistrationMessage(1)
self.assertMessageMatch(
m, command="001", fail_msg="Expected 001 after sending CAP END, got {msg}."
)
@cases.mark_specifications("IRCv3")
def testReqOne(self):
"""Tests requesting a single capability"""
self.addClient(1)
self.sendLine(1, "CAP LS")
self.getCapLs(1)
self.sendLine(1, "USER foo foo foo :foo")
self.sendLine(1, "NICK foo")
self.sendLine(1, "CAP REQ :multi-prefix")
m = self.getRegistrationMessage(1)
self.assertMessageMatch(
m,
command="CAP",
params=[ANYSTR, "ACK", StrRe("multi-prefix ?")],
fail_msg="Expected CAP ACK after sending CAP REQ, got {msg}.",
)
self.sendLine(1, "CAP LIST")
m = self.getRegistrationMessage(1)
self.assertMessageMatch(
m,
command="CAP",
params=[ANYSTR, "LIST", StrRe("multi-prefix ?")],
fail_msg="Expected CAP LIST after sending CAP LIST, got {msg}.",
)
self.sendLine(1, "CAP END")
m = self.getRegistrationMessage(1)
self.assertMessageMatch(
m, command="001", fail_msg="Expected 001 after sending CAP END, got {msg}."
)
@cases.mark_specifications("IRCv3")
@cases.xfailIfSoftware(
["ngIRCd"],
"ngIRCd does not support userhost-in-names",
)
def testReqTwo(self):
"""Tests requesting two capabilities at once"""
self.addClient(1)
self.sendLine(1, "CAP LS")
self.getCapLs(1)
self.sendLine(1, "USER foo foo foo :foo")
self.sendLine(1, "NICK foo")
self.sendLine(1, "CAP REQ :multi-prefix userhost-in-names")
m = self.getRegistrationMessage(1)
self.assertMessageMatch(
m,
command="CAP",
params=[ANYSTR, "ACK", StrRe("multi-prefix userhost-in-names ?")],
fail_msg="Expected CAP ACK after sending CAP REQ, got {msg}.",
)
self.sendLine(1, "CAP LIST")
m = self.getRegistrationMessage(1)
self.assertMessageMatch(
m,
command="CAP",
params=[
ANYSTR,
"LIST",
StrRe(
"(multi-prefix userhost-in-names|userhost-in-names multi-prefix) ?"
),
],
fail_msg="Expected CAP LIST after sending CAP LIST, got {msg}.",
)
self.sendLine(1, "CAP END")
m = self.getRegistrationMessage(1)
self.assertMessageMatch(
m, command="001", fail_msg="Expected 001 after sending CAP END, got {msg}."
)
@cases.mark_specifications("IRCv3")
@cases.xfailIfSoftware(
["ngIRCd"],
"ngIRCd does not support userhost-in-names",
)
def testReqOneThenOne(self):
"""Tests requesting two capabilities in different messages"""
self.addClient(1)
self.sendLine(1, "CAP LS")
self.getCapLs(1)
self.sendLine(1, "USER foo foo foo :foo")
self.sendLine(1, "NICK foo")
self.sendLine(1, "CAP REQ :multi-prefix")
m = self.getRegistrationMessage(1)
self.assertMessageMatch(
m,
command="CAP",
params=[ANYSTR, "ACK", StrRe("multi-prefix ?")],
fail_msg="Expected CAP ACK after sending CAP REQ, got {msg}.",
)
self.sendLine(1, "CAP REQ :userhost-in-names")
m = self.getRegistrationMessage(1)
self.assertMessageMatch(
m,
command="CAP",
params=[ANYSTR, "ACK", StrRe("userhost-in-names ?")],
fail_msg="Expected CAP ACK after sending CAP REQ, got {msg}.",
)
self.sendLine(1, "CAP LIST")
m = self.getRegistrationMessage(1)
self.assertMessageMatch(
m,
command="CAP",
params=[
ANYSTR,
"LIST",
StrRe(
"(multi-prefix userhost-in-names|userhost-in-names multi-prefix) ?"
),
],
fail_msg="Expected CAP LIST after sending CAP LIST, got {msg}.",
)
self.sendLine(1, "CAP END")
m = self.getRegistrationMessage(1)
self.assertMessageMatch(
m, command="001", fail_msg="Expected 001 after sending CAP END, got {msg}."
)
@cases.mark_specifications("IRCv3")
@cases.xfailIfSoftware(
["ngIRCd"],
"ngIRCd does not support userhost-in-names",
)
def testReqPostRegistration(self):
"""Tests requesting more capabilities after CAP END"""
self.addClient(1)
self.sendLine(1, "CAP LS")
self.getCapLs(1)
self.sendLine(1, "USER foo foo foo :foo")
self.sendLine(1, "NICK foo")
self.sendLine(1, "CAP REQ :multi-prefix")
m = self.getRegistrationMessage(1)
self.assertMessageMatch(
m,
command="CAP",
params=[ANYSTR, "ACK", StrRe("multi-prefix ?")],
fail_msg="Expected CAP ACK after sending CAP REQ, got {msg}.",
)
self.sendLine(1, "CAP LIST")
m = self.getRegistrationMessage(1)
self.assertMessageMatch(
m,
command="CAP",
params=[ANYSTR, "LIST", StrRe("multi-prefix ?")],
fail_msg="Expected CAP LIST after sending CAP LIST, got {msg}.",
)
self.sendLine(1, "CAP END")
m = self.getRegistrationMessage(1)
self.assertMessageMatch(
m, command="001", fail_msg="Expected 001 after sending CAP END, got {msg}."
)
self.getMessages(1)
self.sendLine(1, "CAP REQ :userhost-in-names")
m = self.getRegistrationMessage(1)
self.assertMessageMatch(
m,
command="CAP",
params=[ANYSTR, "ACK", StrRe("userhost-in-names ?")],
fail_msg="Expected CAP ACK after sending CAP REQ, got {msg}.",
)
self.sendLine(1, "CAP LIST")
m = self.getMessage(1)
self.assertMessageMatch(
m,
command="CAP",
params=[
ANYSTR,
"LIST",
StrRe(
"(multi-prefix userhost-in-names|userhost-in-names multi-prefix) ?"
),
],
fail_msg="Expected CAP LIST after sending CAP LIST, got {msg}.",
)
@cases.mark_specifications("IRCv3")
def testReqUnavailable(self):
"""Test the server handles gracefully clients which request
@ -260,7 +45,7 @@ class CapTestCase(cases.BaseServerTestCase):
self.assertMessageMatch(
m,
command="CAP",
params=[ANYSTR, "NAK", StrRe("foo ?")],
params=[ANYSTR, "NAK", "foo"],
fail_msg="Expected CAP NAK after requesting non-existing "
"capability, got {msg}.",
)
@ -293,6 +78,10 @@ class CapTestCase(cases.BaseServerTestCase):
)
@cases.mark_specifications("IRCv3")
@cases.xfailIfSoftware(
["UnrealIRCd"],
"UnrealIRCd sends a trailing space on CAP NAK: https://github.com/unrealircd/unrealircd/pull/148",
)
def testNakWhole(self):
"""“The capability identifier set must be accepted as a whole, or
rejected entirely.”
@ -334,12 +123,16 @@ class CapTestCase(cases.BaseServerTestCase):
self.assertMessageMatch(
m,
command="CAP",
params=[ANYSTR, "ACK", StrRe("multi-prefix ?")],
params=[ANYSTR, "ACK", "multi-prefix"],
fail_msg="Expected “CAP ACK :multi-prefix” after "
"sending “CAP REQ :multi-prefix”, but got {msg}.",
)
@cases.mark_specifications("IRCv3")
@cases.xfailIfSoftware(
["UnrealIRCd"],
"UnrealIRCd sends a trailing space on CAP NAK: https://github.com/unrealircd/unrealircd/pull/148",
)
def testCapRemovalByClient(self):
"""Test CAP LIST and removal of caps via CAP REQ :-tagname."""
cap1 = "echo-message"
@ -347,13 +140,8 @@ class CapTestCase(cases.BaseServerTestCase):
self.addClient(1)
self.connectClient("sender")
self.sendLine(1, "CAP LS 302")
caps = set()
while True:
m = self.getRegistrationMessage(1)
caps.update(m.params[-1].split())
if m.params[2] != "*":
break
if not ({cap1, cap2} <= caps):
m = self.getRegistrationMessage(1)
if not ({cap1, cap2} <= set(m.params[2].split())):
raise CapabilityNotSupported(f"{cap1} or {cap2}")
self.sendLine(1, f"CAP REQ :{cap1} {cap2}")
self.sendLine(1, "nick bar")
@ -379,19 +167,17 @@ class CapTestCase(cases.BaseServerTestCase):
m = self.getMessage(1)
self.assertIn("time", m.tags, m)
# remove the multi-prefix cap
# remove the server-time cap
self.sendLine(1, f"CAP REQ :-{cap2}")
m = self.getMessage(1)
# Must be either ACK or NAK
if self.messageDiffers(
m, command="CAP", params=[ANYSTR, "ACK", StrRe(f"-{cap2} ?")]
):
if self.messageDiffers(m, command="CAP", params=[ANYSTR, "ACK", f"-{cap2}"]):
self.assertMessageMatch(
m, command="CAP", params=[ANYSTR, "NAK", StrRe(f"-{cap2} ?")]
m, command="CAP", params=[ANYSTR, "NAK", f"-{cap2}"]
)
raise ImplementationChoice(f"Does not support CAP REQ -{cap2}")
# multi-prefix should be disabled
# server-time should be disabled
self.sendLine(1, "CAP LIST")
messages = self.getMessages(1)
cap_list = [m for m in messages if m.command == "CAP"][0]
@ -456,31 +242,3 @@ class CapTestCase(cases.BaseServerTestCase):
fail_msg="Sending “CAP LIST” as first message got a reply "
"that is not “CAP * LIST :”: {msg}",
)
@cases.mark_specifications("IRCv3")
def testNoMultiline301Response(self):
"""
Current version: "If the client supports CAP version 302, the server MAY send
multiple lines in response to CAP LS and CAP LIST." This should be read as
disallowing multiline responses to pre-302 clients.
-- <https://ircv3.net/specs/extensions/capability-negotiation#multiline-replies-to-cap-ls-and-cap-list>
""" # noqa
self.check301ResponsePreRegistration("bar", "CAP LS")
self.check301ResponsePreRegistration("qux", "CAP LS 301")
self.check301ResponsePostRegistration("baz", "CAP LS")
self.check301ResponsePostRegistration("bat", "CAP LS 301")
def check301ResponsePreRegistration(self, nick, cap_ls):
self.addClient(nick)
self.sendLine(nick, cap_ls)
self.sendLine(nick, "NICK " + nick)
self.sendLine(nick, "USER u s e r")
self.sendLine(nick, "CAP END")
responses = [msg for msg in self.skipToWelcome(nick) if msg.command == "CAP"]
self.assertLessEqual(len(responses), 1, responses)
def check301ResponsePostRegistration(self, nick, cap_ls):
self.connectClient(nick, name=nick)
self.sendLine(nick, cap_ls)
responses = [msg for msg in self.getMessages(nick) if msg.command == "CAP"]
self.assertLessEqual(len(responses), 1, responses)

View File

@ -10,7 +10,7 @@ import pytest
from irctest import cases, runner
from irctest.irc_utils.junkdrawer import random_name
from irctest.patma import ANYSTR, StrRe
from irctest.patma import ANYSTR
CHATHISTORY_CAP = "draft/chathistory"
EVENT_PLAYBACK_CAP = "draft/event-playback"
@ -21,6 +21,28 @@ SUBCOMMANDS = ["LATEST", "BEFORE", "AFTER", "BETWEEN", "AROUND"]
MYSQL_PASSWORD = ""
def validate_chathistory_batch(msgs):
batch_tag = None
closed_batch_tag = None
result = []
for msg in msgs:
if msg.command == "BATCH":
batch_param = msg.params[0]
if batch_tag is None and batch_param[0] == "+":
batch_tag = batch_param[1:]
elif batch_param[0] == "-":
closed_batch_tag = batch_param[1:]
elif (
msg.command == "PRIVMSG"
and batch_tag is not None
and msg.tags.get("batch") == batch_tag
):
if not msg.prefix.startswith("HistServ!"): # FIXME: ergo-specific
result.append(msg.to_history_message())
assert batch_tag == closed_batch_tag
return result
def skip_ngircd(f):
@functools.wraps(f)
def newf(self, *args, **kwargs):
@ -34,26 +56,6 @@ def skip_ngircd(f):
@cases.mark_specifications("IRCv3")
@cases.mark_services
class ChathistoryTestCase(cases.BaseServerTestCase):
def validate_chathistory_batch(self, msgs, target):
(start, *inner_msgs, end) = msgs
self.assertMessageMatch(
start, command="BATCH", params=[StrRe(r"\+.*"), "chathistory", target]
)
batch_tag = start.params[0][1:]
self.assertMessageMatch(end, command="BATCH", params=["-" + batch_tag])
result = []
for msg in inner_msgs:
if (
msg.command == "PRIVMSG"
and batch_tag is not None
and msg.tags.get("batch") == batch_tag
):
if not msg.prefix.startswith("HistServ!"): # FIXME: ergo-specific
result.append(msg.to_history_message())
return result
@staticmethod
def config() -> cases.TestCaseControllerConfig:
return cases.TestCaseControllerConfig(chathistory=True)
@ -306,9 +308,6 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
)
time.sleep(0.002)
self.getMessages(1)
self.getMessages(2)
self.validate_echo_messages(NUM_MESSAGES, echo_messages)
self.validate_chathistory(subcommand, echo_messages, 1, c2)
self.validate_chathistory(subcommand, echo_messages, 2, c1)
@ -402,15 +401,15 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
def _validate_chathistory_LATEST(self, echo_messages, user, chname):
INCLUSIVE_LIMIT = len(echo_messages) * 2
self.sendLine(user, "CHATHISTORY LATEST %s * %d" % (chname, INCLUSIVE_LIMIT))
result = self.validate_chathistory_batch(self.getMessages(user), chname)
result = validate_chathistory_batch(self.getMessages(user))
self.assertEqual(echo_messages, result)
self.sendLine(user, "CHATHISTORY LATEST %s * %d" % (chname, 5))
result = self.validate_chathistory_batch(self.getMessages(user), chname)
result = validate_chathistory_batch(self.getMessages(user))
self.assertEqual(echo_messages[-5:], result)
self.sendLine(user, "CHATHISTORY LATEST %s * %d" % (chname, 1))
result = self.validate_chathistory_batch(self.getMessages(user), chname)
result = validate_chathistory_batch(self.getMessages(user))
self.assertEqual(echo_messages[-1:], result)
self.sendLine(
@ -418,7 +417,7 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
"CHATHISTORY LATEST %s msgid=%s %d"
% (chname, echo_messages[4].msgid, INCLUSIVE_LIMIT),
)
result = self.validate_chathistory_batch(self.getMessages(user), chname)
result = validate_chathistory_batch(self.getMessages(user))
self.assertEqual(echo_messages[5:], result)
self.sendLine(
@ -426,7 +425,7 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
"CHATHISTORY LATEST %s timestamp=%s %d"
% (chname, echo_messages[4].time, INCLUSIVE_LIMIT),
)
result = self.validate_chathistory_batch(self.getMessages(user), chname)
result = validate_chathistory_batch(self.getMessages(user))
self.assertEqual(echo_messages[5:], result)
def _validate_chathistory_BEFORE(self, echo_messages, user, chname):
@ -436,7 +435,7 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
"CHATHISTORY BEFORE %s msgid=%s %d"
% (chname, echo_messages[6].msgid, INCLUSIVE_LIMIT),
)
result = self.validate_chathistory_batch(self.getMessages(user), chname)
result = validate_chathistory_batch(self.getMessages(user))
self.assertEqual(echo_messages[:6], result)
self.sendLine(
@ -444,7 +443,7 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
"CHATHISTORY BEFORE %s timestamp=%s %d"
% (chname, echo_messages[6].time, INCLUSIVE_LIMIT),
)
result = self.validate_chathistory_batch(self.getMessages(user), chname)
result = validate_chathistory_batch(self.getMessages(user))
self.assertEqual(echo_messages[:6], result)
self.sendLine(
@ -452,7 +451,7 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
"CHATHISTORY BEFORE %s timestamp=%s %d"
% (chname, echo_messages[6].time, 2),
)
result = self.validate_chathistory_batch(self.getMessages(user), chname)
result = validate_chathistory_batch(self.getMessages(user))
self.assertEqual(echo_messages[4:6], result)
def _validate_chathistory_AFTER(self, echo_messages, user, chname):
@ -462,7 +461,7 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
"CHATHISTORY AFTER %s msgid=%s %d"
% (chname, echo_messages[3].msgid, INCLUSIVE_LIMIT),
)
result = self.validate_chathistory_batch(self.getMessages(user), chname)
result = validate_chathistory_batch(self.getMessages(user))
self.assertEqual(echo_messages[4:], result)
self.sendLine(
@ -470,14 +469,14 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
"CHATHISTORY AFTER %s timestamp=%s %d"
% (chname, echo_messages[3].time, INCLUSIVE_LIMIT),
)
result = self.validate_chathistory_batch(self.getMessages(user), chname)
result = validate_chathistory_batch(self.getMessages(user))
self.assertEqual(echo_messages[4:], result)
self.sendLine(
user,
"CHATHISTORY AFTER %s timestamp=%s %d" % (chname, echo_messages[3].time, 3),
)
result = self.validate_chathistory_batch(self.getMessages(user), chname)
result = validate_chathistory_batch(self.getMessages(user))
self.assertEqual(echo_messages[4:7], result)
def _validate_chathistory_BETWEEN(self, echo_messages, user, chname):
@ -493,7 +492,7 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
INCLUSIVE_LIMIT,
),
)
result = self.validate_chathistory_batch(self.getMessages(user), chname)
result = validate_chathistory_batch(self.getMessages(user))
self.assertEqual(echo_messages[1:-1], result)
self.sendLine(
@ -506,7 +505,7 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
INCLUSIVE_LIMIT,
),
)
result = self.validate_chathistory_batch(self.getMessages(user), chname)
result = validate_chathistory_batch(self.getMessages(user))
self.assertEqual(echo_messages[1:-1], result)
# BETWEEN forwards and backwards with a limit, should get
@ -516,7 +515,7 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
"CHATHISTORY BETWEEN %s msgid=%s msgid=%s %d"
% (chname, echo_messages[0].msgid, echo_messages[-1].msgid, 3),
)
result = self.validate_chathistory_batch(self.getMessages(user), chname)
result = validate_chathistory_batch(self.getMessages(user))
self.assertEqual(echo_messages[1:4], result)
self.sendLine(
@ -524,7 +523,7 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
"CHATHISTORY BETWEEN %s msgid=%s msgid=%s %d"
% (chname, echo_messages[-1].msgid, echo_messages[0].msgid, 3),
)
result = self.validate_chathistory_batch(self.getMessages(user), chname)
result = validate_chathistory_batch(self.getMessages(user))
self.assertEqual(echo_messages[-4:-1], result)
# same stuff again but with timestamps
@ -533,28 +532,28 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
"CHATHISTORY BETWEEN %s timestamp=%s timestamp=%s %d"
% (chname, echo_messages[0].time, echo_messages[-1].time, INCLUSIVE_LIMIT),
)
result = self.validate_chathistory_batch(self.getMessages(user), chname)
result = validate_chathistory_batch(self.getMessages(user))
self.assertEqual(echo_messages[1:-1], result)
self.sendLine(
user,
"CHATHISTORY BETWEEN %s timestamp=%s timestamp=%s %d"
% (chname, echo_messages[-1].time, echo_messages[0].time, INCLUSIVE_LIMIT),
)
result = self.validate_chathistory_batch(self.getMessages(user), chname)
result = validate_chathistory_batch(self.getMessages(user))
self.assertEqual(echo_messages[1:-1], result)
self.sendLine(
user,
"CHATHISTORY BETWEEN %s timestamp=%s timestamp=%s %d"
% (chname, echo_messages[0].time, echo_messages[-1].time, 3),
)
result = self.validate_chathistory_batch(self.getMessages(user), chname)
result = validate_chathistory_batch(self.getMessages(user))
self.assertEqual(echo_messages[1:4], result)
self.sendLine(
user,
"CHATHISTORY BETWEEN %s timestamp=%s timestamp=%s %d"
% (chname, echo_messages[-1].time, echo_messages[0].time, 3),
)
result = self.validate_chathistory_batch(self.getMessages(user), chname)
result = validate_chathistory_batch(self.getMessages(user))
self.assertEqual(echo_messages[-4:-1], result)
def _validate_chathistory_AROUND(self, echo_messages, user, chname):
@ -562,14 +561,14 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
user,
"CHATHISTORY AROUND %s msgid=%s %d" % (chname, echo_messages[7].msgid, 1),
)
result = self.validate_chathistory_batch(self.getMessages(user), chname)
result = validate_chathistory_batch(self.getMessages(user))
self.assertEqual([echo_messages[7]], result)
self.sendLine(
user,
"CHATHISTORY AROUND %s msgid=%s %d" % (chname, echo_messages[7].msgid, 3),
)
result = self.validate_chathistory_batch(self.getMessages(user), chname)
result = validate_chathistory_batch(self.getMessages(user))
self.assertEqual(echo_messages[6:9], result)
self.sendLine(
@ -577,7 +576,7 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
"CHATHISTORY AROUND %s timestamp=%s %d"
% (chname, echo_messages[7].time, 3),
)
result = self.validate_chathistory_batch(self.getMessages(user), chname)
result = validate_chathistory_batch(self.getMessages(user))
self.assertIn(echo_messages[7], result)
@pytest.mark.arbitrary_client_tags

View File

@ -1,38 +0,0 @@
"""
Channel "no external messages" mode (`RFC 1459
<https://datatracker.ietf.org/doc/html/rfc1459#section-4.2.3.1>`__,
`Modern <https://modern.ircdocs.horse/#no-external-messages-mode>`__)
"""
from irctest import cases
from irctest.numerics import ERR_CANNOTSENDTOCHAN
class NoExternalMessagesTestCase(cases.BaseServerTestCase):
@cases.mark_specifications("RFC1459", "Modern")
def testNoExternalMessagesMode(self):
# test the +n channel mode
self.connectClient("chanop", name="chanop")
self.joinChannel("chanop", "#chan")
self.sendLine("chanop", "MODE #chan +n")
self.getMessages("chanop")
self.connectClient("baz", name="baz")
# this message should be suppressed completely by +n
self.sendLine("baz", "PRIVMSG #chan :hi from baz")
replies = self.getMessages("baz")
reply_cmds = {reply.command for reply in replies}
self.assertIn(ERR_CANNOTSENDTOCHAN, reply_cmds)
self.assertEqual(self.getMessages("chanop"), [])
# set the channel to -n: baz should be able to send now
self.sendLine("chanop", "MODE #chan -n")
replies = self.getMessages("chanop")
modeLines = [line for line in replies if line.command == "MODE"]
self.assertMessageMatch(modeLines[0], command="MODE", params=["#chan", "-n"])
self.sendLine("baz", "PRIVMSG #chan :hi again from baz")
self.getMessages("baz")
relays = self.getMessages("chanop")
self.assertMessageMatch(
relays[0], command="PRIVMSG", params=["#chan", "hi again from baz"]
)

View File

@ -22,17 +22,23 @@ class EchoMessageTestCase(cases.BaseServerTestCase):
@cases.mark_capabilities("echo-message")
def testEchoMessage(self, command, solo, server_time):
"""<http://ircv3.net/specs/extensions/echo-message-3.2.html>"""
capabilities = ["server-time"] if server_time else []
self.connectClient(
"baz",
capabilities=["echo-message", *capabilities],
skip_if_cap_nak=True,
)
if server_time:
self.connectClient(
"baz",
capabilities=["echo-message", "server-time"],
skip_if_cap_nak=True,
)
else:
self.connectClient(
"baz",
capabilities=["echo-message", "server-time"],
skip_if_cap_nak=True,
)
self.sendLine(1, "JOIN #chan")
if not solo:
capabilities = ["server-time"] if server_time else None
self.connectClient("qux", capabilities=capabilities)
self.sendLine(2, "JOIN #chan")

View File

@ -360,8 +360,8 @@ class InviteTestCase(cases.BaseServerTestCase):
self.getMessages(2)
self.sendLine(1, "JOIN #chan")
self.getMessages(1)
self.sendLine(2, "JOIN #chan")
self.getMessages(1)
self.getMessages(2)
self.getMessages(1)

View File

@ -12,7 +12,6 @@ import pytest
from irctest import cases
from irctest.numerics import ERR_UNKNOWNCOMMAND
from irctest.patma import ANYDICT, ANYOPTSTR, NotStrRe, RemainingKeys, StrRe
from irctest.runner import OptionalExtensionNotSupported
class LabeledResponsesTestCase(cases.BaseServerTestCase):
@ -23,10 +22,7 @@ class LabeledResponsesTestCase(cases.BaseServerTestCase):
capabilities=["echo-message", "batch", "labeled-response"],
skip_if_cap_nak=True,
)
if int(self.targmax.get("PRIVMSG", "1") or "4") < 3:
raise OptionalExtensionNotSupported("PRIVMSG to multiple targets")
self.getMessages(1)
self.connectClient(
"bar",
capabilities=["echo-message", "batch", "labeled-response"],

View File

@ -4,7 +4,6 @@ The PRIVMSG and NOTICE commands.
from irctest import cases
from irctest.numerics import ERR_INPUTTOOLONG
from irctest.patma import ANYSTR
class PrivmsgTestCase(cases.BaseServerTestCase):
@ -46,12 +45,12 @@ class PrivmsgTestCase(cases.BaseServerTestCase):
@cases.mark_specifications("RFC1459", "RFC2812")
def testPrivmsgNonexistentUser(self):
"""<https://tools.ietf.org/html/rfc2812#section-3.3.1>"""
"""https://tools.ietf.org/html/rfc2812#section-3.3.1"""
self.connectClient("foo")
self.sendLine(1, "PRIVMSG bar :hey there!")
msg = self.getMessage(1)
# ERR_NOSUCHNICK: 401 <sender> <recipient> :No such nick
self.assertMessageMatch(msg, command="401", params=["foo", "bar", ANYSTR])
# ERR_NOSUCHNICK
self.assertIn(msg.command, ("401"))
class NoticeTestCase(cases.BaseServerTestCase):
@ -101,13 +100,8 @@ class NoticeTestCase(cases.BaseServerTestCase):
class TagsTestCase(cases.BaseServerTestCase):
@cases.mark_capabilities("message-tags")
@cases.xfailIf(
lambda self: bool(
self.controller.software_name == "UnrealIRCd"
and self.controller.software_version == 5
),
"UnrealIRCd <6.0.7 dropped messages with excessively large tags: "
"https://bugs.unrealircd.org/view.php?id=5947",
@cases.xfailIfSoftware(
["UnrealIRCd"], "https://bugs.unrealircd.org/view.php?id=5947"
)
def testLineTooLong(self):
self.connectClient("bar", capabilities=["message-tags"], skip_if_cap_nak=True)

View File

@ -6,8 +6,8 @@ from irctest import cases
class MetadataTestCase(cases.BaseServerTestCase):
valid_metadata_keys = {"display-name", "avatar"}
invalid_metadata_keys = {"indisplay-name", "inavatar"}
valid_metadata_keys = {"valid_key1", "valid_key2"}
invalid_metadata_keys = {"invalid_key1", "invalid_key2"}
@cases.mark_specifications("IRCv3", deprecated=True)
def testInIsupport(self):
@ -36,7 +36,7 @@ class MetadataTestCase(cases.BaseServerTestCase):
def testGetOneUnsetValid(self):
"""<http://ircv3.net/specs/core/metadata-3.2.html#metadata-get>"""
self.connectClient("foo")
self.sendLine(1, "METADATA * GET display-name")
self.sendLine(1, "METADATA * GET valid_key1")
m = self.getMessage(1)
self.assertMessageMatch(
m,
@ -52,7 +52,7 @@ class MetadataTestCase(cases.BaseServerTestCase):
-- <http://ircv3.net/specs/core/metadata-3.2.html#metadata-get>
"""
self.connectClient("foo")
self.sendLine(1, "METADATA * GET display-name avatar")
self.sendLine(1, "METADATA * GET valid_key1 valid_key2")
m = self.getMessage(1)
self.assertMessageMatch(
m,
@ -62,10 +62,10 @@ class MetadataTestCase(cases.BaseServerTestCase):
)
self.assertEqual(
m.params[1],
"display-name",
"valid_key1",
m,
fail_msg="Response to “METADATA * GET display-name avatar"
"did not respond to display-name first: {msg}",
fail_msg="Response to “METADATA * GET valid_key1 valid_key2"
"did not respond to valid_key1 first: {msg}",
)
m = self.getMessage(1)
self.assertMessageMatch(
@ -76,10 +76,10 @@ class MetadataTestCase(cases.BaseServerTestCase):
)
self.assertEqual(
m.params[1],
"avatar",
"valid_key2",
m,
fail_msg="Response to “METADATA * GET display-name avatar"
"did not respond to avatar as second response: {msg}",
fail_msg="Response to “METADATA * GET valid_key1 valid_key2"
"did not respond to valid_key2 as second response: {msg}",
)
@cases.mark_specifications("IRCv3", deprecated=True)
@ -135,7 +135,7 @@ class MetadataTestCase(cases.BaseServerTestCase):
)
self.assertEqual(
m.params[1],
"display-name",
"valid_key1",
m,
fail_msg="Second param of 761 after setting “{expects}” to "
"{}” is not “{expects}”: {msg}.",
@ -190,7 +190,7 @@ class MetadataTestCase(cases.BaseServerTestCase):
def testSetGetValid(self):
"""<http://ircv3.net/specs/core/metadata-3.2.html>"""
self.connectClient("foo")
self.assertSetGetValue("*", "display-name", "myvalue")
self.assertSetGetValue("*", "valid_key1", "myvalue")
@cases.mark_specifications("IRCv3", deprecated=True)
def testSetGetZeroCharInValue(self):
@ -198,7 +198,7 @@ class MetadataTestCase(cases.BaseServerTestCase):
-- <http://ircv3.net/specs/core/metadata-3.2.html#metadata-restrictions>
"""
self.connectClient("foo")
self.assertSetGetValue("*", "display-name", "zero->\0<-zero", "zero->\\0<-zero")
self.assertSetGetValue("*", "valid_key1", "zero->\0<-zero", "zero->\\0<-zero")
@cases.mark_specifications("IRCv3", deprecated=True)
def testSetGetHeartInValue(self):
@ -209,7 +209,7 @@ class MetadataTestCase(cases.BaseServerTestCase):
self.connectClient("foo")
self.assertSetGetValue(
"*",
"display-name",
"valid_key1",
"->{}<-".format(heart),
"zero->{}<-zero".format(heart.encode()),
)
@ -223,7 +223,7 @@ class MetadataTestCase(cases.BaseServerTestCase):
# Sending directly because it is not valid UTF-8 so Python would
# not like it
self.clients[1].conn.sendall(
b"METADATA * SET display-name " b":invalid UTF-8 ->\xc3<-\r\n"
b"METADATA * SET valid_key1 " b":invalid UTF-8 ->\xc3<-\r\n"
)
commands = {m.command for m in self.getMessages(1)}
self.assertNotIn(
@ -233,7 +233,7 @@ class MetadataTestCase(cases.BaseServerTestCase):
"UTF-8 was answered with 761 (RPL_KEYVALUE)",
)
self.clients[1].conn.sendall(
b"METADATA * SET display-name " b":invalid UTF-8: \xc3\r\n"
b"METADATA * SET valid_key1 " b":invalid UTF-8: \xc3\r\n"
)
commands = {m.command for m in self.getMessages(1)}
self.assertNotIn(

View File

@ -249,23 +249,6 @@ class MonitorTestCase(_BaseMonitorTestCase):
extra_format=(messages,),
)
@cases.mark_specifications("IRCv3")
@cases.mark_isupport("MONITOR")
def testMonitorClear(self):
"""“Clears the list of targets being monitored. No output will be returned
for use of this command.“
-- <https://ircv3.net/specs/extensions/monitor#monitor-c>
"""
self.connectClient("foo")
self.check_server_support()
self.sendLine(1, "MONITOR + bar")
self.getMessages(1)
self.sendLine(1, "MONITOR C")
self.sendLine(1, "MONITOR L")
m = self.getMessage(1)
self.assertEqual(m.command, RPL_ENDOFMONLIST)
@cases.mark_specifications("IRCv3")
@cases.mark_isupport("MONITOR")
def testMonitorList(self):
@ -301,35 +284,6 @@ class MonitorTestCase(_BaseMonitorTestCase):
self.sendLine(1, "MONITOR L")
checkMonitorSubjects(self.getMessages(1), "bar", {"bazbat"})
@cases.mark_specifications("IRCv3")
@cases.mark_isupport("MONITOR")
def testMonitorStatus(self):
"""“Outputs for each target in the list being monitored, whether
the client is online or offline. All targets that are online will
be sent using RPL_MONONLINE, all targets that are offline will be
sent using RPL_MONOFFLINE.“
-- <https://ircv3.net/specs/extensions/monitor#monitor-s>
"""
self.connectClient("foo")
self.check_server_support()
self.connectClient("bar")
self.sendLine(1, "MONITOR + bar,baz")
self.getMessages(1)
self.sendLine(1, "MONITOR S")
msgs = self.getMessages(1)
self.assertEqual(
len(msgs),
2,
fail_msg="Expected one RPL_MONONLINE (730) and one RPL_MONOFFLINE (731), got: {}",
extra_format=(msgs,),
)
msgs.sort(key=lambda m: m.command)
self.assertMononline(1, "bar", m=msgs[0])
self.assertMonoffline(1, "baz", m=msgs[1])
@cases.mark_specifications("IRCv3")
@cases.mark_isupport("MONITOR")
def testNickChange(self):
@ -373,7 +327,7 @@ class _BaseExtendedMonitorTestCase(_BaseMonitorTestCase):
"""Tests https://ircv3.net/specs/extensions/extended-monitor.html"""
self.connectClient(
"foo",
capabilities=["extended-monitor", *watcher_caps],
capabilities=["draft/extended-monitor", *watcher_caps],
skip_if_cap_nak=True,
)

View File

@ -15,7 +15,7 @@ class MultiPrefixTestCase(cases.BaseServerTestCase):
These prefixes MUST be in order of rank, from highest to lowest.
"""
self.connectClient("foo", capabilities=["multi-prefix"], skip_if_cap_nak=True)
self.connectClient("foo", capabilities=["multi-prefix"])
self.joinChannel(1, "#chan")
self.sendLine(1, "MODE #chan +v foo")
self.getMessages(1)

View File

@ -10,7 +10,6 @@ TODO: cross-reference Modern
import time
from irctest import cases
from irctest.numerics import RPL_NAMREPLY
class PartTestCase(cases.BaseServerTestCase):
@ -85,12 +84,6 @@ class PartTestCase(cases.BaseServerTestCase):
self.getMessages(1)
self.getMessages(2)
self.sendLine(2, "PRIVMSG #chan :hi everyone")
self.getMessages(2)
self.assertMessageMatch(
self.getMessage(1), command="PRIVMSG", params=["#chan", "hi everyone"]
)
self.sendLine(1, "PART #chan")
# both the PART'ing client and the other channel member should receive
# a PART line:
@ -99,21 +92,6 @@ class PartTestCase(cases.BaseServerTestCase):
m = self.getMessage(2)
self.assertMessageMatch(m, command="PART")
self.sendLine(2, "PRIVMSG #chan :hi again everyone")
self.getMessages(2)
# client 1 has PART'ed and should not receive channel messages:
self.assertEqual(self.getMessages(1), [])
# client 1 should no longer appear in NAMES responses:
names = set()
self.sendLine(2, "NAMES #chan")
for reply in self.getMessages(2):
if reply.command != RPL_NAMREPLY:
continue
names.update(reply.params[-1].replace("@", "").split())
self.assertNotIn("bar", names)
self.assertIn("baz", names)
@cases.mark_specifications("RFC2812")
def testBasicPartRfc2812(self):
"""

View File

@ -1,4 +1,5 @@
import base64
from typing import List
from irctest import cases, runner, scram
from irctest.numerics import ERR_SASLFAIL
@ -11,8 +12,34 @@ class RegistrationTestCase(cases.BaseServerTestCase):
self.controller.registerUser(self, "testuser", "mypassword")
@cases.mark_services
class SaslTestCase(cases.BaseServerTestCase):
class _BaseSasl(cases.BaseServerTestCase):
sasl_ir: bool
capabilities: List[str]
def _doInitialExchange(self, client, mechanism: str, chunk: str):
"""Does the initial C->S, S->C, C->S exchange.
With ``sasl_ir=False``, this is done with the usual three messages exchange
(``AUTHENTICATE <mechanism>``, ``AUTHENTICATE +``, ``AUTHENTICATE <chunk>``)
with ``sasl_ir=True``, this is done in a single C->S message
(``AUTHENTICATE <mechanism> <chunk>``)
See the [sasl-ir spec](https://github.com/ircv3/ircv3-specifications/pull/520)
"""
if self.sasl_ir:
self.sendLine(client, f"AUTHENTICATE {mechanism} {chunk}")
else:
self.sendLine(client, f"AUTHENTICATE {mechanism}")
m = self.getRegistrationMessage(1)
self.assertMessageMatch(
m,
command="AUTHENTICATE",
params=["+"],
fail_msg=f"Sent “AUTHENTICATE {mechanism}”, server should have "
f"replied with “AUTHENTICATE +”, but instead sent: {{msg}}",
)
self.sendLine(client, f"AUTHENTICATE {chunk}")
@cases.mark_specifications("IRCv3")
@cases.skipUnlessHasMechanism("PLAIN")
def testPlain(self):
@ -34,17 +61,8 @@ class SaslTestCase(cases.BaseServerTestCase):
capabilities["sasl"],
fail_msg="Does not have PLAIN mechanism as the controller " "claims",
)
self.requestCapabilities(1, ["sasl"], skip_if_cap_nak=False)
self.sendLine(1, "AUTHENTICATE PLAIN")
m = self.getRegistrationMessage(1)
self.assertMessageMatch(
m,
command="AUTHENTICATE",
params=["+"],
fail_msg="Sent “AUTHENTICATE PLAIN”, server should have "
"replied with “AUTHENTICATE +”, but instead sent: {msg}",
)
self.sendLine(1, "AUTHENTICATE amlsbGVzAGppbGxlcwBzZXNhbWU=")
self.requestCapabilities(1, self.capabilities, skip_if_cap_nak=False)
self._doInitialExchange(1, "PLAIN", "amlsbGVzAGppbGxlcwBzZXNhbWU=")
m = self.getRegistrationMessage(1)
self.assertMessageMatch(
m,
@ -62,17 +80,8 @@ class SaslTestCase(cases.BaseServerTestCase):
).decode()
self.controller.registerUser(self, "foo", password)
self.addClient()
self.requestCapabilities(1, ["sasl"], skip_if_cap_nak=False)
self.sendLine(1, "AUTHENTICATE PLAIN")
m = self.getRegistrationMessage(1)
self.assertMessageMatch(
m,
command="AUTHENTICATE",
params=["+"],
fail_msg="Sent “AUTHENTICATE PLAIN”, server should have "
"replied with “AUTHENTICATE +”, but instead sent: {msg}",
)
self.sendLine(1, "AUTHENTICATE " + authstring)
self.requestCapabilities(1, self.capabilities, skip_if_cap_nak=False)
self._doInitialExchange(1, "PLAIN", authstring)
m = self.getRegistrationMessage(1)
self.assertMessageMatch(
m,
@ -122,17 +131,8 @@ class SaslTestCase(cases.BaseServerTestCase):
capabilities["sasl"],
fail_msg="Does not have PLAIN mechanism as the controller " "claims",
)
self.requestCapabilities(1, ["sasl"], skip_if_cap_nak=False)
self.sendLine(1, "AUTHENTICATE PLAIN")
m = self.getRegistrationMessage(1)
self.assertMessageMatch(
m,
command="AUTHENTICATE",
params=["+"],
fail_msg="Sent “AUTHENTICATE PLAIN”, server should have "
"replied with “AUTHENTICATE +”, but instead sent: {msg}",
)
self.sendLine(1, "AUTHENTICATE AGppbGxlcwBzZXNhbWU=")
self.requestCapabilities(1, self.capabilities, skip_if_cap_nak=False)
self._doInitialExchange(1, "PLAIN", "AGppbGxlcwBzZXNhbWU=")
m = self.getRegistrationMessage(1)
self.assertMessageMatch(
m,
@ -158,8 +158,11 @@ class SaslTestCase(cases.BaseServerTestCase):
capabilities,
fail_msg="Does not have SASL as the controller claims.",
)
self.requestCapabilities(1, ["sasl"], skip_if_cap_nak=False)
self.sendLine(1, "AUTHENTICATE FOO")
self.requestCapabilities(1, self.capabilities, skip_if_cap_nak=False)
if self.sasl_ir:
self.sendLine(1, "AUTHENTICATE FOO AGppbGxlcwBzZXNhbWU=")
else:
self.sendLine(1, "AUTHENTICATE FOO")
m = self.getRegistrationMessage(1)
while m.command == "908": # RPL_SASLMECHS
m = self.getRegistrationMessage(1)
@ -209,17 +212,8 @@ class SaslTestCase(cases.BaseServerTestCase):
capabilities["sasl"],
fail_msg="Does not have PLAIN mechanism as the controller " "claims",
)
self.requestCapabilities(1, ["sasl"], skip_if_cap_nak=False)
self.sendLine(1, "AUTHENTICATE PLAIN")
m = self.getRegistrationMessage(1)
self.assertMessageMatch(
m,
command="AUTHENTICATE",
params=["+"],
fail_msg="Sent “AUTHENTICATE PLAIN”, expected "
"“AUTHENTICATE +” as a response, but got: {msg}",
)
self.sendLine(1, "AUTHENTICATE {}".format(authstring[0:400]))
self.requestCapabilities(1, self.capabilities, skip_if_cap_nak=False)
self._doInitialExchange(1, "PLAIN", authstring[0:400])
self.sendLine(1, "AUTHENTICATE {}".format(authstring[400:]))
self.confirmSuccessfulAuth()
@ -279,17 +273,8 @@ class SaslTestCase(cases.BaseServerTestCase):
capabilities["sasl"],
fail_msg="Does not have PLAIN mechanism as the controller " "claims",
)
self.requestCapabilities(1, ["sasl"], skip_if_cap_nak=False)
self.sendLine(1, "AUTHENTICATE PLAIN")
m = self.getRegistrationMessage(1)
self.assertMessageMatch(
m,
command="AUTHENTICATE",
params=["+"],
fail_msg="Sent “AUTHENTICATE PLAIN”, expected "
"“AUTHENTICATE +” as a response, but got: {msg}",
)
self.sendLine(1, "AUTHENTICATE {}".format(authstring))
self.requestCapabilities(1, self.capabilities, skip_if_cap_nak=False)
self._doInitialExchange(1, "PLAIN", authstring)
self.sendLine(1, "AUTHENTICATE +")
self.confirmSuccessfulAuth()
@ -298,6 +283,12 @@ class SaslTestCase(cases.BaseServerTestCase):
# I don't know how to do it, because it would make the registration
# message's length too big for it to be valid.
@cases.mark_services
class SaslTestCase(_BaseSasl):
sasl_ir = False
capabilities = ["sasl"]
@cases.mark_specifications("IRCv3")
@cases.skipUnlessHasMechanism("SCRAM-SHA-256")
def testScramSha256Success(self):
@ -318,7 +309,7 @@ class SaslTestCase(cases.BaseServerTestCase):
fail_msg="Does not have SCRAM-SHA-256 mechanism as the "
"controller claims",
)
self.requestCapabilities(1, ["sasl"], skip_if_cap_nak=False)
self.requestCapabilities(1, self.capabilities, skip_if_cap_nak=False)
self.sendLine(1, "AUTHENTICATE SCRAM-SHA-256")
m = self.getRegistrationMessage(1)
@ -374,7 +365,7 @@ class SaslTestCase(cases.BaseServerTestCase):
fail_msg="Does not have SCRAM-SHA-256 mechanism as the "
"controller claims",
)
self.requestCapabilities(1, ["sasl"], skip_if_cap_nak=False)
self.requestCapabilities(1, self.capabilities, skip_if_cap_nak=False)
self.sendLine(1, "AUTHENTICATE SCRAM-SHA-256")
m = self.getRegistrationMessage(1)
@ -404,3 +395,36 @@ class SaslTestCase(cases.BaseServerTestCase):
)
m = self.getRegistrationMessage(1)
self.assertMessageMatch(m, command=ERR_SASLFAIL)
@cases.mark_services
class SaslIrTestCase(_BaseSasl):
"""Tests SASL with clients requesting the
[sasl-ir](https://github.com/ircv3/ircv3-specifications/pull/520) cap and using it.
"""
sasl_ir = True
capabilities = ["sasl", "draft/sasl-ir"]
def setUp(self):
super().setUp()
self.connectClient(
"capgetter", capabilities=["draft/sasl-ir"], skip_if_cap_nak=True
)
@cases.mark_services
class ImplicitSaslIrTestCase(_BaseSasl):
"""Tests SASL with clients using the
[sasl-ir](https://github.com/ircv3/ircv3-specifications/pull/520) CAP without
requesting it.
"""
sasl_ir = True
capabilities = ["sasl"]
def setUp(self):
super().setUp()
self.connectClient(
"capgetter", capabilities=["draft/sasl-ir"], skip_if_cap_nak=True
)

View File

@ -1,65 +0,0 @@
"""
`IRCv3 SETNAME<https://ircv3.net/specs/extensions/setname>`_
"""
from irctest import cases
from irctest.numerics import RPL_WHOISUSER
class SetnameMessageTestCase(cases.BaseServerTestCase):
@cases.mark_specifications("IRCv3")
@cases.mark_capabilities("setname")
def testSetnameMessage(self):
self.connectClient("foo", capabilities=["setname"], skip_if_cap_nak=True)
self.sendLine(1, "SETNAME bar")
self.assertMessageMatch(
self.getMessage(1),
command="SETNAME",
params=["bar"],
)
self.sendLine(1, "WHOIS foo")
whoisuser = [m for m in self.getMessages(1) if m.command == RPL_WHOISUSER][0]
self.assertEqual(whoisuser.params[-1], "bar")
@cases.mark_specifications("IRCv3")
@cases.mark_capabilities("setname")
def testSetnameChannel(self):
"""“[Servers] MUST send the server-to-client version of the
SETNAME message to all clients in common channels, as well as
to the client from which it originated, to confirm the change
has occurred.
The SETNAME message MUST NOT be sent to clients which do not
have the setname capability negotiated.“
"""
self.connectClient("foo", capabilities=["setname"], skip_if_cap_nak=True)
self.connectClient("bar", capabilities=["setname"], skip_if_cap_nak=True)
self.connectClient("baz")
self.joinChannel(1, "#chan")
self.joinChannel(2, "#chan")
self.joinChannel(3, "#chan")
self.getMessages(1)
self.getMessages(2)
self.getMessages(3)
self.sendLine(1, "SETNAME qux")
self.assertMessageMatch(
self.getMessage(1),
command="SETNAME",
params=["qux"],
)
self.assertMessageMatch(
self.getMessage(2),
command="SETNAME",
params=["qux"],
)
self.assertEqual(
self.getMessages(3),
[],
"Got SETNAME response when it was not negotiated",
)

View File

@ -46,30 +46,6 @@ class TopicTestCase(cases.BaseServerTestCase):
m = self.getMessage(2)
self.assertMessageMatch(m, command="TOPIC", params=["#chan", "T0P1C"])
@cases.mark_specifications("Modern")
def testTopicUnchanged(self):
""""If the topic of a channel is changed or cleared, every client in that
channel (including the author of the topic change) will receive a TOPIC command"
-- https://modern.ircdocs.horse/#topic-message
"""
self.connectClient("foo")
self.joinChannel(1, "#chan")
self.connectClient("bar")
self.joinChannel(2, "#chan")
# clear waiting msgs about cli 2 joining the channel
self.getMessages(1)
self.getMessages(2)
self.sendLine(1, "TOPIC #chan :T0P1C")
self.getMessages(1)
self.getMessages(2)
self.sendLine(1, "TOPIC #chan :T0P1C")
self.assertEqual(self.getMessages(2), [], "Unchanged topic was transmitted")
self.assertEqual(self.getMessages(1), [], "Unchanged topic was echoed")
@cases.mark_specifications("RFC1459", "RFC2812")
def testTopicMode(self):
"""“Once a user has joined a channel, he receives information about

View File

@ -361,87 +361,6 @@ class WhoTestCase(BaseWhoTestCase, cases.BaseServerTestCase):
params=["otherNick", InsensitiveStr(mask), ANYSTR],
)
@cases.mark_specifications("Modern")
def testWhoMultiChan(self):
"""
When WHO <#chan> is sent, the second parameter of RPL_WHOREPLY must
be ``#chan``. See discussion on Modern:
<https://github.com/ircdocs/modern-irc/issues/209>
"""
self._init()
self.sendLine(1, "JOIN #otherchan")
self.getMessages(1)
self.sendLine(2, "JOIN #otherchan")
self.getMessages(2)
for chan in ["#chan", "#otherchan"]:
self.sendLine(2, f"WHO {chan}")
messages = self.getMessages(2)
self.assertEqual(len(messages), 3, "Unexpected number of messages")
(*replies, end) = messages
# Get them in deterministic order
replies.sort(key=lambda msg: msg.params[5])
self.assertMessageMatch(
replies[0],
command=RPL_WHOREPLY,
params=[
"otherNick",
chan,
ANYSTR,
ANYSTR,
"My.Little.Server",
"coolNick",
ANYSTR,
ANYSTR,
],
)
self.assertMessageMatch(
replies[1],
command=RPL_WHOREPLY,
params=[
"otherNick",
chan,
ANYSTR,
ANYSTR,
"My.Little.Server",
"otherNick",
ANYSTR,
ANYSTR,
],
)
self.assertMessageMatch(
end,
command=RPL_ENDOFWHO,
params=["otherNick", InsensitiveStr(chan), ANYSTR],
)
@cases.mark_specifications("Modern")
def testWhoNickNotExists(self):
"""
When WHO is sent with a non-existing nickname, the server must reply
with a single RPL_ENDOFWHO. See:
<https://github.com/ircdocs/modern-irc/pull/216>
"""
self._init()
self.sendLine(2, "WHO idontexist")
(end,) = self.getMessages(2)
self.assertMessageMatch(
end,
command=RPL_ENDOFWHO,
params=["otherNick", InsensitiveStr("idontexist"), ANYSTR],
)
@cases.mark_specifications("IRCv3")
@cases.mark_isupport("WHOX")
def testWhoxFull(self):

View File

@ -201,6 +201,10 @@ class WhowasTestCase(cases.BaseServerTestCase):
)
@cases.mark_specifications("RFC1459", "RFC2812", "Modern")
@cases.xfailIfSoftware(
["InspIRCd"],
"Feature not released yet: https://github.com/inspircd/inspircd/pull/1967",
)
def testWhowasMultiple(self):
"""
"The history is searched backward, returning the most recent entry first."
@ -211,6 +215,10 @@ class WhowasTestCase(cases.BaseServerTestCase):
self._testWhowasMultiple(second_result=True, whowas_command="WHOWAS nick2")
@cases.mark_specifications("RFC1459", "RFC2812", "Modern")
@cases.xfailIfSoftware(
["InspIRCd"],
"Feature not released yet: https://github.com/inspircd/inspircd/pull/1968",
)
def testWhowasCount1(self):
"""
"If there are multiple entries, up to <count> replies will be returned"
@ -221,6 +229,10 @@ class WhowasTestCase(cases.BaseServerTestCase):
self._testWhowasMultiple(second_result=False, whowas_command="WHOWAS nick2 1")
@cases.mark_specifications("RFC1459", "RFC2812", "Modern")
@cases.xfailIfSoftware(
["InspIRCd"],
"Feature not released yet: https://github.com/inspircd/inspircd/pull/1968",
)
def testWhowasCount2(self):
"""
"If there are multiple entries, up to <count> replies will be returned"
@ -231,6 +243,10 @@ class WhowasTestCase(cases.BaseServerTestCase):
self._testWhowasMultiple(second_result=True, whowas_command="WHOWAS nick2 2")
@cases.mark_specifications("RFC1459", "RFC2812", "Modern")
@cases.xfailIfSoftware(
["InspIRCd"],
"Feature not released yet: https://github.com/inspircd/inspircd/pull/1968",
)
def testWhowasCountNegative(self):
"""
"If a non-positive number is passed as being <count>, then a full search
@ -248,6 +264,10 @@ class WhowasTestCase(cases.BaseServerTestCase):
@cases.xfailIfSoftware(
["ircu2"], "Fix not released yet: https://github.com/UndernetIRC/ircu2/pull/19"
)
@cases.xfailIfSoftware(
["InspIRCd"],
"Feature not released yet: https://github.com/inspircd/inspircd/pull/1967",
)
def testWhowasCountZero(self):
"""
"If a non-positive number is passed as being <count>, then a full search

View File

@ -65,7 +65,7 @@ def get_install_steps(*, software_config, software_id, version_flavor):
install_steps = [
{
"name": f"Checkout {name}",
"uses": "actions/checkout@v3",
"uses": "actions/checkout@v2",
"with": {
"repository": software_config["repository"],
"ref": ref,
@ -94,7 +94,7 @@ def get_build_job(*, software_config, software_id, version_flavor):
cache = [
{
"name": "Cache dependencies",
"uses": "actions/cache@v3",
"uses": "actions/cache@v2",
"with": {
"path": f"~/.cache\n${{ github.workspace }}/{path}\n",
"key": "3-${{ runner.os }}-"
@ -116,18 +116,18 @@ def get_build_job(*, software_config, software_id, version_flavor):
return None
return {
"runs-on": "ubuntu-22.04",
"runs-on": "ubuntu-20.04",
"steps": [
{
"name": "Create directories",
"run": "cd ~/; mkdir -p .local/ go/",
},
*cache,
{"uses": "actions/checkout@v3"},
{"uses": "actions/checkout@v2"},
{
"name": "Set up Python 3.11",
"uses": "actions/setup-python@v4",
"with": {"python-version": 3.11},
"name": "Set up Python 3.7",
"uses": "actions/setup-python@v2",
"with": {"python-version": 3.7},
},
*install_steps,
*upload_steps(software_id),
@ -159,7 +159,7 @@ def get_test_job(*, config, test_config, test_id, version_flavor, jobs):
downloads.append(
{
"name": "Download build artefacts",
"uses": "actions/download-artifact@v3",
"uses": "actions/download-artifact@v2",
"with": {"name": f"installed-{software_id}", "path": "~"},
}
)
@ -191,14 +191,14 @@ def get_test_job(*, config, test_config, test_id, version_flavor, jobs):
unpack = []
return {
"runs-on": "ubuntu-22.04",
"runs-on": "ubuntu-20.04",
"needs": needs,
"steps": [
{"uses": "actions/checkout@v3"},
{"uses": "actions/checkout@v2"},
{
"name": "Set up Python 3.11",
"uses": "actions/setup-python@v4",
"with": {"python-version": 3.11},
"name": "Set up Python 3.7",
"uses": "actions/setup-python@v2",
"with": {"python-version": 3.7},
},
*downloads,
*unpack,
@ -231,7 +231,7 @@ def get_test_job(*, config, test_config, test_id, version_flavor, jobs):
{
"name": "Publish results",
"if": "always()",
"uses": "actions/upload-artifact@v3",
"uses": "actions/upload-artifact@v2",
"with": {
"name": f"pytest-results_{test_id}_{version_flavor.value}",
"path": "pytest.xml",
@ -250,7 +250,7 @@ def upload_steps(software_id):
},
{
"name": "Upload build artefacts",
"uses": "actions/upload-artifact@v3",
"uses": "actions/upload-artifact@v2",
"with": {
"name": f"installed-{software_id}",
"path": "~/artefacts-*.tar.gz",
@ -263,6 +263,7 @@ def upload_steps(software_id):
def generate_workflow(config: dict, version_flavor: VersionFlavor):
on: dict
if version_flavor == VersionFlavor.STABLE:
on = {"push": None, "pull_request": None}
@ -306,15 +307,15 @@ def generate_workflow(config: dict, version_flavor: VersionFlavor):
jobs["publish-test-results"] = {
"name": "Publish Dashboard",
"needs": sorted({f"test-{test_id}" for test_id in config["tests"]} & set(jobs)),
"runs-on": "ubuntu-22.04",
"runs-on": "ubuntu-20.04",
# the build-and-test job might be skipped, we don't need to run
# this job then
"if": "success() || failure()",
"steps": [
{"uses": "actions/checkout@v3"},
{"uses": "actions/checkout@v2"},
{
"name": "Download Artifacts",
"uses": "actions/download-artifact@v3",
"uses": "actions/download-artifact@v2",
"with": {"path": "artifacts"},
},
{

View File

@ -12,9 +12,6 @@ disallow_untyped_defs = False
[mypy-irctest.client_tests.*]
disallow_untyped_defs = False
[mypy-irctest.self_tests.*]
disallow_untyped_defs = False
[mypy-defusedxml.*]
ignore_missing_imports = True

View File

@ -1,342 +0,0 @@
From 42b67ff7218877934abed2a738e164c0dea171b0 Mon Sep 17 00:00:00 2001
From: "Ned T. Crigler" <RuneB@dal.net>
Date: Sun, 26 Feb 2023 17:42:29 -0800
Subject: [PATCH 1/2] Fix compilation on Ubuntu 22.04
Starting with glibc 2.34 "The symbols __dn_comp, __dn_expand,
__dn_skipname, __res_dnok, __res_hnok, __res_mailok, __res_mkquery,
__res_nmkquery, __res_nquery, __res_nquerydomain, __res_nsearch,
__res_nsend, __res_ownok, __res_query, __res_querydomain, __res_search,
__res_send formerly in libresolv have been renamed and no longer have a
__ prefix. They are now available in libc."
https://sourceware.org/pipermail/libc-alpha/2021-August/129718.html
The hex_to_string array in include/dh.h also conflicts with OpenSSL,
which OpenSSL 3.0 now complains about.
---
configure.in | 4 ++--
include/dh.h | 2 +-
include/resolv.h | 6 +++++-
src/dh.c | 2 +-
4 files changed, 9 insertions(+), 5 deletions(-)
diff --git a/configure.in b/configure.in
index e76dee88..11720419 100644
--- a/configure.in
+++ b/configure.in
@@ -374,8 +374,7 @@ AC_C_INLINE
dnl Checks for libraries.
dnl Replace `main' with a function in -lnsl:
AC_CHECK_LIB(nsl, gethostbyname)
-AC_CHECK_FUNC(res_mkquery,, AC_CHECK_LIB(resolv, res_mkquery))
-AC_CHECK_FUNC(__res_mkquery,, AC_CHECK_LIB(resolv, __res_mkquery))
+AC_SEARCH_LIBS([res_mkquery],[resolv],,AC_SEARCH_LIBS([__res_mkquery],[resolv]))
AC_CHECK_LIB(socket, socket, zlib)
AC_CHECK_FUNC(crypt,, AC_CHECK_LIB(descrypt, crypt,,AC_CHECK_LIB(crypt, crypt,,)))
@@ -406,6 +405,7 @@ AC_CHECK_FUNCS([strcasecmp strchr strdup strerror strncasecmp strrchr strtol])
AC_CHECK_FUNCS([strtoul index strerror strtoken strtok inet_addr inet_netof])
AC_CHECK_FUNCS([inet_aton gettimeofday lrand48 sigaction bzero bcmp bcopy])
AC_CHECK_FUNCS([dn_skipname __dn_skipname getrusage times break])
+AC_CHECK_FUNCS([res_init __res_init res_mkquery __res_mkquery dn_expand __dn_expand])
dnl check for various OSes
diff --git a/include/dh.h b/include/dh.h
index 1ca6996a..1817ce1e 100644
--- a/include/dh.h
+++ b/include/dh.h
@@ -45,7 +45,7 @@ struct session_info
static BIGNUM *ircd_prime;
static BIGNUM *ircd_generator;
-static char *hex_to_string[256] =
+static char *dh_hex_to_string[256] =
{
"00", "01", "02", "03", "04", "05", "06", "07",
"08", "09", "0a", "0b", "0c", "0d", "0e", "0f",
diff --git a/include/resolv.h b/include/resolv.h
index b5a8aaa1..5b042d43 100644
--- a/include/resolv.h
+++ b/include/resolv.h
@@ -106,9 +106,13 @@ extern struct state _res;
extern char *p_cdname(), *p_rr(), *p_type(), *p_class(), *p_time();
-#if ((__GNU_LIBRARY__ == 6) && (__GLIBC__ >=2) && (__GLIBC_MINOR__ >= 2))
+#if !defined(HAVE_RES_INIT) && defined(HAVE___RES_INIT)
#define res_init __res_init
+#endif
+#if !defined(HAVE_RES_MKQUERY) && defined(HAVE___RES_MKQUERY)
#define res_mkquery __res_mkquery
+#endif
+#if !defined(HAVE_DN_EXPAND) && defined(HAVE___DN_EXPAND)
#define dn_expand __dn_expand
#endif
diff --git a/src/dh.c b/src/dh.c
index cb065a4f..4b5da282 100644
--- a/src/dh.c
+++ b/src/dh.c
@@ -223,7 +223,7 @@ static void create_prime()
for(i = 0; i < PRIME_BYTES; i++)
{
- char *x = hex_to_string[dh_prime_1024[i]];
+ char *x = dh_hex_to_string[dh_prime_1024[i]];
while(*x)
buf[bufpos++] = *x++;
}
From 135ebbea4c30e23228d00af762fa7da7ca5016bd Mon Sep 17 00:00:00 2001
From: "Ned T. Crigler" <RuneB@dal.net>
Date: Mon, 22 May 2023 15:31:54 -0700
Subject: [PATCH 2/2] Update the dh code to work with OpenSSL 3.0
---
include/dh.h | 8 ++++
src/dh.c | 120 ++++++++++++++++++++++++++++++++++++++++++++++++---
2 files changed, 123 insertions(+), 5 deletions(-)
diff --git a/include/dh.h b/include/dh.h
index 1817ce1e..705e6dee 100644
--- a/include/dh.h
+++ b/include/dh.h
@@ -22,7 +22,11 @@ extern void rc4_destroystate(void *a);
struct session_info
{
+#if OPENSSL_VERSION_NUMBER < 0x30000000L
DH *dh;
+#else
+ EVP_PKEY *dh;
+#endif
unsigned char *session_shared;
size_t session_shared_length;
};
@@ -45,6 +49,10 @@ struct session_info
static BIGNUM *ircd_prime;
static BIGNUM *ircd_generator;
+#if OPENSSL_VERSION_NUMBER >= 0x30000000L
+static EVP_PKEY *ircd_prime_ossl3;
+#endif
+
static char *dh_hex_to_string[256] =
{
"00", "01", "02", "03", "04", "05", "06", "07",
diff --git a/src/dh.c b/src/dh.c
index 4b5da282..f74d2d76 100644
--- a/src/dh.c
+++ b/src/dh.c
@@ -36,6 +36,11 @@
#include <openssl/dh.h>
#include "libcrypto-compat.h"
+#if OPENSSL_VERSION_NUMBER >= 0x30000000L
+#include <openssl/core_names.h>
+#include <openssl/param_build.h>
+#endif
+
#include "memcount.h"
#define DH_HEADER
@@ -215,7 +220,7 @@ static int init_random()
return 0;
}
-static void create_prime()
+static int create_prime()
{
char buf[PRIME_BYTES_HEX];
int i;
@@ -233,6 +238,34 @@ static void create_prime()
BN_hex2bn(&ircd_prime, buf);
ircd_generator = BN_new();
BN_set_word(ircd_generator, dh_gen_1024);
+
+#if OPENSSL_VERSION_NUMBER >= 0x30000000L
+ OSSL_PARAM_BLD *paramBuild = NULL;
+ OSSL_PARAM *param = NULL;
+ EVP_PKEY_CTX *primeCtx = NULL;
+
+ if(!(paramBuild = OSSL_PARAM_BLD_new()) ||
+ !OSSL_PARAM_BLD_push_BN(paramBuild, OSSL_PKEY_PARAM_FFC_P, ircd_prime) ||
+ !OSSL_PARAM_BLD_push_BN(paramBuild, OSSL_PKEY_PARAM_FFC_G, ircd_generator) ||
+ !(param = OSSL_PARAM_BLD_to_param(paramBuild)) ||
+ !(primeCtx = EVP_PKEY_CTX_new_from_name(NULL, "DHX", NULL)) ||
+ EVP_PKEY_fromdata_init(primeCtx) <= 0 ||
+ EVP_PKEY_fromdata(primeCtx, &ircd_prime_ossl3,
+ EVP_PKEY_KEY_PARAMETERS, param) <= 0 ||
+ 1)
+ {
+ if(primeCtx)
+ EVP_PKEY_CTX_free(primeCtx);
+ if(param)
+ OSSL_PARAM_free(param);
+ if(paramBuild)
+ OSSL_PARAM_BLD_free(paramBuild);
+ }
+
+ if(!ircd_prime_ossl3)
+ return -1;
+#endif
+ return 0;
}
int dh_init()
@@ -241,8 +274,7 @@ int dh_init()
ERR_load_crypto_strings();
#endif
- create_prime();
- if(init_random() == -1)
+ if(create_prime() == -1 || init_random() == -1)
return -1;
return 0;
}
@@ -250,7 +282,7 @@ int dh_init()
int dh_generate_shared(void *session, char *public_key)
{
BIGNUM *tmp;
- int len;
+ size_t len;
struct session_info *si = (struct session_info *) session;
if(verify_is_hex(public_key) == 0 || !si || si->session_shared)
@@ -261,13 +293,55 @@ int dh_generate_shared(void *session, char *public_key)
if(!tmp)
return 0;
+#if OPENSSL_VERSION_NUMBER < 0x30000000L
si->session_shared_length = DH_size(si->dh);
si->session_shared = (unsigned char *) malloc(DH_size(si->dh));
len = DH_compute_key(si->session_shared, tmp, si->dh);
+#else
+ OSSL_PARAM_BLD *paramBuild = NULL;
+ OSSL_PARAM *param = NULL;
+ EVP_PKEY_CTX *peerPubKeyCtx = NULL;
+ EVP_PKEY *peerPubKey = NULL;
+ EVP_PKEY_CTX *deriveCtx = NULL;
+
+ len = -1;
+ if(!(paramBuild = OSSL_PARAM_BLD_new()) ||
+ !OSSL_PARAM_BLD_push_BN(paramBuild, OSSL_PKEY_PARAM_FFC_P, ircd_prime) ||
+ !OSSL_PARAM_BLD_push_BN(paramBuild, OSSL_PKEY_PARAM_FFC_G, ircd_generator) ||
+ !OSSL_PARAM_BLD_push_BN(paramBuild, OSSL_PKEY_PARAM_PUB_KEY, tmp) ||
+ !(param = OSSL_PARAM_BLD_to_param(paramBuild)) ||
+ !(peerPubKeyCtx = EVP_PKEY_CTX_new_from_name(NULL, "DHX", NULL)) ||
+ EVP_PKEY_fromdata_init(peerPubKeyCtx) <= 0 ||
+ EVP_PKEY_fromdata(peerPubKeyCtx, &peerPubKey,
+ EVP_PKEY_PUBLIC_KEY, param) <= 0 ||
+ !(deriveCtx = EVP_PKEY_CTX_new(si->dh, NULL)) ||
+ EVP_PKEY_derive_init(deriveCtx) <= 0 ||
+ EVP_PKEY_derive_set_peer(deriveCtx, peerPubKey) <= 0 ||
+ EVP_PKEY_derive(deriveCtx, NULL, &len) <= 0 ||
+ !(si->session_shared = malloc(len)) ||
+ EVP_PKEY_derive(deriveCtx, si->session_shared, &len) <= 0 ||
+ 1)
+ {
+ if(deriveCtx)
+ EVP_PKEY_CTX_free(deriveCtx);
+ if(peerPubKey)
+ EVP_PKEY_free(peerPubKey);
+ if(peerPubKeyCtx)
+ EVP_PKEY_CTX_free(peerPubKeyCtx);
+ if(param)
+ OSSL_PARAM_free(param);
+ if(paramBuild)
+ OSSL_PARAM_BLD_free(paramBuild);
+ }
+#endif
BN_free(tmp);
- if(len < 0)
+ if(len == -1 || !si->session_shared)
+ {
+ if(si->session_shared)
+ free(si->session_shared);
return 0;
+ }
si->session_shared_length = len;
@@ -284,6 +358,7 @@ void *dh_start_session()
memset(si, 0, sizeof(struct session_info));
+#if OPENSSL_VERSION_NUMBER < 0x30000000L
si->dh = DH_new();
if(si->dh == NULL)
return NULL;
@@ -304,7 +379,23 @@ void *dh_start_session()
MyFree(si);
return NULL;
}
+#else
+ EVP_PKEY_CTX *keyGenCtx = NULL;
+ if(!(keyGenCtx = EVP_PKEY_CTX_new_from_pkey(NULL, ircd_prime_ossl3, NULL)) ||
+ EVP_PKEY_keygen_init(keyGenCtx) <= 0 ||
+ EVP_PKEY_generate(keyGenCtx, &si->dh) <= 0 ||
+ 1)
+ {
+ if(keyGenCtx)
+ EVP_PKEY_CTX_free(keyGenCtx);
+ }
+ if(!si->dh)
+ {
+ MyFree(si);
+ return NULL;
+ }
+#endif
return (void *) si;
}
@@ -312,6 +403,7 @@ void dh_end_session(void *session)
{
struct session_info *si = (struct session_info *) session;
+#if OPENSSL_VERSION_NUMBER < 0x30000000L
if(si->dh)
{
DH_free(si->dh);
@@ -324,6 +416,13 @@ void dh_end_session(void *session)
free(si->session_shared);
si->session_shared = NULL;
}
+#else
+ if(si->dh)
+ {
+ EVP_PKEY_free(si->dh);
+ si->dh = NULL;
+ }
+#endif
MyFree(si);
}
@@ -333,6 +432,7 @@ char *dh_get_s_public(char *buf, size_t maxlen, void *session)
struct session_info *si = (struct session_info *) session;
char *tmp;
+#if OPENSSL_VERSION_NUMBER < 0x30000000L
if(!si || !si->dh)
return NULL;
@@ -343,6 +443,16 @@ char *dh_get_s_public(char *buf, size_t maxlen, void *session)
return NULL;
tmp = BN_bn2hex(pub_key);
+#else
+ BIGNUM *pub_key = NULL;
+
+ if(!si || !si->dh)
+ return NULL;
+ if(!EVP_PKEY_get_bn_param(si->dh, OSSL_PKEY_PARAM_PUB_KEY, &pub_key))
+ return NULL;
+ tmp = BN_bn2hex(pub_key);
+ BN_free(pub_key);
+#endif
if(!tmp)
return NULL;

View File

@ -1,23 +0,0 @@
From fa5d445e5e2af735378a1219d2a200ee8aef6561 Mon Sep 17 00:00:00 2001
From: Sadie Powell <sadie@witchery.services>
Date: Sun, 25 Jun 2023 21:50:42 +0100
Subject: [PATCH] Fix Charybdis on Ubuntu 22.04.
---
librb/include/rb_lib.h | 2 ++
1 file changed, 2 insertions(+)
diff --git a/librb/include/rb_lib.h b/librb/include/rb_lib.h
index c02dff68..0dd9c378 100644
--- a/librb/include/rb_lib.h
+++ b/librb/include/rb_lib.h
@@ -258,4 +258,6 @@ pid_t rb_getpid(void);
#include <rb_rawbuf.h>
#include <rb_patricia.h>
+#include <time.h>
+
#endif
--
2.34.1

View File

@ -42,7 +42,7 @@ def partial_compaction(d):
# tests separate
compacted_d = {}
successes = []
for k, v in d.items():
for (k, v) in d.items():
if isinstance(v, CompactedResult) and v.success and v.nb_skipped == 0:
successes.append((k, v))
else:

View File

@ -18,7 +18,6 @@ software:
separate_build_job: true
build_script: |
cd $GITHUB_WORKSPACE/charybdis/
patch -p1 < $GITHUB_WORKSPACE/patches/charybdis_ubuntu22.patch
./autogen.sh
./configure --prefix=$HOME/.local/
make -j 4
@ -107,10 +106,6 @@ software:
cd $GITHUB_WORKSPACE/Bahamut/
patch src/s_user.c < $GITHUB_WORKSPACE/patches/bahamut_localhost.patch
patch src/s_bsd.c < $GITHUB_WORKSPACE/patches/bahamut_mainloop.patch
# <= v2.2.2
patch -p1 < $GITHUB_WORKSPACE/patches/bahamut_ubuntu22.patch || true
echo "#undef THROTTLE_ENABLE" >> include/config.h
libtoolize --force
aclocal
@ -136,7 +131,7 @@ software:
pre_deps:
- uses: actions/setup-go@v2
with:
go-version: '^1.21.0'
go-version: '^1.19.0'
- run: go version
separate_build_job: false
build_script: |
@ -148,7 +143,7 @@ software:
name: InspIRCd
repository: inspircd/inspircd
refs: &inspircd_refs
stable: v3.15.0
stable: v3.12.0
release: null
devel: master
devel_release: insp3
@ -158,13 +153,9 @@ software:
separate_build_job: true
build_script: &inspircd_build_script |
cd $GITHUB_WORKSPACE/inspircd/
# Insp3 <= 3.16.0 and Insp4 <= 4.0.0a21 don't support -DINSPIRCD_UNLIMITED_MAINLOOP
patch src/inspircd.cpp < $GITHUB_WORKSPACE/patches/inspircd_mainloop.patch || true
patch src/inspircd.cpp < $GITHUB_WORKSPACE/patches/inspircd_mainloop.patch
./configure --prefix=$HOME/.local/inspircd --development
CXXFLAGS=-DINSPIRCD_UNLIMITED_MAINLOOP make -j 4
make -j 4
make install
irc2:
name: irc2
@ -277,8 +268,8 @@ software:
name: UnrealIRCd 6
repository: unrealircd/unrealircd
refs:
stable: da3c1c654481a33035b9c703957e1c25d0158259 # 6.0.7
release: da3c1c654481a33035b9c703957e1c25d0158259 # 6.0.7
stable: cedd23ae9cdd5985ce16e9869cbdb808479c3fc4 # 6.0.3
release: cedd23ae9cdd5985ce16e9869cbdb808479c3fc4 # 6.0.3
devel: unreal60_dev
devel_release: null
path: unrealircd
@ -337,7 +328,7 @@ software:
separate_build_job: false
path: Dlk-Services
refs:
stable: &dlk_stable "6db51ea03f039c48fd20427c04cec8ff98df7878"
stable: &dlk_stable "effd18652fc1c847d1959089d9cca9ff9837a8c0"
release: *dlk_stable
devel: "main"
devel_release: *dlk_stable
@ -360,7 +351,7 @@ software:
install_steps:
stable:
- name: Install dependencies
run: pip install limnoria==2023.5.27 cryptography pyxmpp2-scram
run: pip install limnoria==2022.03.17 cryptography pyxmpp2-scram
release:
- name: Install dependencies
run: pip install limnoria cryptography pyxmpp2-scram
@ -384,23 +375,6 @@ software:
run: pip install git+https://github.com/sopel-irc/sopel.git
devel_release: null
thelounge:
name: TheLounge
repository: thelounge/thelounge
separate_build_job: false
refs:
stable: "v4.4.0"
release: "v4.4.0"
devel: "master"
devel_release: null
path: thelounge
build_script: |
cd $GITHUB_WORKSPACE/thelounge
yarn install
NODE_ENV=production yarn build
mkdir -p ~/.local/bin/
ln -s $(pwd)/index.js ~/.local/bin/thelounge
tests:
bahamut:
software: [bahamut]
@ -479,6 +453,3 @@ tests:
sopel:
software: [sopel]
thelounge:
software: [thelounge]