diff --git a/Cargo.lock b/Cargo.lock index e65cd02f69..de2df7e8aa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -391,12 +391,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - [[package]] name = "attohttpc" version = "0.24.1" @@ -428,7 +422,7 @@ dependencies = [ "http 1.1.0", "http-body 1.0.0", "http-body-util", - "hyper 1.2.0", + "hyper 1.4.1", "hyper-util", "itoa", "matchit", @@ -1365,6 +1359,7 @@ dependencies = [ "async_zip", "base64 0.22.1", "brotli", + "bytes", "chrono", "criterion", "deltachat-contact-tools", @@ -1380,7 +1375,10 @@ dependencies = [ "futures-lite 2.3.0", "hex", "hickory-resolver", + "http-body-util", "humansize", + "hyper 1.4.1", + "hyper-util", "image", "iroh", "iroh-gossip", @@ -1406,12 +1404,12 @@ dependencies = [ "rand 0.8.5", "ratelimit", "regex", - "reqwest", "rusqlite", "rust-hsluv", "sanitize-filename", "serde", "serde_json", + "serde_urlencoded", "sha-1", "smallvec", "strum", @@ -2703,25 +2701,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "h2" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa82e28a107a8cc405f0839610bdc9b15f1e25ec7d696aa5cf173edbcb1486ab" -dependencies = [ - "atomic-waker", - "bytes", - "fnv", - "futures-core", - "futures-sink", - "http 1.1.0", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - [[package]] name = "half" version = "2.4.0" @@ -2918,9 +2897,9 @@ dependencies = [ [[package]] name = "http-body-util" -version = "0.1.0" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41cb79eb393015dadd30fc252023adb0b2400a0caee0fa2a077e6e21a551e840" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" dependencies = [ "bytes", "futures-util", @@ -2980,7 +2959,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2 0.3.26", + "h2", "http 0.2.12", "http-body 0.4.6", "httparse", @@ -2996,14 +2975,13 @@ dependencies = [ [[package]] name = "hyper" -version = "1.2.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "186548d73ac615b32a73aafe38fb4f56c0d340e110e5a200bcadbaf2e199263a" +checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" dependencies = [ "bytes", "futures-channel", "futures-util", - "h2 0.4.5", "http 1.1.0", "http-body 1.0.0", "httparse", @@ -3023,7 +3001,7 @@ checksum = "5ee4be2c948921a1a5320b629c4193916ed787a7f7f293fd3f7f5a6c9de74155" dependencies = [ "futures-util", "http 1.1.0", - "hyper 1.2.0", + "hyper 1.4.1", "hyper-util", "rustls 0.23.10", "rustls-pki-types", @@ -3033,34 +3011,18 @@ dependencies = [ "webpki-roots 0.26.1", ] -[[package]] -name = "hyper-tls" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" -dependencies = [ - "bytes", - "http-body-util", - "hyper 1.2.0", - "hyper-util", - "native-tls", - "tokio", - "tokio-native-tls", - "tower-service", -] - [[package]] name = "hyper-util" -version = "0.1.3" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa" +checksum = "cde7055719c54e36e95e8719f95883f22072a48ede39db7fc17a4e1d5281e9b9" dependencies = [ "bytes", "futures-channel", "futures-util", "http 1.1.0", "http-body 1.0.0", - "hyper 1.2.0", + "hyper 1.4.1", "pin-project-lite", "socket2", "tokio", @@ -3080,7 +3042,7 @@ dependencies = [ "iana-time-zone-haiku", "js-sys", "wasm-bindgen", - "windows-core 0.51.1", + "windows-core 0.52.0", ] [[package]] @@ -3350,7 +3312,7 @@ dependencies = [ "anyhow", "erased_set", "http-body-util", - "hyper 1.2.0", + "hyper 1.4.1", "hyper-util", "once_cell", "prometheus-client", @@ -3388,7 +3350,7 @@ dependencies = [ "hostname", "http 1.1.0", "http-body-util", - "hyper 1.2.0", + "hyper 1.4.1", "hyper-util", "igd-next", "iroh-base", @@ -5292,22 +5254,18 @@ checksum = "c7d6d2a27d57148378eb5e111173f4276ad26340ecc5c49a4a2152167a2d6a37" dependencies = [ "base64 0.22.1", "bytes", - "encoding_rs", "futures-core", "futures-util", - "h2 0.4.5", "http 1.1.0", "http-body 1.0.0", "http-body-util", - "hyper 1.2.0", + "hyper 1.4.1", "hyper-rustls", - "hyper-tls", "hyper-util", "ipnet", "js-sys", "log", "mime", - "native-tls", "once_cell", "percent-encoding", "pin-project-lite", @@ -5319,9 +5277,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "sync_wrapper 1.0.0", - "system-configuration 0.5.1", "tokio", - "tokio-native-tls", "tokio-rustls 0.26.0", "tower-service", "url", @@ -6573,16 +6529,6 @@ dependencies = [ "syn 2.0.72", ] -[[package]] -name = "tokio-native-tls" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" -dependencies = [ - "native-tls", - "tokio", -] - [[package]] name = "tokio-rustls" version = "0.24.1" diff --git a/Cargo.toml b/Cargo.toml index 6adcfdef99..1603fa7ff4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,6 +47,7 @@ async-smtp = { version = "0.9", default-features = false, features = ["runtime-t async_zip = { version = "0.0.12", default-features = false, features = ["deflate", "fs"] } base64 = { workspace = true } brotli = { version = "6", default-features=false, features = ["std"] } +bytes = "1" chrono = { workspace = true, features = ["alloc", "clock", "std"] } email = { git = "https://github.com/deltachat/rust-email", branch = "master" } encoded-words = { git = "https://github.com/async-email/encoded-words", branch = "master" } @@ -57,7 +58,10 @@ futures = { workspace = true } futures-lite = { workspace = true } hex = "0.4.0" hickory-resolver = "0.24" +http-body-util = "0.1.2" humansize = "2" +hyper = "1" +hyper-util = "0.1.7" image = { version = "0.25.1", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] } iroh_old = { version = "0.4.2", default-features = false, package = "iroh"} iroh-net = { version = "0.22.0", default-features = false } @@ -79,11 +83,11 @@ quick-xml = "0.36" quoted_printable = "0.5" rand = { workspace = true } regex = { workspace = true } -reqwest = { version = "0.12.5", features = ["json"] } rusqlite = { workspace = true, features = ["sqlcipher"] } rust-hsluv = "0.1" sanitize-filename = { workspace = true } serde_json = { workspace = true } +serde_urlencoded = "0.7.1" serde = { workspace = true, features = ["derive"] } sha-1 = "0.10" smallvec = "1.13.2" @@ -194,8 +198,7 @@ default = ["vendored"] internals = [] vendored = [ "async-native-tls/vendored", - "rusqlite/bundled-sqlcipher-vendored-openssl", - "reqwest/native-tls-vendored" + "rusqlite/bundled-sqlcipher-vendored-openssl" ] [lints.rust] diff --git a/src/net.rs b/src/net.rs index 3a120e7f7f..915290b5a2 100644 --- a/src/net.rs +++ b/src/net.rs @@ -29,13 +29,6 @@ use tls::wrap_tls; /// This constant should be more than the largest expected RTT. pub(crate) const TIMEOUT: Duration = Duration::from_secs(60); -/// Transaction timeout, e.g. for a GET or POST request -/// together with all connection attempts. -/// -/// This is the worst case time user has to wait on a very slow network -/// after clicking a button and before getting an error message. -pub(crate) const TRANSACTION_TIMEOUT: Duration = Duration::from_secs(300); - /// TTL for caches in seconds. pub(crate) const CACHE_TTL: u64 = 30 * 24 * 60 * 60; diff --git a/src/net/http.rs b/src/net/http.rs index 4a66c718fb..109b65f89b 100644 --- a/src/net/http.rs +++ b/src/net/http.rs @@ -1,22 +1,17 @@ //! # HTTP module. -use std::sync::Arc; - -use anyhow::{anyhow, Result}; +use anyhow::{anyhow, bail, Context as _, Result}; +use bytes::Bytes; +use http_body_util::BodyExt; +use hyper_util::rt::TokioIo; use mime::Mime; -use once_cell::sync::Lazy; +use serde::Serialize; use crate::context::Context; -use crate::net::lookup_host_with_cache; +use crate::net::session::SessionStream; +use crate::net::tls::wrap_tls; use crate::socks::Socks5Config; -static LETSENCRYPT_ROOT: Lazy = Lazy::new(|| { - reqwest::tls::Certificate::from_der(include_bytes!( - "../../assets/root-certificates/letsencrypt/isrgrootx1.der" - )) - .unwrap() -}); - /// HTTP(S) GET response. #[derive(Debug)] pub struct Response { @@ -32,48 +27,95 @@ pub struct Response { /// Retrieves the text contents of URL using HTTP GET request. pub async fn read_url(context: &Context, url: &str) -> Result { - Ok(read_url_inner(context, url).await?.text().await?) + let response = read_url_blob(context, url).await?; + let text = String::from_utf8_lossy(&response.blob); + Ok(text.to_string()) } -/// Retrieves the binary contents of URL using HTTP GET request. -pub async fn read_url_blob(context: &Context, url: &str) -> Result { - let response = read_url_inner(context, url).await?; - let content_type = response - .headers() - .get(reqwest::header::CONTENT_TYPE) - .and_then(|value| value.to_str().ok()) - .and_then(|value| value.parse::().ok()); - let mimetype = content_type - .as_ref() - .map(|mime| mime.essence_str().to_string()); - let encoding = content_type.as_ref().and_then(|mime| { - mime.get_param(mime::CHARSET) - .map(|charset| charset.as_str().to_string()) - }); - let blob: Vec = response.bytes().await?.into(); - Ok(Response { - blob, - mimetype, - encoding, - }) -} +async fn get_http_sender( + context: &Context, + parsed_url: hyper::Uri, +) -> Result> +where + B: hyper::body::Body + 'static + Send, + B::Data: Send, + B::Error: Into>, +{ + 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 stream: Box = match scheme { + "http" => { + let port = parsed_url.port_u16().unwrap_or(80); + + // It is safe to use cached IP addresses + // for HTTPS URLs, but for HTTP URLs + // 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 + .connect(context, host, port, load_cache) + .await?; + Box::new(socks5_stream) + } else { + let tcp_stream = crate::net::connect_tcp(context, host, port, load_cache).await?; + Box::new(tcp_stream) + } + } + "https" => { + let port = parsed_url.port_u16().unwrap_or(443); + let load_cache = true; + let strict_tls = true; + + if let Some(socks5_config) = socks5_config_opt { + let socks5_stream = socks5_config + .connect(context, host, port, load_cache) + .await?; + let tls_stream = wrap_tls(strict_tls, host, &[], socks5_stream).await?; + Box::new(tls_stream) + } else { + let tcp_stream = crate::net::connect_tcp(context, host, port, load_cache).await?; + let tls_stream = wrap_tls(strict_tls, host, &[], tcp_stream).await?; + Box::new(tls_stream) + } + } + _ => bail!("Unknown URL scheme"), + }; + + let io = TokioIo::new(stream); + let (sender, conn) = hyper::client::conn::http1::handshake(io).await?; + tokio::task::spawn(conn); -async fn read_url_inner(context: &Context, url: &str) -> Result { - // It is safe to use cached IP addresses - // for HTTPS URLs, but for HTTP URLs - // better resolve from scratch each time to prevent - // cache poisoning attacks from having lasting effects. - let load_cache = url.starts_with("https://"); + Ok(sender) +} - let client = get_client(context, load_cache).await?; +/// Retrieves the binary contents of URL using HTTP GET request. +pub async fn read_url_blob(context: &Context, url: &str) -> Result { let mut url = url.to_string(); // Follow up to 10 http-redirects for _i in 0..10 { - let response = client.get(&url).send().await?; + let parsed_url = url + .parse::() + .with_context(|| format!("Failed to parse URL {url:?}"))?; + + let mut sender = get_http_sender(context, parsed_url.clone()).await?; + let authority = parsed_url + .authority() + .context("URL has no authority")? + .clone(); + + let req = hyper::Request::builder() + .uri(parsed_url.path()) + .header(hyper::header::HOST, authority.as_str()) + .body(http_body_util::Empty::::new())?; + let response = sender.send_request(req).await?; + if response.status().is_redirection() { - let headers = response.headers(); - let header = headers + let header = response + .headers() .get_all("location") .iter() .last() @@ -84,88 +126,127 @@ async fn read_url_inner(context: &Context, url: &str) -> Result().ok()); + let mimetype = content_type + .as_ref() + .map(|mime| mime.essence_str().to_string()); + let encoding = content_type.as_ref().and_then(|mime| { + mime.get_param(mime::CHARSET) + .map(|charset| charset.as_str().to_string()) + }); + let body = response.collect().await?.to_bytes(); + let blob: Vec = body.to_vec(); + return Ok(Response { + blob, + mimetype, + encoding, + }); } Err(anyhow!("Followed 10 redirections")) } -struct CustomResolver { - context: Context, +/// Sends an empty POST request to the URL. +/// +/// Follows redirections. +/// +/// Returns response text and whether request was successful or not. +pub(crate) async fn post_empty(context: &Context, url: &str) -> Result<(String, bool)> { + let mut url = url.to_string(); - /// Whether to return cached results or not. - /// If resolver can be used for URLs - /// without TLS, e.g. HTTP URLs from HTML email, - /// this must be false. If TLS is used - /// and certificate hostnames are checked, - /// it is safe to load cache. - load_cache: bool, -} + for _i in 0..10 { + let parsed_url = url + .parse::() + .with_context(|| format!("Failed to parse URL {url:?}"))?; + let scheme = parsed_url.scheme_str().context("URL has no scheme")?; + if scheme != "https" { + bail!("POST requests to non-HTTPS URLs are not allowed"); + } + + let mut sender = get_http_sender(context, parsed_url.clone()).await?; + let authority = parsed_url + .authority() + .context("URL has no authority")? + .clone(); + let req = hyper::Request::post(parsed_url.path()) + .header(hyper::header::HOST, authority.as_str()) + .body(http_body_util::Empty::::new())?; -impl CustomResolver { - fn new(context: Context, load_cache: bool) -> Self { - Self { - context, - load_cache, + let response = sender.send_request(req).await?; + if response.status().is_redirection() { + let header = response + .headers() + .get_all("location") + .iter() + .last() + .ok_or_else(|| anyhow!("Redirection doesn't have a target location"))? + .to_str()?; + info!(context, "Following redirect to {}", header); + url = header.to_string(); + continue; } + + let response_status = response.status(); + let body = response.collect().await?.to_bytes(); + let text = String::from_utf8_lossy(&body); + let response_text = text.to_string(); + + return Ok((response_text, response_status.is_success())); } + + Err(anyhow!("Followed 10 redirections")) } -impl reqwest::dns::Resolve for CustomResolver { - fn resolve(&self, hostname: reqwest::dns::Name) -> reqwest::dns::Resolving { - let context = self.context.clone(); - let load_cache = self.load_cache; - Box::pin(async move { - let port = 443; // Actual port does not matter. - - let socket_addrs = - lookup_host_with_cache(&context, hostname.as_str(), port, "", load_cache).await; - match socket_addrs { - Ok(socket_addrs) => { - let addrs: reqwest::dns::Addrs = Box::new(socket_addrs.into_iter()); - - Ok(addrs) - } - Err(err) => Err(err.into()), - } - }) - } +/// Posts string to the given URL. +/// +/// Returns true if successful HTTP response code was returned. +#[allow(dead_code)] +pub(crate) async fn post_string(context: &Context, url: &str, body: String) -> Result { + let parsed_url = url + .parse::() + .with_context(|| format!("Failed to parse URL {url:?}"))?; + + let mut sender = get_http_sender(context, parsed_url.clone()).await?; + let authority = parsed_url + .authority() + .context("URL has no authority")? + .clone(); + + let request = hyper::Request::post(parsed_url.path()) + .header(hyper::header::HOST, authority.as_str()) + .body(body)?; + let response = sender.send_request(request).await?; + + Ok(response.status().is_success()) } -pub(crate) async fn get_client(context: &Context, load_cache: bool) -> Result { - let socks5_config = Socks5Config::from_database(&context.sql).await?; - let resolver = Arc::new(CustomResolver::new(context.clone(), load_cache)); - - // `reqwest` uses `hyper-util` crate internally which implements - // [Happy Eyeballs](https://datatracker.ietf.org/doc/html/rfc6555) algorithm. - // On a dual-stack host it starts IPv4 connection attempts in parallel - // to IPv6 connection attempts after 300 ms. - // In the worst case of all connection attempts - // timing out this allows to try four IPv6 and four IPv4 - // addresses before request expires - // if request timeout is set to 5 minutes - // and connection timeout is set to 1 minute. - // - // We do not set write timeout because `reqwest` - // does not support it, but request timeout - // should prevent deadlocks if the server - // does not read the data. - let builder = reqwest::ClientBuilder::new() - .connect_timeout(super::TIMEOUT) - .read_timeout(super::TIMEOUT) - .timeout(super::TRANSACTION_TIMEOUT) - .add_root_certificate(LETSENCRYPT_ROOT.clone()) - .dns_resolver(resolver); - - 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()?) +/// Sends a POST request with x-www-form-urlencoded data. +/// +/// Does not follow redirects. +pub(crate) async fn post_form( + context: &Context, + url: &str, + form: &T, +) -> Result { + let parsed_url = url + .parse::() + .with_context(|| format!("Failed to parse URL {url:?}"))?; + + let encoded_body = serde_urlencoded::to_string(form).context("Failed to encode data")?; + let mut sender = get_http_sender(context, parsed_url.clone()).await?; + let authority = parsed_url + .authority() + .context("URL has no authority")? + .clone(); + let request = hyper::Request::post(parsed_url.path()) + .header(hyper::header::HOST, authority.as_str()) + .header("content-type", "application/x-www-form-urlencoded") + .body(encoded_body)?; + let response = sender.send_request(request).await?; + let bytes = response.collect().await?.to_bytes(); + Ok(bytes) } diff --git a/src/oauth2.rs b/src/oauth2.rs index c235fe2aa8..78303d9aad 100644 --- a/src/oauth2.rs +++ b/src/oauth2.rs @@ -8,6 +8,8 @@ use serde::Deserialize; use crate::config::Config; use crate::context::Context; +use crate::net::http::post_form; +use crate::net::read_url_blob; use crate::provider; use crate::provider::Oauth2Authorizer; use crate::tools::time; @@ -159,25 +161,19 @@ pub(crate) async fn get_oauth2_access_token( // ... and POST - // All OAuth URLs are hardcoded HTTPS URLs, - // so it is safe to load DNS cache. - let load_cache = true; - - let client = crate::net::http::get_client(context, load_cache).await?; - - let response: Response = match client.post(post_url).form(&post_param).send().await { - Ok(resp) => match resp.json().await { + let response: Response = match post_form(context, post_url, &post_param).await { + Ok(resp) => match serde_json::from_slice(&resp) { Ok(response) => response, Err(err) => { warn!( context, - "Failed to parse OAuth2 JSON response from {}: error: {}", token_url, err + "Failed to parse OAuth2 JSON response from {token_url}: {err:#}." ); return Ok(None); } }, Err(err) => { - warn!(context, "Error calling OAuth2 at {}: {:?}", token_url, err); + warn!(context, "Error calling OAuth2 at {token_url}: {err:#}."); return Ok(None); } }; @@ -303,20 +299,9 @@ impl Oauth2 { // "picture": "https://lh4.googleusercontent.com/-Gj5jh_9R0BY/AAAAAAAAAAI/AAAAAAAAAAA/IAjtjfjtjNA/photo.jpg" // } - // All OAuth URLs are hardcoded HTTPS URLs, - // so it is safe to load DNS cache. - let load_cache = true; - - let client = crate::net::http::get_client(context, load_cache) - .await - .context("Failed to get HTTP client")?; - let response = client - .get(userinfo_url) - .send() - .await - .context("Failed to get userinfo")?; - let response: Result, _> = response.json().await; - let parsed = response.context("Error getting userinfo")?; + let response = read_url_blob(context, &userinfo_url).await?; + let parsed: HashMap = + serde_json::from_slice(&response.blob).context("Error getting userinfo")?; // CAVE: serde_json::Value.as_str() removes the quotes of json-strings // but serde_json::Value.to_string() does not! if let Some(addr) = parsed.get("email") { diff --git a/src/push.rs b/src/push.rs index 0498f84739..a91cf155bc 100644 --- a/src/push.rs +++ b/src/push.rs @@ -61,16 +61,13 @@ impl PushSubscriber { return Ok(()); }; - let load_cache = true; - let response = http::get_client(context, load_cache) - .await? - .post("https://notifications.delta.chat/register") - .body(format!("{{\"token\":\"{token}\"}}")) - .send() - .await?; - - let response_status = response.status(); - if response_status.is_success() { + if http::post_string( + context, + "https://notifications.delta.chat/register", + format!("{{\"token\":\"{token}\"}}"), + ) + .await? + { state.heartbeat_subscribed = true; } Ok(()) diff --git a/src/qr.rs b/src/qr.rs index f193715663..7666ab2775 100644 --- a/src/qr.rs +++ b/src/qr.rs @@ -19,6 +19,7 @@ use crate::context::Context; use crate::events::EventType; use crate::key::Fingerprint; use crate::message::Message; +use crate::net::http::post_empty; use crate::peerstate::Peerstate; use crate::token; use crate::tools::validate_id; @@ -645,21 +646,8 @@ async fn set_account_from_qr(context: &Context, qr: &str) -> Result<()> { bail!("DCACCOUNT QR codes must use HTTPS scheme"); } - // As only HTTPS is used, it is safe to load DNS cache. - let load_cache = true; - - let response = crate::net::http::get_client(context, load_cache) - .await? - .post(url_str) - .send() - .await?; - let response_status = response.status(); - let response_text = response - .text() - .await - .context("Cannot create account, request failed: empty response")?; - - if response_status.is_success() { + let (response_text, response_success) = post_empty(context, url_str).await?; + if response_success { let CreateAccountSuccessResponse { password, email } = serde_json::from_str(&response_text) .with_context(|| { format!("Cannot create account, response is malformed:\n{response_text:?}") diff --git a/src/socks.rs b/src/socks.rs index 3a5eecde9a..f36ff92117 100644 --- a/src/socks.rs +++ b/src/socks.rs @@ -8,7 +8,6 @@ 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; @@ -54,20 +53,6 @@ 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( @@ -114,35 +99,3 @@ 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" - ); - } -}