Path: blob/trunk/py/selenium/webdriver/common/bidi/network.py
4012 views
# Licensed to the Software Freedom Conservancy (SFC) under one1# or more contributor license agreements. See the NOTICE file2# distributed with this work for additional information3# regarding copyright ownership. The SFC licenses this file4# to you under the Apache License, Version 2.0 (the5# "License"); you may not use this file except in compliance6# with the License. You may obtain a copy of the License at7#8# http://www.apache.org/licenses/LICENSE-2.09#10# Unless required by applicable law or agreed to in writing,11# software distributed under the License is distributed on an12# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY13# KIND, either express or implied. See the License for the14# specific language governing permissions and limitations15# under the License.1617from __future__ import annotations1819from collections.abc import Callable20from typing import Any2122from selenium.webdriver.common.bidi.common import command_builder23from selenium.webdriver.remote.websocket_connection import WebSocketConnection242526class NetworkEvent:27"""Represents a network event."""2829def __init__(self, event_class: str, **kwargs: Any) -> None:30self.event_class = event_class31self.params = kwargs3233@classmethod34def from_json(cls, json: dict[str, Any]) -> NetworkEvent:35return cls(event_class=json.get("event_class", ""), **json)363738class Network:39EVENTS = {40"before_request": "network.beforeRequestSent",41"response_started": "network.responseStarted",42"response_completed": "network.responseCompleted",43"auth_required": "network.authRequired",44"fetch_error": "network.fetchError",45"continue_request": "network.continueRequest",46"continue_auth": "network.continueWithAuth",47}4849PHASES = {50"before_request": "beforeRequestSent",51"response_started": "responseStarted",52"auth_required": "authRequired",53}5455def __init__(self, conn: WebSocketConnection) -> None:56self.conn = conn57self.intercepts: list[str] = []58self.callbacks: dict[str | int, Any] = {}59self.subscriptions: dict[str, list[int]] = {}6061def _add_intercept(62self,63phases: list[str] | None = None,64contexts: list[str] | None = None,65url_patterns: list[Any] | None = None,66) -> dict[str, Any]:67"""Add an intercept to the network.6869Args:70phases: A list of phases to intercept. Default is None (empty list).71contexts: A list of contexts to intercept. Default is None.72url_patterns: A list of URL patterns to intercept. Default is None.7374Returns:75str: intercept id76"""77if phases is None:78phases = []79params = {}80if contexts is not None:81params["contexts"] = contexts82if url_patterns is not None:83params["urlPatterns"] = url_patterns84if len(phases) > 0:85params["phases"] = phases86else:87params["phases"] = ["beforeRequestSent"]88cmd = command_builder("network.addIntercept", params)8990result: dict[str, Any] = self.conn.execute(cmd)91self.intercepts.append(result["intercept"])92return result9394def _remove_intercept(self, intercept: str | None = None) -> None:95"""Remove a specific intercept, or all intercepts.9697Args:98intercept: The intercept to remove. Default is None.99100Raises:101ValueError: If intercept is not found.102103Note:104If intercept is None, all intercepts will be removed.105"""106if intercept is None:107intercepts_to_remove = self.intercepts.copy() # create a copy before iterating108for intercept_id in intercepts_to_remove: # remove all intercepts109self.conn.execute(command_builder("network.removeIntercept", {"intercept": intercept_id}))110self.intercepts.remove(intercept_id)111else:112try:113self.conn.execute(command_builder("network.removeIntercept", {"intercept": intercept}))114self.intercepts.remove(intercept)115except Exception as e:116raise Exception(f"Exception: {e}")117118def _on_request(self, event_name: str, callback: Callable[[Request], Any]) -> int:119"""Set a callback function to subscribe to a network event.120121Args:122event_name: The event to subscribe to.123callback: The callback function to execute on event.124Takes Request object as argument.125126Returns:127int: callback id128"""129event = NetworkEvent(event_name)130131def _callback(event_data: NetworkEvent) -> None:132request = Request(133network=self,134request_id=event_data.params["request"].get("request", None),135body_size=event_data.params["request"].get("bodySize", None),136cookies=event_data.params["request"].get("cookies", None),137resource_type=event_data.params["request"].get("goog:resourceType", None),138headers=event_data.params["request"].get("headers", None),139headers_size=event_data.params["request"].get("headersSize", None),140timings=event_data.params["request"].get("timings", None),141url=event_data.params["request"].get("url", None),142)143callback(request)144145callback_id: int = self.conn.add_callback(event, _callback)146147if event_name in self.callbacks:148self.callbacks[event_name].append(callback_id)149else:150self.callbacks[event_name] = [callback_id]151152return callback_id153154def add_request_handler(155self,156event: str,157callback: Callable[[Request], Any],158url_patterns: list[Any] | None = None,159contexts: list[str] | None = None,160) -> int:161"""Add a request handler to the network.162163Args:164event: The event to subscribe to.165callback: The callback function to execute on request interception.166Takes Request object as argument.167url_patterns: A list of URL patterns to intercept. Default is None.168contexts: A list of contexts to intercept. Default is None.169170Returns:171int: callback id172"""173try:174event_name = self.EVENTS[event]175phase_name = self.PHASES[event]176except KeyError:177raise Exception(f"Event {event} not found")178179result = self._add_intercept(phases=[phase_name], url_patterns=url_patterns, contexts=contexts)180callback_id = self._on_request(event_name, callback)181182if event_name in self.subscriptions:183self.subscriptions[event_name].append(callback_id)184else:185params: dict[str, Any] = {}186params["events"] = [event_name]187self.conn.execute(command_builder("session.subscribe", params))188self.subscriptions[event_name] = [callback_id]189190self.callbacks[callback_id] = result["intercept"]191return callback_id192193def remove_request_handler(self, event: str, callback_id: int) -> None:194"""Remove a request handler from the network.195196Args:197event: The event to unsubscribe from.198callback_id: The callback id to remove.199"""200try:201event_name = self.EVENTS[event]202except KeyError:203raise Exception(f"Event {event} not found")204205net_event = NetworkEvent(event_name)206207self.conn.remove_callback(net_event, callback_id)208self._remove_intercept(self.callbacks[callback_id])209del self.callbacks[callback_id]210self.subscriptions[event_name].remove(callback_id)211if len(self.subscriptions[event_name]) == 0:212params: dict[str, Any] = {}213params["events"] = [event_name]214self.conn.execute(command_builder("session.unsubscribe", params))215del self.subscriptions[event_name]216217def clear_request_handlers(self) -> None:218"""Clear all request handlers from the network."""219for event_name in self.subscriptions:220net_event = NetworkEvent(event_name)221for callback_id in self.subscriptions[event_name]:222self.conn.remove_callback(net_event, callback_id)223self._remove_intercept(self.callbacks[callback_id])224del self.callbacks[callback_id]225params: dict[str, Any] = {}226params["events"] = [event_name]227self.conn.execute(command_builder("session.unsubscribe", params))228self.subscriptions = {}229230def add_auth_handler(self, username: str, password: str) -> int:231"""Add an authentication handler to the network.232233Args:234username: The username to authenticate with.235password: The password to authenticate with.236237Returns:238int: callback id239"""240event = "auth_required"241242def _callback(request: Request) -> None:243request._continue_with_auth(username, password)244245return self.add_request_handler(event, _callback)246247def remove_auth_handler(self, callback_id: int) -> None:248"""Remove an authentication handler from the network.249250Args:251callback_id: The callback id to remove.252"""253event = "auth_required"254self.remove_request_handler(event, callback_id)255256257class Request:258"""Represents an intercepted network request."""259260def __init__(261self,262network: Network,263request_id: Any,264body_size: int | None = None,265cookies: Any = None,266resource_type: str | None = None,267headers: Any = None,268headers_size: int | None = None,269method: str | None = None,270timings: Any = None,271url: str | None = None,272) -> None:273self.network = network274self.request_id = request_id275self.body_size = body_size276self.cookies = cookies277self.resource_type = resource_type278self.headers = headers279self.headers_size = headers_size280self.method = method281self.timings = timings282self.url = url283284def fail_request(self) -> None:285"""Fail this request."""286if not self.request_id:287raise ValueError("Request not found.")288289params: dict[str, Any] = {"request": self.request_id}290self.network.conn.execute(command_builder("network.failRequest", params))291292def continue_request(293self,294body: Any = None,295method: str | None = None,296headers: Any = None,297cookies: Any = None,298url: str | None = None,299) -> None:300"""Continue after intercepting this request."""301if not self.request_id:302raise ValueError("Request not found.")303304params: dict[str, Any] = {"request": self.request_id}305if body is not None:306params["body"] = body307if method is not None:308params["method"] = method309if headers is not None:310params["headers"] = headers311if cookies is not None:312params["cookies"] = cookies313if url is not None:314params["url"] = url315316self.network.conn.execute(command_builder("network.continueRequest", params))317318def _continue_with_auth(self, username: str | None = None, password: str | None = None) -> None:319"""Continue with authentication.320321Args:322username: The username to authenticate with.323password: The password to authenticate with.324325Note:326If username or password is None, it attempts auth with no credentials.327"""328params: dict[str, Any] = {}329params["request"] = self.request_id330331if not username or not password: # no credentials is valid option332params["action"] = "default"333else:334params["action"] = "provideCredentials"335params["credentials"] = {"type": "password", "username": username, "password": password}336337self.network.conn.execute(command_builder("network.continueWithAuth", params))338339340