Bug 1452562 [wpt PR 10348] - Allow range headers to pass through a service worker, a=testonly

Automatic update from web-platform-testsAllow range headers to pass through a service worker (#10348)

Tests for https://github.com/whatwg/fetch/pull/560

--

wpt-commits: fb6d16d92af29262b6137b79e61f0c4b136c6ac1
wpt-pr: 10348
This commit is contained in:
Jake Archibald 2018-06-06 16:35:52 +00:00 коммит произвёл James Graham
Родитель 71986e71e5
Коммит dce65063fa
11 изменённых файлов: 592 добавлений и 4 удалений

Просмотреть файл

@ -273616,6 +273616,36 @@
{}
]
],
"fetch/range/resources/basic.html": [
[
{}
]
],
"fetch/range/resources/long-wav.py": [
[
{}
]
],
"fetch/range/resources/partial-script.py": [
[
{}
]
],
"fetch/range/resources/range-sw.js": [
[
{}
]
],
"fetch/range/resources/stash-take.py": [
[
{}
]
],
"fetch/range/resources/utils.js": [
[
{}
]
],
"fetch/sec-metadata/README.md": [
[
{}
@ -330610,6 +330640,28 @@
{}
]
],
"fetch/range/general.any.js": [
[
"/fetch/range/general.any.html",
{}
],
[
"/fetch/range/general.any.worker.html",
{}
]
],
"fetch/range/partial-script.window.js": [
[
"/fetch/range/partial-script.window.html",
{}
]
],
"fetch/range/sw.https.window.js": [
[
"/fetch/range/sw.https.window.html",
{}
]
],
"fetch/sec-metadata/fetch.tentative.https.sub.html": [
[
"/fetch/sec-metadata/fetch.tentative.https.sub.html",
@ -559765,6 +559817,42 @@
"bb002c0d5d4d46f426462f776cff00cf600f5a4a",
"support"
],
"fetch/range/general.any.js": [
"2c16c0398373fca53ae80aae1107868c8cdeb6b4",
"testharness"
],
"fetch/range/partial-script.window.js": [
"1352080860b8671290919ab0d09cb41f4100763e",
"testharness"
],
"fetch/range/resources/basic.html": [
"51a23151c28992fe062b36914463de216bd55fbe",
"support"
],
"fetch/range/resources/long-wav.py": [
"a9bdaefeb4e9cefd4bb678832d7ffcbe1b3167f7",
"support"
],
"fetch/range/resources/partial-script.py": [
"d74bf301d56ad7d5ae4067e8e27ec544a21aa2ed",
"support"
],
"fetch/range/resources/range-sw.js": [
"1ec66e1dd8bf9a11b058b90e32ca7caab2233d4d",
"support"
],
"fetch/range/resources/stash-take.py": [
"9d29b6276fa690d6acb366bdb4d60a12bc57ea52",
"support"
],
"fetch/range/resources/utils.js": [
"81cc493a76265cc64408fc2d41a67434ec99391a",
"support"
],
"fetch/range/sw.https.window.js": [
"5bf1ebc9ce82990013831f2f7f55589e29a69bde",
"testharness"
],
"fetch/sec-metadata/README.md": [
"75d58b35c1e5572d02dd3ad90ac65301e35c4bc7",
"support"
@ -603486,7 +603574,7 @@
"support"
],
"service-workers/service-worker/resources/test-helpers.sub.js": [
"74ea529125a5e2c5cd3d350f7c56ae614f82010d",
"079d31394903266dddb067f8f4dba5a94e522bdc",
"support"
],
"service-workers/service-worker/resources/testharness-helpers.js": [

Просмотреть файл

@ -0,0 +1,60 @@
// Helpers that return headers objects with a particular guard
function headersGuardNone(fill) {
if (fill) return new Headers(fill);
return new Headers();
}
function headersGuardResponse(fill) {
const opts = {};
if (fill) opts.headers = fill;
return new Response('', opts).headers;
}
function headersGuardRequest(fill) {
const opts = {};
if (fill) opts.headers = fill;
return new Request('./', opts).headers;
}
function headersGuardRequestNoCors(fill) {
const opts = { mode: 'no-cors' };
if (fill) opts.headers = fill;
return new Request('./', opts).headers;
}
const headerGuardTypes = [
['none', headersGuardNone],
['response', headersGuardResponse],
['request', headersGuardRequest]
];
for (const [guardType, createHeaders] of headerGuardTypes) {
test(() => {
// There are three ways to set headers.
// Filling, appending, and setting. Test each:
let headers = createHeaders({ Range: 'foo' });
assert_equals(headers.get('Range'), 'foo');
headers = createHeaders();
headers.append('Range', 'foo');
assert_equals(headers.get('Range'), 'foo');
headers = createHeaders();
headers.set('Range', 'foo');
assert_equals(headers.get('Range'), 'foo');
}, `Range header setting allowed for guard type: ${guardType}`);
}
test(() => {
let headers = headersGuardRequestNoCors({ Range: 'foo' });
assert_false(headers.has('Range'));
headers = headersGuardRequestNoCors();
headers.append('Range', 'foo');
assert_false(headers.has('Range'));
headers = headersGuardRequestNoCors();
headers.set('Range', 'foo');
assert_false(headers.has('Range'));
}, `Privileged header not allowed for guard type: request-no-cors`);

Просмотреть файл

@ -0,0 +1,7 @@
// META: script=resources/utils.js
// It's weird that browsers do this, but it should continue to work.
promise_test(async t => {
await loadScript('resources/partial-script.py?pretend-offset=90000');
assert_true(self.scriptExecuted);
}, `Script executed from partial response`);

Просмотреть файл

@ -0,0 +1 @@
<!DOCTYPE html>

Просмотреть файл

@ -0,0 +1,111 @@
"""
This generates a 30 minute silent wav, and is capable of
responding to Range requests.
"""
import time
import re
import struct
def create_wav_header(sample_rate, bit_depth, channels, duration):
bytes_per_sample = bit_depth / 8
block_align = bytes_per_sample * channels
byte_rate = sample_rate * block_align
sub_chunk_2_size = duration * byte_rate
data = b''
# ChunkID
data += b'RIFF'
# ChunkSize
data += struct.pack('<L', 36 + sub_chunk_2_size)
# Format
data += b'WAVE'
# Subchunk1ID
data += b'fmt '
# Subchunk1Size
data += struct.pack('<L', 16)
# AudioFormat
data += struct.pack('<H', 1)
# NumChannels
data += struct.pack('<H', channels)
# SampleRate
data += struct.pack('<L', sample_rate)
# ByteRate
data += struct.pack('<L', byte_rate)
# BlockAlign
data += struct.pack('<H', block_align)
# BitsPerSample
data += struct.pack('<H', bit_depth)
# Subchunk2ID
data += b'data'
# Subchunk2Size
data += struct.pack('<L', sub_chunk_2_size)
return data
def main(request, response):
response.headers.set("Content-Type", "audio/wav")
response.headers.set("Accept-Ranges", "bytes")
response.headers.set("Cache-Control", "no-cache")
range_header = request.headers.get('Range', '')
range_received_key = request.GET.first('range-received-key', '')
if range_received_key and range_header:
# This is later collected using stash-take.py
request.stash.put(range_received_key, 'range-header-received', '/fetch/range/')
# Audio details
sample_rate = 8000
bit_depth = 8
channels = 1
duration = 60 * 5
total_length = (sample_rate * bit_depth * channels * duration) / 8
bytes_remaining_to_send = total_length
initial_write = ''
if range_header:
response.status = 206
start, end = re.search(r'^bytes=(\d*)-(\d*)$', range_header).groups()
start = int(start)
end = int(end) if end else 0
if end:
bytes_remaining_to_send = (end + 1) - start
else:
bytes_remaining_to_send = total_length - start
wav_header = create_wav_header(sample_rate, bit_depth, channels, duration)
if start < len(wav_header):
initial_write = wav_header[start:]
if bytes_remaining_to_send < len(initial_write):
initial_write = initial_write[0:bytes_remaining_to_send]
content_range = "bytes {}-{}/{}".format(start, end or total_length - 1, total_length)
response.headers.set("Content-Range", content_range)
else:
initial_write = create_wav_header(sample_rate, bit_depth, channels, duration)
response.headers.set("Content-Length", bytes_remaining_to_send)
response.write_status_headers()
response.writer.write(initial_write)
bytes_remaining_to_send -= len(initial_write)
while bytes_remaining_to_send > 0:
if not response.writer.flush():
break
to_send = b'\x00' * min(bytes_remaining_to_send, sample_rate)
bytes_remaining_to_send -= len(to_send)
response.writer.write(to_send)
# Throttle the stream
time.sleep(0.5)

Просмотреть файл

@ -0,0 +1,30 @@
"""
This generates a partial response containing valid JavaScript.
"""
def main(request, response):
require_range = request.GET.first('require-range', '')
pretend_offset = int(request.GET.first('pretend-offset', '0'))
range_header = request.headers.get('Range', '')
if require_range and not range_header:
response.set_error(412, "Range header required")
response.write()
return
response.headers.set("Content-Type", "text/plain")
response.headers.set("Accept-Ranges", "bytes")
response.headers.set("Cache-Control", "no-cache")
response.status = 206
to_send = 'self.scriptExecuted = true;'
length = len(to_send)
content_range = "bytes {}-{}/{}".format(
pretend_offset, pretend_offset + length - 1, pretend_offset + length)
response.headers.set("Content-Range", content_range)
response.headers.set("Content-Length", length)
response.content = to_send

Просмотреть файл

@ -0,0 +1,142 @@
importScripts('/resources/testharness.js');
setup({ explicit_done: true });
function assert_range_request(request, expectedRangeHeader, name) {
assert_equals(request.headers.get('Range'), expectedRangeHeader, name);
}
async function broadcast(msg) {
for (const client of await clients.matchAll()) {
client.postMessage(msg);
}
}
addEventListener('fetch', event => {
/** @type Request */
const request = event.request;
const url = new URL(request.url);
const action = url.searchParams.get('action');
switch (action) {
case 'range-header-filter-test':
rangeHeaderFilterTest(request);
return;
case 'range-header-passthrough-test':
rangeHeaderPassthroughTest(event);
return;
case 'store-ranged-response':
storeRangedResponse(event);
return;
case 'use-stored-ranged-response':
useStoredRangeResponse(event);
return;
}
});
/**
* @param {Request} request
*/
function rangeHeaderFilterTest(request) {
const rangeValue = request.headers.get('Range');
test(() => {
assert_range_request(new Request(request), rangeValue, `Untampered`);
assert_range_request(new Request(request, {}), rangeValue, `Untampered (no init props set)`);
assert_range_request(new Request(request, { __foo: 'bar' }), rangeValue, `Untampered (only invalid props set)`);
assert_range_request(new Request(request, { mode: 'cors' }), rangeValue, `More permissive mode`);
assert_range_request(request.clone(), rangeValue, `Clone`);
}, "Range headers correctly preserved");
test(() => {
assert_range_request(new Request(request, { headers: { Range: 'foo' } }), null, `Tampered - range header set`);
assert_range_request(new Request(request, { headers: {} }), null, `Tampered - empty headers set`);
assert_range_request(new Request(request, { mode: 'no-cors' }), null, `Tampered – mode set`);
assert_range_request(new Request(request, { cache: 'no-cache' }), null, `Tampered – cache mode set`);
}, "Range headers correctly removed");
test(() => {
let headers;
headers = new Request(request).headers;
headers.delete('does-not-exist');
assert_equals(headers.get('Range'), rangeValue, `Preserved if no header actually removed`);
headers = new Request(request).headers;
headers.append('foo', 'bar');
assert_equals(headers.get('Range'), rangeValue, `Preserved if silent-failure on append (due to request-no-cors guard)`);
headers = new Request(request).headers;
headers.set('foo', 'bar');
assert_equals(headers.get('Range'), rangeValue, `Preserved if silent-failure on set (due to request-no-cors guard)`);
headers = new Request(request).headers;
headers.append('Range', 'foo');
assert_equals(headers.get('Range'), rangeValue, `Preserved if silent-failure on append (due to request-no-cors guard)`);
headers = new Request(request).headers;
headers.set('Range', 'foo');
assert_equals(headers.get('Range'), rangeValue, `Preserved if silent-failure on set (due to request-no-cors guard)`);
headers = new Request(request).headers;
headers.append('Accept', 'whatever');
assert_equals(headers.get('Range'), null, `Stripped if header successfully appended`);
headers = new Request(request).headers;
headers.set('Accept', 'whatever');
assert_equals(headers.get('Range'), null, `Stripped if header successfully set`);
headers = new Request(request).headers;
headers.delete('Accept');
assert_equals(headers.get('Range'), null, `Stripped if header successfully deleted`);
headers = new Request(request).headers;
headers.delete('Range');
assert_equals(headers.get('Range'), null, `Stripped if range header successfully deleted`);
}, "Headers correctly filtered");
done();
}
function rangeHeaderPassthroughTest(event) {
/** @type Request */
const request = event.request;
const url = new URL(request.url);
const key = url.searchParams.get('range-received-key');
event.waitUntil(new Promise(resolve => {
promise_test(async () => {
await fetch(event.request);
const response = await fetch('stash-take.py?key=' + key);
assert_equals(await response.json(), '"range-header-received"');
resolve();
}, `Include range header in network request`);
done();
}));
// Just send back any response, it isn't important for the test.
event.respondWith(new Response(''));
}
let storedRangeResponseP;
function storeRangedResponse(event) {
/** @type Request */
const request = event.request;
const id = new URL(request.url).searchParams.get('id');
storedRangeResponseP = fetch(event.request);
broadcast({ id });
// Just send back any response, it isn't important for the test.
event.respondWith(new Response(''));
}
function useStoredRangeResponse(event) {
event.respondWith(async function() {
const response = await storedRangeResponseP;
if (!response) throw Error("Expected stored range response");
return response.clone();
}());
}

Просмотреть файл

@ -0,0 +1,7 @@
from wptserve.handlers import json_handler
@json_handler
def main(request, response):
key = request.GET.first("key")
return request.server.stash.take(key, '/fetch/range/')

Просмотреть файл

@ -0,0 +1,9 @@
function loadScript(url, { doc = document }={}) {
return new Promise((resolve, reject) => {
const script = doc.createElement('script');
script.onload = () => resolve();
script.onerror = () => reject(Error("Script load failed"));
script.src = url;
doc.body.appendChild(script);
})
}

Просмотреть файл

@ -0,0 +1,128 @@
// META: script=../../../service-workers/service-worker/resources/test-helpers.sub.js
// META: script=/common/utils.js
// META: script=/common/get-host-info.sub.js
// META: script=resources/utils.js
const { REMOTE_HOST } = get_host_info();
const SCOPE = 'resources/basic.html' + Math.random();
function appendAudio(document, url) {
const audio = document.createElement('audio');
audio.muted = true;
audio.src = url;
audio.preload = true;
document.body.appendChild(audio);
}
async function cleanup() {
for (const iframe of document.querySelectorAll('.test-iframe')) {
iframe.parentNode.removeChild(iframe);
}
const reg = await navigator.serviceWorker.getRegistration(SCOPE);
if (reg) await reg.unregister();
}
async function setupRegistration(t) {
await cleanup();
const reg = await navigator.serviceWorker.register('resources/range-sw.js', { scope: SCOPE });
await wait_for_state(t, reg.installing, 'activated');
return reg;
}
function awaitMessage(obj, id) {
return new Promise(resolve => {
obj.addEventListener('message', function listener(event) {
if (event.data.id !== id) return;
obj.removeEventListener('message', listener);
resolve();
});
});
}
promise_test(async t => {
const reg = await setupRegistration(t);
const iframe = await with_iframe(SCOPE);
const w = iframe.contentWindow;
// Trigger a cross-origin range request using media
const url = new URL('long-wav.py?action=range-header-filter-test', w.location);
url.hostname = REMOTE_HOST;
appendAudio(w.document, url);
// See rangeHeaderFilterTest in resources/range-sw.js
await fetch_tests_from_worker(reg.active);
}, `Defer range header filter tests to service worker`);
promise_test(async t => {
const reg = await setupRegistration(t);
const iframe = await with_iframe(SCOPE);
const w = iframe.contentWindow;
// Trigger a cross-origin range request using media
const url = new URL('long-wav.py', w.location);
url.searchParams.set('action', 'range-header-passthrough-test');
url.searchParams.set('range-received-key', token());
url.hostname = REMOTE_HOST;
appendAudio(w.document, url);
// See rangeHeaderPassthroughTest in resources/range-sw.js
await fetch_tests_from_worker(reg.active);
}, `Defer range header passthrough tests to service worker`);
promise_test(async t => {
await setupRegistration(t);
const iframe = await with_iframe(SCOPE);
const w = iframe.contentWindow;
const id = Math.random() + '';
const storedRangeResponse = awaitMessage(w.navigator.serviceWorker, id);
// Trigger a cross-origin range request using media
const url = new URL('partial-script.py', w.location);
url.searchParams.set('require-range', '1');
url.searchParams.set('action', 'store-ranged-response');
url.searchParams.set('id', id);
url.hostname = REMOTE_HOST;
appendAudio(w.document, url);
await storedRangeResponse;
// Fetching should reject
const fetchPromise = w.fetch('?action=use-stored-ranged-response', { mode: 'no-cors' });
promise_rejects(t, new TypeError(), fetchPromise);
// Script loading should error too
const loadScriptPromise = loadScript('?action=use-stored-ranged-response', { doc: w.document });
promise_rejects(t, new Error(), loadScriptPromise);
await loadScriptPromise.catch(() => {});
assert_false(!!w.scriptExecuted, `Partial response shouldn't be executed`);
}, `Ranged response not allowed following no-cors ranged request`);
promise_test(async t => {
await setupRegistration(t);
const iframe = await with_iframe(SCOPE);
const w = iframe.contentWindow;
const id = Math.random() + '';
const storedRangeResponse = awaitMessage(w.navigator.serviceWorker, id);
// Trigger a range request using media
const url = new URL('partial-script.py', w.location);
url.searchParams.set('require-range', '1');
url.searchParams.set('action', 'store-ranged-response');
url.searchParams.set('id', id);
appendAudio(w.document, url);
await storedRangeResponse;
// This should not throw
await w.fetch('?action=use-stored-ranged-response');
// This shouldn't throw either
await loadScript('?action=use-stored-ranged-response', { doc: w.document });
assert_true(w.scriptExecuted, `Partial response should be executed`);
}, `Non-opaque ranged response executed`);

Просмотреть файл

@ -46,9 +46,14 @@ function unreached_rejection(test, prefix) {
});
}
// Adds an iframe to the document and returns a promise that resolves to the
// iframe when it finishes loading. The caller is responsible for removing the
// iframe later if needed.
/**
* Adds an iframe to the document and returns a promise that resolves to the
* iframe when it finishes loading. The caller is responsible for removing the
* iframe later if needed.
*
* @param {string} url
* @returns {HTMLIFrameElement}
*/
function with_iframe(url) {
return new Promise(function(resolve) {
var frame = document.createElement('iframe');