Skip to content

Commit

Permalink
Allow ignoring invalid header lines (fixes seanmonstar#61, seanmonsta…
Browse files Browse the repository at this point in the history
  • Loading branch information
nox committed Apr 26, 2022
1 parent 7e97ef1 commit 67a2898
Showing 1 changed file with 271 additions and 15 deletions.
286 changes: 271 additions & 15 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -247,10 +247,11 @@ pub struct ParserConfig {
allow_obsolete_multiline_headers_in_responses: bool,
allow_multiple_spaces_in_request_line_delimiters: bool,
allow_multiple_spaces_in_response_status_delimiters: bool,
ignore_invalid_headers_in_responses: bool,
}

impl ParserConfig {
/// Sets whether spaces should be allowed after header name.
/// Sets whether spaces and tabs should be allowed after header names in responses.
pub fn allow_spaces_after_header_name_in_responses(
&mut self,
value: bool,
Expand Down Expand Up @@ -361,6 +362,26 @@ impl ParserConfig {
request.parse_with_config_and_uninit_headers(buf, self, headers)
}

/// Sets whether invalid header lines should be silently ignored in responses.
///
/// This mimicks the behaviour of major browsers. You probably don't want this.
/// You should only want this if you are implementing a proxy whose main
/// purpose is to sit in front of browsers whose users access arbitrary content
/// which may be malformed, and they expect everything that works without
/// the proxy to keep working with the proxy.
///
/// This option will prevent `ParserConfig::parse_response` from returning
/// an error encountered when parsing a header, except if the error was caused
/// by the character NUL (ASCII code 0), as Chrome specifically always reject
/// those.
pub fn ignore_invalid_headers_in_responses(
&mut self,
value: bool,
) -> &mut Self {
self.ignore_invalid_headers_in_responses = value;
self
}

/// Parses a response with the given config.
pub fn parse_response<'headers, 'buf>(
&self,
Expand Down Expand Up @@ -947,8 +968,30 @@ fn parse_headers_iter_uninit<'a, 'b>(
}

'headers: loop {
macro_rules! continue_with_new_header_on_next_line {
($bytes:ident, $b:ident, $err:ident) => {
if !config.ignore_invalid_headers_in_responses {
return Err(Error::$err);
}

let mut b = $b;

while b != b'\n' {
if b == b'\0' {
return Err(Error::$err);
}
b = next!($bytes);
}

count += $bytes.pos();
$bytes.slice();

continue 'headers;
};
}

// a newline here means the head is over!
let mut b = next!(bytes);
let b = next!(bytes);
if b == b'\r' {
expect!(bytes.next() == b'\n' => Err(Error::NewLine));
result = Ok(Status::Complete(count + bytes.pos()));
Expand All @@ -959,14 +1002,9 @@ fn parse_headers_iter_uninit<'a, 'b>(
break;
}
if !is_header_name_token(b) {
return Err(Error::HeaderName);
continue_with_new_header_on_next_line!(bytes, b, HeaderName);
}

let uninit_header = match iter.next() {
Some(header) => header,
None => break 'headers
};

// parse header name until colon
let header_name: &str = 'name: loop {
let mut b = next!(bytes);
Expand Down Expand Up @@ -996,7 +1034,7 @@ fn parse_headers_iter_uninit<'a, 'b>(
}
}

return Err(Error::HeaderName);
continue_with_new_header_on_next_line!(bytes, b, HeaderName);
};

let mut b;
Expand All @@ -1017,7 +1055,7 @@ fn parse_headers_iter_uninit<'a, 'b>(
b = next!(bytes);
}
if b != b'\n' {
return Err(Error::HeaderValue);
continue_with_new_header_on_next_line!(bytes, b, HeaderValue);
}

maybe_continue_after_obsolete_line_folding!(bytes, 'whitespace_after_colon);
Expand Down Expand Up @@ -1071,7 +1109,7 @@ fn parse_headers_iter_uninit<'a, 'b>(
} else if b == b'\n' {
1
} else {
return Err(Error::HeaderValue);
continue_with_new_header_on_next_line!(bytes, b, HeaderValue);
};

maybe_continue_after_obsolete_line_folding!(bytes, 'value_lines);
Expand All @@ -1084,6 +1122,11 @@ fn parse_headers_iter_uninit<'a, 'b>(
}
};

let uninit_header = match iter.next() {
Some(header) => header,
None => break 'headers
};

// trim trailing whitespace in the header
let header_value = if let Some(last_visible) = value_slice
.iter()
Expand Down Expand Up @@ -1616,6 +1659,23 @@ mod tests {
assert_eq!(response.headers[1].value, &b"baguette"[..]);
}

#[test]
fn test_ignore_header_line_with_whitespaces_after_header_name() {
let mut headers = [EMPTY_HEADER; 2];
let mut response = Response::new(&mut headers[..]);
let result = ::ParserConfig::default()
.ignore_invalid_headers_in_responses(true)
.parse_response(&mut response, RESPONSE_WITH_WHITESPACE_BETWEEN_HEADER_NAME_AND_COLON);

assert_eq!(result, Ok(Status::Complete(77)));
assert_eq!(response.version.unwrap(), 1);
assert_eq!(response.code.unwrap(), 200);
assert_eq!(response.reason.unwrap(), "OK");
assert_eq!(response.headers.len(), 1);
assert_eq!(response.headers[0].name, "Bread");
assert_eq!(response.headers[0].value, &b"baguette"[..]);
}

static REQUEST_WITH_WHITESPACE_BETWEEN_HEADER_NAME_AND_COLON: &'static [u8] =
b"GET / HTTP/1.1\r\nHost : localhost\r\n\r\n";

Expand Down Expand Up @@ -1888,17 +1948,213 @@ mod tests {
assert_eq!(result, Err(::Error::Status));
}

static RESPONSE_WITH_INVALID_CHAR_BETWEEN_HEADER_NAME_AND_COLON: &'static [u8] =
b"HTTP/1.1 200 OK\r\nAccess-Control-Allow-Credentials\xFF: true\r\nBread: baguette\r\n\r\n";
#[test]
fn test_response_with_empty_header_name() {
const RESPONSE: &[u8] =
b"HTTP/1.1 200 OK\r\n: hello\r\nBread: baguette\r\n\r\n";

let mut headers = [EMPTY_HEADER; 2];
let mut response = Response::new(&mut headers[..]);

let result = ::ParserConfig::default()
.allow_spaces_after_header_name_in_responses(true)
.parse_response(&mut response, RESPONSE);
assert_eq!(result, Err(::Error::HeaderName));

let result = ::ParserConfig::default()
.ignore_invalid_headers_in_responses(true)
.parse_response(&mut response, RESPONSE);
assert_eq!(result, Ok(Status::Complete(45)));

assert_eq!(response.version.unwrap(), 1);
assert_eq!(response.code.unwrap(), 200);
assert_eq!(response.reason.unwrap(), "OK");
assert_eq!(response.headers.len(), 1);
assert_eq!(response.headers[0].name, "Bread");
assert_eq!(response.headers[0].value, &b"baguette"[..]);
}

#[test]
fn test_response_with_invalid_char_between_header_name_and_colon() {
const RESPONSE: &[u8] =
b"HTTP/1.1 200 OK\r\nAccess-Control-Allow-Credentials\xFF : true\r\nBread: baguette\r\n\r\n";

let mut headers = [EMPTY_HEADER; 2];
let mut response = Response::new(&mut headers[..]);

let result = ::ParserConfig::default()
.allow_spaces_after_header_name_in_responses(true)
.parse_response(&mut response, RESPONSE);
assert_eq!(result, Err(::Error::HeaderName));

let result = ::ParserConfig::default()
.ignore_invalid_headers_in_responses(true)
.parse_response(&mut response, RESPONSE);

assert_eq!(result, Ok(Status::Complete(79)));
assert_eq!(response.version.unwrap(), 1);
assert_eq!(response.code.unwrap(), 200);
assert_eq!(response.reason.unwrap(), "OK");
assert_eq!(response.headers.len(), 1);
assert_eq!(response.headers[0].name, "Bread");
assert_eq!(response.headers[0].value, &b"baguette"[..]);
}

#[test]
fn test_ignore_header_line_with_missing_colon() {
const RESPONSE: &[u8] =
b"HTTP/1.1 200 OK\r\nAccess-Control-Allow-Credentials\r\nBread: baguette\r\n\r\n";

let mut headers = [EMPTY_HEADER; 2];
let mut response = Response::new(&mut headers[..]);

let result = ::ParserConfig::default()
.parse_response(&mut response, RESPONSE);
assert_eq!(result, Err(::Error::HeaderName));

let result = ::ParserConfig::default()
.ignore_invalid_headers_in_responses(true)
.parse_response(&mut response, RESPONSE);
assert_eq!(result, Ok(Status::Complete(70)));

assert_eq!(response.version.unwrap(), 1);
assert_eq!(response.code.unwrap(), 200);
assert_eq!(response.reason.unwrap(), "OK");
assert_eq!(response.headers.len(), 1);
assert_eq!(response.headers[0].name, "Bread");
assert_eq!(response.headers[0].value, &b"baguette"[..]);
}

#[test]
fn test_header_with_missing_colon_with_folding() {
const RESPONSE: &[u8] =
b"HTTP/1.1 200 OK\r\nAccess-Control-Allow-Credentials \r\n hello\r\nBread: baguette\r\n\r\n";

let mut headers = [EMPTY_HEADER; 2];
let mut response = Response::new(&mut headers[..]);

let result = ::ParserConfig::default()
.allow_obsolete_multiline_headers_in_responses(true)
.allow_spaces_after_header_name_in_responses(true)
.parse_response(&mut response, RESPONSE);
assert_eq!(result, Err(::Error::HeaderName));

let result = ::ParserConfig::default()
.ignore_invalid_headers_in_responses(true)
.parse_response(&mut response, RESPONSE);
assert_eq!(result, Ok(Status::Complete(81)));

assert_eq!(response.version.unwrap(), 1);
assert_eq!(response.code.unwrap(), 200);
assert_eq!(response.reason.unwrap(), "OK");
assert_eq!(response.headers.len(), 1);
assert_eq!(response.headers[0].name, "Bread");
assert_eq!(response.headers[0].value, &b"baguette"[..]);
}

#[test]
fn test_header_with_nul_in_header_name() {
const RESPONSE: &[u8] =
b"HTTP/1.1 200 OK\r\nAccess-Control-Allow-Cred\0entials: hello\r\nBread: baguette\r\n\r\n";

let mut headers = [EMPTY_HEADER; 2];
let mut response = Response::new(&mut headers[..]);

let result = ::ParserConfig::default()
.parse_response(&mut response, RESPONSE);
assert_eq!(result, Err(::Error::HeaderName));

let result = ::ParserConfig::default()
.ignore_invalid_headers_in_responses(true)
.parse_response(&mut response, RESPONSE);
assert_eq!(result, Err(::Error::HeaderName));
}

#[test]
fn test_forbid_response_with_invalid_char_between_header_name_and_colon() {
fn test_header_with_nul_in_whitespace_before_colon() {
const RESPONSE: &[u8] =
b"HTTP/1.1 200 OK\r\nAccess-Control-Allow-Credentials \0: hello\r\nBread: baguette\r\n\r\n";

let mut headers = [EMPTY_HEADER; 2];
let mut response = Response::new(&mut headers[..]);

let result = ::ParserConfig::default()
.allow_spaces_after_header_name_in_responses(true)
.parse_response(&mut response, RESPONSE_WITH_INVALID_CHAR_BETWEEN_HEADER_NAME_AND_COLON);
.parse_response(&mut response, RESPONSE);
assert_eq!(result, Err(::Error::HeaderName));

let result = ::ParserConfig::default()
.allow_spaces_after_header_name_in_responses(true)
.ignore_invalid_headers_in_responses(true)
.parse_response(&mut response, RESPONSE);
assert_eq!(result, Err(::Error::HeaderName));
}

#[test]
fn test_header_with_nul_in_value() {
const RESPONSE: &[u8] =
b"HTTP/1.1 200 OK\r\nAccess-Control-Allow-Credentials: hell\0o\r\nBread: baguette\r\n\r\n";

let mut headers = [EMPTY_HEADER; 2];
let mut response = Response::new(&mut headers[..]);

let result = ::ParserConfig::default()
.parse_response(&mut response, RESPONSE);
assert_eq!(result, Err(::Error::HeaderValue));

let result = ::ParserConfig::default()
.ignore_invalid_headers_in_responses(true)
.parse_response(&mut response, RESPONSE);
assert_eq!(result, Err(::Error::HeaderValue));
}

#[test]
fn test_header_with_invalid_char_in_value() {
const RESPONSE: &[u8] =
b"HTTP/1.1 200 OK\r\nAccess-Control-Allow-Credentials: hell\x01o\r\nBread: baguette\r\n\r\n";

let mut headers = [EMPTY_HEADER; 2];
let mut response = Response::new(&mut headers[..]);

let result = ::ParserConfig::default()
.parse_response(&mut response, RESPONSE);
assert_eq!(result, Err(::Error::HeaderValue));

let result = ::ParserConfig::default()
.ignore_invalid_headers_in_responses(true)
.parse_response(&mut response, RESPONSE);
assert_eq!(result, Ok(Status::Complete(78)));

assert_eq!(response.version.unwrap(), 1);
assert_eq!(response.code.unwrap(), 200);
assert_eq!(response.reason.unwrap(), "OK");
assert_eq!(response.headers.len(), 1);
assert_eq!(response.headers[0].name, "Bread");
assert_eq!(response.headers[0].value, &b"baguette"[..]);
}

#[test]
fn test_header_with_invalid_char_in_value_with_folding() {
const RESPONSE: &[u8] =
b"HTTP/1.1 200 OK\r\nAccess-Control-Allow-Credentials: hell\x01o \n world!\r\nBread: baguette\r\n\r\n";

let mut headers = [EMPTY_HEADER; 2];
let mut response = Response::new(&mut headers[..]);

let result = ::ParserConfig::default()
.parse_response(&mut response, RESPONSE);
assert_eq!(result, Err(::Error::HeaderValue));

let result = ::ParserConfig::default()
.ignore_invalid_headers_in_responses(true)
.parse_response(&mut response, RESPONSE);
assert_eq!(result, Ok(Status::Complete(88)));

assert_eq!(response.version.unwrap(), 1);
assert_eq!(response.code.unwrap(), 200);
assert_eq!(response.reason.unwrap(), "OK");
assert_eq!(response.headers.len(), 1);
assert_eq!(response.headers[0].name, "Bread");
assert_eq!(response.headers[0].value, &b"baguette"[..]);
}
}

0 comments on commit 67a2898

Please sign in to comment.