Skip to content

Commit 25017af

Browse files
committed
feat: support underscore-prefixed domains with self-signed TLS certificates
Allow Delta Chat core to work with chatmail servers running on underscore-prefixed domains (e.g. _alice.localchat) which use self-signed TLS certificates. This is mirroring related work on chatmail relays: chatmail/relay#855 Underscore domains with self-signed TLS certs can be used by LXC test containers where obtaining real certificates is not practical. When the domain starts with '_', certificate verification is automatically relaxed for IMAP/SMTP connections, dcaccount QR code handling, and iroh relay endpoints. The Python test suite is adapted to also work against such underscore-domain servers, including cross-core tests with older Delta Chat versions. Note: this PR does not support HTTPS requests with underscore domains. They are not currently needed for working with LXC test containers. 14 files changed, +102/-31 lines (excluding Cargo.lock). Cargo.lock: +606/-11 lines from enabling iroh features needed for connecting to iroh relay endpoint on underscore domains. The added dependencies are unfortunate but best considered when finally upgrading to iroh 1.0 (tm).
1 parent e1e8407 commit 25017af

15 files changed

Lines changed: 708 additions & 42 deletions

Cargo.lock

Lines changed: 606 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ hyper = "1"
6767
hyper-util = "0.1.16"
6868
image = { version = "0.25.6", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
6969
iroh-gossip = { version = "0.35", default-features = false, features = ["net"] }
70-
iroh = { version = "0.35", default-features = false }
70+
iroh = { version = "0.35", default-features = false, features = ["test-utils", "metrics"] }
7171
kamadak-exif = "0.6.1"
7272
libc = { workspace = true }
7373
mail-builder = { version = "0.4.4", default-features = false }

deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import random
1010
import subprocess
1111
import sys
12+
import urllib.parse
1213
from typing import AsyncGenerator, Optional
1314

1415
import execnet
@@ -283,8 +284,16 @@ def factory(core_version):
283284
channel = gw.remote_exec(remote_bob_loop)
284285
cm = os.environ.get("CHATMAIL_DOMAIN")
285286

287+
# Build a dclogin QR code for the remote bob account.
288+
# Using dclogin scheme with ic=3&sc=3 allows old cores
289+
# to accept invalid certificates for underscore-prefixed domains.
290+
addr, password = acfactory.get_credentials()
291+
dclogin_qr = f"dclogin://{urllib.parse.quote(addr, safe='@')}?p={urllib.parse.quote(password)}&v=1"
292+
if cm and cm.startswith("_"):
293+
dclogin_qr += "&ic=3&sc=3"
294+
286295
# trigger getting an online account on bob's side
287-
channel.send((accounts_dir, str(rpc_server_path), cm))
296+
channel.send((accounts_dir, str(rpc_server_path), dclogin_qr))
288297

289298
# meanwhile get a local alice account
290299
alice = acfactory.get_online_account()
@@ -316,10 +325,8 @@ def remote_bob_loop(channel):
316325
import os
317326

318327
from deltachat_rpc_client import DeltaChat, Rpc
319-
from deltachat_rpc_client.pytestplugin import ACFactory
320328

321-
accounts_dir, rpc_server_path, chatmail_domain = channel.receive()
322-
os.environ["CHATMAIL_DOMAIN"] = chatmail_domain
329+
accounts_dir, rpc_server_path, dclogin_qr = channel.receive()
323330

324331
# older core versions don't support specifying rpc_server_path
325332
# so we can't just pass `rpc_server_path` argument to Rpc constructor
@@ -330,8 +337,14 @@ def remote_bob_loop(channel):
330337
with rpc:
331338
dc = DeltaChat(rpc)
332339
channel.send(dc.rpc.get_system_info()["deltachat_core_version"])
333-
acfactory = ACFactory(dc)
334-
bob = acfactory.get_online_account()
340+
341+
# Configure account using dclogin scheme directly,
342+
# avoiding the old ACFactory which doesn't handle
343+
# underscore-prefixed domains' TLS on older cores.
344+
bob = dc.add_account()
345+
bob.set_config_from_qr(dclogin_qr)
346+
bob.bring_online()
347+
335348
alice_vcard = channel.receive()
336349
[alice_contact] = bob.import_vcard(alice_vcard)
337350
ns = {"bob": bob, "bob_contact_alice": alice_contact}
@@ -345,3 +358,4 @@ def remote_bob_loop(channel):
345358
except Exception:
346359
# some unserializable result
347360
channel.send(None)
361+

deltachat-rpc-client/tests/conftest.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,11 @@ def connect(self):
3737
host = user.rsplit("@")[-1]
3838
pw = self.account.get_config("mail_pw")
3939

40-
self.conn = MailBox(host, port, ssl_context=ssl.create_default_context())
40+
ssl_context = ssl.create_default_context()
41+
if host.startswith("_"):
42+
ssl_context.check_hostname = False
43+
ssl_context.verify_mode = ssl.CERT_NONE
44+
self.conn = MailBox(host, port, ssl_context=ssl_context)
4145
self.conn.login(user, pw)
4246

4347
self.select_folder("INBOX")

deltachat-rpc-client/tests/test_calls.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ def test_ice_servers(acfactory) -> None:
7777
alice = acfactory.get_online_account()
7878

7979
ice_servers = alice.ice_servers()
80-
assert len(ice_servers) == 1
80+
assert len(ice_servers) >= 1
8181

8282

8383
def test_no_contact_request_call(acfactory) -> None:

deltachat-rpc-client/tests/test_chatlist_events.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ def test_delivery_status_failed(acfactory: ACFactory) -> None:
109109
assert failing_message.get_snapshot().state == const.MessageState.OUT_FAILED
110110

111111

112-
def test_download_on_demand(acfactory: ACFactory) -> None:
112+
def test_download_on_demand(acfactory: ACFactory, data) -> None:
113113
"""
114114
Test if download on demand emits chatlist update events.
115115
This is only needed for last message in chat, but finding that out is too expensive, so it's always emitted
@@ -127,7 +127,7 @@ def test_download_on_demand(acfactory: ACFactory) -> None:
127127
msg.get_snapshot().chat.accept()
128128
bob.get_chat_by_id(chat_id).send_message(
129129
"Hello World, this message is bigger than 5 bytes",
130-
file="../test-data/image/screenshot.jpg",
130+
file=data.get_path("image/screenshot.jpg"),
131131
)
132132

133133
message = alice.wait_for_incoming_msg()

deltachat-rpc-client/tests/test_cross_core.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def test_qr_setup_contact(alice_and_remote_bob, version) -> None:
3636

3737
def test_send_and_receive_message(alice_and_remote_bob) -> None:
3838
"""Test other-core Bob profile can send a message to Alice on current core."""
39-
alice, alice_contact_bob, remote_eval = alice_and_remote_bob("2.20.0")
39+
alice, alice_contact_bob, remote_eval = alice_and_remote_bob("2.24.0")
4040

4141
remote_eval("bob_contact_alice.create_chat().send_text('hello')")
4242

@@ -46,7 +46,7 @@ def test_send_and_receive_message(alice_and_remote_bob) -> None:
4646

4747
def test_second_device(acfactory, alice_and_remote_bob) -> None:
4848
"""Test setting up current version as a second device for old version."""
49-
_alice, alice_contact_bob, remote_eval = alice_and_remote_bob("2.20.0")
49+
_alice, alice_contact_bob, remote_eval = alice_and_remote_bob("2.24.0")
5050

5151
remote_eval("locals().setdefault('future', bob._rpc.provide_backup.future(bob.id))")
5252
qr = remote_eval("bob._rpc.get_backup_qr(bob.id)")

deltachat-rpc-client/tests/test_multitransport.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ def test_change_address(acfactory) -> None:
120120
assert sender_addr2 == new_alice_addr
121121

122122

123-
def test_download_on_demand(acfactory) -> None:
123+
def test_download_on_demand(acfactory, data) -> None:
124124
alice, bob = acfactory.get_online_accounts(2)
125125
alice.set_config("download_limit", "1")
126126

@@ -131,7 +131,7 @@ def test_download_on_demand(acfactory) -> None:
131131

132132
alice.create_chat(bob)
133133
chat_bob_alice = bob.create_chat(alice)
134-
chat_bob_alice.send_message(file="../test-data/image/screenshot.jpg")
134+
chat_bob_alice.send_message(file=data.get_path("image/screenshot.jpg"))
135135
msg = alice.wait_for_incoming_msg()
136136
snapshot = msg.get_snapshot()
137137
assert snapshot.download_state == DownloadState.AVAILABLE

deltachat-rpc-client/tests/test_securejoin.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -141,10 +141,9 @@ def get_broadcast(ac):
141141

142142
def wait_for_broadcast_messages(ac):
143143
snapshot1 = ac.wait_for_incoming_msg().get_snapshot()
144-
assert snapshot1.text == "You joined the channel."
145-
146144
snapshot2 = ac.wait_for_incoming_msg().get_snapshot()
147-
assert snapshot2.text == "Hello everyone!"
145+
texts = {snapshot1.text, snapshot2.text}
146+
assert texts == {"You joined the channel.", "Hello everyone!"}
148147

149148
chat = get_broadcast(ac)
150149
assert snapshot1.chat_id == chat.id

deltachat-rpc-client/tests/test_something.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,11 @@ def test_lowercase_address(acfactory) -> None:
9797

9898
def test_configure_ip(acfactory) -> None:
9999
addr, password = acfactory.get_credentials()
100+
domain = addr.rsplit("@")[-1]
101+
if domain.startswith("_"):
102+
pytest.skip("Underscore domains accept invalid certificates")
100103
account = acfactory.get_unconfigured_account()
101-
ip_address = socket.gethostbyname(addr.rsplit("@")[-1])
104+
ip_address = socket.gethostbyname(domain)
102105

103106
with pytest.raises(JsonRpcError):
104107
account.add_or_update_transport(
@@ -1012,7 +1015,12 @@ def test_configured_imap_certificate_checks(acfactory):
10121015
alice = acfactory.new_configured_account()
10131016

10141017
# Certificate checks should be configured (not None)
1015-
assert "cert_strict" in alice.get_info().used_transport_settings
1018+
info = alice.get_info()
1019+
domain = alice.get_config("addr").split("@")[-1]
1020+
if domain.startswith("_"):
1021+
assert "cert_accept_invalid_certificates" in info.used_transport_settings
1022+
else:
1023+
assert "cert_strict" in info.used_transport_settings
10161024

10171025
# "cert_old_automatic" is the value old Delta Chat core versions used
10181026
# to mean user entered "imap_certificate_checks=0" (Automatic)
@@ -1361,7 +1369,7 @@ def test_synchronize_member_list_on_group_rejoin(acfactory, log):
13611369
assert msg.get_snapshot().chat.num_contacts() == 2
13621370

13631371

1364-
def test_large_message(acfactory) -> None:
1372+
def test_large_message(acfactory, data) -> None:
13651373
"""
13661374
Test sending large message without download limit set,
13671375
so it is sent with pre-message but downloaded without user interaction.
@@ -1371,7 +1379,7 @@ def test_large_message(acfactory) -> None:
13711379
alice_chat_bob = alice.create_chat(bob)
13721380
alice_chat_bob.send_message(
13731381
"Hello World, this message is bigger than 5 bytes",
1374-
file="../test-data/image/screenshot.jpg",
1382+
file=data.get_path("image/screenshot.jpg"),
13751383
)
13761384

13771385
msg = bob.wait_for_incoming_msg()

0 commit comments

Comments
 (0)