fedimint_server/net/api/
mod.rs

1pub mod announcement;
2mod http_auth;
3
4use std::fmt::{self, Formatter};
5use std::net::SocketAddr;
6use std::panic::AssertUnwindSafe;
7use std::str::FromStr;
8use std::time::Duration;
9
10use anyhow::{Context, bail};
11use async_trait::async_trait;
12use fedimint_core::core::ModuleInstanceId;
13use fedimint_core::encoding::{Decodable, Encodable};
14use fedimint_core::module::{ApiEndpoint, ApiEndpointContext, ApiError, ApiRequestErased};
15use fedimint_logging::LOG_NET_API;
16use futures::FutureExt;
17use jsonrpsee::RpcModule;
18use jsonrpsee::server::{PingConfig, RpcServiceBuilder, ServerBuilder, ServerHandle};
19use jsonrpsee::types::ErrorObject;
20use tracing::{error, info};
21
22use crate::metrics;
23use crate::net::api::http_auth::HttpAuthLayer;
24
25#[derive(Clone, Encodable, Decodable, Default)]
26pub struct ApiSecrets(Vec<String>);
27
28impl fmt::Debug for ApiSecrets {
29    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
30        f.debug_struct("ApiSecrets")
31            .field("num_secrets", &self.0.len())
32            .finish()
33    }
34}
35
36impl FromStr for ApiSecrets {
37    type Err = anyhow::Error;
38
39    fn from_str(s: &str) -> anyhow::Result<Self> {
40        if s.is_empty() {
41            return Ok(Self(vec![]));
42        }
43
44        let secrets = s
45            .split(',')
46            .map(str::trim)
47            .map(|s| {
48                if s.is_empty() {
49                    bail!("Empty Api Secret is not allowed")
50                }
51                Ok(s.to_string())
52            })
53            .collect::<anyhow::Result<_>>()?;
54        Ok(ApiSecrets(secrets))
55    }
56}
57
58impl ApiSecrets {
59    pub fn is_empty(&self) -> bool {
60        self.0.is_empty()
61    }
62
63    /// Get "active" secret - one that should be used to call other peers
64    pub fn get_active(&self) -> Option<String> {
65        self.0.first().cloned()
66    }
67
68    /// Get all secrets
69    pub fn get_all(&self) -> &[String] {
70        &self.0
71    }
72
73    /// Get empty value - meaning no secrets to use
74    pub fn none() -> ApiSecrets {
75        Self(vec![])
76    }
77}
78
79/// How long to wait before timing out client connections
80const API_ENDPOINT_TIMEOUT: Duration = Duration::from_secs(60);
81
82/// Has the context necessary for serving API endpoints
83///
84/// Returns the specific `State` the endpoint requires and the
85/// `ApiEndpointContext` which all endpoints can access.
86#[async_trait]
87pub trait HasApiContext<State> {
88    async fn context(
89        &self,
90        request: &ApiRequestErased,
91        id: Option<ModuleInstanceId>,
92    ) -> (&State, ApiEndpointContext<'_>);
93}
94
95pub async fn spawn<T>(
96    name: &'static str,
97    api_bind: SocketAddr,
98    module: RpcModule<T>,
99    max_connections: u32,
100    api_secrets: ApiSecrets,
101) -> ServerHandle {
102    info!(target: LOG_NET_API, "Starting http api on ws://{api_bind}");
103
104    let builder = tower::ServiceBuilder::new().layer(HttpAuthLayer::new(api_secrets.get_all()));
105
106    ServerBuilder::new()
107        .max_connections(max_connections)
108        .enable_ws_ping(PingConfig::new().ping_interval(Duration::from_secs(10)))
109        .set_rpc_middleware(RpcServiceBuilder::new().layer(metrics::jsonrpsee::MetricsLayer))
110        .set_http_middleware(builder)
111        .build(&api_bind.to_string())
112        .await
113        .context(format!("Bind address: {api_bind}"))
114        .context(format!("API name: {name}"))
115        .expect("Could not build API server")
116        .start(module)
117}
118
119pub fn attach_endpoints<State, T>(
120    rpc_module: &mut RpcModule<T>,
121    endpoints: Vec<ApiEndpoint<State>>,
122    module_instance_id: Option<ModuleInstanceId>,
123) where
124    T: HasApiContext<State> + Sync + Send + 'static,
125    State: Sync + Send + 'static,
126{
127    for endpoint in endpoints {
128        let path = if let Some(module_instance_id) = module_instance_id {
129            // This memory leak is fine because it only happens on server startup
130            // and path has to live till the end of program anyways.
131            Box::leak(format!("module_{}_{}", module_instance_id, endpoint.path).into_boxed_str())
132        } else {
133            endpoint.path
134        };
135        // Check if paths contain any abnormal characters
136        assert!(
137            !path.contains(|c: char| !matches!(c, '0'..='9' | 'a'..='z' | '_')),
138            "Constructing bad path name {path}"
139        );
140
141        // Another memory leak that is fine because the function is only called once at
142        // startup
143        let handler: &'static _ = Box::leak(endpoint.handler);
144
145        rpc_module
146            .register_async_method(path, move |params, rpc_state, _extensions| async move {
147                let params = params.one::<serde_json::Value>()?;
148
149                // Using AssertUnwindSafe here is far from ideal. In theory this means we could
150                // end up with an inconsistent state in theory. In practice most API functions
151                // are only reading and the few that do write anything are atomic. Lastly, this
152                // is only the last line of defense
153                AssertUnwindSafe(tokio::time::timeout(API_ENDPOINT_TIMEOUT, async {
154                    let request = serde_json::from_value(params)
155                        .map_err(|e| ApiError::bad_request(e.to_string()))?;
156
157                    let (state, context) = rpc_state.context(&request, module_instance_id).await;
158
159                    (handler)(state, context, request).await
160                }))
161                .catch_unwind()
162                .await
163                .map_err(|_| {
164                    error!(
165                        target: LOG_NET_API,
166                        path, "API handler panicked, DO NOT IGNORE, FIX IT!!!"
167                    );
168                    ErrorObject::owned(500, "API handler panicked", None::<()>)
169                })?
170                .map_err(|tokio::time::error::Elapsed { .. }| {
171                    // TODO: find a better error for this, the error we used before:
172                    // jsonrpsee::core::Error::RequestTimeout
173                    // was moved to be client-side only
174                    ErrorObject::owned(-32000, "Request timeout", None::<()>)
175                })?
176                .map_err(|e| ErrorObject::owned(e.code, e.message, None::<()>))
177            })
178            .expect("Failed to register async method");
179    }
180}