Merge pull request #228 from mlalic/chunksize-fix
Fix chunk size parsing: handle invalid chunk sizes
This commit is contained in:
@@ -100,6 +100,8 @@ mod tests {
|
||||
use std::io::BufferedReader;
|
||||
|
||||
use header::Headers;
|
||||
use header::common::TransferEncoding;
|
||||
use header::common::transfer_encoding::Encoding;
|
||||
use http::HttpReader::EofReader;
|
||||
use http::RawStatus;
|
||||
use mock::MockStream;
|
||||
@@ -124,4 +126,95 @@ mod tests {
|
||||
assert_eq!(b, box MockStream::new());
|
||||
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_chunked_response() {
|
||||
let stream = MockStream::with_input(b"\
|
||||
HTTP/1.1 200 OK\r\n\
|
||||
Transfer-Encoding: chunked\r\n\
|
||||
\r\n\
|
||||
1\r\n\
|
||||
q\r\n\
|
||||
2\r\n\
|
||||
we\r\n\
|
||||
2\r\n\
|
||||
rt\r\n\
|
||||
0\r\n\
|
||||
\r\n"
|
||||
);
|
||||
|
||||
let mut res = Response::new(box stream).unwrap();
|
||||
|
||||
// The status line is correct?
|
||||
assert_eq!(res.status, status::StatusCode::Ok);
|
||||
assert_eq!(res.version, version::HttpVersion::Http11);
|
||||
// The header is correct?
|
||||
match res.headers.get::<TransferEncoding>() {
|
||||
Some(encodings) => {
|
||||
assert_eq!(1, encodings.len());
|
||||
assert_eq!(Encoding::Chunked, encodings[0]);
|
||||
},
|
||||
None => panic!("Transfer-Encoding: chunked expected!"),
|
||||
};
|
||||
// The body is correct?
|
||||
let body = res.read_to_string().unwrap();
|
||||
assert_eq!("qwert", body);
|
||||
}
|
||||
|
||||
/// Tests that when a chunk size is not a valid radix-16 number, an error
|
||||
/// is returned.
|
||||
#[test]
|
||||
fn test_invalid_chunk_size_not_hex_digit() {
|
||||
let stream = MockStream::with_input(b"\
|
||||
HTTP/1.1 200 OK\r\n\
|
||||
Transfer-Encoding: chunked\r\n\
|
||||
\r\n\
|
||||
X\r\n\
|
||||
1\r\n\
|
||||
0\r\n\
|
||||
\r\n"
|
||||
);
|
||||
|
||||
let mut res = Response::new(box stream).unwrap();
|
||||
|
||||
assert!(res.read_to_string().is_err());
|
||||
}
|
||||
|
||||
/// Tests that when a chunk size contains an invalid extension, an error is
|
||||
/// returned.
|
||||
#[test]
|
||||
fn test_invalid_chunk_size_extension() {
|
||||
let stream = MockStream::with_input(b"\
|
||||
HTTP/1.1 200 OK\r\n\
|
||||
Transfer-Encoding: chunked\r\n\
|
||||
\r\n\
|
||||
1 this is an invalid extension\r\n\
|
||||
1\r\n\
|
||||
0\r\n\
|
||||
\r\n"
|
||||
);
|
||||
|
||||
let mut res = Response::new(box stream).unwrap();
|
||||
|
||||
assert!(res.read_to_string().is_err());
|
||||
}
|
||||
|
||||
/// Tests that when a valid extension that contains a digit is appended to
|
||||
/// the chunk size, the chunk is correctly read.
|
||||
#[test]
|
||||
fn test_chunk_size_with_extension() {
|
||||
let stream = MockStream::with_input(b"\
|
||||
HTTP/1.1 200 OK\r\n\
|
||||
Transfer-Encoding: chunked\r\n\
|
||||
\r\n\
|
||||
1;this is an extension with a digit 1\r\n\
|
||||
1\r\n\
|
||||
0\r\n\
|
||||
\r\n"
|
||||
);
|
||||
|
||||
let mut res = Response::new(box stream).unwrap();
|
||||
|
||||
assert_eq!("1", res.read_to_string().unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
69
src/http.rs
69
src/http.rs
@@ -137,17 +137,18 @@ fn read_chunk_size<R: Reader>(rdr: &mut R) -> IoResult<uint> {
|
||||
let mut size = 0u;
|
||||
let radix = 16;
|
||||
let mut in_ext = false;
|
||||
let mut in_chunk_size = true;
|
||||
loop {
|
||||
match try!(rdr.read_byte()) {
|
||||
b@b'0'...b'9' if !in_ext => {
|
||||
b@b'0'...b'9' if in_chunk_size => {
|
||||
size *= radix;
|
||||
size += (b - b'0') as uint;
|
||||
},
|
||||
b@b'a'...b'f' if !in_ext => {
|
||||
b@b'a'...b'f' if in_chunk_size => {
|
||||
size *= radix;
|
||||
size += (b + 10 - b'a') as uint;
|
||||
},
|
||||
b@b'A'...b'F' if !in_ext => {
|
||||
b@b'A'...b'F' if in_chunk_size => {
|
||||
size *= radix;
|
||||
size += (b + 10 - b'A') as uint;
|
||||
},
|
||||
@@ -157,9 +158,28 @@ fn read_chunk_size<R: Reader>(rdr: &mut R) -> IoResult<uint> {
|
||||
_ => return Err(io::standard_error(io::InvalidInput))
|
||||
}
|
||||
},
|
||||
ext => {
|
||||
// If we weren't in the extension yet, the ";" signals its start
|
||||
b';' if !in_ext => {
|
||||
in_ext = true;
|
||||
in_chunk_size = false;
|
||||
},
|
||||
// "Linear white space" is ignored between the chunk size and the
|
||||
// extension separator token (";") due to the "implied *LWS rule".
|
||||
b'\t' | b' ' if !in_ext & !in_chunk_size => {},
|
||||
// LWS can follow the chunk size, but no more digits can come
|
||||
b'\t' | b' ' if in_chunk_size => in_chunk_size = false,
|
||||
// We allow any arbitrary octet once we are in the extension, since
|
||||
// they all get ignored anyway. According to the HTTP spec, valid
|
||||
// extensions would have a more strict syntax:
|
||||
// (token ["=" (token | quoted-string)])
|
||||
// but we gain nothing by rejecting an otherwise valid chunk size.
|
||||
ext if in_ext => {
|
||||
todo!("chunk extension byte={}", ext);
|
||||
},
|
||||
// Finally, if we aren't in the extension and we're reading any
|
||||
// other octet, the chunk size line is invalid!
|
||||
_ => {
|
||||
return Err(io::standard_error(io::InvalidInput));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -689,7 +709,7 @@ fn expect(r: IoResult<u8>, expected: u8) -> HttpResult<()> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::io::{self, MemReader, MemWriter};
|
||||
use std::io::{self, MemReader, MemWriter, IoResult};
|
||||
use std::borrow::Cow::{Borrowed, Owned};
|
||||
use test::Bencher;
|
||||
use uri::RequestUri;
|
||||
@@ -702,7 +722,7 @@ mod tests {
|
||||
use url::Url;
|
||||
|
||||
use super::{read_method, read_uri, read_http_version, read_header,
|
||||
RawHeaderLine, read_status, RawStatus};
|
||||
RawHeaderLine, read_status, RawStatus, read_chunk_size};
|
||||
|
||||
fn mem(s: &str) -> MemReader {
|
||||
MemReader::new(s.as_bytes().to_vec())
|
||||
@@ -811,6 +831,43 @@ mod tests {
|
||||
assert_eq!(s, "foo barb");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_read_chunk_size() {
|
||||
fn read(s: &str, result: IoResult<uint>) {
|
||||
assert_eq!(read_chunk_size(&mut mem(s)), result);
|
||||
}
|
||||
|
||||
read("1\r\n", Ok(1));
|
||||
read("01\r\n", Ok(1));
|
||||
read("0\r\n", Ok(0));
|
||||
read("00\r\n", Ok(0));
|
||||
read("A\r\n", Ok(10));
|
||||
read("a\r\n", Ok(10));
|
||||
read("Ff\r\n", Ok(255));
|
||||
read("Ff \r\n", Ok(255));
|
||||
// Missing LF or CRLF
|
||||
read("F\rF", Err(io::standard_error(io::InvalidInput)));
|
||||
read("F", Err(io::standard_error(io::EndOfFile)));
|
||||
// Invalid hex digit
|
||||
read("X\r\n", Err(io::standard_error(io::InvalidInput)));
|
||||
read("1X\r\n", Err(io::standard_error(io::InvalidInput)));
|
||||
read("-\r\n", Err(io::standard_error(io::InvalidInput)));
|
||||
read("-1\r\n", Err(io::standard_error(io::InvalidInput)));
|
||||
// Acceptable (if not fully valid) extensions do not influence the size
|
||||
read("1;extension\r\n", Ok(1));
|
||||
read("a;ext name=value\r\n", Ok(10));
|
||||
read("1;extension;extension2\r\n", Ok(1));
|
||||
read("1;;; ;\r\n", Ok(1));
|
||||
read("2; extension...\r\n", Ok(2));
|
||||
read("3 ; extension=123\r\n", Ok(3));
|
||||
read("3 ;\r\n", Ok(3));
|
||||
read("3 ; \r\n", Ok(3));
|
||||
// Invalid extensions cause an error
|
||||
read("1 invalid extension\r\n", Err(io::standard_error(io::InvalidInput)));
|
||||
read("1 A\r\n", Err(io::standard_error(io::InvalidInput)));
|
||||
read("1;no CRLF", Err(io::standard_error(io::EndOfFile)));
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn bench_read_method(b: &mut Bencher) {
|
||||
b.bytes = b"CONNECT ".len() as u64;
|
||||
|
||||
@@ -75,6 +75,8 @@ impl<'a> Reader for Request<'a> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use header::common::{Host, TransferEncoding};
|
||||
use header::common::transfer_encoding::Encoding;
|
||||
use mock::MockStream;
|
||||
use super::Request;
|
||||
|
||||
@@ -122,4 +124,103 @@ mod tests {
|
||||
let mut req = Request::new(&mut stream, sock("127.0.0.1:80")).unwrap();
|
||||
assert_eq!(req.read_to_string(), Ok("".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_chunked_request() {
|
||||
let mut stream = MockStream::with_input(b"\
|
||||
POST / HTTP/1.1\r\n\
|
||||
Host: example.domain\r\n\
|
||||
Transfer-Encoding: chunked\r\n\
|
||||
\r\n\
|
||||
1\r\n\
|
||||
q\r\n\
|
||||
2\r\n\
|
||||
we\r\n\
|
||||
2\r\n\
|
||||
rt\r\n\
|
||||
0\r\n\
|
||||
\r\n"
|
||||
);
|
||||
|
||||
let mut req = Request::new(&mut stream, sock("127.0.0.1:80")).unwrap();
|
||||
|
||||
// The headers are correct?
|
||||
match req.headers.get::<Host>() {
|
||||
Some(host) => {
|
||||
assert_eq!("example.domain", host.hostname);
|
||||
},
|
||||
None => panic!("Host header expected!"),
|
||||
};
|
||||
match req.headers.get::<TransferEncoding>() {
|
||||
Some(encodings) => {
|
||||
assert_eq!(1, encodings.len());
|
||||
assert_eq!(Encoding::Chunked, encodings[0]);
|
||||
}
|
||||
None => panic!("Transfer-Encoding: chunked expected!"),
|
||||
};
|
||||
// The content is correctly read?
|
||||
let body = req.read_to_string().unwrap();
|
||||
assert_eq!("qwert", body);
|
||||
}
|
||||
|
||||
/// Tests that when a chunk size is not a valid radix-16 number, an error
|
||||
/// is returned.
|
||||
#[test]
|
||||
fn test_invalid_chunk_size_not_hex_digit() {
|
||||
let mut stream = MockStream::with_input(b"\
|
||||
POST / HTTP/1.1\r\n\
|
||||
Host: example.domain\r\n\
|
||||
Transfer-Encoding: chunked\r\n\
|
||||
\r\n\
|
||||
X\r\n\
|
||||
1\r\n\
|
||||
0\r\n\
|
||||
\r\n"
|
||||
);
|
||||
|
||||
let mut req = Request::new(&mut stream, sock("127.0.0.1:80")).unwrap();
|
||||
|
||||
assert!(req.read_to_string().is_err());
|
||||
}
|
||||
|
||||
/// Tests that when a chunk size contains an invalid extension, an error is
|
||||
/// returned.
|
||||
#[test]
|
||||
fn test_invalid_chunk_size_extension() {
|
||||
let mut stream = MockStream::with_input(b"\
|
||||
POST / HTTP/1.1\r\n\
|
||||
Host: example.domain\r\n\
|
||||
Transfer-Encoding: chunked\r\n\
|
||||
\r\n\
|
||||
1 this is an invalid extension\r\n\
|
||||
1\r\n\
|
||||
0\r\n\
|
||||
\r\n"
|
||||
);
|
||||
|
||||
let mut req = Request::new(&mut stream, sock("127.0.0.1:80")).unwrap();
|
||||
|
||||
assert!(req.read_to_string().is_err());
|
||||
}
|
||||
|
||||
/// Tests that when a valid extension that contains a digit is appended to
|
||||
/// the chunk size, the chunk is correctly read.
|
||||
#[test]
|
||||
fn test_chunk_size_with_extension() {
|
||||
let mut stream = MockStream::with_input(b"\
|
||||
POST / HTTP/1.1\r\n\
|
||||
Host: example.domain\r\n\
|
||||
Transfer-Encoding: chunked\r\n\
|
||||
\r\n\
|
||||
1;this is an extension with a digit 1\r\n\
|
||||
1\r\n\
|
||||
0\r\n\
|
||||
\r\n"
|
||||
);
|
||||
|
||||
let mut req = Request::new(&mut stream, sock("127.0.0.1:80")).unwrap();
|
||||
|
||||
assert_eq!("1", req.read_to_string().unwrap())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user