//! Navigator backend for web use js_sys::{Array, ArrayBuffer, Uint8Array}; use ruffle_core::backend::navigator::{ ErrorResponse, NavigationMethod, NavigatorBackend, OpenURLMode, OwnedFuture, Request, SuccessResponse, }; use ruffle_core::config::NetworkingAccessMode; use ruffle_core::indexmap::IndexMap; use ruffle_core::loader::Error; use std::borrow::Cow; use std::sync::Arc; use tracing_subscriber::layer::Layered; use tracing_subscriber::Registry; use tracing_wasm::WASMLayer; use url::Url; use wasm_bindgen::JsCast; use wasm_bindgen_futures::{spawn_local, JsFuture}; use web_sys::{ window, Blob, BlobPropertyBag, HtmlFormElement, HtmlInputElement, Request as WebRequest, RequestInit, Response as WebResponse, }; pub struct WebNavigatorBackend { log_subscriber: Arc>, allow_script_access: bool, allow_networking: NetworkingAccessMode, upgrade_to_https: bool, base_url: Option, open_url_mode: OpenURLMode, } impl WebNavigatorBackend { pub fn new( allow_script_access: bool, allow_networking: NetworkingAccessMode, upgrade_to_https: bool, base_url: Option, log_subscriber: Arc>, open_url_mode: OpenURLMode, ) -> Self { let window = web_sys::window().expect("window()"); // Upgrade to HTTPS takes effect if the current page is hosted on HTTPS. let upgrade_to_https = upgrade_to_https && window.location().protocol().expect("protocol()") == "https:"; // Retrieve and parse `document.baseURI`. let document_base_uri = || { let document = window.document().expect("document()"); if let Ok(Some(base_uri)) = document.base_uri() { return Url::parse(&base_uri).ok(); } None }; let base_url = if let Some(mut base_url) = base_url { // Adding trailing slash so `Url::parse` will not drop the last part. if !base_url.ends_with('/') { base_url.push('/'); } Url::parse(&base_url) .ok() .or_else(|| document_base_uri().and_then(|base_uri| base_uri.join(&base_url).ok())) } else { document_base_uri() }; if base_url.is_none() { tracing::error!("Could not get base URL for base directory inference."); } Self { allow_script_access, allow_networking, upgrade_to_https, base_url, log_subscriber, open_url_mode, } } fn resolve_url<'a>(&self, url: &'a str) -> Cow<'a, str> { if let Some(base_url) = &self.base_url { if let Ok(url) = base_url.join(url) { return self.pre_process_url(url).to_string().into(); } } url.into() } } impl NavigatorBackend for WebNavigatorBackend { fn navigate_to_url( &self, url: &str, target: &str, vars_method: Option<(NavigationMethod, IndexMap)>, ) { // If the URL is empty, ignore the request. if url.is_empty() { return; } let url = self.resolve_url(url); // If `allowNetworking` is set to `internal` or `none`, block all `navigate_to_url` calls. if self.allow_networking != NetworkingAccessMode::All { tracing::warn!("SWF tried to open a URL, but opening URLs is not allowed"); return; } // If `allowScriptAccess` is disabled, reject the `javascript:` scheme. // Also reject any attempt to open a URL when `target` is a keyword that affects the current tab. let is_javascript_call = if let Ok(url) = Url::parse(&url) { let is_javascript_scheme = url.scheme() == "javascript"; if !self.allow_script_access { if is_javascript_scheme { tracing::warn!("SWF tried to run a script, but script access is not allowed"); return; } else { match target.to_lowercase().as_str() { "_parent" | "_self" | "_top" | "" => { tracing::warn!("SWF tried to open a URL, but opening URLs in the current tab is prevented by script access"); return; } _ => (), } } } is_javascript_scheme } else { false }; let window = window().expect("window()"); if !is_javascript_call { if self.open_url_mode == OpenURLMode::Confirm { let message = format!("The SWF file wants to open the website {}", &url); // TODO: Add a checkbox with a GUI toolkit let confirm = window .confirm_with_message(&message) .expect("confirm_with_message()"); if !confirm { tracing::info!( "SWF tried to open a website, but the user declined the request" ); return; } } else if self.open_url_mode == OpenURLMode::Deny { tracing::warn!("SWF tried to open a website, but opening a website is not allowed"); return; } // If the user confirmed or if in `Allow` mode, open the website. } // TODO: Should we return a result for failed opens? Does Flash care? match vars_method { Some((navmethod, formvars)) => { let document = window.document().expect("document()"); let body = match document.body() { Some(body) => body, None => return, }; let form: HtmlFormElement = document .create_element("form") .expect("create_element() must succeed") .dyn_into() .expect("create_element(\"form\") didn't give us a form"); form.set_method(&navmethod.to_string()); form.set_action(&url); if !target.is_empty() { form.set_target(target); } for (key, value) in formvars { let hidden: HtmlInputElement = document .create_element("input") .expect("create_element() must succeed") .dyn_into() .expect("create_element(\"input\") didn't give us an input"); hidden.set_type("hidden"); hidden.set_name(&key); hidden.set_value(&value); let _ = form.append_child(&hidden); } let _ = body.append_child(&form); let _ = form.submit(); } None => { if target.is_empty() { let _ = window.location().assign(&url); } else { let _ = window.open_with_url_and_target(&url, target); } } }; } fn fetch(&self, request: Request) -> OwnedFuture { let url = self.resolve_url(request.url()).into_owned(); Box::pin(async move { let mut init = RequestInit::new(); init.method(&request.method().to_string()); if let Some((data, mime)) = request.body() { let blob = Blob::new_with_buffer_source_sequence_and_options( &Array::from_iter([Uint8Array::from(data.as_slice()).buffer()]), BlobPropertyBag::new().type_(mime), ) .map_err(|_| ErrorResponse { url: url.clone(), error: Error::FetchError("Got JS error".to_string()), })? .dyn_into() .map_err(|_| ErrorResponse { url: url.clone(), error: Error::FetchError("Got JS error".to_string()), })?; init.body(Some(&blob)); } let web_request = WebRequest::new_with_str_and_init(&url, &init).map_err(|_| ErrorResponse { url: url.clone(), error: Error::FetchError(format!("Unable to create request for {url}")), })?; let headers = web_request.headers(); for (header_name, header_val) in request.headers() { headers .set(header_name, header_val) .map_err(|_| ErrorResponse { url: url.clone(), error: Error::FetchError("Got JS error".to_string()), })?; } let window = web_sys::window().expect("window()"); let fetchval = JsFuture::from(window.fetch_with_request(&web_request)) .await .map_err(|_| ErrorResponse { url: url.clone(), error: Error::FetchError("Got JS error".to_string()), })?; let response: WebResponse = fetchval.dyn_into().map_err(|_| ErrorResponse { url: url.clone(), error: Error::FetchError("Fetch result wasn't a WebResponse".to_string()), })?; let url = response.url(); let status = response.status(); let redirected = response.redirected(); if !response.ok() { let error = Error::HttpNotOk( format!("HTTP status is not ok, got {}", response.status_text()), status, redirected, ); return Err(ErrorResponse { url, error }); } let body: ArrayBuffer = JsFuture::from(response.array_buffer().map_err(|_| { ErrorResponse { url: url.clone(), error: Error::FetchError("Got JS error".to_string()), } })?) .await .map_err(|_| ErrorResponse { url: url.clone(), error: Error::FetchError( "Could not allocate array buffer for response".to_string(), ), })? .dyn_into() .map_err(|_| ErrorResponse { url: url.clone(), error: Error::FetchError("array_buffer result wasn't an ArrayBuffer".to_string()), })?; let body = Uint8Array::new(&body).to_vec(); Ok(SuccessResponse { url, body, status, redirected, }) }) } fn spawn_future(&mut self, future: OwnedFuture<(), Error>) { let subscriber = self.log_subscriber.clone(); spawn_local(async move { let _subscriber = tracing::subscriber::set_default(subscriber); if let Err(e) = future.await { tracing::error!("Asynchronous error occurred: {}", e); } }) } fn pre_process_url(&self, mut url: Url) -> Url { if self.upgrade_to_https && url.scheme() == "http" && url.set_scheme("https").is_err() { tracing::error!("Url::set_scheme failed on: {}", url); } url } }