use std::collections::{BTreeMap, BTreeSet, HashMap};
use std::panic::AssertUnwindSafe;
use std::pin::Pin;
use std::sync::Arc;
use anyhow::anyhow;
use axum::extract::{Path, Query};
use axum::{Extension, Json};
use bitcoin::hashes::sha256;
use fedimint_core::module::{FEDIMINT_GATEWAY_ALPN, IrohGatewayRequest, IrohGatewayResponse};
use fedimint_core::net::iroh::build_iroh_endpoint;
use fedimint_core::task::TaskGroup;
use fedimint_core::util::FmtCompactAnyhow as _;
use fedimint_gateway_common::STOP_ENDPOINT;
use fedimint_logging::LOG_GATEWAY;
use futures::FutureExt as _;
use iroh::endpoint::Incoming;
use reqwest::StatusCode;
use serde::de::DeserializeOwned;
use serde_json::json;
use tracing::{error, info, warn};
use url::Url;
use crate::error::{GatewayError, PublicGatewayError};
use crate::rpc_server::verify_bolt11_preimage_v2_get;
use crate::{Gateway, GatewayState};
type GetHandler = Box<
dyn Fn(
Extension<Arc<Gateway>>,
)
-> Pin<Box<dyn Future<Output = Result<Json<serde_json::Value>, GatewayError>> + Send>>
+ Send
+ Sync,
>;
type PostHandler = Box<
dyn Fn(
Extension<Arc<Gateway>>,
serde_json::Value,
)
-> Pin<Box<dyn Future<Output = Result<Json<serde_json::Value>, GatewayError>> + Send>>
+ Send
+ Sync,
>;
fn make_get_handler<F, Fut>(f: F) -> GetHandler
where
F: Fn(Extension<Arc<Gateway>>) -> Fut + Clone + Send + Sync + 'static,
Fut: Future<Output = Result<Json<serde_json::Value>, GatewayError>> + Send + 'static,
{
Box::new(move |gateway: Extension<Arc<Gateway>>| {
let f = f.clone();
Box::pin(async move {
let res = f(gateway).await?;
Ok(res)
})
})
}
fn make_post_handler<P, F, Fut>(f: F) -> PostHandler
where
P: DeserializeOwned + Send + 'static,
F: Fn(Extension<Arc<Gateway>>, Json<P>) -> Fut + Clone + Send + Sync + 'static,
Fut: Future<Output = Result<Json<serde_json::Value>, GatewayError>> + Send + 'static,
{
Box::new(
move |gateway: Extension<Arc<Gateway>>, value: serde_json::Value| {
let f = f.clone();
Box::pin(async move {
let payload: P = serde_json::from_value(value)
.map_err(|e| PublicGatewayError::Unexpected(anyhow!(e.to_string())))?;
let res = f(gateway, Json(payload)).await?;
Ok(res)
})
},
)
}
pub struct Handlers {
get_handlers: BTreeMap<String, GetHandler>,
post_handlers: BTreeMap<String, PostHandler>,
authenticated_routes: BTreeSet<String>,
}
impl Handlers {
pub fn new() -> Self {
let mut authenticated_routes = BTreeSet::new();
authenticated_routes.insert(STOP_ENDPOINT.to_string());
Handlers {
get_handlers: BTreeMap::new(),
post_handlers: BTreeMap::new(),
authenticated_routes,
}
}
pub fn add_handler<F, Fut>(&mut self, route: &str, f: F, is_authenticated: bool)
where
F: Fn(Extension<Arc<Gateway>>) -> Fut + Clone + Send + Sync + 'static,
Fut: Future<Output = Result<Json<serde_json::Value>, GatewayError>> + Send + 'static,
{
if is_authenticated {
self.authenticated_routes.insert(route.to_string());
}
self.get_handlers
.insert(route.to_string(), make_get_handler(f));
}
pub fn get_handler(&self, route: &str) -> Option<&GetHandler> {
self.get_handlers.get(route)
}
pub fn add_handler_with_payload<P, F, Fut>(&mut self, route: &str, f: F, is_authenticated: bool)
where
P: DeserializeOwned + Send + 'static,
F: Fn(Extension<Arc<Gateway>>, Json<P>) -> Fut + Clone + Send + Sync + 'static,
Fut: Future<Output = Result<Json<serde_json::Value>, GatewayError>> + Send + 'static,
{
if is_authenticated {
self.authenticated_routes.insert(route.to_string());
}
self.post_handlers
.insert(route.to_string(), make_post_handler(f));
}
pub fn get_handler_with_payload(&self, route: &str) -> Option<&PostHandler> {
self.post_handlers.get(route)
}
pub fn is_authenticated(&self, route: &str) -> bool {
self.authenticated_routes.contains(route)
}
}
pub async fn start_iroh_endpoint(
gateway: &Arc<Gateway>,
task_group: TaskGroup,
handlers: Arc<Handlers>,
) -> anyhow::Result<()> {
if let Some(iroh_listen) = gateway.iroh_listen {
info!("Building Iroh Endpoint...");
let iroh_endpoint = build_iroh_endpoint(
gateway.iroh_sk.clone(),
iroh_listen,
gateway.iroh_dns.clone(),
gateway.iroh_relays.clone(),
FEDIMINT_GATEWAY_ALPN,
)
.await?;
let gw_clone = gateway.clone();
let tg_clone = task_group.clone();
let handlers_clone = handlers.clone();
info!("Spawning accept loop...");
task_group.spawn("Gateway Iroh", |_| async move {
while let Some(incoming) = iroh_endpoint.accept().await {
info!("Accepted new connection. Spawning handler...");
tg_clone.spawn_cancellable_silent(
"handle endpoint accept",
handle_incoming_iroh_request(
incoming,
gw_clone.clone(),
handlers_clone.clone(),
tg_clone.clone(),
),
);
}
});
info!(target: LOG_GATEWAY, "Successfully started iroh endpoint");
}
Ok(())
}
async fn handle_incoming_iroh_request(
incoming: Incoming,
gateway: Arc<Gateway>,
handlers: Arc<Handlers>,
task_group: TaskGroup,
) -> anyhow::Result<()> {
let connection = incoming.accept()?.await?;
let remote_node_id = &connection.remote_node_id()?;
info!(%remote_node_id, "Handler received connection");
while let Ok((mut send, mut recv)) = connection.accept_bi().await {
let request = recv.read_to_end(100_000).await?;
let request = serde_json::from_slice::<IrohGatewayRequest>(&request)?;
let (status, body) = run_handler(
&request.route,
handle_request(
&request,
gateway.clone(),
handlers.clone(),
task_group.clone(),
),
)
.await;
let response = IrohGatewayResponse {
status: status.as_u16(),
body: body.0,
};
let response = serde_json::to_vec(&response)?;
send.write_all(&response).await?;
send.finish()?;
}
Ok(())
}
async fn run_handler(
route: &str,
handler: impl Future<Output = anyhow::Result<(StatusCode, Json<serde_json::Value>)>>,
) -> (StatusCode, Json<serde_json::Value>) {
let result = AssertUnwindSafe(handler)
.catch_unwind()
.await
.unwrap_or_else(|_| {
error!(
target: LOG_GATEWAY,
route,
"Gateway API handler panicked, DO NOT IGNORE, FIX IT!!!"
);
Ok((StatusCode::INTERNAL_SERVER_ERROR, Json(json!(()))))
});
result.unwrap_or_else(|err| {
warn!(
target: LOG_GATEWAY,
route,
err = %err.fmt_compact_anyhow(),
"Gateway API handler returned an error"
);
(StatusCode::INTERNAL_SERVER_ERROR, Json(json!(())))
})
}
pub(crate) async fn handle_request(
request: &IrohGatewayRequest,
gateway: Arc<Gateway>,
handlers: Arc<Handlers>,
task_group: TaskGroup,
) -> anyhow::Result<(StatusCode, Json<serde_json::Value>)> {
if matches!(
gateway.get_state().await,
GatewayState::NotConfigured { .. }
) && !crate::rpc_server::is_allowed_not_configured_api(
&axum::http::Method::POST,
&request.route,
) {
return Ok(unknown_route(&request.route));
}
if handlers.is_authenticated(&request.route) && iroh_verify_password(&gateway, request).is_err()
{
return Ok((StatusCode::UNAUTHORIZED, Json(json!(()))));
}
if request.route == STOP_ENDPOINT {
let body = crate::rpc_server::stop(Extension(task_group), Extension(gateway)).await?;
return Ok((StatusCode::OK, body));
}
if let Some(verify_route) = parse_verify_route(&request.route) {
let Ok((payment_hash, query_map)) = verify_route else {
return Ok((StatusCode::BAD_REQUEST, Json(json!(()))));
};
let body =
verify_bolt11_preimage_v2_get(Extension(gateway), Path(payment_hash), Query(query_map))
.await?;
return Ok((StatusCode::OK, body));
}
let (status, body) = if let Some(params) = &request.params {
let Some(handler) = handlers.get_handler_with_payload(&request.route) else {
return Ok(unknown_route(&request.route));
};
(
StatusCode::OK,
handler(Extension(gateway), params.clone()).await?,
)
} else {
let Some(handler) = handlers.get_handler(&request.route) else {
return Ok(unknown_route(&request.route));
};
(StatusCode::OK, handler(Extension(gateway)).await?)
};
Ok((status, body))
}
fn unknown_route(route: &str) -> (StatusCode, Json<serde_json::Value>) {
warn!(
target: LOG_GATEWAY,
route,
"Iroh handler received request with unknown route"
);
(StatusCode::NOT_FOUND, Json(json!(())))
}
fn parse_verify_route(
route: &str,
) -> Option<anyhow::Result<(sha256::Hash, HashMap<String, String>)>> {
if !route.starts_with("/verify/") {
return None;
}
let url = Url::parse(&format!("http://localhost{route}")).ok()?;
let mut segments = url.path_segments()?;
if segments.next() != Some("verify") {
return None;
}
let hash_str = segments.next()?;
if segments.next().is_some() {
return None;
}
let payment_hash = match hash_str.parse::<sha256::Hash>() {
Ok(payment_hash) => payment_hash,
Err(err) => return Some(Err(anyhow!(err).context("Invalid payment hash"))),
};
let query_map: HashMap<String, String> = url.query_pairs().into_owned().collect();
Some(Ok((payment_hash, query_map)))
}
fn iroh_verify_password(
gateway: &Arc<Gateway>,
request: &IrohGatewayRequest,
) -> anyhow::Result<()> {
if let Some(password) = request.password.as_ref()
&& bcrypt::verify(password, &gateway.bcrypt_password_hash)?
{
return Ok(());
}
Err(anyhow!("Invalid password"))
}
#[cfg(test)]
mod tests {
use bitcoin::hashes::Hash as _;
use super::*;
#[tokio::test]
async fn panicking_handler_returns_an_error_instead_of_unwinding() {
let (status, _body) = run_handler("/pay_invoice", async { panic!("handler panic") }).await;
assert_eq!(status, StatusCode::INTERNAL_SERVER_ERROR);
}
#[tokio::test]
async fn failing_handler_is_answered_instead_of_ending_the_connection() {
let (status, _body) = run_handler("/verify/nonsense", async {
Err(anyhow!("Verify route does not contain a payment hash"))
})
.await;
assert_eq!(status, StatusCode::INTERNAL_SERVER_ERROR);
}
#[test]
fn verify_route_parses_the_payment_hash_not_the_route_prefix() {
let payment_hash = sha256::Hash::hash(b"payment hash");
let (parsed_hash, _query) = parse_verify_route(&format!("/verify/{payment_hash}"))
.expect("the route is the verify route")
.expect("the payment hash is the second path segment, not the first");
assert_eq!(parsed_hash, payment_hash);
}
#[test]
fn verify_route_parses_the_wait_query_parameter() {
let payment_hash = sha256::Hash::hash(b"payment hash");
let (parsed_hash, query) = parse_verify_route(&format!("/verify/{payment_hash}?wait"))
.expect("the route is the verify route")
.expect("query parameters do not affect path parsing");
assert_eq!(parsed_hash, payment_hash);
assert!(query.contains_key("wait"));
}
#[test]
fn verify_route_with_a_malformed_payment_hash_is_rejected() {
parse_verify_route("/verify/")
.expect("the route is shaped like the verify route")
.expect_err("a route with an empty payment hash has nothing to verify");
parse_verify_route("/verify/not-a-payment-hash")
.expect("the route is shaped like the verify route")
.expect_err("a payment hash that is not a sha256 hash is rejected");
}
#[test]
fn only_the_exact_verify_route_reaches_the_verify_endpoint() {
let payment_hash = sha256::Hash::hash(b"payment hash");
for route in [
"/verify".to_string(),
format!("/verifyfoo/{payment_hash}"),
format!("/verify_something/{payment_hash}?wait"),
format!("/verify/{payment_hash}/extra"),
format!("/../verify/{payment_hash}"),
format!("/verify/{payment_hash}/../../stop"),
"/verify/../stop".to_string(),
] {
assert!(
parse_verify_route(&route).is_none(),
"{route} must not reach the unauthenticated verify handler"
);
}
}
}