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
This commit is contained in:
Lukas Wagner
2023-01-30 11:31:35 +01:00
committed by Martin Algesten
parent bdcee72c53
commit 20a9ae7977
5 changed files with 31 additions and 18 deletions

View File

@@ -199,14 +199,14 @@ to encode the request body using that.
## Proxying ## 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 always available while the latter must be enabled using the feature
`ureq = { version = "*", features = ["socks-proxy"] }`. `ureq = { version = "*", features = ["socks-proxy"] }`.
Proxies settings are configured on an [Agent] (using [AgentBuilder]). All request sent Proxies settings are configured on an [Agent] (using [AgentBuilder]). All request sent
through the agent will be proxied. through the agent will be proxied.
### Example using HTTP CONNECT ### Example using HTTP
```rust ```rust
fn proxy_example_1() -> std::result::Result<(), ureq::Error> { fn proxy_example_1() -> std::result::Result<(), ureq::Error> {

View File

@@ -225,14 +225,14 @@
//! //!
//! # Proxying //! # 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 //! always available while the latter must be enabled using the feature
//! `ureq = { version = "*", features = ["socks-proxy"] }`. //! `ureq = { version = "*", features = ["socks-proxy"] }`.
//! //!
//! Proxies settings are configured on an [Agent] (using [AgentBuilder]). All request sent //! Proxies settings are configured on an [Agent] (using [AgentBuilder]). All request sent
//! through the agent will be proxied. //! through the agent will be proxied.
//! //!
//! ## Example using HTTP CONNECT //! ## Example using HTTP
//! //!
//! ```rust //! ```rust
//! fn proxy_example_1() -> std::result::Result<(), ureq::Error> { //! fn proxy_example_1() -> std::result::Result<(), ureq::Error> {

View File

@@ -5,7 +5,7 @@ use crate::error::{Error, ErrorKind};
/// Proxy protocol /// Proxy protocol
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum Proto { pub enum Proto {
HTTPConnect, HTTP,
SOCKS4, SOCKS4,
SOCKS4A, SOCKS4A,
SOCKS5, SOCKS5,
@@ -71,7 +71,7 @@ impl Proxy {
/// # Arguments: /// # Arguments:
/// * `proxy` - a str of format `<protocol>://<user>:<password>@<host>:port` . All parts except host are optional. /// * `proxy` - a str of format `<protocol>://<user>:<password>@<host>:port` . All parts except host are optional.
/// # Protocols /// # Protocols
/// * `http`: HTTP Connect /// * `http`: HTTP
/// * `socks4`: SOCKS4 (requires socks feature) /// * `socks4`: SOCKS4 (requires socks feature)
/// * `socks4a`: SOCKS4A (requires socks feature) /// * `socks4a`: SOCKS4A (requires socks feature)
/// * `socks5` and `socks`: SOCKS5 (requires socks feature) /// * `socks5` and `socks`: SOCKS5 (requires socks feature)
@@ -91,7 +91,7 @@ impl Proxy {
let proto = if proxy_parts.len() == 2 { let proto = if proxy_parts.len() == 2 {
match proxy_parts.next() { match proxy_parts.next() {
Some("http") => Proto::HTTPConnect, Some("http") => Proto::HTTP,
Some("socks4") => Proto::SOCKS4, Some("socks4") => Proto::SOCKS4,
Some("socks4a") => Proto::SOCKS4A, Some("socks4a") => Proto::SOCKS4A,
Some("socks") => Proto::SOCKS5, Some("socks") => Proto::SOCKS5,
@@ -99,7 +99,7 @@ impl Proxy {
_ => return Err(ErrorKind::InvalidProxyUrl.new()), _ => return Err(ErrorKind::InvalidProxyUrl.new()),
} }
} else { } else {
Proto::HTTPConnect Proto::HTTP
}; };
let remaining_parts = proxy_parts.next(); let remaining_parts = proxy_parts.next();
@@ -140,7 +140,7 @@ impl Proxy {
)); ));
match self.proto { match self.proto {
Proto::HTTPConnect => format!("Proxy-Authorization: basic {}\r\n", creds), Proto::HTTP => format!("Proxy-Authorization: basic {}\r\n", creds),
_ => String::new(), _ => String::new(),
} }
} else { } else {
@@ -198,7 +198,7 @@ mod tests {
assert_eq!(proxy.password, Some(String::from("p@ssw0rd"))); assert_eq!(proxy.password, Some(String::from("p@ssw0rd")));
assert_eq!(proxy.server, String::from("localhost")); assert_eq!(proxy.server, String::from("localhost"));
assert_eq!(proxy.port, 9999); assert_eq!(proxy.port, 9999);
assert_eq!(proxy.proto, Proto::HTTPConnect); assert_eq!(proxy.proto, Proto::HTTP);
} }
#[test] #[test]
@@ -208,7 +208,7 @@ mod tests {
assert_eq!(proxy.password, Some(String::from("p@ssw0rd"))); assert_eq!(proxy.password, Some(String::from("p@ssw0rd")));
assert_eq!(proxy.server, String::from("localhost")); assert_eq!(proxy.server, String::from("localhost"));
assert_eq!(proxy.port, 9999); assert_eq!(proxy.port, 9999);
assert_eq!(proxy.proto, Proto::HTTPConnect); assert_eq!(proxy.proto, Proto::HTTP);
} }
#[cfg(feature = "socks-proxy")] #[cfg(feature = "socks-proxy")]

View File

@@ -377,7 +377,7 @@ pub(crate) fn connect_host(
// connect with a configured timeout. // connect with a configured timeout.
#[allow(clippy::unnecessary_unwrap)] #[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( connect_socks(
unit, unit,
proxy.clone().unwrap(), proxy.clone().unwrap(),
@@ -423,7 +423,7 @@ pub(crate) fn connect_host(
stream.set_write_timeout(unit.agent.config.timeout_write)?; 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 { if let Some(ref proxy) = proxy {
write!( write!(
stream, stream,

View File

@@ -15,6 +15,7 @@ use crate::body::{self, BodySize, Payload, SizedReader};
use crate::error::{Error, ErrorKind}; use crate::error::{Error, ErrorKind};
use crate::header; use crate::header;
use crate::header::{get_header, Header}; use crate::header::{get_header, Header};
use crate::proxy::Proto;
use crate::resolve::ArcResolver; use crate::resolve::ArcResolver;
use crate::response::Response; use crate::response::Response;
use crate::stream::{self, connect_test, Stream}; 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. // build into a buffer and send in one go.
let mut prelude = PreludeBuilder::new(); 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 // request line
prelude.write_request_line( prelude.write_request_line(&unit.method, &path, unit.url.query().unwrap_or_default())?;
&unit.method,
unit.url.path(),
unit.url.query().unwrap_or_default(),
)?;
// host header if not set by user. // host header if not set by user.
if !header::has_header(&unit.headers, "host") { if !header::has_header(&unit.headers, "host") {