Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/Avalonia.Controls.WebView.Core/Macios/Interop/NSArray.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,20 @@ 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);

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);
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System;
using System.Linq;
using System.Net;

namespace Avalonia.Controls.Macios.Interop.WebKit;

Expand All @@ -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);

Expand All @@ -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);
}
}
}
10 changes: 10 additions & 0 deletions src/Avalonia.Controls.WebView.Core/Macios/MaciosWebViewAdapter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Net;
using Avalonia.Controls;

// ReSharper disable InconsistentNaming
Expand Down Expand Up @@ -40,4 +41,20 @@ internal AppleWKWebViewEnvironmentRequestedEventArgs(DeferralManager deferralMan
/// Gets or sets the name of the script message handler.
/// </summary>
public string? ScriptHandlerMessageName { get; set; }

/// <summary>
/// Gets or sets the proxy address.
/// </summary>
public Uri? ProxyAddress { get; set; }

/// <summary>
/// Gets or sets the credentials used to authenticate with the proxy.
/// If set, this takes precedence over credentials embedded in <see cref="ProxyAddress"/>.
/// </summary>
public NetworkCredential? ProxyCredentials { get; set; }

/// <summary>
/// Gets or sets the list of domains to exclude from the proxy.
/// </summary>
public string[]? ProxyExcludedDomains { get; set; }
Comment on lines +45 to +59

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know it's not typically recommended to reuse types across domains, just because they fit similarly.

But what do you think of using .NET's WebProxy type here?

WebProxy has Address, BypassList and Credentials (proxy.Credentials.GetCredentials("Basic")) that cover these three config properties.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good to me, WebProxy is suitable here. I need to test its behavior.

}
Loading