diff --git a/src/calls.rs b/src/calls.rs index 2154354ac..2ab98c055 100644 --- a/src/calls.rs +++ b/src/calls.rs @@ -617,33 +617,7 @@ struct IceServer { pub credential: Option, } -/// Creates JSON with ICE servers. -async fn create_ice_servers( - context: &Context, - hostname: &str, - port: u16, - username: &str, - password: &str, -) -> Result { - // Do not use cache because there is no TLS. - let load_cache = false; - let urls: Vec = lookup_host_with_cache(context, hostname, port, "", load_cache) - .await? - .into_iter() - .map(|addr| format!("turn:{addr}")) - .collect(); - - let ice_server = IceServer { - urls, - username: Some(username.to_string()), - credential: Some(password.to_string()), - }; - - let json = serde_json::to_string(&[ice_server])?; - Ok(json) -} - -/// Creates JSON with ICE servers from a line received over IMAP METADATA. +/// Creates ICE servers from a line received over IMAP METADATA. /// /// IMAP METADATA returns a line such as /// `example.com:3478:1758650868:8Dqkyyu11MVESBqjbIylmB06rv8=` @@ -653,20 +627,107 @@ async fn create_ice_servers( /// while `8Dqkyyu11MVESBqjbIylmB06rv8=` /// is the password. pub(crate) async fn create_ice_servers_from_metadata( - context: &Context, metadata: &str, -) -> Result<(i64, String)> { +) -> Result<(i64, Vec)> { let (hostname, rest) = metadata.split_once(':').context("Missing hostname")?; let (port, rest) = rest.split_once(':').context("Missing port")?; let port = u16::from_str(port).context("Failed to parse the port")?; let (ts, password) = rest.split_once(':').context("Missing timestamp")?; let expiration_timestamp = i64::from_str(ts).context("Failed to parse the timestamp")?; - let ice_servers = create_ice_servers(context, hostname, port, ts, password).await?; + let ice_servers = vec![UnresolvedIceServer::Turn { + hostname: hostname.to_string(), + port, + username: ts.to_string(), + credential: password.to_string(), + }]; Ok((expiration_timestamp, ice_servers)) } +/// STUN or TURN server with unresolved DNS name. +#[derive(Debug, Clone)] +pub(crate) enum UnresolvedIceServer { + /// STUN server. + Stun { hostname: String, port: u16 }, + + /// TURN server with the username and password. + Turn { + hostname: String, + port: u16, + username: String, + credential: String, + }, +} + +/// Resolves domain names of ICE servers. +/// +/// On failure to resolve, logs the error +/// and skips the server, but does not fail. +pub(crate) async fn resolve_ice_servers( + context: &Context, + unresolved_ice_servers: Vec, +) -> Result { + let mut result: Vec = Vec::new(); + + // Do not use cache because there is no TLS. + let load_cache = false; + + for unresolved_ice_server in unresolved_ice_servers { + match unresolved_ice_server { + UnresolvedIceServer::Stun { hostname, port } => { + match lookup_host_with_cache(context, &hostname, port, "", load_cache).await { + Ok(addrs) => { + let urls: Vec = addrs + .into_iter() + .map(|addr| format!("stun:{addr}")) + .collect(); + let stun_server = IceServer { + urls, + username: None, + credential: None, + }; + result.push(stun_server); + } + Err(err) => { + warn!( + context, + "Failed to resolve STUN {hostname}:{port}: {err:#}." + ); + } + } + } + UnresolvedIceServer::Turn { + hostname, + port, + username, + credential, + } => match lookup_host_with_cache(context, &hostname, port, "", load_cache).await { + Ok(addrs) => { + let urls: Vec = addrs + .into_iter() + .map(|addr| format!("turn:{addr}")) + .collect(); + let turn_server = IceServer { + urls, + username: Some(username), + credential: Some(credential), + }; + result.push(turn_server); + } + Err(err) => { + warn!( + context, + "Failed to resolve TURN {hostname}:{port}: {err:#}." + ); + } + }, + } + } + let json = serde_json::to_string(&result)?; + Ok(json) +} + /// Creates JSON with ICE servers when no TURN servers are known. -pub(crate) async fn create_fallback_ice_servers(context: &Context) -> Result { +pub(crate) fn create_fallback_ice_servers() -> Vec { // Do not use public STUN server from https://stunprotocol.org/. // It changes the hostname every year // (e.g. stunserver2025.stunprotocol.org @@ -674,36 +735,18 @@ pub(crate) async fn create_fallback_ice_servers(context: &Context) -> Result - let hostname = "nine.testrun.org"; - // Do not use cache because there is no TLS. - let load_cache = false; - let urls: Vec = lookup_host_with_cache(context, hostname, STUN_PORT, "", load_cache) - .await? - .into_iter() - .map(|addr| format!("stun:{addr}")) - .collect(); - let stun_server = IceServer { - urls, - username: None, - credential: None, - }; - - let hostname = "turn.delta.chat"; - // Do not use cache because there is no TLS. - let load_cache = false; - let urls: Vec = lookup_host_with_cache(context, hostname, STUN_PORT, "", load_cache) - .await? - .into_iter() - .map(|addr| format!("turn:{addr}")) - .collect(); - let turn_server = IceServer { - urls, - username: Some("public".to_string()), - credential: Some("o4tR7yG4rG2slhXqRUf9zgmHz".to_string()), - }; - - let json = serde_json::to_string(&[stun_server, turn_server])?; - Ok(json) + vec![ + UnresolvedIceServer::Stun { + hostname: "nine.testrun.org".to_string(), + port: STUN_PORT, + }, + UnresolvedIceServer::Turn { + hostname: "turn.delta.chat".to_string(), + port: STUN_PORT, + username: "public".to_string(), + credential: "o4tR7yG4rG2slhXqRUf9zgmHz".to_string(), + }, + ] } /// Returns JSON with ICE servers. @@ -717,7 +760,8 @@ pub(crate) async fn create_fallback_ice_servers(context: &Context) -> Result. pub async fn ice_servers(context: &Context) -> Result { if let Some(ref metadata) = *context.metadata.read().await { - Ok(metadata.ice_servers.clone()) + let ice_servers = resolve_ice_servers(context, metadata.ice_servers.clone()).await?; + Ok(ice_servers) } else { Ok("[]".to_string()) } diff --git a/src/imap.rs b/src/imap.rs index 0e20293e9..4b6a49af7 100644 --- a/src/imap.rs +++ b/src/imap.rs @@ -23,7 +23,9 @@ use num_traits::FromPrimitive; use ratelimit::Ratelimit; use url::Url; -use crate::calls::{create_fallback_ice_servers, create_ice_servers_from_metadata}; +use crate::calls::{ + UnresolvedIceServer, create_fallback_ice_servers, create_ice_servers_from_metadata, +}; use crate::chat::{self, ChatId, ChatIdBlocked, add_device_msg}; use crate::chatlist_events; use crate::config::Config; @@ -134,16 +136,15 @@ pub(crate) struct ServerMetadata { pub iroh_relay: Option, - /// JSON with ICE servers for WebRTC calls - /// and the expiration timestamp. - /// - /// If JSON is about to expire, new TURN credentials - /// should be fetched from the server - /// to be ready for WebRTC calls. - pub ice_servers: String, + /// ICE servers for WebRTC calls. + pub ice_servers: Vec, /// Timestamp when ICE servers are considered /// expired and should be updated. + /// + /// If ICE servers are about to expire, new TURN credentials + /// should be fetched from the server + /// to be ready for WebRTC calls. pub ice_servers_expiration_timestamp: i64, } @@ -1552,7 +1553,7 @@ impl Session { if m.entry == "/shared/vendor/deltachat/turn" && let Some(value) = m.value { - match create_ice_servers_from_metadata(context, &value).await { + match create_ice_servers_from_metadata(&value).await { Ok((parsed_timestamp, parsed_ice_servers)) => { old_metadata.ice_servers_expiration_timestamp = parsed_timestamp; old_metadata.ice_servers = parsed_ice_servers; @@ -1569,7 +1570,7 @@ impl Session { info!(context, "Will use fallback ICE servers."); // Set expiration timestamp 7 days in the future so we don't request it again. old_metadata.ice_servers_expiration_timestamp = time() + 3600 * 24 * 7; - old_metadata.ice_servers = create_fallback_ice_servers(context).await?; + old_metadata.ice_servers = create_fallback_ice_servers(); } return Ok(()); } @@ -1616,7 +1617,7 @@ impl Session { } "/shared/vendor/deltachat/turn" => { if let Some(value) = m.value { - match create_ice_servers_from_metadata(context, &value).await { + match create_ice_servers_from_metadata(&value).await { Ok((parsed_timestamp, parsed_ice_servers)) => { ice_servers_expiration_timestamp = parsed_timestamp; ice_servers = Some(parsed_ice_servers); @@ -1635,7 +1636,7 @@ impl Session { } else { // Set expiration timestamp 7 days in the future so we don't request it again. ice_servers_expiration_timestamp = time() + 3600 * 24 * 7; - create_fallback_ice_servers(context).await? + create_fallback_ice_servers() }; *lock = Some(ServerMetadata {