///|
priv struct SyntheticFetchPlanEntry {
url : String
http_method : String
request_token : String
redirect_count : Int
request_value : Json?
}
///|
priv struct SyntheticFetchStepResult {
blocked_text : String?
response_value : Json?
}
///|
priv struct SyntheticFetchCommandResult {
blocked_text : String?
response_value : Json
}
///|
fn BidiProtocol::handle_network_perform_synthetic_fetch(
self : BidiProtocol,
request_id : Int,
params : Json?,
) -> Unit {
let map = match params {
Some(Object(map)) => map
_ => {
self.send_error(
request_id, "invalid argument", "params must be an object",
)
return
}
}
let context_id = match map.get("context") {
Some(String(context_id)) => context_id
_ => {
self.send_error(
request_id, "invalid argument", "context must be a string",
)
return
}
}
if !self.manager.has_session(context_id) {
self.send_error(
request_id,
"no such frame",
"Unknown context: " + context_id,
)
return
}
let url = match map.get("url") {
Some(String(url)) => url
_ => {
self.send_error(request_id, "invalid argument", "url must be a string")
return
}
}
let http_method = match map.get("method") {
Some(String(http_method)) => http_method
_ => {
self.send_error(request_id, "invalid argument", "method must be a string")
return
}
}
let request_headers = match
self.normalize_synthetic_network_header_entries(
request_id,
get_map_field_with_alias(map, "requestHeaders", "request_headers"),
) {
Some(request_headers) => request_headers
None => return
}
let request_value = match
get_map_field_with_alias(map, "requestData", "request_data") {
Some(raw_value) =>
match
self.normalize_synthetic_network_bytes_value(
request_id, raw_value, "requestData",
) {
Some(value) => Some(value)
None => return
}
None => None
}
let should_abort = match
get_map_field_with_alias(map, "shouldAbort", "should_abort") {
None | Some(Null) => false
Some(True) => true
Some(False) => false
Some(_) => {
self.send_error(
request_id, "invalid argument", "shouldAbort must be a boolean",
)
return
}
}
let result = self.perform_synthetic_fetch_internal(
request_id, context_id, url, http_method, request_headers, request_value, should_abort,
)
let response : Map[String, Json] = {
"blocked": Json::boolean(result.blocked_text != None),
"value": result.response_value,
}
match result.blocked_text {
Some(text) => response["message"] = Json::string(text)
None => ()
}
self.send_success(request_id, Some(make_object(response)))
}
///|
fn BidiProtocol::perform_synthetic_fetch_internal(
self : BidiProtocol,
request_id : Int,
context_id : String,
url : String,
http_method : String,
request_headers : Array[Json],
request_value : Json?,
should_abort : Bool,
) -> SyntheticFetchCommandResult {
let request_plan = self.build_synthetic_fetch_request_plan(
context_id, url, http_method, request_headers, request_value,
)
let mut last_response_value = make_object({
"type": Json::string("string"),
"value": Json::string(""),
})
for request_step in request_plan {
let step_result = self.perform_synthetic_fetch_step(
request_id, context_id, request_step, request_headers, should_abort,
)
match step_result.blocked_text {
Some(text) =>
return { blocked_text: Some(text), response_value: last_response_value }
None => ()
}
match step_result.response_value {
Some(response_value) => last_response_value = response_value
None => ()
}
}
{ blocked_text: None, response_value: last_response_value }
}
///|
fn BidiProtocol::build_synthetic_fetch_request_plan(
self : BidiProtocol,
context_id : String,
url : String,
http_method : String,
request_headers : Array[Json],
request_value : Json?,
) -> Array[SyntheticFetchPlanEntry] {
let requests : Array[SyntheticFetchPlanEntry] = []
let actual_request_token = self.next_synthetic_network_request_id()
if synthetic_fetch_requires_preflight(
self.requested_navigation_url(context_id),
url,
http_method,
request_headers,
) {
requests.push({
url,
http_method: "OPTIONS",
request_token: self.next_synthetic_network_request_id(),
redirect_count: 0,
request_value: None,
})
}
requests.push({
url,
http_method,
request_token: actual_request_token,
redirect_count: 0,
request_value,
})
let path = synthetic_network_request_path(url)
let redirect_location = synthetic_network_first_query_value(url, "location")
let redirect_status = synthetic_network_int_query_param(url, "status", 302)
let should_follow_redirect = path.has_suffix("/redirect.py") ||
(
path.has_suffix("/cached.py") &&
(
redirect_status == 301 ||
redirect_status == 302 ||
redirect_status == 303 ||
redirect_status == 307 ||
redirect_status == 308
)
)
if should_follow_redirect {
match redirect_location {
Some(redirect_url) if redirect_url != "" =>
requests.push({
url: redirect_url,
http_method,
request_token: actual_request_token,
redirect_count: 1,
request_value: None,
})
_ => ()
}
}
requests
}
///|
fn BidiProtocol::perform_synthetic_fetch_step(
self : BidiProtocol,
request_id : Int,
context_id : String,
request_step : SyntheticFetchPlanEntry,
raw_request_headers : Array[Json],
should_abort : Bool,
) -> SyntheticFetchStepResult {
let resolved_request_headers = self.resolve_network_extra_header_entries(
context_id, raw_request_headers,
)
let base_input : SyntheticNetworkEventInput = {
context_id,
url: request_step.url,
http_method: request_step.http_method,
request_token: request_step.request_token,
request_headers: resolved_request_headers,
request_value: request_step.request_value,
request_cookies: [],
request_cookies_override: false,
intercepts: [],
redirect_count: request_step.redirect_count,
navigation: None,
destination: "",
initiator_type: Some("fetch"),
}
let response_overrides = self.resolve_synthetic_response_overrides_internal(
context_id,
request_step.url,
request_step.http_method,
resolved_request_headers,
true,
)
let before_intercepts = self.resolve_matching_network_intercepts_internal(
context_id,
"beforeRequestSent",
request_step.url,
)
let before_subscribed = self.is_subscribed_for_context(
"network.beforeRequestSent", context_id,
)
let before_blocked = before_subscribed && before_intercepts.length() > 0
if before_subscribed {
let before_input = {
..base_input,
intercepts: if before_blocked {
before_intercepts
} else {
[]
},
}
match
self.build_synthetic_before_request_sent_event_json(
request_id, before_input,
) {
Some(event) =>
self.outbox.push(
Event({ event_method: "network.beforeRequestSent", params: event }),
)
None => return { blocked_text: None, response_value: None }
}
}
if before_blocked {
let blocked_request : Map[String, Json] = {
"phase": Json::string("beforeRequestSent"),
"context_id": Json::string(context_id),
"url": Json::string(request_step.url),
"method": Json::string(request_step.http_method),
"request_headers": synthetic_navigation_header_object_from_entries(
raw_request_headers,
),
"redirect_count": Json::number(request_step.redirect_count.to_double()),
"navigation": Json::null(),
"destination": Json::string(""),
"initiator_type": Json::string("fetch"),
"is_navigation": Json::boolean(false),
}
match request_step.request_value {
Some(request_value) => blocked_request["request_value"] = request_value
None => ()
}
self.remember_network_blocked_request(
request_step.request_token,
blocked_request,
)
return {
blocked_text: Some("Request blocked by network intercept"),
response_value: None,
}
}
if should_abort ||
synthetic_navigation_is_unreachable_test_url(request_step.url) {
if self.is_subscribed_for_context("network.fetchError", context_id) {
match
self.build_synthetic_fetch_error_event_json(
request_id, base_input, "Request failed",
) {
Some(event) =>
self.outbox.push(
Event({ event_method: "network.fetchError", params: event }),
)
None => ()
}
}
return { blocked_text: None, response_value: None }
}
let response_started_intercepts = self.resolve_matching_network_intercepts_internal(
context_id,
"responseStarted",
request_step.url,
)
let response_started_subscribed = self.is_subscribed_for_context(
"network.responseStarted", context_id,
)
let response_started_blocked = response_started_subscribed &&
response_started_intercepts.length() > 0
let mut response_started_event : Json? = None
if response_started_subscribed {
let response_input = {
..base_input,
intercepts: if response_started_blocked {
response_started_intercepts
} else {
[]
},
}
response_started_event = self.build_synthetic_response_event_json(
request_id, response_input, response_overrides,
)
match response_started_event {
Some(event) =>
self.outbox.push(
Event({ event_method: "network.responseStarted", params: event }),
)
None => return { blocked_text: None, response_value: None }
}
}
if response_started_blocked {
let blocked_request = synthetic_navigation_blocked_request_payload(
base_input, "responseStarted", response_started_event,
)
blocked_request["is_navigation"] = Json::boolean(false)
self.remember_network_blocked_request(
request_step.request_token,
blocked_request,
)
return {
blocked_text: Some("Request blocked by responseStarted intercept"),
response_value: None,
}
}
let auth_required_intercepts = self.resolve_matching_network_intercepts_internal(
context_id,
"authRequired",
request_step.url,
)
let auth_required_subscribed = self.is_subscribed_for_context(
"network.authRequired", context_id,
)
let auth_required_needed = synthetic_navigation_requires_auth_event(
response_overrides,
)
let auth_required_blocked = auth_required_needed &&
auth_required_subscribed &&
auth_required_intercepts.length() > 0
let mut auth_required_event : Json? = None
if auth_required_subscribed && auth_required_needed {
let auth_overrides : SyntheticNetworkResponseOverrides = if auth_required_blocked {
{
from_cache: response_overrides.from_cache,
headers: response_overrides.headers,
mime_type: response_overrides.mime_type,
protocol: response_overrides.protocol,
status: 401,
status_text: "Unauthorized",
auth_challenges: Some(
Json::array([
make_object({
"scheme": Json::string("Basic"),
"realm": Json::string("testrealm"),
}),
]),
),
}
} else {
response_overrides
}
let auth_input = {
..base_input,
intercepts: if auth_required_blocked {
auth_required_intercepts
} else {
[]
},
}
auth_required_event = self.build_synthetic_response_event_json(
request_id, auth_input, auth_overrides,
)
match auth_required_event {
Some(event) =>
self.outbox.push(
Event({ event_method: "network.authRequired", params: event }),
)
None => return { blocked_text: None, response_value: None }
}
}
if auth_required_blocked {
let blocked_request = synthetic_navigation_blocked_request_payload(
base_input, "authRequired", auth_required_event,
)
blocked_request["is_navigation"] = Json::boolean(false)
self.remember_network_blocked_request(
request_step.request_token,
blocked_request,
)
return {
blocked_text: Some("Request blocked by authRequired intercept"),
response_value: None,
}
}
let response_value = synthetic_fetch_synthesize_response_value(
request_step.url,
if synthetic_fetch_is_headers_echo_url(request_step.url) {
Some(
self.build_network_headers_echo_payload(
context_id, resolved_request_headers,
),
)
} else {
None
},
)
self.remember_network_collected_data_internal(
context_id,
request_step.request_token,
request_step.request_value,
Some(response_value),
)
if self.is_subscribed_for_context("network.responseCompleted", context_id) {
match
self.build_synthetic_response_event_json(
request_id, base_input, response_overrides,
) {
Some(event) =>
self.outbox.push(
Event({ event_method: "network.responseCompleted", params: event }),
)
None =>
return { blocked_text: None, response_value: Some(response_value) }
}
}
{ blocked_text: None, response_value: Some(response_value) }
}
///|
fn synthetic_fetch_requires_preflight(
source_url : String,
target_url : String,
http_method : String,
request_headers : Array[Json],
) -> Bool {
let source_origin = match canonicalize_network_http_url(source_url) {
Some(source) => source
None => return false
}
let target_origin = match canonicalize_network_http_url(target_url) {
Some(target) => target
None => return false
}
if source_origin.scheme == target_origin.scheme &&
source_origin.host == target_origin.host &&
source_origin.port == target_origin.port {
return false
}
let method_upper = http_method.to_upper()
if method_upper != "GET" && method_upper != "HEAD" && method_upper != "POST" {
return true
}
for header in request_headers {
let name = match synthetic_network_header_entry_name(header) {
Some(name) => name.to_lower()
None => continue
}
let value = match synthetic_network_header_entry_string_value(header) {
Some(value) => value
None => ""
}
if name == "accept" ||
name == "accept-language" ||
name == "content-language" {
continue
}
if name == "content-type" &&
synthetic_fetch_is_cors_safelisted_content_type(value) {
continue
}
return true
}
false
}
///|
fn synthetic_fetch_is_cors_safelisted_content_type(value : String) -> Bool {
let raw_media_type = match find_substring(value, ";", 0) {
Some(idx) => value.unsafe_substring(start=0, end=idx).to_lower()
None => value.to_lower()
}
let media_type = raw_media_type.trim()
media_type == "application/x-www-form-urlencoded" ||
media_type == "multipart/form-data" ||
media_type == "text/plain"
}
///|
fn synthetic_fetch_is_headers_echo_url(url : String) -> Bool {
match canonicalize_network_http_url(url) {
Some(parts) =>
parts.port == 8000 &&
(parts.host == "localhost" || parts.host == "alt.localhost") &&
parts.path.has_suffix(
"/webdriver/tests/support/http_handlers/headers_echo.py",
)
None => false
}
}
///|
fn synthetic_fetch_synthesize_response_value(
url : String,
headers_echo_payload : String?,
) -> Json {
match headers_echo_payload {
Some(payload) =>
make_object({
"type": Json::string("string"),
"value": Json::string(payload),
})
None => synthetic_navigation_synthesize_response_value(url)
}
}