# import asyncio # import hashlib # import json # import logging # import subprocess # import zipfile # from pathlib import Path # import aiohttp # import anyio # logger = logging.getLogger(__name__) # def get_extension_id(unpacked_path: str | Path) -> str | None: # manifest_path = Path(unpacked_path) / 'manifest.json' # if not manifest_path.exists(): # return None # # chrome uses a SHA256 hash of the unpacked extension directory path to compute a dynamic id for unpacked extensions # hash_obj = hashlib.sha256() # hash_obj.update(str(unpacked_path).encode('utf-8')) # detected_extension_id = ''.join(chr(int(h, 16) + ord('a')) for h in hash_obj.hexdigest()[:32]) # return detected_extension_id # async def install_extension(extension: dict) -> bool: # manifest_path = Path(extension['unpacked_path']) / 'manifest.json' # crx_path = Path(extension['crx_path']) # # Download extensions using: # # curl -fsSL 'https://clients2.google.com/service/update2/crx?response=redirect&prodversion=1230&acceptformat=crx3&x=id%3D${EXTENSION_ID}%26uc' > extensionname.crx # # unzip -d extensionname extensionname.crx # if not manifest_path.exists() and not crx_path.exists(): # logger.info(f'[🛠️] Downloading missing extension {extension["name"]} {extension["webstore_id"]} -> {crx_path}') # # Download crx file from ext.crx_url -> ext.crx_path # async with aiohttp.ClientSession() as session: # async with session.get(extension['crx_url']) as response: # if response.headers.get('content-length') and response.content: # async with anyio.open(crx_path, 'wb') as f: # await f.write(await response.read()) # else: # logger.warning(f'[⚠️] Failed to download extension {extension["name"]} {extension["webstore_id"]}') # return False # # Unzip crx file from ext.crx_url -> ext.unpacked_path # unpacked_path = Path(extension['unpacked_path']) # unpacked_path.mkdir(parents=True, exist_ok=True) # try: # # Try system unzip first # result = subprocess.run(['/usr/bin/unzip', str(crx_path), '-d', str(unpacked_path)], capture_output=True, text=True) # stdout, stderr = result.stdout, result.stderr # except Exception as err1: # try: # # Fallback to Python's zipfile # with zipfile.ZipFile(crx_path) as zf: # zf.extractall(unpacked_path) # stdout, stderr = '', '' # except Exception as err2: # logger.error(f'[❌] Failed to install {crx_path}: could not unzip crx', exc_info=(err1, err2)) # return False # if not manifest_path.exists(): # logger.error(f'[❌] Failed to install {crx_path}: could not find manifest.json in unpacked_path', stdout, stderr) # return False # return True # async def load_or_install_extension(ext: dict) -> dict: # if not (ext.get('webstore_id') or ext.get('unpacked_path')): # raise ValueError('Extension must have either webstore_id or unpacked_path') # # Set statically computable extension metadata # ext['webstore_id'] = ext.get('webstore_id') or ext.get('id') # ext['name'] = ext.get('name') or ext['webstore_id'] # ext['webstore_url'] = ext.get('webstore_url') or f'https://chromewebstore.google.com/detail/{ext["webstore_id"]}' # ext['crx_url'] = ( # ext.get('crx_url') # or f'https://clients2.google.com/service/update2/crx?response=redirect&prodversion=1230&acceptformat=crx3&x=id%3D{ext["webstore_id"]}%26uc' # ) # ext['crx_path'] = ext.get('crx_path') or str(Path(CHROME_EXTENSIONS_DIR) / f'{ext["webstore_id"]}__{ext["name"]}.crx') # ext['unpacked_path'] = ext.get('unpacked_path') or str(Path(CHROME_EXTENSIONS_DIR) / f'{ext["webstore_id"]}__{ext["name"]}') # manifest_path = Path(ext['unpacked_path']) / 'manifest.json' # def read_manifest(): # with open(manifest_path) as f: # return json.load(f) # def read_version(): # return manifest_path.exists() and read_manifest().get('version') # ext['read_manifest'] = read_manifest # ext['read_version'] = read_version # # if extension is not installed, download and unpack it # if not ext['read_version'](): # await install_extension(ext) # # autodetect id from filesystem path (unpacked extensions dont have stable IDs) # ext['id'] = get_extension_id(ext['unpacked_path']) # ext['version'] = ext['read_version']() # if not ext['version']: # logger.warning(f'[❌] Unable to detect ID and version of installed extension {pretty_path(ext["unpacked_path"])}') # else: # logger.info(f'[➕] Installed extension {ext["name"]} ({ext["version"]})...'.ljust(82) + pretty_path(ext['unpacked_path'])) # return ext # async def is_target_extension(target): # target_type = None # target_ctx = None # target_url = None # try: # target_type = await target.type # target_ctx = await target.worker() or await target.page() or None # target_url = await target.url or (await target_ctx.url if target_ctx else None) # except Exception as err: # if 'No target with given id found' in str(err): # # because this runs on initial browser startup, we sometimes race with closing the initial # # new tab page. it will throw a harmless error if we try to check a target that's already closed, # # ignore it and return null since that page is definitely not an extension's bg page anyway # target_type = 'closed' # target_ctx = None # target_url = 'about:closed' # else: # raise err # target_is_bg = target_type in ['service_worker', 'background_page'] # target_is_extension = target_url and target_url.startswith('chrome-extension://') # extension_id = target_url.split('://')[1].split('/')[0] if target_is_extension else None # manifest_version = '3' if target_type == 'service_worker' else '2' # return { # 'target_type': target_type, # 'target_ctx': target_ctx, # 'target_url': target_url, # 'target_is_bg': target_is_bg, # 'target_is_extension': target_is_extension, # 'extension_id': extension_id, # 'manifest_version': manifest_version, # } # async def load_extension_from_target(extensions, target): # extension_info = await is_target_extension(target) # target_is_bg = extension_info['target_is_bg'] # target_is_extension = extension_info['target_is_extension'] # target_type = extension_info['target_type'] # target_ctx = extension_info['target_ctx'] # target_url = extension_info['target_url'] # extension_id = extension_info['extension_id'] # manifest_version = extension_info['manifest_version'] # if not (target_is_bg and extension_id and target_ctx): # return None # manifest = await target_ctx.evaluate('() => chrome.runtime.getManifest()') # name = manifest.get('name') # version = manifest.get('version') # homepage_url = manifest.get('homepage_url') # options_page = manifest.get('options_page') # options_ui = manifest.get('options_ui', {}) # if not version or not extension_id: # return None # options_url = await target_ctx.evaluate( # '(options_page) => chrome.runtime.getURL(options_page)', # options_page or options_ui.get('page') or 'options.html', # ) # commands = await target_ctx.evaluate(""" # async () => { # return await new Promise((resolve, reject) => { # if (chrome.commands) # chrome.commands.getAll(resolve) # else # resolve({}) # }) # } # """) # # logger.debug(f"[+] Found Manifest V{manifest_version} Extension: {extension_id} {name} {target_url} {len(commands)}") # async def dispatch_eval(*args): # return await target_ctx.evaluate(*args) # async def dispatch_popup(): # return await target_ctx.evaluate( # "() => chrome.action?.openPopup() || chrome.tabs.create({url: chrome.runtime.getURL('popup.html')})" # ) # if manifest_version == '3': # async def dispatch_action(tab=None): # # https://developer.chrome.com/docs/extensions/reference/api/action#event-onClicked # return await target_ctx.evaluate( # """ # async (tab) => { # tab = tab || (await new Promise((resolve) => # chrome.tabs.query({currentWindow: true, active: true}, ([tab]) => resolve(tab)))) # return await chrome.action.onClicked.dispatch(tab) # } # """, # tab, # ) # async def dispatch_message(message, options=None): # # https://developer.chrome.com/docs/extensions/reference/api/runtime # return await target_ctx.evaluate( # """ # async (extension_id, message, options) => { # return await chrome.runtime.sendMessage(extension_id, message, options) # } # """, # extension_id, # message, # options, # ) # async def dispatch_command(command, tab=None): # # https://developer.chrome.com/docs/extensions/reference/api/commands#event-onCommand # return await target_ctx.evaluate( # """ # async (command, tab) => { # return await chrome.commands.onCommand.dispatch(command, tab) # } # """, # command, # tab, # ) # elif manifest_version == '2': # async def dispatch_action(tab=None): # # https://developer.chrome.com/docs/extensions/mv2/reference/browserAction#event-onClicked # return await target_ctx.evaluate( # """ # async (tab) => { # tab = tab || (await new Promise((resolve) => # chrome.tabs.query({currentWindow: true, active: true}, ([tab]) => resolve(tab)))) # return await chrome.browserAction.onClicked.dispatch(tab) # } # """, # tab, # ) # async def dispatch_message(message, options=None): # # https://developer.chrome.com/docs/extensions/mv2/reference/runtime#method-sendMessage # return await target_ctx.evaluate( # """ # async (extension_id, message, options) => { # return await new Promise((resolve) => # chrome.runtime.sendMessage(extension_id, message, options, resolve) # ) # } # """, # extension_id, # message, # options, # ) # async def dispatch_command(command, tab=None): # # https://developer.chrome.com/docs/extensions/mv2/reference/commands#event-onCommand # return await target_ctx.evaluate( # """ # async (command, tab) => { # return await new Promise((resolve) => # chrome.commands.onCommand.dispatch(command, tab, resolve) # ) # } # """, # command, # tab, # ) # existing_extension = next((ext for ext in extensions if ext.get('id') == extension_id), {}) # new_extension = { # **existing_extension, # 'id': extension_id, # 'webstore_name': name, # 'target': target, # 'target_ctx': target_ctx, # 'target_type': target_type, # 'target_url': target_url, # 'manifest_version': manifest_version, # 'manifest': manifest, # 'version': version, # 'homepage_url': homepage_url, # 'options_url': options_url, # 'dispatch_eval': dispatch_eval, # run some JS in the extension's service worker context # 'dispatch_popup': dispatch_popup, # open the extension popup # 'dispatch_action': dispatch_action, # trigger an extension menubar icon click # 'dispatch_message': dispatch_message, # send a chrome runtime message in the service worker context # 'dispatch_command': dispatch_command, # trigger an extension keyboard shortcut command # } # logger.info(f'[➕] Loaded extension {name[:32]} ({version}) {target_type}...'.ljust(82) + target_url) # existing_extension.update(new_extension) # return new_extension # async def get_chrome_extensions_from_persona(CHROME_EXTENSIONS, CHROME_EXTENSIONS_DIR): # logger.info('*************************************************************************') # logger.info(f'[⚙️] Installing {len(CHROME_EXTENSIONS)} chrome extensions from CHROME_EXTENSIONS...') # try: # # read extension metadata from filesystem (installing from Chrome webstore if extension is missing) # for extension in CHROME_EXTENSIONS: # extension.update(await load_or_install_extension(extension)) # # for easier debugging, write parsed extension info to filesystem # await overwrite_file( # CHROME_EXTENSIONS_JSON_PATH.replace('.json', '.present.json'), # CHROME_EXTENSIONS, # ) # except Exception as err: # logger.error(err) # logger.info('*************************************************************************') # return CHROME_EXTENSIONS # _EXTENSIONS_CACHE = None # async def get_chrome_extensions_from_cache(browser, extensions=None, extensions_dir=None): # global _EXTENSIONS_CACHE # if extensions is None: # extensions = CHROME_EXTENSIONS # if extensions_dir is None: # extensions_dir = CHROME_EXTENSIONS_DIR # if _EXTENSIONS_CACHE is None: # logger.info(f'[⚙️] Loading {len(extensions)} chrome extensions from CHROME_EXTENSIONS...') # # find loaded Extensions at runtime / browser launch time & connect handlers # # looks at all the open targets for extension service workers / bg pages # for target in await browser.targets(): # # mutates extensions object in-place to add metadata loaded from filesystem persona dir # await load_extension_from_target(extensions, target) # _EXTENSIONS_CACHE = extensions # # write installed extension metadata to filesystem extensions.json for easier debugging # await overwrite_file( # CHROME_EXTENSIONS_JSON_PATH.replace('.json', '.loaded.json'), # extensions, # ) # await overwrite_symlink( # CHROME_EXTENSIONS_JSON_PATH.replace('.json', '.loaded.json'), # CHROME_EXTENSIONS_JSON_PATH, # ) # return _EXTENSIONS_CACHE # async def setup_2captcha_extension(browser, extensions): # page = None # try: # # open a new tab to finish setting up the 2captcha extension manually using its extension options page # page = await browser.new_page() # options_url = next((ext.get('options_url') for ext in extensions if ext.get('name') == 'captcha2'), None) # await page.goto(options_url) # await asyncio.sleep(2.5) # await page.bring_to_front() # # type in the API key and click the Login button (and auto-close success modal after it pops up) # await page.evaluate("""() => { # const elem = document.querySelector("input[name=apiKey]"); # elem.value = ""; # }""") # await page.type('input[name=apiKey]', API_KEY_2CAPTCHA, delay=25) # # toggle all the important switches to ON # await page.evaluate("""() => { # const checkboxes = Array.from(document.querySelectorAll('input#isPluginEnabled, input[name*=enabledFor], input[name*=autoSolve]')); # for (const checkbox of checkboxes) { # if (!checkbox.checked) checkbox.click(); # } # }""") # dialog_opened = False # async def handle_dialog(dialog): # nonlocal dialog_opened # await asyncio.sleep(0.5) # await dialog.accept() # dialog_opened = True # page.on('dialog', handle_dialog) # await page.click('button#connect') # await asyncio.sleep(2.5) # if not dialog_opened: # raise ValueError( # f'2captcha extension login confirmation dialog never opened, please check its options page manually: {options_url}' # ) # logger.info('[🔑] Configured the 2captcha extension using its options page...') # except Exception as err: # logger.warning(f'[❌] Failed to configure the 2captcha extension using its options page! {err}') # if page: # await page.close()