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; }
}