refactor(nc-desktop): add privacy level user settings (#3664)

This commit is contained in:
Pierre Dommerc
2023-07-19 15:56:00 +02:00
committed by pierre
parent 8cc90be8c6
commit c5f7d066b0
28 changed files with 324 additions and 396 deletions
@@ -1,104 +0,0 @@
name: Publish Nym Connect - desktop (MacOS)
on:
workflow_dispatch:
inputs:
release_tag:
required: true
description: Release tag (nym-connect-s-v*)
type: string
default: 'nym-connect-s-v1.0.0'
push:
tags:
- nym-connect-s-v*
defaults:
run:
working-directory: nym-connect/desktop
jobs:
publish-tauri:
if: ${{ (startsWith(github.ref, 'refs/tags/nym-connect-s-') && github.event_name == 'release') || github.event_name == 'workflow_dispatch' }}
strategy:
fail-fast: false
matrix:
platform: [macos-latest]
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v2
- name: Node
uses: actions/setup-node@v3
with:
node-version: 18
- name: Install Rust stable
uses: actions-rs/toolchain@v1
with:
toolchain: stable
- name: Install the Apple developer certificate for code signing
env:
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
run: |
# create variables
CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
# import certificate and provisioning profile from secrets
echo -n "$APPLE_CERTIFICATE" | base64 --decode --output $CERTIFICATE_PATH
# create temporary keychain
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
# import certificate to keychain
security import $CERTIFICATE_PATH -P "$APPLE_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
security list-keychain -d user -s $KEYCHAIN_PATH
- name: Create env file
uses: timheuer/base64-to-file@v1.2
with:
fileName: '.env'
encodedString: ${{ secrets.WALLET_ADMIN_ADDRESS }}
- name: Prepare for build
env:
RELEASE_TAG: ${{ github.ref_name || inputs.release_tag }}
run: ./scripts/pre_medium_build.sh
- name: Install app dependencies and build it
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ENABLE_CODE_SIGNING: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_IDENTITY_ID }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
NYM_CONNECT_ENABLE_MEDIUM: 1
run: yarn && yarn build
- name: Upload Artifact
uses: actions/upload-artifact@v3
with:
name: nym-connect-s_1.0.0_x64.dmg
path: nym-connect/desktop/target/release/bundle/dmg/nym-connect-s_1*_x64.dmg
retention-days: 30
- name: Clean up keychain
if: ${{ always() }}
run: |
security delete-keychain $RUNNER_TEMP/app-signing.keychain-db
- id: create-release
name: Upload to release based on tag name
uses: softprops/action-gh-release@v1
if: github.event_name == 'release'
with:
files: |
nym-connect/desktop/target/release/bundle/dmg/*.dmg
nym-connect/desktop/target/release/bundle/macos/*.app.tar.gz*
@@ -1,85 +0,0 @@
name: Publish NymConnect S - desktop (Ubuntu)
on:
workflow_dispatch:
inputs:
release_tag:
required: true
description: Release tag (nym-connect-s-v*)
type: string
default: 'nym-connect-s-v1.0.0'
push:
tags:
- nym-connect-s-v*
defaults:
run:
working-directory: nym-connect/desktop
jobs:
publish-tauri:
if: ${{ (startsWith(github.ref, 'refs/tags/nym-connect-s-') && github.event_name == 'release') || github.event_name == 'workflow_dispatch' }}
strategy:
fail-fast: false
matrix:
platform: [custom-runner-linux]
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v2
- name: Tauri dependencies
run: >
sudo apt-get update &&
sudo apt-get install -y webkit2gtk-4.0 libayatana-appindicator3-dev
continue-on-error: true
- name: Node
uses: actions/setup-node@v3
with:
node-version: 18
- name: Install Rust stable
uses: actions-rs/toolchain@v1
with:
toolchain: stable
- name: Install project dependencies
shell: bash
run: cd .. && yarn --network-timeout 100000
- name: Install app dependencies
run: yarn
- name: Create env file
uses: timheuer/base64-to-file@v1.2
with:
fileName: '.env'
encodedString: ${{ secrets.WALLET_ADMIN_ADDRESS }}
- name: Prepare for build
env:
RELEASE_TAG: ${{ github.ref_name || inputs.release_tag }}
run: ./scripts/pre_medium_build.sh
- name: Build app
run: yarn build
env:
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
NYM_CONNECT_ENABLE_MEDIUM: 1
- name: Upload Artifact
uses: actions/upload-artifact@v3
with:
name: nym-connect-s.AppImage.tar.gz
path: nym-connect/desktop/target/release/bundle/appimage/nym-connect-s_1*_amd64.AppImage
retention-days: 30
- id: create-release
name: Upload to release based on tag name
uses: softprops/action-gh-release@v1
if: github.event_name == 'release'
with:
files: |
nym-connect/desktop/target/release/bundle/appimage/*.AppImage
nym-connect/desktop/target/release/bundle/appimage/*.AppImage.tar.gz*
@@ -1,104 +0,0 @@
name: Publish Nym Connect - desktop (Windows 10)
on:
workflow_dispatch:
inputs:
release_tag:
required: true
description: Release tag (nym-connect-s-v*)
type: string
default: 'nym-connect-s-v1.0.0'
push:
tags:
- nym-connect-s-v*
defaults:
run:
working-directory: nym-connect/desktop
jobs:
publish-tauri:
if: ${{ (startsWith(github.ref, 'refs/tags/nym-connect-s-') && github.event_name == 'release') || github.event_name == 'workflow_dispatch' }}
strategy:
fail-fast: false
matrix:
platform: [windows10]
runs-on: ${{ matrix.platform }}
steps:
- name: Clean up first
continue-on-error: true
working-directory: .
run: |
cd ..
del /s /q /A:H nym
rmdir /s /q nym
- uses: actions/checkout@v3
- name: Import signing certificate
env:
WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}
run: |
New-Item -ItemType directory -Path certificate
Set-Content -Path certificate/tempCert.txt -Value $env:WINDOWS_CERTIFICATE
certutil -decode certificate/tempCert.txt certificate/certificate.pfx
Remove-Item -path certificate -include tempCert.txt
Import-PfxCertificate -FilePath certificate/certificate.pfx -CertStoreLocation Cert:\CurrentUser\My -Password (ConvertTo-SecureString -String $env:WINDOWS_CERTIFICATE_PASSWORD -Force -AsPlainText)
- name: Node
uses: actions/setup-node@v3
with:
node-version: 18
- name: Install Rust stable
uses: actions-rs/toolchain@v1
with:
toolchain: stable
- name: Create env file
uses: timheuer/base64-to-file@v1.2
with:
fileName: '.env'
encodedString: ${{ secrets.WALLET_ADMIN_ADDRESS }}
- name: Install project dependencies
shell: bash
run: cd .. && yarn --network-timeout 100000
- name: Install app dependencies
shell: bash
run: yarn --network-timeout 100000
- name: Prepare for build
env:
RELEASE_TAG: ${{ github.ref_name || inputs.release_tag }}
run: ./scripts/pre_medium_build.sh
- name: Build and sign it
shell: bash
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ENABLE_CODE_SIGNING: ${{ secrets.WINDOWS_CERTIFICATE }}
WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
NYM_CONNECT_ENABLE_MEDIUM: 1
run: yarn build
- name: Upload Artifact
uses: actions/upload-artifact@v3
with:
name: nym-connect-s_1.0.0_x64_en-US.msi
path: nym-connect/desktop/target/release/bundle/msi/nym-connect-s_1*_x64_en-US.msi
retention-days: 30
- id: create-release
name: Upload to release based on tag name
uses: softprops/action-gh-release@v1
if: github.event_name == 'release'
with:
files: |
nym-connect/desktop/target/release/bundle/msi/*.msi
nym-connect/desktop/target/release/bundle/msi/*.msi.zip*
+35 -12
View File
@@ -1532,6 +1532,12 @@ dependencies = [
"termcolor",
]
[[package]]
name = "equivalent"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88bffebc5d80432c9b140ee17875ff173a8ab62faad5b257da912bd2f6c1c0a1"
[[package]]
name = "errno"
version = "0.2.8"
@@ -2217,7 +2223,7 @@ dependencies = [
"futures-sink",
"futures-util",
"http",
"indexmap",
"indexmap 1.9.2",
"slab",
"tokio",
"tokio-util",
@@ -2256,6 +2262,12 @@ dependencies = [
"ahash",
]
[[package]]
name = "hashbrown"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a"
[[package]]
name = "hashlink"
version = "0.7.0"
@@ -2618,6 +2630,16 @@ dependencies = [
"serde",
]
[[package]]
name = "indexmap"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d"
dependencies = [
"equivalent",
"hashbrown 0.14.0",
]
[[package]]
name = "infer"
version = "0.7.0"
@@ -3462,6 +3484,7 @@ dependencies = [
"thiserror",
"time",
"tokio",
"toml 0.7.4",
"ts-rs",
"url",
"yaml-rust",
@@ -4530,7 +4553,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9bd9647b268a3d3e14ff09c23201133a62589c658db02bb7388c7246aafe0590"
dependencies = [
"base64 0.21.2",
"indexmap",
"indexmap 1.9.2",
"line-wrap",
"quick-xml 0.28.1",
"serde",
@@ -5129,7 +5152,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02c613288622e5f0c3fdc5dbd4db1c5fbe752746b1d1a56a0630b78fd00de44f"
dependencies = [
"dyn-clone",
"indexmap",
"indexmap 1.9.2",
"schemars_derive",
"serde",
"serde_json",
@@ -5345,9 +5368,9 @@ dependencies = [
[[package]]
name = "serde_spanned"
version = "0.6.2"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93107647184f6027e3b7dcb2e11034cf95ffa1e3a682c67951963ac69c1c007d"
checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186"
dependencies = [
"serde",
]
@@ -5677,7 +5700,7 @@ dependencies = [
"futures-util",
"hashlink 0.7.0",
"hex",
"indexmap",
"indexmap 1.9.2",
"itoa 1.0.6",
"libc",
"libsqlite3-sys",
@@ -5723,7 +5746,7 @@ dependencies = [
"futures-util",
"hashlink 0.8.1",
"hex",
"indexmap",
"indexmap 1.9.2",
"itoa 1.0.6",
"libc",
"libsqlite3-sys",
@@ -6580,20 +6603,20 @@ dependencies = [
[[package]]
name = "toml_datetime"
version = "0.6.2"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a76a9312f5ba4c2dec6b9161fdf25d87ad8a09256ccea5a556fef03c706a10f"
checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
version = "0.19.10"
version = "0.19.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2380d56e8670370eee6566b0bfd4265f65b3f432e8c6d85623f728d4fa31f739"
checksum = "c500344a19072298cd05a7224b3c0c629348b78692bf48466c5238656e315a78"
dependencies = [
"indexmap",
"indexmap 2.0.0",
"serde",
"serde_spanned",
"toml_datetime",
+1 -1
View File
@@ -43,7 +43,7 @@ time = { version = "0.3.17", features = ["local-offset"] }
tokio = { version = "1.24.1", features = ["sync", "time"] }
url = "2.2"
yaml-rust = "0.4"
toml = "0.7"
nym-client-core = { path = "../../../common/client-core" }
nym-api-requests = { path = "../../../nym-api/nym-api-requests" }
@@ -1,10 +1,3 @@
use std::env;
fn main() {
if env::var_os("NYM_CONNECT_ENABLE_MEDIUM").is_some() {
println!("cargo:rustc-cfg=medium_enabled");
}
println!("cargo:rerun-if-changed=build.rs");
tauri_build::build();
}
@@ -26,6 +26,10 @@ mod old_config_v1_1_20_2;
mod persistence;
mod template;
mod upgrade;
mod user_data;
pub use user_data::PrivacyLevel;
pub use user_data::UserData;
static SOCKS5_CONFIG_ID: &str = "nym-connect";
@@ -0,0 +1,67 @@
use eyre::{eyre, Context, Result};
use log::error;
use serde::{Deserialize, Serialize};
use std::{fs, str};
use tauri::api::path::data_dir;
const DATA_DIR: &str = "nym-connect";
const DATA_FILE: &str = "user-data.toml";
#[derive(Serialize, Deserialize, Debug, Copy, Clone, Default)]
pub enum PrivacyLevel {
#[default]
High,
Medium,
}
// User data is read from and write on disk
// Linux: $XDG_DATA_HOME or $HOME/.local/share/
// macOS: $HOME/Library/Application Support
// Windows: {FOLDERID_RoamingAppData}
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub struct UserData {
pub monitoring: Option<bool>,
pub privacy_level: Option<PrivacyLevel>,
}
fn create_directory_path() -> Result<()> {
let mut data_dir = data_dir().ok_or(eyre!("Failed to retrieve data directory"))?;
data_dir.push(DATA_DIR);
fs::create_dir_all(&data_dir).context(format!(
"Failed to create user data directory path {}",
data_dir.display()
))
}
impl UserData {
pub fn read() -> Result<Self> {
// create the full directory path if it is missing
create_directory_path()?;
let mut data_path = data_dir().ok_or(eyre!("Failed to retrieve data directory"))?;
data_path.push(DATA_DIR);
data_path.push(DATA_FILE);
let content = fs::read(&data_path)
.context(format!("Failed to read user data {}", data_path.display()))?;
toml::from_str::<UserData>(str::from_utf8(&content)?).map_err(|e| {
error!("{}", e);
eyre!("{e}")
})
}
pub fn write(&self) -> Result<()> {
// create the full directory path if it is missing
create_directory_path()?;
let mut data_path = data_dir().ok_or(eyre!("Failed to retrieve data directory"))?;
data_path.push(DATA_DIR);
data_path.push(DATA_FILE);
let toml = toml::to_string(&self)?;
fs::write(data_path, toml)?;
Ok(())
}
}
@@ -70,6 +70,8 @@ pub enum BackendError {
NewWindowError,
#[error("unable to parse the specified gateway")]
UnableToParseGateway,
#[error("unable to write user data to disk")]
UserDataWriteError,
#[error("unable to load keys: {source}")]
UnableToLoadKeys {
+12 -9
View File
@@ -9,8 +9,9 @@ use nym_config::defaults::setup_env;
use tauri::Manager;
use tokio::sync::RwLock;
use crate::config::UserData;
use crate::menu::{create_tray_menu, tray_menu_event_handler};
use crate::state::{is_medium_enabled, State};
use crate::state::State;
use crate::window::window_toggle;
mod config;
@@ -25,12 +26,6 @@ mod tasks;
mod window;
fn main() {
if is_medium_enabled() {
println!("medium mode enabled");
std::env::set_var("NYM_CONNECT_DISABLE_COVER", "1");
std::env::set_var("NYM_CONNECT_ENABLE_MIXED_SIZE_PACKETS", "1");
std::env::set_var("NYM_CONNECT_DISABLE_PER_HOP_DELAYS", "1");
}
setup_env(None);
println!("Starting up...");
@@ -40,13 +35,22 @@ fn main() {
log::warn!("Failed to fix PATH: {error}");
}
let user_data = UserData::read().unwrap_or_else(|e| {
println!("{}", e);
println!("Fallback to default");
UserData::default()
});
let context = tauri::generate_context!();
tauri::Builder::default()
.manage(Arc::new(RwLock::new(State::new())))
.manage(Arc::new(RwLock::new(State::new(user_data))))
.invoke_handler(tauri::generate_handler![
crate::operations::config::get_config_file_location,
crate::operations::config::get_config_id,
crate::operations::common::get_env,
crate::operations::common::get_user_data,
crate::operations::common::set_monitoring,
crate::operations::common::set_privacy_level,
crate::operations::connection::connect::get_gateway,
crate::operations::connection::connect::get_service_provider,
crate::operations::connection::connect::set_gateway,
@@ -57,7 +61,6 @@ fn main() {
crate::operations::connection::status::get_connection_status,
crate::operations::connection::status::get_gateway_connection_status,
crate::operations::connection::status::start_connection_health_check_task,
crate::operations::connection::status::is_medium_mode_enabled,
crate::operations::directory::get_services,
crate::operations::directory::get_gateways_detailed,
crate::operations::export::export_keys,
@@ -1,4 +1,9 @@
use crate::config::PrivacyLevel;
use crate::error::Result;
use crate::{config::UserData, state::State};
use std::env;
use std::sync::Arc;
use tokio::sync::RwLock;
#[tauri::command]
pub async fn get_env(variable: String) -> Option<String> {
@@ -7,3 +12,27 @@ pub async fn get_env(variable: String) -> Option<String> {
var
}
#[tauri::command]
pub async fn get_user_data(state: tauri::State<'_, Arc<RwLock<State>>>) -> Result<UserData> {
let guard = state.read().await;
Ok(guard.get_user_data().clone())
}
#[tauri::command]
pub async fn set_monitoring(
enabled: bool,
state: tauri::State<'_, Arc<RwLock<State>>>,
) -> Result<()> {
let mut guard = state.write().await;
guard.set_monitoring(enabled)
}
#[tauri::command]
pub async fn set_privacy_level(
privacy_level: PrivacyLevel,
state: tauri::State<'_, Arc<RwLock<State>>>,
) -> Result<()> {
let mut guard = state.write().await;
guard.set_privacy_level(privacy_level)
}
@@ -12,6 +12,7 @@ pub async fn start_connecting(
window: tauri::Window<tauri::Wry>,
) -> Result<ConnectResult> {
log::trace!("Start connecting");
let (msg_receiver, exit_status_receiver) = {
let mut state_w = state.write().await;
state_w.start_connecting(&window).await?
@@ -5,7 +5,7 @@ use std::sync::Arc;
use tokio::sync::RwLock;
use crate::models::{ConnectionStatusKind, ConnectivityTestResult, GatewayConnectionStatusKind};
use crate::state::{is_medium_enabled, State};
use crate::state::State;
#[tauri::command]
pub async fn get_connection_status(
@@ -42,8 +42,3 @@ pub fn start_connection_health_check_task(
) {
tasks::start_connection_check(state.inner().clone(), window);
}
#[tauri::command]
pub async fn is_medium_mode_enabled(_state: tauri::State<'_, Arc<RwLock<State>>>) -> Result<bool> {
Ok(is_medium_enabled())
}
@@ -1,12 +1,15 @@
use itertools::Itertools;
use crate::config::PrivacyLevel;
use crate::error::Result;
use crate::models::{
DirectoryService, DirectoryServiceProvider, HarbourMasterService, PagedResult,
};
use crate::state::is_medium_enabled;
use crate::state::State;
use nym_api_requests::models::GatewayBondAnnotated;
use nym_contracts_common::types::Percent;
use std::sync::Arc;
use tokio::sync::RwLock;
static SERVICE_PROVIDER_WELLKNOWN_URL: &str =
"https://nymtech.net/.wellknown/connect/service-providers.json";
@@ -20,21 +23,20 @@ static HARBOUR_MASTER_URL: &str = "https://harbourmaster.nymtech.net/v1/services
static GATEWAYS_DETAILED_URL: &str =
"https://validator.nymtech.net/api/v1/status/gateways/detailed";
fn get_services_url() -> &'static str {
if is_medium_enabled() {
return SERVICE_PROVIDER_WELLKNOWN_URL_MEDIUM;
}
SERVICE_PROVIDER_WELLKNOWN_URL
}
#[tauri::command]
pub async fn get_services() -> Result<Vec<DirectoryServiceProvider>> {
pub async fn get_services(
state: tauri::State<'_, Arc<RwLock<State>>>,
) -> Result<Vec<DirectoryServiceProvider>> {
log::trace!("Fetching services");
let all_services = fetch_services().await?;
let guard = state.read().await;
let privacy_level = guard.get_user_data().privacy_level.unwrap_or_default();
let all_services = fetch_services(&privacy_level).await?;
log::trace!("Received: {:#?}", all_services);
// Early return if we're running with medium toggle enabled
if is_medium_enabled() {
if let PrivacyLevel::Medium = privacy_level {
return Ok(all_services.into_iter().flat_map(|sp| sp.items).collect());
}
@@ -100,8 +102,12 @@ fn filter_out_poor_gateways(
.collect()
}
async fn fetch_services() -> Result<Vec<DirectoryService>> {
let services_url = get_services_url();
async fn fetch_services(privacy_level: &PrivacyLevel) -> Result<Vec<DirectoryService>> {
let services_url = match privacy_level {
PrivacyLevel::Medium => SERVICE_PROVIDER_WELLKNOWN_URL_MEDIUM,
_ => SERVICE_PROVIDER_WELLKNOWN_URL,
};
let services_res = reqwest::get(services_url)
.await?
.json::<Vec<DirectoryService>>()
+30 -6
View File
@@ -1,4 +1,5 @@
use futures::SinkExt;
use log::error;
use nym_client_core::error::ClientCoreStatusMessage;
use nym_socks5_client_core::{Socks5ControlMessage, Socks5ControlMessageSender};
use std::time::Duration;
@@ -7,6 +8,8 @@ use tauri::Manager;
use tokio::time::Instant;
use crate::config::Config;
use crate::config::PrivacyLevel;
use crate::config::UserData;
use crate::{
config::{self, socks5_config_id_appended_with},
error::{BackendError, Result},
@@ -65,10 +68,13 @@ pub struct State {
/// The latest end-to-end connectivity test result. The first test is initiated on connection
/// established. Additional tests can be triggered.
connectivity_test_result: ConnectivityTestResult,
/// User data saved on disk, like user settings
user_data: UserData,
}
impl State {
pub fn new() -> Self {
pub fn new(user_data: UserData) -> Self {
State {
status: ConnectionStatusKind::Disconnected,
service_provider: None,
@@ -76,6 +82,7 @@ impl State {
socks5_client_sender: None,
gateway_connectivity: GatewayConnectivity::Good,
connectivity_test_result: ConnectivityTestResult::NotAvailable,
user_data,
}
}
@@ -93,6 +100,26 @@ impl State {
self.gateway_connectivity
}
pub fn get_user_data(&self) -> &UserData {
&self.user_data
}
pub fn set_monitoring(&mut self, enabled: bool) -> Result<()> {
self.user_data.monitoring = Some(enabled);
self.user_data.write().map_err(|e| {
error!("Failed to write user data to disk {e}");
BackendError::UserDataWriteError
})
}
pub fn set_privacy_level(&mut self, privacy_level: PrivacyLevel) -> Result<()> {
self.user_data.privacy_level = Some(privacy_level);
self.user_data.write().map_err(|e| {
error!("Failed to write user data to disk {e}");
BackendError::UserDataWriteError
})
}
pub fn set_gateway_connectivity(&mut self, gateway_connectivity: GatewayConnectivity) {
self.gateway_connectivity = gateway_connectivity
}
@@ -200,8 +227,9 @@ impl State {
&mut self,
) -> Result<(nym_task::StatusReceiver, ExitStatusReceiver)> {
let id = self.get_config_id()?;
let privacy_level = self.user_data.privacy_level.unwrap_or_default();
let (control_tx, msg_rx, exit_status_rx, used_gateway) =
tasks::start_nym_socks5_client(&id).await?;
tasks::start_nym_socks5_client(&id, &privacy_level).await?;
self.socks5_client_sender = Some(control_tx);
self.gateway = Some(used_gateway.gateway_id);
Ok((msg_rx, exit_status_rx))
@@ -244,7 +272,3 @@ impl State {
self.set_state(ConnectionStatusKind::Disconnected, window);
}
}
pub fn is_medium_enabled() -> bool {
cfg!(medium_enabled) || std::env::var("NYM_CONNECT_ENABLE_MEDIUM").is_ok()
}
+6 -8
View File
@@ -10,7 +10,7 @@ use std::sync::Arc;
use tap::TapFallible;
use tokio::sync::RwLock;
use crate::config::Config;
use crate::config::{Config, PrivacyLevel};
use crate::{
error::Result,
events::{self, emit_event, emit_status_event},
@@ -30,23 +30,20 @@ pub enum Socks5ExitStatusMessage {
Failed(Box<dyn std::error::Error + Send>),
}
fn override_config_from_env(config: &mut Config) {
fn override_config_from_env(config: &mut Config, privacy_level: &PrivacyLevel) {
// Disable both the loop cover traffic that runs in the background as well as the Poisson
// process that injects cover traffic into the traffic stream.
if std::env::var("NYM_CONNECT_DISABLE_COVER").is_ok() {
if let PrivacyLevel::Medium = privacy_level {
log::info!("Running in Medium privacy level");
log::warn!("Disabling cover traffic");
config.core.base.set_no_cover_traffic_with_keepalive();
}
if std::env::var("NYM_CONNECT_ENABLE_MIXED_SIZE_PACKETS").is_ok() {
log::warn!("Enabling mixed size packets");
config
.core
.base
.set_secondary_packet_size(Some(PacketSize::ExtendedPacket16));
}
if std::env::var("NYM_CONNECT_DISABLE_PER_HOP_DELAYS").is_ok() {
log::warn!("Disabling per-hop delay");
config.core.base.set_no_per_hop_delays();
}
@@ -55,6 +52,7 @@ fn override_config_from_env(config: &mut Config) {
/// The main SOCKS5 client task. It loads the configuration from file determined by the `id`.
pub async fn start_nym_socks5_client(
id: &str,
privacy_level: &PrivacyLevel,
) -> Result<(
Socks5ControlMessageSender,
nym_task::StatusReceiver,
@@ -65,7 +63,7 @@ pub async fn start_nym_socks5_client(
let mut config = Config::read_from_default_path(id)
.tap_err(|_| log::warn!("Failed to load configuration file"))?;
override_config_from_env(&mut config);
override_config_from_env(&mut config, privacy_level);
log::trace!("Configuration used: {:#?}", config);
@@ -16,7 +16,7 @@ const ConnectionStatusContent: FCWithChildren<{
serviceProvider?: ServiceProvider;
gatewayError: boolean;
}> = ({ status, serviceProvider, gatewayError }) => {
const { speedMode } = useClientContext();
const { userData } = useClientContext();
if (gatewayError) {
return (
@@ -49,10 +49,10 @@ const ConnectionStatusContent: FCWithChildren<{
</Typography>
</Box>
</Tooltip>
{speedMode === 'medium' && (
{userData?.privacy_level === 'Medium' && (
<Stack alignItems="center" color="warning.main">
<Typography variant="caption" color="grey.400">
Speedy mode activated
Speed boost activated
</Typography>
</Stack>
)}
+34 -27
View File
@@ -3,23 +3,24 @@ import { DateTime } from 'luxon';
import { invoke } from '@tauri-apps/api';
import { Error } from 'src/types/error';
import { getVersion } from '@tauri-apps/api/app';
import * as Sentry from '@sentry/react';
import { useEvents } from 'src/hooks/events';
import { UserDefinedGateway, UserDefinedSPAddress } from 'src/types/service-provider';
import { getItemFromStorage, setItemInStorage } from 'src/utils';
import { ConnectionStatusKind, GatewayPerformance, SpeedMode } from '../types';
import { ConnectionStatusKind, GatewayPerformance, PrivacyLevel, UserData } from '../types';
import { ConnectionStatsItem } from '../components/ConnectionStats';
import { ServiceProvider } from '../types/directory';
import initSentry from '../sentry';
const FORAGE_GATEWAY_KEY = 'nym-connect-user-gateway';
const FORAGE_SP_KEY = 'nym-connect-user-sp';
const FORAGE_MONITORING_ENABLED = 'nym-connect-monitoring-enabled';
type ModeType = 'light' | 'dark';
export type TClientContext = {
mode: ModeType;
appVersion?: string;
userData?: UserData;
connectionStatus: ConnectionStatusKind;
connectionStats?: ConnectionStatsItem[];
connectedSince?: DateTime;
@@ -32,7 +33,6 @@ export type TClientContext = {
serviceProviders?: ServiceProvider[];
setMode: (mode: ModeType) => void;
clearError: () => void;
monitoringEnabled: boolean;
setConnectionStatus: (connectionStatus: ConnectionStatusKind) => void;
setConnectionStats: (connectionStats: ConnectionStatsItem[] | undefined) => void;
setConnectedSince: (connectedSince: DateTime | undefined) => void;
@@ -43,7 +43,7 @@ export type TClientContext = {
setUserDefinedGateway: React.Dispatch<React.SetStateAction<UserDefinedGateway>>;
setUserDefinedSPAddress: React.Dispatch<React.SetStateAction<UserDefinedSPAddress>>;
setMonitoring: (value: boolean) => Promise<void>;
speedMode: SpeedMode;
setPrivacyLevel: (value: PrivacyLevel) => Promise<void>;
};
export const ClientContext = createContext({} as TClientContext);
@@ -67,19 +67,26 @@ export const ClientContextProvider: FCWithChildren = ({ children }) => {
isActive: false,
address: undefined,
});
const [monitoringEnabled, setMonitoringEnabled] = useState(false);
const [speedMode, setspeedMode] = useState<SpeedMode>('slow');
const [userData, setUserData] = useState<UserData>();
const getAppVersion = async () => {
const version = await getVersion();
return version;
};
const getUserData = async () => {
const data = await invoke<UserData>('get_user_data');
if (!data.privacy_level) {
data.privacy_level = 'High';
}
setUserData(data);
return data;
};
useEffect(() => {
const initSentryClient = async () => {
const monitoring = await getItemFromStorage({ key: FORAGE_MONITORING_ENABLED });
setMonitoringEnabled(Boolean(monitoring));
if (monitoring === true) {
const data = await getUserData();
if (data.monitoring) {
await initSentry();
}
};
@@ -87,17 +94,6 @@ export const ClientContextProvider: FCWithChildren = ({ children }) => {
initSentryClient();
}, []);
useEffect(() => {
const initSpeedMode = async () => {
const isEnabled = await invoke<boolean>('is_medium_mode_enabled');
if (isEnabled) {
setspeedMode('medium');
}
};
initSpeedMode();
}, []);
useEffect(() => {
setItemInStorage({ key: FORAGE_GATEWAY_KEY, value: userDefinedGateway });
}, [userDefinedGateway]);
@@ -124,7 +120,10 @@ export const ClientContextProvider: FCWithChildren = ({ children }) => {
};
useEvents({
onError: (e) => setError(e),
onError: (e) => {
setError(e);
Sentry.captureException(e);
},
onGatewayPerformanceChange: (performance) => setGatewayPerformance(performance),
onStatusChange: (status) => setConnectionStatus(status),
});
@@ -147,6 +146,7 @@ export const ClientContextProvider: FCWithChildren = ({ children }) => {
} catch (e) {
setError({ title: 'Could not connect', message: e as string });
console.log(e);
Sentry.captureException(e);
}
}, []);
@@ -155,6 +155,7 @@ export const ClientContextProvider: FCWithChildren = ({ children }) => {
await invoke('start_disconnecting');
} catch (e) {
console.log(e);
Sentry.captureException(e);
}
}, []);
@@ -197,8 +198,15 @@ export const ClientContextProvider: FCWithChildren = ({ children }) => {
const clearError = () => setError(undefined);
const setMonitoring = async (value: boolean) => {
setMonitoringEnabled(value);
await setItemInStorage({ key: FORAGE_MONITORING_ENABLED, value });
await invoke('set_monitoring', { enabled: value });
// refresh user data
await getUserData();
};
const setPrivacyLevel = async (value: PrivacyLevel) => {
await invoke('set_privacy_level', { privacyLevel: value });
// refresh user data
await getUserData();
};
const contextValue = useMemo(
@@ -216,7 +224,7 @@ export const ClientContextProvider: FCWithChildren = ({ children }) => {
selectedProvider,
serviceProviders,
connectedSince,
monitoringEnabled,
userData,
setConnectedSince,
setSerivceProvider,
startConnecting,
@@ -228,7 +236,7 @@ export const ClientContextProvider: FCWithChildren = ({ children }) => {
setUserDefinedGateway,
setUserDefinedSPAddress,
setMonitoring,
speedMode,
setPrivacyLevel,
}),
[
mode,
@@ -244,8 +252,7 @@ export const ClientContextProvider: FCWithChildren = ({ children }) => {
selectedProvider,
userDefinedGateway,
userDefinedSPAddress,
monitoringEnabled,
speedMode,
userData,
],
);
@@ -11,8 +11,7 @@ const mockValues: TClientContext = {
showInfoModal: false,
userDefinedGateway: { isActive: false, gateway: '' },
userDefinedSPAddress: { isActive: false, address: '' },
monitoringEnabled: false,
speedMode: 'slow',
userData: { monitoring: false, privacy_level: 'High' },
setShowInfoModal: () => {},
setMode: () => {},
clearError: () => {},
@@ -25,6 +24,7 @@ const mockValues: TClientContext = {
setUserDefinedGateway: () => {},
setUserDefinedSPAddress: () => {},
setMonitoring: async () => {},
setPrivacyLevel: async () => {},
};
export const MockProvider: FCWithChildren<{
+5 -1
View File
@@ -1,5 +1,6 @@
import { useEffect, useRef } from 'react';
import { listen, UnlistenFn } from '@tauri-apps/api/event';
import * as Sentry from '@sentry/react';
import { ConnectionStatusKind, GatewayPerformance } from 'src/types';
import { Error } from 'src/types/error';
import { TauriEvent } from 'src/types/event';
@@ -29,7 +30,10 @@ export const useEvents = ({
.then((result) => {
unlisten.push(result);
})
.catch((e) => console.log(e));
.catch((e) => {
console.log(e);
Sentry.captureException(e);
});
listen('socks5-event', (e: TauriEvent) => {
console.log(e);
@@ -1,5 +1,6 @@
import React from 'react';
import { forage } from '@tauri-apps/tauri-forage';
import * as Sentry from '@sentry/react';
import { DateTime } from 'luxon';
import { useClientContext } from 'src/context/main';
import { useTauriEvents } from 'src/utils';
@@ -28,12 +29,14 @@ export const ConnectionPage = () => {
// eslint-disable-next-line default-case
switch (currentStatus) {
case 'disconnected':
Sentry.captureMessage('start connect', 'info');
await context.setSerivceProvider();
await context.startConnecting();
context.setConnectedSince(DateTime.now());
context.setShowInfoModal(true);
break;
case 'connected':
Sentry.captureMessage('start disconnect', 'info');
await context.startDisconnecting();
context.setConnectedSince(undefined);
break;
+2 -1
View File
@@ -1,5 +1,5 @@
import React from 'react';
import { Apps, HelpOutline, Settings, BugReport } from '@mui/icons-material';
import { Apps, HelpOutline, Settings, BugReport, PrivacyTip } from '@mui/icons-material';
import { Stack, Link, List, ListItem, ListItemButton, ListItemIcon, ListItemText } from '@mui/material';
import { Link as RouterLink } from 'react-router-dom';
import { AppVersion } from 'src/components/AppVersion';
@@ -7,6 +7,7 @@ import { AppVersion } from 'src/components/AppVersion';
const menuSchema = [
{ title: 'Supported apps', icon: Apps, path: 'apps' },
{ title: 'How to connect guide', icon: HelpOutline, path: 'guide' },
{ title: 'Privacy level', icon: PrivacyTip, path: 'privacy-level' },
{ title: 'Please help us improve the app', icon: BugReport, path: 'monitoring' },
{ title: 'Settings', icon: Settings, path: 'settings' },
];
@@ -4,8 +4,8 @@ import { Box, FormControl, FormControlLabel, FormHelperText, Stack, Switch, Typo
import { useClientContext } from 'src/context/main';
export const MonitoringSettings = () => {
const { monitoringEnabled, setMonitoring } = useClientContext();
const [enabled, setEnabled] = useState(monitoringEnabled);
const { userData, setMonitoring } = useClientContext();
const [enabled, setEnabled] = useState(userData?.monitoring || false);
const [loading, setLoading] = useState(false);
const handleChange = async (e: ChangeEvent<HTMLInputElement>) => {
@@ -25,7 +25,13 @@ export const MonitoringSettings = () => {
<FormControl fullWidth>
<FormControlLabel
control={
<Switch checked={enabled} onChange={handleChange} disabled={loading} size="small" sx={{ ml: 1 }} />
<Switch
checked={enabled}
onChange={handleChange}
disabled={loading}
size="small"
sx={{ ml: 1, mr: 1 }}
/>
}
label="Enable"
/>
@@ -0,0 +1,48 @@
import React, { ChangeEvent, useState } from 'react';
import * as Sentry from '@sentry/react';
import { Box, FormControl, FormControlLabel, FormHelperText, Stack, Switch, Typography } from '@mui/material';
import { useClientContext } from 'src/context/main';
export const PrivacyLevelSettings = () => {
const { userData, setPrivacyLevel } = useClientContext();
const [speedBoost, setSpeedBoost] = useState(userData?.privacy_level !== 'High');
const [loading, setLoading] = useState(false);
const handleChange = async (e: ChangeEvent<HTMLInputElement>) => {
setLoading(true);
setSpeedBoost(e.target.checked);
Sentry.captureMessage(`privacy level switched to ${e.target.checked ? 'Medium' : 'High'}`, 'info');
await setPrivacyLevel(e.target.checked ? 'Medium' : 'High');
setLoading(false);
};
return (
<Box height="100%">
<Stack justifyContent="space-between" height="100%">
<Box>
<Typography fontWeight="bold" variant="body2" mb={2}>
Speed boost
</Typography>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={speedBoost}
onChange={handleChange}
disabled={loading}
size="small"
sx={{ ml: 1, mr: 1 }}
/>
}
label="Enable"
/>
<FormHelperText sx={{ m: 0, my: 2 }}>
By activating this option, the connection speed will be relatively faster in exchange of relaxing some
protections
</FormHelperText>
</FormControl>
</Box>
</Stack>
</Box>
);
};
+4 -2
View File
@@ -9,14 +9,15 @@ import { SettingsMenu } from 'src/pages/menu/settings';
import { GatewaySettings } from 'src/pages/menu/settings/GatewaySettings';
import { ServiceProviderSettings } from 'src/pages/menu/settings/ServiceProviderSettings';
import { MonitoringSettings } from '../pages/menu/settings/MonitoringSettings';
import { PrivacyLevelSettings } from '../pages/menu/settings/PrivacyLevelSettings';
import { useClientContext } from '../context/main';
const SentryRoutes = Sentry.withSentryReactRouterV6Routing(Routes);
export const AppRoutes = () => {
const { monitoringEnabled } = useClientContext();
const { userData } = useClientContext();
const RoutesContainer = monitoringEnabled ? SentryRoutes : Routes;
const RoutesContainer = userData?.monitoring ? SentryRoutes : Routes;
return (
<RoutesContainer>
@@ -25,6 +26,7 @@ export const AppRoutes = () => {
<Route index element={<Menu />} />
<Route path="apps" element={<CompatibleApps />} />
<Route path="guide" element={<HelpGuide />} />
<Route path="privacy-level" element={<PrivacyLevelSettings />} />
<Route path="monitoring" element={<MonitoringSettings />} />
<Route path="settings">
<Route index element={<SettingsMenu />} />
-1
View File
@@ -1 +0,0 @@
export type SpeedMode = 'fast' | 'medium' | 'slow';
+1 -1
View File
@@ -1,3 +1,3 @@
export * from './rust';
export * from './connection';
export * from './common';
export * from './user-data';
@@ -0,0 +1,6 @@
export type PrivacyLevel = 'High' | 'Medium';
export type UserData = {
monitoring?: boolean;
privacy_level?: PrivacyLevel;
};