File: System\Net\WebProxy.cs
Web Access
Project: src\src\libraries\System.Net.WebProxy\src\System.Net.WebProxy.csproj (System.Net.WebProxy)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Buffers;
using System.Collections;
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Runtime.Serialization;
using System.Text.RegularExpressions;
using System.Threading;
 
namespace System.Net
{
    /// <summary>
    /// Contains HTTP proxy settings for the <see cref="T:System.Net.Http.HttpClient" /> class.
    /// </summary>
    /// <remarks>
    /// The <see cref="WebProxy"/> class contains the proxy settings that <see cref="T:System.Net.Http.HttpClient"/> instances use to determine whether a Web proxy is used to send requests.
    /// The <see cref="WebProxy"/> class is the base implementation of the <see cref="IWebProxy"/> interface.
    /// </remarks>
    public partial class WebProxy : IWebProxy, ISerializable
    {
        private ChangeTrackingArrayList? _bypassList;
        private Regex[]? _regexBypassList;
 
        /// <summary>
        /// Initializes an empty instance of the <see cref="WebProxy" /> class.
        /// </summary>
        /// <remarks>
        /// <para>
        /// The parameterless constructor initializes an empty instance of the <see cref="WebProxy"/> class with the <see cref="Address"/> property set to <see langword="null"/>.
        /// </para>
        /// <para>
        /// When the <see cref="Address"/> property is <see langword="null"/>, the <see cref="IsBypassed"/> method returns <see langword="true"/> and the <see cref="GetProxy"/> method returns the destination address.
        /// </para>
        /// </remarks>
        public WebProxy() : this((Uri?)null, false, null, null) { }
 
        /// <summary>
        /// Initializes a new instance of the <see cref="WebProxy" /> class from the specified <see cref="Uri" /> instance.
        /// </summary>
        /// <param name="Address">The address of the proxy server.</param>
        public WebProxy(Uri? Address) : this(Address, false, null, null) { }
 
        /// <summary>
        /// Initializes a new instance of the <see cref="WebProxy" /> class with the <see cref="Uri" /> instance and bypass setting.
        /// </summary>
        /// <param name="Address">A <see cref="Uri" /> instance that contains the address of the proxy server.</param>
        /// <param name="BypassOnLocal"><see langword="true" /> to bypass the proxy for local addresses; otherwise, <see langword="false" />.</param>
        public WebProxy(Uri? Address, bool BypassOnLocal) : this(Address, BypassOnLocal, null, null) { }
 
        /// <summary>
        /// Initializes a new instance of the <see cref="WebProxy" /> class with the specified <see cref="Uri" /> instance, bypass setting, and list of URIs to bypass.
        /// </summary>
        /// <param name="Address">A <see cref="Uri" /> instance that contains the address of the proxy server.</param>
        /// <param name="BypassOnLocal"><see langword="true" /> to bypass the proxy for local addresses; otherwise, <see langword="false" />.</param>
        /// <param name="BypassList">An array of regular expression strings that contains the URIs of the servers to bypass.</param>
        public WebProxy(Uri? Address, bool BypassOnLocal, [StringSyntax(StringSyntaxAttribute.Regex, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)] string[]? BypassList) : this(Address, BypassOnLocal, BypassList, null) { }
 
        /// <summary>
        /// Initializes a new instance of the <see cref="WebProxy" /> class with the specified <see cref="Uri" /> instance, bypass setting, list of URIs to bypass, and credentials.
        /// </summary>
        /// <param name="Address">A <see cref="Uri" /> instance that contains the address of the proxy server.</param>
        /// <param name="BypassOnLocal"><see langword="true" /> to bypass the proxy for local addresses; otherwise, <see langword="false" />.</param>
        /// <param name="BypassList">An array of regular expression strings that contains the URIs of the servers to bypass.</param>
        /// <param name="Credentials">An <see cref="ICredentials" /> instance to submit to the proxy server for authentication.</param>
        public WebProxy(Uri? Address, bool BypassOnLocal, [StringSyntax(StringSyntaxAttribute.Regex, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)] string[]? BypassList, ICredentials? Credentials)
        {
            this.Address = Address;
            this.Credentials = Credentials;
            this.BypassProxyOnLocal = BypassOnLocal;
            if (BypassList != null)
            {
                _bypassList = new ChangeTrackingArrayList(BypassList);
                UpdateRegexList(); // prompt creation of the Regex instances so that any exceptions are propagated
            }
        }
 
