Skip to content

Commit

Permalink
Implement header line folding (fixes seanmonstar#37, seanmonstar#68)
Browse files Browse the repository at this point in the history
  • Loading branch information
nox committed Jan 13, 2022
1 parent 333d555 commit 6704249
Showing 1 changed file with 236 additions and 51 deletions.
287 changes: 236 additions & 51 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@ impl<T> Status<T> {
#[derive(Clone, Debug, Default)]
pub struct ParserConfig {
allow_spaces_after_header_name_in_responses: bool,
allow_obsolete_multiline_headers_in_responses: bool,
}

impl ParserConfig {
Expand All @@ -256,6 +257,19 @@ impl ParserConfig {
self
}

/// Sets whether obsolete multiline headers should be allowed.
///
/// This is an obsolete part of HTTP/1. Use at your own risk. If you are
/// building an HTTP library, the newlines (`\r` and `\n`) should be
/// replaced by spaces before handing the header value to the user.
pub fn allow_obsolete_multiline_headers_in_responses(
&mut self,
value: bool,
) -> &mut Self {
self.allow_obsolete_multiline_headers_in_responses = value;
self
}

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

let mut b;

'value: loop {

let value_slice = 'value: loop {
// eat white space between colon and value
'whitespace_after_colon: loop {
b = next!(bytes);
Expand All @@ -813,73 +826,129 @@ fn parse_headers_iter_uninit<'a, 'b>(
continue 'whitespace_after_colon;
} else {
if !is_header_value_token(b) {
break 'value;
if b == b'\r' {
expect!(bytes.next() == b'\n' => Err(Error::HeaderValue));
} else if b != b'\n' {
return Err(Error::HeaderValue);
}

if config.allow_obsolete_multiline_headers_in_responses {
match bytes.peek() {
None => {
// Next byte may be a space, in which case that header
// is using obsolete line folding, so we may have more
// whitespace to skip after colon.
return Ok(Status::Partial);
}
Some(b' ' | b'\t') => {
// The space will be consumed next iteration.
continue 'whitespace_after_colon;
}
_ => {
// There is another byte after the end of the line,
// but it's not whitespace, so it's probably another
// header or the final line return. This header is thus
// empty.
},
}
}

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

break 'value &[][..];
}
break 'whitespace_after_colon;
}
}

// parse value till EOL

simd::match_header_value_vectored(bytes);
'value_lines: loop {
// parse value till EOL

simd::match_header_value_vectored(bytes);

'value_line: loop {
if let Some(mut bytes8) = bytes.next_8() {
macro_rules! check {
($bytes:ident, $i:ident) => ({
b = $bytes.$i();
if !is_header_value_token(b) {
break 'value_line;
}
});
($bytes:ident) => ({
check!($bytes, _0);
check!($bytes, _1);
check!($bytes, _2);
check!($bytes, _3);
check!($bytes, _4);
check!($bytes, _5);
check!($bytes, _6);
check!($bytes, _7);
})
}

check!(bytes8);

continue 'value_line;
}

macro_rules! check {
($bytes:ident, $i:ident) => ({
b = $bytes.$i();
b = next!(bytes);
if !is_header_value_token(b) {
break 'value;
break 'value_line;
}
});
($bytes:ident) => ({
check!($bytes, _0);
check!($bytes, _1);
check!($bytes, _2);
check!($bytes, _3);
check!($bytes, _4);
check!($bytes, _5);
check!($bytes, _6);
check!($bytes, _7);
})
}
while let Some(mut bytes8) = bytes.next_8() {
check!(bytes8);
}
loop {
b = next!(bytes);
if !is_header_value_token(b) {
break 'value;
}
}
}

//found_ctl
let value_slice : &[u8] = if b == b'\r' {
expect!(bytes.next() == b'\n' => Err(Error::HeaderValue));
count += bytes.pos();
// having just check that `\r\n` exists, it's safe to skip those 2 bytes
unsafe {
bytes.slice_skip(2)
}
} else if b == b'\n' {
count += bytes.pos();
// having just check that `\r\n` exists, it's safe to skip 1 byte
unsafe {
bytes.slice_skip(1)
//found_ctl
let skip = if b == b'\r' {
expect!(bytes.next() == b'\n' => Err(Error::HeaderValue));
2
} else if b == b'\n' {
1
} else {
return Err(Error::HeaderValue);
};

if config.allow_obsolete_multiline_headers_in_responses {
match bytes.peek() {
None => {
// Next byte may be a space, in which case that header
// may be using line folding, so we need more data.
return Ok(Status::Partial);
}
Some(b' ' | b'\t') => {
// The space will be consumed next iteration.
continue 'value_lines;
}
_ => {
// There is another byte after the end of the line,
// but it's not a space, so it's probably another
// header or the final line return. We are thus done
// with this current header.
},
}
}

count += bytes.pos();
// having just checked that a newline exists, it's safe to skip it.
unsafe {
break 'value bytes.slice_skip(skip);
}
}
} else {
return Err(Error::HeaderValue);
};

let header_value: &[u8];
// trim trailing whitespace in the header
if let Some(last_visible) = value_slice.iter().rposition(|b| *b != b' ' && *b != b'\t' ) {
let header_value = if let Some(last_visible) = value_slice
.iter()
.rposition(|b| *b != b' ' && *b != b'\t' && *b != b'\r' && *b != b'\n')
{
// There is at least one non-whitespace character.
header_value = &value_slice[0..last_visible+1];
&value_slice[0..last_visible+1]
} else {
// There is no non-whitespace character. This can only happen when value_slice is
// empty.
header_value = value_slice;
}
value_slice
};

*uninit_header = MaybeUninit::new(Header {
name: header_name,
Expand Down Expand Up @@ -1389,6 +1458,122 @@ mod tests {
assert_eq!(result, Err(::Error::HeaderName));
}

static RESPONSE_WITH_OBSOLETE_LINE_FOLDING_AT_START: &'static [u8] =
b"HTTP/1.1 200 OK\r\nLine-Folded-Header: \r\n \r\n hello there\r\n\r\n";

#[test]
fn test_forbid_response_with_obsolete_line_folding_at_start() {
let mut headers = [EMPTY_HEADER; 1];
let mut response = Response::new(&mut headers[..]);
let result = response.parse(RESPONSE_WITH_OBSOLETE_LINE_FOLDING_AT_START);

assert_eq!(result, Err(::Error::HeaderName));
}

#[test]
fn test_allow_response_with_obsolete_line_folding_at_start() {
let mut headers = [EMPTY_HEADER; 1];
let mut response = Response::new(&mut headers[..]);
let result = ::ParserConfig::default()
.allow_obsolete_multiline_headers_in_responses(true)
.parse_response(&mut response, RESPONSE_WITH_OBSOLETE_LINE_FOLDING_AT_START);

assert_eq!(result, Ok(Status::Complete(RESPONSE_WITH_OBSOLETE_LINE_FOLDING_AT_START.len())));
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, "Line-Folded-Header");
assert_eq!(response.headers[0].value, &b"hello there"[..]);
}

static RESPONSE_WITH_OBSOLETE_LINE_FOLDING_AT_END: &'static [u8] =
b"HTTP/1.1 200 OK\r\nLine-Folded-Header: hello there\r\n \r\n \r\n\r\n";

#[test]
fn test_forbid_response_with_obsolete_line_folding_at_end() {
let mut headers = [EMPTY_HEADER; 1];
let mut response = Response::new(&mut headers[..]);
let result = response.parse(RESPONSE_WITH_OBSOLETE_LINE_FOLDING_AT_END);

assert_eq!(result, Err(::Error::HeaderName));
}

#[test]
fn test_allow_response_with_obsolete_line_folding_at_end() {
let mut headers = [EMPTY_HEADER; 1];
let mut response = Response::new(&mut headers[..]);
let result = ::ParserConfig::default()
.allow_obsolete_multiline_headers_in_responses(true)
.parse_response(&mut response, RESPONSE_WITH_OBSOLETE_LINE_FOLDING_AT_END);

assert_eq!(result, Ok(Status::Complete(RESPONSE_WITH_OBSOLETE_LINE_FOLDING_AT_END.len())));
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, "Line-Folded-Header");
assert_eq!(response.headers[0].value, &b"hello there"[..]);
}

static RESPONSE_WITH_OBSOLETE_LINE_FOLDING_IN_MIDDLE: &'static [u8] =
b"HTTP/1.1 200 OK\r\nLine-Folded-Header: hello \r\n \r\n there\r\n\r\n";

#[test]
fn test_forbid_response_with_obsolete_line_folding_in_middle() {
let mut headers = [EMPTY_HEADER; 1];
let mut response = Response::new(&mut headers[..]);
let result = response.parse(RESPONSE_WITH_OBSOLETE_LINE_FOLDING_IN_MIDDLE);

assert_eq!(result, Err(::Error::HeaderName));
}

#[test]
fn test_allow_response_with_obsolete_line_folding_in_middle() {
let mut headers = [EMPTY_HEADER; 1];
let mut response = Response::new(&mut headers[..]);
let result = ::ParserConfig::default()
.allow_obsolete_multiline_headers_in_responses(true)
.parse_response(&mut response, RESPONSE_WITH_OBSOLETE_LINE_FOLDING_IN_MIDDLE);

assert_eq!(result, Ok(Status::Complete(RESPONSE_WITH_OBSOLETE_LINE_FOLDING_IN_MIDDLE.len())));
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, "Line-Folded-Header");
assert_eq!(response.headers[0].value, &b"hello \r\n \r\n there"[..]);
}

