Skip to content
Open
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
45 changes: 41 additions & 4 deletions node/src/proxy_server/client_request_payload_factory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,27 @@ impl ClientRequestPayloadFactory for ClientRequestPayloadFactoryReal {
// So far we've only looked in the client packet; but this message will evaporate
// unless there's no host information in host_opt (from ProxyServer's StreamInfo) either.
None => Err(format!(
"No hostname information found in either client packet or ProxyServer for protocol {:?}",
protocol_pack.proxy_protocol()
"No hostname information found in either client packet ({}) or ProxyServer for protocol {:?}, with StreamKey {}",
protocol_pack.describe_packet(&data),
protocol_pack.proxy_protocol(),
stream_key
)),
}
});
let target_host = match (host_from_request_result_closure(), host_from_history_opt) {
(Ok(host), _) => host,
(Err(_), Some(host)) => host,
(Err(e), None) => {
if ibcd.last_data && ibcd.data.is_empty() {
warning!(
logger,
"Client opened {:?} connection and immediately closed it without sending any data, with StreamKey {}",
protocol_pack.proxy_protocol(),
stream_key
);
} else {
error!(logger, "{}", e);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Unconditional Error Logging Causes Redundancy

An unconditional error!(logger, "{}", e); statement executes after the if/else block. This results in either a warning followed by an error, or a duplicate error log, undermining the intended conditional logging for different failure scenarios.

Fix in Cursor Fix in Web

error!(logger, "{}", e);
return None;
}
Expand Down Expand Up @@ -187,7 +199,32 @@ mod tests {
let result = subject.make(&ibcd, stream_key, None, cryptde.as_ref(), &logger);

assert_eq!(result, None);
TestLogHandler::new().exists_log_containing(&format!("ERROR: {test_name}: No hostname information found in either client packet or ProxyServer for protocol HTTP"));
TestLogHandler::new().exists_log_containing(&format!("ERROR: {test_name}: No hostname information found in either client packet (Malformed HTTP request: '') or ProxyServer for protocol HTTP"));
}

#[test]
fn logs_different_error_and_returns_none_if_connection_is_opened_and_immediately_closed() {
init_test_logging();
let test_name =
"logs_different_error_and_returns_none_if_connection_is_opened_and_immediately_closed";
let ibcd = InboundClientData {
timestamp: SystemTime::now(),
client_addr: SocketAddr::from_str("1.2.3.4:5678").unwrap(),
reception_port_opt: Some(HTTP_PORT),
sequence_number_opt: Some(1),
last_data: true,
is_clandestine: false,
data: vec![],
};
let cryptde = CRYPTDE_PAIR.main.dup();
let stream_key = StreamKey::make_meaningful_stream_key(test_name);
let logger = Logger::new(test_name);
let subject = Box::new(ClientRequestPayloadFactoryReal::new());

let result = subject.make(&ibcd, stream_key, None, cryptde.as_ref(), &logger);

assert_eq!(result, None);
TestLogHandler::new().exists_log_containing(&format!("WARN: {test_name}: Client opened HTTP connection and immediately closed it without sending any data"));
}

#[test]
Expand Down Expand Up @@ -353,7 +390,7 @@ mod tests {
let result = subject.make(&ibcd, stream_key, None, cryptde, &logger);

assert_eq!(result, None);
TestLogHandler::new().exists_log_containing(&format!("ERROR: {test_name}: No hostname information found in either client packet or ProxyServer for protocol TLS"));
TestLogHandler::new().exists_log_containing(&format!("ERROR: {test_name}: No hostname information found in either client packet (ClientHello with no SNI extension) or ProxyServer for protocol TLS"));
}

#[test]
Expand Down
163 changes: 163 additions & 0 deletions node/src/proxy_server/http_protocol_pack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,14 @@ impl ProtocolPack for HttpProtocolPack {
fn server_impersonator(&self) -> Box<dyn ServerImpersonator> {
Box::new(ServerImpersonatorHttp {})
}

fn describe_packet(&self, data: &PlainData) -> String {
if data.as_slice().starts_with(b"HTTP/") {
self.describe_response(data)
} else {
self.describe_request(data)
}
}
}

impl HttpProtocolPack {
Expand Down Expand Up @@ -102,6 +110,82 @@ impl HttpProtocolPack {
Ok(port) => Ok(port),
}
}

fn describe_request(&self, data: &PlainData) -> String {
let first_line_end = data
.as_slice()
.iter()
.position(|&b| b == b'\r')
.unwrap_or(data.len());
let first_line = &data.as_slice()[0..first_line_end];
let mut parts = first_line.split(|&b| b == b' ');
if let (Some(method_bytes), Some(path_bytes), Some(http_version_bytes)) =
(parts.next(), parts.next(), parts.next())
{
let method = Self::from_utf8(method_bytes);
let path = Self::from_utf8(path_bytes);
let http_version = Self::from_utf8(http_version_bytes);
if let Some(host) = self.find_host(data) {
return if path.starts_with('/') {
format!(
"{} {} request to {}{}",
http_version, method, host.name, path
)
} else {
format!("{} {} request to {}", http_version, method, path)
};
} else {
return format!(
"{} {} request to unknown host: {}",
http_version, method, path
);
}
}
format!(
"Malformed HTTP request: {}",
Self::truncate_data_as_string(data, 50)
)
}

fn describe_response(&self, data: &PlainData) -> String {
let first_line_end = data
.as_slice()
.iter()
.position(|&b| b == b'\r')
.unwrap_or(data.as_slice().len());
let first_line = &data.as_slice()[0..first_line_end];
let mut parts = first_line.split(|&b| b == b' ');
if let (Some(http_version_bytes), Some(status_code_bytes), Some(status_text_bytes)) =
(parts.next(), parts.next(), parts.next())
{
let http_with_version = Self::from_utf8(http_version_bytes);
let status_code = Self::from_utf8(status_code_bytes);
let status_text = Self::from_utf8(status_text_bytes);
return format!(
"{} response with status {} {}",
http_with_version, status_code, status_text
);
}
format!(
"Malformed HTTP response: {}",
Self::truncate_data_as_string(data, 50)
)
}

fn truncate_data_as_string(data: &PlainData, truncate_at: usize) -> String {
if data.as_slice().len() > truncate_at {
format!(
"'{}'...",
String::from_utf8_lossy(&data.as_slice()[0..truncate_at])
)
} else {
format!("'{}'", String::from_utf8_lossy(data.as_slice()))
}
}

fn from_utf8(bytes: &[u8]) -> String {
String::from_utf8_lossy(bytes).to_string()
}
}

#[cfg(test)]
Expand Down Expand Up @@ -338,4 +422,83 @@ mod tests {
let data = b"CONNECTX";
assert!(!HttpProtocolPack::is_connect(data));
}

#[test]
fn describe_packet_works_on_get_request_with_header_host() {
let data =
PlainData::new(b"GET /index.html?item=booga HTTP/1.1\r\nHost: www.example.com\r\n\r\n");
let subject = HttpProtocolPack {};

let result = subject.describe_packet(&data);

assert_eq!(
result,
"HTTP/1.1 GET request to www.example.com/index.html?item=booga"
);
}

#[test]
fn describe_packet_works_on_post_request_with_url_host() {
let data = PlainData::new(
b"POST www.example.com/person/1234 HTTP/1.1\r\nContent-Length: 2\r\n\r\n{}",
);
let subject = HttpProtocolPack {};

let result = subject.describe_packet(&data);

assert_eq!(
result,
"HTTP/1.1 POST request to www.example.com/person/1234"
);
}

#[test]
fn describe_packet_works_on_unexpected_request_with_unspecified_host() {
let data = PlainData::new(b"BOOGA /person/1234 HTTP/1.1\r\nContent-Length: 2\r\n\r\n{}");
let subject = HttpProtocolPack {};

let result = subject.describe_packet(&data);

assert_eq!(
result,
"HTTP/1.1 BOOGA request to unknown host: /person/1234"
);
}

#[test]
fn describe_packet_works_on_malformed_request() {
let data = PlainData::new(b"Fourscore_and_seven_years_ago_our_fathers_brought_forth_on_this_continent_a_new_nation,_conceived_in_liberty_and_dedicated_to_the_proposition_that_all_men_are_created_equal.");
let subject = HttpProtocolPack {};

let result = subject.describe_packet(&data);

assert_eq!(
result,
"Malformed HTTP request: 'Fourscore_and_seven_years_ago_our_fathers_brought_'..."
);
}

#[test]
fn describe_packet_works_on_200_response() {
let data =
PlainData::new(b"HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n<html></html>");
let subject = HttpProtocolPack {};

let result = subject.describe_packet(&data);

assert_eq!(result, "HTTP/1.1 response with status 200 OK");
}

#[test]
fn describe_packet_works_on_malformed_response() {
let data = PlainData::new(b"HTTP/Fourscore_and_seven_years_ago_our_fathers_brought_forth_on_this_continent_a_new_nation,_conceived_in_liberty_and_dedicated_to_the_proposition_that_all_men_are_created_equal.");
let subject = HttpProtocolPack {};

let result = subject.describe_packet(&data);

assert_eq!(
result,
"Malformed HTTP response: 'HTTP/Fourscore_and_seven_years_ago_our_fathers_bro'..."
);
}
}
1 change: 1 addition & 0 deletions node/src/proxy_server/protocol_pack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ pub trait ProtocolPack: Send + Sync {
fn standard_port(&self) -> u16;
fn find_host(&self, data: &PlainData) -> Option<Host>;
fn server_impersonator(&self) -> Box<dyn ServerImpersonator>;
fn describe_packet(&self, data: &PlainData) -> String;
}

pub fn from_protocol(protocol: ProxyProtocol) -> Box<dyn ProtocolPack> {
Expand Down
Loading
Loading