        /// <summary>
        /// Initializes a new instance of the <see cref="WebProxy" /> class with the specified host and port number.
        /// </summary>
        /// <param name="Host">The name of the proxy host.</param>
        /// <param name="Port">The port number on <paramref name="Host" /> to use.</param>
        /// <exception cref="UriFormatException">The URI formed by combining <paramref name="Host" /> and <paramref name="Port" /> is not a valid URI.</exception>
        /// <remarks>
        /// The <see cref="WebProxy"/> instance is initialized with the <see cref="Address"/> property set to a <see cref="Uri"/> instance of the form <c>http://</c><paramref name="Host"/><c>:</c><paramref name="Port"/>.
        /// </remarks>
        public WebProxy(string Host, int Port)
            : this(CreateProxyUri(Host, Port), false, null, null)
        {
        }
 
        /// <summary>
        /// Initializes a new instance of the <see cref="WebProxy" /> class with the specified URI.
        /// </summary>
        /// <param name="Address">The URI of the proxy server.</param>
        /// <exception cref="UriFormatException"><paramref name="Address" /> is an invalid URI.</exception>
        public WebProxy(string? Address)
            : this(CreateProxyUri(Address), false, null, null)
        {
        }
 
        /// <summary>
        /// Initializes a new instance of the <see cref="WebProxy" /> class with the specified URI and bypass setting.
        /// </summary>
        /// <param name="Address">The URI of the proxy server.</param>
        /// <param name="BypassOnLocal"><see langword="true" /> to bypass the proxy for local addresses; otherwise, <see langword="false" />.</param>
        /// <exception cref="UriFormatException"><paramref name="Address" /> is an invalid URI.</exception>
        public WebProxy(string? Address, bool BypassOnLocal)
            : this(CreateProxyUri(Address), BypassOnLocal, null, null)
        {
        }
 
        /// <summary>
        /// Initializes a new instance of the <see cref="WebProxy" /> class with the specified URI, bypass setting, and list of URIs to bypass.
        /// </summary>
        /// <param name="Address">The URI of the proxy server.</param>
        /// <param name="BypassOnLocal"><see langword="true" /> to bypass the proxy for local addresses; otherwise, <see langword="false" />.</param>
        /// <param name="BypassList">An array of regular expression strings that contain the URIs of the servers to bypass.</param>
        /// <exception cref="UriFormatException"><paramref name="Address" /> is an invalid URI.</exception>
        public WebProxy(string? Address, bool BypassOnLocal, [StringSyntax(StringSyntaxAttribute.Regex, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)] string[]? BypassList)
            : this(CreateProxyUri(Address), BypassOnLocal, BypassList, null)
        {
        }
 
        /// <summary>
        /// Initializes a new instance of the <see cref="WebProxy" /> class with the specified URI, bypass setting, list of URIs to bypass, and credentials.
        /// </summary>
        /// <param name="Address">The URI of the proxy server.</param>
        /// <param name="BypassOnLocal"><see langword="true" /> to bypass the proxy for local addresses; otherwise, <see langword="false" />.</param>
        /// <param name="BypassList">An array of regular expression strings that contains the URIs of the servers to bypass.</param>
        /// <param name="Credentials">An <see cref="ICredentials" /> instance to submit to the proxy server for authentication.</param>
        /// <exception cref="UriFormatException"><paramref name="Address" /> is an invalid URI.</exception>
        public WebProxy(string? Address, bool BypassOnLocal, [StringSyntax(StringSyntaxAttribute.Regex, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)] string[]? BypassList, ICredentials? Credentials)
            : this(CreateProxyUri(Address), BypassOnLocal, BypassList, Credentials)
        {
        }
 
        /// <summary>
        /// Gets or sets the address of the proxy server.
        /// </summary>
        /// <value>
        /// A <see cref="Uri"/> instance that contains the address of the proxy server.
        /// </value>
        public Uri? Address { get; set; }
 
