Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@

use crate::codec::{Codec, SendError, UserError};
use crate::ext::Protocol;
use crate::frame::{Headers, Pseudo, Reason, Settings, StreamId};
use crate::frame::{self, Headers, Pseudo, Reason, Settings, StreamId};
use crate::proto::{self, Error};
use crate::{FlowControl, PingPong, RecvStream, SendStream};

Expand Down Expand Up @@ -428,10 +428,11 @@ where
/// value of its version field. If the version is set to 2.0, then the
/// request is encoded as per the specification recommends.
///
/// If the version is set to a lower value, then the request is encoded to
/// preserve the characteristics of HTTP 1.1 and lower. Specifically, host
/// headers are permitted and the `:authority` pseudo header is not
/// included.
/// If the version is set to a lower value, then request-target handling is
/// made compatible with HTTP/1.x-style inputs (for example, relative URIs
/// are accepted). Headers are still normalized for HTTP/2 transmission:
/// `Host` is canonicalized into `:authority` and removed from regular
/// headers.
///
/// The caller should always set the request's version field to 2.0 unless
/// specifically transmitting an HTTP 1.1 request over 2.0.
Expand Down Expand Up @@ -1613,7 +1614,7 @@ impl Peer {
Parts {
method,
uri,
headers,
mut headers,
version,
..
},
Expand Down Expand Up @@ -1654,6 +1655,9 @@ impl Peer {
}
}

// Canonicalize Host header into :authority for HTTP/2
frame::canonicalize_host_authority(&mut pseudo, &mut headers);

// Create the HEADERS frame
let mut frame = Headers::new(id, pseudo, headers);

Expand Down
17 changes: 17 additions & 0 deletions src/frame/headers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,23 @@ use crate::hpack::{self, BytesStr};
use http::header::{self, HeaderName, HeaderValue};
use http::{uri, HeaderMap, Method, Request, StatusCode, Uri};

/// Canonicalize `Host` header into `:authority` pseudo-header for HTTP/2.
///
/// - If a `Host` header is present, attempt to parse its first value as a URI authority.
/// - On success, override `:authority` with the parsed value.
/// - Always remove all `Host` headers from the regular header map.
///
/// Callers should only invoke this in an HTTP/2 context.
pub(crate) fn canonicalize_host_authority(pseudo: &mut Pseudo, headers: &mut HeaderMap) {
if let Some(host) = headers.get(header::HOST) {
if let Ok(authority) = uri::Authority::from_maybe_shared(host.as_bytes().to_vec()) {
pseudo.set_authority(BytesStr::from(authority.as_str()));
}
}

headers.remove(header::HOST);
}

use bytes::{Buf, BufMut, Bytes, BytesMut};

use std::fmt;
Expand Down
1 change: 1 addition & 0 deletions src/frame/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ mod window_update;
pub use self::data::Data;
pub use self::go_away::GoAway;
pub use self::head::{Head, Kind};
pub(crate) use self::headers::canonicalize_host_authority;
pub use self::headers::{
parse_u64, Continuation, Headers, Pseudo, PushPromise, PushPromiseHeaderError,
};
Expand Down
7 changes: 5 additions & 2 deletions src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1581,13 +1581,16 @@ impl Peer {
Parts {
method,
uri,
headers,
mut headers,
..
},
_,
) = request.into_parts();

let pseudo = Pseudo::request(method, uri, None);
let mut pseudo = Pseudo::request(method, uri, None);

// Canonicalize Host header into :authority for HTTP/2 push promises
frame::canonicalize_host_authority(&mut pseudo, &mut headers);

