From 20a9ae7977fe4ac26044ef81273e8770b5afb06b Mon Sep 17 00:00:00 2001 From: Lukas Wagner Date: Mon, 30 Jan 2023 11:31:35 +0100 Subject: [PATCH] Proxy: Only use HTTP CONNECT for HTTPS requests Previously, `ureq` used `HTTP CONNECT` for *all* requests, including plain-text HTTP requests. However, some proxy servers like Squid only allow tunneling via the `HTTP CONNECT` method on port 443 - since it is usually only used to proxy HTTPS requests. As a result, it was not possible to use `ureq` with a Squid server in its default configuration. With the changes from this commit, ureq will interact with HTTP proxies in a more standard-conforming way, where `CONNECT` is only used for HTTPS requests. HTTP request paths are transformed in such a way that they comply with RFC 7230 [1]. Tested against Squid 4.13 in standard configuration, with and without basic authentication, for HTTP and HTTPS requests. [1] https://www.rfc-editor.org/rfc/rfc7230#section-5.3.2 --- README.md | 4 ++-- src/lib.rs | 4 ++-- src/proxy.rs | 14 +++++++------- src/stream.rs | 4 ++-- src/unit.rs | 23 ++++++++++++++++++----- 5 files changed, 31 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 8689e4c..fc179e9 100644 --- a/README.md +++ b/README.md @@ -199,14 +199,14 @@ to encode the request body using that. ## Proxying -ureq supports two kinds of proxies, HTTP [`CONNECT`], [`SOCKS4`] and [`SOCKS5`], the former is +ureq supports two kinds of proxies, [`HTTP`], [`SOCKS4`] and [`SOCKS5`], the former is always available while the latter must be enabled using the feature `ureq = { version = "*", features = ["socks-proxy"] }`. Proxies settings are configured on an [Agent] (using [AgentBuilder]). All request sent through the agent will be proxied. -### Example using HTTP CONNECT +### Example using HTTP ```rust fn proxy_example_1() -> std::result::Result<(), ureq::Error> { diff --git a/src/lib.rs b/src/lib.rs index 2e59563..88854c5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -225,14 +225,14 @@ //! //! # Proxying //! -//! ureq supports two kinds of proxies, HTTP [`CONNECT`], [`SOCKS4`] and [`SOCKS5`], the former is +//! ureq supports two kinds of proxies, [`HTTP`], [`SOCKS4`] and [`SOCKS5`], the former is //! always available while the latter must be enabled using the feature //! `ureq = { version = "*", features = ["socks-proxy"] }`. //! //! Proxies settings are configured on an [Agent] (using [AgentBuilder]). All request sent //! through the agent will be proxied. //! -//! ## Example using HTTP CONNECT +//! ## Example using HTTP //! //! ```rust //! fn proxy_example_1() -> std::result::Result<(), ureq::Error> { diff --git a/src/proxy.rs b/src/proxy.rs index c614400..d1ef22c 100644 --- a/src/proxy.rs +++ b/src/proxy.rs @@ -5,7 +5,7 @@ use crate::error::{Error, ErrorKind}; /// Proxy protocol #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub enum Proto { - HTTPConnect, + HTTP, SOCKS4, SOCKS4A, SOCKS5, @@ -71,7 +71,7 @@ impl Proxy { /// # Arguments: /// * `proxy` - a str of format `://:@:port` . All parts except host are optional. /// # Protocols - /// * `http`: HTTP Connect + /// * `http`: HTTP /// * `socks4`: SOCKS4 (requires socks feature) /// * `socks4a`: SOCKS4A (requires socks feature) /// * `socks5` and `socks`: SOCKS5 (requires socks feature) @@ -91,7 +91,7 @@ impl Proxy { let proto = if proxy_parts.len() == 2 { match proxy_parts.next() { - Some("http") => Proto::HTTPConnect, + Some("http") => Proto::HTTP, Some("socks4") => Proto::SOCKS4, Some("socks4a") => Proto::SOCKS4A, Some("socks") => Proto::SOCKS5, @@ -99,7 +99,7 @@ impl Proxy { _ => return Err(ErrorKind::InvalidProxyUrl.new()), } } else { - Proto::HTTPConnect + Proto::HTTP }; let remaining_parts = proxy_parts.next(); @@ -140,7 +140,7 @@ impl Proxy { )); match self.proto { - Proto::HTTPConnect => format!("Proxy-Authorization: basic {}\r\n", creds), + Proto::HTTP => format!("Proxy-Authorization: basic {}\r\n", creds), _ => String::new(), } } else { @@ -198,7 +198,7 @@ mod tests { assert_eq!(proxy.password, Some(String::from("p@ssw0rd"))); assert_eq!(proxy.server, String::from("localhost")); assert_eq!(proxy.port, 9999); - assert_eq!(proxy.proto, Proto::HTTPConnect); + assert_eq!(proxy.proto, Proto::HTTP); } #[test] @@ -208,7 +208,7 @@ mod tests { assert_eq!(proxy.password, Some(String::from("p@ssw0rd"))); assert_eq!(proxy.server, String::from("localhost")); assert_eq!(proxy.port, 9999); - assert_eq!(proxy.proto, Proto::HTTPConnect); + assert_eq!(proxy.proto, Proto::HTTP); } #[cfg(feature = "socks-proxy")] diff --git a/src/stream.rs b/src/stream.rs index 317df21..8587680 100644 --- a/src/stream.rs +++ b/src/stream.rs @@ -377,7 +377,7 @@ pub(crate) fn connect_host( // connect with a configured timeout. #[allow(clippy::unnecessary_unwrap)] - let stream = if proto.is_some() && Some(Proto::HTTPConnect) != proto { + let stream = if proto.is_some() && Some(Proto::HTTP) != proto { connect_socks( unit, proxy.clone().unwrap(), @@ -423,7 +423,7 @@ pub(crate) fn connect_host( stream.set_write_timeout(unit.agent.config.timeout_write)?; } - if proto == Some(Proto::HTTPConnect) { + if proto == Some(Proto::HTTP) && unit.url.scheme() == "https" { if let Some(ref proxy) = proxy { write!( stream, diff --git a/src/unit.rs b/src/unit.rs index 2011bb6..84c41e8 100644 --- a/src/unit.rs +++ b/src/unit.rs @@ -15,6 +15,7 @@ use crate::body::{self, BodySize, Payload, SizedReader}; use crate::error::{Error, ErrorKind}; use crate::header; use crate::header::{get_header, Header}; +use crate::proxy::Proto; use crate::resolve::ArcResolver; use crate::response::Response; use crate::stream::{self, connect_test, Stream}; @@ -404,12 +405,24 @@ fn send_prelude(unit: &Unit, stream: &mut Stream) -> io::Result<()> { // build into a buffer and send in one go. let mut prelude = PreludeBuilder::new(); + let path = if let Some(proxy) = &unit.agent.config.proxy { + // HTTP proxies require the path to be in absolute URI form + // https://www.rfc-editor.org/rfc/rfc7230#section-5.3.2 + match proxy.proto { + Proto::HTTP => format!( + "{}://{}{}", + unit.url.scheme(), + unit.url.host().unwrap(), + unit.url.path() + ), + _ => unit.url.path().into(), + } + } else { + unit.url.path().into() + }; + // request line - prelude.write_request_line( - &unit.method, - unit.url.path(), - unit.url.query().unwrap_or_default(), - )?; + prelude.write_request_line(&unit.method, &path, unit.url.query().unwrap_or_default())?; // host header if not set by user. if !header::has_header(&unit.headers, "host") {