Skip to content

Commit 0b21b83

Browse files
committed
feat: metadata service: make turnserver socket path configurable
also add tests for the turnserver metadata
1 parent dfcaf41 commit 0b21b83

5 files changed

Lines changed: 131 additions & 5 deletions

File tree

chatmaild/src/chatmaild/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ def __init__(self, inipath, params):
4646
self.acme_email = params.get("acme_email", "")
4747
self.imap_rawlog = params.get("imap_rawlog", "false").lower() == "true"
4848
self.imap_compress = params.get("imap_compress", "false").lower() == "true"
49+
self.turn_socket_path = params.get("turn_socket_path", "/run/chatmail-turn/turn.socket")
4950
if "iroh_relay" not in params:
5051
self.iroh_relay = "https://" + params["mail_domain"]
5152
self.enable_iroh_relay = True

chatmaild/src/chatmaild/ini/chatmail.ini.f

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,10 @@
5555
# Deployment Details
5656
#
5757

58-
# SMTP outgoing filtermail and reinjection
58+
# Path to the TURN server Unix socket
59+
turn_socket_path = /run/chatmail-turn/turn.socket
60+
61+
# SMTP outgoing filtermail and reinjection
5962
filtermail_smtp_port = 10080
6063
postfix_reinject_port = 10025
6164

chatmaild/src/chatmaild/metadata.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,12 +76,13 @@ def get_tokens_for_addr(self, addr):
7676

7777

7878
class MetadataDictProxy(DictProxy):
79-
def __init__(self, notifier, metadata, iroh_relay=None, turn_hostname=None):
79+
def __init__(self, notifier, metadata, iroh_relay=None, turn_hostname=None, config=None):
8080
super().__init__()
8181
self.notifier = notifier
8282
self.metadata = metadata
8383
self.iroh_relay = iroh_relay
8484
self.turn_hostname = turn_hostname
85+
self.config = config
8586

8687
def handle_lookup(self, parts):
8788
# Lpriv/43f5f508a7ea0366dff30200c15250e3/devicetoken\tlkj123poi@c2.testrun.org
@@ -101,7 +102,7 @@ def handle_lookup(self, parts):
101102
# Handle `GETMETADATA "" /shared/vendor/deltachat/irohrelay`
102103
return f"O{self.iroh_relay}\n"
103104
elif keyname == "vendor/vendor.dovecot/pvt/server/vendor/deltachat/turn":
104-
res = turn_credentials()
105+
res = turn_credentials(self.config)
105106
port = 3478
106107
return f"O{self.turn_hostname}:{port}:{res}\n"
107108

@@ -146,6 +147,7 @@ def main():
146147
metadata=metadata,
147148
iroh_relay=iroh_relay,
148149
turn_hostname=mail_domain,
150+
config=config,
149151
)
150152

151153
dictproxy.serve_forever_from_socket(socket)
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
"""Tests for turnserver functionality, particularly metadata integration."""
2+
3+
import socket
4+
import tempfile
5+
import threading
6+
from pathlib import Path
7+
8+
from chatmaild.config import read_config, write_initial_config
9+
from chatmaild.metadata import MetadataDictProxy, Metadata
10+
from chatmaild.notifier import Notifier
11+
from chatmaild.turnserver import turn_credentials
12+
13+
14+
def test_turn_credentials_function_with_custom_socket():
15+
"""Test that turn_credentials function works with a custom socket path from config."""
16+
# Create a temporary directory and socket file
17+
temp_dir = Path(tempfile.mkdtemp())
18+
temp_socket_path = temp_dir / "test_turn.socket"
19+
20+
# Create a mock TURN credentials server
21+
def mock_server():
22+
server_sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
23+
server_sock.bind(str(temp_socket_path))
24+
server_sock.listen(1)
25+
26+
# Accept connection and send mock credentials
27+
conn, addr = server_sock.accept()
28+
with conn:
29+
conn.send(b"mock_turn_credentials_abc123\n")
30+
server_sock.close()
31+
32+
# Start server in a background thread
33+
server_thread = threading.Thread(target=mock_server, daemon=True)
34+
server_thread.start()
35+
36+
# Create a config with custom socket path
37+
config_path = temp_dir / "chatmail.ini"
38+
write_initial_config(config_path, "test.example.org", {
39+
"turn_socket_path": str(temp_socket_path)
40+
})
41+
config = read_config(config_path)
42+
43+
# Allow time for server to start
44+
import time
45+
time.sleep(0.01)
46+
47+
# Test that turn_credentials can connect using the config
48+
credentials = turn_credentials(config)
49+
assert credentials == "mock_turn_credentials_abc123"
50+
51+
server_thread.join(timeout=1) # Clean up thread
52+
53+
54+
def test_metadata_turn_lookup_integration(tmp_path):
55+
"""Test that metadata service properly handles TURN metadata lookups."""
56+
# Create mock config with custom turn socket path
57+
config_path = tmp_path / "chatmail.ini"
58+
socket_path = tmp_path / "test_turn.socket"
59+
write_initial_config(config_path, "example.org", {
60+
"turn_socket_path": str(socket_path)
61+
})
62+
config = read_config(config_path)
63+
64+
# Create mock TURN server to return credentials
65+
def mock_turn_server():
66+
import os
67+
os.makedirs(socket_path.parent, exist_ok=True) # Ensure parent directory exists
68+
69+
server_sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
70+
server_sock.bind(str(socket_path))
71+
server_sock.listen(1)
72+
73+
# Accept connection and send mock credentials
74+
conn, addr = server_sock.accept()
75+
with conn:
76+
conn.send(b"test_creds_12345\n")
77+
server_sock.close()
78+
79+
server_thread = threading.Thread(target=mock_turn_server, daemon=True)
80+
server_thread.start()
81+
82+
import time
83+
time.sleep(0.01) # Allow server to start
84+
85+
# Create a MetadataDictProxy with config
86+
queue_dir = tmp_path / "queue"
87+
queue_dir.mkdir()
88+
notifier = Notifier(queue_dir)
89+
metadata = Metadata(tmp_path / "vmail")
90+
91+
dict_proxy = MetadataDictProxy(
92+
notifier=notifier,
93+
metadata=metadata,
94+
iroh_relay="https://example.org",
95+
turn_hostname="example.org",
96+
config=config
97+
)
98+
99+
# Simulate a lookup for TURN credentials using the correct format
100+
# Input: "shared/0123/vendor/vendor.dovecot/pvt/server/vendor/deltachat/turn"
101+
# After parts[0].split("/", 2):
102+
# - keyparts[0] = "shared"
103+
# - keyparts[1] = "0123"
104+
# - keyparts[2] = "vendor/vendor.dovecot/pvt/server/vendor/deltachat/turn"
105+
# So keyname = keyparts[2] should match "vendor/vendor.dovecot/pvt/server/vendor/deltachat/turn"
106+
parts = [
107+
"shared/0123/vendor/vendor.dovecot/pvt/server/vendor/deltachat/turn",
108+
"dummy@user.org"
109+
]
110+
111+
# Call handle_lookup directly
112+
result = dict_proxy.handle_lookup(parts)
113+
114+
# Verify the response format is correct for TURN credentials
115+
assert result.startswith("O") # Output response starts with 'O'
116+
assert ":3478:" in result # Contains port 3478
117+
assert "test_creds_12345" in result # Contains credentials returned by mock server
118+
assert "example.org:3478:test_creds_12345" in result
119+
120+
server_thread.join(timeout=1) # Clean up thread

chatmaild/src/chatmaild/turnserver.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
import socket
33

44

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

0 commit comments

Comments
 (0)