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