tuwunel_api/client/
voip.rs1use std::time::{Duration, SystemTime};
2
3use axum::extract::State;
4use base64::{Engine as _, engine::general_purpose};
5use hmac::{Hmac, Mac};
6use ruma::{SecondsSinceUnixEpoch, UserId, api::client::voip::get_turn_server_info};
7use sha1::Sha1;
8use tuwunel_core::{Err, Result, utils};
9
10use crate::Ruma;
11
12const RANDOM_USER_ID_LENGTH: usize = 10;
13
14type HmacSha1 = Hmac<Sha1>;
15
16pub(crate) async fn turn_server_route(
20 State(services): State<crate::State>,
21 body: Ruma<get_turn_server_info::v3::Request>,
22) -> Result<get_turn_server_info::v3::Response> {
23 if services.server.config.turn_uris.is_empty() {
25 return Err!(Request(NotFound("Not Found")));
26 }
27
28 let turn_secret = &services.globals.turn_secret;
29
30 let (username, password) = if let Some(turn_secret) = turn_secret {
31 let expiry = SecondsSinceUnixEpoch::from_system_time(
32 SystemTime::now()
33 .checked_add(Duration::from_secs(services.config.turn_ttl))
34 .expect("TURN TTL should not get this high"),
35 )
36 .expect("time is valid");
37
38 let random_user_id = || {
39 UserId::parse_with_server_name(
40 utils::random_string(RANDOM_USER_ID_LENGTH).to_lowercase(),
41 &services.server.name,
42 )
43 };
44
45 let user = body.sender_user.map_or_else(random_user_id, Ok)?;
46 let username: String = format!("{}:{}", expiry.get(), user);
47 let mut mac = HmacSha1::new_from_slice(turn_secret.as_bytes())
48 .expect("HMAC can take key of any size");
49
50 mac.update(username.as_bytes());
51 let password: String = general_purpose::STANDARD.encode(mac.finalize().into_bytes());
52
53 (username, password)
54 } else {
55 (services.config.turn_username.clone(), services.config.turn_password.clone())
56 };
57
58 Ok(get_turn_server_info::v3::Response {
59 username,
60 password,
61 uris: services.config.turn_uris.clone(),
62 ttl: Duration::from_secs(services.config.turn_ttl),
63 })
64}