|
//------------------------------------------------------------------------------
// <copyright fieldInfole="_AutoWebProxyScriptEngine.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
//------------------------------------------------------------------------------
namespace System.Net
{
#if !FEATURE_PAL
using System.Net.NetworkInformation;
using System.Security.Principal;
#endif
using System.Collections.Generic;
using System.Globalization;
using System.Net.Configuration;
using System.Security.Permissions;
using System.Threading;
// This class (and its helper classes implementing IWebRequestFinder interface) are responsible for
// determining the location of the PAC file, download and execute it, in order to retrieve proxy
// information.
// This class also monitors the Registry and re-reads proxy configuration settings, if the corresponding
// Registry values change.
internal class AutoWebProxyScriptEngine
{
private bool automaticallyDetectSettings;
private Uri automaticConfigurationScript;
private WebProxy webProxy;
private IWebProxyFinder webProxyFinder;
private bool executeWinHttpGetProxyForUrl = false;
private Timer retryWinHttpGetProxyForUrlTimer;
// Used by abortable lock.
private bool m_LockHeld;
private bool m_UseRegistry;
#if !FEATURE_PAL
// Used to get notifications of network changes and do AutoDetection (which are global).
private int m_NetworkChangeStatus;
private AutoDetector m_AutoDetector;
// This has to hold on to the creating user's registry hive and impersonation context.
private SafeRegistryHandle hkcu;
private WindowsIdentity m_Identity;
#endif // !FEATURE_PAL
[SecurityPermission(SecurityAction.Assert, Flags = SecurityPermissionFlag.ControlPrincipal)]
internal AutoWebProxyScriptEngine(WebProxy proxy, bool useRegistry)
{
GlobalLog.Assert(proxy != null, "'proxy' must be assigned.");
webProxy = proxy;
m_UseRegistry = useRegistry;
#if !FEATURE_PAL
m_AutoDetector = AutoDetector.CurrentAutoDetector;
m_NetworkChangeStatus = m_AutoDetector.NetworkChangeStatus;
SafeRegistryHandle.RegOpenCurrentUser(UnsafeNclNativeMethods.RegistryHelper.KEY_READ, out hkcu);
if (m_UseRegistry)
{
ListenForRegistry();
// Keep track of the identity we used to read the registry, in case we need to read it again later.
m_Identity = WindowsIdentity.GetCurrent();
}
#endif // !FEATURE_PAL
// In Win2003 winhttp added a Windows Service handling the auto-proxy discovery. In XP using winhttp
// APIs will load, compile and execute the wpad file in-process. This will also load COM, since
// WinHttp requires COM to compile the file. For these reasons, we don't use WinHttp on XP, but
// only on newer OS versions where the "WinHTTP Web Proxy Auto-Discovery Service" exists.
webProxyFinder = new HybridWebProxyFinder(this);
}
// AutoWebProxyScriptEngine has special abortable locking. No one should ever lock (this) except the locking helper methods below.
private static class SyncStatus
{
internal const int Unlocked = 0;
internal const int Locking = 1;
internal const int LockOwner = 2;
internal const int AbortedLocked = 3;
internal const int Aborted = 4;
}
private void EnterLock(ref int syncStatus)
{
if (syncStatus == SyncStatus.Unlocked)
{
lock (this)
{
if (syncStatus != SyncStatus.Aborted)
{
syncStatus = SyncStatus.Locking;
while (true)
{
if (!m_LockHeld)
{
syncStatus = SyncStatus.LockOwner;
m_LockHeld = true;
return;
}
Monitor.Wait(this);
if (syncStatus == SyncStatus.Aborted)
{
Monitor.Pulse(this); // This is to ensure that a Pulse meant to let someone take the lock isn't lost.
return;
}
}
}
}
}
}
private void ExitLock(ref int syncStatus)
{
if (syncStatus != SyncStatus.Unlocked && syncStatus != SyncStatus.Aborted)
{
lock (this)
{
m_LockHeld = false;
if (syncStatus == SyncStatus.AbortedLocked)
{
webProxyFinder.Reset();
syncStatus = SyncStatus.Aborted;
}
else
{
syncStatus = SyncStatus.Unlocked;
}
Monitor.Pulse(this);
}
}
}
internal void Abort(ref int syncStatus)
{
lock (this)
{
switch (syncStatus)
{
case SyncStatus.Unlocked:
syncStatus = SyncStatus.Aborted;
break;
case SyncStatus.Locking:
syncStatus = SyncStatus.Aborted;
Monitor.PulseAll(this);
break;
case SyncStatus.LockOwner:
syncStatus = SyncStatus.AbortedLocked;
webProxyFinder.Abort();
break;
}
}
}
// End of locking helper methods.
// The lock is always held while these three are modified.
internal bool AutomaticallyDetectSettings
{
get
{
return automaticallyDetectSettings;
}
set
{
if (automaticallyDetectSettings != value)
{
automaticallyDetectSettings = value;
webProxyFinder.Reset();
}
}
}
internal Uri AutomaticConfigurationScript
{
get
{
return automaticConfigurationScript;
}
set
{
if (!object.Equals(automaticConfigurationScript, value))
{
automaticConfigurationScript = value;
webProxyFinder.Reset();
}
}
}
internal ICredentials Credentials
{
get
{
return webProxy.Credentials;
}
}
internal bool GetProxies(Uri destination, out IList<string> proxyList)
{
int syncStatus = SyncStatus.Unlocked;
return GetProxies(destination, out proxyList, ref syncStatus);
}
internal bool GetProxies(Uri destination, out IList<string> proxyList, ref int syncStatus)
{
GlobalLog.Print("AutoWebProxyScriptEngine#" + ValidationHelper.HashString(this) + "::GetProxies()");
proxyList = null;
#if !FEATURE_PAL
// See if we need to reinitialize based on registry or other changes.
#if !AUTOPROXY_SKIP_CHECK
CheckForChanges(ref syncStatus);
#endif
#endif // !FEATURE_PAL
if (!webProxyFinder.IsValid)
{
// If a zero value is specified in the configuration setting for AutoConfigUrlRetryInterval, the retry feature will be opted-out.
if (retryWinHttpGetProxyForUrlTimer == null && SettingsSectionInternal.Section.AutoConfigUrlRetryInterval != 0)
{
long retryIntervalInMilliseconds = SettingsSectionInternal.Section.AutoConfigUrlRetryInterval * 1000;
retryWinHttpGetProxyForUrlTimer = new Timer(s =>
{
var wr = (WeakReference<AutoWebProxyScriptEngine>)s;
AutoWebProxyScriptEngine thisRef;
if (wr.TryGetTarget(out thisRef))
{
thisRef.executeWinHttpGetProxyForUrl = true;
}
}, new WeakReference<AutoWebProxyScriptEngine>(this),
retryIntervalInMilliseconds, retryIntervalInMilliseconds);
}
if (executeWinHttpGetProxyForUrl)
{
// Reset the value and continue execution.
executeWinHttpGetProxyForUrl = false;
}
else
{
// This is to improve performance on e.g. home networks, where auto-detect will always
// fail, but IE settings turn auto-detect ON by default. I.e. in home networks on each
// call we would try to retrieve the PAC location.
// To retry retrieving the PAC file, need to wait for the next signal from retryWinHttpGetProxyForUrlTimer.
return false;
}
}
else
{
if (retryWinHttpGetProxyForUrlTimer != null)
{
retryWinHttpGetProxyForUrlTimer.Dispose();
retryWinHttpGetProxyForUrlTimer = null;
}
}
// This whole thing has to be locked, both to prevent simultaneous downloading / compilation, and
// because the script isn't threadsafe.
try
{
EnterLock(ref syncStatus);
if (syncStatus != SyncStatus.LockOwner)
{
// This is typically because a download got aborted.
return false;
}
return webProxyFinder.GetProxies(destination, out proxyList);
}
finally
{
ExitLock(ref syncStatus);
GlobalLog.Print("AutoWebProxyScriptEngine#" + ValidationHelper.HashString(this) + "::GetProxies() proxies:" + ValidationHelper.ToString(proxyList));
}
}
internal WebProxyData GetWebProxyData()
{
// PS DevDiv bug #217205 / TFS Dev10 #588370: use winhttp.WinhttpGetIEProxyConfigForCurrentUser
WebProxyDataBuilder builder = null;
if (ComNetOS.IsWin7orLater)
{
builder = new WinHttpWebProxyBuilder();
}
else
{
builder = new RegBlobWebProxyDataBuilder(m_AutoDetector.Connectoid, hkcu);
}
return builder.Build();
}
internal void Close()
{
#if !FEATURE_PAL
// m_AutoDetector is always set up in the constructor, use it to lock
if (m_AutoDetector != null)
{
int syncStatus = SyncStatus.Unlocked;
try
{
EnterLock(ref syncStatus);
GlobalLog.Assert(syncStatus == SyncStatus.LockOwner, "AutoWebProxyScriptEngine#{0}::Close()|Failed to acquire lock.", ValidationHelper.HashString(this));
if (m_AutoDetector != null)
{
registrySuppress = true;
if (registryChangeEventPolicy != null)
{
registryChangeEventPolicy.Close();
registryChangeEventPolicy = null;
}
if (registryChangeEventLM != null)
{
registryChangeEventLM.Close();
registryChangeEventLM = null;
}
if (registryChangeEvent != null)
{
registryChangeEvent.Close();
registryChangeEvent = null;
}
if (regKeyPolicy != null && !regKeyPolicy.IsInvalid)
{
regKeyPolicy.Close();
}
if (regKeyLM != null && !regKeyLM.IsInvalid)
{
regKeyLM.Close();
}
if (regKey != null && !regKey.IsInvalid)
{
regKey.Close();
}
if (hkcu != null)
{
hkcu.RegCloseKey();
hkcu = null;
}
if (m_Identity != null)
{
m_Identity.Dispose();
m_Identity = null;
}
webProxyFinder.Dispose();
m_AutoDetector = null;
}
}
finally
{
ExitLock(ref syncStatus);
}
}
#endif // !FEATURE_PAL
}
#if !FEATURE_PAL
private SafeRegistryHandle regKey;
private SafeRegistryHandle regKeyLM;
private SafeRegistryHandle regKeyPolicy;
private AutoResetEvent registryChangeEvent;
private AutoResetEvent registryChangeEventLM;
private AutoResetEvent registryChangeEventPolicy;
private bool registryChangeDeferred;
private bool registryChangeLMDeferred;
private bool registryChangePolicyDeferred;
private bool needRegistryUpdate;
private bool needConnectoidUpdate;
private bool registrySuppress;
internal void ListenForRegistry()
{
GlobalLog.Print("AutoWebProxyScriptEngine#" + ValidationHelper.HashString(this) + "::ListenForRegistry()");
if (!registrySuppress)
{
if (registryChangeEvent == null)
{
GlobalLog.Print("AutoWebProxyScriptEngine#" + ValidationHelper.HashString(this) + "::ListenForRegistry() hooking HKCU.");
ListenForRegistryHelper(ref regKey, ref registryChangeEvent, IntPtr.Zero,
RegBlobWebProxyDataBuilder.ProxyKey);
}
if (registryChangeEventLM == null)
{
GlobalLog.Print("AutoWebProxyScriptEngine#" + ValidationHelper.HashString(this) + "::ListenForRegistry() hooking HKLM.");
ListenForRegistryHelper(ref regKeyLM, ref registryChangeEventLM,
UnsafeNclNativeMethods.RegistryHelper.HKEY_LOCAL_MACHINE, RegBlobWebProxyDataBuilder.ProxyKey);
}
if (registryChangeEventPolicy == null)
{
GlobalLog.Print("AutoWebProxyScriptEngine#" + ValidationHelper.HashString(this) + "::ListenForRegistry() hooking HKLM/Policies.");
ListenForRegistryHelper(ref regKeyPolicy, ref registryChangeEventPolicy,
UnsafeNclNativeMethods.RegistryHelper.HKEY_LOCAL_MACHINE, RegBlobWebProxyDataBuilder.PolicyKey);
}
// If any succeeded, we should monitor it.
if (registryChangeEvent == null && registryChangeEventLM == null && registryChangeEventPolicy == null)
{
registrySuppress = true;
}
}
}
private void ListenForRegistryHelper(ref SafeRegistryHandle key, ref AutoResetEvent changeEvent, IntPtr baseKey, string subKey)
{
uint errorCode = 0;
// First time through?
if (key == null || key.IsInvalid)
{
if (baseKey == IntPtr.Zero)
{
// Impersonation requires extra effort.
GlobalLog.Print("AutoWebProxyScriptEngine#" + ValidationHelper.HashString(this) + "::ListenForRegistry() RegOpenCurrentUser() using hkcu:" + hkcu.DangerousGetHandle().ToString("x"));
if (hkcu != null)
{
errorCode = hkcu.RegOpenKeyEx(subKey, 0, UnsafeNclNativeMethods.RegistryHelper.KEY_READ, out key);
GlobalLog.Print("AutoWebProxyScriptEngine#" + ValidationHelper.HashString(this) + "::ListenForRegistry() RegOpenKeyEx() returned errorCode:" + errorCode + " key:" + key.DangerousGetHandle().ToString("x"));
}
else
{
errorCode = UnsafeNclNativeMethods.ErrorCodes.ERROR_NOT_FOUND;
}
}
else
{
errorCode = SafeRegistryHandle.RegOpenKeyEx(baseKey, subKey, 0, UnsafeNclNativeMethods.RegistryHelper.KEY_READ, out key);
//GlobalLog.Print("AutoWebProxyScriptEngine#" + ValidationHelper.HashString(this) + "::ListenForRegistry() RegOpenKeyEx() returned errorCode:" + errorCode + " key:" + key.DangerousGetHandle().ToString("x"));
}
if (errorCode == 0)
{
changeEvent = new AutoResetEvent(false);
}
}
if (errorCode == 0)
{
// accessing Handle is protected by a link demand, OK for System.dll
errorCode = key.RegNotifyChangeKeyValue(true, UnsafeNclNativeMethods.RegistryHelper.REG_NOTIFY_CHANGE_LAST_SET, changeEvent.SafeWaitHandle, true);
GlobalLog.Print("AutoWebProxyScriptEngine#" + ValidationHelper.HashString(this) + "::ListenForRegistry() RegNotifyChangeKeyValue() returned errorCode:" + errorCode);
}
if (errorCode != 0)
{
if (key != null && !key.IsInvalid)
{
try
{
errorCode = key.RegCloseKey();
}
catch (Exception exception)
{
if (NclUtilities.IsFatal(exception)) throw;
}
GlobalLog.Print("AutoWebProxyScriptEngine#" + ValidationHelper.HashString(this) + "::ListenForRegistry() RegCloseKey() returned errorCode:" + errorCode);
}
key = null;
if (changeEvent != null)
{
changeEvent.Close();
changeEvent = null;
}
}
}
private void RegistryChanged()
{
GlobalLog.Print("AutoWebProxyScriptEngine#" + ValidationHelper.HashString(this) + "::RegistryChanged()");
if (Logging.On) Logging.PrintWarning(Logging.Web, SR.GetString(SR.net_log_proxy_system_setting_update));
// always refresh settings because they might have changed
WebProxyData webProxyData;
using (m_Identity.Impersonate())
{
webProxyData = GetWebProxyData();
}
webProxy.Update(webProxyData);
}
private void ConnectoidChanged()
{
GlobalLog.Print("AutoWebProxyScriptEngine#" + ValidationHelper.HashString(this) + "::ConnectoidChanged()");
if (Logging.On) Logging.PrintWarning(Logging.Web, SR.GetString(SR.net_log_proxy_update_due_to_ip_config_change));
// Get the new connectoid/detector. Only do this after detecting a change, to avoid ----s with other people detecting changes.
// (We don't want to end up using a detector/connectoid that doesn't match what we read from the registry.)
m_AutoDetector = AutoDetector.CurrentAutoDetector;
if (m_UseRegistry)
{
// update the engine and proxy
WebProxyData webProxyData;
using (m_Identity.Impersonate())
{
webProxyData = GetWebProxyData();
}
webProxy.Update(webProxyData);
}
// Always uninitialized if the connectoid/address changed and we are autodetecting.
if (automaticallyDetectSettings)
webProxyFinder.Reset();
}
internal void CheckForChanges()
{
int syncStatus = SyncStatus.Unlocked;
CheckForChanges(ref syncStatus);
}
[SecurityPermission(SecurityAction.Assert, Flags = SecurityPermissionFlag.ControlPrincipal)]
private void CheckForChanges(ref int syncStatus)
{
// Catch ObjectDisposedException instead of synchronizing with Close().
try
{
bool changed = AutoDetector.CheckForNetworkChanges(ref m_NetworkChangeStatus);
bool ignoreRegistryChange = false;
if (changed || needConnectoidUpdate)
{
try
{
EnterLock(ref syncStatus);
if (changed || needConnectoidUpdate) // Make sure no one else took care of it before we got the lock.
{
needConnectoidUpdate = syncStatus != SyncStatus.LockOwner;
if (!needConnectoidUpdate)
{
ConnectoidChanged();
// We usually get a registry change at the same time. Since the connectoid change does more,
// we can skip reading the registry info twice.
ignoreRegistryChange = true;
}
}
}
finally
{
ExitLock(ref syncStatus);
}
}
if (!m_UseRegistry)
{
return;
}
bool forReal = false;
AutoResetEvent tempEvent = registryChangeEvent;
if (registryChangeDeferred || (forReal = (tempEvent != null && tempEvent.WaitOne(0, false))))
{
try
{
EnterLock(ref syncStatus);
if (forReal || registryChangeDeferred) // Check if someone else handled it before I got the lock.
{
registryChangeDeferred = syncStatus != SyncStatus.LockOwner;
if (!registryChangeDeferred && registryChangeEvent != null)
{
try
{
using (m_Identity.Impersonate())
{
ListenForRegistryHelper(ref regKey, ref registryChangeEvent, IntPtr.Zero,
RegBlobWebProxyDataBuilder.ProxyKey);
}
}
catch
{
throw;
}
needRegistryUpdate = true;
}
}
}
finally
{
ExitLock(ref syncStatus);
}
}
forReal = false;
tempEvent = registryChangeEventLM;
if (registryChangeLMDeferred || (forReal = (tempEvent != null && tempEvent.WaitOne(0, false))))
{
try
{
EnterLock(ref syncStatus);
if (forReal || registryChangeLMDeferred) // Check if someone else handled it before I got the lock.
{
registryChangeLMDeferred = syncStatus != SyncStatus.LockOwner;
if (!registryChangeLMDeferred && registryChangeEventLM != null)
{
try
{
using (m_Identity.Impersonate())
{
ListenForRegistryHelper(ref regKeyLM, ref registryChangeEventLM,
UnsafeNclNativeMethods.RegistryHelper.HKEY_LOCAL_MACHINE,
RegBlobWebProxyDataBuilder.ProxyKey);
}
}
catch
{
throw;
}
needRegistryUpdate = true;
}
}
}
finally
{
ExitLock(ref syncStatus);
}
}
forReal = false;
tempEvent = registryChangeEventPolicy;
if (registryChangePolicyDeferred || (forReal = (tempEvent != null && tempEvent.WaitOne(0, false))))
{
try
{
EnterLock(ref syncStatus);
if (forReal || registryChangePolicyDeferred) // Check if someone else handled it before I got the lock.
{
registryChangePolicyDeferred = syncStatus != SyncStatus.LockOwner;
if (!registryChangePolicyDeferred && registryChangeEventPolicy != null)
{
try
{
using (m_Identity.Impersonate())
{
ListenForRegistryHelper(ref regKeyPolicy, ref registryChangeEventPolicy,
UnsafeNclNativeMethods.RegistryHelper.HKEY_LOCAL_MACHINE,
RegBlobWebProxyDataBuilder.PolicyKey);
}
}
catch
{
throw;
}
needRegistryUpdate = true;
}
}
}
finally
{
ExitLock(ref syncStatus);
}
}
if (needRegistryUpdate)
{
try
{
EnterLock(ref syncStatus);
if (needRegistryUpdate && syncStatus == SyncStatus.LockOwner)
{
needRegistryUpdate = false;
// We don't need to process this now if we just did it for the connectoid.
if (!ignoreRegistryChange)
{
RegistryChanged();
}
}
}
finally
{
ExitLock(ref syncStatus);
}
}
}
catch (ObjectDisposedException) { }
}
private class AutoDetector
{
private static volatile NetworkAddressChangePolled s_AddressChange;
private static volatile UnsafeNclNativeMethods.RasHelper s_RasHelper;
private static int s_CurrentVersion = 0;
private volatile static AutoDetector s_CurrentAutoDetector;
private volatile static bool s_Initialized;
private static object s_LockObject;
static AutoDetector()
{
s_LockObject = new object();
}
private static void Initialize()
{
if (!s_Initialized)
{
lock (s_LockObject)
{
if (!s_Initialized)
{
s_CurrentAutoDetector = new AutoDetector(UnsafeNclNativeMethods.RasHelper.GetCurrentConnectoid(), 1);
if (NetworkChange.CanListenForNetworkChanges)
{
s_AddressChange = new NetworkAddressChangePolled();
}
if (UnsafeNclNativeMethods.RasHelper.RasSupported)
{
s_RasHelper = new UnsafeNclNativeMethods.RasHelper();
}
s_CurrentVersion = 1;
s_Initialized = true;
}
}
}
}
internal static bool CheckForNetworkChanges(ref int changeStatus)
{
Initialize();
CheckForChanges();
int oldStatus = changeStatus;
changeStatus = Volatile.Read(ref s_CurrentVersion);
return oldStatus != changeStatus;
}
private static void CheckForChanges()
{
bool changed = false;
if (s_RasHelper != null && s_RasHelper.HasChanged)
{
s_RasHelper.Reset();
changed = true;
}
if (s_AddressChange != null && s_AddressChange.CheckAndReset())
{
changed = true;
}
if (changed)
{
int currentVersion = Interlocked.Increment(ref s_CurrentVersion);
s_CurrentAutoDetector = new AutoDetector(UnsafeNclNativeMethods.RasHelper.GetCurrentConnectoid(), currentVersion);
}
}
internal static AutoDetector CurrentAutoDetector
{
get
{
Initialize();
return s_CurrentAutoDetector;
}
}
private readonly string m_Connectoid;
private readonly int m_CurrentVersion;
private AutoDetector(string connectoid, int currentVersion)
{
m_Connectoid = connectoid;
m_CurrentVersion = currentVersion;
}
internal string Connectoid
{
get
{
return m_Connectoid;
}
}
internal int NetworkChangeStatus
{
get
{
return m_CurrentVersion;
}
}
}
#endif
}
}
|