Path: blob/trunk/py/selenium/webdriver/remote/remote_connection.py
1864 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 typing import Optional23from urllib import parse24from urllib.parse import unquote, urlparse2526import urllib32728from selenium import __version__2930from . import utils31from .client_config import ClientConfig32from .command import Command33from .errorhandler import ErrorCode3435LOGGER = logging.getLogger(__name__)3637remote_commands = {38Command.NEW_SESSION: ("POST", "/session"),39Command.QUIT: ("DELETE", "/session/$sessionId"),40Command.W3C_GET_CURRENT_WINDOW_HANDLE: ("GET", "/session/$sessionId/window"),41Command.W3C_GET_WINDOW_HANDLES: ("GET", "/session/$sessionId/window/handles"),42Command.GET: ("POST", "/session/$sessionId/url"),43Command.GO_FORWARD: ("POST", "/session/$sessionId/forward"),44Command.GO_BACK: ("POST", "/session/$sessionId/back"),45Command.REFRESH: ("POST", "/session/$sessionId/refresh"),46Command.W3C_EXECUTE_SCRIPT: ("POST", "/session/$sessionId/execute/sync"),47Command.W3C_EXECUTE_SCRIPT_ASYNC: ("POST", "/session/$sessionId/execute/async"),48Command.GET_CURRENT_URL: ("GET", "/session/$sessionId/url"),49Command.GET_TITLE: ("GET", "/session/$sessionId/title"),50Command.GET_PAGE_SOURCE: ("GET", "/session/$sessionId/source"),51Command.SCREENSHOT: ("GET", "/session/$sessionId/screenshot"),52Command.ELEMENT_SCREENSHOT: ("GET", "/session/$sessionId/element/$id/screenshot"),53Command.FIND_ELEMENT: ("POST", "/session/$sessionId/element"),54Command.FIND_ELEMENTS: ("POST", "/session/$sessionId/elements"),55Command.W3C_GET_ACTIVE_ELEMENT: ("GET", "/session/$sessionId/element/active"),56Command.FIND_CHILD_ELEMENT: ("POST", "/session/$sessionId/element/$id/element"),57Command.FIND_CHILD_ELEMENTS: ("POST", "/session/$sessionId/element/$id/elements"),58Command.CLICK_ELEMENT: ("POST", "/session/$sessionId/element/$id/click"),59Command.CLEAR_ELEMENT: ("POST", "/session/$sessionId/element/$id/clear"),60Command.GET_ELEMENT_TEXT: ("GET", "/session/$sessionId/element/$id/text"),61Command.SEND_KEYS_TO_ELEMENT: ("POST", "/session/$sessionId/element/$id/value"),62Command.GET_ELEMENT_TAG_NAME: ("GET", "/session/$sessionId/element/$id/name"),63Command.IS_ELEMENT_SELECTED: ("GET", "/session/$sessionId/element/$id/selected"),64Command.IS_ELEMENT_ENABLED: ("GET", "/session/$sessionId/element/$id/enabled"),65Command.GET_ELEMENT_RECT: ("GET", "/session/$sessionId/element/$id/rect"),66Command.GET_ELEMENT_ATTRIBUTE: ("GET", "/session/$sessionId/element/$id/attribute/$name"),67Command.GET_ELEMENT_PROPERTY: ("GET", "/session/$sessionId/element/$id/property/$name"),68Command.GET_ELEMENT_ARIA_ROLE: ("GET", "/session/$sessionId/element/$id/computedrole"),69Command.GET_ELEMENT_ARIA_LABEL: ("GET", "/session/$sessionId/element/$id/computedlabel"),70Command.GET_SHADOW_ROOT: ("GET", "/session/$sessionId/element/$id/shadow"),71Command.FIND_ELEMENT_FROM_SHADOW_ROOT: ("POST", "/session/$sessionId/shadow/$shadowId/element"),72Command.FIND_ELEMENTS_FROM_SHADOW_ROOT: ("POST", "/session/$sessionId/shadow/$shadowId/elements"),73Command.GET_ALL_COOKIES: ("GET", "/session/$sessionId/cookie"),74Command.ADD_COOKIE: ("POST", "/session/$sessionId/cookie"),75Command.GET_COOKIE: ("GET", "/session/$sessionId/cookie/$name"),76Command.DELETE_ALL_COOKIES: ("DELETE", "/session/$sessionId/cookie"),77Command.DELETE_COOKIE: ("DELETE", "/session/$sessionId/cookie/$name"),78Command.SWITCH_TO_FRAME: ("POST", "/session/$sessionId/frame"),79Command.SWITCH_TO_PARENT_FRAME: ("POST", "/session/$sessionId/frame/parent"),80Command.SWITCH_TO_WINDOW: ("POST", "/session/$sessionId/window"),81Command.NEW_WINDOW: ("POST", "/session/$sessionId/window/new"),82Command.CLOSE: ("DELETE", "/session/$sessionId/window"),83Command.GET_ELEMENT_VALUE_OF_CSS_PROPERTY: ("GET", "/session/$sessionId/element/$id/css/$propertyName"),84Command.EXECUTE_ASYNC_SCRIPT: ("POST", "/session/$sessionId/execute_async"),85Command.SET_TIMEOUTS: ("POST", "/session/$sessionId/timeouts"),86Command.GET_TIMEOUTS: ("GET", "/session/$sessionId/timeouts"),87Command.W3C_DISMISS_ALERT: ("POST", "/session/$sessionId/alert/dismiss"),88Command.W3C_ACCEPT_ALERT: ("POST", "/session/$sessionId/alert/accept"),89Command.W3C_SET_ALERT_VALUE: ("POST", "/session/$sessionId/alert/text"),90Command.W3C_GET_ALERT_TEXT: ("GET", "/session/$sessionId/alert/text"),91Command.W3C_ACTIONS: ("POST", "/session/$sessionId/actions"),92Command.W3C_CLEAR_ACTIONS: ("DELETE", "/session/$sessionId/actions"),93Command.SET_WINDOW_RECT: ("POST", "/session/$sessionId/window/rect"),94Command.GET_WINDOW_RECT: ("GET", "/session/$sessionId/window/rect"),95Command.W3C_MAXIMIZE_WINDOW: ("POST", "/session/$sessionId/window/maximize"),96Command.SET_SCREEN_ORIENTATION: ("POST", "/session/$sessionId/orientation"),97Command.GET_SCREEN_ORIENTATION: ("GET", "/session/$sessionId/orientation"),98Command.GET_NETWORK_CONNECTION: ("GET", "/session/$sessionId/network_connection"),99Command.SET_NETWORK_CONNECTION: ("POST", "/session/$sessionId/network_connection"),100Command.GET_LOG: ("POST", "/session/$sessionId/se/log"),101Command.GET_AVAILABLE_LOG_TYPES: ("GET", "/session/$sessionId/se/log/types"),102Command.CURRENT_CONTEXT_HANDLE: ("GET", "/session/$sessionId/context"),103Command.CONTEXT_HANDLES: ("GET", "/session/$sessionId/contexts"),104Command.SWITCH_TO_CONTEXT: ("POST", "/session/$sessionId/context"),105Command.FULLSCREEN_WINDOW: ("POST", "/session/$sessionId/window/fullscreen"),106Command.MINIMIZE_WINDOW: ("POST", "/session/$sessionId/window/minimize"),107Command.PRINT_PAGE: ("POST", "/session/$sessionId/print"),108Command.ADD_VIRTUAL_AUTHENTICATOR: ("POST", "/session/$sessionId/webauthn/authenticator"),109Command.REMOVE_VIRTUAL_AUTHENTICATOR: (110"DELETE",111"/session/$sessionId/webauthn/authenticator/$authenticatorId",112),113Command.ADD_CREDENTIAL: ("POST", "/session/$sessionId/webauthn/authenticator/$authenticatorId/credential"),114Command.GET_CREDENTIALS: ("GET", "/session/$sessionId/webauthn/authenticator/$authenticatorId/credentials"),115Command.REMOVE_CREDENTIAL: (116"DELETE",117"/session/$sessionId/webauthn/authenticator/$authenticatorId/credentials/$credentialId",118),119Command.REMOVE_ALL_CREDENTIALS: (120"DELETE",121"/session/$sessionId/webauthn/authenticator/$authenticatorId/credentials",122),123Command.SET_USER_VERIFIED: ("POST", "/session/$sessionId/webauthn/authenticator/$authenticatorId/uv"),124Command.UPLOAD_FILE: ("POST", "/session/$sessionId/se/file"),125Command.GET_DOWNLOADABLE_FILES: ("GET", "/session/$sessionId/se/files"),126Command.DOWNLOAD_FILE: ("POST", "/session/$sessionId/se/files"),127Command.DELETE_DOWNLOADABLE_FILES: ("DELETE", "/session/$sessionId/se/files"),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: Optional[str] = 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: Optional[ClientConfig] = None157158system = 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:173174Timeout value in seconds for all http requests made to the175Remote Connection176"""177warnings.warn(178"get_timeout() in RemoteConnection is deprecated, get timeout from client_config instead",179DeprecationWarning,180stacklevel=2,181)182return cls._client_config.timeout183184@classmethod185def set_timeout(cls, timeout):186"""Override the default timeout.187188:Args:189- timeout - timeout value for http requests in seconds190"""191warnings.warn(192"set_timeout() in RemoteConnection is deprecated, set timeout in client_config instead",193DeprecationWarning,194stacklevel=2,195)196cls._client_config.timeout = timeout197198@classmethod199def reset_timeout(cls):200"""Reset the http request timeout to socket._GLOBAL_DEFAULT_TIMEOUT."""201warnings.warn(202"reset_timeout() in RemoteConnection is deprecated, use reset_timeout() in client_config instead",203DeprecationWarning,204stacklevel=2,205)206cls._client_config.reset_timeout()207208@classmethod209def get_certificate_bundle_path(cls):210""":Returns:211212Paths of the .pem encoded certificate to verify connection to213command executor. Defaults to certifi.where() or214REQUESTS_CA_BUNDLE env variable if set.215"""216warnings.warn(217"get_certificate_bundle_path() in RemoteConnection is deprecated, get ca_certs from client_config instead",218DeprecationWarning,219stacklevel=2,220)221return cls._client_config.ca_certs222223@classmethod224def set_certificate_bundle_path(cls, path):225"""Set the path to the certificate bundle to verify connection to226command executor. Can also be set to None to disable certificate227validation.228229:Args:230- path - path of a .pem encoded certificate chain.231"""232warnings.warn(233"set_certificate_bundle_path() in RemoteConnection is deprecated, set ca_certs in client_config instead",234DeprecationWarning,235stacklevel=2,236)237cls._client_config.ca_certs = path238239@classmethod240def get_remote_connection_headers(cls, parsed_url, keep_alive=False):241"""Get headers for remote request.242243:Args:244- parsed_url - The parsed url245- keep_alive (Boolean) - Is this a keep-alive connection (default: False)246"""247248headers = {249"Accept": "application/json",250"Content-Type": "application/json;charset=UTF-8",251"User-Agent": cls.user_agent,252}253254if parsed_url.username:255warnings.warn(256"Embedding username and password in URL could be insecure, use ClientConfig instead", stacklevel=2257)258base64string = b64encode(f"{parsed_url.username}:{parsed_url.password}".encode())259headers.update({"Authorization": f"Basic {base64string.decode()}"})260261if keep_alive:262headers.update({"Connection": "keep-alive"})263264if cls.extra_headers:265headers.update(cls.extra_headers)266267return headers268269def _identify_http_proxy_auth(self):270parsed_url = urlparse(self._proxy_url)271if parsed_url.username and parsed_url.password:272return True273274def _separate_http_proxy_auth(self):275parsed_url = urlparse(self._proxy_url)276proxy_without_auth = f"{parsed_url.scheme}://{parsed_url.hostname}:{parsed_url.port}"277auth = f"{parsed_url.username}:{parsed_url.password}"278return proxy_without_auth, auth279280def _get_connection_manager(self):281pool_manager_init_args = {"timeout": self._client_config.timeout}282pool_manager_init_args.update(283self._client_config.init_args_for_pool_manager.get("init_args_for_pool_manager", {})284)285286if self._client_config.ignore_certificates:287pool_manager_init_args["cert_reqs"] = "CERT_NONE"288urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)289elif self._client_config.ca_certs:290pool_manager_init_args["cert_reqs"] = "CERT_REQUIRED"291pool_manager_init_args["ca_certs"] = self._client_config.ca_certs292293if self._proxy_url:294if self._proxy_url.lower().startswith("sock"):295from urllib3.contrib.socks import SOCKSProxyManager296297return SOCKSProxyManager(self._proxy_url, **pool_manager_init_args)298if self._identify_http_proxy_auth():299self._proxy_url, self._basic_proxy_auth = self._separate_http_proxy_auth()300pool_manager_init_args["proxy_headers"] = urllib3.make_headers(301proxy_basic_auth=unquote(self._basic_proxy_auth)302)303return urllib3.ProxyManager(self._proxy_url, **pool_manager_init_args)304305return urllib3.PoolManager(**pool_manager_init_args)306307def __init__(308self,309remote_server_addr: Optional[str] = None,310keep_alive: Optional[bool] = True,311ignore_proxy: Optional[bool] = False,312ignore_certificates: Optional[bool] = False,313init_args_for_pool_manager: Optional[dict] = None,314client_config: Optional[ClientConfig] = None,315):316self._client_config = client_config or ClientConfig(317remote_server_addr=remote_server_addr,318keep_alive=keep_alive,319ignore_certificates=ignore_certificates,320init_args_for_pool_manager=init_args_for_pool_manager,321)322323# Keep backward compatibility for AppiumConnection - https://github.com/SeleniumHQ/selenium/issues/14694324RemoteConnection._timeout = self._client_config.timeout325RemoteConnection._ca_certs = self._client_config.ca_certs326RemoteConnection._client_config = self._client_config327RemoteConnection.extra_headers = self._client_config.extra_headers or RemoteConnection.extra_headers328RemoteConnection.user_agent = self._client_config.user_agent or RemoteConnection.user_agent329330if remote_server_addr:331warnings.warn(332"setting remote_server_addr in RemoteConnection() is deprecated, set in client_config instead",333DeprecationWarning,334stacklevel=2,335)336337if not keep_alive:338warnings.warn(339"setting keep_alive in RemoteConnection() is deprecated, set in client_config instead",340DeprecationWarning,341stacklevel=2,342)343344if ignore_certificates:345warnings.warn(346"setting ignore_certificates in RemoteConnection() is deprecated, set in client_config instead",347DeprecationWarning,348stacklevel=2,349)350351if init_args_for_pool_manager:352warnings.warn(353"setting init_args_for_pool_manager in RemoteConnection() is deprecated, set in client_config instead",354DeprecationWarning,355stacklevel=2,356)357358if ignore_proxy:359warnings.warn(360"setting ignore_proxy in RemoteConnection() is deprecated, set in client_config instead",361DeprecationWarning,362stacklevel=2,363)364self._proxy_url = None365else:366self._proxy_url = self._client_config.get_proxy_url()367368if self._client_config.keep_alive:369self._conn = self._get_connection_manager()370self._commands = remote_commands371372extra_commands = {}373374def add_command(self, name, method, url):375"""Register a new command."""376self._commands[name] = (method, url)377378def get_command(self, name: str):379"""Retrieve a command if it exists."""380return self._commands.get(name)381382def execute(self, command, params):383"""Send a command to the remote server.384385Any path substitutions required for the URL mapped to the command should be386included in the command parameters.387388:Args:389- command - A string specifying the command to execute.390- params - A dictionary of named parameters to send with the command as391its JSON payload.392"""393command_info = self._commands.get(command) or self.extra_commands.get(command)394assert command_info is not None, f"Unrecognised command {command}"395path_string = command_info[1]396path = string.Template(path_string).substitute(params)397substitute_params = {word[1:] for word in path_string.split("/") if word.startswith("$")} # remove dollar sign398if isinstance(params, dict) and substitute_params:399for word in substitute_params:400del params[word]401data = utils.dump_json(params)402url = f"{self._client_config.remote_server_addr}{path}"403trimmed = self._trim_large_entries(params)404LOGGER.debug("%s %s %s", command_info[0], url, str(trimmed))405return self._request(command_info[0], url, body=data)406407def _request(self, method, url, body=None):408"""Send an HTTP request to the remote server.409410:Args:411- method - A string for the HTTP method to send the request with.412- url - A string for the URL to send the request to.413- body - A string for request body. Ignored unless method is POST or PUT.414415:Returns:416A dictionary with the server's parsed JSON response.417"""418parsed_url = parse.urlparse(url)419headers = self.get_remote_connection_headers(parsed_url, self._client_config.keep_alive)420auth_header = self._client_config.get_auth_header()421422if auth_header:423headers.update(auth_header)424425if body and method not in ("POST", "PUT"):426body = None427428if self._client_config.keep_alive:429response = self._conn.request(method, url, body=body, headers=headers, timeout=self._client_config.timeout)430statuscode = response.status431else:432conn = self._get_connection_manager()433with conn as http:434response = http.request(method, url, body=body, headers=headers, timeout=self._client_config.timeout)435statuscode = response.status436data = response.data.decode("UTF-8")437LOGGER.debug("Remote response: status=%s | data=%s | headers=%s", response.status, data, response.headers)438try:439if 300 <= statuscode < 304:440return self._request("GET", response.headers.get("location", None))441if statuscode == 401:442return {"status": statuscode, "value": "Authorization Required"}443if statuscode >= 400:444return {"status": statuscode, "value": response.reason if not data else data.strip()}445content_type = []446if response.headers.get("Content-Type", None):447content_type = response.headers.get("Content-Type", None).split(";")448if not any([x.startswith("image/png") for x in content_type]):449try:450data = utils.load_json(data.strip())451except ValueError:452if 199 < statuscode < 300:453status = ErrorCode.SUCCESS454else:455status = ErrorCode.UNKNOWN_ERROR456return {"status": status, "value": data.strip()}457458# Some drivers incorrectly return a response459# with no 'value' field when they should return null.460if "value" not in data:461data["value"] = None462return data463data = {"status": 0, "value": data}464return data465finally:466LOGGER.debug("Finished Request")467response.close()468469def close(self):470"""Clean up resources when finished with the remote_connection."""471if hasattr(self, "_conn"):472self._conn.clear()473474def _trim_large_entries(self, input_dict, max_length=100):475"""Truncate string values in a dictionary if they exceed max_length.476477:param dict: Dictionary with potentially large values478:param max_length: Maximum allowed length of string values479:return: Dictionary with truncated string values480"""481output_dictionary = {}482for key, value in input_dict.items():483if isinstance(value, dict):484output_dictionary[key] = self._trim_large_entries(value, max_length)485elif isinstance(value, str) and len(value) > max_length:486output_dictionary[key] = value[:max_length] + "..."487else:488output_dictionary[key] = value489490return output_dictionary491492493