Add support for PAC (per-test proxy configuration) (#34145)
Tests can now delcare that they use a PAC file
(Proxy Auto-Configuration, see https://developer.mozilla.org/en-US/docs/Web/HTTP/Proxy_servers_and_tunneling/Proxy_Auto-Configuration_PAC_file)
The syntax:
`<meta name="pac" content="relative/url/to/pac-file.js">`
See RFC112
diff --git a/infrastructure/metadata/infrastructure/server/test-pac.html.ini b/infrastructure/metadata/infrastructure/server/test-pac.html.ini
new file mode 100644
index 0000000..26ea697
--- /dev/null
+++ b/infrastructure/metadata/infrastructure/server/test-pac.html.ini
@@ -0,0 +1,4 @@
+[test-pac.html]
+ [test that PAC metadata is respected]
+ expected:
+ if product == "safari": FAIL # Safari WebDriver does not support PAC
\ No newline at end of file
diff --git a/infrastructure/resources/ok.txt b/infrastructure/resources/ok.txt
new file mode 100644
index 0000000..a0aba93
--- /dev/null
+++ b/infrastructure/resources/ok.txt
@@ -0,0 +1 @@
+OK
\ No newline at end of file
diff --git a/infrastructure/resources/ok.txt.headers b/infrastructure/resources/ok.txt.headers
new file mode 100644
index 0000000..23de552
--- /dev/null
+++ b/infrastructure/resources/ok.txt.headers
@@ -0,0 +1 @@
+Access-Control-Allow-Origin: *
\ No newline at end of file
diff --git a/infrastructure/server/resources/proxy.sub.pac b/infrastructure/server/resources/proxy.sub.pac
new file mode 100644
index 0000000..78ce023
--- /dev/null
+++ b/infrastructure/server/resources/proxy.sub.pac
@@ -0,0 +1,7 @@
+function FindProxyForURL(url, host) {
+ if (dnsDomainIs(host, '.wpt.test')) {
+ return "PROXY 127.0.0.1:{{ports[http][0]}}"
+ }
+
+ return "DIRECT";
+}
diff --git a/infrastructure/server/test-pac.html b/infrastructure/server/test-pac.html
new file mode 100644
index 0000000..598836d
--- /dev/null
+++ b/infrastructure/server/test-pac.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML>
+<title>test behavior of PROXY configuration (PAC)</title>
+<meta name="pac" content="resources/proxy.sub.pac">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+ promise_test(async t => {
+ const response = await fetch('http://not-a-real-domain.wpt.test/infrastructure/resources/ok.txt');
+ const text = await response.text();
+ assert_equals(text, 'OK');
+ }, 'test that PAC metadata is respected');
+</script>
diff --git a/tools/manifest/item.py b/tools/manifest/item.py
index ec61c9e..02a72ee 100644
--- a/tools/manifest/item.py
+++ b/tools/manifest/item.py
@@ -189,6 +189,11 @@
return self._extras.get("timeout")
@property
+ def pac(self):
+ # type: () -> Optional[Text]
+ return self._extras.get("pac")
+
+ @property
def testdriver(self):
# type: () -> Optional[Text]
return self._extras.get("testdriver")
@@ -208,6 +213,8 @@
rv = super().to_json()
if self.timeout is not None:
rv[-1]["timeout"] = self.timeout
+ if self.pac is not None:
+ rv[-1]["pac"] = self.pac
if self.testdriver:
rv[-1]["testdriver"] = self.testdriver
if self.jsshell:
diff --git a/tools/manifest/sourcefile.py b/tools/manifest/sourcefile.py
index 88da179..3919b5a 100644
--- a/tools/manifest/sourcefile.py
+++ b/tools/manifest/sourcefile.py
@@ -478,6 +478,14 @@
return self.root.findall(".//{http://www.w3.org/1999/xhtml}meta[@name='timeout']")
@cached_property
+ def pac_nodes(self):
+ # type: () -> List[ElementTree.Element]
+ """List of ElementTree Elements corresponding to nodes in a test that
+ specify PAC (proxy auto-config)"""
+ assert self.root is not None
+ return self.root.findall(".//{http://www.w3.org/1999/xhtml}meta[@name='pac']")
+
+ @cached_property
def script_metadata(self):
# type: () -> Optional[List[Tuple[Text, Text]]]
if self.name_is_worker or self.name_is_multi_global or self.name_is_window:
@@ -510,6 +518,23 @@
return None
@cached_property
+ def pac(self):
+ # type: () -> Optional[Text]
+ """The PAC (proxy config) of a test or reference file. A URL or null"""
+ if self.script_metadata:
+ for (meta, content) in self.script_metadata:
+ if meta == 'pac':
+ return content
+
+ if self.root is None:
+ return None
+
+ if self.pac_nodes:
+ return self.pac_nodes[0].attrib.get("content", None)
+
+ return None
+
+ @cached_property
def viewport_nodes(self):
# type: () -> List[ElementTree.Element]
"""List of ElementTree Elements corresponding to nodes in a test that
@@ -1008,6 +1033,7 @@
self.url_base,
global_variant_url(self.rel_url, suffix) + variant,
timeout=self.timeout,
+ pac=self.pac,
jsshell=jsshell,
script_metadata=self.script_metadata
)
@@ -1025,6 +1051,7 @@
self.url_base,
test_url + variant,
timeout=self.timeout,
+ pac=self.pac,
script_metadata=self.script_metadata
)
for variant in self.test_variants
@@ -1040,6 +1067,7 @@
self.url_base,
test_url + variant,
timeout=self.timeout,
+ pac=self.pac,
script_metadata=self.script_metadata
)
for variant in self.test_variants
@@ -1066,6 +1094,7 @@
self.url_base,
url,
timeout=self.timeout,
+ pac=self.pac,
testdriver=testdriver,
script_metadata=self.script_metadata
))
diff --git a/tools/manifest/tests/test_manifest.py b/tools/manifest/tests/test_manifest.py
index 2a38d23..a7f3d31 100644
--- a/tools/manifest/tests/test_manifest.py
+++ b/tools/manifest/tests/test_manifest.py
@@ -295,7 +295,7 @@
m = manifest.Manifest.from_json("/", json_str)
# Update it with timeout="long"
- s2 = SourceFileWithTest("test1", "1"*40, item.TestharnessTest, timeout="long")
+ s2 = SourceFileWithTest("test1", "1"*40, item.TestharnessTest, timeout="long", pac="proxy.pac")
tree, sourcefile_mock = tree_and_sourcefile_mocks([(s2, None, True)])
with mock.patch("tools.manifest.manifest.SourceFile", side_effect=sourcefile_mock):
m.update(tree)
@@ -303,7 +303,7 @@
assert json_str == {
'items': {'testharness': {'test1': [
"1"*40,
- (None, {'timeout': 'long'})
+ (None, {'timeout': 'long', 'pac': 'proxy.pac'})
]}},
'url_base': '/',
'version': 8
diff --git a/tools/manifest/tests/test_sourcefile.py b/tools/manifest/tests/test_sourcefile.py
index 98a1783..c0b281d 100644
--- a/tools/manifest/tests/test_sourcefile.py
+++ b/tools/manifest/tests/test_sourcefile.py
@@ -849,7 +849,6 @@
assert s.content_is_ref_node
assert s.fuzzy == expected
-
@pytest.mark.parametrize("fuzzy, expected", [
([b"1;200"], {None: [[1, 1], [200, 200]]}),
([b"ref-2.html:0-1;100-200"], {("/foo/test.html", "/foo/ref-2.html", "=="): [[0, 1], [100, 200]]}),
@@ -868,6 +867,15 @@
assert s.content_is_ref_node
assert s.fuzzy == expected
+@pytest.mark.parametrize("pac, expected", [
+ (b"proxy.pac", "proxy.pac")])
+def test_pac(pac, expected):
+ content = b"""
+<meta name=pac content="%s">
+""" % pac
+
+ s = create("foo/test.html", content)
+ assert s.pac == expected
@pytest.mark.parametrize("page_ranges, expected", [
(b"1-2", [[1, 2]]),
diff --git a/tools/wptrunner/wptrunner/browsers/base.py b/tools/wptrunner/wptrunner/browsers/base.py
index 4f73efb..65db5f6 100644
--- a/tools/wptrunner/wptrunner/browsers/base.py
+++ b/tools/wptrunner/wptrunner/browsers/base.py
@@ -159,6 +159,9 @@
log. Returns a boolean indicating whether a crash occured."""
return False
+ @property
+ def pac(self):
+ return None
class NullBrowser(Browser):
def __init__(self, logger, **kwargs):
@@ -289,7 +292,7 @@
def __init__(self, logger, binary=None, webdriver_binary=None,
webdriver_args=None, host="127.0.0.1", port=None, base_path="/",
- env=None, **kwargs):
+ env=None, supports_pac=True, **kwargs):
super().__init__(logger)
if webdriver_binary is None:
@@ -302,6 +305,7 @@
self.host = host
self._port = port
+ self._supports_pac = supports_pac
self.base_path = base_path
self.env = os.environ.copy() if env is None else env
@@ -312,6 +316,7 @@
self._output_handler = None
self._cmd = None
self._proc = None
+ self._pac = None
def make_command(self):
"""Returns the full command for starting the server process as a list."""
@@ -400,4 +405,13 @@
def executor_browser(self):
return ExecutorBrowser, {"webdriver_url": self.url,
"host": self.host,
- "port": self.port}
+ "port": self.port,
+ "pac": self.pac}
+
+ def settings(self, test):
+ self._pac = test.environment.get("pac", None) if self._supports_pac else None
+ return {"pac": self._pac}
+
+ @property
+ def pac(self):
+ return self._pac
diff --git a/tools/wptrunner/wptrunner/browsers/safari.py b/tools/wptrunner/wptrunner/browsers/safari.py
index 7aed3a4..ba533f4 100644
--- a/tools/wptrunner/wptrunner/browsers/safari.py
+++ b/tools/wptrunner/wptrunner/browsers/safari.py
@@ -157,6 +157,7 @@
webdriver_binary,
webdriver_args=webdriver_args,
port=None,
+ supports_pac=False,
env=env)
if "/" not in webdriver_binary:
diff --git a/tools/wptrunner/wptrunner/executors/executormarionette.py b/tools/wptrunner/wptrunner/executors/executormarionette.py
index 418b6bf..051821c 100644
--- a/tools/wptrunner/wptrunner/executors/executormarionette.py
+++ b/tools/wptrunner/wptrunner/executors/executormarionette.py
@@ -780,6 +780,16 @@
self.executor.original_pref_values[name] = self.prefs.get(name)
self.prefs.set(name, value)
+ pac = new_environment.get("pac", None)
+
+ if pac != old_environment.get("pac", None):
+ if pac is None:
+ self.prefs.clear("network.proxy.type")
+ self.prefs.clear("network.proxy.autoconfig_url")
+ else:
+ self.prefs.set("network.proxy.type", 2)
+ self.prefs.set("network.proxy.autoconfig_url",
+ urljoin(self.executor.server_url("http"), pac))
class ExecuteAsyncScriptRun(TimedRunner):
def set_timeout(self):
diff --git a/tools/wptrunner/wptrunner/executors/executorwebdriver.py b/tools/wptrunner/wptrunner/executors/executorwebdriver.py
index 9fbaf2b..4ba78b2 100644
--- a/tools/wptrunner/wptrunner/executors/executorwebdriver.py
+++ b/tools/wptrunner/wptrunner/executors/executorwebdriver.py
@@ -350,6 +350,18 @@
self.capabilities = browser.capabilities
else:
merge_dicts(self.capabilities, browser.capabilities)
+
+ pac = browser.pac
+ if pac is not None:
+ if self.capabilities is None:
+ self.capabilities = {}
+ merge_dicts(self.capabilities, {"proxy":
+ {
+ "proxyType": "pac",
+ "proxyAutoconfigUrl": urljoin(executor.server_url("http"), pac)
+ }
+ })
+
self.url = browser.webdriver_url
self.webdriver = None
diff --git a/tools/wptrunner/wptrunner/wpttest.py b/tools/wptrunner/wptrunner/wpttest.py
index 96fc108..e40a535 100644
--- a/tools/wptrunner/wptrunner/wpttest.py
+++ b/tools/wptrunner/wptrunner/wpttest.py
@@ -211,12 +211,13 @@
result_cls = None # type: ClassVar[Type[Result]]
subtest_result_cls = None # type: ClassVar[Type[SubtestResult]]
test_type = None # type: ClassVar[str]
+ pac = None
default_timeout = 10 # seconds
long_timeout = 60 # seconds
def __init__(self, url_base, tests_root, url, inherit_metadata, test_metadata,
- timeout=None, path=None, protocol="http", subdomain=False):
+ timeout=None, path=None, protocol="http", subdomain=False, pac=None):
self.url_base = url_base
self.tests_root = tests_root
self.url = url
@@ -229,6 +230,9 @@
"protocol": protocol,
"prefs": self.prefs}
+ if pac is not None:
+ self.environment["pac"] = urljoin(self.url, pac)
+
def __eq__(self, other):
if not isinstance(other, Test):
return False
@@ -458,9 +462,9 @@
def __init__(self, url_base, tests_root, url, inherit_metadata, test_metadata,
timeout=None, path=None, protocol="http", testdriver=False,
- jsshell=False, scripts=None, subdomain=False):
+ jsshell=False, scripts=None, subdomain=False, pac=None):
Test.__init__(self, url_base, tests_root, url, inherit_metadata, test_metadata, timeout,
- path, protocol, subdomain)
+ path, protocol, subdomain, pac)
self.testdriver = testdriver
self.jsshell = jsshell
@@ -469,6 +473,7 @@
@classmethod
def from_manifest(cls, manifest_file, manifest_item, inherit_metadata, test_metadata):
timeout = cls.long_timeout if manifest_item.timeout == "long" else cls.default_timeout
+ pac = manifest_item.pac
testdriver = manifest_item.testdriver if hasattr(manifest_item, "testdriver") else False
jsshell = manifest_item.jsshell if hasattr(manifest_item, "jsshell") else False
script_metadata = manifest_item.script_metadata or []
@@ -480,6 +485,7 @@
inherit_metadata,
test_metadata,
timeout=timeout,
+ pac=pac,
path=os.path.join(manifest_file.tests_root, manifest_item.path),
protocol=server_protocol(manifest_item),
testdriver=testdriver,