feat: metadata service: make turnserver socket path configurable

also add tests for the turnserver metadata
This commit is contained in:
Mark Felder
2026-02-06 17:19:01 -08:00
parent dfcaf415b1
commit 0b21b83199
5 changed files with 131 additions and 5 deletions

View File

@@ -46,6 +46,7 @@ class Config:
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://" + params["mail_domain"]
self.enable_iroh_relay = True

View File

@@ -55,7 +55,10 @@ passthrough_recipients =
# Deployment Details
#
# SMTP outgoing filtermail and reinjection
# 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

View File

@@ -76,12 +76,13 @@ class Metadata:
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, config=None):
super().__init__()
self.notifier = notifier
self.metadata = metadata
self.iroh_relay = iroh_relay
self.turn_hostname = turn_hostname
self.config = config
def handle_lookup(self, parts):
# Lpriv/43f5f508a7ea0366dff30200c15250e3/devicetoken\tlkj123poi@c2.testrun.org
@@ -101,7 +102,7 @@ class MetadataDictProxy(DictProxy):
# Handle `GETMETADATA "" /shared/vendor/deltachat/irohrelay`
return f"O{self.iroh_relay}\n"
elif keyname == "vendor/vendor.dovecot/pvt/server/vendor/deltachat/turn":
res = turn_credentials()
res = turn_credentials(self.config)
port = 3478
return f"O{self.turn_hostname}:{port}:{res}\n"
@@ -146,6 +147,7 @@ def main():
metadata=metadata,
iroh_relay=iroh_relay,
turn_hostname=mail_domain,
config=config,
)
dictproxy.serve_forever_from_socket(socket)

View File

@@ -0,0 +1,120 @@
"""Tests for turnserver functionality, particularly metadata integration."""
import socket
import tempfile
import threading
from pathlib import Path
from chatmaild.config import read_config, write_initial_config
from chatmaild.metadata import MetadataDictProxy, Metadata
from chatmaild.notifier import Notifier
from chatmaild.turnserver import turn_credentials
def test_turn_credentials_function_with_custom_socket():
"""Test that turn_credentials function works with a custom socket path from config."""
# Create a temporary directory and socket file
temp_dir = Path(tempfile.mkdtemp())
temp_socket_path = temp_dir / "test_turn.socket"
# Create a mock TURN credentials server
def mock_server():
server_sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
server_sock.bind(str(temp_socket_path))
server_sock.listen(1)
# Accept connection and send mock credentials
conn, addr = server_sock.accept()
with conn:
conn.send(b"mock_turn_credentials_abc123\n")
server_sock.close()
# Start server in a background thread
server_thread = threading.Thread(target=mock_server, daemon=True)
server_thread.start()
# Create a config with custom socket path
config_path = temp_dir / "chatmail.ini"
write_initial_config(config_path, "test.example.org", {
"turn_socket_path": str(temp_socket_path)
})
config = read_config(config_path)
# Allow time for server to start
import time
time.sleep(0.01)
# Test that turn_credentials can connect using the config
credentials = turn_credentials(config)
assert credentials == "mock_turn_credentials_abc123"
server_thread.join(timeout=1) # Clean up thread
def test_metadata_turn_lookup_integration(tmp_path):
"""Test that metadata service properly handles TURN metadata lookups."""
# Create mock config with custom turn socket path
config_path = tmp_path / "chatmail.ini"
socket_path = tmp_path / "test_turn.socket"
write_initial_config(config_path, "example.org", {
"turn_socket_path": str(socket_path)
})
config = read_config(config_path)
# Create mock TURN server to return credentials
def mock_turn_server():
import os
os.makedirs(socket_path.parent, exist_ok=True) # Ensure parent directory exists
server_sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
server_sock.bind(str(socket_path))
server_sock.listen(1)
# Accept connection and send mock credentials
conn, addr = server_sock.accept()
with conn:
conn.send(b"test_creds_12345\n")
server_sock.close()
server_thread = threading.Thread(target=mock_turn_server, daemon=True)
server_thread.start()
import time
time.sleep(0.01) # Allow server to start
# Create a MetadataDictProxy with config
queue_dir = tmp_path / "queue"
queue_dir.mkdir()
notifier = Notifier(queue_dir)
metadata = Metadata(tmp_path / "vmail")
dict_proxy = MetadataDictProxy(
notifier=notifier,
metadata=metadata,
iroh_relay="https://example.org",
turn_hostname="example.org",
config=config
)
# Simulate a lookup for TURN credentials using the correct format
# Input: "shared/0123/vendor/vendor.dovecot/pvt/server/vendor/deltachat/turn"
# After parts[0].split("/", 2):
# - keyparts[0] = "shared"
# - keyparts[1] = "0123"
# - keyparts[2] = "vendor/vendor.dovecot/pvt/server/vendor/deltachat/turn"
# So keyname = keyparts[2] should match "vendor/vendor.dovecot/pvt/server/vendor/deltachat/turn"
parts = [
"shared/0123/vendor/vendor.dovecot/pvt/server/vendor/deltachat/turn",
"dummy@user.org"
]
# Call handle_lookup directly
result = dict_proxy.handle_lookup(parts)
# Verify the response format is correct for TURN credentials
assert result.startswith("O") # Output response starts with 'O'
assert ":3478:" in result # Contains port 3478
assert "test_creds_12345" in result # Contains credentials returned by mock server
assert "example.org:3478:test_creds_12345" in result
server_thread.join(timeout=1) # Clean up thread

View File

@@ -2,8 +2,8 @@
import socket
def turn_credentials() -> str:
def turn_credentials(config) -> str:
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as client_socket:
client_socket.connect("/run/chatmail-turn/turn.socket")
client_socket.connect(config.turn_socket_path)
with client_socket.makefile("rb") as file:
return file.readline().decode("utf-8").strip()