diff --git a/Cargo.lock b/Cargo.lock index 5cce5f235..c20522b4c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9016,6 +9016,8 @@ dependencies = [ "gtk", "http 1.3.1", "raw-window-handle", + "serde", + "serde_json", "tauri-runtime", "tauri-utils", "url", diff --git a/crates/tauri-runtime-cef/Cargo.toml b/crates/tauri-runtime-cef/Cargo.toml index b6f9c7f3d..aabff5d35 100644 --- a/crates/tauri-runtime-cef/Cargo.toml +++ b/crates/tauri-runtime-cef/Cargo.toml @@ -17,6 +17,8 @@ url = "2" http = "1" cef = "141.6.0" cef-dll-sys = "141.6.0" +serde = { version = "1", features = ["derive"] } +serde_json = "1" [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 2ebfb4f66..09d4c3fd3 100644 --- a/crates/tauri-runtime-cef/src/cef_impl.rs +++ b/crates/tauri-runtime-cef/src/cef_impl.rs @@ -4,11 +4,11 @@ use std::{ collections::HashMap, sync::{ atomic::{AtomicU32, Ordering}, - Arc, + Arc, Mutex, OnceLock, }, }; use tauri_runtime::{ - webview::UriSchemeProtocol, + webview::{InitializationScript, UriSchemeProtocol}, window::{PendingWindow, WindowId}, RunEvent, UserEvent, }; @@ -17,6 +17,43 @@ 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, +} + +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 From for InitializationScript { + fn from(script: SerializableInitScript) -> Self { + Self { + script: script.script, + for_main_frame_only: script.for_main_frame_only, + } + } +} + +// 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())) +} + #[derive(Clone)] pub struct Context { pub windows: Arc>>, @@ -25,6 +62,9 @@ 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 { @@ -54,6 +94,178 @@ wrap_app! { fn browser_process_handler(&self) -> Option { 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>) { + if let Some(registrar) = registrar { + // Standard CEF scheme options for custom protocols: + // - FETCH_ENABLED: Allows Fetch API requests + // - SECURE: Treats as secure like https (no mixed content warnings) + // - CORS_ENABLED: Allows CORS requests + // - STANDARD: Standard URL scheme behavior + let scheme_options = (cef_dll_sys::cef_scheme_options_t::CEF_SCHEME_OPTION_FETCH_ENABLED as i32) + | (cef_dll_sys::cef_scheme_options_t::CEF_SCHEME_OPTION_SECURE as i32) + | (cef_dll_sys::cef_scheme_options_t::CEF_SCHEME_OPTION_CORS_ENABLED as i32) + | (cef_dll_sys::cef_scheme_options_t::CEF_SCHEME_OPTION_STANDARD as i32); + + for scheme in ["ipc", "tauri"] { + let scheme_name = CefString::from(scheme); + registrar.add_custom_scheme(Some(&scheme_name), scheme_options); + } + } + } + } +} + +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(); + } } } @@ -71,12 +283,108 @@ wrap_browser_process_handler! { } wrap_client! { - struct BrowserClient; + struct BrowserClient { + context: Context, + pending_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)), + )) + } + + 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 + } } } @@ -168,7 +476,10 @@ fn create_window( let webview = pending.webview.unwrap(); - let mut client = BrowserClient::new(); + // Get initialization scripts from webview attributes + let initialization_scripts = webview.webview_attributes.initialization_scripts.clone(); + + let mut client = BrowserClient::new(context.clone(), initialization_scripts.clone()); let url = CefString::from(webview.url.as_str()); let global_context = @@ -183,6 +494,10 @@ fn create_window( Option::<&mut RequestContextHandler>::None, ); if let Some(request_context) = &request_context { + // Ensure schemes are registered with proper flags (fetch-enabled, secure, etc.) + // This is done in App::on_register_custom_schemes, but we also track + // which schemes we've seen + for (scheme, handler) in webview.uri_scheme_protocols { let label = label.clone(); request_context.register_scheme_handler_factory( 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 899997ea1..5d8253ab6 100644 --- a/crates/tauri-runtime-cef/src/cef_impl/request_handler.rs +++ b/crates/tauri-runtime-cef/src/cef_impl/request_handler.rs @@ -95,11 +95,15 @@ wrap_resource_handler! { let label = self.context.label.clone(); let handler = self.context.handler.clone(); + let data = read_request_body(request); let headers = get_request_headers(request); + 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().uri(url.as_str()).body(data).unwrap(); + 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); }); diff --git a/crates/tauri-runtime-cef/src/lib.rs b/crates/tauri-runtime-cef/src/lib.rs index a50f35fe5..8934bdcb3 100644 --- a/crates/tauri-runtime-cef/src/lib.rs +++ b/crates/tauri-runtime-cef/src/lib.rs @@ -1082,6 +1082,7 @@ 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()); @@ -1097,11 +1098,8 @@ impl CefRuntime { ); if is_browser_process { - println!("launch browser process"); assert!(ret == -1, "cannot execute browser process"); } else { - let process_type = CefString::from(&cmd.switch_value(Some(&switch))); - println!("launch process {process_type}"); assert!(ret >= 0, "cannot execute non-browser process"); // non-browser process does not initialize cef std::process::exit(0); diff --git a/crates/tauri/scripts/ipc-protocol.js b/crates/tauri/scripts/ipc-protocol.js index 9ca244053..ebdfc01a1 100644 --- a/crates/tauri/scripts/ipc-protocol.js +++ b/crates/tauri/scripts/ipc-protocol.js @@ -22,6 +22,7 @@ function sendIpcMessage(message) { const { cmd, callback, error, payload, options } = message + console.log(customProtocolIpcFailed, canUseCustomProtocol, cmd) if ( !customProtocolIpcFailed && (canUseCustomProtocol || cmd === fetchChannelDataCommand) @@ -34,39 +35,104 @@ headers.set('Tauri-Error', error) headers.set('Tauri-Invoke-Key', __TAURI_INVOKE_KEY__) - 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]) + // 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) } - }) - .then( - ([callbackId, data]) => { - window.__TAURI_INTERNALS__.runCallback(callbackId, data) - }, - (e) => { + + xhr.onerror = function () { console.warn( - 'IPC custom protocol failed, Tauri will now use the postMessage interface instead', - e + 'IPC custom protocol failed with XMLHttpRequest, trying Fetch API', + xhr.status ) - // 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) + // 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() } else { // otherwise use the postMessage interface const { data } = processIpcMessage({