From 088b0c1780f078e08d1aba5ca4ce41fffb79a23a Mon Sep 17 00:00:00 2001 From: Muffin Date: Tue, 9 Jan 2024 03:45:27 -0600 Subject: [PATCH] Experiment with `staticFetch` to improve reliability of large data: URLs --- .../tw-unsandboxed-extension-runner.js | 7 ++++ src/util/tw-static-fetch.js | 36 ++++++++++++++++++ test/unit/tw_static_fetch.js | 38 +++++++++++++++++++ 3 files changed, 81 insertions(+) create mode 100644 src/util/tw-static-fetch.js create mode 100644 test/unit/tw_static_fetch.js diff --git a/src/extension-support/tw-unsandboxed-extension-runner.js b/src/extension-support/tw-unsandboxed-extension-runner.js index dacea3fd259..6f44d7cb22f 100644 --- a/src/extension-support/tw-unsandboxed-extension-runner.js +++ b/src/extension-support/tw-unsandboxed-extension-runner.js @@ -2,6 +2,7 @@ const ScratchCommon = require('./tw-extension-api-common'); const createScratchX = require('./tw-scratchx-compatibility-layer'); const AsyncLimiter = require('../util/async-limiter'); const createTranslate = require('./tw-l10n'); +const staticFetch = require('../util/tw-static-fetch'); /* eslint-disable require-await */ @@ -97,6 +98,12 @@ const setupUnsandboxedExtensionAPI = vm => new Promise(resolve => { Scratch.fetch = async (url, options) => { const actualURL = url instanceof Request ? url.url : url; + + const staticFetchResult = staticFetch(url); + if (staticFetchResult) { + return staticFetchResult; + } + if (!await Scratch.canFetch(actualURL)) { throw new Error(`Permission to fetch ${actualURL} rejected.`); } diff --git a/src/util/tw-static-fetch.js b/src/util/tw-static-fetch.js new file mode 100644 index 00000000000..db718b9d195 --- /dev/null +++ b/src/util/tw-static-fetch.js @@ -0,0 +1,36 @@ +/** + * @fileoverview + * The new URL() and fetch() provided by the browser tend to buckle when dealing with URLs that are + * tens of megabytes in length, which can be common when working with data: URLs in extensions. + * + * To help avoid that, this file can "statically" parse some data: URLs without going through + * unreliable browser APIs. + */ + +const Base64Util = require('./base64-util'); + +/** + * @param {string} url + * @returns {Response|null} + */ +const staticFetch = url => { + try { + const simpleDataUrlMatch = url.match(/^data:([/-\w\d]*);base64,/i); + if (simpleDataUrlMatch) { + const contentType = simpleDataUrlMatch[1].toLowerCase(); + const base64 = url.substring(simpleDataUrlMatch[0].length); + const decoded = Base64Util.base64ToUint8Array(base64); + return new Response(decoded, { + headers: { + 'content-type': contentType, + 'content-length': decoded.byteLength + } + }); + } + } catch (e) { + // not robust enough yet to care about these errors + } + return null; +}; + +module.exports = staticFetch; diff --git a/test/unit/tw_static_fetch.js b/test/unit/tw_static_fetch.js new file mode 100644 index 00000000000..51df28c4ca0 --- /dev/null +++ b/test/unit/tw_static_fetch.js @@ -0,0 +1,38 @@ +const {test} = require('tap'); +const staticFetch = require('../../src/util/tw-static-fetch'); + +test('fetch simple base64', t => { + const res = staticFetch('data:text/plain;base64,VGVzdGluZyB0ZXN0aW5nIDEyMw=='); + res.text().then(text => { + t.equal(text, 'Testing testing 123'); + t.equal(res.status, 200); + t.equal(res.ok, true); + t.equal(res.headers.get('content-type'), 'text/plain'); + t.equal(res.headers.get('content-length'), '19'); + t.end(); + }); +}); + +test('fetch base64 with all possible bytes', t => { + // eslint-disable-next-line max-len + const res = staticFetch('Data:Application/Octet-Stream;BASE64,AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn+AgYKDhIWGh4iJiouMjY6PkJGSk5SVlpeYmZqbnJ2en6ChoqOkpaanqKmqq6ytrq+wsbKztLW2t7i5uru8vb6/wMHCw8TFxsfIycrLzM3Oz9DR0tPU1dbX2Nna29zd3t/g4eLj5OXm5+jp6uvs7e7v8PHy8/T19vf4+fr7/P3+/w=='); + res.arrayBuffer().then(buffer => { + t.same(Array.from(new Uint8Array(buffer)), Array(256) + .fill() + .map((_, index) => index) + ); + t.equal(res.headers.get('content-type'), 'application/octet-stream'); + t.equal(res.headers.get('content-length'), '256'); + t.end(); + }); +}); + +test('fetch not data:', t => { + t.equal(staticFetch('blob:https://turbowarp.org/54346944-16cf-4ce9-aed4-e1df8ad0d779'), null); + t.equal(staticFetch('https://example.com/'), null); + t.equal(staticFetch('http://example.com/'), null); + t.equal(staticFetch('file:///etc/hosts'), null); + t.equal(staticFetch('oegirjdf'), null); + t.equal(staticFetch(''), null); + t.end(); +});