Files
llm/scripts/normalize_1c_its_cookie.py

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())