//! The host agent's TLS clients: the room service over `https://` and the relay over `wss://`, //! through a TLS front on loopback whose certificate is made here. //! //! - Trusting that certificate (given as a PEM file, as `--ca` gives it), the service answers over //! `https://` and the relay's session opens over `wss://`. //! - Trusting only the system's roots, which never signed it, each refuses the server: the //! certificate's chain is verified. //! - A certificate for another name than the host the URL names is refused even when its chain is //! trusted: the name is verified too. use joule_moq_relay::cert::RelayIdentity; use joule_moq_relay::client::ClientOptions; use joule_moq_relay::{RelayConfig, RelayServer}; use joule_moq_remote::service::RoomService; use joule_moq_remote::tls::{self, Trust}; use joule_moq_remote::wire; use std::net::SocketAddr; use std::time::Duration; use tokio::io::{AsyncReadExt, AsyncWriteExt}; /// A plain HTTP backend that answers every request with a small JSON body. async fn backend() -> SocketAddr { let l = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); let at = l.local_addr().unwrap(); tokio::spawn(async move { loop { let Ok((mut s, _)) = l.accept().await else { return }; tokio::spawn(async move { let mut buf = vec![0u8; 4096]; let mut got = Vec::new(); while !got.windows(4).any(|w| w == b"\r\n\r\n") { match s.read(&mut buf).await { Ok(0) | Err(_) => return, Ok(n) => got.extend_from_slice(&buf[..n]), } } let host = String::from_utf8_lossy(&got).lines().find_map(|l| l.strip_prefix("Host: ").map(str::to_string)).unwrap_or_default(); let body = format!("{{\"over\":\"tls\",\"host\":\"{host}\"}}"); let _ = s.write_all(format!("HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", body.len()).as_bytes()).await; let _ = s.shutdown().await; }); } }); at } fn identity(names: &[&str]) -> RelayIdentity { RelayIdentity::dev_self_signed(&names.iter().map(|n| n.to_string()).collect::>()).expect("a certificate") } /// The certificate's chain as a PEM file, as `--ca` names one. fn pem_file(id: &RelayIdentity, name: &str) -> std::path::PathBuf { let p = std::env::temp_dir().join(format!("wai-remote-tls-{}-{name}.pem", std::process::id())); std::fs::write(&p, id.chain.iter().map(tls::pem_of).collect::()).unwrap(); p } #[tokio::test(flavor = "multi_thread", worker_threads = 4)] async fn the_room_service_over_https_is_verified() { let id = identity(&["127.0.0.1"]); let front = tls::front(id.chain.clone(), id.key.clone_key(), backend().await).await.unwrap(); let ca = pem_file(&id, "service"); let trusting = Trust::system_and_pem(&ca).expect("the PEM file reads"); let _ = std::fs::remove_file(&ca); let url = format!("https://{front}"); let svc = RoomService::new(&url).unwrap().trusting(trusting); let a = svc.call("GET", "/room", None, &[]).await.expect("the service answers over TLS"); assert_eq!(a.status, 200); assert_eq!(a.body["over"], "tls"); assert_eq!(a.body["host"], front.to_string(), "the request's Host is the client's, through the front"); // The system's roots never signed this certificate: the service is refused. let refused = RoomService::new(&url).unwrap().call("GET", "/room", None, &[]).await; let e = refused.expect_err("an untrusted certificate is refused"); assert!(e.contains("certificate"), "{e}"); } #[tokio::test(flavor = "multi_thread", worker_threads = 4)] async fn a_certificate_for_another_name_is_refused() { let id = identity(&["relay.invalid"]); let front = tls::front(id.chain.clone(), id.key.clone_key(), backend().await).await.unwrap(); let svc = RoomService::new(&format!("https://{front}")).unwrap().trusting(Trust::only(id.chain.clone())); let e = svc.call("GET", "/room", None, &[]).await.expect_err("the certificate names another host"); assert!(e.contains("certificate"), "{e}"); } #[tokio::test(flavor = "multi_thread", worker_threads = 4)] async fn the_relay_over_wss_is_verified() { let mut c = RelayConfig::loopback(); c.quic_listen = None; let server = RelayServer::start(c).await.expect("the relay starts"); let plain = server.websocket_addr.expect("a WebSocket listener"); let path = server.relay().config().path.clone(); let id = identity(&["127.0.0.1"]); let front = tls::front(id.chain.clone(), id.key.clone_key(), plain).await.unwrap(); let url = format!("wss://{front}{path}"); let client = tokio::time::timeout(Duration::from_secs(20), wire::connect(&url, ClientOptions::default(), &Trust::only(id.chain.clone()))) .await .expect("in time") .expect("a session over wss:// with its certificate trusted"); client.close(0, "done"); let e = match wire::connect(&url, ClientOptions::default(), &Trust::system()).await { Ok(_) => panic!("a session with a certificate the system's roots never signed"), Err(e) => e, }; assert!(e.contains("certificate"), "{e}"); let other = identity(&["relay.invalid"]); let front2 = tls::front(other.chain.clone(), other.key.clone_key(), plain).await.unwrap(); let e = match wire::connect(&format!("wss://{front2}{path}"), ClientOptions::default(), &Trust::only(other.chain.clone())).await { Ok(_) => panic!("a session with a certificate for another name"), Err(e) => e, }; assert!(e.contains("certificate"), "{e}"); server.shutdown().await; }