Skip to content

Commit 0f6bd46

Browse files
committed
Fix request pseudo-header construction for CONNECT & OPTION methods
CONNECT & OPTIONS request has special requirements regarding :path & :scheme pseudo-header which were not met. Construction of pseudo-header was fixed to not include :path & :scheme fields for CONNECT method. Empty :path field for OPTIONS requests now translates to '*' value sent in :path field. In addition to tests included in MR the CONNECT request changes were tested against server based on hyper 1.2.
1 parent 0d66e3c commit 0f6bd46

File tree

4 files changed

+264
-57
lines changed

4 files changed

+264
-57
lines changed

src/frame/headers.rs

+179-16
Original file line numberDiff line numberDiff line change
@@ -554,32 +554,38 @@ impl Pseudo {
554554
pub fn request(method: Method, uri: Uri, protocol: Option<Protocol>) -> Self {
555555
let parts = uri::Parts::from(uri);
556556

557-
let mut path = parts
558-
.path_and_query
559-
.map(|v| BytesStr::from(v.as_str()))
560-
.unwrap_or(BytesStr::from_static(""));
561-
562-
match method {
563-
Method::OPTIONS | Method::CONNECT => {}
564-
_ if path.is_empty() => {
565-
path = BytesStr::from_static("/");
566-
}
567-
_ => {}
568-
}
557+
let (scheme, path) = if method == Method::CONNECT && protocol.is_none() {
558+
(None, None)
559+
} else {
560+
let path = parts
561+
.path_and_query
562+
.map(|v| BytesStr::from(v.as_str()))
563+
.unwrap_or(BytesStr::from_static(""));
564+
565+
let path = if !path.is_empty() {
566+
path
567+
} else {
568+
if method == Method::OPTIONS {
569+
BytesStr::from_static("*")
570+
} else {
571+
BytesStr::from_static("/")
572+
}
573+
};
574+
575+
(parts.scheme, Some(path))
576+
};
569577

570578
let mut pseudo = Pseudo {
571579
method: Some(method),
572580
scheme: None,
573581
authority: None,
574-
path: Some(path).filter(|p| !p.is_empty()),
582+
path,
575583
protocol,
576584
status: None,
577585
};
578586

579587
// If the URI includes a scheme component, add it to the pseudo headers
580-
//
581-
// TODO: Scheme must be set...
582-
if let Some(scheme) = parts.scheme {
588+
if let Some(scheme) = scheme {
583589
pseudo.set_scheme(scheme);
584590
}
585591

@@ -1048,4 +1054,161 @@ mod test {
10481054
let mut buf = BytesMut::new();
10491055
huffman::decode(src, &mut buf).unwrap()
10501056
}
1057+
1058+
#[test]
1059+
fn test_connect_request_pseudo_headers_omits_path_and_scheme() {
1060+
// a) CONNECT requests MUST NOT include :scheme & :path pseudo-header fields
1061+
// See: https://datatracker.ietf.org/doc/html/rfc9113#section-8.5
1062+
1063+
assert_eq!(
1064+
Pseudo::request(
1065+
Method::CONNECT,
1066+
Uri::from_static("https://example.com:8443"),
1067+
None
1068+
),
1069+
Pseudo {
1070+
method: Method::CONNECT.into(),
1071+
authority: BytesStr::from_static("example.com:8443").into(),
1072+
..Default::default()
1073+
}
1074+
);
1075+
1076+
assert_eq!(
1077+
Pseudo::request(
1078+
Method::CONNECT,
1079+
Uri::from_static("https://example.com/test"),
1080+
None
1081+
),
1082+
Pseudo {
1083+
method: Method::CONNECT.into(),
1084+
authority: BytesStr::from_static("example.com").into(),
1085+
..Default::default()
1086+
}
1087+
);
1088+
1089+
assert_eq!(
1090+
Pseudo::request(Method::CONNECT, Uri::from_static("example.com:8443"), None),
1091+
Pseudo {
1092+
method: Method::CONNECT.into(),
1093+
authority: BytesStr::from_static("example.com:8443").into(),
1094+
..Default::default()
1095+
}
1096+
);
1097+
}
1098+
1099+
#[test]
1100+
fn test_extended_connect_request_pseudo_headers_includes_path_and_scheme() {
1101+
// On requests that contain the :protocol pseudo-header field, the
1102+
// :scheme and :path pseudo-header fields of the target URI (see
1103+
// Section 5) MUST also be included.
1104+
// See: https://datatracker.ietf.org/doc/html/rfc8441#section-4
1105+
1106+
assert_eq!(
1107+
Pseudo::request(
1108+
Method::CONNECT,
1109+
Uri::from_static("https://example.com:8443"),
1110+
Protocol::from_static("the-bread-protocol").into()
1111+
),
1112+
Pseudo {
1113+
method: Method::CONNECT.into(),
1114+
authority: BytesStr::from_static("example.com:8443").into(),
1115+
scheme: BytesStr::from_static("https").into(),
1116+
path: BytesStr::from_static("/").into(),
1117+
protocol: Protocol::from_static("the-bread-protocol").into(),
1118+
..Default::default()
1119+
}
1120+
);
1121+
1122+
assert_eq!(
1123+
Pseudo::request(
1124+
Method::CONNECT,
1125+
Uri::from_static("https://example.com:8443/test"),
1126+
Protocol::from_static("the-bread-protocol").into()
1127+
),
1128+
Pseudo {
1129+
method: Method::CONNECT.into(),
1130+
authority: BytesStr::from_static("example.com:8443").into(),
1131+
scheme: BytesStr::from_static("https").into(),
1132+
path: BytesStr::from_static("/test").into(),
1133+
protocol: Protocol::from_static("the-bread-protocol").into(),
1134+
..Default::default()
1135+
}
1136+
);
1137+
1138+
assert_eq!(
1139+
Pseudo::request(
1140+
Method::CONNECT,
1141+
Uri::from_static("http://example.com/a/b/c"),
1142+
Protocol::from_static("the-bread-protocol").into()
1143+
),
1144+
Pseudo {
1145+
method: Method::CONNECT.into(),
1146+
authority: BytesStr::from_static("example.com").into(),
1147+
scheme: BytesStr::from_static("http").into(),
1148+
path: BytesStr::from_static("/a/b/c").into(),
1149+
protocol: Protocol::from_static("the-bread-protocol").into(),
1150+
..Default::default()
1151+
}
1152+
);
1153+
}
1154+
1155+
#[test]
1156+
fn test_options_request_with_empty_path_has_asterisk_as_pseudo_path() {
1157+
// an OPTIONS request for an "http" or "https" URI that does not include a path component;
1158+
// these MUST include a ":path" pseudo-header field with a value of '*' (see Section 7.1 of [HTTP]).
1159+
// See: https://datatracker.ietf.org/doc/html/rfc9113#section-8.3.1
1160+
assert_eq!(
1161+
Pseudo::request(Method::OPTIONS, Uri::from_static("example.com:8080"), None,),
1162+
Pseudo {
1163+
method: Method::OPTIONS.into(),
1164+
authority: BytesStr::from_static("example.com:8080").into(),
1165+
path: BytesStr::from_static("*").into(),
1166+
..Default::default()
1167+
}
1168+
);
1169+
}
1170+
1171+
#[test]
1172+
fn test_non_option_and_non_connect_requests_include_path_and_scheme() {
1173+
let methods = [
1174+
Method::GET,
1175+
Method::POST,
1176+
Method::PUT,
1177+
Method::DELETE,
1178+
Method::HEAD,
1179+
Method::PATCH,
1180+
Method::TRACE,
1181+
];
1182+
1183+
for method in methods {
1184+
assert_eq!(
1185+
Pseudo::request(
1186+
method.clone(),
1187+
Uri::from_static("http://example.com:8080"),
1188+
None,
1189+
),
1190+
Pseudo {
1191+
method: method.clone().into(),
1192+
authority: BytesStr::from_static("example.com:8080").into(),
1193+
scheme: BytesStr::from_static("http").into(),
1194+
path: BytesStr::from_static("/").into(),
1195+
..Default::default()
1196+
}
1197+
);
1198+
assert_eq!(
1199+
Pseudo::request(
1200+
method.clone(),
1201+
Uri::from_static("https://example.com/a/b/c"),
1202+
None,
1203+
),
1204+
Pseudo {
1205+
method: method.into(),
1206+
authority: BytesStr::from_static("example.com").into(),
1207+
scheme: BytesStr::from_static("https").into(),
1208+
path: BytesStr::from_static("/a/b/c").into(),
1209+
..Default::default()
1210+
}
1211+
);
1212+
}
1213+
}
10511214
}

tests/h2-support/src/frames.rs

+9-16
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,7 @@ use std::fmt;
44
use bytes::Bytes;
55
use http::{HeaderMap, StatusCode};
66

7-
use h2::{
8-
ext::Protocol,
9-
frame::{self, Frame, StreamId},
10-
};
7+
use h2::frame::{self, Frame, StreamId};
118

129
pub const SETTINGS: &[u8] = &[0, 0, 0, 4, 0, 0, 0, 0, 0];
1310
pub const SETTINGS_ACK: &[u8] = &[0, 0, 0, 4, 1, 0, 0, 0, 0];
@@ -124,19 +121,24 @@ impl Mock<frame::Headers> {
124121
M::Error: fmt::Debug,
125122
{
126123
let method = method.try_into().unwrap();
127-
let (id, _, fields) = self.into_parts();
124+
let (id, pseudo, fields) = self.into_parts();
128125
let frame = frame::Headers::new(
129126
id,
130127
frame::Pseudo {
131-
scheme: None,
132128
method: Some(method),
133-
..Default::default()
129+
..pseudo
134130
},
135131
fields,
136132
);
137133
Mock(frame)
138134
}
139135

136+
pub fn pseudo(self, pseudo: frame::Pseudo) -> Self {
137+
let (id, _, fields) = self.into_parts();
138+
let frame = frame::Headers::new(id, pseudo, fields);
139+
Mock(frame)
140+
}
141+
140142
pub fn response<S>(self, status: S) -> Self
141143
where
142144
S: TryInto<http::StatusCode>,
@@ -184,15 +186,6 @@ impl Mock<frame::Headers> {
184186
Mock(frame::Headers::new(id, pseudo, fields))
185187
}
186188

187-
pub fn protocol(self, value: &str) -> Self {
188-
let (id, mut pseudo, fields) = self.into_parts();
189-
let value = Protocol::from(value);
190-
191-
pseudo.set_protocol(value);
192-
193-
Mock(frame::Headers::new(id, pseudo, fields))
194-
}
195-
196189
pub fn eos(mut self) -> Self {
197190
self.0.set_end_stream();
198191
self

tests/h2-tests/tests/client_request.rs

+47-2
Original file line numberDiff line numberDiff line change
@@ -585,6 +585,45 @@ async fn http_2_request_without_scheme_or_authority() {
585585
join(srv, h2).await;
586586
}
587587

588+
#[tokio::test]
589+
async fn http_2_connect_request_omit_scheme_and_path_fields() {
590+
h2_support::trace_init!();
591+
let (io, mut srv) = mock::new();
592+
593+
let srv = async move {
594+
let settings = srv.assert_client_handshake().await;
595+
assert_default_settings!(settings);
596+
srv.recv_frame(
597+
frames::headers(1)
598+
.pseudo(frame::Pseudo {
599+
method: Method::CONNECT.into(),
600+
authority: util::byte_str("tunnel.example.com:8443").into(),
601+
..Default::default()
602+
})
603+
.eos(),
604+
)
605+
.await;
606+
srv.send_frame(frames::headers(1).response(200).eos()).await;
607+
};
608+
609+
let h2 = async move {
610+
let (mut client, mut h2) = client::handshake(io).await.expect("handshake");
611+
612+
// In HTTP_2 CONNECT request the ":scheme" and ":path" pseudo-header fields MUST be omitted.
613+
let request = Request::builder()
614+
.version(Version::HTTP_2)
615+
.method(Method::CONNECT)
616+
.uri("https://tunnel.example.com:8443/")
617+
.body(())
618+
.unwrap();
619+
620+
let (response, _) = client.send_request(request, true).unwrap();
621+
h2.drive(response).await.unwrap();
622+
};
623+
624+
join(srv, h2).await;
625+
}
626+
588627
#[test]
589628
#[ignore]
590629
fn request_with_h1_version() {}
@@ -1521,8 +1560,14 @@ async fn extended_connect_request() {
15211560

15221561
srv.recv_frame(
15231562
frames::headers(1)
1524-
.request("CONNECT", "http://bread/baguette")
1525-
.protocol("the-bread-protocol")
1563+
.pseudo(frame::Pseudo {
1564+
method: Method::CONNECT.into(),
1565+
scheme: util::byte_str("http").into(),
1566+
authority: util::byte_str("bread").into(),
1567+
path: util::byte_str("/baguette").into(),
1568+
protocol: Protocol::from_static("the-bread-protocol").into(),
1569+
..Default::default()
1570+
})
15261571
.eos(),
15271572
)
15281573
.await;

0 commit comments

Comments
 (0)