Skip to content

Commit 9940bd0

Browse files
seanmonstarCinisBorn
authored andcommitted
fix(http1): detect TE: trailers caselessly and with other values (#4152)
1 parent a7a3438 commit 9940bd0

3 files changed

Lines changed: 127 additions & 6 deletions

File tree

src/headers.rs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,26 @@ fn connection_has(value: &HeaderValue, needle: &str) -> bool {
4242
false
4343
}
4444

45+
#[cfg(feature = "http1")]
46+
pub(super) fn te_is_trailers(headers: &http::HeaderMap) -> bool {
47+
header_value_list_has(headers.get_all(http::header::TE).into_iter(), "trailers")
48+
}
49+
50+
#[cfg(feature = "http1")]
51+
fn header_value_list_has(values: http::header::ValueIter<'_, HeaderValue>, needle: &str) -> bool {
52+
for value in values {
53+
if let Ok(line) = value.to_str() {
54+
for token in line.split(',') {
55+
if token.trim().eq_ignore_ascii_case(needle) {
56+
return true;
57+
}
58+
}
59+
}
60+
}
61+
62+
false
63+
}
64+
4565
#[cfg(all(feature = "http1", feature = "server"))]
4666
pub(super) fn content_length_parse(value: &HeaderValue) -> Option<u64> {
4767
from_digits(value.as_bytes())
@@ -166,3 +186,35 @@ pub(super) fn add_chunked(mut entry: http::header::OccupiedEntry<'_, HeaderValue
166186

167187
entry.insert(HeaderValue::from_static(CHUNKED));
168188
}
189+
190+
#[cfg(all(test, feature = "http1"))]
191+
mod tests {
192+
use super::te_is_trailers;
193+
use http::header::{HeaderValue, TE};
194+
use http::HeaderMap;
195+
196+
#[test]
197+
fn te_is_trailers_accepts_comma_separated_values() {
198+
let mut headers = HeaderMap::new();
199+
headers.insert(TE, HeaderValue::from_static("gzip, Trailers"));
200+
201+
assert!(te_is_trailers(&headers));
202+
}
203+
204+
#[test]
205+
fn te_is_trailers_accepts_multiple_header_lines() {
206+
let mut headers = HeaderMap::new();
207+
headers.append(TE, HeaderValue::from_static("gzip"));
208+
headers.append(TE, HeaderValue::from_static("trailers"));
209+
210+
assert!(te_is_trailers(&headers));
211+
}
212+
213+
#[test]
214+
fn te_is_trailers_rejects_missing_trailers_token() {
215+
let mut headers = HeaderMap::new();
216+
headers.insert(TE, HeaderValue::from_static("gzip"));
217+
218+
assert!(!te_is_trailers(&headers));
219+
}
220+
}

src/proto/h1/conn.rs

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use std::time::Duration;
1111
use crate::rt::{Read, Write};
1212
use bytes::{Buf, Bytes};
1313
use futures_core::ready;
14-
use http::header::{HeaderValue, CONNECTION, TE};
14+
use http::header::{HeaderValue, CONNECTION};
1515
use http::{HeaderMap, Method, Version};
1616
use http_body::Frame;
1717
use httparse::ParserConfig;
@@ -325,11 +325,7 @@ where
325325
));
326326
}
327327

328-
self.state.allow_trailer_fields = msg
329-
.head
330-
.headers
331-
.get(TE)
332-
.map_or(false, |te_header| te_header == "trailers");
328+
self.state.allow_trailer_fields = headers::te_is_trailers(&msg.head.headers);
333329

334330
Poll::Ready(Some(Ok((msg.head, msg.decode, wants))))
335331
}

tests/server.rs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3335,6 +3335,79 @@ fn http1_trailer_fields_not_allowed() {
33353335
assert_eq!(body, expected_body);
33363336
}
33373337

3338+
#[test]
3339+
fn http1_trailer_fields_allowed_with_comma_separated_te() {
3340+
let body = futures_util::stream::once(async move { Ok("hello".into()) });
3341+
let mut headers = HeaderMap::new();
3342+
headers.insert("chunky-trailer", "header data".parse().unwrap());
3343+
3344+
let server = serve();
3345+
server
3346+
.reply()
3347+
.header("transfer-encoding", "chunked")
3348+
.header("trailer", "chunky-trailer")
3349+
.body_stream_with_trailers(body, headers);
3350+
let mut req = connect(server.addr());
3351+
req.write_all(
3352+
b"\
3353+
GET / HTTP/1.1\r\n\
3354+
Host: example.domain\r\n\
3355+
Connection: keep-alive\r\n\
3356+
TE: gzip, Trailers\r\n\
3357+
\r\n\
3358+
",
3359+
)
3360+
.expect("writing");
3361+
3362+
let chunky_trailer_chunk = b"\r\nchunky-trailer: header data\r\n\r\n";
3363+
let res = read_until(&mut req, |buf| buf.ends_with(chunky_trailer_chunk)).expect("reading");
3364+
let sres = s(&res);
3365+
3366+
let date_fragment = "GMT\r\n\r\n";
3367+
let pos = sres.find(date_fragment).expect("find GMT");
3368+
let body = &sres[pos + date_fragment.len()..];
3369+
3370+
let expected_body = "5\r\nhello\r\n0\r\nchunky-trailer: header data\r\n\r\n";
3371+
assert_eq!(body, expected_body);
3372+
}
3373+
3374+
#[test]
3375+
fn http1_trailer_fields_allowed_with_multiple_te_headers() {
3376+
let body = futures_util::stream::once(async move { Ok("hello".into()) });
3377+
let mut headers = HeaderMap::new();
3378+
headers.insert("chunky-trailer", "header data".parse().unwrap());
3379+
3380+
let server = serve();
3381+
server
3382+
.reply()
3383+
.header("transfer-encoding", "chunked")
3384+
.header("trailer", "chunky-trailer")
3385+
.body_stream_with_trailers(body, headers);
3386+
let mut req = connect(server.addr());
3387+
req.write_all(
3388+
b"\
3389+
GET / HTTP/1.1\r\n\
3390+
Host: example.domain\r\n\
3391+
Connection: keep-alive\r\n\
3392+
TE: gzip\r\n\
3393+
TE: trailers\r\n\
3394+
\r\n\
3395+
",
3396+
)
3397+
.expect("writing");
3398+
3399+
let chunky_trailer_chunk = b"\r\nchunky-trailer: header data\r\n\r\n";
3400+
let res = read_until(&mut req, |buf| buf.ends_with(chunky_trailer_chunk)).expect("reading");
3401+
let sres = s(&res);
3402+
3403+
let date_fragment = "GMT\r\n\r\n";
3404+
let pos = sres.find(date_fragment).expect("find GMT");
3405+
let body = &sres[pos + date_fragment.len()..];
3406+
3407+
let expected_body = "5\r\nhello\r\n0\r\nchunky-trailer: header data\r\n\r\n";
3408+
assert_eq!(body, expected_body);
3409+
}
3410+
33383411
#[test]
33393412
fn http1_trailer_recv_fields() {
33403413
let server = serve();

0 commit comments

Comments
 (0)