mirror of
https://github.com/chatmail/core.git
synced 2026-05-02 21:06:31 +03:00
feat: get ICE servers from IMAP METADATA
This commit is contained in:
93
src/calls.rs
93
src/calls.rs
@@ -18,6 +18,7 @@ use anyhow::{Context as _, Result, ensure};
|
|||||||
use sdp::SessionDescription;
|
use sdp::SessionDescription;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::io::Cursor;
|
use std::io::Cursor;
|
||||||
|
use std::str::FromStr;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tokio::task;
|
use tokio::task;
|
||||||
use tokio::time::sleep;
|
use tokio::time::sleep;
|
||||||
@@ -38,6 +39,8 @@ const RINGING_SECONDS: i64 = 60;
|
|||||||
const CALL_ACCEPTED_TIMESTAMP: Param = Param::Arg;
|
const CALL_ACCEPTED_TIMESTAMP: Param = Param::Arg;
|
||||||
const CALL_ENDED_TIMESTAMP: Param = Param::Arg4;
|
const CALL_ENDED_TIMESTAMP: Param = Param::Arg4;
|
||||||
|
|
||||||
|
const STUN_PORT: u16 = 3478;
|
||||||
|
|
||||||
/// Set if incoming call was ended explicitly
|
/// Set if incoming call was ended explicitly
|
||||||
/// by the other side before we accepted it.
|
/// by the other side before we accepted it.
|
||||||
///
|
///
|
||||||
@@ -536,21 +539,14 @@ struct IceServer {
|
|||||||
pub credential: Option<String>,
|
pub credential: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns JSON with ICE servers.
|
/// Creates JSON with ICE servers.
|
||||||
///
|
async fn create_ice_servers(
|
||||||
/// <https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/RTCPeerConnection#iceservers>
|
context: &Context,
|
||||||
///
|
hostname: &str,
|
||||||
/// All returned servers are resolved to their IP addresses.
|
port: u16,
|
||||||
/// The primary point of DNS lookup is that Delta Chat Desktop
|
username: &str,
|
||||||
/// relies on the servers being specified by IP,
|
password: &str,
|
||||||
/// because it itself cannot utilize DNS. See
|
) -> Result<String> {
|
||||||
/// <https://github.com/deltachat/deltachat-desktop/issues/5447>.
|
|
||||||
pub async fn ice_servers(context: &Context) -> Result<String> {
|
|
||||||
let hostname = "ci-chatmail.testrun.org";
|
|
||||||
let port = 3478;
|
|
||||||
let username = "ohV8aec1".to_string();
|
|
||||||
let password = "zo3theiY".to_string();
|
|
||||||
|
|
||||||
// Do not use cache because there is no TLS.
|
// Do not use cache because there is no TLS.
|
||||||
let load_cache = false;
|
let load_cache = false;
|
||||||
let urls: Vec<String> = lookup_host_with_cache(context, hostname, port, "", load_cache)
|
let urls: Vec<String> = lookup_host_with_cache(context, hostname, port, "", load_cache)
|
||||||
@@ -561,13 +557,76 @@ pub async fn ice_servers(context: &Context) -> Result<String> {
|
|||||||
|
|
||||||
let ice_server = IceServer {
|
let ice_server = IceServer {
|
||||||
urls,
|
urls,
|
||||||
username: Some(username),
|
username: Some(username.to_string()),
|
||||||
credential: Some(password),
|
credential: Some(password.to_string()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let json = serde_json::to_string(&[ice_server])?;
|
let json = serde_json::to_string(&[ice_server])?;
|
||||||
Ok(json)
|
Ok(json)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Creates JSON with ICE servers from a line received over IMAP METADATA.
|
||||||
|
///
|
||||||
|
/// IMAP METADATA returns a line such as
|
||||||
|
/// `example.com:3478:1758650868:8Dqkyyu11MVESBqjbIylmB06rv8=`
|
||||||
|
///
|
||||||
|
/// 1758650868 is the username and expiration timestamp
|
||||||
|
/// at the same time,
|
||||||
|
/// while `8Dqkyyu11MVESBqjbIylmB06rv8=`
|
||||||
|
/// is the password.
|
||||||
|
pub(crate) async fn create_ice_servers_from_metadata(
|
||||||
|
context: &Context,
|
||||||
|
metadata: &str,
|
||||||
|
) -> Result<(i64, String)> {
|
||||||
|
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?;
|
||||||
|
Ok((expiration_timestamp, ice_servers))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates JSON with ICE servers when no TURN servers are known.
|
||||||
|
pub(crate) async fn create_fallback_ice_servers(context: &Context) -> Result<String> {
|
||||||
|
// Public STUN server from https://stunprotocol.org/,
|
||||||
|
// an open source STUN server.
|
||||||
|
let hostname = "stunserver2025.stunprotocol.org";
|
||||||
|
|
||||||
|
// Do not use cache because there is no TLS.
|
||||||
|
let load_cache = false;
|
||||||
|
let urls: Vec<String> = lookup_host_with_cache(context, hostname, STUN_PORT, "", load_cache)
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.map(|addr| format!("stun:{addr}"))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let ice_server = IceServer {
|
||||||
|
urls,
|
||||||
|
username: None,
|
||||||
|
credential: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let json = serde_json::to_string(&[ice_server])?;
|
||||||
|
Ok(json)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns JSON with ICE servers.
|
||||||
|
///
|
||||||
|
/// <https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/RTCPeerConnection#iceservers>
|
||||||
|
///
|
||||||
|
/// All returned servers are resolved to their IP addresses.
|
||||||
|
/// The primary point of DNS lookup is that Delta Chat Desktop
|
||||||
|
/// relies on the servers being specified by IP,
|
||||||
|
/// because it itself cannot utilize DNS. See
|
||||||
|
/// <https://github.com/deltachat/deltachat-desktop/issues/5447>.
|
||||||
|
pub async fn ice_servers(context: &Context) -> Result<String> {
|
||||||
|
if let Some(ref metadata) = *context.metadata.read().await {
|
||||||
|
Ok(metadata.ice_servers.clone())
|
||||||
|
} else {
|
||||||
|
Ok("[]".to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod calls_tests;
|
mod calls_tests;
|
||||||
|
|||||||
80
src/imap.rs
80
src/imap.rs
@@ -24,6 +24,7 @@ use rand::Rng;
|
|||||||
use ratelimit::Ratelimit;
|
use ratelimit::Ratelimit;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
|
use crate::calls::{create_fallback_ice_servers, create_ice_servers_from_metadata};
|
||||||
use crate::chat::{self, ChatId, ChatIdBlocked};
|
use crate::chat::{self, ChatId, ChatIdBlocked};
|
||||||
use crate::chatlist_events;
|
use crate::chatlist_events;
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
@@ -47,7 +48,7 @@ use crate::receive_imf::{
|
|||||||
};
|
};
|
||||||
use crate::scheduler::connectivity::ConnectivityStore;
|
use crate::scheduler::connectivity::ConnectivityStore;
|
||||||
use crate::stock_str;
|
use crate::stock_str;
|
||||||
use crate::tools::{self, create_id, duration_to_str};
|
use crate::tools::{self, create_id, duration_to_str, time};
|
||||||
|
|
||||||
pub(crate) mod capabilities;
|
pub(crate) mod capabilities;
|
||||||
mod client;
|
mod client;
|
||||||
@@ -123,6 +124,18 @@ pub(crate) struct ServerMetadata {
|
|||||||
pub admin: Option<String>,
|
pub admin: Option<String>,
|
||||||
|
|
||||||
pub iroh_relay: Option<Url>,
|
pub iroh_relay: Option<Url>,
|
||||||
|
|
||||||
|
/// 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,
|
||||||
|
|
||||||
|
/// Timestamp when ICE servers are considered
|
||||||
|
/// expired and should be updated.
|
||||||
|
pub ice_servers_expiration_timestamp: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl async_imap::Authenticator for OAuth2 {
|
impl async_imap::Authenticator for OAuth2 {
|
||||||
@@ -1535,7 +1548,43 @@ impl Session {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut lock = context.metadata.write().await;
|
let mut lock = context.metadata.write().await;
|
||||||
if (*lock).is_some() {
|
if let Some(ref mut old_metadata) = *lock {
|
||||||
|
let now = time();
|
||||||
|
|
||||||
|
// Refresh TURN server credentials if they expire in 12 hours.
|
||||||
|
if now + 3600 * 12 < old_metadata.ice_servers_expiration_timestamp {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
info!(context, "ICE servers expired, requesting new credentials.");
|
||||||
|
let mailbox = "";
|
||||||
|
let options = "";
|
||||||
|
let metadata = self
|
||||||
|
.get_metadata(mailbox, options, "(/shared/vendor/deltachat/turn)")
|
||||||
|
.await?;
|
||||||
|
let mut got_turn_server = false;
|
||||||
|
for m in metadata {
|
||||||
|
if m.entry == "/shared/vendor/deltachat/turn" {
|
||||||
|
if let Some(value) = m.value {
|
||||||
|
match create_ice_servers_from_metadata(context, &value).await {
|
||||||
|
Ok((parsed_timestamp, parsed_ice_servers)) => {
|
||||||
|
old_metadata.ice_servers_expiration_timestamp = parsed_timestamp;
|
||||||
|
old_metadata.ice_servers = parsed_ice_servers;
|
||||||
|
got_turn_server = false;
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
warn!(context, "Failed to parse TURN server metadata: {err:#}.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !got_turn_server {
|
||||||
|
// 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?;
|
||||||
|
}
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1547,6 +1596,8 @@ impl Session {
|
|||||||
let mut comment = None;
|
let mut comment = None;
|
||||||
let mut admin = None;
|
let mut admin = None;
|
||||||
let mut iroh_relay = None;
|
let mut iroh_relay = None;
|
||||||
|
let mut ice_servers = None;
|
||||||
|
let mut ice_servers_expiration_timestamp = 0;
|
||||||
|
|
||||||
let mailbox = "";
|
let mailbox = "";
|
||||||
let options = "";
|
let options = "";
|
||||||
@@ -1554,7 +1605,7 @@ impl Session {
|
|||||||
.get_metadata(
|
.get_metadata(
|
||||||
mailbox,
|
mailbox,
|
||||||
options,
|
options,
|
||||||
"(/shared/comment /shared/admin /shared/vendor/deltachat/irohrelay)",
|
"(/shared/comment /shared/admin /shared/vendor/deltachat/irohrelay /shared/vendor/deltachat/turn)",
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
for m in metadata {
|
for m in metadata {
|
||||||
@@ -1577,13 +1628,36 @@ impl Session {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"/shared/vendor/deltachat/turn" => {
|
||||||
|
if let Some(value) = m.value {
|
||||||
|
match create_ice_servers_from_metadata(context, &value).await {
|
||||||
|
Ok((parsed_timestamp, parsed_ice_servers)) => {
|
||||||
|
ice_servers_expiration_timestamp = parsed_timestamp;
|
||||||
|
ice_servers = Some(parsed_ice_servers);
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
warn!(context, "Failed to parse TURN server metadata: {err:#}.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let ice_servers = if let Some(ice_servers) = ice_servers {
|
||||||
|
ice_servers
|
||||||
|
} 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?
|
||||||
|
};
|
||||||
|
|
||||||
*lock = Some(ServerMetadata {
|
*lock = Some(ServerMetadata {
|
||||||
comment,
|
comment,
|
||||||
admin,
|
admin,
|
||||||
iroh_relay,
|
iroh_relay,
|
||||||
|
ice_servers,
|
||||||
|
ice_servers_expiration_timestamp,
|
||||||
});
|
});
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user