Skip to content
Merged
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
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1654,6 +1654,18 @@ field available.
These helpers only declare or parse request metadata. RTTP does not infer or
enforce consent, tracking, legal, or serving policy.

### Bounded Early-Data request metadata

`HttpClient::early_data()` emits `Early-Data: 1`. On the server,
`Request::early_data()` and `HttpRequest::early_data()` parse the same bounded
singleton RFC 8470 `1` signal through the shared protocol representation,
returning `Ok(None)` when the field is absent and a parser error for
malformed, unsupported, oversized, duplicate, or control-byte values while
leaving the raw `Early-Data` field available.

These helpers only declare or parse request metadata. RTTP does not enable
0-RTT transport, decide replay safety, retry, or apply serving policy.

### Bounded Pragma metadata

`rttp-protocol` owns the shared `Pragma` primitive. Client helpers format
Expand Down Expand Up @@ -1865,6 +1877,7 @@ gain additional HTTP/2 header-block handling.
| Set-Cookie | Client `Response::set_cookies()` and server `HttpResponse::with_set_cookie()`/`set_cookies()` share bounded protocol `Set-Cookie` response metadata, preserve multiple field lines and raw headers, redact cookie values from typed debug and errors, and reject invalid typed metadata including duplicate attributes, valued flag attributes, non-standard `SameSite`, invalid `Max-Age`, malformed quoted values, and bounds violations; client `Response::cookies()`/`cookie()` are a legacy compatibility view that only exposes protocol-accepted fields and silently omits invalid `Set-Cookie` fields while raw headers remain available | No cookie jar, persistence, domain/path matching, expiry enforcement, SameSite or partitioning policy, or automatic request `Cookie` emission |
| Variant-Vary | Client `Response::variant_vary()` and server `HttpResponse::with_variant_vary()`/`variant_vary()` share bounded RFC 2295 `Variant-Vary` response metadata through the protocol `VariantVary` type while preserving raw headers on accessor errors | No cache-key construction, variant selection, `Alternates`/`TCN`/`Vary` synthesis, transparent content negotiation, or cache behavior |
| Sec-GPC | Client `sec_gpc` emits bounded `Sec-GPC: 1` request metadata; server `Request::sec_gpc()` and `HttpRequest::sec_gpc()` parse typed received values while preserving raw headers on errors | No consent inference, tracking-policy enforcement, legal policy, serving policy, retries, or browser state |
| Early-Data | Client `early_data` emits bounded RFC 8470 `Early-Data: 1` request metadata; server `Request::early_data()` and `HttpRequest::early_data()` parse typed received values while preserving raw headers on errors | No 0-RTT transport enablement, replay-safety decision, retry behavior, or server acceptance policy |
| Upgrade-Insecure-Requests | Client `upgrade_insecure_requests` emits bounded singleton `Upgrade-Insecure-Requests: 1` request metadata; server `Request::upgrade_insecure_requests()` and `HttpRequest::upgrade_insecure_requests()` parse typed received values while preserving raw headers on errors | No URL rewriting, redirecting, Content-Security-Policy enforcement, HSTS, or automatic scheme selection |
| Depth | Client `depth` emits bounded singleton WebDAV `Depth` request metadata through the shared protocol type, replacing an existing same-name field; server `Request::depth()` and `HttpRequest::depth()` parse typed received values while preserving raw headers on errors | No resource traversal, WebDAV method selection, method-policy enforcement, retry, or forwarding policy |
| Destination | Client `destination` emits bounded singleton WebDAV `Destination` request metadata through the shared protocol type, replacing an existing same-name field; server `Request::destination()` and `HttpRequest::destination()` parse typed received values while preserving raw headers on errors | No destination resolution, URI normalization, authorization, COPY/MOVE execution, or application resource policy |
Expand Down Expand Up @@ -3164,6 +3177,7 @@ TLS or async accept loops.
| Set-Cookie | `HttpResponse::with_set_cookie` and `HttpResponse::set_cookies` declare or parse bounded protocol `Set-Cookie` response metadata, preserve multiple field lines and raw headers, redact cookie values from typed debug and errors, and reject invalid typed metadata including duplicate attributes, valued flag attributes, non-standard `SameSite`, invalid `Max-Age`, malformed quoted values, and bounds violations | No cookie jar, persistence, domain/path matching, expiry enforcement, SameSite or partitioning policy, or automatic request `Cookie` emission |
| Variant-Vary | `HttpVariantVary`, `HttpResponse::with_variant_vary`, and `HttpResponse::variant_vary` declare or parse bounded RFC 2295 `Variant-Vary` response metadata through the shared protocol type, replacing raw duplicates on declaration and preserving raw headers on accessor errors | No cache-key construction, variant selection, `Alternates`/`TCN`/`Vary` synthesis, transparent content negotiation, or cache behavior |
| Sec-GPC | `Request::sec_gpc` and `HttpRequest::sec_gpc` parse bounded singleton `Sec-GPC` `1`-signal metadata and preserve raw values on errors | No consent inference, tracking-policy enforcement, legal policy, serving policy, retries, or browser state |
| Early-Data | `Request::early_data` and `HttpRequest::early_data` parse bounded singleton RFC 8470 `Early-Data` `1`-signal metadata and preserve raw values on errors | No 0-RTT transport enablement, replay-safety decision, retry behavior, or server acceptance policy |
| Upgrade-Insecure-Requests | `Request::upgrade_insecure_requests` and `HttpRequest::upgrade_insecure_requests` parse bounded singleton `Upgrade-Insecure-Requests` `1`-token metadata and preserve raw values on errors | No URL rewriting, redirecting, Content-Security-Policy enforcement, HSTS, or automatic scheme selection |
| Depth | `Request::depth` and `HttpRequest::depth` parse bounded singleton WebDAV `Depth` request metadata through the shared protocol type and preserve raw values on errors | No resource traversal, WebDAV method selection, method-policy enforcement, retry, or forwarding policy |
| Destination | `Request::destination` and `HttpRequest::destination` parse bounded singleton WebDAV `Destination` request metadata through the shared protocol type and preserve raw values on errors | No destination resolution, URI normalization, authorization, COPY/MOVE execution, or application resource policy |
Expand Down
12 changes: 12 additions & 0 deletions crates/rttp-client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1377,6 +1377,17 @@ This helper only declares request metadata. RTTP does not infer or enforce
consent, tracking, legal, or serving policy. Callers that need values outside
the helper can retain raw-header control with `header(("Sec-GPC", "..."))`.

