diff --git a/README.md b/README.md index 99bba6f..61fdedf 100644 --- a/README.md +++ b/README.md @@ -222,14 +222,27 @@ Commands: state.webSettings.customUserAgentString = "MyApp/1.2.3" ``` -Desktop note: +Desktop note on user-agent: * applied at creation time * changing it **recreates** the WebView (debounced) -* JS context/history may be lost +* JS context/history may be lostd 👉 Set it early. +### Proxy (desktop only) + +```kotlin +// HTTP CONNECT Proxy +state.webSettings.desktopWebSettings.proxyConfig = ProxyConfig.Http("proxy.tld", 8888) +// SOCKS5 Proxy +state.webSettings.desktopWebSettings.proxyConfig = ProxyConfig.Socks5("proxy.tld", 1080) +``` + +Proxy is only supported on Windows, macOS 14.0+ and Linux. + +**NOTE:** You need to set the proxy **before** the WebView gets created. + --- ### Logging diff --git a/demo-shared/src/commonMain/kotlin/io/github/kdroidfilter/webview/demo/DemoToolsPanel.kt b/demo-shared/src/commonMain/kotlin/io/github/kdroidfilter/webview/demo/DemoToolsPanel.kt index a11ec9b..14e18e9 100644 --- a/demo-shared/src/commonMain/kotlin/io/github/kdroidfilter/webview/demo/DemoToolsPanel.kt +++ b/demo-shared/src/commonMain/kotlin/io/github/kdroidfilter/webview/demo/DemoToolsPanel.kt @@ -453,4 +453,4 @@ private fun inlineHtml(): String = - """.trimIndent() + """.trimIndent() \ No newline at end of file diff --git a/webview-compose/src/commonMain/kotlin/io/github/kdroidfilter/webview/setting/PlatformWebSettings.kt b/webview-compose/src/commonMain/kotlin/io/github/kdroidfilter/webview/setting/PlatformWebSettings.kt index 6718398..5aa308c 100644 --- a/webview-compose/src/commonMain/kotlin/io/github/kdroidfilter/webview/setting/PlatformWebSettings.kt +++ b/webview-compose/src/commonMain/kotlin/io/github/kdroidfilter/webview/setting/PlatformWebSettings.kt @@ -22,6 +22,7 @@ sealed class PlatformWebSettings { var incognito: Boolean = false, var autoplayWithoutUserInteraction: Boolean = false, var focused: Boolean = true, + var proxyConfig: ProxyConfig? = null, ) : PlatformWebSettings() data class IOSWebSettings( diff --git a/webview-compose/src/commonMain/kotlin/io/github/kdroidfilter/webview/setting/ProxyConfig.kt b/webview-compose/src/commonMain/kotlin/io/github/kdroidfilter/webview/setting/ProxyConfig.kt new file mode 100644 index 0000000..c6f99a7 --- /dev/null +++ b/webview-compose/src/commonMain/kotlin/io/github/kdroidfilter/webview/setting/ProxyConfig.kt @@ -0,0 +1,18 @@ +package io.github.kdroidfilter.webview.setting + +sealed class ProxyConfig { + abstract val host: String + abstract val port: Int + + data class Http( + override val host: String, + override val port: Int + ) : ProxyConfig() + + data class Socks5( + override val host: String, + override val port: Int + ) : ProxyConfig() + + override fun toString() = "$host:$port" +} diff --git a/webview-compose/src/jvmMain/kotlin/io/github/kdroidfilter/webview/web/WebViewDesktop.kt b/webview-compose/src/jvmMain/kotlin/io/github/kdroidfilter/webview/web/WebViewDesktop.kt index cc741b8..ffda91c 100644 --- a/webview-compose/src/jvmMain/kotlin/io/github/kdroidfilter/webview/web/WebViewDesktop.kt +++ b/webview-compose/src/jvmMain/kotlin/io/github/kdroidfilter/webview/web/WebViewDesktop.kt @@ -1,6 +1,15 @@ package io.github.kdroidfilter.webview.web -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.key +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.awt.SwingPanel import androidx.compose.ui.graphics.Color @@ -10,6 +19,7 @@ import io.github.kdroidfilter.webview.jsbridge.WebViewJsBridge import io.github.kdroidfilter.webview.jsbridge.parseJsMessage import io.github.kdroidfilter.webview.request.WebRequest import io.github.kdroidfilter.webview.request.WebRequestInterceptResult +import io.github.kdroidfilter.webview.setting.ProxyConfig import io.github.kdroidfilter.webview.wry.Rgba import kotlinx.coroutines.delay @@ -33,7 +43,8 @@ actual fun defaultWebViewFactory(param: WebViewFactoryParam): NativeWebView = enableNavigationGestures = param.state.webSettings.desktopWebSettings.enableNavigationGestures, incognito = param.state.webSettings.desktopWebSettings.incognito, autoplayWithoutUserInteraction = param.state.webSettings.desktopWebSettings.autoplayWithoutUserInteraction, - focused = param.state.webSettings.desktopWebSettings.focused + focused = param.state.webSettings.desktopWebSettings.focused, + proxyConfig = param.state.webSettings.desktopWebSettings.proxyConfig?.toJvmProxyConfig(), ) else -> NativeWebView( @@ -49,7 +60,8 @@ actual fun defaultWebViewFactory(param: WebViewFactoryParam): NativeWebView = enableNavigationGestures = param.state.webSettings.desktopWebSettings.enableNavigationGestures, incognito = param.state.webSettings.desktopWebSettings.incognito, autoplayWithoutUserInteraction = param.state.webSettings.desktopWebSettings.autoplayWithoutUserInteraction, - focused = param.state.webSettings.desktopWebSettings.focused + focused = param.state.webSettings.desktopWebSettings.focused, + proxyConfig = param.state.webSettings.desktopWebSettings.proxyConfig?.toJvmProxyConfig(), ) } @@ -212,6 +224,11 @@ actual fun ActualWebView( } } +internal fun ProxyConfig.toJvmProxyConfig(): io.github.kdroidfilter.webview.wry.JvmProxyConfig = when (this) { + is ProxyConfig.Http -> io.github.kdroidfilter.webview.wry.JvmProxyConfig.Http(host, port) + is ProxyConfig.Socks5 -> io.github.kdroidfilter.webview.wry.JvmProxyConfig.Socks5(host, port) +} + private fun Color.toRgba(): Rgba { val argb: Int = this.toArgb() // 0xAARRGGBB (sRGB) val a: UByte = ((argb ushr 24) and 0xFF).toUByte() @@ -219,4 +236,4 @@ private fun Color.toRgba(): Rgba { val g: UByte = ((argb ushr 8) and 0xFF).toUByte() val b: UByte = (argb and 0xFF).toUByte() return Rgba(r = r, g = g, b = b, a = a) -} +} \ No newline at end of file diff --git a/wrywebview/Cargo.toml b/wrywebview/Cargo.toml index 4bf65f6..89aa112 100644 --- a/wrywebview/Cargo.toml +++ b/wrywebview/Cargo.toml @@ -12,7 +12,7 @@ path = "src/main/rust/lib.rs" [dependencies] thiserror = "2.0.11" uniffi = "0.29.4" -wry = { version = "0.54.2", features = ["devtools"] } +wry = { version = "0.54.2", features = ["mac-proxy", "devtools"] } [profile.release] opt-level = "z" diff --git a/wrywebview/src/main/kotlin/io/github/kdroidfilter/webview/wry/JvmProxyConfig.kt b/wrywebview/src/main/kotlin/io/github/kdroidfilter/webview/wry/JvmProxyConfig.kt new file mode 100644 index 0000000..3ade578 --- /dev/null +++ b/wrywebview/src/main/kotlin/io/github/kdroidfilter/webview/wry/JvmProxyConfig.kt @@ -0,0 +1,25 @@ +package io.github.kdroidfilter.webview.wry + +/** + * JVM-specific proxy config + */ +sealed class JvmProxyConfig { + abstract val host: String + abstract val port: Int + + data class Http( + override val host: String, + override val port: Int + ) : JvmProxyConfig() { + override fun toProxy(): Proxy.Http = Proxy.Http(Address(host, port.toUShort())) + } + + data class Socks5( + override val host: String, + override val port: Int + ) : JvmProxyConfig() { + override fun toProxy(): Proxy.Socks5 = Proxy.Socks5(Address(host, port.toUShort())) + } + + internal abstract fun toProxy(): Proxy +} diff --git a/wrywebview/src/main/kotlin/io/github/kdroidfilter/webview/wry/WryWebViewPanel.kt b/wrywebview/src/main/kotlin/io/github/kdroidfilter/webview/wry/WryWebViewPanel.kt index d020588..470b614 100644 --- a/wrywebview/src/main/kotlin/io/github/kdroidfilter/webview/wry/WryWebViewPanel.kt +++ b/wrywebview/src/main/kotlin/io/github/kdroidfilter/webview/wry/WryWebViewPanel.kt @@ -9,6 +9,7 @@ import javax.swing.JPanel import javax.swing.SwingUtilities import javax.swing.Timer import kotlin.concurrent.thread +import kotlin.properties.Delegates class WryWebViewPanel( @@ -25,6 +26,7 @@ class WryWebViewPanel( private val incognito: Boolean = false, private val autoplayWithoutUserInteraction: Boolean = false, private val focused: Boolean = true, + proxyConfig: JvmProxyConfig? = null, private val bridgeLogger: (String) -> Unit = { System.err.println(it) } ) : JPanel() { private val host = SkikoInterop.createHost() @@ -35,6 +37,7 @@ class WryWebViewPanel( private val dataDirectory: String? = dataDirectory?.trim()?.takeIf { it.isNotEmpty() } private val customUserAgent: String? = customUserAgent?.trim()?.takeIf { it.isNotEmpty() } private val initScript: String? = initScript?.trim()?.takeIf { it.isNotEmpty() } + private val proxy: Proxy? = proxyConfig?.toProxy() private var pendingUrlWithHeaders: String? = null private var pendingHeaders: Map = emptyMap() private var pendingHtml: String? = null @@ -401,6 +404,7 @@ class WryWebViewPanel( height = height, url = initialUrl, userAgent = userAgent, + proxy = proxy, dataDirectory = dataDir, zoom = supportZoom, transparent = transparent, @@ -412,7 +416,7 @@ class WryWebViewPanel( incognito = incognito, autoplay = autoplayWithoutUserInteraction, focused = focused, - navHandler = handler + navHandler = handler, ) updateBounds() startGtkPumpIfNeeded() @@ -444,7 +448,6 @@ class WryWebViewPanel( true } } - createInFlight = true stopCreateTimer() thread(name = "wry-webview-create", isDaemon = true) { @@ -455,6 +458,7 @@ class WryWebViewPanel( height = height, url = initialUrl, userAgent = userAgent, + proxy = proxy, dataDirectory = dataDir, zoom = supportZoom, transparent = transparent, @@ -466,7 +470,7 @@ class WryWebViewPanel( incognito = incognito, autoplay = autoplayWithoutUserInteraction, focused = focused, - navHandler = handler + navHandler = handler, ) } catch (e: RuntimeException) { System.err.println("Failed to create Wry webview: ${e.message}") @@ -771,6 +775,7 @@ private object NativeBindings { height: Int, url: String, userAgent: String?, + proxy: Proxy? = null, dataDirectory: String?, zoom: Boolean, transparent: Boolean, @@ -782,7 +787,7 @@ private object NativeBindings { incognito: Boolean, autoplay: Boolean, focused: Boolean, - navHandler: NavigationHandler? + navHandler: NavigationHandler?, ): ULong { return io.github.kdroidfilter.webview.wry.createWebview( parentHandle = parentHandle, @@ -790,6 +795,7 @@ private object NativeBindings { height = height, url = url, userAgent = userAgent, + proxy = proxy, dataDirectory = dataDirectory, zoom = zoom, transparent = transparent, diff --git a/wrywebview/src/main/rust/lib.rs b/wrywebview/src/main/rust/lib.rs index 62a74f5..ad363dd 100644 --- a/wrywebview/src/main/rust/lib.rs +++ b/wrywebview/src/main/rust/lib.rs @@ -17,7 +17,9 @@ use wry::cookie::time::OffsetDateTime; use wry::cookie::{Cookie, Expiration, SameSite}; use wry::http::header::HeaderName; use wry::http::{HeaderMap, HeaderValue}; -use wry::{WebContext, WebViewBuilder, RGBA}; +use crate::Proxy::Http; +use crate::Proxy::Socks5; +use wry::{WebContext, WebViewBuilder, RGBA, ProxyConfig, ProxyEndpoint}; pub use error::WebViewError; @@ -69,6 +71,18 @@ pub struct WebViewCookie { pub is_http_only: Option, } +#[derive(uniffi::Enum)] +pub enum Proxy { + Http(Address), + Socks5(Address), +} + +#[derive(Debug, Clone, uniffi::Record)] +pub struct Address { + pub host: String, + pub port: u16 +} + #[derive(Debug, Clone, Copy, uniffi::Record)] pub struct Rgba { pub r: u8, @@ -168,6 +182,13 @@ fn cookie_from_record(cookie: WebViewCookie) -> Result, WebViewE Ok(builder.build()) } +fn proxy_from_record(proxy: Proxy) -> ProxyConfig { + match proxy { + Http(addr) => ProxyConfig::Http(ProxyEndpoint { host: addr.host, port: addr.port.to_string() }), + Socks5(addr) => ProxyConfig::Socks5(ProxyEndpoint { host: (addr.host), port: (addr.port.to_string()) }) + } +} + use std::sync::atomic::AtomicBool; static LOG_ENABLED: AtomicBool = AtomicBool::new(false); @@ -237,6 +258,7 @@ fn create_webview_inner( height: i32, url: String, user_agent: Option, + proxy_config: Option, data_directory: Option, zoom: bool, transparent: bool, @@ -303,6 +325,10 @@ fn create_webview_inner( builder = builder.with_initialization_script(is); } + if let Some(proxy) = proxy_config { + builder = builder.with_proxy_config(proxy) + } + if let Some(ua) = user_agent { builder = builder.with_user_agent(ua); } @@ -414,6 +440,7 @@ pub fn create_webview( height: i32, url: String, user_agent: Option, + proxy: Option, data_directory: Option, zoom: bool, transparent: bool, @@ -427,52 +454,54 @@ pub fn create_webview( focused: bool, nav_handler: Option> ) -> Result { - #[cfg(target_os = "linux")] - { - return run_on_gtk_thread(move || { - create_webview_inner( - parent_handle, - width, - height, - url, - user_agent, - data_directory, - zoom, - transparent, - background_color, - init_script, - clipboard, - dev_tools, - navigation_gestures, - incognito, - autoplay, - focused, - nav_handler - ) - }); - } - - #[cfg(not(target_os = "linux"))] - run_on_main_thread( - move || create_webview_inner( - parent_handle, - width, height, - url, - user_agent, - data_directory, - zoom, - transparent, - background_color, - init_script, - clipboard, - dev_tools, - navigation_gestures, - incognito, - autoplay, - focused, - nav_handler - ) - ) + #[cfg(target_os = "linux")] + { + return run_on_gtk_thread(move || { + create_webview_inner( + parent_handle, + width, + height, + url, + user_agent, + proxy.map(proxy_from_record), + data_directory, + zoom, + transparent, + background_color, + init_script, + clipboard, + dev_tools, + navigation_gestures, + incognito, + autoplay, + focused, + nav_handler, + ) + }); + } + + #[cfg(not(target_os = "linux"))] + run_on_main_thread( + move || create_webview_inner( + parent_handle, + width, height, + url, + user_agent, + proxy.map(proxy_from_record), + data_directory, + zoom, + transparent, + background_color, + init_script, + clipboard, + dev_tools, + navigation_gestures, + incognito, + autoplay, + focused, + nav_handler, + ) + ) } // ============================================================================