diff --git a/scripts/capture-g7-runtime-evidence.py b/scripts/capture-g7-runtime-evidence.py index c6c4ac5..3e0a833 100644 --- a/scripts/capture-g7-runtime-evidence.py +++ b/scripts/capture-g7-runtime-evidence.py @@ -27,6 +27,10 @@ SNAPSHOT_SCHEMA_VERSION = "vignette.voice-runtime.v1" MAX_SAMPLES = 7_200 MAX_INTERVAL_SECONDS = 60.0 MAX_CAPTURE_WINDOW_SECONDS = 7_200.0 +_BROWSER_UA = ( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " + "(KHTML, like Gecko) Chrome/140.0 Safari/537.36" +) class EvidenceFailure(RuntimeError): @@ -158,6 +162,9 @@ def fetch_snapshot( headers={ "Accept": "application/json", "Cookie": f"{cookie_name}={cookie_value}", + # Cloudflare는 Python urllib 기본 User-Agent를 공개 API 도달 전에 + # 거부한다. 공개 배포 probe와 같은 브라우저 호환 전송 경계를 쓴다. + "User-Agent": _BROWSER_UA, }, method="GET", ) diff --git a/scripts/test_g7_runtime_evidence.py b/scripts/test_g7_runtime_evidence.py index 7740244..08e68da 100644 --- a/scripts/test_g7_runtime_evidence.py +++ b/scripts/test_g7_runtime_evidence.py @@ -4,9 +4,11 @@ from __future__ import annotations import copy import importlib.util +import json import sys import unittest from pathlib import Path +from unittest import mock RUNNER_PATH = Path(__file__).with_name("capture-g7-runtime-evidence.py") @@ -103,6 +105,42 @@ class G7RuntimeEvidenceTests(unittest.TestCase): self.assertNotIn("secret", serialized) self.assertFalse(evidence["cookie_value_logged"]) + def test_public_fetch_uses_browser_compatible_user_agent(self) -> None: + captured_request = None + + class Response: + status = 200 + + def __enter__(self): + return self + + def __exit__(self, *_args): + return False + + def read(self, _limit): + return json.dumps(snapshot()).encode("utf-8") + + def urlopen(request, **_kwargs): + nonlocal captured_request + captured_request = request + return Response() + + with mock.patch.object(self.runner.urllib.request, "urlopen", urlopen): + result = self.runner.fetch_snapshot( + url="https://api.example.test/admin/voice-runtime", + cookie_name="sid", + cookie_value="secret", + timeout_seconds=5.0, + ) + + self.assertEqual("vignette.voice-runtime.v1", result["schema_version"]) + self.assertIsNotNone(captured_request) + headers = { + name.lower(): value for name, value in captured_request.header_items() + } + self.assertEqual(self.runner._BROWSER_UA, headers["user-agent"]) + self.assertEqual("application/json", headers["accept"]) + def test_worker_drift_fails_closed(self) -> None: first = snapshot() second = copy.deepcopy(first)