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 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 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
73pub struct RouterState {
75 pub dependencies: Arc<ServeCommandDependenciesContainer>,
76 pub configuration: RouterConfig,
77}
78
79impl RouterState {
80 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
112pub 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 .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 .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}