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