use std::{io::Write, net::SocketAddr, sync::Arc, time::Duration};
use anapaya_aa_client::AnapayaAaTokenSource;
use anapaya_aa_protobuf::proto::anapaya::aa::v1::{
AuthenticateByKeyRequest, AuthenticateByKeyResponse,
};
use anyhow::Context;
use axum::{
Router,
response::{IntoResponse, Response},
routing::{get, post},
};
use axum_connect_rpc::{
error::{CrpcError, CrpcErrorCode},
extractor::ConnectRpc,
};
use pocketscion::util::{
dev_auth_token,
topologies::{IA132, IA212, PsSetup, UnderlayType, minimal::minimal_topology},
};
use scion_h3_axum::ScionH3AxumServer;
use scion_http3::{Client, Config, Request, scion_quic::quic::config::QuicConfig};
use scion_stack::{ScionStackBuilder, resolver::txt::ScionTxtDnsResolver};
use sciparse::address::ip_socket_addr::ScionSocketIpAddr;
use tempfile::NamedTempFile;
use tokio::net::TcpListener;
use tokio_util::sync::CancellationToken;
use url::Url;
const SERVER_NAME: &str = "localhost";
const MAX_BODY_SIZE: usize = 1024;
const API_KEY: &str = "example-api-key";
const DEVICE_ID: &str = "example-device";
#[tokio::main]
async fn main() -> anyhow::Result<()> {
run().await
}
async fn run() -> anyhow::Result<()> {
scion_sdk_utils::rustls::select_ring_crypto_provider();
let ps = minimal_topology(UnderlayType::Udp).await;
let shutdown = CancellationToken::new();
let (server_addr, cert_file) = start_server(&ps, shutdown.clone()).await?;
println!("HTTP/3 server listening on {server_addr}");
let aa_url = start_aa(shutdown.clone()).await?;
println!("Stand-in AA listening on {aa_url}");
let endhost_api = ps.endhost_api(IA132).context("no endhost API")?;
let config = Config::new(endhost_api)
.with_auth_token_source(AnapayaAaTokenSource::new(API_KEY, &aa_url, DEVICE_ID)?);
let config = config
.with_quic_config(
QuicConfig::builder()
.ca_certs_file(cert_file.path().to_str().context("cert path")?)
.build(),
)
.with_resolver(Arc::new(
ScionTxtDnsResolver::new()?.with_override(SERVER_NAME, vec![server_addr.host()]),
));
let client = Client::new(config);
let url = format!("https://{SERVER_NAME}:{}/hello", server_addr.port());
let response = client.get(&url).await?;
let (body, _trailers) = response.text(Some(MAX_BODY_SIZE)).await?;
println!("GET {url} -> {body:?}");
anyhow::ensure!(body == "world", "unexpected GET body: {body:?}");
let url = format!("https://{SERVER_NAME}:{}/echo", server_addr.port());
let sent = "round-trip over HTTP/3 over SCION";
let request = Request::post(&url)
.header("content-type", "text/plain; charset=utf-8")
.body(sent)
.request_timeout(Duration::from_secs(10))
.build()?;
let response = client.request(request).await?;
let (body, _trailers) = response.text(Some(MAX_BODY_SIZE)).await?;
println!("POST {url} -> {body:?}");
anyhow::ensure!(body == sent, "echo mismatch: sent {sent:?}, got {body:?}");
client.close().await;
shutdown.cancel();
Ok(())
}
async fn start_aa(shutdown: CancellationToken) -> anyhow::Result<Url> {
let listener = TcpListener::bind(SocketAddr::from(([127, 0, 0, 1], 0))).await?;
let url = format!("http://{}", listener.local_addr()?).parse()?;
let app = Router::new().route(
"/anapaya.aa.v1.AuthService/AuthenticateByKey",
post(authenticate),
);
tokio::spawn(async move {
let _ = axum::serve(listener, app)
.with_graceful_shutdown(async move { shutdown.cancelled().await })
.await;
});
Ok(url)
}
async fn authenticate(ConnectRpc(request): ConnectRpc<AuthenticateByKeyRequest>) -> Response {
if request.api_key != API_KEY {
return CrpcError::new(
CrpcErrorCode::Unauthenticated,
"unknown API key".to_string(),
)
.into_response();
}
ConnectRpc(AuthenticateByKeyResponse {
snap_token: dev_auth_token(),
..Default::default()
})
.into_response()
}
async fn start_server(
ps: &PsSetup,
shutdown: CancellationToken,
) -> anyhow::Result<(ScionSocketIpAddr, NamedTempFile)> {
let stack = ScionStackBuilder::new()
.with_endhost_api(ps.endhost_api(IA212).context("no endhost API")?)
.with_auth_token(dev_auth_token())
.build()
.await?;
let socket = Arc::new(stack.bind(None).await?);
let server_addr = socket.local_addr();
let cert = rcgen::generate_simple_self_signed(vec![SERVER_NAME.to_string()])?;
let mut cert_file = NamedTempFile::new()?;
let mut key_file = NamedTempFile::new()?;
cert_file
.as_file_mut()
.write_all(cert.cert.pem().as_bytes())?;
key_file
.as_file_mut()
.write_all(cert.signing_key.serialize_pem().as_bytes())?;
let mut server_config = QuicConfig::builder().build().to_quiche_config()?;
server_config
.load_cert_chain_from_pem_file(cert_file.path().to_str().context("cert path")?)
.map_err(|e| anyhow::anyhow!("loading certificate: {e}"))?;
server_config
.load_priv_key_from_pem_file(key_file.path().to_str().context("key path")?)
.map_err(|e| anyhow::anyhow!("loading key: {e}"))?;
let app = Router::new()
.route("/hello", get(|| async { "world" }))
.route("/echo", post(|body: String| async move { body }));
tokio::spawn({
async move {
let _stack = stack;
let _key_file = key_file;
let _ = ScionH3AxumServer::serve_with_graceful_shutdown(
socket,
app,
server_config,
shutdown,
)
.await;
}
});
Ok((server_addr, cert_file))
}
#[cfg(test)]
mod tests {
use test_log::test;
#[test(tokio::test)]
#[ntest::timeout(120_000)]
async fn example_runs() {
super::run().await.unwrap();
}
}