Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
35 changes: 35 additions & 0 deletions src/frame/headers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,41 @@ impl fmt::Debug for Headers {

// ===== util =====

/// Canonicalize `Host` header into `:authority` pseudo-header for HTTP/2.
///
/// Per [RFC 9113 §8.3.1][rfc]:
///
/// - Clients that generate HTTP/2 requests directly MUST use the `:authority`
/// pseudo-header field.
/// - Clients MUST NOT generate a request with a `Host` header field that
/// differs from the `:authority` pseudo-header field.
/// - An intermediary that forwards a request over HTTP/2 MAY retain any
/// `Host` header field.
///
/// This function enforces consistency using "Host wins" semantics: if a
/// `Host` header is present and parseable as a URI authority, its value
/// overrides `:authority`. The `Host` header is retained on the wire so
/// that intermediaries can forward it (e.g. when proxying HTTP/1.1 traffic
/// over an HTTP/2 transport).
///
/// [rfc]: https://www.rfc-editor.org/rfc/rfc9113.html#section-8.3.1
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()));
// Collapse any duplicate Host values into a single value that
// matches the newly-set :authority.
if let Ok(normalized) = HeaderValue::from_str(authority.as_str()) {
headers.insert(header::HOST, normalized);
}
} else {
// Host is unparseable as an authority; remove it to avoid
// sending a value that differs from :authority.
headers.remove(header::HOST);
}
}
}

#[derive(Debug, PartialEq, Eq)]
pub struct ParseU64Error;

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
254 changes: 254 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,260 @@ async fn reset_before_headers_reaches_peer_without_headers() {
join(srv, client).await;
}

#[tokio::test]
async fn host_authority_mismatch_host_wins() {
// When Host differs from URI authority, Host wins and becomes :authority.
// Host is retained.
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()
})
.field("host", "example.com")
.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() {
// 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. Host wins and is retained on the wire.
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()
})
.field("host", "example.com")
.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_retains_host() {
// When Host matches URI authority, Host header is retained on the wire.
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()
})
.field("host", "example.com")
.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.
// Host headers are collapsed to the first value.
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()
})
.field("host", "first.example")
.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), URI authority is kept
// and invalid Host is stripped to avoid violating RFC 9113 §8.3.1.
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. The scheme check fires before canonicalization runs, so Host cannot rescue
// a relative URI.
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