mirror of
https://github.com/tauri-apps/tauri.git
synced 2026-04-03 10:11:15 +02:00
run init_script before app loads
This commit is contained in:
4
Cargo.lock
generated
4
Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user