diff --git a/src/parser.rs b/src/parser.rs index 65ab7a4..0c135e8 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -100,6 +100,8 @@ pub fn parse_head(buf: &[u8], max_headers: usize) -> Result Result { expect_100 = true; } + "host" => { + // Presence/uniqueness enforced post-loop; validity here. + host_count += 1; + if !valid_host(value) { + host_ok = false; + } + } _ => {} } headers.append(&name_lower, value.to_string()); } + // Host (RFC 9112 §3.2): an HTTP/1.1 request MUST carry exactly one valid + // Host; a missing, duplicate, or malformed Host is a 400. HTTP/1.0 may + // omit Host, but a duplicate or invalid one is still rejected on any + // version (ambiguous / malformed authority). + if host_count > 1 || !host_ok { + return Err(ParseError::Malformed); + } + if version == HttpVersion::Http11 && host_count == 0 { + return Err(ParseError::Malformed); + } + if chunked { // Transfer-Encoding is an HTTP/1.1 mechanism; a 1.0 request // carrying it is malformed. And a request carrying BOTH a @@ -163,6 +183,32 @@ pub fn parse_head(buf: &[u8], max_headers: usize) -> Result bool { + !value.is_empty() + && value.bytes().all(|b| { + b.is_ascii_alphanumeric() + || matches!( + b, + // unreserved punctuation + b'-' | b'.' | b'_' | b'~' + // sub-delims + | b'!' | b'$' | b'&' | b'\'' | b'(' | b')' + | b'*' | b'+' | b',' | b';' | b'=' + // pct-encoded lead + | b'%' + // IP-literal brackets + port separator + | b'[' | b']' | b':' + ) + }) +} + // --------------------------------------------------------------------------- // Conn assembly // --------------------------------------------------------------------------- @@ -403,6 +449,55 @@ mod tests { } } + // --- Host (RFC 9112 §3.2) ------------------------------------------- + + #[test] + fn parse_missing_host_http11_is_malformed() { + let req = b"GET / HTTP/1.1\r\n\r\n"; + match parse_head(req, 64) { + Err(ParseError::Malformed) => {} + _ => panic!("expected Malformed for missing Host on 1.1"), + } + } + + #[test] + fn parse_missing_host_http10_is_allowed() { + // Host is optional in HTTP/1.0. + let req = b"GET / HTTP/1.0\r\n\r\n"; + assert!(parse_head(req, 64).is_ok(), "1.0 may omit Host"); + } + + #[test] + fn parse_duplicate_host_is_malformed() { + let req = b"GET / HTTP/1.1\r\nHost: a\r\nHost: b\r\n\r\n"; + match parse_head(req, 64) { + Err(ParseError::Malformed) => {} + _ => panic!("expected Malformed for duplicate Host"), + } + } + + #[test] + fn parse_invalid_host_value_is_malformed() { + // Embedded whitespace — invalid in an RFC 3986 authority. + let req = b"GET / HTTP/1.1\r\nHost: bad host\r\n\r\n"; + match parse_head(req, 64) { + Err(ParseError::Malformed) => {} + _ => panic!("expected Malformed for invalid Host"), + } + } + + #[test] + fn parse_valid_hosts_accepted() { + // Positive controls: reg-name, reg-name:port, and IPv6-literal:port. + for req in [ + b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n".as_slice(), + b"GET / HTTP/1.1\r\nHost: example.com:8080\r\n\r\n".as_slice(), + b"GET / HTTP/1.1\r\nHost: [::1]:443\r\n\r\n".as_slice(), + ] { + assert!(parse_head(req, 64).is_ok(), "should accept a valid Host"); + } + } + #[test] fn serialise_basic_200() { let conn = Conn::new().put_status(200).put_body("hi");