fedimint_gateway_server/
iroh_server.rs

1use std::collections::{BTreeMap, BTreeSet, HashMap};
2use std::pin::Pin;
3use std::sync::Arc;
4
5use anyhow::anyhow;
6use axum::extract::{Path, Query};
7use axum::{Extension, Json};
8use bitcoin::hashes::sha256;
9use fedimint_core::net::iroh::build_iroh_endpoint;
10use fedimint_core::task::TaskGroup;
11use fedimint_gateway_common::{
12    FEDIMINT_GATEWAY_ALPN, IrohGatewayRequest, IrohGatewayResponse, STOP_ENDPOINT,
13};
14use fedimint_logging::LOG_GATEWAY;
15use iroh::endpoint::Incoming;
16use reqwest::StatusCode;
17use serde::de::DeserializeOwned;
18use serde_json::json;
19use tracing::info;
20use url::Url;
21
22use crate::Gateway;
23use crate::error::{GatewayError, PublicGatewayError};
24use crate::rpc_server::verify_bolt11_preimage_v2_get;
25
26/// Handler for a GET request, which must contain no parameters and return
27/// `serde_json::Value`
28type GetHandler = Box<
29    dyn Fn(
30            Extension<Arc<Gateway>>,
31        )
32            -> Pin<Box<dyn Future<Output = Result<Json<serde_json::Value>, GatewayError>> + Send>>
33        + Send
34        + Sync,
35>;
36
37/// Handler for a POST request, which must contain `serde_json::Value` encoded
38/// parameters and return `serde_json::Value`.
39type PostHandler = Box<
40    dyn Fn(
41            Extension<Arc<Gateway>>,
42            serde_json::Value,
43        )
44            -> Pin<Box<dyn Future<Output = Result<Json<serde_json::Value>, GatewayError>> + Send>>
45        + Send
46        + Sync,
47>;
48
49/// Creates a GET handler for the Iroh endpoint by wrapping it in a closure.
50fn make_get_handler<F, Fut>(f: F) -> GetHandler
51where
52    F: Fn(Extension<Arc<Gateway>>) -> Fut + Clone + Send + Sync + 'static,
53    Fut: Future<Output = Result<Json<serde_json::Value>, GatewayError>> + Send + 'static,
54{
55    Box::new(move |gateway: Extension<Arc<Gateway>>| {
56        let f = f.clone();
57        Box::pin(async move {
58            let res = f(gateway).await?;
59            Ok(res)
60        })
61    })
62}
63
64/// Creates a POST handler for the Iroh endpoint by wrapping it in a closure.
65fn make_post_handler<P, F, Fut>(f: F) -> PostHandler
66where
67    P: DeserializeOwned + Send + 'static,
68    F: Fn(Extension<Arc<Gateway>>, Json<P>) -> Fut + Clone + Send + Sync + 'static,
69    Fut: Future<Output = Result<Json<serde_json::Value>, GatewayError>> + Send + 'static,
70{
71    Box::new(
72        move |gateway: Extension<Arc<Gateway>>, value: serde_json::Value| {
73            let f = f.clone();
74            Box::pin(async move {
75                let payload: P = serde_json::from_value(value)
76                    .map_err(|e| PublicGatewayError::Unexpected(anyhow!(e.to_string())))?;
77                let res = f(gateway, Json(payload)).await?;
78                Ok(res)
79            })
80        },
81    )
82}
83
84/// Helper struct for registering handlers that are called by the Iroh
85/// `Endpoint`. GET handlers and POST handlers are registered separately, since
86/// they contain different function signatures. If a route is authenticated, it
87/// is also stored in `authenticated_routes` which is checked when the specific
88/// handler is called.
89pub struct Handlers {
90    get_handlers: BTreeMap<String, GetHandler>,
91    post_handlers: BTreeMap<String, PostHandler>,
92    authenticated_routes: BTreeSet<String>,
93}
94
95impl Handlers {
96    pub fn new() -> Self {
97        let mut authenticated_routes = BTreeSet::new();
98        authenticated_routes.insert(STOP_ENDPOINT.to_string());
99        Handlers {
100            get_handlers: BTreeMap::new(),
101            post_handlers: BTreeMap::new(),
102            authenticated_routes,
103        }
104    }
105
106    pub fn add_handler<F, Fut>(&mut self, route: &str, f: F, is_authenticated: bool)
107    where
108        F: Fn(Extension<Arc<Gateway>>) -> Fut + Clone + Send + Sync + 'static,
109        Fut: Future<Output = Result<Json<serde_json::Value>, GatewayError>> + Send + 'static,
110    {
111        if is_authenticated {
112            self.authenticated_routes.insert(route.to_string());
113        }
114        self.get_handlers
115            .insert(route.to_string(), make_get_handler(f));
116    }
117
118    pub fn get_handler(&self, route: &str) -> Option<&GetHandler> {
119        self.get_handlers.get(route)
120    }
121
122    pub fn add_handler_with_payload<P, F, Fut>(&mut self, route: &str, f: F, is_authenticated: bool)
123    where
124        P: DeserializeOwned + Send + 'static,
125        F: Fn(Extension<Arc<Gateway>>, Json<P>) -> Fut + Clone + Send + Sync + 'static,
126        Fut: Future<Output = Result<Json<serde_json::Value>, GatewayError>> + Send + 'static,
127    {
128        if is_authenticated {
129            self.authenticated_routes.insert(route.to_string());
130        }
131
132        self.post_handlers
133            .insert(route.to_string(), make_post_handler(f));
134    }
135
136    pub fn get_handler_with_payload(&self, route: &str) -> Option<&PostHandler> {
137        self.post_handlers.get(route)
138    }
139
140    pub fn is_authenticated(&self, route: &str) -> bool {
141        self.authenticated_routes.contains(route)
142    }
143}
144
145/// Create the Iroh `Endpoint` and spawn a thread that starts listening for
146/// requests.
147pub async fn start_iroh_endpoint(
148    gateway: &Arc<Gateway>,
149    task_group: TaskGroup,
150    handlers: Arc<Handlers>,
151) -> anyhow::Result<()> {
152    info!("Building Iroh Endpoint...");
153    let iroh_endpoint = build_iroh_endpoint(
154        gateway.iroh_sk.clone(),
155        gateway.iroh_listen,
156        gateway.iroh_dns.clone(),
157        gateway.iroh_relays.clone(),
158        FEDIMINT_GATEWAY_ALPN,
159    )
160    .await?;
161    let gw_clone = gateway.clone();
162    let tg_clone = task_group.clone();
163    let handlers_clone = handlers.clone();
164    info!("Spawning accept loop...");
165    task_group.spawn("Gateway Iroh", |_| async move {
166        while let Some(incoming) = iroh_endpoint.accept().await {
167            info!("Accepted new connection. Spawning handler...");
168            tg_clone.spawn_cancellable_silent(
169                "handle endpoint accept",
170                handle_incoming_iroh_request(
171                    incoming,
172                    gw_clone.clone(),
173                    handlers_clone.clone(),
174                    tg_clone.clone(),
175                ),
176            );
177        }
178    });
179
180    info!(target: LOG_GATEWAY, "Successfully started iroh endpoint");
181
182    Ok(())
183}
184
185/// Handle a specific Iroh request. The request must be deserialized, matched to
186/// a handler, executed, then return a response to the caller.
187async fn handle_incoming_iroh_request(
188    incoming: Incoming,
189    gateway: Arc<Gateway>,
190    handlers: Arc<Handlers>,
191    task_group: TaskGroup,
192) -> anyhow::Result<()> {
193    let connection = incoming.accept()?.await?;
194    let remote_node_id = &connection.remote_node_id()?;
195    info!(%remote_node_id, "Handler received connection");
196    while let Ok((mut send, mut recv)) = connection.accept_bi().await {
197        let request = recv.read_to_end(100_000).await?;
198        let request = serde_json::from_slice::<IrohGatewayRequest>(&request)?;
199
200        let (status, body) = handle_request(
201            &request,
202            gateway.clone(),
203            handlers.clone(),
204            task_group.clone(),
205        )
206        .await?;
207
208        let response = IrohGatewayResponse {
209            status: status.as_u16(),
210            body: body.0,
211        };
212        let response = serde_json::to_vec(&response)?;
213
214        send.write_all(&response).await?;
215        send.finish()?;
216    }
217    Ok(())
218}
219
220/// Checks if the requested route is authenticated and will reject the request
221/// if the authentication is incorrect. Then it will lookup the specific handler
222/// in `Handlers`, execute it, and return the function's JSON along with an HTTP
223/// status code.
224async fn handle_request(
225    request: &IrohGatewayRequest,
226    gateway: Arc<Gateway>,
227    handlers: Arc<Handlers>,
228    task_group: TaskGroup,
229) -> anyhow::Result<(StatusCode, Json<serde_json::Value>)> {
230    if handlers.is_authenticated(&request.route) && iroh_verify_password(&gateway, request).is_err()
231    {
232        return Ok((StatusCode::UNAUTHORIZED, Json(json!(()))));
233    }
234
235    // The STOP endpoint is handled outside of the `Handlers` struct since it has a
236    // different function signature (it needs a `TaskGroup`).
237    if request.route == STOP_ENDPOINT {
238        let body = crate::rpc_server::stop(Extension(task_group), Extension(gateway)).await?;
239        return Ok((StatusCode::OK, body));
240    }
241
242    // The handlers struct also currently does not support query parameters. The
243    // LNURL-verify endpoint is the only endpoint that requires these, so we
244    // handle these separately as well.
245    if request.route.starts_with("/verify") {
246        // Use dummy URL for easier parsing
247        let url = Url::parse(&format!("http://localhost{}", request.route))?;
248        // Extract segments: /verify/<payment_hash>
249        let mut segments = url.path_segments().unwrap();
250        let hash_str = segments.next();
251
252        let payment_hash: sha256::Hash = hash_str.ok_or(anyhow!("No has present"))?.parse()?;
253
254        // Parse query params (?wait etc.)
255        let query_map: HashMap<String, String> = url.query_pairs().into_owned().collect();
256
257        let body =
258            verify_bolt11_preimage_v2_get(Extension(gateway), Path(payment_hash), Query(query_map))
259                .await?;
260
261        return Ok((StatusCode::OK, body));
262    }
263
264    let (status, body) = match &request.params {
265        Some(params) => {
266            if let Some(handler) = handlers.get_handler_with_payload(&request.route) {
267                (
268                    StatusCode::OK,
269                    handler(Extension(gateway), params.clone()).await?,
270                )
271            } else {
272                return Err(anyhow!("Iroh handler received request with unknown route"));
273            }
274        }
275        None => {
276            if let Some(handler) = handlers.get_handler(&request.route) {
277                (StatusCode::OK, handler(Extension(gateway)).await?)
278            } else {
279                return Err(anyhow!("Iroh handler received request with unknown route"));
280            }
281        }
282    };
283
284    Ok((status, body))
285}
286
287/// Verifies if the supplied password in the Iroh request matches the gateway's
288/// password
289fn iroh_verify_password(
290    gateway: &Arc<Gateway>,
291    request: &IrohGatewayRequest,
292) -> anyhow::Result<()> {
293    if let Some(password) = request.password.as_ref()
294        && bcrypt::verify(password, &gateway.bcrypt_password_hash.to_string())?
295    {
296        return Ok(());
297    }
298
299    Err(anyhow!("Invalid password"))
300}