diff --git a/README_session_binder.md b/README_session_binder.md new file mode 100644 index 0000000..fd50ad9 --- /dev/null +++ b/README_session_binder.md @@ -0,0 +1,75 @@ +# Session Binder + +Утилита для управления сессиями браузера в локальной среде разработки. +Позволяет быстро переключаться между аккаунтами, вставляя cookies в Chrome через CDP (Chrome DevTools Protocol). + +## Требования + +- Python 3.10+ +- Google Chrome / Chromium +- playwright + +## Установка + +```bash +pip install -r requirements-session-binder.txt +playwright install chromium +``` + +## Использование + +### 1. Запустите Chrome в режиме отладки + +```bash +google-chrome --remote-debugging-port=9222 --user-data-dir=/tmp/chrome-debug +``` + +### 2. Подготовьте файл cookies + +Формат — JSON-массив объектов с полями `name` и `value`. +Опционально: `domain`, `path`, `httpOnly`, `secure`, `sameSite`, `expires`. + +```json +[ + {"name": "session_id", "value": "abc123"}, + {"name": "auth_token", "value": "eyJhbG..."} +] +``` + +См. пример: [`cookies_example.json`](cookies_example.json) + +### 3. Запустите утилиту + +```bash +python session_binder.py --cookies cookies.json --url https://example.com +``` + +### Параметры + +| Флаг | Описание | По умолчанию | +|---|---|---| +| `--cookies` | Путь к JSON-файлу с cookies | **обязательный** | +| `--url` | URL для навигации после вставки cookies | **обязательный** | +| `--wait` | Время ожидания (секунды) для ручной привязки | `60` | +| `--port` | Порт CDP (Chrome remote debugging) | `9222` | +| `-v` / `--verbose` | Подробное логирование | выкл. | + +### Пример + +```bash +# Запустить Chrome +google-chrome --remote-debugging-port=9222 --user-data-dir=/tmp/chrome-debug & + +# Вставить cookies и открыть сайт +python session_binder.py --cookies cookies_example.json --url https://example.com --wait 120 + +# Нажмите Ctrl+C чтобы завершить досрочно +``` + +## Как это работает + +1. Подключается к запущенному Chrome через CDP на указанном порту. +2. Вставляет cookies из JSON-файла в контекст браузера (`context.add_cookies()`). +3. Переходит на указанный URL. +4. Даёт разработчику время (`--wait`) для ручной привязки сессии (ввод email, телефона и т.д.). +5. Закрывает браузер по истечении таймера или по Ctrl+C. diff --git a/cookies_example.json b/cookies_example.json new file mode 100644 index 0000000..2c0a0d6 --- /dev/null +++ b/cookies_example.json @@ -0,0 +1,17 @@ +[ + { + "name": "session_id", + "value": "abc123def456" + }, + { + "name": "auth_token", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + }, + { + "name": "preferences", + "value": "lang=ru&theme=dark", + "path": "/", + "httpOnly": false, + "secure": false + } +] diff --git a/requirements-session-binder.txt b/requirements-session-binder.txt new file mode 100644 index 0000000..13cbe37 --- /dev/null +++ b/requirements-session-binder.txt @@ -0,0 +1 @@ +playwright>=1.40 diff --git a/session_binder.py b/session_binder.py new file mode 100644 index 0000000..4161993 --- /dev/null +++ b/session_binder.py @@ -0,0 +1,194 @@ +#!/usr/bin/env python3 +"""Session Binder — utility for managing browser sessions in a local dev environment. + +Opens a local Chrome instance via CDP (Chrome DevTools Protocol), +injects cookies from a JSON file, navigates to the target URL, +and gives the developer time to manually bind the session. +""" + +import argparse +import json +import logging +import sys +import time +from pathlib import Path +from urllib.parse import urlparse + +from playwright.sync_api import sync_playwright, Error as PlaywrightError + +logger = logging.getLogger("session_binder") + +DEFAULT_CDP_PORT = 9222 +DEFAULT_WAIT_SECONDS = 60 + + +def setup_logging(verbose: bool = False) -> None: + level = logging.DEBUG if verbose else logging.INFO + handler = logging.StreamHandler(sys.stdout) + handler.setFormatter( + logging.Formatter( + "[%(asctime)s] %(levelname)s — %(message)s", datefmt="%H:%M:%S" + ) + ) + logger.setLevel(level) + logger.addHandler(handler) + + +def load_cookies(path: Path, url: str) -> list[dict]: + """Load cookies from a JSON file and normalise them for Playwright. + + Accepted formats: + 1. List of {name, value} pairs — domain is derived from *url*. + 2. List of full Playwright cookie dicts (must contain at least + ``name``, ``value`` and ``domain``). + """ + with open(path, encoding="utf-8") as fh: + raw: list[dict] = json.load(fh) + + if not isinstance(raw, list): + raise ValueError("cookies JSON must be a list of objects") + + parsed_url = urlparse(url) + domain = parsed_url.hostname or parsed_url.netloc + + cookies: list[dict] = [] + for entry in raw: + if "name" not in entry or "value" not in entry: + raise ValueError(f"each cookie must have 'name' and 'value': {entry!r}") + + cookie: dict = { + "name": entry["name"], + "value": entry["value"], + "domain": entry.get("domain", domain), + "path": entry.get("path", "/"), + } + + if "expires" in entry: + cookie["expires"] = entry["expires"] + if "httpOnly" in entry: + cookie["httpOnly"] = entry["httpOnly"] + if "secure" in entry: + cookie["secure"] = entry["secure"] + if "sameSite" in entry: + cookie["sameSite"] = entry["sameSite"] + + cookies.append(cookie) + + return cookies + + +def run( + cookies_path: Path, + url: str, + wait_seconds: int = DEFAULT_WAIT_SECONDS, + cdp_port: int = DEFAULT_CDP_PORT, +) -> None: + cookies = load_cookies(cookies_path, url) + logger.info("Loaded %d cookie(s) from %s", len(cookies), cookies_path) + + cdp_url = f"http://127.0.0.1:{cdp_port}" + + with sync_playwright() as pw: + logger.info("Connecting to Chrome via CDP at %s ...", cdp_url) + try: + browser = pw.chromium.connect_over_cdp(cdp_url) + except PlaywrightError as exc: + logger.error( + "Cannot connect to Chrome on port %d. " + "Make sure Chrome is running with --remote-debugging-port=%d\n" + "Example: google-chrome --remote-debugging-port=%d --user-data-dir=/tmp/chrome-debug", + cdp_port, + cdp_port, + cdp_port, + ) + raise SystemExit(1) from exc + + context = browser.contexts[0] if browser.contexts else browser.new_context() + page = context.new_page() + + logger.info( + "Injecting cookies for domain(s): %s", + ", ".join({c["domain"] for c in cookies}), + ) + context.add_cookies(cookies) + + logger.info("Navigating to %s ...", url) + page.goto(url, wait_until="domcontentloaded") + logger.info("Page loaded. Title: %s", page.title()) + + logger.info( + "You have %d seconds to manually bind the session (email / phone / etc.).", + wait_seconds, + ) + logger.info("Press Ctrl+C to finish early.") + + try: + for remaining in range(wait_seconds, 0, -1): + if remaining % 10 == 0 or remaining <= 5: + logger.info(" %d seconds remaining ...", remaining) + time.sleep(1) + except KeyboardInterrupt: + logger.info("Interrupted by user.") + + logger.info("Closing browser ...") + page.close() + context.close() + browser.close() + + logger.info("Done.") + + +def parse_args(argv: list[str] | None = None) -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Session Binder — inject cookies into a local Chrome via CDP.", + ) + parser.add_argument( + "--cookies", + required=True, + type=Path, + help="Path to a JSON file with cookies (list of {name, value} objects).", + ) + parser.add_argument( + "--url", + required=True, + help="Target URL to navigate to after cookie injection.", + ) + parser.add_argument( + "--wait", + type=int, + default=DEFAULT_WAIT_SECONDS, + help=f"Seconds to wait for manual session binding (default: {DEFAULT_WAIT_SECONDS}).", + ) + parser.add_argument( + "--port", + type=int, + default=DEFAULT_CDP_PORT, + help=f"Chrome remote debugging port (default: {DEFAULT_CDP_PORT}).", + ) + parser.add_argument( + "-v", + "--verbose", + action="store_true", + help="Enable debug logging.", + ) + return parser.parse_args(argv) + + +def main(argv: list[str] | None = None) -> None: + args = parse_args(argv) + setup_logging(args.verbose) + + if not args.cookies.exists(): + logger.error("Cookies file not found: %s", args.cookies) + raise SystemExit(1) + + run( + cookies_path=args.cookies, + url=args.url, + wait_seconds=args.wait, + cdp_port=args.port, + ) + + +if __name__ == "__main__": + main()