197 lines
6.2 KiB
Python
197 lines
6.2 KiB
Python
from __future__ import annotations
|
|
|
|
import argparse
|
|
import http.cookies
|
|
import json
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
|
|
COOKIE_ATTRS = {
|
|
"domain",
|
|
"path",
|
|
"expires",
|
|
"max-age",
|
|
"secure",
|
|
"httponly",
|
|
"samesite",
|
|
"priority",
|
|
"partitioned",
|
|
}
|
|
|
|
|
|
def clean_domain(value: str) -> str:
|
|
return value.strip().lower().lstrip(".")
|
|
|
|
|
|
def domain_matches(domain: str, target_domain: str) -> bool:
|
|
domain = clean_domain(domain)
|
|
target_domain = clean_domain(target_domain)
|
|
return domain == target_domain or domain.endswith("." + target_domain)
|
|
|
|
|
|
def cookie_pair(name: str, value: str) -> str | None:
|
|
name = name.strip()
|
|
value = value.strip()
|
|
if not name or name.lower() in COOKIE_ATTRS:
|
|
return None
|
|
return f"{name}={value}"
|
|
|
|
|
|
def parse_json_cookies(text: str, target_domain: str) -> list[str]:
|
|
try:
|
|
data = json.loads(text)
|
|
except json.JSONDecodeError:
|
|
return []
|
|
if isinstance(data, dict):
|
|
if isinstance(data.get("cookies"), list):
|
|
data = data["cookies"]
|
|
else:
|
|
data = [data]
|
|
if not isinstance(data, list):
|
|
return []
|
|
pairs = []
|
|
for item in data:
|
|
if not isinstance(item, dict):
|
|
continue
|
|
domain = str(item.get("domain") or item.get("host") or "")
|
|
if domain and not domain_matches(domain, target_domain):
|
|
continue
|
|
pair = cookie_pair(str(item.get("name") or ""), str(item.get("value") or ""))
|
|
if pair:
|
|
pairs.append(pair)
|
|
return pairs
|
|
|
|
|
|
def parse_netscape_cookies(text: str, target_domain: str) -> list[str]:
|
|
pairs = []
|
|
for raw_line in text.splitlines():
|
|
line = raw_line.strip()
|
|
if not line or line.startswith("#") and not line.startswith("#HttpOnly_"):
|
|
continue
|
|
if line.startswith("#HttpOnly_"):
|
|
line = line.removeprefix("#HttpOnly_")
|
|
parts = line.split("\t")
|
|
if len(parts) < 7:
|
|
continue
|
|
domain, _include_subdomains, _path, _secure, _expires, name, value = parts[:7]
|
|
if not domain_matches(domain, target_domain):
|
|
continue
|
|
pair = cookie_pair(name, value)
|
|
if pair:
|
|
pairs.append(pair)
|
|
return pairs
|
|
|
|
|
|
def parse_set_cookie_lines(text: str, target_domain: str) -> list[str]:
|
|
pairs = []
|
|
for raw_line in text.splitlines():
|
|
line = raw_line.strip()
|
|
if not line:
|
|
continue
|
|
line = re.sub(r"^(set-cookie|cookie)\s*:\s*", "", line, flags=re.IGNORECASE)
|
|
if "domain=" in line.lower():
|
|
domain_match = re.search(r"(?:^|;\s*)domain=([^;]+)", line, flags=re.IGNORECASE)
|
|
if domain_match and not domain_matches(domain_match.group(1), target_domain):
|
|
continue
|
|
first = line.split(";", 1)[0].strip()
|
|
if "=" not in first:
|
|
continue
|
|
name, value = first.split("=", 1)
|
|
pair = cookie_pair(name, value)
|
|
if pair:
|
|
pairs.append(pair)
|
|
return pairs
|
|
|
|
|
|
def parse_cookie_header(text: str) -> list[str]:
|
|
normalized = " ".join(line.strip() for line in text.splitlines() if line.strip())
|
|
normalized = re.sub(r"^cookie\s*:\s*", "", normalized, flags=re.IGNORECASE)
|
|
if not normalized:
|
|
return []
|
|
pairs = []
|
|
for part in normalized.split(";"):
|
|
part = part.strip()
|
|
if "=" not in part:
|
|
continue
|
|
name, value = part.split("=", 1)
|
|
pair = cookie_pair(name, value)
|
|
if pair:
|
|
pairs.append(pair)
|
|
return pairs
|
|
|
|
|
|
def dedupe_pairs(pairs: list[str]) -> list[str]:
|
|
by_name: dict[str, str] = {}
|
|
order: list[str] = []
|
|
for pair in pairs:
|
|
name = pair.split("=", 1)[0]
|
|
if name not in by_name:
|
|
order.append(name)
|
|
by_name[name] = pair
|
|
return [by_name[name] for name in order]
|
|
|
|
|
|
def normalize_cookie(text: str, target_domain: str) -> dict[str, Any]:
|
|
raw = text.strip()
|
|
warnings = []
|
|
if not raw:
|
|
return {"ok": False, "cookie": "", "pairs": [], "warnings": ["empty_input"], "errors": ["empty_input"]}
|
|
if "yandex." in raw.lower():
|
|
warnings.append("contains_yandex_marker")
|
|
|
|
domain_attrs = re.findall(r"(?:^|;\s*)domain=([^;\r\n]+)", raw, flags=re.IGNORECASE)
|
|
has_domain_attrs = bool(domain_attrs)
|
|
has_target_domain_attr = any(domain_matches(domain, target_domain) for domain in domain_attrs)
|
|
cookie_header_pairs = [] if has_domain_attrs and not has_target_domain_attr else parse_cookie_header(raw)
|
|
parsers = [
|
|
("json", parse_json_cookies(raw, target_domain)),
|
|
("netscape", parse_netscape_cookies(raw, target_domain)),
|
|
("set_cookie", parse_set_cookie_lines(raw, target_domain)),
|
|
("cookie_header", cookie_header_pairs),
|
|
]
|
|
best_name, best_pairs = max(parsers, key=lambda item: len(item[1]))
|
|
pairs = dedupe_pairs(best_pairs)
|
|
errors = []
|
|
if not pairs:
|
|
errors.append("no_cookie_pairs_for_target_domain")
|
|
if has_domain_attrs and not has_target_domain_attr:
|
|
errors.append("domain_attributes_do_not_match_target")
|
|
if any(pair.lower().startswith("domain=") for pair in pairs):
|
|
errors.append("domain_attribute_in_cookie_header")
|
|
return {
|
|
"ok": not errors,
|
|
"format": best_name,
|
|
"target_domain": target_domain,
|
|
"cookie": "; ".join(pairs),
|
|
"pairs": pairs,
|
|
"pair_count": len(pairs),
|
|
"warnings": warnings,
|
|
"errors": errors,
|
|
}
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description="Normalize a 1C:ITS cookie input to a Cookie request header.")
|
|
parser.add_argument("--input-file", type=Path)
|
|
parser.add_argument("--target-domain", default="its.1c.ru")
|
|
parser.add_argument("--cookie-only", action="store_true")
|
|
args = parser.parse_args()
|
|
|
|
text = args.input_file.read_text(encoding="utf-8-sig") if args.input_file else sys.stdin.read()
|
|
result = normalize_cookie(text, args.target_domain)
|
|
if args.cookie_only:
|
|
if not result["ok"]:
|
|
print("; ".join(result["errors"]), file=sys.stderr)
|
|
return 1
|
|
print(result["cookie"])
|
|
return 0
|
|
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
return 0 if result["ok"] else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|