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
11 changes: 10 additions & 1 deletion crates/rttp-client/src/connection/async_connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,13 @@ use std::sync::Arc;
use crate::connection::connection::{
connect_tcp_stream, parse_proxy_connect_response, prepend_informational_responses,
request_expects_continue, Connection, ExpectContinueResult,
MAX_PROXY_CONNECT_INFORMATIONAL_RESPONSES,
};
use crate::connection::connection_reader::{
is_skippable_informational_status, parse_informational_response, response_body_kind,
response_connection_reusable, response_connection_should_close, response_headers,
response_status_code, validate_response_trailer_header, ResponseBodyKind, ResponseParts,
MAX_CHUNKED_RESPONSE_LINE_BYTES,
MAX_CHUNKED_RESPONSE_LINE_BYTES, MAX_RESPONSE_HEAD_BYTES,
};
use crate::error;
use crate::request::RawRequest;
Expand Down Expand Up @@ -1176,10 +1177,14 @@ async fn async_read_proxy_connect_response<S>(stream: &mut S) -> error::Result<(
where
S: AsyncRead + Unpin,
{
let mut informational_responses = 0;
loop {
let mut header = Vec::new();
let mut byte = [0u8; 1];
loop {
if header.len() == MAX_RESPONSE_HEAD_BYTES {
return Err(error::bad_proxy("Proxy response head is too large"));
}
let read = stream.read(&mut byte).await.map_err(error::request)?;
if read == 0 {
return Err(if header.is_empty() {
Expand All @@ -1196,6 +1201,10 @@ where
let status_code = response_status_code(&header)
.map_err(|_| error::bad_proxy("parse proxy server response error."))?;
if is_skippable_informational_status(status_code) {
if informational_responses == MAX_PROXY_CONNECT_INFORMATIONAL_RESPONSES {
return Err(error::bad_proxy("Too many informational proxy responses"));
}
informational_responses += 1;
continue;
}
return parse_proxy_connect_response(&header);
Expand Down
45 changes: 44 additions & 1 deletion crates/rttp-client/src/connection/connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use crate::connection::connection_reader::{
is_skippable_informational_status, parse_informational_response, read_response_head,
read_response_header, read_response_parts_after_header,
read_response_parts_after_header_with_informational_and_limit, response_status_code,
ConnectionReader, ResponseParts,
ConnectionReader, ResponseParts, MAX_RESPONSE_HEAD_BYTES,
};
use crate::request::{RawRequest, RequestBody};
use crate::response::{InformationalResponse, Response};
Expand Down Expand Up @@ -587,9 +587,13 @@ where
R: io::Read,
{
let mut header = Vec::new();
let mut informational_responses = 0;
let mut byte = [0u8; 1];

loop {
if header.len() == MAX_RESPONSE_HEAD_BYTES {
return Err(error::bad_proxy("Proxy response head is too large"));
}
let read = reader.read(&mut byte).map_err(error::request)?;
if read == 0 {
if header.is_empty() {
Expand All @@ -602,6 +606,10 @@ where
if header.ends_with(b"\r\n\r\n") {
let status_code = proxy_connect_response_status_code(&header)?;
if is_skippable_informational_status(status_code) {
if informational_responses == MAX_PROXY_CONNECT_INFORMATIONAL_RESPONSES {
return Err(error::bad_proxy("Too many informational proxy responses"));
}
informational_responses += 1;
header.clear();
continue;
}
Expand All @@ -610,6 +618,8 @@ where
}
}

pub(crate) const MAX_PROXY_CONNECT_INFORMATIONAL_RESPONSES: usize = 16;

fn proxy_connect_response_status_code(header: &[u8]) -> error::Result<u16> {
let header = String::from_utf8(header.to_vec())
.map_err(|_| error::bad_proxy("parse proxy server response error."))?;
Expand Down Expand Up @@ -1255,7 +1265,9 @@ mod tests {
use super::{
connect_tcp_stream, parse_proxy_connect_response, proxy_authorization_value,
read_proxy_connect_response, strip_userinfo_for_cross_origin_redirect, write_http_request,
MAX_PROXY_CONNECT_INFORMATIONAL_RESPONSES,
};
use crate::connection::connection_reader::MAX_RESPONSE_HEAD_BYTES;

struct PartialWriter {
max_chunk: usize,
Expand Down Expand Up @@ -1341,6 +1353,37 @@ mod tests {
assert_eq!(raw.len() as u64, reader.position());
}

#[test]
fn test_read_proxy_connect_response_rejects_oversized_head() {
let raw = format!(
"HTTP/1.1 200 Connection Established\r\nX-Fill: {}",
"a".repeat(MAX_RESPONSE_HEAD_BYTES)
);
let mut reader = Cursor::new(raw.as_bytes());

let error = read_proxy_connect_response(&mut reader)
.expect_err("oversized proxy response head should be rejected");

assert!(error
.to_string()
.contains("Proxy response head is too large"));
assert_eq!(MAX_RESPONSE_HEAD_BYTES as u64, reader.position());
}

#[test]
fn test_read_proxy_connect_response_rejects_excessive_informational_sequence() {
let raw =
"HTTP/1.1 103 Early Hints\r\n\r\n".repeat(MAX_PROXY_CONNECT_INFORMATIONAL_RESPONSES + 1);
let mut reader = Cursor::new(raw.as_bytes());

let error = read_proxy_connect_response(&mut reader)
.expect_err("excessive informational proxy responses should be rejected");

assert!(error
.to_string()
.contains("Too many informational proxy responses"));
}

#[test]
fn test_strip_userinfo_for_cross_origin_redirect_removes_redirect_credentials() {
let base = Url::parse("http://user:secret@example.test/start").unwrap();
Expand Down
25 changes: 25 additions & 0 deletions crates/rttp-client/tests/test_http_async.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2510,3 +2510,28 @@ fn test_async_https_proxy_with_auth_uses_connect_tunnel() {
assert_eq!("OK", response.body().string().unwrap());
});
}

#[test]
#[cfg(all(feature = "async", feature = "tls-rustls"))]
fn test_async_https_proxy_rejects_excessive_informational_connect_responses() {
let response = "HTTP/1.1 103 Early Hints\r\n\r\n".repeat(17);
let (proxy_addr, proxy_handle) = support::spawn_proxy_connect_response_server(response);

block_on(async {
let error = client()
.get()
.url("https://localhost/")
.proxy(Proxy::http("127.0.0.1", u32::from(proxy_addr.port())))
.rasync()
.await
.expect_err("excessive informational proxy responses should fail");

assert!(error
.to_string()
.contains("Too many informational proxy responses"));
});
assert!(
proxy_handle.join().expect("proxy response server"),
"async client should close the rejected proxy connection"
);
}
25 changes: 25 additions & 0 deletions crates/rttp-client/tests/test_http_basic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2156,6 +2156,31 @@ fn test_https_proxy_with_auth_uses_connect_tunnel() {
assert_eq!("OK", response.body().string().unwrap());
}

#[test]
#[cfg(feature = "tls-rustls")]
fn test_https_proxy_rejects_oversized_connect_response_and_closes_connection() {
let response = format!(
"HTTP/1.1 200 Connection Established\r\nX-Fill: {}",
"a".repeat(support::response::MAX_HEAD_BYTES)
);
let (proxy_addr, proxy_handle) = support::spawn_proxy_connect_response_server(response);

let error = client()
.get()
.url("https://localhost/")
.proxy(Proxy::http("127.0.0.1", u32::from(proxy_addr.port())))
.emit()
.expect_err("oversized proxy CONNECT response should fail");

assert!(error
.to_string()
.contains("Proxy response head is too large"));
assert!(
proxy_handle.join().expect("proxy response server"),
"client should close the rejected proxy connection"
);
}

#[test]
fn test_connection_closed() {
let (addr, _handle) = support::spawn_http_server_count(5);
Expand Down
30 changes: 29 additions & 1 deletion crates/rttp-test-support/src/client_helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -998,6 +998,34 @@ fn spawn_socks5_proxy_server_with_auth(
(addr, handle)
}

pub fn spawn_proxy_connect_response_server(
response: impl Into<Vec<u8>>,
) -> (SocketAddr, JoinHandle<bool>) {
let (listener, addr) = bind_local_http_listener("proxy CONNECT response server");
let response = response.into();
let handle = thread::spawn(move || {
let Ok((mut client, _)) = listener.accept() else {
return false;
};
let _ = read_http_request(&mut client);
if client.write_all(&response).is_err() || client.flush().is_err() {
return true;
}

let _ = client.set_read_timeout(Some(Duration::from_secs(1)));
let mut byte = [0u8; 1];
match client.read(&mut byte) {
Ok(0) => true,
Err(error) => matches!(
error.kind(),
io::ErrorKind::ConnectionReset | io::ErrorKind::BrokenPipe
),
Ok(_) => false,
}
});
(addr, handle)
}

#[cfg(feature = "tls-rustls")]
pub fn spawn_https_proxy_server_with_credentials(
username: &'static str,
Expand Down Expand Up @@ -1032,7 +1060,7 @@ pub fn spawn_https_proxy_server_with_credentials(
.to_string();
let mut server = TcpStream::connect(&target).expect("connect tls target");

let _ = client.write_all(b"HTTP/1.1 200 Conne");
let _ = client.write_all(b"HTTP/1.1 103 Early Hints\r\n\r\nHTTP/1.1 200 Conne");
let _ = client.flush();
thread::sleep(Duration::from_millis(20));
let _ = client.write_all(b"ction Established\r\nProxy-Agent: test\r\n\r\n");
Expand Down