Updated script that can be controled by Nodejs web app

This commit is contained in:
mac OS
2024-11-25 12:24:18 +07:00
parent c440eda1f4
commit 8b0ab2bd3a
8662 changed files with 1803808 additions and 34 deletions

View File

@ -0,0 +1,16 @@
# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.

View File

@ -0,0 +1,153 @@
# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
import base64
import os
from typing import BinaryIO
from typing import Dict
from typing import List
from typing import Optional
from typing import Union
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
from selenium.webdriver.common.options import ArgOptions
class ChromiumOptions(ArgOptions):
KEY = "goog:chromeOptions"
def __init__(self) -> None:
super().__init__()
self._binary_location: str = ""
self._extension_files: List[str] = []
self._extensions: List[str] = []
self._experimental_options: Dict[str, Union[str, int, dict, List[str]]] = {}
self._debugger_address: Optional[str] = None
@property
def binary_location(self) -> str:
""":Returns: The location of the binary, otherwise an empty string."""
return self._binary_location
@binary_location.setter
def binary_location(self, value: str) -> None:
"""Allows you to set where the chromium binary lives.
:Args:
- value: path to the Chromium binary
"""
if not isinstance(value, str):
raise TypeError(self.BINARY_LOCATION_ERROR)
self._binary_location = value
@property
def debugger_address(self) -> Optional[str]:
""":Returns: The address of the remote devtools instance."""
return self._debugger_address
@debugger_address.setter
def debugger_address(self, value: str) -> None:
"""Allows you to set the address of the remote devtools instance that
the ChromeDriver instance will try to connect to during an active wait.
:Args:
- value: address of remote devtools instance if any (hostname[:port])
"""
if not isinstance(value, str):
raise TypeError("Debugger Address must be a string")
self._debugger_address = value
@property
def extensions(self) -> List[str]:
""":Returns: A list of encoded extensions that will be loaded."""
def _decode(file_data: BinaryIO) -> str:
# Should not use base64.encodestring() which inserts newlines every
# 76 characters (per RFC 1521). Chromedriver has to remove those
# unnecessary newlines before decoding, causing performance hit.
return base64.b64encode(file_data.read()).decode("utf-8")
encoded_extensions = []
for extension in self._extension_files:
with open(extension, "rb") as f:
encoded_extensions.append(_decode(f))
return encoded_extensions + self._extensions
def add_extension(self, extension: str) -> None:
"""Adds the path to the extension to a list that will be used to
extract it to the ChromeDriver.
:Args:
- extension: path to the \\*.crx file
"""
if extension:
extension_to_add = os.path.abspath(os.path.expanduser(extension))
if os.path.exists(extension_to_add):
self._extension_files.append(extension_to_add)
else:
raise OSError("Path to the extension doesn't exist")
else:
raise ValueError("argument can not be null")
def add_encoded_extension(self, extension: str) -> None:
"""Adds Base64 encoded string with extension data to a list that will
be used to extract it to the ChromeDriver.
:Args:
- extension: Base64 encoded string with extension data
"""
if extension:
self._extensions.append(extension)
else:
raise ValueError("argument can not be null")
@property
def experimental_options(self) -> dict:
""":Returns: A dictionary of experimental options for chromium."""
return self._experimental_options
def add_experimental_option(self, name: str, value: Union[str, int, dict, List[str]]) -> None:
"""Adds an experimental option which is passed to chromium.
:Args:
name: The experimental option name.
value: The option value.
"""
self._experimental_options[name] = value
def to_capabilities(self) -> dict:
"""Creates a capabilities with all the options that have been set
:Returns: A dictionary with everything."""
caps = self._caps
chrome_options = self.experimental_options.copy()
if self.mobile_options:
chrome_options.update(self.mobile_options)
chrome_options["extensions"] = self.extensions
if self.binary_location:
chrome_options["binary"] = self.binary_location
chrome_options["args"] = self._arguments
if self.debugger_address:
chrome_options["debuggerAddress"] = self.debugger_address
caps[self.KEY] = chrome_options
return caps
@property
def default_capabilities(self) -> dict:
return DesiredCapabilities.CHROME.copy()

View File

