diff --git a/crates/tauri-runtime-cef/src/cef_impl.rs b/crates/tauri-runtime-cef/src/cef_impl.rs index 50d094cd6..2485a68d9 100644 --- a/crates/tauri-runtime-cef/src/cef_impl.rs +++ b/crates/tauri-runtime-cef/src/cef_impl.rs @@ -120,6 +120,70 @@ wrap_browser_process_handler! { } } +wrap_load_handler! { + struct BrowserLoadHandler { + initialization_scripts: Vec, + } + + impl LoadHandler { + fn on_load_end( + &self, + browser: Option<&mut Browser>, + frame: Option<&mut Frame>, + http_status_code: ::std::os::raw::c_int, + ) { + // Only execute scripts for successful loads (200-299) + if http_status_code < 200 || http_status_code >= 300 { + return; + } + + let Some(frame) = frame else { return }; + + // Get the URL to check if it's a remote URL + let url = frame.url(); + let url_str = cef::CefString::from(&url).to_string(); + let url_obj = url::Url::parse(&url_str).ok(); + + // Only execute scripts for remote URLs (http/https) + // Custom schemes use HTML injection + let is_remote_url = url_obj + .as_ref() + .map(|u| matches!(u.scheme(), "http" | "https")) + .unwrap_or(false); + + if !is_remote_url { + return; + } + + let is_main_frame = frame.is_main() == 1; + + // Filter scripts based on frame type + let scripts_to_execute: Vec<_> = if is_main_frame { + self.initialization_scripts.clone() + } else { + self.initialization_scripts + .iter() + .filter(|s| !s.script.for_main_frame_only) + .cloned() + .collect() + }; + + // Execute each script via frame.execute_java_script + for script in scripts_to_execute { + let script_text = script.script.script.clone(); + let script_url = format!("{}://__tauri_init_script__", url_obj.as_ref().map(|u| u.scheme()).unwrap_or("http")); + + // Execute JavaScript in the frame + frame.execute_java_script( + Some(&cef::CefString::from(script_text.as_str())), + Some(&cef::CefString::from(script_url.as_str())), + 0, + ); + } + } + } +} + wrap_client! { struct BrowserClient { initialization_scripts: Vec, @@ -132,6 +196,12 @@ wrap_client! { self.initialization_scripts.clone(), )) } + + fn load_handler(&self) -> Option { + Some(BrowserLoadHandler::new( + self.initialization_scripts.clone(), + )) + } } } 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 bb67ab801..691f0df30 100644 --- a/crates/tauri-runtime-cef/src/cef_impl/request_handler.rs +++ b/crates/tauri-runtime-cef/src/cef_impl/request_handler.rs @@ -21,112 +21,88 @@ use url::Url; use super::CefInitScript; -// ResponseFilter that injects initialization scripts into HTML responses -wrap_response_filter! { - pub struct HtmlScriptInjectionFilter { - initialization_scripts: Vec, - processed_html: RefCell>>, - output_offset: RefCell, +fn csp_inject_initialization_scripts_hashes( + existing_csp: Option<&str>, + initialization_scripts: Option<&[CefInitScript]>, +) -> Option { + let Some(scripts) = initialization_scripts else { + return existing_csp.map(|s| s.to_string()); + }; + + if scripts.is_empty() { + return existing_csp.map(|s| s.to_string()); } - impl ResponseFilter { - fn init_filter(&self) -> ::std::os::raw::c_int { - // Return 1 to enable buffered mode (RESPONSE_FILTER_NEED_MORE_DATA) - 1 - } + // For custom schemes, include ALL script hashes (we inject all scripts into HTML) + // This matches the HTML injection behavior in inject_scripts_into_html_body + let script_hashes: Vec = scripts.iter().map(|s| s.hash.clone()).collect(); - 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 script_hashes.is_empty() { + return existing_csp.map(|s| s.to_string()); + } - if let Some(data_in) = data_in { - let input_size = data_in.len(); + let new_csp = if let Some(existing_csp) = existing_csp { + // Parse CSP using tauri-utils + let mut csp_map: std::collections::HashMap = + Csp::Policy(existing_csp.to_string()).into(); - // 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); + // 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()])); - 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 - }; + // Extend with script hashes + script_src.extend(script_hashes); - // 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.script.as_str())); - head.prepend(script_el); - } + // 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() + }; - // 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()); - } - } + Some(new_csp) +} - // 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; - } - } +/// Helper function to inject initialization scripts into HTML body +fn inject_scripts_into_html_body( + body: &[u8], + initialization_scripts: Option<&[CefInitScript]>, +) -> Option> { + // Check if body is valid UTF-8 HTML + let Ok(body_str) = std::str::from_utf8(body) else { + return None; + }; - 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); + // Parse HTML and inject scripts + let document = parse_html(body_str.to_string()); - if remaining > 0 { - let buffer_size = data_out.capacity(); - let to_write = remaining.min(buffer_size); + 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 + }; - 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() - } + // Inject initialization scripts (for custom schemes, inject all scripts) + if let Some(scripts) = initialization_scripts { + for init_script in 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.script.as_str())); + head.prepend(script_el); } } + + // Serialize the modified HTML + Some(serialize_node(&document)) } wrap_resource_request_handler! { @@ -135,85 +111,7 @@ wrap_resource_request_handler! { } impl ResourceRequestHandler { - // Store CSP header from on_resource_response for use in filter - fn on_resource_response( - &self, - _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>, - ) -> 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; - }; - - 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.clone() - } else { - self.initialization_scripts - .iter() - .filter(|s| !s.script.for_main_frame_only) - .cloned() - .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, - RefCell::new(None), - RefCell::new(0), - )) - } fn on_before_resource_load( &self, @@ -271,10 +169,29 @@ wrap_resource_handler! { let callback = ThreadSafe(callback.clone()); // TODO: thread safety let response_store = ThreadSafe(self.context.response.clone()); + let initialization_scripts = self.context.initialization_scripts.clone(); let responder = Box::new(move |response: http::Response>| { + // Check if this is an HTML response that needs script injection + let content_type = response.headers().get(CONTENT_TYPE); + let is_html = 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 response = http::Response::from_parts(parts, Cursor::new(body.into_owned())); + let body_bytes = body.into_owned(); + + // Inject scripts into HTML body if applicable + let modified_body = if is_html { + inject_scripts_into_html_body(&body_bytes, initialization_scripts.as_deref()) + .unwrap_or(body_bytes) + } else { + body_bytes + }; + + let response = http::Response::from_parts(parts, Cursor::new(modified_body)); response_store.into_owned().borrow_mut().replace(response); + let callback = callback.into_owned(); callback.cont(); }); @@ -350,59 +267,17 @@ wrap_resource_handler! { } } - // 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(); + let new_csp = csp_inject_initialization_scripts_hashes( + csp_header.as_deref(), + self.context.initialization_scripts.as_deref(), + ); - 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.as_str()); - 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 + if let Some(new_csp) = new_csp { + let csp_header_name = CefString::from(CONTENT_SECURITY_POLICY.as_str()); response.set_header_by_name( - Some(&CefString::from(CONTENT_SECURITY_POLICY.as_str())), - Some(&CefString::from(csp.as_str())), - 0, + Some(&csp_header_name), + Some(&CefString::from(new_csp.as_str())), + 0, // overwrite ); }