mirror of
https://github.com/lexmount/moli.git
synced 2026-10-07 08:01:00 +00:00
fix(fetch): preserve redirect mode when filtering responses
This commit is contained in:
@@ -478,6 +478,7 @@ impl NativeModuleGraphFetchRequest {
|
||||
&initiator_url,
|
||||
&head,
|
||||
request_mode,
|
||||
moli_fetch::RequestRedirectMode::Follow,
|
||||
).is_none();
|
||||
if !crate::subresource_integrity::response_matches_subresource_integrity_metadata(
|
||||
&body_bytes,
|
||||
|
||||
@@ -554,6 +554,7 @@ impl JsContextHost {
|
||||
credentials_mode: moli_fetch::RequestCredentialsMode,
|
||||
request_mode: moli_fetch::RequestMode,
|
||||
request_origin: moli_url::WebOrigin,
|
||||
redirect_mode: moli_fetch::RequestRedirectMode,
|
||||
network_partition_key: Option<String>,
|
||||
policy_context: crate::types::SubresourcePolicyContext,
|
||||
blob_url_entry: Option<crate::network_host::CapturedBlobUrl>,
|
||||
@@ -589,6 +590,7 @@ impl JsContextHost {
|
||||
keepalive,
|
||||
connect_policy,
|
||||
csp_report_context,
|
||||
redirect_mode,
|
||||
),
|
||||
),
|
||||
deferred_request_started: false,
|
||||
@@ -832,6 +834,7 @@ impl JsContextHost {
|
||||
credentials_mode: moli_fetch::RequestCredentialsMode,
|
||||
request_mode: moli_fetch::RequestMode,
|
||||
request_origin: moli_url::WebOrigin,
|
||||
redirect_mode: moli_fetch::RequestRedirectMode,
|
||||
network_partition_key: Option<String>,
|
||||
policy_context: crate::types::SubresourcePolicyContext,
|
||||
mut info: PendingSubresourceFetchInfo,
|
||||
@@ -870,6 +873,7 @@ impl JsContextHost {
|
||||
keepalive,
|
||||
connect_policy,
|
||||
csp_report_context,
|
||||
redirect_mode,
|
||||
),
|
||||
),
|
||||
deferred_request_started: defer_request_started,
|
||||
|
||||
@@ -320,6 +320,7 @@ fn window_fetch_callback_in_relevant_realm<'s>(
|
||||
FetchResponseRequest {
|
||||
method: &prepared.method,
|
||||
mode: prepared.request_mode,
|
||||
redirect_mode: prepared.redirect_mode,
|
||||
},
|
||||
response,
|
||||
);
|
||||
|
||||
@@ -28,6 +28,7 @@ pub(super) fn record_intercepted_fetch(
|
||||
prepared.credentials_mode,
|
||||
prepared.request_mode,
|
||||
prepared.request_origin.clone(),
|
||||
prepared.redirect_mode,
|
||||
prepared.network_partition_key,
|
||||
prepared.policy_context,
|
||||
prepared.blob_url_entry,
|
||||
@@ -266,6 +267,7 @@ pub(super) fn spawn_network_fetch(
|
||||
prepared.credentials_mode,
|
||||
prepared.request_mode,
|
||||
prepared.request_origin.clone(),
|
||||
prepared.redirect_mode,
|
||||
prepared.network_partition_key.clone(),
|
||||
prepared.policy_context,
|
||||
PendingSubresourceFetchInfo {
|
||||
|
||||
@@ -64,6 +64,7 @@ pub(super) fn dispatch_service_worker_fetch(
|
||||
prepared.credentials_mode,
|
||||
prepared.request_mode,
|
||||
prepared.request_origin.clone(),
|
||||
prepared.redirect_mode,
|
||||
prepared.network_partition_key.clone(),
|
||||
prepared.policy_context,
|
||||
PendingSubresourceFetchInfo {
|
||||
|
||||
@@ -5,7 +5,7 @@ use super::super::fetch_surface::{
|
||||
};
|
||||
use super::*;
|
||||
use crate::types::NetworkBodySourceId;
|
||||
use moli_fetch::RequestMode;
|
||||
use moli_fetch::{RequestMode, RequestRedirectMode};
|
||||
use moli_url::WebOrigin;
|
||||
use moli_webapi_declare::WebApiObject;
|
||||
|
||||
@@ -13,6 +13,7 @@ use moli_webapi_declare::WebApiObject;
|
||||
pub(crate) struct FetchResponseRequest<'a> {
|
||||
pub(crate) method: &'a str,
|
||||
pub(crate) mode: RequestMode,
|
||||
pub(crate) redirect_mode: RequestRedirectMode,
|
||||
}
|
||||
|
||||
fn is_redirect_status(status: u16) -> bool {
|
||||
@@ -83,10 +84,10 @@ struct FetchResponseBodyDeclaration<'scope> {
|
||||
fn response_filter(
|
||||
request_origin: impl Into<WebOrigin>,
|
||||
head: &moli_fetch::ResponseHead,
|
||||
request_mode: RequestMode,
|
||||
request: FetchResponseRequest<'_>,
|
||||
) -> FetchResponseFilter {
|
||||
let request_origin = request_origin.into();
|
||||
network_response_filter(&request_origin, head, request_mode)
|
||||
network_response_filter(&request_origin, head, request.mode, request.redirect_mode)
|
||||
.map_or(FetchResponseFilter::None, Into::into)
|
||||
}
|
||||
|
||||
@@ -98,11 +99,12 @@ pub(crate) fn network_response_filter(
|
||||
request_origin: impl Into<WebOrigin>,
|
||||
head: &moli_fetch::ResponseHead,
|
||||
request_mode: RequestMode,
|
||||
redirect_mode: RequestRedirectMode,
|
||||
) -> Option<crate::types::AsyncSubresourceFetchResponseFilter> {
|
||||
let request_origin = request_origin.into();
|
||||
use crate::types::AsyncSubresourceFetchResponseFilter;
|
||||
|
||||
if is_redirect_status(head.status) {
|
||||
if redirect_mode == RequestRedirectMode::Manual && is_redirect_status(head.status) {
|
||||
Some(AsyncSubresourceFetchResponseFilter::OpaqueRedirect)
|
||||
} else if request_mode == RequestMode::NoCors
|
||||
&& head.url_list().has_cross_origin_url(&request_origin)
|
||||
@@ -214,7 +216,7 @@ pub(crate) fn build_fetch_response_object_from_body_source_for_request_mode_with
|
||||
let request_origin = request_origin.into();
|
||||
let filter = filter_override
|
||||
.map(FetchResponseFilter::from)
|
||||
.unwrap_or_else(|| response_filter(&request_origin, &head, request.mode));
|
||||
.unwrap_or_else(|| response_filter(&request_origin, &head, request));
|
||||
let obj = build_fetch_response_object_head(scope, &request_origin, &head, filter, None);
|
||||
let body_stream = if response_has_null_body(request.method, head.status) {
|
||||
None
|
||||
@@ -235,7 +237,7 @@ pub(crate) fn build_fetch_response_object_from_subresource_body_for_request_mode
|
||||
body: crate::protocol_types::SubresourceResponseBody,
|
||||
) -> v8::Local<'s, v8::Object> {
|
||||
let request_origin = request_origin.into();
|
||||
let filter = response_filter(&request_origin, &head, request.mode);
|
||||
let filter = response_filter(&request_origin, &head, request);
|
||||
let obj = build_fetch_response_object_head(scope, &request_origin, &head, filter, None);
|
||||
let body_stream = if response_has_null_body(request.method, head.status) {
|
||||
None
|
||||
@@ -316,7 +318,7 @@ fn build_fetch_response_object_from_stream_for_request_mode_with_surface_url<'s>
|
||||
let request_origin = request_origin.into();
|
||||
let filter = filter_override
|
||||
.map(FetchResponseFilter::from)
|
||||
.unwrap_or_else(|| response_filter(&request_origin, &head, request.mode));
|
||||
.unwrap_or_else(|| response_filter(&request_origin, &head, request));
|
||||
let filtered_surface_url = (filter == FetchResponseFilter::OpaqueRedirect)
|
||||
.then_some(filtered_surface_url)
|
||||
.flatten();
|
||||
|
||||
@@ -587,7 +587,7 @@ pub(crate) fn external_script_source_load_outcome_from_response(
|
||||
let request_mode = external_script_request_mode(script.kind, &script.fetch_metadata);
|
||||
let head = response.head();
|
||||
let response_filter =
|
||||
crate::network_host::network_response_filter(&script.initiator_url, &head, request_mode);
|
||||
crate::network_host::network_response_filter(&script.initiator_url, &head, request_mode, moli_fetch::RequestRedirectMode::Follow);
|
||||
let cors_error = if request_mode == RequestMode::Cors {
|
||||
crate::network_host::validate_cors_response_chain(
|
||||
&script.initiator_url,
|
||||
|
||||
@@ -4794,6 +4794,51 @@ async fn window_fetch_dns_failure_rejects_and_records_network_failure() {
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn redirect_filter_intercepted_fetch_keeps_redirect_mode_when_resumed() {
|
||||
run_page_vm_async_test(async move {
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let base = format!("http://{}", listener.local_addr().unwrap());
|
||||
let server = tokio::spawn(async move {
|
||||
let mut requests = Vec::new();
|
||||
for _ in 0..7 {
|
||||
let (mut socket, _) = tokio::time::timeout(Duration::from_secs(5), listener.accept()).await.unwrap().unwrap();
|
||||
let head = read_http_request_head(&mut socket).await.unwrap();
|
||||
let path = head.lines().next().unwrap().split_whitespace().nth(1).unwrap();
|
||||
let status = if path == "/target" { 200 } else { 302 };
|
||||
let location = if path == "/with-location" { "Location: /target\r\n" } else { "" };
|
||||
socket.write_all(format!("HTTP/1.1 {status} Test Response\r\n{location}Content-Type: text/plain\r\nContent-Length: 2\r\nCache-Control: no-store\r\nConnection: close\r\n\r\nok").as_bytes()).await.unwrap();
|
||||
requests.push(path.to_owned());
|
||||
}
|
||||
requests
|
||||
});
|
||||
let mut page_vm = test_page_vm_with_document_url(Url::parse(&format!("{base}/page")).unwrap());
|
||||
let local_executor = page_vm.local_executor.clone();
|
||||
local_executor.run(async move {
|
||||
page_vm.vm_mut().set_fetch_subresource_interception(true, Some(SubresourceResourceType::Fetch));
|
||||
for redirect in ["follow", "manual", "error"] {
|
||||
for path in ["without-location", "with-location"] {
|
||||
page_vm.vm_mut().eval(&format!(
|
||||
"globalThis.__resumedRedirect = 'pending'; fetch('/{path}', {{redirect: '{redirect}'}}).then(async response => {{ __resumedRedirect = JSON.stringify([response.type, response.status, await response.text()]); }}, error => {{ __resumedRedirect = error.name; }});"
|
||||
))?;
|
||||
let requests = page_vm.vm_mut().take_pending_subresource_fetch_infos();
|
||||
assert_eq!(requests.len(), 1);
|
||||
page_vm.continue_pending_subresource_fetch(requests[0].internal_id, None, None, None, None, false, false)?;
|
||||
drive_websocket_until_done(&mut page_vm, "String(__resumedRedirect !== 'pending')", "resumed fetch should settle").await?;
|
||||
let expected = match redirect {
|
||||
"error" => "TypeError".to_owned(),
|
||||
"manual" => serde_json::json!(["opaqueredirect", 0, ""]).to_string(),
|
||||
_ => serde_json::json!(["basic", if path == "with-location" { 200 } else { 302 }, "ok"]).to_string(),
|
||||
};
|
||||
assert_eq!(page_vm.vm_mut().eval("__resumedRedirect")?, expected, "{redirect}/{path}");
|
||||
}
|
||||
}
|
||||
Ok::<_, anyhow::Error>(())
|
||||
}).await.unwrap();
|
||||
assert_eq!(server.await.unwrap(), ["/without-location", "/with-location", "/target", "/without-location", "/with-location", "/without-location", "/with-location"]);
|
||||
}).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn window_fetch_redirect_error_rejects_before_following_redirect() {
|
||||
run_page_vm_async_test(async move {
|
||||
|
||||
@@ -1420,6 +1420,14 @@ impl ScriptVm {
|
||||
.with_request_origin(pending.request_origin.clone())
|
||||
.with_request_mode(pending.request_mode)
|
||||
.with_use_cors_preflight(pending.continuation.use_cors_preflight())
|
||||
.with_redirect_mode(
|
||||
pending
|
||||
.continuation
|
||||
.window_fetch()
|
||||
.map_or(moli_fetch::RequestRedirectMode::Follow, |fetch| {
|
||||
fetch.redirect_mode()
|
||||
}),
|
||||
)
|
||||
.with_credentials_mode(pending.credentials_mode)
|
||||
.with_network_partition_key(pending.network_partition_key.clone())
|
||||
.with_subframe_context(pending.info.frame_id.is_some());
|
||||
@@ -1693,6 +1701,14 @@ impl ScriptVm {
|
||||
))
|
||||
.with_request_mode(pending_fetch.request_mode)
|
||||
.with_use_cors_preflight(pending_fetch.continuation.use_cors_preflight())
|
||||
.with_redirect_mode(
|
||||
pending_fetch
|
||||
.continuation
|
||||
.window_fetch()
|
||||
.map_or(moli_fetch::RequestRedirectMode::Follow, |fetch| {
|
||||
fetch.redirect_mode()
|
||||
}),
|
||||
)
|
||||
.with_credentials_mode(pending_fetch.credentials_mode)
|
||||
.with_auth(auth.into())
|
||||
.with_subframe_context(pending_fetch.info.frame_id.is_some());
|
||||
@@ -3721,19 +3737,23 @@ impl ScriptVm {
|
||||
moli_trace::cdp_runtime_trace_enabled().then(Instant::now);
|
||||
match pending.continuation {
|
||||
PendingSubresourceContinuation::Fetch(fetch) => {
|
||||
let redirect_mode = fetch.redirect_mode();
|
||||
let resolver = fetch
|
||||
.into_resolver()
|
||||
.expect("detached keepalive completion is handled before V8 entry");
|
||||
let resolver = v8::Local::new(scope, &resolver);
|
||||
let (head, body) = observable_response.into_body();
|
||||
let (mut head, body) = observable_response.into_body();
|
||||
if let Some(status_text) = response_status_text {
|
||||
head.status_text = Some(status_text);
|
||||
}
|
||||
let body = if opaque_response_blocked {
|
||||
moli_fetch::ResponseBody::materialized_bytes(Vec::new())
|
||||
} else {
|
||||
body
|
||||
};
|
||||
let response_filter = opaque_response_blocked
|
||||
.then_some(AsyncSubresourceFetchResponseFilter::Opaque)
|
||||
.or(response_filter);
|
||||
// ORB discards the internal body; request policy
|
||||
// still selects opaque versus opaqueredirect.
|
||||
// Preserve explicit service-worker filter overrides.
|
||||
let response_obj =
|
||||
crate::network_host::build_fetch_response_object_from_body_source_for_request_mode_with_filter(
|
||||
scope,
|
||||
@@ -3741,19 +3761,12 @@ impl ScriptVm {
|
||||
crate::network_host::FetchResponseRequest {
|
||||
method: &response_request_method,
|
||||
mode: pending.request_mode,
|
||||
redirect_mode,
|
||||
},
|
||||
head,
|
||||
body,
|
||||
response_filter,
|
||||
);
|
||||
if let Some(status_text) = response_status_text.as_deref() {
|
||||
crate::network_host::set_response_slot_string(
|
||||
scope,
|
||||
response_obj,
|
||||
crate::network_host::RESPONSE_STATUS_TEXT_SLOT,
|
||||
status_text,
|
||||
);
|
||||
}
|
||||
resolver.resolve(scope, response_obj.into());
|
||||
}
|
||||
PendingSubresourceContinuation::Xhr { xhr, .. } => {
|
||||
@@ -4965,6 +4978,7 @@ impl ScriptVm {
|
||||
crate::network_host::FetchResponseRequest {
|
||||
method: &started.request_method,
|
||||
mode: pending.request_mode,
|
||||
redirect_mode: fetch.redirect_mode(),
|
||||
},
|
||||
observable_head,
|
||||
started.body_source_id,
|
||||
|
||||
@@ -12,6 +12,7 @@ mod misc;
|
||||
mod null_body;
|
||||
mod open_validation;
|
||||
mod query_realms;
|
||||
mod redirect_filter;
|
||||
mod response_type;
|
||||
mod shadow_dom;
|
||||
mod streaming_failure;
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
use super::*;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
|
||||
#[test]
|
||||
fn redirect_filter_completion_keeps_status_text_and_explicit_filters() {
|
||||
use crate::types::AsyncSubresourceFetchResponseFilter::{Opaque, OpaqueRedirect};
|
||||
for status in [200, 302] {
|
||||
for redirect in ["follow", "manual"] {
|
||||
for filter in [None, Some(Opaque), Some(OpaqueRedirect)] {
|
||||
let mut vm = new_storage_test_vm("https://redirect-filter.test/");
|
||||
vm.set_fetch_subresource_interception(
|
||||
true,
|
||||
Some(crate::types::SubresourceResourceType::Fetch),
|
||||
);
|
||||
vm.eval(&format!(
|
||||
"globalThis.filteredResult = 'pending'; fetch('/response', {{redirect: '{redirect}'}}).then(async response => {{ filteredResult = JSON.stringify([response.type, response.status, response.statusText, response.headers.get('x-visible'), await response.text()]); }});"
|
||||
)).unwrap();
|
||||
let requests = vm.take_pending_subresource_fetch_infos();
|
||||
assert_eq!(requests.len(), 1);
|
||||
let request = &requests[0];
|
||||
vm.complete_async_subresource_fetch(
|
||||
crate::types::AsyncSubresourceFetchCompletion {
|
||||
internal_id: request.internal_id,
|
||||
request_url: request.url.clone(),
|
||||
request_method: request.method.clone(),
|
||||
request_headers: Vec::new(),
|
||||
request_body: None,
|
||||
response_status_text: Some("Override Text".to_owned()),
|
||||
skip_fetch_security_validation: true,
|
||||
response_filter: filter,
|
||||
network_error_text: None,
|
||||
result: Ok(
|
||||
crate::protocol_types::NavigationResponse::from_head_and_body(
|
||||
moli_fetch::ResponseHead {
|
||||
final_url: request.url.clone(),
|
||||
status,
|
||||
status_text: Some("Original Text".to_owned()),
|
||||
headers: vec![("X-Visible".to_owned(), "present".to_owned())],
|
||||
request_cookie_report: None,
|
||||
cookie_set_reports: Vec::new(),
|
||||
redirected: false,
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: false,
|
||||
negotiated_http_version: None,
|
||||
},
|
||||
"body".to_owned(),
|
||||
b"body".to_vec(),
|
||||
),
|
||||
),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let expected_type = match filter {
|
||||
Some(Opaque) => "opaque",
|
||||
Some(OpaqueRedirect) => "opaqueredirect",
|
||||
None if redirect == "manual" && status == 302 => "opaqueredirect",
|
||||
None => "basic",
|
||||
};
|
||||
let filtered = expected_type != "basic";
|
||||
let expected = serde_json::json!([
|
||||
expected_type,
|
||||
if filtered { 0 } else { status },
|
||||
if filtered { "" } else { "Override Text" },
|
||||
if filtered { None } else { Some("present") },
|
||||
if filtered { "" } else { "body" },
|
||||
]);
|
||||
assert_eq!(
|
||||
vm.eval("filteredResult").unwrap(),
|
||||
expected.to_string(),
|
||||
"{status}/{redirect}/{filter:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn check_redirect_filter_modes(worker: bool) {
|
||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let address = listener.local_addr().unwrap();
|
||||
let base = format!("http://{address}");
|
||||
let (stop_tx, mut stop_rx) = tokio::sync::oneshot::channel();
|
||||
let server = tokio::spawn(async move {
|
||||
let mut requests = 0;
|
||||
loop {
|
||||
let mut socket = tokio::select! {
|
||||
result = listener.accept() => result.unwrap().0,
|
||||
_ = &mut stop_rx => break,
|
||||
};
|
||||
let mut head = Vec::new();
|
||||
while !head.ends_with(b"\r\n\r\n") {
|
||||
assert!(head.len() < 8192);
|
||||
let mut byte = [0];
|
||||
assert_eq!(socket.read(&mut byte).await.unwrap(), 1);
|
||||
head.push(byte[0]);
|
||||
}
|
||||
let head = String::from_utf8(head).unwrap();
|
||||
let mut parts = head.lines().next().unwrap().split_whitespace();
|
||||
let method = parts.next().unwrap();
|
||||
let path = parts.next().unwrap();
|
||||
let status: u16 = path.trim_start_matches('/').parse().unwrap();
|
||||
let response = format!(
|
||||
"HTTP/1.1 {status} Matrix Response\r\nContent-Type: text/plain\r\nContent-Length: 13\r\nConnection: close\r\nCache-Control: no-store\r\nAccess-Control-Allow-Origin: *\r\nAccess-Control-Expose-Headers: X-Visible\r\nX-Visible: present\r\n\r\n{}",
|
||||
if method == "HEAD" {
|
||||
""
|
||||
} else {
|
||||
"redirect body"
|
||||
}
|
||||
);
|
||||
socket.write_all(response.as_bytes()).await.unwrap();
|
||||
requests += 1;
|
||||
}
|
||||
requests
|
||||
});
|
||||
let mut config = moli_fetch::FetchConfig::default();
|
||||
config.set_http_no_proxy(Some("*".to_owned()));
|
||||
let loader = ResourceRequestClient::new(&config).unwrap();
|
||||
let mut vm = new_page_task_executor_test_vm_with_loader(&format!("{base}/page"), &loader);
|
||||
let probe = format!(
|
||||
r#"(async () => {{
|
||||
const base = {base:?};
|
||||
let count = 0;
|
||||
for (const status of [200, 301, 302, 303, 307, 308]) {{
|
||||
for (const redirect of ['follow', 'manual', 'error']) {{
|
||||
for (const mode of ['cors', 'same-origin', 'no-cors']) {{
|
||||
for (const remote of [false, true]) {{
|
||||
for (const method of ['GET', 'HEAD']) {{
|
||||
for (const override of [false, true]) {{
|
||||
const label = [status, redirect, mode, remote, method, override].join('/');
|
||||
const assert = (value, field) => {{ if (!value) throw new Error(label + ': ' + field); }};
|
||||
const url = new URL('/' + status, base);
|
||||
if (remote) url.hostname = 'localhost';
|
||||
const request = new Request(url.href, {{method, mode, redirect: override ? 'follow' : redirect}});
|
||||
let response, error;
|
||||
try {{ response = await fetch(request.clone(), override ? {{redirect}} : undefined); }}
|
||||
catch (value) {{ error = value; }}
|
||||
count++;
|
||||
if ((remote && mode === 'same-origin') || (redirect === 'error' && status !== 200)) {{
|
||||
assert(error instanceof TypeError && !response, 'TypeError rejection');
|
||||
continue;
|
||||
}}
|
||||
assert(!error && response instanceof Response, 'response');
|
||||
const manual = redirect === 'manual' && status !== 200;
|
||||
const opaque = !manual && remote && mode === 'no-cors';
|
||||
const filtered = manual || opaque;
|
||||
assert(response.type === (manual ? 'opaqueredirect' : opaque ? 'opaque' : remote ? 'cors' : 'basic'), 'type=' + response.type);
|
||||
assert(response.status === (filtered ? 0 : status), 'status');
|
||||
assert(response.statusText === (filtered ? '' : 'Matrix Response'), 'statusText');
|
||||
assert(response.ok === (!filtered && status === 200), 'ok');
|
||||
assert(!response.redirected, 'redirected without Location');
|
||||
if (!manual) assert(response.url === (opaque ? '' : url.href), 'url');
|
||||
assert(response.headers.get('X-Visible') === (filtered ? null : 'present'), 'headers');
|
||||
if (filtered) assert(response.headers.entries().next().done, 'filtered headers');
|
||||
assert((response.body === null) === (filtered || method === 'HEAD'), 'body nullability');
|
||||
const clone = response.clone();
|
||||
const expected = filtered || method === 'HEAD' ? '' : 'redirect body';
|
||||
assert(await response.text() === expected, 'body');
|
||||
assert(await clone.text() === expected, 'clone body');
|
||||
assert(response.bodyUsed === (!filtered && method !== 'HEAD'), 'bodyUsed');
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
return String(count);
|
||||
}})()"#
|
||||
);
|
||||
let script = if worker {
|
||||
let source = format!(
|
||||
"Promise.resolve().then(() => {probe}).then(value => {{ postMessage(value); close(); }}, error => {{ postMessage(String(error.stack || error)); close(); }});"
|
||||
);
|
||||
format!(
|
||||
"globalThis.redirectFilterResult = 'pending'; const worker = new Worker(URL.createObjectURL(new Blob([{}], {{type: 'text/javascript'}}))); worker.onmessage = event => {{ redirectFilterResult = event.data; }}; worker.onerror = event => {{ redirectFilterResult = event.message; event.preventDefault(); }};",
|
||||
serde_json::to_string(&source).unwrap()
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"globalThis.redirectFilterResult = 'pending'; Promise.resolve().then(() => {probe}).then(value => {{ redirectFilterResult = value; }}, error => {{ redirectFilterResult = String(error.stack || error); }});"
|
||||
)
|
||||
};
|
||||
vm.eval(&script).unwrap();
|
||||
tokio::time::timeout(std::time::Duration::from_secs(20), async {
|
||||
while vm.eval("redirectFilterResult === 'pending'").unwrap() == "true" {
|
||||
wait_for_one_selected_page_task_executor_test_turn(&mut vm, &loader)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
})
|
||||
.await
|
||||
.expect("redirect response matrix should finish");
|
||||
stop_tx.send(()).unwrap();
|
||||
let requests = server.await.unwrap();
|
||||
assert_eq!(vm.eval("redirectFilterResult").unwrap(), "432");
|
||||
// Cross-origin same-origin-mode requests are rejected before transport.
|
||||
assert_eq!(requests, 360);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn redirect_filter_preserves_window_fetch_modes() {
|
||||
check_redirect_filter_modes(false).await;
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn redirect_filter_preserves_worker_fetch_modes() {
|
||||
check_redirect_filter_modes(true).await;
|
||||
}
|
||||
@@ -222,6 +222,7 @@ fn pending_fetch_continuation<'s>(
|
||||
)
|
||||
.expect("test Fetch should capture its CSP report context"),
|
||||
moli_url::WebOrigin::from_url(host.document_url()),
|
||||
moli_fetch::RequestRedirectMode::Follow,
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -3420,6 +3421,7 @@ fn install_streaming_fetch_response_fixture(
|
||||
scope,
|
||||
&document_url,
|
||||
crate::network_host::FetchResponseRequest {
|
||||
redirect_mode: moli_fetch::RequestRedirectMode::Follow,
|
||||
method: "GET",
|
||||
mode: moli_fetch::RequestMode::Cors,
|
||||
},
|
||||
|
||||
@@ -311,6 +311,7 @@ fn register_pending_window_fetch_for_test(
|
||||
moli_fetch::RequestCredentialsMode::SameOrigin,
|
||||
moli_fetch::RequestMode::Cors,
|
||||
(&url).into(),
|
||||
moli_fetch::RequestRedirectMode::Follow,
|
||||
None,
|
||||
Default::default(),
|
||||
crate::types::PendingSubresourceFetchInfo {
|
||||
@@ -475,6 +476,7 @@ fn register_pending_window_fetch_with_connect_policy_for_test(
|
||||
moli_fetch::RequestCredentialsMode::SameOrigin,
|
||||
moli_fetch::RequestMode::Cors,
|
||||
(&document_url).into(),
|
||||
moli_fetch::RequestRedirectMode::Follow,
|
||||
None,
|
||||
Default::default(),
|
||||
crate::types::PendingSubresourceFetchInfo {
|
||||
|
||||
@@ -9217,6 +9217,7 @@ fn fetched_null_bodies_discard_payloads_without_registering_pending_streams() {
|
||||
for opaque in [false, true] {
|
||||
for source in ["response", "bytes", "subresource", "stream", "preload"] {
|
||||
let request = FetchResponseRequest {
|
||||
redirect_mode: moli_fetch::RequestRedirectMode::Follow,
|
||||
method,
|
||||
mode: if opaque { moli_fetch::RequestMode::NoCors } else { moli_fetch::RequestMode::Cors },
|
||||
};
|
||||
@@ -9296,6 +9297,7 @@ fn response_clone_tees_pending_network_body_after_parent_consumption() {
|
||||
scope,
|
||||
&document_url,
|
||||
crate::network_host::FetchResponseRequest {
|
||||
redirect_mode: moli_fetch::RequestRedirectMode::Follow,
|
||||
method: "GET",
|
||||
mode: moli_fetch::RequestMode::Cors,
|
||||
},
|
||||
@@ -9401,6 +9403,7 @@ fn pending_fetch_body_pipe_through_text_decoder_stream_pulls_future_chunks() {
|
||||
scope,
|
||||
&document_url,
|
||||
crate::network_host::FetchResponseRequest {
|
||||
redirect_mode: moli_fetch::RequestRedirectMode::Follow,
|
||||
method: "GET",
|
||||
mode: moli_fetch::RequestMode::Cors,
|
||||
},
|
||||
@@ -9522,6 +9525,7 @@ fn materialize_response_object_preserves_redirected_slot() {
|
||||
scope,
|
||||
&document_url,
|
||||
crate::network_host::FetchResponseRequest {
|
||||
redirect_mode: moli_fetch::RequestRedirectMode::Follow,
|
||||
method: "GET",
|
||||
mode: moli_fetch::RequestMode::Cors,
|
||||
},
|
||||
@@ -9574,7 +9578,8 @@ fn filtered_response_materialization_preserves_internal_url_without_exposing_url
|
||||
let response = crate::network_host::build_fetch_response_object_from_body_source_for_request_mode_with_filter(
|
||||
scope,
|
||||
&document_url,
|
||||
crate::network_host::FetchResponseRequest { method: "GET", mode: moli_fetch::RequestMode::Cors },
|
||||
crate::network_host::FetchResponseRequest {
|
||||
redirect_mode: moli_fetch::RequestRedirectMode::Follow, method: "GET", mode: moli_fetch::RequestMode::Cors },
|
||||
moli_fetch::ResponseHead {
|
||||
status_text: None,
|
||||
final_url: final_url.clone(),
|
||||
|
||||
@@ -292,6 +292,7 @@ impl ServiceWorkerRuntimeService {
|
||||
};
|
||||
let request_mode = job.request.request_mode;
|
||||
let credentials_mode = job.request.credentials_mode;
|
||||
let redirect_mode = job.request.redirect_mode;
|
||||
job.resource_task_runner.spawn(async move {
|
||||
let result = match request_client
|
||||
.fetch_raw_stream_with_cancel(request, cancel_handle)
|
||||
@@ -321,6 +322,7 @@ impl ServiceWorkerRuntimeService {
|
||||
&request_origin,
|
||||
&head,
|
||||
request_mode,
|
||||
redirect_mode,
|
||||
);
|
||||
let body = response.clone_body_bytes();
|
||||
let navigation_response =
|
||||
@@ -542,35 +544,32 @@ impl ServiceWorkerRuntimeService {
|
||||
}
|
||||
}
|
||||
if is_redirect_status(response.status)
|
||||
&& job.request.redirect_mode != moli_fetch::RequestRedirectMode::Manual
|
||||
&& job.request.redirect_mode == moli_fetch::RequestRedirectMode::Error
|
||||
{
|
||||
self.complete_fetch_with_network_failure(
|
||||
job,
|
||||
"FetchEvent.respondWith rejected a redirect Response for a request whose redirect mode is error".to_owned(),
|
||||
crate::network_host::FAILED_ERROR_TEXT.to_owned(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if is_redirect_status(response.status)
|
||||
&& job.request.redirect_mode == moli_fetch::RequestRedirectMode::Follow
|
||||
{
|
||||
match service_worker_redirect_target(&job, &response) {
|
||||
Ok(Some(next_url)) => match job.request.redirect_mode {
|
||||
moli_fetch::RequestRedirectMode::Error => {
|
||||
self.complete_fetch_with_network_failure(
|
||||
job,
|
||||
format!(
|
||||
"FetchEvent.respondWith rejected a redirect Response for a request whose redirect mode is error: {next_url}"
|
||||
),
|
||||
crate::network_host::FAILED_ERROR_TEXT.to_owned(),
|
||||
);
|
||||
Ok(Some(next_url)) => {
|
||||
if let Err(error) =
|
||||
apply_service_worker_synthetic_redirect(&mut job, response, next_url)
|
||||
{
|
||||
self.complete_fetch_with_failure(job, error);
|
||||
return;
|
||||
}
|
||||
moli_fetch::RequestRedirectMode::Manual => {}
|
||||
moli_fetch::RequestRedirectMode::Follow => {
|
||||
if let Err(error) =
|
||||
apply_service_worker_synthetic_redirect(&mut job, response, next_url)
|
||||
{
|
||||
self.complete_fetch_with_failure(job, error);
|
||||
return;
|
||||
}
|
||||
let request = fetch_request_for_job(&job);
|
||||
if let Err(job) = self.dispatch_controlled_fetch_job(job, request) {
|
||||
self.dispatch_fetch_fallback(*job);
|
||||
}
|
||||
return;
|
||||
let request = fetch_request_for_job(&job);
|
||||
if let Err(job) = self.dispatch_controlled_fetch_job(job, request) {
|
||||
self.dispatch_fetch_fallback(*job);
|
||||
}
|
||||
},
|
||||
return;
|
||||
}
|
||||
Ok(None) => {}
|
||||
Err(error) => {
|
||||
self.complete_fetch_with_network_failure(
|
||||
@@ -600,7 +599,7 @@ impl ServiceWorkerRuntimeService {
|
||||
self.complete_fetch_with_failure(job, message);
|
||||
return;
|
||||
}
|
||||
let response_filter = service_worker_fetch_response_filter(&response);
|
||||
let response_filter = service_worker_fetch_response_filter(&response, job.redirect_mode);
|
||||
let navigation_response = crate::protocol_types::NavigationResponse::from_head_and_body(
|
||||
moli_fetch::ResponseHead {
|
||||
status_text: Some(response.status_text.clone()),
|
||||
@@ -881,8 +880,10 @@ fn service_worker_fetch_response_rejection(
|
||||
|
||||
fn service_worker_fetch_response_filter(
|
||||
response: &ServiceWorkerFetchResponse,
|
||||
redirect_mode: moli_fetch::RequestRedirectMode,
|
||||
) -> Option<AsyncSubresourceFetchResponseFilter> {
|
||||
if is_redirect_status(response.status)
|
||||
if redirect_mode == moli_fetch::RequestRedirectMode::Manual
|
||||
&& is_redirect_status(response.status)
|
||||
&& !matches!(response.response_type.as_str(), "opaque" | "opaqueredirect")
|
||||
{
|
||||
Some(AsyncSubresourceFetchResponseFilter::OpaqueRedirect)
|
||||
@@ -1493,6 +1494,71 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn redirect_filter_service_worker_responses_without_location_preserve_request_mode() {
|
||||
use moli_fetch::RequestRedirectMode::{Error, Follow, Manual};
|
||||
for status in [301, 302, 303, 307, 308] {
|
||||
for mode in [Follow, Manual, Error] {
|
||||
let service = new_service_worker_runtime_service();
|
||||
let event_id = ServiceWorkerEventId(1);
|
||||
let version_id = ServiceWorkerVersionId(1);
|
||||
let run = RendererServiceWorkerRunIdentity::fresh();
|
||||
let mut completion_queue =
|
||||
crate::page_task_queue::RendererResourceCompletionTestHarness::new();
|
||||
let request_url = insert_active_fetch_job_with_redirect_mode(
|
||||
&service,
|
||||
event_id,
|
||||
version_id,
|
||||
&run,
|
||||
1,
|
||||
completion_queue.sender(),
|
||||
mode,
|
||||
);
|
||||
let headers = vec![("X-Visible".to_owned(), "present".to_owned())];
|
||||
service.finish_fetch_event_completed(ServiceWorkerFetchCompletion {
|
||||
event_id,
|
||||
owner: crate::service_worker_runtime::ServiceWorkerRunOwner::new(
|
||||
version_id, run,
|
||||
),
|
||||
result: ServiceWorkerFetchResult::Response(ServiceWorkerFetchResponse {
|
||||
final_url: None,
|
||||
response_type: "default".to_owned(),
|
||||
redirected: false,
|
||||
status,
|
||||
status_text: "No Location".to_owned(),
|
||||
headers: headers.clone(),
|
||||
body: b"redirect body".to_vec(),
|
||||
}),
|
||||
});
|
||||
let completion = pop_async_subresource_completion(&mut completion_queue);
|
||||
if mode == Error {
|
||||
assert!(
|
||||
completion
|
||||
.result
|
||||
.unwrap_err()
|
||||
.contains("redirect mode is error")
|
||||
);
|
||||
assert_eq!(
|
||||
completion.network_error_text.as_deref(),
|
||||
Some(crate::network_host::FAILED_ERROR_TEXT)
|
||||
);
|
||||
} else {
|
||||
assert_eq!(
|
||||
completion.response_filter,
|
||||
(mode == Manual)
|
||||
.then_some(AsyncSubresourceFetchResponseFilter::OpaqueRedirect)
|
||||
);
|
||||
let response = completion.result.unwrap();
|
||||
assert_eq!(response.status, status);
|
||||
assert_eq!(response.final_url, request_url);
|
||||
assert_eq!(response.headers, headers);
|
||||
assert_eq!(response.body_text(), "redirect body");
|
||||
assert!(!response.redirected);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn response_completion_projects_opaque_response_filter_to_subresource_queue() {
|
||||
let service = new_service_worker_runtime_service();
|
||||
@@ -1714,7 +1780,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
completion.result.err().as_deref(),
|
||||
Some(
|
||||
"FetchEvent.respondWith rejected a redirect Response for a request whose redirect mode is error: https://example.test/redirected.txt"
|
||||
"FetchEvent.respondWith rejected a redirect Response for a request whose redirect mode is error"
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
|
||||
@@ -245,6 +245,7 @@ pub(super) struct PendingWindowFetchContinuation {
|
||||
keepalive: bool,
|
||||
connect_policy: crate::document_runtime::DocumentConnectPolicySnapshot,
|
||||
csp_report_context: crate::network_host::WindowCspReportRequestContext,
|
||||
redirect_mode: moli_fetch::RequestRedirectMode,
|
||||
}
|
||||
|
||||
enum PendingWindowFetchPromise {
|
||||
@@ -258,12 +259,14 @@ impl PendingWindowFetchContinuation {
|
||||
keepalive: bool,
|
||||
connect_policy: crate::document_runtime::DocumentConnectPolicySnapshot,
|
||||
csp_report_context: crate::network_host::WindowCspReportRequestContext,
|
||||
redirect_mode: moli_fetch::RequestRedirectMode,
|
||||
) -> Self {
|
||||
Self {
|
||||
promise: PendingWindowFetchPromise::Active(resolver),
|
||||
keepalive,
|
||||
connect_policy,
|
||||
csp_report_context,
|
||||
redirect_mode,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -304,6 +307,11 @@ impl PendingWindowFetchContinuation {
|
||||
pub(super) fn csp_report_context(&self) -> &crate::network_host::WindowCspReportRequestContext {
|
||||
&self.csp_report_context
|
||||
}
|
||||
|
||||
|
||||
pub(super) fn redirect_mode(&self) -> moli_fetch::RequestRedirectMode {
|
||||
self.redirect_mode
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||
|
||||
@@ -2618,6 +2618,7 @@ pub(in crate::worker) fn start_worker_streaming_fetch(
|
||||
pending.document_url.clone(),
|
||||
pending.request_mode,
|
||||
pending.request_method.clone(),
|
||||
pending.redirect_mode,
|
||||
observable_head,
|
||||
))
|
||||
}
|
||||
@@ -2633,8 +2634,14 @@ pub(in crate::worker) fn start_worker_streaming_fetch(
|
||||
}
|
||||
return;
|
||||
}
|
||||
if let Some((resolver, document_url, request_mode, request_method, observable_head)) =
|
||||
response_input
|
||||
if let Some((
|
||||
resolver,
|
||||
document_url,
|
||||
request_mode,
|
||||
request_method,
|
||||
redirect_mode,
|
||||
observable_head,
|
||||
)) = response_input
|
||||
{
|
||||
let response_obj = build_fetch_response_object_from_stream_for_request_mode(
|
||||
scope,
|
||||
@@ -2642,6 +2649,7 @@ pub(in crate::worker) fn start_worker_streaming_fetch(
|
||||
crate::network_host::FetchResponseRequest {
|
||||
method: &request_method,
|
||||
mode: request_mode,
|
||||
redirect_mode,
|
||||
},
|
||||
observable_head,
|
||||
started.body_source_id,
|
||||
@@ -3034,6 +3042,7 @@ pub(in crate::worker) fn drain_worker_fetch_completion_result(
|
||||
crate::network_host::FetchResponseRequest {
|
||||
method: &pending.request_method,
|
||||
mode: pending.request_mode,
|
||||
redirect_mode: pending.redirect_mode,
|
||||
},
|
||||
head,
|
||||
body,
|
||||
@@ -3052,6 +3061,7 @@ pub(in crate::worker) fn drain_worker_fetch_completion_result(
|
||||
crate::network_host::FetchResponseRequest {
|
||||
method: &pending.request_method,
|
||||
mode: pending.request_mode,
|
||||
redirect_mode: pending.redirect_mode,
|
||||
},
|
||||
head,
|
||||
body,
|
||||
|
||||
@@ -3567,6 +3567,8 @@ fn start_service_worker_navigation_preload_response_in_context(
|
||||
crate::network_host::FetchResponseRequest {
|
||||
method: &started.request_method,
|
||||
mode: started.request_mode,
|
||||
// Navigation preload always fetches with manual redirects.
|
||||
redirect_mode: moli_fetch::RequestRedirectMode::Manual,
|
||||
},
|
||||
head,
|
||||
started.body_source_id,
|
||||
|
||||
Reference in New Issue
Block a user