Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Explorer API - add port check and node description/stats proxy #731

Merged
merged 14 commits into from
Aug 10, 2021
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions explorer-api/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ isocountry = "0.3.2"
reqwest = "0.11.4"
rocket = {version = "0.5.0-rc.1", features=["json"] }
serde = "1.0.126"
humantime-serde = "1.0"
serde_json = "1.0.66"
tokio = {version = "1.9.0", features = ["full"] }
chrono = { version = "0.4.19", features = ["serde"] }
Expand Down
2 changes: 1 addition & 1 deletion explorer-api/src/country_statistics/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use crate::state::ExplorerApiStateContext;
use rocket::serde::json::Json;
use rocket::{Route, State};

pub fn make_default_routes() -> Vec<Route> {
pub fn country_statistics_make_default_routes() -> Vec<Route> {
routes_with_openapi![index]
}

Expand Down
31 changes: 9 additions & 22 deletions explorer-api/src/country_statistics/mod.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
use isocountry::CountryCode;
use log::{info, trace, warn};
use mixnet_contract::MixNodeBond;
use reqwest::Error as ReqwestError;
use validator_client::Config;

use models::GeoLocation;

Expand Down Expand Up @@ -39,7 +37,15 @@ impl CountryStatistics {

/// 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;
// force the mixnode cache to invalidate
let mixnode_bonds = self
.state
.inner
.mix_nodes
.clone()
.refresh_and_get()
.await
.value;

let mut distribution = CountryNodesDistribution::new();

Expand Down Expand Up @@ -103,22 +109,3 @@ async fn locate(ip: &str) -> Result<GeoLocation, ReqwestError> {
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)
}
26 changes: 26 additions & 0 deletions explorer-api/src/http/cors.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
use rocket::fairing::{Fairing, Info, Kind};
use rocket::http::Header;
use rocket::{Request, Response};

// See https://github.com/SergioBenitez/Rocket/issues/25#issuecomment-838566038.
// Tried to use `rocket_cors` however it requires lots of nightly builds,
// so went for this instead to keep things simple

pub(crate) struct Cors;

#[rocket::async_trait]
impl Fairing for Cors {
fn info(&self) -> Info {
Info {
name: "Add CORS headers to responses",
kind: Kind::Response,
}
}

async fn on_response<'r>(&self, _request: &'r Request<'_>, response: &mut Response<'r>) {
response.set_header(Header::new("Access-Control-Allow-Origin", "*"));
response.set_header(Header::new("Access-Control-Allow-Methods", "GET, OPTIONS"));
response.set_header(Header::new("Access-Control-Allow-Headers", "*"));
response.set_header(Header::new("Access-Control-Allow-Credentials", "true"));
}
}
27 changes: 18 additions & 9 deletions explorer-api/src/http/mod.rs
Original file line number Diff line number Diff line change
@@ -1,28 +1,37 @@
mod swagger;
use log::info;
use rocket_okapi::swagger_ui::make_swagger_ui;

use crate::country_statistics::http::make_default_routes;
use crate::country_statistics::http::country_statistics_make_default_routes;
use crate::http::cors::Cors;
use crate::http::swagger::get_docs;
use crate::mix_node::http::mix_node_make_default_routes;
use crate::ping::http::ping_make_default_routes;
use crate::state::ExplorerApiStateContext;
use log::info;
use rocket_okapi::swagger_ui::make_swagger_ui;
use rocket::Request;

mod cors;
mod swagger;

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("/countries", country_statistics_make_default_routes())
.mount("/ping", ping_make_default_routes())
.mount("/mix-node", mix_node_make_default_routes())
.mount("/swagger", make_swagger_ui(&get_docs()))
// .register("/", catchers![not_found])
.register("/", catchers![not_found])
.manage(state)
// .manage(descriptor)
// .manage(node_stats_pointer)
.attach(Cors)
.launch()
.await
});
}

#[catch(404)]
pub(crate) fn not_found(req: &Request) -> String {
format!("I couldn't find '{}'. Try something else?", req.uri())
}
1 change: 1 addition & 0 deletions explorer-api/src/http/swagger.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ pub(crate) fn get_docs() -> SwaggerUIConfig {
urls: vec![
UrlObject::new("Country statistics", "/countries/openapi.json"),
UrlObject::new("Node ping", "/ping/openapi.json"),
UrlObject::new("Mix node", "/mix-node/openapi.json"),
],
..Default::default()
}
Expand Down
2 changes: 2 additions & 0 deletions explorer-api/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ use log::info;

mod country_statistics;
mod http;
mod mix_node;
mod mix_nodes;
mod ping;
mod state;

