gatewayd: strict status lines and chunk lines; no as casts (M4a review, findings 1 and 3)

A status line splits on single spaces only, and every chunk line must end in CRLF. The bounded
`as` casts in http.rs, handshake.rs and proto's sha1.rs become try_from and from.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-24 01:23:51 -07:00
co-authored by Claude Opus 5.5
parent 131b3677fd
commit 0081f24b70
5 changed files with 87 additions and 28 deletions
+57
View File
@@ -0,0 +1,57 @@
//! The HTTP reader is strict where it can be: a status line with anything but single spaces, and a
//! chunk line not ended by CRLF, are protocol errors (M4a review, finding 3).
use std::io::Cursor;
use gatewayd::http::{HttpError, read_body, read_head};
fn read(response: &[u8]) -> Result<Vec<u8>, HttpError> {
let mut c = Cursor::new(response.to_vec());
let head = read_head(&mut c)?;
read_body(&mut c, &head)
}
#[test]
fn a_status_line_has_single_spaces() {
for line in [
"HTTP/1.1 200 OK",
"HTTP/1.1 200 OK",
" HTTP/1.1 200 OK",
"HTTP/1.1\t200 OK",
] {
let response = format!("{line}\r\nContent-Length: 0\r\n\r\n");
let got = read(response.as_bytes());
if line == "HTTP/1.1 200 OK" {
// The reason phrase is free text: two spaces inside it are allowed.
assert!(got.is_ok(), "{line:?}: {got:?}");
} else {
assert!(
matches!(got, Err(HttpError::Protocol(_))),
"{line:?}: {got:?}"
);
}
}
assert!(
read(b"HTTP/1.1 204\r\n\r\n").is_ok(),
"a status line without a reason phrase"
);
}
#[test]
fn chunk_lines_end_in_crlf() {
let head = "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n";
let good = format!("{head}2\r\nab\r\n0\r\n\r\n");
assert_eq!(read(good.as_bytes()).unwrap(), b"ab");
for body in [
"2\nab\r\n0\r\n\r\n",
"2\r\nab\n0\r\n\r\n",
"2\r\nab\r\n0\n\r\n",
"2\r\nab\r\n0\r\n\n",
] {
let got = read(format!("{head}{body}").as_bytes());
assert!(
matches!(got, Err(HttpError::Protocol(_))),
"{body:?}: {got:?}"
);
}
}