Add Network Explorer API (#720)
* Adding the explorer API * Added explorer-api to workspace * Re-jigged explorer api cargo paths * Fixed compiler warnings * Removing unused code * network-explorer-api: configure state with env var API_STATE_FILE or fall back to default value of `explorer-api-state.json` * network-explorer-api: updates to `Cargo.lock` file after rebasing * network-explorer-api: make clippy happy
This commit is contained in:
Generated
+164
-12
@@ -367,6 +367,20 @@ dependencies = [
|
||||
"keystream",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
"serde",
|
||||
"time 0.1.44",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cipher"
|
||||
version = "0.2.5"
|
||||
@@ -385,7 +399,7 @@ dependencies = [
|
||||
"ansi_term",
|
||||
"atty",
|
||||
"bitflags",
|
||||
"strsim",
|
||||
"strsim 0.8.0",
|
||||
"textwrap",
|
||||
"unicode-width",
|
||||
"vec_map",
|
||||
@@ -477,7 +491,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ffdf8865bac3d9a3bde5bde9088ca431b11f5d37c7a578b8086af77248b76627"
|
||||
dependencies = [
|
||||
"percent-encoding",
|
||||
"time",
|
||||
"time 0.2.27",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
@@ -693,6 +707,41 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "757c0ded2af11d8e739c4daea1ac623dd1624b06c844cf3f5a39f1bdbd99bb12"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"darling_macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_core"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c34d8efb62d0c2d7f60ece80f75e5c63c1588ba68032740494b0b9a996466e3"
|
||||
dependencies = [
|
||||
"fnv",
|
||||
"ident_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim 0.10.0",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_macro"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ade7bff147130fe5e6d39f089c6bd49ec0250f35d70b2eebf72afdfc919f15cc"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dashmap"
|
||||
version = "4.0.2"
|
||||
@@ -913,6 +962,26 @@ dependencies = [
|
||||
"termcolor",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "explorer-api"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"isocountry",
|
||||
"log",
|
||||
"mixnet-contract",
|
||||
"okapi",
|
||||
"pretty_env_logger",
|
||||
"reqwest",
|
||||
"rocket",
|
||||
"rocket_okapi",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"validator-client",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fake-simd"
|
||||
version = "0.1.2"
|
||||
@@ -1184,7 +1253,7 @@ dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"wasi 0.10.2+wasi-snapshot-preview1",
|
||||
"wasi 0.10.0+wasi-snapshot-preview1",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
@@ -1426,6 +1495,12 @@ dependencies = [
|
||||
"tokio-native-tls",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ident_case"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "0.2.3"
|
||||
@@ -1487,6 +1562,16 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "isocountry"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ea1dc4bf0fb4904ba83ffdb98af3d9c325274e92e6e295e4151e86c96363e04"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "0.4.7"
|
||||
@@ -1782,6 +1867,16 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-integer"
|
||||
version = "0.1.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.14"
|
||||
@@ -2117,6 +2212,17 @@ dependencies = [
|
||||
"sphinx",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "okapi"
|
||||
version = "0.6.0-alpha-1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb085e00daf8d75b9dbf0ffdb4738e69503e28898d9641fa8bdc6ad536c7bcf4"
|
||||
dependencies = [
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.8.0"
|
||||
@@ -2701,7 +2807,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"state",
|
||||
"tempfile",
|
||||
"time",
|
||||
"time 0.2.27",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tokio-util",
|
||||
@@ -2764,11 +2870,38 @@ dependencies = [
|
||||
"smallvec",
|
||||
"stable-pattern",
|
||||
"state",
|
||||
"time",
|
||||
"time 0.2.27",
|
||||
"tokio",
|
||||
"uncased",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rocket_okapi"
|
||||
version = "0.7.0-alpha-1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b2f4f48fb070f9f6c56d5663df5fa8a514406207744f4abd84661bfb24efd7d"
|
||||
dependencies = [
|
||||
"okapi",
|
||||
"rocket",
|
||||
"rocket_okapi_codegen",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rocket_okapi_codegen"
|
||||
version = "0.7.0-alpha-1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "88ccf1550e1c806461a6b08e2ab64eb10701d41bf50bde59ab9aa3a57ab14d41"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rocket_http",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rocket_sync_db_pools"
|
||||
version = "0.1.0-rc.1"
|
||||
@@ -2852,6 +2985,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc6ab463ae35acccb5cba66c0084c985257b797d288b6050cc2f6ac1b266cb78"
|
||||
dependencies = [
|
||||
"dyn-clone",
|
||||
"indexmap",
|
||||
"schemars_derive",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -2989,9 +3123,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.64"
|
||||
version = "1.0.66"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79"
|
||||
checksum = "336b10da19a12ad094b59d870ebde26a45402e5b470add4b5fd03c5048a32127"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"ryu",
|
||||
@@ -3277,7 +3411,7 @@ dependencies = [
|
||||
"sqlx-rt",
|
||||
"stringprep",
|
||||
"thiserror",
|
||||
"time",
|
||||
"time 0.2.27",
|
||||
"tokio-stream",
|
||||
"url",
|
||||
"webpki",
|
||||
@@ -3423,6 +3557,12 @@ version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "1.0.0"
|
||||
@@ -3516,6 +3656,17 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.1.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"wasi 0.10.0+wasi-snapshot-preview1",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.2.27"
|
||||
@@ -3571,9 +3722,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.8.0"
|
||||
version = "1.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "570c2eb13b3ab38208130eccd41be92520388791207fde783bda7c1e8ace28d4"
|
||||
checksum = "4b7b349f11a7047e6d1276853e612d152f5e8a352c61917887cc2169e2366b4c"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"bytes",
|
||||
@@ -3582,6 +3733,7 @@ dependencies = [
|
||||
"mio",
|
||||
"num_cpus",
|
||||
"once_cell",
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"tokio-macros",
|
||||
@@ -3951,9 +4103,9 @@ checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.10.2+wasi-snapshot-preview1"
|
||||
version = "0.10.0+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
|
||||
checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
|
||||
@@ -39,6 +39,7 @@ members = [
|
||||
"common/socks5/proxy-helpers",
|
||||
"common/topology",
|
||||
"common/wasm-utils",
|
||||
"explorer-api",
|
||||
"gateway",
|
||||
"gateway/gateway-requests",
|
||||
"mixnode",
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
target
|
||||
explorer-api-state.json
|
||||
Generated
+2438
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,23 @@
|
||||
[package]
|
||||
name = "explorer-api"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
isocountry = "0.3.2"
|
||||
reqwest = "0.11.4"
|
||||
rocket = {version = "0.5.0-rc.1", features=["json"] }
|
||||
serde = "1.0.126"
|
||||
serde_json = "1.0.66"
|
||||
tokio = {version = "1.9.0", features = ["full"] }
|
||||
chrono = { version = "0.4.19", features = ["serde"] }
|
||||
schemars = { version = "0.8", features = ["preserve_order"] }
|
||||
okapi = { version = "0.6.0-alpha-1", features = ["derive_json_schema"] }
|
||||
rocket_okapi = "0.7.0-alpha-1"
|
||||
log = "0.4.0"
|
||||
pretty_env_logger = "0.4.0"
|
||||
|
||||
mixnet-contract = { path = "../common/mixnet-contract" }
|
||||
validator-client = { path = "../common/client-libs/validator-client" }
|
||||
@@ -0,0 +1,17 @@
|
||||
Network Explorer API
|
||||
====================
|
||||
|
||||
An API that can:
|
||||
|
||||
* calculate how many nodes are in which country, by checking the IPs of all nodes against an external service
|
||||
* serve "hello world" via HTTP
|
||||
|
||||
|
||||
TODO:
|
||||
|
||||
* record the number of mixnodes on a given date and write to a file for later retrieval
|
||||
* store the nodes per country state in a variable
|
||||
* grab mixnode description info via reqwest and serve it (avoid mixed-content errors)
|
||||
* serve it all over http
|
||||
* dependency injection
|
||||
* tests
|
||||
@@ -0,0 +1,35 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
pub type CountryNodesDistribution = HashMap<String, u32>;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ConcurrentCountryNodesDistribution {
|
||||
inner: Arc<RwLock<CountryNodesDistribution>>,
|
||||
}
|
||||
|
||||
impl ConcurrentCountryNodesDistribution {
|
||||
pub(crate) fn new() -> Self {
|
||||
ConcurrentCountryNodesDistribution {
|
||||
inner: Arc::new(RwLock::new(CountryNodesDistribution::new())),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn attach(country_node_distribution: CountryNodesDistribution) -> Self {
|
||||
ConcurrentCountryNodesDistribution {
|
||||
inner: Arc::new(RwLock::new(country_node_distribution)),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn set_all(&mut self, country_node_distribution: CountryNodesDistribution) {
|
||||
self.inner
|
||||
.write()
|
||||
.await
|
||||
.clone_from(&country_node_distribution)
|
||||
}
|
||||
|
||||
pub(crate) async fn get_all(&self) -> HashMap<String, u32> {
|
||||
self.inner.read().await.clone()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
use crate::country_statistics::country_nodes_distribution::CountryNodesDistribution;
|
||||
use crate::state::ExplorerApiStateContext;
|
||||
use rocket::serde::json::Json;
|
||||
use rocket::{Route, State};
|
||||
|
||||
pub fn make_default_routes() -> Vec<Route> {
|
||||
routes_with_openapi![index]
|
||||
}
|
||||
|
||||
// We could either separate stuff by structure (like this, http is separate), or we could just
|
||||
// stick the http route directly into each sub-application (e.g. put this file into the
|
||||
// "country_statistics" module directly
|
||||
#[openapi(tag = "country_statistics")]
|
||||
#[get("/")]
|
||||
pub(crate) async fn index(
|
||||
state: &State<ExplorerApiStateContext>,
|
||||
) -> Json<CountryNodesDistribution> {
|
||||
Json(state.inner.country_node_distribution.get_all().await)
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
use isocountry::CountryCode;
|
||||
use log::{info, trace, warn};
|
||||
use mixnet_contract::MixNodeBond;
|
||||
use reqwest::Error as ReqwestError;
|
||||
use validator_client::Config;
|
||||
|
||||
use models::GeoLocation;
|
||||
|
||||
use crate::country_statistics::country_nodes_distribution::CountryNodesDistribution;
|
||||
use crate::state::ExplorerApiStateContext;
|
||||
|
||||
pub mod country_nodes_distribution;
|
||||
pub mod http;
|
||||
mod models;
|
||||
|
||||
pub(crate) struct CountryStatistics {
|
||||
state: ExplorerApiStateContext,
|
||||
}
|
||||
|
||||
impl CountryStatistics {
|
||||
pub(crate) fn new(state: ExplorerApiStateContext) -> Self {
|
||||
CountryStatistics { state }
|
||||
}
|
||||
|
||||
pub(crate) fn start(mut self) {
|
||||
info!("Spawning task runner...");
|
||||
tokio::spawn(async move {
|
||||
let mut interval_timer = tokio::time::interval(std::time::Duration::from_secs(60 * 60));
|
||||
loop {
|
||||
// wait for the next interval tick
|
||||
interval_timer.tick().await;
|
||||
|
||||
info!("Running task...");
|
||||
self.calculate_nodes_per_country().await;
|
||||
info!("Done");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Retrieves the current list of mixnodes from the validators and calculates how many nodes are in each country
|
||||
async fn calculate_nodes_per_country(&mut self) {
|
||||
let mixnode_bonds = retrieve_mixnodes().await;
|
||||
|
||||
let mut distribution = CountryNodesDistribution::new();
|
||||
|
||||
info!("Locating mixnodes...");
|
||||
for (i, bond) in mixnode_bonds.iter().enumerate() {
|
||||
match locate(&bond.mix_node.host).await {
|
||||
Ok(location) => {
|
||||
let country_code = map_2_letter_to_3_letter_country_code(&location);
|
||||
*(distribution.entry(country_code)).or_insert(0) += 1;
|
||||
|
||||
trace!(
|
||||
"Ip {} is located in {:#?}",
|
||||
bond.mix_node.host,
|
||||
map_2_letter_to_3_letter_country_code(&location)
|
||||
);
|
||||
|
||||
if (i % 100) == 0 {
|
||||
info!(
|
||||
"Located {} mixnodes in {} countries",
|
||||
i + 1,
|
||||
distribution.len()
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(e) => warn!("❌ Oh no! Location failed {}", e),
|
||||
}
|
||||
}
|
||||
|
||||
// replace the shared distribution to be the new distribution
|
||||
self.state
|
||||
.inner
|
||||
.country_node_distribution
|
||||
.set_all(distribution)
|
||||
.await;
|
||||
|
||||
info!(
|
||||
"Locating mixnodes done: {:?}",
|
||||
self.state.inner.country_node_distribution.get_all().await
|
||||
);
|
||||
|
||||
// keep state on disk, so that when this process dies it can start up again and users get some data
|
||||
self.state.write_to_file().await;
|
||||
}
|
||||
}
|
||||
|
||||
fn map_2_letter_to_3_letter_country_code(geo: &GeoLocation) -> String {
|
||||
match CountryCode::for_alpha2(&geo.country_code) {
|
||||
Ok(three_letter_country_code) => three_letter_country_code.alpha3().to_string(),
|
||||
Err(_e) => {
|
||||
warn!(
|
||||
"❌ Oh no! map_2_letter_to_3_letter_country_code failed for '{:#?}'",
|
||||
geo
|
||||
);
|
||||
"???".to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn locate(ip: &str) -> Result<GeoLocation, ReqwestError> {
|
||||
let response = reqwest::get(format!("{}{}", crate::GEO_IP_SERVICE, ip)).await?;
|
||||
let location = response.json::<GeoLocation>().await?;
|
||||
Ok(location)
|
||||
}
|
||||
|
||||
async fn retrieve_mixnodes() -> Vec<MixNodeBond> {
|
||||
let client = new_validator_client();
|
||||
|
||||
info!("About to retrieve mixnode bonds...");
|
||||
|
||||
let bonds: Vec<MixNodeBond> = match client.get_cached_mix_nodes().await {
|
||||
Ok(result) => result,
|
||||
Err(e) => panic!("Unable to retrieve mixnode bonds: {:?}", e),
|
||||
};
|
||||
info!("Fetched {} mixnode bonds", bonds.len());
|
||||
bonds
|
||||
}
|
||||
|
||||
// TODO: inject constants
|
||||
fn new_validator_client() -> validator_client::Client {
|
||||
let config = Config::new(vec![crate::VALIDATOR_API.to_string()], crate::CONTRACT);
|
||||
validator_client::Client::new(config)
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) struct GeoLocation {
|
||||
pub(crate) ip: String,
|
||||
pub(crate) country_code: String,
|
||||
pub(crate) country_name: String,
|
||||
pub(crate) region_code: String,
|
||||
pub(crate) region_name: String,
|
||||
pub(crate) city: String,
|
||||
pub(crate) zip_code: String,
|
||||
pub(crate) time_zone: String,
|
||||
pub(crate) latitude: f32,
|
||||
pub(crate) longitude: f32,
|
||||
pub(crate) metro_code: u32,
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
mod swagger;
|
||||
|
||||
use crate::country_statistics::http::make_default_routes;
|
||||
use crate::http::swagger::get_docs;
|
||||
use crate::ping::http::ping_make_default_routes;
|
||||
use crate::state::ExplorerApiStateContext;
|
||||
use log::info;
|
||||
use rocket_okapi::swagger_ui::make_swagger_ui;
|
||||
|
||||
pub(crate) fn start(state: ExplorerApiStateContext) {
|
||||
tokio::spawn(async move {
|
||||
info!("Starting up...");
|
||||
|
||||
let config = rocket::config::Config::release_default();
|
||||
|
||||
rocket::build()
|
||||
.configure(config)
|
||||
.mount("/countries", make_default_routes())
|
||||
.mount("/ping", ping_make_default_routes())
|
||||
.mount("/swagger", make_swagger_ui(&get_docs()))
|
||||
// .register("/", catchers![not_found])
|
||||
.manage(state)
|
||||
// .manage(descriptor)
|
||||
// .manage(node_stats_pointer)
|
||||
.launch()
|
||||
.await
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
use rocket_okapi::swagger_ui::{SwaggerUIConfig, UrlObject};
|
||||
|
||||
pub(crate) fn get_docs() -> SwaggerUIConfig {
|
||||
SwaggerUIConfig {
|
||||
urls: vec![
|
||||
UrlObject::new("Country statistics", "/countries/openapi.json"),
|
||||
UrlObject::new("Node ping", "/ping/openapi.json"),
|
||||
],
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
#[macro_use]
|
||||
extern crate rocket;
|
||||
#[macro_use]
|
||||
extern crate rocket_okapi;
|
||||
|
||||
use log::info;
|
||||
|
||||
mod country_statistics;
|
||||
mod http;
|
||||
mod ping;
|
||||
mod state;
|
||||
|
||||
const VALIDATOR_API: &str = "http://testnet-milhon-validator1.nymtech.net:8080";
|
||||
const CONTRACT: &str = "punk10pyejy66429refv3g35g2t7am0was7yalwrzen";
|
||||
const GEO_IP_SERVICE: &str = "https://freegeoip.app/json/";
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
setup_logging();
|
||||
let mut explorer_api = ExplorerApi::new();
|
||||
explorer_api.run().await;
|
||||
}
|
||||
|
||||
pub struct ExplorerApi {
|
||||
state: state::ExplorerApiStateContext,
|
||||
}
|
||||
|
||||
impl ExplorerApi {
|
||||
fn new() -> ExplorerApi {
|
||||
ExplorerApi {
|
||||
state: state::ExplorerApiStateContext::new(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn run(&mut self) {
|
||||
info!("Explorer API starting up...");
|
||||
|
||||
// spawn concurrent tasks
|
||||
country_statistics::CountryStatistics::new(self.state.clone()).start();
|
||||
http::start(self.state.clone());
|
||||
|
||||
// wait for user to press ctrl+C
|
||||
self.wait_for_interrupt().await
|
||||
}
|
||||
|
||||
async fn wait_for_interrupt(&self) {
|
||||
if let Err(e) = tokio::signal::ctrl_c().await {
|
||||
error!(
|
||||
"There was an error while capturing SIGINT - {:?}. We will terminate regardless",
|
||||
e
|
||||
);
|
||||
}
|
||||
info!(
|
||||
"Received SIGINT - the mixnode will terminate now (threads are not yet nicely stopped, if you see stack traces that's alright)."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn setup_logging() {
|
||||
let mut log_builder = pretty_env_logger::formatted_timed_builder();
|
||||
if let Ok(s) = ::std::env::var("RUST_LOG") {
|
||||
log_builder.parse_filters(&s);
|
||||
} else {
|
||||
// default to 'Info'
|
||||
log_builder.filter(None, log::LevelFilter::Info);
|
||||
}
|
||||
|
||||
log_builder
|
||||
.filter_module("tokio_reactor", log::LevelFilter::Warn)
|
||||
.filter_module("reqwest", log::LevelFilter::Warn)
|
||||
.init();
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
use rocket::serde::json::Json;
|
||||
use rocket::Route;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
pub fn ping_make_default_routes() -> Vec<Route> {
|
||||
routes_with_openapi![index]
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, JsonSchema)]
|
||||
pub(crate) struct PingResponse {
|
||||
response_time: u32,
|
||||
}
|
||||
|
||||
#[openapi(tag = "ping")]
|
||||
#[get("/")]
|
||||
pub(crate) async fn index() -> Json<PingResponse> {
|
||||
Json(PingResponse { response_time: 42 })
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod http;
|
||||
@@ -0,0 +1,82 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs::File;
|
||||
use std::path::Path;
|
||||
|
||||
use log::info;
|
||||
|
||||
use crate::country_statistics::country_nodes_distribution::{
|
||||
ConcurrentCountryNodesDistribution, CountryNodesDistribution,
|
||||
};
|
||||
|
||||
// TODO: change to an environment variable with a default value
|
||||
const STATE_FILE: &str = "explorer-api-state.json";
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ExplorerApiState {
|
||||
pub(crate) country_node_distribution: ConcurrentCountryNodesDistribution,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ExplorerApiStateOnDisk {
|
||||
pub(crate) country_node_distribution: CountryNodesDistribution,
|
||||
pub(crate) as_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct ExplorerApiStateContext {
|
||||
pub(crate) inner: ExplorerApiState,
|
||||
state_file: String,
|
||||
}
|
||||
|
||||
impl ExplorerApiStateContext {
|
||||
pub(crate) fn new() -> Self {
|
||||
ExplorerApiStateContext {
|
||||
inner: ExplorerApiStateContext::read_from_file(),
|
||||
state_file: std::env::var("API_STATE_FILE").unwrap_or_else(|_| STATE_FILE.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn read_from_file() -> ExplorerApiState {
|
||||
let json_file = get_state_file_path();
|
||||
let json_file_path = Path::new(&json_file);
|
||||
info!("Loading state from file {:?}...", json_file);
|
||||
match File::open(json_file_path) {
|
||||
Ok(file) => {
|
||||
let state: ExplorerApiStateOnDisk =
|
||||
serde_json::from_reader(file).expect("error while reading json");
|
||||
info!("Loaded state from file {:?}: {:?}", json_file, state);
|
||||
ExplorerApiState {
|
||||
country_node_distribution: ConcurrentCountryNodesDistribution::attach(
|
||||
state.country_node_distribution,
|
||||
),
|
||||
}
|
||||
}
|
||||
Err(_e) => {
|
||||
warn!(
|
||||
"Failed to load state from file {:?}, starting with empty state!",
|
||||
json_file
|
||||
);
|
||||
ExplorerApiState {
|
||||
country_node_distribution: ConcurrentCountryNodesDistribution::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn write_to_file(&self) {
|
||||
let json_file = get_state_file_path().to_string();
|
||||
let json_file_path = Path::new(&json_file);
|
||||
let file = File::create(json_file_path).expect("unable to create state json file");
|
||||
let state = ExplorerApiStateOnDisk {
|
||||
country_node_distribution: self.inner.country_node_distribution.get_all().await,
|
||||
as_at: Utc::now(),
|
||||
};
|
||||
serde_json::to_writer(file, &state).expect("error writing state to disk");
|
||||
info!("Saved file to '{:?}'", json_file_path.canonicalize());
|
||||
}
|
||||
}
|
||||
|
||||
fn get_state_file_path() -> String {
|
||||
std::env::var("API_STATE_FILE").unwrap_or_else(|_| STATE_FILE.to_string())
|
||||
}
|
||||
Reference in New Issue
Block a user