fix: validate RequestInit consistently before fetch abort

This commit is contained in:
ldm0
2026-09-27 06:43:22 +08:00
parent 8cbb5b653f
commit 3025bcdaff
13 changed files with 500 additions and 91 deletions
@@ -5534,6 +5534,8 @@ fetch/api/crashtests/request.html
fetch/api/policies/csp-blocked-worker.html
fetch/api/policies/referrer-no-referrer-worker.html
fetch/api/request/multi-globals/url-parsing.html
fetch/api/request/request-error.any.js?moli-wpt-any=dedicatedworker
fetch/api/request/request-error.any.js?moli-wpt-any=window
fetch/api/request/url-encoding.html
fetch/api/response/many-empty-chunks-crash.html
fetch/api/response/multi-globals/url-parsing.html
@@ -0,0 +1,175 @@
async function runRequestInitValidationProbe(scenario, url) {
const errors = [];
const check = (value, label) => { if (!value) errors.push(label); };
const controller = new AbortController();
const reason = {probe: 'already aborted'};
controller.abort(reason);
const fetchError = async (input, init, expected, label, fetchFunction = fetch, receiver = self) => {
let promise;
try { promise = fetchFunction.call(receiver, input, init); }
catch (error) { errors.push(label + ': synchronous throw ' + error); return; }
try { await promise; errors.push(label + ': fulfilled'); }
catch (error) {
check(expected === reason ? error === reason : error instanceof expected,
label + ': wrong rejection ' + error);
}
};
const throwsTypeError = (input, init, label) => {
try { new Request(input, init); errors.push(label + ': accepted'); }
catch (error) { check(error instanceof TypeError, label + ': wrong exception ' + error); }
};
if (scenario === 'invalid') {
const invalid = [
['navigate', {mode: 'navigate'}],
['default mode with only-if-cached', {cache: 'only-if-cached'}],
['cors with only-if-cached', {mode: 'cors', cache: 'only-if-cached'}],
['no-cors with only-if-cached', {mode: 'no-cors', cache: 'only-if-cached'}],
['invalid referrer', {referrer: 'http://:invalid'}],
...['', 'IN VALID', ' GET', 'GET\t', 'a/b', 'a:b', 'é', '\u0100', 'TRACE']
.map(method => ['method ' + JSON.stringify(method), {method}]),
...[['cache', 'reload'], ['referrerPolicy', 'origin'], ['duplex', 'half'],
['mode', 'cors'], ['credentials', 'omit'], ['redirect', 'follow']]
.flatMap(([key, valid]) => ['BAD', null, valid.toUpperCase(), ' ' + valid]
.map(value => [key + ' ' + JSON.stringify(value), {[key]: value}]))
];
for (const [label, init] of invalid) {
throwsTypeError(url, init, label);
await fetchError(url, init, TypeError, label + ' live');
await fetchError(url, {...init, signal: controller.signal}, TypeError, label + ' aborted');
}
for (const credentials of ['user:pass', 'user', ':pass']) {
const credentialURL = new URL(url);
const [username, password = ''] = credentials.split(':');
credentialURL.username = username;
credentialURL.password = password;
throwsTypeError(credentialURL.href, {}, 'URL credentials ' + credentials);
await fetchError(credentialURL.href, {signal: controller.signal}, TypeError,
'URL credentials ' + credentials + ' aborted');
}
} else if (scenario === 'valid') {
const valid = [
[{window: undefined}, {}], [{window: null}, {}],
[{method: null}, {method: 'null'}], [{method: 'pOsT'}, {method: 'POST'}],
[{method: "a!#$%&'*+-.^_`|~"}, {method: "a!#$%&'*+-.^_`|~"}],
[{referrer: ''}, {referrer: ''}],
[{referrer: 'about:client'}, {referrer: 'about:client'}],
[{referrer: new URL('/referrer', url).href}, {referrer: new URL('/referrer', url).href}],
[{referrer: 'https://other.invalid/'}, {referrer: 'about:client'}],
[{duplex: 'half'}, {duplex: 'half'}],
...['default', 'no-store', 'reload', 'no-cache', 'force-cache', 'only-if-cached']
.map(cache => [{cache, mode: 'same-origin'}, {cache, mode: 'same-origin'}]),
...['', 'no-referrer', 'no-referrer-when-downgrade', 'same-origin', 'origin',
'strict-origin', 'origin-when-cross-origin', 'strict-origin-when-cross-origin', 'unsafe-url']
.map(referrerPolicy => [{referrerPolicy}, {referrerPolicy}])
];
for (const [init, expected] of valid) {
const request = new Request(url, init);
for (const [key, value] of Object.entries(expected)) {
check(request[key] === value, JSON.stringify(init) + ': ' + key + '=' + request[key]);
}
await fetchError(url, {...init, signal: controller.signal}, reason, JSON.stringify(init));
}
} else if (scenario === 'inheritance') {
const makeSource = () => new Request(url, {
mode: 'same-origin', cache: 'only-if-cached', method: 'POST', body: 'original body'
});
for (const init of [{mode: 'cors'}, {mode: 'no-cors'}, {mode: 'navigate'},
{referrer: 'http://:invalid'}, {cache: 'BAD'}, {referrerPolicy: null}]) {
const source = makeSource();
throwsTypeError(source, init, 'inherited Request ' + JSON.stringify(init));
check(!source.bodyUsed, 'failed constructor consumed input body');
if (!source.bodyUsed) check(await source.text() === 'original body', 'constructor input body remains readable');
for (const aborted of [false, true]) {
const source = makeSource();
const promise = fetchError(source, {...init, ...(aborted ? {signal: controller.signal} : {})},
TypeError, 'inherited fetch ' + JSON.stringify(init) + ' aborted=' + aborted);
check(!source.bodyUsed, 'failed fetch synchronously consumed input body');
await promise;
check(!source.bodyUsed, 'failed fetch consumed input body');
if (!source.bodyUsed) check(await source.text() === 'original body', 'fetch input body remains readable');
}
}
const cached = new Request(url, {mode: 'same-origin', cache: 'only-if-cached'});
for (const init of [{}, {mode: undefined, cache: undefined}, {cache: 'reload', mode: 'cors'}]) {
const copy = new Request(cached, init);
check(copy.mode === (init.mode || 'same-origin'), 'inherited effective mode');
check(copy.cache === (init.cache || 'only-if-cached'), 'inherited effective cache');
await fetchError(cached, {...init, signal: controller.signal}, reason, 'valid inheritance');
}
} else if (scenario === 'realm') {
if (document.readyState === 'loading') {
await new Promise(resolve => addEventListener('load', resolve, {once: true}));
}
const frame = document.createElement('iframe');
const loaded = new Promise(resolve => frame.onload = resolve);
frame.src = 'about:blank';
document.body.append(frame);
await loaded;
const child = frame.contentWindow;
for (const [fn, receiver, ErrorConstructor, PromiseConstructor] of [
[fetch, child, TypeError, Promise],
[child.fetch, self, child.TypeError, child.Promise]
]) {
for (const init of [{cache: 'BAD'}, {referrerPolicy: null}, {duplex: 'full'}, {redirect: null}]) {
await fetchError(url, {...init, signal: controller.signal}, ErrorConstructor,
'conversion realm ' + JSON.stringify(init), fn, receiver);
}
for (const init of [{mode: 'navigate'}, {cache: 'only-if-cached'},
{referrer: 'http://:invalid'}]) {
await fetchError(url, {...init, signal: controller.signal}, ErrorConstructor,
'construction realm ' + JSON.stringify(init), fn, receiver);
const promise = fn.call(receiver, url, init);
promise.catch(() => {});
check(promise instanceof PromiseConstructor, 'construction rejection promise realm');
}
}
frame.remove();
} else if (scenario === 'getters') {
for (const init of [{mode: 'navigate'}, {mode: 'cors', cache: 'only-if-cached'},
{window: 1}, {referrer: 'http://:invalid'}]) {
for (const member of ['body', 'signal']) {
const marker = {member};
const options = {method: 'POST', ...init, get [member]() { throw marker; }};
try { new Request(url, options); errors.push(member + ' getter accepted'); }
catch (error) { check(error === marker, member + ' getter exception replaced'); }
}
}
const source = new Request(url, {signal: controller.signal});
const copy = new Request(source, {signal: undefined});
check(copy.signal.aborted && copy.signal.reason === reason, 'undefined signal preserves inheritance');
} else if (scenario === 'window') {
// Fetch's RequestInit.window is still specified and covered by WPT, even
// though Chromium 145 ignores the member.
const invalid = [
['window string', {window: 'null'}],
['window false', {window: false}],
['window zero', {window: 0}],
['window object', {window: {toString() { throw new Error('must not coerce window'); }}}],
];
for (const [label, init] of invalid) {
throwsTypeError(url, init, label);
await fetchError(url, init, TypeError, label + ' live');
await fetchError(url, {...init, signal: controller.signal}, TypeError, label + ' aborted');
const source = new Request(url, {method: 'POST', body: 'original body'});
throwsTypeError(source, init, label + ' inherited');
check(!source.bodyUsed, 'window rejection consumed constructor input');
if (!source.bodyUsed) {
await fetchError(source, {...init, signal: controller.signal}, TypeError, label + ' inherited aborted');
check(!source.bodyUsed, 'window rejection consumed fetch input');
if (!source.bodyUsed) check(await source.text() === 'original body', 'window input remains readable');
}
}
let reads = 0;
const init = {get window() { ++reads; return null; }};
new Request(url, init);
check(reads === 1, 'Request reads window once');
reads = 0;
await fetchError(url, {get window() { ++reads; return null; }, signal: controller.signal},
reason, 'fetch reads window');
check(reads === 1, 'fetch reads window once');
} else {
throw new Error('unknown scenario ' + scenario);
}
return {errors};
}
+2
View File
@@ -22,6 +22,8 @@ mod abort_signal;
mod callback_cleanup;
#[path = "web_apis/event_dispatch.rs"]
mod event_dispatch;
#[path = "web_apis/request_init.rs"]
mod request_init;
fn diagnostic_global<'a>(
page: &'a moli_core::page::Page,
+57
View File
@@ -0,0 +1,57 @@
use super::*;
async fn assert_request_init_validation(scenario: &str, targets: &[&str]) -> Result<()> {
let server = FixtureServer::spawn().await?;
let browser = Browser::new(AppConfig::default())?;
let source = format!(
"{}\nrunRequestInitValidationProbe({}, {}).then(finish, error => finish({{error: String(error)}}));",
include_str!("../fixtures/runtime/request_init_validation.js"),
serde_json::to_string(scenario)?,
serde_json::to_string(&server.url("/compat/child-dynamic-markup-document"))?,
);
for target in targets {
let observed = tokio::time::timeout(
Duration::from_secs(20),
super::event_dispatch::run_probe(&browser, &server, target, &source),
)
.await??;
assert_eq!(
observed,
serde_json::json!({"errors": []}),
"{scenario}/{target}"
);
}
server.shutdown().await;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn request_and_fetch_reject_invalid_init_before_abort() -> Result<()> {
assert_request_init_validation("invalid", &["window", "child", "worker"]).await
}
#[tokio::test(flavor = "multi_thread")]
async fn request_and_fetch_accept_valid_init_and_preserve_abort_reason() -> Result<()> {
assert_request_init_validation("valid", &["window", "child", "worker"]).await
}
#[tokio::test(flavor = "multi_thread")]
async fn request_and_fetch_validate_inherited_modes_without_consuming_invalid_input() -> Result<()>
{
assert_request_init_validation("inheritance", &["window", "child", "worker"]).await
}
#[tokio::test(flavor = "multi_thread")]
async fn fetch_init_errors_and_rejected_promises_use_the_function_realm() -> Result<()> {
assert_request_init_validation("realm", &["window", "child"]).await
}
#[tokio::test(flavor = "multi_thread")]
async fn request_init_window_accepts_only_null_without_consuming_invalid_input() -> Result<()> {
assert_request_init_validation("window", &["window", "child", "worker"]).await
}
#[tokio::test(flavor = "multi_thread")]
async fn request_init_getter_exceptions_precede_construction_validation() -> Result<()> {
assert_request_init_validation("getters", &["window", "child", "worker"]).await
}
+1 -1
View File
@@ -131,7 +131,7 @@ pub(crate) use self::request::{FetchArgumentError, RequestUrlError, convert_fetc
pub(crate) use self::request::{
mark_request_input_body_used_for_fetch, request_input_snapshot,
try_resolve_request_constructor_url, try_resolve_request_constructor_url_for_base,
try_resolve_request_constructor_url_for_child,
try_resolve_request_constructor_url_for_child, validate_request_url_credentials,
};
pub(crate) use self::request::{
parse_fetch_init, parse_request_redirect_mode_label, request_object_credentials_mode,
@@ -7,9 +7,9 @@ use self::paths::{
reject_csp_fetch, reject_offline_fetch, reject_url_policy_fetch, resolve_local_fetch,
spawn_network_fetch,
};
use self::request::prepare_window_fetch_request;
use self::request::{PreparedWindowFetchRequest, prepare_window_fetch_request};
use self::service_worker::dispatch_service_worker_fetch;
use super::input::{ParsedWindowFetchInput, parse_window_fetch_input};
use super::input::parse_window_fetch_input;
use super::promise::{make_rejected_promise, make_rejected_promise_with_value};
use super::*;
use crate::native_bridge::abort::abort_error_value;
@@ -181,25 +181,38 @@ pub(crate) fn window_fetch_callback<'s>(
);
return;
};
if let Some(request_body_owner) = parsed.request_body_owner.take() {
let request_body_owner = v8::Local::new(scope, request_body_owner);
mark_request_input_body_used_for_fetch(scope, request_body_owner);
}
let fetch_context = crate::native_bridge::WindowFetchContext::from_realm(binding);
let relevant_context = {
let context = fetch_context.script_realm().context(scope);
v8::Global::new(scope, context)
};
let relevant_context = v8::Local::new(scope, &relevant_context);
let request_body_owner = parsed.request_body_owner.take();
let prepared = {
let scope = &mut v8::ContextScope::new(scope, relevant_context);
prepare_window_fetch_request(scope, parsed, fetch_context, unsafe { &*host_ptr })
};
let prepared = match prepared {
Ok(prepared) => prepared,
Err(error) => {
// Request construction uses the receiver's settings, but Blink's
// binding rejects construction exceptions in the function realm.
rv.set(make_rejected_promise(scope, &error.to_string()).into());
return;
}
};
let scope = &mut v8::ContextScope::new(scope, relevant_context);
window_fetch_callback_in_relevant_realm(scope, parsed, signal, fetch_context, rv);
if let Some(request_body_owner) = request_body_owner {
let request_body_owner = v8::Local::new(scope, request_body_owner);
mark_request_input_body_used_for_fetch(scope, request_body_owner);
}
window_fetch_callback_in_relevant_realm(scope, prepared, signal, rv);
}
fn window_fetch_callback_in_relevant_realm<'s>(
scope: &mut v8::PinScope<'s, '_>,
parsed: ParsedWindowFetchInput,
prepared: PreparedWindowFetchRequest,
signal: Option<v8::Global<v8::Object>>,
fetch_context: crate::native_bridge::WindowFetchContext,
mut rv: v8::ReturnValue<'s, v8::Value>,
) {
let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else {
@@ -209,13 +222,6 @@ fn window_fetch_callback_in_relevant_realm<'s>(
let host = unsafe { &mut *host_ptr };
let signal = signal.as_ref().map(|signal| v8::Local::new(scope, signal));
let prepared = match prepare_window_fetch_request(scope, parsed, fetch_context, host) {
Ok(prepared) => prepared,
Err(error) => {
rv.set(make_rejected_promise(scope, &error.to_string()).into());
return;
}
};
host.break_on_dom_debugger_xhr_or_fetch_network_request(prepared.resolved_url.as_str());
if let Some(signal) = signal
&& host.abort_signal_aborted(scope, signal)
@@ -11,6 +11,13 @@ pub(super) enum FetchPrepareError {
PolicyContextUnavailable,
ReportContextUnavailable,
Url(ResolveContextUrlError),
WebIdl(crate::webidl::WebIdlError),
}
impl From<crate::webidl::WebIdlError> for FetchPrepareError {
fn from(error: crate::webidl::WebIdlError) -> Self {
Self::WebIdl(error)
}
}
impl From<ResolveContextUrlError> for FetchPrepareError {
@@ -38,6 +45,7 @@ impl fmt::Display for FetchPrepareError {
f.write_str("fetch: document report context is unavailable")
}
Self::Url(error) => error.fmt(f),
Self::WebIdl(error) => error.fmt(f),
}
}
}
@@ -46,6 +54,7 @@ impl std::error::Error for FetchPrepareError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Url(error) => Some(error),
Self::WebIdl(error) => Some(error),
_ => None,
}
}
@@ -125,6 +134,11 @@ pub(super) fn prepare_window_fetch_request<'s>(
let request_headers =
merge_byte_string_request_headers(host.extra_http_headers(), &request_headers);
let resolved_url = resolve_context_url(&base_url, &parsed.url, None)?;
validate_request_url_credentials(&resolved_url)?;
let referrer = parsed
.init_validation
.validate(scope, parsed.request_mode.as_ref(), &parsed.cache)?
.unwrap_or(parsed.referrer);
Ok(PreparedWindowFetchRequest {
frame_id,
@@ -147,7 +161,7 @@ pub(super) fn prepare_window_fetch_request<'s>(
redirect_mode: parsed.redirect_mode,
priority: parsed.priority,
cache: parsed.cache,
referrer: parsed.referrer,
referrer,
referrer_policy: parsed.referrer_policy,
integrity: parsed.integrity,
keepalive: parsed.keepalive,
@@ -17,6 +17,7 @@ pub(super) struct ParsedWindowFetchInput {
pub(super) integrity: String,
pub(super) keepalive: bool,
pub(super) request_body_owner: Option<v8::Global<v8::Object>>,
pub(super) init_validation: super::super::request::RequestInitValidation,
}
pub(super) fn parse_window_fetch_input<'s>(
@@ -56,7 +57,8 @@ pub(super) fn parse_window_fetch_input<'s>(
}
let inherited_credentials = request_object_credentials_mode(scope, req_obj)?;
let request_mode = init
.request_mode
.validation
.mode
.or_else(|| moli_fetch::RequestMode::from_str(&inherited.mode).ok())
.unwrap_or(moli_fetch::RequestMode::Cors);
validate_no_cors_method(request_mode, &method)?;
@@ -85,16 +87,20 @@ pub(super) fn parse_window_fetch_input<'s>(
redirect_mode,
priority,
cache: init.cache.unwrap_or(inherited.cache),
referrer: init.referrer.unwrap_or(inherited.referrer),
referrer: inherited.referrer,
referrer_policy: init.referrer_policy.unwrap_or(inherited.referrer_policy),
integrity: init.integrity.unwrap_or(inherited.integrity),
keepalive: init.keepalive.unwrap_or(inherited.keepalive),
request_body_owner,
init_validation: init.validation,
})
} else {
let url = fetch_request_info_url(scope, arg0)?;
let init = parse_fetch_init(scope, args, 1)?;
let request_mode = init.request_mode.unwrap_or(moli_fetch::RequestMode::Cors);
let request_mode = init
.validation
.mode
.unwrap_or(moli_fetch::RequestMode::Cors);
validate_no_cors_method(request_mode, &init.method)?;
let headers = if request_mode == moli_fetch::RequestMode::NoCors {
filter_headers_for_guard(&init.headers, HeadersGuard::RequestNoCors)
@@ -117,11 +123,12 @@ pub(super) fn parse_window_fetch_input<'s>(
redirect_mode,
priority: init.priority,
cache: init.cache.unwrap_or_else(|| "default".to_owned()),
referrer: init.referrer.unwrap_or_else(|| "about:client".to_owned()),
referrer: "about:client".to_owned(),
referrer_policy: init.referrer_policy.unwrap_or_default(),
integrity: init.integrity.unwrap_or_default(),
keepalive: init.keepalive.unwrap_or(false),
request_body_owner: None,
init_validation: init.validation,
})
}
}
+4 -2
View File
@@ -12,11 +12,13 @@ pub(crate) use self::bindings::request_constructor_callback;
pub(crate) use self::error::{FetchArgumentError, RequestUrlError};
pub(crate) use self::init::convert_fetch_arguments;
pub(in crate::network_host) use self::init::request_credentials_mode_label;
pub(crate) use self::init::{parse_fetch_init, request_object_credentials_mode};
pub(crate) use self::init::{
RequestInitValidation, parse_fetch_init, request_object_credentials_mode,
};
pub(crate) use self::init::{parse_request_redirect_mode_label, request_redirect_mode_label};
pub(in crate::network_host) use self::input::normalize_request_method;
pub(crate) use self::input::{
mark_request_input_body_used_for_fetch, request_input_snapshot,
try_resolve_request_constructor_url, try_resolve_request_constructor_url_for_base,
try_resolve_request_constructor_url_for_child,
try_resolve_request_constructor_url_for_child, validate_request_url_credentials,
};
@@ -1,14 +1,14 @@
use super::super::init::RequestInitMembers;
use super::super::input::{
normalize_request_method, normalize_request_referrer, request_body_already_used_error,
normalize_fetch_request_method, request_body_already_used_error,
request_headers_guard_for_mode, request_input_snapshot_for_constructor, request_input_url,
request_method_allows_body, request_signal_snapshot_from_value,
try_resolve_request_constructor_url_for_scope,
try_resolve_request_constructor_url_for_scope, validate_request_url_credentials,
};
use super::super::*;
use crate::web_api_interfaces;
use crate::webidl;
use moli_fetch::{RequestMode, RequestRedirectMode};
use moli_fetch::RequestRedirectMode;
pub(super) struct RequestConstructionState {
pub(super) url_resolved: String,
@@ -51,6 +51,9 @@ pub(super) fn request_initial_state<'s>(
.map_err(|_| {
webidl::WebIdlError::custom_message("Failed to construct 'Request': invalid URL")
})?;
if let Ok(url) = url::Url::parse(&url_resolved) {
validate_request_url_credentials(&url)?;
}
let method = inherited
.as_ref()
.map(|snapshot| snapshot.method.clone())
@@ -134,10 +137,17 @@ pub(super) fn apply_request_init_overrides<'s>(
state: &mut RequestConstructionState,
) -> Result<(), webidl::WebIdlError> {
let parsed = webidl::parse_dictionary_object::<RequestInitMembers>(scope, init)?;
let validation = parsed.validation();
if let Some(mode) = validation.mode {
state.mode = mode.as_ref().to_owned();
}
if let Some(cache) = parsed.cache {
state.cache = cache.0.to_owned();
}
let method_overridden = parsed.method.is_some();
if let Some(method) = parsed.method {
state.method = normalize_request_method(&method)?;
state.method = normalize_fetch_request_method(&method)?;
}
let init_body_value = webidl::property_result(
scope,
@@ -168,20 +178,19 @@ pub(super) fn apply_request_init_overrides<'s>(
if let Some(headers) = parsed.headers {
state.headers = headers;
}
let signal_key = v8str(scope, "signal");
if init.has(scope, signal_key.into()).unwrap_or(false) {
let signal = init
.get(scope, signal_key.into())
.unwrap_or_else(|| v8::undefined(scope).into());
if let Some(signal) = webidl::property_result(
scope,
init,
"signal",
webidl::Context::member("RequestInit", "signal"),
)?
.filter(|value| !value.is_undefined())
{
state.signal = request_signal_snapshot_from_value(scope, signal)?;
}
if let Some(mode) = parsed.mode.map(|value| value.0) {
if mode == RequestMode::Navigate {
return Err(webidl::WebIdlError::custom_message(
"Cannot construct a Request with a RequestInit whose mode member is \"navigate\".",
));
}
state.mode = mode.as_ref().to_owned();
if let Some(referrer) = validation.validate(scope, &state.mode, &state.cache)?
{
state.referrer = referrer;
}
if state.mode == "no-cors" && !moli_fetch::is_cors_safelisted_method(&state.method) {
return Err(webidl::WebIdlError::custom_message(
@@ -191,17 +200,11 @@ pub(super) fn apply_request_init_overrides<'s>(
if let Some(credentials_mode) = parsed.credentials_mode.map(|value| value.0) {
state.credentials = request_credentials_mode_label(credentials_mode).to_owned();
}
if let Some(cache) = parsed.cache {
state.cache = cache;
}
if let Some(redirect) = parsed.redirect {
state.redirect_mode = redirect.0;
}
if let Some(referrer) = parsed.referrer {
state.referrer = normalize_request_referrer(scope, &referrer);
}
if let Some(referrer_policy) = parsed.referrer_policy {
state.referrer_policy = referrer_policy;
state.referrer_policy = referrer_policy.0.to_owned();
}
if let Some(integrity) = parsed.integrity {
state.integrity = integrity;
@@ -209,8 +212,8 @@ pub(super) fn apply_request_init_overrides<'s>(
if let Some(priority) = parsed.priority {
state.priority = priority.0;
}
if let Some(duplex) = parsed.duplex {
state.duplex = duplex;
if parsed.duplex.is_some() {
state.duplex = "half".to_owned();
}
if parsed.keepalive == Some(true) && init_body_is_readable_stream {
return Err(webidl::WebIdlError::custom_message(
+121 -24
View File
@@ -1,4 +1,4 @@
use super::input::{normalize_request_method, normalize_request_referrer};
use super::input::{normalize_fetch_request_method, normalize_request_referrer};
use super::*;
use crate::webidl;
use moli_fetch::{FetchPriorityHint, RequestCredentialsMode, RequestMode, RequestRedirectMode};
@@ -40,12 +40,11 @@ pub(crate) struct ParsedFetchInit {
pub(crate) body_content_type: Option<String>,
pub(crate) headers: Vec<(String, String)>,
pub(crate) headers_present: bool,
pub(crate) request_mode: Option<RequestMode>,
pub(crate) validation: RequestInitValidation,
pub(crate) credentials_mode: Option<RequestCredentialsMode>,
pub(crate) redirect_mode: Option<RequestRedirectMode>,
pub(crate) priority: Option<FetchPriorityHint>,
pub(crate) cache: Option<String>,
pub(crate) referrer: Option<String>,
pub(crate) referrer_policy: Option<String>,
pub(crate) integrity: Option<String>,
pub(crate) keepalive: Option<bool>,
@@ -61,12 +60,11 @@ impl Default for ParsedFetchInit {
body_content_type: None,
headers: Vec::new(),
headers_present: false,
request_mode: None,
validation: RequestInitValidation::default(),
credentials_mode: None,
redirect_mode: None,
priority: None,
cache: None,
referrer: None,
referrer_policy: None,
integrity: None,
keepalive: None,
@@ -74,6 +72,42 @@ impl Default for ParsedFetchInit {
}
}
#[derive(Debug, Clone, Default)]
pub(crate) struct RequestInitValidation {
pub(crate) mode: Option<RequestMode>,
window_non_null: bool,
referrer: Option<String>,
}
impl RequestInitValidation {
// Run construction checks after dictionary conversion, using the effective
// inherited/overridden values and the request's relevant realm. Fetch must
// complete these checks before consuming an input body or observing abort.
pub(crate) fn validate(
&self,
scope: &mut v8::PinScope<'_, '_>,
mode: &str,
cache: &str,
) -> Result<Option<String>, webidl::WebIdlError> {
if self.window_non_null {
return Err(webidl::WebIdlError::custom_message("RequestInit's window member must be null"));
}
let referrer = self
.referrer
.as_deref()
.map(|value| normalize_request_referrer(scope, value))
.transpose()
.map_err(|_| webidl::WebIdlError::custom_message("Request referrer is not a valid URL"))?;
if self.mode == Some(RequestMode::Navigate) {
return Err(webidl::WebIdlError::custom_message("Cannot construct a Request with mode navigate"));
}
if cache == "only-if-cached" && mode != "same-origin" {
return Err(webidl::WebIdlError::custom_message("Request cache only-if-cached requires mode same-origin"));
}
Ok(referrer)
}
}
#[derive(Clone, Copy, webidl::WebIdlEnum)]
#[webidl(name = "RequestCredentials", parse_with = parse_request_credentials_mode_webidl)]
pub(super) struct RequestCredentialsModeWebIdl(pub(super) RequestCredentialsMode);
@@ -90,25 +124,40 @@ pub(super) struct RequestRedirectModeWebIdl(pub(super) RequestRedirectMode);
#[webidl(name = "RequestPriority", parse_with = parse_request_priority_webidl)]
pub(super) struct RequestPriorityWebIdl(pub(super) FetchPriorityHint);
#[derive(Clone, Copy, webidl::WebIdlEnum)]
#[webidl(name = "RequestCache", parse_with = parse_request_cache_webidl)]
pub(super) struct RequestCacheWebIdl(pub(super) &'static str);
#[derive(Clone, Copy, webidl::WebIdlEnum)]
#[webidl(name = "ReferrerPolicy", parse_with = parse_referrer_policy_webidl)]
pub(super) struct ReferrerPolicyWebIdl(pub(super) &'static str);
#[derive(Clone, Copy, webidl::WebIdlEnum)]
#[webidl(name = "RequestDuplex")]
pub(super) enum RequestDuplexWebIdl {
#[webidl(token = "half")]
Half,
}
#[derive(webidl::WebIdlDictionary)]
#[webidl(prefix = "RequestInit")]
pub(super) struct RequestInitMembers {
#[webidl(legacy_nullish)]
#[webidl(converter = "byte_string")]
pub(super) method: Option<String>,
#[webidl(legacy_nullish)]
pub(super) cache: Option<String>,
#[webidl(converter = "enum")]
pub(super) cache: Option<RequestCacheWebIdl>,
#[webidl(converter = "enum")]
pub(super) mode: Option<RequestModeWebIdl>,
#[webidl(legacy_nullish, converter = "enum")]
#[webidl(converter = "enum")]
pub(super) redirect: Option<RequestRedirectModeWebIdl>,
#[webidl(legacy_nullish)]
#[webidl(converter = "usv_string")]
pub(super) referrer: Option<String>,
#[webidl(legacy_nullish)]
pub(super) referrer_policy: Option<String>,
#[webidl(converter = "enum")]
pub(super) referrer_policy: Option<ReferrerPolicyWebIdl>,
#[webidl(legacy_nullish)]
pub(super) integrity: Option<String>,
#[webidl(legacy_nullish)]
pub(super) duplex: Option<String>,
#[webidl(converter = "enum")]
pub(super) duplex: Option<RequestDuplexWebIdl>,
#[webidl(with = request_init_headers_member)]
pub(super) headers: Option<Vec<(String, String)>>,
#[webidl(name = "credentials", converter = "enum")]
@@ -116,6 +165,32 @@ pub(super) struct RequestInitMembers {
#[webidl(converter = "enum")]
pub(super) priority: Option<RequestPriorityWebIdl>,
pub(super) keepalive: Option<bool>,
#[webidl(name = "window", with = request_init_window_member)]
window_non_null: bool,
}
impl RequestInitMembers {
pub(super) fn validation(&self) -> RequestInitValidation {
RequestInitValidation {
mode: self.mode.map(|value| value.0),
window_non_null: self.window_non_null,
referrer: self.referrer.clone(),
}
}
}
fn request_init_window_member<'s>(
scope: &mut v8::PinScope<'s, '_>,
object: v8::Local<'s, v8::Object>,
_key: &str,
) -> Result<bool, webidl::WebIdlError> {
webidl::property_result(
scope,
object,
"window",
webidl::Context::member("RequestInit", "window"),
)
.map(|value| value.is_some_and(|value| !value.is_null_or_undefined()))
}
fn request_init_headers_member<'s>(
@@ -148,6 +223,7 @@ pub(crate) fn parse_fetch_init<'s>(
};
let init = webidl::parse_dictionary_object::<RequestInitMembers>(scope, init_object)?;
let validation = init.validation();
let priority = init.priority.map(|value| value.0);
let method_present = init_object
.has(scope, v8str(scope, "method").into())
@@ -156,7 +232,7 @@ pub(crate) fn parse_fetch_init<'s>(
})?;
let method = init
.method
.map(|s| normalize_request_method(&s))
.map(|s| normalize_fetch_request_method(&s))
.transpose()?
.unwrap_or_else(|| "GET".to_owned());
@@ -197,13 +273,8 @@ pub(crate) fn parse_fetch_init<'s>(
let mut extra_headers = init.headers.unwrap_or_default();
append_default_body_content_type(&mut extra_headers, body_content_type.as_deref());
let request_mode = init.mode.map(|value| value.0);
let credentials_mode = init.credentials_mode.map(|value| value.0);
let redirect_mode = init.redirect.map(|value| value.0);
let referrer = init
.referrer
.map(|referrer| normalize_request_referrer(scope, &referrer));
Ok(ParsedFetchInit {
method,
method_present,
@@ -212,13 +283,12 @@ pub(crate) fn parse_fetch_init<'s>(
body_content_type,
headers: extra_headers,
headers_present,
request_mode,
validation,
credentials_mode,
redirect_mode,
priority,
cache: init.cache,
referrer,
referrer_policy: init.referrer_policy,
cache: init.cache.map(|value| value.0.to_owned()),
referrer_policy: init.referrer_policy.map(|value| value.0.to_owned()),
integrity: init.integrity,
keepalive: init.keepalive,
})
@@ -272,6 +342,33 @@ fn parse_request_priority_webidl(value: &str) -> Option<RequestPriorityWebIdl> {
.map(RequestPriorityWebIdl)
}
fn parse_request_cache_webidl(value: &str) -> Option<RequestCacheWebIdl> {
Some(RequestCacheWebIdl(match value {
"default" => "default",
"no-store" => "no-store",
"reload" => "reload",
"no-cache" => "no-cache",
"force-cache" => "force-cache",
"only-if-cached" => "only-if-cached",
_ => return None,
}))
}
fn parse_referrer_policy_webidl(value: &str) -> Option<ReferrerPolicyWebIdl> {
Some(ReferrerPolicyWebIdl(match value {
"" => "",
"no-referrer" => "no-referrer",
"no-referrer-when-downgrade" => "no-referrer-when-downgrade",
"same-origin" => "same-origin",
"origin" => "origin",
"strict-origin" => "strict-origin",
"origin-when-cross-origin" => "origin-when-cross-origin",
"strict-origin-when-cross-origin" => "strict-origin-when-cross-origin",
"unsafe-url" => "unsafe-url",
_ => return None,
}))
}
#[cfg(test)]
mod tests {
use super::*;
@@ -3,6 +3,34 @@ use super::*;
use crate::web_api_interfaces;
use crate::webidl;
pub(super) fn normalize_fetch_request_method(method: &str) -> Result<String, webidl::WebIdlError> {
if method.is_empty()
|| !method.bytes().all(|byte| {
byte.is_ascii_alphanumeric()
|| matches!(
byte,
b'!' | b'#'
| b'$'
| b'%'
| b'&'
| b'\''
| b'*'
| b'+'
| b'-'
| b'.'
| b'^'
| b'_'
| b'`'
| b'|'
| b'~'
)
})
{
return Err(webidl::WebIdlError::custom_message("Request method is not a valid HTTP token"));
}
normalize_request_method(method)
}
pub(in crate::network_host) fn normalize_request_method(
method: &str,
) -> Result<String, webidl::WebIdlError> {
@@ -108,7 +136,7 @@ fn request_input_snapshot_inner<'s>(
return Ok(None);
};
let method = defined_object_string_property(scope, object, "method")
.map(|value| normalize_request_method(&value))
.map(|value| normalize_fetch_request_method(&value))
.transpose()?
.unwrap_or_else(|| "GET".to_owned());
let mode =
@@ -169,7 +197,7 @@ fn request_input_snapshot_from_private_slots<'s>(
) -> Result<RequestInputSnapshot, webidl::WebIdlError> {
let url = request_slot_string(scope, object, REQUEST_URL_SLOT).unwrap_or_default();
let method = request_slot_string(scope, object, REQUEST_METHOD_SLOT)
.map(|value| normalize_request_method(&value))
.map(|value| normalize_fetch_request_method(&value))
.transpose()?
.unwrap_or_else(|| "GET".to_owned());
let mode =
@@ -306,11 +334,11 @@ fn object_bool_property(
.map(|value| value.boolean_value(scope))
}
pub(super) fn resolve_request_constructor_url(
scope: &mut v8::PinScope<'_, '_>,
input: &str,
) -> String {
try_resolve_request_constructor_url(scope, input).unwrap_or_else(|_| input.to_owned())
pub(crate) fn validate_request_url_credentials(url: &url::Url) -> Result<(), webidl::WebIdlError> {
if !url.username().is_empty() || url.password().is_some_and(|value| !value.is_empty()) {
return Err(webidl::WebIdlError::custom_message("Request URL must not include credentials"));
}
Ok(())
}
pub(crate) fn try_resolve_request_constructor_url(
@@ -378,19 +406,22 @@ pub(crate) fn try_resolve_request_constructor_url_for_scope(
}
}
pub(super) fn normalize_request_referrer(scope: &mut v8::PinScope<'_, '_>, input: &str) -> String {
pub(super) fn normalize_request_referrer(
scope: &mut v8::PinScope<'_, '_>,
input: &str,
) -> Result<String, RequestUrlError> {
if input.is_empty() || input == "about:client" {
return input.to_owned();
return Ok(input.to_owned());
}
let resolved = resolve_request_constructor_url(scope, input);
let resolved = try_resolve_request_constructor_url(scope, input)?;
let Some(context_url) = current_request_context_url(scope) else {
return resolved;
return Ok(resolved);
};
if moli_url::parsed_same_origin(&resolved, &context_url) {
resolved
Ok(resolved)
} else {
"about:client".to_owned()
Ok("about:client".to_owned())
}
}
@@ -1873,7 +1873,8 @@ pub(in crate::worker) fn resolve_worker_fetch_input<'s>(
credentials_mode,
redirect_mode,
priority,
metadata,
mut metadata,
init_validation,
) = if let Some(inherited) = inherited {
let req_obj = v8::Local::<v8::Object>::try_from(arg0).expect("request-like object");
request_like = Some(req_obj);
@@ -1900,7 +1901,8 @@ pub(in crate::worker) fn resolve_worker_fetch_input<'s>(
}
let inherited_credentials = request_object_credentials_mode(scope, req_obj)?;
let request_mode = init
.request_mode
.validation
.mode
.or_else(|| moli_fetch::RequestMode::from_str(&inherited.mode).ok())
.unwrap_or(moli_fetch::RequestMode::Cors);
validate_worker_no_cors_method(request_mode, &method)?;
@@ -1923,7 +1925,7 @@ pub(in crate::worker) fn resolve_worker_fetch_input<'s>(
});
let metadata = ServiceWorkerFetchRequestMetadata {
cache: init.cache.unwrap_or(inherited.cache),
referrer: init.referrer.unwrap_or(inherited.referrer),
referrer: inherited.referrer,
referrer_policy: init.referrer_policy.unwrap_or(inherited.referrer_policy),
integrity: init.integrity.unwrap_or(inherited.integrity),
keepalive: init.keepalive.unwrap_or(inherited.keepalive),
@@ -1938,6 +1940,7 @@ pub(in crate::worker) fn resolve_worker_fetch_input<'s>(
redirect_mode,
priority,
metadata,
init.validation,
)
} else {
let url = webidl::convert::<webidl::UsvString>(
@@ -1947,7 +1950,10 @@ pub(in crate::worker) fn resolve_worker_fetch_input<'s>(
)
.map(String::from)?;
let init = parse_fetch_init(scope, args, 1)?;
let request_mode = init.request_mode.unwrap_or(moli_fetch::RequestMode::Cors);
let request_mode = init
.validation
.mode
.unwrap_or(moli_fetch::RequestMode::Cors);
validate_worker_no_cors_method(request_mode, &init.method)?;
let headers = if request_mode == moli_fetch::RequestMode::NoCors {
filter_headers_for_guard(&init.headers, HeadersGuard::RequestNoCors)
@@ -1960,7 +1966,7 @@ pub(in crate::worker) fn resolve_worker_fetch_input<'s>(
let redirect_mode = init.redirect_mode.unwrap_or(RequestRedirectMode::Follow);
let metadata = ServiceWorkerFetchRequestMetadata {
cache: init.cache.unwrap_or_else(|| "default".to_owned()),
referrer: init.referrer.unwrap_or_else(|| "about:client".to_owned()),
referrer: "about:client".to_owned(),
referrer_policy: init.referrer_policy.unwrap_or_default(),
integrity: init.integrity.unwrap_or_default(),
keepalive: init.keepalive.unwrap_or(false),
@@ -1975,9 +1981,16 @@ pub(in crate::worker) fn resolve_worker_fetch_input<'s>(
redirect_mode,
init.priority,
metadata,
init.validation,
)
};
let resolved_url = resolve_context_url(base_url, &url_input, None)?;
crate::network_host::validate_request_url_credentials(&resolved_url)?;
if let Some(referrer) =
init_validation.validate(scope, request_mode.as_ref(), &metadata.cache)?
{
metadata.referrer = referrer;
}
let signal = worker_fetch_signal_option(scope, args, request_like)?;
if consumes_request_body && let Some(request_like) = request_like {
crate::network_host::mark_request_input_body_used_for_fetch(scope, request_like);