mithril_aggregator/http_server/routes/
router.rs

1use crate::ServeCommandDependenciesContainer;
2use crate::http_server::SERVER_BASE_PATH;
3use crate::http_server::routes::{
4    artifact_routes, certificate_routes, epoch_routes, root_routes, signatures_routes,
5    signer_routes, statistics_routes, status,
6};
7use crate::tools::url_sanitizer::SanitizedUrlWithTrailingSlash;
8
9use mithril_common::api_version::APIVersionProvider;
10use mithril_common::entities::SignedEntityTypeDiscriminants;
11use mithril_common::{
12    CardanoNetwork, MITHRIL_API_VERSION_HEADER, MITHRIL_CLIENT_TYPE_HEADER,
13    MITHRIL_ORIGIN_TAG_HEADER,
14};
15
16use std::collections::{BTreeSet, HashSet};
17use std::path::PathBuf;
18use std::sync::Arc;
19use warp::http::Method;
20use warp::http::StatusCode;
21use warp::{Filter, Rejection, Reply};
22
23use super::{middlewares, proof_routes};
24
25/// HTTP Server configuration
26pub struct RouterConfig {
27    pub network: CardanoNetwork,
28    pub server_url: SanitizedUrlWithTrailingSlash,
29    pub allowed_discriminants: BTreeSet<SignedEntityTypeDiscriminants>,
30    pub cardano_transactions_prover_max_hashes_allowed_by_request: usize,
31    pub cardano_db_artifacts_directory: PathBuf,
32    pub snapshot_directory: PathBuf,
33    pub cardano_node_version: String,
34    pub allow_http_serve_directory: bool,
35    pub origin_tag_white_list: HashSet<String>,
36}
37
38#[cfg(test)]
39impl RouterConfig {
40    pub fn dummy() -> Self {
41        Self {
42            network: CardanoNetwork::DevNet(87),
43            server_url: SanitizedUrlWithTrailingSlash::parse("http://0.0.0.0:8000/").unwrap(),
44            allowed_discriminants: BTreeSet::from([
45                SignedEntityTypeDiscriminants::MithrilStakeDistribution,
46                SignedEntityTypeDiscriminants::CardanoStakeDistribution,
47            ]),
48            cardano_transactions_prover_max_hashes_allowed_by_request: 1_000,
49            cardano_db_artifacts_directory: PathBuf::from("/dummy/cardano-db/directory"),
50            snapshot_directory: PathBuf::from("/dummy/snapshot/directory"),
51            cardano_node_version: "1.2.3".to_string(),
52            allow_http_serve_directory: false,
53            origin_tag_white_list: HashSet::from(["DUMMY_TAG".to_string()]),
54        }
55    }
56
57    pub fn dummy_with_origin_tag_white_list(origin_tag_white_list: &[&str]) -> Self {
58        Self {
59            origin_tag_white_list: origin_tag_white_list
60                .iter()
61                .map(|tag| tag.to_string())
62                .collect(),
63            ..RouterConfig::dummy()
64        }
65    }
66}
67
68/// Shared state for the router
69pub struct RouterState {
70    pub dependencies: Arc<ServeCommandDependenciesContainer>,
71    pub configuration: RouterConfig,
72}
73
74impl RouterState {
75    /// `RouterState` factory
76    pub fn new(
77        dependencies: Arc<ServeCommandDependenciesContainer>,
78        configuration: RouterConfig,
79    ) -> Self {
80        Self {
81            dependencies,
82            configuration,
83        }
84    }
85}
86
87#[cfg(test)]
88impl RouterState {
89    pub fn new_with_dummy_config(dependencies: Arc<ServeCommandDependenciesContainer>) -> Self {
90        Self {
91            dependencies,
92            configuration: RouterConfig::dummy(),
93        }
94    }
95
96    pub fn new_with_origin_tag_white_list(
97        dependencies: Arc<ServeCommandDependenciesContainer>,
98        origin_tag_white_list: &[&str],
99    ) -> Self {
100        Self {
101            dependencies,
102            configuration: RouterConfig::dummy_with_origin_tag_white_list(origin_tag_white_list),
103        }
104    }
105}
106
107/// Routes
108pub fn routes(
109    state: Arc<RouterState>,
110) -> impl Filter<Extract = (impl Reply,), Error = Rejection> + Clone {
111    let cors = warp::cors()
112        .allow_any_origin()
113        .allow_headers(vec![
114            "content-type",
115            MITHRIL_API_VERSION_HEADER,
116            MITHRIL_ORIGIN_TAG_HEADER,
117            MITHRIL_CLIENT_TYPE_HEADER,
118        ])
119        .allow_methods(vec![Method::GET, Method::POST, Method::OPTIONS]);
120
121    warp::any()
122        .and(warp::path(SERVER_BASE_PATH))
123        .and(
124            certificate_routes::routes(&state)
125                .or(artifact_routes::snapshot::routes(&state))
126                .or(artifact_routes::cardano_database::routes(&state))
127                .or(artifact_routes::mithril_stake_distribution::routes(&state))
128                .or(artifact_routes::cardano_stake_distribution::routes(&state))
129                .or(artifact_routes::cardano_transaction::routes(&state))
130                .or(proof_routes::routes(&state))
131                .or(signer_routes::routes(&state))
132                .or(signatures_routes::routes(&state))
133                .or(epoch_routes::routes(&state))
134                .or(statistics_routes::routes(&state))
135                .or(root_routes::routes(&state))
136                .or(status::routes(&state)),
137        )
138        .recover(handle_custom)
139        .and(middlewares::with_api_version_provider(&state))
140        .map(|reply, api_version_provider: Arc<APIVersionProvider>| {
141            warp::reply::with_header(
142                reply,
143                MITHRIL_API_VERSION_HEADER,
144                &api_version_provider.compute_current_version().unwrap().to_string(),
145            )
146        })
147        .with(cors)
148        .with(middlewares::log_route_call(&state))
149}
150
151pub async fn handle_custom(reject: Rejection) -> Result<impl Reply, Rejection> {
152    if reject.is_not_found() {
153        Ok(StatusCode::NOT_FOUND)
154    } else {
155        Err(reject)
156    }
157}
158
159#[cfg(test)]
160mod tests {
161    use warp::test::RequestBuilder;
162
163    use mithril_common::{MITHRIL_CLIENT_TYPE_HEADER, MITHRIL_ORIGIN_TAG_HEADER};
164
165    use crate::initialize_dependencies;
166
167    use super::*;
168
169    #[tokio::test]
170    async fn test_404_response_should_include_status_code_and_headers() {
171        let container = Arc::new(initialize_dependencies!().await);
172        let state = RouterState::new_with_dummy_config(container);
173        let routes = routes(Arc::new(state));
174
175        let response = warp::test::request()
176            .path("/aggregator/a-route-that-does-not-exist")
177            // We need to set the Origin header to trigger the CORS middleware
178            .header("Origin", "http://localhost")
179            .reply(&routes)
180            .await;
181        let response_headers = response.headers();
182
183        assert_eq!(response.status(), StatusCode::NOT_FOUND);
184        assert!(
185            response_headers.get(MITHRIL_API_VERSION_HEADER).is_some(),
186            "API version header should be present, headers: {response_headers:?}",
187        );
188        assert!(
189            response_headers.get("access-control-allow-origin").is_some(),
190            "CORS headers should be present, headers: {response_headers:?}",
191        );
192    }
193
194    #[tokio::test]
195    async fn test_authorized_request_headers() {
196        fn request_with_access_control_request_headers(headers: String) -> RequestBuilder {
197            warp::test::request()
198                .method("OPTIONS")
199                .path("/aggregator")
200                // We need to set the Origin header to trigger the CORS middleware
201                .header("Origin", "http://localhost")
202                .header("access-control-request-method", "GET")
203                .header("access-control-request-headers", headers)
204        }
205
206        let container = Arc::new(initialize_dependencies!().await);
207        let state = RouterState::new_with_dummy_config(container);
208        let routes = routes(Arc::new(state));
209
210        assert!(
211            !request_with_access_control_request_headers("unauthorized_header".to_string())
212                .reply(&routes)
213                .await
214                .status()
215                .is_success()
216        );
217
218        assert!(request_with_access_control_request_headers(format!(
219            "{MITHRIL_API_VERSION_HEADER},{MITHRIL_ORIGIN_TAG_HEADER},{MITHRIL_CLIENT_TYPE_HEADER}"
220        ))
221        .reply(&routes)
222        .await
223        .status()
224        .is_success());
225    }
226}