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

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -98,13 +98,6 @@ SOPEL_SELECTORS := \
(foo or not foo) \ (foo or not foo) \
$(EXTRA_SELECTORS) $(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 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 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 # 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 \ --controller=irctest.controllers.sopel \
-k '$(SOPEL_SELECTORS)' -k '$(SOPEL_SELECTORS)'
thelounge:
$(PYTEST) $(PYTEST_ARGS) \
--controller=irctest.controllers.thelounge \
-k '$(THELOUNGE_SELECTORS)'
unrealircd: unrealircd:
$(PYTEST) $(PYTEST_ARGS) \ $(PYTEST) $(PYTEST_ARGS) \
--controller=irctest.controllers.unrealircd \ --controller=irctest.controllers.unrealircd \

View File

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

View File

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

View File

@ -19,10 +19,6 @@ SHOWLISTMODES="1"
NOOPEROVERRIDE="" NOOPEROVERRIDE=""
OPEROVERRIDEVERIFY="" OPEROVERRIDEVERIFY=""
GENCERTIFICATE="1" GENCERTIFICATE="1"
EXTRAPARA=""
# 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"
ADVANCED="" ADVANCED=""

View File

@ -1,7 +1,6 @@
from __future__ import annotations from __future__ import annotations
import dataclasses import dataclasses
import multiprocessing
import os import os
from pathlib import Path from pathlib import Path
import shutil import shutil
@ -10,18 +9,7 @@ import subprocess
import tempfile import tempfile
import textwrap import textwrap
import time import time
from typing import ( from typing import IO, Any, Callable, Dict, List, Optional, Set, Tuple, Type
IO,
Any,
Callable,
Dict,
List,
MutableMapping,
Optional,
Set,
Tuple,
Type,
)
import irctest import irctest
@ -70,49 +58,15 @@ class _BaseController:
supported_sasl_mechanisms: Set[str] supported_sasl_mechanisms: Set[str]
proc: Optional[subprocess.Popen] 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): def __init__(self, test_config: TestCaseControllerConfig):
self.test_config = test_config self.test_config = test_config
self.proc = None 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: def check_is_alive(self) -> None:
assert self.proc assert self.proc
self.proc.poll() self.proc.poll()
if self.proc.returncode is not None: if self.proc.returncode is not None:
raise ProcessStopped(f"process returned {self.proc.returncode}") raise ProcessStopped()
def kill_proc(self) -> None: def kill_proc(self) -> None:
"""Terminates the controlled process, waits for it to exit, and """Terminates the controlled process, waits for it to exit, and
@ -130,11 +84,6 @@ class _BaseController:
if self.proc: if self.proc:
self.kill_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): class DirectoryBasedController(_BaseController):
"""Helper for controllers whose software configuration is based on an """Helper for controllers whose software configuration is based on an
@ -253,6 +202,9 @@ class BaseServerController(_BaseController):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.faketime_enabled = False self.faketime_enabled = False
def get_hostname_and_port(self) -> Tuple[str, int]:
return find_hostname_and_port()
def run( def run(
self, self,
hostname: str, hostname: str,
@ -261,6 +213,8 @@ class BaseServerController(_BaseController):
password: Optional[str], password: Optional[str],
ssl: bool, ssl: bool,
run_services: bool, run_services: bool,
valid_metadata_keys: Optional[Set[str]],
invalid_metadata_keys: Optional[Set[str]],
faketime: Optional[str], faketime: Optional[str],
) -> None: ) -> None:
raise NotImplementedError() raise NotImplementedError()

View File

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

View File

@ -228,7 +228,7 @@ class SaslTestCase(cases.BaseClientTestCase):
self.assertEqual(m.params, ["+"], m) self.assertEqual(m.params, ["+"], m)
@cases.skipUnlessHasMechanism("SCRAM-SHA-256") @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.""" """Test SCRAM-SHA-256 authentication with a bad password."""
auth = authentication.Authentication( auth = authentication.Authentication(
mechanisms=[authentication.Mechanisms.scram_sha_256], mechanisms=[authentication.Mechanisms.scram_sha_256],
@ -261,36 +261,6 @@ class SaslTestCase(cases.BaseClientTestCase):
with self.assertRaises(scram.NotAuthorizedException): with self.assertRaises(scram.NotAuthorizedException):
authenticator.response(msg) 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): class Irc302SaslTestCase(cases.BaseClientTestCase):
@cases.skipUnlessHasMechanism("PLAIN") @cases.skipUnlessHasMechanism("PLAIN")

View File

@ -3,7 +3,12 @@ import shutil
import subprocess import subprocess
from typing import Optional, Set, 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 = """ TEMPLATE_CONFIG = """
global {{ global {{
@ -107,14 +112,21 @@ class BahamutController(BaseServerController, DirectoryBasedController):
password: Optional[str], password: Optional[str],
ssl: bool, ssl: bool,
run_services: 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], 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 assert self.proc is None
self.port = port self.port = port
self.hostname = hostname self.hostname = hostname
self.create_config() self.create_config()
(unused_hostname, unused_port) = self.get_hostname_and_port() (unused_hostname, unused_port) = find_hostname_and_port()
(services_hostname, services_port) = self.get_hostname_and_port() (services_hostname, services_port) = find_hostname_and_port()
password_field = "passwd {};".format(password) if password else "" password_field = "passwd {};".format(password) if password else ""

View File

@ -1,8 +1,13 @@
import shutil import shutil
import subprocess 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 = """ TEMPLATE_SSL_CONFIG = """
ssl_private_key = "{key_path}"; ssl_private_key = "{key_path}";
@ -36,13 +41,19 @@ class BaseHybridController(BaseServerController, DirectoryBasedController):
password: Optional[str], password: Optional[str],
ssl: bool, ssl: bool,
run_services: bool, run_services: bool,
valid_metadata_keys: Optional[Set[str]] = None,
invalid_metadata_keys: Optional[Set[str]] = None,
faketime: Optional[str], 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 assert self.proc is None
self.port = port self.port = port
self.hostname = hostname self.hostname = hostname
self.create_config() 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 "" password_field = 'password = "{}";'.format(password) if password else ""
if ssl: if ssl:
self.gen_ssl() self.gen_ssl()

View File

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

View File

@ -1,5 +1,5 @@
import os import os
from typing import Optional, Tuple, Type from typing import Optional, Set, Tuple, Type
from irctest.basecontrollers import BaseServerController from irctest.basecontrollers import BaseServerController
@ -39,6 +39,9 @@ class ExternalServerController(BaseServerController):
password: Optional[str], password: Optional[str],
ssl: bool, ssl: bool,
run_services: 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], faketime: Optional[str],
) -> None: ) -> None:
pass pass

View File

@ -1,9 +1,14 @@
import functools import functools
import shutil import shutil
import subprocess 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 = """ TEMPLATE_CONFIG = """
# Clients: # Clients:
@ -73,7 +78,6 @@ TEMPLATE_CONFIG = """
<module name="m_muteban"> # for testing mute extbans <module name="m_muteban"> # for testing mute extbans
<module name="namesx"> # For multi-prefix <module name="namesx"> # For multi-prefix
<module name="sasl"> <module name="sasl">
<module name="uhnames"> # For userhost-in-names
# HELP/HELPOP # HELP/HELPOP
<module name="alias"> # for the HELP alias <module name="alias"> # for the HELP alias
@ -121,13 +125,20 @@ class InspircdController(BaseServerController, DirectoryBasedController):
password: Optional[str], password: Optional[str],
ssl: bool, ssl: bool,
run_services: 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, faketime: Optional[str] = None,
) -> None: ) -> None:
if valid_metadata_keys or invalid_metadata_keys:
raise NotImplementedByController(
"Defining valid and invalid METADATA keys."
)
assert self.proc is None assert self.proc is None
self.port = port self.port = port
self.hostname = hostname self.hostname = hostname
self.create_config() 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 "" password_field = 'password="{}"'.format(password) if password else ""

