Skip to main content

fedimint_gateway_server/
iroh_server.rs

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::Gateway;
25use crate::error::{GatewayError, PublicGatewayError};
26use crate::rpc_server::verify_bolt11_preimage_v2_get;
27
28/// Handler for a GET request, which must contain no parameters and return
29/// `serde_json::Value`
30type 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
39/// Handler for a POST request, which must contain `serde_json::Value` encoded
40/// parameters and return `serde_json::Value`.
41type 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
51/// Creates a GET handler for the Iroh endpoint by wrapping it in a closure.
52fn 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
66/// Creates a POST handler for the Iroh endpoint by wrapping it in a closure.
67fn 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
86/// Helper struct for registering handlers that are called by the Iroh
87/// `Endpoint`. GET handlers and POST handlers are registered separately, since
88/// they contain different function signatures. If a route is authenticated, it
89/// is also stored in `authenticated_routes` which is checked when the specific
90/// handler is called.
91pub 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
147/// Create the Iroh `Endpoint` and spawn a thread that starts listening for
148/// requests.
149pub 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
189/// Handle a specific Iroh request. The request must be deserialized, matched to
190/// a handler, executed, then return a response to the caller.
191async 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
227/// Runs a request handler, turning a panic or an error into a 500 response for
228/// the caller that triggered it.
229///
230/// Iroh requests are spawned on the gateway's root task group, so a panic
231/// escaping a handler trips the task group's panic guard and shuts the whole
232/// gateway down. The HTTP path does not need this: `axum::serve` spawns its
233/// connection tasks outside any task group, so a panic there only drops that
234/// one connection.
235///
236/// A failing handler has to be answered as well: propagating the error would
237/// end the connection's request loop, leaving the caller with a closed stream
238/// and no response at all. Like the HTTP path, the response body carries no
239/// details about the failure, since callers are unauthenticated.
240async 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    // Using `AssertUnwindSafe` here is far from ideal. In theory this means we
245    // could end up with an inconsistent state. In practice this is only the last
246    // line of defense, and losing the gateway process entirely is strictly worse.
247    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
272/// Checks if the requested route is authenticated and will reject the request
273/// if the authentication is incorrect. Then it will lookup the specific handler
274/// in `Handlers`, execute it, and return the function's JSON along with an HTTP
275/// status code.
276async 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 handlers.is_authenticated(&request.route) && iroh_verify_password(&gateway, request).is_err()
283    {
284        return Ok((StatusCode::UNAUTHORIZED, Json(json!(()))));
285    }
286
287    // The STOP endpoint is handled outside of the `Handlers` struct since it has a
288    // different function signature (it needs a `TaskGroup`).
289    if request.route == STOP_ENDPOINT {
290        let body = crate::rpc_server::stop(Extension(task_group), Extension(gateway)).await?;
291        return Ok((StatusCode::OK, body));
292    }
293
294    // The handlers struct also currently does not support query parameters. The
295    // LNURL-verify endpoint is the only endpoint that requires these, so we
296    // handle these separately as well.
297    if let Some(verify_route) = parse_verify_route(&request.route) {
298        let Ok((payment_hash, query_map)) = verify_route else {
299            // The route is the caller's input, so a payment hash we cannot parse is
300            // their mistake, the same way the HTTP path's `Path<sha256::Hash>`
301            // extractor rejects a malformed payment hash.
302            return Ok((StatusCode::BAD_REQUEST, Json(json!(()))));
303        };
304
305        let body =
306            verify_bolt11_preimage_v2_get(Extension(gateway), Path(payment_hash), Query(query_map))
307                .await?;
308
309        return Ok((StatusCode::OK, body));
310    }
311
312    let (status, body) = if let Some(params) = &request.params {
313        let Some(handler) = handlers.get_handler_with_payload(&request.route) else {
314            return Ok(unknown_route(&request.route));
315        };
316
317        (
318            StatusCode::OK,
319            handler(Extension(gateway), params.clone()).await?,
320        )
321    } else {
322        let Some(handler) = handlers.get_handler(&request.route) else {
323            return Ok(unknown_route(&request.route));
324        };
325
326        (StatusCode::OK, handler(Extension(gateway)).await?)
327    };
328
329    Ok((status, body))
330}
331
332/// Response for a route that no handler is registered for.
333fn unknown_route(route: &str) -> (StatusCode, Json<serde_json::Value>) {
334    warn!(
335        target: LOG_GATEWAY,
336        route,
337        "Iroh handler received request with unknown route"
338    );
339
340    (StatusCode::NOT_FOUND, Json(json!(())))
341}
342
343/// Parses the LNURL-verify route `/verify/{payment_hash}` into the payment hash
344/// and the query parameters (`?wait`) of the request.
345///
346/// Returns `None` if the route is not shaped like the verify route at all, so
347/// that the caller falls through to the regular handler lookup and answers with
348/// a 404. Only the exact `/verify/{payment_hash}` shape is this endpoint, the
349/// same way the HTTP path registers it as an exact route: neither a route that
350/// merely starts with `/verify` nor one that appends further path segments may
351/// reach this unauthenticated handler.
352fn parse_verify_route(
353    route: &str,
354) -> Option<anyhow::Result<(sha256::Hash, HashMap<String, String>)>> {
355    // Check the prefix on the raw route, so that a route the URL parser below
356    // normalizes into the verify route (`/../verify/{payment_hash}`) is not this
357    // endpoint either.
358    if !route.starts_with("/verify/") {
359        return None;
360    }
361
362    // Use dummy URL for easier parsing
363    let url = Url::parse(&format!("http://localhost{route}")).ok()?;
364
365    // Extract segments: /verify/<payment_hash>. The first segment is the route
366    // prefix itself, so the payment hash is the second one, and there must not be
367    // a third. The prefix is re-checked here because the raw route may normalize
368    // to a different path (`/verify/../stop`).
369    let mut segments = url.path_segments()?;
370
371    if segments.next() != Some("verify") {
372        return None;
373    }
374
375    let hash_str = segments.next()?;
376
377    if segments.next().is_some() {
378        return None;
379    }
380
381    let payment_hash = match hash_str.parse::<sha256::Hash>() {
382        Ok(payment_hash) => payment_hash,
383        Err(err) => return Some(Err(anyhow!(err).context("Invalid payment hash"))),
384    };
385
386    // Parse query params (?wait etc.)
387    let query_map: HashMap<String, String> = url.query_pairs().into_owned().collect();
388
389    Some(Ok((payment_hash, query_map)))
390}
391
392/// Verifies if the supplied password in the Iroh request matches the gateway's
393/// password
394fn iroh_verify_password(
395    gateway: &Arc<Gateway>,
396    request: &IrohGatewayRequest,
397) -> anyhow::Result<()> {
398    if let Some(password) = request.password.as_ref()
399        && bcrypt::verify(password, &gateway.bcrypt_password_hash)?
400    {
401        return Ok(());
402    }
403
404    Err(anyhow!("Invalid password"))
405}
406
407#[cfg(test)]
408mod tests {
409    use bitcoin::hashes::Hash as _;
410
411    use super::*;
412
413    #[tokio::test]
414    async fn panicking_handler_returns_an_error_instead_of_unwinding() {
415        let (status, _body) = run_handler("/pay_invoice", async { panic!("handler panic") }).await;
416
417        assert_eq!(status, StatusCode::INTERNAL_SERVER_ERROR);
418    }
419
420    #[tokio::test]
421    async fn failing_handler_is_answered_instead_of_ending_the_connection() {
422        let (status, _body) = run_handler("/verify/nonsense", async {
423            Err(anyhow!("Verify route does not contain a payment hash"))
424        })
425        .await;
426
427        assert_eq!(status, StatusCode::INTERNAL_SERVER_ERROR);
428    }
429
430    #[test]
431    fn verify_route_parses_the_payment_hash_not_the_route_prefix() {
432        let payment_hash = sha256::Hash::hash(b"payment hash");
433
434        let (parsed_hash, _query) = parse_verify_route(&format!("/verify/{payment_hash}"))
435            .expect("the route is the verify route")
436            .expect("the payment hash is the second path segment, not the first");
437
438        assert_eq!(parsed_hash, payment_hash);
439    }
440
441    #[test]
442    fn verify_route_parses_the_wait_query_parameter() {
443        let payment_hash = sha256::Hash::hash(b"payment hash");
444
445        let (parsed_hash, query) = parse_verify_route(&format!("/verify/{payment_hash}?wait"))
446            .expect("the route is the verify route")
447            .expect("query parameters do not affect path parsing");
448
449        assert_eq!(parsed_hash, payment_hash);
450        assert!(query.contains_key("wait"));
451    }
452
453    #[test]
454    fn verify_route_with_a_malformed_payment_hash_is_rejected() {
455        parse_verify_route("/verify/")
456            .expect("the route is shaped like the verify route")
457            .expect_err("a route with an empty payment hash has nothing to verify");
458        parse_verify_route("/verify/not-a-payment-hash")
459            .expect("the route is shaped like the verify route")
460            .expect_err("a payment hash that is not a sha256 hash is rejected");
461    }
462
463    #[test]
464    fn only_the_exact_verify_route_reaches_the_verify_endpoint() {
465        let payment_hash = sha256::Hash::hash(b"payment hash");
466
467        // A route that is not exactly `/verify/{payment_hash}` is not this endpoint,
468        // no matter that it starts with `/verify` or contains a valid payment hash
469        // somewhere. Falling through to the handler lookup answers it with a 404,
470        // like the HTTP path's exact route does.
471        for route in [
472            "/verify".to_string(),
473            format!("/verifyfoo/{payment_hash}"),
474            format!("/verify_something/{payment_hash}?wait"),
475            format!("/verify/{payment_hash}/extra"),
476            // Routes the URL parser normalizes are not the verify route either,
477            // whether they normalize into it or out of it.
478            format!("/../verify/{payment_hash}"),
479            format!("/verify/{payment_hash}/../../stop"),
480            "/verify/../stop".to_string(),
481        ] {
482            assert!(
483                parse_verify_route(&route).is_none(),
484                "{route} must not reach the unauthenticated verify handler"
485            );
486        }
487    }
488}