Коммит.

This commit is contained in:
Alex 2026-06-15 13:46:12 +03:00
parent c3f0eaf509
commit f655edf578
2 changed files with 373 additions and 0 deletions

115
netscape_cookies/README.md Normal file
View File

@ -0,0 +1,115 @@
# Cookie Parser
Утилита `parse_netscape_cookies.py` разбирает cookies из нескольких популярных форматов и приводит их к удобному JSON или строке `Cookie` header.
## Поддерживаемые входные форматы
### Netscape HTTP Cookie File
Формат `cookies.txt`, который часто экспортируют браузерные расширения и CLI-инструменты.
```text
.example.com TRUE / TRUE 1789148954 session_id abc123
```
### JSON-массив cookies
Формат, похожий на экспорт из Chrome extensions API.
```json
[
{
"domain": ".example.com",
"expirationDate": 1789148954.129235,
"hostOnly": false,
"httpOnly": false,
"name": "session_id",
"path": "/",
"sameSite": "lax",
"secure": true,
"session": false,
"storeId": "0",
"value": "abc123"
}
]
```
Также поддерживается объект вида:
```json
{
"cookies": []
}
```
### Cookie header
Обычная строка из HTTP-заголовка `Cookie`.
```text
session_id=abc123; theme=dark;
```
У этого формата нет метаданных вроде `domain`, `expires`, `secure` и `httpOnly`, поэтому скрипт заполняет их пустыми или безопасными значениями по умолчанию.
## Использование
Вывести JSON:
```bash
python3 parse_netscape_cookies.py cookies.txt
```
Прочитать из stdin:
```bash
cat cookies.txt | python3 parse_netscape_cookies.py
```
Вывести строку `Cookie` header:
```bash
python3 parse_netscape_cookies.py cookies.json --format header
```
URL-decode значений:
```bash
python3 parse_netscape_cookies.py cookies.txt --decode
```
Явно указать входной формат:
```bash
python3 parse_netscape_cookies.py input.txt --input-format netscape
python3 parse_netscape_cookies.py input.json --input-format json
python3 parse_netscape_cookies.py header.txt --input-format header
```
По умолчанию используется `--input-format auto`.
## Выходной JSON
Каждая cookie приводится к структуре:
```json
{
"domain": ".example.com",
"include_subdomains": true,
"path": "/",
"secure": true,
"expires": 1789148954.129235,
"name": "session_id",
"value": "abc123",
"http_only": false,
"host_only": false,
"same_site": "lax",
"session": false,
"store_id": "0",
"expires_iso": "2026-09-11T17:49:14.129235+00:00"
}
```
## Важно
Cookies могут давать доступ к аккаунту пользователя. Не отправляйте их в логи, чаты или сторонние сервисы без явного согласия пользователя.

View File

