| // <copyright file="NetworkManager.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 OpenQA.Selenium.DevTools; |
| using System; |
| using System.Collections.Generic; |
| using System.Diagnostics.CodeAnalysis; |
| using System.Threading.Tasks; |
| |
| namespace OpenQA.Selenium; |
| |
| /// <summary> |
| /// Provides methods for monitoring, intercepting, and modifying network requests and responses. |
| /// </summary> |
| public class NetworkManager : INetwork |
| { |
| private readonly Lazy<DevToolsSession> session; |
| private readonly List<NetworkRequestHandler> requestHandlers = new List<NetworkRequestHandler>(); |
| private readonly List<NetworkResponseHandler> responseHandlers = new List<NetworkResponseHandler>(); |
| private readonly List<NetworkAuthenticationHandler> authenticationHandlers = new List<NetworkAuthenticationHandler>(); |
| |
| /// <summary> |
| /// Initializes a new instance of the <see cref="NetworkManager"/> class. |
| /// </summary> |
| /// <param name="driver">The <see cref="IWebDriver"/> instance on which the network should be monitored.</param> |
| [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Warnings are added to StartMonitoring and StopMonitoring")] |
| [UnconditionalSuppressMessage("Trimming", "IL3050", Justification = "Warnings are added to StartMonitoring and StopMonitoring")] |
| public NetworkManager(IWebDriver driver) |
| { |
| // Use of Lazy<T> means this exception won't be thrown until the user first |
| // attempts to access the DevTools session, probably on the first call to |
| // StartMonitoring(). |
| this.session = new Lazy<DevToolsSession>(() => |
| { |
| if (driver is not IDevTools devToolsDriver) |
| { |
| throw new WebDriverException("Driver must implement IDevTools to use these features"); |
| } |
| |
| return devToolsDriver.GetDevToolsSession(); |
| }); |
| } |
| |
| /// <summary> |
| /// Occurs when a browser sends a network request. |
| /// </summary> |
| public event EventHandler<NetworkRequestSentEventArgs>? NetworkRequestSent; |
| |
| /// <summary> |
| /// Occurs when a browser receives a network response. |
| /// </summary> |
| public event EventHandler<NetworkResponseReceivedEventArgs>? NetworkResponseReceived; |
| |
| /// <summary> |
| /// Asynchronously starts monitoring for network traffic. |
| /// </summary> |
| /// <returns>A task that represents the asynchronous operation.</returns> |
| [RequiresUnreferencedCode("NetworkManager is currently implemented with CDP. When it is implemented with BiDi, AOT will be supported")] |
| [RequiresDynamicCode("NetworkManager is currently implemented with CDP. When it is implemented with BiDi, AOT will be supported.")] |
| public async Task StartMonitoring() |
| { |
| this.session.Value.Domains.Network.RequestPaused += OnRequestPaused; |
| this.session.Value.Domains.Network.AuthRequired += OnAuthRequired; |
| this.session.Value.Domains.Network.ResponsePaused += OnResponsePaused; |
| await this.session.Value.Domains.Network.EnableFetchForAllPatterns().ConfigureAwait(false); |
| await this.session.Value.Domains.Network.EnableNetwork().ConfigureAwait(false); |
| await this.session.Value.Domains.Network.DisableNetworkCaching().ConfigureAwait(false); |
| } |
| |
| /// <summary> |
| /// Asynchronously stops monitoring for network traffic. |
| /// </summary> |
| /// <returns>A task that represents the asynchronous operation.</returns> |
| [RequiresUnreferencedCode("Network monitoring is currently implemented with CDP. When it is implemented with BiDi, AOT will be supported")] |
| [RequiresDynamicCode("Network monitoring is currently implemented with CDP. When it is implemented with BiDi, AOT will be supported.")] |
| public async Task StopMonitoring() |
| { |
| this.session.Value.Domains.Network.ResponsePaused -= OnResponsePaused; |
| this.session.Value.Domains.Network.AuthRequired -= OnAuthRequired; |
| this.session.Value.Domains.Network.RequestPaused -= OnRequestPaused; |
| await this.session.Value.Domains.Network.EnableNetworkCaching().ConfigureAwait(false); |
| } |
| |
| /// <summary> |
| /// Adds a <see cref="NetworkRequestHandler"/> to examine incoming network requests, |
| /// and optionally modify the request or provide a response. |
| /// </summary> |
| /// <param name="handler">The <see cref="NetworkRequestHandler"/> to add.</param> |
| /// <exception cref="ArgumentNullException">If <paramref name="handler"/> is <see langword="null"/>.</exception> |
| public void AddRequestHandler(NetworkRequestHandler handler) |
| { |
| if (handler == null) |
| { |
| throw new ArgumentNullException(nameof(handler), "Request handler cannot be null"); |
| } |
| |
| if (handler.RequestMatcher == null) |
| { |
| throw new ArgumentException("Matcher for request cannot be null", nameof(handler)); |
| } |
| |
| if (handler.RequestTransformer == null && handler.ResponseSupplier == null) |
| { |
| throw new ArgumentException("Request transformer and response supplier cannot both be null", nameof(handler)); |
| } |
| |
| this.requestHandlers.Add(handler); |
| } |
| |
| /// <summary> |
| /// Clears all added <see cref="NetworkRequestHandler"/> instances. |
| /// </summary> |
| public void ClearRequestHandlers() |
| { |
| this.requestHandlers.Clear(); |
| } |
| |
| /// <summary> |
| /// Adds a <see cref="NetworkAuthenticationHandler"/> to supply authentication |
| /// credentials for network requests. |
| /// </summary> |
| /// <param name="handler">The <see cref="NetworkAuthenticationHandler"/> to add.</param> |
| public void AddAuthenticationHandler(NetworkAuthenticationHandler handler) |
| { |
| if (handler == null) |
| { |
| throw new ArgumentNullException(nameof(handler), "Authentication handler cannot be null"); |
| } |
| |
| if (handler.UriMatcher == null) |
| { |
| throw new ArgumentException("Matcher for delegate for URL cannot be null", nameof(handler)); |
| } |
| |
| if (handler.Credentials == null) |
| { |
| throw new ArgumentException("Credentials to use for authentication cannot be null", nameof(handler)); |
| } |
| |
| if (handler.Credentials is not PasswordCredentials) |
| { |
| throw new ArgumentException("Credentials must contain user name and password (PasswordCredentials)", nameof(handler)); |
| } |
| |
| this.authenticationHandlers.Add(handler); |
| } |
| |
| /// <summary> |
| /// Clears all added <see cref="NetworkAuthenticationHandler"/> instances. |
| /// </summary> |
| public void ClearAuthenticationHandlers() |
| { |
| this.authenticationHandlers.Clear(); |
| } |
| |
| /// <summary> |
| /// Adds a <see cref="NetworkResponseHandler"/> to examine received network responses, |
| /// and optionally modify the response. |
| /// </summary> |
| /// <param name="handler">The <see cref="NetworkResponseHandler"/> to add.</param> |
| public void AddResponseHandler(NetworkResponseHandler handler) |
| { |
| if (handler == null) |
| { |
| throw new ArgumentNullException(nameof(handler), "Request handler cannot be null"); |
| } |
| |
| if (handler.ResponseMatcher == null) |
| { |
| throw new ArgumentException("Matcher for response cannot be null", nameof(handler)); |
| } |
| |
| this.responseHandlers.Add(handler); |
| } |
| |
| /// <summary> |
| /// Clears all added <see cref="NetworkResponseHandler"/> instances. |
| /// </summary> |
| public void ClearResponseHandlers() |
| { |
| this.responseHandlers.Clear(); |
| } |
| |
| private async Task OnAuthRequired(object sender, AuthRequiredEventArgs e) |
| { |
| string requestId = e.RequestId; |
| Uri uri = new Uri(e.Uri); |
| bool successfullyAuthenticated = false; |
| foreach (var authenticationHandler in this.authenticationHandlers) |
| { |
| if (authenticationHandler.UriMatcher!.Invoke(uri)) |
| { |
| PasswordCredentials credentials = (PasswordCredentials)authenticationHandler.Credentials!; |
| await this.session.Value.Domains.Network.ContinueWithAuth(e.RequestId, credentials.UserName, credentials.Password).ConfigureAwait(false); |
| successfullyAuthenticated = true; |
| break; |
| } |
| } |
| |
| if (!successfullyAuthenticated) |
| { |
| await this.session.Value.Domains.Network.CancelAuth(e.RequestId).ConfigureAwait(false); |
| } |
| } |
| |
| private async Task OnRequestPaused(object sender, RequestPausedEventArgs e) |
| { |
| if (this.NetworkRequestSent != null) |
| { |
| this.NetworkRequestSent(this, new NetworkRequestSentEventArgs(e.RequestData)); |
| } |
| |
| foreach (var handler in this.requestHandlers) |
| { |
| if (handler.RequestMatcher!.Invoke(e.RequestData)) |
| { |
| if (handler.RequestTransformer != null) |
| { |
| await this.session.Value.Domains.Network.ContinueRequest(handler.RequestTransformer(e.RequestData)).ConfigureAwait(false); |
| return; |
| } |
| |
| if (handler.ResponseSupplier != null) |
| { |
| await this.session.Value.Domains.Network.ContinueRequestWithResponse(e.RequestData, handler.ResponseSupplier(e.RequestData)).ConfigureAwait(false); |
| return; |
| } |
| } |
| } |
| |
| await this.session.Value.Domains.Network.ContinueRequestWithoutModification(e.RequestData).ConfigureAwait(false); |
| } |
| |
| private async Task OnResponsePaused(object sender, ResponsePausedEventArgs e) |
| { |
| if (e.ResponseData.Headers.Count > 0) |
| { |
| // If no headers are present, the body cannot be retrieved. |
| await this.session.Value.Domains.Network.AddResponseBody(e.ResponseData).ConfigureAwait(false); |
| } |
| |
| if (this.NetworkResponseReceived != null) |
| { |
| this.NetworkResponseReceived(this, new NetworkResponseReceivedEventArgs(e.ResponseData)); |
| } |
| |
| foreach (var handler in this.responseHandlers) |
| { |
| if (handler.ResponseMatcher!.Invoke(e.ResponseData)) |
| { |
| // NOTE: We create a dummy HttpRequestData object here, because the ContinueRequestWithResponse |
| // method demands one; however, the only property used by that method is the RequestId property. |
| // It might be better to refactor that method signature to simply pass the request ID, or |
| // alternatively, just pass the response data, which should also contain the request ID anyway. |
| HttpRequestData requestData = new HttpRequestData { RequestId = e.ResponseData.RequestId }; |
| await this.session.Value.Domains.Network.ContinueRequestWithResponse(requestData, handler.ResponseTransformer!(e.ResponseData)).ConfigureAwait(false); |
| return; |
| } |
| } |
| |
| await this.session.Value.Domains.Network.ContinueResponseWithoutModification(e.ResponseData).ConfigureAwait(false); |
| } |
| } |