1use std::collections::{BTreeMap, BTreeSet, HashMap};
2use std::panic::AssertUnwindSafe;
3use std::pin::Pin;
4use std::sync::Arc;
5
6use anyhow::anyhow;
7use axum::extract::{Path, Query};
8use axum::{Extension, Json};
9use bitcoin::hashes::sha256;
10use fedimint_core::module::{FEDIMINT_GATEWAY_ALPN, IrohGatewayRequest, IrohGatewayResponse};
11use fedimint_core::net::iroh::build_iroh_endpoint;
12use fedimint_core::task::TaskGroup;
13use fedimint_core::util::FmtCompactAnyhow as _;
14use fedimint_gateway_common::STOP_ENDPOINT;
15use fedimint_logging::LOG_GATEWAY;
16use futures::FutureExt as _;
17use iroh::endpoint::Incoming;
18use reqwest::StatusCode;
19use serde::de::DeserializeOwned;
20use serde_json::json;
21use tracing::{error, info, warn};
22use url::Url;
23
24use crate::error::{GatewayError, PublicGatewayError};
25use crate::rpc_server::verify_bolt11_preimage_v2_get;
26use crate::{Gateway, GatewayState};
27
28type GetHandler = Box<
31 dyn Fn(
32 Extension<Arc<Gateway>>,
33 )
34 -> Pin<Box<dyn Future<Output = Result<Json<serde_json::Value>, GatewayError>> + Send>>
35 + Send
36 + Sync,
37>;
38
39type PostHandler = Box<
42 dyn Fn(
43 Extension<Arc<Gateway>>,
44 serde_json::Value,
45 )
46 -> Pin<Box<dyn Future<Output = Result<Json<serde_json::Value>, GatewayError>> + Send>>
47 + Send
48 + Sync,
49>;
50
51fn make_get_handler<F, Fut>(f: F) -> GetHandler
53where
54 F: Fn(Extension<Arc<Gateway>>) -> Fut + Clone + Send + Sync + 'static,
55 Fut: Future<Output = Result<Json<serde_json::Value>, GatewayError>> + Send + 'static,
56{
57 Box::new(move |gateway: Extension<Arc<Gateway>>| {
58 let f = f.clone();
59 Box::pin(async move {
60 let res = f(gateway).await?;
61 Ok(res)
62 })
63 })
64}
65
66fn make_post_handler<P, F, Fut>(f: F) -> PostHandler
68where
69 P: DeserializeOwned + Send + 'static,
70 F: Fn(Extension<Arc<Gateway>>, Json<P>) -> Fut + Clone + Send + Sync + 'static,
71 Fut: Future<Output = Result<Json<serde_json::Value>, GatewayError>> + Send + 'static,
72{
73 Box::new(
74 move |gateway: Extension<Arc<Gateway>>, value: serde_json::Value| {
75 let f = f.clone();
76 Box::pin(async move {
77 let payload: P = serde_json::from_value(value)
78 .map_err(|e| PublicGatewayError::Unexpected(anyhow!(e.to_string())))?;
79 let res = f(gateway, Json(payload)).await?;
80 Ok(res)
81 })
82 },
83 )
84}
85
86pub struct Handlers {
92 get_handlers: BTreeMap<String, GetHandler>,
93 post_handlers: BTreeMap<String, PostHandler>,
94 authenticated_routes: BTreeSet<String>,
95}
96
97impl Handlers {
98 pub fn new() -> Self {
99 let mut authenticated_routes = BTreeSet::new();
100 authenticated_routes.insert(STOP_ENDPOINT.to_string());
101 Handlers {
102 get_handlers: BTreeMap::new(),
103 post_handlers: BTreeMap::new(),
104 authenticated_routes,
105 }
106 }
107
108 pub fn add_handler<F, Fut>(&mut self, route: &str, f: F, is_authenticated: bool)
109 where
110 F: Fn(Extension<Arc<Gateway>>) -> Fut + Clone + Send + Sync + 'static,
111 Fut: Future<Output = Result<Json<serde_json::Value>, GatewayError>> + Send + 'static,
112 {
113 if is_authenticated {
114 self.authenticated_routes.insert(route.to_string());
115 }
116 self.get_handlers
117 .insert(route.to_string(), make_get_handler(f));
118 }
119
120 pub fn get_handler(&self, route: &str) -> Option<&GetHandler> {
121 self.get_handlers.get(route)
122 }
123
124 pub fn add_handler_with_payload<P, F, Fut>(&mut self, route: &str, f: F, is_authenticated: bool)
125 where
126 P: DeserializeOwned + Send + 'static,
127 F: Fn(Extension<Arc<Gateway>>, Json<P>) -> Fut + Clone + Send + Sync + 'static,
128 Fut: Future<Output = Result<Json<serde_json::Value>, GatewayError>> + Send + 'static,
129 {
130 if is_authenticated {
131 self.authenticated_routes.insert(route.to_string());
132 }
133
134 self.post_handlers
135 .insert(route.to_string(), make_post_handler(f));
136 }
137
138 pub fn get_handler_with_payload(&self, route: &str) -> Option<&PostHandler> {
139 self.post_handlers.get(route)
140 }
141
142 pub fn is_authenticated(&self, route: &str) -> bool {
143 self.authenticated_routes.contains(route)
144 }
145}
146
147pub async fn start_iroh_endpoint(
150 gateway: &Arc<Gateway>,
151 task_group: TaskGroup,
152 handlers: Arc<Handlers>,
153) -> anyhow::Result<()> {
154 if let Some(iroh_listen) = gateway.iroh_listen {
155 info!("Building Iroh Endpoint...");
156 let iroh_endpoint = build_iroh_endpoint(
157 gateway.iroh_sk.clone(),
158 iroh_listen,
159 gateway.iroh_dns.clone(),
160 gateway.iroh_relays.clone(),
161 FEDIMINT_GATEWAY_ALPN,
162 )
163 .await?;
164 let gw_clone = gateway.clone();
165 let tg_clone = task_group.clone();
166 let handlers_clone = handlers.clone();
167 info!("Spawning accept loop...");
168 task_group.spawn("Gateway Iroh", |_| async move {
169 while let Some(incoming) = iroh_endpoint.accept().await {
170 info!("Accepted new connection. Spawning handler...");
171 tg_clone.spawn_cancellable_silent(
172 "handle endpoint accept",
173 handle_incoming_iroh_request(
174 incoming,
175 gw_clone.clone(),
176 handlers_clone.clone(),
177 tg_clone.clone(),
178 ),
179 );
180 }
181 });
182
183 info!(target: LOG_GATEWAY, "Successfully started iroh endpoint");
184 }
185
186 Ok(())
187}
188
189async fn handle_incoming_iroh_request(
192 incoming: Incoming,
193 gateway: Arc<Gateway>,
194 handlers: Arc<Handlers>,
195 task_group: TaskGroup,
196) -> anyhow::Result<()> {
197 let connection = incoming.accept()?.await?;
198 let remote_node_id = &connection.remote_node_id()?;
199 info!(%remote_node_id, "Handler received connection");
200 while let Ok((mut send, mut recv)) = connection.accept_bi().await {
201 let request = recv.read_to_end(100_000).await?;
202 let request = serde_json::from_slice::<IrohGatewayRequest>(&request)?;
203
204 let (status, body) = run_handler(
205 &request.route,
206 handle_request(
207 &request,
208 gateway.clone(),
209 handlers.clone(),
210 task_group.clone(),
211 ),
212 )
213 .await;
214
215 let response = IrohGatewayResponse {
216 status: status.as_u16(),
217 body: body.0,
218 };
219 let response = serde_json::to_vec(&response)?;
220
221 send.write_all(&response).await?;
222 send.finish()?;
223 }
224 Ok(())
225}
226
227async fn run_handler(
241 route: &str,
242 handler: impl Future<Output = anyhow::Result<(StatusCode, Json<serde_json::Value>)>>,
243) -> (StatusCode, Json<serde_json::Value>) {
244 let result = AssertUnwindSafe(handler)
248 .catch_unwind()
249 .await
250 .unwrap_or_else(|_| {
251 error!(
252 target: LOG_GATEWAY,
253 route,
254 "Gateway API handler panicked, DO NOT IGNORE, FIX IT!!!"
255 );
256
257 Ok((StatusCode::INTERNAL_SERVER_ERROR, Json(json!(()))))
258 });
259
260 result.unwrap_or_else(|err| {
261 warn!(
262 target: LOG_GATEWAY,
263 route,
264 err = %err.fmt_compact_anyhow(),
265 "Gateway API handler returned an error"
266 );
267
268 (StatusCode::INTERNAL_SERVER_ERROR, Json(json!(())))
269 })
270}
271
272pub(crate) async fn handle_request(
277 request: &IrohGatewayRequest,
278 gateway: Arc<Gateway>,
279 handlers: Arc<Handlers>,
280 task_group: TaskGroup,
281) -> anyhow::Result<(StatusCode, Json<serde_json::Value>)> {
282 if matches!(
283 gateway.get_state().await,
284 GatewayState::NotConfigured { .. }
285 ) && !crate::rpc_server::is_allowed_not_configured_api(
286 &axum::http::Method::POST,
287 &request.route,
288 ) {
289 return Ok(unknown_route(&request.route));
290 }
291
292 if handlers.is_authenticated(&request.route) && iroh_verify_password(&gateway, request).is_err()
293 {
294 return Ok((StatusCode::UNAUTHORIZED, Json(json!(()))));
295 }
296
297 if request.route == STOP_ENDPOINT {
300 let body = crate::rpc_server::stop(Extension(task_group), Extension(gateway)).await?;
301 return Ok((StatusCode::OK, body));
302 }
303
304 if let Some(verify_route) = parse_verify_route(&request.route) {
308 let Ok((payment_hash, query_map)) = verify_route else {
309 return Ok((StatusCode::BAD_REQUEST, Json(json!(()))));
313 };
314
315 let body =
316 verify_bolt11_preimage_v2_get(Extension(gateway), Path(payment_hash), Query(query_map))
317 .await?;
318
319 return Ok((StatusCode::OK, body));
320 }
321
322 let (status, body) = if let Some(params) = &request.params {
323 let Some(handler) = handlers.get_handler_with_payload(&request.route) else {
324 return Ok(unknown_route(&request.route));
325 };
326
327 (
328 StatusCode::OK,
329 handler(Extension(gateway), params.clone()).await?,
330 )
331 } else {
332 let Some(handler) = handlers.get_handler(&request.route) else {
333 return Ok(unknown_route(&request.route));
334 };
335
336 (StatusCode::OK, handler(Extension(gateway)).await?)
337 };
338
339 Ok((status, body))
340}
341
342fn unknown_route(route: &str) -> (StatusCode, Json<serde_json::Value>) {
344 warn!(
345 target: LOG_GATEWAY,
346 route,
347 "Iroh handler received request with unknown route"
348 );
349
350 (StatusCode::NOT_FOUND, Json(json!(())))
351}
352
353fn parse_verify_route(
363 route: &str,
364) -> Option<anyhow::Result<(sha256::Hash, HashMap<String, String>)>> {
365 if !route.starts_with("/verify/") {
369 return None;
370 }
371
372 let url = Url::parse(&format!("http://localhost{route}")).ok()?;
374
375 let mut segments = url.path_segments()?;
380
381 if segments.next() != Some("verify") {
382 return None;
383 }
384
385 let hash_str = segments.next()?;
386
387 if segments.next().is_some() {
388 return None;
389 }
390
391 let payment_hash = match hash_str.parse::<sha256::Hash>() {
392 Ok(payment_hash) => payment_hash,
393 Err(err) => return Some(Err(anyhow!(err).context("Invalid payment hash"))),
394 };
395
396 let query_map: HashMap<String, String> = url.query_pairs().into_owned().collect();
398
399 Some(Ok((payment_hash, query_map)))
400}
401
402fn iroh_verify_password(
405 gateway: &Arc<Gateway>,
406 request: &IrohGatewayRequest,
407) -> anyhow::Result<()> {
408 if let Some(password) = request.password.as_ref()
409 && bcrypt::verify(password, &gateway.bcrypt_password_hash)?
410 {
411 return Ok(());
412 }
413
414 Err(anyhow!("Invalid password"))
415}
416
417#[cfg(test)]
418mod tests {
419 use bitcoin::hashes::Hash as _;
420
421 use super::*;
422
423 #[tokio::test]
424 async fn panicking_handler_returns_an_error_instead_of_unwinding() {
425 let (status, _body) = run_handler("/pay_invoice", async { panic!("handler panic") }).await;
426
427 assert_eq!(status, StatusCode::INTERNAL_SERVER_ERROR);
428 }
429
430 #[tokio::test]
431 async fn failing_handler_is_answered_instead_of_ending_the_connection() {
432 let (status, _body) = run_handler("/verify/nonsense", async {
433 Err(anyhow!("Verify route does not contain a payment hash"))
434 })
435 .await;
436
437 assert_eq!(status, StatusCode::INTERNAL_SERVER_ERROR);
438 }
439
440 #[test]
441 fn verify_route_parses_the_payment_hash_not_the_route_prefix() {
442 let payment_hash = sha256::Hash::hash(b"payment hash");
443
444 let (parsed_hash, _query) = parse_verify_route(&format!("/verify/{payment_hash}"))
445 .expect("the route is the verify route")
446 .expect("the payment hash is the second path segment, not the first");
447
448 assert_eq!(parsed_hash, payment_hash);
449 }
450
451 #[test]
452 fn verify_route_parses_the_wait_query_parameter() {
453 let payment_hash = sha256::Hash::hash(b"payment hash");
454
455 let (parsed_hash, query) = parse_verify_route(&format!("/verify/{payment_hash}?wait"))
456 .expect("the route is the verify route")
457 .expect("query parameters do not affect path parsing");
458
459 assert_eq!(parsed_hash, payment_hash);
460 assert!(query.contains_key("wait"));
461 }
462
463 #[test]
464 fn verify_route_with_a_malformed_payment_hash_is_rejected() {
465 parse_verify_route("/verify/")
466 .expect("the route is shaped like the verify route")
467 .expect_err("a route with an empty payment hash has nothing to verify");
468 parse_verify_route("/verify/not-a-payment-hash")
469 .expect("the route is shaped like the verify route")
470 .expect_err("a payment hash that is not a sha256 hash is rejected");
471 }
472
473 #[test]
474 fn only_the_exact_verify_route_reaches_the_verify_endpoint() {
475 let payment_hash = sha256::Hash::hash(b"payment hash");
476
477 for route in [
482 "/verify".to_string(),
483 format!("/verifyfoo/{payment_hash}"),
484 format!("/verify_something/{payment_hash}?wait"),
485 format!("/verify/{payment_hash}/extra"),
486 format!("/../verify/{payment_hash}"),
489 format!("/verify/{payment_hash}/../../stop"),
490 "/verify/../stop".to_string(),
491 ] {
492 assert!(
493 parse_verify_route(&route).is_none(),
494 "{route} must not reach the unauthenticated verify handler"
495 );
496 }
497 }
498}