Path: blob/trunk/py/selenium/webdriver/remote/remote_connection.py
4024 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.1617import logging18import string19import sys20import warnings21from base64 import b64encode22from urllib import parse23from urllib.parse import unquote, urlparse2425import urllib32627from selenium import __version__28from selenium.common.exceptions import WebDriverException29from selenium.webdriver.remote import utils30from selenium.webdriver.remote.client_config import ClientConfig31from selenium.webdriver.remote.command import Command32from selenium.webdriver.remote.errorhandler import ErrorCode3334LOGGER = logging.getLogger(__name__)3536remote_commands = {37Command.NEW_SESSION: ("POST", "/session"),38Command.QUIT: ("DELETE", "/session/$sessionId"),39Command.W3C_GET_CURRENT_WINDOW_HANDLE: ("GET", "/session/$sessionId/window"),40Command.W3C_GET_WINDOW_HANDLES: ("GET", "/session/$sessionId/window/handles"),41Command.GET: ("POST", "/session/$sessionId/url"),42Command.GO_FORWARD: ("POST", "/session/$sessionId/forward"),43Command.GO_BACK: ("POST", "/session/$sessionId/back"),44Command.REFRESH: ("POST", "/session/$sessionId/refresh"),45Command.W3C_EXECUTE_SCRIPT: ("POST", "/session/$sessionId/execute/sync"),46Command.W3C_EXECUTE_SCRIPT_ASYNC: ("POST", "/session/$sessionId/execute/async"),47Command.GET_CURRENT_URL: ("GET", "/session/$sessionId/url"),48Command.GET_TITLE: ("GET", "/session/$sessionId/title"),49Command.GET_PAGE_SOURCE: ("GET", "/session/$sessionId/source"),50Command.SCREENSHOT: ("GET", "/session/$sessionId/screenshot"),51Command.ELEMENT_SCREENSHOT: ("GET", "/session/$sessionId/element/$id/screenshot"),52Command.FIND_ELEMENT: ("POST", "/session/$sessionId/element"),53Command.FIND_ELEMENTS: ("POST", "/session/$sessionId/elements"),54Command.W3C_GET_ACTIVE_ELEMENT: ("GET", "/session/$sessionId/element/active"),55Command.FIND_CHILD_ELEMENT: ("POST", "/session/$sessionId/element/$id/element"),56Command.FIND_CHILD_ELEMENTS: ("POST", "/session/$sessionId/element/$id/elements"),57Command.CLICK_ELEMENT: ("POST", "/session/$sessionId/element/$id/click"),58Command.CLEAR_ELEMENT: ("POST", "/session/$sessionId/element/$id/clear"),59Command.GET_ELEMENT_TEXT: ("GET", "/session/$sessionId/element/$id/text"),60Command.SEND_KEYS_TO_ELEMENT: ("POST", "/session/$sessionId/element/$id/value"),61Command.GET_ELEMENT_TAG_NAME: ("GET", "/session/$sessionId/element/$id/name"),62Command.IS_ELEMENT_SELECTED: ("GET", "/session/$sessionId/element/$id/selected"),63Command.IS_ELEMENT_ENABLED: ("GET", "/session/$sessionId/element/$id/enabled"),64Command.GET_ELEMENT_RECT: ("GET", "/session/$sessionId/element/$id/rect"),65Command.GET_ELEMENT_ATTRIBUTE: ("GET", "/session/$sessionId/element/$id/attribute/$name"),66Command.GET_ELEMENT_PROPERTY: ("GET", "/session/$sessionId/element/$id/property/$name"),67Command.GET_ELEMENT_ARIA_ROLE: ("GET", "/session/$sessionId/element/$id/computedrole"),68Command.GET_ELEMENT_ARIA_LABEL: ("GET", "/session/$sessionId/element/$id/computedlabel"),69Command.GET_SHADOW_ROOT: ("GET", "/session/$sessionId/element/$id/shadow"),70Command.FIND_ELEMENT_FROM_SHADOW_ROOT: ("POST", "/session/$sessionId/shadow/$shadowId/element"),71Command.FIND_ELEMENTS_FROM_SHADOW_ROOT: ("POST", "/session/$sessionId/shadow/$shadowId/elements"),72Command.GET_ALL_COOKIES: ("GET", "/session/$sessionId/cookie"),73Command.ADD_COOKIE: ("POST", "/session/$sessionId/cookie"),74Command.GET_COOKIE: ("GET", "/session/$sessionId/cookie/$name"),75Command.DELETE_ALL_COOKIES: ("DELETE", "/session/$sessionId/cookie"),76Command.DELETE_COOKIE: ("DELETE", "/session/$sessionId/cookie/$name"),77Command.SWITCH_TO_FRAME: ("POST", "/session/$sessionId/frame"),78Command.SWITCH_TO_PARENT_FRAME: ("POST", "/session/$sessionId/frame/parent"),79Command.SWITCH_TO_WINDOW: ("POST", "/session/$sessionId/window"),80Command.NEW_WINDOW: ("POST", "/session/$sessionId/window/new"),81Command.CLOSE: ("DELETE", "/session/$sessionId/window"),82Command.GET_ELEMENT_VALUE_OF_CSS_PROPERTY: ("GET", "/session/$sessionId/element/$id/css/$propertyName"),83Command.EXECUTE_ASYNC_SCRIPT: ("POST", "/session/$sessionId/execute_async"),84Command.SET_TIMEOUTS: ("POST", "/session/$sessionId/timeouts"),85Command.GET_TIMEOUTS: ("GET", "/session/$sessionId/timeouts"),86Command.W3C_DISMISS_ALERT: ("POST", "/session/$sessionId/alert/dismiss"),87Command.W3C_ACCEPT_ALERT: ("POST", "/session/$sessionId/alert/accept"),88Command.W3C_SET_ALERT_VALUE: ("POST", "/session/$sessionId/alert/text"),89Command.W3C_GET_ALERT_TEXT: ("GET", "/session/$sessionId/alert/text"),90Command.W3C_ACTIONS: ("POST", "/session/$sessionId/actions"),91Command.W3C_CLEAR_ACTIONS: ("DELETE", "/session/$sessionId/actions"),92Command.SET_WINDOW_RECT: ("POST", "/session/$sessionId/window/rect"),93Command.GET_WINDOW_RECT: ("GET", "/session/$sessionId/window/rect"),94Command.W3C_MAXIMIZE_WINDOW: ("POST", "/session/$sessionId/window/maximize"),95Command.SET_SCREEN_ORIENTATION: ("POST", "/session/$sessionId/orientation"),96Command.GET_SCREEN_ORIENTATION: ("GET", "/session/$sessionId/orientation"),97Command.GET_NETWORK_CONNECTION: ("GET", "/session/$sessionId/network_connection"),98Command.SET_NETWORK_CONNECTION: ("POST", "/session/$sessionId/network_connection"),99Command.GET_LOG: ("POST", "/session/$sessionId/se/log"),100Command.GET_AVAILABLE_LOG_TYPES: ("GET", "/session/$sessionId/se/log/types"),101Command.CURRENT_CONTEXT_HANDLE: ("GET", "/session/$sessionId/context"),102Command.CONTEXT_HANDLES: ("GET", "/session/$sessionId/contexts"),103Command.SWITCH_TO_CONTEXT: ("POST", "/session/$sessionId/context"),104Command.FULLSCREEN_WINDOW: ("POST", "/session/$sessionId/window/fullscreen"),105Command.MINIMIZE_WINDOW: ("POST", "/session/$sessionId/window/minimize"),106Command.PRINT_PAGE: ("POST", "/session/$sessionId/print"),107Command.ADD_VIRTUAL_AUTHENTICATOR: ("POST", "/session/$sessionId/webauthn/authenticator"),108Command.REMOVE_VIRTUAL_AUTHENTICATOR: (109"DELETE",110"/session/$sessionId/webauthn/authenticator/$authenticatorId",111),112Command.ADD_CREDENTIAL: ("POST", "/session/$sessionId/webauthn/authenticator/$authenticatorId/credential"),113Command.GET_CREDENTIALS: ("GET", "/session/$sessionId/webauthn/authenticator/$authenticatorId/credentials"),114Command.REMOVE_CREDENTIAL: (115"DELETE",116"/session/$sessionId/webauthn/authenticator/$authenticatorId/credentials/$credentialId",117),118Command.REMOVE_ALL_CREDENTIALS: (119"DELETE",120"/session/$sessionId/webauthn/authenticator/$authenticatorId/credentials",121),122Command.SET_USER_VERIFIED: ("POST", "/session/$sessionId/webauthn/authenticator/$authenticatorId/uv"),123Command.UPLOAD_FILE: ("POST", "/session/$sessionId/se/file"),124Command.GET_DOWNLOADABLE_FILES: ("GET", "/session/$sessionId/se/files"),125Command.DOWNLOAD_FILE: ("POST", "/session/$sessionId/se/files"),126Command.DELETE_DOWNLOADABLE_FILES: ("DELETE", "/session/$sessionId/se/files"),127Command.FIRE_SESSION_EVENT: ("POST", "/session/$sessionId/se/event"),128# Federated Credential Management (FedCM)129Command.GET_FEDCM_TITLE: ("GET", "/session/$sessionId/fedcm/gettitle"),130Command.GET_FEDCM_DIALOG_TYPE: ("GET", "/session/$sessionId/fedcm/getdialogtype"),131Command.GET_FEDCM_ACCOUNT_LIST: ("GET", "/session/$sessionId/fedcm/accountlist"),132Command.CLICK_FEDCM_DIALOG_BUTTON: ("POST", "/session/$sessionId/fedcm/clickdialogbutton"),133Command.CANCEL_FEDCM_DIALOG: ("POST", "/session/$sessionId/fedcm/canceldialog"),134Command.SELECT_FEDCM_ACCOUNT: ("POST", "/session/$sessionId/fedcm/selectaccount"),135Command.SET_FEDCM_DELAY: ("POST", "/session/$sessionId/fedcm/setdelayenabled"),136Command.RESET_FEDCM_COOLDOWN: ("POST", "/session/$sessionId/fedcm/resetcooldown"),137}138139140class RemoteConnection:141"""A connection with the Remote WebDriver server.142143Communicates with the server using the WebDriver wire protocol:144https://github.com/SeleniumHQ/selenium/wiki/JsonWireProtocol145"""146147browser_name: str | None = None148# Keep backward compatibility for AppiumConnection - https://github.com/SeleniumHQ/selenium/issues/14694149import os150import socket151152import certifi153154_timeout = socket.getdefaulttimeout()155_ca_certs = os.getenv("REQUESTS_CA_BUNDLE") if "REQUESTS_CA_BUNDLE" in os.environ else certifi.where()156_client_config: ClientConfig157158system = sys.platform159if system == "darwin":160system = "mac"161162# Class variables for headers163extra_headers = None164user_agent = f"selenium/{__version__} (python {system})"165166@property167def client_config(self):168return self._client_config169170@classmethod171def get_timeout(cls):172"""Returns timeout value in seconds for all http requests made to the Remote Connection.173174Returns:175Timeout value in seconds for all http requests made to the176Remote Connection177"""178warnings.warn(179"get_timeout() in RemoteConnection is deprecated, get timeout from client_config instead",180DeprecationWarning,181stacklevel=2,182)183return cls._client_config.timeout184185@classmethod186def set_timeout(cls, timeout):187"""Override the default timeout.188189Args:190timeout: timeout value for http requests in seconds191"""192warnings.warn(193"set_timeout() in RemoteConnection is deprecated, set timeout in client_config instead",194DeprecationWarning,195stacklevel=2,196)197cls._client_config.timeout = timeout198199@classmethod200def reset_timeout(cls):201"""Reset the http request timeout to socket._GLOBAL_DEFAULT_TIMEOUT."""202warnings.warn(203"reset_timeout() in RemoteConnection is deprecated, use reset_timeout() in client_config instead",204DeprecationWarning,205stacklevel=2,206)207cls._client_config.reset_timeout()208209@classmethod210def get_certificate_bundle_path(cls):211"""Returns paths of the .pem encoded certificate to verify connection to command executor.212213Returns:214Paths of the .pem encoded certificate to verify connection to215command executor. Defaults to certifi.where() or216REQUESTS_CA_BUNDLE env variable if set.217"""218warnings.warn(219"get_certificate_bundle_path() in RemoteConnection is deprecated, get ca_certs from client_config instead",220DeprecationWarning,221stacklevel=2,222)223return cls._client_config.ca_certs224225@classmethod226def set_certificate_bundle_path(cls, path):227"""Set the path to the certificate bundle for verifying command executor connection.228229Can also be set to None to disable certificate validation.230231Args:232path: path of a .pem encoded certificate chain.233"""234warnings.warn(235"set_certificate_bundle_path() in RemoteConnection is deprecated, set ca_certs in client_config instead",236DeprecationWarning,237stacklevel=2,238)239cls._client_config.ca_certs = path240241@classmethod242def get_remote_connection_headers(cls, parsed_url, keep_alive=False):243"""Get headers for remote request.244245Args:246parsed_url: The parsed url247keep_alive: Is this a keep-alive connection (default: False)248"""249headers = {250"Accept": "application/json",251"Content-Type": "application/json;charset=UTF-8",252"User-Agent": cls.user_agent,253}254255if parsed_url.username:256warnings.warn(257"Embedding username and password in URL could be insecure, use ClientConfig instead", stacklevel=2258)259base64string = b64encode(f"{parsed_url.username}:{parsed_url.password}".encode())260headers.update({"Authorization": f"Basic {base64string.decode()}"})261262if keep_alive:263headers.update({"Connection": "keep-alive"})264265if cls.extra_headers:266headers.update(cls.extra_headers)267268return headers269270def _identify_http_proxy_auth(self):271parsed_url = urlparse(self._proxy_url)272if parsed_url.username and parsed_url.password:273return True274275def _separate_http_proxy_auth(self):276parsed_url = urlparse(self._proxy_url)277proxy_without_auth = f"{parsed_url.scheme}://{parsed_url.hostname}:{parsed_url.port}"278auth = f"{parsed_url.username}:{parsed_url.password}"279return proxy_without_auth, auth280281def _get_connection_manager(self):282pool_manager_init_args = {"timeout": self._client_config.timeout}283pool_manager_init_args.update(284self._client_config.init_args_for_pool_manager.get("init_args_for_pool_manager", {})285)286287if self._client_config.ignore_certificates:288pool_manager_init_args["cert_reqs"] = "CERT_NONE"289urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)290elif self._client_config.ca_certs:291pool_manager_init_args["cert_reqs"] = "CERT_REQUIRED"292pool_manager_init_args["ca_certs"] = self._client_config.ca_certs293294if self._proxy_url:295if self._proxy_url.lower().startswith("sock"):296from urllib3.contrib.socks import SOCKSProxyManager297298return SOCKSProxyManager(self._proxy_url, **pool_manager_init_args)299if self._identify_http_proxy_auth():300self._proxy_url, self._basic_proxy_auth = self._separate_http_proxy_auth()301pool_manager_init_args["proxy_headers"] = urllib3.make_headers(302proxy_basic_auth=unquote(self._basic_proxy_auth)303)304return urllib3.ProxyManager(self._proxy_url, **pool_manager_init_args)305306return urllib3.PoolManager(**pool_manager_init_args)307308def __init__(309self,310remote_server_addr: str | None = None,311keep_alive: bool = True,312ignore_proxy: bool = False,313ignore_certificates: bool | None = False,314init_args_for_pool_manager: dict | None = None,315client_config: ClientConfig | None = None,316):317if client_config:318self._client_config = client_config319elif remote_server_addr:320self._client_config = ClientConfig(321remote_server_addr=remote_server_addr,322keep_alive=keep_alive,323ignore_certificates=ignore_certificates,324init_args_for_pool_manager=init_args_for_pool_manager,325)326else:327raise WebDriverException("Must provide either 'remote_server_addr' or 'client_config'")328329# Keep backward compatibility for AppiumConnection - https://github.com/SeleniumHQ/selenium/issues/14694330RemoteConnection._timeout = self._client_config.timeout331RemoteConnection._ca_certs = self._client_config.ca_certs332RemoteConnection._client_config = self._client_config333RemoteConnection.extra_headers = self._client_config.extra_headers or RemoteConnection.extra_headers334RemoteConnection.user_agent = self._client_config.user_agent or RemoteConnection.user_agent335336if remote_server_addr:337warnings.warn(338"setting remote_server_addr in RemoteConnection() is deprecated, set in client_config instead",339DeprecationWarning,340stacklevel=2,341)342343if not keep_alive:344warnings.warn(345"setting keep_alive in RemoteConnection() is deprecated, set in client_config instead",346DeprecationWarning,347stacklevel=2,348)349350if ignore_certificates:351warnings.warn(352"setting ignore_certificates in RemoteConnection() is deprecated, set in client_config instead",353DeprecationWarning,354stacklevel=2,355)356357if init_args_for_pool_manager:358warnings.warn(359"setting init_args_for_pool_manager in RemoteConnection() is deprecated, set in client_config instead",360DeprecationWarning,361stacklevel=2,362)363364if ignore_proxy:365self._proxy_url = None366else:367self._proxy_url = self._client_config.get_proxy_url()368369if self._client_config.keep_alive:370self._conn = self._get_connection_manager()371self._commands = remote_commands372373extra_commands: dict[str, str] = {}374375def add_command(self, name, method, url):376"""Register a new command."""377self._commands[name] = (method, url)378379def get_command(self, name: str):380"""Retrieve a command if it exists."""381return self._commands.get(name)382383def execute(self, command, params):384"""Send a command to the remote server.385386Any path substitutions required for the URL mapped to the command should be387included in the command parameters.388389Args:390command: A string specifying the command to execute.391params: A dictionary of named parameters to send with the command as392its JSON payload.393"""394command_info = self._commands.get(command) or self.extra_commands.get(command)395assert command_info is not None, f"Unrecognised command {command}"396path_string = command_info[1]397path = string.Template(path_string).substitute(params)398substitute_params = {word[1:] for word in path_string.split("/") if word.startswith("$")} # remove dollar sign399if isinstance(params, dict) and substitute_params:400for word in substitute_params:401del params[word]402data = utils.dump_json(params)403url = f"{self._client_config.remote_server_addr}{path}"404trimmed = self._trim_large_entries(params)405LOGGER.debug("%s %s %s", command_info[0], url, str(trimmed))406return self._request(command_info[0], url, body=data)407408def _request(self, method, url, body=None) -> dict:409"""Send an HTTP request to the remote server.410411Args:412method: A string for the HTTP method to send the request with.413url: A string for the URL to send the request to.414body: A string for request body. Ignored unless method is POST or PUT.415416Returns:417A dictionary with the server's parsed JSON response.418"""419parsed_url = parse.urlparse(url)420headers = self.get_remote_connection_headers(parsed_url, self._client_config.keep_alive)421auth_header = self._client_config.get_auth_header()422423if auth_header:424headers.update(auth_header)425426if body and method not in ("POST", "PUT"):427body = None428429if self._client_config.keep_alive:430response = self._conn.request(method, url, body=body, headers=headers, timeout=self._client_config.timeout)431statuscode = response.status432else:433conn = self._get_connection_manager()434with conn as http:435response = http.request(method, url, body=body, headers=headers, timeout=self._client_config.timeout)436statuscode = response.status437data = response.data.decode("UTF-8")438LOGGER.debug("Remote response: status=%s | data=%s | headers=%s", response.status, data, response.headers)439try:440if 300 <= statuscode < 304:441return self._request("GET", response.headers.get("location", None))442if statuscode == 401:443return {"status": statuscode, "value": "Authorization Required"}444if statuscode >= 400:445return {"status": statuscode, "value": response.reason if not data else data.strip()}446content_type = []447if response.headers.get("Content-Type", None):448content_type = response.headers.get("Content-Type", None).split(";")449if not any([x.startswith("image/png") for x in content_type]):450try:451data = utils.load_json(data.strip())452except ValueError:453if 199 < statuscode < 300:454status = ErrorCode.SUCCESS455else:456status = ErrorCode.UNKNOWN_ERROR # type: ignore457return {"status": status, "value": data.strip()}458459# Some drivers incorrectly return a response460# with no 'value' field when they should return null.461if "value" not in data:462data["value"] = None463return data464data = {"status": 0, "value": data}465return data466finally:467LOGGER.debug("Finished Request")468response.close()469470def close(self):471"""Clean up resources when finished with the remote_connection."""472if hasattr(self, "_conn"):473self._conn.clear()474475def _trim_large_entries(self, input_dict, max_length=100) -> dict | str:476"""Truncate string values in a dictionary if they exceed max_length.477478Args:479input_dict: Dictionary with potentially large values480max_length: Maximum allowed length of string values481482Returns:483Dictionary with truncated string values484"""485output_dictionary = {}486for key, value in input_dict.items():487if isinstance(value, dict):488output_dictionary[key] = self._trim_large_entries(value, max_length)489elif isinstance(value, str) and len(value) > max_length:490output_dictionary[key] = value[:max_length] + "..."491else:492output_dictionary[key] = value493494return output_dictionary495496497