From 7457925458a045469e34980d1e5a03faabddc5f6 Mon Sep 17 00:00:00 2001 From: Optio Agent Date: Sat, 28 Mar 2026 00:35:26 +0000 Subject: [PATCH 1/2] fix(proxy): refuse tunneling non-HTTP protocols through HTTP proxy When --proxytunnel (-p) is set with an HTTP proxy, all protocols must attempt a CONNECT tunnel through the proxy before protocol-specific handling. Previously, non-HTTP protocols either failed with URL parse errors (missing default ports) or were rejected as unsupported before reaching the proxy tunnel logic. Changes: - Add default ports for dict, imap, imaps, mqtt, mqtts, pop3, pop3s, smtp, smtps, and telnet in Url::port_or_default() - Add CONNECT tunnel pre-check in do_single_request() for protocols that don't handle their own tunneling (e.g. gopher, dict, telnet, rtsp, ldap, smb, sftp, scp, mqtt, ws) - Add is_http_proxy_tunnel() getter on Easy for CLI scheme validation - Bypass early scheme validation in CLI when proxy tunnel is active - Add gopher, rtsp, ldap, smb to the supported protocol list Passes curl test 445 (Refuse tunneling protocols through HTTP proxy). Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/liburlx/src/easy.rs | 53 ++++++++++++++++++++++++++++++++ crates/liburlx/src/url.rs | 10 ++++++ crates/urlx-cli/src/transfer.rs | 54 ++++++++++++++++++++------------- 3 files changed, 96 insertions(+), 21 deletions(-) diff --git a/crates/liburlx/src/easy.rs b/crates/liburlx/src/easy.rs index 9f40ea4..59ed8b7 100644 --- a/crates/liburlx/src/easy.rs +++ b/crates/liburlx/src/easy.rs @@ -1526,6 +1526,12 @@ impl Easy { self.http_proxy_tunnel = enable; } + /// Returns true if HTTP CONNECT tunnel mode is enabled (`--proxytunnel`). + #[must_use] + pub const fn is_http_proxy_tunnel(&self) -> bool { + self.http_proxy_tunnel + } + /// Use HTTP/1.0 for CONNECT proxy requests. /// /// When enabled, the CONNECT request to the proxy uses HTTP/1.0 instead @@ -5788,6 +5794,53 @@ async fn do_single_request( redirected_from_http: bool, fail_on_error: bool, ) -> Result { + // When HTTP proxy tunnel (-p) is active, attempt CONNECT tunnel for all + // non-HTTP protocols that don't already have their own tunnel handling. + // The proxy decides whether to allow the tunnel — if it refuses (e.g. 503), + // we return the error. (curl compat: test 445) + if http_proxy_tunnel { + let is_http_proxy = proxy.is_some_and(|p| { + let s = p.scheme(); + s == "http" || s == "https" + }); + if is_http_proxy { + let scheme = url.scheme(); + // Protocols that already handle tunneling internally: http/https (in the + // HTTP code path below), ftp/ftps (FtpProxyConfig::HttpConnect), + // smtp/imap/pop3 (establish_email_proxy_tunnel). + // For all other protocols, attempt the tunnel here first. + let already_handles_tunnel = matches!( + scheme, + "http" + | "https" + | "ftp" + | "ftps" + | "smtp" + | "smtps" + | "imap" + | "imaps" + | "pop3" + | "pop3s" + ); + if !already_handles_tunnel { + // Attempt the CONNECT tunnel — if it fails, return the error. + // If it succeeds, drop the tunnel stream and proceed to + // protocol-specific handling (which will connect directly). + let _tunnel_stream = establish_email_proxy_tunnel( + proxy, + http_proxy_tunnel, + proxy_credentials, + proxy_headers, + verbose, + proxy_http_10, + headers, + url, + ) + .await?; + } + } + } + // Handle non-HTTP schemes directly match url.scheme() { "file" => { diff --git a/crates/liburlx/src/url.rs b/crates/liburlx/src/url.rs index 4990f4b..b97d53e 100644 --- a/crates/liburlx/src/url.rs +++ b/crates/liburlx/src/url.rs @@ -185,6 +185,16 @@ impl Url { "rtsps" => Some(322), "ldap" => Some(389), "ldaps" => Some(636), + "dict" => Some(2628), + "imap" => Some(143), + "imaps" => Some(993), + "mqtt" => Some(1883), + "mqtts" => Some(8883), + "pop3" => Some(110), + "pop3s" => Some(995), + "smtp" => Some(25), + "smtps" => Some(465), + "telnet" => Some(23), _ => None, }) } diff --git a/crates/urlx-cli/src/transfer.rs b/crates/urlx-cli/src/transfer.rs index 71de0b0..c877caa 100644 --- a/crates/urlx-cli/src/transfer.rs +++ b/crates/urlx-cli/src/transfer.rs @@ -3106,29 +3106,41 @@ pub fn run_multi( // Early scheme validation: reject unsupported protocols before attempting // DNS resolution or connection (curl compat: test 760). + // When HTTP proxy tunnel (-p) is active, skip this check — all protocols + // can be tunneled via CONNECT and the proxy decides whether to allow them + // (curl compat: test 445). if let Some(u) = easy.url_ref() { let scheme = u.scheme().to_lowercase(); - let supported = matches!( - scheme.as_str(), - "http" - | "https" - | "ftp" - | "ftps" - | "sftp" - | "scp" - | "file" - | "dict" - | "tftp" - | "mqtt" - | "ws" - | "wss" - | "smtp" - | "smtps" - | "imap" - | "imaps" - | "pop3" - | "pop3s" - ); + let tunnel_bypass = easy.is_http_proxy_tunnel() && easy.has_http_proxy(); + let supported = tunnel_bypass + || matches!( + scheme.as_str(), + "http" + | "https" + | "ftp" + | "ftps" + | "sftp" + | "scp" + | "file" + | "dict" + | "tftp" + | "mqtt" + | "ws" + | "wss" + | "smtp" + | "smtps" + | "imap" + | "imaps" + | "pop3" + | "pop3s" + | "gopher" + | "gophers" + | "rtsp" + | "ldap" + | "ldaps" + | "smb" + | "smbs" + ); if !supported { if !silent || show_error { eprintln!("curl: (1) Protocol \"{scheme}\" not supported"); From 4690430e235a5f2467652f009bad5ab225028cc8 Mon Sep 17 00:00:00 2001 From: Optio Agent Date: Sat, 28 Mar 2026 00:56:06 +0000 Subject: [PATCH 2/2] fix(proxy): handle RTSP and FTPS proxy tunneling (curl test 445) - Add CONNECT tunnel handling for RTSP in the early dispatch path (RTSP bypasses do_single_request, so the generic tunnel code didn't run for it) - Remove FTPS from the already_handles_tunnel exclusion list so the generic CONNECT tunnel code handles proxy rejection for FTPS URLs All protocols now correctly attempt CONNECT tunnel when -p is active with an HTTP proxy, matching curl's behavior. Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/liburlx/src/easy.rs | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/crates/liburlx/src/easy.rs b/crates/liburlx/src/easy.rs index 59ed8b7..3324850 100644 --- a/crates/liburlx/src/easy.rs +++ b/crates/liburlx/src/easy.rs @@ -3005,6 +3005,28 @@ impl Easy { // Handle RTSP directly (uses persistent session, bypasses HTTP pipeline) if url.scheme() == "rtsp" { + // When proxy tunnel is active, attempt CONNECT tunnel before RTSP + // dispatch (same as the generic tunnel code in do_single_request, + // which RTSP bypasses). (curl compat: test 445) + if self.http_proxy_tunnel { + let is_http_proxy = effective_proxy.is_some_and(|p| { + let s = p.scheme(); + s == "http" || s == "https" + }); + if is_http_proxy { + let _tunnel_stream = establish_email_proxy_tunnel( + effective_proxy, + self.http_proxy_tunnel, + self.proxy_credentials.as_ref(), + &self.proxy_headers, + self.verbose, + self.proxy_http_10, + &headers, + &url, + ) + .await?; + } + } let result = crate::protocol::rtsp::perform_with_session( &url, &headers, @@ -5811,16 +5833,7 @@ async fn do_single_request( // For all other protocols, attempt the tunnel here first. let already_handles_tunnel = matches!( scheme, - "http" - | "https" - | "ftp" - | "ftps" - | "smtp" - | "smtps" - | "imap" - | "imaps" - | "pop3" - | "pop3s" + "http" | "https" | "ftp" | "smtp" | "smtps" | "imap" | "imaps" | "pop3" | "pop3s" ); if !already_handles_tunnel { // Attempt the CONNECT tunnel — if it fails, return the error.