author | Jake Archibald <jaffathecake@gmail.com> |
Wed, 06 Jun 2018 16:35:52 +0000 | |
changeset 422208 | 8b458f3d30b63afa745527bc31744f4971749224 |
parent 422207 | 6424db14b6d5ab082bb7006836003de754ac1f67 |
child 422209 | 098ab2cd9e0dbbf21c7a602266bacbc499c59dc1 |
push id | 34122 |
push user | ebalazs@mozilla.com |
push date | Mon, 11 Jun 2018 09:37:00 +0000 |
treeherder | mozilla-central@9941eb8c3b29 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | testonly |
bugs | 1452562, 10348 |
milestone | 62.0a1 |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
--- a/testing/web-platform/meta/MANIFEST.json +++ b/testing/web-platform/meta/MANIFEST.json @@ -273611,16 +273611,46 @@ {} ] ], "fetch/origin/resources/redirect-and-stash.py": [ [ {} ] ], + "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": [ [ {} ] ], "fetch/sec-metadata/resources/echo-as-json.py": [ [ {} @@ -330605,16 +330635,38 @@ "/fetch/origin/no-cors.any.html", {} ], [ "/fetch/origin/no-cors.any.worker.html", {} ] ], + "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", {} ] ], "fetch/sec-metadata/iframe.tentative.https.sub.html": [ [ @@ -559760,16 +559812,52 @@ "fetch/origin/no-cors.any.js": [ "e9774c1c02bac3833b042c4895733d7bc8b36456", "testharness" ], "fetch/origin/resources/redirect-and-stash.py": [ "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" ], "fetch/sec-metadata/fetch.tentative.https.sub.html": [ "6ce26d3683b78ca74a20b14ccca077cf375d742b", "testharness" ], @@ -603481,17 +603569,17 @@ "31ca62ad01049549f05ec52dda828defcbe2ac6e", "support" ], "service-workers/service-worker/resources/svg-target-reftest-frame.html": [ "0f7bf8a7cccb1fb4aa5948fb7a684857006b295f", "support" ], "service-workers/service-worker/resources/test-helpers.sub.js": [ - "74ea529125a5e2c5cd3d350f7c56ae614f82010d", + "079d31394903266dddb067f8f4dba5a94e522bdc", "support" ], "service-workers/service-worker/resources/testharness-helpers.js": [ "15f29256ea90b9c4f91411e570d5dd1979ff73b2", "support" ], "service-workers/service-worker/resources/unregister-controller-page.html": [ "feae7f538da58d45e38f6f26da5ce6af0c91857f",
new file mode 100644 --- /dev/null +++ b/testing/web-platform/tests/fetch/range/general.any.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`); +
new file mode 100644 --- /dev/null +++ b/testing/web-platform/tests/fetch/range/partial-script.window.js @@ -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`);
new file mode 100644 --- /dev/null +++ b/testing/web-platform/tests/fetch/range/resources/basic.html @@ -0,0 +1,1 @@ +<!DOCTYPE html>
new file mode 100644 --- /dev/null +++ b/testing/web-platform/tests/fetch/range/resources/long-wav.py @@ -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)
new file mode 100644 --- /dev/null +++ b/testing/web-platform/tests/fetch/range/resources/partial-script.py @@ -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
new file mode 100644 --- /dev/null +++ b/testing/web-platform/tests/fetch/range/resources/range-sw.js @@ -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(); + }()); +}
new file mode 100644 --- /dev/null +++ b/testing/web-platform/tests/fetch/range/resources/stash-take.py @@ -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/')
new file mode 100644 --- /dev/null +++ b/testing/web-platform/tests/fetch/range/resources/utils.js @@ -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); + }) +}
new file mode 100644 --- /dev/null +++ b/testing/web-platform/tests/fetch/range/sw.https.window.js @@ -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`);
--- a/testing/web-platform/tests/service-workers/service-worker/resources/test-helpers.sub.js +++ b/testing/web-platform/tests/service-workers/service-worker/resources/test-helpers.sub.js @@ -41,19 +41,24 @@ function unreached_fulfillment(test, pre function unreached_rejection(test, prefix) { return test.step_func(function(error) { var reason = error.message || error.name || error; var error_prefix = prefix || 'unexpected rejection'; assert_unreached(error_prefix + ': ' + reason); }); } -// 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'); frame.className = 'test-iframe'; frame.src = url; frame.onload = function() { resolve(frame); }; document.body.appendChild(frame); });