        /// <summary>
        /// Gets or sets a value that indicates whether to bypass the proxy server for local addresses.
        /// </summary>
        /// <value>
        /// <see langword="true"/> to bypass the proxy server for local addresses; otherwise, <see langword="false"/>. The default value is <see langword="false"/>.
        /// </value>
        public bool BypassProxyOnLocal { get; set; }
 
        /// <summary>
        /// Gets or sets an array of addresses that do not use the proxy server.
        /// </summary>
        /// <value>
        /// An array of regular expression strings that contains the URIs of servers that should not use the proxy server when accessed.
        /// </value>
        [AllowNull]
        public string[] BypassList
        {
            get
            {
                if (_bypassList == null)
                {
                    return Array.Empty<string>();
                }
 
                var bypassList = new string[_bypassList.Count];
                _bypassList.CopyTo(bypassList);
                return bypassList;
            }
            set
            {
                _bypassList = value != null ? new ChangeTrackingArrayList(value) : null;
                UpdateRegexList(); // prompt creation of the Regex instances so that any exceptions are propagated
            }
        }
 
        /// <summary>
        /// Gets or sets an <see cref="ArrayList"/> of addresses that do not use the proxy server.
        /// </summary>
        /// <value>
        /// An <see cref="ArrayList"/> that contains a list of regular expressions that represents URIs that do not use the proxy server when accessed.
        /// </value>
        public ArrayList BypassArrayList => _bypassList ??= new ChangeTrackingArrayList();
 
        /// <summary>
        /// Gets or sets the credentials to submit to the proxy server for authentication.
        /// </summary>
        /// <value>
        /// An <see cref="ICredentials"/> instance that contains the credentials to submit to the proxy server for authentication.
        /// </value>
        public ICredentials? Credentials { get; set; }
 
        /// <summary>
        /// Gets or sets a value that controls whether the <see cref="CredentialCache.DefaultCredentials"/> are sent with requests.
        /// </summary>
        /// <value>
        /// <see langword="true"/> if the default credentials are used; otherwise, <see langword="false"/>. The default value is <see langword="false"/>.
        /// </value>
        public bool UseDefaultCredentials
        {
            get => Credentials == CredentialCache.DefaultCredentials;
            set => Credentials = value ? CredentialCache.DefaultCredentials : null;
        }
 
        /// <summary>
        /// Returns the URI of a proxy.
        /// </summary>
        /// <param name="destination">The <see cref="Uri"/> instance of the requested Internet resource.</param>
        /// <returns>
        /// The <see cref="Uri"/> instance of the Internet resource, if the resource is on the bypass list; otherwise, the <see cref="Uri"/> instance of the proxy.
        /// </returns>
        /// <exception cref="ArgumentNullException">The <paramref name="destination"/> parameter is <see langword="null"/>.</exception>
        public Uri? GetProxy(Uri destination)
        {
            ArgumentNullException.ThrowIfNull(destination);
 
            return IsBypassed(destination) ? destination : Address;
        }
 
        private static Uri? CreateProxyUri(string? address, int? port = null)
        {
            if (address is null)
            {
                return null;
            }
 
            if (!address.Contains("://", StringComparison.Ordinal))
            {
                address = "http://" + address;
            }
 
            var proxyUri = new Uri(address);
 
            if (port.HasValue && proxyUri.IsAbsoluteUri)
            {
                proxyUri = new UriBuilder(proxyUri) { Port = port.Value }.Uri;
            }
 
            return proxyUri;
        }
 
