mithril_aggregator/http_server/routes/
router.rs1use crate::ServeCommandDependenciesContainer;
2use crate::http_server::SERVER_BASE_PATH;
3use crate::http_server::routes::{
4 artifact_routes, certificate_routes, epoch_routes, protocol_configuration_routes, root_routes,
5 signatures_routes, 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
27pub struct RouterConfig {
29 pub network: CardanoNetwork,
30 pub server_url: SanitizedUrlWithTrailingSlash,
31 pub allowed_discriminants: BTreeSet<SignedEntityTypeDiscriminants>,
32 pub cardano_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_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
77pub struct RouterState {
79 pub dependencies: Arc<ServeCommandDependenciesContainer>,
80 pub configuration: RouterConfig,
81}
82
83impl RouterState {
84 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
116pub 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(artifact_routes::cardano_blocks_transactions::routes(&state))
140 .or(proof_routes::routes(&state))
141 .or(signer_routes::routes(&state))
142 .or(signatures_routes::routes(&state))
143 .or(epoch_routes::routes(&state))
144 .or(protocol_configuration_routes::routes(&state))
145 .or(statistics_routes::routes(&state))
146 .or(root_routes::routes(&state))
147 .or(status::routes(&state)),
148 )
149 .recover(handle_custom)
150 .and(middlewares::with_api_version_provider(&state))
151 .map(|reply, api_version_provider: Arc<APIVersionProvider>| {
152 warp::reply::with_header(
153 reply,
154 MITHRIL_API_VERSION_HEADER,
155 &api_version_provider.compute_current_version().unwrap().to_string(),
156 )
157 })
158 .with(cors)
159 .with(middlewares::log_route_call(&state))
160}
161
162pub async fn handle_custom(reject: Rejection) -> Result<impl Reply, Rejection> {
163 if reject.is_not_found() {
164 Ok(StatusCode::NOT_FOUND)
165 } else {
166 Err(reject)
167 }
168}
169
170#[cfg(test)]
171mod tests {
172 use warp::test::RequestBuilder;
173
174 use mithril_common::{MITHRIL_CLIENT_TYPE_HEADER, MITHRIL_ORIGIN_TAG_HEADER};
175
176 use crate::initialize_dependencies;
177
178 use super::*;
179
180 #[tokio::test]
181 async fn test_404_response_should_include_status_code_and_headers() {
182 let container = Arc::new(initialize_dependencies!().await);
183 let state = RouterState::new_with_dummy_config(container);
184 let routes = routes(Arc::new(state));
185
186 let response = warp::test::request()
187 .path("/aggregator/a-route-that-does-not-exist")
188 .header("Origin", "http://localhost")
190 .reply(&routes)
191 .await;
192 let response_headers = response.headers();
193
194 assert_eq!(response.status(), StatusCode::NOT_FOUND);
195 assert!(
196 response_headers.get(MITHRIL_API_VERSION_HEADER).is_some(),
197 "API version header should be present, headers: {response_headers:?}",
198 );
199 assert!(
200 response_headers.get("access-control-allow-origin").is_some(),
201 "CORS headers should be present, headers: {response_headers:?}",
202 );
203 }
204
205 #[tokio::test]
206 async fn test_authorized_request_headers() {
207 fn request_with_access_control_request_headers(headers: String) -> RequestBuilder {
208 warp::test::request()
209 .method("OPTIONS")
210 .path("/aggregator")
211 .header("Origin", "http://localhost")
213 .header("access-control-request-method", "GET")
214 .header("access-control-request-headers", headers)
215 }
216
217 let container = Arc::new(initialize_dependencies!().await);
218 let state = RouterState::new_with_dummy_config(container);
219 let routes = routes(Arc::new(state));
220
221 assert!(
222 !request_with_access_control_request_headers("unauthorized_header".to_string())
223 .reply(&routes)
224 .await
225 .status()
226 .is_success()
227 );
228
229 assert!(request_with_access_control_request_headers(format!(
230 "{MITHRIL_API_VERSION_HEADER},{MITHRIL_ORIGIN_TAG_HEADER},{MITHRIL_CLIENT_TYPE_HEADER}"
231 ))
232 .reply(&routes)
233 .await
234 .status()
235 .is_success());
236 }
237}