View File

@ -1,6 +1,6 @@
import shutil import shutil
import subprocess import subprocess
from typing import Optional, Type from typing import Optional, Set, Type
from irctest.basecontrollers import ( from irctest.basecontrollers import (
BaseServerController, BaseServerController,
@ -49,8 +49,14 @@ class Irc2Controller(BaseServerController, DirectoryBasedController):
password: Optional[str], password: Optional[str],
ssl: bool, ssl: bool,
run_services: bool, run_services: bool,
valid_metadata_keys: Optional[Set[str]] = None,
invalid_metadata_keys: Optional[Set[str]] = None,
faketime: Optional[str], faketime: Optional[str],
) -> None: ) -> None:
if valid_metadata_keys or invalid_metadata_keys:
raise NotImplementedByController(
"Defining valid and invalid METADATA keys."
)
if ssl: if ssl:
raise NotImplementedByController("TLS") raise NotImplementedByController("TLS")
if run_services: if run_services:

View File

@ -1,6 +1,6 @@
import shutil import shutil
import subprocess import subprocess
from typing import Optional, Type from typing import Optional, Set, Type
from irctest.basecontrollers import ( from irctest.basecontrollers import (
BaseServerController, BaseServerController,
@ -68,8 +68,14 @@ class Ircu2Controller(BaseServerController, DirectoryBasedController):
password: Optional[str], password: Optional[str],
ssl: bool, ssl: bool,
run_services: bool, run_services: bool,
valid_metadata_keys: Optional[Set[str]] = None,
invalid_metadata_keys: Optional[Set[str]] = None,
faketime: Optional[str], faketime: Optional[str],
) -> None: ) -> None:
if valid_metadata_keys or invalid_metadata_keys:
raise NotImplementedByController(
"Defining valid and invalid METADATA keys."
)
if ssl: if ssl:
raise NotImplementedByController("TLS") raise NotImplementedByController("TLS")
if run_services: if run_services:

View File

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

View File

@ -2,7 +2,12 @@ import shutil
import subprocess import subprocess
from typing import Optional, Set, 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 = """ TEMPLATE_CONFIG = """
[Global] [Global]
@ -48,13 +53,20 @@ class NgircdController(BaseServerController, DirectoryBasedController):
password: Optional[str], password: Optional[str],
ssl: bool, ssl: bool,
run_services: 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], 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 assert self.proc is None
self.port = port self.port = port
self.hostname = hostname self.hostname = hostname
self.create_config() 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 "" password_field = "Password = {}".format(password) if password else ""

View File