## Bounded Early-Data request metadata

`HttpClient::early_data()` emits `Early-Data: 1` through the shared protocol
`EarlyData` representation. A second call or an existing raw `Early-Data`
field is replaced with the canonical value before a socket is opened.

This helper only declares RFC 8470 request metadata. RTTP does not enable
0-RTT transport, decide replay safety, retry, or apply server acceptance
policy. Callers that need values outside the helper can retain raw-header
control with `header(("Early-Data", "..."))`.

## Bounded Sec-Required-Document-Policy request metadata

`HttpClient::sec_required_document_policy(value)` validates one WICG Document
Expand Down Expand Up @@ -1810,6 +1821,7 @@ header-block model.
| Referer | `referer` emits one bounded canonical `Referer` request field through the shared protocol type, replacing existing case-insensitive fields; absolute, relative, and scheme-relative URI references are accepted, and raw `header(("Referer", value))` remains available as a fallback | No `Referrer-Policy` enforcement, trust decisions, CSRF protection, redaction, URL canonicalization, or redirect behavior |
| User-Agent | `user_agent` emits one bounded canonical `User-Agent` request field through the shared protocol type, replacing existing case-insensitive fields so typed values win over raw headers and the automatic default; absent typed/raw values retain `Mozilla/5.0 rttp/{version}` | No fingerprinting, platform discovery, product policy, global or environment-based defaults, or automatic policy beyond the existing default header |
| Sec-GPC | `sec_gpc` emits bounded `Sec-GPC: 1` request metadata through the shared protocol type | No consent inference, tracking-policy enforcement, legal policy, serving policy, retries, or browser state |
| Early-Data | `early_data` emits bounded RFC 8470 `Early-Data: 1` request metadata through the shared protocol type, replacing existing same-name fields before connecting | No 0-RTT transport enablement, replay-safety decision, retry behavior, or server acceptance policy |
| Sec-Required-Document-Policy | `sec_required_document_policy` emits bounded WICG Document Policy dictionary request metadata through the shared protocol type, replacing existing same-name fields and rejecting malformed, control, duplicate, or oversized input before connecting | No document-policy enforcement, required-policy comparison against `Document-Policy`, document-load blocking, feature enablement, or report sending |
| Upgrade-Insecure-Requests | `upgrade_insecure_requests` emits bounded singleton `Upgrade-Insecure-Requests: 1` request metadata | No URL rewriting, redirecting, Content-Security-Policy enforcement, HSTS, or automatic scheme selection |
| Max-Forwards | `max_forwards` emits bounded singleton `Max-Forwards` request metadata through the shared protocol type | No hop decrement, proxy routing, TRACE/OPTIONS selection, retry, or forwarding policy |
Expand Down
11 changes: 11 additions & 0 deletions crates/rttp-client/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ use rttp_protocol::cdn_loop::{CdnLoop, MAX_CDN_LOOP_VALUE_BYTES};
use rttp_protocol::depth::Depth;
use rttp_protocol::destination::Destination;
use rttp_protocol::dnt::Dnt;
use rttp_protocol::early_data::EarlyData;
use rttp_protocol::expect::Expect;
use rttp_protocol::fetch_metadata::{
SecFetchDest, SecFetchMode, SecFetchSite, SecFetchUser, SecPurpose,
Expand Down Expand Up @@ -530,6 +531,16 @@ impl HttpClient {
Ok(self.header(Header::new("Sec-GPC", sec_gpc.header_value())))
}

/// Set `Early-Data: 1` request metadata.
///
/// This declares the RFC 8470 early-data signal only; it does not enable
/// 0-RTT transport, decide replay safety, or apply server acceptance policy.
pub fn early_data(&mut self) -> error::Result<&mut Self> {
let early_data =
EarlyData::parse("1").map_err(|error| error::builder_with_message(error.to_string()))?;
Ok(self.header(Header::new("Early-Data", early_data.header_value())))
}

/// Set bounded `Sec-Required-Document-Policy` request metadata.
///
/// The value is validated through the shared protocol
Expand Down
1 change: 1 addition & 0 deletions crates/rttp-client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ pub use rttp_protocol::delta_base::{DeltaBase, DeltaBaseParseError};
pub use rttp_protocol::depth::{Depth, DepthParseError};
pub use rttp_protocol::destination::{Destination, DestinationParseError};
pub use rttp_protocol::dnt::{Dnt, DntParseError};
pub use rttp_protocol::early_data::{EarlyData, EarlyDataParseError};
pub use rttp_protocol::expect::{Expect, ExpectParseError};
pub use rttp_protocol::fetch_metadata::{
SecFetchDest, SecFetchMode, SecFetchSite, SecFetchUser, SecPurpose,
Expand Down
29 changes: 17 additions & 12 deletions crates/rttp-client/tests/metadata_facade.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,18 +46,19 @@ use rttp_client::response::{
use rttp_client::{
AIm, AImMember, AImParameter, AImParseError, AcceptDatetime, AcceptDatetimeParseError, Baggage,
BaggageMember, BaggageParseError, BaggageProperty, Depth, DepthParseError, Destination,
DestinationParseError, Dnt, DntParseError, Expect, ExpectParseError, From, FromParseError,
HttpClient, If, IfCondition, IfList, IfParseError, IfPredicate, IfResourceTag,
IfScheduleTagMatch, IfScheduleTagMatchParseError, IfStateToken, Negotiate, NegotiateDirective,
NegotiateParseError, Overwrite, OverwriteParseError, SecFetchDest, SecFetchMode, SecFetchSite,
SecFetchUser, SecGpc, SecGpcParseError, SecPurpose, SecRequiredDocumentPolicy,
SecRequiredDocumentPolicyDirective, SecRequiredDocumentPolicyParseError,
SecRequiredDocumentPolicyValue, SecWebSocketKey, SecWebSocketKeyParseError, Tcn, TcnDirective,
TcnParseError, Timeout, TimeoutParseError, TimeoutType, TraceParent, TraceParentParseError,
TraceState, TraceStateMember, TraceStateParseError, UpgradeInsecureRequests,
UpgradeInsecureRequestsParseError, UserAgent, UserAgentMember, UserAgentParseError,
Via as ClientVia, ViaParseError as ClientViaParseError, XForwardedFor, XForwardedForParseError,
XForwardedHost, XForwardedHostParseError, XForwardedProto, XForwardedProtoParseError,
DestinationParseError, Dnt, DntParseError, EarlyData, EarlyDataParseError, Expect,
ExpectParseError, From, FromParseError, HttpClient, If, IfCondition, IfList, IfParseError,
IfPredicate, IfResourceTag, IfScheduleTagMatch, IfScheduleTagMatchParseError, IfStateToken,
Negotiate, NegotiateDirective, NegotiateParseError, Overwrite, OverwriteParseError, SecFetchDest,
SecFetchMode, SecFetchSite, SecFetchUser, SecGpc, SecGpcParseError, SecPurpose,
SecRequiredDocumentPolicy, SecRequiredDocumentPolicyDirective,
SecRequiredDocumentPolicyParseError, SecRequiredDocumentPolicyValue, SecWebSocketKey,
SecWebSocketKeyParseError, Tcn, TcnDirective, TcnParseError, Timeout, TimeoutParseError,
TimeoutType, TraceParent, TraceParentParseError, TraceState, TraceStateMember,
TraceStateParseError, UpgradeInsecureRequests, UpgradeInsecureRequestsParseError, UserAgent,
UserAgentMember, UserAgentParseError, Via as ClientVia, ViaParseError as ClientViaParseError,
XForwardedFor, XForwardedForParseError, XForwardedHost, XForwardedHostParseError,
XForwardedProto, XForwardedProtoParseError,
};
use rttp_test_support as support;

Expand Down Expand Up @@ -402,6 +403,9 @@ fn response_facade_exports_representative_bounded_metadata_types() {
let _: DntParseError = Dnt::parse("on").expect_err("invalid DNT should be rejected");
let sec_gpc = SecGpc::parse("1").expect("Sec-GPC should parse");
let _: SecGpcParseError = SecGpc::parse("0").expect_err("invalid Sec-GPC should be rejected");
let early_data = EarlyData::parse("1").expect("Early-Data should parse");
let _: EarlyDataParseError =
EarlyData::parse("0").expect_err("invalid Early-Data should be rejected");
let sec_required_document_policy =
SecRequiredDocumentPolicy::parse("oversized-images=2.0, unsized-media=?0, *;report-to=default")
.expect("Sec-Required-Document-Policy should parse");
Expand Down Expand Up @@ -734,6 +738,7 @@ fn response_facade_exports_representative_bounded_metadata_types() {
assert_eq!(fetch_user.header_value(), "?1");
assert_eq!(dnt.header_value(), "1");
assert_eq!(sec_gpc.header_value(), "1");
assert_eq!(early_data.header_value(), "1");
assert_eq!(sec_required_document_policy.directives().len(), 3);
assert_eq!(
sec_required_document_policy
Expand Down
25 changes: 25 additions & 0 deletions crates/rttp-client/tests/test_raw_request_capture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5908,6 +5908,31 @@ fn sec_gpc_helper_emits_one_request_signal() {
assert_eq!(Some("1"), header_value(&request, "Sec-GPC"));
}

#[test]
fn early_data_helper_emits_one_canonical_request_signal() {
let request = capture_request(|base_url| {
client()
.post()
.url(format!("{}/submit", base_url))
.header(("Early-Data", "0"))
.early_data()
.expect("Early-Data should be accepted")
.emit()
.expect("request should succeed");
});
let request = request_text(&request);

assert_eq!(Some("1"), header_value(&request, "Early-Data"));
assert_eq!(
1,
request
.lines()
.filter(|line| line.to_ascii_lowercase().starts_with("early-data:"))
.count(),
"typed Early-Data should replace an existing same-name field"
);
}

#[test]
fn sec_required_document_policy_helper_emits_canonical_metadata() {
let request = capture_request(|base_url| {
Expand Down
11 changes: 11 additions & 0 deletions crates/rttp-protocol/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,17 @@ and size validator shared with `IM`. It reports declared request metadata
only; it does not select a preferred instance manipulation or apply delta
encodings.

## Early-Data

`early_data` parses singleton RFC 8470 `Early-Data` request metadata. The
only accepted value is the case-sensitive `1` signal with optional surrounding
SP or HTAB. `header_value()` emits the canonical `1` value. Missing values,
unsupported values, duplicate fields, comma-lists, forbidden control bytes,
and values over 64 KiB are errors.

This type reports declared request metadata only; it does not enable 0-RTT
transport, decide replay safety, retry, or apply server acceptance policy.

## IM

`im` parses one or more RFC 3229 `IM` field values into an ordered list of
Expand Down
97 changes: 97 additions & 0 deletions crates/rttp-protocol/src/early_data.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
//! Bounded, policy-free `Early-Data` request metadata parsing.
//!
//! This module validates the RFC 8470 request field value only. Callers
//! decide whether and how to handle replay or 0-RTT policy.

use std::error::Error;
use std::fmt;

/// Maximum bytes accepted in an `Early-Data` field value.
pub const MAX_EARLY_DATA_VALUE_BYTES: usize = 64 * 1024;

/// Parsed, bounded RFC 8470 `Early-Data` request metadata.
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
pub struct EarlyData;

impl EarlyData {
pub fn parse(value: impl AsRef<str>) -> Result<Self, EarlyDataParseError> {
Self::parse_values([value.as_ref()])
}

pub fn parse_values<'a, I>(values: I) -> Result<Self, EarlyDataParseError>
where
I: IntoIterator<Item = &'a str>,
{
parse_singleton(values)
}

pub fn header_value(&self) -> &'static str {
"1"
}
}

/// An error returned when `Early-Data` metadata is malformed or exceeds bounds.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct EarlyDataParseError {
message: String,
}

impl EarlyDataParseError {
fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
}
}
}

impl fmt::Display for EarlyDataParseError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(&self.message)
}
}

impl Error for EarlyDataParseError {}

fn parse_singleton<'a, I>(values: I) -> Result<EarlyData, EarlyDataParseError>
where
I: IntoIterator<Item = &'a str>,
{
let mut values = values.into_iter();
let value = values.next().ok_or_else(invalid_value)?;
validate_value(value)?;
let mut has_duplicate = false;
for value in values {
has_duplicate = true;
validate_value(value)?;
}
if has_duplicate {
return Err(EarlyDataParseError::new(
"duplicate Early-Data header fields",
));
}

if value.trim_matches([' ', '\t']) != "1" {
return Err(invalid_value());
}

Ok(EarlyData)
}

fn validate_value(value: &str) -> Result<(), EarlyDataParseError> {
if value.len() > MAX_EARLY_DATA_VALUE_BYTES {
return Err(EarlyDataParseError::new(
"Early-Data header value is too large",
));
}
if value
.bytes()
.any(|byte| byte.is_ascii_control() && byte != b'\t')
{
return Err(EarlyDataParseError::new("invalid Early-Data control byte"));
}
Ok(())
}

fn invalid_value() -> EarlyDataParseError {
EarlyDataParseError::new("invalid Early-Data header value")
}
1 change: 1 addition & 0 deletions crates/rttp-protocol/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ pub mod digest;
pub mod dnt;
pub mod document_policy;
pub mod document_policy_report_only;
pub mod early_data;
pub mod entity_tag;
pub mod expect;
pub mod fetch_metadata;
Expand Down
Loading