From fa198c3b5e5e543f9ac6d78a495bc2250da8c2d9 Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 8 Feb 2023 23:26:39 +0000 Subject: [PATCH] Use SOCKS5 configuration for HTTP requests --- CHANGELOG.md | 1 + src/configure/read_url.rs | 4 ++- src/http.rs | 19 +++++++++++--- src/login_param.rs | 2 +- src/oauth2.rs | 7 ++++-- src/qr.rs | 7 +++++- src/socks.rs | 52 ++++++++++++++++++++++++++++++++++++--- 7 files changed, 80 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b97d693e..af3614bd5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Use read/write timeouts instead of per-command timeouts for SMTP #3985 - Cache DNS results for SMTP connections #3985 - Prefer TLS over STARTTLS during autoconfiguration #4021 +- Use SOCKS5 configuration for HTTP requests #4017 ## Fixes - Fix Securejoin for multiple devices on a joining side #3982 diff --git a/src/configure/read_url.rs b/src/configure/read_url.rs index b0cdd989f..d164a7007 100644 --- a/src/configure/read_url.rs +++ b/src/configure/read_url.rs @@ -1,6 +1,7 @@ use anyhow::{anyhow, format_err}; use crate::context::Context; +use crate::socks::Socks5Config; pub async fn read_url(context: &Context, url: &str) -> anyhow::Result { match read_url_inner(context, url).await { @@ -16,7 +17,8 @@ pub async fn read_url(context: &Context, url: &str) -> anyhow::Result { } pub async fn read_url_inner(context: &Context, url: &str) -> anyhow::Result { - let client = crate::http::get_client()?; + let socks5_config = Socks5Config::from_database(&context.sql).await?; + let client = crate::http::get_client(socks5_config)?; let mut url = url.to_string(); // Follow up to 10 http-redirects diff --git a/src/http.rs b/src/http.rs index 17e73a10d..8eed8b55e 100644 --- a/src/http.rs +++ b/src/http.rs @@ -4,10 +4,21 @@ use std::time::Duration; use anyhow::Result; +use crate::socks::Socks5Config; + const HTTP_TIMEOUT: Duration = Duration::from_secs(30); -pub(crate) fn get_client() -> Result { - Ok(reqwest::ClientBuilder::new() - .timeout(HTTP_TIMEOUT) - .build()?) +pub(crate) fn get_client(socks5_config: Option) -> Result { + let builder = reqwest::ClientBuilder::new().timeout(HTTP_TIMEOUT); + let builder = if let Some(socks5_config) = socks5_config { + let proxy = reqwest::Proxy::all(socks5_config.to_url())?; + builder.proxy(proxy) + } else { + // Disable usage of "system" proxy configured via environment variables. + // It is enabled by default in `reqwest`, see + // + // for documentation. + builder.no_proxy() + }; + Ok(builder.build()?) } diff --git a/src/login_param.rs b/src/login_param.rs index fa33db7c6..ee674c1fc 100644 --- a/src/login_param.rs +++ b/src/login_param.rs @@ -162,7 +162,7 @@ impl LoginParam { .await? .and_then(|provider_id| get_provider_by_id(&provider_id)); - let socks5_config = Socks5Config::from_database(context).await?; + let socks5_config = Socks5Config::from_database(&context.sql).await?; Ok(LoginParam { addr, diff --git a/src/oauth2.rs b/src/oauth2.rs index bde9f2240..70c58c1d2 100644 --- a/src/oauth2.rs +++ b/src/oauth2.rs @@ -12,6 +12,7 @@ use crate::config::Config; use crate::context::Context; use crate::provider; use crate::provider::Oauth2Authorizer; +use crate::socks::Socks5Config; use crate::tools::time; const OAUTH2_GMAIL: Oauth2 = Oauth2 { @@ -158,7 +159,8 @@ pub async fn get_oauth2_access_token( } // ... and POST - let client = crate::http::get_client()?; + let socks5_config = Socks5Config::from_database(&context.sql).await?; + let client = crate::http::get_client(socks5_config)?; let response: Response = match client.post(post_url).form(&post_param).send().await { Ok(resp) => match resp.json().await { @@ -284,7 +286,8 @@ impl Oauth2 { // "verified_email": true, // "picture": "https://lh4.googleusercontent.com/-Gj5jh_9R0BY/AAAAAAAAAAI/AAAAAAAAAAA/IAjtjfjtjNA/photo.jpg" // } - let client = match crate::http::get_client() { + let socks5_config = Socks5Config::from_database(&context.sql).await.ok()?; + let client = match crate::http::get_client(socks5_config) { Ok(cl) => cl, Err(err) => { warn!(context, "failed to get HTTP client: {}", err); diff --git a/src/qr.rs b/src/qr.rs index 4fa8138cf..b9385aede 100644 --- a/src/qr.rs +++ b/src/qr.rs @@ -22,6 +22,7 @@ use crate::context::Context; use crate::key::Fingerprint; use crate::message::Message; use crate::peerstate::Peerstate; +use crate::socks::Socks5Config; use crate::tools::time; use crate::{token, EventType}; @@ -377,7 +378,11 @@ struct CreateAccountErrorResponse { #[allow(clippy::indexing_slicing)] async fn set_account_from_qr(context: &Context, qr: &str) -> Result<()> { let url_str = &qr[DCACCOUNT_SCHEME.len()..]; - let response = crate::http::get_client()?.post(url_str).send().await?; + let socks5_config = Socks5Config::from_database(&context.sql).await?; + let response = crate::http::get_client(socks5_config)? + .post(url_str) + .send() + .await?; let response_status = response.status(); let response_text = response.text().await.with_context(|| { format!("Cannot create account, request to {url_str:?} failed: empty response") diff --git a/src/socks.rs b/src/socks.rs index 5fc933f94..f409008ff 100644 --- a/src/socks.rs +++ b/src/socks.rs @@ -9,11 +9,13 @@ use fast_socks5::client::{Config, Socks5Stream}; use fast_socks5::util::target_addr::ToTargetAddr; use fast_socks5::AuthenticationMethod; use fast_socks5::Socks5Command; +use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC}; use tokio::net::TcpStream; use tokio_io_timeout::TimeoutStream; use crate::context::Context; use crate::net::connect_tcp; +use crate::sql::Sql; #[derive(Default, Debug, Clone, PartialEq, Eq)] pub struct Socks5Config { @@ -24,9 +26,7 @@ pub struct Socks5Config { impl Socks5Config { /// Reads SOCKS5 configuration from the database. - pub async fn from_database(context: &Context) -> Result> { - let sql = &context.sql; - + pub async fn from_database(sql: &Sql) -> Result> { let enabled = sql.get_raw_config_bool("socks5_enabled").await?; if enabled { let host = sql.get_raw_config("socks5_host").await?.unwrap_or_default(); @@ -55,6 +55,20 @@ impl Socks5Config { } } + /// Converts SOCKS5 configuration into URL. + pub fn to_url(&self) -> String { + // `socks5h` means that hostname is resolved into address by the proxy + // and DNS requests should not leak. + let mut url = "socks5h://".to_string(); + if let Some((username, password)) = &self.user_password { + let username_urlencoded = utf8_percent_encode(username, NON_ALPHANUMERIC).to_string(); + let password_urlencoded = utf8_percent_encode(password, NON_ALPHANUMERIC).to_string(); + url += &format!("{username_urlencoded}:{password_urlencoded}@"); + } + url += &format!("{}:{}", self.host, self.port); + url + } + /// If `load_dns_cache` is true, loads cached DNS resolution results. /// Use this only if the connection is going to be protected with TLS checks. pub async fn connect( @@ -103,3 +117,35 @@ impl fmt::Display for Socks5Config { ) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_socks5h_url() { + let config = Socks5Config { + host: "127.0.0.1".to_string(), + port: 9050, + user_password: None, + }; + assert_eq!(config.to_url(), "socks5h://127.0.0.1:9050"); + + let config = Socks5Config { + host: "example.org".to_string(), + port: 1080, + user_password: Some(("root".to_string(), "toor".to_string())), + }; + assert_eq!(config.to_url(), "socks5h://root:toor@example.org:1080"); + + let config = Socks5Config { + host: "example.org".to_string(), + port: 1080, + user_password: Some(("root".to_string(), "foo/?\\@".to_string())), + }; + assert_eq!( + config.to_url(), + "socks5h://root:foo%2F%3F%5C%40@example.org:1080" + ); + } +}