Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

update proxy setting on registry changes #103364

Merged
merged 15 commits into from
Jul 8, 2024
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.Win32.SafeHandles;
using System;
using System.Runtime.InteropServices;

internal static partial class Interop
{
internal static partial class Advapi32
{
internal const int REG_NOTIFY_CHANGE_NAME = 0x1;
internal const int REG_NOTIFY_CHANGE_ATTRIBUTES = 0x2;
internal const int REG_NOTIFY_CHANGE_LAST_SET = 0x4;
internal const int REG_NOTIFY_CHANGE_SECURITY = 0x8;
internal const int REG_NOTIFY_THREAD_AGNOSTIC = 0x10000000;

[LibraryImport(Libraries.Advapi32, EntryPoint = "RegNotifyChangeKeyValue", StringMarshalling = StringMarshalling.Utf16)]
internal static partial int RegNotifyChangeKeyValue(
SafeHandle hKey,
[MarshalAs(UnmanagedType.Bool)] bool watchSubtree,
uint notifyFilter,
SafeHandle hEvent,
[MarshalAs(UnmanagedType.Bool)] bool asynchronous);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -505,7 +505,7 @@ public async Task MultiProxy_PAC_Failover_Succeeds()
winInetProxyHelperType.GetField("_proxyBypass", Reflection.BindingFlags.Instance | Reflection.BindingFlags.NonPublic).SetValue(winInetProxyHelper, null);

// Create a HttpWindowsProxy with our custom WinInetProxyHelper.
IWebProxy httpWindowsProxy = (IWebProxy)Activator.CreateInstance(Type.GetType("System.Net.Http.HttpWindowsProxy, System.Net.Http", true), Reflection.BindingFlags.NonPublic | Reflection.BindingFlags.Instance, null, new[] { winInetProxyHelper, null }, null);
IWebProxy httpWindowsProxy = (IWebProxy)Activator.CreateInstance(Type.GetType("System.Net.Http.HttpWindowsProxy, System.Net.Http", true), Reflection.BindingFlags.Public | Reflection.BindingFlags.NonPublic| Reflection.BindingFlags.Instance, null, new[] { winInetProxyHelper }, null);

Task<bool> nextFailedConnection = null;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,6 @@ public static IEnumerable<object[]> ManualSettingsMemberData()
yield return new object[] { new Uri("http://localhost"), true };
}

[Fact]
public void TryCreate_WinInetProxySettingsAllOff_ReturnsFalse()
{
Assert.False(HttpWindowsProxy.TryCreate(out IWebProxy webProxy));
}

[Theory]
[MemberData(nameof(ManualSettingsMemberData))]
Expand All @@ -44,7 +39,7 @@ public void GetProxy_BothAutoDetectAndManualSettingsButFailedAutoDetect_ManualSe
FakeRegistry.WinInetProxySettings.ProxyBypass = ManualSettingsProxyBypassList;
TestControl.PACFileNotDetectedOnNetwork = true;

Assert.True(HttpWindowsProxy.TryCreate(out IWebProxy webProxy));
IWebProxy webProxy = new HttpWindowsProxy();

// The first GetProxy() call will try using WinInetProxyHelper (and thus WinHTTP) since AutoDetect is on.
Uri proxyUri1 = webProxy.GetProxy(destination);
Expand Down Expand Up @@ -74,7 +69,7 @@ public void GetProxy_ManualSettingsOnly_ManualSettingsUsed(
FakeRegistry.WinInetProxySettings.Proxy = ManualSettingsProxyHost;
FakeRegistry.WinInetProxySettings.ProxyBypass = ManualSettingsProxyBypassList;

Assert.True(HttpWindowsProxy.TryCreate(out IWebProxy webProxy));
IWebProxy webProxy = new HttpWindowsProxy();
Uri proxyUri = webProxy.GetProxy(destination);
if (bypassProxy)
{
Expand All @@ -90,7 +85,7 @@ public void GetProxy_ManualSettingsOnly_ManualSettingsUsed(
public void IsBypassed_ReturnsFalse()
{
FakeRegistry.WinInetProxySettings.AutoDetect = true;
Assert.True(HttpWindowsProxy.TryCreate(out IWebProxy webProxy));
IWebProxy webProxy = new HttpWindowsProxy();
Assert.False(webProxy.IsBypassed(new Uri("http://www.microsoft.com/")));
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@
Link="ProductionCode\IMultiWebProxy.cs" />
<Compile Include="..\..\..\System.Net.Http\src\System\Net\Http\SocketsHttpHandler\MultiProxy.cs"
Link="ProductionCode\MultiProxy.cs" />
<Compile Include="$(CommonPath)\Interop\Windows\Advapi32\Interop.RegNotifyChangeKeyValue.cs"
Link="Common\Interop\Windows\Advapi32\Interop.RegNotifyChangeKeyValue.cs" />
<Compile Include="APICallHistory.cs" />
<Compile Include="ClientCertificateHelper.cs" />
<Compile Include="ClientCertificateScenarioTest.cs" />
Expand Down
3 changes: 3 additions & 0 deletions src/libraries/System.Net.Http/src/System.Net.Http.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,8 @@
Link="Common\Interop\Windows\WinHttp\Interop.winhttp_types.cs" />
<Compile Include="$(CommonPath)\Interop\Windows\WinHttp\Interop.winhttp.cs"
Link="Common\Interop\Windows\WinHttp\Interop.winhttp.cs" />
<Compile Include="$(CommonPath)\Interop\Windows\Advapi32\Interop.RegNotifyChangeKeyValue.cs"
Link="Common\Interop\Windows\Advapi32\Interop.RegNotifyChangeKeyValue.cs" />
<Compile Include="$(CommonPath)\System\CharArrayHelpers.cs"
Link="Common\System\CharArrayHelpers.cs" />
<Compile Include="$(CommonPath)\System\Net\HttpKnownHeaderNames.cs"
Expand Down Expand Up @@ -480,6 +482,7 @@
<Reference Include="System.Runtime" />
<Reference Include="System.Runtime.InteropServices" />
<Reference Include="System.Threading" />
<Reference Include="Microsoft.Win32.Registry" Condition="'$(TargetPlatformIdentifier)' == 'windows'" />
</ItemGroup>

<ItemGroup Condition="'$(TargetPlatformIdentifier)' != '' and '$(TargetPlatformIdentifier)' != 'windows' and '$(TargetPlatformIdentifier)' != 'browser'">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,45 +6,81 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO.Compression;
using System.Net.NetworkInformation;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using Microsoft.Win32;
using SafeWinHttpHandle = Interop.WinHttp.SafeWinHttpHandle;

namespace System.Net.Http
{
internal sealed class HttpWindowsProxy : IMultiWebProxy, IDisposable
{
private readonly MultiProxy _insecureProxy; // URI of the http system proxy if set
private readonly MultiProxy _secureProxy; // URI of the https system proxy if set
private readonly FailedProxyCache _failedProxies = new FailedProxyCache();
private readonly List<string>? _bypass; // list of domains not to proxy
private readonly bool _bypassLocal; // we should bypass domain considered local
private readonly List<IPAddress>? _localIp;
private readonly RegistryKey? _internetSettingsRegistry = Registry.CurrentUser?.OpenSubKey("Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings");
private MultiProxy _insecureProxy; // URI of the http system proxy if set
private MultiProxy _secureProxy; // URI of the https system proxy if set
private FailedProxyCache _failedProxies = new FailedProxyCache();
private List<string>? _bypass; // list of domains not to proxy
private List<IPAddress>? _localIp;
private ICredentials? _credentials;
private readonly WinInetProxyHelper _proxyHelper;
private WinInetProxyHelper _proxyHelper;
private SafeWinHttpHandle? _sessionHandle;
private bool _disposed;
private EventWaitHandle _waitHandle = new EventWaitHandle(false, EventResetMode.AutoReset);
private const int RegistrationFlags = Interop.Advapi32.REG_NOTIFY_CHANGE_NAME | Interop.Advapi32.REG_NOTIFY_CHANGE_LAST_SET | Interop.Advapi32.REG_NOTIFY_CHANGE_ATTRIBUTES | Interop.Advapi32.REG_NOTIFY_THREAD_AGNOSTIC;
private RegisteredWaitHandle? _registeredWaitHandle;

public static bool TryCreate([NotNullWhen(true)] out IWebProxy? proxy)
// 'proxy' used from tests via Reflection
public HttpWindowsProxy(WinInetProxyHelper? proxy = null)
{
// This will get basic proxy setting from system using existing
// WinInetProxyHelper functions. If no proxy is enabled, it will return null.
SafeWinHttpHandle? sessionHandle = null;
proxy = null;

WinInetProxyHelper proxyHelper = new WinInetProxyHelper();
if (!proxyHelper.ManualSettingsOnly && !proxyHelper.AutoSettingsUsed)
if (_internetSettingsRegistry != null && proxy == null)
{
return false;
// we register for change notifications so we can react to changes during lifetime.
if (Interop.Advapi32.RegNotifyChangeKeyValue(_internetSettingsRegistry.Handle, true, RegistrationFlags, _waitHandle.SafeWaitHandle, true) == 0)
{
_registeredWaitHandle = ThreadPool.RegisterWaitForSingleObject(_waitHandle, RegistryChangeNotificationCallback, this, -1, false);
}
}

_proxyHelper = proxy ?? new WinInetProxyHelper();
UpdateConfiguration(_proxyHelper);
wfurt marked this conversation as resolved.
Show resolved Hide resolved
}

private static void RegistryChangeNotificationCallback(object? state, bool timedOut)
{
HttpWindowsProxy proxy = (HttpWindowsProxy)state!;
if (!proxy._disposed)
{

// This is executed from threadpool. we should not ever throw here.
try
{
// We need to register for notification every time. We regisrerand lock before we process configuration
// so if there is update it would be serialized to ensure consistency.
Interop.Advapi32.RegNotifyChangeKeyValue(proxy._internetSettingsRegistry!.Handle, true, RegistrationFlags, proxy._waitHandle.SafeWaitHandle, true);
lock (proxy)
{
proxy.UpdateConfiguration();
}
}
catch (Exception ex)
{
if (NetEventSource.Log.IsEnabled()) NetEventSource.Error(proxy, $"Failed to refresh proxy configuration: {ex.Message}");
}
}
}

private void UpdateConfiguration(WinInetProxyHelper? proxyHelper = null)
{

proxyHelper ??= new WinInetProxyHelper();

if (proxyHelper.AutoSettingsUsed)
{
if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(proxyHelper, $"AutoSettingsUsed, calling {nameof(Interop.WinHttp.WinHttpOpen)}");
sessionHandle = Interop.WinHttp.WinHttpOpen(
SafeWinHttpHandle? sessionHandle = Interop.WinHttp.WinHttpOpen(
IntPtr.Zero,
Interop.WinHttp.WINHTTP_ACCESS_TYPE_NO_PROXY,
Interop.WinHttp.WINHTTP_NO_PROXY_NAME,
Expand All @@ -56,18 +92,10 @@ public static bool TryCreate([NotNullWhen(true)] out IWebProxy? proxy)
// Proxy failures are currently ignored by managed handler.
if (NetEventSource.Log.IsEnabled()) NetEventSource.Error(proxyHelper, $"{nameof(Interop.WinHttp.WinHttpOpen)} returned invalid handle");
sessionHandle.Dispose();
return false;
}
}

proxy = new HttpWindowsProxy(proxyHelper, sessionHandle);
return true;
}

private HttpWindowsProxy(WinInetProxyHelper proxyHelper, SafeWinHttpHandle? sessionHandle)
{
_proxyHelper = proxyHelper;
_sessionHandle = sessionHandle;
_sessionHandle = sessionHandle;
}

if (proxyHelper.ManualSettingsUsed)
{
Expand All @@ -80,10 +108,12 @@ private HttpWindowsProxy(WinInetProxyHelper proxyHelper, SafeWinHttpHandle? sess
{
int idx = 0;
string? tmp;
bool bypassLocal = false;
List<IPAddress>? localIp = null;

// Process bypass list for manual setting.
// Initial list size is best guess based on string length assuming each entry is at least 5 characters on average.
_bypass = new List<string>(proxyHelper.ProxyBypass.Length / 5);
List<string>? bypass = new List<string>(proxyHelper.ProxyBypass.Length / 5);

while (idx < proxyHelper.ProxyBypass.Length)
{
Expand Down Expand Up @@ -114,7 +144,7 @@ private HttpWindowsProxy(WinInetProxyHelper proxyHelper, SafeWinHttpHandle? sess
}
else if (string.Compare(proxyHelper.ProxyBypass, start, "<local>", 0, 7, StringComparison.OrdinalIgnoreCase) == 0)
{
_bypassLocal = true;
bypassLocal = true;
tmp = null;
}
else
Expand All @@ -137,28 +167,29 @@ private HttpWindowsProxy(WinInetProxyHelper proxyHelper, SafeWinHttpHandle? sess
continue;
}

_bypass.Add(tmp);
}
if (_bypass.Count == 0)
{
// Bypass string only had garbage we did not parse.
_bypass = null;
bypass.Add(tmp);
}
}

if (_bypassLocal)
{
_localIp = new List<IPAddress>();
foreach (NetworkInterface netInterface in NetworkInterface.GetAllNetworkInterfaces())
_bypass = bypass.Count > 0 ? bypass : null;

if (bypassLocal)
{
IPInterfaceProperties ipProps = netInterface.GetIPProperties();
foreach (UnicastIPAddressInformation addr in ipProps.UnicastAddresses)
localIp = new List<IPAddress>();
foreach (NetworkInterface netInterface in NetworkInterface.GetAllNetworkInterfaces())
{
_localIp.Add(addr.Address);
IPInterfaceProperties ipProps = netInterface.GetIPProperties();
foreach (UnicastIPAddressInformation addr in ipProps.UnicastAddresses)
{
localIp.Add(addr.Address);
}
}
}

_localIp = localIp?.Count > 0 ? localIp : null;
}
}

_proxyHelper = proxyHelper;
}

public void Dispose()
Expand All @@ -171,6 +202,9 @@ public void Dispose()
{
SafeWinHttpHandle.DisposeAndClearHandle(ref _sessionHandle);
}

_internetSettingsRegistry?.Dispose();
wfurt marked this conversation as resolved.
Show resolved Hide resolved
wfurt marked this conversation as resolved.
Show resolved Hide resolved
_registeredWaitHandle?.Unregister(null);
}
}

Expand All @@ -179,6 +213,11 @@ public void Dispose()
/// </summary>
public Uri? GetProxy(Uri uri)
{
if (!_proxyHelper.AutoSettingsUsed && !_proxyHelper.ManualSettingsOnly)
{
return null;
}

GetMultiProxy(uri).ReadNext(out Uri? proxyUri, out _);
return proxyUri;
}
Expand Down Expand Up @@ -240,7 +279,7 @@ public MultiProxy GetMultiProxy(Uri uri)
// Fallback to manual settings if present.
if (_proxyHelper.ManualSettingsUsed)
{
if (_bypassLocal)
if (_localIp != null)
{
IPAddress? address;

Expand All @@ -261,7 +300,7 @@ public MultiProxy GetMultiProxy(Uri uri)
{
// Host is valid IP address.
// Check if it belongs to local system.
foreach (IPAddress a in _localIp!)
foreach (IPAddress a in _localIp)
{
if (a.Equals(address))
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@ public static IWebProxy ConstructSystemProxy()
{
if (!HttpEnvironmentProxy.TryCreate(out IWebProxy? proxy))
{
HttpWindowsProxy.TryCreate(out proxy);
// We create instance even if there is currently no proxy as that can change during application run.
proxy = new HttpWindowsProxy();
}

return proxy ?? new HttpNoProxy();
return proxy;
}
}
}
Loading
Loading