fedimint_server/net/api/
mod.rs

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