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