From 8ae93b9c76b2efe14e93febd009e31fc459275a8 Mon Sep 17 00:00:00 2001 From: Amr Bashir Date: Sun, 1 Jan 2023 00:45:43 +0200 Subject: [PATCH] feat: add headers when loading URLs, closes #816 (#826) * feat: add `load_url_with_headers`, close #816 * windows * android * macOS * fix macos header usage * Restore examples/custom_titlebar.rs * remove duplication * add kotlin code * cleanup duplicated code * refactor signature * fix examples * Revert "fix examples" This reverts commit de22b48449f20bf104b498f8e41ff460685d2776. * Revert "refactor signature" This reverts commit c9e1a61841133b80b5d617ce471f69812ec31916. * rename to `with_url_and_headers` Co-authored-by: Lucas Nogueira --- .changes/load_url_with_headers.md | 5 +++ src/webview/android/binding.rs | 19 +------- src/webview/android/kotlin/RustWebView.kt | 7 +++ src/webview/android/main_pipe.rs | 52 +++++++++++++++------- src/webview/android/mod.rs | 24 +++++++++- src/webview/mod.rs | 21 +++++++-- src/webview/webkitgtk/mod.rs | 24 +++++++--- src/webview/webkitgtk/web_context.rs | 37 +++++++++++----- src/webview/webview2/mod.rs | 54 +++++++++++++++++++++-- src/webview/wkwebview/mod.rs | 23 ++++++++-- 10 files changed, 203 insertions(+), 63 deletions(-) create mode 100644 .changes/load_url_with_headers.md diff --git a/.changes/load_url_with_headers.md b/.changes/load_url_with_headers.md new file mode 100644 index 000000000..6be3ad2e7 --- /dev/null +++ b/.changes/load_url_with_headers.md @@ -0,0 +1,5 @@ +--- +"wry": "patch" +--- + +Add `WebViewBuilder::with_headers` and `WebView::load_url_with_headers` to navigate to urls with headers. \ No newline at end of file diff --git a/src/webview/android/binding.rs b/src/webview/android/binding.rs index ca1fe133d..81099d56c 100644 --- a/src/webview/android/binding.rs +++ b/src/webview/android/binding.rs @@ -13,7 +13,7 @@ use tao::platform::android::ndk_glue::jni::{ JNIEnv, }; -use super::{IPC, REQUEST_HANDLER, TITLE_CHANGE_HANDLER}; +use super::{create_headers_map, IPC, REQUEST_HANDLER, TITLE_CHANGE_HANDLER}; fn handle_request(env: JNIEnv, request: JObject) -> Result { let mut request_builder = Request::builder(); @@ -105,22 +105,7 @@ fn handle_request(env: JNIEnv, request: JObject) -> Result { (JObject::null().into(), JObject::null().into()) }; - let hashmap = env.find_class("java/util/HashMap")?; - let response_headers = env.new_object(hashmap, "()V", &[])?; - for (key, value) in response.headers().iter() { - env.call_method( - response_headers, - "put", - "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", - &[ - env.new_string(key.as_str())?.into(), - // TODO can we handle this better? - env - .new_string(String::from_utf8_lossy(value.as_bytes()))? - .into(), - ], - )?; - } + let response_headers = create_headers_map(&env, response.headers())?; let bytes = response.body(); diff --git a/src/webview/android/kotlin/RustWebView.kt b/src/webview/android/kotlin/RustWebView.kt index 83ec8eca7..da9fb1d53 100644 --- a/src/webview/android/kotlin/RustWebView.kt +++ b/src/webview/android/kotlin/RustWebView.kt @@ -8,6 +8,7 @@ import android.annotation.SuppressLint import android.webkit.* import android.content.Context import android.os.Build +import kotlin.collections.Map class RustWebView(context: Context): WebView(context) { init { @@ -26,5 +27,11 @@ class RustWebView(context: Context): WebView(context) { } } + fun loadUrlMainThread(url: String, additionalHttpHeaders: Map) { + post { + super.loadUrl(url, additionalHttpHeaders) + } + } + {{class-extension}} } diff --git a/src/webview/android/main_pipe.rs b/src/webview/android/main_pipe.rs index b28e34570..89bfab1f4 100644 --- a/src/webview/android/main_pipe.rs +++ b/src/webview/android/main_pipe.rs @@ -9,13 +9,13 @@ use std::os::unix::prelude::*; use tao::platform::android::ndk_glue::{ jni::{ errors::Error as JniError, - objects::{GlobalRef, JObject}, + objects::{GlobalRef, JObject, JString}, JNIEnv, }, PACKAGE, }; -use super::find_my_class; +use super::{create_headers_map, find_my_class}; static CHANNEL: Lazy<(Sender, Receiver)> = Lazy::new(|| bounded(8)); pub static MAIN_PIPE: Lazy<[RawFd; 2]> = Lazy::new(|| { @@ -50,6 +50,7 @@ impl MainPipe<'_> { devtools, transparent, background_color, + headers, } = attrs; // Create webview let rust_webview_class = find_my_class( @@ -65,12 +66,7 @@ impl MainPipe<'_> { // Load URL if let Ok(url) = env.new_string(url) { - env.call_method( - webview, - "loadUrlMainThread", - "(Ljava/lang/String;)V", - &[url.into()], - )?; + load_url(env, webview, url, headers, true)?; } // Enable devtools @@ -178,15 +174,10 @@ impl MainPipe<'_> { f(env, activity, webview.as_obj()); } } - WebViewMessage::LoadUrl(url) => { + WebViewMessage::LoadUrl(url, headers) => { if let Some(webview) = &self.webview { - let s = env.new_string(url)?; - env.call_method( - webview.as_obj(), - "loadUrl", - "(Ljava/lang/String;)V", - &[s.into()], - )?; + let url = env.new_string(url)?; + load_url(env, webview.as_obj(), url, headers, false)?; } } } @@ -195,6 +186,32 @@ impl MainPipe<'_> { } } +fn load_url<'a>( + env: JNIEnv<'a>, + webview: JObject<'a>, + url: JString<'a>, + headers: Option, + main_thread: bool, +) -> Result<(), JniError> { + let function = if main_thread { + "loadUrlMainThread" + } else { + "loadUrl" + }; + if let Some(headers) = headers { + let headers_map = create_headers_map(&env, &headers)?; + env.call_method( + webview, + function, + "(Ljava/lang/String;Ljava/util/Map;)V", + &[url.into(), headers_map.into()], + )?; + } else { + env.call_method(webview, function, "(Ljava/lang/String;)V", &[url.into()])?; + } + Ok(()) +} + fn set_background_color<'a>( env: JNIEnv<'a>, webview: JObject<'a>, @@ -223,7 +240,7 @@ pub enum WebViewMessage { GetWebViewVersion(Sender>), GetUrl(Sender), Jni(Box), - LoadUrl(String), + LoadUrl(String, Option), } #[derive(Debug)] @@ -232,4 +249,5 @@ pub struct CreateWebViewAttributes { pub devtools: bool, pub transparent: bool, pub background_color: Option, + pub headers: Option, } diff --git a/src/webview/android/mod.rs b/src/webview/android/mod.rs index cec06ba88..d46fe13ba 100644 --- a/src/webview/android/mod.rs +++ b/src/webview/android/mod.rs @@ -21,7 +21,7 @@ use tao::platform::android::ndk_glue::{ JNIEnv, }, ndk::looper::{FdEvent, ForeignLooper}, - PACKAGE, + JMap, PACKAGE, }; use url::Url; @@ -155,6 +155,7 @@ impl InnerWebView { custom_protocols, background_color, transparent, + headers, .. } = attributes; @@ -173,6 +174,7 @@ impl InnerWebView { devtools, background_color, transparent, + headers, })); } @@ -282,7 +284,11 @@ impl InnerWebView { } pub fn load_url(&self, url: &str) { - MainPipe::send(WebViewMessage::LoadUrl(url.to_string())); + MainPipe::send(WebViewMessage::LoadUrl(url.to_string(), None)); + } + + pub fn load_url_with_headers(&self, url: &str, headers: http::HeaderMap) { + MainPipe::send(WebViewMessage::LoadUrl(url.to_string(), Some(headers))); } } @@ -342,3 +348,17 @@ fn find_my_class<'a>( .l()?; Ok(my_class.into()) } + +fn create_headers_map<'a, 'b>( + env: &'a JNIEnv, + headers: &http::HeaderMap, +) -> std::result::Result, JniError> { + let obj = env.new_object("java/util/HashMap", "()V", &[])?; + let headers_map = JMap::from_env(&env, obj)?; + for (name, value) in headers.iter() { + let key = env.new_string(name)?; + let value = env.new_string(value.to_str().unwrap_or_default())?; + headers_map.put(key.into(), value.into())?; + } + Ok(headers_map) +} diff --git a/src/webview/mod.rs b/src/webview/mod.rs index c9b2f923d..72ba2888a 100644 --- a/src/webview/mod.rs +++ b/src/webview/mod.rs @@ -48,8 +48,7 @@ use webview2_com::Microsoft::Web::WebView2::Win32::ICoreWebView2Controller; #[cfg(target_os = "windows")] use windows::{Win32::Foundation::HWND, Win32::UI::WindowsAndMessaging::DestroyWindow}; -use std::borrow::Cow; -use std::{path::PathBuf, rc::Rc}; +use std::{borrow::Cow, path::PathBuf, rc::Rc}; pub use url::Url; @@ -83,6 +82,8 @@ pub struct WebViewAttributes { pub background_color: Option, /// Whether load the provided URL to [`WebView`]. pub url: Option, + /// Headers used when loading the requested `url`. + pub headers: Option, /// Whether page zooming by hotkeys is enabled /// /// ## Platform-specific @@ -237,6 +238,7 @@ impl Default for WebViewAttributes { transparent: false, background_color: None, url: None, + headers: None, html: None, initialization_scripts: vec![], custom_protocols: vec![], @@ -437,15 +439,24 @@ impl<'a> WebViewBuilder<'a> { self } + /// Load the provided URL with given headers when the builder calling [`WebViewBuilder::build`] to create the + /// [`WebView`]. The provided URL must be valid. + pub fn with_url_and_headers(mut self, url: &str, headers: http::HeaderMap) -> Result { + self.webview.url = Some(url.parse()?); + self.webview.headers = Some(headers); + Ok(self) + } + /// Load the provided URL when the builder calling [`WebViewBuilder::build`] to create the /// [`WebView`]. The provided URL must be valid. pub fn with_url(mut self, url: &str) -> Result { self.webview.url = Some(Url::parse(url)?); + self.webview.headers = None; Ok(self) } /// Load the provided HTML string when the builder calling [`WebViewBuilder::build`] to create the - /// [`WebView`]. This will be ignored if `url` is already provided. + /// [`WebView`]. This will be ignored if `url` is provided. /// /// # Warning /// The Page loaded from html string will have different Origin on different platforms. And @@ -798,6 +809,10 @@ impl WebView { pub fn load_url(&self, url: &str) { self.webview.load_url(url) } + + pub fn load_url_with_headers(&self, url: &str, headers: http::HeaderMap) { + self.webview.load_url_with_headers(url, headers) + } } /// An event enumeration sent to [`FileDropHandler`]. diff --git a/src/webview/webkitgtk/mod.rs b/src/webview/webkitgtk/mod.rs index 5f6b8af3a..0ca41bd10 100644 --- a/src/webview/webkitgtk/mod.rs +++ b/src/webview/webkitgtk/mod.rs @@ -8,17 +8,16 @@ use glib::signal::Inhibit; use gtk::prelude::*; #[cfg(any(debug_assertions, feature = "devtools"))] use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::Arc; use std::{ collections::hash_map::DefaultHasher, hash::{Hash, Hasher}, rc::Rc, - sync::Mutex, + sync::{Arc, Mutex}, }; use url::Url; use webkit2gtk::{ - traits::*, LoadEvent, NavigationPolicyDecision, PolicyDecisionType, UserContentInjectedFrames, - UserScript, UserScriptInjectionTime, WebView, WebViewBuilder, + traits::*, LoadEvent, NavigationPolicyDecision, PolicyDecisionType, URIRequest, + UserContentInjectedFrames, UserScript, UserScriptInjectionTime, WebView, WebViewBuilder, }; use webkit2gtk_sys::{ webkit_get_major_version, webkit_get_micro_version, webkit_get_minor_version, @@ -361,7 +360,7 @@ impl InnerWebView { // Navigation if let Some(url) = attributes.url { - web_context.queue_load_uri(Rc::clone(&w.webview), url); + web_context.queue_load_uri(Rc::clone(&w.webview), url, attributes.headers); web_context.flush_queue_loader(); } else if let Some(html) = attributes.html { w.webview.load_html(&html, Some("http://localhost")); @@ -461,6 +460,21 @@ impl InnerWebView { pub fn load_url(&self, url: &str) { self.webview.load_uri(url) } + + pub fn load_url_with_headers(&self, url: &str, headers: http::HeaderMap) { + let req = URIRequest::builder().uri(url).build(); + + if let Some(ref mut req_headers) = req.http_headers() { + for (header, value) in headers.iter() { + req_headers.append( + header.to_string().as_str(), + value.to_str().unwrap_or_default(), + ); + } + } + + self.webview.load_request(&req); + } } pub fn platform_webview_version() -> Result { diff --git a/src/webview/webkitgtk/web_context.rs b/src/webview/webkitgtk/web_context.rs index 4ce54eace..f566f675d 100644 --- a/src/webview/webkitgtk/web_context.rs +++ b/src/webview/webkitgtk/web_context.rs @@ -21,8 +21,8 @@ use std::{ }; use url::Url; use webkit2gtk::{ - traits::*, ApplicationInfo, CookiePersistentStorage, LoadEvent, UserContentManager, WebContext, - WebContextBuilder, WebView, WebsiteDataManagerBuilder, + traits::*, ApplicationInfo, CookiePersistentStorage, LoadEvent, URIRequest, UserContentManager, + WebContext, WebContextBuilder, WebView, WebsiteDataManagerBuilder, }; #[derive(Debug)] @@ -124,7 +124,7 @@ pub trait WebContextExt { /// Add a [`WebView`] to the queue waiting to be opened. /// /// See the `WebviewUriLoader` for more information. - fn queue_load_uri(&self, webview: Rc, url: Url); + fn queue_load_uri(&self, webview: Rc, url: Url, headers: Option); /// Flush all queued [`WebView`]s waiting to load a uri. /// @@ -177,8 +177,8 @@ impl WebContextExt for super::WebContext { } } - fn queue_load_uri(&self, webview: Rc, url: Url) { - self.os.webview_uri_loader.push(webview, url) + fn queue_load_uri(&self, webview: Rc, url: Url, headers: Option) { + self.os.webview_uri_loader.push(webview, url, headers) } fn flush_queue_loader(&self) { @@ -402,7 +402,7 @@ where #[derive(Debug, Default)] struct WebviewUriLoader { lock: AtomicBool, - queue: Mutex, Url)>>, + queue: Mutex, Url, Option)>>, } impl WebviewUriLoader { @@ -417,13 +417,13 @@ impl WebviewUriLoader { } /// Add a [`WebView`] to the queue. - fn push(&self, webview: Rc, url: Url) { + fn push(&self, webview: Rc, url: Url, headers: Option) { let mut queue = self.queue.lock().expect("poisoned load queue"); - queue.push_back((webview, url)) + queue.push_back((webview, url, headers)) } /// Remove a [`WebView`] from the queue and return it. - fn pop(&self) -> Option<(Rc, Url)> { + fn pop(&self) -> Option<(Rc, Url, Option)> { let mut queue = self.queue.lock().expect("poisoned load queue"); queue.pop_front() } @@ -431,7 +431,7 @@ impl WebviewUriLoader { /// Load the next uri to load if the lock is not engaged. fn flush(self: Rc) { if !self.is_locked() { - if let Some((webview, url)) = self.pop() { + if let Some((webview, url, headers)) = self.pop() { // we do not need to listen to failed events because those will finish the change event anyways webview.connect_load_changed(move |_, event| { if let LoadEvent::Finished = event { @@ -440,7 +440,22 @@ impl WebviewUriLoader { }; }); - webview.load_uri(url.as_str()); + if let Some(headers) = headers { + let req = URIRequest::builder().uri(url.as_str()).build(); + + if let Some(ref mut req_headers) = req.http_headers() { + for (header, value) in headers.iter() { + req_headers.append( + header.to_string().as_str(), + value.to_str().unwrap_or_default(), + ); + } + } + + webview.load_request(&req); + } else { + webview.load_uri(url.as_str()); + } } else { self.unlock(); } diff --git a/src/webview/webview2/mod.rs b/src/webview/webview2/mod.rs index 49ba9e935..f3a48d87a 100644 --- a/src/webview/webview2/mod.rs +++ b/src/webview/webview2/mod.rs @@ -53,6 +53,7 @@ impl From for Error { pub(crate) struct InnerWebView { pub controller: ICoreWebView2Controller, webview: ICoreWebView2, + env: ICoreWebView2Environment, // Store FileDropController in here to make sure it gets dropped when // the webview gets dropped, otherwise we'll have a memory leak #[allow(dead_code)] @@ -84,6 +85,7 @@ impl InnerWebView { Ok(Self { controller, webview, + env, file_drop_controller, }) } @@ -696,10 +698,16 @@ window.addEventListener('mousemove', (e) => window.chrome.webview.postMessage('_ .as_str() .replace(&format!("{}://", name), &format!("https://{}.", name)) } - unsafe { - webview - .Navigate(PCWSTR::from_raw(encode_wide(url_string).as_ptr())) - .map_err(webview2_com::Error::WindowsError)?; + + if let Some(headers) = attributes.headers { + load_url_with_headers(&webview, env, &url_string, headers); + } else { + let url = PCWSTR::from_raw(encode_wide(url_string).as_ptr()); + unsafe { + webview + .Navigate(url) + .map_err(webview2_com::Error::WindowsError)?; + } } } } else if let Some(html) = attributes.html { @@ -849,6 +857,10 @@ window.addEventListener('mousemove', (e) => window.chrome.webview.postMessage('_ let _ = unsafe { self.webview.Navigate(PCWSTR::from_raw(url.as_ptr())) }; } + pub fn load_url_with_headers(&self, url: &str, headers: http::HeaderMap) { + load_url_with_headers(&self.webview, &self.env, url, headers); + } + pub fn set_theme(&self, theme: Theme) { set_theme(&self.webview, theme); } @@ -858,6 +870,40 @@ fn encode_wide(string: impl AsRef) -> Vec { string.as_ref().encode_wide().chain(once(0)).collect() } +fn load_url_with_headers( + webview: &ICoreWebView2, + env: &ICoreWebView2Environment, + url: &str, + headers: http::HeaderMap, +) { + let url = encode_wide(url); + + let headers_map = { + let mut headers_map = String::new(); + for (name, value) in headers.iter() { + let header_key = name.to_string(); + if let Ok(value) = value.to_str() { + let _ = writeln!(headers_map, "{}: {}", header_key, value); + } + } + encode_wide(headers_map) + }; + + unsafe { + let env = env.cast::().unwrap(); + + if let Ok(request) = env.CreateWebResourceRequest( + PCWSTR::from_raw(url.as_ptr()), + PCWSTR::from_raw(encode_wide("GET").as_ptr()), + None, + PCWSTR::from_raw(headers_map.as_ptr()), + ) { + let webview: ICoreWebView2_10 = webview.cast().unwrap(); + let _ = webview.NavigateWithWebResourceRequest(&request); + } + }; +} + pub fn set_background_color( controller: &ICoreWebView2Controller, background_color: RGBA, diff --git a/src/webview/wkwebview/mod.rs b/src/webview/wkwebview/mod.rs index cb70603ef..30ad7a65d 100644 --- a/src/webview/wkwebview/mod.rs +++ b/src/webview/wkwebview/mod.rs @@ -771,7 +771,7 @@ r#"Object.defineProperty(window, 'ipc', { w.navigate_to_string(path); } } else { - w.navigate_to_url(url.as_str()); + w.navigate_to_url(url.as_str(), attributes.headers); } } else if let Some(html) = attributes.html { w.navigate_to_string(&html); @@ -872,14 +872,25 @@ r#"Object.defineProperty(window, 'ipc', { } pub fn load_url(&self, url: &str) { - self.navigate_to_url(url) + self.navigate_to_url(url, None) } - fn navigate_to_url(&self, url: &str) { + pub fn load_url_with_headers(&self, url: &str, headers: http::HeaderMap) { + self.navigate_to_url(url, Some(headers)) + } + + fn navigate_to_url(&self, url: &str, headers: Option) { // Safety: objc runtime calls are unsafe unsafe { let url: id = msg_send![class!(NSURL), URLWithString: NSString::new(url)]; - let request: id = msg_send![class!(NSURLRequest), requestWithURL: url]; + let request: id = msg_send![class!(NSMutableURLRequest), requestWithURL: url]; + if let Some(headers) = headers { + for (name, value) in headers.iter() { + let key = NSString::new(name.as_str()); + let value = NSString::new(value.to_str().unwrap_or_default()); + let _: () = msg_send![request, addValue:value.as_ptr() forHTTPHeaderField:key.as_ptr()]; + } + } let () = msg_send![self.webview, loadRequest: request]; } } @@ -1058,4 +1069,8 @@ impl NSString { str::from_utf8_unchecked(bytes) } } + + fn as_ptr(&self) -> id { + self.0 + } }