diff --git a/moli-benchmark/moli_benchmark/wpt_cross/case_set.py b/moli-benchmark/moli_benchmark/wpt_cross/case_set.py index 14aa0fd813..113d7743c1 100644 --- a/moli-benchmark/moli_benchmark/wpt_cross/case_set.py +++ b/moli-benchmark/moli_benchmark/wpt_cross/case_set.py @@ -865,12 +865,16 @@ def _script_handler_reference_patterns(directory: str) -> tuple[re.Pattern[str], @lru_cache(maxsize=None) def _service_worker_script_handler_reference_patterns(directory: str) -> tuple[re.Pattern[str], ...]: references = [] - for name in ( - "redirect.py", "update-worker.py", "import-scripts-version.py", - "import-scripts-get.py", "import-scripts-echo.py", - "subdir/import-scripts-echo.py", "scope2/import-scripts-echo.py", - ): - resource = "service-workers/service-worker/resources/" + name + resources = tuple( + "service-workers/service-worker/resources/" + name + for name in ( + "redirect.py", "update-worker.py", "update-worker-from-file.py", + "update-during-installation-worker.py", + "import-scripts-version.py", "import-scripts-get.py", "import-scripts-echo.py", + "subdir/import-scripts-echo.py", "scope2/import-scripts-echo.py", + ) + ) + ("service-workers/service-worker/ServiceWorkerGlobalScope/resources/update-worker.py",) + for resource in resources: relative = posixpath.relpath(resource, directory) references.extend(("/" + resource, relative, "./" + relative)) return tuple( diff --git a/moli-benchmark/moli_benchmark/wpt_cross/server.py b/moli-benchmark/moli_benchmark/wpt_cross/server.py index 3379ca9bed..6ca0c7d2f8 100644 --- a/moli-benchmark/moli_benchmark/wpt_cross/server.py +++ b/moli-benchmark/moli_benchmark/wpt_cross/server.py @@ -29,6 +29,8 @@ import http.client import json import math import mimetypes +import os +import random import re import socket import struct @@ -147,6 +149,9 @@ FETCH_INSPECT_HEADERS_PATH = "/fetch/api/resources/inspect-headers.py" SERVICE_WORKER_SCRIPT_RESOURCE_PATHS = { "/service-workers/service-worker/resources/redirect.py", "/service-workers/service-worker/resources/update-worker.py", + "/service-workers/service-worker/resources/update-worker-from-file.py", + "/service-workers/service-worker/resources/update-during-installation-worker.py", + "/service-workers/service-worker/ServiceWorkerGlobalScope/resources/update-worker.py", "/service-workers/service-worker/resources/import-scripts-version.py", "/service-workers/service-worker/resources/import-scripts-get.py", "/service-workers/service-worker/resources/import-scripts-echo.py", @@ -3603,7 +3608,33 @@ def _make_handler( ("Pragma", "no-cache"), ("Content-Type", "application/javascript"), ] - if path.endswith("/import-scripts-version.py"): + if path.endswith("/update-worker-from-file.py"): + count = fetch_stash.increment(params["Key"][0], path=parsed.path) + if count > 2: + self.send_error(500, "Unknown update worker state") + return + filename = os.fsdecode(params["First" if count == 1 else "Second"][0].encode("latin-1")) + source = ((wpt_root / path.lstrip("/")).parent / filename).resolve() + source.relative_to(wpt_root.resolve()) + body = source.read_bytes() + elif path.endswith("/ServiceWorkerGlobalScope/resources/update-worker.py"): + headers = [ + ("Cache-Control", "max-age: 0"), + ("Content-Type", "application/javascript"), + ] + source = (wpt_root / path.lstrip("/")).with_suffix(".js") + script = source.read_text(encoding="utf-8") + body = f"// {time.time()}\n{script}".encode("utf-8") + elif path.endswith("/update-during-installation-worker.py"): + headers = [ + ("Content-Type", "application/javascript"), + ("Cache-Control", "max-age=0"), + ] + body = ( + f"// {random.random()}\n" + "importScripts('update-during-installation-worker.js');" + ).encode("ascii") + elif path.endswith("/import-scripts-version.py"): # Match the upstream delay so update checks see new bytes. if stopping.wait(0.1): self.close_connection = True @@ -3653,6 +3684,9 @@ def _make_handler( if "\r" in value or "\n" in value: raise ValueError("invalid response header") value.encode("latin-1") + except OSError: + self.send_error(500) + return except (KeyError, ValueError, UnicodeError): self.send_error(400) return diff --git a/moli-benchmark/tests/test_wpt_service_worker_script_server.py b/moli-benchmark/tests/test_wpt_service_worker_script_server.py index fa88713089..19a1ecf8f0 100644 --- a/moli-benchmark/tests/test_wpt_service_worker_script_server.py +++ b/moli-benchmark/tests/test_wpt_service_worker_script_server.py @@ -2,6 +2,7 @@ from __future__ import annotations import re import tempfile +import time import unittest import uuid from concurrent.futures import ThreadPoolExecutor @@ -31,12 +32,17 @@ class ServiceWorkerScriptFixtureTests(unittest.TestCase): resources = self.root / DIRECTORY / "resources" resources.mkdir(parents=True) for name in ( - "redirect.py", "update-worker.py", "import-scripts-version.py", + "redirect.py", "update-worker.py", "update-worker-from-file.py", "import-scripts-version.py", + "update-during-installation-worker.py", "import-scripts-get.py", "import-scripts-echo.py", "subdir/import-scripts-echo.py", "scope2/import-scripts-echo.py", ): (resources / name).parent.mkdir(exist_ok=True) (resources / name).write_text("# Python source must not be sent as a script") + global_resources = self.root / DIRECTORY / "ServiceWorkerGlobalScope/resources" + global_resources.mkdir(parents=True) + (global_resources / "update-worker.py").write_text("# Python source must not be sent as a script") + (global_resources / "update-worker.js").write_bytes(b"// caf\xc3\xa9\r\nself.ready = true;\r\n") self.stack.enter_context(patch( "moli_benchmark.wpt_cross.server._global_ipv6_address", return_value=None )) @@ -49,7 +55,8 @@ class ServiceWorkerScriptFixtureTests(unittest.TestCase): ) -> tuple[int, list[tuple[str, str]], bytes]: connection = HTTPConnection("127.0.0.1", port, timeout=5) try: - connection.request(method, RESOURCES + resource + "?" + query) + path = resource if resource.startswith("/") else RESOURCES + resource + connection.request(method, path + "?" + query) response = connection.getresponse() return ( response.status, @@ -181,6 +188,85 @@ class ServiceWorkerScriptFixtureTests(unittest.TestCase): self.assertLess(versions[0], versions[1]) self.assertLess(versions[1], versions[2]) + def test_update_from_file_preserves_bytes_and_shares_only_its_own_stash(self) -> None: + first, second = self.server(), self.server() + resources = self.root / DIRECTORY / "resources" + before, after = b"// before\r\n\xff\n", b"// after\x00\n" + (resources / "café.js").write_bytes(before) + (resources / "after.js").write_bytes(after) + key = str(uuid.uuid4()) + query = urlencode([ + ("Key", key), ("First", "café.js"), ("First", "ignored.js"), + ("Second", "after.js"), + ]) + for port, expected in ( + (first.port, before), + (second.port, before), + (first.alternate_port, after), + ): + status, headers, body = self.request(port, "update-worker-from-file.py", query) + self.assertEqual((status, body), (200, expected)) + self.assertEqual(dict(headers)["content-type"], "application/javascript") + self.assertEqual(dict(headers)["cache-control"], "no-cache, must-revalidate") + self.assertEqual(dict(headers)["pragma"], "no-cache") + self.assertEqual(self.request(first.port, "update-worker-from-file.py", query)[0], 500) + self.assertEqual( + self.request(first.port, "update-worker.py", urlencode({"Key": key, "Mode": "normal"}))[2], + b"/* 1 */ ", + ) + head_query = urlencode({"Key": str(uuid.uuid4()), "First": "café.js", "Second": "after.js"}) + status, headers, body = self.request(first.port, "update-worker-from-file.py", head_query, method="HEAD") + self.assertEqual((status, dict(headers)["content-length"], body), (200, str(len(before)), b"")) + self.assertEqual(self.request(first.port, "update-worker-from-file.py", head_query, method="POST")[2], after) + failure_query = urlencode({"Key": str(uuid.uuid4()), "First": "missing.js", "Second": "after.js"}) + self.assertEqual(self.request(first.port, "update-worker-from-file.py", failure_query)[0], 500) + self.assertEqual(self.request(first.port, "update-worker-from-file.py", failure_query)[2], after) + + def test_global_scope_update_script_uses_timestamp_and_original_text(self) -> None: + server = self.server() + resource = "/" + DIRECTORY + "/ServiceWorkerGlobalScope/resources/update-worker.py" + script = "// café\nself.ready = true;\n".encode("utf-8") + versions = [] + for method in ("GET", "POST", "OPTIONS", "HEAD"): + before = time.time() + status, headers, body = self.request(server.port, resource, method=method) + after = time.time() + self.assertEqual(status, 200) + self.assertEqual(dict(headers)["cache-control"], "max-age: 0") + self.assertEqual(dict(headers)["content-type"], "application/javascript") + self.assertNotIn("pragma", dict(headers)) + if method == "HEAD": + self.assertEqual(body, b"") + self.assertGreater(int(dict(headers)["content-length"]), len(script)) + continue + stamp, source = body.split(b"\n", 1) + self.assertEqual(source, script) + self.assertTrue(stamp.startswith(b"// ")) + versions.append(float(stamp[3:])) + self.assertLessEqual(before, versions[-1]) + self.assertLessEqual(versions[-1], after) + self.assertEqual(versions, sorted(set(versions))) + + def test_installation_update_script_changes_without_rewriting_its_import(self) -> None: + server = self.server() + versions = [] + for method in ("GET", "POST", "OPTIONS", "HEAD"): + status, headers, body = self.request(server.port, "update-during-installation-worker.py", method=method) + self.assertEqual(status, 200) + self.assertEqual(dict(headers)["content-type"], "application/javascript") + self.assertEqual(dict(headers)["cache-control"], "max-age=0") + self.assertNotIn("pragma", dict(headers)) + if method == "HEAD": + self.assertEqual(body, b"") + continue + version, script = body.split(b"\n", 1) + self.assertEqual(script, b"importScripts('update-during-installation-worker.js');") + self.assertTrue(version.startswith(b"// ")) + versions.append(float(version[3:])) + self.assertGreaterEqual(versions[-1], 0) + self.assertLess(versions[-1], 1) + self.assertEqual(len(set(versions)), 3) + def test_imported_assignments_preserve_raw_parameters_and_directory(self) -> None: server = self.server() for resource, query, expected in ( @@ -212,6 +298,8 @@ class ServiceWorkerScriptFixtureTests(unittest.TestCase): ("update-worker.py", "Mode=normal"), ("update-worker.py", "Key=invalid&Mode=normal"), ("update-worker.py", "Key=" + str(uuid.uuid4())), + ("update-worker-from-file.py", "First=x&Second=y"), + ("update-worker-from-file.py", urlencode({"Key": str(uuid.uuid4()), "First": "../../../../outside.js"})), ("import-scripts-get.py", "output=x"), ("import-scripts-get.py", "msg=x"), ("import-scripts-echo.py", ""), @@ -223,6 +311,10 @@ class ServiceWorkerScriptFixtureTests(unittest.TestCase): cases = { "redirect-relative.html": ("resources/redirect.py", True), "update-absolute.html": (RESOURCES + "update-worker.py", True), + "update-from-file.html": ("resources/update-worker-from-file.py", True), + "update-installing.html": ("resources/update-during-installation-worker.py", True), + "ServiceWorkerGlobalScope/update.html": ("resources/update-worker.py", True), + "ServiceWorkerGlobalScope/update-wrong.html": ("resources/update-worker-from-file.py", False), "sub/version-relative.html": ("../resources/import-scripts-version.py", True), "sub/redirect-dot-relative.html": ("./../resources/redirect.py", True), "sub/redirect-wrong-relative.html": ("resources/redirect.py", False),