use std::ffi::OsString;
use std::net::IpAddr;
use std::net::SocketAddr;
use std::sync::Arc;
use std::sync::OnceLock;
use std::time::Duration;
use nameth::NamedEnumValues as _;
use nameth::nameth;
use reqwest::Url;
use tracing::debug;
use tracing::warn;
use trz_gateway_common::id::ClientName;
use trz_gateway_common::is_global::IsGlobal;
use trz_gateway_common::p2p::GOOGLE_STUN;
use trz_gateway_common::p2p::peer_connection::IceServer;
use trz_gateway_common::security_configuration::trusted_store::TrustedStoreConfig;
use uuid::Uuid;
pub trait ClientConfig: IsGlobal {
fn base_url(&self) -> Result<Url, url::ParseError> {
let port = if cfg!(debug_assertions) { 3000 } else { 3001 };
let mut url = Url::parse("https://localhost")?;
url.set_port(Some(port))
.unwrap_or_else(|()| warn!("Failed ot set port on {url:?}"));
Ok(url)
}
fn client_name(&self) -> ClientName {
static CLIENT_ID: OnceLock<ClientName> = OnceLock::new();
fn make_default_hostname() -> ClientName {
match hostname::get().map(OsString::into_string) {
Ok(Ok(hostname)) => return hostname.into(),
Err(error) => debug!("Failed to get the hostname with hostname::get(): {error}"),
Ok(Err(error)) => debug!("Failed to parse the hostname string: {error:?}"),
}
return Uuid::new_v4().to_string().into();
}
CLIENT_ID.get_or_init(make_default_hostname).clone()
}
type GatewayPki: TrustedStoreConfig;
fn gateway_pki(&self) -> Self::GatewayPki;
fn gateway_sni_override(&self) -> Option<&str> {
None
}
fn transport(&self) -> ClientTransport {
ClientTransport::Direct
}
fn url(&self, path: &str) -> Result<Url, SniOverrideError> {
Ok(self.base_url()?.join(path)?)
}
}
impl<T: ClientConfig> ClientConfig for Arc<T> {
fn base_url(&self) -> Result<Url, url::ParseError> {
self.as_ref().base_url()
}
fn client_name(&self) -> ClientName {
self.as_ref().client_name()
}
type GatewayPki = T::GatewayPki;
fn gateway_pki(&self) -> Self::GatewayPki {
self.as_ref().gateway_pki()
}
fn gateway_sni_override(&self) -> Option<&str> {
self.as_ref().gateway_sni_override()
}
fn transport(&self) -> ClientTransport {
self.as_ref().transport()
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub enum ClientTransport {
#[default]
Direct,
WebRtc(P2pClientConfig),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct P2pClientConfig {
pub signaling_url: String,
pub server_name: ClientName,
pub ice_servers: Vec<IceServer>,
pub signaling_timeout: Duration,
pub handshake_timeout: Duration,
pub connect_timeout: Duration,
}
impl P2pClientConfig {
pub fn new(signaling_url: impl Into<String>, server_name: ClientName) -> Self {
Self {
signaling_url: signaling_url.into(),
server_name,
ice_servers: vec![IceServer {
urls: vec![GOOGLE_STUN.to_owned()],
..IceServer::default()
}],
signaling_timeout: Duration::from_secs(10),
handshake_timeout: Duration::from_secs(30),
connect_timeout: Duration::from_secs(45),
}
}
}
pub(crate) fn set_gateway_sni_override(
url: &mut Url,
gateway_sni_override: Option<&str>,
) -> Result<(), SniOverrideError> {
if let Some(gateway_sni_override) = gateway_sni_override {
url.set_host(Some(gateway_sni_override))
.map_err(|_| SniOverrideError::InvalidSniOverride(gateway_sni_override.to_owned()))?;
}
Ok(())
}
pub(crate) fn gateway_sni_override_resolution<C: ClientConfig>(
client_config: &C,
) -> Result<Option<(String, SocketAddr)>, SniOverrideError> {
let Some(gateway_sni_override) = client_config.gateway_sni_override() else {
return Ok(None);
};
let url = client_config.base_url()?;
let Some(host) = url.host_str() else {
return Err(SniOverrideError::MissingBaseUrlHost);
};
let Ok(ip) = host.parse::<IpAddr>() else {
return Ok(None);
};
let port = url
.port_or_known_default()
.ok_or(SniOverrideError::MissingBaseUrlPort)?;
Ok(Some((
gateway_sni_override.to_owned(),
SocketAddr::new(ip, port),
)))
}
#[nameth]
#[derive(thiserror::Error, Debug)]
pub enum SniOverrideError {
#[error("[{n}] Failed to parse the Gateway URL: {0}", n = self.name())]
Url(#[from] url::ParseError),
#[error("[{n}] The Gateway URL must include a host when using SNI override", n = self.name())]
MissingBaseUrlHost,
#[error("[{n}] The Gateway URL must include or imply a port when using SNI override", n = self.name())]
MissingBaseUrlPort,
#[error("[{n}] Invalid SNI override: {0}", n = self.name())]
InvalidSniOverride(String),
}
#[cfg(test)]
mod tests {
use trz_gateway_common::p2p::GOOGLE_STUN;
use super::*;
#[test]
fn transport_defaults_to_direct_and_p2p_defaults_are_bounded() {
assert_eq!(ClientTransport::Direct, ClientTransport::default());
let config = P2pClientConfig::new("https://signal.example", "gateway".into());
assert_eq!("https://signal.example", config.signaling_url);
assert_eq!("gateway", config.server_name.as_ref());
assert_eq!(vec![GOOGLE_STUN], config.ice_servers[0].urls);
assert!(config.signaling_timeout < config.handshake_timeout);
assert!(config.handshake_timeout < config.connect_timeout);
}
}