Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
SeleniumHQ
GitHub Repository: SeleniumHQ/Selenium
Path: blob/trunk/py/selenium/webdriver/remote/remote_connection.py
4024 views
1
# Licensed to the Software Freedom Conservancy (SFC) under one
2
# or more contributor license agreements. See the NOTICE file
3
# distributed with this work for additional information
4
# regarding copyright ownership. The SFC licenses this file
5
# to you under the Apache License, Version 2.0 (the
6
# "License"); you may not use this file except in compliance
7
# with the License. You may obtain a copy of the License at
8
#
9
# http://www.apache.org/licenses/LICENSE-2.0
10
#
11
# Unless required by applicable law or agreed to in writing,
12
# software distributed under the License is distributed on an
13
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
# KIND, either express or implied. See the License for the
15
# specific language governing permissions and limitations
16
# under the License.
17
18
import logging
19
import string
20
import sys
21
import warnings
22
from base64 import b64encode
23
from urllib import parse
24
from urllib.parse import unquote, urlparse
25
26
import urllib3
27
28
from selenium import __version__
29
from selenium.common.exceptions import WebDriverException
30
from selenium.webdriver.remote import utils
31
from selenium.webdriver.remote.client_config import ClientConfig
32
from selenium.webdriver.remote.command import Command
33
from selenium.webdriver.remote.errorhandler import ErrorCode
34
35
LOGGER = logging.getLogger(__name__)
36
37
remote_commands = {
38
Command.NEW_SESSION: ("POST", "/session"),
39
Command.QUIT: ("DELETE", "/session/$sessionId"),
40
Command.W3C_GET_CURRENT_WINDOW_HANDLE: ("GET", "/session/$sessionId/window"),
41
Command.W3C_GET_WINDOW_HANDLES: ("GET", "/session/$sessionId/window/handles"),
42
Command.GET: ("POST", "/session/$sessionId/url"),
43
Command.GO_FORWARD: ("POST", "/session/$sessionId/forward"),
44
Command.GO_BACK: ("POST", "/session/$sessionId/back"),
45
Command.REFRESH: ("POST", "/session/$sessionId/refresh"),
46
Command.W3C_EXECUTE_SCRIPT: ("POST", "/session/$sessionId/execute/sync"),
47
Command.W3C_EXECUTE_SCRIPT_ASYNC: ("POST", "/session/$sessionId/execute/async"),
48
Command.GET_CURRENT_URL: ("GET", "/session/$sessionId/url"),
49
Command.GET_TITLE: ("GET", "/session/$sessionId/title"),
50
Command.GET_PAGE_SOURCE: ("GET", "/session/$sessionId/source"),
51
Command.SCREENSHOT: ("GET", "/session/$sessionId/screenshot"),
52
Command.ELEMENT_SCREENSHOT: ("GET", "/session/$sessionId/element/$id/screenshot"),
53
Command.FIND_ELEMENT: ("POST", "/session/$sessionId/element"),
54
Command.FIND_ELEMENTS: ("POST", "/session/$sessionId/elements"),
55
Command.W3C_GET_ACTIVE_ELEMENT: ("GET", "/session/$sessionId/element/active"),
56
Command.FIND_CHILD_ELEMENT: ("POST", "/session/$sessionId/element/$id/element"),
57
Command.FIND_CHILD_ELEMENTS: ("POST", "/session/$sessionId/element/$id/elements"),
58
Command.CLICK_ELEMENT: ("POST", "/session/$sessionId/element/$id/click"),
59
Command.CLEAR_ELEMENT: ("POST", "/session/$sessionId/element/$id/clear"),
60
Command.GET_ELEMENT_TEXT: ("GET", "/session/$sessionId/element/$id/text"),
61
Command.SEND_KEYS_TO_ELEMENT: ("POST", "/session/$sessionId/element/$id/value"),
62
Command.GET_ELEMENT_TAG_NAME: ("GET", "/session/$sessionId/element/$id/name"),
63
Command.IS_ELEMENT_SELECTED: ("GET", "/session/$sessionId/element/$id/selected"),
64
Command.IS_ELEMENT_ENABLED: ("GET", "/session/$sessionId/element/$id/enabled"),
65
Command.GET_ELEMENT_RECT: ("GET", "/session/$sessionId/element/$id/rect"),
66
Command.GET_ELEMENT_ATTRIBUTE: ("GET", "/session/$sessionId/element/$id/attribute/$name"),
67
Command.GET_ELEMENT_PROPERTY: ("GET", "/session/$sessionId/element/$id/property/$name"),
68
Command.GET_ELEMENT_ARIA_ROLE: ("GET", "/session/$sessionId/element/$id/computedrole"),
69
Command.GET_ELEMENT_ARIA_LABEL: ("GET", "/session/$sessionId/element/$id/computedlabel"),
70
Command.GET_SHADOW_ROOT: ("GET", "/session/$sessionId/element/$id/shadow"),
71
Command.FIND_ELEMENT_FROM_SHADOW_ROOT: ("POST", "/session/$sessionId/shadow/$shadowId/element"),
72
Command.FIND_ELEMENTS_FROM_SHADOW_ROOT: ("POST", "/session/$sessionId/shadow/$shadowId/elements"),
73
Command.GET_ALL_COOKIES: ("GET", "/session/$sessionId/cookie"),
74
Command.ADD_COOKIE: ("POST", "/session/$sessionId/cookie"),
75
Command.GET_COOKIE: ("GET", "/session/$sessionId/cookie/$name"),
76
Command.DELETE_ALL_COOKIES: ("DELETE", "/session/$sessionId/cookie"),
77
Command.DELETE_COOKIE: ("DELETE", "/session/$sessionId/cookie/$name"),
78
Command.SWITCH_TO_FRAME: ("POST", "/session/$sessionId/frame"),
79
Command.SWITCH_TO_PARENT_FRAME: ("POST", "/session/$sessionId/frame/parent"),
80
Command.SWITCH_TO_WINDOW: ("POST", "/session/$sessionId/window"),
81
Command.NEW_WINDOW: ("POST", "/session/$sessionId/window/new"),
82
Command.CLOSE: ("DELETE", "/session/$sessionId/window"),
83
Command.GET_ELEMENT_VALUE_OF_CSS_PROPERTY: ("GET", "/session/$sessionId/element/$id/css/$propertyName"),
84
Command.EXECUTE_ASYNC_SCRIPT: ("POST", "/session/$sessionId/execute_async"),
85
Command.SET_TIMEOUTS: ("POST", "/session/$sessionId/timeouts"),
86
Command.GET_TIMEOUTS: ("GET", "/session/$sessionId/timeouts"),
87
Command.W3C_DISMISS_ALERT: ("POST", "/session/$sessionId/alert/dismiss"),
88
Command.W3C_ACCEPT_ALERT: ("POST", "/session/$sessionId/alert/accept"),
89
Command.W3C_SET_ALERT_VALUE: ("POST", "/session/$sessionId/alert/text"),
90
Command.W3C_GET_ALERT_TEXT: ("GET", "/session/$sessionId/alert/text"),
91
Command.W3C_ACTIONS: ("POST", "/session/$sessionId/actions"),
92
Command.W3C_CLEAR_ACTIONS: ("DELETE", "/session/$sessionId/actions"),
93
Command.SET_WINDOW_RECT: ("POST", "/session/$sessionId/window/rect"),
94
Command.GET_WINDOW_RECT: ("GET", "/session/$sessionId/window/rect"),
95
Command.W3C_MAXIMIZE_WINDOW: ("POST", "/session/$sessionId/window/maximize"),
96
Command.SET_SCREEN_ORIENTATION: ("POST", "/session/$sessionId/orientation"),
97
Command.GET_SCREEN_ORIENTATION: ("GET", "/session/$sessionId/orientation"),
98
Command.GET_NETWORK_CONNECTION: ("GET", "/session/$sessionId/network_connection"),
99
Command.SET_NETWORK_CONNECTION: ("POST", "/session/$sessionId/network_connection"),
100
Command.GET_LOG: ("POST", "/session/$sessionId/se/log"),
101
Command.GET_AVAILABLE_LOG_TYPES: ("GET", "/session/$sessionId/se/log/types"),
102
Command.CURRENT_CONTEXT_HANDLE: ("GET", "/session/$sessionId/context"),
103
Command.CONTEXT_HANDLES: ("GET", "/session/$sessionId/contexts"),
104
Command.SWITCH_TO_CONTEXT: ("POST", "/session/$sessionId/context"),
105
Command.FULLSCREEN_WINDOW: ("POST", "/session/$sessionId/window/fullscreen"),
106
Command.MINIMIZE_WINDOW: ("POST", "/session/$sessionId/window/minimize"),
107
Command.PRINT_PAGE: ("POST", "/session/$sessionId/print"),
108
Command.ADD_VIRTUAL_AUTHENTICATOR: ("POST", "/session/$sessionId/webauthn/authenticator"),
109
Command.REMOVE_VIRTUAL_AUTHENTICATOR: (
110
"DELETE",
111
"/session/$sessionId/webauthn/authenticator/$authenticatorId",
112
),
113
Command.ADD_CREDENTIAL: ("POST", "/session/$sessionId/webauthn/authenticator/$authenticatorId/credential"),
114
Command.GET_CREDENTIALS: ("GET", "/session/$sessionId/webauthn/authenticator/$authenticatorId/credentials"),
115
Command.REMOVE_CREDENTIAL: (
116
"DELETE",
117
"/session/$sessionId/webauthn/authenticator/$authenticatorId/credentials/$credentialId",
118
),
119
Command.REMOVE_ALL_CREDENTIALS: (
120
"DELETE",
121
"/session/$sessionId/webauthn/authenticator/$authenticatorId/credentials",
122
),
123
Command.SET_USER_VERIFIED: ("POST", "/session/$sessionId/webauthn/authenticator/$authenticatorId/uv"),
124
Command.UPLOAD_FILE: ("POST", "/session/$sessionId/se/file"),
125
Command.GET_DOWNLOADABLE_FILES: ("GET", "/session/$sessionId/se/files"),
126
Command.DOWNLOAD_FILE: ("POST", "/session/$sessionId/se/files"),
127
Command.DELETE_DOWNLOADABLE_FILES: ("DELETE", "/session/$sessionId/se/files"),
128
Command.FIRE_SESSION_EVENT: ("POST", "/session/$sessionId/se/event"),
129
# Federated Credential Management (FedCM)
130
Command.GET_FEDCM_TITLE: ("GET", "/session/$sessionId/fedcm/gettitle"),
131
Command.GET_FEDCM_DIALOG_TYPE: ("GET", "/session/$sessionId/fedcm/getdialogtype"),
132
Command.GET_FEDCM_ACCOUNT_LIST: ("GET", "/session/$sessionId/fedcm/accountlist"),
133
Command.CLICK_FEDCM_DIALOG_BUTTON: ("POST", "/session/$sessionId/fedcm/clickdialogbutton"),
134
Command.CANCEL_FEDCM_DIALOG: ("POST", "/session/$sessionId/fedcm/canceldialog"),
135
Command.SELECT_FEDCM_ACCOUNT: ("POST", "/session/$sessionId/fedcm/selectaccount"),
136
Command.SET_FEDCM_DELAY: ("POST", "/session/$sessionId/fedcm/setdelayenabled"),
137
Command.RESET_FEDCM_COOLDOWN: ("POST", "/session/$sessionId/fedcm/resetcooldown"),
138
}
139
140
141
class RemoteConnection:
142
"""A connection with the Remote WebDriver server.
143
144
Communicates with the server using the WebDriver wire protocol:
145
https://github.com/SeleniumHQ/selenium/wiki/JsonWireProtocol
146
"""
147
148
browser_name: str | None = None
149
# Keep backward compatibility for AppiumConnection - https://github.com/SeleniumHQ/selenium/issues/14694
150
import os
151
import socket
152
153
import certifi
154
155
_timeout = socket.getdefaulttimeout()
156
_ca_certs = os.getenv("REQUESTS_CA_BUNDLE") if "REQUESTS_CA_BUNDLE" in os.environ else certifi.where()
157
_client_config: ClientConfig
158
159
system = sys.platform
160
if system == "darwin":
161
system = "mac"
162
163
# Class variables for headers
164
extra_headers = None
165
user_agent = f"selenium/{__version__} (python {system})"
166
167
@property
168
def client_config(self):
169
return self._client_config
170
171
@classmethod
172
def get_timeout(cls):
173
"""Returns timeout value in seconds for all http requests made to the Remote Connection.
174
175
Returns:
176
Timeout value in seconds for all http requests made to the
177
Remote Connection
178
"""
179
warnings.warn(
180
"get_timeout() in RemoteConnection is deprecated, get timeout from client_config instead",
181
DeprecationWarning,
182
stacklevel=2,
183
)
184
return cls._client_config.timeout
185
186
@classmethod
187
def set_timeout(cls, timeout):
188
"""Override the default timeout.
189
190
Args:
191
timeout: timeout value for http requests in seconds
192
"""
193
warnings.warn(
194
"set_timeout() in RemoteConnection is deprecated, set timeout in client_config instead",
195
DeprecationWarning,
196
stacklevel=2,
197
)
198
cls._client_config.timeout = timeout
199
200
@classmethod
201
def reset_timeout(cls):
202
"""Reset the http request timeout to socket._GLOBAL_DEFAULT_TIMEOUT."""
203
warnings.warn(
204
"reset_timeout() in RemoteConnection is deprecated, use reset_timeout() in client_config instead",
205
DeprecationWarning,
206
stacklevel=2,
207
)
208
cls._client_config.reset_timeout()
209
210
@classmethod
211
def get_certificate_bundle_path(cls):
212
"""Returns paths of the .pem encoded certificate to verify connection to command executor.
213
214
Returns:
215
Paths of the .pem encoded certificate to verify connection to
216
command executor. Defaults to certifi.where() or
217
REQUESTS_CA_BUNDLE env variable if set.
218
"""
219
warnings.warn(
220
"get_certificate_bundle_path() in RemoteConnection is deprecated, get ca_certs from client_config instead",
221
DeprecationWarning,
222
stacklevel=2,
223
)
224
return cls._client_config.ca_certs
225
226
@classmethod
227
def set_certificate_bundle_path(cls, path):
228
"""Set the path to the certificate bundle for verifying command executor connection.
229
230
Can also be set to None to disable certificate validation.
231
232
Args:
233
path: path of a .pem encoded certificate chain.
234
"""
235
warnings.warn(
236
"set_certificate_bundle_path() in RemoteConnection is deprecated, set ca_certs in client_config instead",
237
DeprecationWarning,
238
stacklevel=2,
239
)
240
cls._client_config.ca_certs = path
241
242
@classmethod
243
def get_remote_connection_headers(cls, parsed_url, keep_alive=False):
244
"""Get headers for remote request.
245
246
Args:
247
parsed_url: The parsed url
248
keep_alive: Is this a keep-alive connection (default: False)
249
"""
250
headers = {
251
"Accept": "application/json",
252
"Content-Type": "application/json;charset=UTF-8",
253
"User-Agent": cls.user_agent,
254
}
255
256
if parsed_url.username:
257
warnings.warn(
258
"Embedding username and password in URL could be insecure, use ClientConfig instead", stacklevel=2
259
)
260
base64string = b64encode(f"{parsed_url.username}:{parsed_url.password}".encode())
261
headers.update({"Authorization": f"Basic {base64string.decode()}"})
262
263
if keep_alive:
264
headers.update({"Connection": "keep-alive"})
265
266
if cls.extra_headers:
267
headers.update(cls.extra_headers)
268
269
return headers
270
271
def _identify_http_proxy_auth(self):
272
parsed_url = urlparse(self._proxy_url)
273
if parsed_url.username and parsed_url.password:
274
return True
275
276
def _separate_http_proxy_auth(self):
277
parsed_url = urlparse(self._proxy_url)
278
proxy_without_auth = f"{parsed_url.scheme}://{parsed_url.hostname}:{parsed_url.port}"
279
auth = f"{parsed_url.username}:{parsed_url.password}"
280
return proxy_without_auth, auth
281
282
def _get_connection_manager(self):
283
pool_manager_init_args = {"timeout": self._client_config.timeout}
284
pool_manager_init_args.update(
285
self._client_config.init_args_for_pool_manager.get("init_args_for_pool_manager", {})
286
)
287
288
if self._client_config.ignore_certificates:
289
pool_manager_init_args["cert_reqs"] = "CERT_NONE"
290
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
291
elif self._client_config.ca_certs:
292
pool_manager_init_args["cert_reqs"] = "CERT_REQUIRED"
293
pool_manager_init_args["ca_certs"] = self._client_config.ca_certs
294
295
if self._proxy_url:
296
if self._proxy_url.lower().startswith("sock"):
297
from urllib3.contrib.socks import SOCKSProxyManager
298
299
return SOCKSProxyManager(self._proxy_url, **pool_manager_init_args)
300
if self._identify_http_proxy_auth():
301
self._proxy_url, self._basic_proxy_auth = self._separate_http_proxy_auth()
302
pool_manager_init_args["proxy_headers"] = urllib3.make_headers(
303
proxy_basic_auth=unquote(self._basic_proxy_auth)
304
)
305
return urllib3.ProxyManager(self._proxy_url, **pool_manager_init_args)
306
307
return urllib3.PoolManager(**pool_manager_init_args)
308
309
def __init__(
310
self,
311
remote_server_addr: str | None = None,
312
keep_alive: bool = True,
313
ignore_proxy: bool = False,
314
ignore_certificates: bool | None = False,
315
init_args_for_pool_manager: dict | None = None,
316
client_config: ClientConfig | None = None,
317
):
318
if client_config:
319
self._client_config = client_config
320
elif remote_server_addr:
321
self._client_config = ClientConfig(
322
remote_server_addr=remote_server_addr,
323
keep_alive=keep_alive,
324
ignore_certificates=ignore_certificates,
325
init_args_for_pool_manager=init_args_for_pool_manager,
326
)
327
else:
328
raise WebDriverException("Must provide either 'remote_server_addr' or 'client_config'")
329
330
# Keep backward compatibility for AppiumConnection - https://github.com/SeleniumHQ/selenium/issues/14694
331
RemoteConnection._timeout = self._client_config.timeout
332
RemoteConnection._ca_certs = self._client_config.ca_certs
333
RemoteConnection._client_config = self._client_config
334
RemoteConnection.extra_headers = self._client_config.extra_headers or RemoteConnection.extra_headers
335
RemoteConnection.user_agent = self._client_config.user_agent or RemoteConnection.user_agent
336
337
if remote_server_addr:
338
warnings.warn(
339
"setting remote_server_addr in RemoteConnection() is deprecated, set in client_config instead",
340
DeprecationWarning,
341
stacklevel=2,
342
)
343
344
if not keep_alive:
345
warnings.warn(
346
"setting keep_alive in RemoteConnection() is deprecated, set in client_config instead",
347
DeprecationWarning,
348
stacklevel=2,
349
)
350
351
if ignore_certificates:
352
warnings.warn(
353
"setting ignore_certificates in RemoteConnection() is deprecated, set in client_config instead",
354
DeprecationWarning,
355
stacklevel=2,
356
)
357
358
if init_args_for_pool_manager:
359
warnings.warn(
360
"setting init_args_for_pool_manager in RemoteConnection() is deprecated, set in client_config instead",
361
DeprecationWarning,
362
stacklevel=2,
363
)
364
365
if ignore_proxy:
366
self._proxy_url = None
367
else:
368
self._proxy_url = self._client_config.get_proxy_url()
369
370
if self._client_config.keep_alive:
371
self._conn = self._get_connection_manager()
372
self._commands = remote_commands
373
374
extra_commands: dict[str, str] = {}
375
376
def add_command(self, name, method, url):
377
"""Register a new command."""
378
self._commands[name] = (method, url)
379
380
def get_command(self, name: str):
381
"""Retrieve a command if it exists."""
382
return self._commands.get(name)
383
384
def execute(self, command, params):
385
"""Send a command to the remote server.
386
387
Any path substitutions required for the URL mapped to the command should be
388
included in the command parameters.
389
390
Args:
391
command: A string specifying the command to execute.
392
params: A dictionary of named parameters to send with the command as
393
its JSON payload.
394
"""
395
command_info = self._commands.get(command) or self.extra_commands.get(command)
396
assert command_info is not None, f"Unrecognised command {command}"
397
path_string = command_info[1]
398
path = string.Template(path_string).substitute(params)
399
substitute_params = {word[1:] for word in path_string.split("/") if word.startswith("$")} # remove dollar sign
400
if isinstance(params, dict) and substitute_params:
401
for word in substitute_params:
402
del params[word]
403
data = utils.dump_json(params)
404
url = f"{self._client_config.remote_server_addr}{path}"
405
trimmed = self._trim_large_entries(params)
406
LOGGER.debug("%s %s %s", command_info[0], url, str(trimmed))
407
return self._request(command_info[0], url, body=data)
408
409
def _request(self, method, url, body=None) -> dict:
410
"""Send an HTTP request to the remote server.
411
412
Args:
413
method: A string for the HTTP method to send the request with.
414
url: A string for the URL to send the request to.
415
body: A string for request body. Ignored unless method is POST or PUT.
416
417
Returns:
418
A dictionary with the server's parsed JSON response.
419
"""
420
parsed_url = parse.urlparse(url)
421
headers = self.get_remote_connection_headers(parsed_url, self._client_config.keep_alive)
422
auth_header = self._client_config.get_auth_header()
423
424
if auth_header:
425
headers.update(auth_header)
426
427
if body and method not in ("POST", "PUT"):
428
body = None
429
430
if self._client_config.keep_alive:
431
response = self._conn.request(method, url, body=body, headers=headers, timeout=self._client_config.timeout)
432
statuscode = response.status
433
else:
434
conn = self._get_connection_manager()
435
with conn as http:
436
response = http.request(method, url, body=body, headers=headers, timeout=self._client_config.timeout)
437
statuscode = response.status
438
data = response.data.decode("UTF-8")
439
LOGGER.debug("Remote response: status=%s | data=%s | headers=%s", response.status, data, response.headers)
440
try:
441
if 300 <= statuscode < 304:
442
return self._request("GET", response.headers.get("location", None))
443
if statuscode == 401:
444
return {"status": statuscode, "value": "Authorization Required"}
445
if statuscode >= 400:
446
return {"status": statuscode, "value": response.reason if not data else data.strip()}
447
content_type = []
448
if response.headers.get("Content-Type", None):
449
content_type = response.headers.get("Content-Type", None).split(";")
450
if not any([x.startswith("image/png") for x in content_type]):
451
try:
452
data = utils.load_json(data.strip())
453
except ValueError:
454
if 199 < statuscode < 300:
455
status = ErrorCode.SUCCESS
456
else:
457
status = ErrorCode.UNKNOWN_ERROR # type: ignore
458
return {"status": status, "value": data.strip()}
459
460
# Some drivers incorrectly return a response
461
# with no 'value' field when they should return null.
462
if "value" not in data:
463
data["value"] = None
464
return data
465
data = {"status": 0, "value": data}
466
return data
467
finally:
468
LOGGER.debug("Finished Request")
469
response.close()
470
471
def close(self):
472
"""Clean up resources when finished with the remote_connection."""
473
if hasattr(self, "_conn"):
474
self._conn.clear()
475
476
def _trim_large_entries(self, input_dict, max_length=100) -> dict | str:
477
"""Truncate string values in a dictionary if they exceed max_length.
478
479
Args:
480
input_dict: Dictionary with potentially large values
481
max_length: Maximum allowed length of string values
482
483
Returns:
484
Dictionary with truncated string values
485
"""
486
output_dictionary = {}
487
for key, value in input_dict.items():
488
if isinstance(value, dict):
489
output_dictionary[key] = self._trim_large_entries(value, max_length)
490
elif isinstance(value, str) and len(value) > max_length:
491
output_dictionary[key] = value[:max_length] + "..."
492
else:
493
output_dictionary[key] = value
494
495
return output_dictionary
496
497