commit b64a617615eb2e869e86742c1785483ec1ada2dd Author: Alexandr Date: Mon Jan 25 11:31:25 2021 +0300 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f242497 --- /dev/null +++ b/.gitignore @@ -0,0 +1,78 @@ +.idea/ +bin/ +# Created by .ignore support plugin (hsz.mobi) +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST +*.manifest +*.spec +pip-log.txt +pip-delete-this-directory.txt +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ +*.mo +*.pot +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal +instance/ +.webassets-cache +.scrapy +docs/_build/ +.pybuilder/ +target/ +.ipynb_checkpoints +profile_default/ +ipython_config.py +__pypackages__/ +celerybeat-schedule +celerybeat.pid +*.sage.py +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ +.spyderproject +.spyproject +.ropeproject +/site +.mypy_cache/ +.dmypy.json +dmypy.json +.pyre/ +.pytype/ +cython_debug/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..d5aa330 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +Дополнение, которое дает возможность установить в браузер прокси с авторизацией по логину и паролю, а также дает возможность установить куки без предварительного открытия страницы для которой эти куки предназначены. + +Пример есть в директории example diff --git a/_locales/en/messages.json b/_locales/en/messages.json new file mode 100644 index 0000000..de5421b --- /dev/null +++ b/_locales/en/messages.json @@ -0,0 +1,8 @@ +{ + "name": { + "message": "Selenium helper for setup proxy and cookies" + }, + "description": { + "message": "Setup proxy with authentication and cookies" + } +} diff --git a/_locales/ru/messages.json b/_locales/ru/messages.json new file mode 100644 index 0000000..c5b85ef --- /dev/null +++ b/_locales/ru/messages.json @@ -0,0 +1,8 @@ +{ + "name": { + "message": "Selenium helper" + }, + "description": { + "message": "Это расширение дает возможность установить в браузер прокси с авторизацией и куки без предварительного открытия страницы для которой эти куки предназначены." + } +} diff --git a/app128.png b/app128.png new file mode 100644 index 0000000..cb199c3 Binary files /dev/null and b/app128.png differ diff --git a/app32.png b/app32.png new file mode 100644 index 0000000..509604f Binary files /dev/null and b/app32.png differ diff --git a/app64.png b/app64.png new file mode 100644 index 0000000..66dad89 Binary files /dev/null and b/app64.png differ diff --git a/data/content.js b/data/content.js new file mode 100644 index 0000000..e7c9387 --- /dev/null +++ b/data/content.js @@ -0,0 +1,103 @@ +function set_cookies(cookies) { + for (let cookie of cookies) { + browser.cookies.set(cookie) + } +} + + +function set_proxy(PROXY) { + // PROXY = { + // "type": "http", + // "host": "IP address", + // "port": "PORT", + // "username": "USER NAME", + // "password": "PASSWORD" + // } + const TARGET = ""; + const CREDENTIALS = { + username: PROXY.username, + password: PROXY.password + } + const pendingRequests = []; + + function handleProxyRequest(requestInfo) { + return {type: PROXY.type, host: PROXY.host, port: PROXY.port}; + } + + // A request has completed. + // We can stop worrying about it. + function completed(requestDetails) { + var index = pendingRequests.indexOf(requestDetails.requestId); + if (index > -1) { + pendingRequests.splice(index, 1); + } + } + + function provideCredentialsSync(requestDetails) { + // If we have seen this request before, then + // assume our credentials were bad, and give up. + if (pendingRequests.indexOf(requestDetails.requestId) != -1) { + console.log(`bad credentials for: ${requestDetails.requestId}`); + return {cancel: true}; + } + pendingRequests.push(requestDetails.requestId); + console.log(`providing credentials for: ${requestDetails.requestId}`); + return {authCredentials: CREDENTIALS}; + } + + browser.proxy.onRequest.addListener( + handleProxyRequest, + {urls: [TARGET]} + ); + + browser.webRequest.onAuthRequired.addListener( + provideCredentialsSync, + {urls: [TARGET]}, + ["blocking"] + ); + + browser.webRequest.onCompleted.addListener( + completed, + {urls: [TARGET]} + ); + + browser.webRequest.onErrorOccurred.addListener( + completed, + {urls: [TARGET]} + ); +} + + +function main() { + + function setProxy(result) { + if (result.proxy) { + console.log("Setup proxy..."); + set_proxy(result.proxy); + } + } + + function setCookies(result) { + if (result.cookies) { + console.log("Setup cookies..."); + set_cookies(result.cookies); + } + } + + function onError(error) { + console.log(`Error: ${error}`); + } + + let proxy = browser.storage.sync.get("proxy"); + proxy.then(setProxy, onError); + + let cookies = browser.storage.sync.get("cookies"); + cookies.then(setCookies, onError); +} + + +browser.runtime.onMessage.addListener(function(message, sender, reply) { + if (message.update) { + main(); + } +}); diff --git a/data/options.html b/data/options.html new file mode 100644 index 0000000..4114894 --- /dev/null +++ b/data/options.html @@ -0,0 +1,17 @@ + + + + + +
+ + + + + + + +
+ + + diff --git a/data/options.js b/data/options.js new file mode 100644 index 0000000..bfbf4a5 --- /dev/null +++ b/data/options.js @@ -0,0 +1,58 @@ +function saveOptions(e) { + e.preventDefault(); + let cookies = []; + try { + cookies = JSON.parse(document.getElementById("cookies").value); + } catch {}; + console.log(cookies) + browser.storage.sync.set({ + proxy: { + "type": document.getElementById("type").value, + "host": document.getElementById("host").value, + "port": document.getElementById("port").value, + "username": document.getElementById("username").value, + "password": document.getElementById("password").value + }, + cookies: cookies + }); +} + + +function restoreOptions() { + + function setCurrentProxy(result) { + if (result.proxy) { + document.getElementById("type").value = result.proxy.type; + document.getElementById("host").value = result.proxy.host; + document.getElementById("port").value = result.proxy.port; + document.getElementById("username").value = result.proxy.username; + document.getElementById("password").value = result.proxy.password; + } + } + + function setCurrentCookies(result) { + if (result.cookies) { + document.getElementById("cookies").value = JSON.stringify(result.cookies); + } + } + + function onError(error) { + console.log(`Error: ${error}`); + } + + let proxy = browser.storage.sync.get("proxy"); + proxy.then(setCurrentProxy, onError); + + let cookies = browser.storage.sync.get("cookies"); + cookies.then(setCurrentCookies, onError); +} + + +function notifyExtension(e) { + browser.runtime.sendMessage({"update": true}); +} + + +document.addEventListener("DOMContentLoaded", restoreOptions); +document.getElementById("options_form").addEventListener("submit", saveOptions); +window.addEventListener("submit", notifyExtension); diff --git a/example/example.py b/example/example.py new file mode 100644 index 0000000..4fba584 --- /dev/null +++ b/example/example.py @@ -0,0 +1,40 @@ +import logging +import os.path + +import selenium.webdriver + +import selenium_helper + + +def main(): + logger = logging.getLogger(__name__) + profile = selenium.webdriver.FirefoxProfile() + # отключаем предупреждение при открытии about:config + profile.set_preference("browser.aboutConfig.showWarning", False) + # отключаем трекинг navigator.webdriver + profile.set_preference("dom.webdriver.enabled", False) + # отключаем уведомления от сайтов + profile.set_preference("dom.webnotifications.enabled", False) + + driver = selenium.webdriver.Firefox( + firefox_binary="bin/firefox-84.0.2/firefox-bin", executable_path="bin/geckodriver", firefox_profile=profile) + + try: + selenium_helper_extention = selenium_helper.SeleniumHelperExtension(driver) + selenium_helper_extention.install(os.path.abspath("releases/selenium_helper-0.0.4-fx.xpi")) + proxy = "http://username:password@host:port" + cookies = [ + selenium_helper.Cookie(name="cookie", value="test", url="https://whoer.net") + ] + selenium_helper_extention.begin_options().set_proxy(proxy).set_cookies(cookies).apply_options() + + driver.get("https://whoer.net/") + input("Enter to exit...") + except Exception as e: + logger.exception(f"{type(e).__name__}{getattr(e, 'args', None)}") + finally: + driver.quit() + + +if __name__ == '__main__': + main() diff --git a/example/selenium_helper.py b/example/selenium_helper.py new file mode 100644 index 0000000..8c0f0f4 --- /dev/null +++ b/example/selenium_helper.py @@ -0,0 +1,350 @@ +#!/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) diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..769646d --- /dev/null +++ b/manifest.json @@ -0,0 +1,40 @@ +{ + "manifest_version": 2, + "version": "0.0.4", + "minimum_chrome_version": "76.0", + "applications": { + "gecko": { + "id": "eet1ra8cauDu@AlexandrSokolov" + } + }, + "default_locale": "en", + "name": "__MSG_name__", + "short_name": "__MSG_name__", + "description": "__MSG_description__", + "author": "Alexandr Sokolov", + "homepage_url": "https://github.com/alexsok-bit/selenium_helper", + "icons": { + "32": "app32.png", + "64": "app64.png", + "128": "app128.png" + }, + "permissions": [ + "proxy", + "storage", + "cookies", + "webRequest", + "webRequestBlocking", + "" + ], + "options_ui": { + "page": "data/options.html" + }, + "browser_specific_settings": { + "gecko": { + "id": "eet1ra8cauDu@AlexandrSokolov" + } + }, + "background": { + "scripts": ["data/content.js"] + } +} diff --git a/releases/selenium_helper-0.0.4-fx.xpi b/releases/selenium_helper-0.0.4-fx.xpi new file mode 100644 index 0000000..5da9238 Binary files /dev/null and b/releases/selenium_helper-0.0.4-fx.xpi differ