2026-10-12 20:18:51 +08:00
|
|
|
use chatmask_server::{Request, Response, PROTOCOL};
|
|
|
|
|
use futures::StreamExt;
|
|
|
|
|
use libp2p::{
|
|
|
|
|
identity, noise, request_response, swarm::SwarmEvent, tcp, yamux, StreamProtocol, Swarm,
|
|
|
|
|
SwarmBuilder,
|
|
|
|
|
};
|
|
|
|
|
use std::{
|
|
|
|
|
fs,
|
|
|
|
|
io::{BufRead, BufReader},
|
|
|
|
|
process::{Child, Command, Stdio},
|
|
|
|
|
sync::mpsc,
|
|
|
|
|
time::Duration,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
fn client(key: identity::Keypair) -> Swarm<request_response::cbor::Behaviour<Request, Response>> {
|
|
|
|
|
SwarmBuilder::with_existing_identity(key)
|
|
|
|
|
.with_tokio()
|
|
|
|
|
.with_tcp(
|
|
|
|
|
tcp::Config::default(),
|
|
|
|
|
noise::Config::new,
|
|
|
|
|
yamux::Config::default,
|
|
|
|
|
)
|
|
|
|
|
.unwrap()
|
|
|
|
|
.with_quic()
|
|
|
|
|
.with_behaviour(|_| {
|
|
|
|
|
request_response::cbor::Behaviour::new(
|
|
|
|
|
[(
|
|
|
|
|
StreamProtocol::new(PROTOCOL),
|
|
|
|
|
request_response::ProtocolSupport::Full,
|
|
|
|
|
)],
|
|
|
|
|
request_response::Config::default(),
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
.unwrap()
|
|
|
|
|
.build()
|
|
|
|
|
}
|
|
|
|
|
struct Server(Child);
|
|
|
|
|
impl Drop for Server {
|
|
|
|
|
fn drop(&mut self) {
|
|
|
|
|
let _ = self.0.kill();
|
|
|
|
|
let _ = self.0.wait();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
struct Directory(std::path::PathBuf);
|
|
|
|
|
impl Drop for Directory {
|
|
|
|
|
fn drop(&mut self) {
|
|
|
|
|
// The target is the exact unique temporary directory created by this test.
|
|
|
|
|
if self.0.starts_with(std::env::temp_dir()) {
|
|
|
|
|
let _ = fs::remove_dir_all(&self.0);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
async fn real_server_registers_observed_udp_addresses_signals_both_peers_and_clients_send_directly()
|
|
|
|
|
{
|
|
|
|
|
let dir = Directory(std::env::temp_dir().join(format!(
|
|
|
|
|
"chatmask-coordination-{}",
|
|
|
|
|
libp2p::PeerId::random()
|
|
|
|
|
)));
|
|
|
|
|
fs::create_dir(&dir.0).unwrap();
|
|
|
|
|
let mut server = Server(
|
|
|
|
|
Command::new(env!("CARGO_BIN_EXE_chatmask-server"))
|
|
|
|
|
.args(["--listen", "/ip4/127.0.0.1/udp/0/quic-v1"])
|
|
|
|
|
.arg("--key")
|
|
|
|
|
.arg(dir.0.join("server.key"))
|
|
|
|
|
.stdout(Stdio::piped())
|
|
|
|
|
.stderr(Stdio::inherit())
|
|
|
|
|
.spawn()
|
|
|
|
|
.unwrap(),
|
|
|
|
|
);
|
|
|
|
|
let (tx, rx) = mpsc::channel();
|
|
|
|
|
let stdout = server.0.stdout.take().unwrap();
|
|
|
|
|
std::thread::spawn(move || {
|
|
|
|
|
for line in BufReader::new(stdout).lines() {
|
|
|
|
|
if tx.send(line.unwrap()).is_err() {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let control = loop {
|
|
|
|
|
let line = rx.recv_timeout(Duration::from_secs(10)).unwrap();
|
|
|
|
|
if let Some(value) = line.strip_prefix("Control address: ") {
|
|
|
|
|
break value.parse::<libp2p::Multiaddr>().unwrap();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
let key_a = identity::Keypair::generate_ed25519();
|
|
|
|
|
let key_b = identity::Keypair::generate_ed25519();
|
|
|
|
|
let a_peer = key_a.public().to_peer_id();
|
|
|
|
|
let b_peer = key_b.public().to_peer_id();
|
|
|
|
|
let server_peer = match control.iter().last().unwrap() {
|
|
|
|
|
libp2p::multiaddr::Protocol::P2p(peer) => peer,
|
|
|
|
|
_ => panic!(),
|
|
|
|
|
};
|
|
|
|
|
let mut a = client(key_a.clone());
|
|
|
|
|
let mut b = client(key_b.clone());
|
|
|
|
|
a.listen_on("/ip4/127.0.0.1/udp/0/quic-v1".parse().unwrap())
|
|
|
|
|
.unwrap();
|
|
|
|
|
b.listen_on("/ip4/127.0.0.1/udp/0/quic-v1".parse().unwrap())
|
|
|
|
|
.unwrap();
|
|
|
|
|
a.dial(control.clone()).unwrap();
|
|
|
|
|
b.dial(control).unwrap();
|
2026-10-12 23:42:18 +08:00
|
|
|
a.behaviour_mut().send_request(
|
|
|
|
|
&server_peer,
|
|
|
|
|
Request::Register {
|
|
|
|
|
candidates: vec!["10.20.30.40:9001".parse().unwrap()],
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
b.behaviour_mut().send_request(
|
|
|
|
|
&server_peer,
|
|
|
|
|
Request::Register {
|
|
|
|
|
candidates: vec!["10.20.30.40:9002".parse().unwrap()],
|
|
|
|
|
},
|
|
|
|
|
);
|
2026-10-12 20:18:51 +08:00
|
|
|
let (mut endpoint_a, mut endpoint_b) = (None, None);
|
|
|
|
|
tokio::time::timeout(Duration::from_secs(10), async {
|
|
|
|
|
while endpoint_a.is_none() || endpoint_b.is_none() {
|
|
|
|
|
tokio::select! {
|
|
|
|
|
event = a.select_next_some() => if let SwarmEvent::Behaviour(request_response::Event::Message { message: request_response::Message::Response { response: Response::Registered { endpoint, .. }, .. }, .. }) = event { endpoint_a = Some(endpoint); },
|
|
|
|
|
event = b.select_next_some() => if let SwarmEvent::Behaviour(request_response::Event::Message { message: request_response::Message::Response { response: Response::Registered { endpoint, .. }, .. }, .. }) = event { endpoint_b = Some(endpoint); },
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}).await.unwrap();
|
|
|
|
|
let endpoint_a = endpoint_a.unwrap();
|
|
|
|
|
let endpoint_b = endpoint_b.unwrap();
|
|
|
|
|
a.behaviour_mut().send_request(
|
|
|
|
|
&server_peer,
|
|
|
|
|
Request::Connect {
|
|
|
|
|
peer: b_peer.to_string(),
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
let (mut notice_a, mut notice_b, mut started) = (None, None, None);
|
|
|
|
|
tokio::time::timeout(Duration::from_secs(10), async {
|
|
|
|
|
while notice_a.is_none() || notice_b.is_none() || started.is_none() {
|
|
|
|
|
tokio::select! {
|
|
|
|
|
event = a.select_next_some() => if let SwarmEvent::Behaviour(request_response::Event::Message { peer, message, .. }) = event {
|
|
|
|
|
assert_eq!(peer, server_peer);
|
|
|
|
|
match message {
|
2026-10-12 23:42:18 +08:00
|
|
|
request_response::Message::Request { request: Request::Punch { peer, endpoint, candidates, attempt, timeout_ms }, channel, .. } => {
|
|
|
|
|
assert_eq!(peer, b_peer.to_string()); assert_eq!(endpoint, endpoint_b); assert_eq!(candidates, vec!["10.20.30.40:9002".parse().unwrap()]); assert_eq!(timeout_ms, 12000); notice_a = Some(attempt);
|
2026-10-12 20:18:51 +08:00
|
|
|
a.behaviour_mut().send_response(channel, Response::Ack).unwrap();
|
|
|
|
|
},
|
|
|
|
|
request_response::Message::Response { response: Response::Started { attempt }, .. } => started = Some(attempt),
|
|
|
|
|
_ => {},
|
|
|
|
|
}
|
|
|
|
|
},
|
2026-10-12 23:42:18 +08:00
|
|
|
event = b.select_next_some() => if let SwarmEvent::Behaviour(request_response::Event::Message { peer: sender, message: request_response::Message::Request { request: Request::Punch { peer, endpoint, candidates, attempt, timeout_ms }, channel, .. }, .. }) = event {
|
|
|
|
|
assert_eq!(sender, server_peer); assert_eq!(peer, a_peer.to_string()); assert_eq!(endpoint, endpoint_a); assert_eq!(candidates, vec!["10.20.30.40:9001".parse().unwrap()]); assert_eq!(timeout_ms, 12000); notice_b = Some(attempt);
|
2026-10-12 20:18:51 +08:00
|
|
|
b.behaviour_mut().send_response(channel, Response::Ack).unwrap();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}).await.unwrap();
|
|
|
|
|
assert_eq!(notice_a, notice_b);
|
|
|
|
|
assert_eq!(notice_a, started);
|
|
|
|
|
assert_ne!(endpoint_a.port(), 0);
|
|
|
|
|
assert_ne!(endpoint_b.port(), 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn identity_is_stable_and_corruption_does_not_rotate_it() {
|
|
|
|
|
let dir =
|
|
|
|
|
Directory(std::env::temp_dir().join(format!("chatmask-key-{}", libp2p::PeerId::random())));
|
|
|
|
|
let path = dir.0.join("server.key");
|
|
|
|
|
let key = chatmask_server::load_identity(&path).unwrap();
|
|
|
|
|
assert_eq!(
|
|
|
|
|
key.public().to_peer_id(),
|
|
|
|
|
chatmask_server::load_identity(&path)
|
|
|
|
|
.unwrap()
|
|
|
|
|
.public()
|
|
|
|
|
.to_peer_id()
|
|
|
|
|
);
|
|
|
|
|
fs::write(&path, b"damaged").unwrap();
|
|
|
|
|
assert!(chatmask_server::load_identity(&path).is_err());
|
|
|
|
|
assert_eq!(fs::read(&path).unwrap(), b"damaged");
|
|
|
|
|
}
|