1 using System.IO; 2 using System.Collections; 3 using System.Collections.Specialized; 4 using System.Threading; 5 using System.Text; 6 using System.Net.Cache; 7 using System.Globalization; 8 using System.Net.Configuration; 9 using System.Security.Permissions; 10 using System.Collections.Generic; 11 using System.Runtime.InteropServices; 12 using Microsoft.Win32; 13 using System.Diagnostics.CodeAnalysis; 14 15 namespace System.Net 16 { 17 // This WebProxyFinder implementation has the following purpose: 18 // - use WinHttp APIs to determine the location of the PAC file 19 // - use System.Net classes (WebRequest) to download the PAC file 20 // - use Microsoft.JScript to compile and execute the JavaScript in the PAC file. 21 internal sealed class NetWebProxyFinder : BaseWebProxyFinder 22 { 23 private static readonly char[] splitChars = new char[] { ';' }; 24 private static TimerThread.Queue timerQueue; 25 private static readonly TimerThread.Callback timerCallback = new TimerThread.Callback(RequestTimeoutCallback); 26 private static readonly WaitCallback abortWrapper = new WaitCallback(AbortWrapper); 27 28 private RequestCache backupCache; 29 private AutoWebProxyScriptWrapper scriptInstance; 30 private Uri engineScriptLocation; 31 private Uri scriptLocation; 32 private bool scriptDetectionFailed; 33 private object lockObject; 34 // Keep the following fields volatile, since we're accessing them outside of lock blocks 35 private volatile WebRequest request; 36 private volatile bool aborted; 37 NetWebProxyFinder(AutoWebProxyScriptEngine engine)38 public NetWebProxyFinder(AutoWebProxyScriptEngine engine) 39 : base(engine) 40 { 41 backupCache = new SingleItemRequestCache(RequestCacheManager.IsCachingEnabled); 42 lockObject = new object(); 43 } 44 GetProxies(Uri destination, out IList<string> proxyList)45 public override bool GetProxies(Uri destination, out IList<string> proxyList) 46 { 47 try 48 { 49 proxyList = null; 50 51 EnsureEngineAvailable(); 52 53 // after EnsureEngineAvailable we expect State to be CompilationSuccess, otherwise return. 54 if (State != AutoWebProxyState.Completed) 55 { 56 // the script can't run, say we're not ready and bypass 57 return false; 58 } 59 60 bool result = false; 61 try 62 { 63 string proxyListString = scriptInstance.FindProxyForURL(destination.ToString(), destination.Host); 64 GlobalLog.Print("NetWebProxyFinder#" + ValidationHelper.HashString(this) + "::GetProxies() calling ExecuteFindProxyForURL() for destination:" + ValidationHelper.ToString(destination) + " returned scriptReturn:" + ValidationHelper.ToString(proxyList)); 65 66 proxyList = ParseScriptResult(proxyListString); 67 68 result = true; 69 } 70 catch (Exception exception) 71 { 72 if (Logging.On) Logging.PrintWarning(Logging.Web, SR.GetString(SR.net_log_proxy_script_execution_error, exception)); 73 } 74 75 return result; 76 } 77 finally 78 { 79 // Reset state of 'aborted', since next call to GetProxies() must not use previous aborted state. 80 aborted = false; 81 } 82 } 83 Abort()84 public override void Abort() 85 { 86 // All we abort is a running WebRequest. The following lock (and the one in DownloadAndCompile) 87 // is used to "atomically" access the two fields 'aborted' and 'request': If Abort() gets 88 // called before 'request' is set, the 'aborted' field will signal to DownloadAndCompile, that 89 // it should not bother creating a request and just throw. If 'request' was already created 90 // by DownloadAndCompile, the following code will make sure the request gets aborted. 91 lock (lockObject) 92 { 93 aborted = true; 94 95 if (request != null) 96 { 97 ThreadPool.UnsafeQueueUserWorkItem(abortWrapper, request); 98 } 99 } 100 } 101 Dispose(bool disposing)102 protected override void Dispose(bool disposing) 103 { 104 if (disposing) 105 { 106 if (scriptInstance != null) 107 { 108 scriptInstance.Close(); 109 } 110 } 111 } 112 113 // Ensures that (if state is AutoWebProxyState.CompilationSuccess) there is an engine available to execute script. 114 // Figures out the script location (might discover if needed). 115 // Calls DownloadAndCompile(). EnsureEngineAvailable()116 private void EnsureEngineAvailable() 117 { 118 GlobalLog.Enter("NetWebProxyFinder#" + ValidationHelper.HashString(this) + "::EnsureEngineAvailable"); 119 120 if (State == AutoWebProxyState.Uninitialized || engineScriptLocation == null) 121 { 122 #if !FEATURE_PAL 123 if (Engine.AutomaticallyDetectSettings) 124 { 125 GlobalLog.Print("NetWebProxyFinder#" + ValidationHelper.HashString(this) + "::EnsureEngineAvailable() Attempting auto-detection."); 126 DetectScriptLocation(); 127 if (scriptLocation != null) 128 { 129 // 130 // Successfully detected or user has flipped the automaticallyDetectSettings bit. 131 // Attempt a non conclusive DownloadAndCompile() so we can fallback 132 // 133 GlobalLog.Print("NetWebProxyFinder#" + ValidationHelper.HashString(this) + "::EnsureEngineAvailable() discovered:" + ValidationHelper.ToString(scriptLocation) + " engineScriptLocation:" + ValidationHelper.ToString(engineScriptLocation)); 134 if (scriptLocation.Equals(engineScriptLocation)) 135 { 136 State = AutoWebProxyState.Completed; 137 GlobalLog.Leave("NetWebProxyFinder#" + ValidationHelper.HashString(this) + "::EnsureEngineAvailable", ValidationHelper.ToString(State)); 138 return; 139 } 140 AutoWebProxyState newState = DownloadAndCompile(scriptLocation); 141 if (newState == AutoWebProxyState.Completed) 142 { 143 State = AutoWebProxyState.Completed; 144 engineScriptLocation = scriptLocation; 145 GlobalLog.Leave("NetWebProxyFinder#" + ValidationHelper.HashString(this) + "::EnsureEngineAvailable", ValidationHelper.ToString(State)); 146 return; 147 } 148 } 149 } 150 #endif // !FEATURE_PAL 151 152 // Either Auto-Detect wasn't enabled or something failed with it. Try the manual script location. 153 if ((Engine.AutomaticConfigurationScript != null) && !aborted) 154 { 155 GlobalLog.Print("NetWebProxyFinder#" + ValidationHelper.HashString(this) + "::EnsureEngineAvailable() using automaticConfigurationScript:" + ValidationHelper.ToString(Engine.AutomaticConfigurationScript) + " engineScriptLocation:" + ValidationHelper.ToString(engineScriptLocation)); 156 if (Engine.AutomaticConfigurationScript.Equals(engineScriptLocation)) 157 { 158 State = AutoWebProxyState.Completed; 159 GlobalLog.Leave("NetWebProxyFinder#" + ValidationHelper.HashString(this) + "::EnsureEngineAvailable", ValidationHelper.ToString(State)); 160 return; 161 } 162 State = DownloadAndCompile(Engine.AutomaticConfigurationScript); 163 if (State == AutoWebProxyState.Completed) 164 { 165 engineScriptLocation = Engine.AutomaticConfigurationScript; 166 GlobalLog.Leave("NetWebProxyFinder#" + ValidationHelper.HashString(this) + "::EnsureEngineAvailable", ValidationHelper.ToString(State)); 167 return; 168 } 169 } 170 } 171 else 172 { 173 // We always want to call DownloadAndCompile to check the expiration. 174 GlobalLog.Print("NetWebProxyFinder#" + ValidationHelper.HashString(this) + "::EnsureEngineAvailable() State:" + State + " engineScriptLocation:" + ValidationHelper.ToString(engineScriptLocation)); 175 State = DownloadAndCompile(engineScriptLocation); 176 if (State == AutoWebProxyState.Completed) 177 { 178 GlobalLog.Leave("NetWebProxyFinder#" + ValidationHelper.HashString(this) + "::EnsureEngineAvailable", ValidationHelper.ToString(State)); 179 return; 180 } 181 182 // There's still an opportunity to fail over to the automaticConfigurationScript. 183 if (!engineScriptLocation.Equals(Engine.AutomaticConfigurationScript) && !aborted) 184 { 185 GlobalLog.Print("NetWebProxyFinder#" + ValidationHelper.HashString(this) + "::EnsureEngineAvailable() Update failed. Falling back to automaticConfigurationScript:" + ValidationHelper.ToString(Engine.AutomaticConfigurationScript)); 186 State = DownloadAndCompile(Engine.AutomaticConfigurationScript); 187 if (State == AutoWebProxyState.Completed) 188 { 189 engineScriptLocation = Engine.AutomaticConfigurationScript; 190 GlobalLog.Leave("NetWebProxyFinder#" + ValidationHelper.HashString(this) + "::EnsureEngineAvailable", ValidationHelper.ToString(State)); 191 return; 192 } 193 } 194 } 195 196 // Everything failed. Set this instance to mostly-dead. It will wake up again if there's a reg/connectoid change. 197 GlobalLog.Print("NetWebProxyFinder#" + ValidationHelper.HashString(this) + "::EnsureEngineAvailable() All failed."); 198 State = AutoWebProxyState.DiscoveryFailure; 199 200 if (scriptInstance != null) 201 { 202 scriptInstance.Close(); 203 scriptInstance = null; 204 } 205 206 engineScriptLocation = null; 207 208 GlobalLog.Leave("NetWebProxyFinder#" + ValidationHelper.HashString(this) + "::EnsureEngineAvailable", ValidationHelper.ToString(State)); 209 } 210 211 212 // Downloads and compiles the script from a given Uri. 213 // This code can be called by config for a downloaded control, we need to assert. 214 // This code is called holding the lock. DownloadAndCompile(Uri location)215 private AutoWebProxyState DownloadAndCompile(Uri location) 216 { 217 GlobalLog.Print("NetWebProxyFinder#" + ValidationHelper.HashString(this) + "::DownloadAndCompile() location:" + ValidationHelper.ToString(location)); 218 AutoWebProxyState newState = AutoWebProxyState.DownloadFailure; 219 WebResponse response = null; 220 TimerThread.Timer timer = null; 221 AutoWebProxyScriptWrapper newScriptInstance = null; 222 223 // Can't assert this in declarative form (DCR?). This Assert() is needed to be able to create the request to download the proxy script. 224 ExceptionHelper.WebPermissionUnrestricted.Assert(); 225 try 226 { 227 lock (lockObject) 228 { 229 if (aborted) 230 { 231 throw new WebException(NetRes.GetWebStatusString("net_requestaborted", 232 WebExceptionStatus.RequestCanceled), WebExceptionStatus.RequestCanceled); 233 } 234 235 request = WebRequest.Create(location); 236 } 237 238 request.Timeout = Timeout.Infinite; 239 request.CachePolicy = new RequestCachePolicy(RequestCacheLevel.Default); 240 request.ConnectionGroupName = "__WebProxyScript"; 241 242 // We have an opportunity here, if caching is disabled AppDomain-wide, to override it with a 243 // custom, trivial cache-provider to get a similar semantic. 244 // 245 // We also want to have a backup caching key in the case when IE has locked an expired script response 246 // 247 if (request.CacheProtocol != null) 248 { 249 GlobalLog.Print("NetWebProxyFinder#" + ValidationHelper.HashString(this) + "::DownloadAndCompile() Using backup caching."); 250 request.CacheProtocol = new RequestCacheProtocol(backupCache, request.CacheProtocol.Validator); 251 } 252 253 HttpWebRequest httpWebRequest = request as HttpWebRequest; 254 if (httpWebRequest != null) 255 { 256 httpWebRequest.Accept = "*/*"; 257 httpWebRequest.UserAgent = this.GetType().FullName + "/" + Environment.Version; 258 httpWebRequest.KeepAlive = false; 259 httpWebRequest.Pipelined = false; 260 httpWebRequest.InternalConnectionGroup = true; 261 } 262 else 263 { 264 FtpWebRequest ftpWebRequest = request as FtpWebRequest; 265 if (ftpWebRequest != null) 266 { 267 ftpWebRequest.KeepAlive = false; 268 } 269 } 270 271 // Use no proxy, default cache - initiate the download. 272 request.Proxy = null; 273 request.Credentials = Engine.Credentials; 274 275 // Use our own timeout timer so that it can encompass the whole request, not just the headers. 276 if (timerQueue == null) 277 { 278 timerQueue = TimerThread.GetOrCreateQueue(SettingsSectionInternal.Section.DownloadTimeout); 279 } 280 timer = timerQueue.CreateTimer(timerCallback, request); 281 response = request.GetResponse(); 282 283 // Check Last Modified. 284 DateTime lastModified = DateTime.MinValue; 285 HttpWebResponse httpResponse = response as HttpWebResponse; 286 if (httpResponse != null) 287 { 288 lastModified = httpResponse.LastModified; 289 } 290 else 291 { 292 FtpWebResponse ftpResponse = response as FtpWebResponse; 293 if (ftpResponse != null) 294 { 295 lastModified = ftpResponse.LastModified; 296 } 297 } 298 GlobalLog.Print("NetWebProxyFinder#" + ValidationHelper.HashString(this) + "::DownloadAndCompile() lastModified:" + lastModified.ToString() + " (script):" + (scriptInstance == null ? "(null)" : scriptInstance.LastModified.ToString())); 299 if (scriptInstance != null && lastModified != DateTime.MinValue && scriptInstance.LastModified == lastModified) 300 { 301 newScriptInstance = scriptInstance; 302 newState = AutoWebProxyState.Completed; 303 } 304 else 305 { 306 string scriptBody = null; 307 byte[] scriptBuffer = null; 308 using (Stream responseStream = response.GetResponseStream()) 309 { 310 SingleItemRequestCache.ReadOnlyStream ros = responseStream as SingleItemRequestCache.ReadOnlyStream; 311 if (ros != null) 312 { 313 scriptBuffer = ros.Buffer; 314 } 315 if (scriptInstance != null && scriptBuffer != null && scriptBuffer == scriptInstance.Buffer) 316 { 317 scriptInstance.LastModified = lastModified; 318 newScriptInstance = scriptInstance; 319 newState = AutoWebProxyState.Completed; 320 GlobalLog.Print("NetWebProxyFinder#" + ValidationHelper.HashString(this) + "::DownloadAndCompile() Buffer matched - reusing Engine."); 321 } 322 else 323 { 324 using (StreamReader streamReader = new StreamReader(responseStream)) 325 { 326 scriptBody = streamReader.ReadToEnd(); 327 } 328 } 329 } 330 331 WebResponse tempResponse = response; 332 response = null; 333 tempResponse.Close(); 334 timer.Cancel(); 335 timer = null; 336 337 if (newState != AutoWebProxyState.Completed) 338 { 339 GlobalLog.Print("NetWebProxyFinder#" + ValidationHelper.HashString(this) + "::DownloadAndCompile() IsFromCache:" + tempResponse.IsFromCache.ToString() + " scriptInstance:" + ValidationHelper.HashString(scriptInstance)); 340 if (scriptInstance != null && scriptBody == scriptInstance.ScriptBody) 341 { 342 GlobalLog.Print("NetWebProxyFinder#" + ValidationHelper.HashString(this) + "::DownloadAndCompile() Script matched - using existing Engine."); 343 scriptInstance.LastModified = lastModified; 344 if (scriptBuffer != null) 345 { 346 scriptInstance.Buffer = scriptBuffer; 347 } 348 newScriptInstance = scriptInstance; 349 newState = AutoWebProxyState.Completed; 350 } 351 else 352 { 353 GlobalLog.Print("NetWebProxyFinder#" + ValidationHelper.HashString(this) + "::DownloadAndCompile() Creating AutoWebProxyScriptWrapper."); 354 newScriptInstance = new AutoWebProxyScriptWrapper(); 355 newScriptInstance.LastModified = lastModified; 356 357 if (newScriptInstance.Compile(location, scriptBody, scriptBuffer)) 358 { 359 newState = AutoWebProxyState.Completed; 360 } 361 else 362 { 363 newState = AutoWebProxyState.CompilationFailure; 364 } 365 } 366 } 367 } 368 } 369 catch (Exception exception) 370 { 371 if (Logging.On) Logging.PrintWarning(Logging.Web, SR.GetString(SR.net_log_proxy_script_download_compile_error, exception)); 372 GlobalLog.Print("NetWebProxyFinder#" + ValidationHelper.HashString(this) + "::DownloadAndCompile() Download() threw:" + ValidationHelper.ToString(exception)); 373 } 374 finally 375 { 376 if (timer != null) 377 { 378 timer.Cancel(); 379 } 380 381 // 382 try 383 { 384 if (response != null) 385 { 386 response.Close(); 387 } 388 } 389 finally 390 { 391 WebPermission.RevertAssert(); 392 393 // The request is not needed anymore. Set it to null, so if Abort() gets called, 394 // after this point, it will result in a no-op. 395 request = null; 396 } 397 } 398 399 if ((newState == AutoWebProxyState.Completed) && (scriptInstance != newScriptInstance)) 400 { 401 if (scriptInstance != null) 402 { 403 scriptInstance.Close(); 404 } 405 406 scriptInstance = newScriptInstance; 407 } 408 409 GlobalLog.Print("NetWebProxyFinder#" + ValidationHelper.HashString(this) + "::DownloadAndCompile() retuning newState:" + ValidationHelper.ToString(newState)); 410 return newState; 411 } 412 ParseScriptResult(string scriptReturn)413 private static IList<string> ParseScriptResult(string scriptReturn) 414 { 415 IList<string> result = new List<string>(); 416 417 if (scriptReturn == null) 418 { 419 return result; 420 } 421 422 string[] proxyListStrings = scriptReturn.Split(splitChars); 423 string proxyAuthority; 424 foreach (string s in proxyListStrings) 425 { 426 string proxyString = s.Trim(' '); 427 if (!proxyString.StartsWith("PROXY ", StringComparison.OrdinalIgnoreCase)) 428 { 429 if (string.Compare("DIRECT", proxyString, StringComparison.OrdinalIgnoreCase) == 0) 430 { 431 proxyAuthority = null; 432 } 433 else 434 { 435 continue; 436 } 437 } 438 else 439 { 440 // remove prefix "PROXY " (6 chars) from the string and trim additional leading spaces. 441 proxyAuthority = proxyString.Substring(6).TrimStart(' '); 442 Uri uri = null; 443 bool tryParse = Uri.TryCreate("http://" + proxyAuthority, UriKind.Absolute, out uri); 444 if (!tryParse || uri.UserInfo.Length > 0 || uri.HostNameType == UriHostNameType.Basic || uri.AbsolutePath.Length != 1 || proxyAuthority[proxyAuthority.Length - 1] == '/' || proxyAuthority[proxyAuthority.Length - 1] == '#' || proxyAuthority[proxyAuthority.Length - 1] == '?') 445 { 446 continue; 447 } 448 } 449 result.Add(proxyAuthority); 450 } 451 452 return result; 453 } 454 DetectScriptLocation()455 private void DetectScriptLocation() 456 { 457 if (scriptDetectionFailed || scriptLocation != null) 458 { 459 return; 460 } 461 462 GlobalLog.Print("NetWebProxyFinder::DetectScriptLocation() Attempting discovery PROXY_AUTO_DETECT_TYPE_DHCP."); 463 scriptLocation = SafeDetectAutoProxyUrl(UnsafeNclNativeMethods.WinHttp.AutoDetectType.Dhcp); 464 465 if (scriptLocation == null) 466 { 467 GlobalLog.Print("NetWebProxyFinder::DetectScriptLocation() Attempting discovery AUTO_DETECT_TYPE_DNS_A."); 468 scriptLocation = SafeDetectAutoProxyUrl(UnsafeNclNativeMethods.WinHttp.AutoDetectType.DnsA); 469 } 470 471 if (scriptLocation == null) 472 { 473 GlobalLog.Print("NetWebProxyFinder::DetectScriptLocation() Discovery failed."); 474 scriptDetectionFailed = true; 475 } 476 } 477 478 // from wininet.h 479 // 480 // #define INTERNET_MAX_PATH_LENGTH 2048 481 // #define INTERNET_MAX_PROTOCOL_NAME "gopher" // longest protocol name 482 // #define INTERNET_MAX_URL_LENGTH ((sizeof(INTERNET_MAX_PROTOCOL_NAME) - 1) \ 483 // + sizeof("://") \ 484 // + INTERNET_MAX_PATH_LENGTH) 485 // 486 private const int MaximumProxyStringLength = 2058; 487 488 /// <devdoc> 489 /// <para> 490 /// Called to discover script location. This performs 491 /// autodetection using the method specified in the detectFlags. 492 /// </para> 493 /// </devdoc> 494 [SuppressMessage("Microsoft.Reliability","CA2001:AvoidCallingProblematicMethods", MessageId="System.Runtime.InteropServices.SafeHandle.DangerousGetHandle", Justification="Implementation requires DangerousGetHandle")] SafeDetectAutoProxyUrl( UnsafeNclNativeMethods.WinHttp.AutoDetectType discoveryMethod)495 private static unsafe Uri SafeDetectAutoProxyUrl( 496 UnsafeNclNativeMethods.WinHttp.AutoDetectType discoveryMethod) 497 { 498 Uri autoProxy = null; 499 500 #if !FEATURE_PAL 501 string url = null; 502 503 GlobalLog.Print("NetWebProxyFinder::SafeDetectAutoProxyUrl() Using WinHttp."); 504 SafeGlobalFree autoProxyUrl; 505 bool success = UnsafeNclNativeMethods.WinHttp.WinHttpDetectAutoProxyConfigUrl(discoveryMethod, out autoProxyUrl); 506 if (!success) 507 { 508 if (autoProxyUrl != null) 509 { 510 autoProxyUrl.SetHandleAsInvalid(); 511 } 512 } 513 else 514 { 515 url = new string((char*)autoProxyUrl.DangerousGetHandle()); 516 autoProxyUrl.Close(); 517 } 518 519 if (url != null) 520 { 521 bool parsed = Uri.TryCreate(url, UriKind.Absolute, out autoProxy); 522 if (!parsed) 523 { 524 if (Logging.On) Logging.PrintWarning(Logging.Web, SR.GetString(SR.net_log_proxy_autodetect_script_location_parse_error, ValidationHelper.ToString(url))); 525 GlobalLog.Print("NetWebProxyFinder::SafeDetectAutoProxyUrl() Uri.TryParse() failed url:" + ValidationHelper.ToString(url)); 526 } 527 } 528 else 529 { 530 if (Logging.On) Logging.PrintWarning(Logging.Web, SR.GetString(SR.net_log_proxy_autodetect_failed)); 531 GlobalLog.Print("NetWebProxyFinder::SafeDetectAutoProxyUrl() DetectAutoProxyUrl() returned false"); 532 } 533 #endif // !FEATURE_PAL 534 535 return autoProxy; 536 } 537 538 // RequestTimeoutCallback - Called by the TimerThread to abort a request. This just posts ThreadPool work item - Abort() does too 539 // much to be done on the timer thread (timer thread should never block or call user code). RequestTimeoutCallback(TimerThread.Timer timer, int timeNoticed, object context)540 private static void RequestTimeoutCallback(TimerThread.Timer timer, int timeNoticed, object context) 541 { 542 ThreadPool.UnsafeQueueUserWorkItem(abortWrapper, context); 543 } 544 AbortWrapper(object context)545 private static void AbortWrapper(object context) 546 { 547 #if DEBUG 548 GlobalLog.SetThreadSource(ThreadKinds.Worker); 549 using (GlobalLog.SetThreadKind(ThreadKinds.System)) 550 { 551 #endif 552 if (context != null) 553 { 554 ((WebRequest)context).Abort(); 555 } 556 #if DEBUG 557 } 558 #endif 559 } 560 } 561 } 562