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