@ -0,0 +1,258 @@
#!/usr/bin/env python3
"""Parse cookies from Netscape, JSON, or Cookie header formats."""
from __future__ import annotations
import argparse
from http.cookies import SimpleCookie
import json
import sys
from dataclasses import asdict, dataclass
from datetime import datetime, timezone
from pathlib import Path
from typing import Iterable
from urllib.parse import unquote
@dataclass(frozen=True)
class NetscapeCookie:
domain: str
include_subdomains: bool
path: str
secure: bool
expires: float | None
name: str
value: str
http_only: bool = False
host_only: bool | None = None
same_site: str | None = None
session: bool | None = None
store_id: str | None = None
@property
def expires_iso(self) -> str | None:
if self.expires is None or self.expires == 0:
return None
return datetime.fromtimestamp(self.expires, tz=timezone.utc).isoformat()
def to_dict(self) -> dict[str, object]:
data = asdict(self)
data["expires_iso"] = self.expires_iso
return data
def parse_bool(value: str, *, line_number: int, field_name: str) -> bool:
normalized = value.strip().upper()
if normalized == "TRUE":
return True
if normalized == "FALSE":
return False
raise ValueError(f"Line {line_number}: {field_name} must be TRUE or FALSE, got {value!r}")
def parse_expires(value: str, *, line_number: int) -> int | None:
try:
expires = int(value)
except ValueError as exc:
raise ValueError(f"Line {line_number}: expires must be an integer, got {value!r}") from exc
return None if expires == 0 else expires
def maybe_decode(value: str, decode_value: bool) -> str:
return unquote(value) if decode_value else value
def parse_netscape_cookie_line(line: str, *, line_number: int, decode_value: bool = False) -> NetscapeCookie | None:
stripped = line.rstrip("\n")
if not stripped or stripped.startswith("# Netscape") or stripped.startswith("# http") or stripped.startswith("# This"):
return None
http_only = False
if stripped.startswith("#HttpOnly_"):
http_only = True
stripped = stripped.removeprefix("#HttpOnly_")
elif stripped.startswith("#"):
return None
fields = stripped.split("\t")
if len(fields) != 7:
raise ValueError(f"Line {line_number}: expected 7 tab-separated fields, got {len(fields)}")
domain, include_subdomains, path, secure, expires, name, value = fields
return NetscapeCookie(
domain=domain,
include_subdomains=parse_bool(include_subdomains, line_number=line_number, field_name="include_subdomains"),
path=path,
secure=parse_bool(secure, line_number=line_number, field_name="secure"),
expires=parse_expires(expires, line_number=line_number),
name=name,
value=maybe_decode(value, decode_value),
http_only=http_only,
host_only=not parse_bool(include_subdomains, line_number=line_number, field_name="include_subdomains"),
session=expires == "0",
)
def parse_netscape_cookies(text: str, *, decode_value: bool = False) -> list[NetscapeCookie]:
cookies: list[NetscapeCookie] = []
for line_number, line in enumerate(text.splitlines(), start=1):
cookie = parse_netscape_cookie_line(line, line_number=line_number, decode_value=decode_value)
if cookie is not None:
cookies.append(cookie)
return cookies
def parse_json_cookies(text: str, *, decode_value: bool = False) -> list[NetscapeCookie]:
try:
payload = json.loads(text)
except json.JSONDecodeError as exc:
raise ValueError(f"Invalid JSON: {exc}") from exc
if isinstance(payload, dict) and isinstance(payload.get("cookies"), list):
payload = payload["cookies"]
if not isinstance(payload, list):
raise ValueError("JSON input must be a cookie array or an object with a cookies array")
cookies: list[NetscapeCookie] = []
for index, item in enumerate(payload, start=1):
if not isinstance(item, dict):
raise ValueError(f"JSON cookie #{index}: expected object, got {type(item).__name__}")
name = item.get("name")
value = item.get("value", "")
if not isinstance(name, str) or not name:
raise ValueError(f"JSON cookie #{index}: name must be a non-empty string")
if not isinstance(value, str):
value = str(value)
domain = str(item.get("domain", ""))
host_only = item.get("hostOnly")
include_subdomains = not bool(host_only) if host_only is not None else domain.startswith(".")
expiration_date = item.get("expirationDate")
session = bool(item.get("session", expiration_date in (None, 0)))
cookies.append(
NetscapeCookie(
domain=domain,
include_subdomains=include_subdomains,
path=str(item.get("path", "/")),
secure=bool(item.get("secure", False)),
expires=None if expiration_date in (None, 0) else float(expiration_date),
name=name,
value=maybe_decode(value, decode_value),
http_only=bool(item.get("httpOnly", False)),
host_only=bool(host_only) if host_only is not None else not include_subdomains,
same_site=item.get("sameSite"),
session=session,
store_id=str(item["storeId"]) if item.get("storeId") is not None else None,
)
)
return cookies
def parse_cookie_header(text: str, *, decode_value: bool = False) -> list[NetscapeCookie]:
header = text.strip().rstrip(";")
if not header:
return []
parsed = SimpleCookie()
try:
parsed.load(header)
except Exception as exc:
raise ValueError(f"Invalid Cookie header: {exc}") from exc
if not parsed:
raise ValueError("Cookie header did not contain any name=value pairs")
return [
NetscapeCookie(
domain="",
include_subdomains=False,
path="/",
secure=False,
expires=None,
name=morsel.key,
value=maybe_decode(morsel.value, decode_value),
host_only=True,
session=True,
)
for morsel in parsed.values()
]
def detect_input_format(text: str) -> str:
stripped = text.lstrip()
if not stripped:
return "header"
if stripped.startswith("[") or stripped.startswith("{"):
return "json"
if any("\t" in line for line in stripped.splitlines()):
return "netscape"
return "header"
def parse_cookies(text: str, *, input_format: str = "auto", decode_value: bool = False) -> list[NetscapeCookie]:
selected_format = detect_input_format(text) if input_format == "auto" else input_format
if selected_format == "netscape":
return parse_netscape_cookies(text, decode_value=decode_value)
if selected_format == "json":
return parse_json_cookies(text, decode_value=decode_value)
if selected_format == "header":
return parse_cookie_header(text, decode_value=decode_value)
raise ValueError(f"Unsupported input format: {input_format}")
def build_cookie_header(cookies: Iterable[NetscapeCookie]) -> str:
return "; ".join(f"{cookie.name}={cookie.value}" for cookie in cookies)
def read_input(path: str | None) -> str:
if path:
return Path(path).read_text(encoding="utf-8")
return sys.stdin.read()
def main() -> int:
parser = argparse.ArgumentParser(
description="Parse cookies from Netscape, JSON, or Cookie header formats.",
)
parser.add_argument("file", nargs="?", help="Path to cookies.txt. If omitted, reads stdin.")
parser.add_argument("--decode", action="store_true", help="URL-decode cookie values.")
parser.add_argument(
"--input-format",
choices=("auto", "netscape", "json", "header"),
default="auto",
help="Input format. Default: auto.",
)
parser.add_argument(
"--format",
choices=("json", "header"),
default="json",
help="Output format. Default: json.",
)
args = parser.parse_args()
try:
cookies = parse_cookies(read_input(args.file), input_format=args.input_format, decode_value=args.decode)
except OSError as exc:
print(f"Cannot read input: {exc}", file=sys.stderr)
return 1
except ValueError as exc:
print(f"Cannot parse cookies: {exc}", file=sys.stderr)
return 2
if args.format == "header":
print(build_cookie_header(cookies))
else:
print(json.dumps([cookie.to_dict() for cookie in cookies], ensure_ascii=False, indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())