s4s-editor / local-scratch-vm /src /extension-support /tw-unsandboxed-extension-runner.js
soiz1's picture
Upload 811 files
30c32c8 verified
raw
history blame
5.46 kB
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');
/**
* Parse a URL object or return null.
* @param {string} url
* @returns {URL|null}
*/
const parseURL = url => {
try {
return new URL(url, location.href);
} catch (e) {
return null;
}
};
/**
* Sets up the global.Scratch API for an unsandboxed extension.
* @param {VirtualMachine} vm
* @returns {Promise<object[]>} Resolves with a list of extension objects when Scratch.extensions.register is called.
*/
const setupUnsandboxedExtensionAPI = vm => new Promise(resolve => {
const extensionObjects = [];
const register = extensionObject => {
extensionObjects.push(extensionObject);
resolve(extensionObjects);
};
// Create a new copy of global.Scratch for each extension
const Scratch = Object.assign({}, global.Scratch || {}, ScratchCommon);
Scratch.extensions = {
unsandboxed: true,
isPenguinMod: true,
register
};
Scratch.vm = vm;
Scratch.renderer = vm.runtime.renderer;
Scratch.canFetch = async url => {
const parsed = parseURL(url);
if (!parsed) {
return false;
}
// Always allow protocols that don't involve a remote request.
if (parsed.protocol === 'blob:' || parsed.protocol === 'data:') {
return true;
}
return vm.securityManager.canFetch(parsed.href);
};
Scratch.canOpenWindow = async url => {
const parsed = parseURL(url);
if (!parsed) {
return false;
}
// Always reject protocols that would allow code execution.
// eslint-disable-next-line no-script-url
if (parsed.protocol === 'javascript:') {
return false;
}
return vm.securityManager.canOpenWindow(parsed.href);
};
Scratch.canRedirect = async url => {
const parsed = parseURL(url);
if (!parsed) {
return false;
}
// Always reject protocols that would allow code execution.
// eslint-disable-next-line no-script-url
if (parsed.protocol === 'javascript:') {
return false;
}
return vm.securityManager.canRedirect(parsed.href);
};
Scratch.fetch = async (url, options) => {
const actualURL = url instanceof Request ? url.url : url;
if (!await Scratch.canFetch(actualURL)) {
throw new Error(`Permission to fetch ${actualURL} rejected.`);
}
return fetch(url, options);
};
Scratch.openWindow = async (url, features) => {
if (!await Scratch.canOpenWindow(url)) {
throw new Error(`Permission to open tab ${url} rejected.`);
}
return window.open(url, '_blank', features);
};
Scratch.redirect = async url => {
if (!await Scratch.canRedirect(url)) {
throw new Error(`Permission to redirect to ${url} rejected.`);
}
location.href = url;
};
Scratch.canRecordAudio = async () => vm.securityManager.canRecordAudio();
Scratch.canRecordVideo = async () => vm.securityManager.canRecordVideo();
Scratch.canReadClipboard = async () => vm.securityManager.canReadClipboard();
Scratch.canNotify = async () => vm.securityManager.canNotify();
Scratch.canGeolocate = async () => vm.securityManager.canGeolocate();
Scratch.canEmbed = async url => {
const parsed = parseURL(url);
if (!parsed) {
return false;
}
return vm.securityManager.canEmbed(parsed.href);
};
Scratch.canUnsandbox = async () => vm.securityManager.canUnsandbox();
Scratch.translate = createTranslate(vm);
global.Scratch = Scratch;
global.ScratchExtensions = createScratchX(Scratch);
vm.emit('CREATE_UNSANDBOXED_EXTENSION_API', Scratch);
});
/**
* Disable the existing global.Scratch unsandboxed extension APIs.
* This helps debug poorly designed extensions.
*/
const teardownUnsandboxedExtensionAPI = () => {
// We can assume global.Scratch already exists.
global.Scratch.extensions.register = () => {
throw new Error('Too late to register new extensions.');
};
};
/**
* Load an unsandboxed extension from an arbitrary URL. This is dangerous.
* @param {string} extensionURL
* @param {Virtualmachine} vm
* @returns {Promise<object[]>} Resolves with a list of extension objects if the extension was loaded successfully.
*/
const loadUnsandboxedExtension = (extensionURL, vm) => new Promise((resolve, reject) => {
setupUnsandboxedExtensionAPI(vm).then(resolve);
const script = document.createElement('script');
script.onerror = () => {
reject(new Error(`Error in unsandboxed script ${extensionURL}. Check the console for more information.`));
};
script.src = extensionURL;
document.body.appendChild(script);
}).then(objects => {
teardownUnsandboxedExtensionAPI();
return objects;
});
// Because loading unsandboxed extensions requires messing with global state (global.Scratch),
// only let one extension load at a time.
const limiter = new AsyncLimiter(loadUnsandboxedExtension, 1);
const load = (extensionURL, vm) => limiter.do(extensionURL, vm);
module.exports = {
setupUnsandboxedExtensionAPI,
load
};