Ok(frame::PushPromise::new(
stream_id,
Expand Down
6 changes: 6 additions & 0 deletions tests/h2-support/src/frames.rs
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,12 @@ impl Mock<frame::PushPromise> {
Mock(frame)
}

pub fn pseudo(self, pseudo: frame::Pseudo) -> Self {
let (id, promised, _, fields) = self.into_parts();
let frame = frame::PushPromise::new(id, promised, pseudo, fields);
Mock(frame)
}

pub fn fields(self, fields: HeaderMap) -> Self {
let (id, promised, pseudo, _) = self.into_parts();
let frame = frame::PushPromise::new(id, promised, pseudo, fields);
Expand Down
249 changes: 249 additions & 0 deletions tests/h2-tests/tests/client_request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2059,6 +2059,255 @@ async fn reset_before_headers_reaches_peer_without_headers() {
join(srv, client).await;
}

#[tokio::test]
async fn host_authority_mismatch_promotes_host_to_authority() {
// When Host differs from URI authority, Host should win for :authority
// and Host header should be stripped from regular headers.
h2_support::trace_init!();
let (io, mut srv) = mock::new();

let srv = async move {
let settings = srv.assert_client_handshake().await;
assert_default_settings!(settings);
srv.recv_frame(
frames::headers(1)
.pseudo(frame::Pseudo {
method: Method::GET.into(),
scheme: util::byte_str("https").into(),
authority: util::byte_str("example.com").into(),
path: util::byte_str("/").into(),
..Default::default()
})
.eos(),
)
.await;
srv.send_frame(frames::headers(1).response(200).eos()).await;
};

let h2 = async move {
let (mut client, mut h2) = client::handshake(io).await.expect("handshake");

let request = Request::builder()
.version(Version::HTTP_2)
.method(Method::GET)
.uri("https://example.net/")
.header("host", "example.com")
.body(())
.unwrap();

let (response, _) = client.send_request(request, true).unwrap();
h2.drive(response).await.unwrap();
};

join(srv, h2).await;
}

#[tokio::test]
async fn host_authority_http11_version_still_promotes_host_to_authority() {
// Real integration path: higher layers (for example hyper/hyper-util)
// commonly pass Request values with the default HTTP/1.1 version even when
// the selected transport is HTTP/2. We still need HTTP/2-compliant
// canonicalization on the wire, so Host must promote to :authority here.
h2_support::trace_init!();
let (io, mut srv) = mock::new();

let srv = async move {
let settings = srv.assert_client_handshake().await;
assert_default_settings!(settings);
srv.recv_frame(
frames::headers(1)
.pseudo(frame::Pseudo {
method: Method::GET.into(),
scheme: util::byte_str("https").into(),
authority: util::byte_str("example.com").into(),
path: util::byte_str("/").into(),
..Default::default()
})
.eos(),
)
.await;
srv.send_frame(frames::headers(1).response(200).eos()).await;
};

let h2 = async move {
let (mut client, mut h2) = client::handshake(io).await.expect("handshake");

let request = Request::builder()
// Keep the default request version to model the common caller behavior.
.method(Method::GET)
.uri("https://example.net/")
.header("host", "example.com")
.body(())
.unwrap();

let (response, _) = client.send_request(request, true).unwrap();
h2.drive(response).await.unwrap();
};

join(srv, h2).await;
}

#[tokio::test]
async fn host_authority_matching_strips_host() {
// When Host matches URI authority, Host header should still be stripped.
h2_support::trace_init!();
let (io, mut srv) = mock::new();

let srv = async move {
let settings = srv.assert_client_handshake().await;
assert_default_settings!(settings);
srv.recv_frame(
frames::headers(1)
.pseudo(frame::Pseudo {
method: Method::GET.into(),
scheme: util::byte_str("https").into(),
authority: util::byte_str("example.com").into(),
path: util::byte_str("/").into(),
..Default::default()
})
.eos(),
)
.await;
srv.send_frame(frames::headers(1).response(200).eos()).await;
};

let h2 = async move {
let (mut client, mut h2) = client::handshake(io).await.expect("handshake");

let request = Request::builder()
.version(Version::HTTP_2)
.method(Method::GET)
.uri("https://example.com/")
.header("host", "example.com")
.body(())
.unwrap();

let (response, _) = client.send_request(request, true).unwrap();
h2.drive(response).await.unwrap();
};

join(srv, h2).await;
}

#[tokio::test]
async fn host_authority_duplicate_host_first_wins() {
// When multiple Host headers are present, first value is used for :authority.
h2_support::trace_init!();
let (io, mut srv) = mock::new();

let srv = async move {
let settings = srv.assert_client_handshake().await;
assert_default_settings!(settings);
srv.recv_frame(
frames::headers(1)
.pseudo(frame::Pseudo {
method: Method::GET.into(),
scheme: util::byte_str("https").into(),
authority: util::byte_str("first.example").into(),
path: util::byte_str("/").into(),
..Default::default()
})
.eos(),
)
.await;
srv.send_frame(frames::headers(1).response(200).eos()).await;
};

let h2 = async move {
let (mut client, mut h2) = client::handshake(io).await.expect("handshake");

let request = Request::builder()
.version(Version::HTTP_2)
.method(Method::GET)
.uri("https://example.net/")
.header("host", "first.example")
.header("host", "second.example")
.body(())
.unwrap();

let (response, _) = client.send_request(request, true).unwrap();
h2.drive(response).await.unwrap();
};

join(srv, h2).await;
}

#[tokio::test]
async fn host_authority_invalid_host_keeps_uri_authority() {
// When Host is invalid (unparseable as authority), keep URI authority and strip Host.
h2_support::trace_init!();
let (io, mut srv) = mock::new();

let srv = async move {
let settings = srv.assert_client_handshake().await;
assert_default_settings!(settings);
srv.recv_frame(
frames::headers(1)
.pseudo(frame::Pseudo {
method: Method::GET.into(),
scheme: util::byte_str("https").into(),
authority: util::byte_str("example.net").into(),
path: util::byte_str("/").into(),
..Default::default()
})
.eos(),
)
.await;
srv.send_frame(frames::headers(1).response(200).eos()).await;
};

let h2 = async move {
let (mut client, mut h2) = client::handshake(io).await.expect("handshake");

let request = Request::builder()
.version(Version::HTTP_2)
.method(Method::GET)
.uri("https://example.net/")
.header("host", "not:a/good authority")
.body(())
.unwrap();

let (response, _) = client.send_request(request, true).unwrap();
h2.drive(response).await.unwrap();
};

join(srv, h2).await;
}

#[tokio::test]
async fn host_authority_relative_uri_http2_still_errors() {
// Relative URI with HTTP/2 version should still produce MissingUriSchemeAndAuthority error,
// even when a Host header is provided. The Host canonicalization does not synthesize
// authority for relative URIs.
h2_support::trace_init!();
let (io, mut srv) = mock::new();

let srv = async move {
let settings = srv.assert_client_handshake().await;
assert_default_settings!(settings);
};

let h2 = async move {
let (mut client, h2) = client::handshake(io).await.expect("handshake");

let request = Request::builder()
.version(Version::HTTP_2)
.method(Method::GET)
.uri("/")
.header("host", "example.com")
.body(())
.unwrap();

client
.send_request(request, true)
.expect_err("should be UserError");
let _: () = h2.await.expect("h2");
drop(client);
};

join(srv, h2).await;
}

const SETTINGS: &[u8] = &[0, 0, 0, 4, 0, 0, 0, 0, 0];
const SETTINGS_ACK: &[u8] = &[0, 0, 0, 4, 1, 0, 0, 0, 0];

Expand Down
Loading