diff --git a/Cargo.lock b/Cargo.lock index c20522b4c..de2f20bf8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9011,13 +9011,17 @@ dependencies = [ name = "tauri-runtime-cef" version = "0.1.0" dependencies = [ + "base64 0.22.1", "cef", "cef-dll-sys", "gtk", + "html5ever", "http 1.3.1", + "kuchikiki", "raw-window-handle", "serde", "serde_json", + "sha2", "tauri-runtime", "tauri-utils", "url", diff --git a/crates/tauri-runtime-cef/Cargo.toml b/crates/tauri-runtime-cef/Cargo.toml index aabff5d35..ee18be494 100644 --- a/crates/tauri-runtime-cef/Cargo.toml +++ b/crates/tauri-runtime-cef/Cargo.toml @@ -11,7 +11,10 @@ rust-version.workspace = true [dependencies] tauri-runtime = { version = "2.4.0", path = "../tauri-runtime" } -tauri-utils = { version = "2.2.0", path = "../tauri-utils" } +tauri-utils = { version = "2.2.0", path = "../tauri-utils", features = [ + "html-manipulation", +] } +html5ever = "0.29" raw-window-handle = "0.6" url = "2" http = "1" @@ -19,6 +22,9 @@ cef = "141.6.0" cef-dll-sys = "141.6.0" serde = { version = "1", features = ["derive"] } serde_json = "1" +kuchiki = { package = "kuchikiki", version = "0.8.8-speedreader" } +sha2 = "0.10" +base64 = "0.22" [target."cfg(any(target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies] gtk = { version = "0.18", features = ["v3_24"] } diff --git a/crates/tauri-runtime-cef/src/cef_impl.rs b/crates/tauri-runtime-cef/src/cef_impl.rs index 09d4c3fd3..a538fdcbc 100644 --- a/crates/tauri-runtime-cef/src/cef_impl.rs +++ b/crates/tauri-runtime-cef/src/cef_impl.rs @@ -1,10 +1,12 @@ +use base64::Engine; use cef::{rc::*, *}; +use sha2::{Digest, Sha256}; use std::{ cell::RefCell, collections::HashMap, sync::{ atomic::{AtomicU32, Ordering}, - Arc, Mutex, OnceLock, + Arc, }, }; use tauri_runtime::{ @@ -12,47 +14,37 @@ use tauri_runtime::{ window::{PendingWindow, WindowId}, RunEvent, UserEvent, }; +use tauri_utils::html::normalize_script_for_csp; use crate::{AppWindow, CefRuntime, Message}; mod request_handler; -// Message name for process messages containing initialization scripts -const INIT_SCRIPTS_MESSAGE_NAME: &str = "tauri_init_scripts"; - -// Serializable version of InitializationScript for process messages -#[derive(serde::Serialize, serde::Deserialize)] -struct SerializableInitScript { - script: String, - for_main_frame_only: bool, +#[derive(Clone)] +pub struct CefInitScript { + pub script: InitializationScript, + pub hash: String, } -impl From<&InitializationScript> for SerializableInitScript { - fn from(script: &InitializationScript) -> Self { - Self { - script: script.script.clone(), - for_main_frame_only: script.for_main_frame_only, - } +impl CefInitScript { + pub fn new(script: InitializationScript) -> Self { + let hash = hash_script(script.script.as_str()); + Self { script, hash } } } -impl From for InitializationScript { - fn from(script: SerializableInitScript) -> Self { - Self { - script: script.script, - for_main_frame_only: script.for_main_frame_only, - } - } +fn hash_script(script: &str) -> String { + let normalized = normalize_script_for_csp(script.as_bytes()); + let mut hasher = Sha256::new(); + hasher.update(&normalized); + let hash = hasher.finalize(); + format!( + "'sha256-{}'", + base64::engine::general_purpose::STANDARD.encode(hash) + ) } -// Static registry in render process to store initialization scripts received via process messages -// This is populated when the browser process sends scripts via process messages -static RENDER_INIT_SCRIPTS_REGISTRY: OnceLock>>> = - OnceLock::new(); - -fn get_render_init_scripts_registry() -> &'static Mutex>> { - RENDER_INIT_SCRIPTS_REGISTRY.get_or_init(|| Mutex::new(HashMap::new())) -} +// Initialization scripts are now injected into HTML responses via ResourceHandler #[derive(Clone)] pub struct Context { @@ -62,9 +54,6 @@ pub struct Context { pub next_webview_id: Arc, pub next_window_event_id: Arc, pub next_webview_event_id: Arc, - /// Initialization scripts stored per browser ID - /// Scripts are stored here and then registered to shared registry when browser is available - pub initialization_scripts: Arc>>>, } impl Context { @@ -95,10 +84,6 @@ wrap_app! { Some(AppBrowserProcessHandler::new(self.context.clone())) } - fn render_process_handler(&self) -> Option { - Some(AppRenderProcessHandler::new(self.context.clone())) - } - /// Called before the process starts to register custom schemes. /// This is where we mark schemes as fetch-enabled, secure, and CORS-enabled. fn on_register_custom_schemes(&self, registrar: Option<&mut SchemeRegistrar>) { @@ -122,153 +107,6 @@ wrap_app! { } } -wrap_render_process_handler! { - struct AppRenderProcessHandler { - context: Context, - } - - impl RenderProcessHandler { - /// Called when a process message is received from the browser process. - /// We use this to receive initialization scripts. - fn on_process_message_received( - &self, - _browser: Option<&mut Browser>, - _frame: Option<&mut Frame>, - source_process: ProcessId, - message: Option<&mut ProcessMessage>, - ) -> ::std::os::raw::c_int { - // Only handle messages from browser process - if source_process.as_ref() != &cef_dll_sys::cef_process_id_t::PID_BROWSER { - return 0; - } - - let Some(message) = message else { return 0 }; - - // Check if this is our initialization scripts message - let msg_name = message.name(); - let msg_name_str = { - let name = CefString::from(&msg_name).to_string(); - name - }; - - if msg_name_str != INIT_SCRIPTS_MESSAGE_NAME { - return 0; - } - - // Extract browser_id and scripts from message arguments - let Some(args) = message.argument_list() else { return 0 }; - - // First argument: browser_id (int) - let browser_id = args.int(0); - - // Second argument: scripts as JSON string - let scripts_json = { - let str_free = args.string(1); - CefString::from(&str_free).to_string() - }; - - // Deserialize scripts - if let Ok(serializable_scripts) = serde_json::from_str::>(&scripts_json) { - let scripts: Vec = serializable_scripts - .into_iter() - .map(InitializationScript::from) - .collect(); - get_render_init_scripts_registry() - .lock() - .unwrap() - .insert(browser_id, scripts); - } - - 1 // Message handled - } - - /// Called when a new V8 JavaScript context is created. - /// This happens: - /// - When the browser first loads a page - /// - When navigating to a new page (context is recreated) - /// - When navigating in iframes (each frame gets its own context) - /// - /// This ensures initialization scripts run before page scripts on every navigation. - fn on_context_created( - &self, - browser: Option<&mut Browser>, - frame: Option<&mut Frame>, - context: Option<&mut V8Context>, - ) { - let Some(frame) = frame else { return }; - let Some(context) = context else { return }; - let Some(browser) = browser else { return }; - - // Get browser ID to look up initialization scripts - let browser_id = browser.identifier(); - - // Check if this is the main frame - let is_main_frame = frame.is_main() == 1; - - // Get initialization scripts for this browser from render process registry - // Scripts are received via process messages from the browser process - let scripts = { - get_render_init_scripts_registry() - .lock() - .unwrap() - .get(&browser_id) - .cloned() - .unwrap_or_default() - }; - if scripts.is_empty() { - return; - } - - // Enter the V8 context so we can execute scripts synchronously - context.enter(); - - // Get the frame URL for proper script execution context - let frame_url = { - let url_free = frame.url(); - let url = CefString::from(&url_free).to_string(); - url - }; - - // Inject each initialization script directly into the V8 context - // Use V8Context::eval for synchronous execution - this runs immediately, - // before any page scripts can execute - for script in &scripts { - // If script is for main frame only, skip if this is not the main frame - if script.for_main_frame_only && !is_main_frame { - continue; - } - - // Execute the script synchronously in the V8 context - // This runs immediately before any page scripts can execute - let script_str = CefString::from(script.script.as_str()); - let url_str = CefString::from(frame_url.as_str()); - let mut retval = None; - let mut exception = None; - - // eval returns 1 on success, 0 on failure - let result = context.eval( - Some(&script_str), - Some(&url_str), - 0, - Some(&mut retval), - Some(&mut exception), - ); - - if result == 0 { - // Script execution failed - log exception if available - if let Some(exc) = exception { - let msg = exc.message(); - eprintln!("Failed to execute initialization script: {}", CefString::from(&msg).to_string()); - } - } - } - - // Exit the V8 context - context.exit(); - } - } -} - wrap_browser_process_handler! { struct AppBrowserProcessHandler { context: Context, @@ -283,108 +121,17 @@ wrap_browser_process_handler! { } wrap_client! { - struct BrowserClient { - context: Context, - pending_scripts: Vec, + struct BrowserClient { + initialization_scripts: Vec, } impl Client { fn request_handler(&self) -> Option { - Some(request_handler::WebRequestHandler::new()) - } - - fn load_handler(&self) -> Option { - Some(LoadHandlerImpl::new( - self.context.clone(), - self.pending_scripts.clone(), - Arc::new(Mutex::new(false)), + // Use pre-computed script hashes (computed once at webview creation) + Some(request_handler::WebRequestHandler::new( + self.initialization_scripts.clone(), )) } - - fn life_span_handler(&self) -> Option { - Some(LifeSpanHandlerImpl::new( - self.context.clone(), - self.pending_scripts.clone(), - )) - } - } -} - -wrap_life_span_handler! { - struct LifeSpanHandlerImpl { - context: Context, - pending_scripts: Vec, - } - - impl LifeSpanHandler { - /// Called after the browser is created. - /// Send initialization scripts to render process immediately so they're available - /// when OnContextCreated is called. - fn on_after_created(&self, browser: Option<&mut Browser>) { - if let Some(browser) = browser { - let browser_id = browser.identifier(); - - // Store scripts in Context (browser process) - if !self.pending_scripts.is_empty() { - // Store in Context for browser process access - self.context - .initialization_scripts - .lock() - .unwrap() - .insert(browser_id, self.pending_scripts.clone()); - - // Send scripts to render process via process message - // Do this as early as possible so scripts are available before OnContextCreated - if let Some(main_frame) = browser.main_frame() { - // Convert to serializable format and serialize to JSON - let serializable_scripts: Vec = self - .pending_scripts - .iter() - .map(SerializableInitScript::from) - .collect(); - - if let Ok(scripts_json) = serde_json::to_string(&serializable_scripts) { - // Create process message - let mut msg = process_message_create(Some(&CefString::from(INIT_SCRIPTS_MESSAGE_NAME))) - .expect("Failed to create process message"); - - // Set arguments: browser_id and scripts JSON - if let Some(args) = msg.argument_list() { - args.set_int(0, browser_id); - args.set_string(1, Some(&CefString::from(scripts_json.as_str()))); - - // Send to render process - main_frame.send_process_message( - ProcessId::from(cef_dll_sys::cef_process_id_t::PID_RENDERER), - Some(&mut msg), - ); - } - } - } - } - } - } - } -} - -wrap_load_handler! { - struct LoadHandlerImpl { - context: Context, - pending_scripts: Vec, - scripts_registered: Arc>, - } - - impl LoadHandler { - fn on_loading_state_change( - &self, - browser: Option<&mut Browser>, - _is_loading: ::std::os::raw::c_int, - _can_go_back: ::std::os::raw::c_int, - _can_go_forward: ::std::os::raw::c_int, - ) { - // Scripts are now sent in LifeSpanHandler::on_after_created (earlier), - // so we don't need to send them here anymore - } } } @@ -477,9 +224,15 @@ fn create_window( let webview = pending.webview.unwrap(); // Get initialization scripts from webview attributes - let initialization_scripts = webview.webview_attributes.initialization_scripts.clone(); + // Pre-compute script hashes once at webview creation time + let initialization_scripts: Vec<_> = webview + .webview_attributes + .initialization_scripts + .into_iter() + .map(CefInitScript::new) + .collect(); - let mut client = BrowserClient::new(context.clone(), initialization_scripts.clone()); + let mut client = BrowserClient::new(initialization_scripts.clone()); let url = CefString::from(webview.url.as_str()); let global_context = @@ -508,12 +261,11 @@ fn create_window( label, handler: Arc::new(handler) as Arc, response: Arc::new(RefCell::new(None)), + initialization_scripts: Some(initialization_scripts.clone()), }, )), ); } - } else { - eprintln!("failed to create context"); } let browser_view = browser_view_create( diff --git a/crates/tauri-runtime-cef/src/cef_impl/request_handler.rs b/crates/tauri-runtime-cef/src/cef_impl/request_handler.rs index 2841a2ef7..e8420b247 100644 --- a/crates/tauri-runtime-cef/src/cef_impl/request_handler.rs +++ b/crates/tauri-runtime-cef/src/cef_impl/request_handler.rs @@ -6,39 +6,290 @@ use std::{ }; use cef::{rc::*, *}; +use html5ever::{interface::QualName, namespace_url, ns, LocalName}; use http::{header::CONTENT_TYPE, HeaderMap, HeaderName, HeaderValue}; -use tauri_runtime::webview::UriSchemeProtocol; +use kuchiki::NodeRef; +use tauri_runtime::webview::{InitializationScript, UriSchemeProtocol}; +use tauri_utils::{ + config::{Csp, CspDirectiveSources}, + html::{parse as parse_html, serialize_node}, +}; use url::Url; -wrap_resource_request_handler! { - pub struct WebResourceRequestHandler; +use super::CefInitScript; - impl ResourceRequestHandler { - fn resource_handler( - &self, - browser: Option<&mut Browser>, - frame: Option<&mut Frame>, - request: Option<&mut Request>, - ) -> Option { - None +// Note: We handle head element manipulation inline in the filter + +// ResponseFilter that injects initialization scripts into HTML responses +// For HTTP/HTTPS with CSP headers, we inject a modified CSP meta tag +wrap_response_filter! { + pub struct HtmlScriptInjectionFilter { + initialization_scripts: Vec, + script_hashes: Vec, // Pre-computed script hashes + csp_header: Option, // Original CSP header from HTTP response (if any) + processed_html: RefCell>>, + output_offset: RefCell, + } + + impl ResponseFilter { + fn init_filter(&self) -> ::std::os::raw::c_int { + // Return 1 to enable buffered mode (RESPONSE_FILTER_NEED_MORE_DATA) + 1 } + fn filter( + &self, + data_in: Option<&mut Vec>, + data_in_read: Option<&mut usize>, + data_out: Option<&mut Vec>, + data_out_written: Option<&mut usize>, + ) -> ResponseFilterStatus { + use cef_dll_sys::cef_response_filter_status_t::*; + + if let Some(data_in) = data_in { + let input_size = data_in.len(); + + // Process the HTML once (on first chunk) + if self.processed_html.borrow().is_none() { + if let Ok(html_str) = String::from_utf8(data_in.clone()) { + let document = parse_html(html_str); + + 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 + }; + + // If CSP header exists, inject/modify CSP meta tag with script hashes + // This ensures injected scripts work even when HTTP response has CSP header + if let Some(ref original_csp) = self.csp_header { + // Parse CSP using tauri-utils + let mut csp_map: std::collections::HashMap = + Csp::Policy(original_csp.clone()).into(); + + // Update or create script-src directive with script hashes + let script_src = csp_map + .entry("script-src".to_string()) + .or_insert_with(|| CspDirectiveSources::List(vec!["'self'".to_string()])); + + // Extend with script hashes + script_src.extend(self.script_hashes.clone()); + + // Convert back to CSP string + let updated_csp = Csp::DirectiveMap(csp_map).to_string(); + // Check if CSP meta tag already exists + let should_update_meta = if let Ok(ref meta_node) = document.select_first("meta[http-equiv='Content-Security-Policy']") { + let element = meta_node.as_node().as_element().unwrap(); + let mut attrs = element.attributes.borrow_mut(); + attrs.insert("content", updated_csp.clone()); + false // Updated existing meta tag, don't create new one + } else { + true // Need to create new meta tag + }; + + if should_update_meta { + use kuchiki::{Attribute, ExpandedName}; + let csp_meta = NodeRef::new_element( + QualName::new(None, ns!(html), LocalName::from("meta")), + vec![ + ( + ExpandedName::new(ns!(), LocalName::from("http-equiv")), + Attribute { + prefix: None, + value: "Content-Security-Policy".into(), + }, + ), + ( + ExpandedName::new(ns!(), LocalName::from("content")), + Attribute { + prefix: None, + value: updated_csp.into(), + }, + ), + ], + ); + head.prepend(csp_meta); + } + } + + // iterate in reverse order since we are prepending each script to the head tag + for init_script in self.initialization_scripts.iter().rev() { + 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); + } + + // Serialize the modified HTML + let modified_html = serialize_node(&document); + *self.processed_html.borrow_mut() = Some(modified_html); + } else { + // Not valid UTF-8, pass through unchanged + *self.processed_html.borrow_mut() = Some(data_in.clone()); + } + } + + // Mark all input as read (CEF requirement: must read all input) + if let Some(data_in_read) = data_in_read { + *data_in_read = input_size; + } + } + + if let Some(data_out) = data_out { + if let Some(processed) = self.processed_html.borrow().as_ref() { + let offset = *self.output_offset.borrow(); + let remaining = processed.len().saturating_sub(offset); + + if remaining > 0 { + let buffer_size = data_out.capacity(); + let to_write = remaining.min(buffer_size); + + data_out.clear(); + data_out.extend_from_slice(&processed[offset..offset + to_write]); + + if let Some(written) = data_out_written { + *written = to_write; + } + + *self.output_offset.borrow_mut() += to_write; + + if *self.output_offset.borrow() >= processed.len() { + RESPONSE_FILTER_DONE.into() + } else { + RESPONSE_FILTER_NEED_MORE_DATA.into() + } + } else { + // No remaining data to write + RESPONSE_FILTER_DONE.into() + } + } else { + // No processed HTML yet - need more input data + RESPONSE_FILTER_NEED_MORE_DATA.into() + } + } else { + // No output buffer provided + // If we have input, we've already processed it, so we're done + // If we don't have input, this is a final call and we're done + RESPONSE_FILTER_DONE.into() + } + } + } +} + +wrap_resource_request_handler! { + pub struct WebResourceRequestHandler { + initialization_scripts: Vec, + } + + impl ResourceRequestHandler { + // Store CSP header from on_resource_response for use in filter fn on_resource_response( &self, - browser: Option<&mut Browser>, + _browser: Option<&mut Browser>, + _frame: Option<&mut Frame>, + _request: Option<&mut Request>, + _response: Option<&mut Response>, + ) -> ::std::os::raw::c_int { + // Response is read-only here, but we can read the CSP header + // and it will be available in resource_response_filter + 0 + } + + fn resource_response_filter( + &self, + _browser: Option<&mut Browser>, frame: Option<&mut Frame>, request: Option<&mut Request>, response: Option<&mut Response>, - ) -> ::std::os::raw::c_int { - Default::default() + ) -> Option { + let Some(response) = response else { + return None; + }; + + // Skip DevTools URLs - they use internal resources that shouldn't be modified + if let Some(request) = request { + let url = CefString::from(&request.url()).to_string(); + if url.starts_with("devtools://") { + return None; + } + } + + let content_type = response.mime_type(); + let content_type_str = CefString::from(&content_type).to_string().to_lowercase(); + + // Only process HTML responses + if !content_type_str.starts_with("text/html") { + return None; + } + + let Some(frame) = frame else { + return None; + }; + + // Extract CSP header if present + let csp_header = { + let csp_header_name = CefString::from("Content-Security-Policy"); + let existing_csp = response.header_by_name(Some(&csp_header_name)); + // check if csp_header is not null manually - I believe header_by_name should return Option instead + let csp_header: Option<&cef_dll_sys::_cef_string_utf16_t> = (&existing_csp).into(); + if csp_header.is_some() { + Some(CefString::from(&existing_csp).to_string()) + } else { + None + } + }; + + let is_main_frame = frame.is_main() == 1; + + // Filter scripts based on frame type + let scripts_to_inject: Vec<_> = if is_main_frame { + self.initialization_scripts.iter().map(|s| s.script.clone()).collect() + } else { + self.initialization_scripts + .iter() + .filter(|s| !s.script.for_main_frame_only) + .map(|s| s.script.clone()) + .collect() + }; + + if scripts_to_inject.is_empty() { + return None; + } + + // Get pre-computed hashes for the scripts + let script_hashes: Vec = if is_main_frame { + self.initialization_scripts.iter().map(|s| s.hash.clone()).collect() + } else { + self.initialization_scripts + .iter() + .filter(|s| !s.script.for_main_frame_only) + .map(|s| s.hash.clone()) + .collect() + }; + + // Return a filter that will inject scripts and update CSP in HTML if header exists + Some(HtmlScriptInjectionFilter::new( + scripts_to_inject, + script_hashes, + csp_header, + RefCell::new(None), + RefCell::new(0), + )) } fn on_before_resource_load( &self, - browser: Option<&mut Browser>, - frame: Option<&mut Frame>, - request: Option<&mut Request>, - callback: Option<&mut Callback>, + _browser: Option<&mut Browser>, + _frame: Option<&mut Frame>, + _request: Option<&mut Request>, + _callback: Option<&mut Callback>, ) -> ReturnValue { sys::cef_return_value_t::RV_CONTINUE.into() } @@ -46,20 +297,24 @@ wrap_resource_request_handler! { } wrap_request_handler! { - pub struct WebRequestHandler; + pub struct WebRequestHandler { + initialization_scripts: Vec, + } impl RequestHandler { 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>, + _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 { - Some(WebResourceRequestHandler::new()) + Some(WebResourceRequestHandler::new( + self.initialization_scripts.clone(), + )) } } } @@ -118,7 +373,7 @@ wrap_resource_handler! { 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>, + _callback: Option<&mut ResourceReadCallback>, ) -> ::std::os::raw::c_int { let Ok(bytes_to_read) = usize::try_from(bytes_to_read) else { return 0; @@ -147,16 +402,79 @@ wrap_resource_handler! { response.set_status(response_data.status().as_u16() as i32); let mut content_type = None; + let mut csp_header: Option = None; + // First pass: collect CSP header and set other headers 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.as_str().eq_ignore_ascii_case("content-security-policy") { + csp_header = Some(value.to_string()); + } else { + response.set_header_by_name(Some(&name.as_str().into()), Some(&value.into()), 0); + } if name == CONTENT_TYPE { content_type.replace(value.into()); } } + // Update CSP header with script hashes for custom schemes + if let Some(ref initialization_scripts) = self.context.initialization_scripts { + if !initialization_scripts.is_empty() { + let scripts_to_include: Vec<_> = initialization_scripts + .iter() + .filter(|s| !s.script.for_main_frame_only) + .cloned() + .collect(); + + if !scripts_to_include.is_empty() { + let script_hashes: Vec = scripts_to_include + .iter() + .map(|s| s.hash.clone()) + .collect(); + + let csp_header_name = CefString::from("Content-Security-Policy"); + let new_csp = if let Some(existing_csp) = csp_header { + // Parse CSP using tauri-utils + let mut csp_map: std::collections::HashMap = + Csp::Policy(existing_csp).into(); + + // Update or create script-src directive with script hashes + let script_src = csp_map + .entry("script-src".to_string()) + .or_insert_with(|| CspDirectiveSources::List(vec!["'self'".to_string()])); + + // Extend with script hashes + script_src.extend(script_hashes); + + // Convert back to CSP string + Csp::DirectiveMap(csp_map).to_string() + } else { + // No existing CSP, create new one with just script-src + let mut csp_map = std::collections::HashMap::new(); + let mut script_src = CspDirectiveSources::List(vec!["'self'".to_string()]); + script_src.extend(script_hashes); + csp_map.insert("script-src".to_string(), script_src); + Csp::DirectiveMap(csp_map).to_string() + }; + + response.set_header_by_name( + Some(&csp_header_name), + Some(&CefString::from(new_csp.as_str())), + 1, // overwrite + ); + } + } + } else if let Some(csp) = csp_header { + // No scripts to inject, just copy the original CSP header + response.set_header_by_name( + Some(&CefString::from("Content-Security-Policy")), + Some(&CefString::from(csp.as_str())), + 0, + ); + } + response.set_mime_type(Some(&content_type.unwrap_or_else(|| "text/plain".into()))); response_length.map(|length| { @@ -178,10 +496,10 @@ wrap_scheme_handler_factory! { impl SchemeHandlerFactory { fn create( &self, - browser: Option<&mut Browser>, - frame: Option<&mut Frame>, - scheme_name: Option<&CefString>, - request: Option<&mut Request>, + _browser: Option<&mut Browser>, + _frame: Option<&mut Frame>, + _scheme_name: Option<&CefString>, + _request: Option<&mut Request>, ) -> Option { Some(WebResourceHandler::new(self.context.clone())) } @@ -193,6 +511,7 @@ pub struct UriSchemeContext { pub label: String, pub handler: Arc, pub response: Arc>>>>>, + pub initialization_scripts: Option>, } struct ThreadSafe(T); diff --git a/crates/tauri-runtime-cef/src/lib.rs b/crates/tauri-runtime-cef/src/lib.rs index 8934bdcb3..2c78a9818 100644 --- a/crates/tauri-runtime-cef/src/lib.rs +++ b/crates/tauri-runtime-cef/src/lib.rs @@ -1082,7 +1082,6 @@ impl CefRuntime { next_webview_id: Default::default(), next_window_id: Default::default(), next_window_event_id: Default::default(), - initialization_scripts: Arc::new(Mutex::new(HashMap::new())), }; let mut app = cef_impl::TauriApp::new(cef_context.clone()); diff --git a/crates/tauri/scripts/ipc-protocol.js b/crates/tauri/scripts/ipc-protocol.js index ebdfc01a1..9ca244053 100644 --- a/crates/tauri/scripts/ipc-protocol.js +++ b/crates/tauri/scripts/ipc-protocol.js @@ -22,7 +22,6 @@ function sendIpcMessage(message) { const { cmd, callback, error, payload, options } = message - console.log(customProtocolIpcFailed, canUseCustomProtocol, cmd) if ( !customProtocolIpcFailed && (canUseCustomProtocol || cmd === fetchChannelDataCommand) @@ -35,104 +34,39 @@ headers.set('Tauri-Error', error) headers.set('Tauri-Invoke-Key', __TAURI_INVOKE_KEY__) - // For CEF runtime, try XMLHttpRequest first as it may populate PostData correctly - // Fetch API has a known issue with custom schemes in CEF - XMLHttpRequest may work - // Detect CEF: check for CEF-specific properties or user agent - const useXHR = typeof window.cef !== 'undefined' || - navigator.userAgent.includes('CEF') || - navigator.userAgent.includes('Chromium Embedded') - - if (useXHR) { - // Try XMLHttpRequest for CEF - it may handle POST bodies to custom schemes correctly - try { - const xhr = new XMLHttpRequest() - const url = window.__TAURI_INTERNALS__.convertFileSrc(cmd, 'ipc') - - xhr.open('POST', url, true) - - // Set headers - headers.forEach((value, key) => { - xhr.setRequestHeader(key, value) - }) - - xhr.onload = function () { - const callbackId = xhr.getResponseHeader('Tauri-Response') === 'ok' ? callback : error - const contentType = (xhr.getResponseHeader('content-type') || '').split(',')[0] - - let responseData - switch (contentType) { - case 'application/json': - try { - responseData = JSON.parse(xhr.responseText) - } catch { - responseData = xhr.responseText - } - break - case 'text/plain': - responseData = xhr.responseText - break - default: - responseData = xhr.response - break - } - - window.__TAURI_INTERNALS__.runCallback(callbackId, responseData) + fetch(window.__TAURI_INTERNALS__.convertFileSrc(cmd, 'ipc'), { + method: 'POST', + body: data, + headers + }) + .then((response) => { + const callbackId = + response.headers.get('Tauri-Response') === 'ok' ? callback : error + // we need to split here because on Android the content-type gets duplicated + switch ((response.headers.get('content-type') || '').split(',')[0]) { + case 'application/json': + return response.json().then((r) => [callbackId, r]) + case 'text/plain': + return response.text().then((r) => [callbackId, r]) + default: + return response.arrayBuffer().then((r) => [callbackId, r]) } - - xhr.onerror = function () { - console.warn( - 'IPC custom protocol failed with XMLHttpRequest, trying Fetch API', - xhr.status - ) - // Fall through to Fetch API - sendWithFetch() - } - - xhr.send(data) - return - } catch (e) { - console.warn('XMLHttpRequest failed, falling back to Fetch API', e) - // Fall through to Fetch API - } - } - - function sendWithFetch() { - fetch(window.__TAURI_INTERNALS__.convertFileSrc(cmd, 'ipc'), { - method: 'POST', - body: data, - headers }) - .then((response) => { - const callbackId = - response.headers.get('Tauri-Response') === 'ok' ? callback : error - // we need to split here because on Android the content-type gets duplicated - switch ((response.headers.get('content-type') || '').split(',')[0]) { - case 'application/json': - return response.json().then((r) => [callbackId, r]) - case 'text/plain': - return response.text().then((r) => [callbackId, r]) - default: - return response.arrayBuffer().then((r) => [callbackId, r]) - } - }) - .then( - ([callbackId, data]) => { - window.__TAURI_INTERNALS__.runCallback(callbackId, data) - }, - (e) => { - console.warn( - 'IPC custom protocol failed, Tauri will now use the postMessage interface instead', - e - ) - // failed to use the custom protocol IPC (either the webview blocked a custom protocol or it was a CSP error) - // so we need to fallback to the postMessage interface - customProtocolIpcFailed = true - sendIpcMessage(message) - } - ) - } - - sendWithFetch() + .then( + ([callbackId, data]) => { + window.__TAURI_INTERNALS__.runCallback(callbackId, data) + }, + (e) => { + console.warn( + 'IPC custom protocol failed, Tauri will now use the postMessage interface instead', + e + ) + // failed to use the custom protocol IPC (either the webview blocked a custom protocol or it was a CSP error) + // so we need to fallback to the postMessage interface + customProtocolIpcFailed = true + sendIpcMessage(message) + } + ) } else { // otherwise use the postMessage interface const { data } = processIpcMessage({