170 lines
5.5 KiB
Python
170 lines
5.5 KiB
Python
from __future__ import annotations
|
|
|
|
import argparse
|
|
import base64
|
|
import hashlib
|
|
import json
|
|
import re
|
|
from pathlib import Path
|
|
from typing import Any, Iterator
|
|
from urllib.parse import urlsplit, urlunsplit
|
|
|
|
|
|
_REGION_COOKIE_RE = re.compile(r"(?:^|;\s*)region_ticket=([^;]*)", re.IGNORECASE)
|
|
|
|
|
|
def _endpoint(url: str) -> str:
|
|
parts = urlsplit(url)
|
|
return urlunsplit((parts.scheme, parts.netloc, parts.path, "", ""))
|
|
|
|
|
|
def _headers(message: dict[str, Any]) -> Iterator[tuple[str, str]]:
|
|
for header in message.get("headers") or []:
|
|
if not isinstance(header, dict):
|
|
continue
|
|
name = header.get("name")
|
|
value = header.get("value")
|
|
if isinstance(name, str) and isinstance(value, str):
|
|
yield name.lower(), value
|
|
|
|
|
|
def _ticket_summary(ticket: str) -> dict[str, Any]:
|
|
return {
|
|
"prefix": ticket[:3],
|
|
"length": len(ticket),
|
|
"sha256_12": hashlib.sha256(ticket.encode()).hexdigest()[:12],
|
|
}
|
|
|
|
|
|
def _cookie_ticket(message: dict[str, Any]) -> str | None:
|
|
for name, value in _headers(message):
|
|
if name != "cookie":
|
|
continue
|
|
match = _REGION_COOKIE_RE.search(value)
|
|
if match and match.group(1):
|
|
return match.group(1)
|
|
for cookie in message.get("cookies") or []:
|
|
if not isinstance(cookie, dict):
|
|
continue
|
|
if str(cookie.get("name", "")).lower() == "region_ticket":
|
|
value = cookie.get("value")
|
|
if isinstance(value, str) and value:
|
|
return value
|
|
return None
|
|
|
|
|
|
def _set_cookie_tickets(message: dict[str, Any]) -> Iterator[str]:
|
|
for name, value in _headers(message):
|
|
if name != "set-cookie":
|
|
continue
|
|
match = _REGION_COOKIE_RE.search(value)
|
|
if match and match.group(1):
|
|
yield match.group(1)
|
|
|
|
|
|
def _response_json(response: dict[str, Any]) -> Any | None:
|
|
content = response.get("content") or {}
|
|
text = content.get("text")
|
|
if not isinstance(text, str) or not text:
|
|
return None
|
|
if str(content.get("encoding", "")).lower() == "base64":
|
|
try:
|
|
text = base64.b64decode(text).decode("utf-8", errors="replace")
|
|
except (ValueError, UnicodeError):
|
|
return None
|
|
try:
|
|
return json.loads(text.lstrip("\ufeff"))
|
|
except (TypeError, ValueError):
|
|
return None
|
|
|
|
|
|
def _region_nodes(value: Any, path: str = "$") -> Iterator[tuple[str, dict[str, Any]]]:
|
|
if isinstance(value, dict):
|
|
for key, child in value.items():
|
|
child_path = f"{path}.{key}"
|
|
if key == "region" and isinstance(child, dict):
|
|
ticket = child.get("ticket")
|
|
if isinstance(ticket, str) and ticket:
|
|
yield child_path, child
|
|
yield from _region_nodes(child, child_path)
|
|
elif isinstance(value, list):
|
|
for index, child in enumerate(value):
|
|
yield from _region_nodes(child, f"{path}[{index}]")
|
|
|
|
|
|
def _event_base(index: int, entry: dict[str, Any]) -> dict[str, Any]:
|
|
request = entry.get("request") or {}
|
|
response = entry.get("response") or {}
|
|
return {
|
|
"entry_index": index,
|
|
"started": entry.get("startedDateTime", ""),
|
|
"method": request.get("method", ""),
|
|
"url": _endpoint(str(request.get("url", ""))),
|
|
"status": response.get("status", 0),
|
|
}
|
|
|
|
|
|
def analyze_har(path: Path) -> dict[str, Any]:
|
|
with path.open("r", encoding="utf-8", errors="replace") as stream:
|
|
har = json.load(stream)
|
|
|
|
entries = har.get("log", {}).get("entries", [])
|
|
response_regions: list[dict[str, Any]] = []
|
|
response_set_cookies: list[dict[str, Any]] = []
|
|
request_cookies: list[dict[str, Any]] = []
|
|
|
|
for index, entry in enumerate(entries):
|
|
if not isinstance(entry, dict):
|
|
continue
|
|
request = entry.get("request") or {}
|
|
response = entry.get("response") or {}
|
|
event = _event_base(index, entry)
|
|
|
|
request_ticket = _cookie_ticket(request)
|
|
if request_ticket:
|
|
request_cookies.append({**event, "ticket": _ticket_summary(request_ticket)})
|
|
|
|
for ticket in _set_cookie_tickets(response):
|
|
response_set_cookies.append({**event, "ticket": _ticket_summary(ticket)})
|
|
|
|
body = _response_json(response)
|
|
for json_path, region in _region_nodes(body):
|
|
ticket = str(region["ticket"])
|
|
response_regions.append(
|
|
{
|
|
**event,
|
|
"json_path": json_path,
|
|
"uid": str(region.get("uid", "")),
|
|
"name": str(region.get("name", "")),
|
|
"ticket": _ticket_summary(ticket),
|
|
}
|
|
)
|
|
|
|
return {
|
|
"source": str(path),
|
|
"entry_count": len(entries),
|
|
"response_regions": response_regions,
|
|
"response_set_cookies": response_set_cookies,
|
|
"request_cookies": request_cookies,
|
|
}
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description="审计 HAR 中 region_ticket 的下发和使用时间线")
|
|
parser.add_argument("har", nargs="+", type=Path)
|
|
parser.add_argument("--out", type=Path)
|
|
args = parser.parse_args()
|
|
|
|
reports = [analyze_har(path) for path in args.har]
|
|
output = json.dumps(reports, ensure_ascii=False, indent=2)
|
|
if args.out:
|
|
args.out.parent.mkdir(parents=True, exist_ok=True)
|
|
args.out.write_text(output + "\n", encoding="utf-8")
|
|
else:
|
|
print(output)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|