use crate::client::{EnclaveCertVerifier, TtkClient};
use axum::extract::State;
use axum::http::StatusCode;
use axum::routing::{get, post};
use axum::{Json, Router};
use base64::{engine::general_purpose::STANDARD, Engine as _};
use log::{info, warn};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
type SendError = Box<dyn std::error::Error + Send + Sync>;
#[derive(Clone, Debug)]
pub struct Evidence {
pub nitro: Vec<u8>,
pub eat: Vec<u8>,
}
pub const DEFAULT_RELAY_PORT: u16 = 4433;
pub const RELAY_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
pub const RELAY_CONNECT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(3);
pub const ALLOW_MOCK_RELAY_ENV: &str = "TTK_ALLOW_MOCK_ATTESTATION";
pub type RelayVerifierFactory = Arc<dyn Fn() -> EnclaveCertVerifier + Send + Sync>;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FafRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub relay_server: Option<String>,
pub message: String,
pub key: String,
}
pub(crate) fn build_router(
evidence: Arc<Evidence>,
relay_verifier: RelayVerifierFactory,
) -> Router {
let eat_b64 = STANDARD.encode(&evidence.eat);
Router::new()
.route("/", get(|| async { "Hello from Enclave over HTTP/3!" }))
.route("/evidence.eat", get(move || async move { eat_b64 }))
.route("/faf", post(faf))
.with_state(relay_verifier)
}
async fn faf(
State(relay_verifier): State<RelayVerifierFactory>,
Json(request): Json<FafRequest>,
) -> (StatusCode, String) {
let FafRequest {
relay_server,
message,
key,
} = request;
let Some(relay_server) = relay_server else {
info!(
"/faf: accepted a {}-byte message as the last hop",
message.len()
);
return (StatusCode::OK, "delivered".to_string());
};
let (host, port) = match parse_relay_server(&relay_server) {
Ok(target) => target,
Err(e) => {
return (
StatusCode::BAD_REQUEST,
format!("invalid relay_server: {e}"),
)
}
};
let forward = FafRequest {
relay_server: None,
message,
key,
};
let relayed = tokio::time::timeout(
RELAY_TIMEOUT,
forward_to_relay(&host, port, relay_verifier(), &forward),
)
.await;
match relayed {
Ok(Ok(StatusCode::OK)) => {
info!("/faf: relayed to {host}:{port}");
(StatusCode::OK, "relayed".to_string())
}
Ok(Ok(status)) => {
warn!("/faf: relay {host}:{port} answered {status}");
(StatusCode::BAD_GATEWAY, format!("relay answered {status}"))
}
Ok(Err(e)) => {
warn!("/faf: relaying to {host}:{port} failed: {e}");
(StatusCode::BAD_GATEWAY, format!("relay failed: {e}"))
}
Err(_) => {
warn!("/faf: relay {host}:{port} timed out");
(StatusCode::GATEWAY_TIMEOUT, "relay timed out".to_string())
}
}
}
pub fn parse_relay_server(relay_server: &str) -> Result<(String, u16), String> {
let uri: axum::http::Uri = relay_server
.trim()
.parse()
.map_err(|e| format!("{relay_server:?}: {e}"))?;
if let Some(scheme) = uri.scheme_str() {
if scheme != "https" {
return Err(format!(
"unsupported scheme {scheme:?}; the relay speaks HTTP/3"
));
}
}
let authority = uri
.authority()
.ok_or_else(|| format!("{relay_server:?} has no host"))?;
if authority.as_str().contains('@') {
return Err("user info is not allowed".to_string());
}
let host = authority
.host()
.trim_start_matches('[')
.trim_end_matches(']');
if host.is_empty() {
return Err(format!("{relay_server:?} has no host"));
}
let port = authority.port_u16().unwrap_or(DEFAULT_RELAY_PORT);
Ok((host.to_string(), port))
}
async fn connect_to_relay(
host: &str,
port: u16,
verifier: EnclaveCertVerifier,
) -> Result<TtkClient, SendError> {
let mut last_error: SendError = format!("{host} did not resolve").into();
for addr in tokio::net::lookup_host((host, port)).await? {
let connecting = TtkClient::connect_with_verifier(addr, host, verifier.clone());
match tokio::time::timeout(RELAY_CONNECT_TIMEOUT, connecting).await {
Ok(Ok(client)) => return Ok(client),
Ok(Err(e)) => last_error = e,
Err(_) => last_error = format!("connecting to {addr} timed out").into(),
}
}
Err(last_error)
}
async fn forward_to_relay(
host: &str,
port: u16,
verifier: EnclaveCertVerifier,
request: &FafRequest,
) -> Result<StatusCode, SendError> {
let mut client = connect_to_relay(host, port, verifier).await?;
let response = client.post_json("/faf", request).await;
let _ = client.close().await;
Ok(response?.status)
}