Expand Down
41 changes: 41 additions & 0 deletions explorer-api/src/mix_node/cache.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
use std::collections::HashMap;
use std::time::{Duration, SystemTime};

#[derive(Clone)]
pub(crate) struct Cache<T: Clone> {
inner: HashMap<String, CacheItem<T>>,
}

impl<T: Clone> Cache<T> {
pub(crate) fn new() -> Self {
Cache {
inner: HashMap::new(),
}
}

pub(crate) fn get(&self, identity_key: &str) -> Option<T>
where
T: Clone,
{
self.inner
.get(identity_key)
.filter(|cache_item| cache_item.valid_until > SystemTime::now())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a suggestion, perhaps there should be some way of removing stale entries from the cache? Right now everything will just stay there forever

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it is a bit wasteful generally - although I'm hoping the memory requirements for thousands of nodes will still be megabytes. I can add a cleanup background task sometime if it becomes a problem.

.map(|cache_item| cache_item.value.clone())
}

pub(crate) fn set(&mut self, identity_key: &str, value: T) {
self.inner.insert(
identity_key.to_string(),
CacheItem {
valid_until: SystemTime::now() + Duration::from_secs(60 * 30),
value,
},
);
}
}

#[derive(Clone)]
pub(crate) struct CacheItem<T> {
pub(crate) value: T,
pub(crate) valid_until: std::time::SystemTime,
}
118 changes: 118 additions & 0 deletions explorer-api/src/mix_node/http.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
use reqwest::Error as ReqwestError;

use rocket::serde::json::Json;
use rocket::{Route, State};

use crate::mix_node::models::{NodeDescription, NodeStats};
use crate::state::ExplorerApiStateContext;

pub fn mix_node_make_default_routes() -> Vec<Route> {
routes_with_openapi![get_description, get_stats]
}

#[openapi(tag = "mix_node")]
#[get("/<pubkey>/description")]
pub(crate) async fn get_description(
pubkey: &str,
state: &State<ExplorerApiStateContext>,
) -> Option<Json<NodeDescription>> {
match state
.inner
.mix_node_cache
.clone()
.get_description(pubkey)
.await
{
Some(cache_value) => {
trace!("Returning cached value for {}", pubkey);
Some(Json(cache_value))
}
None => {
trace!("No valid cache value for {}", pubkey);
match state.inner.get_mix_node(pubkey).await {
jstuczyn marked this conversation as resolved.
Show resolved Hide resolved
Some(bond) => {
match get_mix_node_description(
&bond.mix_node.host,
&bond.mix_node.http_api_port,
)
.await
{
Ok(response) => {
// cache the response and return as the HTTP response
state
.inner
.mix_node_cache
.set_description(pubkey, response.clone())
.await;
Some(Json(response))
}
Err(e) => {
error!(
"Unable to get description for {} on {}:{} -> {}",
pubkey, bond.mix_node.host, bond.mix_node.http_api_port, e
);
Option::None
}
}
}
None => Option::None,
}
}
}
}

#[openapi(tag = "mix_node")]
#[get("/<pubkey>/stats")]
pub(crate) async fn get_stats(
pubkey: &str,
state: &State<ExplorerApiStateContext>,
) -> Option<Json<NodeStats>> {
match state.inner.mix_node_cache.get_node_stats(pubkey).await {
Some(cache_value) => {
trace!("Returning cached value for {}", pubkey);
Some(Json(cache_value))
}
None => {
trace!("No valid cache value for {}", pubkey);
match state.inner.get_mix_node(pubkey).await {
Some(bond) => {
match get_mix_node_stats(&bond.mix_node.host, &bond.mix_node.http_api_port)
.await
{
Ok(response) => {
// cache the response and return as the HTTP response
state
.inner
.mix_node_cache
.set_node_stats(pubkey, response.clone())
.await;
Some(Json(response))
}
Err(e) => {
error!(
"Unable to get description for {} on {}:{} -> {}",
pubkey, bond.mix_node.host, bond.mix_node.http_api_port, e
);
Option::None
}
}
}
None => Option::None,
}
}
}
}

async fn get_mix_node_description(host: &str, port: &u16) -> Result<NodeDescription, ReqwestError> {
mmsinclair marked this conversation as resolved.
Show resolved Hide resolved
reqwest::get(format!("http://{}:{}/description", host, port))
.await?
.json::<NodeDescription>()
.await
}

async fn get_mix_node_stats(host: &str, port: &u16) -> Result<NodeStats, ReqwestError> {
reqwest::get(format!("http://{}:{}/stats", host, port))
.await?
.json::<NodeStats>()
.await
}
3 changes: 3 additions & 0 deletions explorer-api/src/mix_node/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
mod cache;
pub(crate) mod http;
pub(crate) mod models;
Loading