Skip to content

Commit

Permalink
update proxy setting on registry changes (dotnet#103364)
Browse files Browse the repository at this point in the history
* update proxy setting on registry changes

* udpate

* build fixes

* console

* registry

* winhttp

* invalid

* feedback

* feedback

* 'feedback'

* Apply suggestions from code review

Co-authored-by: Jan Kotas <jkotas@microsoft.com>

* MemberNotNull

---------

Co-authored-by: Jan Kotas <jkotas@microsoft.com>
  • Loading branch information
wfurt and jkotas authored Jul 8, 2024
1 parent 4e278fe commit e71e0f4
Show file tree
Hide file tree
Showing 9 changed files with 132 additions and 104 deletions.
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 @@ -384,6 +384,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 @@ -479,6 +481,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);
}
}

UpdateConfiguration(proxy);
}

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

[MemberNotNull(nameof(_proxyHelper))]
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,10 @@ public void Dispose()
{
SafeWinHttpHandle.DisposeAndClearHandle(ref _sessionHandle);
}

_waitHandle?.Dispose();
_internetSettingsRegistry?.Dispose();
_registeredWaitHandle?.Unregister(null);
}
}

Expand All @@ -179,6 +214,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 +280,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 +301,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

0 comments on commit e71e0f4

Please sign in to comment.