| // <copyright file="Proxy.cs" company="Selenium Committers"> |
| // Licensed to the Software Freedom Conservancy (SFC) under one |
| // or more contributor license agreements. See the NOTICE file |
| // distributed with this work for additional information |
| // regarding copyright ownership. The SFC licenses this file |
| // to you under the Apache License, Version 2.0 (the |
| // "License"); you may not use this file except in compliance |
| // with the License. You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, |
| // software distributed under the License is distributed on an |
| // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| // KIND, either express or implied. See the License for the |
| // specific language governing permissions and limitations |
| // under the License. |
| // </copyright> |
| |
| using System; |
| using System.Collections.Generic; |
| using System.Collections.ObjectModel; |
| using System.Globalization; |
| using System.Text.Json.Serialization; |
| |
| namespace OpenQA.Selenium; |
| |
| /// <summary> |
| /// Describes the kind of proxy. |
| /// </summary> |
| /// <remarks> |
| /// Keep these in sync with the Firefox preferences numbers: |
| /// http://kb.mozillazine.org/Network.proxy.type |
| /// </remarks> |
| public enum ProxyKind |
| { |
| /// <summary> |
| /// Direct connection, no proxy (default on Windows). |
| /// </summary> |
| Direct = 0, |
| |
| /// <summary> |
| /// Manual proxy settings (e.g., for httpProxy). |
| /// </summary> |
| Manual, |
| |
| /// <summary> |
| /// Proxy automatic configuration from URL. |
| /// </summary> |
| ProxyAutoConfigure, |
| |
| /// <summary> |
| /// Use proxy automatic detection. |
| /// </summary> |
| AutoDetect = 4, |
| |
| /// <summary> |
| /// Use the system values for proxy settings (default on Linux). |
| /// </summary> |
| System, |
| |
| /// <summary> |
| /// No proxy type is specified. |
| /// </summary> |
| Unspecified |
| } |
| |
| /// <summary> |
| /// Describes proxy settings to be used with a driver instance. |
| /// </summary> |
| public class Proxy |
| { |
| private ProxyKind proxyKind = ProxyKind.Unspecified; |
| private bool isAutoDetect; |
| private string? httpProxyLocation; |
| private string? proxyAutoConfigUrl; |
| private string? sslProxyLocation; |
| private string? socksProxyLocation; |
| private string? socksUserName; |
| private string? socksPassword; |
| private int? socksVersion; |
| private List<string> noProxyAddresses = new List<string>(); |
| |
| /// <summary> |
| /// Initializes a new instance of the <see cref="Proxy"/> class. |
| /// </summary> |
| public Proxy() |
| { |
| } |
| |
| /// <summary> |
| /// Initializes a new instance of the <see cref="Proxy"/> class with the given proxy settings. |
| /// </summary> |
| /// <param name="settings">A dictionary of settings to use with the proxy.</param> |
| /// <exception cref="ArgumentNullException">If <paramref name="settings"/> is <see langword="null"/>.</exception> |
| /// <exception cref="ArgumentException">If The "noProxy" value is a list with a <see langword="null"/> element.</exception> |
| public Proxy(Dictionary<string, object> settings) |
| { |
| if (settings == null) |
| { |
| throw new ArgumentNullException(nameof(settings), "settings dictionary cannot be null"); |
| } |
| |
| if (settings.TryGetValue("proxyType", out object? proxyTypeObj) && proxyTypeObj?.ToString() is string proxyType) |
| { |
| // Special-case "PAC" since that is the correct serialization. |
| if (proxyType.Equals("pac", StringComparison.InvariantCultureIgnoreCase)) |
| { |
| this.Kind = ProxyKind.ProxyAutoConfigure; |
| } |
| else |
| { |
| ProxyKind rawType = (ProxyKind)Enum.Parse(typeof(ProxyKind), proxyType, ignoreCase: true); |
| this.Kind = rawType; |
| } |
| } |
| |
| if (settings.TryGetValue("httpProxy", out object? httpProxyObj) && httpProxyObj?.ToString() is string httpProxy) |
| { |
| this.HttpProxy = httpProxy; |
| } |
| |
| if (settings.TryGetValue("noProxy", out object? noProxy) && noProxy != null) |
| { |
| List<string> bypassAddresses = new List<string>(); |
| if (noProxy is string addressesAsString) |
| { |
| bypassAddresses.AddRange(addressesAsString.Split(';')); |
| } |
| else |
| { |
| if (noProxy is object?[] addressesAsArray) |
| { |
| foreach (object? address in addressesAsArray) |
| { |
| bypassAddresses.Add(address?.ToString() ?? throw new ArgumentException("Proxy bypass address list \"noProxy\" contained a null element", nameof(settings))); |
| } |
| } |
| } |
| |
| this.AddBypassAddresses(bypassAddresses); |
| } |
| |
| if (settings.TryGetValue("proxyAutoconfigUrl", out object? proxyAutoconfigUrlObj) && proxyAutoconfigUrlObj?.ToString() is string proxyAutoconfigUrl) |
| { |
| this.ProxyAutoConfigUrl = proxyAutoconfigUrl; |
| } |
| |
| if (settings.TryGetValue("sslProxy", out object? sslProxyObj) && sslProxyObj?.ToString() is string sslProxy) |
| { |
| this.SslProxy = sslProxy; |
| } |
| |
| if (settings.TryGetValue("socksProxy", out object? socksProxyObj) && socksProxyObj?.ToString() is string socksProxy) |
| { |
| this.SocksProxy = socksProxy; |
| } |
| |
| if (settings.TryGetValue("socksUsername", out object? socksUsernameObj) && socksUsernameObj?.ToString() is string socksUsername) |
| { |
| this.SocksUserName = socksUsername; |
| } |
| |
| if (settings.TryGetValue("socksPassword", out object? socksPasswordObj) && socksPasswordObj?.ToString() is string socksPassword) |
| { |
| this.SocksPassword = socksPassword; |
| } |
| |
| if (settings.TryGetValue("socksVersion", out object? socksVersion) && socksVersion != null) |
| { |
| this.SocksVersion = Convert.ToInt32(socksVersion); |
| } |
| |
| if (settings.TryGetValue("autodetect", out object? autodetect) && autodetect != null) |
| { |
| this.IsAutoDetect = Convert.ToBoolean(autodetect); |
| } |
| } |
| |
| /// <summary> |
| /// Gets or sets the type of proxy. |
| /// </summary> |
| [JsonIgnore] |
| public ProxyKind Kind |
| { |
| get => this.proxyKind; |
| |
| set |
| { |
| this.VerifyProxyTypeCompatilibily(value); |
| this.proxyKind = value; |
| } |
| } |
| |
| /// <summary> |
| /// Gets the type of proxy as a string for JSON serialization. |
| /// </summary> |
| [JsonPropertyName("proxyType")] |
| public string SerializableProxyKind |
| { |
| get |
| { |
| if (this.proxyKind == ProxyKind.ProxyAutoConfigure) |
| { |
| return "pac"; |
| } |
| |
| return this.proxyKind.ToString().ToLowerInvariant(); |
| } |
| } |
| |
| /// <summary> |
| /// Gets or sets a value indicating whether the proxy uses automatic detection. |
| /// </summary> |
| [JsonIgnore] |
| public bool IsAutoDetect |
| { |
| get => this.isAutoDetect; |
| |
| set |
| { |
| if (this.isAutoDetect == value) |
| { |
| return; |
| } |
| |
| this.VerifyProxyTypeCompatilibily(ProxyKind.AutoDetect); |
| this.proxyKind = ProxyKind.AutoDetect; |
| this.isAutoDetect = value; |
| } |
| } |
| |
| /// <summary> |
| /// Gets or sets the value of the proxy for the HTTP protocol. |
| /// </summary> |
| [JsonPropertyName("httpProxy")] |
| [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] |
| public string? HttpProxy |
| { |
| get => this.httpProxyLocation; |
| |
| set |
| { |
| this.VerifyProxyTypeCompatilibily(ProxyKind.Manual); |
| this.proxyKind = ProxyKind.Manual; |
| this.httpProxyLocation = value; |
| } |
| } |
| |
| /// <summary> |
| /// Gets the list of address for which to bypass the proxy as an array. |
| /// </summary> |
| [JsonPropertyName("noProxy")] |
| [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] |
| public ReadOnlyCollection<string>? BypassProxyAddresses |
| { |
| get |
| { |
| if (this.noProxyAddresses.Count == 0) |
| { |
| return null; |
| } |
| |
| return this.noProxyAddresses.AsReadOnly(); |
| } |
| } |
| |
| /// <summary> |
| /// Gets or sets the URL used for proxy automatic configuration. |
| /// </summary> |
| [JsonPropertyName("proxyAutoconfigUrl")] |
| [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] |
| public string? ProxyAutoConfigUrl |
| { |
| get => this.proxyAutoConfigUrl; |
| |
| set |
| { |
| this.VerifyProxyTypeCompatilibily(ProxyKind.ProxyAutoConfigure); |
| this.proxyKind = ProxyKind.ProxyAutoConfigure; |
| this.proxyAutoConfigUrl = value; |
| } |
| } |
| |
| /// <summary> |
| /// Gets or sets the value of the proxy for the SSL protocol. |
| /// </summary> |
| [JsonPropertyName("sslProxy")] |
| [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] |
| public string? SslProxy |
| { |
| get => this.sslProxyLocation; |
| |
| set |
| { |
| this.VerifyProxyTypeCompatilibily(ProxyKind.Manual); |
| this.proxyKind = ProxyKind.Manual; |
| this.sslProxyLocation = value; |
| } |
| } |
| |
| /// <summary> |
| /// Gets or sets the value of the proxy for the SOCKS protocol. |
| /// </summary> |
| [JsonPropertyName("socksProxy")] |
| [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] |
| public string? SocksProxy |
| { |
| get => this.socksProxyLocation; |
| |
| set |
| { |
| this.VerifyProxyTypeCompatilibily(ProxyKind.Manual); |
| this.proxyKind = ProxyKind.Manual; |
| this.socksProxyLocation = value; |
| } |
| } |
| |
| /// <summary> |
| /// Gets or sets the value of username for the SOCKS proxy. |
| /// </summary> |
| [JsonPropertyName("socksUsername")] |
| [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] |
| public string? SocksUserName |
| { |
| get => this.socksUserName; |
| |
| set |
| { |
| this.VerifyProxyTypeCompatilibily(ProxyKind.Manual); |
| this.proxyKind = ProxyKind.Manual; |
| this.socksUserName = value; |
| } |
| } |
| |
| /// <summary> |
| /// Gets or sets the value of the protocol version for the SOCKS proxy. |
| /// Value can be <see langword="null"/> if not set. |
| /// </summary> |
| [JsonPropertyName("socksVersion")] |
| [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] |
| public int? SocksVersion |
| { |
| get => this.socksVersion; |
| |
| set |
| { |
| if (value == null) |
| { |
| this.socksVersion = value; |
| } |
| else |
| { |
| if (value.Value <= 0) |
| { |
| throw new ArgumentOutOfRangeException(nameof(value), "SocksVersion must be a positive integer"); |
| } |
| |
| this.VerifyProxyTypeCompatilibily(ProxyKind.Manual); |
| this.proxyKind = ProxyKind.Manual; |
| this.socksVersion = value; |
| } |
| } |
| } |
| |
| /// <summary> |
| /// Gets or sets the value of password for the SOCKS proxy. |
| /// </summary> |
| [JsonPropertyName("socksPassword")] |
| [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] |
| public string? SocksPassword |
| { |
| get => this.socksPassword; |
| |
| set |
| { |
| this.VerifyProxyTypeCompatilibily(ProxyKind.Manual); |
| this.proxyKind = ProxyKind.Manual; |
| this.socksPassword = value; |
| } |
| } |
| |
| /// <summary> |
| /// Adds a single address to the list of addresses against which the proxy will not be used. |
| /// </summary> |
| /// <param name="address">The address to add.</param> |
| public void AddBypassAddress(string address) |
| { |
| if (string.IsNullOrEmpty(address)) |
| { |
| throw new ArgumentException("address must not be null or empty", nameof(address)); |
| } |
| |
| this.AddBypassAddresses(address); |
| } |
| |
| /// <summary> |
| /// Adds addresses to the list of addresses against which the proxy will not be used. |
| /// </summary> |
| /// <param name="addressesToAdd">An array of addresses to add.</param> |
| public void AddBypassAddresses(params string[] addressesToAdd) |
| { |
| this.AddBypassAddresses((IEnumerable<string>)addressesToAdd); |
| } |
| |
| /// <summary> |
| /// Adds addresses to the list of addresses against which the proxy will not be used. |
| /// </summary> |
| /// <param name="addressesToAdd">An <see cref="IEnumerable{T}"/> object of arguments to add.</param> |
| public void AddBypassAddresses(IEnumerable<string> addressesToAdd) |
| { |
| if (addressesToAdd == null) |
| { |
| throw new ArgumentNullException(nameof(addressesToAdd), "addressesToAdd must not be null"); |
| } |
| |
| this.VerifyProxyTypeCompatilibily(ProxyKind.Manual); |
| this.proxyKind = ProxyKind.Manual; |
| this.noProxyAddresses.AddRange(addressesToAdd); |
| } |
| |
| /// <summary> |
| /// Returns a dictionary suitable for serializing to the W3C Specification |
| /// dialect of the wire protocol. |
| /// </summary> |
| /// <returns>A dictionary suitable for serializing to the W3C Specification |
| /// dialect of the wire protocol.</returns> |
| internal Dictionary<string, object?>? ToCapability() |
| { |
| return this.AsDictionary(true); |
| } |
| |
| /// <summary> |
| /// Returns a dictionary suitable for serializing to the OSS dialect of the |
| /// wire protocol. |
| /// </summary> |
| /// <returns>A dictionary suitable for serializing to the OSS dialect of the |
| /// wire protocol.</returns> |
| internal Dictionary<string, object?>? ToLegacyCapability() |
| { |
| return this.AsDictionary(false); |
| } |
| |
| private Dictionary<string, object?>? AsDictionary(bool isSpecCompliant) |
| { |
| Dictionary<string, object?>? serializedDictionary = null; |
| if (this.proxyKind != ProxyKind.Unspecified) |
| { |
| serializedDictionary = new Dictionary<string, object?>(); |
| if (this.proxyKind == ProxyKind.ProxyAutoConfigure) |
| { |
| serializedDictionary["proxyType"] = "pac"; |
| if (!string.IsNullOrEmpty(this.proxyAutoConfigUrl)) |
| { |
| serializedDictionary["proxyAutoconfigUrl"] = this.proxyAutoConfigUrl; |
| } |
| } |
| else |
| { |
| serializedDictionary["proxyType"] = this.proxyKind.ToString().ToLowerInvariant(); |
| } |
| |
| if (!string.IsNullOrEmpty(this.httpProxyLocation)) |
| { |
| serializedDictionary["httpProxy"] = this.httpProxyLocation; |
| } |
| |
| if (!string.IsNullOrEmpty(this.sslProxyLocation)) |
| { |
| serializedDictionary["sslProxy"] = this.sslProxyLocation; |
| } |
| |
| if (!string.IsNullOrEmpty(this.socksProxyLocation)) |
| { |
| if (!this.socksVersion.HasValue) |
| { |
| throw new InvalidOperationException("Must have a version value set (usually 4 or 5) when specifying a SOCKS proxy"); |
| } |
| |
| string socksAuth = string.Empty; |
| if (!string.IsNullOrEmpty(this.socksUserName) && !string.IsNullOrEmpty(this.socksPassword)) |
| { |
| // TODO: this is probably inaccurate as to how this is supposed |
| // to look. |
| socksAuth = this.socksUserName + ":" + this.socksPassword + "@"; |
| } |
| |
| serializedDictionary["socksProxy"] = socksAuth + this.socksProxyLocation; |
| serializedDictionary["socksVersion"] = this.socksVersion.Value; |
| } |
| |
| if (this.noProxyAddresses.Count > 0) |
| { |
| serializedDictionary["noProxy"] = this.GetNoProxyAddressList(isSpecCompliant); |
| } |
| } |
| |
| return serializedDictionary; |
| } |
| |
| private object? GetNoProxyAddressList(bool isSpecCompliant) |
| { |
| object? addresses = null; |
| if (isSpecCompliant) |
| { |
| List<object> addressList = [.. this.noProxyAddresses]; |
| addresses = addressList; |
| } |
| else |
| { |
| addresses = this.BypassProxyAddresses; |
| } |
| |
| return addresses; |
| } |
| |
| private void VerifyProxyTypeCompatilibily(ProxyKind compatibleProxy) |
| { |
| if (this.proxyKind != ProxyKind.Unspecified && this.proxyKind != compatibleProxy) |
| { |
| string errorMessage = string.Format( |
| CultureInfo.InvariantCulture, |
| "Specified proxy type {0} is not compatible with current setting {1}", |
| compatibleProxy.ToString().ToUpperInvariant(), |
| this.proxyKind.ToString().ToUpperInvariant()); |
| |
| throw new InvalidOperationException(errorMessage); |
| } |
| } |
| } |