diff --git a/src/constants.rs b/src/constants.rs index 085137341..affd7700d 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -4,12 +4,16 @@ use deltachat_derive::{FromSql, ToSql}; use once_cell::sync::Lazy; +use percent_encoding::{AsciiSet, NON_ALPHANUMERIC}; use serde::{Deserialize, Serialize}; use crate::chat::ChatId; pub static DC_VERSION_STR: Lazy = Lazy::new(|| env!("CARGO_PKG_VERSION").to_string()); +/// Set of characters to percent-encode in email addresses and names. +pub(crate) const NON_ALPHANUMERIC_WITHOUT_DOT: &AsciiSet = &NON_ALPHANUMERIC.remove(b'.'); + #[derive( Debug, Default, diff --git a/src/net/proxy.rs b/src/net/proxy.rs index ef121d222..f0a9c32c4 100644 --- a/src/net/proxy.rs +++ b/src/net/proxy.rs @@ -12,13 +12,14 @@ use fast_socks5::client::Socks5Stream; use fast_socks5::util::target_addr::ToTargetAddr; use fast_socks5::AuthenticationMethod; use fast_socks5::Socks5Command; -use percent_encoding::{percent_encode, NON_ALPHANUMERIC}; +use percent_encoding::{percent_encode, utf8_percent_encode, NON_ALPHANUMERIC}; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::net::TcpStream; use tokio_io_timeout::TimeoutStream; use url::Url; use crate::config::Config; +use crate::constants::NON_ALPHANUMERIC_WITHOUT_DOT; use crate::context::Context; use crate::net::connect_tcp; use crate::net::session::SessionStream; @@ -41,6 +42,12 @@ impl PartialEq for ShadowsocksConfig { impl Eq for ShadowsocksConfig {} +impl ShadowsocksConfig { + fn to_url(&self) -> String { + self.server_config.to_url() + } +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct HttpConfig { /// HTTP proxy host. @@ -84,6 +91,17 @@ impl HttpConfig { }; Ok(http_config) } + + fn to_url(&self, scheme: &str) -> String { + let host = utf8_percent_encode(&self.host, NON_ALPHANUMERIC_WITHOUT_DOT); + if let Some((user, password)) = &self.user_password { + let user = utf8_percent_encode(user, NON_ALPHANUMERIC); + let password = utf8_percent_encode(password, NON_ALPHANUMERIC); + format!("{scheme}://{user}:{password}@{host}:{}", self.port) + } else { + format!("{scheme}://{host}:{}", self.port) + } + } } #[derive(Debug, Clone, PartialEq, Eq)] @@ -123,6 +141,17 @@ impl Socks5Config { Ok(socks_stream) } + + fn to_url(&self) -> String { + let host = utf8_percent_encode(&self.host, NON_ALPHANUMERIC_WITHOUT_DOT); + if let Some((user, password)) = &self.user_password { + let user = utf8_percent_encode(user, NON_ALPHANUMERIC); + let password = utf8_percent_encode(password, NON_ALPHANUMERIC); + format!("socks5://{user}:{password}@{host}:{}", self.port) + } else { + format!("socks5://{host}:{}", self.port) + } + } } #[derive(Debug, Clone, PartialEq, Eq)] @@ -217,7 +246,7 @@ where impl ProxyConfig { /// Creates a new proxy configuration by parsing given proxy URL. - fn from_url(url: &str) -> Result { + pub(crate) fn from_url(url: &str) -> Result { let url = Url::parse(url).context("Cannot parse proxy URL")?; match url.scheme() { "http" => { @@ -272,6 +301,19 @@ impl ProxyConfig { } } + /// Serializes proxy config into an URL. + /// + /// This function can be used to normalize proxy URL + /// by parsing it and serializing back. + pub(crate) fn to_url(&self) -> String { + match self { + Self::Http(http_config) => http_config.to_url("http"), + Self::Https(http_config) => http_config.to_url("https"), + Self::Socks5(socks5_config) => socks5_config.to_url(), + Self::Shadowsocks(shadowsocks_config) => shadowsocks_config.to_url(), + } + } + /// Migrates legacy `socks5_host`, `socks5_port`, `socks5_user` and `socks5_password` /// config into `proxy_url` if `proxy_url` is unset or empty. /// diff --git a/src/qr.rs b/src/qr.rs index 36bcfb882..22cfa2b95 100644 --- a/src/qr.rs +++ b/src/qr.rs @@ -20,7 +20,7 @@ use crate::events::EventType; use crate::key::Fingerprint; use crate::message::Message; use crate::net::http::post_empty; -use crate::net::proxy::DEFAULT_SOCKS_PORT; +use crate::net::proxy::{ProxyConfig, DEFAULT_SOCKS_PORT}; use crate::peerstate::Peerstate; use crate::token; use crate::tools::validate_id; @@ -723,6 +723,10 @@ pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<()> { .get_config(Config::ProxyUrl) .await? .unwrap_or_default(); + + // Normalize the URL. + let url = ProxyConfig::from_url(&url)?.to_url(); + let proxy_urls: Vec<&str> = std::iter::once(url.as_str()) .chain( old_proxy_url_value @@ -1787,6 +1791,17 @@ mod tests { ) ); + // SOCKS5 config does not have port 1080 explicitly specified, + // but should bring `socks5://1.2.3.4:1080` to the top instead of creating another entry. + set_config_from_qr(&t, "socks5://1.2.3.4").await?; + assert_eq!( + t.get_config(Config::ProxyUrl).await?, + Some( + "socks5://1.2.3.4:1080\nss://YWVzLTEyOC1nY206dGVzdA@192.168.100.1:8888#Example1\nsocks5://foo:666\nsocks5://Da:x%26%25%24X@jau:1080" + .to_string() + ) + ); + Ok(()) } diff --git a/src/securejoin.rs b/src/securejoin.rs index 5a25ad72b..3b2a7ecb4 100644 --- a/src/securejoin.rs +++ b/src/securejoin.rs @@ -1,13 +1,13 @@ //! Implementation of [SecureJoin protocols](https://securejoin.delta.chat/). use anyhow::{ensure, Context as _, Error, Result}; -use percent_encoding::{utf8_percent_encode, AsciiSet, NON_ALPHANUMERIC}; +use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC}; use crate::aheader::EncryptPreference; use crate::chat::{self, get_chat_id_by_grpid, Chat, ChatId, ChatIdBlocked, ProtectionStatus}; use crate::chatlist_events; use crate::config::Config; -use crate::constants::{Blocked, Chattype}; +use crate::constants::{Blocked, Chattype, NON_ALPHANUMERIC_WITHOUT_DOT}; use crate::contact::{Contact, ContactId, Origin}; use crate::context::Context; use crate::e2ee::ensure_secret_key_exists; @@ -34,9 +34,6 @@ use qrinvite::QrInvite; use crate::token::Namespace; -/// Set of characters to percent-encode in email addresses and names. -pub const NON_ALPHANUMERIC_WITHOUT_DOT: &AsciiSet = &NON_ALPHANUMERIC.remove(b'.'); - fn inviter_progress(context: &Context, contact_id: ContactId, progress: usize) { debug_assert!( progress <= 1000,