Skip to content

Commit

Permalink
Support credentials mode in <script type=webbundle>
Browse files Browse the repository at this point in the history
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 WICG/webpackage#692.
- PR is WICG/webpackage#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 <[email protected]>
Reviewed-by: Takashi Toyoshima <[email protected]>
Reviewed-by: Hiroshige Hayashizaki <[email protected]>
Commit-Queue: Hayato Ito <[email protected]>
Cr-Commit-Position: refs/heads/main@{#934410}
  • Loading branch information
hayatoito authored and chromium-wpt-export-bot committed Oct 25, 2021
1 parent 4287b41 commit 590d697
Show file tree
Hide file tree
Showing 7 changed files with 266 additions and 5 deletions.
29 changes: 29 additions & 0 deletions web-bundle/resources/check-cookie-and-return-bundle.py
Original file line number Diff line number Diff line change
@@ -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, [], "")
5 changes: 5 additions & 0 deletions web-bundle/resources/generate-test-wbns.sh
Original file line number Diff line number Diff line change
Expand Up @@ -156,3 +156,8 @@ gen-bundle \
-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
29 changes: 29 additions & 0 deletions web-bundle/resources/simple-cross-origin.har
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
]
}
}
8 changes: 5 additions & 3 deletions web-bundle/resources/test-helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -158,9 +158,11 @@ function createWebBundleElement(url, resources, options) {
}
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;
}
Expand Down
Binary file added web-bundle/resources/wbn/simple-cross-origin.wbn
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -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>
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -105,6 +105,51 @@
assert_equals(webBundleFetchCount(), 0);
}, "'remove(), then append()' should reuse webbundle resources");

promise_test(async (t) => {
t.add_cleanup(cleanUp);
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();
Expand Down

0 comments on commit 590d697

Please sign in to comment.