summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/fetch/range/resources
diff options
context:
space:
mode:
Diffstat (limited to 'testing/web-platform/tests/fetch/range/resources')
-rw-r--r--testing/web-platform/tests/fetch/range/resources/basic.html1
-rw-r--r--testing/web-platform/tests/fetch/range/resources/long-wav.py134
-rw-r--r--testing/web-platform/tests/fetch/range/resources/partial-script.py29
-rw-r--r--testing/web-platform/tests/fetch/range/resources/partial-text.py53
-rw-r--r--testing/web-platform/tests/fetch/range/resources/range-sw.js218
-rw-r--r--testing/web-platform/tests/fetch/range/resources/stash-take.py7
-rw-r--r--testing/web-platform/tests/fetch/range/resources/utils.js36
-rw-r--r--testing/web-platform/tests/fetch/range/resources/video-with-range.py43
8 files changed, 521 insertions, 0 deletions
diff --git a/testing/web-platform/tests/fetch/range/resources/basic.html b/testing/web-platform/tests/fetch/range/resources/basic.html
new file mode 100644
index 0000000000..0e76edd65b
--- /dev/null
+++ b/testing/web-platform/tests/fetch/range/resources/basic.html
@@ -0,0 +1 @@
+<!DOCTYPE html>
diff --git a/testing/web-platform/tests/fetch/range/resources/long-wav.py b/testing/web-platform/tests/fetch/range/resources/long-wav.py
new file mode 100644
index 0000000000..acfc81a718
--- /dev/null
+++ b/testing/web-platform/tests/fetch/range/resources/long-wav.py
@@ -0,0 +1,134 @@
+"""
+This generates a 30 minute silent wav, and is capable of
+responding to Range requests.
+"""
+import time
+import re
+import struct
+
+from wptserve.utils import isomorphic_decode
+
+def create_wav_header(sample_rate, bit_depth, channels, duration):
+ bytes_per_sample = int(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):
+ if request.method == u"OPTIONS":
+ response.status = (404, b"Not Found")
+ response.headers.set(b"Content-Type", b"text/plain")
+ return b"Preflight not accepted"
+
+ response.headers.set(b"Content-Type", b"audio/wav")
+ response.headers.set(b"Accept-Ranges", b"bytes")
+ response.headers.set(b"Cache-Control", b"no-cache")
+ response.headers.set(b"Access-Control-Allow-Origin", request.headers.get(b'Origin', b''))
+
+ range_header = request.headers.get(b'Range', b'')
+ range_header_match = range_header and re.search(r'^bytes=(\d*)-(\d*)$', isomorphic_decode(range_header))
+ range_received_key = request.GET.first(b'range-received-key', b'')
+ accept_encoding_key = request.GET.first(b'accept-encoding-key', b'')
+
+ if range_received_key and range_header:
+ # Remove any current value
+ request.server.stash.take(range_received_key, b'/fetch/range/')
+ # This is later collected using stash-take.py
+ request.server.stash.put(range_received_key, u'range-header-received', b'/fetch/range/')
+
+ if accept_encoding_key:
+ # Remove any current value
+ request.server.stash.take(
+ accept_encoding_key,
+ b'/fetch/range/'
+ )
+ # This is later collected using stash-take.py
+ request.server.stash.put(
+ accept_encoding_key,
+ isomorphic_decode(request.headers.get(b'Accept-Encoding', b'')),
+ b'/fetch/range/'
+ )
+
+ # Audio details
+ sample_rate = 8000
+ bit_depth = 8
+ channels = 1
+ duration = 60 * 5
+
+ total_length = int((sample_rate * bit_depth * channels * duration) / 8)
+ bytes_remaining_to_send = total_length
+ initial_write = b''
+
+ if range_header_match:
+ response.status = 206
+ start, end = range_header_match.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 = b"bytes %d-%d/%d" % (start, end or total_length - 1, total_length)
+
+ response.headers.set(b"Content-Range", content_range)
+ else:
+ initial_write = create_wav_header(sample_rate, bit_depth, channels, duration)
+
+ response.headers.set(b"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:
+ to_send = b'\x00' * min(bytes_remaining_to_send, sample_rate)
+ bytes_remaining_to_send -= len(to_send)
+
+ if not response.writer.write(to_send):
+ break
+
+ # Throttle the stream
+ time.sleep(0.5)
diff --git a/testing/web-platform/tests/fetch/range/resources/partial-script.py b/testing/web-platform/tests/fetch/range/resources/partial-script.py
new file mode 100644
index 0000000000..a9570ec355
--- /dev/null
+++ b/testing/web-platform/tests/fetch/range/resources/partial-script.py
@@ -0,0 +1,29 @@
+"""
+This generates a partial response containing valid JavaScript.
+"""
+
+def main(request, response):
+ require_range = request.GET.first(b'require-range', b'')
+ pretend_offset = int(request.GET.first(b'pretend-offset', b'0'))
+ range_header = request.headers.get(b'Range', b'')
+
+ if require_range and not range_header:
+ response.set_error(412, u"Range header required")
+ response.write()
+ return
+
+ response.headers.set(b"Content-Type", b"text/plain")
+ response.headers.set(b"Accept-Ranges", b"bytes")
+ response.headers.set(b"Cache-Control", b"no-cache")
+ response.status = 206
+
+ to_send = b'self.scriptExecuted = true;'
+ length = len(to_send)
+
+ content_range = b"bytes %d-%d/%d" % (
+ pretend_offset, pretend_offset + length - 1, pretend_offset + length)
+
+ response.headers.set(b"Content-Range", content_range)
+ response.headers.set(b"Content-Length", length)
+
+ response.content = to_send
diff --git a/testing/web-platform/tests/fetch/range/resources/partial-text.py b/testing/web-platform/tests/fetch/range/resources/partial-text.py
new file mode 100644
index 0000000000..fa3d1171b6
--- /dev/null
+++ b/testing/web-platform/tests/fetch/range/resources/partial-text.py
@@ -0,0 +1,53 @@
+"""
+This generates a partial response for a 100-byte text file.
+"""
+import re
+
+from wptserve.utils import isomorphic_decode
+
+def main(request, response):
+ total_length = int(request.GET.first(b'length', b'100'))
+ partial_code = int(request.GET.first(b'partial', b'206'))
+ content_type = request.GET.first(b'type', b'text/plain')
+ range_header = request.headers.get(b'Range', b'')
+
+ # Send a 200 if there is no range request
+ if not range_header:
+ to_send = ''.zfill(total_length)
+ response.headers.set(b"Content-Type", content_type)
+ response.headers.set(b"Cache-Control", b"no-cache")
+ response.headers.set(b"Content-Length", total_length)
+ response.content = to_send
+ return
+
+ # Simple range parsing, requires specifically "bytes=xxx-xxxx"
+ range_header_match = re.search(r'^bytes=(\d*)-(\d*)$', isomorphic_decode(range_header))
+ start, end = range_header_match.groups()
+ start = int(start)
+ end = int(end) if end else total_length
+ length = end - start
+
+ # Error the request if the range goes beyond the length
+ if length <= 0 or end > total_length:
+ response.set_error(416, u"Range Not Satisfiable")
+ # set_error sets the MIME type to application/json, which - for a
+ # no-cors media request - will be blocked by ORB. We'll just force
+ # the expected MIME type here, whichfixes the test, but doesn't make
+ # sense in general.
+ response.headers = [(b"Content-Type", content_type)]
+ response.write()
+ return
+
+ # Generate a partial response of the requested length
+ to_send = ''.zfill(length)
+ response.headers.set(b"Content-Type", content_type)
+ response.headers.set(b"Accept-Ranges", b"bytes")
+ response.headers.set(b"Cache-Control", b"no-cache")
+ response.status = partial_code
+
+ content_range = b"bytes %d-%d/%d" % (start, end, total_length)
+
+ response.headers.set(b"Content-Range", content_range)
+ response.headers.set(b"Content-Length", length)
+
+ response.content = to_send
diff --git a/testing/web-platform/tests/fetch/range/resources/range-sw.js b/testing/web-platform/tests/fetch/range/resources/range-sw.js
new file mode 100644
index 0000000000..b47823f03b
--- /dev/null
+++ b/testing/web-platform/tests/fetch/range/resources/range-sw.js
@@ -0,0 +1,218 @@
+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', async 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;
+ case 'broadcast-accept-encoding':
+ broadcastAcceptEncoding(event);
+ return;
+ case 'record-media-range-request':
+ return recordMediaRangeRequest(event);
+ case 'use-media-range-request':
+ useMediaRangeRequest(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();
+ }());
+}
+
+function broadcastAcceptEncoding(event) {
+ /** @type Request */
+ const request = event.request;
+ const id = new URL(request.url).searchParams.get('id');
+
+ broadcast({
+ id,
+ acceptEncoding: request.headers.get('Accept-Encoding')
+ });
+
+ // Just send back any response, it isn't important for the test.
+ event.respondWith(new Response(''));
+}
+
+let rangeResponse = {};
+
+async function recordMediaRangeRequest(event) {
+ /** @type Request */
+ const request = event.request;
+ const url = new URL(request.url);
+ const urlParams = new URLSearchParams(url.search);
+ const size = urlParams.get("size");
+ const id = urlParams.get('id');
+ const key = 'size' + size;
+
+ if (key in rangeResponse) {
+ // Don't re-fetch ranges we already have.
+ const clonedResponse = rangeResponse[key].clone();
+ event.respondWith(clonedResponse);
+ } else if (event.request.headers.get("range") === "bytes=0-") {
+ // Generate a bogus 206 response to trigger subsequent range requests
+ // of the desired size.
+ const length = urlParams.get("length") + 100;
+ const body = "A".repeat(Number(size));
+ event.respondWith(new Response(body, {status: 206, headers: {
+ "Content-Type": "audio/mp4",
+ "Content-Range": `bytes 0-1/${length}`
+ }}));
+ } else if (event.request.headers.get("range") === `bytes=${Number(size)}-`) {
+ // Pass through actual range requests which will attempt to fetch up to the
+ // length in the original response which is bigger than the actual resource
+ // to make sure 206 and 416 responses are treated the same.
+ rangeResponse[key] = await fetch(event.request);
+
+ // Let the client know we have the range response for the given ID
+ broadcast({id});
+ } else {
+ event.respondWith(Promise.reject(Error("Invalid Request")));
+ }
+}
+
+function useMediaRangeRequest(event) {
+ /** @type Request */
+ const request = event.request;
+ const url = new URL(request.url);
+ const urlParams = new URLSearchParams(url.search);
+ const size = urlParams.get("size");
+ const key = 'size' + size;
+
+ // Send a clone of the range response to preload.
+ if (key in rangeResponse) {
+ const clonedResponse = rangeResponse[key].clone();
+ event.respondWith(clonedResponse);
+ } else {
+ event.respondWith(Promise.reject(Error("Invalid Request")));
+ }
+}
diff --git a/testing/web-platform/tests/fetch/range/resources/stash-take.py b/testing/web-platform/tests/fetch/range/resources/stash-take.py
new file mode 100644
index 0000000000..6cf6ff585b
--- /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(b"key")
+ return request.server.stash.take(key, b'/fetch/range/')
diff --git a/testing/web-platform/tests/fetch/range/resources/utils.js b/testing/web-platform/tests/fetch/range/resources/utils.js
new file mode 100644
index 0000000000..ad2853b33d
--- /dev/null
+++ b/testing/web-platform/tests/fetch/range/resources/utils.js
@@ -0,0 +1,36 @@
+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);
+ })
+}
+
+function preloadImage(url, { doc = document }={}) {
+ return new Promise((resolve, reject) => {
+ const preload = doc.createElement('link');
+ preload.rel = 'preload';
+ preload.as = 'image';
+ preload.onload = () => resolve();
+ preload.onerror = () => resolve();
+ preload.href = url;
+ doc.body.appendChild(preload);
+ })
+}
+
+/**
+ *
+ * @param {Document} document
+ * @param {string|URL} url
+ * @returns {HTMLAudioElement}
+ */
+function appendAudio(document, url) {
+ const audio = document.createElement('audio');
+ audio.muted = true;
+ audio.src = url;
+ audio.preload = true;
+ document.body.appendChild(audio);
+ return audio;
+}
diff --git a/testing/web-platform/tests/fetch/range/resources/video-with-range.py b/testing/web-platform/tests/fetch/range/resources/video-with-range.py
new file mode 100644
index 0000000000..2d15ccf3c4
--- /dev/null
+++ b/testing/web-platform/tests/fetch/range/resources/video-with-range.py
@@ -0,0 +1,43 @@
+import re
+import os
+import json
+from wptserve.utils import isomorphic_decode
+
+def main(request, response):
+ path = os.path.join(request.doc_root, u"media", "sine440.mp3")
+ total_size = os.path.getsize(path)
+ rewrites = json.loads(request.GET.first(b'rewrites', '[]'))
+ range_header = request.headers.get(b'Range')
+ range_header_match = range_header and re.search(r'^bytes=(\d*)-(\d*)$', isomorphic_decode(range_header))
+ start = None
+ end = None
+ if range_header_match:
+ response.status = 206
+ start, end = range_header_match.groups()
+ if range_header:
+ status = 206
+ else:
+ status = 200
+ for rewrite in rewrites:
+ req_start, req_end = rewrite['request']
+ if start == req_start or req_start == '*':
+ if end == req_end or req_end == '*':
+ if 'response' in rewrite:
+ start, end = rewrite['response']
+ if 'status' in rewrite:
+ status = rewrite['status']
+
+ start = int(start or 0)
+ end = int(end or total_size)
+ headers = []
+ if status == 206:
+ headers.append((b"Content-Range", b"bytes %d-%d/%d" % (start, end - 1, total_size)))
+ headers.append((b"Accept-Ranges", b"bytes"))
+
+ headers.append((b"Content-Type", b"audio/mp3"))
+ headers.append((b"Content-Length", str(end - start)))
+ headers.append((b"Cache-Control", b"no-cache"))
+ video_file = open(path, "rb")
+ video_file.seek(start)
+ content = video_file.read(end)
+ return status, headers, content