Support credentials mode in <script type=webbundle>
The current <script>-based API doesn't allow to specify a credential
mode, while <link>-based APIs allowed that by its `crossorigin`
attribute. This is one of the gaps between two APIs.
This CL supports credentials for <script type=webbundle>. Now <script
type=webbundle> can have a `credentials` to specify a request's
credentials mode, such as:
<script type="webbundle">
{
source: "subresources.wbn",
credentials: "omit",
resources: ["a.js", "b.js", "c.png"],
}
</script>
Regarding 're-using webbundle resources', we also check an equality of
credentials mode of two script elements, in addition to their bundle
URLs so that we don't re-use webbundle resources wrongly. See [1] for
details.
The related spec and explainer are:
- The spec issue is https://github.com/WICG/webpackage/issues/692.
- PR is https://github.com/WICG/webpackage/pull/694, which was already
merged.
- Updataed Explainer:
https://github.com/WICG/webpackage/blob/main/explainers/subresource-loading.md#requests-mode-and-credentials-mode
[1] https://docs.google.com/document/d/1q_SodTcLuwya4cXt1gIRaVrkiaBfwWyPvkY1fqRKkgM/edit?resourcekey=0-dqrFOGVCYsg8WRZ4RFgwuw#heading=h.mnfqu6560twe
Bug: 1262005
Change-Id: I897e272962219ae10adf3dcd9c0c0e689b6c1d7b
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3234563
Reviewed-by: Kunihiko Sakamoto <ksakamoto@chromium.org>
Reviewed-by: Takashi Toyoshima <toyoshim@chromium.org>
Reviewed-by: Hiroshige Hayashizaki <hiroshige@chromium.org>
Commit-Queue: Hayato Ito <hayato@chromium.org>
Cr-Commit-Position: refs/heads/main@{#934410}
diff --git a/web-bundle/resources/check-cookie-and-return-bundle.py b/web-bundle/resources/check-cookie-and-return-bundle.py
new file mode 100644
index 0000000..89a22c4
--- /dev/null
+++ b/web-bundle/resources/check-cookie-and-return-bundle.py
@@ -0,0 +1,29 @@
+import os
+
+
+def main(request, response):
+ origin = request.headers.get(b"origin")
+
+ if origin is not None:
+ response.headers.set(b"Access-Control-Allow-Origin", origin)
+ response.headers.set(b"Access-Control-Allow-Methods", b"GET")
+ response.headers.set(b"Access-Control-Allow-Credentials", b"true")
+
+ headers = [
+ (b"Content-Type", b"application/webbundle"),
+ (b"X-Content-Type-Options", b"nosniff"),
+ ]
+
+ cookie = request.cookies.first(b"milk", None)
+ if (cookie is not None) and cookie.value == b"1":
+ if request.GET.get(b"bundle", None) == b"cross-origin":
+ bundle = "./wbn/simple-cross-origin.wbn"
+ else:
+ bundle = "./wbn/subresource.wbn"
+ with open(
+ os.path.join(os.path.dirname(__file__), bundle),
+ "rb",
+ ) as f:
+ return (200, headers, f.read())
+ else:
+ return (400, [], "")
diff --git a/web-bundle/resources/generate-test-wbns.sh b/web-bundle/resources/generate-test-wbns.sh
index d9b7f4e..f80a265 100755
--- a/web-bundle/resources/generate-test-wbns.sh
+++ b/web-bundle/resources/generate-test-wbns.sh
@@ -156,3 +156,8 @@
-version b2 \
-har uuid-in-package.har \
-o wbn/uuid-in-package.wbn
+
+gen-bundle \
+ -version b2 \
+ -har simple-cross-origin.har \
+ -o wbn/simple-cross-origin.wbn
diff --git a/web-bundle/resources/simple-cross-origin.har b/web-bundle/resources/simple-cross-origin.har
new file mode 100644
index 0000000..8566a6c
--- /dev/null
+++ b/web-bundle/resources/simple-cross-origin.har
@@ -0,0 +1,29 @@
+{
+ "log": {
+ "entries": [
+ {
+ "request": {
+ "method": "GET",
+ "url": "https://www1.web-platform.test:8444/web-bundle/resources/wbn/simple-cross-origin.txt",
+ "headers": []
+ },
+ "response": {
+ "status": 200,
+ "headers": [
+ {
+ "name": "Content-type",
+ "value": "text/plain"
+ },
+ {
+ "name": "Access-Control-Allow-Origin",
+ "value": "*"
+ }
+ ],
+ "content": {
+ "text": "hello from simple-cross-origin.txt"
+ }
+ }
+ }
+ ]
+ }
+}
diff --git a/web-bundle/resources/test-helpers.js b/web-bundle/resources/test-helpers.js
index b512937..c1ff011 100644
--- a/web-bundle/resources/test-helpers.js
+++ b/web-bundle/resources/test-helpers.js
@@ -158,9 +158,11 @@
}
const script = document.createElement("script");
script.type = "webbundle";
- script.textContent =
- JSON.stringify({"source": url, "resources": resources});
- // TODO(crbug.com/1245166): Support |options.crossOrigin|.
+ const json_rule = {"source": url, "resources": resources};
+ if (options && options.credentials) {
+ json_rule.credentials = options.credentials;
+ }
+ script.textContent = JSON.stringify(json_rule);
// TODO(crbug.com/1245166): Support |options.scopes|.
return script;
}
diff --git a/web-bundle/resources/wbn/simple-cross-origin.wbn b/web-bundle/resources/wbn/simple-cross-origin.wbn
new file mode 100644
index 0000000..a00cce0
--- /dev/null
+++ b/web-bundle/resources/wbn/simple-cross-origin.wbn
Binary files differ
diff --git a/web-bundle/subresource-loading/script-credentials.https.tentative.sub.html b/web-bundle/subresource-loading/script-credentials.https.tentative.sub.html
new file mode 100644
index 0000000..a1c8285
--- /dev/null
+++ b/web-bundle/subresource-loading/script-credentials.https.tentative.sub.html
@@ -0,0 +1,151 @@
+<!DOCTYPE html>
+<title>
+ Credentials in WebBundle subresource loading
+</title>
+<link
+ rel="help"
+ href="https://github.com/WICG/webpackage/blob/main/explainers/subresource-loading.md#requests-mode-and-credentials-mode"
+/>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/test-helpers.js"></script>
+<body>
+ <script>
+ // In this wpt, we test a request's credential mode, which controls
+ // whether UA sends a credential or not to fetch a bundle.
+
+ // If UA sends a credential, check-cookie-and-return-{cross-oriigin}-bundle.py
+ // returns a valid format webbundle. Then, a subresource fetch should be successful.
+ // Otherwise, a subresource fetch should be rejected.
+
+ window.TEST_WEB_BUNDLE_ELEMENT_TYPE = "script";
+ setup(() => {
+ assert_true(HTMLScriptElement.supports("webbundle"));
+ });
+
+ document.cookie = "milk=1; path=/";
+
+ // Make sure to set a cookie for a cross-origin domain from where a cross
+ // origin bundle is served.
+ const setCookiePromise = fetch(
+ "https://{{domains[www1]}}:{{ports[https][0]}}/cookies/resources/set-cookie.py?name=milk&path=/web-bundle/resources/",
+ {
+ mode: "no-cors",
+ credentials: "include",
+ }
+ );
+
+ const same_origin_bundle = "../resources/check-cookie-and-return-bundle.py";
+ const cross_origin_bundle = "https://{{domains[www1]}}:{{ports[https][0]}}/web-bundle/resources/check-cookie-and-return-bundle.py?bundle=cross-origin";
+
+ const same_origin_bundle_subresource = "../resources/wbn/root.js";
+ const cross_origin_bundle_subresource = "https://{{domains[www1]}}:{{ports[https][0]}}/web-bundle/resources/wbn/simple-cross-origin.txt";
+
+ async function assertSubresourceCanBeFetched() {
+ const response = await fetch(same_origin_bundle_subresource);
+ const text = await response.text();
+ assert_equals(text, "export * from './submodule.js';\n");
+ }
+
+ async function assertCrossOriginSubresourceCanBeFetched() {
+ const response = await fetch(cross_origin_bundle_subresource);
+ const text = await response.text();
+ assert_equals(text, "hello from simple-cross-origin.txt");
+ }
+
+ function createScriptWebBundle(credentials) {
+ const options = {};
+ if (credentials) {
+ options.credentials = credentials;
+ }
+ return createWebBundleElement(same_origin_bundle, [same_origin_bundle_subresource], options);
+ }
+
+ function createScriptWebBundleCrossOrigin(credentials) {
+ const options = {};
+ if (credentials) {
+ options.credentials = credentials;
+ }
+ return createWebBundleElement(cross_origin_bundle, [cross_origin_bundle_subresource], options);
+ }
+
+ promise_test(async (t) => {
+ const script = createScriptWebBundle();
+ document.body.append(script);
+ t.add_cleanup(() => script.remove());
+
+ await assertSubresourceCanBeFetched();
+ }, "The default should send a credential to a same origin bundle");
+
+ promise_test(async (t) => {
+ const script = createScriptWebBundle("invalid");
+ document.body.append(script);
+ t.add_cleanup(() => script.remove());
+
+ await assertSubresourceCanBeFetched();
+ }, "An invalid value should send a credential to a same origin bundle");
+
+ promise_test(async (t) => {
+ const script = createScriptWebBundle("omit");
+ document.body.append(script);
+ t.add_cleanup(() => script.remove());
+
+ return promise_rejects_js(t, TypeError, fetch(same_origin_bundle_subresource))
+ }, "'omit' should not send a credential to a same origin bundle");
+
+ promise_test(async (t) => {
+ const script = createScriptWebBundle("same-origin");
+ document.body.append(script);
+ t.add_cleanup(() => script.remove());
+
+ await assertSubresourceCanBeFetched();
+ }, "'same-origin' should send a credential to a same origin bundle");
+
+ promise_test(async (t) => {
+ const script = createScriptWebBundle("include");
+ document.body.append(script);
+ t.add_cleanup(() => script.remove());
+
+ await assertSubresourceCanBeFetched();
+ }, "'include' should send a credential to a same origin bundle");
+
+ promise_test(async (t) => {
+ await setCookiePromise;
+
+ const script = createScriptWebBundleCrossOrigin("omit");
+ document.body.append(script);
+ t.add_cleanup(() => script.remove());
+
+ return promise_rejects_js(t, TypeError, fetch(cross_origin_bundle_subresource))
+ }, "'omit' should not send a credential to a cross origin bundle");
+
+ promise_test(async (t) => {
+ await setCookiePromise;
+
+ const script = createScriptWebBundleCrossOrigin("same-origin");
+ document.body.append(script);
+ t.add_cleanup(() => script.remove());
+
+ return promise_rejects_js(t, TypeError, fetch(cross_origin_bundle_subresource))
+ }, "'same-origin' should not send a credential to a cross origin bundle");
+
+ promise_test(async (t) => {
+ await setCookiePromise;
+
+ const script = createScriptWebBundleCrossOrigin("include");
+ document.body.append(script);
+ t.add_cleanup(() => script.remove());
+
+ await assertCrossOriginSubresourceCanBeFetched();
+ }, "'include' should send a credential to a cross origin bundle");
+
+
+ promise_test(async (t) => {
+ const script = createScriptWebBundleCrossOrigin("invalid");
+ document.body.append(script);
+ t.add_cleanup(() => script.remove());
+
+ return promise_rejects_js(t, TypeError, fetch(cross_origin_bundle_subresource))
+ }, "An invalid value should not send a credential to a cross origin bundle");
+ </script>
+</body>
diff --git a/web-bundle/subresource-loading/script-reuse-web-bundle-resource.https.tentative.html b/web-bundle/subresource-loading/script-reuse-web-bundle-resource.https.tentative.html
index e07ee7b..7429ae0 100644
--- a/web-bundle/subresource-loading/script-reuse-web-bundle-resource.https.tentative.html
+++ b/web-bundle/subresource-loading/script-reuse-web-bundle-resource.https.tentative.html
@@ -52,8 +52,8 @@
return createWebBundleElement(wbn_url, /*resources=*/ [resource1]);
}
- function createScriptWebBundle2() {
- return createWebBundleElement(wbn_url, /*resources=*/ [resource2]);
+ function createScriptWebBundle2(options) {
+ return createWebBundleElement(wbn_url, /*resources=*/ [resource2], /*options=*/ options);
}
async function appendScriptWebBundle1AndFetchResource1() {
@@ -110,6 +110,51 @@
await appendScriptWebBundle1AndFetchResource1();
clearWebBundleFetchCount();
+ // Remove script1, then append script2 with an explicit 'same-origin' credentials mode.
+ script1.remove();
+ script2 = createScriptWebBundle2({ credentials: "same-origin" });
+ document.body.append(script2);
+
+ await assertResource1CanNotBeFetched();
+ await assertResource2CanBeFetched();
+ assert_equals(webBundleFetchCount(), 0);
+ }, "Should reuse webbundle resources if a credential mode is same");
+
+ promise_test(async (t) => {
+ t.add_cleanup(cleanUp);
+ await appendScriptWebBundle1AndFetchResource1();
+ clearWebBundleFetchCount();
+
+ // Remove script1, then append script2 with a different credentials mode.
+ script1.remove();
+ script2 = createScriptWebBundle2({ credentials: "omit" });
+ document.body.append(script2);
+
+ await assertResource1CanNotBeFetched();
+ await assertResource2CanBeFetched();
+ assert_equals(webBundleFetchCount(), 1);
+ }, "Should not reuse webbundle resources if a credentials mode is different (same-origin vs omit)");
+
+ promise_test(async (t) => {
+ t.add_cleanup(cleanUp);
+ await appendScriptWebBundle1AndFetchResource1();
+ clearWebBundleFetchCount();
+
+ // Remove script1, then append script2 with a different credentials mode.
+ script1.remove();
+ script2 = createScriptWebBundle2({ credentials: "include" });
+ document.body.append(script2);
+
+ await assertResource1CanNotBeFetched();
+ await assertResource2CanBeFetched();
+ assert_equals(webBundleFetchCount(), 1);
+ }, "Should not reuse webbundle resources if a credential mode is different (same-origin vs include");
+
+ promise_test(async (t) => {
+ t.add_cleanup(cleanUp);
+ await appendScriptWebBundle1AndFetchResource1();
+ clearWebBundleFetchCount();
+
// Remove script1, then append the removed one.
script1.remove();
document.body.append(script1);