mirror of
https://github.com/chatmail/core.git
synced 2026-09-22 04:58:47 +03:00
api: add init_transports() for multi-relay onboarding
Closes #8693 and supsersedes #8707 from which the API and some overall shape of this commit is inspired. UIs call `init_transports(None)` or `init_transports(qr)` to initialize a first transport on the fresh profile, with more transports added in the background later.
This commit is contained in:
@@ -546,6 +546,18 @@ impl CommandApi {
|
||||
ctx.add_transport_from_qr(&qr).await
|
||||
}
|
||||
|
||||
/// Adds an initial transport on a randomly chosen chatmail relay
|
||||
/// and lets the profile add further ones in the background.
|
||||
///
|
||||
/// A `DCACCOUNT:` or `DCLOGIN:` `qr` code adds a single transport
|
||||
/// while securejoin codes add the inviter's relays to the candidates.
|
||||
///
|
||||
/// Does nothing if the profile already has a transport.
|
||||
async fn init_transports(&self, account_id: u32, qr: Option<String>) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
ctx.init_transports(qr.as_deref()).await
|
||||
}
|
||||
|
||||
/// Returns the list of all email accounts that are used as a transport in the current profile.
|
||||
/// Use [Self::add_or_update_transport()] to add or change a transport
|
||||
/// and [Self::delete_transport()] to remove a transport.
|
||||
|
||||
@@ -139,6 +139,18 @@ class Account:
|
||||
"""Add a new transport using a QR code."""
|
||||
yield self._rpc.add_transport_from_qr.future(self.id, qr)
|
||||
|
||||
@futuremethod
|
||||
def init_transports(self, qr: Optional[str] = None):
|
||||
"""Add an initial transport on a randomly chosen chatmail relay.
|
||||
|
||||
The profile then adds further ones in the background.
|
||||
A ``DCACCOUNT:`` or ``DCLOGIN:`` ``qr`` code adds a single transport
|
||||
while securejoin codes add the inviter's relays to the candidates.
|
||||
|
||||
Does nothing if the profile already has a transport.
|
||||
"""
|
||||
yield self._rpc.init_transports.future(self.id, qr)
|
||||
|
||||
def delete_transport(self, addr: str):
|
||||
"""Delete a transport."""
|
||||
self._rpc.delete_transport(self.id, addr)
|
||||
|
||||
@@ -24,6 +24,12 @@ def wait_for_imap_message(imap):
|
||||
time.sleep(1)
|
||||
|
||||
|
||||
def test_init_transports(acf):
|
||||
account = acf.get_unconfigured_account()
|
||||
account.init_transports(acf.get_account_qr())
|
||||
assert len(account.list_transports()) == 1
|
||||
|
||||
|
||||
def test_add_second_address(acf) -> None:
|
||||
account = acf.new_configured_account()
|
||||
assert len(account.list_transports()) == 1
|
||||
|
||||
@@ -630,9 +630,12 @@ CREATE TABLE broadcast_secrets(
|
||||
|
||||
|
||||
-- Candidate chatmail relays for automatic relay management.
|
||||
-- Holds the hosts a QR code contributed and the default relays already tried;
|
||||
-- the default list itself lives in `autorelay.rs` and is not stored here.
|
||||
CREATE TABLE relay_candidates(
|
||||
host TEXT PRIMARY KEY NOT NULL,
|
||||
last_tried INTEGER NOT NULL DEFAULT 0 -- Timestamp of the last connection attempt.
|
||||
-- Timestamp of the last connection attempt, 0 if the host was never tried.
|
||||
last_tried INTEGER NOT NULL DEFAULT 0
|
||||
) STRICT;
|
||||
|
||||
CREATE TABLE transports (
|
||||
|
||||
149
src/autorelay.rs
149
src/autorelay.rs
@@ -1,29 +1,22 @@
|
||||
//! # Automatic relay handling (experimental, still in development)
|
||||
//! # Automatic multi-relay onboarding
|
||||
//!
|
||||
//! Chatmail relays create an account on first login,
|
||||
//! so a profile can add further transports on its own without user interaction.
|
||||
//! Candidate hosts come from the `relay_candidates` table,
|
||||
//! which migrations seed with a list of known chatmail relays.
|
||||
//!
|
||||
//! Status of implementation:
|
||||
//! Additions are attempted right before going into IMAP IDLE,
|
||||
//! i.e. only while connected and with nothing more important to do,
|
||||
//! and only if a UI opted in via [`Config::Autorelay`].
|
||||
//! Once a profile has reached `NUM_TRANSPORTS_TARGET` transports,
|
||||
//! [`Config::AutorelayFinished`] is set and nothing is ever added again,
|
||||
//! so deleting a transport later does not pull in a replacement.
|
||||
//! Support for automatically onboarding a profile on transport
|
||||
//! candidates without the user choosing a relay.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::pin::Pin;
|
||||
|
||||
use anyhow::Result;
|
||||
use anyhow::{Result, format_err};
|
||||
use deltachat_contact_tools::addr_normalize;
|
||||
use rand::distr::{Alphanumeric, SampleString};
|
||||
use rand::seq::IndexedRandom;
|
||||
use rand::seq::{IndexedRandom, SliceRandom};
|
||||
use rusqlite::Transaction;
|
||||
|
||||
use crate::config::{self, Config};
|
||||
use crate::configure::{EnteredLoginParam, configure};
|
||||
use crate::log::{LogExt, warn};
|
||||
use crate::login_param::{EnteredCertificateChecks, EnteredImapLoginParam};
|
||||
use crate::{configure::EnteredLoginParam, context::Context, tools::time};
|
||||
use crate::{context::Context, tools::time};
|
||||
|
||||
/// The target number of transports.
|
||||
const NUM_TRANSPORTS_TARGET: usize = 3;
|
||||
@@ -32,6 +25,65 @@ const AUTOMATIC_ADDITION_DEBOUNCE_SECONDS: i64 = 60 * 60; // one hour
|
||||
/// How long we ignore a relay candidate after failing to connect to it:
|
||||
const BACKOFF_PERIOD_FOR_NOT_WORKING_RELAY: i64 = 60 * 60 * 24 * 7; // one week
|
||||
|
||||
/// Sorted relay list a profile can attempt to onboard on without the user choosing one.
|
||||
const DEFAULT_RELAY_CANDIDATES: &[&str] = &[
|
||||
"chat.adminforge.de", // iroh relay 404s
|
||||
"chat.me.ke",
|
||||
"chat.nuvon.app",
|
||||
"chat.tinydispatch.org",
|
||||
"chat.vim.wtf",
|
||||
"chatmail.uk",
|
||||
"chtml.ca",
|
||||
"deltachat.me",
|
||||
"e2e.sus.fr",
|
||||
"e2ee.wang",
|
||||
"mailchat.pl",
|
||||
"nchrcht.la10cy.net",
|
||||
"nine.testrun.org",
|
||||
"sweetfern.net",
|
||||
"tarpit.fun",
|
||||
];
|
||||
|
||||
/// Records the hosts of `addrs` as relay candidates.
|
||||
pub(crate) async fn add_relay_candidates(context: &Context, addrs: &[String]) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
.transaction(|tx| hosts_of(addrs).try_for_each(|host| save_relay_candidate(tx, host, 0)))
|
||||
.await
|
||||
}
|
||||
|
||||
/// Adds a first transport on the relay candidate that answers fastest.
|
||||
///
|
||||
/// All candidates are probed at once with a TCP connection to their HTTPS port
|
||||
/// and configured in the order in which the connections complete,
|
||||
/// stopping at the first success. Candidates that fail the probe are skipped.
|
||||
pub(crate) async fn add_transport_from_candidates(
|
||||
context: &Context,
|
||||
skip_network: bool,
|
||||
) -> Result<()> {
|
||||
let mut candidates = triable_relay_candidates(context, time()).await?;
|
||||
candidates.shuffle(&mut rand::rng());
|
||||
let mut last_err = format_err!("No relay candidates");
|
||||
|
||||
// We patiently try each candidate in turn which might take a while
|
||||
// if many hosts are unreachable but eventually succeeds if one candidate works.
|
||||
let mark_as_autorelay = true;
|
||||
for host in &candidates {
|
||||
let param = login_param_from_host(host, mark_as_autorelay);
|
||||
match configure(context, ¶m, skip_network).await {
|
||||
Ok(()) => {
|
||||
info!(context, "Added a transport on relay {host}.");
|
||||
return Ok(());
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(context, "Failed to add relay {host}: {err:#}.");
|
||||
last_err = err;
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(last_err)
|
||||
}
|
||||
|
||||
pub(crate) fn maybe_add_additional_relays(
|
||||
context: Context,
|
||||
) -> Pin<Box<dyn Future<Output = ()> + Send>> {
|
||||
@@ -93,10 +145,7 @@ async fn maybe_add_additional_relays_inner(context: &Context, skip_network: bool
|
||||
return Ok(relay_added);
|
||||
}
|
||||
|
||||
// First, query all candidates that were not tried since `BACKOFF_PERIOD_FOR_NOT_WORKING_RELAY` seconds.
|
||||
// Hosts that are already used are excluded.
|
||||
let candidates = load_relay_candidates(context, now).await?;
|
||||
|
||||
let candidates = triable_relay_candidates(context, now).await?;
|
||||
let Some(host) = candidates.choose(&mut rand::rng()) else {
|
||||
info!(
|
||||
context,
|
||||
@@ -113,14 +162,11 @@ async fn maybe_add_additional_relays_inner(context: &Context, skip_network: bool
|
||||
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE relay_candidates SET last_tried=? WHERE host=?",
|
||||
(now, host),
|
||||
)
|
||||
.transaction(|tx| save_relay_candidate(tx, host, now))
|
||||
.await?;
|
||||
let mark_as_autorelay = true;
|
||||
let param = login_param_from_host(host, mark_as_autorelay);
|
||||
let res = crate::configure::configure(context, ¶m, skip_network).await;
|
||||
let res = configure(context, ¶m, skip_network).await;
|
||||
if let Err(e) = res {
|
||||
warn!(
|
||||
context,
|
||||
@@ -135,30 +181,49 @@ async fn maybe_add_additional_relays_inner(context: &Context, skip_network: bool
|
||||
Ok(relay_added)
|
||||
}
|
||||
|
||||
async fn load_relay_candidates(context: &Context, now: i64) -> Result<Vec<String>, anyhow::Error> {
|
||||
async fn triable_relay_candidates(context: &Context, now: i64) -> Result<Vec<String>> {
|
||||
let cutoff_timestamp = now.saturating_sub(BACKOFF_PERIOD_FOR_NOT_WORKING_RELAY);
|
||||
let candidates: Vec<String> = context
|
||||
let mut last_tried: BTreeMap<String, i64> = context
|
||||
.sql
|
||||
.query_map_vec(
|
||||
// This also selects candidates which have last_tried in the future,
|
||||
// essentially treating them as never tried,
|
||||
// so if some timestamp far in the future is accidentally stored,
|
||||
// we are not stuck never trying the candidate.
|
||||
// After trying the candidate, last_tried will be corrected to the current time.
|
||||
"SELECT host FROM relay_candidates WHERE (last_tried<? OR last_tried>?)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM transports
|
||||
WHERE substr(addr, instr(addr, '@') + 1) = host
|
||||
)",
|
||||
(cutoff_timestamp, now),
|
||||
|row| Ok(row.get::<_, String>(0)?),
|
||||
)
|
||||
.query_map_collect("SELECT host, last_tried FROM relay_candidates", (), |row| {
|
||||
Ok((row.get(0)?, row.get(1)?))
|
||||
})
|
||||
.await?;
|
||||
for host in DEFAULT_RELAY_CANDIDATES {
|
||||
last_tried.entry(host.to_string()).or_insert(0);
|
||||
}
|
||||
let self_addrs = context.get_self_addrs().await?;
|
||||
let used_hosts: Vec<&str> = hosts_of(&self_addrs).collect();
|
||||
|
||||
// We also try candidates which have `last_tried` in the future,
|
||||
// which on next failure get `last_tried` reset to the current time.
|
||||
let candidates = last_tried
|
||||
.into_iter()
|
||||
.filter(|(host, last_tried)| {
|
||||
(*last_tried < cutoff_timestamp || *last_tried > now)
|
||||
&& !used_hosts.contains(&host.as_str())
|
||||
})
|
||||
.map(|(host, _)| host)
|
||||
.collect();
|
||||
|
||||
Ok(candidates)
|
||||
}
|
||||
|
||||
/// Returns the host of each address in `addrs`.
|
||||
fn hosts_of(addrs: &[String]) -> impl Iterator<Item = &str> {
|
||||
addrs.iter().filter_map(|a| Some(a.rsplit_once('@')?.1))
|
||||
}
|
||||
|
||||
/// Records `host` as a relay candidate, overwriting a stored `last_tried`.
|
||||
fn save_relay_candidate(tx: &Transaction, host: &str, last_tried: i64) -> Result<()> {
|
||||
tx.execute(
|
||||
"INSERT INTO relay_candidates (host, last_tried) VALUES (?, ?)
|
||||
ON CONFLICT(host) DO UPDATE SET last_tried=excluded.last_tried",
|
||||
(host, last_tried),
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn login_param_from_host(host: &str, mark_as_autorelay: bool) -> EnteredLoginParam {
|
||||
let rng = &mut rand::rng();
|
||||
let username = Alphanumeric.sample_string(rng, 9);
|
||||
|
||||
@@ -1,42 +1,140 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use super::*;
|
||||
use crate::test_utils::TestContext;
|
||||
use crate::test_utils::{TestContext, TestContextManager};
|
||||
use crate::tools::SystemTime;
|
||||
|
||||
/// Tests that the default relays are candidates without a row in the table.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_load_relay_candidates_single() -> Result<()> {
|
||||
async fn test_triable_relay_candidates_defaults() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let t = &tcm.unconfigured().await;
|
||||
let now = time();
|
||||
|
||||
assert!(DEFAULT_RELAY_CANDIDATES.is_sorted());
|
||||
let mut candidates = triable_relay_candidates(t, now).await?;
|
||||
candidates.sort();
|
||||
assert_eq!(candidates, DEFAULT_RELAY_CANDIDATES);
|
||||
|
||||
let tried = DEFAULT_RELAY_CANDIDATES[0];
|
||||
save_relay_candidates(t, &[tried], now).await?;
|
||||
let candidates = triable_relay_candidates(t, now).await?;
|
||||
assert_eq!(candidates.len(), DEFAULT_RELAY_CANDIDATES.len() - 1);
|
||||
assert!(!candidates.contains(&tried.to_string()));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that a transport is added on a candidate from the given addresses.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_add_transport_from_candidates() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let t = &tcm.unconfigured().await;
|
||||
mark_defaults_tried(t, time()).await?;
|
||||
let addrs_from_qr = [
|
||||
"alice@example.org".to_string(),
|
||||
"bob@example.org".to_string(),
|
||||
];
|
||||
let skip_network = true;
|
||||
add_relay_candidates(t, &addrs_from_qr).await?;
|
||||
add_transport_from_candidates(t, skip_network).await?;
|
||||
|
||||
let transports = t.list_transports().await?;
|
||||
assert_eq!(transports.len(), 1);
|
||||
assert!(transports[0].addr.ends_with("@example.org"));
|
||||
let untried = untried_relay_candidates(t).await?;
|
||||
assert_eq!(untried, ["example.org"]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests correct add_transport_from_candidates error handling.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_add_transport_from_candidates_failure() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let t = &tcm.unconfigured().await;
|
||||
mark_defaults_tried(t, time()).await?;
|
||||
save_relay_candidates(t, &["bad host", "worse host"], 0).await?;
|
||||
|
||||
let skip_network = false;
|
||||
let err = add_transport_from_candidates(t, skip_network)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(format!("{err:#}").contains("Bad email-address"));
|
||||
assert!(!t.is_configured().await?);
|
||||
let untried = untried_relay_candidates(t).await?;
|
||||
assert_eq!(untried, ["bad host", "worse host"]);
|
||||
t.assert_warns_or_errors(&[
|
||||
"Failed to add relay bad host",
|
||||
"Failed to add relay worse host",
|
||||
])
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn untried_relay_candidates(t: &TestContext) -> Result<Vec<String>> {
|
||||
t.sql
|
||||
.query_map_vec(
|
||||
"SELECT host FROM relay_candidates WHERE last_tried=0 ORDER BY host",
|
||||
(),
|
||||
|row| Ok(row.get(0)?),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn save_relay_candidates(t: &TestContext, hosts: &[&str], last_tried: i64) -> Result<()> {
|
||||
t.sql
|
||||
.transaction(|tx| {
|
||||
for host in hosts {
|
||||
save_relay_candidate(tx, host, last_tried)?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// Keeps the default relays out of `triable_relay_candidates()`.
|
||||
async fn mark_defaults_tried(t: &TestContext, now: i64) -> Result<()> {
|
||||
save_relay_candidates(t, DEFAULT_RELAY_CANDIDATES, now).await
|
||||
}
|
||||
|
||||
/// Tests that saving a candidate overwrites its stored timestamp.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_save_relay_candidate() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let t = &tcm.unconfigured().await;
|
||||
let now = time();
|
||||
|
||||
for last_tried in [0, now, 0] {
|
||||
t.sql
|
||||
.transaction(|tx| save_relay_candidate(tx, "relay.example", last_tried))
|
||||
.await?;
|
||||
let stored: Option<i64> = t
|
||||
.sql
|
||||
.query_get_value(
|
||||
"SELECT last_tried FROM relay_candidates WHERE host=?",
|
||||
("relay.example",),
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(stored, Some(last_tried));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_triable_relay_candidates_single() -> Result<()> {
|
||||
let t = &TestContext::new_alice().await;
|
||||
enable_config(t).await;
|
||||
let now = time();
|
||||
|
||||
t.sql.execute("DELETE FROM relay_candidates", ()).await?;
|
||||
mark_defaults_tried(t, now).await?;
|
||||
|
||||
// This host should be returned by load_relay_candidates():
|
||||
t.sql
|
||||
.execute(
|
||||
"INSERT INTO relay_candidates (host, last_tried) VALUES (?, ?)",
|
||||
("never_tried.example", 0),
|
||||
)
|
||||
.await?;
|
||||
save_relay_candidates(t, &["never_tried.example", "example.org"], 0).await?;
|
||||
save_relay_candidates(t, &["recent.example"], now).await?;
|
||||
|
||||
// This host was recently tried and should not be returned:
|
||||
t.sql
|
||||
.execute(
|
||||
"INSERT INTO relay_candidates (host, last_tried) VALUES (?, ?)",
|
||||
("recent.example", now),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// This host is already in use (alice@example.org) and should not be returned:
|
||||
t.sql
|
||||
.execute(
|
||||
"INSERT INTO relay_candidates (host, last_tried) VALUES (?, ?)",
|
||||
("example.org", 0),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let candidates = load_relay_candidates(t, now).await?;
|
||||
let candidates = triable_relay_candidates(t, now).await?;
|
||||
|
||||
assert_eq!(candidates, vec!["never_tried.example".to_string()]);
|
||||
|
||||
@@ -44,22 +142,15 @@ async fn test_load_relay_candidates_single() -> Result<()> {
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_load_relay_candidates_multiple() -> Result<()> {
|
||||
async fn test_triable_relay_candidates_multiple() -> Result<()> {
|
||||
let t = &TestContext::new().await;
|
||||
enable_config(t).await;
|
||||
let now = time();
|
||||
|
||||
t.sql.execute("DELETE FROM relay_candidates", ()).await?;
|
||||
for host in ["a.example", "b.example", "c.example"] {
|
||||
t.sql
|
||||
.execute(
|
||||
"INSERT INTO relay_candidates (host, last_tried) VALUES (?, ?)",
|
||||
(host, 0),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
mark_defaults_tried(t, now).await?;
|
||||
save_relay_candidates(t, &["a.example", "b.example", "c.example"], 0).await?;
|
||||
|
||||
let mut candidates = load_relay_candidates(t, now).await?;
|
||||
let mut candidates = triable_relay_candidates(t, now).await?;
|
||||
candidates.sort();
|
||||
|
||||
assert_eq!(
|
||||
@@ -160,13 +251,8 @@ async fn test_maybe_add_additional_relays_add_one() -> Result<()> {
|
||||
enable_config(t).await;
|
||||
let now = time();
|
||||
|
||||
t.sql.execute("DELETE FROM relay_candidates", ()).await?;
|
||||
t.sql
|
||||
.execute(
|
||||
"INSERT INTO relay_candidates (host, last_tried) VALUES (?, ?)",
|
||||
("relay.example", 0),
|
||||
)
|
||||
.await?;
|
||||
mark_defaults_tried(t, now).await?;
|
||||
save_relay_candidates(t, &["relay.example"], 0).await?;
|
||||
|
||||
let transports_before = t.count_transports().await?;
|
||||
|
||||
@@ -189,15 +275,8 @@ async fn test_maybe_add_additional_relays_add_multiple() -> Result<()> {
|
||||
enable_config(t).await;
|
||||
let now = time();
|
||||
|
||||
t.sql.execute("DELETE FROM relay_candidates", ()).await?;
|
||||
for host in ["a.example", "b.example", "c.example", "d.example"] {
|
||||
t.sql
|
||||
.execute(
|
||||
"INSERT INTO relay_candidates (host, last_tried) VALUES (?, ?)",
|
||||
(host, 0),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
mark_defaults_tried(t, now).await?;
|
||||
save_relay_candidates(t, &["a.example", "b.example", "c.example", "d.example"], 0).await?;
|
||||
|
||||
let skip_network = true;
|
||||
let relay_added = maybe_add_additional_relays_inner(t, skip_network).await?;
|
||||
@@ -218,14 +297,9 @@ async fn test_maybe_add_additional_relays_failure() -> Result<()> {
|
||||
enable_config(t).await;
|
||||
let now = time();
|
||||
|
||||
t.sql.execute("DELETE FROM relay_candidates", ()).await?;
|
||||
mark_defaults_tried(t, now).await?;
|
||||
for i in 1..10 {
|
||||
t.sql
|
||||
.execute(
|
||||
"INSERT INTO relay_candidates (host, last_tried) VALUES (?, ?)",
|
||||
(format!("{i}.invalid.example"), 0),
|
||||
)
|
||||
.await?;
|
||||
save_relay_candidates(t, &[format!("{i}.invalid.example").as_str()], 0).await?;
|
||||
}
|
||||
|
||||
let transports_before = t.count_transports().await?;
|
||||
@@ -254,7 +328,7 @@ async fn test_maybe_add_additional_relays_failure() -> Result<()> {
|
||||
|
||||
// ...but not all, because there might be many relay candidates
|
||||
// and we don't want to try all of them in a single call:
|
||||
assert_eq!(load_relay_candidates(t, now).await?.is_empty(), false);
|
||||
assert_eq!(triable_relay_candidates(t, now).await?.is_empty(), false);
|
||||
|
||||
t.assert_warns_or_errors(&[
|
||||
"DNS lookup with memory cache failure",
|
||||
|
||||
@@ -31,7 +31,7 @@ use crate::login_param::EnteredCertificateChecks;
|
||||
pub use crate::login_param::EnteredLoginParam;
|
||||
use crate::net::proxy::ProxyConfig;
|
||||
use crate::provider::{self, Protocol, Socket};
|
||||
use crate::qr::{login_param_from_account_qr, login_param_from_login_qr};
|
||||
use crate::qr::{Qr, check_qr, login_param_from_account_qr, login_param_from_login_qr};
|
||||
use crate::smtp::Smtp;
|
||||
use crate::sync::Sync::Nosync;
|
||||
use crate::tools::time;
|
||||
@@ -40,7 +40,7 @@ use crate::transport::{
|
||||
ConnectionCandidate, delete_transport_row, maybe_update_sending_transport,
|
||||
purge_transport_caches, send_sync_transports, transport_addrs,
|
||||
};
|
||||
use crate::{EventType, stock_str};
|
||||
use crate::{EventType, autorelay, stock_str};
|
||||
|
||||
/// Maximum number of relays.
|
||||
///
|
||||
@@ -189,6 +189,62 @@ impl Context {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Adds an initial transport on a randomly chosen chatmail relay
|
||||
/// and lets the profile add further ones in the background.
|
||||
///
|
||||
/// A `DCACCOUNT:` or `DCLOGIN:` `qr` code adds a single transport
|
||||
/// while securejoin codes add the inviter's relays to the candidates.
|
||||
///
|
||||
/// Does nothing if the profile already has a transport.
|
||||
pub async fn init_transports(&self, qr: Option<&str>) -> Result<()> {
|
||||
if self.is_configured().await? {
|
||||
return Ok(());
|
||||
}
|
||||
if let Some(qr) = qr {
|
||||
match check_qr(self, qr).await? {
|
||||
Qr::Account { .. } | Qr::Login { .. } => {
|
||||
return self.add_transport_from_qr(qr).await;
|
||||
}
|
||||
Qr::AskVerifyContact { addrs, .. }
|
||||
| Qr::AskVerifyGroup { addrs, .. }
|
||||
| Qr::AskJoinBroadcast { addrs, .. } => {
|
||||
autorelay::add_relay_candidates(self, &addrs).await?
|
||||
}
|
||||
_ => bail!("QR code does not contain a relay"),
|
||||
}
|
||||
}
|
||||
|
||||
let cancel_channel = self.alloc_ongoing().await?;
|
||||
let skip_network = false;
|
||||
let res = autorelay::add_transport_from_candidates(self, skip_network)
|
||||
.race(cancel_channel.recv().map(|_| Err(format_err!("Canceled"))))
|
||||
.await;
|
||||
self.free_ongoing().await;
|
||||
|
||||
let configured = self.is_configured().await?;
|
||||
match res {
|
||||
Ok(()) => {}
|
||||
Err(err) if configured => {
|
||||
warn!(
|
||||
self,
|
||||
"Onboarding interrupted after adding a transport: {err:#}."
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
let error_msg = stock_str::configuration_failed(self, &format!("{err:#}"));
|
||||
self.emit_event(EventType::ConfigureProgress {
|
||||
progress: 0,
|
||||
comment: Some(error_msg.clone()),
|
||||
});
|
||||
bail!(error_msg);
|
||||
}
|
||||
}
|
||||
self.set_config_bool(Config::Autorelay, true).await?;
|
||||
emit_progress(self, 1000);
|
||||
self.start_io().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns the list of all email accounts that are used as a transport in the current profile.
|
||||
/// Use [Self::add_or_update_transport()] to add or change a transport
|
||||
/// and [Self::delete_transport()] to delete a transport.
|
||||
@@ -689,6 +745,28 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that init_transports() fails on a bad code
|
||||
/// and does nothing on a profile that already has a transport.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_init_transports() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let t = &tcm.unconfigured().await;
|
||||
assert!(t.init_transports(Some("not a qr code")).await.is_err());
|
||||
assert!(!t.is_configured().await?);
|
||||
|
||||
let alice = &tcm.alice().await;
|
||||
let invite = "openpgp4fpr:79252762C34C5096AF57958F4FC3D21A81B0F0A7#a=cli%40invite.example&i=TbnwJ6lSvD5&s=0ejvbdFSQxB";
|
||||
alice.init_transports(Some(invite)).await?;
|
||||
let candidates = alice
|
||||
.sql
|
||||
.count("SELECT COUNT(*) FROM relay_candidates", ())
|
||||
.await?;
|
||||
assert_eq!(candidates, 0);
|
||||
assert_eq!(alice.count_transports().await?, 1);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_configured_param() -> Result<()> {
|
||||
let t = &TestContext::new().await;
|
||||
|
||||
@@ -13,6 +13,7 @@ use serde::Deserialize;
|
||||
|
||||
use crate::autorelay::login_param_from_host;
|
||||
use crate::config::Config;
|
||||
use crate::configure::MAX_RELAYS;
|
||||
use crate::contact::{Contact, ContactId, Origin};
|
||||
use crate::context::Context;
|
||||
use crate::key::Fingerprint;
|
||||
@@ -500,7 +501,8 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
|
||||
addrs.push(normalize_address(primary_addr)?);
|
||||
};
|
||||
if let Some(secondary_addrs_raw) = param.get("r") {
|
||||
for secondary_address in secondary_addrs_raw.split(',') {
|
||||
let max_secondary = MAX_RELAYS.saturating_sub(addrs.len());
|
||||
for secondary_address in secondary_addrs_raw.split(',').take(max_secondary) {
|
||||
addrs.push(normalize_address(secondary_address)?)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -361,6 +361,23 @@ async fn test_decode_openpgp_secure_join() -> Result<()> {
|
||||
bail!("Wrong QR code type");
|
||||
}
|
||||
|
||||
// A bad invite code must not be able to steer us onto arbitrarily many relays.
|
||||
let relays: Vec<String> = (0..20).map(|i| format!("cli%40r{i}.example.org")).collect();
|
||||
let qr = check_qr(
|
||||
&ctx.ctx,
|
||||
&format!(
|
||||
"openpgp4fpr:79252762C34C5096AF57958F4FC3D21A81B0F0A7#a=cli%40deltachat.de&r={}&i=TbnwJ6lSvD5&s=0ejvbdFSQxB",
|
||||
relays.join(",")
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Qr::AskVerifyContact { addrs, .. } = qr {
|
||||
assert_eq!(addrs.len(), MAX_RELAYS);
|
||||
} else {
|
||||
bail!("Wrong QR code type");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -2673,6 +2673,13 @@ CREATE TABLE smtp2 (
|
||||
.await?;
|
||||
}
|
||||
|
||||
inc_and_check(&mut migration_version, 167)?;
|
||||
if dbversion < migration_version {
|
||||
// The default relay candidates are no longer seeded into the table.
|
||||
sql.execute_migration("DELETE FROM relay_candidates", migration_version)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let new_version = sql
|
||||
.get_raw_config_int(VERSION_CFG)
|
||||
.await?
|
||||
|
||||
Reference in New Issue
Block a user