use crate::config::TapHttpConfig;
use crate::error::{Error, Result};
use crate::event::{EventBus, EventLogger};
use crate::handler::{handle_didcomm, handle_health_check, handle_well_known_did};
use std::convert::Infallible;
use std::net::SocketAddr;
use std::sync::Arc;
use tap_node::TapNode;
use tokio::sync::oneshot;
use tracing::{error, info, warn};
use warp::{Filter, Rejection, Reply};
pub struct TapHttpServer {
config: TapHttpConfig,
node: Arc<TapNode>,
shutdown_tx: Option<oneshot::Sender<()>>,
event_bus: Arc<EventBus>,
}
impl TapHttpServer {
pub fn new(config: TapHttpConfig, node: TapNode) -> Self {
if config.rate_limit.is_some() {
warn!("Rate limiting is configured but not yet implemented");
}
if config.tls.is_some() {
warn!("TLS is configured but not yet fully implemented");
}
let event_bus = Arc::new(EventBus::new());
if let Some(logger_config) = &config.event_logger {
let event_logger = EventLogger::new(logger_config.clone());
event_bus.subscribe(event_logger);
info!("Event logging enabled");
}
Self {
config,
node: Arc::new(node),
shutdown_tx: None,
event_bus,
}
}
pub async fn start(&mut self) -> Result<()> {
let addr: SocketAddr = self
.config
.server_addr()
.parse()
.map_err(|e| Error::Http(format!("Invalid address: {}", e)))?;
let node = self.node.clone();
let event_bus = self.event_bus.clone();
let endpoint_path = self
.config
.didcomm_endpoint
.trim_start_matches('/')
.to_string();
let didcomm_route = warp::path(endpoint_path)
.and(warp::post())
.and(warp::header::optional::<String>("content-type"))
.and(warp::body::content_length_limit(1024 * 1024))
.and(warp::body::bytes())
.and(with_node(node.clone()))
.and(with_event_bus(event_bus.clone()))
.and_then(handle_didcomm);
let health_route = warp::path("health")
.and(warp::get())
.and(with_event_bus(event_bus.clone()))
.and_then(handle_health_check);
let enable_web_did = self.config.enable_web_did;
if enable_web_did {
info!("Web DID hosting enabled at /.well-known/did.json");
let max_agents = self.config.max_agents;
let well_known_route = warp::path(".well-known")
.and(warp::path("did.json"))
.and(warp::path::end())
.and(warp::get())
.and(warp::header::optional::<String>("host"))
.and(with_node(node.clone()))
.and(with_event_bus(event_bus.clone()))
.and(warp::any().map(move || max_agents))
.and_then(handle_well_known_did);
let routes = didcomm_route
.or(health_route)
.or(well_known_route)
.with(warp::log("tap_http"))
.with(warp::reply::with::header(
"X-Content-Type-Options",
"nosniff",
))
.with(warp::reply::with::header("X-Frame-Options", "DENY"))
.with(warp::reply::with::header("Cache-Control", "no-store"))
.with(warp::reply::with::header(
"Content-Security-Policy",
"default-src 'none'",
))
.recover(handle_rejection);
return self.spawn_server(routes, addr, event_bus).await;
}
let routes = didcomm_route
.or(health_route)
.with(warp::log("tap_http"))
.with(warp::reply::with::header(
"X-Content-Type-Options",
"nosniff",
))
.with(warp::reply::with::header("X-Frame-Options", "DENY"))
.with(warp::reply::with::header("Cache-Control", "no-store"))
.with(warp::reply::with::header(
"Content-Security-Policy",
"default-src 'none'",
))
.recover(handle_rejection);
self.spawn_server(routes, addr, event_bus).await
}
pub async fn stop(&mut self) -> Result<()> {
if let Some(tx) = self.shutdown_tx.take() {
let _ = tx.send(());
info!("Sent shutdown signal to TAP HTTP server");
} else {
warn!("TAP HTTP server is not running");
}
Ok(())
}
pub fn node(&self) -> &Arc<TapNode> {
&self.node
}
pub fn config(&self) -> &TapHttpConfig {
&self.config
}
pub fn event_bus(&self) -> &Arc<EventBus> {
&self.event_bus
}
async fn spawn_server<F>(
&mut self,
routes: F,
addr: SocketAddr,
event_bus: Arc<EventBus>,
) -> Result<()>
where
F: Filter<Error = Infallible> + Clone + Send + Sync + 'static,
F::Extract: Reply,
{
let (tx, rx) = oneshot::channel::<()>();
self.shutdown_tx = Some(tx);
info!("Starting TAP HTTP server on {}", addr);
self.event_bus
.publish_server_started(addr.to_string())
.await;
let event_bus_clone = event_bus.clone();
let (_, server) = warp::serve(routes).bind_with_graceful_shutdown(addr, async move {
rx.await.ok();
info!("Shutting down TAP HTTP server");
event_bus_clone.publish_server_stopped().await;
});
tokio::spawn(server);
info!("TAP HTTP server started on {}", addr);
Ok(())
}
}
fn with_node(
node: Arc<TapNode>,
) -> impl Filter<Extract = (Arc<TapNode>,), Error = Infallible> + Clone {
warp::any().map(move || node.clone())
}
fn with_event_bus(
event_bus: Arc<EventBus>,
) -> impl Filter<Extract = (Arc<EventBus>,), Error = Infallible> + Clone {
warp::any().map(move || event_bus.clone())
}
#[derive(Debug)]
struct RateLimitedError;
impl warp::reject::Reject for RateLimitedError {}
async fn handle_rejection(err: Rejection) -> std::result::Result<impl Reply, Infallible> {
use crate::error::Error;
let error_response = if err.is_not_found() {
let err = Error::Http("Resource not found".to_string());
err.to_response()
} else if err.find::<warp::reject::PayloadTooLarge>().is_some() {
let err = Error::Http("Payload too large".to_string());
err.to_response()
} else if err.find::<warp::reject::UnsupportedMediaType>().is_some() {
let err = Error::Http("Unsupported media type".to_string());
err.to_response()
} else if err.find::<warp::reject::MethodNotAllowed>().is_some() {
let err = Error::Http("Method not allowed".to_string());
err.to_response()
} else if err.find::<RateLimitedError>().is_some() {
let err = Error::RateLimit("Too many requests, please try again later".to_string());
err.to_response()
} else {
error!("Unhandled rejection: {:?}", err);
let err = Error::Unknown("Internal server error".to_string());
err.to_response()
};
Ok(error_response)
}