Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions README_session_binder.md
Original file line number Diff line number Diff line change
@@ -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.
17 changes: 17 additions & 0 deletions cookies_example.json
Original file line number Diff line number Diff line change
@@ -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
}
]
1 change: 1 addition & 0 deletions requirements-session-binder.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
playwright>=1.40
194 changes: 194 additions & 0 deletions session_binder.py
Original file line number Diff line number Diff line change
@@ -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()