diff --git a/Cargo.toml b/Cargo.toml index de7d0f341..58c2387b5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ devtools = [ ] transparent = [ ] fullscreen = [ ] linux-body = [ "webkit2gtk/v2_40" ] +mac-proxy = [ ] [dependencies] libc = "0.2" diff --git a/examples/proxy.rs b/examples/proxy.rs new file mode 100644 index 000000000..f411b0278 --- /dev/null +++ b/examples/proxy.rs @@ -0,0 +1,44 @@ +// Copyright 2020-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use wry::webview::{ProxyConfig, ProxyEndpoint}; + +fn main() -> wry::Result<()> { + use wry::{ + application::{ + event::{Event, StartCause, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + window::WindowBuilder, + }, + webview::WebViewBuilder, + }; + + let event_loop = EventLoop::new(); + let window = WindowBuilder::new() + .with_title("Proxy Test") + .build(&event_loop)?; + + let http_proxy = ProxyConfig::Http(ProxyEndpoint { + host: "localhost".to_string(), + port: "3128".to_string(), + }); + + let _webview = WebViewBuilder::new(window)? + .with_proxy_config(http_proxy) + .with_url("https://www.myip.com/")? + .build()?; + + event_loop.run(move |event, _, control_flow| { + *control_flow = ControlFlow::Wait; + + match event { + Event::NewEvents(StartCause::Init) => println!("Wry has started!"), + Event::WindowEvent { + event: WindowEvent::CloseRequested, + .. + } => *control_flow = ControlFlow::Exit, + _ => (), + } + }); +} diff --git a/src/lib.rs b/src/lib.rs index 16526fd24..37ddb0505 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -165,4 +165,6 @@ pub enum Error { #[cfg(target_os = "android")] #[error(transparent)] JniError(#[from] tao::platform::android::ndk_glue::jni::errors::Error), + #[error("Failed to create proxy endpoint")] + ProxyEndpointCreationFailed, } diff --git a/src/webview/mod.rs b/src/webview/mod.rs index d4abc7bf7..a8ba6faea 100644 --- a/src/webview/mod.rs +++ b/src/webview/mod.rs @@ -4,6 +4,7 @@ //! [`WebView`] struct and associated types. +mod proxy; mod web_context; pub use web_context::WebContext; @@ -50,6 +51,7 @@ use windows::{Win32::Foundation::HWND, Win32::UI::WindowsAndMessaging::DestroyWi use std::{borrow::Cow, path::PathBuf, rc::Rc}; +pub use proxy::{ProxyConfig, ProxyEndpoint}; pub use url::Url; #[cfg(target_os = "windows")] @@ -235,6 +237,12 @@ pub struct WebViewAttributes { /// Set a handler closure to process page load events. pub on_page_load_handler: Option>, + + /// Set a proxy configuration for the webview. Supports HTTP CONNECT and SOCKSv5 proxies + /// + /// - **macOS**: Requires macOS 14.0+ and the `mac-proxy` feature flag to be enabled. + /// - **Android / iOS:** Not supported. + pub proxy_config: Option, } impl Default for WebViewAttributes { @@ -267,6 +275,7 @@ impl Default for WebViewAttributes { incognito: false, autoplay: true, on_page_load_handler: None, + proxy_config: None, } } } @@ -655,6 +664,16 @@ impl<'a> WebViewBuilder<'a> { self } + /// Set a proxy configuration for the webview. + /// + /// - **macOS**: Requires macOS 14.0+ and the `mac-proxy` feature flag to be enabled. Supports HTTP CONNECT and SOCKSv5 proxies. + /// - **Windows / Linux**: Supports HTTP CONNECT and SOCKSv5 proxies. + /// - **Android / iOS:** Not supported. + pub fn with_proxy_config(mut self, configuration: ProxyConfig) -> Self { + self.webview.proxy_config = Some(configuration); + self + } + /// Consume the builder and create the [`WebView`]. /// /// Platform-specific behavior: @@ -682,7 +701,8 @@ pub trait WebViewBuilderExtWindows { /// ## Warning /// /// By default wry passes `--disable-features=msWebOOUI,msPdfOOUI,msSmartScreenProtection` - /// and `--autoplay-policy=no-user-gesture-required` if autoplay is enabled + /// `--autoplay-policy=no-user-gesture-required` if autoplay is enabled + /// and `--proxy-server=://:` if a proxy is set. /// so if you use this method, you have to add these arguments yourself if you want to keep the same behavior. fn with_additional_browser_args>(self, additional_args: S) -> Self; diff --git a/src/webview/proxy.rs b/src/webview/proxy.rs new file mode 100644 index 000000000..c7a426070 --- /dev/null +++ b/src/webview/proxy.rs @@ -0,0 +1,15 @@ +#[derive(Debug, Clone)] +pub struct ProxyEndpoint { + /// Proxy server host (e.g. 192.168.0.100, localhost, example.com, etc.) + pub host: String, + /// Proxy server port (e.g. 1080, 3128, etc.) + pub port: String, +} + +#[derive(Debug, Clone)] +pub enum ProxyConfig { + /// Connect to proxy server via HTTP CONNECT + Http(ProxyEndpoint), + /// Connect to proxy server via SOCKSv5 + Socks5(ProxyEndpoint), +} diff --git a/src/webview/webkitgtk/mod.rs b/src/webview/webkitgtk/mod.rs index 93fa6fc7c..85ed28117 100644 --- a/src/webview/webkitgtk/mod.rs +++ b/src/webview/webkitgtk/mod.rs @@ -15,9 +15,9 @@ use std::{ }; use url::Url; use webkit2gtk::{ - traits::*, AutoplayPolicy, LoadEvent, NavigationPolicyDecision, PolicyDecisionType, SettingsExt, - URIRequest, UserContentInjectedFrames, UserScript, UserScriptInjectionTime, WebView, - WebViewBuilder, WebsitePoliciesBuilder, + traits::*, AutoplayPolicy, LoadEvent, NavigationPolicyDecision, NetworkProxyMode, + NetworkProxySettings, PolicyDecisionType, SettingsExt, URIRequest, UserContentInjectedFrames, + UserScript, UserScriptInjectionTime, WebView, WebViewBuilder, WebsitePoliciesBuilder, }; use webkit2gtk_sys::{ webkit_get_major_version, webkit_get_micro_version, webkit_get_minor_version, @@ -29,7 +29,7 @@ pub use web_context::WebContextImpl; use crate::{ application::{platform::unix::*, window::Window}, - webview::{web_context::WebContext, PageLoadEvent, WebViewAttributes, RGBA}, + webview::{proxy::ProxyConfig, web_context::WebContext, PageLoadEvent, WebViewAttributes, RGBA}, Error, Result, }; @@ -71,7 +71,20 @@ impl InnerWebView { } } }; - + if let Some(proxy_setting) = attributes.proxy_config { + let proxy_uri = match proxy_setting { + ProxyConfig::Http(endpoint) => format!("http://{}:{}", endpoint.host, endpoint.port), + ProxyConfig::Socks5(endpoint) => { + format!("socks5://{}:{}", endpoint.host, endpoint.port) + } + }; + use webkit2gtk::WebContextExt; + if let Some(website_data_manager) = web_context.context().website_data_manager() { + let mut settings = NetworkProxySettings::new(Some(&proxy_uri.as_str()), &[]); + website_data_manager + .set_network_proxy_settings(NetworkProxyMode::Custom, Some(&mut settings)); + } + } let webview = { let mut webview = WebViewBuilder::new(); webview = webview.user_content_manager(web_context.manager()); diff --git a/src/webview/webview2/mod.rs b/src/webview/webview2/mod.rs index 9781cb963..439008061 100644 --- a/src/webview/webview2/mod.rs +++ b/src/webview/webview2/mod.rs @@ -5,7 +5,7 @@ mod file_drop; use crate::{ - webview::{PageLoadEvent, WebContext, WebViewAttributes, RGBA}, + webview::{proxy::ProxyConfig, PageLoadEvent, WebContext, WebViewAttributes, RGBA}, Error, Result, }; @@ -13,8 +13,14 @@ use file_drop::FileDropController; use url::Url; use std::{ - collections::HashSet, fmt::Write, iter::once, mem::MaybeUninit, os::windows::prelude::OsStrExt, - path::PathBuf, rc::Rc, sync::mpsc, sync::Arc, + collections::HashSet, + fmt::Write, + iter::once, + mem::MaybeUninit, + os::windows::prelude::OsStrExt, + path::PathBuf, + rc::Rc, + sync::{mpsc, Arc}, }; use once_cell::unsync::OnceCell; @@ -74,7 +80,7 @@ impl InnerWebView { let file_drop_handler = attributes.file_drop_handler.take(); let file_drop_window = window.clone(); - let env = Self::create_environment(&web_context, pl_attrs.clone(), attributes.autoplay)?; + let env = Self::create_environment(&web_context, pl_attrs.clone(), &attributes)?; let controller = Self::create_controller(hwnd, &env, attributes.incognito)?; let webview = Self::init_webview(window, hwnd, attributes, &env, &controller, pl_attrs)?; @@ -95,7 +101,7 @@ impl InnerWebView { fn create_environment( web_context: &Option<&mut WebContext>, pl_attrs: super::PlatformSpecificWebViewAttributes, - autoplay: bool, + attributes: &WebViewAttributes, ) -> webview2_com::Result { let (tx, rx) = mpsc::channel(); @@ -105,6 +111,35 @@ impl InnerWebView { .and_then(|path| path.to_str()) .map(String::from); + let argument = PCWSTR::from_raw( + encode_wide(pl_attrs.additional_browser_args.unwrap_or_else(|| { + // remove "mini menu" - See https://github.com/tauri-apps/wry/issues/535 + // and "smart screen" - See https://github.com/tauri-apps/tauri/issues/1345 + format!( + "--disable-features=msWebOOUI,msPdfOOUI,msSmartScreenProtection{}{}", + if attributes.autoplay { + " --autoplay-policy=no-user-gesture-required" + } else { + "" + }, + if let Some(proxy_setting) = &attributes.proxy_config { + match proxy_setting { + ProxyConfig::Http(endpoint) => { + format!(" --proxy-server=http://{}:{}", endpoint.host, endpoint.port) + } + ProxyConfig::Socks5(endpoint) => format!( + " --proxy-server=socks5://{}:{}", + endpoint.host, endpoint.port + ), + } + } else { + "".to_string() + } + ) + })) + .as_ptr(), + ); + CreateCoreWebView2EnvironmentCompletedHandler::wait_for_async_operation( Box::new(move |environmentcreatedhandler| unsafe { let options = { @@ -126,21 +161,7 @@ impl InnerWebView { options }; - let _ = options.SetAdditionalBrowserArguments(PCWSTR::from_raw( - encode_wide(pl_attrs.additional_browser_args.unwrap_or_else(|| { - // remove "mini menu" - See https://github.com/tauri-apps/wry/issues/535 - // and "smart screen" - See https://github.com/tauri-apps/tauri/issues/1345 - format!( - "--disable-features=msWebOOUI,msPdfOOUI,msSmartScreenProtection{}", - if autoplay { - " --autoplay-policy=no-user-gesture-required" - } else { - "" - } - ) - })) - .as_ptr(), - )); + let _ = options.SetAdditionalBrowserArguments(argument); if let Some(data_directory) = data_directory { CreateCoreWebView2EnvironmentWithOptions( diff --git a/src/webview/wkwebview/mod.rs b/src/webview/wkwebview/mod.rs index 6696aaf54..70c2131ed 100644 --- a/src/webview/wkwebview/mod.rs +++ b/src/webview/wkwebview/mod.rs @@ -6,6 +6,8 @@ mod download; #[cfg(target_os = "macos")] mod file_drop; mod navigation; +#[cfg(feature = "mac-proxy")] +mod proxy; #[cfg(target_os = "macos")] mod synthetic_mouse_events; @@ -43,6 +45,14 @@ use file_drop::{add_file_drop_methods, set_file_drop_handler}; #[cfg(target_os = "ios")] use crate::application::platform::ios::WindowExtIOS; +#[cfg(feature = "mac-proxy")] +use crate::webview::{ + proxy::ProxyConfig, + wkwebview::proxy::{ + nw_endpoint_t, nw_proxy_config_create_http_connect, nw_proxy_config_create_socksv5, + }, +}; + use crate::{ application::{ dpi::{LogicalSize, PhysicalSize}, @@ -314,6 +324,23 @@ impl InnerWebView { let _preference: id = msg_send![config, preferences]; let _yes: id = msg_send![class!(NSNumber), numberWithBool:1]; + #[cfg(feature = "mac-proxy")] + if let Some(proxy_config) = attributes.proxy_config { + let proxy_config = match proxy_config { + ProxyConfig::Http(endpoint) => { + let nw_endpoint = nw_endpoint_t::try_from(endpoint).unwrap(); + nw_proxy_config_create_http_connect(nw_endpoint, nil) + } + ProxyConfig::Socks5(endpoint) => { + let nw_endpoint = nw_endpoint_t::try_from(endpoint).unwrap(); + nw_proxy_config_create_socksv5(nw_endpoint) + } + }; + + let proxies: id = msg_send![class!(NSArray), arrayWithObject: proxy_config]; + let () = msg_send![data_store, setProxyConfigurations: proxies]; + } + #[cfg(target_os = "macos")] (*webview).set_ivar(ACCEPT_FIRST_MOUSE, attributes.accept_first_mouse); @@ -1123,6 +1150,14 @@ impl NSString { } } + #[allow(dead_code)] // only used when `mac-proxy` feature is enabled + fn to_cstr(&self) -> *const c_char { + unsafe { + let utf_8_string = msg_send![self.0, UTF8String]; + utf_8_string + } + } + fn as_ptr(&self) -> id { self.0 } diff --git a/src/webview/wkwebview/proxy.rs b/src/webview/wkwebview/proxy.rs new file mode 100644 index 000000000..b2c1f46a2 --- /dev/null +++ b/src/webview/wkwebview/proxy.rs @@ -0,0 +1,67 @@ +// Copyright 2020-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use cocoa::base::nil; +use libc::c_char; + +use crate::{webview::proxy::ProxyEndpoint, Error}; + +use super::NSString; + +#[allow(non_camel_case_types)] +pub type nw_endpoint_t = *mut objc::runtime::Object; +#[allow(non_camel_case_types)] +pub type nw_relay_hop_t = *mut objc::runtime::Object; +#[allow(non_camel_case_types)] +pub type nw_protocol_options_t = *mut objc::runtime::Object; +#[allow(non_camel_case_types)] +pub type nw_proxy_config_t = *mut objc::runtime::Object; + +#[link(name = "Network", kind = "framework")] +extern "C" { + #[allow(dead_code)] + fn nw_endpoint_create_url(url: *const c_char) -> nw_endpoint_t; + #[allow(dead_code)] + fn nw_endpoint_get_url(endpoint: nw_endpoint_t) -> *const c_char; + fn nw_endpoint_create_host(host: *const c_char, port: *const c_char) -> nw_endpoint_t; + #[allow(dead_code)] + fn nw_proxy_config_set_username_and_password( + proxy_config: nw_proxy_config_t, + username: *const c_char, + password: *const c_char, + ); + #[allow(dead_code)] + fn nw_relay_hop_create( + http3_relay_endpoint: nw_endpoint_t, + http2_relay_endpoint: nw_endpoint_t, + relay_tls_options: nw_protocol_options_t, + ) -> nw_relay_hop_t; + #[allow(dead_code)] + fn nw_proxy_config_create_relay( + first_hop: nw_relay_hop_t, + second_hop: nw_relay_hop_t, + ) -> nw_proxy_config_t; + pub fn nw_proxy_config_create_socksv5(proxy_endpoint: nw_endpoint_t) -> nw_proxy_config_t; + pub fn nw_proxy_config_create_http_connect( + proxy_endpoint: nw_endpoint_t, + proxy_tls_options: nw_protocol_options_t, + ) -> nw_proxy_config_t; +} + +impl TryFrom for nw_endpoint_t { + type Error = Error; + fn try_from(endpoint: ProxyEndpoint) -> Result { + unsafe { + let endpoint_host = NSString::new(&endpoint.host).to_cstr(); + let endpoint_port = NSString::new(&endpoint.port).to_cstr(); + let endpoint = nw_endpoint_create_host(endpoint_host, endpoint_port); + + match endpoint { + #[allow(non_upper_case_globals)] + nil => Err(Error::ProxyEndpointCreationFailed), + _ => Ok(endpoint), + } + } + } +}