run init_script before app loads

This commit is contained in:
Lucas Nogueira
2025-11-01 13:27:40 -03:00
parent def097e278
commit 28bcd6f86e
6 changed files with 430 additions and 416 deletions

4
Cargo.lock generated
View File

@@ -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",

View File

@@ -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"] }

View File

@@ -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<SerializableInitScript> 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<Mutex<HashMap<i32, Vec<InitializationScript>>>> =
OnceLock::new();
fn get_render_init_scripts_registry() -> &'static Mutex<HashMap<i32, Vec<InitializationScript>>> {
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<T: UserEvent> {
@@ -62,9 +54,6 @@ pub struct Context<T: UserEvent> {
pub next_webview_id: Arc<AtomicU32>,
pub next_window_event_id: Arc<AtomicU32>,
pub next_webview_event_id: Arc<AtomicU32>,
/// Initialization scripts stored per browser ID
/// Scripts are stored here and then registered to shared registry when browser is available
pub initialization_scripts: Arc<Mutex<HashMap<i32, Vec<InitializationScript>>>>,
}
impl<T: UserEvent> Context<T> {
@@ -95,10 +84,6 @@ wrap_app! {
Some(AppBrowserProcessHandler::new(self.context.clone()))
}
fn render_process_handler(&self) -> Option<RenderProcessHandler> {
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<T: UserEvent> {
context: Context<T>,
}
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::<Vec<SerializableInitScript>>(&scripts_json) {
let scripts: Vec<InitializationScript> = 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<T: UserEvent> {
context: Context<T>,
@@ -283,108 +121,17 @@ wrap_browser_process_handler! {
}
wrap_client! {
struct BrowserClient<T: UserEvent> {
context: Context<T>,
pending_scripts: Vec<InitializationScript>,
struct BrowserClient {
initialization_scripts: Vec<CefInitScript>,
}
impl Client {
fn request_handler(&self) -> Option<RequestHandler> {
Some(request_handler::WebRequestHandler::new())
}
fn load_handler(&self) -> Option<LoadHandler> {
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<LifeSpanHandler> {
Some(LifeSpanHandlerImpl::new(
self.context.clone(),
self.pending_scripts.clone(),
))
}
}
}
wrap_life_span_handler! {
struct LifeSpanHandlerImpl<T: UserEvent> {
context: Context<T>,
pending_scripts: Vec<InitializationScript>,
}
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<SerializableInitScript> = 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<T: UserEvent> {
context: Context<T>,
pending_scripts: Vec<InitializationScript>,
scripts_registered: Arc<Mutex<bool>>,
}
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<T: UserEvent>(
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<T: UserEvent>(
label,
handler: Arc::new(handler) as Arc<UriSchemeProtocol>,
response: Arc::new(RefCell::new(None)),
initialization_scripts: Some(initialization_scripts.clone()),
},
)),
);
}
} else {
eprintln!("failed to create context");
}
let browser_view = browser_view_create(

View File

@@ -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<ResourceHandler> {
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<InitializationScript>,
script_hashes: Vec<String>, // Pre-computed script hashes
csp_header: Option<String>, // Original CSP header from HTTP response (if any)
processed_html: RefCell<Option<Vec<u8>>>,
output_offset: RefCell<usize>,
}
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<u8>>,
data_in_read: Option<&mut usize>,
data_out: Option<&mut Vec<u8>>,
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<String, CspDirectiveSources> =
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<CefInitScript>,
}
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<ResponseFilter> {
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<String> = 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<CefInitScript>,
}
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<ResourceRequestHandler> {
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<String> = 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<String> = 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<String, CspDirectiveSources> =
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<ResourceHandler> {
Some(WebResourceHandler::new(self.context.clone()))
}
@@ -193,6 +511,7 @@ pub struct UriSchemeContext {
pub label: String,
pub handler: Arc<UriSchemeProtocol>,
pub response: Arc<RefCell<Option<http::Response<Cursor<Vec<u8>>>>>>,
pub initialization_scripts: Option<Vec<CefInitScript>>,
}
struct ThreadSafe<T>(T);

View File

@@ -1082,7 +1082,6 @@ impl<T: UserEvent> CefRuntime<T> {
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());

View File

@@ -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({