From a543abda36fc1f912908d4c8e519c42c6a4da93e Mon Sep 17 00:00:00 2001 From: Reza Alizadeh Majd Date: Mon, 22 Apr 2024 11:43:54 +0330 Subject: [PATCH 1/6] read proxy info from config file --- data/src/config.rs | 6 ++++++ data/src/config/server.rs | 20 ++++++++++++++++++++ data/src/server.rs | 18 +++++++++++++++--- 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/data/src/config.rs b/data/src/config.rs index ca172c906..de40c368e 100644 --- a/data/src/config.rs +++ b/data/src/config.rs @@ -10,6 +10,7 @@ pub use self::channel::Channel; pub use self::file_transfer::FileTransfer; pub use self::keys::Keyboard; pub use self::notification::{Notification, Notifications}; +use self::server::ProxyConfig; pub use self::server::Server; pub use self::sidebar::Sidebar; use crate::environment::config_dir; @@ -32,6 +33,7 @@ const DEFAULT_THEME_FILE_NAME: &str = "ferra.toml"; pub struct Config { pub themes: Themes, pub servers: ServerMap, + pub proxy: Option, pub font: Font, pub scale_factor: ScaleFactor, pub buffer: Buffer, @@ -117,6 +119,7 @@ impl Config { #[serde(default)] pub theme: String, pub servers: ServerMap, + pub proxy: Option, #[serde(default)] pub font: Font, #[serde(default)] @@ -142,6 +145,7 @@ impl Config { theme, mut servers, font, + proxy, scale_factor, buffer, sidebar, @@ -152,6 +156,7 @@ impl Config { } = toml::from_str(content.as_ref()).map_err(|e| Error::Parse(e.to_string()))?; servers.read_password_files()?; + servers.update_proxy(&proxy)?; let themes = Self::load_themes(&theme).unwrap_or_default(); @@ -159,6 +164,7 @@ impl Config { themes, servers, font, + proxy, scale_factor, buffer, sidebar, diff --git a/data/src/config/server.rs b/data/src/config/server.rs index 1325ea4c9..6337d16d0 100644 --- a/data/src/config/server.rs +++ b/data/src/config/server.rs @@ -27,6 +27,8 @@ pub struct Server { /// The port to connect on. #[serde(default = "default_port")] pub port: u16, + /// Proxy configuration to use for connecting to the server. + pub proxy: Option, /// The password to connect to the server. pub password: Option, /// The file with the password to connect to the server. @@ -107,6 +109,24 @@ impl Server { } } +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum ProxyConfigType { + Socks5, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct ProxyConfig { + #[serde(rename = "type")] + pub proxy_type: ProxyConfigType, + pub host: String, + pub port: u16, + #[serde(default)] + pub username: String, + #[serde(default)] + pub password: String, +} + #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "kebab-case")] pub enum IdentifySyntax { diff --git a/data/src/server.rs b/data/src/server.rs index 8887ddbb3..f4c47b8c4 100644 --- a/data/src/server.rs +++ b/data/src/server.rs @@ -7,8 +7,9 @@ use irc::proto; use serde::{Deserialize, Serialize}; use crate::config; -use crate::config::Error; +use crate::config::server::ProxyConfig; use crate::config::server::Sasl; +use crate::config::Error; pub type Handle = Sender; @@ -62,18 +63,29 @@ impl Map { self.0.iter().map(Entry::from) } + pub fn update_proxy(&mut self, proxy: &Option) -> Result<(), Error> { + for (_, config) in self.0.iter_mut() { + config.proxy = proxy.clone(); + } + Ok(()) + } + pub fn read_password_files(&mut self) -> Result<(), Error> { for (_, config) in self.0.iter_mut() { if let Some(pass_file) = &config.password_file { if config.password.is_some() { - return Err(Error::Parse("Only one of password and password_file can be set.".to_string())); + return Err(Error::Parse( + "Only one of password and password_file can be set.".to_string(), + )); } let pass = fs::read_to_string(pass_file)?; config.password = Some(pass); } if let Some(nick_pass_file) = &config.nick_password_file { if config.nick_password.is_some() { - return Err(Error::Parse("Only one of nick_password and nick_password_file can be set.".to_string())); + return Err(Error::Parse( + "Only one of nick_password and nick_password_file can be set.".to_string(), + )); } let nick_pass = fs::read_to_string(nick_pass_file)?; config.nick_password = Some(nick_pass); From fe349ebcfeb91c22fab3d9050b76c4e70e894870 Mon Sep 17 00:00:00 2001 From: Reza Alizadeh Majd Date: Tue, 23 Apr 2024 08:41:25 +0330 Subject: [PATCH 2/6] add support for socks5 prox using the tokio-socks crate --- Cargo.lock | 13 ++++++++++++ data/src/config/server.rs | 18 +++++++++++++++- data/src/file_transfer/manager.rs | 9 +++++--- data/src/file_transfer/task.rs | 10 ++++++++- irc/Cargo.toml | 1 + irc/src/connection.rs | 34 ++++++++++++++++++++++++++++++- src/screen/dashboard.rs | 7 +++++-- 7 files changed, 84 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f9acb15e6..4802cf8d9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2165,6 +2165,7 @@ dependencies = [ "thiserror", "tokio", "tokio-rustls", + "tokio-socks", "tokio-util", ] @@ -4219,6 +4220,18 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-socks" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51165dfa029d2a65969413a6cc96f354b86b464498702f174a4efa13608fd8c0" +dependencies = [ + "either", + "futures-util", + "thiserror", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.15" diff --git a/data/src/config/server.rs b/data/src/config/server.rs index 6337d16d0..498c2e13e 100644 --- a/data/src/config/server.rs +++ b/data/src/config/server.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use std::path::PathBuf; use std::time::Duration; -use irc::connection; +use irc::connection::{self, Proxy}; use serde::{Deserialize, Deserializer}; #[derive(Debug, Clone, Default, Deserialize)] @@ -101,10 +101,13 @@ impl Server { connection::Security::Unsecured }; + let proxy = self.proxy.clone().map(|p| p.into()); + connection::Config { server: &self.server, port: self.port, security, + proxy, } } } @@ -127,6 +130,19 @@ pub struct ProxyConfig { pub password: String, } +impl Into for ProxyConfig { + fn into(self) -> Proxy { + match self.proxy_type { + ProxyConfigType::Socks5 => irc::connection::Proxy::Socks5 { + host: self.host, + port: self.port, + username: self.username, + password: self.password, + }, + } + } +} + #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "kebab-case")] pub enum IdentifySyntax { diff --git a/data/src/file_transfer/manager.rs b/data/src/file_transfer/manager.rs index f81f325aa..18c9cbbe4 100644 --- a/data/src/file_transfer/manager.rs +++ b/data/src/file_transfer/manager.rs @@ -7,6 +7,7 @@ use std::{ use chrono::Utc; use futures::{stream::BoxStream, StreamExt}; +use irc::connection::Proxy; use itertools::Itertools; use rand::Rng; @@ -47,15 +48,17 @@ pub struct Manager { /// Queued = waiting for port assignment queued: VecDeque, used_ports: HashMap, + proxy: Option, } impl Manager { - pub fn new(config: config::FileTransfer) -> Self { + pub fn new(config: config::FileTransfer, proxy: Option) -> Self { Self { config, items: HashMap::new(), queued: VecDeque::new(), used_ports: HashMap::new(), + proxy, } } @@ -117,7 +120,7 @@ impl Manager { }; let task = Task::send(id, path, filename, to, reverse, server_handle); - let (handle, stream) = task.spawn(self.server(), Duration::from_secs(self.config.timeout)); + let (handle, stream) = task.spawn(self.server(), Duration::from_secs(self.config.timeout), &self.proxy); self.items.insert( id, @@ -184,7 +187,7 @@ impl Manager { }; let task = Task::receive(id, dcc_send, from, server_handle); - let (handle, stream) = task.spawn(self.server(), Duration::from_secs(self.config.timeout)); + let (handle, stream) = task.spawn(self.server(), Duration::from_secs(self.config.timeout), &self.proxy); self.items.insert( id, diff --git a/data/src/file_transfer/task.rs b/data/src/file_transfer/task.rs index 0c10992a2..3187b183d 100644 --- a/data/src/file_transfer/task.rs +++ b/data/src/file_transfer/task.rs @@ -11,7 +11,7 @@ use futures::{ channel::mpsc::{self, Receiver, Sender}, SinkExt, Stream, }; -use irc::{connection, proto::command, BytesCodec, Connection}; +use irc::{connection::{self, Proxy}, proto::command, BytesCodec, Connection}; use sha2::{Digest, Sha256}; use thiserror::Error; use tokio::{ @@ -109,9 +109,11 @@ impl Task { self, server: Option, timeout: Duration, + proxy: &Option, ) -> (Handle, impl Stream) { let (action_sender, action_receiver) = mpsc::channel(1); let (update_sender, update_receiver) = mpsc::channel(100); + let proxy = proxy.clone(); let task = tokio::spawn(async move { let mut update = update_sender.clone(); @@ -132,6 +134,7 @@ impl Task { update_sender, server, timeout, + &proxy, ) .await { @@ -157,6 +160,7 @@ impl Task { update_sender, server, timeout, + &proxy, ) .await { @@ -214,6 +218,7 @@ async fn receive( mut update: Sender, server: Option, timeout: Duration, + proxy: &Option, ) -> Result<(), Error> { // Wait for approval let Some(Action::Approve { save_to }) = action.next().await else { @@ -281,6 +286,7 @@ async fn receive( server: &host.to_string(), port: port.get(), security: connection::Security::Unsecured, + proxy: proxy.clone(), }, BytesCodec::new(), ) @@ -356,6 +362,7 @@ async fn send( mut update: Sender, server: Option, timeout: Duration, + proxy: &Option, ) -> Result<(), Error> { let mut file = File::open(path).await?; let size = file.metadata().await?.len(); @@ -394,6 +401,7 @@ async fn send( server: &host.to_string(), port: port.get(), security: connection::Security::Unsecured, + proxy: proxy.clone(), }, BytesCodec::new(), ) diff --git a/irc/Cargo.toml b/irc/Cargo.toml index 0a5fb41b1..8dbad786d 100644 --- a/irc/Cargo.toml +++ b/irc/Cargo.toml @@ -10,6 +10,7 @@ futures = "0.3.28" thiserror = "1.0.30" tokio = { version = "1.29", features = ["net", "full"] } tokio-rustls = { version = "0.26.0", default-features = false, features = ["tls12", "ring"] } +tokio-socks = "0.5.1" tokio-util = { version = "0.7", features = ["codec"] } rustls-native-certs = "0.7.0" rustls-pemfile = "2.1.1" diff --git a/irc/src/connection.rs b/irc/src/connection.rs index 7b7eeeda0..ee6ece8c1 100644 --- a/irc/src/connection.rs +++ b/irc/src/connection.rs @@ -5,6 +5,7 @@ use futures::{Sink, SinkExt, Stream, StreamExt}; use tokio::io::AsyncWriteExt; use tokio::net::{TcpListener, TcpStream}; use tokio_rustls::client::TlsStream; +use tokio_socks::tcp::Socks5Stream; use tokio_util::codec; use tokio_util::codec::Framed; @@ -26,16 +27,45 @@ pub enum Security<'a> { }, } +#[derive(Debug, Clone)] +pub enum Proxy { + Socks5 { + host: String, + port: u16, + username: String, + password: String, + }, +} + #[derive(Debug, Clone)] pub struct Config<'a> { pub server: &'a str, pub port: u16, pub security: Security<'a>, + pub proxy: Option, } impl Connection { pub async fn new(config: Config<'_>, codec: Codec) -> Result { - let tcp = TcpStream::connect((config.server, config.port)).await?; + let target = (config.server, config.port); + let tcp = match config.proxy { + None => TcpStream::connect(target).await?, + Some(Proxy::Socks5 { + host, + port, + username, + password, + }) => { + let proxy = (host.as_str(), port); + if username.trim().is_empty() { + Socks5Stream::connect(proxy, target).await?.into_inner() + } else { + Socks5Stream::connect_with_password(proxy, target, &username, &password) + .await? + .into_inner() + } + } + }; if let Security::Secured { accept_invalid_certs, @@ -99,6 +129,8 @@ pub enum Error { Tls(#[from] tls::Error), #[error("io error: {0}")] Io(#[from] std::io::Error), + #[error("proxy error: {0}")] + Proxy(#[from] tokio_socks::Error), } macro_rules! delegate { diff --git a/src/screen/dashboard.rs b/src/screen/dashboard.rs index 3c67f6e7f..d1571668c 100644 --- a/src/screen/dashboard.rs +++ b/src/screen/dashboard.rs @@ -60,6 +60,7 @@ pub enum Event { impl Dashboard { pub fn empty(config: &Config) -> (Self, Command) { let (panes, _) = pane_grid::State::new(Pane::new(Buffer::Empty, config)); + let proxy = config.proxy.clone().map(|p| p.into()); let mut dashboard = Dashboard { panes, @@ -68,7 +69,7 @@ impl Dashboard { history: history::Manager::default(), last_changed: None, command_bar: None, - file_transfers: file_transfer::Manager::new(config.file_transfer.clone()), + file_transfers: file_transfer::Manager::new(config.file_transfer.clone(), proxy), }; let command = dashboard.track(); @@ -1276,6 +1277,8 @@ impl Dashboard { } } + let proxy = config.proxy.clone().map(|p| p.into()); + Self { panes: pane_grid::State::with_configuration(configuration(dashboard.pane)), focus: None, @@ -1283,7 +1286,7 @@ impl Dashboard { history: history::Manager::default(), last_changed: None, command_bar: None, - file_transfers: file_transfer::Manager::new(config.file_transfer.clone()), + file_transfers: file_transfer::Manager::new(config.file_transfer.clone(), proxy), } } } From a5330e565d6aedae592ea6b5d686a4f74d8c9305 Mon Sep 17 00:00:00 2001 From: Reza Alizadeh Majd Date: Wed, 24 Apr 2024 18:42:58 +0330 Subject: [PATCH 3/6] switch form tokio-socks to fast-socks5 --- Cargo.lock | 34 +++++++++++++++++++++------------- irc/Cargo.toml | 2 +- irc/src/connection.rs | 29 +++++++++++++++++++++-------- 3 files changed, 43 insertions(+), 22 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4802cf8d9..9a1e9eb35 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -120,6 +120,12 @@ dependencies = [ "libc", ] +[[package]] +name = "anyhow" +version = "1.0.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519" + [[package]] name = "approx" version = "0.5.1" @@ -1271,6 +1277,20 @@ dependencies = [ "zune-inflate", ] +[[package]] +name = "fast-socks5" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f89f36d4ee12370d30d57b16c7e190950a1a916e7dbbb5fd5a412f5ef913fe84" +dependencies = [ + "anyhow", + "async-trait", + "log", + "thiserror", + "tokio", + "tokio-stream", +] + [[package]] name = "fast-srgb8" version = "1.0.0" @@ -2158,6 +2178,7 @@ name = "irc" version = "0.1.0" dependencies = [ "bytes", + "fast-socks5", "futures", "irc_proto", "rustls-native-certs", @@ -2165,7 +2186,6 @@ dependencies = [ "thiserror", "tokio", "tokio-rustls", - "tokio-socks", "tokio-util", ] @@ -4220,18 +4240,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "tokio-socks" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51165dfa029d2a65969413a6cc96f354b86b464498702f174a4efa13608fd8c0" -dependencies = [ - "either", - "futures-util", - "thiserror", - "tokio", -] - [[package]] name = "tokio-stream" version = "0.1.15" diff --git a/irc/Cargo.toml b/irc/Cargo.toml index 8dbad786d..2194d187e 100644 --- a/irc/Cargo.toml +++ b/irc/Cargo.toml @@ -6,11 +6,11 @@ edition = "2021" [dependencies] bytes = "1.4.0" +fast-socks5 = "0.9.6" futures = "0.3.28" thiserror = "1.0.30" tokio = { version = "1.29", features = ["net", "full"] } tokio-rustls = { version = "0.26.0", default-features = false, features = ["tls12", "ring"] } -tokio-socks = "0.5.1" tokio-util = { version = "0.7", features = ["codec"] } rustls-native-certs = "0.7.0" rustls-pemfile = "2.1.1" diff --git a/irc/src/connection.rs b/irc/src/connection.rs index ee6ece8c1..5fc85ca03 100644 --- a/irc/src/connection.rs +++ b/irc/src/connection.rs @@ -1,11 +1,11 @@ use std::net::IpAddr; use std::path::PathBuf; +use fast_socks5::client::{Config as SocksConfig, Socks5Stream}; use futures::{Sink, SinkExt, Stream, StreamExt}; use tokio::io::AsyncWriteExt; use tokio::net::{TcpListener, TcpStream}; use tokio_rustls::client::TlsStream; -use tokio_socks::tcp::Socks5Stream; use tokio_util::codec; use tokio_util::codec::Framed; @@ -47,9 +47,8 @@ pub struct Config<'a> { impl Connection { pub async fn new(config: Config<'_>, codec: Codec) -> Result { - let target = (config.server, config.port); let tcp = match config.proxy { - None => TcpStream::connect(target).await?, + None => TcpStream::connect((config.server, config.port)).await?, Some(Proxy::Socks5 { host, port, @@ -58,11 +57,25 @@ impl Connection { }) => { let proxy = (host.as_str(), port); if username.trim().is_empty() { - Socks5Stream::connect(proxy, target).await?.into_inner() + Socks5Stream::connect( + proxy, + config.server.to_string(), + config.port, + SocksConfig::default(), + ) + .await? + .get_socket() } else { - Socks5Stream::connect_with_password(proxy, target, &username, &password) - .await? - .into_inner() + Socks5Stream::connect_with_password( + proxy, + config.server.to_string(), + config.port, + username, + password, + SocksConfig::default(), + ) + .await? + .get_socket() } } }; @@ -130,7 +143,7 @@ pub enum Error { #[error("io error: {0}")] Io(#[from] std::io::Error), #[error("proxy error: {0}")] - Proxy(#[from] tokio_socks::Error), + Proxy(#[from] fast_socks5::SocksError), } macro_rules! delegate { From 4c0a11614f78a871623d36bb48f4e15cf897fe9d Mon Sep 17 00:00:00 2001 From: Reza Alizadeh Majd Date: Wed, 24 Apr 2024 19:10:40 +0330 Subject: [PATCH 4/6] --wip-- [skip ci] --- data/src/config.rs | 7 +++--- data/src/config/proxy.rs | 32 ++++++++++++++++++++++++++ data/src/config/server.rs | 47 +++++++++------------------------------ data/src/server.rs | 4 ++-- 4 files changed, 49 insertions(+), 41 deletions(-) create mode 100644 data/src/config/proxy.rs diff --git a/data/src/config.rs b/data/src/config.rs index de40c368e..36643dc83 100644 --- a/data/src/config.rs +++ b/data/src/config.rs @@ -10,7 +10,7 @@ pub use self::channel::Channel; pub use self::file_transfer::FileTransfer; pub use self::keys::Keyboard; pub use self::notification::{Notification, Notifications}; -use self::server::ProxyConfig; +use self::proxy::Proxy; pub use self::server::Server; pub use self::sidebar::Sidebar; use crate::environment::config_dir; @@ -23,6 +23,7 @@ pub mod channel; pub mod file_transfer; mod keys; pub mod notification; +pub mod proxy; pub mod server; pub mod sidebar; @@ -33,7 +34,7 @@ const DEFAULT_THEME_FILE_NAME: &str = "ferra.toml"; pub struct Config { pub themes: Themes, pub servers: ServerMap, - pub proxy: Option, + pub proxy: Option, pub font: Font, pub scale_factor: ScaleFactor, pub buffer: Buffer, @@ -119,7 +120,7 @@ impl Config { #[serde(default)] pub theme: String, pub servers: ServerMap, - pub proxy: Option, + pub proxy: Option, #[serde(default)] pub font: Font, #[serde(default)] diff --git a/data/src/config/proxy.rs b/data/src/config/proxy.rs new file mode 100644 index 000000000..95508f433 --- /dev/null +++ b/data/src/config/proxy.rs @@ -0,0 +1,32 @@ +use serde::Deserialize; + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum Kind { + Socks5, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct Proxy { + #[serde(rename = "type")] + pub proxy_type: Kind, + pub host: String, + pub port: u16, + #[serde(default)] + pub username: String, + #[serde(default)] + pub password: String, +} + +impl Into for Proxy { + fn into(self) -> irc::connection::Proxy { + match self.proxy_type { + Kind::Socks5 => irc::connection::Proxy::Socks5 { + host: self.host, + port: self.port, + username: self.username, + password: self.password, + }, + } + } +} diff --git a/data/src/config/server.rs b/data/src/config/server.rs index 498c2e13e..1dc4a353a 100644 --- a/data/src/config/server.rs +++ b/data/src/config/server.rs @@ -2,9 +2,11 @@ use std::collections::HashMap; use std::path::PathBuf; use std::time::Duration; -use irc::connection::{self, Proxy}; +use irc::connection; use serde::{Deserialize, Deserializer}; +use super::proxy::Proxy; + #[derive(Debug, Clone, Default, Deserialize)] pub struct Server { /// The client's nickname. @@ -28,7 +30,7 @@ pub struct Server { #[serde(default = "default_port")] pub port: u16, /// Proxy configuration to use for connecting to the server. - pub proxy: Option, + pub proxy: Option, /// The password to connect to the server. pub password: Option, /// The file with the password to connect to the server. @@ -112,37 +114,6 @@ impl Server { } } -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub enum ProxyConfigType { - Socks5, -} - -#[derive(Debug, Clone, Deserialize)] -pub struct ProxyConfig { - #[serde(rename = "type")] - pub proxy_type: ProxyConfigType, - pub host: String, - pub port: u16, - #[serde(default)] - pub username: String, - #[serde(default)] - pub password: String, -} - -impl Into for ProxyConfig { - fn into(self) -> Proxy { - match self.proxy_type { - ProxyConfigType::Socks5 => irc::connection::Proxy::Socks5 { - host: self.host, - port: self.port, - username: self.username, - password: self.password, - }, - } - } -} - #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "kebab-case")] pub enum IdentifySyntax { @@ -159,7 +130,7 @@ pub enum Sasl { /// Account password, password: Option, /// Account password file - password_file: Option + password_file: Option, }, External { /// The path to PEM encoded X509 user certificate for external auth @@ -179,10 +150,14 @@ impl Sasl { pub fn param(&self) -> String { match self { - Sasl::Plain { username, password, .. } => { + Sasl::Plain { + username, password, .. + } => { use base64::engine::Engine; - let password = password.as_ref().expect("SASL password must exist at this point!"); + let password = password + .as_ref() + .expect("SASL password must exist at this point!"); base64::engine::general_purpose::STANDARD .encode(format!("{username}\x00{username}\x00{password}")) diff --git a/data/src/server.rs b/data/src/server.rs index f4c47b8c4..8c476330c 100644 --- a/data/src/server.rs +++ b/data/src/server.rs @@ -7,7 +7,7 @@ use irc::proto; use serde::{Deserialize, Serialize}; use crate::config; -use crate::config::server::ProxyConfig; +use crate::config::proxy::Proxy; use crate::config::server::Sasl; use crate::config::Error; @@ -63,7 +63,7 @@ impl Map { self.0.iter().map(Entry::from) } - pub fn update_proxy(&mut self, proxy: &Option) -> Result<(), Error> { + pub fn update_proxy(&mut self, proxy: &Option) -> Result<(), Error> { for (_, config) in self.0.iter_mut() { config.proxy = proxy.clone(); } From efd7b549f6b6ba0ba24d13533ccf920507b65320 Mon Sep 17 00:00:00 2001 From: Cory Forsstrom Date: Thu, 25 Apr 2024 15:10:20 -0700 Subject: [PATCH 5/6] Single source of truth for proxy config Instead of injecting proxy into the server config and feeding it into the file transfers manager on first boot, we instead pass in the config proxy from `view` when building new server stream tasks or new file transfer tasks. This ensures we are always referencing the latest proxy config, which is very important since we now support config reload. --- data/src/config.rs | 3 +- data/src/config/proxy.rs | 22 ++++++------- data/src/config/server.rs | 10 ++---- data/src/file_transfer/manager.rs | 25 ++++++++++----- data/src/file_transfer/task.rs | 19 ++++++----- data/src/server.rs | 8 ----- data/src/stream.rs | 11 +++++-- irc/src/connection.rs | 47 ++++++++-------------------- irc/src/connection/proxy.rs | 52 +++++++++++++++++++++++++++++++ src/main.rs | 8 +++-- src/screen/dashboard.rs | 27 +++++++++------- src/stream.rs | 6 ++-- 12 files changed, 137 insertions(+), 101 deletions(-) create mode 100644 irc/src/connection/proxy.rs diff --git a/data/src/config.rs b/data/src/config.rs index 36643dc83..b87765ea7 100644 --- a/data/src/config.rs +++ b/data/src/config.rs @@ -10,7 +10,7 @@ pub use self::channel::Channel; pub use self::file_transfer::FileTransfer; pub use self::keys::Keyboard; pub use self::notification::{Notification, Notifications}; -use self::proxy::Proxy; +pub use self::proxy::Proxy; pub use self::server::Server; pub use self::sidebar::Sidebar; use crate::environment::config_dir; @@ -157,7 +157,6 @@ impl Config { } = toml::from_str(content.as_ref()).map_err(|e| Error::Parse(e.to_string()))?; servers.read_password_files()?; - servers.update_proxy(&proxy)?; let themes = Self::load_themes(&theme).unwrap_or_default(); diff --git a/data/src/config/proxy.rs b/data/src/config/proxy.rs index 95508f433..c3c226786 100644 --- a/data/src/config/proxy.rs +++ b/data/src/config/proxy.rs @@ -9,23 +9,21 @@ pub enum Kind { #[derive(Debug, Clone, Deserialize)] pub struct Proxy { #[serde(rename = "type")] - pub proxy_type: Kind, + pub kind: Kind, pub host: String, pub port: u16, - #[serde(default)] - pub username: String, - #[serde(default)] - pub password: String, + pub username: Option, + pub password: Option, } -impl Into for Proxy { - fn into(self) -> irc::connection::Proxy { - match self.proxy_type { +impl From for irc::connection::Proxy { + fn from(proxy: Proxy) -> irc::connection::Proxy { + match proxy.kind { Kind::Socks5 => irc::connection::Proxy::Socks5 { - host: self.host, - port: self.port, - username: self.username, - password: self.password, + host: proxy.host, + port: proxy.port, + username: proxy.username, + password: proxy.password, }, } } diff --git a/data/src/config/server.rs b/data/src/config/server.rs index 1dc4a353a..d53bb377d 100644 --- a/data/src/config/server.rs +++ b/data/src/config/server.rs @@ -5,7 +5,7 @@ use std::time::Duration; use irc::connection; use serde::{Deserialize, Deserializer}; -use super::proxy::Proxy; +use crate::config; #[derive(Debug, Clone, Default, Deserialize)] pub struct Server { @@ -29,8 +29,6 @@ pub struct Server { /// The port to connect on. #[serde(default = "default_port")] pub port: u16, - /// Proxy configuration to use for connecting to the server. - pub proxy: Option, /// The password to connect to the server. pub password: Option, /// The file with the password to connect to the server. @@ -91,7 +89,7 @@ pub struct Server { } impl Server { - pub fn connection(&self) -> connection::Config { + pub fn connection(&self, proxy: Option) -> connection::Config { let security = if self.use_tls { connection::Security::Secured { accept_invalid_certs: self.dangerously_accept_invalid_certs, @@ -103,13 +101,11 @@ impl Server { connection::Security::Unsecured }; - let proxy = self.proxy.clone().map(|p| p.into()); - connection::Config { server: &self.server, port: self.port, security, - proxy, + proxy: proxy.map(From::from), } } } diff --git a/data/src/file_transfer/manager.rs b/data/src/file_transfer/manager.rs index 18c9cbbe4..d44d57676 100644 --- a/data/src/file_transfer/manager.rs +++ b/data/src/file_transfer/manager.rs @@ -7,7 +7,6 @@ use std::{ use chrono::Utc; use futures::{stream::BoxStream, StreamExt}; -use irc::connection::Proxy; use itertools::Itertools; use rand::Rng; @@ -48,17 +47,15 @@ pub struct Manager { /// Queued = waiting for port assignment queued: VecDeque, used_ports: HashMap, - proxy: Option, } impl Manager { - pub fn new(config: config::FileTransfer, proxy: Option) -> Self { + pub fn new(config: config::FileTransfer) -> Self { Self { config, items: HashMap::new(), queued: VecDeque::new(), used_ports: HashMap::new(), - proxy, } } @@ -81,7 +78,7 @@ impl Manager { }) } - pub fn send(&mut self, request: SendRequest) -> Option { + pub fn send(&mut self, request: SendRequest, proxy: Option) -> Option { let SendRequest { to, path, @@ -120,7 +117,11 @@ impl Manager { }; let task = Task::send(id, path, filename, to, reverse, server_handle); - let (handle, stream) = task.spawn(self.server(), Duration::from_secs(self.config.timeout), &self.proxy); + let (handle, stream) = task.spawn( + self.server(), + Duration::from_secs(self.config.timeout), + proxy, + ); self.items.insert( id, @@ -133,7 +134,11 @@ impl Manager { Some(Event::NewTransfer(file_transfer, stream.boxed())) } - pub fn receive(&mut self, request: ReceiveRequest) -> Option { + pub fn receive( + &mut self, + request: ReceiveRequest, + proxy: Option<&config::Proxy>, + ) -> Option { let ReceiveRequest { from, dcc_send, @@ -187,7 +192,11 @@ impl Manager { }; let task = Task::receive(id, dcc_send, from, server_handle); - let (handle, stream) = task.spawn(self.server(), Duration::from_secs(self.config.timeout), &self.proxy); + let (handle, stream) = task.spawn( + self.server(), + Duration::from_secs(self.config.timeout), + proxy.cloned(), + ); self.items.insert( id, diff --git a/data/src/file_transfer/task.rs b/data/src/file_transfer/task.rs index 3187b183d..ca2d45471 100644 --- a/data/src/file_transfer/task.rs +++ b/data/src/file_transfer/task.rs @@ -11,7 +11,7 @@ use futures::{ channel::mpsc::{self, Receiver, Sender}, SinkExt, Stream, }; -use irc::{connection::{self, Proxy}, proto::command, BytesCodec, Connection}; +use irc::{connection, proto::command, BytesCodec, Connection}; use sha2::{Digest, Sha256}; use thiserror::Error; use tokio::{ @@ -23,7 +23,7 @@ use tokio::{ use tokio_stream::StreamExt; use super::Id; -use crate::{dcc, server, user::Nick}; +use crate::{config, dcc, server, user::Nick}; /// 16 KiB pub const BUFFER_SIZE: usize = 16 * 1024; @@ -109,11 +109,10 @@ impl Task { self, server: Option, timeout: Duration, - proxy: &Option, + proxy: Option, ) -> (Handle, impl Stream) { let (action_sender, action_receiver) = mpsc::channel(1); let (update_sender, update_receiver) = mpsc::channel(100); - let proxy = proxy.clone(); let task = tokio::spawn(async move { let mut update = update_sender.clone(); @@ -134,7 +133,7 @@ impl Task { update_sender, server, timeout, - &proxy, + proxy, ) .await { @@ -160,7 +159,7 @@ impl Task { update_sender, server, timeout, - &proxy, + proxy, ) .await { @@ -218,7 +217,7 @@ async fn receive( mut update: Sender, server: Option, timeout: Duration, - proxy: &Option, + proxy: Option, ) -> Result<(), Error> { // Wait for approval let Some(Action::Approve { save_to }) = action.next().await else { @@ -286,7 +285,7 @@ async fn receive( server: &host.to_string(), port: port.get(), security: connection::Security::Unsecured, - proxy: proxy.clone(), + proxy: proxy.map(From::from), }, BytesCodec::new(), ) @@ -362,7 +361,7 @@ async fn send( mut update: Sender, server: Option, timeout: Duration, - proxy: &Option, + proxy: Option, ) -> Result<(), Error> { let mut file = File::open(path).await?; let size = file.metadata().await?.len(); @@ -401,7 +400,7 @@ async fn send( server: &host.to_string(), port: port.get(), security: connection::Security::Unsecured, - proxy: proxy.clone(), + proxy: proxy.map(From::from), }, BytesCodec::new(), ) diff --git a/data/src/server.rs b/data/src/server.rs index 8c476330c..9110ef441 100644 --- a/data/src/server.rs +++ b/data/src/server.rs @@ -7,7 +7,6 @@ use irc::proto; use serde::{Deserialize, Serialize}; use crate::config; -use crate::config::proxy::Proxy; use crate::config::server::Sasl; use crate::config::Error; @@ -63,13 +62,6 @@ impl Map { self.0.iter().map(Entry::from) } - pub fn update_proxy(&mut self, proxy: &Option) -> Result<(), Error> { - for (_, config) in self.0.iter_mut() { - config.proxy = proxy.clone(); - } - Ok(()) - } - pub fn read_password_files(&mut self) -> Result<(), Error> { for (_, config) in self.0.iter_mut() { if let Some(pass_file) = &config.password_file { diff --git a/data/src/stream.rs b/data/src/stream.rs index 82a30c3c3..2bcab6e39 100644 --- a/data/src/stream.rs +++ b/data/src/stream.rs @@ -67,7 +67,11 @@ struct Stream { receiver: mpsc::Receiver, } -pub async fn run(server: server::Entry, mut sender: mpsc::Sender) -> Never { +pub async fn run( + server: server::Entry, + proxy: Option, + mut sender: mpsc::Sender, +) -> Never { let server::Entry { server, config } = server; let reconnect_delay = Duration::from_secs(config.reconnect_delay); @@ -96,7 +100,7 @@ pub async fn run(server: server::Entry, mut sender: mpsc::Sender) -> Nev } } - match connect(server.clone(), config.clone()).await { + match connect(server.clone(), config.clone(), proxy.clone()).await { Ok((stream, client)) => { log::info!("[{server}] connected"); @@ -253,8 +257,9 @@ pub async fn run(server: server::Entry, mut sender: mpsc::Sender) -> Nev async fn connect( server: Server, config: config::Server, + proxy: Option, ) -> Result<(Stream, Client), connection::Error> { - let connection = Connection::new(config.connection(), irc::Codec).await?; + let connection = Connection::new(config.connection(proxy), irc::Codec).await?; let (sender, receiver) = mpsc::channel(100); diff --git a/irc/src/connection.rs b/irc/src/connection.rs index 5fc85ca03..fdfb5f889 100644 --- a/irc/src/connection.rs +++ b/irc/src/connection.rs @@ -1,7 +1,6 @@ use std::net::IpAddr; use std::path::PathBuf; -use fast_socks5::client::{Config as SocksConfig, Socks5Stream}; use futures::{Sink, SinkExt, Stream, StreamExt}; use tokio::io::AsyncWriteExt; use tokio::net::{TcpListener, TcpStream}; @@ -9,6 +8,9 @@ use tokio_rustls::client::TlsStream; use tokio_util::codec; use tokio_util::codec::Framed; +pub use self::proxy::Proxy; + +mod proxy; mod tls; pub enum Connection { @@ -27,16 +29,6 @@ pub enum Security<'a> { }, } -#[derive(Debug, Clone)] -pub enum Proxy { - Socks5 { - host: String, - port: u16, - username: String, - password: String, - }, -} - #[derive(Debug, Clone)] pub struct Config<'a> { pub server: &'a str, @@ -55,28 +47,15 @@ impl Connection { username, password, }) => { - let proxy = (host.as_str(), port); - if username.trim().is_empty() { - Socks5Stream::connect( - proxy, - config.server.to_string(), - config.port, - SocksConfig::default(), - ) - .await? - .get_socket() - } else { - Socks5Stream::connect_with_password( - proxy, - config.server.to_string(), - config.port, - username, - password, - SocksConfig::default(), - ) - .await? - .get_socket() - } + proxy::connect_socks5( + host, + port, + config.server.to_string(), + config.port, + username, + password, + ) + .await? } }; @@ -143,7 +122,7 @@ pub enum Error { #[error("io error: {0}")] Io(#[from] std::io::Error), #[error("proxy error: {0}")] - Proxy(#[from] fast_socks5::SocksError), + Proxy(#[from] proxy::Error), } macro_rules! delegate { diff --git a/irc/src/connection/proxy.rs b/irc/src/connection/proxy.rs new file mode 100644 index 000000000..a04a01317 --- /dev/null +++ b/irc/src/connection/proxy.rs @@ -0,0 +1,52 @@ +use fast_socks5::client::{Config as Socks5Config, Socks5Stream}; +use thiserror::Error; +use tokio::net::TcpStream; + +#[derive(Debug, Clone)] +pub enum Proxy { + Socks5 { + host: String, + port: u16, + username: Option, + password: Option, + }, +} + +pub async fn connect_socks5( + proxy_server: String, + proxy_port: u16, + target_server: String, + target_port: u16, + username: Option, + password: Option, +) -> Result { + let stream = if let Some((username, password)) = username.zip(password) { + Socks5Stream::connect_with_password( + (proxy_server, proxy_port), + target_server, + target_port, + username, + password, + Socks5Config::default(), + ) + .await? + .get_socket() + } else { + Socks5Stream::connect( + (proxy_server, proxy_port), + target_server, + target_port, + Socks5Config::default(), + ) + .await? + .get_socket() + }; + + Ok(stream) +} + +#[derive(Debug, Error)] +pub enum Error { + #[error("socks5 error: {0}")] + Socks5(#[from] fast_socks5::SocksError), +} diff --git a/src/main.rs b/src/main.rs index 241fbac7f..d871f5bd0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -645,8 +645,12 @@ impl Application for Halloy { fn subscription(&self) -> Subscription { let tick = iced::time::every(Duration::from_secs(1)).map(Message::Tick); - let streams = - Subscription::batch(self.servers.entries().map(stream::run)).map(Message::Stream); + let streams = Subscription::batch( + self.servers + .entries() + .map(|entry| stream::run(entry, self.config.proxy.clone())), + ) + .map(Message::Stream); Subscription::batch(vec![tick, streams, events().map(Message::Event)]) } diff --git a/src/screen/dashboard.rs b/src/screen/dashboard.rs index d1571668c..4b4d5eb88 100644 --- a/src/screen/dashboard.rs +++ b/src/screen/dashboard.rs @@ -60,7 +60,6 @@ pub enum Event { impl Dashboard { pub fn empty(config: &Config) -> (Self, Command) { let (panes, _) = pane_grid::State::new(Pane::new(Buffer::Empty, config)); - let proxy = config.proxy.clone().map(|p| p.into()); let mut dashboard = Dashboard { panes, @@ -69,7 +68,7 @@ impl Dashboard { history: history::Manager::default(), last_changed: None, command_bar: None, - file_transfers: file_transfer::Manager::new(config.file_transfer.clone(), proxy), + file_transfers: file_transfer::Manager::new(config.file_transfer.clone()), }; let command = dashboard.track(); @@ -594,12 +593,15 @@ impl Dashboard { Message::SendFileSelected(server, to, path) => { if let Some(server_handle) = clients.get_server_handle(&server) { if let Some(path) = path { - if let Some(event) = self.file_transfers.send(file_transfer::SendRequest { - to, - path, - server: server.clone(), - server_handle: server_handle.clone(), - }) { + if let Some(event) = self.file_transfers.send( + file_transfer::SendRequest { + to, + path, + server: server.clone(), + server_handle: server_handle.clone(), + }, + config.proxy.clone(), + ) { return (self.handle_file_transfer_event(&server, event), None); } } @@ -1201,7 +1203,10 @@ impl Dashboard { request: file_transfer::ReceiveRequest, config: &Config, ) -> Option> { - if let Some(event) = self.file_transfers.receive(request.clone()) { + if let Some(event) = self + .file_transfers + .receive(request.clone(), config.proxy.as_ref()) + { let notification = &config.notifications.file_transfer_request; if notification.enabled { @@ -1277,8 +1282,6 @@ impl Dashboard { } } - let proxy = config.proxy.clone().map(|p| p.into()); - Self { panes: pane_grid::State::with_configuration(configuration(dashboard.pane)), focus: None, @@ -1286,7 +1289,7 @@ impl Dashboard { history: history::Manager::default(), last_changed: None, command_bar: None, - file_transfers: file_transfer::Manager::new(config.file_transfer.clone(), proxy), + file_transfers: file_transfer::Manager::new(config.file_transfer.clone()), } } } diff --git a/src/stream.rs b/src/stream.rs index c666e5ed8..c86076758 100644 --- a/src/stream.rs +++ b/src/stream.rs @@ -1,11 +1,11 @@ -use data::server; pub use data::stream::{self, *}; +use data::{config, server}; use iced::{subscription, Subscription}; -pub fn run(entry: server::Entry) -> Subscription { +pub fn run(entry: server::Entry, proxy: Option) -> Subscription { // Channel messages are batched every 50ms so channel size 10 ~= 500ms which // app thread should more than easily keep up with subscription::channel(entry.server.clone(), 10, move |sender| { - stream::run(entry, sender) + stream::run(entry, proxy, sender) }) } From 733a24b9e90d3194c5b5870a3138a269c6987901 Mon Sep 17 00:00:00 2001 From: Cory Forsstrom Date: Thu, 25 Apr 2024 15:31:14 -0700 Subject: [PATCH 6/6] Update changelog and add docs --- CHANGELOG.md | 1 + book/src/SUMMARY.md | 1 + book/src/configuration/proxy.md | 20 ++++++++++++++++++++ 3 files changed, 22 insertions(+) create mode 100644 book/src/configuration/proxy.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 572bafb98..bb3280c29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ Added: - User information added to context menu - Support for IRCv3 CAP NEW and CAP DEL subcommands - Enable support for IRCv3 `multi-prefix` +- Added support for `socks5` proxy configuration (see [proxy configuration](https://halloy.squidowl.org/configuration/proxy.html)) Changed: diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index a38ee6a43..2ba161ff9 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -10,6 +10,7 @@ - [Font](configuration/font.md) - [Keyboard](configuration/keyboard.md) - [Notifications](configuration/notifications.md) + - [Proxy](configuration/proxy.md) - [Scale factor](configuration/scale-factor.md) - [Servers](configuration/servers.md) - [Sidebar](configuration/sidebar.md) diff --git a/book/src/configuration/proxy.md b/book/src/configuration/proxy.md new file mode 100644 index 000000000..8e83c66c9 --- /dev/null +++ b/book/src/configuration/proxy.md @@ -0,0 +1,20 @@ +# Proxy + +## `[proxy]` Section + +Example + +```toml +[proxy] +type = "socks5" +host = "" +port = +``` + +| Key | Description | Default | +| :--------- | :------------------------------------------------ | :---------- | +| `type` | Proxy type. Only `socks5` is currently supported. | `""` | +| `host` | Proxy host to connect to | `""` | +| `port` | Proxy port to connect on | `""` | +| `username` | Proxy username, optional | `""` | +| `password` | Proxy password, optional | `""` |