diff --git a/deltachat-rpc-client/tests/test_cross_core.py b/deltachat-rpc-client/tests/test_cross_core.py index 12d14afe1..ac528a7f6 100644 --- a/deltachat-rpc-client/tests/test_cross_core.py +++ b/deltachat-rpc-client/tests/test_cross_core.py @@ -43,6 +43,28 @@ def test_qr_setup_contact(acf, alice_and_remote_bob, version) -> None: alice2.wait_for_securejoin_inviter_success() +@pytest.mark.parametrize("version", ["2.24.0"]) +def test_qr_setup_contact_multitransport(acf, alice_and_remote_bob, version) -> None: + """Test other-core Bob profile can do securejoin with Alice on current core, with multiple transports.""" + alice, alice_contact_bob, remote_eval = alice_and_remote_bob(version) + relay_qr = acf.get_account_qr() + alice.add_transport_from_qr(relay_qr) + alice.add_transport_from_qr(relay_qr) + + qr_code = alice.get_qr_code() + remote_eval(f"bob.secure_join({qr_code!r})") + alice.wait_for_securejoin_inviter_success() + + # Test that Alice verified Bob's profile. + alice_contact_bob_snapshot = alice_contact_bob.get_snapshot() + assert alice_contact_bob_snapshot.is_verified + + remote_eval("bob.wait_for_securejoin_joiner_success()") + + # Test that Bob verified Alice's profile. + assert remote_eval("bob_contact_alice.get_snapshot().is_verified") + + def test_send_and_receive_message(alice_and_remote_bob) -> None: """Test other-core Bob profile can send a message to Alice on current core.""" alice, alice_contact_bob, remote_eval = alice_and_remote_bob("2.23.0") diff --git a/deltachat-rpc-client/tests/test_multitransport.py b/deltachat-rpc-client/tests/test_multitransport.py index 4a52e413a..975608690 100644 --- a/deltachat-rpc-client/tests/test_multitransport.py +++ b/deltachat-rpc-client/tests/test_multitransport.py @@ -1,3 +1,5 @@ +import urllib.parse + import pytest from deltachat_rpc_client import EventType @@ -319,3 +321,33 @@ def test_remove_primary_transport(acf, log) -> None: assert msg2.text == "Hello again!" assert msg2.chat.get_basic_snapshot().chat_type == ChatType.SINGLE assert msg2.chat == alice.create_chat(bob) + + +def test_qr_works_after_removing_primary_transport(acf, log) -> None: + log.section("Alice setups an account and adds two additional relays") + alice = acf.new_configured_account() + relay_qr = acf.get_account_qr() + alice.add_transport_from_qr(relay_qr) + alice.add_transport_from_qr(relay_qr) + + first_addr = alice.list_transports()[0]["addr"] + second_addr = alice.list_transports()[1]["addr"] + third_addr = alice.list_transports()[2]["addr"] + + log.section("Alice creates a QR code") + chat_qr = alice.get_qr_code() + chat_qr_unquoted = urllib.parse.unquote(chat_qr) + assert f"&a={first_addr}" in chat_qr_unquoted + assert f"&r={third_addr},{second_addr}" in chat_qr_unquoted + + log.section("Alice removes first and second transport") + alice.set_config("configured_addr", third_addr) + alice.delete_transport(first_addr) + alice.delete_transport(second_addr) + + log.section("Bob scans the QR code, which still works") + alice.bring_online() + bob = acf.get_online_account() + bob.secure_join(chat_qr) + alice.wait_for_securejoin_inviter_success() + bob.wait_for_securejoin_joiner_success() diff --git a/src/qr.rs b/src/qr.rs index b710886f1..7ebe21bc7 100644 --- a/src/qr.rs +++ b/src/qr.rs @@ -463,6 +463,8 @@ pub fn format_backup(qr: &Qr) -> Result { /// or: `OPENPGP4FPR:FINGERPRINT#a=ADDR&g=GROUPNAME&x=GROUPID&i=INVITENUMBER&s=AUTH` /// or: `OPENPGP4FPR:FINGERPRINT#a=ADDR&b=BROADCAST_NAME&x=BROADCAST_ID&j=INVITENUMBER&s=AUTH` /// or: `OPENPGP4FPR:FINGERPRINT#a=ADDR` +/// +/// with optional `&r=ADDRS` param. async fn decode_openpgp(context: &Context, qr: &str) -> Result { let payload = qr .get(OPENPGP4FPR_SCHEME.len()..) @@ -492,10 +494,17 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result { }) .collect(); - let addr = if let Some(addr) = param.get("a") { - Some(normalize_address(addr)?) - } else { - None + let addrs = { + let mut addrs = Vec::new(); + if let Some(primary_addr) = param.get("a") { + addrs.push(normalize_address(primary_addr)?); + }; + if let Some(secondary_addrs_raw) = param.get("r") { + for secondary_address in secondary_addrs_raw.split(',') { + addrs.push(normalize_address(secondary_address)?) + } + } + addrs }; let name = decode_name(¶m, "n")?.unwrap_or_default(); @@ -528,7 +537,9 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result { invitenumber = Some("".to_string()); } - if let (Some(addr), Some(invitenumber), Some(authcode)) = (&addr, invitenumber, authcode) { + if let (Some(addr), Some(invitenumber), Some(authcode)) = + (addrs.first(), invitenumber, authcode) + { let addr = ContactAddress::new(addr)?; let (contact_id, _) = Contact::add_or_lookup_ext( context, @@ -571,7 +582,7 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result { grpid, contact_id, fingerprint, - addrs: vec![addr.to_string()], + addrs, invitenumber, authcode, is_v3, @@ -608,7 +619,7 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result { grpid, contact_id, fingerprint, - addrs: vec![addr.to_string()], + addrs, invitenumber, authcode, is_v3, @@ -634,16 +645,16 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result { Ok(Qr::AskVerifyContact { contact_id, fingerprint, - addrs: vec![addr.to_string()], + addrs, invitenumber, authcode, is_v3, }) } - } else if let Some(addr) = addr { + } else if let Some(addr) = addrs.first() { let fingerprint = fingerprint.hex(); let (contact_id, _) = - Contact::add_or_lookup_ext(context, "", &addr, &fingerprint, Origin::UnhandledQrScan) + Contact::add_or_lookup_ext(context, "", addr, &fingerprint, Origin::UnhandledQrScan) .await?; let contact = Contact::get_by_id(context, contact_id).await?; diff --git a/src/securejoin.rs b/src/securejoin.rs index 8a2eec24d..7102e7bf2 100644 --- a/src/securejoin.rs +++ b/src/securejoin.rs @@ -139,6 +139,18 @@ pub async fn get_securejoin_qr(context: &Context, chat: Option) -> Resul let self_addr = context.get_primary_self_addr().await?; let self_addr_urlencoded = utf8_percent_encode(&self_addr, DISALLOWED_CHARACTERS).to_string(); + let r_param = context + .get_published_secondary_self_addrs() + .await? + .into_iter() + .reduce(|acc, addr| { + format!( + "{acc},{}", + utf8_percent_encode(&addr, DISALLOWED_CHARACTERS) + ) + }) + .map_or(String::default(), |addrs| format!("&r={addrs}")); + let self_name = context .get_config(Config::Displayname) .await? @@ -165,11 +177,11 @@ pub async fn get_securejoin_qr(context: &Context, chat: Option) -> Resul if chat.typ == Chattype::OutBroadcast { // For historic reansons, broadcasts currently use j instead of i for the invitenumber. format!( - "https://i.delta.chat/#{fingerprint}&v=3&x={grpid}&j={invitenumber}&s={auth}&a={self_addr_urlencoded}&n={self_name_urlencoded}&b={chat_name_urlencoded}", + "https://i.delta.chat/#{fingerprint}&v=3&x={grpid}&j={invitenumber}&s={auth}&a={self_addr_urlencoded}{r_param}&n={self_name_urlencoded}&b={chat_name_urlencoded}", ) } else { format!( - "https://i.delta.chat/#{fingerprint}&v=3&x={grpid}&i={invitenumber}&s={auth}&a={self_addr_urlencoded}&n={self_name_urlencoded}&g={chat_name_urlencoded}", + "https://i.delta.chat/#{fingerprint}&v=3&x={grpid}&i={invitenumber}&s={auth}&a={self_addr_urlencoded}{r_param}&n={self_name_urlencoded}&g={chat_name_urlencoded}", ) } } else { @@ -182,7 +194,7 @@ pub async fn get_securejoin_qr(context: &Context, chat: Option) -> Resul context.scheduler.interrupt_smtp().await; format!( - "https://i.delta.chat/#{fingerprint}&v=3&i={invitenumber}&s={auth}&a={self_addr_urlencoded}&n={self_name_urlencoded}", + "https://i.delta.chat/#{fingerprint}&v=3&i={invitenumber}&s={auth}&a={self_addr_urlencoded}{r_param}&n={self_name_urlencoded}", ) };