Initial SQL-only 1C adapter baseline
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
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())
|
||||
Reference in New Issue
Block a user