use std::{
borrow::Cow,
io::{Cursor, Read},
sync::{Arc, Mutex},
};
use cef::{rc::*, *};
use dioxus_debug_cell::RefCell;
use html5ever::{LocalName, interface::QualName, namespace_url, ns};
use http::{
HeaderMap, HeaderName, HeaderValue,
header::{CONTENT_SECURITY_POLICY, CONTENT_TYPE, ORIGIN},
};
use kuchiki::{Attribute, ExpandedName, NodeRef};
use tauri_runtime::{
UserEvent,
webview::{NavigationHandler, UriSchemeProtocolHandler},
window::WindowId,
};
use tauri_utils::{
config::{Csp, CspDirectiveSources},
html::{parse as parse_html, serialize_node},
};
use url::Url;
use crate::{
cef_impl::client::{
DragDropEventTarget, DragDropState, WebDragDropResourceRequestHandler,
WebDragDropResourceRequestHandlerArgs,
},
macros::wrap_with_args,
runtime::RuntimeContext,
webview::{CefInitScript, INITIAL_LOAD_URL},
};
type HttpResponse = Arc<RefCell<Option<http::Response<Cursor<Vec<u8>>>>>>;
pub(crate) type SchemeRegistry = Arc<
Mutex<
std::collections::HashMap<
(i32, String),
(
String,
Arc<Box<UriSchemeProtocolHandler>>,
Arc<Vec<CefInitScript>>,
),
>,
>,
>;
fn csp_inject_initialization_scripts_hashes(
existing_csp: String,
initialization_scripts: &[CefInitScript],
is_main_frame: bool,
) -> String {
if initialization_scripts.is_empty() {
return existing_csp;
}
let script_hashes: Vec<String> = initialization_scripts
.iter()
.filter(|script| script.runs_in_frame(is_main_frame))
.map(|s| s.hash.clone())
.collect();
if script_hashes.is_empty() {
return existing_csp;
}
let mut csp_map: std::collections::HashMap<String, CspDirectiveSources> =
Csp::Policy(existing_csp.to_string()).into();
let script_src = csp_map
.entry("script-src".to_string())
.or_insert_with(|| CspDirectiveSources::List(vec!["'self'".to_string()]));
script_src.extend(script_hashes);
Csp::DirectiveMap(csp_map).to_string()
}
fn inject_scripts_into_html_body(
body: &[u8],
initialization_scripts: &[CefInitScript],
is_main_frame: bool,
) -> Option<Vec<u8>> {
let Ok(body_str) = std::str::from_utf8(body) else {
return None;
};
let document = parse_html(body_str.to_string());
let head = if let Ok(ref head_node) = document.select_first("head") {
head_node.as_node().clone()
} else {
let head_node = NodeRef::new_element(
QualName::new(None, ns!(html), LocalName::from("head")),
None,
);
document.prepend(head_node.clone());
head_node
};
for init_script in initialization_scripts
.iter()
.rev()
.filter(|script| script.runs_in_frame(is_main_frame))
{
let script_el = NodeRef::new_element(QualName::new(None, ns!(html), "script".into()), None);
script_el.append(NodeRef::new_text(init_script.script.as_str()));
head.prepend(script_el);
}
keep_encoding_declaration_first(&document, &head);
Some(serialize_node(&document))
}
fn keep_encoding_declaration_first(document: &NodeRef, head: &NodeRef) {
let existing_declaration = document.select("meta").ok().and_then(|metas| {
metas.into_iter().find(|meta| {
let attributes = meta.attributes.borrow();
attributes.get("charset").is_some()
|| attributes
.get("http-equiv")
.map(|value| value.trim().eq_ignore_ascii_case("content-type"))
.unwrap_or(false)
})
});
match existing_declaration {
Some(declaration) => {
let node = declaration.as_node().clone();
node.detach();
head.prepend(node);
}
None => head.prepend(NodeRef::new_element(
QualName::new(None, ns!(html), LocalName::from("meta")),
[(
ExpandedName::new(ns!(), LocalName::from("charset")),
Attribute {
prefix: None,
value: "utf-8".into(),
},
)],
)),
}
}
wrap_with_args! {
wrap_request_handler => WebRequestHandlerArgs;
pub struct WebRequestHandler<T: UserEvent> {
navigation_handler: Option<Arc<NavigationHandler>>,
frame_event_handler: Option<Arc<crate::FrameEventHandler>>,
context: RuntimeContext<T>,
window_id: WindowId,
webview_id: u32,
drag_drop_event_target: DragDropEventTarget,
drag_drop_handler_enabled: bool,
drag_drop_state: Arc<Mutex<DragDropState>>,
web_content_process_terminate_handler: Option<Arc<tauri_runtime::webview::OnWebContentProcessTerminateHandler>>,
certificate_errors: crate::CertificateErrorPolicy,
}
impl RequestHandler {
fn on_certificate_error(
&self,
_browser: Option<&mut Browser>,
cert_error: Errorcode,
request_url: Option<&CefString>,
_ssl_info: Option<&mut Sslinfo>,
_callback: Option<&mut Callback>,
) -> ::std::os::raw::c_int {
match self.certificate_errors {
crate::CertificateErrorPolicy::ChromeInterstitial => 0,
crate::CertificateErrorPolicy::Cancel => {
let origin = request_url
.map(ToString::to_string)
.and_then(|url| url::Url::parse(&url).ok())
.map(|url| url.origin().ascii_serialization())
.unwrap_or_else(|| "an unknown origin".to_string());
log::warn!(
"cancelling a request to {origin}: its TLS certificate did not validate \
(Chromium error {cert_error:?}), and the runtime is set to \
CertificateErrorPolicy::Cancel"
);
1
}
}
}
fn on_render_process_terminated(
&self,
browser: Option<&mut Browser>,
status: TerminationStatus,
error_code: ::std::os::raw::c_int,
error_string: Option<&CefString>,
) {
let mut frame = browser.as_ref().and_then(|browser| browser.main_frame());
crate::frame::emit_frame_event(&self.frame_event_handler, browser, frame.as_mut(), crate::FrameEventKind::RendererTerminated);
if let Some(handler) = &self.web_content_process_terminate_handler {
handler(tauri_runtime::webview::WebContentProcessTermination {
reason: termination_reason(status),
error_code: Some(error_code),
error_string: error_string.map(ToString::to_string),
});
}
}
fn on_before_browse(
&self,
browser: Option<&mut Browser>,
frame: Option<&mut Frame>,
request: Option<&mut Request>,
_user_gesture: ::std::os::raw::c_int,
_is_redirect: ::std::os::raw::c_int,
) -> ::std::os::raw::c_int {
let _ = (&self.context, self.window_id, self.webview_id);
let Some(frame) = frame else {
return 0;
};
let Some(request) = request else {
return 0;
};
let url_str = CefString::from(&request.url()).to_string();
if url_str == INITIAL_LOAD_URL {
return 0;
}
let Ok(url) = url::Url::parse(&url_str) else {
return 0;
};
if frame.is_main() != 0
&& self.navigation_handler.as_ref().is_some_and(|handler| !handler(&url))
{
return 1;
}
crate::frame::emit_frame_event(
&self.frame_event_handler,
browser,
Some(frame),
crate::FrameEventKind::NavigationStarted { url },
);
0
}
fn resource_request_handler(
&self,
_browser: Option<&mut Browser>,
_frame: Option<&mut Frame>,
_request: Option<&mut Request>,
_is_navigation: ::std::os::raw::c_int,
_is_download: ::std::os::raw::c_int,
_request_initiator: Option<&CefString>,
_disable_default_handling: Option<&mut ::std::os::raw::c_int>,
) -> Option<ResourceRequestHandler> {
if !self.drag_drop_handler_enabled {
return None;
}
Some(WebDragDropResourceRequestHandler::build(
WebDragDropResourceRequestHandlerArgs {
context: self.context.clone(),
window_id: self.window_id,
webview_id: self.webview_id,
drag_drop_event_target: self.drag_drop_event_target,
drag_drop_handler_enabled: self.drag_drop_handler_enabled,
drag_drop_state: self.drag_drop_state.clone(),
},
))
}
}
}
wrap_with_args! {
wrap_resource_handler => WebResourceHandlerArgs;
pub struct WebResourceHandler {
webview_label: String,
handler: Arc<Box<UriSchemeProtocolHandler>>,
initialization_scripts: Arc<Vec<CefInitScript>>,
is_main_frame: bool,
initiator_origin: Option<String>,
response: HttpResponse,
}
impl ResourceHandler {
fn process_request(
&self,
request: Option<&mut Request>,
callback: Option<&mut Callback>,
) -> ::std::os::raw::c_int {
let Some(request) = request else { return 0 };
let Some(callback) = callback else { return 0 };
let url = CefString::from(&request.url()).to_string();
let url = Url::parse(&url).ok();
if let Some(url) = url {
let callback = ThreadSafe(callback.clone());
let response_store = ThreadSafe(self.response.clone());
let initialization_scripts = self.initialization_scripts.clone();
let is_main_frame = self.is_main_frame;
let responder = Box::new(move |response: http::Response<Cow<'static, [u8]>>| {
let is_html = response
.headers()
.get(CONTENT_TYPE)
.and_then(|ct| ct.to_str().ok())
.map(|ct| ct.to_lowercase().starts_with("text/html"))
.unwrap_or(false);
let (parts, body) = response.into_parts();
let body_bytes = body.into_owned();
let body_bytes = if is_html {
inject_scripts_into_html_body(&body_bytes, &initialization_scripts, is_main_frame)
.unwrap_or(body_bytes)
} else {
body_bytes
};
let mut response = http::Response::from_parts(parts, Cursor::new(body_bytes));
if let Some(csp) = response.headers_mut().get_mut(CONTENT_SECURITY_POLICY) {
let csp_string = csp.to_str().unwrap_or_default().to_string();
let new_csp = csp_inject_initialization_scripts_hashes(
csp_string,
&initialization_scripts,
is_main_frame,
);
if let Ok(new_csp) = HeaderValue::from_str(&new_csp) {
*csp = new_csp;
}
}
response_store.into_owned().borrow_mut().replace(response);
let callback = callback.into_owned();
callback.cont();
});
let label = self.webview_label.clone();
let handler = self.handler.clone();
let data = read_request_body(request);
let mut headers = get_request_headers(request);
if let Some(initiator_origin) = &self.initiator_origin {
let origin_missing_or_null = headers
.get(ORIGIN)
.map(|value| value.as_bytes() == b"null")
.unwrap_or(true);
if origin_missing_or_null && let Ok(value) = HeaderValue::from_str(initiator_origin) {
headers.insert(ORIGIN, value);
}
}
let method_str = CefString::from(&request.method()).to_string();
let method = http::Method::from_bytes(method_str.as_bytes()).unwrap_or(http::Method::GET);
std::thread::spawn(move || {
let mut http_request = http::Request::builder()
.method(method)
.uri(url.as_str())
.body(data)
.unwrap();
*http_request.headers_mut() = headers;
(**handler)(&label, http_request, responder);
});
1
} else {
0
}
}
fn read(
&self,
data_out: *mut u8,
bytes_to_read: ::std::os::raw::c_int,
bytes_read: Option<&mut ::std::os::raw::c_int>,
_callback: Option<&mut ResourceReadCallback>,
) -> ::std::os::raw::c_int {
let Ok(bytes_to_read) = usize::try_from(bytes_to_read) else {
return 0;
};
let data_out = unsafe { std::slice::from_raw_parts_mut(data_out, bytes_to_read) };
let count = self
.response
.borrow_mut()
.as_mut()
.and_then(|response| response.body_mut().read(data_out).ok())
.unwrap_or(0);
if let Some(bytes_read) = bytes_read {
let Ok(count) = count.try_into() else {
return 0;
};
*bytes_read = count;
if count > 0 {
return 1;
}
}
0
}
fn response_headers(
&self,
response: Option<&mut Response>,
response_length: Option<&mut i64>,
redirect_url: Option<&mut CefString>,
) {
let (Some(response), Some(response_data)) = (response, &*self.response.borrow()) else {
return;
};
response.set_status(response_data.status().as_u16() as i32);
let mut content_type = None;
for (name, value) in response_data.headers() {
let Ok(value) = value.to_str() else {
continue;
};
response.set_header_by_name(Some(&name.as_str().into()), Some(&value.into()), 0);
if name == CONTENT_TYPE {
content_type.replace(value.to_string());
}
}
response.set_header_by_name(Some(&"Cache-Control".into()), Some(&"no-store".into()), 1);
let mime_type = content_type
.as_ref()
.and_then(|t| t.split(';').next())
.map(str::trim)
.unwrap_or("text/plain");
response.set_mime_type(Some(&mime_type.into()));
if let Some(length) = response_length {
*length = -1;
}
if let Some(redirect_url) = redirect_url {
let _ = std::mem::take(redirect_url);
}
}
}
}
wrap_scheme_handler_factory! {
pub struct UriSchemeHandlerFactory {
registry: SchemeRegistry,
scheme: String,
}
impl SchemeHandlerFactory {
fn create(
&self,
browser: Option<&mut Browser>,
frame: Option<&mut Frame>,
_scheme_name: Option<&CefString>,
_request: Option<&mut Request>,
) -> Option<ResourceHandler> {
let (webview_label, handler, initialization_scripts) = match browser {
Some(browser) => {
let id = browser.identifier();
self
.registry
.lock()
.unwrap()
.get(&(id, self.scheme.clone()))
.cloned()?
}
None => {
let (webview_label, handler, _) = self
.registry
.lock()
.unwrap()
.iter()
.find(|((_, scheme), _)| *scheme == self.scheme)
.map(|(_, entry)| entry.clone())?;
(webview_label, handler, Arc::new(Vec::new()))
}
};
let is_main_frame = frame.as_ref().map(|frame| frame.is_main() == 1).unwrap_or(true);
let initiator_origin = frame
.filter(|frame| frame.is_main() == 1)
.map(|frame| CefString::from(&frame.url()).to_string())
.and_then(|url| Url::parse(&url).ok())
.map(|url| url.origin().ascii_serialization())
.filter(|origin| origin != "null");
Some(WebResourceHandler::build(WebResourceHandlerArgs {
webview_label,
handler,
initialization_scripts,
is_main_frame,
initiator_origin,
response: Arc::new(RefCell::new(None)),
}))
}
}
}
struct ThreadSafe<T>(T);
impl<T> ThreadSafe<T> {
fn into_owned(self) -> T {
self.0
}
}
unsafe impl<T> Send for ThreadSafe<T> {}
unsafe impl<T> Sync for ThreadSafe<T> {}
fn read_request_body(request: &mut Request) -> Vec<u8> {
let mut body = Vec::new();
if let Some(post_data) = request.post_data() {
let mut elements = vec![None; post_data.element_count()];
post_data.elements(Some(&mut elements));
for element in elements.into_iter().flatten() {
match element.get_type().as_ref() {
sys::cef_postdataelement_type_t::PDE_TYPE_BYTES => {
let size = element.bytes_count();
if size > 0 {
let mut buf = vec![0u8; size];
let copied = element.bytes(size, buf.as_mut_ptr());
unsafe {
buf.set_len(copied);
}
body.extend(buf);
}
}
sys::cef_postdataelement_type_t::PDE_TYPE_FILE => {
let file_path = CefString::from(&element.file()).to_string();
if let Ok(mut file) = std::fs::File::open(&file_path) {
use std::io::Read;
let mut buf = Vec::new();
if file.read_to_end(&mut buf).is_ok() {
body.extend(buf);
}
}
}
_ => {}
}
}
}
body
}
fn get_request_headers(request: &mut Request) -> HeaderMap {
let mut headers = HeaderMap::new();
let mut map = CefStringMultimap::new();
request.header_map(Some(&mut map));
for (name, value) in map {
for v in value {
headers.append(
HeaderName::from_bytes(name.as_bytes()).unwrap(),
HeaderValue::from_str(&v).unwrap(),
);
}
}
headers
}
fn termination_reason(
status: TerminationStatus,
) -> tauri_runtime::webview::WebContentProcessTerminationReason {
use tauri_runtime::webview::WebContentProcessTerminationReason as Reason;
match status {
TerminationStatus::ABNORMAL_TERMINATION => Reason::Abnormal,
TerminationStatus::PROCESS_WAS_KILLED => Reason::Killed,
TerminationStatus::PROCESS_CRASHED => Reason::Crashed,
TerminationStatus::PROCESS_OOM => Reason::OutOfMemory,
TerminationStatus::LAUNCH_FAILED => Reason::LaunchFailed,
TerminationStatus::INTEGRITY_FAILURE => Reason::IntegrityFailure,
_ => Reason::Unknown,
}
}
#[cfg(test)]
mod termination_tests {
use super::*;
use tauri_runtime::webview::WebContentProcessTerminationReason as Reason;
#[test]
fn preserves_every_cef_termination_reason() {
for (status, expected) in [
(TerminationStatus::ABNORMAL_TERMINATION, Reason::Abnormal),
(TerminationStatus::PROCESS_WAS_KILLED, Reason::Killed),
(TerminationStatus::PROCESS_CRASHED, Reason::Crashed),
(TerminationStatus::PROCESS_OOM, Reason::OutOfMemory),
(TerminationStatus::LAUNCH_FAILED, Reason::LaunchFailed),
(
TerminationStatus::INTEGRITY_FAILURE,
Reason::IntegrityFailure,
),
(TerminationStatus::NUM_VALUES, Reason::Unknown),
] {
assert_eq!(termination_reason(status), expected);
}
}
}