From b54a573f617e734f8dad9f1fde7759cba0de52b4 Mon Sep 17 00:00:00 2001 From: ardocrat Date: Mon, 9 Jun 2025 12:27:36 +0300 Subject: [PATCH] tor: proxy settings --- src/gui/views/settings/tor.rs | 546 ++++++++++++++++++++++++++++++++++ src/http/client.rs | 52 ++-- src/tor/config.rs | 47 ++- src/tor/tor.rs | 264 +++++++++------- src/tor/types.rs | 57 +++- 5 files changed, 829 insertions(+), 137 deletions(-) create mode 100644 src/gui/views/settings/tor.rs diff --git a/src/gui/views/settings/tor.rs b/src/gui/views/settings/tor.rs new file mode 100644 index 00000000..853d4ec7 --- /dev/null +++ b/src/gui/views/settings/tor.rs @@ -0,0 +1,546 @@ +// Copyright 2025 The Grim Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use egui::{Align, Id, Layout, RichText, StrokeKind}; +use url::Url; + +use crate::gui::icons::{CLOUD_CHECK, NOTCHES, PENCIL, SCAN, TERMINAL}; +use crate::gui::platform::PlatformCallbacks; +use crate::gui::views::types::{ContentContainer, ModalPosition}; +use crate::gui::views::{CameraScanContent, FilePickContent, FilePickContentType, Modal, TextEdit, View}; +use crate::gui::Colors; +use crate::tor::{TorBridge, TorConfig, TorProxy}; + +/// Transport settings content. +pub struct TorSettingsContent { + /// Flag to check if settings were changed. + pub settings_changed: bool, + + /// Proxy URL input value for [`Modal`]. + proxy_url_edit: String, + /// Flag to check if entered proxy address was correct. + proxy_url_error: bool, + + /// Tor bridge binary path value for [`Modal`]. + bridge_bin_path_edit: String, + /// Button to pick binary file for bridge. + bridge_bin_pick_file: FilePickContent, + + /// Tor bridge connection line value for [`Modal`]. + bridge_conn_line_edit: String, + /// Bridge line QR code scanner [`Modal`] content. + bridge_qr_scan_content: Option, +} + +/// Identifier for proxy URL edit [`Modal`]. +const PROXY_URL_EDIT_MODAL: &'static str = "tor_proxy_edit_modal"; +/// Identifier for bridge binary path input [`Modal`]. +const BRIDGE_BIN_EDIT_MODAL: &'static str = "bridge_bin_edit_modal"; +/// Identifier for bridge connection line input [`Modal`]. +const BRIDGE_CONN_LINE_EDIT_MODAL: &'static str = "bridge_conn_line_edit_modal"; +/// Identifier for [`Modal`] to scan bridge line from QR code. +const SCAN_BRIDGE_CONN_LINE_MODAL: &'static str = "scan_bridge_conn_line_modal"; + +impl ContentContainer for TorSettingsContent { + fn modal_ids(&self) -> Vec<&'static str> { + vec![ + PROXY_URL_EDIT_MODAL, + BRIDGE_BIN_EDIT_MODAL, + BRIDGE_CONN_LINE_EDIT_MODAL, + SCAN_BRIDGE_CONN_LINE_MODAL + ] + } + + fn modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) { + match modal.id { + PROXY_URL_EDIT_MODAL => self.proxy_modal_ui(ui, cb), + BRIDGE_BIN_EDIT_MODAL => self.bridge_bin_edit_modal_ui(ui, cb), + BRIDGE_CONN_LINE_EDIT_MODAL => self.bridge_conn_line_edit_modal_ui(ui, cb), + SCAN_BRIDGE_CONN_LINE_MODAL => { + if let Some(content) = self.bridge_qr_scan_content.as_mut() { + let mut close = false; + content.modal_ui(ui, cb, |res| { + let line = res.text(); + // Save connection line after scanning. + let bridge = TorConfig::get_bridge().unwrap(); + TorBridge::save_bridge_conn_line(&bridge, line); + self.settings_changed = true; + close = true; + }); + if close { + self.bridge_qr_scan_content = None; + cb.stop_camera(); + Modal::close(); + } + } + } + _ => {} + } + } + + fn container_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { + ui.label(RichText::new(format!("{}:", t!("wallets.conn_method"))) + .size(17.0) + .color(Colors::inactive_text())); + ui.add_space(10.0); + + let mut proxy = TorConfig::get_proxy(); + let current_proxy = proxy.clone(); + ui.columns(2, |columns| { + columns[0].vertical_centered(|ui| { + let name = t!("network_settings.default"); + View::radio_value(ui, &mut proxy, None, name); + }); + columns[1].vertical_centered(|ui| { + let name = t!("app_settings.proxy"); + let val = current_proxy.clone() + .unwrap_or(TorProxy::SOCKS5(TorProxy::DEFAULT_SOCKS5_URL.to_string())); + View::radio_value(ui, &mut proxy, Some(val), name); + }); + }); + ui.add_space(14.0); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(6.0); + + if let Some(p) = proxy.as_mut() { + ui.label(RichText::new(format!("{}:", t!("app_settings.proxy"))) + .size(17.0) + .color(Colors::inactive_text())); + ui.add_space(10.0); + ui.columns(2, |columns| { + columns[0].vertical_centered(|ui| { + let value = TorConfig::get_socks5_proxy(); + View::radio_value(ui, p, value, "SOCKS5".to_string()); + }); + columns[1].vertical_centered(|ui| { + let value = TorConfig::get_http_proxy(); + View::radio_value(ui, p, value, "HTTP".to_string()); + }); + }); + ui.add_space(14.0); + // Show proxy settings. + self.proxy_item_ui(p.url(), ui); + ui.add_space(8.0); + } + + // Check if proxy type was changed to save. + if current_proxy != proxy { + TorConfig::save_proxy(proxy.clone()); + self.settings_changed = true; + } + if proxy.is_some() { + return; + } + + let bridge = TorConfig::get_bridge(); + ui.vertical_centered(|ui| { + ui.label(RichText::new(t!("transport.bridges_desc")) + .size(17.0) + .color(Colors::inactive_text())); + + // Draw checkbox to enable/disable bridges. + View::checkbox(ui, bridge.is_some(), t!("transport.bridges"), || { + // Save value. + let value = if bridge.is_some() { + None + } else { + let default_bridge = TorConfig::get_obfs4(); + self.bridge_bin_path_edit = default_bridge.binary_path(); + self.bridge_conn_line_edit = default_bridge.connection_line(); + Some(default_bridge) + }; + TorConfig::save_bridge(value); + self.settings_changed = true; + }); + }); + + // Draw bridges selection and path. + if bridge.is_some() { + let current_bridge = bridge.unwrap(); + let mut bridge = current_bridge.clone(); + + ui.add_space(6.0); + ui.columns(2, |columns| { + columns[0].vertical_centered(|ui| { + // Show Obfs4 bridge selector. + let obfs4 = TorConfig::get_obfs4(); + let name = obfs4.protocol_name().to_uppercase(); + View::radio_value(ui, &mut bridge, obfs4, name); + }); + columns[1].vertical_centered(|ui| { + // Show Snowflake bridge selector. + let snowflake = TorConfig::get_snowflake(); + let name = snowflake.protocol_name().to_uppercase(); + View::radio_value(ui, &mut bridge, snowflake, name); + }); + }); + ui.add_space(14.0); + + // Check if bridge type was changed to save. + if current_bridge != bridge { + TorConfig::save_bridge(Some(bridge.clone())); + self.bridge_bin_path_edit = bridge.binary_path(); + self.bridge_conn_line_edit = bridge.connection_line(); + self.settings_changed = true; + } + + if let Some(br) = TorConfig::get_bridge().as_ref() { + // Show bridge binary setup. + self.bridge_bin_ui(ui, br, cb); + + ui.add_space(10.0); + + // Show bridge connection line setup. + self.bridge_conn_line_ui(ui, br, cb); + } + + ui.add_space(8.0); + } + } +} + +impl Default for TorSettingsContent { + fn default() -> Self { + // Setup Tor bridge binary path edit text. + let bridge = TorConfig::get_bridge(); + let (bin_path, conn_line) = if let Some(b) = bridge { + (b.binary_path(), b.connection_line()) + } else { + ("".to_string(), "".to_string()) + }; + Self { + settings_changed: false, + proxy_url_edit: "".to_string(), + proxy_url_error: false, + bridge_bin_path_edit: bin_path, + bridge_bin_pick_file: FilePickContent::new( + FilePickContentType::ItemButton(View::item_rounding(0, 1, true)) + ).no_parse(), + bridge_conn_line_edit: conn_line, + bridge_qr_scan_content: None, + } + } +} + +impl TorSettingsContent { + /// Draw proxy edit modal content. + fn proxy_modal_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { + let on_save = |c: &mut TorSettingsContent| { + let http = "http://"; + let socks = "socks5://"; + let url = c.proxy_url_edit.trim().to_string(); + c.proxy_url_error = Url::parse(url.as_str()).is_err(); + if !c.proxy_url_error { + let proxy = TorConfig::get_proxy().unwrap(); + if url.contains(socks) { + TorConfig::save_proxy(Some(TorProxy::SOCKS5(url))); + } else if url.contains(http) { + TorConfig::save_proxy(Some(TorProxy::HTTP(url))); + } else { + match proxy { + TorProxy::SOCKS5(_) => { + TorConfig::save_proxy(Some(TorProxy::SOCKS5(url))); + } + TorProxy::HTTP(_) => { + TorConfig::save_proxy(Some(TorProxy::HTTP(url))); + } + } + } + Modal::close(); + } + }; + + ui.add_space(6.0); + ui.vertical_centered(|ui| { + let label = format!("{}:", t!("enter_url")); + ui.label(RichText::new(label).size(17.0).color(Colors::gray())); + ui.add_space(8.0); + + // Draw proxy URL text edit. + let mut edit = TextEdit::new(Id::from("proxy_url_edit").with(PROXY_URL_EDIT_MODAL)) + .paste(); + edit.ui(ui, &mut self.proxy_url_edit, cb); + if edit.enter_pressed { + on_save(self); + } + + // Show error when specified address is incorrect. + if self.proxy_url_error { + ui.add_space(10.0); + ui.label(RichText::new(t!("wallets.invalid_url")) + .size(16.0) + .color(Colors::red())); + } + ui.add_space(12.0); + + // Show modal buttons. + ui.scope(|ui| { + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { + Modal::close(); + }); + }); + columns[1].vertical_centered_justified(|ui| { + View::button(ui, t!("modal.save"), Colors::white_or_black(false), || { + on_save(self); + }); + }); + }); + ui.add_space(6.0); + }); + }); + } + + /// Draw proxy item content. + fn proxy_item_ui(&mut self, url: String, ui: &mut egui::Ui) { + // Setup layout size. + let mut rect = ui.available_rect_before_wrap(); + rect.set_height(56.0); + + // Draw round background. + let bg_rect = rect.clone(); + let item_rounding = View::item_rounding(0, 1, false); + ui.painter().rect(bg_rect, + item_rounding, + Colors::fill(), + View::item_stroke(), + StrokeKind::Middle); + + ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { + View::item_button(ui, View::item_rounding(0, 1, true), PENCIL, None, || { + self.proxy_url_edit = url.clone(); + // Show proxy URL edit modal. + Modal::new(PROXY_URL_EDIT_MODAL) + .position(ModalPosition::CenterTop) + .title(t!("app_settings.proxy")) + .show(); + }); + let layout_size = ui.available_size(); + ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { + ui.add_space(12.0); + ui.vertical(|ui| { + ui.add_space(4.0); + View::ellipsize_text(ui, url, 18.0, Colors::title(false)); + ui.add_space(1.0); + + let value = format!("{} {}", CLOUD_CHECK, t!("network_settings.enabled")); + ui.label(RichText::new(value).size(15.0).color(Colors::gray())); + ui.add_space(3.0); + }); + }); + }); + } + + /// Draw bridge binary setup content. + fn bridge_bin_ui(&mut self, ui: &mut egui::Ui, bridge: &TorBridge, cb: &dyn PlatformCallbacks) { + // Setup layout size. + let mut rect = ui.available_rect_before_wrap(); + rect.set_height(56.0); + + // Draw round background. + let bg_rect = rect.clone(); + let item_rounding = View::item_rounding(0, 1, false); + ui.painter().rect(bg_rect, + item_rounding, + Colors::fill(), + View::item_stroke(), + StrokeKind::Middle); + + ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { + self.bridge_bin_pick_file.ui(ui, cb, |path| { + TorBridge::save_bridge_bin_path(bridge, path); + self.settings_changed = true; + }); + View::item_button(ui, View::item_rounding(1, 3, true), PENCIL, None, || { + self.bridge_bin_path_edit = bridge.binary_path(); + // Show binary path edit modal. + let title = bridge.protocol_name(); + Modal::new(BRIDGE_BIN_EDIT_MODAL) + .position(ModalPosition::CenterTop) + .title(title) + .show(); + }); + let layout_size = ui.available_size(); + ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { + ui.add_space(12.0); + ui.vertical(|ui| { + ui.add_space(4.0); + View::ellipsize_text(ui, bridge.binary_path(), 18.0, Colors::title(false)); + ui.add_space(1.0); + let value = format!("{} {}", + TERMINAL, + t!("transport.bin_file").replace(":", "")); + ui.label(RichText::new(value).size(15.0).color(Colors::gray())); + ui.add_space(3.0); + }); + }); + }); + } + + /// Draw bridge binary input [`Modal`] content. + fn bridge_bin_edit_modal_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { + let on_save = |c: &mut TorSettingsContent| { + let bridge = TorConfig::get_bridge().unwrap(); + TorBridge::save_bridge_bin_path(&bridge, c.bridge_bin_path_edit.clone()); + Modal::close(); + }; + + ui.add_space(6.0); + ui.vertical_centered(|ui| { + ui.label(RichText::new(t!("transport.bin_file")) + .size(17.0) + .color(Colors::gray())); + ui.add_space(8.0); + + // Draw p2p port text edit. + let mut edit = TextEdit::new(Id::from(BRIDGE_BIN_EDIT_MODAL)).paste(); + edit.ui(ui, &mut self.bridge_bin_path_edit, cb); + if edit.enter_pressed { + on_save(self); + } + ui.add_space(12.0); + + // Show modal buttons. + ui.scope(|ui| { + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { + // Close modal. + Modal::close(); + }); + }); + columns[1].vertical_centered_justified(|ui| { + View::button(ui, t!("modal.save"), Colors::white_or_black(false), || { + on_save(self); + }); + }); + }); + ui.add_space(6.0); + }); + }); + } + + /// Draw bridge connection line setup content. + fn bridge_conn_line_ui(&mut self, + ui: &mut egui::Ui, + bridge: &TorBridge, + cb: &dyn PlatformCallbacks) { + // Setup layout size. + let mut rect = ui.available_rect_before_wrap(); + rect.set_height(56.0); + + // Draw round background. + let bg_rect = rect.clone(); + let item_rounding = View::item_rounding(0, 1, false); + ui.painter().rect(bg_rect, + item_rounding, + Colors::fill(), + View::item_stroke(), + StrokeKind::Middle); + + ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { + View::item_button(ui, View::item_rounding(0, 1, true), SCAN, None, || { + self.show_qr_scan_bridge_modal(cb); + }); + View::item_button(ui, View::item_rounding(1, 3 , true), PENCIL, None, || { + self.bridge_conn_line_edit = bridge.connection_line(); + // Show connection line edit modal. + let title = bridge.protocol_name(); + Modal::new(BRIDGE_CONN_LINE_EDIT_MODAL) + .position(ModalPosition::CenterTop) + .title(title) + .show(); + }); + let layout_size = ui.available_size(); + ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { + ui.add_space(12.0); + ui.vertical(|ui| { + ui.add_space(4.0); + View::ellipsize_text(ui, bridge.connection_line(), 18.0, Colors::title(false)); + ui.add_space(1.0); + let value = format!("{} {}", + NOTCHES, + t!("transport.conn_line").replace(":", "")); + ui.label(RichText::new(value).size(15.0).color(Colors::gray())); + ui.add_space(3.0); + }); + }); + }); + } + + /// Show bridge connection line QR code scanner. + fn show_qr_scan_bridge_modal(&mut self, cb: &dyn PlatformCallbacks) { + self.bridge_qr_scan_content = Some(CameraScanContent::default()); + // Show QR code scan modal. + Modal::new(SCAN_BRIDGE_CONN_LINE_MODAL) + .position(ModalPosition::CenterTop) + .title(t!("scan_qr")) + .closeable(false) + .show(); + cb.start_camera(); + } + + /// Draw bridge connection line input [`Modal`] content. + fn bridge_conn_line_edit_modal_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { + let on_save = |c: &mut TorSettingsContent| { + let bridge = TorConfig::get_bridge().unwrap(); + TorBridge::save_bridge_conn_line(&bridge, c.bridge_conn_line_edit.clone()); + Modal::close(); + }; + + ui.add_space(6.0); + ui.vertical_centered(|ui| { + ui.label(RichText::new(t!("transport.conn_line")) + .size(17.0) + .color(Colors::gray())); + ui.add_space(8.0); + + // Draw connection line text edit. + let mut edit = TextEdit::new(Id::from(BRIDGE_CONN_LINE_EDIT_MODAL)).paste(); + edit.ui(ui, &mut self.bridge_conn_line_edit, cb); + if edit.enter_pressed { + on_save(self); + } + ui.add_space(12.0); + + // Show modal buttons. + ui.scope(|ui| { + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { + // Close modal. + Modal::close(); + }); + }); + columns[1].vertical_centered_justified(|ui| { + View::button(ui, t!("modal.save"), Colors::white_or_black(false), || { + on_save(self); + }); + }); + }); + ui.add_space(6.0); + }); + }); + } +} \ No newline at end of file diff --git a/src/http/client.rs b/src/http/client.rs index 0b4b949b..efca43ad 100644 --- a/src/http/client.rs +++ b/src/http/client.rs @@ -30,33 +30,43 @@ pub struct HttpClient { impl HttpClient { /// Send request. pub async fn send(req: Request>) -> Result, Error> { - let res = if AppConfig::use_proxy() { + if AppConfig::use_proxy() { if let Some(url) = AppConfig::socks_proxy_url() { - let connector = HttpsConnector::new(); - let uri = url.parse().unwrap(); - let proxy = hyper_socks2::SocksConnector { - proxy_addr: uri, - auth: None, - connector, - }.with_tls().unwrap(); - let client = Client::builder(TokioExecutor::new()) - .build::<_, Full>(proxy); - client.request(req).await + Self::send_socks_proxy(url, req).await } else { - let url = AppConfig::http_proxy_url().unwrap(); - let uri = url.parse().unwrap(); - let proxy = Proxy::new(Intercept::All, uri); - let connector = HttpsConnector::new(); - let proxy_connector = ProxyConnector::from_proxy(connector, proxy).unwrap(); - let client = Client::builder(TokioExecutor::new()) - .build::<_, Full>(proxy_connector); - client.request(req).await + Self::send_http_proxy(AppConfig::http_proxy_url().unwrap(), req).await } } else { let client = Client::builder(TokioExecutor::new()) .build::<_, Full>(HttpsConnector::new()); client.request(req).await - }; - res + } + } + + /// Create socks proxy client. + pub async fn send_socks_proxy(proxy_url: String, req: Request>) + -> Result, Error> { + let connector = HttpsConnector::new(); + let uri = proxy_url.parse().unwrap(); + let proxy = hyper_socks2::SocksConnector { + proxy_addr: uri, + auth: None, + connector, + }.with_tls().unwrap(); + let client = Client::builder(TokioExecutor::new()) + .build::<_, Full>(proxy); + client.request(req).await + } + + /// Create http proxy client. + pub async fn send_http_proxy(proxy_url: String, req: Request>) + -> Result, Error> { + let uri = proxy_url.parse().unwrap(); + let proxy = Proxy::new(Intercept::All, uri); + let connector = HttpsConnector::new(); + let proxy_connector = ProxyConnector::from_proxy(connector, proxy).unwrap(); + let client = Client::builder(TokioExecutor::new()) + .build::<_, Full>(proxy_connector); + client.request(req).await } } \ No newline at end of file diff --git a/src/tor/config.rs b/src/tor/config.rs index 9b03117e..4feb4d4d 100644 --- a/src/tor/config.rs +++ b/src/tor/config.rs @@ -16,11 +16,18 @@ use std::path::PathBuf; use serde_derive::{Deserialize, Serialize}; use crate::Settings; -use crate::tor::TorBridge; +use crate::tor::{TorBridge, TorProxy}; /// Tor configuration. #[derive(Serialize, Deserialize, Clone)] pub struct TorConfig { + /// Proxy for tor connections. + proxy: Option, + /// SOCKS5 proxy type. + proxy_socks5: TorProxy, + /// HTTP proxy type. + proxy_http: TorProxy, + /// Selected bridge type. bridge: Option, /// Obfs4 bridge type. @@ -32,6 +39,9 @@ pub struct TorConfig { impl Default for TorConfig { fn default() -> Self { Self { + proxy: None, + proxy_socks5: TorProxy::HTTP(TorProxy::DEFAULT_SOCKS5_URL.to_string()), + proxy_http: TorProxy::HTTP(TorProxy::DEFAULT_HTTP_URL.to_string()), bridge: None, obfs4: TorBridge::Obfs4( TorBridge::DEFAULT_OBFS4_BIN_PATH.to_string(), @@ -123,4 +133,39 @@ impl TorConfig { let r_config = Settings::tor_config_to_read(); r_config.snowflake.clone() } + + /// Save proxy for Tor connections. + pub fn save_proxy(proxy: Option) { + let mut w_config = Settings::tor_config_to_update(); + w_config.proxy = proxy.clone(); + if let Some(p) = proxy { + match p { + TorProxy::SOCKS5(_) => { + w_config.proxy_socks5 = p + } + TorProxy::HTTP(_) => { + w_config.proxy_http = p + } + } + } + w_config.save(); + } + + /// Get used proxy for Tor connections. + pub fn get_proxy() -> Option { + let r_config = Settings::tor_config_to_read(); + r_config.proxy.clone() + } + + /// Get saved SOCKS5 proxy. + pub fn get_socks5_proxy() -> TorProxy { + let r_config = Settings::tor_config_to_read(); + r_config.proxy_socks5.clone() + } + + /// Get saved HTTP proxy. + pub fn get_http_proxy() -> TorProxy { + let r_config = Settings::tor_config_to_read(); + r_config.proxy_http.clone() + } } \ No newline at end of file diff --git a/src/tor/tor.rs b/src/tor/tor.rs index 3ecaf5b3..99fbbbe4 100644 --- a/src/tor/tor.rs +++ b/src/tor/tor.rs @@ -13,25 +13,25 @@ // limitations under the License. use arti_client::config::pt::TransportConfigBuilder; -use futures::task::SpawnExt; -use lazy_static::lazy_static; -use std::collections::{BTreeMap, BTreeSet}; -use std::net::{IpAddr, Ipv4Addr, SocketAddr}; -use std::str::FromStr; -use std::sync::Arc; -use std::{fs, thread}; -use std::time::Duration; - use arti_client::config::{CfgPath, TorClientConfigBuilder}; use arti_client::{TorClient, TorClientConfig}; use curve25519_dalek::digest::Digest; use ed25519_dalek::hazmat::ExpandedSecretKey; use fs_mistrust::Mistrust; +use futures::task::SpawnExt; use grin_util::secp::SecretKey; +use http_body_util::BodyExt; +use lazy_static::lazy_static; use parking_lot::RwLock; use sha2::Sha512; -use tls_api_native_tls::TlsConnector; +use std::collections::{BTreeMap, BTreeSet}; +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use std::str::FromStr; +use std::sync::Arc; +use std::time::Duration; +use std::{fs, thread}; use tls_api::{TlsConnector as TlsConnectorTrait, TlsConnectorBuilder}; +use tls_api_native_tls::TlsConnector; use tokio::time::sleep; use tor_hscrypto::pk::{HsIdKey, HsIdKeypair}; use tor_hsrproxy::config::{ @@ -47,8 +47,9 @@ use tor_llcrypto::pk::ed25519::ExpandedKeypair; use tor_rtcompat::tokio::TokioNativeTlsRuntime; use tor_rtcompat::Runtime; +use crate::http::HttpClient; use crate::tor::http::ArtiHttpConnector; -use crate::tor::TorConfig; +use crate::tor::{TorConfig, TorProxy}; lazy_static! { /// Static thread-aware state of [`Node`] to be updated from separate thread. @@ -81,7 +82,8 @@ impl Default for Tor { let config = Self::build_config(); let client = TorClient::with_runtime(runtime) .config(config.clone()) - .create_unbootstrapped().unwrap(); + .create_unbootstrapped() + .unwrap(); Self { running_services: Arc::new(RwLock::new(BTreeMap::new())), starting_services: Arc::new(RwLock::new(BTreeSet::new())), @@ -120,37 +122,67 @@ impl Tor { pub fn rebuild_client() { let config = Self::build_config(); let r_client = TOR_SERVER_STATE.client_config.read(); - r_client - .0 + r_client.0 .reconfigure(&config, tor_config::Reconfigure::AllOrNothing) .unwrap(); } /// Send post request using Tor. pub async fn post(body: String, url: String) -> Option { - // Bootstrap client. - let (client, _) = Self::client_config(); - client.bootstrap().await.unwrap(); - // Create http tor-powered client to post data. - let tls_connector = TlsConnector::builder().unwrap().build().unwrap(); - let tor_connector = ArtiHttpConnector::new(client, tls_connector); - let http = hyper_tor::Client::builder().build::<_, hyper_tor::Body>(tor_connector); - // Create request. - let req = hyper_tor::Request::builder() - .method(hyper_tor::Method::POST) - .uri(url) - .body(hyper_tor::Body::from(body)) - .unwrap(); - // Send request. - let mut resp = None; - match http.request(req).await { - Ok(r) => match hyper_tor::body::to_bytes(r).await { - Ok(raw) => resp = Some(String::from_utf8_lossy(&raw).to_string()), + if let Some(proxy) = TorConfig::get_proxy() { + let req = hyper::Request::builder() + .method(hyper::Method::POST) + .uri(url) + .body(http_body_util::Full::from(body)) + .unwrap(); + let res = match proxy { + TorProxy::SOCKS5(url) => { + HttpClient::send_socks_proxy(url, req).await + } + TorProxy::HTTP(url) => { + HttpClient::send_http_proxy(url, req).await + } + }; + match res { + Ok(res) => { + let body = res.into_body().collect().await.unwrap().to_bytes().into(); + Some(String::from_utf8(body).unwrap()) + } + Err(_) => { + None + } + } + } else { + if let Some(b) = TorConfig::get_bridge() { + if !fs::exists(b.binary_path()).unwrap() { + return None; + } + } + // Bootstrap client. + let (client, _) = Self::client_config(); + let client = client.isolated_client(); + client.bootstrap().await.unwrap(); + // Create http tor-powered client to post data. + let tls_connector = TlsConnector::builder().unwrap().build().unwrap(); + let tor_connector = ArtiHttpConnector::new(client, tls_connector); + let http = hyper_tor::Client::builder().build::<_, hyper_tor::Body>(tor_connector); + // Create request. + let req = hyper_tor::Request::builder() + .method(hyper_tor::Method::POST) + .uri(url) + .body(hyper_tor::Body::from(body)) + .unwrap(); + // Send request. + let mut resp = None; + match http.request(req).await { + Ok(r) => match hyper_tor::body::to_bytes(r).await { + Ok(raw) => resp = Some(String::from_utf8_lossy(&raw).to_string()), + Err(_) => {} + }, Err(_) => {} - }, - Err(_) => {} + } + resp } - resp } fn client_config() -> (TorClient, TorClientConfig) { @@ -253,86 +285,11 @@ impl Tor { service.clone(), request, hs_nickname.clone(), - )) - .await - .unwrap(); - - // Check service availability if not checking. - if Self::is_service_checking(&service_id) { - return; - } - let client_check = client_thread.clone(); + )).await.unwrap(); + // Check service availability. let addr = service.onion_address().unwrap().to_string(); let url = format!("http://{}/", addr); - thread::spawn(move || { - // Wait 1 second to start. - thread::sleep(Duration::from_millis(1000)); - let runtime = client_thread.runtime(); - // Put service to checking. - { - let mut w_services = TOR_SERVER_STATE.checking_services.write(); - w_services.insert(service_id.clone()); - } - runtime - .spawn(async move { - let tls_conn = - TlsConnector::builder().unwrap().build().unwrap(); - let tor_conn = - ArtiHttpConnector::new(client_check.clone(), tls_conn); - let http = - hyper_tor::Client::builder().build::<_, hyper_tor::Body>(tor_conn); - - const MAX_ERRORS: i32 = 3; - let mut errors_count = 0; - loop { - if !Self::is_service_running(&service_id) { - // Remove service from checking. - let mut w_services = - TOR_SERVER_STATE.checking_services.write(); - w_services.remove(&service_id); - break; - } - // Send request. - let duration = match http - .get(hyper_tor::Uri::from_str(url.clone().as_str()).unwrap()) - .await - { - Ok(_) => { - // Remove service from starting. - let mut w_services = - TOR_SERVER_STATE.starting_services.write(); - w_services.remove(&service_id); - // Remove service from failed. - let mut w_services = - TOR_SERVER_STATE.failed_services.write(); - w_services.remove(&service_id); - // Check again after 50 seconds. - Duration::from_millis(50000) - } - Err(_) => { - // Restart service on 3rd error. - errors_count += 1; - if errors_count == MAX_ERRORS { - errors_count = 0; - let key = key.clone(); - let service_id = service_id.clone(); - thread::spawn(move || { - Self::restart_service( - port, - key, - &service_id, - ); - }); - } - Duration::from_millis(5000) - } - }; - // Wait to check service again. - sleep(duration).await; - } - }) - .unwrap(); - }); + Self::check_service(service_id, client_thread, url, port, key); return; } on_error(service_id); @@ -341,6 +298,87 @@ impl Tor { }); } + /// Check service availability. + fn check_service(service_id: String, + client: TorClient, + url: String, + port: u16, + key: SecretKey) { + if Self::is_service_checking(&service_id) { + return; + } + let client_check = client.clone(); + thread::spawn(move || { + // Wait 1 second to start. + thread::sleep(Duration::from_millis(1000)); + let runtime = client.runtime(); + // Put service to checking. + { + let mut w_services = TOR_SERVER_STATE.checking_services.write(); + w_services.insert(service_id.clone()); + } + runtime + .spawn(async move { + let tls_conn = + TlsConnector::builder().unwrap().build().unwrap(); + let tor_conn = + ArtiHttpConnector::new(client_check.clone(), tls_conn); + let http = + hyper_tor::Client::builder().build::<_, hyper_tor::Body>(tor_conn); + + const MAX_ERRORS: i32 = 3; + let mut errors_count = 0; + loop { + if !Self::is_service_running(&service_id) { + // Remove service from checking. + let mut w_services = + TOR_SERVER_STATE.checking_services.write(); + w_services.remove(&service_id); + break; + } + // Send request. + let duration = match http + .get(hyper_tor::Uri::from_str(url.clone().as_str()).unwrap()) + .await + { + Ok(_) => { + // Remove service from starting. + let mut w_services = + TOR_SERVER_STATE.starting_services.write(); + w_services.remove(&service_id); + // Remove service from failed. + let mut w_services = + TOR_SERVER_STATE.failed_services.write(); + w_services.remove(&service_id); + // Check again after 50 seconds. + Duration::from_millis(50000) + } + Err(_) => { + // Restart service on 3rd error. + errors_count += 1; + if errors_count == MAX_ERRORS { + errors_count = 0; + let key = key.clone(); + let service_id = service_id.clone(); + thread::spawn(move || { + Self::restart_service( + port, + key, + &service_id, + ); + }); + } + Duration::from_millis(5000) + } + }; + // Wait to check service again. + sleep(duration).await; + } + }) + .unwrap(); + }); + } + /// Launch Onion service proxy. async fn run_service_proxy( addr: SocketAddr, diff --git a/src/tor/types.rs b/src/tor/types.rs index 3cc90a96..4b859878 100644 --- a/src/tor/types.rs +++ b/src/tor/types.rs @@ -13,13 +13,38 @@ // limitations under the License. use serde_derive::{Deserialize, Serialize}; +use crate::tor::TorConfig; + +/// Tor connection proxy type. +#[derive(Serialize, Deserialize, Clone, PartialEq)] +pub enum TorProxy { + /// SOCKS5 proxy URL. + SOCKS5(String), + /// HTTP proxy URL. + HTTP(String) +} + +impl TorProxy { + /// Default SOCKS5 proxy URL. + pub const DEFAULT_SOCKS5_URL: &'static str = "socks5://127.0.0.1:9050"; + /// Default HTTP proxy URL. + pub const DEFAULT_HTTP_URL: &'static str = "http://127.0.0.1:9050"; + + /// Get proxy URL. + pub fn url(&self) -> String { + match self { + TorProxy::SOCKS5(url) => url.into(), + TorProxy::HTTP(url) => url.into() + } + } +} /// Tor network bridge type. #[derive(Serialize, Deserialize, Clone, PartialEq)] pub enum TorBridge { - /// Obfs4 bridge with connection line and binary path. + /// Obfs4 bridge with binary path and connection line. Obfs4(String, String), - /// Snowflake bridge with connection line and binary path. + /// Snowflake bridge with binary path and connection line. Snowflake(String, String) } @@ -57,4 +82,32 @@ impl TorBridge { TorBridge::Snowflake(_, line) => line.clone() } } + + /// Save binary path to provided bridge. + pub fn save_bridge_bin_path(bridge: &TorBridge, path: String) { + match bridge { + TorBridge::Obfs4(_, line) => { + TorConfig::save_bridge(Some(TorBridge::Obfs4(path, line.into()))); + } + TorBridge::Snowflake(_, line) => { + TorConfig::save_bridge(Some(TorBridge::Snowflake(path, line.into()))); + } + } + } + + /// Save connection line to provided bridge. + pub fn save_bridge_conn_line(bridge: &TorBridge, line: String) { + match bridge { + TorBridge::Obfs4(path, _) => { + TorConfig::save_bridge( + Some(TorBridge::Obfs4(path.into(), line)) + ); + } + TorBridge::Snowflake(path, _) => { + TorConfig::save_bridge( + Some(TorBridge::Snowflake(path.into(), line)) + ); + } + } + } } \ No newline at end of file