mirror of
https://github.com/chatmail/relay.git
synced 2026-05-14 18:04:38 +00:00
Compare commits
1 Commits
main
...
turnserver
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
59dceb202d |
@@ -26,6 +26,7 @@ chatmail-expire = "chatmaild.expire:daily_expire_main"
|
|||||||
chatmail-quota-expire = "chatmaild.expire:quota_expire_main"
|
chatmail-quota-expire = "chatmaild.expire:quota_expire_main"
|
||||||
chatmail-fsreport = "chatmaild.fsreport:main"
|
chatmail-fsreport = "chatmaild.fsreport:main"
|
||||||
lastlogin = "chatmaild.lastlogin:main"
|
lastlogin = "chatmaild.lastlogin:main"
|
||||||
|
turnserver = "chatmaild.turnserver:main"
|
||||||
|
|
||||||
[project.entry-points.pytest11]
|
[project.entry-points.pytest11]
|
||||||
"chatmaild.testplugin" = "chatmaild.tests.plugin"
|
"chatmaild.testplugin" = "chatmaild.tests.plugin"
|
||||||
|
|||||||
@@ -10,7 +10,12 @@ from chatmaild.user import User
|
|||||||
def read_config(inipath):
|
def read_config(inipath):
|
||||||
assert Path(inipath).exists(), inipath
|
assert Path(inipath).exists(), inipath
|
||||||
cfg = iniconfig.IniConfig(inipath)
|
cfg = iniconfig.IniConfig(inipath)
|
||||||
return Config(inipath, params=cfg.sections["params"])
|
params = cfg.sections["params"]
|
||||||
|
default_config_content = get_default_config_content(params["mail_domain"])
|
||||||
|
df_params = iniconfig.IniConfig("ini", data=default_config_content)["params"]
|
||||||
|
new_params = dict(df_params.items())
|
||||||
|
new_params.update(params)
|
||||||
|
return Config(inipath, params=new_params)
|
||||||
|
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
@@ -31,18 +36,16 @@ class Config:
|
|||||||
|
|
||||||
self.max_user_send_per_minute = int(params.get("max_user_send_per_minute", 60))
|
self.max_user_send_per_minute = int(params.get("max_user_send_per_minute", 60))
|
||||||
self.max_user_send_burst_size = int(params.get("max_user_send_burst_size", 10))
|
self.max_user_send_burst_size = int(params.get("max_user_send_burst_size", 10))
|
||||||
self.max_mailbox_size = params.get("max_mailbox_size", "500M")
|
self.max_mailbox_size = params["max_mailbox_size"]
|
||||||
self.max_message_size = int(params.get("max_message_size", 31457280))
|
self.max_message_size = int(params.get("max_message_size", "31457280"))
|
||||||
self.delete_mails_after = params.get("delete_mails_after", "20")
|
self.delete_mails_after = params["delete_mails_after"]
|
||||||
self.delete_large_after = params.get("delete_large_after", "7")
|
self.delete_large_after = params["delete_large_after"]
|
||||||
self.delete_inactive_users_after = int(
|
self.delete_inactive_users_after = int(params["delete_inactive_users_after"])
|
||||||
params.get("delete_inactive_users_after", 90)
|
self.username_min_length = int(params["username_min_length"])
|
||||||
)
|
self.username_max_length = int(params["username_max_length"])
|
||||||
self.username_min_length = int(params.get("username_min_length", 9))
|
self.password_min_length = int(params["password_min_length"])
|
||||||
self.username_max_length = int(params.get("username_max_length", 9))
|
self.passthrough_senders = params["passthrough_senders"].split()
|
||||||
self.password_min_length = int(params.get("password_min_length", 9))
|
self.passthrough_recipients = params["passthrough_recipients"].split()
|
||||||
self.passthrough_senders = params.get("passthrough_senders", "").split()
|
|
||||||
self.passthrough_recipients = params.get("passthrough_recipients", "").split()
|
|
||||||
self.www_folder = params.get("www_folder", "")
|
self.www_folder = params.get("www_folder", "")
|
||||||
self.filtermail_smtp_port = int(params.get("filtermail_smtp_port", "10080"))
|
self.filtermail_smtp_port = int(params.get("filtermail_smtp_port", "10080"))
|
||||||
self.filtermail_smtp_port_incoming = int(
|
self.filtermail_smtp_port_incoming = int(
|
||||||
@@ -63,9 +66,7 @@ class Config:
|
|||||||
self.acme_email = params.get("acme_email", "")
|
self.acme_email = params.get("acme_email", "")
|
||||||
self.imap_rawlog = params.get("imap_rawlog", "false").lower() == "true"
|
self.imap_rawlog = params.get("imap_rawlog", "false").lower() == "true"
|
||||||
self.imap_compress = params.get("imap_compress", "false").lower() == "true"
|
self.imap_compress = params.get("imap_compress", "false").lower() == "true"
|
||||||
self.turn_socket_path = params.get(
|
self.turn_socket_path = params.get("turn_socket_path", "/run/chatmail-turn/turn.socket")
|
||||||
"turn_socket_path", "/run/chatmail-turn/turn.socket"
|
|
||||||
)
|
|
||||||
if "iroh_relay" not in params:
|
if "iroh_relay" not in params:
|
||||||
self.iroh_relay = "https://" + raw_domain
|
self.iroh_relay = "https://" + raw_domain
|
||||||
self.enable_iroh_relay = True
|
self.enable_iroh_relay = True
|
||||||
@@ -164,7 +165,31 @@ def get_default_config_content(mail_domain, **overrides):
|
|||||||
for name, value in extra.items():
|
for name, value in extra.items():
|
||||||
new_line = f"{name} = {value}"
|
new_line = f"{name} = {value}"
|
||||||
new_lines.append(new_line)
|
new_lines.append(new_line)
|
||||||
return "\n".join(new_lines)
|
|
||||||
|
content = "\n".join(new_lines)
|
||||||
|
|
||||||
|
# apply testrun privacy overrides
|
||||||
|
|
||||||
|
if mail_domain.endswith(".testrun.org"):
|
||||||
|
override_inipath = inidir.joinpath("override-testrun.ini")
|
||||||
|
privacy = iniconfig.IniConfig(override_inipath)["privacy"]
|
||||||
|
lines = []
|
||||||
|
for line in content.split("\n"):
|
||||||
|
for key, value in privacy.items():
|
||||||
|
value_lines = value.format(mail_domain=mail_domain).strip().split("\n")
|
||||||
|
if not line.startswith(f"{key} =") or not value_lines:
|
||||||
|
continue
|
||||||
|
if len(value_lines) == 1:
|
||||||
|
lines.append(f"{key} = {value}")
|
||||||
|
else:
|
||||||
|
lines.append(f"{key} =")
|
||||||
|
for vl in value_lines:
|
||||||
|
lines.append(f" {vl}")
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
lines.append(line)
|
||||||
|
content = "\n".join(lines)
|
||||||
|
return content
|
||||||
|
|
||||||
|
|
||||||
def is_valid_ipv4(address: str) -> bool:
|
def is_valid_ipv4(address: str) -> bool:
|
||||||
|
|||||||
@@ -12,42 +12,42 @@ mail_domain = {mail_domain}
|
|||||||
#
|
#
|
||||||
|
|
||||||
# email sending rate per user and minute
|
# email sending rate per user and minute
|
||||||
#max_user_send_per_minute = 60
|
max_user_send_per_minute = 60
|
||||||
|
|
||||||
# per-user max burst size for sending rate limiting (GCRA bucket capacity)
|
# per-user max burst size for sending rate limiting (GCRA bucket capacity)
|
||||||
#max_user_send_burst_size = 10
|
max_user_send_burst_size = 10
|
||||||
|
|
||||||
# maximum mailbox size of a chatmail address
|
# maximum mailbox size of a chatmail address
|
||||||
# (Oldest messages will be removed automatically, so mailboxes never run full)
|
# Oldest messages will be removed automatically, so mailboxes never run full.
|
||||||
#max_mailbox_size = 500M
|
max_mailbox_size = 500M
|
||||||
|
|
||||||
# maximum message size for an e-mail in bytes
|
# maximum message size for an e-mail in bytes
|
||||||
#max_message_size = 31457280
|
max_message_size = 31457280
|
||||||
|
|
||||||
# days after which mails are unconditionally deleted
|
# days after which mails are unconditionally deleted
|
||||||
#delete_mails_after = 20
|
delete_mails_after = 20
|
||||||
|
|
||||||
# days after which large messages (>200k) are unconditionally deleted
|
# days after which large messages (>200k) are unconditionally deleted
|
||||||
#delete_large_after = 7
|
delete_large_after = 7
|
||||||
|
|
||||||
# days after which users without a successful login are deleted (database and mails)
|
# days after which users without a successful login are deleted (database and mails)
|
||||||
#delete_inactive_users_after = 90
|
delete_inactive_users_after = 90
|
||||||
|
|
||||||
# minimum length a username must have
|
# minimum length a username must have
|
||||||
#username_min_length = 9
|
username_min_length = 9
|
||||||
|
|
||||||
# maximum length a username can have
|
# maximum length a username can have
|
||||||
#username_max_length = 9
|
username_max_length = 9
|
||||||
|
|
||||||
# minimum length a password must have
|
# minimum length a password must have
|
||||||
#password_min_length = 9
|
password_min_length = 9
|
||||||
|
|
||||||
# list of chatmail addresses which can send outbound un-encrypted mail
|
# list of chatmail addresses which can send outbound un-encrypted mail
|
||||||
#passthrough_senders =
|
passthrough_senders =
|
||||||
|
|
||||||
# list of e-mail recipients for which to accept outbound un-encrypted mails
|
# list of e-mail recipients for which to accept outbound un-encrypted mails
|
||||||
# (space-separated, item may start with "@" to whitelist whole recipient domains)
|
# (space-separated, item may start with "@" to whitelist whole recipient domains)
|
||||||
#passthrough_recipients =
|
passthrough_recipients =
|
||||||
|
|
||||||
# Use externally managed TLS certificates instead of built-in acmetool.
|
# Use externally managed TLS certificates instead of built-in acmetool.
|
||||||
# Paths refer to files on the deployment server (not the build machine).
|
# Paths refer to files on the deployment server (not the build machine).
|
||||||
@@ -63,11 +63,22 @@ mail_domain = {mail_domain}
|
|||||||
# Deployment Details
|
# Deployment Details
|
||||||
#
|
#
|
||||||
|
|
||||||
|
# Path to the TURN server Unix socket
|
||||||
|
turn_socket_path = /run/chatmail-turn/turn.socket
|
||||||
|
|
||||||
|
# SMTP outgoing filtermail and reinjection
|
||||||
|
filtermail_smtp_port = 10080
|
||||||
|
postfix_reinject_port = 10025
|
||||||
|
|
||||||
|
# SMTP incoming filtermail and reinjection
|
||||||
|
filtermail_smtp_port_incoming = 10081
|
||||||
|
postfix_reinject_port_incoming = 10026
|
||||||
|
|
||||||
# if set to "True" IPv6 is disabled
|
# if set to "True" IPv6 is disabled
|
||||||
#disable_ipv6 = False
|
disable_ipv6 = False
|
||||||
|
|
||||||
# Your email adress, which will be used in acmetool to manage Let's Encrypt SSL certificates
|
# Your email adress, which will be used in acmetool to manage Let's Encrypt SSL certificates
|
||||||
#acme_email =
|
acme_email =
|
||||||
|
|
||||||
# Defaults to https://iroh.{{mail_domain}} and running `iroh-relay` on the chatmail
|
# Defaults to https://iroh.{{mail_domain}} and running `iroh-relay` on the chatmail
|
||||||
# service.
|
# service.
|
||||||
@@ -100,13 +111,13 @@ mail_domain = {mail_domain}
|
|||||||
# in per-maildir ".in/.out" files.
|
# in per-maildir ".in/.out" files.
|
||||||
# Note that you need to manually cleanup these files
|
# Note that you need to manually cleanup these files
|
||||||
# so use this option with caution on production servers.
|
# so use this option with caution on production servers.
|
||||||
#imap_rawlog = false
|
imap_rawlog = false
|
||||||
|
|
||||||
# set to true if you want to enable the IMAP COMPRESS Extension,
|
# set to true if you want to enable the IMAP COMPRESS Extension,
|
||||||
# which allows IMAP connections to be efficiently compressed.
|
# which allows IMAP connections to be efficiently compressed.
|
||||||
# WARNING: Enabling this makes it impossible to hibernate IMAP
|
# WARNING: Enabling this makes it impossible to hibernate IMAP
|
||||||
# processes which will result in much higher memory/RAM usage.
|
# processes which will result in much higher memory/RAM usage.
|
||||||
#imap_compress = false
|
imap_compress = false
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
|||||||
16
chatmaild/src/chatmaild/ini/override-testrun.ini
Normal file
16
chatmaild/src/chatmaild/ini/override-testrun.ini
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
|
||||||
|
[privacy]
|
||||||
|
|
||||||
|
passthrough_recipients = privacy@testrun.org echo@{mail_domain}
|
||||||
|
|
||||||
|
privacy_postal =
|
||||||
|
Merlinux GmbH, Represented by the managing director H. Krekel,
|
||||||
|
Reichgrafen Str. 20, 79102 Freiburg, Germany
|
||||||
|
|
||||||
|
privacy_mail = privacy@testrun.org
|
||||||
|
privacy_pdo =
|
||||||
|
Prof. Dr. Fabian Schmieder, lexICT UG (limited), Ostfeldstr. 49, 30559 Hannover.
|
||||||
|
You can contact him at *delta-privacy@merlinux.eu* (Keyword: DPO)
|
||||||
|
privacy_supervisor =
|
||||||
|
State Commissioner for Data Protection and Freedom of Information of
|
||||||
|
Baden-Württemberg in 70173 Stuttgart, Germany.
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import logging
|
import logging
|
||||||
import socket
|
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
@@ -8,14 +7,7 @@ from .config import read_config
|
|||||||
from .dictproxy import DictProxy
|
from .dictproxy import DictProxy
|
||||||
from .filedict import FileDict
|
from .filedict import FileDict
|
||||||
from .notifier import Notifier
|
from .notifier import Notifier
|
||||||
|
from .turnserver import turn_credentials
|
||||||
|
|
||||||
def turn_credentials(turn_socket_path):
|
|
||||||
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as client_socket:
|
|
||||||
client_socket.settimeout(5)
|
|
||||||
client_socket.connect(turn_socket_path)
|
|
||||||
with client_socket.makefile("rb") as file:
|
|
||||||
return file.readline().decode("utf-8").strip()
|
|
||||||
|
|
||||||
|
|
||||||
def _is_valid_token_timestamp(timestamp, now):
|
def _is_valid_token_timestamp(timestamp, now):
|
||||||
@@ -87,14 +79,7 @@ class Metadata:
|
|||||||
|
|
||||||
|
|
||||||
class MetadataDictProxy(DictProxy):
|
class MetadataDictProxy(DictProxy):
|
||||||
def __init__(
|
def __init__(self, notifier, metadata, iroh_relay=None, turn_hostname=None, turn_socket_path=None):
|
||||||
self,
|
|
||||||
notifier,
|
|
||||||
metadata,
|
|
||||||
iroh_relay=None,
|
|
||||||
turn_hostname=None,
|
|
||||||
turn_socket_path=None,
|
|
||||||
):
|
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.notifier = notifier
|
self.notifier = notifier
|
||||||
self.metadata = metadata
|
self.metadata = metadata
|
||||||
|
|||||||
@@ -13,12 +13,7 @@ def test_read_config_basic(example_config):
|
|||||||
assert not example_config.privacy_pdo and not example_config.privacy_postal
|
assert not example_config.privacy_pdo and not example_config.privacy_postal
|
||||||
|
|
||||||
inipath = example_config._inipath
|
inipath = example_config._inipath
|
||||||
inipath.write_text(
|
inipath.write_text(inipath.read_text().replace("60", "37"))
|
||||||
inipath.read_text().replace(
|
|
||||||
"#max_user_send_per_minute = 60",
|
|
||||||
"max_user_send_per_minute = 37",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
example_config = read_config(inipath)
|
example_config = read_config(inipath)
|
||||||
assert example_config.max_user_send_per_minute == 37
|
assert example_config.max_user_send_per_minute == 37
|
||||||
assert example_config.mail_domain == "chat.example.org"
|
assert example_config.mail_domain == "chat.example.org"
|
||||||
@@ -36,17 +31,26 @@ def test_read_config_basic_using_defaults(tmp_path, maildomain):
|
|||||||
example_config = read_config(inipath)
|
example_config = read_config(inipath)
|
||||||
assert example_config.max_user_send_per_minute == 60
|
assert example_config.max_user_send_per_minute == 60
|
||||||
assert example_config.filtermail_smtp_port_incoming == 10081
|
assert example_config.filtermail_smtp_port_incoming == 10081
|
||||||
assert example_config.filtermail_smtp_port == 10080
|
|
||||||
assert example_config.postfix_reinject_port == 10025
|
|
||||||
assert example_config.max_user_send_per_minute == 60
|
def test_read_config_testrun(make_config):
|
||||||
assert example_config.max_mailbox_size == "500M"
|
config = make_config("something.testrun.org")
|
||||||
assert example_config.delete_mails_after == "20"
|
assert config.mail_domain == "something.testrun.org"
|
||||||
assert example_config.delete_large_after == "7"
|
assert len(config.privacy_postal.split("\n")) > 1
|
||||||
assert example_config.username_min_length == 9
|
assert len(config.privacy_supervisor.split("\n")) > 1
|
||||||
assert example_config.username_max_length == 9
|
assert len(config.privacy_pdo.split("\n")) > 1
|
||||||
assert example_config.password_min_length == 9
|
assert config.privacy_mail == "privacy@testrun.org"
|
||||||
assert example_config.passthrough_recipients == []
|
assert config.filtermail_smtp_port == 10080
|
||||||
assert example_config.passthrough_senders == []
|
assert config.postfix_reinject_port == 10025
|
||||||
|
assert config.max_user_send_per_minute == 60
|
||||||
|
assert config.max_mailbox_size == "500M"
|
||||||
|
assert config.delete_mails_after == "20"
|
||||||
|
assert config.delete_large_after == "7"
|
||||||
|
assert config.username_min_length == 9
|
||||||
|
assert config.username_max_length == 9
|
||||||
|
assert config.password_min_length == 9
|
||||||
|
assert "privacy@testrun.org" in config.passthrough_recipients
|
||||||
|
assert config.passthrough_senders == []
|
||||||
|
|
||||||
|
|
||||||
def test_config_userstate_paths(make_config, tmp_path):
|
def test_config_userstate_paths(make_config, tmp_path):
|
||||||
|
|||||||
@@ -324,7 +324,7 @@ def test_turn_credentials_exception_returns_N(notifier, metadata, monkeypatch):
|
|||||||
turn_hostname="turn.example.org",
|
turn_hostname="turn.example.org",
|
||||||
)
|
)
|
||||||
|
|
||||||
def mock_turn_credentials(turn_socket_path):
|
def mock_turn_credentials():
|
||||||
raise ConnectionRefusedError("socket not available")
|
raise ConnectionRefusedError("socket not available")
|
||||||
|
|
||||||
monkeypatch.setattr(chatmaild.metadata, "turn_credentials", mock_turn_credentials)
|
monkeypatch.setattr(chatmaild.metadata, "turn_credentials", mock_turn_credentials)
|
||||||
@@ -348,9 +348,7 @@ def test_turn_credentials_success(notifier, metadata, monkeypatch):
|
|||||||
turn_hostname="turn.example.org",
|
turn_hostname="turn.example.org",
|
||||||
)
|
)
|
||||||
|
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(chatmaild.metadata, "turn_credentials", lambda: "user:pass")
|
||||||
chatmaild.metadata, "turn_credentials", lambda path: "user:pass"
|
|
||||||
)
|
|
||||||
|
|
||||||
transactions = {}
|
transactions = {}
|
||||||
res = dictproxy.handle_dovecot_request(
|
res = dictproxy.handle_dovecot_request(
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
import socket
|
|
||||||
import threading
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from chatmaild.metadata import turn_credentials
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def turn_socket(tmp_path):
|
|
||||||
sock_path = str(tmp_path / "turn.socket")
|
|
||||||
server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
||||||
server.bind(sock_path)
|
|
||||||
server.listen(1)
|
|
||||||
yield sock_path, server
|
|
||||||
server.close()
|
|
||||||
|
|
||||||
|
|
||||||
def test_turn_credentials_timeout(turn_socket):
|
|
||||||
sock_path, server = turn_socket
|
|
||||||
with pytest.raises(socket.timeout):
|
|
||||||
# Inside turn_credentials the kernel listen backlog (1)
|
|
||||||
# completes connect() without accept()
|
|
||||||
# so the client blocks on readline() until the 5s timeout fires.
|
|
||||||
turn_credentials(sock_path)
|
|
||||||
|
|
||||||
|
|
||||||
def test_turn_credentials_connection_refused_on_not_existing_socket(tmp_path):
|
|
||||||
missing = str(tmp_path / "nonexistent.socket")
|
|
||||||
with pytest.raises((ConnectionRefusedError, FileNotFoundError)):
|
|
||||||
turn_credentials(missing)
|
|
||||||
|
|
||||||
|
|
||||||
def test_turn_credentials_socket_success(turn_socket):
|
|
||||||
sock_path, server = turn_socket
|
|
||||||
|
|
||||||
def respond():
|
|
||||||
conn, _ = server.accept()
|
|
||||||
conn.sendall(b"testuser:testpass\n")
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
t = threading.Thread(target=respond, daemon=True)
|
|
||||||
t.start()
|
|
||||||
|
|
||||||
result = turn_credentials(sock_path)
|
|
||||||
assert result == "testuser:testpass"
|
|
||||||
73
chatmaild/src/chatmaild/tests/test_turnserver.py
Normal file
73
chatmaild/src/chatmaild/tests/test_turnserver.py
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import socket
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from chatmaild.turnserver import turn_credentials
|
||||||
|
|
||||||
|
SOCKET_PATH = "/run/chatmail-turn/turn.socket"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def turn_socket(tmp_path):
|
||||||
|
"""Create a real Unix socket server at a temp path."""
|
||||||
|
sock_path = str(tmp_path / "turn.socket")
|
||||||
|
server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||||
|
server.bind(sock_path)
|
||||||
|
server.listen(1)
|
||||||
|
yield sock_path, server
|
||||||
|
server.close()
|
||||||
|
|
||||||
|
|
||||||
|
def _call_turn_credentials(sock_path):
|
||||||
|
"""Call turn_credentials but connect to sock_path instead of hardcoded path."""
|
||||||
|
original_connect = socket.socket.connect
|
||||||
|
|
||||||
|
def patched_connect(self, address):
|
||||||
|
if address == SOCKET_PATH:
|
||||||
|
address = sock_path
|
||||||
|
return original_connect(self, address)
|
||||||
|
|
||||||
|
with patch.object(socket.socket, "connect", patched_connect):
|
||||||
|
return turn_credentials()
|
||||||
|
|
||||||
|
|
||||||
|
def test_turn_credentials_timeout(turn_socket):
|
||||||
|
"""Server accepts but never responds — must raise socket.timeout."""
|
||||||
|
sock_path, server = turn_socket
|
||||||
|
|
||||||
|
def accept_and_hang():
|
||||||
|
conn, _ = server.accept()
|
||||||
|
time.sleep(30)
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
t = threading.Thread(target=accept_and_hang, daemon=True)
|
||||||
|
t.start()
|
||||||
|
|
||||||
|
with pytest.raises(socket.timeout):
|
||||||
|
_call_turn_credentials(sock_path)
|
||||||
|
|
||||||
|
|
||||||
|
def test_turn_credentials_connection_refused(tmp_path):
|
||||||
|
"""Socket file doesn't exist — must raise ConnectionRefusedError or FileNotFoundError."""
|
||||||
|
missing = str(tmp_path / "nonexistent.socket")
|
||||||
|
with pytest.raises((ConnectionRefusedError, FileNotFoundError)):
|
||||||
|
_call_turn_credentials(missing)
|
||||||
|
|
||||||
|
|
||||||
|
def test_turn_credentials_success(turn_socket):
|
||||||
|
"""Server responds with credentials — must return stripped string."""
|
||||||
|
sock_path, server = turn_socket
|
||||||
|
|
||||||
|
def respond():
|
||||||
|
conn, _ = server.accept()
|
||||||
|
conn.sendall(b"testuser:testpass\n")
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
t = threading.Thread(target=respond, daemon=True)
|
||||||
|
t.start()
|
||||||
|
|
||||||
|
result = _call_turn_credentials(sock_path)
|
||||||
|
assert result == "testuser:testpass"
|
||||||
10
chatmaild/src/chatmaild/turnserver.py
Normal file
10
chatmaild/src/chatmaild/turnserver.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import socket
|
||||||
|
|
||||||
|
|
||||||
|
def turn_credentials(turn_socket_path) -> str:
|
||||||
|
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as client_socket:
|
||||||
|
client_socket.settimeout(5)
|
||||||
|
client_socket.connect(turn_socket_path)
|
||||||
|
with client_socket.makefile("rb") as file:
|
||||||
|
return file.readline().decode("utf-8").strip()
|
||||||
Reference in New Issue
Block a user