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;
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
25pub 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
68pub struct RouterState {
70 pub dependencies: Arc<ServeCommandDependenciesContainer>,
71 pub configuration: RouterConfig,
72}
73
74impl RouterState {
75 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
107pub 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 .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 .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}