"""BROWSER_ALLOWED_HOSTS activation-candidate validation. The candidate value is the ALREADY-DEPLOYED Tinyproxy programming-domain set (deploy/egress/tinyproxy-filter) plus the already-present github.com / pypi.org / npmjs.org -- no invented domains. NOTE: the task text said "npmjs.com", but the deployed filter (tinyproxy-filter line 23, new-host-checklist.md, and docs/platform-browser.md) all say npmjs.org, so the candidate follows the DEPLOYED set (npmjs.org); npmjs.com is deliberately NOT in the candidate -- adding it would widen the allowlist beyond the proxy, breaking allowlist==proxy parity. If the npm website itself must be browsable, that is a separate filter change for the parent to decide, not something this round invents. Because the Tinyproxy lines are subdomain-inclusive (``^\\(.*\\.\\)*domain$``), the env value uses leading-dot entries, which are the allowlist encoding of the same subdomain-inclusive set; plain entries would NOT match subdomains (proven by test_plain_entries_would_break_subdomains) and would therefore regress docs.python.org etc. All checks run against fake browsers / the filter file itself: no production tenant, no network, no real credentials. """ import os import re import sys import tempfile import unittest sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) from playwright_browser import Browser, BrowserError # noqa: E402 from playwright_browser import _check_allowed_url # noqa: E402 # The activation candidate: the deployed Tinyproxy programming # set, encoded subdomain-inclusively. Domain SET is fixed by the # task (no new domains); only the encoding (dot prefix) follows # from the filter's own subdomain rule. CANDIDATE_DOMAINS = [ "python.org", "nodejs.org", "go.dev", "pkg.go.dev", "rust-lang.org", "developer.mozilla.org", "crates.io", "stackoverflow.com", "cve.mitre.org", "nvd.nist.gov", "github.com", "pypi.org", "npmjs.org", ] CANDIDATE = ",".join("." + d for d in CANDIDATE_DOMAINS) REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) FILTER_PATH = os.path.join(REPO_ROOT, "deploy", "egress", "tinyproxy-filter") class _FakePage: def __init__(self): self.url = "" self.calls = [] self.mouse = self def title(self): return "T" def goto(self, url, timeout=None, wait_until=None): self.calls.append(("goto", url)) self.url = url def locator(self, selector): raise AssertionError("no locator use in activation tests") def click(self, selector, timeout=None): self.calls.append(("click", selector)) def close(self): pass class _FakeHandle: def __init__(self, page): self._page = page def new_page(self, viewport=None): return self._page def close(self): pass class _FakePW: def __init__(self, page): self._page = page self.launch_count = 0 def start(self): return self @property def chromium(self): outer = self class _Chromium: def launch(self, **kwargs): outer.launch_count += 1 return _FakeHandle(outer._page) return _Chromium() def stop(self): pass class _EnvGuard: def setUp(self): self._old = os.environ.get("BROWSER_ALLOWED_HOSTS") def tearDown(self): if self._old is None: os.environ.pop("BROWSER_ALLOWED_HOSTS", None) else: os.environ["BROWSER_ALLOWED_HOSTS"] = self._old def _filter_domains(): """Domains covered by subdomain-inclusive filter lines.""" with open(FILTER_PATH, encoding="utf-8") as fh: lines = fh.read().splitlines() out = set() for line in lines: m = re.match(r"^\^\\\(\.\*\\.\\\)\*(.+)\$$", line) if m: out.add(m.group(1).replace("\\.", ".")) return out class ActivationCandidateTest(_EnvGuard, unittest.TestCase): def setUp(self): _EnvGuard.setUp(self) os.environ["BROWSER_ALLOWED_HOSTS"] = CANDIDATE self._tmp = tempfile.TemporaryDirectory() self.page = _FakePage() self.factory = _FakePW(self.page) self.browser = Browser(cwd=self._tmp.name, proxy="http://proxy:8888", browser_factory=lambda: self.factory) def tearDown(self): self._tmp.cleanup() _EnvGuard.tearDown(self) def test_candidate_matches_deployed_filter_set(self): # Every candidate domain has a subdomain-inclusive line # in the deployed filter; the candidate invents nothing. filt = _filter_domains() for domain in CANDIDATE_DOMAINS: self.assertIn(domain, filt, domain) def test_candidate_allows_bare_and_subdomains(self): for domain in CANDIDATE_DOMAINS: _check_allowed_url("https://%s/" % domain) _check_allowed_url("https://www.%s/docs" % domain) # The cases the filter explicitly exists for. _check_allowed_url("https://docs.python.org/3/") _check_allowed_url("https://docs.rust-lang.org/") _check_allowed_url("https://api.github.com/") _check_allowed_url("https://pypi.org/project/x/") def test_candidate_blocks_suffix_tricks_and_outsiders(self): for bad in ("https://notpython.org/", "https://python.org.evil.com/", "https://evilgithub.com/", "https://api.diante.me/", "https://www.amazon.com/", "https://mail.google.com/"): with self.assertRaises(BrowserError, msg=bad): _check_allowed_url(bad) def test_plain_entries_would_break_subdomains(self): # Why the candidate uses dot forms: plain "python.org" # does not match docs.python.org under the established # navigate semantics, so a plain-entry activation would # regress exactly the docs the filter was added for. os.environ["BROWSER_ALLOWED_HOSTS"] = ( "python.org,nodejs.org,go.dev,pkg.go.dev,rust-lang.org," "developer.mozilla.org,crates.io,stackoverflow.com," "cve.mitre.org,nvd.nist.gov,github.com,pypi.org,npmjs.org") with self.assertRaises(BrowserError): _check_allowed_url("https://docs.python.org/3/") def test_file_allowed_localhost_blocked_under_candidate(self): out = self.browser.navigate("file:///tmp/page.html") self.assertTrue(out["ok"]) with self.assertRaises(BrowserError): self.browser.navigate("http://localhost:8000/") # localhost blocking is a behaviour CHANGE vs unset: # local dev servers need an explicit localhost entry. self.assertEqual(self.factory.launch_count, 1) def test_task_level_navigate_allow_and_block(self): out = self.browser.navigate("https://docs.python.org/3/") self.assertTrue(out["ok"]) launches = self.factory.launch_count with self.assertRaises(BrowserError): self.browser.navigate("https://evil.com/") self.assertEqual(self.factory.launch_count, launches) def test_unset_means_no_change(self): os.environ.pop("BROWSER_ALLOWED_HOSTS", None) out = self.browser.navigate("https://anything.example/") self.assertTrue(out["ok"]) _check_allowed_url("https://anything.example/")