use std::{
io::Write,
sync::{
Arc, Mutex as StdMutex,
atomic::{AtomicBool, AtomicUsize, Ordering},
},
time::Duration,
};
use async_trait::async_trait;
use axum::{
Router,
routing::{get, post},
};
use scion_quic::{
quic::config::QuicConfig,
socket::GenericScionUdpSocket,
test_util::{BreakableScionSocket, MockScionSocket},
};
use scion_stack::{
resolver::{ResolveError, ScionDnsResolver},
stack::ScionSocketBindError,
};
use sciparse::address::{ip_addr::ScionIpAddr, ip_socket_addr::ScionSocketIpAddr};
use tempfile::NamedTempFile;
use tokio_util::sync::CancellationToken;
use crate::{
config::Config,
epoch::{Epoch, Network, SocketBinder},
};
pub(crate) const SERVER_PORT: u16 = 4443;
pub(crate) fn server_scion_ip() -> ScionIpAddr {
"1-ff00:0:110,10.0.0.2".parse().unwrap()
}
pub(crate) fn dead_scion_ip() -> ScionIpAddr {
"1-ff00:0:110,10.0.0.1".parse().unwrap()
}
fn server_addr() -> ScionSocketIpAddr {
let ip = server_scion_ip();
ScionSocketIpAddr::new(ip.isd_asn(), ip.ip(), SERVER_PORT)
}
fn client_addr() -> ScionSocketIpAddr {
let ip: ScionIpAddr = "1-ff00:0:111,10.0.1.1".parse().unwrap();
ScionSocketIpAddr::new(ip.isd_asn(), ip.ip(), 0)
}
pub(crate) fn test_config() -> Config {
Config::new("https://endhost-api.invalid".parse().unwrap())
.with_quic_config(
QuicConfig::builder()
.verify_peer(false)
.handshake_timeout(Duration::from_millis(400))
.build(),
)
.with_connection_attempt_delay(Duration::from_millis(50))
}
pub(crate) fn test_router() -> (Router, Arc<AtomicUsize>) {
let hits = Arc::new(AtomicUsize::new(0));
let router = Router::new()
.route(
"/hello",
get({
let hits = hits.clone();
move || {
let hits = hits.clone();
async move {
hits.fetch_add(1, Ordering::SeqCst);
"world"
}
}
}),
)
.route("/echo", post(|body: String| async move { body }))
.route(
"/slow",
get(|| {
async {
tokio::time::sleep(Duration::from_millis(500)).await;
"slow"
}
}),
);
(router, hits)
}
pub(crate) struct TestServerHarness {
router: Router,
server_quic: QuicConfig,
cert_file: NamedTempFile,
key_file: NamedTempFile,
cancel: CancellationToken,
sockets: StdMutex<Vec<Arc<BreakableScionSocket>>>,
pub(crate) binds: AtomicUsize,
stall_first_bind: AtomicBool,
stall_gate: tokio::sync::Notify,
}
impl TestServerHarness {
pub(crate) fn new(router: Router) -> Arc<Self> {
Self::with_server_quic(router, QuicConfig::builder().verify_peer(false).build())
}
pub(crate) fn with_server_quic(router: Router, server_quic: QuicConfig) -> Arc<Self> {
let cert = rcgen::generate_simple_self_signed(vec!["localhost".to_string()]).unwrap();
let mut cert_file = NamedTempFile::new().unwrap();
let mut key_file = NamedTempFile::new().unwrap();
cert_file
.as_file_mut()
.write_all(cert.cert.pem().as_bytes())
.unwrap();
key_file
.as_file_mut()
.write_all(cert.signing_key.serialize_pem().as_bytes())
.unwrap();
Arc::new(TestServerHarness {
router,
server_quic,
cert_file,
key_file,
cancel: CancellationToken::new(),
sockets: StdMutex::new(Vec::new()),
binds: AtomicUsize::new(0),
stall_first_bind: AtomicBool::new(false),
stall_gate: tokio::sync::Notify::new(),
})
}
pub(crate) fn stall_first_bind(&self) {
self.stall_first_bind.store(true, Ordering::SeqCst);
}
pub(crate) fn release_stalled(&self) {
self.stall_gate.notify_waiters();
}
pub(crate) fn break_sockets(&self) {
for socket in self.sockets.lock().unwrap().drain(..) {
socket.break_socket();
}
}
fn server_config(&self) -> scion_quic::reexport::squiche::Config {
let mut config = self.server_quic.to_quiche_config().unwrap();
config
.load_cert_chain_from_pem_file(self.cert_file.path().to_str().unwrap())
.unwrap();
config
.load_priv_key_from_pem_file(self.key_file.path().to_str().unwrap())
.unwrap();
config
}
}
impl Drop for TestServerHarness {
fn drop(&mut self) {
self.cancel.cancel();
}
}
#[async_trait]
impl SocketBinder for TestServerHarness {
async fn bind(&self) -> Result<Arc<dyn GenericScionUdpSocket>, ScionSocketBindError> {
if self.stall_first_bind.swap(false, Ordering::SeqCst) {
self.stall_gate.notified().await;
}
self.binds.fetch_add(1, Ordering::SeqCst);
let (client_socket, server_socket) =
MockScionSocket::pair(1024, client_addr(), server_addr());
let cancel = self.cancel.clone();
let router = self.router.clone();
let config = self.server_config();
tokio::spawn(async move {
let _ = scion_h3_axum::ScionH3AxumServer::serve_with_graceful_shutdown(
Arc::new(server_socket),
router,
config,
cancel,
)
.await;
});
let client_socket = Arc::new(BreakableScionSocket::new(Arc::new(client_socket)));
self.sockets.lock().unwrap().push(client_socket.clone());
Ok(client_socket)
}
}
pub(crate) struct StaticResolver {
addrs: Vec<ScionIpAddr>,
pub(crate) calls: AtomicUsize,
}
impl StaticResolver {
pub(crate) fn new(addrs: Vec<ScionIpAddr>) -> Arc<Self> {
Arc::new(StaticResolver {
addrs,
calls: AtomicUsize::new(0),
})
}
}
#[async_trait]
impl ScionDnsResolver for StaticResolver {
async fn resolve(&self, _domain: &str) -> Result<Vec<ScionIpAddr>, ResolveError> {
self.calls.fetch_add(1, Ordering::SeqCst);
Ok(self.addrs.clone())
}
}
pub(crate) struct FailingResolver;
#[async_trait]
impl ScionDnsResolver for FailingResolver {
async fn resolve(&self, domain: &str) -> Result<Vec<ScionIpAddr>, ResolveError> {
Err(ResolveError::DnsLookup(format!(
"no DNS in tests: {domain}"
)))
}
}
pub(crate) fn test_network(
binder: Arc<dyn SocketBinder>,
resolver: Arc<dyn ScionDnsResolver>,
) -> Arc<Network> {
Arc::new(Network::new(binder, resolver))
}
pub(crate) fn test_epoch(
binder: Arc<dyn SocketBinder>,
resolver: Arc<dyn ScionDnsResolver>,
config: &Config,
) -> Arc<Epoch> {
Arc::new(Epoch::new(binder, resolver, config))
}