ruffle/web/src/navigator.rs

288 lines
9.9 KiB
Rust
Raw Normal View History

//! Navigator backend for web
use js_sys::{Array, ArrayBuffer, Uint8Array};
use ruffle_core::backend::navigator::{
NavigationMethod, NavigatorBackend, OpenURLMode, OwnedFuture, Request, Response,
};
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;
2019-09-04 02:15:03 +00:00
use wasm_bindgen::JsCast;
2019-11-09 02:10:21 +00:00
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<Layered<WASMLayer, Registry>>,
allow_script_access: bool,
allow_networking: NetworkingAccessMode,
upgrade_to_https: bool,
base_url: Option<Url>,
open_url_mode: OpenURLMode,
}
impl WebNavigatorBackend {
pub fn new(
allow_script_access: bool,
allow_networking: NetworkingAccessMode,
upgrade_to_https: bool,
base_url: Option<String>,
log_subscriber: Arc<Layered<WASMLayer, Registry>>,
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('/');
2021-09-07 04:44:24 +00:00
}
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() {
2023-01-04 11:33:10 +00:00
tracing::error!("Could not get base URL for base directory inference.");
2021-09-07 04:44:24 +00:00
}
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 {
2019-09-17 03:37:11 +00:00
fn navigate_to_url(
&self,
url: &str,
target: &str,
vars_method: Option<(NavigationMethod, IndexMap<String, String>)>,
2019-09-17 03:37:11 +00:00
) {
// If the URL is empty, ignore the request.
2021-03-21 06:02:57 +00:00
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.
core: Add navigate_to_url call configuration options New configuration options (changing the navigate_to_url call handling) have been added. The default behaviour has been changed as well. A NavigateWebsiteHandlingMode enum has been added to Ruffle (in Rust and Typescript). It contains the values "Allow", "Confirm" and "Deny" and describes how navigate_to_url website calls should be handled. Allow means that all website calls are allowed, Confirm means that a confirmation window opens with each website call and Deny means that all website calls are denied. A respective navigate_website_handling_mode variable has been added to the desktop CLI and to the JS config. The default value is "Confirm" in each. The variable is given to the navigator (ExternalNavigatorBackend or WebNavigatorBackend, depending on the platform) and is saved in it. On each navigate_to_url website call, the respective navigator is now checking navigate_website_handling_mode and acts correspondingly (allows it, opens a confirmation window or denies it). This changes the default behaviour of Ruffle from allowing all website calls to opening a confirmation window with each website call. On Safari, the confirm window can cause the background music to stop, but this seems to be an issue with Safari. Closes #838. Additionally, an allow_javascript_calls variable (which defaults to false) has been added to the desktop CLI. The variable is given to the desktop navigator and is saved in it. If a navigate_to_url javascript call is executed on desktop, the navigator is now checking allow_javascript_calls and acts correspondingly (allows it or denies it). This changes the default behaviour of Ruffle on desktop to not allowing javascript calls. Closes #9316.
2023-04-09 16:27:20 +00:00
let js_call = if let Ok(url) = Url::parse(&url) {
2021-03-21 06:02:57 +00:00
if !self.allow_script_access && url.scheme() == "javascript" {
2023-01-04 11:33:10 +00:00
tracing::warn!("SWF tried to run a script, but script access is not allowed");
2021-03-21 06:02:57 +00:00
return;
}
core: Add navigate_to_url call configuration options New configuration options (changing the navigate_to_url call handling) have been added. The default behaviour has been changed as well. A NavigateWebsiteHandlingMode enum has been added to Ruffle (in Rust and Typescript). It contains the values "Allow", "Confirm" and "Deny" and describes how navigate_to_url website calls should be handled. Allow means that all website calls are allowed, Confirm means that a confirmation window opens with each website call and Deny means that all website calls are denied. A respective navigate_website_handling_mode variable has been added to the desktop CLI and to the JS config. The default value is "Confirm" in each. The variable is given to the navigator (ExternalNavigatorBackend or WebNavigatorBackend, depending on the platform) and is saved in it. On each navigate_to_url website call, the respective navigator is now checking navigate_website_handling_mode and acts correspondingly (allows it, opens a confirmation window or denies it). This changes the default behaviour of Ruffle from allowing all website calls to opening a confirmation window with each website call. On Safari, the confirm window can cause the background music to stop, but this seems to be an issue with Safari. Closes #838. Additionally, an allow_javascript_calls variable (which defaults to false) has been added to the desktop CLI. The variable is given to the desktop navigator and is saved in it. If a navigate_to_url javascript call is executed on desktop, the navigator is now checking allow_javascript_calls and acts correspondingly (allows it or denies it). This changes the default behaviour of Ruffle on desktop to not allowing javascript calls. Closes #9316.
2023-04-09 16:27:20 +00:00
url.scheme() == "javascript"
} else {
false
};
let window = window().expect("window()");
if !js_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
core: Add navigate_to_url call configuration options New configuration options (changing the navigate_to_url call handling) have been added. The default behaviour has been changed as well. A NavigateWebsiteHandlingMode enum has been added to Ruffle (in Rust and Typescript). It contains the values "Allow", "Confirm" and "Deny" and describes how navigate_to_url website calls should be handled. Allow means that all website calls are allowed, Confirm means that a confirmation window opens with each website call and Deny means that all website calls are denied. A respective navigate_website_handling_mode variable has been added to the desktop CLI and to the JS config. The default value is "Confirm" in each. The variable is given to the navigator (ExternalNavigatorBackend or WebNavigatorBackend, depending on the platform) and is saved in it. On each navigate_to_url website call, the respective navigator is now checking navigate_website_handling_mode and acts correspondingly (allows it, opens a confirmation window or denies it). This changes the default behaviour of Ruffle from allowing all website calls to opening a confirmation window with each website call. On Safari, the confirm window can cause the background music to stop, but this seems to be an issue with Safari. Closes #838. Additionally, an allow_javascript_calls variable (which defaults to false) has been added to the desktop CLI. The variable is given to the desktop navigator and is saved in it. If a navigate_to_url javascript call is executed on desktop, the navigator is now checking allow_javascript_calls and acts correspondingly (allows it or denies it). This changes the default behaviour of Ruffle on desktop to not allowing javascript calls. Closes #9316.
2023-04-09 16:27:20 +00:00
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 {
core: Add navigate_to_url call configuration options New configuration options (changing the navigate_to_url call handling) have been added. The default behaviour has been changed as well. A NavigateWebsiteHandlingMode enum has been added to Ruffle (in Rust and Typescript). It contains the values "Allow", "Confirm" and "Deny" and describes how navigate_to_url website calls should be handled. Allow means that all website calls are allowed, Confirm means that a confirmation window opens with each website call and Deny means that all website calls are denied. A respective navigate_website_handling_mode variable has been added to the desktop CLI and to the JS config. The default value is "Confirm" in each. The variable is given to the navigator (ExternalNavigatorBackend or WebNavigatorBackend, depending on the platform) and is saved in it. On each navigate_to_url website call, the respective navigator is now checking navigate_website_handling_mode and acts correspondingly (allows it, opens a confirmation window or denies it). This changes the default behaviour of Ruffle from allowing all website calls to opening a confirmation window with each website call. On Safari, the confirm window can cause the background music to stop, but this seems to be an issue with Safari. Closes #838. Additionally, an allow_javascript_calls variable (which defaults to false) has been added to the desktop CLI. The variable is given to the desktop navigator and is saved in it. If a navigate_to_url javascript call is executed on desktop, the navigator is now checking allow_javascript_calls and acts correspondingly (allows it or denies it). This changes the default behaviour of Ruffle on desktop to not allowing javascript calls. Closes #9316.
2023-04-09 16:27:20 +00:00
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);
}
2021-01-10 14:04:31 +00:00
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");
2021-01-10 14:04:31 +00:00
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);
}
}
};
2021-01-10 14:04:31 +00:00
}
fn fetch(&self, request: Request) -> OwnedFuture<Response, Error> {
let url = self.resolve_url(request.url()).into_owned();
2019-11-09 02:10:21 +00:00
Box::pin(async move {
let mut init = RequestInit::new();
2023-04-22 14:08:10 +00:00
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(|_| Error::FetchError("Got JS error".to_string()))?
.dyn_into()
.map_err(|_| Error::FetchError("Got JS error".to_string()))?;
init.body(Some(&blob));
}
let web_request = WebRequest::new_with_str_and_init(&url, &init)
2022-10-26 23:46:09 +00:00
.map_err(|_| 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(|_| 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(|_| Error::FetchError("Got JS error".to_string()))?;
2021-03-27 19:43:43 +00:00
let response: WebResponse = fetchval
.dyn_into()
.map_err(|_| Error::FetchError("Fetch result wasn't a WebResponse".to_string()))?;
let status = response.status();
let redirected = response.redirected();
if !response.ok() {
return Err(Error::HttpNotOk(
format!("HTTP status is not ok, got {}", response.status_text()),
status,
redirected,
));
}
2021-03-27 19:43:43 +00:00
let url = response.url();
let body: ArrayBuffer = JsFuture::from(
response
.array_buffer()
.map_err(|_| Error::FetchError("Got JS error".to_string()))?,
)
.await
.map_err(|_| {
Error::FetchError("Could not allocate array buffer for response".to_string())
})?
.dyn_into()
.map_err(|_| {
Error::FetchError("array_buffer result wasn't an ArrayBuffer".to_string())
})?;
let body = Uint8Array::new(&body).to_vec();
Ok(Response {
url,
body,
status,
redirected,
})
2019-11-09 02:10:21 +00:00
})
}
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 {
2023-01-04 11:33:10 +00:00
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() {
2023-01-04 11:33:10 +00:00
tracing::error!("Url::set_scheme failed on: {}", url);
}
url
}
2019-09-17 03:37:11 +00:00
}