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