Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion chatmaild/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ chatmail-expire = "chatmaild.expire:daily_expire_main"
chatmail-quota-expire = "chatmaild.expire:quota_expire_main"
chatmail-fsreport = "chatmaild.fsreport:main"
lastlogin = "chatmaild.lastlogin:main"
turnserver = "chatmaild.turnserver:main"

[project.entry-points.pytest11]
"chatmaild.testplugin" = "chatmaild.tests.plugin"
Expand Down
3 changes: 3 additions & 0 deletions chatmaild/src/chatmaild/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ def __init__(self, inipath, params):
self.acme_email = params.get("acme_email", "")
self.imap_rawlog = params.get("imap_rawlog", "false").lower() == "true"
self.imap_compress = params.get("imap_compress", "false").lower() == "true"
self.turn_socket_path = params.get(
"turn_socket_path", "/run/chatmail-turn/turn.socket"
)
if "iroh_relay" not in params:
self.iroh_relay = "https://" + raw_domain
self.enable_iroh_relay = True
Expand Down
24 changes: 21 additions & 3 deletions chatmaild/src/chatmaild/metadata.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import logging
import socket
import sys
import time
from contextlib import contextmanager
Expand All @@ -7,7 +8,14 @@
from .dictproxy import DictProxy
from .filedict import FileDict
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):
Expand Down Expand Up @@ -79,12 +87,20 @@ def get_tokens_for_addr(self, addr):


class MetadataDictProxy(DictProxy):
def __init__(self, notifier, metadata, iroh_relay=None, turn_hostname=None):
def __init__(
self,
notifier,
metadata,
iroh_relay=None,
turn_hostname=None,
turn_socket_path=None,
):
super().__init__()
self.notifier = notifier
self.metadata = metadata
self.iroh_relay = iroh_relay
self.turn_hostname = turn_hostname
self.turn_socket_path = turn_socket_path

def handle_lookup(self, parts):
# Lpriv/43f5f508a7ea0366dff30200c15250e3/devicetoken\tlkj123poi@c2.testrun.org
Expand All @@ -101,7 +117,7 @@ def handle_lookup(self, parts):
return f"O{self.iroh_relay}\n"
case "turn":
try:
res = turn_credentials()
res = turn_credentials(self.turn_socket_path)
except Exception:
logging.exception("failed to get TURN credentials")
return "N\n"
Expand Down Expand Up @@ -135,6 +151,7 @@ def main():
config = read_config(config_path)
iroh_relay = config.iroh_relay
mail_domain = config.mail_domain
socket_path = config.turn_socket_path

vmail_dir = config.mailboxes_dir
if not vmail_dir.exists():
Expand All @@ -152,6 +169,7 @@ def main():
metadata=metadata,
iroh_relay=iroh_relay,
turn_hostname=mail_domain,
turn_socket_path=socket_path,
)

dictproxy.serve_forever_from_socket(socket)
6 changes: 4 additions & 2 deletions chatmaild/src/chatmaild/tests/test_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,7 @@ def test_turn_credentials_exception_returns_N(notifier, metadata, monkeypatch):
turn_hostname="turn.example.org",
)

def mock_turn_credentials():
def mock_turn_credentials(turn_socket_path):
raise ConnectionRefusedError("socket not available")

monkeypatch.setattr(chatmaild.metadata, "turn_credentials", mock_turn_credentials)
Expand All @@ -348,7 +348,9 @@ def test_turn_credentials_success(notifier, metadata, monkeypatch):
turn_hostname="turn.example.org",
)

monkeypatch.setattr(chatmaild.metadata, "turn_credentials", lambda: "user:pass")
monkeypatch.setattr(
chatmaild.metadata, "turn_credentials", lambda path: "user:pass"
)

transactions = {}
res = dictproxy.handle_dovecot_request(
Expand Down
46 changes: 46 additions & 0 deletions chatmaild/src/chatmaild/tests/test_turn.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
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 changes: 0 additions & 73 deletions chatmaild/src/chatmaild/tests/test_turnserver.py

This file was deleted.

10 changes: 0 additions & 10 deletions chatmaild/src/chatmaild/turnserver.py

This file was deleted.