Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions crates/rttp-client/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ pub struct Config {
write_timeout: u64,
auto_redirect: bool,
max_redirect: u32,
allow_https_to_http_redirects: bool,
verify_ssl_hostname: bool,
verify_ssl_cert: bool,
http2_max_frame_size: Option<usize>,
Expand All @@ -55,6 +56,7 @@ impl Default for Config {
.write_timeout(10000)
.auto_redirect(false)
.max_redirect(0)
.allow_https_to_http_redirects(false)
.build()
}
}
Expand All @@ -78,6 +80,9 @@ impl Config {
pub fn max_redirect(&self) -> u32 {
self.max_redirect
}
pub fn allow_https_to_http_redirects(&self) -> bool {
self.allow_https_to_http_redirects
}
pub fn verify_ssl_cert(&self) -> bool {
self.verify_ssl_cert
}
Expand Down Expand Up @@ -123,6 +128,7 @@ impl ConfigBuilder {
write_timeout: 10000,
auto_redirect: false,
max_redirect: 0,
allow_https_to_http_redirects: false,
verify_ssl_hostname: true,
verify_ssl_cert: true,
http2_max_frame_size: None,
Expand Down Expand Up @@ -154,6 +160,17 @@ impl ConfigBuilder {
self.config.max_redirect = max_redirect;
self
}
/// Allow automatic redirects from HTTPS URLs to HTTP URLs.
///
/// This is disabled by default because following such a redirect removes
/// transport security from the request.
pub fn allow_https_to_http_redirects(
&mut self,
allow_https_to_http_redirects: bool,
) -> &mut Self {
self.config.allow_https_to_http_redirects = allow_https_to_http_redirects;
self
}
pub fn verify_ssl_hostname(&mut self, verify_ssl_hostname: bool) -> &mut Self {
self.config.verify_ssl_hostname = verify_ssl_hostname;
self
Expand Down Expand Up @@ -214,6 +231,7 @@ mod tests {
.write_timeout(4321)
.auto_redirect(true)
.max_redirect(5)
.allow_https_to_http_redirects(true)
.verify_ssl_hostname(false)
.verify_ssl_cert(false)
.http2_max_frame_size(16_384)
Expand All @@ -224,6 +242,7 @@ mod tests {
assert_eq!(config.write_timeout(), 4321);
assert!(config.auto_redirect());
assert_eq!(config.max_redirect(), 5);
assert!(config.allow_https_to_http_redirects());
assert!(!config.verify_ssl_hostname());
assert!(!config.verify_ssl_cert());
assert_eq!(Some(16_384), config.http2_max_frame_size());
Expand All @@ -245,6 +264,10 @@ mod tests {
builder_config.auto_redirect()
);
assert_eq!(default_config.max_redirect(), builder_config.max_redirect());
assert_eq!(
default_config.allow_https_to_http_redirects(),
builder_config.allow_https_to_http_redirects()
);
assert_eq!(
default_config.verify_ssl_hostname(),
builder_config.verify_ssl_hostname()
Expand Down
6 changes: 6 additions & 0 deletions crates/rttp-client/src/connection/async_connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,12 @@ impl<'a> AsyncConnection<'a> {
}

let redirect_url = self.conn.resolve_redirect_url(&url, location)?;
if url.scheme() == "https"
&& redirect_url.url.scheme() == "http"
&& !config.allow_https_to_http_redirects()
{
return Err(error::https_to_http_redirect(url));
}
let strip_sensitive_headers = !self.conn.is_same_origin_url(&url, &redirect_url.url);
if !self.conn.is_same_origin_url(&url, &redirect_url.url)
|| redirect_url.url.scheme() != "http"
Expand Down
6 changes: 6 additions & 0 deletions crates/rttp-client/src/connection/block_connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,12 @@ impl<'a> BlockConnection<'a> {
}

let redirect_url = self.conn.resolve_redirect_url(&url, location)?;
if url.scheme() == "https"
&& redirect_url.url.scheme() == "http"
&& !config.allow_https_to_http_redirects()
{
return Err(error::https_to_http_redirect(url));
}
let strip_sensitive_headers = !self.conn.is_same_origin_url(&url, &redirect_url.url);
if !self.conn.is_same_origin_url(&url, &redirect_url.url)
|| redirect_url.url.scheme() != "http"
Expand Down
8 changes: 8 additions & 0 deletions crates/rttp-client/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,14 @@ pub(crate) fn too_many_redirects(url: Url) -> Error {
Error::new(Kind::Redirect, Some("too many redirects")).with_url(url)
}

pub(crate) fn https_to_http_redirect(url: Url) -> Error {
Error::new(
Kind::Redirect,
Some("HTTPS to HTTP redirect is not allowed"),
)
.with_url(url)
}

#[allow(dead_code)]
pub(crate) fn status_code(url: Url, status: StatusCode) -> Error {
Error::new(Kind::Status(status), None::<Error>).with_url(url)
Expand Down
57 changes: 57 additions & 0 deletions crates/rttp-client/tests/test_http_async.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1975,6 +1975,63 @@ fn test_async_https() {
});
}

#[test]
#[cfg(all(feature = "async", feature = "tls-rustls"))]
fn test_async_https_to_http_redirect_is_rejected_before_target_connection() {
let target = TcpListener::bind("127.0.0.1:0").expect("bind redirect target");
let target_addr = target.local_addr().expect("redirect target address");
let (addr, _handle) = support::spawn_tls_redirect_server(format!("http://{}/final", target_addr));
block_on(async {
let error = client()
.get()
.url(format!("https://{}/", addr))
.config(
Config::builder()
.auto_redirect(true)
.verify_ssl_cert(false)
.verify_ssl_hostname(false),
)
.rasync()
.await
.expect_err("HTTPS to HTTP redirect should be rejected by default");

assert!(error.is_redirect());
assert!(error.to_string().contains("HTTPS to HTTP redirect"));
assert_eq!("https", error.url().expect("redirect source URL").scheme());
});
target
.set_nonblocking(true)
.expect("set redirect target nonblocking");
assert!(matches!(
target.accept(),
Err(ref error) if error.kind() == std::io::ErrorKind::WouldBlock
));
}

#[test]
#[cfg(all(feature = "async", feature = "tls-rustls"))]
fn test_async_https_to_http_redirect_can_be_enabled_explicitly() {
let (target_addr, _target_handle) = support::spawn_http_server();
let (addr, _handle) = support::spawn_tls_redirect_server(format!("http://{}/final", target_addr));
block_on(async {
let response = client()
.get()
.url(format!("https://{}/", addr))
.config(
Config::builder()
.auto_redirect(true)
.allow_https_to_http_redirects(true)
.verify_ssl_cert(false)
.verify_ssl_hostname(false),
)
.rasync()
.await
.expect("explicit HTTPS to HTTP redirect opt-in should succeed");

assert!(response.ok());
});
}

#[test]
#[cfg(feature = "async")]
fn test_async_http_proxy_uses_absolute_form_for_http_requests() {
Expand Down
51 changes: 51 additions & 0 deletions crates/rttp-client/tests/test_http_basic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1193,6 +1193,57 @@ fn test_https() {
assert!(response.is_ok());
}

#[test]
#[cfg(feature = "tls-rustls")]
fn test_https_to_http_redirect_is_rejected_before_target_connection() {
let target = TcpListener::bind("127.0.0.1:0").expect("bind redirect target");
let target_addr = target.local_addr().expect("redirect target address");
let (addr, _handle) = support::spawn_tls_redirect_server(format!("http://{}/final", target_addr));
let error = client()
.get()
.url(format!("https://{}/", addr))
.config(
Config::builder()
.auto_redirect(true)
.verify_ssl_cert(false)
.verify_ssl_hostname(false),
)
.emit()
.expect_err("HTTPS to HTTP redirect should be rejected by default");

assert!(error.is_redirect());
assert!(error.to_string().contains("HTTPS to HTTP redirect"));
assert_eq!("https", error.url().expect("redirect source URL").scheme());
target
.set_nonblocking(true)
.expect("set redirect target nonblocking");
assert!(matches!(
target.accept(),
Err(ref error) if error.kind() == io::ErrorKind::WouldBlock
));
}

#[test]
#[cfg(feature = "tls-rustls")]
fn test_https_to_http_redirect_can_be_enabled_explicitly() {
let (target_addr, _target_handle) = support::spawn_http_server();
let (addr, _handle) = support::spawn_tls_redirect_server(format!("http://{}/final", target_addr));
let response = client()
.get()
.url(format!("https://{}/", addr))
.config(
Config::builder()
.auto_redirect(true)
.allow_https_to_http_redirects(true)
.verify_ssl_cert(false)
.verify_ssl_hostname(false),
)
.emit()
.expect("explicit HTTPS to HTTP redirect opt-in should succeed");

assert!(response.ok());
}

#[test]
fn test_http_with_url() {
let (addr, _handle) = support::spawn_http_server();
Expand Down
39 changes: 39 additions & 0 deletions crates/rttp-test-support/src/client_helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1087,3 +1087,42 @@ pub fn spawn_tls_server() -> (SocketAddr, JoinHandle<()>) {
});
(addr, handle)
}

#[cfg(feature = "tls-rustls")]
pub fn spawn_tls_redirect_server(location: String) -> (SocketAddr, JoinHandle<()>) {
use rcgen::generate_simple_self_signed;
use rustls::pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer};
use rustls::{ServerConfig, ServerConnection, StreamOwned};
use std::sync::Arc;

let cert = generate_simple_self_signed(vec!["localhost".to_string()]).expect("generate cert");
let cert_der = cert.serialize_der().expect("cert der");
let key_der = cert.serialize_private_key_der();
let config = ServerConfig::builder()
.with_no_client_auth()
.with_single_cert(
vec![CertificateDer::from(cert_der)],
PrivateKeyDer::from(PrivatePkcs8KeyDer::from(key_der)),
)
.expect("set cert");
let config = Arc::new(config);

let (listener, addr) = bind_local_http_listener("tls redirect server");
let handle = thread::spawn(move || {
if let Ok((stream, _)) = listener.accept() {
let session = ServerConnection::new(config).expect("server connection");
let mut tls = StreamOwned::new(session, stream);
let _ = read_http_request(&mut tls);
let response = format!(
"HTTP/1.1 302 Found\r\nLocation: {}\r\nContent-Length: 0\r\nConnection: close\r\n\r\n",
location
);
let _ = tls.write_all(response.as_bytes());
let _ = tls.flush();
tls.conn.send_close_notify();
let _ = tls.flush();
}
});

(addr, handle)
}