mithril_aggregator/http_server/routes/
router.rs1use crate::http_server::routes::{
2 artifact_routes, certificate_routes, epoch_routes, http_server_child_logger, root_routes,
3 signatures_routes, signer_routes, statistics_routes, status,
4};
5use crate::http_server::SERVER_BASE_PATH;
6use crate::tools::url_sanitizer::SanitizedUrlWithTrailingSlash;
7use crate::DependencyContainer;
8
9use mithril_common::api_version::APIVersionProvider;
10use mithril_common::entities::SignedEntityTypeDiscriminants;
11use mithril_common::{CardanoNetwork, MITHRIL_API_VERSION_HEADER};
12
13use slog::{warn, Logger};
14use std::collections::BTreeSet;
15use std::path::PathBuf;
16use std::sync::Arc;
17use warp::http::Method;
18use warp::http::StatusCode;
19use warp::reject::Reject;
20use warp::{Filter, Rejection, Reply};
21
22use super::{middlewares, proof_routes};
23
24#[derive(Debug)]
25pub struct VersionMismatchError;
26
27impl Reject for VersionMismatchError {}
28
29#[derive(Debug)]
30pub struct VersionParseError;
31
32impl Reject for VersionParseError {}
33
34pub struct RouterConfig {
36 pub network: CardanoNetwork,
37 pub server_url: SanitizedUrlWithTrailingSlash,
38 pub allowed_discriminants: BTreeSet<SignedEntityTypeDiscriminants>,
39 pub cardano_transactions_prover_max_hashes_allowed_by_request: usize,
40 pub cardano_db_artifacts_directory: PathBuf,
41 pub snapshot_directory: PathBuf,
42 pub cardano_node_version: String,
43 pub allow_http_serve_directory: bool,
44}
45
46#[cfg(test)]
47impl RouterConfig {
48 pub fn dummy() -> Self {
49 Self {
50 network: CardanoNetwork::DevNet(87),
51 server_url: SanitizedUrlWithTrailingSlash::parse("http://0.0.0.0:8000/").unwrap(),
52 allowed_discriminants: BTreeSet::from([
53 SignedEntityTypeDiscriminants::MithrilStakeDistribution,
54 SignedEntityTypeDiscriminants::CardanoStakeDistribution,
55 ]),
56 cardano_transactions_prover_max_hashes_allowed_by_request: 1_000,
57 cardano_db_artifacts_directory: PathBuf::from("/dummy/cardano-db/directory"),
58 snapshot_directory: PathBuf::from("/dummy/snapshot/directory"),
59 cardano_node_version: "1.2.3".to_string(),
60 allow_http_serve_directory: false,
61 }
62 }
63}
64
65pub struct RouterState {
67 pub dependencies: Arc<DependencyContainer>,
68 pub configuration: RouterConfig,
69}
70
71impl RouterState {
72 pub fn new(dependencies: Arc<DependencyContainer>, configuration: RouterConfig) -> Self {
74 Self {
75 dependencies,
76 configuration,
77 }
78 }
79}
80
81#[cfg(test)]
82impl RouterState {
83 pub fn new_with_dummy_config(dependencies: Arc<DependencyContainer>) -> Self {
84 Self {
85 dependencies,
86 configuration: RouterConfig::dummy(),
87 }
88 }
89}
90
91pub fn routes(
93 state: Arc<RouterState>,
94) -> impl Filter<Extract = (impl Reply,), Error = Rejection> + Clone {
95 let cors = warp::cors()
96 .allow_any_origin()
97 .allow_headers(vec!["content-type", MITHRIL_API_VERSION_HEADER])
98 .allow_methods(vec![Method::GET, Method::POST, Method::OPTIONS]);
99
100 warp::any()
101 .and(header_must_be(
102 state.dependencies.api_version_provider.clone(),
103 http_server_child_logger(&state.dependencies.root_logger),
104 ))
105 .and(warp::path(SERVER_BASE_PATH))
106 .and(
107 certificate_routes::routes(&state)
108 .or(artifact_routes::snapshot::routes(&state))
109 .or(artifact_routes::cardano_database::routes(&state))
110 .or(artifact_routes::mithril_stake_distribution::routes(&state))
111 .or(artifact_routes::cardano_stake_distribution::routes(&state))
112 .or(artifact_routes::cardano_transaction::routes(&state))
113 .or(proof_routes::routes(&state))
114 .or(signer_routes::routes(&state))
115 .or(signatures_routes::routes(&state))
116 .or(epoch_routes::routes(&state))
117 .or(statistics_routes::routes(&state))
118 .or(root_routes::routes(&state))
119 .or(status::routes(&state)),
120 )
121 .recover(handle_custom)
122 .and(middlewares::with_api_version_provider(&state))
123 .map(|reply, api_version_provider: Arc<APIVersionProvider>| {
124 warp::reply::with_header(
125 reply,
126 MITHRIL_API_VERSION_HEADER,
127 &api_version_provider
128 .compute_current_version()
129 .unwrap()
130 .to_string(),
131 )
132 })
133 .with(cors)
134 .with(middlewares::log_route_call(&state))
135}
136
137fn header_must_be(
139 api_version_provider: Arc<APIVersionProvider>,
140 logger: Logger,
141) -> impl Filter<Extract = (), Error = Rejection> + Clone {
142 warp::header::optional(MITHRIL_API_VERSION_HEADER)
143 .and(warp::any().map(move || api_version_provider.clone()))
144 .and(warp::any().map(move || logger.clone()))
145 .and_then(
146 move |maybe_header: Option<String>,
147 api_version_provider: Arc<APIVersionProvider>,
148 logger: Logger| async move {
149 match maybe_header {
150 None => Ok(()),
151 Some(version) => match semver::Version::parse(&version) {
152 Ok(version)
153 if api_version_provider
154 .compute_current_version_requirement()
155 .unwrap()
156 .matches(&version)
157 .to_owned() =>
158 {
159 Ok(())
160 }
161 Ok(_version) => Err(warp::reject::custom(VersionMismatchError)),
162 Err(err) => {
163 warn!(logger, "api_version_check::parse_error"; "error" => ?err);
164 Err(warp::reject::custom(VersionParseError))
165 }
166 },
167 }
168 },
169 )
170 .untuple_one()
171}
172
173pub async fn handle_custom(reject: Rejection) -> Result<impl Reply, Rejection> {
174 if reject.find::<VersionMismatchError>().is_some() {
175 Ok(StatusCode::PRECONDITION_FAILED)
176 } else if reject.is_not_found() {
177 Ok(StatusCode::NOT_FOUND)
178 } else {
179 Err(reject)
180 }
181}
182
183#[cfg(test)]
184mod tests {
185 use semver::Version;
186 use std::collections::HashMap;
187
188 use mithril_common::{
189 entities::Epoch,
190 era::{EraChecker, SupportedEra},
191 };
192
193 use crate::initialize_dependencies;
194 use crate::test_tools::TestLogger;
195
196 use super::*;
197
198 #[tokio::test]
199 async fn test_no_version() {
200 let era_checker = EraChecker::new(SupportedEra::dummy(), Epoch(1));
201 let api_version_provider = Arc::new(APIVersionProvider::new(Arc::new(era_checker)));
202 let filters = header_must_be(api_version_provider, TestLogger::stdout());
203 warp::test::request()
204 .path("/aggregator/whatever")
205 .filter(&filters)
206 .await
207 .expect("request without a version in headers should not be rejected");
208 }
209
210 #[tokio::test]
211 async fn test_parse_version_error() {
212 let era_checker = EraChecker::new(SupportedEra::dummy(), Epoch(1));
213 let api_version_provider = Arc::new(APIVersionProvider::new(Arc::new(era_checker)));
214 let filters = header_must_be(api_version_provider, TestLogger::stdout());
215 warp::test::request()
216 .header(MITHRIL_API_VERSION_HEADER, "not_a_version")
217 .path("/aggregator/whatever")
218 .filter(&filters)
219 .await
220 .expect_err(
221 r#"request with an unparsable version should be rejected with a version parse error"#,
222 );
223 }
224
225 #[tokio::test]
226 async fn test_bad_version() {
227 let era_checker = EraChecker::new(SupportedEra::dummy(), Epoch(1));
228 let mut version_provider = APIVersionProvider::new(Arc::new(era_checker));
229 let mut open_api_versions = HashMap::new();
230 open_api_versions.insert("openapi.yaml".to_string(), Version::new(1, 0, 0));
231 version_provider.update_open_api_versions(open_api_versions);
232 let api_version_provider = Arc::new(version_provider);
233 let filters = header_must_be(api_version_provider, TestLogger::stdout());
234 warp::test::request()
235 .header(MITHRIL_API_VERSION_HEADER, "0.0.999")
236 .path("/aggregator/whatever")
237 .filter(&filters)
238 .await
239 .expect_err(r#"request with bad version "0.0.999" should be rejected with a version mismatch error"#);
240 }
241
242 #[tokio::test]
243 async fn test_good_version() {
244 let era_checker = EraChecker::new(SupportedEra::dummy(), Epoch(1));
245 let mut version_provider = APIVersionProvider::new(Arc::new(era_checker));
246 let mut open_api_versions = HashMap::new();
247 open_api_versions.insert("openapi.yaml".to_string(), Version::new(0, 1, 0));
248 version_provider.update_open_api_versions(open_api_versions);
249 let api_version_provider = Arc::new(version_provider);
250 let filters = header_must_be(api_version_provider, TestLogger::stdout());
251 warp::test::request()
252 .header(MITHRIL_API_VERSION_HEADER, "0.1.2")
253 .path("/aggregator/whatever")
254 .filter(&filters)
255 .await
256 .expect(r#"request with the good version "0.1.2" should not be rejected"#);
257 }
258
259 #[tokio::test]
260 async fn test_404_response_should_include_status_code_and_headers() {
261 let container = Arc::new(initialize_dependencies!().await);
262 let state = RouterState::new_with_dummy_config(container);
263 let routes = routes(Arc::new(state));
264
265 let response = warp::test::request()
266 .path("/aggregator/a-route-that-does-not-exist")
267 .header("Origin", "http://localhost")
269 .reply(&routes)
270 .await;
271 let response_headers = response.headers();
272
273 assert_eq!(response.status(), StatusCode::NOT_FOUND);
274 assert!(
275 response_headers.get(MITHRIL_API_VERSION_HEADER).is_some(),
276 "API version header should be present, headers: {response_headers:?}",
277 );
278 assert!(
279 response_headers
280 .get("access-control-allow-origin")
281 .is_some(),
282 "CORS headers should be present, headers: {response_headers:?}",
283 );
284 }
285}