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