From 25877fd95a145fef8248e37ae9dbe7b6364caab2 Mon Sep 17 00:00:00 2001 From: jdholtz Date: Wed, 16 Aug 2023 07:42:07 -0500 Subject: [PATCH] Add support for Chromedriver versions 115+ --- undetected_chromedriver/patcher.py | 114 +++++++++++++++++++++++------ 1 file changed, 90 insertions(+), 24 deletions(-) diff --git a/undetected_chromedriver/patcher.py b/undetected_chromedriver/patcher.py index 4c062f6..08c6434 100644 --- a/undetected_chromedriver/patcher.py +++ b/undetected_chromedriver/patcher.py @@ -3,9 +3,11 @@ from distutils.version import LooseVersion import io +import json import logging import os import pathlib +import platform import random import re import shutil @@ -24,21 +26,9 @@ IS_POSIX = sys.platform.startswith(("darwin", "cygwin", "linux", "linux2")) class Patcher(object): lock = Lock() - url_repo = "https://chromedriver.storage.googleapis.com" - zip_name = "chromedriver_%s.zip" exe_name = "chromedriver%s" platform = sys.platform - if platform.endswith("win32"): - zip_name %= "win32" - exe_name %= ".exe" - if platform.endswith(("linux", "linux2")): - zip_name %= "linux64" - exe_name %= "" - if platform.endswith("darwin"): - zip_name %= "mac64" - exe_name %= "" - if platform.endswith("win32"): d = "~/appdata/roaming/undetected_chromedriver" elif "LAMBDA_TASK_ROOT" in os.environ: @@ -97,9 +87,53 @@ class Patcher(object): self._custom_exe_path = True self.executable_path = executable_path + # Set the correct repository to download the Chromedriver from + self.is_old_chromedriver = version_main and version_main <= 114 + if self.is_old_chromedriver: + self.url_repo = "https://chromedriver.storage.googleapis.com" + else: + self.url_repo = "https://googlechromelabs.github.io/chrome-for-testing" + self.version_main = version_main self.version_full = None + self._set_platform_name() + + def _set_platform_name(self): + """ + Set the platform and exe name based on the platform undetected_chromedriver is running on + in order to download the correct chromedriver. + """ + if self.platform.endswith("win32"): + self.platform_name = "win32" + self.exe_name %= ".exe" + if self.platform.endswith(("linux", "linux2")): + self.platform_name = "linux64" + self.exe_name %= "" + if self.platform.endswith("darwin"): + self.platform_name = self._get_mac_platform_name() + self.exe_name %= "" + + def _get_mac_platform_name(self): + """ + The Mac platform name changes based on the architecture and Chromedriver version desired + """ + platform_name = "mac" + is_arm_arch = any(["aarch64", "arm"] in platform.machine()) + + if self.is_old_chromedriver: + if is_arm_arch: + platform_name += "_arm64" + else: + platform_name += "64" + else: + if is_arm_arch: + platform_name += "-arm64" + else: + platform_name += "-x64" + + return platform_name + def auto(self, executable_path=None, force=False, version_main=None, _=None): """ @@ -217,12 +251,32 @@ class Patcher(object): :return: version string :rtype: LooseVersion """ - path = "/latest_release" - if self.version_main: - path += f"_{self.version_main}" - path = path.upper() + # Endpoint for old versions of Chromedriver (114 and below) + if self.is_old_chromedriver: + path = f"/latest_release_{self.version_main}" + path = path.upper() + logger.debug("getting release number from %s" % path) + return LooseVersion(urlopen(self.url_repo + path).read().decode()) + + # Endpoint for new versions of Chromedriver (115+) + if not self.version_main: + # Fetch the latest version + path = "/last-known-good-versions-with-downloads.json" + logger.debug("getting release number from %s" % path) + with urlopen(self.url_repo + path) as conn: + response = conn.read().decode() + + last_versions = json.loads(response) + return LooseVersion(last_versions["channels"]["Stable"]["version"]) + + # Fetch the latest minor version of the major version provided + path = "/latest-versions-per-milestone-with-downloads.json" logger.debug("getting release number from %s" % path) - return LooseVersion(urlopen(self.url_repo + path).read().decode()) + with urlopen(self.url_repo + path) as conn: + response = conn.read().decode() + + major_versions = json.loads(response) + return LooseVersion(major_versions["milestones"][str(self.version_main)]["version"]) def parse_exe_version(self): with io.open(self.executable_path, "rb") as f: @@ -237,10 +291,16 @@ class Patcher(object): :return: path to downloaded file """ - u = "%s/%s/%s" % (self.url_repo, self.version_full.vstring, self.zip_name) - logger.debug("downloading from %s" % u) - # return urlretrieve(u, filename=self.data_path)[0] - return urlretrieve(u)[0] + zip_name = f"chromedriver_{self.platform_name}.zip" + if self.is_old_chromedriver: + download_url = "%s/%s/%s" % (self.url_repo, self.version_full.vstring, zip_name) + else: + zip_name = zip_name.replace("_", "-", 1) + download_url = "https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/%s/%s/%s" + download_url %= (self.version_full.vstring, self.platform_name, zip_name) + + logger.debug("downloading from %s" % download_url) + return urlretrieve(download_url)[0] def unzip_package(self, fp): """ @@ -248,6 +308,12 @@ class Patcher(object): :return: path to unpacked executable """ + exe_path = self.exe_name + if not self.is_old_chromedriver: + # The new chromedriver unzips into its own folder + zip_name = f"chromedriver-{self.platform_name}" + exe_path = os.path.join(zip_name, self.exe_name) + logger.debug("unzipping %s" % fp) try: os.unlink(self.zip_path) @@ -256,10 +322,10 @@ class Patcher(object): os.makedirs(self.zip_path, mode=0o755, exist_ok=True) with zipfile.ZipFile(fp, mode="r") as zf: - zf.extract(self.exe_name, self.zip_path) - os.rename(os.path.join(self.zip_path, self.exe_name), self.executable_path) + zf.extract(exe_path, self.zip_path) + os.rename(os.path.join(self.zip_path, exe_path), self.executable_path) os.remove(fp) - os.rmdir(self.zip_path) + shutil.rmtree(self.zip_path) os.chmod(self.executable_path, 0o755) return self.executable_path