        private void UpdateRegexList()
        {
            if (_bypassList is ChangeTrackingArrayList bypassList)
            {
                Regex[]? regexBypassList = null;
                if (bypassList.Count > 0)
                {
                    regexBypassList = new Regex[bypassList.Count];
                    for (int i = 0; i < regexBypassList.Length; i++)
                    {
                        regexBypassList[i] = new Regex((string)bypassList[i]!, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
                    }
                }
 
                _regexBypassList = regexBypassList;
                bypassList.IsChanged = false;
            }
            else
            {
                _regexBypassList = null;
            }
        }
 
        private bool IsMatchInBypassList(Uri input)
        {
            // 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[] regexBypassList)
            {
                bool isDefaultPort = input.IsDefaultPort;
                int lengthRequired = checked(input.Scheme.Length + 3 + input.Host.Length +
                    // 1 for ':' and 5 for max formatted length of a port (16 bit value)
                    (isDefaultPort ? 0 : 1 + 5));
 
                int charsWritten;
                Span<char> url = (uint)lengthRequired <= 256 ? stackalloc char[256] : new char[lengthRequired];
                bool formatted = isDefaultPort ?
                    url.TryWrite($"{input.Scheme}://{input.Host}", out charsWritten) :
                    url.TryWrite($"{input.Scheme}://{input.Host}:{(uint)input.Port}", out charsWritten);
                Debug.Assert(formatted);
                url = url.Slice(0, charsWritten);
 
                foreach (Regex r in regexBypassList)
                {
                    if (r.IsMatch(url))
                    {
                        return true;
                    }
                }
            }
 
            return false;
        }
 
        /// <summary>
        /// Indicates whether to use the proxy server for the specified host.
        /// </summary>
        /// <param name="host">The <see cref="Uri"/> instance of the host to check for proxy use.</param>
        /// <returns>
        /// <see langword="true"/> if the proxy server should not be used for <paramref name="host"/>; otherwise, <see langword="false"/>.
        /// </returns>
        /// <exception cref="ArgumentNullException">The <paramref name="host"/> parameter is <see langword="null"/>.</exception>
        public bool IsBypassed(Uri host)
        {
            ArgumentNullException.ThrowIfNull(host);
 
            return
                Address == null ||
                (BypassProxyOnLocal && IsLocal(host)) ||
                IsMatchInBypassList(host);
        }
 
        /// <summary>
        /// Initializes an instance of the <see cref="WebProxy" /> class using previously serialized content.
        /// </summary>
        /// <param name="serializationInfo">The serialization data.</param>
        /// <param name="streamingContext">The context for the serialized data.</param>
        /// <exception cref="PlatformNotSupportedException">This method is not supported and will always throw <see cref="PlatformNotSupportedException"/>.</exception>
        /// <remarks>
        /// This method is called by the system to deserialize a <see cref="WebProxy"/> instance; applications do not call it.
        /// </remarks>
        [Obsolete(Obsoletions.LegacyFormatterImplMessage, DiagnosticId = Obsoletions.LegacyFormatterImplDiagId, UrlFormat = Obsoletions.SharedUrlFormat)]
        [EditorBrowsable(EditorBrowsableState.Never)]
        protected WebProxy(SerializationInfo serializationInfo, StreamingContext streamingContext) =>
            throw new PlatformNotSupportedException();
 
        void ISerializable.GetObjectData(SerializationInfo serializationInfo, StreamingContext streamingContext) =>
            throw new PlatformNotSupportedException();
 
        /// <summary>
        /// Populates a <see cref="SerializationInfo"/> with the data that is needed to serialize the target object.
        /// </summary>
        /// <param name="serializationInfo">The <see cref="SerializationInfo"/> to populate with data.</param>
        /// <param name="streamingContext">A <see cref="StreamingContext"/> that specifies the destination for this serialization.</param>
        /// <exception cref="PlatformNotSupportedException">This method is not supported and will always throw <see cref="PlatformNotSupportedException"/>.</exception>
        protected virtual void GetObjectData(SerializationInfo serializationInfo, StreamingContext streamingContext) =>
            throw new PlatformNotSupportedException();
 
        /// <summary>
        /// Returns the proxy information configured by the system.
        /// </summary>
        /// <returns>
        /// A <see cref="WebProxy"/> instance that contains the nondynamic proxy settings from Internet options.
        /// </returns>
        /// <exception cref="PlatformNotSupportedException">This method is not supported on .NET Core and will always throw <see cref="PlatformNotSupportedException"/>.</exception>
        [Obsolete("WebProxy.GetDefaultProxy has been deprecated. Use the proxy selected for you by default.")]
        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) { }
 
            // While this type isn't intended to be mutated concurrently with reads, non-concurrent updates
            // to the list might result in lazy initialization, and it's possible concurrent HTTP requests could race
            // to trigger that initialization.
            public volatile bool IsChanged;
 
            // 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();
            }
        }
    }
}