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