From 95080c3ecc18b6adf6b8ddc6842952f1bfe64aff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Tue, 24 Jan 2023 11:04:30 +0100 Subject: [PATCH] nym-connect: add connection health test (#2883) * nym-connect: add connect health test * nym-connect: redo connection test * nym-connect: strongly typed response * Fix clippy * nym-connect: also send event on connection check success * nym-connect: tidy --- nym-connect/src-tauri/src/error.rs | 34 ++-- nym-connect/src-tauri/src/main.rs | 1 + .../src/operations/connection/connect.rs | 5 +- .../src/operations/connection/disconnect.rs | 1 + .../src/operations/connection/status.rs | 27 ++++ .../src/operations/growth/api_client.rs | 1 + nym-connect/src-tauri/src/operations/http.rs | 37 +++++ nym-connect/src-tauri/src/operations/mod.rs | 1 + nym-connect/src-tauri/src/state.rs | 12 +- nym-connect/src-tauri/src/tasks.rs | 146 +++++++++++++----- 10 files changed, 207 insertions(+), 58 deletions(-) create mode 100644 nym-connect/src-tauri/src/operations/http.rs diff --git a/nym-connect/src-tauri/src/error.rs b/nym-connect/src-tauri/src/error.rs index 53b8c9df44..50a672f8ac 100644 --- a/nym-connect/src-tauri/src/error.rs +++ b/nym-connect/src-tauri/src/error.rs @@ -15,12 +15,12 @@ pub enum BackendError { #[from] source: std::io::Error, }, - #[error("String formatting error: {source}")] + #[error("string formatting error: {source}")] FmtError { #[from] source: std::fmt::Error, }, - #[error("Tauri error: {source}")] + #[error("tauri error: {source}")] TauriError { #[from] source: tauri::Error, @@ -46,30 +46,36 @@ pub enum BackendError { source: crate::operations::growth::api_client::ApiClientError, }, - #[error("Could not send disconnect signal to the SOCKS5 client")] + #[error("could not send disconnect signal to the SOCKS5 client")] CoundNotSendDisconnectSignal, - #[error("No service provider set")] + #[error("no service provider set")] NoServiceProviderSet, - #[error("No gateway provider set")] + #[error("no gateway provider set")] NoGatewaySet, - #[error("Initialization failed with a panic")] + #[error("initialization failed with a panic")] InitializationPanic, - #[error("Could not get config id before gateway is set")] + #[error("could not get config id before gateway is set")] CouldNotGetIdWithoutGateway, - #[error("Could initialize without gateway set")] + #[error("could initialize without gateway set")] CouldNotInitWithoutGateway, - #[error("Could initialize without service provider set")] + #[error("could initialize without service provider set")] CouldNotInitWithoutServiceProvider, - #[error("Could not get file name")] + #[error("could not get file name")] CouldNotGetFilename, - #[error("Could not get config file location")] + #[error("could not get config file location")] CouldNotGetConfigFilename, - #[error("Could not load existing gateway configuration")] + #[error("could not load existing gateway configuration")] CouldNotLoadExistingGatewayConfiguration(std::io::Error), - #[error("Unable to open a new window")] + #[error("unable to open a new window")] NewWindowError, - #[error("Unable to parse the specified gateway")] + #[error("unable to parse the specified gateway")] UnableToParseGateway, + + #[error("HTTP get request failed: {status_code}")] + RequestFail { + url: reqwest::Url, + status_code: reqwest::StatusCode, + }, } impl Serialize for BackendError { diff --git a/nym-connect/src-tauri/src/main.rs b/nym-connect/src-tauri/src/main.rs index 608eca4130..9eccb6151b 100644 --- a/nym-connect/src-tauri/src/main.rs +++ b/nym-connect/src-tauri/src/main.rs @@ -41,6 +41,7 @@ fn main() { crate::config::get_config_file_location, crate::config::get_config_id, crate::operations::connection::status::get_connection_status, + crate::operations::connection::status::run_health_check, crate::operations::connection::connect::get_gateway, crate::operations::connection::connect::get_service_provider, crate::operations::connection::connect::set_gateway, diff --git a/nym-connect/src-tauri/src/operations/connection/connect.rs b/nym-connect/src-tauri/src/operations/connection/connect.rs index 6170fec0b4..1d83748698 100644 --- a/nym-connect/src-tauri/src/operations/connection/connect.rs +++ b/nym-connect/src-tauri/src/operations/connection/connect.rs @@ -11,6 +11,7 @@ pub async fn start_connecting( state: tauri::State<'_, Arc>>, window: tauri::Window, ) -> Result { + log::trace!("Start connecting"); let (msg_receiver, exit_status_receiver) = { let mut state_w = state.write().await; state_w.start_connecting(&window).await? @@ -18,8 +19,8 @@ pub async fn start_connecting( // Setup task for checking status let state = state.inner().clone(); - tasks::start_disconnect_listener(state, window.clone(), exit_status_receiver); - tasks::start_status_listener(window, msg_receiver); + tasks::start_disconnect_listener(state.clone(), window.clone(), exit_status_receiver); + tasks::start_status_listener(state, window.clone(), msg_receiver); Ok(ConnectResult { address: "PLACEHOLDER".to_string(), diff --git a/nym-connect/src-tauri/src/operations/connection/disconnect.rs b/nym-connect/src-tauri/src/operations/connection/disconnect.rs index 2361213e40..b75bf49410 100644 --- a/nym-connect/src-tauri/src/operations/connection/disconnect.rs +++ b/nym-connect/src-tauri/src/operations/connection/disconnect.rs @@ -9,6 +9,7 @@ pub async fn start_disconnecting( state: tauri::State<'_, Arc>>, window: tauri::Window, ) -> Result { + log::trace!("Start disconnecting"); let mut guard = state.write().await; guard.start_disconnecting(&window).await?; diff --git a/nym-connect/src-tauri/src/operations/connection/status.rs b/nym-connect/src-tauri/src/operations/connection/status.rs index ccf02400f3..a5f613a386 100644 --- a/nym-connect/src-tauri/src/operations/connection/status.rs +++ b/nym-connect/src-tauri/src/operations/connection/status.rs @@ -1,11 +1,14 @@ use crate::error::Result; use std::sync::Arc; +use serde::{Deserialize, Serialize}; use tokio::sync::RwLock; use crate::models::ConnectionStatusKind; use crate::state::State; +static HEALTH_CHECK_URL: &str = "https://nymtech.net/.wellknown/connect/healthcheck.json"; + #[tauri::command] pub async fn get_connection_status( state: tauri::State<'_, Arc>>, @@ -13,3 +16,27 @@ pub async fn get_connection_status( let state = state.read().await; Ok(state.get_status()) } + +#[derive(Serialize, Deserialize, Debug)] +struct ConnectionSuccess { + status: String, +} + +#[tauri::command] +pub async fn run_health_check() -> bool { + log::info!("Running network health check"); + match crate::operations::http::socks5_get::<_, ConnectionSuccess>(HEALTH_CHECK_URL).await { + Ok(res) if res.status == "ok" => { + log::info!("Healthcheck success!"); + true + } + Ok(res) => { + log::error!("Healthcheck failed with status: {}", res.status); + false + } + Err(err) => { + log::error!("Healthcheck failed: {err}"); + false + } + } +} diff --git a/nym-connect/src-tauri/src/operations/growth/api_client.rs b/nym-connect/src-tauri/src/operations/growth/api_client.rs index eb13749ad5..c069cb6a9d 100644 --- a/nym-connect/src-tauri/src/operations/growth/api_client.rs +++ b/nym-connect/src-tauri/src/operations/growth/api_client.rs @@ -83,6 +83,7 @@ impl GrowthApiClient { } } + // TODO: use the method in `operations::http` instead pub(crate) async fn post( &self, url: &str, diff --git a/nym-connect/src-tauri/src/operations/http.rs b/nym-connect/src-tauri/src/operations/http.rs new file mode 100644 index 0000000000..5cfe3e1f53 --- /dev/null +++ b/nym-connect/src-tauri/src/operations/http.rs @@ -0,0 +1,37 @@ +use crate::error::{BackendError, Result}; +use serde::de::DeserializeOwned; +use tap::TapFallible; + +pub async fn socks5_get(url: U) -> Result +where + U: reqwest::IntoUrl + std::fmt::Display, + T: DeserializeOwned, +{ + log::info!(">>> GET {url}"); + let proxy = reqwest::Proxy::all("socks5h://127.0.0.1:1080")?; + let client = reqwest::Client::builder() + .proxy(proxy) + .timeout(std::time::Duration::from_secs(20)) + .build()?; + + let resp = client.get(url).send().await.tap_err(|err| { + log::error!("<<< Request send error: {err}"); + })?; + + if resp.status().is_client_error() || resp.status().is_server_error() { + log::error!("<<< {}", resp.status()); + return Err(BackendError::RequestFail { + url: resp.url().clone(), + status_code: resp.status(), + }); + } + + let response_body = resp.text().await.tap_err(|err| { + log::error!("<<< Request error: {err}"); + })?; + log::info!("<<< {response_body}"); + + Ok(serde_json::from_str(&response_body).tap_err(|err| { + log::error!("<<< JSON parsing error: {err}"); + })?) +} diff --git a/nym-connect/src-tauri/src/operations/mod.rs b/nym-connect/src-tauri/src/operations/mod.rs index 1d0fa5b211..9d7d45f236 100644 --- a/nym-connect/src-tauri/src/operations/mod.rs +++ b/nym-connect/src-tauri/src/operations/mod.rs @@ -3,4 +3,5 @@ pub mod directory; pub mod export; pub mod growth; pub mod help; +pub mod http; pub mod window; diff --git a/nym-connect/src-tauri/src/state.rs b/nym-connect/src-tauri/src/state.rs index f0aa869edd..72e16a6748 100644 --- a/nym-connect/src-tauri/src/state.rs +++ b/nym-connect/src-tauri/src/state.rs @@ -43,7 +43,6 @@ impl State { } } - #[allow(unused)] pub fn get_status(&self) -> ConnectionStatusKind { self.status.clone() } @@ -111,9 +110,7 @@ impl State { } // Kick off the main task and get the channel for controlling it - let (msg_receiver, exit_status_receiver) = self.start_nym_socks5_client()?; - self.set_state(ConnectionStatusKind::Connected, window); - Ok((msg_receiver, exit_status_receiver)) + self.start_nym_socks5_client() } /// Create a configuration file @@ -142,9 +139,16 @@ impl State { Ok((msg_rx, exit_status_rx)) } + /// Once the SOCKS5 client is operational, the status listener would call this + pub fn mark_connected(&mut self, window: &tauri::Window) { + log::trace!("state::mark_connected"); + self.set_state(ConnectionStatusKind::Connected, window); + } + /// Disconnect by sending a message to the SOCKS5 client thread. Once it has finished and is /// disconnected, the disconnect handler will mark it as disconnected. pub async fn start_disconnecting(&mut self, window: &tauri::Window) -> Result<()> { + log::trace!("state::start_disconnecting"); self.set_state(ConnectionStatusKind::Disconnecting, window); // Send shutdown message diff --git a/nym-connect/src-tauri/src/tasks.rs b/nym-connect/src-tauri/src/tasks.rs index 3201648e6e..7f1e320031 100644 --- a/nym-connect/src-tauri/src/tasks.rs +++ b/nym-connect/src-tauri/src/tasks.rs @@ -1,7 +1,11 @@ -use client_core::config::{ClientCoreConfigTrait, GatewayEndpointConfig}; +use client_core::{ + config::{ClientCoreConfigTrait, GatewayEndpointConfig}, + error::ClientCoreStatusMessage, +}; use futures::{channel::mpsc, StreamExt}; use std::sync::Arc; use tap::TapFallible; +use task::manager::TaskStatus; use tokio::sync::RwLock; use config_common::NymConfig; @@ -9,7 +13,7 @@ use config_common::NymConfig; use nym_socks5::client::NymClient as Socks5NymClient; use nym_socks5::client::{config::Config as Socks5Config, Socks5ControlMessageSender}; -use crate::{error::Result, state::State}; +use crate::{error::Result, models::ConnectionStatusKind, operations::connection, state::State}; pub type ExitStatusReceiver = futures::channel::oneshot::Receiver; @@ -89,8 +93,78 @@ struct Payload { message: String, } +impl Payload { + fn new(title: String, message: String) -> Self { + Self { title, message } + } +} + +fn emit_event(event: &str, title: &str, msg: &str, window: &tauri::Window) { + if let Err(err) = window.emit(event, Payload::new(title.into(), msg.into())) { + log::error!("Failed to emit tauri event: {err}"); + } +} + +fn emit_status_event( + event: &str, + msg: Box, + window: &tauri::Window, +) { + if let Err(err) = window.emit(event, Payload::new("SOCKS5 update".into(), msg.to_string())) { + log::error!("Failed to emit tauri event: {err}"); + } +} + +pub fn start_connection_check(state: Arc>, window: tauri::Window) { + log::debug!("Starting connection check handler"); + tokio::spawn(async move { + if state.read().await.get_status() != ConnectionStatusKind::Connected { + log::error!("SOCKS5 connection status check failed: not connected"); + return; + } + + log::info!("Running connection health check"); + if connection::status::run_health_check().await { + emit_event( + "socks5-connection-success-event", + "SOCKS5 success", + "SOCKS5 connection health check successful", + &window, + ); + } else { + if state.read().await.get_status() != ConnectionStatusKind::Connected { + log::debug!("SOCKS5 connection status check cancelled: not connected"); + } + log::error!("SOCKS5 connection health check failed"); + emit_event( + "socks5-connection-fail-event", + "SOCKS5 error", + "SOCKS5 connection health check failed", + &window, + ); + } + + log::debug!("Connection check handler exiting"); + }); +} + +async fn handle_connection_ready( + state: &Arc>, + window: &tauri::Window, + msg: Box, +) { + { + let mut state_w = state.write().await; + state_w.mark_connected(window); + } + + emit_status_event("socks5-connected-event", msg, window); + start_connection_check(state.clone(), window.clone()); +} + /// The status listener listens for non-exit status messages from the background socks5 proxy task. pub fn start_status_listener( + state: Arc>, window: tauri::Window, mut msg_receiver: task::StatusReceiver, ) { @@ -98,16 +172,21 @@ pub fn start_status_listener( tokio::spawn(async move { while let Some(msg) = msg_receiver.next().await { log::info!("SOCKS5 proxy sent status message: {}", msg); - window - .emit( - "socks5-status-event", - Payload { - title: "SOCKS5 update".into(), - message: msg.to_string(), - }, - ) - .unwrap(); + + if let Some(TaskStatus::Ready) = msg.downcast_ref::() { + handle_connection_ready(&state, &window, msg).await; + } else if let Some(_gateway_status) = msg.downcast_ref::() { + // TODO: use this instead once we change on the frontend too + //let event_name = match gateway_status { + // ClientCoreStatusMessage::GatewayIsSlow => "socks5-gateway-status", + // ClientCoreStatusMessage::GatewayIsVerySlow => "socks5-gateway-status", + //}; + emit_status_event("socks5-status-event", msg, &window); + } else { + emit_status_event("socks5-status-event", msg, &window); + } } + log::info!("Status listener exiting"); }); } @@ -123,39 +202,30 @@ pub fn start_disconnect_listener( match exit_status_receiver.await { Ok(Socks5ExitStatusMessage::Stopped) => { log::info!("SOCKS5 task reported it has finished"); - window - .emit( - "socks5-event", - Payload { - title: "SOCKS5 finished".into(), - message: "SOCKS5 task reported it has finished".into(), - }, - ) - .unwrap(); + emit_event( + "socks5-event", + "SOCKS5 finished", + "SOCKS5 task reported it has finished", + &window, + ); } Ok(Socks5ExitStatusMessage::Failed(err)) => { log::info!("SOCKS5 task reported error: {err}"); - window - .emit( - "socks5-event", - Payload { - title: "SOCKS5 error".into(), - message: format!("SOCKS5 failed: {err}"), - }, - ) - .unwrap(); + emit_event( + "socks5-event", + "SOCKS5 error", + &format!("SOCKS5 failed: {err}"), + &window, + ); } Err(_) => { log::info!("SOCKS5 task appears to have stopped abruptly"); - window - .emit( - "socks5-event", - Payload { - title: "SOCKS5 error".into(), - message: "SOCKS5 stopped abruptly. Please try reconnecting.".into(), - }, - ) - .unwrap(); + emit_event( + "socks5-event", + "SOCKS5 error", + "SOCKS5 stopped abruptly. Please try reconnecting.", + &window, + ); } }