Skip to content

Commit d4b5bd4

Browse files
authoredJul 27, 2022
fix(http1): trim obs-folded headers when unfolding (#2926)
1 parent 509672a commit d4b5bd4

File tree

2 files changed

+119
-64
lines changed

2 files changed

+119
-64
lines changed
 

‎src/proto/h1/role.rs

+87-7
Original file line numberDiff line numberDiff line change
@@ -990,14 +990,11 @@ impl Http1Transaction for Client {
990990
.h1_parser_config
991991
.obsolete_multiline_headers_in_responses_are_allowed()
992992
{
993-
for header in &headers_indices[..headers_len] {
993+
for header in &mut headers_indices[..headers_len] {
994994
// SAFETY: array is valid up to `headers_len`
995-
let header = unsafe { &*header.as_ptr() };
996-
for b in &mut slice[header.value.0..header.value.1] {
997-
if *b == b'\r' || *b == b'\n' {
998-
*b = b' ';
999-
}
1000-
}
995+
let header = unsafe { &mut *header.as_mut_ptr() };
996+
Client::obs_fold_line(&mut slice, header);
997+
1001998
}
1002999
}
10031000

@@ -1344,6 +1341,65 @@ impl Client {
13441341

13451342
set_content_length(headers, len)
13461343
}
1344+
1345+
fn obs_fold_line(all: &mut [u8], idx: &mut HeaderIndices) {
1346+
// If the value has obs-folded text, then in-place shift the bytes out
1347+
// of here.
1348+
//
1349+
// https://httpwg.org/specs/rfc9112.html#line.folding
1350+
//
1351+
// > A user agent that receives an obs-fold MUST replace each received
1352+
// > obs-fold with one or more SP octets prior to interpreting the
1353+
// > field value.
1354+
//
1355+
// This means strings like "\r\n\t foo" must replace the "\r\n\t " with
1356+
// a single space.
1357+
1358+
let buf = &mut all[idx.value.0..idx.value.1];
1359+
1360+
// look for a newline, otherwise bail out
1361+
let first_nl = match buf.iter().position(|b| *b == b'\n') {
1362+
Some(i) => i,
1363+
None => return,
1364+
};
1365+
1366+
// not on standard slices because whatever, sigh
1367+
fn trim_start(mut s: &[u8]) -> &[u8] {
1368+
while let [first, rest @ ..] = s {
1369+
if first.is_ascii_whitespace() {
1370+
s = rest;
1371+
} else {
1372+
break;
1373+
}
1374+
}
1375+
s
1376+
}
1377+
1378+
fn trim_end(mut s: &[u8]) -> &[u8] {
1379+
while let [rest @ .., last] = s {
1380+
if last.is_ascii_whitespace() {
1381+
s = rest;
1382+
} else {
1383+
break;
1384+
}
1385+
}
1386+
s
1387+
}
1388+
1389+
fn trim(s: &[u8]) -> &[u8] {
1390+
trim_start(trim_end(s))
1391+
}
1392+
1393+
// TODO(perf): we could do the moves in-place, but this is so uncommon
1394+
// that it shouldn't matter.
1395+
let mut unfolded = trim_end(&buf[..first_nl]).to_vec();
1396+
for line in buf[first_nl + 1..].split(|b| *b == b'\n') {
1397+
unfolded.push(b' ');
1398+
unfolded.extend_from_slice(trim(line));
1399+
}
1400+
buf[..unfolded.len()].copy_from_slice(&unfolded);
1401+
idx.value.1 = idx.value.0 + unfolded.len();
1402+
}
13471403
}
13481404

13491405
fn set_content_length(headers: &mut HeaderMap, len: u64) -> Encoder {
@@ -2384,6 +2440,30 @@ mod tests {
23842440
);
23852441
}
23862442

2443+
#[cfg(feature = "client")]
2444+
#[test]
2445+
fn test_client_obs_fold_line() {
2446+
fn unfold(src: &str) -> String {
2447+
let mut buf = src.as_bytes().to_vec();
2448+
let mut idx = HeaderIndices {
2449+
name: (0, 0),
2450+
value: (0, buf.len()),
2451+
};
2452+
Client::obs_fold_line(&mut buf, &mut idx);
2453+
String::from_utf8(buf[idx.value.0 .. idx.value.1].to_vec()).unwrap()
2454+
}
2455+
2456+
assert_eq!(
2457+
unfold("a normal line"),
2458+
"a normal line",
2459+
);
2460+
2461+
assert_eq!(
2462+
unfold("obs\r\n fold\r\n\t line"),
2463+
"obs fold line",
2464+
);
2465+
}
2466+
23872467
#[test]
23882468
fn test_client_request_encode_title_case() {
23892469
use crate::proto::BodyLength;

‎tests/client.rs

+32-57
Original file line numberDiff line numberDiff line change
@@ -1124,6 +1124,38 @@ test! {
11241124
body: &b"Mmmmh, baguettes."[..],
11251125
}
11261126

1127+
test! {
1128+
name: client_obs_fold_headers,
1129+
1130+
server:
1131+
expected: "\
1132+
GET / HTTP/1.1\r\n\
1133+
host: {addr}\r\n\
1134+
\r\n\
1135+
",
1136+
reply: "\
1137+
HTTP/1.1 200 OK\r\n\
1138+
Content-Length: 0\r\n\
1139+
Fold: just\r\n some\r\n\t folding\r\n\
1140+
\r\n\
1141+
",
1142+
1143+
client:
1144+
options: {
1145+
http1_allow_obsolete_multiline_headers_in_responses: true,
1146+
},
1147+
request: {
1148+
method: GET,
1149+
url: "http://{addr}/",
1150+
},
1151+
response:
1152+
status: OK,
1153+
headers: {
1154+
"fold" => "just some folding",
1155+
},
1156+
body: None,
1157+
}
1158+
11271159
mod dispatch_impl {
11281160
use super::*;
11291161
use std::io::{self, Read, Write};
@@ -2232,63 +2264,6 @@ mod conn {
22322264
future::join(server, client).await;
22332265
}
22342266

2235-
#[tokio::test]
2236-
async fn get_obsolete_line_folding() {
2237-
let _ = ::pretty_env_logger::try_init();
2238-
let listener = TkTcpListener::bind(SocketAddr::from(([127, 0, 0, 1], 0)))
2239-
.await
2240-
.unwrap();
2241-
let addr = listener.local_addr().unwrap();
2242-
2243-
let server = async move {
2244-
let mut sock = listener.accept().await.unwrap().0;
2245-
let mut buf = [0; 4096];
2246-
let n = sock.read(&mut buf).await.expect("read 1");
2247-
2248-
// Notably:
2249-
// - Just a path, since just a path was set
2250-
// - No host, since no host was set
2251-
let expected = "GET /a HTTP/1.1\r\n\r\n";
2252-
assert_eq!(s(&buf[..n]), expected);
2253-
2254-
sock.write_all(b"HTTP/1.1 200 OK\r\nContent-Length: \r\n 0\r\nLine-Folded-Header: hello\r\n world \r\n \r\n\r\n")
2255-
.await
2256-
.unwrap();
2257-
};
2258-
2259-
let client = async move {
2260-
let tcp = tcp_connect(&addr).await.expect("connect");
2261-
let (mut client, conn) = conn::Builder::new()
2262-
.http1_allow_obsolete_multiline_headers_in_responses(true)
2263-
.handshake::<_, Body>(tcp)
2264-
.await
2265-
.expect("handshake");
2266-
2267-
tokio::task::spawn(async move {
2268-
conn.await.expect("http conn");
2269-
});
2270-
2271-
let req = Request::builder()
2272-
.uri("/a")
2273-
.body(Default::default())
2274-
.unwrap();
2275-
let mut res = client.send_request(req).await.expect("send_request");
2276-
assert_eq!(res.status(), hyper::StatusCode::OK);
2277-
assert_eq!(res.headers().len(), 2);
2278-
assert_eq!(
2279-
res.headers().get(http::header::CONTENT_LENGTH).unwrap(),
2280-
"0"
2281-
);
2282-
assert_eq!(
2283-
res.headers().get("line-folded-header").unwrap(),
2284-
"hello world"
2285-
);
2286-
assert!(res.body_mut().data().await.is_none());
2287-
};
2288-
2289-
future::join(server, client).await;
2290-
}
2291-
22922267
#[tokio::test]
22932268
async fn get_custom_reason_phrase() {
22942269
let _ = ::pretty_env_logger::try_init();

0 commit comments

Comments
 (0)
Please sign in to comment.