From 85f92b98d6cdb45c576340db04d26e345b3b49e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A5=9E=E6=A8=B9=E6=A1=9C=E4=B9=83?= Date: Sat, 19 Sep 2026 00:31:35 +0800 Subject: [PATCH] Add proxy support for WKWebView --- .../Macios/Interop/NSArray.cs | 7 ++ .../Macios/Interop/NetworkFramework.cs | 34 ++++++++ .../Interop/WebKit/WKWebsiteDataStore.cs | 83 +++++++++++++++++++ .../Macios/MaciosWebViewAdapter.cs | 10 +++ ...eWKWebViewEnvironmentRequestedEventArgs.cs | 17 ++++ 5 files changed, 151 insertions(+) create mode 100644 src/Avalonia.Controls.WebView.Core/Macios/Interop/NetworkFramework.cs diff --git a/src/Avalonia.Controls.WebView.Core/Macios/Interop/NSArray.cs b/src/Avalonia.Controls.WebView.Core/Macios/Interop/NSArray.cs index ce9157f..d4d9af8 100644 --- a/src/Avalonia.Controls.WebView.Core/Macios/Interop/NSArray.cs +++ b/src/Avalonia.Controls.WebView.Core/Macios/Interop/NSArray.cs @@ -4,8 +4,10 @@ namespace Avalonia.Controls.Macios.Interop; internal class NSArray(IntPtr handle, bool owns) : NSObject(handle, owns) { + private static readonly IntPtr s_class = Libobjc.objc_getClass("NSArray"); private static readonly IntPtr s_count = Libobjc.sel_getUid("count"); private static readonly IntPtr s_getObjects = Libobjc.sel_getUid("getObjects:range:"); + private static readonly IntPtr s_arrayWithObject = Libobjc.sel_getUid("arrayWithObject:"); public nint Count => Libobjc.intptr_objc_msgSend(Handle, s_count); @@ -13,4 +15,9 @@ public void GetObjects(IntPtr objects, nint from, nint length) { Libobjc.void_objc_msgSend(Handle, s_getObjects, objects, from, length); } + + public static NSArray FromObject(IntPtr @object) + { + return new NSArray(Libobjc.intptr_objc_msgSend(s_class, s_arrayWithObject, @object), false); + } } diff --git a/src/Avalonia.Controls.WebView.Core/Macios/Interop/NetworkFramework.cs b/src/Avalonia.Controls.WebView.Core/Macios/Interop/NetworkFramework.cs new file mode 100644 index 0000000..d17dd6a --- /dev/null +++ b/src/Avalonia.Controls.WebView.Core/Macios/Interop/NetworkFramework.cs @@ -0,0 +1,34 @@ +using System; +using System.Runtime.InteropServices; + +namespace Avalonia.Controls.Macios.Interop; + +internal static partial class NetworkFramework +{ + private const string Framework = "/System/Library/Frameworks/Network.framework/Network"; + + [LibraryImport(Framework)] + public static partial IntPtr nw_endpoint_create_host( + [MarshalAs(UnmanagedType.LPStr)] string host, + [MarshalAs(UnmanagedType.LPStr)] string port); + + [LibraryImport(Framework)] + public static partial IntPtr nw_proxy_config_create_http_connect(IntPtr endpoint, IntPtr tlsOptions); + [LibraryImport(Framework)] + public static partial IntPtr nw_proxy_config_create_socksv5(IntPtr endpoint); + + [LibraryImport(Framework)] + public static partial IntPtr nw_tls_create_options(); + + [LibraryImport(Framework)] + public static partial void nw_proxy_config_add_excluded_domain(IntPtr config, [MarshalAs(UnmanagedType.LPStr)] string excludedDomain); + + [LibraryImport(Framework)] + public static partial void nw_proxy_config_set_username(IntPtr config, [MarshalAs(UnmanagedType.LPStr)] string username); + + [LibraryImport(Framework)] + public static partial void nw_proxy_config_set_password(IntPtr config, [MarshalAs(UnmanagedType.LPStr)] string password); + + [LibraryImport(Framework)] + public static partial void nw_release(IntPtr obj); +} diff --git a/src/Avalonia.Controls.WebView.Core/Macios/Interop/WebKit/WKWebsiteDataStore.cs b/src/Avalonia.Controls.WebView.Core/Macios/Interop/WebKit/WKWebsiteDataStore.cs index c8cd6e6..7e0b5e5 100644 --- a/src/Avalonia.Controls.WebView.Core/Macios/Interop/WebKit/WKWebsiteDataStore.cs +++ b/src/Avalonia.Controls.WebView.Core/Macios/Interop/WebKit/WKWebsiteDataStore.cs @@ -1,4 +1,6 @@ using System; +using System.Linq; +using System.Net; namespace Avalonia.Controls.Macios.Interop.WebKit; @@ -9,6 +11,7 @@ internal class WKWebsiteDataStore(IntPtr handle, bool owns) : NSObject(handle, o private static readonly IntPtr s_defaultDataStore = Libobjc.sel_getUid("defaultDataStore"); private static readonly IntPtr s_nonPersistentDataStore = Libobjc.sel_getUid("nonPersistentDataStore"); private static readonly IntPtr s_dataStoreForIdentifier = Libobjc.sel_getUid("dataStoreForIdentifier:"); + private static readonly IntPtr s_setProxyConfigurations = Libobjc.sel_getUid("setProxyConfigurations:"); public WKHTTPCookieStore HttpCookieStore => new(Libobjc.intptr_objc_msgSend(Handle, s_httpCookieStore), false); @@ -22,4 +25,84 @@ public static WKWebsiteDataStore ForIdentifier(Guid identifier) return new WKWebsiteDataStore( Libobjc.intptr_objc_msgSend(s_class, s_dataStoreForIdentifier, nsIdentifier.Handle), true); } + + public void SetProxyConfiguration(Uri proxyAddress, string[] excludedDomains, NetworkCredential? credentials = null) + { + if (proxyAddress.Port <= 0) + throw new ArgumentException( + $"Proxy address '{proxyAddress}' must specify an explicit port.", nameof(proxyAddress)); + + var endpoint = NetworkFramework.nw_endpoint_create_host(proxyAddress.Host, proxyAddress.Port.ToString()); + + try + { + IntPtr proxyConfig; + IntPtr? tlsOptions = null; + + switch (proxyAddress.Scheme) + { + case "http": + proxyConfig = NetworkFramework.nw_proxy_config_create_http_connect(endpoint, IntPtr.Zero); + break; + + case "https": + tlsOptions = NetworkFramework.nw_tls_create_options(); + proxyConfig = NetworkFramework.nw_proxy_config_create_http_connect(endpoint, tlsOptions.Value); + break; + + case "socks5": + proxyConfig = NetworkFramework.nw_proxy_config_create_socksv5(endpoint); + break; + + default: + throw new NotSupportedException($"Proxy scheme '{proxyAddress.Scheme}' is not supported."); + } + + try + { + var (username, password) = credentials is not null ? + (credentials.UserName, credentials.Password) : + ParseUserInfo(proxyAddress.UserInfo); + + if (!string.IsNullOrEmpty(username)) + NetworkFramework.nw_proxy_config_set_username(proxyConfig, username); + + if (!string.IsNullOrEmpty(password)) + NetworkFramework.nw_proxy_config_set_password(proxyConfig, password); + + foreach (var excludedDomain in excludedDomains.Where(d => !string.IsNullOrWhiteSpace(d))) + { + NetworkFramework.nw_proxy_config_add_excluded_domain(proxyConfig, excludedDomain); + } + + using var array = NSArray.FromObject(proxyConfig); + _ = Libobjc.intptr_objc_msgSend(Handle, s_setProxyConfigurations, array.Handle); + } + finally + { + NetworkFramework.nw_release(proxyConfig); + + if (tlsOptions.HasValue) + NetworkFramework.nw_release(tlsOptions.Value); + } + } + finally + { + NetworkFramework.nw_release(endpoint); + } + + static (string? Username, string? Password) ParseUserInfo(string userInfo) + { + if (string.IsNullOrEmpty(userInfo)) + return (null, null); + + var colonIndex = userInfo.IndexOf(':'); + if (colonIndex < 0) + return (Uri.UnescapeDataString(userInfo), null); + + var username = Uri.UnescapeDataString(userInfo.AsSpan(0, colonIndex)); + var password = Uri.UnescapeDataString(userInfo.AsSpan(colonIndex + 1)); + return (username, password); + } + } } diff --git a/src/Avalonia.Controls.WebView.Core/Macios/MaciosWebViewAdapter.cs b/src/Avalonia.Controls.WebView.Core/Macios/MaciosWebViewAdapter.cs index 5976f53..4340679 100644 --- a/src/Avalonia.Controls.WebView.Core/Macios/MaciosWebViewAdapter.cs +++ b/src/Avalonia.Controls.WebView.Core/Macios/MaciosWebViewAdapter.cs @@ -71,6 +71,16 @@ public MaciosWebViewAdapter(AppleWKWebViewEnvironmentRequestedEventArgs options) _ => WKWebsiteDataStore.Default, }; + if (options.ProxyAddress is not null && + (OperatingSystem.IsIOSVersionAtLeast(17, 0) || + OperatingSystem.IsMacOSVersionAtLeast(14, 0))) + { + _config.WebsiteDataStore.SetProxyConfiguration( + options.ProxyAddress, + options.ProxyExcludedDomains ?? [], + options.ProxyCredentials); + } + _config.Preferences.MediaDevicesEnabled = true; // undocumented, but necessary for getUserMedia to work _config.Preferences.DeveloperExtrasEnabled = options.EnableDevTools; diff --git a/src/Avalonia.Controls.WebView.Core/Platform/AppleWKWebViewEnvironmentRequestedEventArgs.cs b/src/Avalonia.Controls.WebView.Core/Platform/AppleWKWebViewEnvironmentRequestedEventArgs.cs index 53953a4..d0fb7b5 100644 --- a/src/Avalonia.Controls.WebView.Core/Platform/AppleWKWebViewEnvironmentRequestedEventArgs.cs +++ b/src/Avalonia.Controls.WebView.Core/Platform/AppleWKWebViewEnvironmentRequestedEventArgs.cs @@ -1,4 +1,5 @@ using System; +using System.Net; using Avalonia.Controls; // ReSharper disable InconsistentNaming @@ -40,4 +41,20 @@ internal AppleWKWebViewEnvironmentRequestedEventArgs(DeferralManager deferralMan /// Gets or sets the name of the script message handler. /// public string? ScriptHandlerMessageName { get; set; } + + /// + /// Gets or sets the proxy address. + /// + public Uri? ProxyAddress { get; set; } + + /// + /// Gets or sets the credentials used to authenticate with the proxy. + /// If set, this takes precedence over credentials embedded in . + /// + public NetworkCredential? ProxyCredentials { get; set; } + + /// + /// Gets or sets the list of domains to exclude from the proxy. + /// + public string[]? ProxyExcludedDomains { get; set; } }