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 std::io::BufferedReader;
|
||||||
|
|
||||||
use header::Headers;
|
use header::Headers;
|
||||||
|
use header::common::TransferEncoding;
|
||||||
|
use header::common::transfer_encoding::Encoding;
|
||||||
use http::HttpReader::EofReader;
|
use http::HttpReader::EofReader;
|
||||||
use http::RawStatus;
|
use http::RawStatus;
|
||||||
use mock::MockStream;
|
use mock::MockStream;
|
||||||
@@ -124,4 +126,95 @@ mod tests {
|
|||||||
assert_eq!(b, box MockStream::new());
|
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 mut size = 0u;
|
||||||
let radix = 16;
|
let radix = 16;
|
||||||
let mut in_ext = false;
|
let mut in_ext = false;
|
||||||
|
let mut in_chunk_size = true;
|
||||||
loop {
|
loop {
|
||||||
match try!(rdr.read_byte()) {
|
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 *= radix;
|
||||||
size += (b - b'0') as uint;
|
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 *= radix;
|
||||||
size += (b + 10 - b'a') as uint;
|
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 *= radix;
|
||||||
size += (b + 10 - b'A') as uint;
|
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))
|
_ => 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_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);
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::io::{self, MemReader, MemWriter};
|
use std::io::{self, MemReader, MemWriter, IoResult};
|
||||||
use std::borrow::Cow::{Borrowed, Owned};
|
use std::borrow::Cow::{Borrowed, Owned};
|
||||||
use test::Bencher;
|
use test::Bencher;
|
||||||
use uri::RequestUri;
|
use uri::RequestUri;
|
||||||
@@ -702,7 +722,7 @@ mod tests {
|
|||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use super::{read_method, read_uri, read_http_version, read_header,
|
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 {
|
fn mem(s: &str) -> MemReader {
|
||||||
MemReader::new(s.as_bytes().to_vec())
|
MemReader::new(s.as_bytes().to_vec())
|
||||||
@@ -811,6 +831,43 @@ mod tests {
|
|||||||
assert_eq!(s, "foo barb");
|
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]
|
#[bench]
|
||||||
fn bench_read_method(b: &mut Bencher) {
|
fn bench_read_method(b: &mut Bencher) {
|
||||||
b.bytes = b"CONNECT ".len() as u64;
|
b.bytes = b"CONNECT ".len() as u64;
|
||||||
|
|||||||
@@ -75,6 +75,8 @@ impl<'a> Reader for Request<'a> {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use header::common::{Host, TransferEncoding};
|
||||||
|
use header::common::transfer_encoding::Encoding;
|
||||||
use mock::MockStream;
|
use mock::MockStream;
|
||||||
use super::Request;
|
use super::Request;
|
||||||
|
|
||||||
@@ -122,4 +124,103 @@ mod tests {
|
|||||||
let mut req = Request::new(&mut stream, sock("127.0.0.1:80")).unwrap();
|
let mut req = Request::new(&mut stream, sock("127.0.0.1:80")).unwrap();
|
||||||
assert_eq!(req.read_to_string(), Ok("".to_string()));
|
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