blob: 26f84db946d24f163bcc76ec11c646b801cb8860 [file] [log] [blame]
// <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);
}
}