Skip to content

Commit e71e0f4

Browse files
wfurtjkotas
andauthored
update proxy setting on registry changes (#103364)
* 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 <[email protected]> * MemberNotNull --------- Co-authored-by: Jan Kotas <[email protected]>
1 parent 4e278fe commit e71e0f4

File tree

9 files changed

+132
-104
lines changed

9 files changed

+132
-104
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.Win32.SafeHandles;
5+
using System;
6+
using System.Runtime.InteropServices;
7+
8+
internal static partial class Interop
9+
{
10+
internal static partial class Advapi32
11+
{
12+
internal const int REG_NOTIFY_CHANGE_NAME = 0x1;
13+
internal const int REG_NOTIFY_CHANGE_ATTRIBUTES = 0x2;
14+
internal const int REG_NOTIFY_CHANGE_LAST_SET = 0x4;
15+
internal const int REG_NOTIFY_CHANGE_SECURITY = 0x8;
16+
internal const int REG_NOTIFY_THREAD_AGNOSTIC = 0x10000000;
17+
18+
[LibraryImport(Libraries.Advapi32, EntryPoint = "RegNotifyChangeKeyValue", StringMarshalling = StringMarshalling.Utf16)]
19+
internal static partial int RegNotifyChangeKeyValue(
20+
SafeHandle hKey,
21+
[MarshalAs(UnmanagedType.Bool)] bool watchSubtree,
22+
uint notifyFilter,
23+
SafeHandle hEvent,
24+
[MarshalAs(UnmanagedType.Bool)] bool asynchronous);
25+
}
26+
}

src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Proxy.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -505,7 +505,7 @@ public async Task MultiProxy_PAC_Failover_Succeeds()
505505
winInetProxyHelperType.GetField("_proxyBypass", Reflection.BindingFlags.Instance | Reflection.BindingFlags.NonPublic).SetValue(winInetProxyHelper, null);
506506

507507
// Create a HttpWindowsProxy with our custom WinInetProxyHelper.
508-
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);
508+
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);
509509

510510
Task<bool> nextFailedConnection = null;
511511

src/libraries/System.Net.Http.WinHttpHandler/tests/UnitTests/HttpWindowsProxyTest.cs

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,6 @@ public static IEnumerable<object[]> ManualSettingsMemberData()
2828
yield return new object[] { new Uri("http://localhost"), true };
2929
}
3030

31-
[Fact]
32-
public void TryCreate_WinInetProxySettingsAllOff_ReturnsFalse()
33-
{
34-
Assert.False(HttpWindowsProxy.TryCreate(out IWebProxy webProxy));
35-
}
3631

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

47-
Assert.True(HttpWindowsProxy.TryCreate(out IWebProxy webProxy));
42+
IWebProxy webProxy = new HttpWindowsProxy();
4843

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

