diff --git a/package.json b/package.json index ebf96e516..415d190ec 100644 --- a/package.json +++ b/package.json @@ -57,4 +57,4 @@ "typescript-eslint": "^8.8.1", "vite": "^5.4.9" } -} \ No newline at end of file +} diff --git a/public/locales/af/settings.json b/public/locales/af/settings.json index 826f594e9..a2f731325 100644 --- a/public/locales/af/settings.json +++ b/public/locales/af/settings.json @@ -71,5 +71,13 @@ "invalid-seed-words": "Voer 24 woorde in, geskei deur spasies", "yes": "Yes", "cancel": "Cancel", - "report-issue": "Rapporteer \"n probleem" + "report-issue": "Rapporteer \"n probleem", + "app-restart-required": "App restart required", + "setup-tor-settings": "Setup Tor Settings for Privacy Control and Connectivity", + "control-port": "Control Port", + "save": "Save", + "errors": { + "invalid-control-port": "Control Port configuration is invalid", + "invalid-bridge": "Bridge configuration is invalid" + } } \ No newline at end of file diff --git a/public/locales/cn/settings.json b/public/locales/cn/settings.json index cac52ab32..6c10c064f 100644 --- a/public/locales/cn/settings.json +++ b/public/locales/cn/settings.json @@ -71,5 +71,13 @@ "action-requires-restart": "此操作需要重启以应用更改", "low-hash-rate-warning": "您的哈希率非常低。尝试关闭除Tari Universe以外的所有应用程序", "invalid-seed-words": "输入24个用空格分隔的单词", - "report-issue": "报告问题" + "report-issue": "报告问题", + "app-restart-required": "App restart required", + "setup-tor-settings": "Setup Tor Settings for Privacy Control and Connectivity", + "control-port": "Control Port", + "save": "Save", + "errors": { + "invalid-control-port": "Control Port configuration is invalid", + "invalid-bridge": "Bridge configuration is invalid" + } } \ No newline at end of file diff --git a/public/locales/de/settings.json b/public/locales/de/settings.json index b258b68f4..e9bcda3e2 100644 --- a/public/locales/de/settings.json +++ b/public/locales/de/settings.json @@ -71,5 +71,13 @@ "importing-wallet": "Wallet wird importiert", "action-requires-restart": "Diese Aktion erfordert einen Neustart, um Änderungen anzuwenden", "low-hash-rate-warning": "Ihre Hashrate ist sehr niedrig. Versuchen Sie, alle Apps außer Tari Universe zu schließen", - "invalid-seed-words": "Geben Sie 24 Wörter ein, getrennt durch Leerzeichen" + "invalid-seed-words": "Geben Sie 24 Wörter ein, getrennt durch Leerzeichen", + "app-restart-required": "App restart required", + "setup-tor-settings": "Setup Tor Settings for Privacy Control and Connectivity", + "control-port": "Control Port", + "save": "Save", + "errors": { + "invalid-control-port": "Control Port configuration is invalid", + "invalid-bridge": "Bridge configuration is invalid" + } } \ No newline at end of file diff --git a/public/locales/en/settings.json b/public/locales/en/settings.json index 590499cdd..2ff0f3299 100644 --- a/public/locales/en/settings.json +++ b/public/locales/en/settings.json @@ -97,5 +97,13 @@ "yes": "Yes", "your-feedback": "Describe your issue, including your Telegram handle if you have one, so that we can contact you with updates.", - "your-reference": "Your reference:
{{logRef}}" + "your-reference": "Your reference:
{{logRef}}", + "app-restart-required": "App restart required", + "setup-tor-settings": "Setup Tor Settings for Privacy Control and Connectivity", + "control-port": "Control Port", + "save": "Save", + "errors": { + "invalid-control-port": "Control Port configuration is invalid", + "invalid-bridge": "Bridge configuration is invalid" + } } diff --git a/public/locales/fr/settings.json b/public/locales/fr/settings.json index 79a4c6834..14c32c75e 100644 --- a/public/locales/fr/settings.json +++ b/public/locales/fr/settings.json @@ -71,5 +71,13 @@ "report-issue": "Signaler un problème", "importing-wallet": "Importation du portefeuille", "action-requires-restart": "Cette action nécessite un redémarrage pour appliquer les modifications", - "invalid-seed-words": "Entrez 24 mots séparés par des espaces" + "invalid-seed-words": "Entrez 24 mots séparés par des espaces", + "app-restart-required": "App restart required", + "setup-tor-settings": "Setup Tor Settings for Privacy Control and Connectivity", + "control-port": "Control Port", + "save": "Save", + "errors": { + "invalid-control-port": "Control Port configuration is invalid", + "invalid-bridge": "Bridge configuration is invalid" + } } \ No newline at end of file diff --git a/public/locales/hi/settings.json b/public/locales/hi/settings.json index 846b76390..25272bcbf 100644 --- a/public/locales/hi/settings.json +++ b/public/locales/hi/settings.json @@ -71,5 +71,13 @@ "action-requires-restart": "इस क्रिया के लिए परिवर्तनों को लागू करने हेतु पुनः आरंभ करना आवश्यक है", "low-hash-rate-warning": "आपकी हैश रेट बहुत कम है। Tari Universe के अलावा सभी ऐप्स को बंद करने का प्रयास करें", "invalid-seed-words": "24 शब्दों को स्पेस से अलग करके दर्ज करें", - "report-issue": "समस्या की रिपोर्ट करें" + "report-issue": "समस्या की रिपोर्ट करें", + "app-restart-required": "App restart required", + "setup-tor-settings": "Setup Tor Settings for Privacy Control and Connectivity", + "control-port": "Control Port", + "save": "Save", + "errors": { + "invalid-control-port": "Control Port configuration is invalid", + "invalid-bridge": "Bridge configuration is invalid" + } } \ No newline at end of file diff --git a/public/locales/id/settings.json b/public/locales/id/settings.json index ae51f6f1e..4a4a29c4e 100644 --- a/public/locales/id/settings.json +++ b/public/locales/id/settings.json @@ -71,5 +71,13 @@ "action-requires-restart": "Tindakan ini memerlukan restart untuk menerapkan perubahan", "low-hash-rate-warning": "Hashrate Anda sangat rendah. Coba tutup semua aplikasi selain Tari Universe", "invalid-seed-words": "Masukkan 24 kata yang dipisahkan oleh spasi", - "report-issue": "Laporkan masalah" + "report-issue": "Laporkan masalah", + "app-restart-required": "App restart required", + "setup-tor-settings": "Setup Tor Settings for Privacy Control and Connectivity", + "control-port": "Control Port", + "save": "Save", + "errors": { + "invalid-control-port": "Control Port configuration is invalid", + "invalid-bridge": "Bridge configuration is invalid" + } } \ No newline at end of file diff --git a/public/locales/ja/settings.json b/public/locales/ja/settings.json index ee49c7810..348de3ea2 100644 --- a/public/locales/ja/settings.json +++ b/public/locales/ja/settings.json @@ -71,5 +71,13 @@ "action-requires-restart": "この操作には再起動が必要です。変更を適用します", "low-hash-rate-warning": "ハッシュレートが非常に低いです。Tari Universe以外のすべてのアプリを閉じてみてください", "invalid-seed-words": "スペースで区切って24の単語を入力してください", - "report-issue": "問題を報告する" + "report-issue": "問題を報告する", + "app-restart-required": "App restart required", + "setup-tor-settings": "Setup Tor Settings for Privacy Control and Connectivity", + "control-port": "Control Port", + "save": "Save", + "errors": { + "invalid-control-port": "Control Port configuration is invalid", + "invalid-bridge": "Bridge configuration is invalid" + } } \ No newline at end of file diff --git a/public/locales/ko/settings.json b/public/locales/ko/settings.json index 255bf47d0..9c9b451ef 100644 --- a/public/locales/ko/settings.json +++ b/public/locales/ko/settings.json @@ -71,5 +71,13 @@ "action-requires-restart": "이 작업은 변경 사항을 적용하기 위해 재시작이 필요합니다", "low-hash-rate-warning": "해시레이트가 매우 낮습니다. Tari Universe 외의 모든 앱을 닫아보세요", "invalid-seed-words": "공백으로 구분된 24개의 단어를 입력하세요", - "report-issue": "문제 보고" + "report-issue": "문제 보고", + "app-restart-required": "App restart required", + "setup-tor-settings": "Setup Tor Settings for Privacy Control and Connectivity", + "control-port": "Control Port", + "save": "Save", + "errors": { + "invalid-control-port": "Control Port configuration is invalid", + "invalid-bridge": "Bridge configuration is invalid" + } } \ No newline at end of file diff --git a/public/locales/pl/settings.json b/public/locales/pl/settings.json index 0a5d47892..8ba465561 100644 --- a/public/locales/pl/settings.json +++ b/public/locales/pl/settings.json @@ -73,5 +73,13 @@ "action-requires-restart": "Ta akcja wymaga ponownego uruchomienia, aby wprowadzić zmiany", "low-hash-rate-warning": "Twój hash rate jest bardzo niski. Spróbuj zamknąć wszystkie aplikacje poza Tari Universe", "invalid-seed-words": "Wprowadź 24 słowa oddzielone spacjami", - "report-issue": "Zgłoś problem" + "report-issue": "Zgłoś problem", + "app-restart-required": "Wymagane ponowne uruchomienie", + "setup-tor-settings": "Skonfiguruj sieć Tor w celu prywatności połączenia", + "control-port": "Control Port", + "save": "Zapisz", + "errors": { + "invalid-control-port": "Konfiguracja Control Portu nieprawidłowa", + "invalid-bridge": "Konfiguracja Bridge nieprawidłowa" + } } \ No newline at end of file diff --git a/public/locales/ru/settings.json b/public/locales/ru/settings.json index 97241a6ab..ecff26821 100644 --- a/public/locales/ru/settings.json +++ b/public/locales/ru/settings.json @@ -71,5 +71,13 @@ "action-requires-restart": "Для применения изменений требуется перезапуск", "low-hash-rate-warning": "Ваш хешрейт очень низкий. Попробуйте закрыть все приложения, кроме Tari Universe", "invalid-seed-words": "Введите 24 слова, разделенные пробелами", - "report-issue": "Сообщить о проблеме" + "report-issue": "Сообщить о проблеме", + "app-restart-required": "App restart required", + "setup-tor-settings": "Setup Tor Settings for Privacy Control and Connectivity", + "control-port": "Control Port", + "save": "Save", + "errors": { + "invalid-control-port": "Control Port configuration is invalid", + "invalid-bridge": "Bridge configuration is invalid" + } } \ No newline at end of file diff --git a/public/locales/tr/settings.json b/public/locales/tr/settings.json index 25263f504..9a2c201d8 100644 --- a/public/locales/tr/settings.json +++ b/public/locales/tr/settings.json @@ -71,5 +71,13 @@ "action-requires-restart": "Bu işlem değişikliklerin uygulanması için yeniden başlatma gerektirir", "low-hash-rate-warning": "Hashrate\"iniz çok düşük. Tari Evreni dışındaki tüm uygulamaları kapatmayı deneyin", "invalid-seed-words": "Boşluklarla ayrılmış 24 kelime girin", - "report-issue": "Bir sorunu bildir" + "report-issue": "Bir sorunu bildir", + "app-restart-required": "App restart required", + "setup-tor-settings": "Setup Tor Settings for Privacy Control and Connectivity", + "control-port": "Control Port", + "save": "Save", + "errors": { + "invalid-control-port": "Control Port configuration is invalid", + "invalid-bridge": "Bridge configuration is invalid" + } } \ No newline at end of file diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 6ddbcbebd..e34cbc7c7 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -2413,6 +2413,12 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hex-literal" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebdb29d2ea9ed0083cd8cece49bbd968021bd99b0849edb4a9a7ee0fdf6a4e0" + [[package]] name = "hex-literal" version = "0.4.1" @@ -3613,7 +3619,7 @@ dependencies = [ "curve25519-dalek", "fixed-hash", "hex", - "hex-literal", + "hex-literal 0.4.1", "sealed", "serde", "thiserror", @@ -5775,12 +5781,9 @@ dependencies = [ [[package]] name = "sha1" -version = "0.6.1" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1da05c97445caa12d05e848c4a4fcbbea29e748ac28f7e80e9b010392063770" -dependencies = [ - "sha1_smol", -] +checksum = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d" [[package]] name = "sha1" @@ -5793,12 +5796,6 @@ dependencies = [ "digest", ] -[[package]] -name = "sha1_smol" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" - [[package]] name = "sha2" version = "0.10.8" @@ -6416,6 +6413,7 @@ dependencies = [ "thiserror", "tokio", "tokio-util 0.7.12", + "tor-hash-passwd", "winreg 0.52.0", "xz2", "zip 2.2.0", @@ -7475,6 +7473,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "tor-hash-passwd" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b83cd43a176c0c19d5db4401283e8f5c296b9c6c7fa29029de15cc445f26e12" +dependencies = [ + "hex", + "hex-literal 0.3.4", + "rand 0.8.5", + "sha1 0.6.0", + "thiserror", +] + [[package]] name = "tower" version = "0.4.13" @@ -8803,7 +8814,7 @@ dependencies = [ "rand 0.8.5", "serde", "serde_repr", - "sha1 0.6.1", + "sha1 0.6.0", "static_assertions", "tracing", "uds_windows", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 24ca9b7a9..5ff532d5e 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -83,6 +83,7 @@ tauri-plugin-single-instance = {git = "https://github.com/tauri-apps/plugins-wor thiserror = "1.0.26" tokio = {version = "1", features = ["full"]} tokio-util = {version = "0.7.11", features = ["compat"]} +tor-hash-passwd = "1.0.1" xz2 = {version = "0.1.7", features = ["static"]}# static bind lzma zip = "2.2.0" diff --git a/src-tauri/src/binaries/adapter_tor.rs b/src-tauri/src/binaries/adapter_tor.rs index 55bcaeb96..2b1c356a0 100644 --- a/src-tauri/src/binaries/adapter_tor.rs +++ b/src-tauri/src/binaries/adapter_tor.rs @@ -17,8 +17,11 @@ pub(crate) struct TorReleaseAdapter {} #[async_trait] impl LatestVersionApiAdapter for TorReleaseAdapter { async fn fetch_releases_list(&self) -> Result, Error> { - let cdn_tor_bundle_url = "https://cdn-universe.tari.com/torbrowser/13.5.7/tor-expert-bundle-windows-x86_64-13.5.7.tar.gz"; - + let platform = get_platform_name(); + let cdn_tor_bundle_url = format!( + "https://cdn-universe.tari.com/torbrowser/13.5.7/tor-expert-bundle-{}-13.5.7.tar.gz", + platform + ); let mut cdn_responded = false; let client = reqwest::Client::new(); @@ -38,7 +41,7 @@ impl LatestVersionApiAdapter for TorReleaseAdapter { version: "13.5.7".parse().expect("Bad tor version"), assets: vec![VersionAsset { url: cdn_tor_bundle_url.to_string(), - name: "tor-expert-bundle-windows-x86_64-13.5.7.tar.gz".to_string(), + name: format!("tor-expert-bundle-{}-13.5.7.tar.gz", platform), }], }; return Ok(vec![version]); @@ -48,9 +51,9 @@ impl LatestVersionApiAdapter for TorReleaseAdapter { let version = VersionDownloadInfo { version: "13.5.7".parse().expect("Bad tor version"), assets: vec![VersionAsset { - url: "https://archive.torproject.org/tor-package-archive/torbrowser/13.5.7/tor-expert-bundle-windows-x86_64-13.5.7.tar.gz".to_string(), - name: "tor-expert-bundle-windows-x86_64-13.5.7.tar.gz".to_string(), - }], + url: format!("https://dist.torproject.org/torbrowser/13.5.7/tor-expert-bundle-{}-13.5.7.tar.gz", platform), + name: format!("tor-expert-bundle-{}-13.5.7.tar.gz", platform), + }] }; Ok(vec![version]) } @@ -109,14 +112,14 @@ impl LatestVersionApiAdapter for TorReleaseAdapter { } if cfg!(target_os = "macos") && cfg!(target_arch = "x86_64") { - panic!("Unsupported OS"); + name_suffix = r"macos-x86_64.*\.gz"; } if cfg!(target_os = "macos") && cfg!(target_arch = "aarch64") { - panic!("Unsupported OS"); + name_suffix = r"macos-aarch64.*\.gz"; } if cfg!(target_os = "linux") { - panic!("Unsupported OS"); + name_suffix = r"linux-x86_64.*\.gz"; } if name_suffix.is_empty() { panic!("Unsupported OS"); @@ -136,3 +139,19 @@ impl LatestVersionApiAdapter for TorReleaseAdapter { Ok(platform.clone()) } } + +fn get_platform_name() -> String { + if cfg!(target_os = "windows") { + return "windows-x86_64".to_string(); + } + if cfg!(target_os = "macos") && cfg!(target_arch = "x86_64") { + return "macos-x86_64".to_string(); + } + if cfg!(target_os = "macos") && cfg!(target_arch = "aarch64") { + return "macos-aarch64".to_string(); + } + if cfg!(target_os = "linux") { + return "linux-x86_64".to_string(); + } + panic!("Unsupported OS"); +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index da2eda43a..d49d45f62 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -7,6 +7,7 @@ use external_dependencies::{ExternalDependencies, ExternalDependency, RequiredEx use futures_util::future::Join; use log::trace; use log::{debug, error, info, warn}; +use regex::Regex; use sentry::protocol::Event; use sentry_tauri::sentry; use serde::Serialize; @@ -22,6 +23,7 @@ use tari_shutdown::Shutdown; use tauri::async_runtime::{block_on, JoinHandle}; use tauri::{Manager, RunEvent, UpdaterEvent}; use tokio::sync::RwLock; +use tor_adapter::TorConfig; use wallet_adapter::TransactionInfo; use app_config::AppConfig; @@ -298,6 +300,40 @@ async fn set_airdrop_access_token( Ok(()) } +#[tauri::command] +async fn get_tor_config( + _window: tauri::Window, + state: tauri::State<'_, UniverseAppState>, + _app: tauri::AppHandle, +) -> Result { + let timer = Instant::now(); + let tor_config = state.tor_manager.get_tor_config().await; + if timer.elapsed() > MAX_ACCEPTABLE_COMMAND_TIME { + warn!(target: LOG_TARGET, "get_tor_config took too long: {:?}", timer.elapsed()); + } + Ok(tor_config) +} + +#[tauri::command] +async fn set_tor_config( + config: TorConfig, + _window: tauri::Window, + state: tauri::State<'_, UniverseAppState>, + _app: tauri::AppHandle, +) -> Result { + let timer = Instant::now(); + let tor_config = state + .tor_manager + .set_tor_config(config) + .await + .map_err(|e| e.to_string())?; + + if timer.elapsed() > MAX_ACCEPTABLE_COMMAND_TIME { + warn!(target: LOG_TARGET, "set_tor_config took too long: {:?}", timer.elapsed()); + } + Ok(tor_config) +} + #[tauri::command] async fn get_app_in_memory_config( _window: tauri::Window, @@ -575,7 +611,7 @@ async fn setup_inner( .unwrap_or(Duration::from_secs(0)) > Duration::from_secs(60 * 60 * 6); - if use_tor && cfg!(target_os = "windows") { + if use_tor { progress.set_max(5).await; progress .update("checking-latest-version-tor".to_string(), None, 0) @@ -672,7 +708,7 @@ async fn setup_inner( .await .inspect_err(|e| error!(target: LOG_TARGET, "Could not detect gpu miner: {:?}", e)); - if use_tor && cfg!(target_os = "windows") { + if use_tor { state .tor_manager .ensure_started( @@ -1137,6 +1173,28 @@ async fn stop_mining<'r>(state: tauri::State<'_, UniverseAppState>) -> Result<() Ok(()) } +#[tauri::command] +async fn fetch_tor_bridges() -> Result, String> { + let timer = Instant::now(); + let res_html = reqwest::get("https://bridges.torproject.org/bridges?transport=obfs4") + .await + .map_err(|e| e.to_string())? + .text() + .await + .map_err(|e| e.to_string())?; + + let re = Regex::new(r"obfs4.*?").unwrap(); + let bridges: Vec = re + .find_iter(&res_html) + .map(|m| m.as_str().trim_end_matches("
").to_string()) + .collect(); + info!(target: LOG_TARGET, "Fetched default bridges: {:?}", bridges); + if timer.elapsed() > MAX_ACCEPTABLE_COMMAND_TIME { + warn!(target: LOG_TARGET, "fetch_default_tor_bridges took too long: {:?}", timer.elapsed()); + } + Ok(bridges) +} + #[tauri::command] fn open_log_dir(app: tauri::AppHandle) { let log_dir = app @@ -1956,7 +2014,10 @@ fn main() { get_external_dependencies, set_use_tor, get_transaction_history, - import_seed_words + import_seed_words, + get_tor_config, + set_tor_config, + fetch_tor_bridges ]) .build(tauri::generate_context!()) .inspect_err( diff --git a/src-tauri/src/node_adapter.rs b/src-tauri/src/node_adapter.rs index 885072dfb..bd6fbe681 100644 --- a/src-tauri/src/node_adapter.rs +++ b/src-tauri/src/node_adapter.rs @@ -105,6 +105,8 @@ impl ProcessAdapter for MinotariNodeAdapter { // .to_string(), // ); args.push("-p".to_string()); + args.push("use_libtor=false".to_string()); + args.push("-p".to_string()); args.push(format!( "base_node.p2p.auxiliary_tcp_listener_address=/ip4/0.0.0.0/tcp/{0}", self.tcp_listener_port diff --git a/src-tauri/src/tor_adapter.rs b/src-tauri/src/tor_adapter.rs index 2b1864e08..6db176567 100644 --- a/src-tauri/src/tor_adapter.rs +++ b/src-tauri/src/tor_adapter.rs @@ -1,9 +1,12 @@ use std::path::PathBuf; -use anyhow::Error; +use anyhow::{anyhow, Error}; use async_trait::async_trait; -use log::info; +use log::{debug, info}; +use serde::{Deserialize, Serialize}; use tari_shutdown::Shutdown; +use tokio::fs; +use tor_hash_passwd::EncryptedKey; use crate::{ process_adapter::{ @@ -15,19 +18,106 @@ use crate::{ const LOG_TARGET: &str = "tari::universe::tor_adapter"; pub(crate) struct TorAdapter { - control_port: u16, socks_port: u16, + password: String, + config_file: Option, + config: TorConfig, } impl TorAdapter { pub fn new() -> Self { - let control_port = 9051; let socks_port = 9050; + let password = "tari is the best".to_string(); + Self { - control_port, socks_port, + password, + config_file: None, + config: TorConfig::default(), + } + } + + pub async fn load_or_create_config( + &mut self, + config_path: PathBuf, + ) -> Result<(), anyhow::Error> { + let file: PathBuf = config_path.join("tor_config.json"); + self.config_file = Some(file.clone()); + + if file.exists() { + debug!(target: LOG_TARGET, "Loading tor config from file: {:?}", file); + let config = fs::read_to_string(&file).await?; + self.apply_loaded_config(config); + } else { + info!(target: LOG_TARGET, "App config does not exist or is corrupt. Creating new one"); } + self.update_config_file().await?; + Ok(()) + } + + fn apply_loaded_config(&mut self, config: String) { + self.config = serde_json::from_str::(&config).unwrap_or(TorConfig::default()); } + + async fn update_config_file(&mut self) -> Result<(), anyhow::Error> { + let file = self + .config_file + .clone() + .ok_or_else(|| anyhow!("Tor config file not set"))?; + + let config = serde_json::to_string(&self.config)?; + debug!(target: LOG_TARGET, "Updating tor config file: {:?} {:?}", file, self.config.clone()); + fs::write(file, config).await?; + + Ok(()) + } + + pub fn get_tor_config(&self) -> TorConfig { + self.config.clone() + } + + pub async fn set_tor_config(&mut self, config: TorConfig) -> Result { + self.config = config.clone(); + + // match self.apply_tor_config_changes(config.clone()).await { + // Ok(_) => info!(target: LOG_TARGET, "Tor config changes applied successfully"), + // Err(e) => { + // warn!(target: LOG_TARGET, "Failed to apply Tor config changes: {:?}", e); + // return Err(e); + // } + // } + + self.update_config_file().await?; + Ok(config) + } + + // pub async fn apply_tor_config_changes(&self, config: TorConfig) -> Result<(), Error> { + // let mut setconf_commands: Vec = vec![]; + // let control_port_address = "127.0.0.1:9051"; + + // // Establish a TCP connection + // let mut stream = TcpStream::connect(control_port_address)?; + + // // Authenticate + // setconf_commands.push(format!("AUTHENTICATE \"{}\"\n", self.password.clone())); + + // setconf_commands.push("SETCONF".to_string()); + + // // Set Bridge instances + // if config.use_bridges { + // for bridge in config.bridges { + // setconf_commands.push(format!("Bridge=\"{}\"", bridge)) + // } + // } + // // Set UseBridges + // setconf_commands.push(format!("UseBridges={}", config.use_bridges as u8)); + // // Set ControlPort + // setconf_commands.push(format!("ControlPort=127.0.0.1:{}", config.control_port)); + + // stream.write_all(setconf_commands.join(" ").as_bytes())?; + + // Ok(()) + // } } impl ProcessAdapter for TorAdapter { @@ -49,23 +139,48 @@ impl ProcessAdapter for TorAdapter { let working_dir_string = convert_to_string(working_dir)?; let log_dir_string = convert_to_string(log_dir.join("tor.log"))?; + let mut lyrebird_path = binary_version_path.clone(); + lyrebird_path.pop(); + lyrebird_path.push("pluggable_transports"); + lyrebird_path.push("lyrebird"); + if cfg!(target_os = "windows") { + lyrebird_path.set_extension("exe"); + } - let args: Vec = vec![ + let mut args: Vec = vec![ "--allow-missing-torrc".to_string(), + "--ignore-missing-torrc".to_string(), "--clientonly".to_string(), "1".to_string(), "--socksport".to_string(), self.socks_port.to_string(), "--controlport".to_string(), - format!("127.0.0.1:{}", self.control_port), + format!("127.0.0.1:{}", self.config.control_port), + "--HashedControlPassword".to_string(), + EncryptedKey::hash_password(&self.password).to_string(), "--clientuseipv6".to_string(), "1".to_string(), "--DataDirectory".to_string(), working_dir_string, "--Log".to_string(), format!("notice file {}", log_dir_string), + // Used by tor bridges + // TODO: This does not work when path has space on windows. + // Consider running lyrebird binary manually + "--ClientTransportPlugin".to_string(), + format!("obfs4 exec {} managed", convert_to_string(lyrebird_path)?), ]; - info!(target: LOG_TARGET, "Starting tor with args: {:?}", args); + + if self.config.use_bridges { + for bridge in &self.config.bridges { + args.push("--Bridge".to_string()); + args.push(bridge.clone()); + } + + args.push("--UseBridges".to_string()); + args.push("1".to_string()); + } + Ok(( ProcessInstance { shutdown: inner_shutdown, @@ -102,3 +217,20 @@ impl StatusMonitor for TorStatusMonitor { HealthStatus::Healthy } } + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TorConfig { + control_port: u16, + use_bridges: bool, + bridges: Vec, +} + +impl Default for TorConfig { + fn default() -> Self { + TorConfig { + control_port: 9051, + use_bridges: false, + bridges: Vec::new(), + } + } +} diff --git a/src-tauri/src/tor_manager.rs b/src-tauri/src/tor_manager.rs index e0cbfb732..4a30dfa4e 100644 --- a/src-tauri/src/tor_manager.rs +++ b/src-tauri/src/tor_manager.rs @@ -2,7 +2,7 @@ use std::{path::PathBuf, sync::Arc}; use tokio::sync::RwLock; use crate::process_watcher::ProcessWatcher; -use crate::tor_adapter::TorAdapter; +use crate::tor_adapter::{TorAdapter, TorConfig}; use tari_shutdown::ShutdownSignal; pub(crate) struct TorManager { @@ -36,6 +36,10 @@ impl TorManager { ) -> Result<(), anyhow::Error> { { let mut process_watcher = self.watcher.write().await; + process_watcher + .adapter + .load_or_create_config(config_path.clone()) + .await?; process_watcher .start( app_shutdown, @@ -68,6 +72,19 @@ impl TorManager { Ok(()) } + pub async fn get_tor_config(&self) -> TorConfig { + self.watcher.read().await.adapter.get_tor_config() + } + + pub async fn set_tor_config(&self, config: TorConfig) -> Result { + self.watcher + .write() + .await + .adapter + .set_tor_config(config) + .await + } + pub async fn stop(&self) -> Result { let mut process_watcher = self.watcher.write().await; let exit_code = process_watcher.stop().await?; diff --git a/src/containers/Settings/ExperimentalSettings.tsx b/src/containers/Settings/ExperimentalSettings.tsx index 56823a47d..f8b424ba5 100644 --- a/src/containers/Settings/ExperimentalSettings.tsx +++ b/src/containers/Settings/ExperimentalSettings.tsx @@ -1,4 +1,3 @@ -import { useCallback } from 'react'; import { AnimatePresence } from 'framer-motion'; import { useUIStore } from '@app/store/useUIStore.ts'; import ExperimentalWarning from './sections/experimental/ExperimentalWarning.tsx'; @@ -8,23 +7,11 @@ import DebugSettings from '@app/containers/Settings/sections/experimental/DebugS import AppVersions from '@app/containers/Settings/sections/experimental/AppVersions.tsx'; import VisualMode from '@app/containers/Dashboard/components/VisualMode.tsx'; import { SettingsGroup, SettingsGroupWrapper } from '@app/containers/Settings/components/SettingsGroup.styles.ts'; -import { useAppConfigStore } from '@app/store/useAppConfigStore.ts'; -import { ToggleSwitch } from '@app/components/elements/ToggleSwitch.tsx'; -import { useTranslation } from 'react-i18next'; import GpuDevices from './sections/experimental/GpuDevices.tsx'; +import { TorMarkup } from './sections/experimental/TorMarkup'; export const ExperimentalSettings = () => { const showExperimental = useUIStore((s) => s.showExperimental); - const useTor = useAppConfigStore((s) => s.use_tor); - const setUseTor = useAppConfigStore((s) => s.setUseTor); - const setDialogToShow = useUIStore((s) => s.setDialogToShow); - const { t } = useTranslation('settings', { useSuspense: false }); - - const toggleUseTor = useCallback(() => { - setUseTor(!useTor).then(() => { - setDialogToShow('restart'); - }); - }, [setDialogToShow, setUseTor, useTor]); return ( <> @@ -37,18 +24,11 @@ export const ExperimentalSettings = () => { + - - - )} diff --git a/src/containers/Settings/sections/experimental/TorMarkup/TorMarkup.styles.ts b/src/containers/Settings/sections/experimental/TorMarkup/TorMarkup.styles.ts new file mode 100644 index 000000000..9c2866908 --- /dev/null +++ b/src/containers/Settings/sections/experimental/TorMarkup/TorMarkup.styles.ts @@ -0,0 +1,22 @@ +import { Input } from '@app/components/elements/inputs/Input'; +import { Typography } from '@app/components/elements/Typography'; +import styled from 'styled-components'; + +export const StyledInput = styled(Input)<{ hasError?: boolean }>(({ theme, hasError }) => ({ + borderColor: hasError ? theme.palette.error.main : theme.palette.colors.darkAlpha[10], + marginLeft: '15px', +})); + +export const ErrorTypography = styled(Typography)(({ theme }) => ({ + color: theme.palette.error.main, + marginLeft: '15px', + // Prevent jumping when the error message appears + minHeight: '14px', +})); + +export const SaveButtonWrapper = styled.div({ + marginLeft: '15px', + alignSelf: 'flex-end', + // Prevent jumping when save available + minHeight: '36px', +}); diff --git a/src/containers/Settings/sections/experimental/TorMarkup/TorMarkup.tsx b/src/containers/Settings/sections/experimental/TorMarkup/TorMarkup.tsx new file mode 100644 index 000000000..67338ddcd --- /dev/null +++ b/src/containers/Settings/sections/experimental/TorMarkup/TorMarkup.tsx @@ -0,0 +1,194 @@ +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useUIStore } from '@app/store/useUIStore.ts'; +import { + SettingsGroup, + SettingsGroupAction, + SettingsGroupContent, + SettingsGroupTitle, + SettingsGroupWrapper, +} from '@app/containers/Settings/components/SettingsGroup.styles.ts'; +import { useAppConfigStore } from '@app/store/useAppConfigStore.ts'; +import { ToggleSwitch } from '@app/components/elements/ToggleSwitch.tsx'; +import { useTranslation } from 'react-i18next'; +import { invoke } from '@tauri-apps/api/tauri'; +import { Typography } from '@app/components/elements/Typography'; +import { TorConfig } from '@app/types/app-status'; +import { Input } from '@app/components/elements/inputs/Input'; +import { Button } from '@app/components/elements/Button'; +import { ErrorTypography, StyledInput, SaveButtonWrapper } from './TorMarkup.styles'; + +interface EditedTorConfig { + // it's also string here to prevent an empty value + control_port: string | number; + use_bridges: boolean; + bridges: string[]; +} + +const hasBridgeError = (bridge: string) => { + // TODO: How should we validate the bridge? (IPv4, IPv6, different formats) + if (!bridge || bridge.trim().length === 0) return true; + return false; +}; + +const hasControlPortError = (cp: number) => { + if (!cp || cp <= 0) return true; + return false; +}; + +export const TorMarkup = () => { + const { t } = useTranslation('settings', { useSuspense: false }); + const setDialogToShow = useUIStore((s) => s.setDialogToShow); + const [defaultTorConfig, setDefaultTorConfig] = useState(); + const defaultUseTor = useAppConfigStore((s) => s.use_tor); + const setUseTor = useAppConfigStore((s) => s.setUseTor); + const [editedUseTor, setEditedUseTor] = useState(Boolean(defaultUseTor)); + + const [editedConfig, setEditedConfig] = useState(); + + useEffect(() => { + invoke('get_tor_config') + .then((torConfig: TorConfig) => { + setEditedConfig(torConfig); + setDefaultTorConfig(torConfig); + }) + .catch((e) => console.error(e)); + }, []); + + const onSave = useCallback(async () => { + if (editedUseTor !== defaultUseTor) { + await setUseTor(editedUseTor); + } + + if (editedConfig && JSON.stringify(defaultTorConfig) !== JSON.stringify(editedConfig)) { + try { + console.info('Updating Tor Config: ', { + ...editedConfig, + control_port: Number(editedConfig.control_port), + }); + const updatedConfig = await invoke('set_tor_config', { + config: { + ...editedConfig, + control_port: Number(editedConfig.control_port), + }, + }); + setDefaultTorConfig(updatedConfig); + } catch (error) { + console.error(error); + } + } + setDialogToShow('restart'); + }, [defaultTorConfig, defaultUseTor, editedConfig, editedUseTor, setDialogToShow, setUseTor]); + + const isSaveButtonVisible = useMemo(() => { + if (editedUseTor !== defaultUseTor) return true; + + if (JSON.stringify(defaultTorConfig) === JSON.stringify(editedConfig)) return false; + if ( + (editedConfig?.use_bridges && + (!editedConfig?.bridges?.length || editedConfig?.bridges.some((bridge) => hasBridgeError(bridge))) && + !editedConfig?.control_port) || + Number(editedConfig?.control_port) <= 0 + ) + return false; + return true; + }, [defaultTorConfig, defaultUseTor, editedConfig, editedUseTor]); + + const toggleUseBridges = useCallback(async () => { + const updated_use_bridges = !editedConfig?.use_bridges; + let bridges = editedConfig?.bridges || []; + if (updated_use_bridges && Number(bridges?.length) < 2) { + bridges = await invoke('fetch_tor_bridges'); + } + + setEditedConfig((prev) => ({ + ...(prev as TorConfig), + use_bridges: updated_use_bridges, + bridges, + })); + }, [editedConfig?.bridges, editedConfig?.use_bridges]); + + return ( + + + + + + Tor +  ({t('app-restart-required').toUpperCase()}) + + + {t('setup-tor-settings')} + + + setEditedUseTor((p) => !p)} /> + + + {editedUseTor && editedConfig && ( + + + + {t('control-port')} + + { + if (target.value && isNaN(+target.value)) return; + setEditedConfig((prev) => ({ + ...(prev as TorConfig), + control_port: target.value !== '' ? +target.value.trim() : '', + })); + }} + /> + + {hasControlPortError(+editedConfig.control_port) && t('errors.invalid-control-port')} + + + + {editedConfig.use_bridges && ( + <> + { + setEditedConfig((prev) => ({ + ...(prev as TorConfig), + bridges: [target.value.trim(), prev?.bridges[1] || ''], + })); + }} + /> + + {hasBridgeError(editedConfig.bridges[0]) && t('errors.invalid-bridge')} + + { + setEditedConfig((prev) => ({ + ...(prev as TorConfig), + bridges: [prev?.bridges[0] || '', e.target.value.trim()], + })); + }} + /> + + {hasBridgeError(editedConfig.bridges[1]) && t('errors.invalid-bridge')} + + + )} + + )} + + {isSaveButtonVisible && } + + + ); +}; diff --git a/src/containers/Settings/sections/experimental/TorMarkup/index.ts b/src/containers/Settings/sections/experimental/TorMarkup/index.ts new file mode 100644 index 000000000..dc52b748b --- /dev/null +++ b/src/containers/Settings/sections/experimental/TorMarkup/index.ts @@ -0,0 +1 @@ +export { TorMarkup } from './TorMarkup'; diff --git a/src/types/app-status.ts b/src/types/app-status.ts index 78a707aaa..2f7d3dd6d 100644 --- a/src/types/app-status.ts +++ b/src/types/app-status.ts @@ -1,6 +1,12 @@ import { Language } from '@app/i18initializer'; import { modeType } from '../store/types'; +export interface TorConfig { + control_port: number; + use_bridges: boolean; + bridges: string[]; +} + export interface AppConfig { config_version: number; config_file?: string; diff --git a/src/types/invoke.ts b/src/types/invoke.ts index 7c26aab6b..295cd1af2 100644 --- a/src/types/invoke.ts +++ b/src/types/invoke.ts @@ -6,6 +6,7 @@ import { MinerMetrics, P2poolStatsResult, TariWalletDetails, + TorConfig, TransactionInfo, } from './app-status'; import { Language } from '@app/i18initializer'; @@ -56,6 +57,9 @@ declare module '@tauri-apps/api/tauri' { function invoke(param: 'set_use_tor', payload: { useTor: boolean }): Promise; function invoke(param: 'get_transaction_history'): Promise; function invoke(param: 'import_seed_words', payload: { seedWords: string[] }): Promise; + function invoke(param: 'get_tor_config'): Promise; + function invoke(param: 'set_tor_config', payload: { config: TorConfig }): Promise; + function invoke(param: 'fetch_tor_bridges'): Promise; function invoke( param: 'log_web_message', payload: { level: 'log' | 'error' | 'warn' | 'info'; message: string }