diff --git a/src/libraries/System.Net.WebProxy/src/System/Net/WebProxy.cs b/src/libraries/System.Net.WebProxy/src/System/Net/WebProxy.cs index 9460000a28e752..e76ae87717ad42 100644 --- a/src/libraries/System.Net.WebProxy/src/System/Net/WebProxy.cs +++ b/src/libraries/System.Net.WebProxy/src/System/Net/WebProxy.cs @@ -13,7 +13,7 @@ namespace System.Net { public partial class WebProxy : IWebProxy, ISerializable { - private ArrayList? _bypassList; + private ChangeTrackingArrayList? _bypassList; private Regex[]? _regexBypassList; public WebProxy() : this((Uri?)null, false, null, null) { } @@ -31,8 +31,8 @@ public WebProxy(Uri? Address, bool BypassOnLocal, [StringSyntax(StringSyntaxAttr this.BypassProxyOnLocal = BypassOnLocal; if (BypassList != null) { - _bypassList = new ArrayList(BypassList); - UpdateRegexList(true); + _bypassList = new ChangeTrackingArrayList(BypassList); + UpdateRegexList(); // prompt creation of the Regex instances so that any exceptions are propagated } } @@ -71,7 +71,9 @@ public string[] BypassList get { if (_bypassList == null) + { return Array.Empty(); + } var bypassList = new string[_bypassList.Count]; _bypassList.CopyTo(bypassList); @@ -79,12 +81,12 @@ public string[] BypassList } set { - _bypassList = value != null ? new ArrayList(value) : null; - UpdateRegexList(true); + _bypassList = value != null ? new ChangeTrackingArrayList(value) : null; + UpdateRegexList(); // prompt creation of the Regex instances so that any exceptions are propagated } } - public ArrayList BypassArrayList => _bypassList ??= new ArrayList(); + public ArrayList BypassArrayList => _bypassList ??= new ChangeTrackingArrayList(); public ICredentials? Credentials { get; set; } @@ -123,40 +125,41 @@ public bool UseDefaultCredentials return proxyUri; } - private void UpdateRegexList(bool canThrow) + private void UpdateRegexList() { Regex[]? regexBypassList = null; - ArrayList? bypassList = _bypassList; - try + if (_bypassList is ChangeTrackingArrayList bypassList) { - if (bypassList != null && bypassList.Count > 0) + bypassList.IsChanged = false; + if (bypassList.Count > 0) { regexBypassList = new Regex[bypassList.Count]; - for (int i = 0; i < bypassList.Count; i++) + for (int i = 0; i < regexBypassList.Length; i++) { regexBypassList[i] = new Regex((string)bypassList[i]!, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); } } } - catch - { - if (!canThrow) - { - _regexBypassList = null; - return; - } - throw; - } - // Update field here, as it could throw earlier in the loop _regexBypassList = regexBypassList; } private bool IsMatchInBypassList(Uri input) { - UpdateRegexList(canThrow: false); + // Update our list of Regex instances if the ArrayList has changed. + if (_bypassList is ChangeTrackingArrayList bypassList && bypassList.IsChanged) + { + try + { + UpdateRegexList(); + } + catch + { + _regexBypassList = null; + } + } - if (_regexBypassList is Regex[] bypassList) + if (_regexBypassList is Regex[] regexBypassList) { bool isDefaultPort = input.IsDefaultPort; int lengthRequired = input.Scheme.Length + 3 + input.Host.Length; @@ -173,7 +176,7 @@ private bool IsMatchInBypassList(Uri input) Debug.Assert(formatted); url = url.Slice(0, charsWritten); - foreach (Regex r in bypassList) + foreach (Regex r in regexBypassList) { if (r.IsMatch(url)) { @@ -209,5 +212,83 @@ public static WebProxy GetDefaultProxy() => // The .NET Framework here returns a proxy that fetches IE settings and // executes JavaScript to determine the correct proxy. throw new PlatformNotSupportedException(); + + private sealed class ChangeTrackingArrayList : ArrayList + { + public ChangeTrackingArrayList() { } + + public ChangeTrackingArrayList(ICollection c) : base(c) { } + + public bool IsChanged { get; set; } + + // Override the methods that can add, remove, or change the regexes in the bypass list. + // Methods that only read (like CopyTo, BinarySearch, etc.) and methods that reorder + // the collection but that don't change the overall list of regexes (e.g. Sort) do not + // need to be overridden. + + public override object? this[int index] + { + get => base[index]; + set + { + IsChanged = true; + base[index] = value; + } + } + + public override int Add(object? value) + { + IsChanged = true; + return base.Add(value); + } + + public override void AddRange(ICollection c) + { + IsChanged = true; + base.AddRange(c); + } + + public override void Insert(int index, object? value) + { + IsChanged = true; + base.Insert(index, value); + } + + public override void InsertRange(int index, ICollection c) + { + IsChanged = true; + base.InsertRange(index, c); + } + + public override void SetRange(int index, ICollection c) + { + IsChanged = true; + base.SetRange(index, c); + } + + public override void Remove(object? obj) + { + IsChanged = true; + base.Remove(obj); + } + + public override void RemoveAt(int index) + { + IsChanged = true; + base.RemoveAt(index); + } + + public override void RemoveRange(int index, int count) + { + IsChanged = true; + base.RemoveRange(index, count); + } + + public override void Clear() + { + IsChanged = true; + base.Clear(); + } + } } } diff --git a/src/libraries/System.Net.WebProxy/tests/WebProxyTest.cs b/src/libraries/System.Net.WebProxy/tests/WebProxyTest.cs index cfb2cd10e947a0..917a67b70c9f6e 100644 --- a/src/libraries/System.Net.WebProxy/tests/WebProxyTest.cs +++ b/src/libraries/System.Net.WebProxy/tests/WebProxyTest.cs @@ -132,6 +132,47 @@ public static void WebProxy_InvalidBypassUrl_AddedDirectlyToList_SilentlyEaten() p.IsBypassed(new Uri("http://microsoft.com")); // exception should be silently eaten } + [Fact] + public static void WebProxy_BypassUrl_BypassArrayListChangedDirectly_IsBypassedAsExpected() + { + var p = new WebProxy("http://microsoft.com", BypassOnLocal: false); + Assert.False(p.IsBypassed(new Uri("http://bing.com"))); + + p.BypassArrayList.Add("bing"); + Assert.True(p.IsBypassed(new Uri("http://bing.com"))); + + p.BypassArrayList.Remove("bing"); + Assert.False(p.IsBypassed(new Uri("http://bing.com"))); + + p.BypassArrayList.AddRange(new[] { "dot.net" }); + Assert.True(p.IsBypassed(new Uri("http://dot.net"))); + + p.BypassArrayList.InsertRange(0, new[] { "bing" }); + Assert.True(p.IsBypassed(new Uri("http://bing.com"))); + + p.BypassArrayList.SetRange(0, new[] { "example", "microsoft" }); + Assert.True(p.IsBypassed(new Uri("http://example.com"))); + Assert.True(p.IsBypassed(new Uri("http://microsoft.com"))); + Assert.False(p.IsBypassed(new Uri("http://bing.com"))); + Assert.False(p.IsBypassed(new Uri("http://dot.net"))); + + p.BypassArrayList.Clear(); + Assert.False(p.IsBypassed(new Uri("http://example.com"))); + Assert.False(p.IsBypassed(new Uri("http://microsoft.com"))); + + p.BypassArrayList.Insert(0, "bing"); + p.BypassArrayList.Insert(1, "example"); + Assert.True(p.IsBypassed(new Uri("http://bing.com"))); + Assert.True(p.IsBypassed(new Uri("http://example.com"))); + + p.BypassArrayList.RemoveAt(0); + Assert.False(p.IsBypassed(new Uri("http://bing.com"))); + Assert.True(p.IsBypassed(new Uri("http://example.com"))); + + p.BypassArrayList.RemoveRange(0, 1); + Assert.False(p.IsBypassed(new Uri("http://example.com"))); + } + [Fact] public static void WebProxy_BypassList_DoesntContainUrl_NotBypassed() {