77-
Assert.True(HttpWindowsProxy.TryCreate(out IWebProxy webProxy));
72+
IWebProxy webProxy = new HttpWindowsProxy();
7873
Uri proxyUri = webProxy.GetProxy(destination);
7974
if (bypassProxy)
8075
{
@@ -90,7 +85,7 @@ public void GetProxy_ManualSettingsOnly_ManualSettingsUsed(
9085
public void IsBypassed_ReturnsFalse()
9186
{
9287
FakeRegistry.WinInetProxySettings.AutoDetect = true;
93-
Assert.True(HttpWindowsProxy.TryCreate(out IWebProxy webProxy));
88+
IWebProxy webProxy = new HttpWindowsProxy();
9489
Assert.False(webProxy.IsBypassed(new Uri("http://www.microsoft.com/")));
9590
}
9691
}

src/libraries/System.Net.Http.WinHttpHandler/tests/UnitTests/System.Net.Http.WinHttpHandler.Unit.Tests.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,8 @@
9696
Link="ProductionCode\IMultiWebProxy.cs" />
9797
<Compile Include="..\..\..\System.Net.Http\src\System\Net\Http\SocketsHttpHandler\MultiProxy.cs"
9898
Link="ProductionCode\MultiProxy.cs" />
99+
<Compile Include="$(CommonPath)\Interop\Windows\Advapi32\Interop.RegNotifyChangeKeyValue.cs"
100+
Link="Common\Interop\Windows\Advapi32\Interop.RegNotifyChangeKeyValue.cs" />
99101
<Compile Include="APICallHistory.cs" />
100102
<Compile Include="ClientCertificateHelper.cs" />
101103
<Compile Include="ClientCertificateScenarioTest.cs" />

src/libraries/System.Net.Http/src/System.Net.Http.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,8 @@
384384
Link="Common\Interop\Windows\WinHttp\Interop.winhttp_types.cs" />
385385
<Compile Include="$(CommonPath)\Interop\Windows\WinHttp\Interop.winhttp.cs"
386386
Link="Common\Interop\Windows\WinHttp\Interop.winhttp.cs" />
387+
<Compile Include="$(CommonPath)\Interop\Windows\Advapi32\Interop.RegNotifyChangeKeyValue.cs"
388+
Link="Common\Interop\Windows\Advapi32\Interop.RegNotifyChangeKeyValue.cs" />
387389
<Compile Include="$(CommonPath)\System\CharArrayHelpers.cs"
388390
Link="Common\System\CharArrayHelpers.cs" />
389391
<Compile Include="$(CommonPath)\System\Net\HttpKnownHeaderNames.cs"
@@ -479,6 +481,7 @@
479481
<Reference Include="System.Runtime" />
480482
<Reference Include="System.Runtime.InteropServices" />
481483
<Reference Include="System.Threading" />
484+
<Reference Include="Microsoft.Win32.Registry" Condition="'$(TargetPlatformIdentifier)' == 'windows'" />
482485
</ItemGroup>
483486

484487
<ItemGroup Condition="'$(TargetPlatformIdentifier)' != '' and '$(TargetPlatformIdentifier)' != 'windows' and '$(TargetPlatformIdentifier)' != 'browser'">

src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpWindowsProxy.cs

Lines changed: 85 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -6,45 +6,81 @@
66
using System.Collections.Generic;
77
using System.Diagnostics;
88
using System.Diagnostics.CodeAnalysis;
9-
using System.IO.Compression;
109
using System.Net.NetworkInformation;
1110
using System.Runtime.InteropServices;
1211
using System.Text;
1312
using System.Threading;
13+
using Microsoft.Win32;
1414
using SafeWinHttpHandle = Interop.WinHttp.SafeWinHttpHandle;
1515

1616
namespace System.Net.Http
1717
{
1818
internal sealed class HttpWindowsProxy : IMultiWebProxy, IDisposable
1919
{
20-
private readonly MultiProxy _insecureProxy; // URI of the http system proxy if set
21-
private readonly MultiProxy _secureProxy; // URI of the https system proxy if set
22-
private readonly FailedProxyCache _failedProxies = new FailedProxyCache();
23-
private readonly List<string>? _bypass; // list of domains not to proxy
24-
private readonly bool _bypassLocal; // we should bypass domain considered local
25-
private readonly List<IPAddress>? _localIp;
20+
private readonly RegistryKey? _internetSettingsRegistry = Registry.CurrentUser?.OpenSubKey("Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings");
21+
private MultiProxy _insecureProxy; // URI of the http system proxy if set
22+
private MultiProxy _secureProxy; // URI of the https system proxy if set
23+
private FailedProxyCache _failedProxies = new FailedProxyCache();
24+
private List<string>? _bypass; // list of domains not to proxy
25+
private List<IPAddress>? _localIp;
2626
private ICredentials? _credentials;
27-
private readonly WinInetProxyHelper _proxyHelper;
27+
private WinInetProxyHelper _proxyHelper;
2828
private SafeWinHttpHandle? _sessionHandle;
2929
private bool _disposed;
30+
private EventWaitHandle _waitHandle = new EventWaitHandle(false, EventResetMode.AutoReset);
31+
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;
32+
private RegisteredWaitHandle? _registeredWaitHandle;
3033

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

38-
WinInetProxyHelper proxyHelper = new WinInetProxyHelper();
39-
if (!proxyHelper.ManualSettingsOnly && !proxyHelper.AutoSettingsUsed)
38+
if (_internetSettingsRegistry != null && proxy == null)
4039
{
41-
return false;
40+
// we register for change notifications so we can react to changes during lifetime.
41+
if (Interop.Advapi32.RegNotifyChangeKeyValue(_internetSettingsRegistry.Handle, true, RegistrationFlags, _waitHandle.SafeWaitHandle, true) == 0)
42+
{
43+
_registeredWaitHandle = ThreadPool.RegisterWaitForSingleObject(_waitHandle, RegistryChangeNotificationCallback, this, -1, false);
44+
}
45+
}
46+
47+
UpdateConfiguration(proxy);
48+
}
49+
50+
private static void RegistryChangeNotificationCallback(object? state, bool timedOut)
51+
{
52+
HttpWindowsProxy proxy = (HttpWindowsProxy)state!;
53+
if (!proxy._disposed)
54+
{
55+
56+
// This is executed from threadpool. we should not ever throw here.
57+
try
58+
{
59+
// We need to register for notification every time. We regisrerand lock before we process configuration
60+
// so if there is update it would be serialized to ensure consistency.
61+
Interop.Advapi32.RegNotifyChangeKeyValue(proxy._internetSettingsRegistry!.Handle, true, RegistrationFlags, proxy._waitHandle.SafeWaitHandle, true);
62+
lock (proxy)
63+
{
64+
proxy.UpdateConfiguration();
65+
}
66+
}
67+
catch (Exception ex)
68+
{
69+
if (NetEventSource.Log.IsEnabled()) NetEventSource.Error(proxy, $"Failed to refresh proxy configuration: {ex.Message}");
70+
}
4271
}
72+
}
73+
74+
[MemberNotNull(nameof(_proxyHelper))]
75+
private void UpdateConfiguration(WinInetProxyHelper? proxyHelper = null)
76+
{
77+
78+
proxyHelper ??= new WinInetProxyHelper();
4379

4480
if (proxyHelper.AutoSettingsUsed)
4581
{
4682
if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(proxyHelper, $"AutoSettingsUsed, calling {nameof(Interop.WinHttp.WinHttpOpen)}");
47-
sessionHandle = Interop.WinHttp.WinHttpOpen(
83+
SafeWinHttpHandle? sessionHandle = Interop.WinHttp.WinHttpOpen(
4884
IntPtr.Zero,
4985
Interop.WinHttp.WINHTTP_ACCESS_TYPE_NO_PROXY,
5086
Interop.WinHttp.WINHTTP_NO_PROXY_NAME,
@@ -56,18 +92,10 @@ public static bool TryCreate([NotNullWhen(true)] out IWebProxy? proxy)
5692
// Proxy failures are currently ignored by managed handler.
5793
if (NetEventSource.Log.IsEnabled()) NetEventSource.Error(proxyHelper, $"{nameof(Interop.WinHttp.WinHttpOpen)} returned invalid handle");
5894
sessionHandle.Dispose();
59-
return false;
6095
}
61-
}
62-
63-
proxy = new HttpWindowsProxy(proxyHelper, sessionHandle);
64-
return true;
65-
}
6696

67-
private HttpWindowsProxy(WinInetProxyHelper proxyHelper, SafeWinHttpHandle? sessionHandle)
68-
{
69-
_proxyHelper = proxyHelper;
70-
_sessionHandle = sessionHandle;
97+
_sessionHandle = sessionHandle;
98+
}
7199

72100
if (proxyHelper.ManualSettingsUsed)
73101
{
@@ -80,10 +108,12 @@ private HttpWindowsProxy(WinInetProxyHelper proxyHelper, SafeWinHttpHandle? sess
80108
{
81109
int idx = 0;
82110
string? tmp;
111+
bool bypassLocal = false;
112+
List<IPAddress>? localIp = null;
83113

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

88118
while (idx < proxyHelper.ProxyBypass.Length)
89119
{
@@ -114,7 +144,7 @@ private HttpWindowsProxy(WinInetProxyHelper proxyHelper, SafeWinHttpHandle? sess
114144
}
115145
else if (string.Compare(proxyHelper.ProxyBypass, start, "<local>", 0, 7, StringComparison.OrdinalIgnoreCase) == 0)
116146
{
117-
_bypassLocal = true;
147+
bypassLocal = true;
118148
tmp = null;
119149
}
120150
else
@@ -137,28 +167,29 @@ private HttpWindowsProxy(WinInetProxyHelper proxyHelper, SafeWinHttpHandle? sess
137167
continue;
138168
}
139169

140-
_bypass.Add(tmp);
141-
}
142-
if (_bypass.Count == 0)
143-
{
144-
// Bypass string only had garbage we did not parse.
145-
_bypass = null;
170+
bypass.Add(tmp);
146171
}
147-
}
148172

149-
if (_bypassLocal)
150-
{
151-
_localIp = new List<IPAddress>();
152-
foreach (NetworkInterface netInterface in NetworkInterface.GetAllNetworkInterfaces())
173+
_bypass = bypass.Count > 0 ? bypass : null;
174+
175+
if (bypassLocal)
153176
{
154-
IPInterfaceProperties ipProps = netInterface.GetIPProperties();
155-
foreach (UnicastIPAddressInformation addr in ipProps.UnicastAddresses)
177+
localIp = new List<IPAddress>();
178+
foreach (NetworkInterface netInterface in NetworkInterface.GetAllNetworkInterfaces())
156179
{
157-
_localIp.Add(addr.Address);
180+
IPInterfaceProperties ipProps = netInterface.GetIPProperties();
181+
foreach (UnicastIPAddressInformation addr in ipProps.UnicastAddresses)
182+
{
183+
localIp.Add(addr.Address);
184+
}
158185
}
159186
}
187+
188+
_localIp = localIp?.Count > 0 ? localIp : null;
160189
}
161190
}
191+
192+
_proxyHelper = proxyHelper;
162193
}
163194

164195
public void Dispose()
@@ -171,6 +202,10 @@ public void Dispose()
171202
{
172203
SafeWinHttpHandle.DisposeAndClearHandle(ref _sessionHandle);
173204
}
205+
206+
_waitHandle?.Dispose();
207+
_internetSettingsRegistry?.Dispose();
208+
_registeredWaitHandle?.Unregister(null);
174209
}
175210
}
176211

@@ -179,6 +214,11 @@ public void Dispose()
179214
/// </summary>
180215
public Uri? GetProxy(Uri uri)
181216
{
217+
if (!_proxyHelper.AutoSettingsUsed && !_proxyHelper.ManualSettingsOnly)
218+
{
219+
return null;
220+
}
221+
182222
GetMultiProxy(uri).ReadNext(out Uri? proxyUri, out _);
183223
return proxyUri;
184224
}
@@ -240,7 +280,7 @@ public MultiProxy GetMultiProxy(Uri uri)
240280
// Fallback to manual settings if present.
241281
if (_proxyHelper.ManualSettingsUsed)
242282
{
243-
if (_bypassLocal)
283+
if (_localIp != null)
244284
{
245285
IPAddress? address;
246286

@@ -261,7 +301,7 @@ public MultiProxy GetMultiProxy(Uri uri)
261301
{
262302
// Host is valid IP address.
263303
// Check if it belongs to local system.
264-
foreach (IPAddress a in _localIp!)
304+
foreach (IPAddress a in _localIp)
265305
{
266306
if (a.Equals(address))
267307
{

src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/SystemProxyInfo.Windows.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,11 @@ public static IWebProxy ConstructSystemProxy()
1010
{
1111
if (!HttpEnvironmentProxy.TryCreate(out IWebProxy? proxy))
1212
{
13-
HttpWindowsProxy.TryCreate(out proxy);
13+
// We create instance even if there is currently no proxy as that can change during application run.
14+
proxy = new HttpWindowsProxy();
1415
}
1516

16-
return proxy ?? new HttpNoProxy();
17+
return proxy;
1718
}
1819
}
1920
}

0 commit comments

Comments
 (0)