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

Handle error response when fetching discovery endpoint #1452

Merged
merged 7 commits into from
Nov 5, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions nym-vpn-core/Cargo.lock

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

5 changes: 5 additions & 0 deletions nym-vpn-core/crates/nym-vpn-network-config/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ license.workspace = true

[dependencies]
anyhow.workspace = true
itertools.workspace = true
reqwest = { workspace = true, default-features = false, features = [
"blocking",
"rustls-tls",
Expand All @@ -18,7 +19,11 @@ reqwest = { workspace = true, default-features = false, features = [
nym-config.workspace = true
serde.workspace = true
serde_json.workspace = true
tempfile.workspace = true
tokio = { workspace = true, features = ["time", "macros"] }
tokio-util.workspace = true
tracing.workspace = true
url = { workspace = true, features = ["serde"] }

[build-dependencies]
serde_json.workspace = true
87 changes: 87 additions & 0 deletions nym-vpn-core/crates/nym-vpn-network-config/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only

use std::path::Path;

const DEFAULT_DIR: &str = "default";
const MAINNET_DISCOVERY_JSON: &str = "mainnet_discovery.json";
const DEFAULT_ENVS_JSON: &str = "envs.json";

fn default_envs() {
let json_path = Path::new(DEFAULT_DIR).join(DEFAULT_ENVS_JSON);

let json_str = std::fs::read_to_string(json_path).expect("Failed to read JSON file");
let networks: Vec<String> = serde_json::from_str(&json_str).expect("Failed to parse JSON file");

let networks_literal = networks
.iter()
.map(|s| format!("\"{}\"", s))
.collect::<Vec<String>>()
.join(", ");

let generated_code = format!(
r#"
impl Default for RegisteredNetworks {{
fn default() -> Self {{
RegisteredNetworks {{
inner: [ {networks_literal} ]
.iter()
.cloned()
.map(String::from)
.collect::<std::collections::HashSet<_>>(),
}}
}}
}}
"#,
networks_literal = networks_literal,
);

let out_dir = std::env::var("OUT_DIR").unwrap();
let dest_path = Path::new(&out_dir).join("default_envs.rs");
std::fs::write(&dest_path, generated_code).expect("Failed to write generated code");
}

fn default_mainnet_discovery() {
let json_path = Path::new(DEFAULT_DIR).join(MAINNET_DISCOVERY_JSON);
println!("cargo:rerun-if-changed={}", json_path.display());

let json_str = std::fs::read_to_string(json_path).expect("Failed to read JSON file");
let json_value: serde_json::Value =
serde_json::from_str(&json_str).expect("Failed to parse JSON file");

let network_name = json_value["network_name"]
.as_str()
.expect("Failed to parse network name");
let nym_api_url = json_value["nym_api_url"]
.as_str()
.expect("Failed to parse nym_api_url");
let nym_vpn_api_url = json_value["nym_vpn_api_url"]
.as_str()
.expect("Failed to parse nym_vpn_api_url");

let generated_code = format!(
r#"
impl Default for Discovery {{
fn default() -> Self {{
Self {{
network_name: "{}".to_string(),
nym_api_url: "{}".parse().expect("Failed to parse NYM API URL"),
nym_vpn_api_url: "{}".parse().expect("Failed to parse NYM VPN API URL"),
}}
}}
}}
"#,
network_name, nym_api_url, nym_vpn_api_url
);

let out_dir = std::env::var("OUT_DIR").unwrap();
let dest_path = Path::new(&out_dir).join("default_discovery.rs");
std::fs::write(&dest_path, generated_code).expect("Failed to write generated code");
}

fn main() {
default_envs();
default_mainnet_discovery();

println!("cargo:rerun-if-changed={DEFAULT_DIR}");
}
6 changes: 6 additions & 0 deletions nym-vpn-core/crates/nym-vpn-network-config/default/envs.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[
"mainnet",
"sandbox",
"canary",
"qa"
]
100 changes: 72 additions & 28 deletions nym-vpn-core/crates/nym-vpn-network-config/src/bootstrap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,16 @@ use super::{nym_network::NymNetwork, MAX_FILE_AGE, NETWORKS_SUBDIR};
const DISCOVERY_FILE: &str = "discovery.json";
const DISCOVERY_WELLKNOWN: &str = "https://nymvpn.com/api/public/v1/.wellknown";

#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
pub struct Discovery {
pub(super) network_name: String,
pub(super) nym_api_url: Url,
pub(super) nym_vpn_api_url: Url,
}

// Include the generated Default implementation
include!(concat!(env!("OUT_DIR"), "/default_discovery.rs"));

impl Discovery {
fn path(config_dir: &Path, network_name: &str) -> PathBuf {
config_dir
Expand Down Expand Up @@ -48,11 +51,23 @@ impl Discovery {
pub fn fetch(network_name: &str) -> anyhow::Result<Self> {
let discovery: DiscoveryResponse = {
let url = Self::endpoint(network_name)?;

tracing::info!("Fetching nym network discovery from: {}", url);
reqwest::blocking::get(url.clone())
let response = reqwest::blocking::get(url.clone())
.inspect_err(|err| tracing::warn!("{}", err))
.with_context(|| format!("Failed to fetch discovery from {}", url))?
.json()
.with_context(|| "Failed to parse discovery")
.error_for_status()
.inspect_err(|err| tracing::warn!("{}", err))
.with_context(|| "Discovery endpoint returned error response".to_owned())?;

let text_response = response
.text()
.inspect_err(|err| tracing::warn!("{}", err))
.with_context(|| "Failed to read response text")?;
tracing::debug!("Discovery response: {:#?}", text_response);

serde_json::from_str(&text_response)
.with_context(|| "Failed to parse discovery response")
}?;
if discovery.network_name != network_name {
anyhow::bail!("Network name mismatch between requested and fetched discovery")
Expand Down Expand Up @@ -92,13 +107,32 @@ impl Discovery {
Ok(())
}

fn try_update_file(config_dir: &Path, network_name: &str) -> anyhow::Result<()> {
if Self::path_is_stale(config_dir, network_name)? {
Self::fetch(network_name)?.write_to_file(config_dir)?;
}
Ok(())
}

pub(super) fn ensure_exists(config_dir: &Path, network_name: &str) -> anyhow::Result<Self> {
if !Self::path(config_dir, network_name).exists() && network_name == "mainnet" {
tracing::info!("No discovery file found, writing default discovery file");
Self::default()
.write_to_file(config_dir)
.inspect_err(|err| tracing::warn!("Failed to write default discovery file: {err}"))
.ok();
}

// Download the file if it doesn't exists, or if the file is too old, refresh it.
// TODO: in the future, we should only refresh the discovery file when the tunnel is up.
// Probably in a background task.
if Self::path_is_stale(config_dir, network_name)? {
Self::fetch(network_name)?.write_to_file(config_dir)?;
}

Self::try_update_file(config_dir, network_name)
.inspect_err(|err| {
tracing::warn!("Failed to refresh discovery file: {err}");
tracing::warn!("Attempting to use existing discovery file");
})
.ok();

Self::read_from_file(config_dir, network_name)
}
Expand All @@ -119,27 +153,6 @@ impl Discovery {
}
}

impl Default for Discovery {
fn default() -> Self {
let default_network_details = NymNetworkDetails::default();
Self {
network_name: default_network_details.network_name,
nym_api_url: default_network_details
.endpoints
.first()
.and_then(|e| e.api_url().clone())
.expect("default network details not setup correctly"),
nym_vpn_api_url: default_network_details
.nym_vpn_api_url
.map(|url| {
url.parse()
.expect("default network details not setup correctly")
})
.expect("default network details not setup correctly"),
}
}
}

// The response type we fetch from the discovery endpoint
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
struct DiscoveryResponse {
Expand All @@ -166,3 +179,34 @@ impl TryFrom<DiscoveryResponse> for Discovery {
})
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_discovery_endpoint() {
let network_name = "mainnet";
let url = Discovery::endpoint(network_name).unwrap();
assert_eq!(
url,
"https://nymvpn.com/api/public/v1/.wellknown/mainnet/discovery.json"
.parse()
.unwrap()
);
}

#[test]
fn test_discovery_fetch() {
let network_name = "mainnet";
let discovery = Discovery::fetch(network_name).unwrap();
assert_eq!(discovery.network_name, network_name);
}

#[test]
fn test_discovery_default_same_as_fetched() {
let default_discovery = Discovery::default();
let fetched_discovery = Discovery::fetch(&default_discovery.network_name).unwrap();
assert_eq!(default_discovery, fetched_discovery);
}
}
Loading
Loading