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, 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
27pub 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
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(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 .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 .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}