@ -0,0 +1,60 @@
# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from typing import Optional
from selenium.webdriver.remote.client_config import ClientConfig
from selenium.webdriver.remote.remote_connection import RemoteConnection
class ChromiumRemoteConnection(RemoteConnection):
def __init__(
self,
remote_server_addr: str,
vendor_prefix: str,
browser_name: str,
keep_alive: bool = True,
ignore_proxy: Optional[bool] = False,
client_config: Optional[ClientConfig] = None,
) -> None:
client_config = client_config or ClientConfig(
remote_server_addr=remote_server_addr, keep_alive=keep_alive, timeout=120
)
super().__init__(
ignore_proxy=ignore_proxy,
client_config=client_config,
)
self.browser_name = browser_name
commands = self._remote_commands(vendor_prefix)
for key, value in commands.items():
self._commands[key] = value
def _remote_commands(self, vendor_prefix):
remote_commands = {
"launchApp": ("POST", "/session/$sessionId/chromium/launch_app"),
"setPermissions": ("POST", "/session/$sessionId/permissions"),
"setNetworkConditions": ("POST", "/session/$sessionId/chromium/network_conditions"),
"getNetworkConditions": ("GET", "/session/$sessionId/chromium/network_conditions"),
"deleteNetworkConditions": ("DELETE", "/session/$sessionId/chromium/network_conditions"),
"executeCdpCommand": ("POST", f"/session/$sessionId/{vendor_prefix}/cdp/execute"),
"getSinks": ("GET", f"/session/$sessionId/{vendor_prefix}/cast/get_sinks"),
"getIssueMessage": ("GET", f"/session/$sessionId/{vendor_prefix}/cast/get_issue_message"),
"setSinkToUse": ("POST", f"/session/$sessionId/{vendor_prefix}/cast/set_sink_to_use"),
"startDesktopMirroring": ("POST", f"/session/$sessionId/{vendor_prefix}/cast/start_desktop_mirroring"),
"startTabMirroring": ("POST", f"/session/$sessionId/{vendor_prefix}/cast/start_tab_mirroring"),
"stopCasting": ("POST", f"/session/$sessionId/{vendor_prefix}/cast/stop_casting"),
}
return remote_commands

View File

@ -0,0 +1,66 @@
# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
import typing
from io import IOBase
from selenium.types import SubprocessStdAlias
from selenium.webdriver.common import service
class ChromiumService(service.Service):
"""A Service class that is responsible for the starting and stopping the
WebDriver instance of the ChromiumDriver.
:param executable_path: install path of the executable.
:param port: Port for the service to run on, defaults to 0 where the operating system will decide.
:param service_args: (Optional) List of args to be passed to the subprocess when launching the executable.
:param log_output: (Optional) int representation of STDOUT/DEVNULL, any IO instance or String path to file.
:param env: (Optional) Mapping of environment variables for the new process, defaults to `os.environ`.
"""
def __init__(
self,
executable_path: str = None,
port: int = 0,
service_args: typing.Optional[typing.List[str]] = None,
log_output: SubprocessStdAlias = None,
env: typing.Optional[typing.Mapping[str, str]] = None,
driver_path_env_key: str = None,
**kwargs,
) -> None:
self.service_args = service_args or []
driver_path_env_key = driver_path_env_key or "SE_CHROMEDRIVER"
if isinstance(log_output, str):
self.service_args.append(f"--log-path={log_output}")
self.log_output: typing.Optional[IOBase] = None
elif isinstance(log_output, IOBase):
self.log_output = log_output
else:
self.log_output = log_output
super().__init__(
executable_path=executable_path,
port=port,
env=env,
log_output=self.log_output,
driver_path_env_key=driver_path_env_key,
**kwargs,
)
def command_line_args(self) -> typing.List[str]:
return [f"--port={self.port}"] + self.service_args

View File

