diff --git a/Cargo.lock b/Cargo.lock index b072f346b..5c872d785 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -551,6 +551,19 @@ dependencies = [ "digest", ] +[[package]] +name = "blake3" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d82033247fd8e890df8f740e407ad4d038debb9eb1f40533fffb32e7d17dc6f7" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -631,6 +644,12 @@ version = "3.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ea184aa71bb362a1157c896979544cc23974e08fd265f29ea96b59f0b4a555b" +[[package]] +name = "byte_string" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11aade7a05aa8c3a351cedc44c3fc45806430543382fcc4743a9b757a2a0b4ed" + [[package]] name = "bytemuck" version = "1.16.3" @@ -716,9 +735,12 @@ dependencies = [ [[package]] name = "cc" -version = "1.1.7" +version = "1.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26a5c3fd7bfa1ce3897a3a3501d362b2d87b7f2583ebcb4a949ec25911025cbc" +checksum = "e9d013ecb737093c0e86b151a7b837993cf9ec6c502946cfb44bedc392421e0b" +dependencies = [ + "shlex", +] [[package]] name = "cfb-mode" @@ -752,6 +774,19 @@ dependencies = [ "cpufeatures", ] +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", + "zeroize", +] + [[package]] name = "charset" version = "0.1.3" @@ -774,7 +809,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -1288,6 +1323,7 @@ dependencies = [ "parking_lot", "percent-encoding", "pgp", + "pin-project", "pretty_assertions", "proptest", "qrcodegen", @@ -1303,6 +1339,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "sha-1", + "shadowsocks", "smallvec", "strum", "strum_macros", @@ -3294,6 +3331,12 @@ dependencies = [ "linked-hash-map", ] +[[package]] +name = "lru_time_cache" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9106e1d747ffd48e6be5bb2d97fa706ed25b144fbee4d5c02eae110cd8d6badd" + [[package]] name = "mailparse" version = "0.15.0" @@ -4811,6 +4854,25 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "ring-compat" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccce7bae150b815f0811db41b8312fcb74bffa4cab9cee5429ee00f356dd5bd4" +dependencies = [ + "aead", + "digest", + "ecdsa", + "ed25519", + "generic-array", + "p256", + "p384", + "pkcs8", + "rand_core 0.6.4", + "ring", + "signature", +] + [[package]] name = "ripemd" version = "0.1.3" @@ -5176,6 +5238,16 @@ dependencies = [ "serde", ] +[[package]] +name = "sendfd" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "604b71b8fc267e13bb3023a2c901126c8f349393666a6d98ac1ae5729b701798" +dependencies = [ + "libc", + "tokio", +] + [[package]] name = "serde" version = "1.0.209" @@ -5322,6 +5394,60 @@ dependencies = [ "keccak", ] +[[package]] +name = "shadowsocks" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06b6af20f0f009894644c9fb149ce6244c69b0a264ffcf7a53cbb3dd4883e4a3" +dependencies = [ + "aes", + "async-trait", + "base64 0.22.1", + "blake3", + "byte_string", + "bytes", + "cfg-if", + "futures", + "libc", + "log", + "lru_time_cache", + "once_cell", + "percent-encoding", + "pin-project", + "rand 0.8.5", + "sendfd", + "serde", + "serde_json", + "serde_urlencoded", + "shadowsocks-crypto", + "socket2", + "spin 0.9.8", + "thiserror", + "tokio", + "tokio-tfo", + "url", + "windows-sys 0.59.0", +] + +[[package]] +name = "shadowsocks-crypto" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e49ecfad8b27f3df28848af11f08aa10df0c6b74b45748131753913be23373" +dependencies = [ + "aes", + "aes-gcm", + "blake3", + "bytes", + "cfg-if", + "chacha20poly1305", + "hkdf", + "md-5", + "rand 0.8.5", + "ring-compat", + "sha1", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -5341,6 +5467,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "signal-hook-registry" version = "1.4.1" @@ -5917,6 +6049,23 @@ dependencies = [ "xattr", ] +[[package]] +name = "tokio-tfo" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb4382c6371e29365853d2b71e915d5398df46312a2158097d8bb3f54d0f1b4" +dependencies = [ + "cfg-if", + "futures", + "libc", + "log", + "once_cell", + "pin-project", + "socket2", + "tokio", + "windows-sys 0.52.0", +] + [[package]] name = "tokio-tungstenite" version = "0.21.0" @@ -6531,7 +6680,7 @@ dependencies = [ "windows-core 0.52.0", "windows-implement", "windows-interface", - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -6549,7 +6698,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -6589,7 +6738,16 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.5", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", ] [[package]] @@ -6609,18 +6767,18 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.5", - "windows_aarch64_msvc 0.52.5", - "windows_i686_gnu 0.52.5", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", "windows_i686_gnullvm", - "windows_i686_msvc 0.52.5", - "windows_x86_64_gnu 0.52.5", - "windows_x86_64_gnullvm 0.52.5", - "windows_x86_64_msvc 0.52.5", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] [[package]] @@ -6631,9 +6789,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" @@ -6643,9 +6801,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" @@ -6655,15 +6813,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" @@ -6673,9 +6831,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" @@ -6685,9 +6843,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" @@ -6697,9 +6855,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" @@ -6709,9 +6867,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" diff --git a/Cargo.toml b/Cargo.toml index 4e8e337b6..730490d5c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -77,6 +77,7 @@ once_cell = { workspace = true } percent-encoding = "2.3" parking_lot = "0.12" pgp = { version = "0.13.2", default-features = false } +pin-project = "1" qrcodegen = "1.7.0" quick-xml = "0.36" quoted_printable = "0.5" @@ -89,6 +90,7 @@ serde_json = { workspace = true } serde_urlencoded = "0.7.1" serde = { workspace = true, features = ["derive"] } sha-1 = "0.10" +shadowsocks = { version = "1.20.2", default-features = false, features = ["aead-cipher-2022"] } smallvec = "1.13.2" strum = "0.26" strum_macros = "0.26" diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index f83f3fe8f..c3e1608cb 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -403,11 +403,8 @@ char* dc_get_blobdir (const dc_context_t* context); * - `send_port` = SMTP-port, guessed if left out * - `send_security`= SMTP-socket, one of @ref DC_SOCKET, defaults to #DC_SOCKET_AUTO * - `server_flags` = IMAP-/SMTP-flags as a combination of @ref DC_LP flags, guessed if left out - * - `socks5_enabled` = SOCKS5 enabled - * - `socks5_host` = SOCKS5 proxy server host - * - `socks5_port` = SOCKS5 proxy server port - * - `socks5_user` = SOCKS5 proxy username - * - `socks5_password` = SOCKS5 proxy password + * - `proxy_enabled` = Proxy enabled. Disabled by default. + * - `proxy_url` = Proxy URL. May contain multiple URLs separated by newline, but only the first one is used. * - `imap_certificate_checks` = how to check IMAP certificates, one of the @ref DC_CERTCK flags, defaults to #DC_CERTCK_AUTO (0) * - `smtp_certificate_checks` = deprecated option, should be set to the same value as `imap_certificate_checks` but ignored by the new core * - `displayname` = Own name to use when sending messages. MUAs are allowed to spread this way e.g. using CC, defaults to empty diff --git a/deltachat-ffi/src/lib.rs b/deltachat-ffi/src/lib.rs index 4e2b0745e..29ac3a827 100644 --- a/deltachat-ffi/src/lib.rs +++ b/deltachat-ffi/src/lib.rs @@ -4537,19 +4537,16 @@ pub unsafe extern "C" fn dc_provider_new_from_email_with_dns( let addr = to_string_lossy(addr); let ctx = &*context; - let socks5_enabled = block_on(async move { - ctx.get_config_bool(config::Config::Socks5Enabled) - .await - .context("Can't get config") - .log_err(ctx) - }); + let proxy_enabled = block_on(ctx.get_config_bool(config::Config::ProxyEnabled)) + .context("Can't get config") + .log_err(ctx); - match socks5_enabled { - Ok(socks5_enabled) => { + match proxy_enabled { + Ok(proxy_enabled) => { match block_on(provider::get_provider_info_by_addr( ctx, addr.as_str(), - socks5_enabled, + proxy_enabled, )) .log_err(ctx) .unwrap_or_default() diff --git a/deltachat-jsonrpc/src/api.rs b/deltachat-jsonrpc/src/api.rs index f3e20874c..6ce7e9da7 100644 --- a/deltachat-jsonrpc/src/api.rs +++ b/deltachat-jsonrpc/src/api.rs @@ -321,12 +321,12 @@ impl CommandApi { ) -> Result> { let ctx = self.get_context(account_id).await?; - let socks5_enabled = ctx - .get_config_bool(deltachat::config::Config::Socks5Enabled) + let proxy_enabled = ctx + .get_config_bool(deltachat::config::Config::ProxyEnabled) .await?; let provider_info = - get_provider_info(&ctx, email.split('@').last().unwrap_or(""), socks5_enabled).await; + get_provider_info(&ctx, email.split('@').last().unwrap_or(""), proxy_enabled).await; Ok(ProviderInfo::from_dc_type(provider_info)) } diff --git a/deltachat-jsonrpc/src/lib.rs b/deltachat-jsonrpc/src/lib.rs index 6c262ea87..eb633cbd3 100644 --- a/deltachat-jsonrpc/src/lib.rs +++ b/deltachat-jsonrpc/src/lib.rs @@ -83,7 +83,7 @@ mod tests { assert_eq!(result, response.to_owned()); } { - let request = r#"{"jsonrpc":"2.0","method":"batch_set_config","id":2,"params":[1,{"addr":"","mail_user":"","mail_pw":"","mail_server":"","mail_port":"","mail_security":"","imap_certificate_checks":"","send_user":"","send_pw":"","send_server":"","send_port":"","send_security":"","smtp_certificate_checks":"","socks5_enabled":"0","socks5_host":"","socks5_port":"","socks5_user":"","socks5_password":""}]}"#; + let request = r#"{"jsonrpc":"2.0","method":"batch_set_config","id":2,"params":[1,{"addr":"","mail_user":"","mail_pw":"","mail_server":"","mail_port":"","mail_security":"","imap_certificate_checks":"","send_user":"","send_pw":"","send_server":"","send_port":"","send_security":"","smtp_certificate_checks":""}]}"#; let response = r#"{"jsonrpc":"2.0","id":2,"result":null}"#; session.handle_incoming(request).await; let result = receiver.recv().await?; diff --git a/deltachat-repl/src/cmdline.rs b/deltachat-repl/src/cmdline.rs index d754735d0..2749ea39c 100644 --- a/deltachat-repl/src/cmdline.rs +++ b/deltachat-repl/src/cmdline.rs @@ -1249,10 +1249,10 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu } "providerinfo" => { ensure!(!arg1.is_empty(), "Argument missing."); - let socks5_enabled = context - .get_config_bool(config::Config::Socks5Enabled) + let proxy_enabled = context + .get_config_bool(config::Config::ProxyEnabled) .await?; - match provider::get_provider_info(&context, arg1, socks5_enabled).await { + match provider::get_provider_info(&context, arg1, proxy_enabled).await { Some(info) => { println!("Information for provider belonging to {arg1}:"); println!("status: {}", info.status as u32); diff --git a/deltachat-rpc-client/tests/test_something.py b/deltachat-rpc-client/tests/test_something.py index 0e74650b8..7f1a15352 100644 --- a/deltachat-rpc-client/tests/test_something.py +++ b/deltachat-rpc-client/tests/test_something.py @@ -433,7 +433,7 @@ def test_provider_info(rpc) -> None: assert provider_info["id"] == "gmail" # Disable MX record resolution. - rpc.set_config(account_id, "socks5_enabled", "1") + rpc.set_config(account_id, "proxy_enabled", "1") provider_info = rpc.get_provider_info(account_id, "github.com") assert provider_info is None diff --git a/deny.toml b/deny.toml index 635ab6b05..e58a8141e 100644 --- a/deny.toml +++ b/deny.toml @@ -69,7 +69,7 @@ skip = [ { name = "windows-core", version = "<0.54.0" }, { name = "windows_i686_gnu", version = "<0.52" }, { name = "windows_i686_msvc", version = "<0.52" }, - { name = "windows-sys", version = "<0.52" }, + { name = "windows-sys", version = "<0.59" }, { name = "windows-targets", version = "<0.52" }, { name = "windows", version = "<0.54.0" }, { name = "windows_x86_64_gnullvm", version = "<0.52" }, diff --git a/node/test/test.mjs b/node/test/test.mjs index 2a0860040..5c9d67ef7 100644 --- a/node/test/test.mjs +++ b/node/test/test.mjs @@ -271,7 +271,7 @@ describe('Basic offline Tests', function () { 'sync_msgs', 'sentbox_watch', 'show_emails', - 'socks5_enabled', + 'proxy_enabled', 'sqlite_version', 'uptime', 'used_account_settings', diff --git a/src/config.rs b/src/config.rs index e547e6b00..1342bad6f 100644 --- a/src/config.rs +++ b/src/config.rs @@ -91,21 +91,43 @@ pub enum Config { /// Should not be extended in the future, create new config keys instead. ServerFlags, + /// True if proxy is enabled. + /// + /// Can be used to disable proxy without erasing known URLs. + ProxyEnabled, + + /// Proxy URL. + /// + /// Supported URLs schemes are `socks5://` (SOCKS5) and `ss://` (Shadowsocks). + /// + /// May contain multiple URLs separated by newline, in which case the first one is used. + ProxyUrl, + /// True if SOCKS5 is enabled. /// /// Can be used to disable SOCKS5 without erasing SOCKS5 configuration. + /// + /// Deprecated in favor of `ProxyEnabled`. Socks5Enabled, /// SOCKS5 proxy server hostname or address. + /// + /// Deprecated in favor of `ProxyUrl`. Socks5Host, /// SOCKS5 proxy server port. + /// + /// Deprecated in favor of `ProxyUrl`. Socks5Port, /// SOCKS5 proxy server username. + /// + /// Deprecated in favor of `ProxyUrl`. Socks5User, /// SOCKS5 proxy server password. + /// + /// Deprecated in favor of `ProxyUrl`. Socks5Password, /// Own name to use in the `From:` field when sending messages. @@ -638,6 +660,7 @@ impl Context { fn check_config(key: Config, value: Option<&str>) -> Result<()> { match key { Config::Socks5Enabled + | Config::ProxyEnabled | Config::BccSelf | Config::E2eeEnabled | Config::MdnsEnabled diff --git a/src/configure.rs b/src/configure.rs index 8e4dcdddf..d836a11eb 100644 --- a/src/configure.rs +++ b/src/configure.rs @@ -196,8 +196,8 @@ async fn get_configured_param( param.smtp.password.clone() }; - let socks5_config = param.socks5_config.clone(); - let socks5_enabled = socks5_config.is_some(); + let proxy_config = param.proxy_config.clone(); + let proxy_enabled = proxy_config.is_some(); let mut addr = param.addr.clone(); if param.oauth2 { @@ -240,7 +240,7 @@ async fn get_configured_param( "checking internal provider-info for offline autoconfig" ); - provider = provider::get_provider_info(ctx, ¶m_domain, socks5_enabled).await; + provider = provider::get_provider_info(ctx, ¶m_domain, proxy_enabled).await; if let Some(provider) = provider { if provider.server.is_empty() { info!(ctx, "Offline autoconfig found, but no servers defined."); @@ -356,7 +356,7 @@ async fn get_configured_param( .collect(), smtp_user: param.smtp.user.clone(), smtp_password, - socks5_config: param.socks5_config.clone(), + proxy_config: param.proxy_config.clone(), provider, certificate_checks: match param.certificate_checks { EnteredCertificateChecks::Automatic => ConfiguredCertificateChecks::Automatic, @@ -388,7 +388,7 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result Result Result".to_string()), ); res.insert("is_configured", is_configured.to_string()); - res.insert("socks5_enabled", socks5_enabled.to_string()); + res.insert("proxy_enabled", proxy_enabled.to_string()); res.insert("entered_account_settings", l.to_string()); res.insert("used_account_settings", l2); @@ -1693,6 +1693,8 @@ mod tests { "server_flags", "skip_start_messages", "smtp_certificate_checks", + "proxy_url", // May contain passwords, don't leak it to the logs. + "socks5_enabled", // SOCKS5 options are deprecated. "socks5_host", "socks5_port", "socks5_user", diff --git a/src/imap.rs b/src/imap.rs index 4391cf3c0..39724c5a1 100644 --- a/src/imap.rs +++ b/src/imap.rs @@ -37,12 +37,12 @@ use crate::login_param::{ }; use crate::message::{self, Message, MessageState, MessengerMessage, MsgId, Viewtype}; use crate::mimeparser; +use crate::net::proxy::ProxyConfig; use crate::oauth2::get_oauth2_access_token; use crate::receive_imf::{ from_field_to_contact_id, get_prefetch_parent_message, receive_imf_inner, ReceivedMsg, }; use crate::scheduler::connectivity::ConnectivityStore; -use crate::socks::Socks5Config; use crate::sql; use crate::stock_str; use crate::tools::{self, create_id, duration_to_str}; @@ -80,8 +80,9 @@ pub(crate) struct Imap { /// Password. password: String, - /// SOCKS 5 configuration. - socks5_config: Option, + /// Proxy configuration. + proxy_config: Option, + strict_tls: bool, oauth2: bool, @@ -237,7 +238,7 @@ impl Imap { pub fn new( lp: Vec, password: String, - socks5_config: Option, + proxy_config: Option, addr: &str, strict_tls: bool, oauth2: bool, @@ -248,7 +249,7 @@ impl Imap { addr: addr.to_string(), lp, password, - socks5_config, + proxy_config, strict_tls, oauth2, login_failed_once: false, @@ -271,7 +272,7 @@ impl Imap { let imap = Self::new( param.imap.clone(), param.imap_password.clone(), - param.socks5_config.clone(), + param.proxy_config.clone(), ¶m.addr, param.strict_tls(), param.oauth2, @@ -336,7 +337,7 @@ impl Imap { let connection_candidate = lp.connection.clone(); let client = match Client::connect( context, - self.socks5_config.clone(), + self.proxy_config.clone(), self.strict_tls, connection_candidate, ) diff --git a/src/imap/client.rs b/src/imap/client.rs index b6b6ffeb3..a52d337dc 100644 --- a/src/imap/client.rs +++ b/src/imap/client.rs @@ -4,7 +4,6 @@ use std::ops::{Deref, DerefMut}; use anyhow::{Context as _, Result}; use async_imap::Client as ImapClient; use async_imap::Session as ImapSession; -use fast_socks5::client::Socks5Stream; use tokio::io::BufWriter; use super::capabilities::Capabilities; @@ -12,12 +11,12 @@ use super::session::Session; use crate::context::Context; use crate::login_param::{ConnectionCandidate, ConnectionSecurity}; use crate::net::dns::{lookup_host_with_cache, update_connect_timestamp}; +use crate::net::proxy::ProxyConfig; use crate::net::session::SessionStream; use crate::net::tls::wrap_tls; use crate::net::{ connect_tcp_inner, connect_tls_inner, run_connection_attempts, update_connection_history, }; -use crate::socks::Socks5Config; use crate::tools::time; #[derive(Debug)] @@ -157,25 +156,25 @@ impl Client { pub async fn connect( context: &Context, - socks5_config: Option, + proxy_config: Option, strict_tls: bool, candidate: ConnectionCandidate, ) -> Result { let host = &candidate.host; let port = candidate.port; let security = candidate.security; - if let Some(socks5_config) = socks5_config { + if let Some(proxy_config) = proxy_config { let client = match security { ConnectionSecurity::Tls => { - Client::connect_secure_socks5(context, host, port, strict_tls, socks5_config) + Client::connect_secure_proxy(context, host, port, strict_tls, proxy_config) .await? } ConnectionSecurity::Starttls => { - Client::connect_starttls_socks5(context, host, port, socks5_config, strict_tls) + Client::connect_starttls_proxy(context, host, port, proxy_config, strict_tls) .await? } ConnectionSecurity::Plain => { - Client::connect_insecure_socks5(context, host, port, socks5_config).await? + Client::connect_insecure_proxy(context, host, port, proxy_config).await? } }; Ok(client) @@ -249,17 +248,17 @@ impl Client { Ok(client) } - async fn connect_secure_socks5( + async fn connect_secure_proxy( context: &Context, domain: &str, port: u16, strict_tls: bool, - socks5_config: Socks5Config, + proxy_config: ProxyConfig, ) -> Result { - let socks5_stream = socks5_config + let proxy_stream = proxy_config .connect(context, domain, port, strict_tls) .await?; - let tls_stream = wrap_tls(strict_tls, domain, alpn(port), socks5_stream).await?; + let tls_stream = wrap_tls(strict_tls, domain, alpn(port), proxy_stream).await?; let buffered_stream = BufWriter::new(tls_stream); let session_stream: Box = Box::new(buffered_stream); let mut client = Client::new(session_stream); @@ -270,14 +269,14 @@ impl Client { Ok(client) } - async fn connect_insecure_socks5( + async fn connect_insecure_proxy( context: &Context, domain: &str, port: u16, - socks5_config: Socks5Config, + proxy_config: ProxyConfig, ) -> Result { - let socks5_stream = socks5_config.connect(context, domain, port, false).await?; - let buffered_stream = BufWriter::new(socks5_stream); + let proxy_stream = proxy_config.connect(context, domain, port, false).await?; + let buffered_stream = BufWriter::new(proxy_stream); let session_stream: Box = Box::new(buffered_stream); let mut client = Client::new(session_stream); let _greeting = client @@ -287,20 +286,20 @@ impl Client { Ok(client) } - async fn connect_starttls_socks5( + async fn connect_starttls_proxy( context: &Context, hostname: &str, port: u16, - socks5_config: Socks5Config, + proxy_config: ProxyConfig, strict_tls: bool, ) -> Result { - let socks5_stream = socks5_config + let proxy_stream = proxy_config .connect(context, hostname, port, strict_tls) .await?; // Run STARTTLS command and convert the client back into a stream. - let buffered_socks5_stream = BufWriter::new(socks5_stream); - let mut client = ImapClient::new(buffered_socks5_stream); + let buffered_proxy_stream = BufWriter::new(proxy_stream); + let mut client = ImapClient::new(buffered_proxy_stream); let _greeting = client .read_response() .await @@ -309,10 +308,10 @@ impl Client { .run_command_and_check_ok("STARTTLS", None) .await .context("STARTTLS command failed")?; - let buffered_socks5_stream = client.into_inner(); - let socks5_stream: Socks5Stream<_> = buffered_socks5_stream.into_inner(); + let buffered_proxy_stream = client.into_inner(); + let proxy_stream = buffered_proxy_stream.into_inner(); - let tls_stream = wrap_tls(strict_tls, hostname, &[], socks5_stream) + let tls_stream = wrap_tls(strict_tls, hostname, &[], proxy_stream) .await .context("STARTTLS upgrade failed")?; let buffered_stream = BufWriter::new(tls_stream); diff --git a/src/lib.rs b/src/lib.rs index 4f6bafb9a..2c35c8389 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -84,7 +84,6 @@ mod scheduler; pub mod securejoin; mod simplify; mod smtp; -mod socks; pub mod stock_str; mod sync; mod timesmearing; diff --git a/src/login_param.rs b/src/login_param.rs index 90a41d774..7ee7887a9 100644 --- a/src/login_param.rs +++ b/src/login_param.rs @@ -11,8 +11,8 @@ use crate::configure::server_params::{expand_param_vector, ServerParams}; use crate::constants::{DC_LP_AUTH_FLAGS, DC_LP_AUTH_NORMAL, DC_LP_AUTH_OAUTH2}; use crate::context::Context; use crate::net::load_connection_timestamp; +use crate::net::proxy::ProxyConfig; use crate::provider::{Protocol, Provider, Socket, UsernamePattern}; -use crate::socks::Socks5Config; use crate::sql::Sql; /// User-entered setting for certificate checks. @@ -116,7 +116,8 @@ pub struct EnteredLoginParam { /// invalid hostnames pub certificate_checks: EnteredCertificateChecks, - pub socks5_config: Option, + /// Proxy configuration. + pub proxy_config: Option, pub oauth2: bool, } @@ -195,7 +196,7 @@ impl EnteredLoginParam { .unwrap_or_default(); let oauth2 = matches!(server_flags & DC_LP_AUTH_FLAGS, DC_LP_AUTH_OAUTH2); - let socks5_config = Socks5Config::from_database(&context.sql).await?; + let proxy_config = ProxyConfig::load(context).await?; Ok(EnteredLoginParam { addr, @@ -214,7 +215,7 @@ impl EnteredLoginParam { password: send_pw, }, certificate_checks, - socks5_config, + proxy_config, oauth2, }) } @@ -380,7 +381,8 @@ pub struct ConfiguredLoginParam { pub smtp_password: String, - pub socks5_config: Option, + /// Proxy configuration. + pub proxy_config: Option, pub provider: Option<&'static Provider>, @@ -679,7 +681,7 @@ impl ConfiguredLoginParam { }]; } - let socks5_config = Socks5Config::from_database(&context.sql).await?; + let proxy_config = ProxyConfig::load(context).await?; Ok(Some(ConfiguredLoginParam { addr, @@ -691,7 +693,7 @@ impl ConfiguredLoginParam { smtp_password: send_pw, certificate_checks, provider, - socks5_config, + proxy_config, oauth2, })) } @@ -778,7 +780,7 @@ impl ConfiguredLoginParam { let provider_strict_tls = self.provider.map(|provider| provider.opt.strict_tls); match self.certificate_checks { ConfiguredCertificateChecks::OldAutomatic => { - provider_strict_tls.unwrap_or(self.socks5_config.is_some()) + provider_strict_tls.unwrap_or(self.proxy_config.is_some()) } ConfiguredCertificateChecks::Automatic => provider_strict_tls.unwrap_or(true), ConfiguredCertificateChecks::Strict => true, @@ -863,8 +865,8 @@ mod tests { }], smtp_user: "".to_string(), smtp_password: "bar".to_string(), - // socks5_config is not saved by `save_to_database`, using default value - socks5_config: None, + // proxy_config is not saved by `save_to_database`, using default value + proxy_config: None, provider: None, certificate_checks: ConfiguredCertificateChecks::Strict, oauth2: false, @@ -967,7 +969,7 @@ mod tests { ], smtp_user: "alice@posteo.de".to_string(), smtp_password: "foobarbaz".to_string(), - socks5_config: None, + proxy_config: None, provider: get_provider_by_id("posteo"), certificate_checks: ConfiguredCertificateChecks::Strict, oauth2: false, diff --git a/src/net.rs b/src/net.rs index ae534b5cb..20016d3f6 100644 --- a/src/net.rs +++ b/src/net.rs @@ -17,6 +17,7 @@ use crate::tools::time; pub(crate) mod dns; pub(crate) mod http; +pub(crate) mod proxy; pub(crate) mod session; pub(crate) mod tls; diff --git a/src/net/http.rs b/src/net/http.rs index 9dbf1fc5f..6b7bdcbb2 100644 --- a/src/net/http.rs +++ b/src/net/http.rs @@ -8,9 +8,9 @@ use mime::Mime; use serde::Serialize; use crate::context::Context; +use crate::net::proxy::ProxyConfig; use crate::net::session::SessionStream; use crate::net::tls::wrap_tls; -use crate::socks::Socks5Config; /// HTTP(S) GET response. #[derive(Debug)] @@ -43,7 +43,7 @@ where { let scheme = parsed_url.scheme_str().context("URL has no scheme")?; let host = parsed_url.host().context("URL has no host")?; - let socks5_config_opt = Socks5Config::from_database(&context.sql).await?; + let proxy_config_opt = ProxyConfig::load(context).await?; let stream: Box = match scheme { "http" => { @@ -54,11 +54,11 @@ where // better resolve from scratch each time to prevent // cache poisoning attacks from having lasting effects. let load_cache = false; - if let Some(socks5_config) = socks5_config_opt { - let socks5_stream = socks5_config + if let Some(proxy_config) = proxy_config_opt { + let proxy_stream = proxy_config .connect(context, host, port, load_cache) .await?; - Box::new(socks5_stream) + Box::new(proxy_stream) } else { let tcp_stream = crate::net::connect_tcp(context, host, port, load_cache).await?; Box::new(tcp_stream) @@ -69,11 +69,11 @@ where let load_cache = true; let strict_tls = true; - if let Some(socks5_config) = socks5_config_opt { - let socks5_stream = socks5_config + if let Some(proxy_config) = proxy_config_opt { + let proxy_stream = proxy_config .connect(context, host, port, load_cache) .await?; - let tls_stream = wrap_tls(strict_tls, host, &[], socks5_stream).await?; + let tls_stream = wrap_tls(strict_tls, host, &[], proxy_stream).await?; Box::new(tls_stream) } else { let tcp_stream = crate::net::connect_tcp(context, host, port, load_cache).await?; diff --git a/src/net/proxy.rs b/src/net/proxy.rs new file mode 100644 index 000000000..aab81d3d5 --- /dev/null +++ b/src/net/proxy.rs @@ -0,0 +1,435 @@ +//! # Proxy support. +//! +//! Delta Chat supports SOCKS5 and Shadowsocks protocols. + +use std::fmt; +use std::pin::Pin; + +use anyhow::{format_err, Context as _, Result}; +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 pin_project::pin_project; +use tokio::io::{AsyncRead, AsyncWrite}; +use tokio::net::TcpStream; +use tokio_io_timeout::TimeoutStream; + +use crate::config::Config; +use crate::context::Context; +use crate::net::connect_tcp; +use crate::net::session::SessionStream; +use crate::sql::Sql; + +/// Default SOCKS5 port according to [RFC 1928](https://tools.ietf.org/html/rfc1928). +pub const DEFAULT_SOCKS_PORT: u16 = 1080; + +#[derive(Debug, Clone)] +pub struct ShadowsocksConfig { + pub server_config: shadowsocks::config::ServerConfig, +} + +impl PartialEq for ShadowsocksConfig { + fn eq(&self, other: &Self) -> bool { + self.server_config.to_url() == other.server_config.to_url() + } +} + +impl Eq for ShadowsocksConfig {} + +/// Wrapper for Shadowsocks stream implementing +/// `Debug` and `SessionStream`. +/// +/// Passes `AsyncRead` and `AsyncWrite` traits through. +#[pin_project] +pub(crate) struct ShadowsocksStream { + #[pin] + pub(crate) stream: shadowsocks::ProxyClientStream, +} + +impl std::fmt::Debug for ShadowsocksStream { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "ShadowsocksStream") + } +} + +impl AsyncRead for ShadowsocksStream +where + S: AsyncRead + AsyncWrite + Unpin, +{ + fn poll_read( + self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &mut tokio::io::ReadBuf<'_>, + ) -> std::task::Poll> { + self.project().stream.poll_read(cx, buf) + } +} + +impl AsyncWrite for ShadowsocksStream +where + S: AsyncRead + AsyncWrite + Unpin, +{ + fn poll_write( + self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &[u8], + ) -> std::task::Poll> { + self.project().stream.poll_write(cx, buf) + } + + fn poll_flush( + self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + self.project().stream.poll_flush(cx) + } + + fn poll_shutdown( + self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + self.project().stream.poll_shutdown(cx) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Socks5Config { + pub host: String, + pub port: u16, + pub user_password: Option<(String, String)>, +} + +impl Socks5Config { + async fn connect( + &self, + context: &Context, + target_host: &str, + target_port: u16, + load_dns_cache: bool, + ) -> Result>>>> { + let tcp_stream = connect_tcp(context, &self.host, self.port, load_dns_cache) + .await + .context("Failed to connect to SOCKS5 proxy")?; + + let authentication_method = if let Some((username, password)) = self.user_password.as_ref() + { + Some(AuthenticationMethod::Password { + username: username.into(), + password: password.into(), + }) + } else { + None + }; + let mut socks_stream = + Socks5Stream::use_stream(tcp_stream, authentication_method, Default::default()).await?; + let target_addr = (target_host, target_port).to_target_addr()?; + socks_stream + .request(Socks5Command::TCPConnect, target_addr) + .await?; + + Ok(socks_stream) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ProxyConfig { + Socks5(Socks5Config), + + Shadowsocks(ShadowsocksConfig), +} + +impl ProxyConfig { + /// Creates a new proxy configuration by parsing given proxy URL. + fn from_url(url: &str) -> Result { + let url = url::Url::parse(url).context("Cannot parse proxy URL")?; + match url.scheme() { + "ss" => { + let server_config = shadowsocks::config::ServerConfig::from_url(url.as_str())?; + let shadowsocks_config = ShadowsocksConfig { server_config }; + Ok(Self::Shadowsocks(shadowsocks_config)) + } + + // Because of `curl` convention, + // `socks5` URL scheme may be expected to resolve domain names locally + // with `socks5h` URL scheme meaning that hostnames are passed to the proxy. + // Resolving hostnames locally is not supported + // in Delta Chat when using a proxy + // to prevent DNS leaks. + // Because of this we do not distinguish + // between `socks5` and `socks5h`. + "socks5" => { + let host = url + .host_str() + .context("socks5 URL has no host")? + .to_string(); + let port = url.port().unwrap_or(DEFAULT_SOCKS_PORT); + let user_password = if let Some(password) = url.password() { + let username = percent_encoding::percent_decode_str(url.username()) + .decode_utf8() + .context("SOCKS5 username is not a valid UTF-8")? + .to_string(); + let password = percent_encoding::percent_decode_str(password) + .decode_utf8() + .context("SOCKS5 password is not a valid UTF-8")? + .to_string(); + Some((username, password)) + } else { + None + }; + let socks5_config = Socks5Config { + host, + port, + user_password, + }; + Ok(Self::Socks5(socks5_config)) + } + scheme => Err(format_err!("Unknown URL scheme {scheme:?}")), + } + } + + /// Migrates legacy `socks5_host`, `socks5_port`, `socks5_user` and `socks5_password` + /// config into `proxy_url` if `proxy_url` is unset or empty. + /// + /// Unsets `socks5_host`, `socks5_port`, `socks5_user` and `socks5_password` in any case. + async fn migrate_socks_config(sql: &Sql) -> Result<()> { + if sql.get_raw_config("proxy_url").await?.is_none() { + // Load legacy SOCKS5 settings. + if let Some(host) = sql + .get_raw_config("socks5_host") + .await? + .filter(|s| !s.is_empty()) + { + let port: u16 = sql + .get_raw_config_int("socks5_port") + .await? + .unwrap_or(DEFAULT_SOCKS_PORT.into()) as u16; + let user = sql.get_raw_config("socks5_user").await?.unwrap_or_default(); + let pass = sql + .get_raw_config("socks5_password") + .await? + .unwrap_or_default(); + + let mut proxy_url = "socks5://".to_string(); + if !pass.is_empty() { + proxy_url += &percent_encode(user.as_bytes(), NON_ALPHANUMERIC).to_string(); + proxy_url += ":"; + proxy_url += &percent_encode(pass.as_bytes(), NON_ALPHANUMERIC).to_string(); + proxy_url += "@"; + }; + proxy_url += &host; + proxy_url += ":"; + proxy_url += &port.to_string(); + + sql.set_raw_config("proxy_url", Some(&proxy_url)).await?; + } else { + sql.set_raw_config("proxy_url", Some("")).await?; + } + + let socks5_enabled = sql.get_raw_config("socks5_enabled").await?; + sql.set_raw_config("proxy_enabled", socks5_enabled.as_deref()) + .await?; + } + + sql.set_raw_config("socks5_enabled", None).await?; + sql.set_raw_config("socks5_host", None).await?; + sql.set_raw_config("socks5_port", None).await?; + sql.set_raw_config("socks5_user", None).await?; + sql.set_raw_config("socks5_password", None).await?; + Ok(()) + } + + /// Reads proxy configuration from the database. + pub async fn load(context: &Context) -> Result> { + Self::migrate_socks_config(&context.sql) + .await + .context("Failed to migrate legacy SOCKS config")?; + + let enabled = context.get_config_bool(Config::ProxyEnabled).await?; + if !enabled { + return Ok(None); + } + + let proxy_url = context + .get_config(Config::ProxyUrl) + .await? + .unwrap_or_default(); + let proxy_url = proxy_url + .split_once('\n') + .map_or(proxy_url.clone(), |(first_url, _rest)| { + first_url.to_string() + }); + let proxy_config = Self::from_url(&proxy_url).context("Failed to parse proxy URL")?; + Ok(Some(proxy_config)) + } + + /// 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( + &self, + context: &Context, + target_host: &str, + target_port: u16, + load_dns_cache: bool, + ) -> Result> { + match self { + ProxyConfig::Socks5(socks5_config) => { + let socks5_stream = socks5_config + .connect(context, target_host, target_port, load_dns_cache) + .await?; + Ok(Box::new(socks5_stream)) + } + ProxyConfig::Shadowsocks(ShadowsocksConfig { server_config }) => { + let shadowsocks_context = shadowsocks::context::Context::new_shared( + shadowsocks::config::ServerType::Local, + ); + + let tcp_stream = { + let server_addr = server_config.addr(); + let host = server_addr.host(); + let port = server_addr.port(); + connect_tcp(context, &host, port, load_dns_cache) + .await + .context("Failed to connect to Shadowsocks proxy")? + }; + + let proxy_client_stream = shadowsocks::ProxyClientStream::from_stream( + shadowsocks_context, + tcp_stream, + server_config, + (target_host.to_string(), target_port), + ); + let shadowsocks_stream = ShadowsocksStream { + stream: proxy_client_stream, + }; + + Ok(Box::new(shadowsocks_stream)) + } + } + } +} + +impl fmt::Display for Socks5Config { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "host:{},port:{},user_password:{}", + self.host, + self.port, + if let Some(user_password) = self.user_password.clone() { + format!("user: {}, password: ***", user_password.0) + } else { + "user: None".to_string() + } + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::config::Config; + use crate::test_utils::TestContext; + + #[test] + fn test_socks5_url() { + let proxy_config = ProxyConfig::from_url("socks5://127.0.0.1:9050").unwrap(); + assert_eq!( + proxy_config, + ProxyConfig::Socks5(Socks5Config { + host: "127.0.0.1".to_string(), + port: 9050, + user_password: None + }) + ); + + let proxy_config = ProxyConfig::from_url("socks5://foo:bar@127.0.0.1:9150").unwrap(); + assert_eq!( + proxy_config, + ProxyConfig::Socks5(Socks5Config { + host: "127.0.0.1".to_string(), + port: 9150, + user_password: Some(("foo".to_string(), "bar".to_string())) + }) + ); + + let proxy_config = ProxyConfig::from_url("socks5://%66oo:b%61r@127.0.0.1:9150").unwrap(); + assert_eq!( + proxy_config, + ProxyConfig::Socks5(Socks5Config { + host: "127.0.0.1".to_string(), + port: 9150, + user_password: Some(("foo".to_string(), "bar".to_string())) + }) + ); + } + + #[test] + fn test_shadowsocks_url() { + // Example URL from . + let proxy_config = + ProxyConfig::from_url("ss://YWVzLTEyOC1nY206dGVzdA@192.168.100.1:8888#Example1") + .unwrap(); + assert!(matches!(proxy_config, ProxyConfig::Shadowsocks(_))); + } + + #[test] + fn test_invalid_proxy_url() { + assert!(ProxyConfig::from_url("foobar://127.0.0.1:9050").is_err()); + assert!(ProxyConfig::from_url("abc").is_err()); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_socks5_migration() -> Result<()> { + let t = TestContext::new().await; + + // Test that config is migrated on attempt to load even if disabled. + t.set_config(Config::Socks5Host, Some("127.0.0.1")).await?; + t.set_config(Config::Socks5Port, Some("9050")).await?; + + let proxy_config = ProxyConfig::load(&t).await?; + // Even though proxy is not enabled, config should be migrated. + assert_eq!(proxy_config, None); + + assert_eq!( + t.get_config(Config::ProxyUrl).await?.unwrap(), + "socks5://127.0.0.1:9050" + ); + Ok(()) + } + + // Test SOCKS5 setting migration if proxy was never configured. + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_socks5_migration_unconfigured() -> Result<()> { + let t = TestContext::new().await; + + // Try to load config to trigger migration. + assert_eq!(ProxyConfig::load(&t).await?, None); + + assert_eq!(t.get_config(Config::ProxyEnabled).await?, None); + assert_eq!( + t.get_config(Config::ProxyUrl).await?.unwrap(), + String::new() + ); + Ok(()) + } + + // Test SOCKS5 setting migration if SOCKS5 host is empty. + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_socks5_migration_empty() -> Result<()> { + let t = TestContext::new().await; + + t.set_config(Config::Socks5Host, Some("")).await?; + + // Try to load config to trigger migration. + assert_eq!(ProxyConfig::load(&t).await?, None); + + assert_eq!(t.get_config(Config::ProxyEnabled).await?, None); + assert_eq!( + t.get_config(Config::ProxyUrl).await?.unwrap(), + String::new() + ); + Ok(()) + } +} diff --git a/src/net/session.rs b/src/net/session.rs index 2c4294875..4dc8fbc33 100644 --- a/src/net/session.rs +++ b/src/net/session.rs @@ -1,3 +1,4 @@ +use crate::net::proxy::ShadowsocksStream; use async_native_tls::TlsStream; use fast_socks5::client::Socks5Stream; use std::pin::Pin; @@ -44,6 +45,11 @@ impl SessionStream for Socks5Stream { self.get_socket_mut().set_read_timeout(timeout) } } +impl SessionStream for ShadowsocksStream { + fn set_read_timeout(&mut self, timeout: Option) { + self.stream.get_mut().set_read_timeout(timeout) + } +} /// Session stream with a read buffer. pub(crate) trait SessionBufStream: SessionStream + AsyncBufRead {} diff --git a/src/oauth2.rs b/src/oauth2.rs index 78303d9aa..10669682a 100644 --- a/src/oauth2.rs +++ b/src/oauth2.rs @@ -62,8 +62,8 @@ pub async fn get_oauth2_url( addr: &str, redirect_uri: &str, ) -> Result> { - let socks5_enabled = context.get_config_bool(Config::Socks5Enabled).await?; - if let Some(oauth2) = Oauth2::from_address(context, addr, socks5_enabled).await { + let proxy_enabled = context.get_config_bool(Config::ProxyEnabled).await?; + if let Some(oauth2) = Oauth2::from_address(context, addr, proxy_enabled).await { context .sql .set_raw_config("oauth2_pending_redirect_uri", Some(redirect_uri)) @@ -83,8 +83,8 @@ pub(crate) async fn get_oauth2_access_token( code: &str, regenerate: bool, ) -> Result> { - let socks5_enabled = context.get_config_bool(Config::Socks5Enabled).await?; - if let Some(oauth2) = Oauth2::from_address(context, addr, socks5_enabled).await { + let proxy_enabled = context.get_config_bool(Config::ProxyEnabled).await?; + if let Some(oauth2) = Oauth2::from_address(context, addr, proxy_enabled).await { let lock = context.oauth2_mutex.lock().await; // read generated token @@ -232,8 +232,8 @@ pub(crate) async fn get_oauth2_addr( addr: &str, code: &str, ) -> Result> { - let socks5_enabled = context.get_config_bool(Config::Socks5Enabled).await?; - let oauth2 = match Oauth2::from_address(context, addr, socks5_enabled).await { + let proxy_enabled = context.get_config_bool(Config::ProxyEnabled).await?; + let oauth2 = match Oauth2::from_address(context, addr, proxy_enabled).await { Some(o) => o, None => return Ok(None), }; diff --git a/src/qr.rs b/src/qr.rs index 6f159d08d..8d4995d62 100644 --- a/src/qr.rs +++ b/src/qr.rs @@ -7,7 +7,7 @@ use anyhow::{anyhow, bail, ensure, Context as _, Result}; pub use dclogin_scheme::LoginOptions; use deltachat_contact_tools::{addr_normalize, may_be_valid_addr, ContactAddress}; use once_cell::sync::Lazy; -use percent_encoding::percent_decode_str; +use percent_encoding::{percent_decode_str, percent_encode, NON_ALPHANUMERIC}; use serde::Deserialize; use self::dclogin_scheme::configure_from_login_qr; @@ -20,6 +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::peerstate::Peerstate; use crate::token; use crate::tools::validate_id; @@ -541,16 +542,15 @@ fn decode_webrtc_instance(_context: &Context, qr: &str) -> Result { fn decode_tg_socks_proxy(_context: &Context, qr: &str) -> Result { let url = url::Url::parse(qr).context("Invalid t.me/socks url")?; - const SOCKS5_DEFAULT_PORT: u16 = 1080; let mut host: Option = None; - let mut port: u16 = SOCKS5_DEFAULT_PORT; + let mut port: u16 = DEFAULT_SOCKS_PORT; let mut user: Option = None; let mut pass: Option = None; for (key, value) in url.query_pairs() { if key == "server" { host = Some(value.to_string()); } else if key == "port" { - port = value.parse().unwrap_or(SOCKS5_DEFAULT_PORT); + port = value.parse().unwrap_or(DEFAULT_SOCKS_PORT); } else if key == "user" { user = Some(value.to_string()); } else if key == "pass" { @@ -661,22 +661,33 @@ pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<()> { user, pass, } => { - // disable proxy before changing settings to not use a combination of old and new - context - .set_config_bool(Config::Socks5Enabled, false) - .await?; + let mut proxy_url = "socks5://".to_string(); + if let Some(pass) = pass { + proxy_url += &percent_encode(user.unwrap_or_default().as_bytes(), NON_ALPHANUMERIC) + .to_string(); + proxy_url += ":"; + proxy_url += &percent_encode(pass.as_bytes(), NON_ALPHANUMERIC).to_string(); + proxy_url += "@"; + }; + proxy_url += &host; + proxy_url += ":"; + proxy_url += &port.to_string(); - context.set_config(Config::Socks5Host, Some(&host)).await?; + let old_proxy_url_value = context + .get_config(Config::ProxyUrl) + .await? + .unwrap_or_default(); + let proxy_urls: Vec<&str> = std::iter::once(proxy_url.as_str()) + .chain( + old_proxy_url_value + .split('\n') + .filter(|s| !s.is_empty() && *s != proxy_url), + ) + .collect(); context - .set_config_u32(Config::Socks5Port, u32::from(port)) + .set_config(Config::ProxyUrl, Some(&proxy_urls.join("\n"))) .await?; - context - .set_config(Config::Socks5User, user.as_deref()) - .await?; - context - .set_config(Config::Socks5Password, pass.as_deref()) - .await?; - context.set_config_bool(Config::Socks5Enabled, true).await?; + context.set_config_bool(Config::ProxyEnabled, true).await?; } Qr::WithdrawVerifyContact { invitenumber, @@ -1630,50 +1641,48 @@ mod tests { async fn test_set_socks5_proxy_config_from_qr() -> Result<()> { let t = TestContext::new().await; - assert_eq!(t.get_config_bool(Config::Socks5Enabled).await?, false); + assert_eq!(t.get_config_bool(Config::ProxyEnabled).await?, false); let res = set_config_from_qr(&t, "https://t.me/socks?server=foo&port=666").await; assert!(res.is_ok()); - assert_eq!(t.get_config_bool(Config::Socks5Enabled).await?, true); + assert_eq!(t.get_config_bool(Config::ProxyEnabled).await?, true); assert_eq!( - t.get_config(Config::Socks5Host).await?, - Some("foo".to_string()) + t.get_config(Config::ProxyUrl).await?, + Some("socks5://foo:666".to_string()) ); - assert_eq!(t.get_config_u32(Config::Socks5Port).await?, 666); - assert_eq!(t.get_config(Config::Socks5User).await?, None); - assert_eq!(t.get_config(Config::Socks5Password).await?, None); - // make sure, user&password are reset when not specified in the URL - t.set_config(Config::Socks5User, Some("alice")).await?; - t.set_config(Config::Socks5Password, Some("secret")).await?; + // Test URL without port. let res = set_config_from_qr(&t, "https://t.me/socks?server=1.2.3.4").await; assert!(res.is_ok()); - assert_eq!(t.get_config_bool(Config::Socks5Enabled).await?, true); + assert_eq!(t.get_config_bool(Config::ProxyEnabled).await?, true); assert_eq!( - t.get_config(Config::Socks5Host).await?, - Some("1.2.3.4".to_string()) + t.get_config(Config::ProxyUrl).await?, + Some("socks5://1.2.3.4:1080\nsocks5://foo:666".to_string()) ); - assert_eq!(t.get_config_u32(Config::Socks5Port).await?, 1080); - assert_eq!(t.get_config(Config::Socks5User).await?, None); - assert_eq!(t.get_config(Config::Socks5Password).await?, None); // make sure, user&password are set when specified in the URL + // Password is an URL-encoded "x&%$X". let res = set_config_from_qr(&t, "https://t.me/socks?server=jau&user=Da&pass=x%26%25%24X").await; assert!(res.is_ok()); - assert_eq!(t.get_config_bool(Config::Socks5Enabled).await?, true); assert_eq!( - t.get_config(Config::Socks5Host).await?, - Some("jau".to_string()) + t.get_config(Config::ProxyUrl).await?, + Some( + "socks5://Da:x%26%25%24X@jau:1080\nsocks5://1.2.3.4:1080\nsocks5://foo:666" + .to_string() + ) ); - assert_eq!(t.get_config_u32(Config::Socks5Port).await?, 1080); + + // Scanning existing proxy brings it to the top in the list. + let res = set_config_from_qr(&t, "https://t.me/socks?server=foo&port=666").await; + assert!(res.is_ok()); + assert_eq!(t.get_config_bool(Config::ProxyEnabled).await?, true); assert_eq!( - t.get_config(Config::Socks5User).await?, - Some("Da".to_string()) - ); - assert_eq!( - t.get_config(Config::Socks5Password).await?, - Some("x&%$X".to_string()) + t.get_config(Config::ProxyUrl).await?, + Some( + "socks5://foo:666\nsocks5://Da:x%26%25%24X@jau:1080\nsocks5://1.2.3.4:1080" + .to_string() + ) ); Ok(()) diff --git a/src/smtp.rs b/src/smtp.rs index 59c2fb59b..5c1d1af2f 100644 --- a/src/smtp.rs +++ b/src/smtp.rs @@ -18,9 +18,9 @@ use crate::login_param::{ConfiguredLoginParam, ConfiguredServerLoginParam}; use crate::message::Message; use crate::message::{self, MsgId}; use crate::mimefactory::MimeFactory; +use crate::net::proxy::ProxyConfig; use crate::net::session::SessionBufStream; use crate::scheduler::connectivity::ConnectivityStore; -use crate::socks::Socks5Config; use crate::sql; use crate::stock_str::unencrypted_email; use crate::tools::{self, time_elapsed}; @@ -95,7 +95,7 @@ impl Smtp { context, &lp.smtp, &lp.smtp_password, - &lp.socks5_config, + &lp.proxy_config, &lp.addr, lp.strict_tls(), lp.oauth2, @@ -110,7 +110,7 @@ impl Smtp { context: &Context, login_params: &[ConfiguredServerLoginParam], password: &str, - socks5_config: &Option, + proxy_config: &Option, addr: &str, strict_tls: bool, oauth2: bool, @@ -130,7 +130,7 @@ impl Smtp { info!(context, "SMTP trying to connect to {}.", &lp.connection); let transport = match connect::connect_and_auth( context, - socks5_config, + proxy_config, strict_tls, lp.connection.clone(), oauth2, diff --git a/src/smtp/connect.rs b/src/smtp/connect.rs index 29169a6c6..8f6382a6d 100644 --- a/src/smtp/connect.rs +++ b/src/smtp/connect.rs @@ -9,13 +9,13 @@ use tokio::io::BufStream; use crate::context::Context; use crate::login_param::{ConnectionCandidate, ConnectionSecurity}; use crate::net::dns::{lookup_host_with_cache, update_connect_timestamp}; +use crate::net::proxy::ProxyConfig; use crate::net::session::SessionBufStream; use crate::net::tls::wrap_tls; use crate::net::{ connect_tcp_inner, connect_tls_inner, run_connection_attempts, update_connection_history, }; use crate::oauth2::get_oauth2_access_token; -use crate::socks::Socks5Config; use crate::tools::time; /// Converts port number to ALPN list. @@ -31,7 +31,7 @@ fn alpn(port: u16) -> &'static [&'static str] { #[allow(clippy::too_many_arguments)] pub(crate) async fn connect_and_auth( context: &Context, - socks5_config: &Option, + proxy_config: &Option, strict_tls: bool, candidate: ConnectionCandidate, oauth2: bool, @@ -40,7 +40,7 @@ pub(crate) async fn connect_and_auth( password: &str, ) -> Result>> { let session_stream = - connect_stream(context, socks5_config.clone(), strict_tls, candidate).await?; + connect_stream(context, proxy_config.clone(), strict_tls, candidate).await?; let client = async_smtp::SmtpClient::new() .smtp_utf8(true) .without_greeting(); @@ -127,7 +127,7 @@ async fn connection_attempt( /// to unify the result regardless of whether TLS or STARTTLS is used. async fn connect_stream( context: &Context, - socks5_config: Option, + proxy_config: Option, strict_tls: bool, candidate: ConnectionCandidate, ) -> Result> { @@ -135,18 +135,17 @@ async fn connect_stream( let port = candidate.port; let security = candidate.security; - if let Some(socks5_config) = socks5_config { + if let Some(proxy_config) = proxy_config { let stream = match security { ConnectionSecurity::Tls => { - connect_secure_socks5(context, host, port, strict_tls, socks5_config.clone()) - .await? + connect_secure_proxy(context, host, port, strict_tls, proxy_config.clone()).await? } ConnectionSecurity::Starttls => { - connect_starttls_socks5(context, host, port, strict_tls, socks5_config.clone()) + connect_starttls_proxy(context, host, port, strict_tls, proxy_config.clone()) .await? } ConnectionSecurity::Plain => { - connect_insecure_socks5(context, host, port, socks5_config.clone()).await? + connect_insecure_proxy(context, host, port, proxy_config.clone()).await? } }; Ok(stream) @@ -192,37 +191,37 @@ async fn skip_smtp_greeting(stream: &mut } } -async fn connect_secure_socks5( +async fn connect_secure_proxy( context: &Context, hostname: &str, port: u16, strict_tls: bool, - socks5_config: Socks5Config, + proxy_config: ProxyConfig, ) -> Result> { - let socks5_stream = socks5_config + let proxy_stream = proxy_config .connect(context, hostname, port, strict_tls) .await?; - let tls_stream = wrap_tls(strict_tls, hostname, alpn(port), socks5_stream).await?; + let tls_stream = wrap_tls(strict_tls, hostname, alpn(port), proxy_stream).await?; let mut buffered_stream = BufStream::new(tls_stream); skip_smtp_greeting(&mut buffered_stream).await?; let session_stream: Box = Box::new(buffered_stream); Ok(session_stream) } -async fn connect_starttls_socks5( +async fn connect_starttls_proxy( context: &Context, hostname: &str, port: u16, strict_tls: bool, - socks5_config: Socks5Config, + proxy_config: ProxyConfig, ) -> Result> { - let socks5_stream = socks5_config + let proxy_stream = proxy_config .connect(context, hostname, port, strict_tls) .await?; // Run STARTTLS command and convert the client back into a stream. let client = SmtpClient::new().smtp_utf8(true); - let transport = SmtpTransport::new(client, BufStream::new(socks5_stream)).await?; + let transport = SmtpTransport::new(client, BufStream::new(proxy_stream)).await?; let tcp_stream = transport.starttls().await?.into_inner(); let tls_stream = wrap_tls(strict_tls, hostname, &[], tcp_stream) .await @@ -232,16 +231,14 @@ async fn connect_starttls_socks5( Ok(session_stream) } -async fn connect_insecure_socks5( +async fn connect_insecure_proxy( context: &Context, hostname: &str, port: u16, - socks5_config: Socks5Config, + proxy_config: ProxyConfig, ) -> Result> { - let socks5_stream = socks5_config - .connect(context, hostname, port, false) - .await?; - let mut buffered_stream = BufStream::new(socks5_stream); + let proxy_stream = proxy_config.connect(context, hostname, port, false).await?; + let mut buffered_stream = BufStream::new(proxy_stream); skip_smtp_greeting(&mut buffered_stream).await?; let session_stream: Box = Box::new(buffered_stream); Ok(session_stream) diff --git a/src/socks.rs b/src/socks.rs deleted file mode 100644 index f36ff9211..000000000 --- a/src/socks.rs +++ /dev/null @@ -1,101 +0,0 @@ -//! # SOCKS5 support. - -use std::fmt; -use std::pin::Pin; - -use anyhow::Result; -use fast_socks5::client::{Config, Socks5Stream}; -use fast_socks5::util::target_addr::ToTargetAddr; -use fast_socks5::AuthenticationMethod; -use fast_socks5::Socks5Command; -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 { - pub host: String, - pub port: u16, - pub user_password: Option<(String, String)>, -} - -impl Socks5Config { - /// Reads SOCKS5 configuration from the database. - 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(); - let port: u16 = sql - .get_raw_config_int("socks5_port") - .await? - .unwrap_or_default() as u16; - let user = sql.get_raw_config("socks5_user").await?.unwrap_or_default(); - let password = sql - .get_raw_config("socks5_password") - .await? - .unwrap_or_default(); - - let socks5_config = Self { - host, - port, - user_password: if !user.is_empty() { - Some((user, password)) - } else { - None - }, - }; - Ok(Some(socks5_config)) - } else { - Ok(None) - } - } - - /// 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( - &self, - context: &Context, - target_host: &str, - target_port: u16, - load_dns_cache: bool, - ) -> Result>>>> { - let tcp_stream = connect_tcp(context, &self.host, self.port, load_dns_cache).await?; - - let authentication_method = if let Some((username, password)) = self.user_password.as_ref() - { - Some(AuthenticationMethod::Password { - username: username.into(), - password: password.into(), - }) - } else { - None - }; - let mut socks_stream = - Socks5Stream::use_stream(tcp_stream, authentication_method, Config::default()).await?; - let target_addr = (target_host, target_port).to_target_addr()?; - socks_stream - .request(Socks5Command::TCPConnect, target_addr) - .await?; - - Ok(socks_stream) - } -} - -impl fmt::Display for Socks5Config { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "host:{},port:{},user_password:{}", - self.host, - self.port, - if let Some(user_password) = self.user_password.clone() { - format!("user: {}, password: ***", user_password.0) - } else { - "user: None".to_string() - } - ) - } -}