mod common;
use std::path::Path;
use std::time::Duration;
use tokio::time::timeout;
use weida::{
ClientTls, FilesOptions, GiveUp, Identity, IdentityEvent, IdentitySource, PeerEvent,
ReconnectPolicy, Runtime, RuntimeConfig, ServerTls, Trust,
};
const DEADLINE: Duration = Duration::from_secs(15);
async fn within<F: Future>(f: F) -> F::Output {
timeout(DEADLINE, f).await.expect("operation timed out")
}
fn private_dir(tag: &str) -> std::path::PathBuf {
let dir = std::env::temp_dir().join(format!(
"weida-id-{tag}-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("clock")
.as_nanos()
));
let _ = std::fs::remove_dir_all(&dir);
dir
}
fn resign(dir: &Path, names: &[&str]) -> String {
let key_pem = std::fs::read_to_string(dir.join("key.pem")).expect("key.pem");
let key = rcgen::KeyPair::from_pem(&key_pem).expect("key pair");
let names: Vec<String> = names.iter().map(|n| (*n).to_owned()).collect();
let params = rcgen::CertificateParams::new(names).expect("params");
params.self_signed(&key).expect("self-sign").pem()
}
#[tokio::test]
async fn a_bootstrapped_directory_yields_the_same_key_twice() {
let dir = private_dir("bootstrap");
let first = IdentitySource::files(
&dir,
FilesOptions {
names: vec!["localhost".into()],
..FilesOptions::default()
},
)
.expect("bootstrap");
let fingerprint = first.fingerprint().expect("fingerprint");
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mode = |p: &Path| std::fs::metadata(p).expect("metadata").permissions().mode() & 0o777;
assert_eq!(mode(&dir), 0o700, "the directory is owner-only");
assert_eq!(mode(&dir.join("key.pem")), 0o600, "the key is owner-only");
assert_eq!(mode(&dir.join("cert.pem")), 0o600);
}
drop(first);
let second = IdentitySource::files(&dir, FilesOptions::default()).expect("load");
assert_eq!(second.fingerprint().expect("fingerprint"), fingerprint);
let mut events = second.events();
assert!(!second.reload().expect("reload"), "nothing changed");
assert!(
timeout(Duration::from_millis(50), events.recv())
.await
.is_err(),
"no event for an unchanged directory"
);
std::fs::remove_file(dir.join("cert.pem")).expect("remove cert");
let err = IdentitySource::files(&dir, FilesOptions::default())
.expect_err("a key without its certificate");
assert!(matches!(err, weida::Error::Tls(_)), "{err:?}");
let _ = std::fs::remove_dir_all(&dir);
}
#[tokio::test]
async fn a_renewed_certificate_is_served_without_rebinding() {
let dir = private_dir("renew");
let source = IdentitySource::files(
&dir,
FilesOptions {
names: vec!["localhost".into(), "127.0.0.1".into()],
poll: Duration::from_millis(1),
},
)
.expect("bootstrap");
let fingerprint = source.fingerprint().expect("fingerprint");
let old_cert = std::fs::read_to_string(dir.join("cert.pem")).expect("cert.pem");
let mut events = source.events();
let server = Runtime::new(RuntimeConfig::default()).expect("server runtime");
let listener = server.listener();
let binding = listener
.bind_quic(
"127.0.0.1:0".parse().expect("loopback"),
ServerTls::new(source.clone()),
)
.await
.expect("bind");
let url = format!("weida://127.0.0.1:{}/jobs", binding.local_addr().port());
let puller = listener.puller("/jobs").expect("puller");
let client = Runtime::new(RuntimeConfig::default()).expect("client runtime");
let before = client.pusher(Trust::anchor(old_cert.clone()));
within(before.connect(&url))
.await
.expect("connect under the old certificate");
within(before.send(b"old")).await.expect("send");
let got = within(puller.recv()).await.expect("recv");
assert_eq!(within(got.collect(64)).await.expect("collect"), b"old");
let new_cert = resign(&dir, &["localhost"]);
assert_ne!(new_cert, old_cert);
std::fs::write(dir.join("cert.pem"), &new_cert).expect("write cert");
assert!(source.reload().expect("reload"), "the change was picked up");
let event = within(events.recv()).await;
assert_eq!(
event,
Some(IdentityEvent::Renewed { fingerprint }),
"same key, new certificate"
);
assert_eq!(source.fingerprint().expect("fingerprint"), fingerprint);
let stale = Runtime::new(RuntimeConfig::default()).expect("second client runtime");
let by_ip = stale.pusher(Trust::anchor(old_cert.clone()));
let err = within(by_ip.connect(&url))
.await
.expect_err("the renewed certificate does not name 127.0.0.1");
assert!(
matches!(err, weida::Error::Untrusted(fp) if fp == fingerprint),
"{err:?}"
);
let by_name = stale.pusher(Trust::anchor(new_cert));
let named = format!("weida://localhost:{}/jobs", binding.local_addr().port());
within(by_name.connect(&named))
.await
.expect("connect by the renewed name");
within(by_name.send(b"new")).await.expect("send");
let got = within(puller.recv()).await.expect("recv");
assert_eq!(within(got.collect(64)).await.expect("collect"), b"new");
within(before.send(b"still"))
.await
.expect("send on the old connection");
let got = within(puller.recv()).await.expect("recv");
assert_eq!(within(got.collect(64)).await.expect("collect"), b"still");
client.shutdown().await;
stale.shutdown().await;
server.shutdown().await;
let _ = std::fs::remove_dir_all(&dir);
}
#[tokio::test]
async fn a_replaced_key_is_a_new_peer_for_a_pinning_client() {
let dir = private_dir("rekey");
let source = IdentitySource::files(
&dir,
FilesOptions {
names: vec!["localhost".into(), "127.0.0.1".into()],
poll: Duration::from_millis(1),
},
)
.expect("bootstrap");
let old = source.fingerprint().expect("fingerprint");
let mut identity_events = source.events();
let server = Runtime::new(RuntimeConfig::default()).expect("server runtime");
let listener = server.listener();
let addr: std::net::SocketAddr = "127.0.0.1:0".parse().expect("loopback");
let binding = listener
.bind_quic(addr, ServerTls::new(source.clone()))
.await
.expect("bind");
let addr = binding.local_addr();
let url = format!("weida://{old}@127.0.0.1:{}/jobs", addr.port());
let puller = listener.puller("/jobs").expect("puller");
let client = Runtime::new(RuntimeConfig {
reconnect: ReconnectPolicy {
initial: Duration::from_millis(5),
max: Duration::from_millis(50),
..ReconnectPolicy::default()
},
..RuntimeConfig::default()
})
.expect("client runtime");
let pusher = client.pusher(ClientTls::new(Trust::by_address()));
let mut peer_events = pusher.events();
within(pusher.connect(&url)).await.expect("connect pinned");
assert!(matches!(
within(peer_events.recv()).await,
Some(PeerEvent::Connected { .. })
));
within(pusher.send(b"before")).await.expect("send");
let got = within(puller.recv()).await.expect("recv");
assert_eq!(within(got.collect(64)).await.expect("collect"), b"before");
let fresh = Identity::generate_for(["localhost", "127.0.0.1"]).expect("generate");
std::fs::write(dir.join("cert.pem"), fresh.certificate_pem().expect("pem")).expect("cert");
std::fs::write(dir.join("key.pem"), fresh.to_pem().expect("pem")).expect("key");
assert!(source.reload().expect("reload"));
let new = source.fingerprint().expect("fingerprint");
assert_ne!(new, old);
assert_eq!(
within(identity_events.recv()).await,
Some(IdentityEvent::KeyChanged { from: old, to: new })
);
drop(puller);
binding.close().await;
drop(binding);
assert!(matches!(
within(peer_events.recv()).await,
Some(PeerEvent::Lost { .. })
));
drop(listener);
server.shutdown().await;
let server = Runtime::new(RuntimeConfig::default()).expect("server runtime again");
let listener = server.listener();
let rebound = within(async {
loop {
match listener
.bind_quic(addr, ServerTls::new(source.clone()))
.await
{
Ok(binding) => break binding,
Err(_) => tokio::time::sleep(Duration::from_millis(10)).await,
}
}
})
.await;
let _puller = listener.puller("/jobs").expect("puller");
let gave_up = within(async {
loop {
match peer_events.recv().await.expect("events") {
PeerEvent::Retrying { .. } => continue,
event => break event,
}
}
})
.await;
assert!(
matches!(
gave_up,
PeerEvent::GaveUp {
why: GiveUp::PeerChanged { presented: Some(fp) },
..
} if fp == new
),
"the redial refuses the new key, got {gave_up:?}"
);
assert_eq!(pusher.peer_count(), 0);
drop(rebound);
client.shutdown().await;
server.shutdown().await;
let _ = std::fs::remove_dir_all(&dir);
}
#[tokio::test]
async fn static_sources_configured_alike_share_a_connection() {
let a = ClientTls::new(Trust::anchor("-ca-"));
let b = ClientTls::new(Trust::anchor("-ca-"));
assert_eq!(a, b, "equal by content");
let dir_a = private_dir("pool-a");
let dir_b = private_dir("pool-b");
let ida = IdentitySource::files(&dir_a, FilesOptions::default()).expect("a");
let idb = IdentitySource::files(&dir_b, FilesOptions::default()).expect("b");
assert_ne!(
a.clone().with_identity(ida.clone()),
a.clone().with_identity(idb),
"different sources"
);
assert_eq!(
a.clone().with_identity(ida.clone()),
a.with_identity(ida),
"the same source"
);
let _ = std::fs::remove_dir_all(&dir_a);
let _ = std::fs::remove_dir_all(&dir_b);
}