@ -0,0 +1,193 @@
# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from selenium.webdriver.chromium.remote_connection import ChromiumRemoteConnection
from selenium.webdriver.common.driver_finder import DriverFinder
from selenium.webdriver.common.options import ArgOptions
from selenium.webdriver.common.service import Service
from selenium.webdriver.remote.webdriver import WebDriver as RemoteWebDriver
class ChromiumDriver(RemoteWebDriver):
"""Controls the WebDriver instance of ChromiumDriver and allows you to
drive the browser."""
def __init__(
self,
browser_name: str = None,
vendor_prefix: str = None,
options: ArgOptions = ArgOptions(),
service: Service = None,
keep_alive: bool = True,
) -> None:
"""Creates a new WebDriver instance of the ChromiumDriver. Starts the
service and then creates new WebDriver instance of ChromiumDriver.
:Args:
- browser_name - Browser name used when matching capabilities.
- vendor_prefix - Company prefix to apply to vendor-specific WebDriver extension commands.
- options - this takes an instance of ChromiumOptions
- service - Service object for handling the browser driver if you need to pass extra details
- keep_alive - Whether to configure ChromiumRemoteConnection to use HTTP keep-alive.
"""
self.service = service
finder = DriverFinder(self.service, options)
if finder.get_browser_path():
options.binary_location = finder.get_browser_path()
options.browser_version = None
self.service.path = self.service.env_path() or finder.get_driver_path()
self.service.start()
executor = ChromiumRemoteConnection(
remote_server_addr=self.service.service_url,
browser_name=browser_name,
vendor_prefix=vendor_prefix,
keep_alive=keep_alive,
ignore_proxy=options._ignore_local_proxy,
)
try:
super().__init__(command_executor=executor, options=options)
except Exception:
self.quit()
raise
self._is_remote = False
def launch_app(self, id):
"""Launches Chromium app specified by id."""
return self.execute("launchApp", {"id": id})
def get_network_conditions(self):
"""Gets Chromium network emulation settings.
:Returns: A dict. For example: {'latency': 4,
'download_throughput': 2, 'upload_throughput': 2, 'offline':
False}
"""
return self.execute("getNetworkConditions")["value"]
def set_network_conditions(self, **network_conditions) -> None:
"""Sets Chromium network emulation settings.
:Args:
- network_conditions: A dict with conditions specification.
:Usage:
::
driver.set_network_conditions(
offline=False,
latency=5, # additional latency (ms)
download_throughput=500 * 1024, # maximal throughput
upload_throughput=500 * 1024) # maximal throughput
Note: 'throughput' can be used to set both (for download and upload).
"""
self.execute("setNetworkConditions", {"network_conditions": network_conditions})
def delete_network_conditions(self) -> None:
"""Resets Chromium network emulation settings."""
self.execute("deleteNetworkConditions")
def set_permissions(self, name: str, value: str) -> None:
"""Sets Applicable Permission.
:Args:
- name: The item to set the permission on.
- value: The value to set on the item
:Usage:
::
driver.set_permissions('clipboard-read', 'denied')
"""
self.execute("setPermissions", {"descriptor": {"name": name}, "state": value})
def execute_cdp_cmd(self, cmd: str, cmd_args: dict):
"""Execute Chrome Devtools Protocol command and get returned result The
command and command args should follow chrome devtools protocol
domains/commands, refer to link
https://chromedevtools.github.io/devtools-protocol/
:Args:
- cmd: A str, command name
- cmd_args: A dict, command args. empty dict {} if there is no command args
:Usage:
::
driver.execute_cdp_cmd('Network.getResponseBody', {'requestId': requestId})
:Returns:
A dict, empty dict {} if there is no result to return.
For example to getResponseBody:
{'base64Encoded': False, 'body': 'response body string'}
"""
return self.execute("executeCdpCommand", {"cmd": cmd, "params": cmd_args})["value"]
def get_sinks(self) -> list:
""":Returns: A list of sinks available for Cast."""
return self.execute("getSinks")["value"]
def get_issue_message(self):
""":Returns: An error message when there is any issue in a Cast
session."""
return self.execute("getIssueMessage")["value"]
def set_sink_to_use(self, sink_name: str) -> dict:
"""Sets a specific sink, using its name, as a Cast session receiver
target.
:Args:
- sink_name: Name of the sink to use as the target.
"""
return self.execute("setSinkToUse", {"sinkName": sink_name})
def start_desktop_mirroring(self, sink_name: str) -> dict:
"""Starts a desktop mirroring session on a specific receiver target.
:Args:
- sink_name: Name of the sink to use as the target.
"""
return self.execute("startDesktopMirroring", {"sinkName": sink_name})
def start_tab_mirroring(self, sink_name: str) -> dict:
"""Starts a tab mirroring session on a specific receiver target.
:Args:
- sink_name: Name of the sink to use as the target.
"""
return self.execute("startTabMirroring", {"sinkName": sink_name})
def stop_casting(self, sink_name: str) -> dict:
"""Stops the existing Cast session on a specific receiver target.
:Args:
- sink_name: Name of the sink to stop the Cast session.
"""
return self.execute("stopCasting", {"sinkName": sink_name})
def quit(self) -> None:
"""Closes the browser and shuts down the ChromiumDriver executable."""
try:
super().quit()
except Exception:
# We don't care about the message because something probably has gone wrong
pass
finally:
self.service.stop()