@ -1,6 +1,6 @@
import shutil import shutil
import subprocess import subprocess
from typing import Optional, Type from typing import Optional, Set, Type
from irctest.basecontrollers import ( from irctest.basecontrollers import (
BaseServerController, BaseServerController,
@ -67,8 +67,14 @@ class SnircdController(BaseServerController, DirectoryBasedController):
password: Optional[str], password: Optional[str],
ssl: bool, ssl: bool,
run_services: bool, run_services: bool,
valid_metadata_keys: Optional[Set[str]] = None,
invalid_metadata_keys: Optional[Set[str]] = None,
faketime: Optional[str], faketime: Optional[str],
) -> None: ) -> None:
if valid_metadata_keys or invalid_metadata_keys:
raise NotImplementedByController(
"Defining valid and invalid METADATA keys."
)
if ssl: if ssl:
raise NotImplementedByController("TLS") raise NotImplementedByController("TLS")
if run_services: 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 shutil
import subprocess import subprocess
import textwrap 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 = """ TEMPLATE_CONFIG = """
include "modules.default.conf"; include "modules.default.conf";
@ -96,7 +101,7 @@ set {{
}} }}
modes-on-join "+H 100:1d"; // Enables CHATHISTORY modes-on-join "+H 100:1d"; // Enables CHATHISTORY
{set_v6only} {set_extras}
}} }}
@ -112,31 +117,13 @@ files {{
}} }}
oper "operuser" {{ oper "operuser" {{
password "operpassword"; password = "operpassword";
mask *; mask *;
class clients; class clients;
operclass netadmin; 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]: def _filelock(path: Path) -> Callable[[], ContextManager]:
"""Alternative to :cls:`multiprocessing.Lock` that works with pytest-xdist""" """Alternative to :cls:`multiprocessing.Lock` that works with pytest-xdist"""
@ -199,8 +186,15 @@ class UnrealircdController(BaseServerController, DirectoryBasedController):
password: Optional[str], password: Optional[str],
ssl: bool, ssl: bool,
run_services: 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], 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 assert self.proc is None
self.port = port self.port = port
self.hostname = hostname self.hostname = hostname
@ -213,54 +207,64 @@ class UnrealircdController(BaseServerController, DirectoryBasedController):
loadmodule "cloak_md5"; 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: else:
extras = "" extras = ""
set_v6only = "" set_extras = ""
with self.open_file("empty.txt") as fd: with self.open_file("empty.txt") as fd:
fd.write("\n") fd.write("\n")
password_field = 'password "{}";'.format(password) if password else "" 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(): 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( self.proc = subprocess.Popen(
[ [
*faketime_cmd, *faketime_cmd,

View File

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

View File

@ -18,7 +18,7 @@ class Artifact:
download_url: str download_url: str
@property @property
def public_download_url(self) -> str: def public_download_url(self):
# GitHub API is not available publicly for artifacts, we need to use # GitHub API is not available publicly for artifacts, we need to use
# a third-party proxy to access it... # a third-party proxy to access it...
name = urllib.parse.quote(self.name) 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 # Set to not-None if we find a Keys() operator in the dict keys
remaining_keys_wildcard = None 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): if isinstance(expected_key, RemainingKeys):
remaining_keys_wildcard = (expected_key.key, expected_value) remaining_keys_wildcard = (expected_key.key, expected_value)
else: else:
@ -168,7 +168,7 @@ def match_dict(
if remaining_keys_wildcard: if remaining_keys_wildcard:
(expected_key, expected_value) = 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): if not match_string(key, expected_key):
return False return False
if not match_string(value, expected_value): if not match_string(value, expected_value):

View File

@ -4,32 +4,11 @@
""" """
from irctest import cases from irctest import cases
from irctest.patma import ANYSTR, StrRe from irctest.patma import ANYSTR
from irctest.runner import CapabilityNotSupported, ImplementationChoice from irctest.runner import CapabilityNotSupported, ImplementationChoice
class CapTestCase(cases.BaseServerTestCase): 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") @cases.mark_specifications("IRCv3")
def testNoReq(self): def testNoReq(self):
"""Test the server handles gracefully clients which do not send """Test the server handles gracefully clients which do not send
@ -44,206 +23,12 @@ class CapTestCase(cases.BaseServerTestCase):
self.getCapLs(1) self.getCapLs(1)
self.sendLine(1, "USER foo foo foo :foo") self.sendLine(1, "USER foo foo foo :foo")
self.sendLine(1, "NICK 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") self.sendLine(1, "CAP END")
m = self.getRegistrationMessage(1) m = self.getRegistrationMessage(1)
self.assertMessageMatch( self.assertMessageMatch(
m, command="001", fail_msg="Expected 001 after sending CAP END, got {msg}." 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") @cases.mark_specifications("IRCv3")
def testReqUnavailable(self): def testReqUnavailable(self):
"""Test the server handles gracefully clients which request """Test the server handles gracefully clients which request
@ -260,7 +45,7 @@ class CapTestCase(cases.BaseServerTestCase):
self.assertMessageMatch( self.assertMessageMatch(
m, m,
command="CAP", command="CAP",
params=[ANYSTR, "NAK", StrRe("foo ?")], params=[ANYSTR, "NAK", "foo"],
fail_msg="Expected CAP NAK after requesting non-existing " fail_msg="Expected CAP NAK after requesting non-existing "
"capability, got {msg}.", "capability, got {msg}.",
) )
@ -293,6 +78,10 @@ class CapTestCase(cases.BaseServerTestCase):
) )
@cases.mark_specifications("IRCv3") @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): def testNakWhole(self):
"""“The capability identifier set must be accepted as a whole, or """“The capability identifier set must be accepted as a whole, or
rejected entirely.” rejected entirely.”
@ -334,12 +123,16 @@ class CapTestCase(cases.BaseServerTestCase):
self.assertMessageMatch( self.assertMessageMatch(
m, m,
command="CAP", command="CAP",
params=[ANYSTR, "ACK", StrRe("multi-prefix ?")], params=[ANYSTR, "ACK", "multi-prefix"],
fail_msg="Expected “CAP ACK :multi-prefix” after " fail_msg="Expected “CAP ACK :multi-prefix” after "
"sending “CAP REQ :multi-prefix”, but got {msg}.", "sending “CAP REQ :multi-prefix”, but got {msg}.",
) )
@cases.mark_specifications("IRCv3") @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): def testCapRemovalByClient(self):
"""Test CAP LIST and removal of caps via CAP REQ :-tagname.""" """Test CAP LIST and removal of caps via CAP REQ :-tagname."""
cap1 = "echo-message" cap1 = "echo-message"
@ -347,13 +140,8 @@ class CapTestCase(cases.BaseServerTestCase):
self.addClient(1) self.addClient(1)
self.connectClient("sender") self.connectClient("sender")
self.sendLine(1, "CAP LS 302") self.sendLine(1, "CAP LS 302")
caps = set() m = self.getRegistrationMessage(1)
while True: if not ({cap1, cap2} <= set(m.params[2].split())):
m = self.getRegistrationMessage(1)
caps.update(m.params[-1].split())
if m.params[2] != "*":
break
if not ({cap1, cap2} <= caps):
raise CapabilityNotSupported(f"{cap1} or {cap2}") raise CapabilityNotSupported(f"{cap1} or {cap2}")
self.sendLine(1, f"CAP REQ :{cap1} {cap2}") self.sendLine(1, f"CAP REQ :{cap1} {cap2}")
self.sendLine(1, "nick bar") self.sendLine(1, "nick bar")
@ -379,19 +167,17 @@ class CapTestCase(cases.BaseServerTestCase):
m = self.getMessage(1) m = self.getMessage(1)
self.assertIn("time", m.tags, m) self.assertIn("time", m.tags, m)
# remove the multi-prefix cap # remove the server-time cap
self.sendLine(1, f"CAP REQ :-{cap2}") self.sendLine(1, f"CAP REQ :-{cap2}")
m = self.getMessage(1) m = self.getMessage(1)
# Must be either ACK or NAK # Must be either ACK or NAK
if self.messageDiffers( if self.messageDiffers(m, command="CAP", params=[ANYSTR, "ACK", f"-{cap2}"]):
m, command="CAP", params=[ANYSTR, "ACK", StrRe(f"-{cap2} ?")]
):
self.assertMessageMatch( 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}") raise ImplementationChoice(f"Does not support CAP REQ -{cap2}")
# multi-prefix should be disabled # server-time should be disabled
self.sendLine(1, "CAP LIST") self.sendLine(1, "CAP LIST")
messages = self.getMessages(1) messages = self.getMessages(1)
cap_list = [m for m in messages if m.command == "CAP"][0] 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 " fail_msg="Sending “CAP LIST” as first message got a reply "
"that is not “CAP * LIST :”: {msg}", "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 import cases, runner
from irctest.irc_utils.junkdrawer import random_name from irctest.irc_utils.junkdrawer import random_name
from irctest.patma import ANYSTR, StrRe from irctest.patma import ANYSTR
CHATHISTORY_CAP = "draft/chathistory" CHATHISTORY_CAP = "draft/chathistory"
EVENT_PLAYBACK_CAP = "draft/event-playback" EVENT_PLAYBACK_CAP = "draft/event-playback"
@ -21,6 +21,28 @@ SUBCOMMANDS = ["LATEST", "BEFORE", "AFTER", "BETWEEN", "AROUND"]
MYSQL_PASSWORD = "" 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): def skip_ngircd(f):
@functools.wraps(f) @functools.wraps(f)
def newf(self, *args, **kwargs): def newf(self, *args, **kwargs):
@ -34,26 +56,6 @@ def skip_ngircd(f):
@cases.mark_specifications("IRCv3") @cases.mark_specifications("IRCv3")
@cases.mark_services @cases.mark_services
class ChathistoryTestCase(cases.BaseServerTestCase): 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 @staticmethod
def config() -> cases.TestCaseControllerConfig: def config() -> cases.TestCaseControllerConfig:
return cases.TestCaseControllerConfig(chathistory=True) return cases.TestCaseControllerConfig(chathistory=True)
@ -306,9 +308,6 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
) )
time.sleep(0.002) time.sleep(0.002)
self.getMessages(1)
self.getMessages(2)
self.validate_echo_messages(NUM_MESSAGES, echo_messages) self.validate_echo_messages(NUM_MESSAGES, echo_messages)
self.validate_chathistory(subcommand, echo_messages, 1, c2) self.validate_chathistory(subcommand, echo_messages, 1, c2)
self.validate_chathistory(subcommand, echo_messages, 2, c1) 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): def _validate_chathistory_LATEST(self, echo_messages, user, chname):
INCLUSIVE_LIMIT = len(echo_messages) * 2 INCLUSIVE_LIMIT = len(echo_messages) * 2
self.sendLine(user, "CHATHISTORY LATEST %s * %d" % (chname, INCLUSIVE_LIMIT)) 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.assertEqual(echo_messages, result)
self.sendLine(user, "CHATHISTORY LATEST %s * %d" % (chname, 5)) 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.assertEqual(echo_messages[-5:], result)
self.sendLine(user, "CHATHISTORY LATEST %s * %d" % (chname, 1)) 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.assertEqual(echo_messages[-1:], result)
self.sendLine( self.sendLine(
@ -418,7 +417,7 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
"CHATHISTORY LATEST %s msgid=%s %d" "CHATHISTORY LATEST %s msgid=%s %d"
% (chname, echo_messages[4].msgid, INCLUSIVE_LIMIT), % (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.assertEqual(echo_messages[5:], result)
self.sendLine( self.sendLine(
@ -426,7 +425,7 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
"CHATHISTORY LATEST %s timestamp=%s %d" "CHATHISTORY LATEST %s timestamp=%s %d"
% (chname, echo_messages[4].time, INCLUSIVE_LIMIT), % (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) self.assertEqual(echo_messages[5:], result)
def _validate_chathistory_BEFORE(self, echo_messages, user, chname): def _validate_chathistory_BEFORE(self, echo_messages, user, chname):
@ -436,7 +435,7 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
"CHATHISTORY BEFORE %s msgid=%s %d" "CHATHISTORY BEFORE %s msgid=%s %d"
% (chname, echo_messages[6].msgid, INCLUSIVE_LIMIT), % (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.assertEqual(echo_messages[:6], result)
self.sendLine( self.sendLine(
@ -444,7 +443,7 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
"CHATHISTORY BEFORE %s timestamp=%s %d" "CHATHISTORY BEFORE %s timestamp=%s %d"
% (chname, echo_messages[6].time, INCLUSIVE_LIMIT), % (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.assertEqual(echo_messages[:6], result)
self.sendLine( self.sendLine(
@ -452,7 +451,7 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
"CHATHISTORY BEFORE %s timestamp=%s %d" "CHATHISTORY BEFORE %s timestamp=%s %d"
% (chname, echo_messages[6].time, 2), % (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) self.assertEqual(echo_messages[4:6], result)
def _validate_chathistory_AFTER(self, echo_messages, user, chname): def _validate_chathistory_AFTER(self, echo_messages, user, chname):
@ -462,7 +461,7 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
"CHATHISTORY AFTER %s msgid=%s %d" "CHATHISTORY AFTER %s msgid=%s %d"
% (chname, echo_messages[3].msgid, INCLUSIVE_LIMIT), % (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.assertEqual(echo_messages[4:], result)
self.sendLine( self.sendLine(
@ -470,14 +469,14 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
"CHATHISTORY AFTER %s timestamp=%s %d" "CHATHISTORY AFTER %s timestamp=%s %d"
% (chname, echo_messages[3].time, INCLUSIVE_LIMIT), % (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.assertEqual(echo_messages[4:], result)
self.sendLine( self.sendLine(
user, user,
"CHATHISTORY AFTER %s timestamp=%s %d" % (chname, echo_messages[3].time, 3), "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) self.assertEqual(echo_messages[4:7], result)
def _validate_chathistory_BETWEEN(self, echo_messages, user, chname): def _validate_chathistory_BETWEEN(self, echo_messages, user, chname):
@ -493,7 +492,7 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
INCLUSIVE_LIMIT, 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.assertEqual(echo_messages[1:-1], result)
self.sendLine( self.sendLine(
@ -506,7 +505,7 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
INCLUSIVE_LIMIT, 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.assertEqual(echo_messages[1:-1], result)
# BETWEEN forwards and backwards with a limit, should get # 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" "CHATHISTORY BETWEEN %s msgid=%s msgid=%s %d"
% (chname, echo_messages[0].msgid, echo_messages[-1].msgid, 3), % (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.assertEqual(echo_messages[1:4], result)
self.sendLine( self.sendLine(
@ -524,7 +523,7 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
"CHATHISTORY BETWEEN %s msgid=%s msgid=%s %d" "CHATHISTORY BETWEEN %s msgid=%s msgid=%s %d"
% (chname, echo_messages[-1].msgid, echo_messages[0].msgid, 3), % (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) self.assertEqual(echo_messages[-4:-1], result)
# same stuff again but with timestamps # same stuff again but with timestamps
@ -533,28 +532,28 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
"CHATHISTORY BETWEEN %s timestamp=%s timestamp=%s %d" "CHATHISTORY BETWEEN %s timestamp=%s timestamp=%s %d"
% (chname, echo_messages[0].time, echo_messages[-1].time, INCLUSIVE_LIMIT), % (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.assertEqual(echo_messages[1:-1], result)
self.sendLine( self.sendLine(
user, user,
"CHATHISTORY BETWEEN %s timestamp=%s timestamp=%s %d" "CHATHISTORY BETWEEN %s timestamp=%s timestamp=%s %d"
% (chname, echo_messages[-1].time, echo_messages[0].time, INCLUSIVE_LIMIT), % (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.assertEqual(echo_messages[1:-1], result)
self.sendLine( self.sendLine(
user, user,
"CHATHISTORY BETWEEN %s timestamp=%s timestamp=%s %d" "CHATHISTORY BETWEEN %s timestamp=%s timestamp=%s %d"
% (chname, echo_messages[0].time, echo_messages[-1].time, 3), % (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.assertEqual(echo_messages[1:4], result)
self.sendLine( self.sendLine(
user, user,
"CHATHISTORY BETWEEN %s timestamp=%s timestamp=%s %d" "CHATHISTORY BETWEEN %s timestamp=%s timestamp=%s %d"
% (chname, echo_messages[-1].time, echo_messages[0].time, 3), % (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) self.assertEqual(echo_messages[-4:-1], result)
def _validate_chathistory_AROUND(self, echo_messages, user, chname): def _validate_chathistory_AROUND(self, echo_messages, user, chname):
@ -562,14 +561,14 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
user, user,
"CHATHISTORY AROUND %s msgid=%s %d" % (chname, echo_messages[7].msgid, 1), "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.assertEqual([echo_messages[7]], result)
self.sendLine( self.sendLine(
user, user,
"CHATHISTORY AROUND %s msgid=%s %d" % (chname, echo_messages[7].msgid, 3), "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.assertEqual(echo_messages[6:9], result)
self.sendLine( self.sendLine(
@ -577,7 +576,7 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
"CHATHISTORY AROUND %s timestamp=%s %d" "CHATHISTORY AROUND %s timestamp=%s %d"
% (chname, echo_messages[7].time, 3), % (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) self.assertIn(echo_messages[7], result)
@pytest.mark.arbitrary_client_tags @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") @cases.mark_capabilities("echo-message")
def testEchoMessage(self, command, solo, server_time): def testEchoMessage(self, command, solo, server_time):
"""<http://ircv3.net/specs/extensions/echo-message-3.2.html>""" """<http://ircv3.net/specs/extensions/echo-message-3.2.html>"""
capabilities = ["server-time"] if server_time else [] if server_time:
self.connectClient(
self.connectClient( "baz",
"baz", capabilities=["echo-message", "server-time"],
capabilities=["echo-message", *capabilities], skip_if_cap_nak=True,
skip_if_cap_nak=True, )
) else:
self.connectClient(
"baz",
capabilities=["echo-message", "server-time"],
skip_if_cap_nak=True,
)
self.sendLine(1, "JOIN #chan") self.sendLine(1, "JOIN #chan")
if not solo: if not solo:
capabilities = ["server-time"] if server_time else None
self.connectClient("qux", capabilities=capabilities) self.connectClient("qux", capabilities=capabilities)
self.sendLine(2, "JOIN #chan") self.sendLine(2, "JOIN #chan")

View File

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

View File

@ -12,7 +12,6 @@ import pytest
from irctest import cases from irctest import cases
from irctest.numerics import ERR_UNKNOWNCOMMAND from irctest.numerics import ERR_UNKNOWNCOMMAND
from irctest.patma import ANYDICT, ANYOPTSTR, NotStrRe, RemainingKeys, StrRe from irctest.patma import ANYDICT, ANYOPTSTR, NotStrRe, RemainingKeys, StrRe
from irctest.runner import OptionalExtensionNotSupported
class LabeledResponsesTestCase(cases.BaseServerTestCase): class LabeledResponsesTestCase(cases.BaseServerTestCase):
@ -23,10 +22,7 @@ class LabeledResponsesTestCase(cases.BaseServerTestCase):
capabilities=["echo-message", "batch", "labeled-response"], capabilities=["echo-message", "batch", "labeled-response"],
skip_if_cap_nak=True, 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.getMessages(1)
self.connectClient( self.connectClient(
"bar", "bar",
capabilities=["echo-message", "batch", "labeled-response"], capabilities=["echo-message", "batch", "labeled-response"],

View File

@ -4,7 +4,6 @@ The PRIVMSG and NOTICE commands.
from irctest import cases from irctest import cases
from irctest.numerics import ERR_INPUTTOOLONG from irctest.numerics import ERR_INPUTTOOLONG
from irctest.patma import ANYSTR
class PrivmsgTestCase(cases.BaseServerTestCase): class PrivmsgTestCase(cases.BaseServerTestCase):
@ -46,12 +45,12 @@ class PrivmsgTestCase(cases.BaseServerTestCase):
@cases.mark_specifications("RFC1459", "RFC2812") @cases.mark_specifications("RFC1459", "RFC2812")
def testPrivmsgNonexistentUser(self): 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.connectClient("foo")
self.sendLine(1, "PRIVMSG bar :hey there!") self.sendLine(1, "PRIVMSG bar :hey there!")
msg = self.getMessage(1) msg = self.getMessage(1)
# ERR_NOSUCHNICK: 401 <sender> <recipient> :No such nick # ERR_NOSUCHNICK
self.assertMessageMatch(msg, command="401", params=["foo", "bar", ANYSTR]) self.assertIn(msg.command, ("401"))
class NoticeTestCase(cases.BaseServerTestCase): class NoticeTestCase(cases.BaseServerTestCase):
@ -101,13 +100,8 @@ class NoticeTestCase(cases.BaseServerTestCase):
class TagsTestCase(cases.BaseServerTestCase): class TagsTestCase(cases.BaseServerTestCase):
@cases.mark_capabilities("message-tags") @cases.mark_capabilities("message-tags")
@cases.xfailIf( @cases.xfailIfSoftware(
lambda self: bool( ["UnrealIRCd"], "https://bugs.unrealircd.org/view.php?id=5947"
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",
) )
def testLineTooLong(self): def testLineTooLong(self):
self.connectClient("bar", capabilities=["message-tags"], skip_if_cap_nak=True) 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): class MetadataTestCase(cases.BaseServerTestCase):
valid_metadata_keys = {"display-name", "avatar"} valid_metadata_keys = {"valid_key1", "valid_key2"}
invalid_metadata_keys = {"indisplay-name", "inavatar"} invalid_metadata_keys = {"invalid_key1", "invalid_key2"}
@cases.mark_specifications("IRCv3", deprecated=True) @cases.mark_specifications("IRCv3", deprecated=True)
def testInIsupport(self): def testInIsupport(self):
@ -36,7 +36,7 @@ class MetadataTestCase(cases.BaseServerTestCase):
def testGetOneUnsetValid(self): def testGetOneUnsetValid(self):
"""<http://ircv3.net/specs/core/metadata-3.2.html#metadata-get>""" """<http://ircv3.net/specs/core/metadata-3.2.html#metadata-get>"""
self.connectClient("foo") self.connectClient("foo")
self.sendLine(1, "METADATA * GET display-name") self.sendLine(1, "METADATA * GET valid_key1")
m = self.getMessage(1) m = self.getMessage(1)
self.assertMessageMatch( self.assertMessageMatch(
m, m,
@ -52,7 +52,7 @@ class MetadataTestCase(cases.BaseServerTestCase):
-- <http://ircv3.net/specs/core/metadata-3.2.html#metadata-get> -- <http://ircv3.net/specs/core/metadata-3.2.html#metadata-get>
""" """
self.connectClient("foo") self.connectClient("foo")
self.sendLine(1, "METADATA * GET display-name avatar") self.sendLine(1, "METADATA * GET valid_key1 valid_key2")
m = self.getMessage(1) m = self.getMessage(1)
self.assertMessageMatch( self.assertMessageMatch(
m, m,
@ -62,10 +62,10 @@ class MetadataTestCase(cases.BaseServerTestCase):
) )
self.assertEqual( self.assertEqual(
m.params[1], m.params[1],
"display-name", "valid_key1",
m, m,
fail_msg="Response to “METADATA * GET display-name avatar" fail_msg="Response to “METADATA * GET valid_key1 valid_key2"
"did not respond to display-name first: {msg}", "did not respond to valid_key1 first: {msg}",
) )
m = self.getMessage(1) m = self.getMessage(1)
self.assertMessageMatch( self.assertMessageMatch(
@ -76,10 +76,10 @@ class MetadataTestCase(cases.BaseServerTestCase):
) )
self.assertEqual( self.assertEqual(
m.params[1], m.params[1],
"avatar", "valid_key2",
m, m,
fail_msg="Response to “METADATA * GET display-name avatar" fail_msg="Response to “METADATA * GET valid_key1 valid_key2"
"did not respond to avatar as second response: {msg}", "did not respond to valid_key2 as second response: {msg}",
) )
@cases.mark_specifications("IRCv3", deprecated=True) @cases.mark_specifications("IRCv3", deprecated=True)
@ -135,7 +135,7 @@ class MetadataTestCase(cases.BaseServerTestCase):
) )
self.assertEqual( self.assertEqual(
m.params[1], m.params[1],
"display-name", "valid_key1",
m, m,
fail_msg="Second param of 761 after setting “{expects}” to " fail_msg="Second param of 761 after setting “{expects}” to "
"{}” is not “{expects}”: {msg}.", "{}” is not “{expects}”: {msg}.",
@ -190,7 +190,7 @@ class MetadataTestCase(cases.BaseServerTestCase):
def testSetGetValid(self): def testSetGetValid(self):
"""<http://ircv3.net/specs/core/metadata-3.2.html>""" """<http://ircv3.net/specs/core/metadata-3.2.html>"""
self.connectClient("foo") self.connectClient("foo")
self.assertSetGetValue("*", "display-name", "myvalue") self.assertSetGetValue("*", "valid_key1", "myvalue")
@cases.mark_specifications("IRCv3", deprecated=True) @cases.mark_specifications("IRCv3", deprecated=True)
def testSetGetZeroCharInValue(self): def testSetGetZeroCharInValue(self):
@ -198,7 +198,7 @@ class MetadataTestCase(cases.BaseServerTestCase):
-- <http://ircv3.net/specs/core/metadata-3.2.html#metadata-restrictions> -- <http://ircv3.net/specs/core/metadata-3.2.html#metadata-restrictions>
""" """
self.connectClient("foo") 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) @cases.mark_specifications("IRCv3", deprecated=True)
def testSetGetHeartInValue(self): def testSetGetHeartInValue(self):
@ -209,7 +209,7 @@ class MetadataTestCase(cases.BaseServerTestCase):
self.connectClient("foo") self.connectClient("foo")
self.assertSetGetValue( self.assertSetGetValue(
"*", "*",
"display-name", "valid_key1",
"->{}<-".format(heart), "->{}<-".format(heart),
"zero->{}<-zero".format(heart.encode()), "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 # Sending directly because it is not valid UTF-8 so Python would
# not like it # not like it
self.clients[1].conn.sendall( 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)} commands = {m.command for m in self.getMessages(1)}
self.assertNotIn( self.assertNotIn(
@ -233,7 +233,7 @@ class MetadataTestCase(cases.BaseServerTestCase):
"UTF-8 was answered with 761 (RPL_KEYVALUE)", "UTF-8 was answered with 761 (RPL_KEYVALUE)",
) )
self.clients[1].conn.sendall( 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)} commands = {m.command for m in self.getMessages(1)}
self.assertNotIn( self.assertNotIn(

View File

@ -249,23 +249,6 @@ class MonitorTestCase(_BaseMonitorTestCase):
extra_format=(messages,), 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_specifications("IRCv3")
@cases.mark_isupport("MONITOR") @cases.mark_isupport("MONITOR")
def testMonitorList(self): def testMonitorList(self):
@ -301,35 +284,6 @@ class MonitorTestCase(_BaseMonitorTestCase):
self.sendLine(1, "MONITOR L") self.sendLine(1, "MONITOR L")
checkMonitorSubjects(self.getMessages(1), "bar", {"bazbat"}) 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_specifications("IRCv3")
@cases.mark_isupport("MONITOR") @cases.mark_isupport("MONITOR")
def testNickChange(self): def testNickChange(self):
@ -373,7 +327,7 @@ class _BaseExtendedMonitorTestCase(_BaseMonitorTestCase):
"""Tests https://ircv3.net/specs/extensions/extended-monitor.html""" """Tests https://ircv3.net/specs/extensions/extended-monitor.html"""
self.connectClient( self.connectClient(
"foo", "foo",
capabilities=["extended-monitor", *watcher_caps], capabilities=["draft/extended-monitor", *watcher_caps],
skip_if_cap_nak=True, 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. 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.joinChannel(1, "#chan")
self.sendLine(1, "MODE #chan +v foo") self.sendLine(1, "MODE #chan +v foo")
self.getMessages(1) self.getMessages(1)

View File

@ -10,7 +10,6 @@ TODO: cross-reference Modern
import time import time
from irctest import cases from irctest import cases
from irctest.numerics import RPL_NAMREPLY
class PartTestCase(cases.BaseServerTestCase): class PartTestCase(cases.BaseServerTestCase):
@ -85,12 +84,6 @@ class PartTestCase(cases.BaseServerTestCase):
self.getMessages(1) self.getMessages(1)
self.getMessages(2) 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") self.sendLine(1, "PART #chan")
# both the PART'ing client and the other channel member should receive # both the PART'ing client and the other channel member should receive
# a PART line: # a PART line:
@ -99,21 +92,6 @@ class PartTestCase(cases.BaseServerTestCase):
m = self.getMessage(2) m = self.getMessage(2)
self.assertMessageMatch(m, command="PART") 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") @cases.mark_specifications("RFC2812")
def testBasicPartRfc2812(self): def testBasicPartRfc2812(self):
""" """

View File

@ -1,4 +1,5 @@
import base64 import base64
from typing import List
from irctest import cases, runner, scram from irctest import cases, runner, scram
from irctest.numerics import ERR_SASLFAIL from irctest.numerics import ERR_SASLFAIL
@ -11,8 +12,34 @@ class RegistrationTestCase(cases.BaseServerTestCase):
self.controller.registerUser(self, "testuser", "mypassword") self.controller.registerUser(self, "testuser", "mypassword")
@cases.mark_services class _BaseSasl(cases.BaseServerTestCase):
class SaslTestCase(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.mark_specifications("IRCv3")
@cases.skipUnlessHasMechanism("PLAIN") @cases.skipUnlessHasMechanism("PLAIN")
def testPlain(self): def testPlain(self):
@ -34,17 +61,8 @@ class SaslTestCase(cases.BaseServerTestCase):
capabilities["sasl"], capabilities["sasl"],
fail_msg="Does not have PLAIN mechanism as the controller " "claims", fail_msg="Does not have PLAIN 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 PLAIN") self._doInitialExchange(1, "PLAIN", "amlsbGVzAGppbGxlcwBzZXNhbWU=")
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=")
m = self.getRegistrationMessage(1) m = self.getRegistrationMessage(1)
self.assertMessageMatch( self.assertMessageMatch(
m, m,
@ -62,17 +80,8 @@ class SaslTestCase(cases.BaseServerTestCase):
).decode() ).decode()
self.controller.registerUser(self, "foo", password) self.controller.registerUser(self, "foo", password)
self.addClient() self.addClient()
self.requestCapabilities(1, ["sasl"], skip_if_cap_nak=False) self.requestCapabilities(1, self.capabilities, skip_if_cap_nak=False)
self.sendLine(1, "AUTHENTICATE PLAIN") self._doInitialExchange(1, "PLAIN", authstring)
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)
m = self.getRegistrationMessage(1) m = self.getRegistrationMessage(1)
self.assertMessageMatch( self.assertMessageMatch(
m, m,
@ -122,17 +131,8 @@ class SaslTestCase(cases.BaseServerTestCase):
capabilities["sasl"], capabilities["sasl"],
fail_msg="Does not have PLAIN mechanism as the controller " "claims", fail_msg="Does not have PLAIN 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 PLAIN") self._doInitialExchange(1, "PLAIN", "AGppbGxlcwBzZXNhbWU=")
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=")
m = self.getRegistrationMessage(1) m = self.getRegistrationMessage(1)
self.assertMessageMatch( self.assertMessageMatch(
m, m,
@ -158,8 +158,11 @@ class SaslTestCase(cases.BaseServerTestCase):
capabilities, capabilities,
fail_msg="Does not have SASL as the controller claims.", fail_msg="Does not have SASL 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 FOO") if self.sasl_ir:
self.sendLine(1, "AUTHENTICATE FOO AGppbGxlcwBzZXNhbWU=")
else:
self.sendLine(1, "AUTHENTICATE FOO")
m = self.getRegistrationMessage(1) m = self.getRegistrationMessage(1)
while m.command == "908": # RPL_SASLMECHS while m.command == "908": # RPL_SASLMECHS
m = self.getRegistrationMessage(1) m = self.getRegistrationMessage(1)
@ -209,17 +212,8 @@ class SaslTestCase(cases.BaseServerTestCase):
capabilities["sasl"], capabilities["sasl"],
fail_msg="Does not have PLAIN mechanism as the controller " "claims", fail_msg="Does not have PLAIN 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 PLAIN") self._doInitialExchange(1, "PLAIN", authstring[0:400])
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.sendLine(1, "AUTHENTICATE {}".format(authstring[400:])) self.sendLine(1, "AUTHENTICATE {}".format(authstring[400:]))
self.confirmSuccessfulAuth() self.confirmSuccessfulAuth()
@ -279,17 +273,8 @@ class SaslTestCase(cases.BaseServerTestCase):
capabilities["sasl"], capabilities["sasl"],
fail_msg="Does not have PLAIN mechanism as the controller " "claims", fail_msg="Does not have PLAIN 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 PLAIN") self._doInitialExchange(1, "PLAIN", authstring)
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.sendLine(1, "AUTHENTICATE +") self.sendLine(1, "AUTHENTICATE +")
self.confirmSuccessfulAuth() self.confirmSuccessfulAuth()
@ -298,6 +283,12 @@ class SaslTestCase(cases.BaseServerTestCase):
# I don't know how to do it, because it would make the registration # I don't know how to do it, because it would make the registration
# message's length too big for it to be valid. # 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.mark_specifications("IRCv3")
@cases.skipUnlessHasMechanism("SCRAM-SHA-256") @cases.skipUnlessHasMechanism("SCRAM-SHA-256")
def testScramSha256Success(self): def testScramSha256Success(self):
@ -318,7 +309,7 @@ class SaslTestCase(cases.BaseServerTestCase):
fail_msg="Does not have SCRAM-SHA-256 mechanism as the " fail_msg="Does not have SCRAM-SHA-256 mechanism as the "
"controller claims", "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") self.sendLine(1, "AUTHENTICATE SCRAM-SHA-256")
m = self.getRegistrationMessage(1) m = self.getRegistrationMessage(1)
@ -374,7 +365,7 @@ class SaslTestCase(cases.BaseServerTestCase):
fail_msg="Does not have SCRAM-SHA-256 mechanism as the " fail_msg="Does not have SCRAM-SHA-256 mechanism as the "
"controller claims", "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") self.sendLine(1, "AUTHENTICATE SCRAM-SHA-256")
m = self.getRegistrationMessage(1) m = self.getRegistrationMessage(1)
@ -404,3 +395,36 @@ class SaslTestCase(cases.BaseServerTestCase):
) )
m = self.getRegistrationMessage(1) m = self.getRegistrationMessage(1)
self.assertMessageMatch(m, command=ERR_SASLFAIL) 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) m = self.getMessage(2)
self.assertMessageMatch(m, command="TOPIC", params=["#chan", "T0P1C"]) 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") @cases.mark_specifications("RFC1459", "RFC2812")
def testTopicMode(self): def testTopicMode(self):
"""“Once a user has joined a channel, he receives information about """“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], 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_specifications("IRCv3")
@cases.mark_isupport("WHOX") @cases.mark_isupport("WHOX")
def testWhoxFull(self): def testWhoxFull(self):

View File

@ -201,6 +201,10 @@ class WhowasTestCase(cases.BaseServerTestCase):
) )
@cases.mark_specifications("RFC1459", "RFC2812", "Modern") @cases.mark_specifications("RFC1459", "RFC2812", "Modern")
@cases.xfailIfSoftware(
["InspIRCd"],
"Feature not released yet: https://github.com/inspircd/inspircd/pull/1967",
)
def testWhowasMultiple(self): def testWhowasMultiple(self):
""" """
"The history is searched backward, returning the most recent entry first." "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") self._testWhowasMultiple(second_result=True, whowas_command="WHOWAS nick2")
@cases.mark_specifications("RFC1459", "RFC2812", "Modern") @cases.mark_specifications("RFC1459", "RFC2812", "Modern")
@cases.xfailIfSoftware(
["InspIRCd"],
"Feature not released yet: https://github.com/inspircd/inspircd/pull/1968",
)
def testWhowasCount1(self): def testWhowasCount1(self):
""" """
"If there are multiple entries, up to <count> replies will be returned" "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") self._testWhowasMultiple(second_result=False, whowas_command="WHOWAS nick2 1")
@cases.mark_specifications("RFC1459", "RFC2812", "Modern") @cases.mark_specifications("RFC1459", "RFC2812", "Modern")
@cases.xfailIfSoftware(
["InspIRCd"],
"Feature not released yet: https://github.com/inspircd/inspircd/pull/1968",
)
def testWhowasCount2(self): def testWhowasCount2(self):
""" """
"If there are multiple entries, up to <count> replies will be returned" "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") self._testWhowasMultiple(second_result=True, whowas_command="WHOWAS nick2 2")
@cases.mark_specifications("RFC1459", "RFC2812", "Modern") @cases.mark_specifications("RFC1459", "RFC2812", "Modern")
@cases.xfailIfSoftware(
["InspIRCd"],
"Feature not released yet: https://github.com/inspircd/inspircd/pull/1968",
)
def testWhowasCountNegative(self): def testWhowasCountNegative(self):
""" """
"If a non-positive number is passed as being <count>, then a full search "If a non-positive number is passed as being <count>, then a full search
@ -248,6 +264,10 @@ class WhowasTestCase(cases.BaseServerTestCase):
@cases.xfailIfSoftware( @cases.xfailIfSoftware(
["ircu2"], "Fix not released yet: https://github.com/UndernetIRC/ircu2/pull/19" ["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): def testWhowasCountZero(self):
""" """
"If a non-positive number is passed as being <count>, then a full search "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 = [ install_steps = [
{ {
"name": f"Checkout {name}", "name": f"Checkout {name}",
"uses": "actions/checkout@v3", "uses": "actions/checkout@v2",
"with": { "with": {
"repository": software_config["repository"], "repository": software_config["repository"],
"ref": ref, "ref": ref,
@ -94,7 +94,7 @@ def get_build_job(*, software_config, software_id, version_flavor):
cache = [ cache = [
{ {
"name": "Cache dependencies", "name": "Cache dependencies",
"uses": "actions/cache@v3", "uses": "actions/cache@v2",
"with": { "with": {
"path": f"~/.cache\n${{ github.workspace }}/{path}\n", "path": f"~/.cache\n${{ github.workspace }}/{path}\n",
"key": "3-${{ runner.os }}-" "key": "3-${{ runner.os }}-"
@ -116,18 +116,18 @@ def get_build_job(*, software_config, software_id, version_flavor):
return None return None
return { return {
"runs-on": "ubuntu-22.04", "runs-on": "ubuntu-20.04",
"steps": [ "steps": [
{ {
"name": "Create directories", "name": "Create directories",
"run": "cd ~/; mkdir -p .local/ go/", "run": "cd ~/; mkdir -p .local/ go/",
}, },
*cache, *cache,
{"uses": "actions/checkout@v3"}, {"uses": "actions/checkout@v2"},
{ {
"name": "Set up Python 3.11", "name": "Set up Python 3.7",
"uses": "actions/setup-python@v4", "uses": "actions/setup-python@v2",
"with": {"python-version": 3.11}, "with": {"python-version": 3.7},
}, },
*install_steps, *install_steps,
*upload_steps(software_id), *upload_steps(software_id),
@ -159,7 +159,7 @@ def get_test_job(*, config, test_config, test_id, version_flavor, jobs):
downloads.append( downloads.append(
{ {
"name": "Download build artefacts", "name": "Download build artefacts",
"uses": "actions/download-artifact@v3", "uses": "actions/download-artifact@v2",
"with": {"name": f"installed-{software_id}", "path": "~"}, "with": {"name": f"installed-{software_id}", "path": "~"},
} }
) )
@ -191,14 +191,14 @@ def get_test_job(*, config, test_config, test_id, version_flavor, jobs):
unpack = [] unpack = []
return { return {
"runs-on": "ubuntu-22.04", "runs-on": "ubuntu-20.04",
"needs": needs, "needs": needs,
"steps": [ "steps": [
{"uses": "actions/checkout@v3"}, {"uses": "actions/checkout@v2"},
{ {
"name": "Set up Python 3.11", "name": "Set up Python 3.7",
"uses": "actions/setup-python@v4", "uses": "actions/setup-python@v2",
"with": {"python-version": 3.11}, "with": {"python-version": 3.7},
}, },
*downloads, *downloads,
*unpack, *unpack,
@ -231,7 +231,7 @@ def get_test_job(*, config, test_config, test_id, version_flavor, jobs):
{ {
"name": "Publish results", "name": "Publish results",
"if": "always()", "if": "always()",
"uses": "actions/upload-artifact@v3", "uses": "actions/upload-artifact@v2",
"with": { "with": {
"name": f"pytest-results_{test_id}_{version_flavor.value}", "name": f"pytest-results_{test_id}_{version_flavor.value}",
"path": "pytest.xml", "path": "pytest.xml",
@ -250,7 +250,7 @@ def upload_steps(software_id):
}, },
{ {
"name": "Upload build artefacts", "name": "Upload build artefacts",
"uses": "actions/upload-artifact@v3", "uses": "actions/upload-artifact@v2",
"with": { "with": {
"name": f"installed-{software_id}", "name": f"installed-{software_id}",
"path": "~/artefacts-*.tar.gz", "path": "~/artefacts-*.tar.gz",
@ -263,6 +263,7 @@ def upload_steps(software_id):
def generate_workflow(config: dict, version_flavor: VersionFlavor): def generate_workflow(config: dict, version_flavor: VersionFlavor):
on: dict on: dict
if version_flavor == VersionFlavor.STABLE: if version_flavor == VersionFlavor.STABLE:
on = {"push": None, "pull_request": None} on = {"push": None, "pull_request": None}
@ -306,15 +307,15 @@ def generate_workflow(config: dict, version_flavor: VersionFlavor):
jobs["publish-test-results"] = { jobs["publish-test-results"] = {
"name": "Publish Dashboard", "name": "Publish Dashboard",
"needs": sorted({f"test-{test_id}" for test_id in config["tests"]} & set(jobs)), "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 # the build-and-test job might be skipped, we don't need to run
# this job then # this job then
"if": "success() || failure()", "if": "success() || failure()",
"steps": [ "steps": [
{"uses": "actions/checkout@v3"}, {"uses": "actions/checkout@v2"},
{ {
"name": "Download Artifacts", "name": "Download Artifacts",
"uses": "actions/download-artifact@v3", "uses": "actions/download-artifact@v2",
"with": {"path": "artifacts"}, "with": {"path": "artifacts"},
}, },
{ {

View File

@ -12,9 +12,6 @@ disallow_untyped_defs = False
[mypy-irctest.client_tests.*] [mypy-irctest.client_tests.*]
disallow_untyped_defs = False disallow_untyped_defs = False
[mypy-irctest.self_tests.*]
disallow_untyped_defs = False
[mypy-defusedxml.*] [mypy-defusedxml.*]
ignore_missing_imports = True 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 # tests separate
compacted_d = {} compacted_d = {}
successes = [] 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: if isinstance(v, CompactedResult) and v.success and v.nb_skipped == 0:
successes.append((k, v)) successes.append((k, v))
else: else:

View File

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