Tonejs / Tone.js

A Web Audio framework for making interactive music in the browser.
https://tonejs.github.io
MIT License
13.45k stars 977 forks source link

Uses restricted feature: worker #1210

Open ChrisMiami opened 1 year ago

ChrisMiami commented 1 year ago

Describe the bug

In Chrome Version 117.0.5938.149 (Official Build) (arm64), I got the following error when Tone.js.min was loaded:

Refused to create a worker from 'blob:<my-server/guid>' because it violates the following Content Security Policy directive: "script-src 'unsafe-inline' 'unsafe-eval'". Note that 'worker-src' was not explicitly set, so 'script-src' is used as a fallback. Note that '' matches only URLs with network schemes ('http', 'https', 'ws', 'wss'), or URLs whose scheme matches self's scheme. The scheme 'blob:' must be added explicitly.

also

[Deprecation] The ScriptProcessorNode is deprecated. Use AudioWorkletNode instead. (https://bit.ly/audio-worklet)

To Reproduce

Add tone.js.min to a web page (yes, you have to rename to tone.js.min.js) and then look at the DevTools. The error is self-replicating.

The code that causes this problem is in Ticker.ts:

    /**
     * Generate a web worker
     */
    private _createWorker(): void {

        const blob = new Blob([
            /* javascript */`
            // the initial timeout time
            let timeoutTime =  ${(this._updateInterval * 1000).toFixed(1)};
            // onmessage callback
            self.onmessage = function(msg){
                timeoutTime = parseInt(msg.data);
            };
            // the tick function which posts a message
            // and schedules a new tick
            function tick(){
                setTimeout(tick, timeoutTime);
                self.postMessage('tick');
            }
            // call tick initially
            tick();
            `
        ], { type: "text/javascript" });
        const blobUrl = URL.createObjectURL(blob);
        const worker = new Worker(blobUrl);

        worker.onmessage = this._callback.bind(this);

        this._worker = worker;
    }

For some reason, the best solution here is to attempt to inject code into the page and then evaluate it. Super bad idea for pages where security is even a minor concern! Is there no other way to accomplish this feature?

Expected behavior Not having an error.

What I've tried Learning TypeScript, learning why this code is here, asking ChatGPT to figure out a better way to do it, trying to figure out how this project is built. Too much to do within my little deadline.

Additional context There MUST be some way to do this that doesn't involve hacking the page. I know setTimeout() might be inferior, but maybe it'd be better than having to not use the entire project.

ChrisMiami commented 1 year ago

Note: this is only a bug in a page with a Content-Security-Policy as described in the Chrome error message. It works fine in a wide-open test page. Not so much in a business environment.

ChrisMiami commented 12 months ago

As I look deeper into this (illegally, as nobody but me thinks audio feedback is of any importance), I realize that the code mentioned is written so that Tone.js is entirely self-contained and deliverable via CDN. If I just excise the JS inside the blob and put it into a co-resident file, and serve the lib from my servers, then heck, I bet it could actually work! Maybe...

So, I extracted the template literal into its own file (TickWorker.js) and am creating the blob using 'TickWorker.js' as its argument. Ironically, I find that the worker is entirely optional but the error it causes doesn't raise an exception that would cause Tone to use the setTimeout fallback in Ticker._createClock(). If the side-by-side file doesn't work to create the blob, I'll probably just comment out the whole thing and go with setTimeout.

chrisguttandin commented 12 months ago

It should work if you add 'blob:' as a script-src. It's not the safest thing to do but given that the page already allows any URL as well as inlined and eval-ed JavaScript I would argue it doesn't further increase the potential risk of an attack.

ChrisMiami commented 12 months ago

I don't have access to either the server or the source of the page - the content I create is hosted inside an iFrame that's contained in a commercial solution. I tried everything to get the Worker created with "known" source code, but it just wasn't possible. I ended up just throwing so setTimeout would be used. The hosting app does offer a worker that performs a setTimeout but I had to move on before being able to figure out how to match up the different APIs (between their worker timer and Tone's ticker worker).

chrisguttandin commented 11 months ago

Okay, I see. I took another look at the source code you referenced above and it actually catches the error and handles it correctly.

https://github.com/Tonejs/Tone.js/blob/08df7ad68cb9ed4c88d697f2230e3864ca15d206/Tone/core/clock/Ticker.ts#L96-L108

Running console.log(Tone.context.clockSource); should log 'timeout' in your case.

I believe this is Chrome's strange behavior of logging network errors even though they are handled programmatically. As far as I know there is no way to avoid this despite not triggering the error in the first place. You could do this by changing the code linked above to not create a worker when asked to do so.

private _createClock(): void {
    if (this._type === "worker") {
        // workers not supported, fallback to timeout
        this._type = "timeout";
        this._createClock();
    } else if (this._type === "timeout") {
        this._createTimeout();
    }
}

By the way the ScriptProcessorNode deprecation warning in the console is also unavoidable in Chrome.