static RESPONSE_WITH_OBSOLETE_LINE_FOLDING_IN_EMPTY_HEADER: &'static [u8] =
b"HTTP/1.1 200 OK\r\nLine-Folded-Header: \r\n \r\n \r\n\r\n";

#[test]
fn test_forbid_response_with_obsolete_line_folding_in_empty_header() {
let mut headers = [EMPTY_HEADER; 1];
let mut response = Response::new(&mut headers[..]);
let result = response.parse(RESPONSE_WITH_OBSOLETE_LINE_FOLDING_IN_EMPTY_HEADER);

assert_eq!(result, Err(::Error::HeaderName));
}

#[test]
fn test_allow_response_with_obsolete_line_folding_in_empty_header() {
let mut headers = [EMPTY_HEADER; 1];
let mut response = Response::new(&mut headers[..]);
let result = ::ParserConfig::default()
.allow_obsolete_multiline_headers_in_responses(true)
.parse_response(&mut response, RESPONSE_WITH_OBSOLETE_LINE_FOLDING_IN_EMPTY_HEADER);

assert_eq!(result, Ok(Status::Complete(RESPONSE_WITH_OBSOLETE_LINE_FOLDING_IN_EMPTY_HEADER.len())));
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, "Line-Folded-Header");
assert_eq!(response.headers[0].value, &b""[..]);
}

#[test]
fn test_chunk_size() {
assert_eq!(parse_chunk_size(b"0\r\n"), Ok(Status::Complete((3, 0))));
Expand Down

0 comments on commit 6704249

Please sign in to comment.