351 lines
12 KiB
Python
351 lines
12 KiB
Python
#!/usr/bin/env python3
|
|
# -*- encoding: utf-8 -*-
|
|
#
|
|
# Created at 19.01.2021.
|
|
# Python 3.7.3 x64
|
|
# Contacts: alexandrsokolov@cock.li
|
|
#
|
|
import copy
|
|
import json
|
|
import urllib.parse
|
|
from functools import lru_cache
|
|
from typing import List
|
|
|
|
from selenium.common.exceptions import NoSuchElementException
|
|
from selenium.webdriver import Firefox
|
|
from selenium.webdriver.common.keys import Keys
|
|
|
|
OPTION_PROXY_TYPE = "proxy_type"
|
|
OPTION_PROXY_HOST = "proxy_host"
|
|
OPTION_PROXY_PORT = "proxy_port"
|
|
OPTION_PROXY_USERNAME = "proxy_username"
|
|
OPTION_PROXY_PASSWORD = "proxy_password"
|
|
OPTION_PROXY_COOKIES = "proxy_cookies"
|
|
|
|
|
|
class NoAddonException(Exception):
|
|
pass
|
|
|
|
|
|
class CookieException(Exception):
|
|
pass
|
|
|
|
|
|
class Cookie(dict):
|
|
"""https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/cookies/set"""
|
|
|
|
def __init__(self, *, domain=None, expiration_date=None, first_party_domain=None, http_only=None, name=None,
|
|
path=None, same_site=None, secure=None, store_id=None, url=None, value=None):
|
|
super().__init__()
|
|
if domain:
|
|
self.domain = domain
|
|
if expiration_date:
|
|
self.expiration_date = expiration_date
|
|
if first_party_domain:
|
|
self.first_party_domain = first_party_domain
|
|
if http_only:
|
|
self.http_only = http_only
|
|
if name:
|
|
self.name = name
|
|
if path:
|
|
self.path = path
|
|
if same_site:
|
|
self.same_site = same_site
|
|
if secure:
|
|
self.secure = secure
|
|
if store_id:
|
|
self.store_id = store_id
|
|
if url:
|
|
self.url = url
|
|
if value:
|
|
self.value = value
|
|
|
|
@property
|
|
def domain(self):
|
|
"""A string representing the domain of the cookie. If omitted, the cookie becomes a host-only cookie."""
|
|
return self.get("domain")
|
|
|
|
@domain.setter
|
|
def domain(self, value):
|
|
self["domain"] = value
|
|
|
|
@domain.deleter
|
|
def domain(self):
|
|
del self["domain"]
|
|
|
|
@property
|
|
def expiration_date(self):
|
|
"""A number that represents the expiration date of the cookie as the number of seconds since the UNIX epoch.
|
|
If omitted, the cookie becomes a session cookie."""
|
|
return self.get("expirationDate")
|
|
|
|
@expiration_date.setter
|
|
def expiration_date(self, value):
|
|
self["expirationDate"] = value
|
|
|
|
@expiration_date.deleter
|
|
def expiration_date(self):
|
|
del self["expirationDate"]
|
|
|
|
@property
|
|
def first_party_domain(self):
|
|
"""A string representing the first-party domain with which the cookie to will be associated.
|
|
This property must be supplied if the browser has first-party isolation enabled. See First-party isolation
|
|
https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/cookies#First-party_isolation"""
|
|
return self.get("firstPartyDomain")
|
|
|
|
@first_party_domain.setter
|
|
def first_party_domain(self, value):
|
|
self["firstPartyDomain"] = value
|
|
|
|
@first_party_domain.deleter
|
|
def first_party_domain(self):
|
|
del self["firstPartyDomain"]
|
|
|
|
@property
|
|
def http_only(self):
|
|
"""A boolean that specifies whether the cookie should be marked as HttpOnly (true), or not (false).
|
|
If omitted, it defaults to false."""
|
|
return self.get("httpOnly")
|
|
|
|
@http_only.setter
|
|
def http_only(self, value):
|
|
self["httpOnly"] = value
|
|
|
|
@http_only.deleter
|
|
def http_only(self):
|
|
del self["httpOnly"]
|
|
|
|
@property
|
|
def name(self):
|
|
"""A string representing the name of the cookie. If omitted, this is empty by default."""
|
|
return self.get("name")
|
|
|
|
@name.setter
|
|
def name(self, value):
|
|
self["name"] = value
|
|
|
|
@name.deleter
|
|
def name(self):
|
|
del self["name"]
|
|
|
|
@property
|
|
def path(self):
|
|
"""A string representing the path of the cookie.
|
|
If omitted, this defaults to the path portion of the URL parameter."""
|
|
return self.get("path")
|
|
|
|
@path.setter
|
|
def path(self, value):
|
|
self["path"] = value
|
|
|
|
@path.deleter
|
|
def path(self):
|
|
del self["path"]
|
|
|
|
@property
|
|
def same_site(self):
|
|
"""A cookies.SameSiteStatus value that indicates the SameSite state of the cookie.
|
|
If omitted, it defaults to 0, 'no_restriction'.
|
|
https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/cookies/SameSiteStatus"""
|
|
return self.get("sameSite")
|
|
|
|
@same_site.setter
|
|
def same_site(self, value):
|
|
self["sameSite"] = value
|
|
|
|
@same_site.deleter
|
|
def same_site(self):
|
|
del self["sameSite"]
|
|
|
|
@property
|
|
def secure(self):
|
|
"""A boolean that specifies whether the cookie should be marked as secure (true), or not (false).
|
|
If omitted, it defaults to false."""
|
|
return self.get("secure")
|
|
|
|
@secure.setter
|
|
def secure(self, value):
|
|
self["secure"] = value
|
|
|
|
@secure.deleter
|
|
def secure(self):
|
|
del self["secure"]
|
|
|
|
@property
|
|
def store_id(self):
|
|
"""A string representing the ID of the cookie store in which to set the cookie.
|
|
If omitted, the cookie is set in the current execution context's cookie store by default."""
|
|
return self.get("storeId")
|
|
|
|
@store_id.setter
|
|
def store_id(self, value):
|
|
self["storeId"] = value
|
|
|
|
@store_id.deleter
|
|
def store_id(self):
|
|
del self["storeId"]
|
|
|
|
@property
|
|
def url(self):
|
|
"""A string representing the request-URI to associate with the cookie.
|
|
This value can affect the default domain and path values of the created cookie."""
|
|
return self.get("url")
|
|
|
|
@url.setter
|
|
def url(self, value):
|
|
self["url"] = value
|
|
|
|
@url.deleter
|
|
def url(self):
|
|
del self["url"]
|
|
|
|
@property
|
|
def value(self):
|
|
"""A string representing the value of the cookie.
|
|
If omitted, this is empty by default."""
|
|
return self.get("value")
|
|
|
|
@value.setter
|
|
def value(self, value):
|
|
self["value"] = value
|
|
|
|
@value.deleter
|
|
def value(self):
|
|
del self["value"]
|
|
|
|
|
|
class SeleniumHelperExtension:
|
|
"""Обертка для установки и настройки дополнения"""
|
|
|
|
_uuid = None
|
|
|
|
_options = {
|
|
OPTION_PROXY_TYPE: "type",
|
|
OPTION_PROXY_HOST: "host",
|
|
OPTION_PROXY_PORT: "port",
|
|
OPTION_PROXY_USERNAME: "username",
|
|
OPTION_PROXY_PASSWORD: "password",
|
|
OPTION_PROXY_COOKIES: "cookies"
|
|
}
|
|
|
|
def __init__(self, driver: Firefox, raise_for_about_config=True):
|
|
self.raise_for_about_config = raise_for_about_config
|
|
self._driver = driver
|
|
|
|
@property
|
|
def uuid(self):
|
|
return self._uuid
|
|
|
|
@property
|
|
def internal_uuid(self):
|
|
installed_addons = json.loads(self._get_preference("extensions.webextensions.uuids"))
|
|
try:
|
|
return installed_addons[self.uuid]
|
|
except KeyError:
|
|
raise NoAddonException("Addon `selenium_helper` not found. Is it installed?")
|
|
|
|
@property
|
|
def setup_url(self):
|
|
return f"moz-extension://{self.internal_uuid}/data/options.html"
|
|
|
|
def set_uuid(self, uuid: str):
|
|
"""если дополнение уже установлено, требуется его UUID для того чтобы найти дополнение в списке
|
|
установленных и получить его внутренний UUID"""
|
|
self._uuid = uuid
|
|
|
|
def install_debug(self):
|
|
"""открываем страницу отладки дополнений и предоставляем возможность установить дополнение из файла"""
|
|
self._driver.get("about:debugging#/runtime/this-firefox")
|
|
if self._uuid:
|
|
input("Install extension from file and press Enter to continue...")
|
|
else:
|
|
self._uuid = input("Install extension from file and enter UUID:")
|
|
|
|
def install(self, xpi_path: str):
|
|
"""установка дополнения в браузер"""
|
|
self._uuid = self._driver.install_addon(xpi_path)
|
|
|
|
def begin_options(self):
|
|
"""открываем настройки дополнения"""
|
|
self._driver.get(self.setup_url)
|
|
return self
|
|
|
|
def set_proxy(self, proxy_string: str):
|
|
"""заполняем поля для прокси"""
|
|
proxy = urllib.parse.urlparse(proxy_string)
|
|
self._fill_option(OPTION_PROXY_TYPE, proxy.scheme)
|
|
self._fill_option(OPTION_PROXY_HOST, proxy.hostname)
|
|
self._fill_option(OPTION_PROXY_PORT, proxy.port)
|
|
self._fill_option(OPTION_PROXY_USERNAME, proxy.username)
|
|
self._fill_option(OPTION_PROXY_PASSWORD, proxy.password)
|
|
return self
|
|
|
|
def set_cookies(self, cookies: List[Cookie], default_url=None, ignore_errors=False):
|
|
"""устанавливаем куки. у кук должен быть url, если его нет, то попытаемся создать его из полей domain и path,
|
|
если это не получится сделать, то будем использовать переменную default_url, если она не установлена,
|
|
то выбрасываем исключение, а если ignore_errors установлен в True, то пропускаем проблемную куку."""
|
|
clear_value = self._clear_cookies(cookies, default_url, ignore_errors)
|
|
|
|
value = json.dumps(clear_value)
|
|
|
|
self._fill_option(OPTION_PROXY_COOKIES, value)
|
|
return self
|
|
|
|
def apply_options(self):
|
|
"""сохраняем изменения сделанные на странице настроек"""
|
|
self._driver.find_element_by_id("submit").click()
|
|
|
|
def _clear_cookies(self, cookies: List[Cookie], default_url=None, ignore_errors=False):
|
|
clear_value = []
|
|
|
|
for cookie in copy.deepcopy(cookies):
|
|
if not cookie.get("url"):
|
|
try:
|
|
domain = cookie["domain"][1:] if cookie["domain"].startswith(".") else cookie["domain"]
|
|
url = f"http://{domain}{cookie.get('path', '/')}"
|
|
except KeyError:
|
|
url = default_url
|
|
|
|
if url is None:
|
|
if ignore_errors:
|
|
continue
|
|
else:
|
|
raise CookieException(f"No url specified for cookie {cookie}")
|
|
|
|
cookie["url"] = url
|
|
clear_value.append(cookie)
|
|
return clear_value
|
|
|
|
def _fill_option(self, name, value):
|
|
self._fill_element(self._driver.find_element_by_id(self._options[name]), value)
|
|
|
|
@lru_cache()
|
|
def _get_preference(self, name):
|
|
|
|
def get_search_box_with_wait_about_config_approved():
|
|
try:
|
|
search_box = self._driver.find_element_by_id("about-config-search") # noqa
|
|
except NoSuchElementException:
|
|
if self.raise_for_about_config:
|
|
raise
|
|
else:
|
|
input("Approve warning message and press Enter to continue...")
|
|
return get_search_box_with_wait_about_config_approved()
|
|
else:
|
|
return search_box
|
|
|
|
self._driver.get("about:config")
|
|
|
|
search_box = get_search_box_with_wait_about_config_approved()
|
|
search_box.clear()
|
|
search_box.send_keys(name)
|
|
search_box.send_keys(Keys.ENTER)
|
|
|
|
search_result = self._driver.find_elements_by_xpath("//table[@id='prefs']/tr/td/span")
|
|
return search_result[0].text
|
|
|
|
def _fill_element(self, element, value: str): # noqa
|
|
element.clear()
|
|
element.send_keys(value)
|